[080]DoIP入门介绍

2023-05-09  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读