前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[080]DoIP入门介绍

[080]DoIP入门介绍

作者头像
王小二
发布2023-05-23 10:45:59
3.1K0
发布2023-05-23 10:45:59
举报
文章被收录于专栏:王小二的Android站

一、简介

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 下载代码

代码语言:javascript
复制
git clone https://gitlab.com/rohfle/doip-simulator.git

修改一下日志的级别

代码语言:javascript
复制
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中的车辆上的网关

代码语言:javascript
复制
python3 doipserver.py

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

代码语言:javascript
复制
python3 doipclient.py

server端的结果

代码语言:javascript
复制
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端的结果

代码语言:javascript
复制
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端的伪代码就是如下

代码语言:javascript
复制
while  {
     监听13400端口来的请求,如果有请求就返回announcement信息
     timout时间到了
     广播announcement
}
代码语言:javascript
复制
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

代码语言:javascript
复制
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就可以调用

代码语言:javascript
复制
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的数据传输了。

代码语言:javascript
复制
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请求
代码语言:javascript
复制
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的数据包也比较简单的。

代码语言:javascript
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、简介
  • 二、整车的通信示意图
  • 三、DoIP的协议格式
  • 四、doip-simulator
    • 4.1 下载代码
      • 4.2 执行结果
      • 五、代码分析
        • 5.1 服务发现和建立连接
          • 5.2 发送和接收UDS的数据
          • 六、总结
          相关产品与服务
          腾讯云代码分析
          腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,助力维护团队卓越代码文化。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档