美文网首页
[080]DoIP入门介绍

[080]DoIP入门介绍

作者: 王小二的技术栈 | 来源:发表于2023-05-09 11:29 被阅读0次

    一、简介

    DoIP是 Diagnostic communication over Internet Protocol的缩写,其实就是基于以太网的UDS协议的数据进行传输。其本身也是一种协议,规范于ISO13400标准。由于DoIP可以传输大量数据,以及响应速度快,且可以通过以太网进行远程诊断,刷写,OTA等任务,因此DoIP逐步成为代替传统的CAN。

    二、整车的通信示意图

    诊断上位机可以通过网关,基于doip可以访问到对应域上目标芯片,进行诊断,刷写,OTA等任务。


    2.1

    三、DoIP的协议格式

    2.2
    3.1 DoIP报文由协议头(header)+ 负载(payload)组成

    协议头[8 byte]由下面四个字段组成
    Protocol version [1 byte]
    Inverse protocol version [1 byte]
    Payload type [2 byte]
    Payload length [4 byte]

    3.2 DoIP Payload中由DoIP source/Target Address和UDS Message组成

    DoIP source/Target Address代表这次UDS通信的两端的标志id,有点类似于互联网通信中的IP了,因为最早UDS协议是运行在CAN上的,DoIP source/Target Address就相当于CAN上的IP。

    四、doip-simulator

    光看前面的介绍,可能有点抽象,我们可以跑一个开源的项目来深入理解。

    4.1 下载代码

    git clone https://gitlab.com/rohfle/doip-simulator.git
    

    修改一下日志的级别

    diff --git a/doipclient.py b/doipclient.py
    index 1c77d03..06247ef 100644
    --- a/doipclient.py
    +++ b/doipclient.py
    @@ -36,7 +36,7 @@ STEERING_MULTIPLIER = float(os.environ.get("APP_STEERING_MULTIPLIER", 14)) # 14
     STEERING_DEADZONE_CAR = int(os.environ.get("APP_STEERING_DEADZONE_CAR", 0)) # 0
     STEERING_DEADZONE_XBOX = int(os.environ.get("APP_STEERING_DEADZONE_XBOX", 10000)) # 10000
    
    -logging.basicConfig(level=logging.WARNING)
    +logging.basicConfig(level=logging.INFO)
    
     def debug_parser(func):
         def print_args(*args, **kwargs):
    diff --git a/doipserver.py b/doipserver.py
    index a738a8d..31d3e91 100644
    --- a/doipserver.py
    +++ b/doipserver.py
    @@ -23,7 +23,7 @@ from lib import utils
     from lib.simulator import fstep, framp, fsine, IdentifierDataSimulator
    
     import logging
    -logging.basicConfig(level=logging.WARNING)
    +logging.basicConfig(level=logging.INFO)
    
    
     def accelerator_format(n):
    

    4.2 执行结果

    打开两个窗口分别执行以下指令,当然最好是两台设备分别执行,因为我在mac电脑上遇到过,Address already in use!的问题。
    在我的wsl ubuntu一台设备上可以跑,因为ubuntu 20.04支持SO_REUSEADDR,但是mac不支持SO_REUSEADDR。

    使用SO_REUSEADDR选项:可以在创建socket对象时,设置SO_REUSEADDR选项,来让一个端口可以被多个进程或线程同时绑定

    这个就相当于图2.1中的车辆上的网关

    python3 doipserver.py
    

    这个就相当于图2.1中的车辆上的诊断上位机

    python3 doipclient.py
    

    server端的结果

    kobe@41001005-26-0:~/study/doip/doip-simulator$ python3 doipserver.py
    INFO:discovery:Starting UDP discovery thread
    Serving on ('0.0.0.0', 13400) //监听在13400端口上,看有没有VehicleIdentityRequest
    INFO:discovery:Vehicle identity requested by IP 172.31.68.132. Responding with vehicle announcement.//发现了请求,回复announcement
    INFO:server:Connection established with ('172.31.68.132', 43292)//正式建立连接
    INFO:simulator:Generating value for Dummy Accelerator (0x3200 on target 0x3300)
    INFO:simulator:Dummy Accelerator value at time 1683682540.2956088 is bytearray(b'\xcc')
    

    client端的结果

    kobe@41001005-26-0:~/study/doip/doip-simulator$ python3 doipclient.py
    INFO:root:Looking for DOIP gateway... //寻找整车的网关
    INFO:root:Received Vehicle Announcement from 172.31.68.132! //收到server端发出的广播
    INFO:root:Routing activated successfully. //路由激活成功
    INFO:root:Data read loop time 2.91 milliseconds with length 3
    INFO:root:Data read loop time 1.95 milliseconds with length 3
    

    五、代码分析

    通过代码分析我们来看看两个关键流程:服务发现建立链接和建立连接后发送UDS数据

    5.1 服务发现和建立连接

    5.1.1 server端

    其实server端的伪代码就是如下

    while  {
         监听13400端口来的请求,如果有请求就返回announcement信息
         timout时间到了
         广播announcement
    }
    
    def run(self, *args, **kwargs):
            logger.info('Starting UDP discovery thread')
            self.sock.bind(('', 13400))//监听在'0.0.0.0:13400'看有没有client请求与他连接。
            self.sock.settimeout(0.5)//设置监听的timeout时间
            self.running = True
            self.announcement = self.generate_announcement(self.address, self.config).render()
            self.last_broadcast_time = time.time() - self.broadcast_interval
            while self.running:
                try:
                    //监听有没有client与他连接
                    data, addr = self.sock.recvfrom(1024)
                    message, used = doip.parse(bytearray(data))
                    logger.debug("Message received from %s:%i : %s", addr[0], addr[1], message)
                    if type(message) is doip.VehicleIdentityRequest:
                        logger.info('Vehicle identity requested by IP {}. '
                                    'Responding with vehicle announcement.'.format(addr[0]))
                        //如果发现有request,就发送announcement给client
                        self.sock.sendto(self.announcement, addr)
                except SocketTimeout:
                    pass
                except Exception as err:
                    logger.error('Error:', str(err))
                    logger.exception('Trace:')
                //timeout时间过了就广播announcement
                now_time = time.time()
                if now_time - self.last_broadcast_time > self.broadcast_interval:
                    self.last_broadcast_time = now_time
                    self.sock.sendto(self.announcement, ('255.255.255.255', 13400))
    
    

    announcement的内容,最重要的内容其实就是本车的IP,vin(车辆标识),mac

    def generate_announcement(self, address, config):
            vin = config['vin']
            mac = config['mac']
            return doip.VehicleAnnouncement(vin=vin, logical_address=address, eid=mac, gid=mac)//关键内容
    
    config = {
        'vin': 'TESTVIN0000012345',
        'mac': int('123456789ABC', 16),
        ....
    }
    
    5.1.2 client端

    广播13400端口的请求,因为前面5.1中server会监听自己的13400端口,也就会收到整个请求,然后返回server的IP,这样子client端拿到IP就可以调用

    def discover_doip():
        """Find the IP of the DOIP gateway"""
        # errors and their response:
        # - TimeoutException - cooldown before resend vehicle identity request
        # - network unreachable - caught by parent function
        s = socket(AF_INET, SOCK_DGRAM)
        s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
        if NETWORK_INTERFACE is not None:
            s.setsockopt(SOL_SOCKET, 25, str(NETWORK_INTERFACE + '\0').encode('utf-8'))
        s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        s.settimeout(DOIP_TIMEOUT)
        s.bind(('', 13400))
    
        logging.info("Looking for DOIP gateway...")
    
        while True:
            request = doip.VehicleIdentityRequest()
            logging.debug('SEND: %s', request)
            s.sendto(request.render(), ('255.255.255.255', 13400))//广播VehicleIdentityRequest
            try:
                start = time.time()
                while True:
                    data, addr =  s.recvfrom(1024)
                    data = bytearray(data)
                    response, used = doip.parse(data)
                    logging.debug('RECV: %s', response)
                    //收到VehicleAnnouncement的信息,返回IP
                    if type(response) is doip.VehicleAnnouncement:
                        logging.info('Received Vehicle Announcement from %s!' % (addr[0]))
                        return addr[0]
                    if time.time() - start > DOIP_TIMEOUT:
                        logging.warning('No vehicle announcement received. Requesting identity again immediately...')
                        break
            except timeout:
                logging.warning('No vehicle announcement received. Waiting 2 seconds before trying again...')
                time.sleep(2)
    

    拿到server端的IP就可以建立连接,就可以happy的进行doip的数据传输了。

    while True:
        try:
            gateway_addr = discover_doip()//获得server端的IP
            setup_doip(gateway_addr)//设置doip的网关地址,也就是client应该把数据发给谁。
        except OSError as err:
            logging.error("Error: %s", str(err))
            time.sleep(5)
    
    def setup_doip(gateway_addr):
        while True:
            try:
                s = socket(AF_INET, SOCK_STREAM)
                s.settimeout(DOIP_TIMEOUT)
                s.connect((gateway_addr, 13400))
    
    5.1.3 关键点

    有没有发现server端核心点在于双方都约定了13400的端口,都监听在这个端口上,并都在这个往这个端口发送请求服务,提供服务的广播,这样子client端就可以拿到server的IP,然后建立起连接,为什么用这个端口,是因为 ISO-13400规定的。

    任何跨进程跨设备通信的起点其实就是在于某个约定,Doip就是约定13400端口,约定了announcement和VehicleIdentityRequest。
    类比到android系统中binder,其实就是约定了都是通过ServiceManager,提供服务String-Binder的格式,查询服务String。
    例如someip的协议,服务的发现,也是按照某个约定实现的,后续我会介绍someip的时候再细聊。

    5.1.3

    5.2 发送和接收UDS的数据

    5.2.1 死循环发送datamap中的模拟UDS请求
    def run_doip(s):
        while True:
            data_payload = {}
            start = time.time()
            for target_address, identifiers in config['datamap'].items():
                for identifier, meta in identifiers.items():
                    label, key, parser = meta
                    logging.debug('Getting identifier 0x{:04x} ({})'.format(identifier, label))
                    uds_request = uds.ReadDataByIdentifier(identifier)
                    data = uds_request.render() //生成uds的数据包
                    request = doip.DiagnosticMessage(target_address, DOIP_SOURCE_ADDRESS, data) //跳转到5.2.2
                    logging.debug("SEND %s", str(request))
                    s.send(request.render())
                    value = receive_doip(s, identifier, parser)
                    if value is not None:
                        data_payload[key] = value
            if len(data_payload) > 0:
                serial_thread.send(data_payload)
            else:
                logging.warning('Data read loop result is empty')
            time_taken = time.time() - start
            logging.info('Data read loop time %.2f milliseconds with length %i', time_taken * 1000, len(data_payload))
    
    config = {
        'datamap': {
            # target_address (hex) : {
            #   identifier (hex) : tuple(label (str), key (str), parser (func))
            # }
            0x3300: {
                0x3200: ('Dummy Accelerator', 'accelerator', parse_accelerator),
                0x3230: ('Dummy Brake', 'brake', parse_brake_pressure),
            },
            0x3301: {
                0x3250: ('Dummy Steering', 'steering', parse_steering_angle),
            }
        }
    }
    
    5.2.2 打包成DoIP的数据包

    可以参考DoIP的协议格式章节,打包DoIP的数据包也比较简单的。

    class DiagnosticMessage(DOIPMessage):
    
        def render(self):
            source_address = self.params['source_address']
            target_address = self.params['target_address']
            userdata = self.params['userdata']
            data = bytearray(utils.num_to_bytes(source_address, 2))
            data += bytearray(utils.num_to_bytes(target_address, 2))
            data += bytearray(userdata)
            if len(data) < 5:
                raise InvalidMessage('Rendered diagnostic message is less than 5 bytes long')
            else:
                return super().render(data)
    
    class DOIPMessage(object):
    
        def render(self, data):
            if self.payload_type is None:
                raise Exception('DOIPMessage subclass has no payload_type value')
            header = bytearray([0x2, 0xfd])
            header += bytearray(utils.num_to_bytes(self.payload_type, 2))
            header += bytearray(utils.num_to_bytes(len(data), 4))
            return header + data
    
    5.2.3 Server端

    Server端也就收到DoIP的包,然后按照规则解包处理即可,代码就不继续解读了。


    六、总结

    这是我从手机转到汽车领域的第一篇真正意义的技术文章,讲讲自己的一些感受。

    从单芯片单系统变成了多芯片多系统,每个芯片上有对应的内核,中间件,应用程序,为了让车上的所有芯片的系统都规范行为,就有了类似这种Doip,UDS的协议规范,汽车领域工作一定要看很多规范文档,所以看英语文档的能力一定要提高。

    后续的文章我还是会和Android做一些类比,我相信很多Android的设计理念和汽车系统中用的很多设计理念都是互相借鉴的,甚至说可能是前者学习后者。

    因为刚刚进入汽车领域,如果本文中有讲的不好的,请大佬指正。

    参考文献
    https://gitlab.com/rohfle/doip-simulator

    相关文章

      网友评论

          本文标题:[080]DoIP入门介绍

          本文链接:https://www.haomeiwen.com/subject/nottsdtx.html