[080]DoIP入门介绍
一、简介
DoIP是 Diagnostic communication over Internet Protocol的缩写,其实就是基于以太网的UDS协议的数据进行传输。其本身也是一种协议,规范于ISO13400标准。由于DoIP可以传输大量数据,以及响应速度快,且可以通过以太网进行远程诊断,刷写,OTA等任务,因此DoIP逐步成为代替传统的CAN。
二、整车的通信示意图
诊断上位机可以通过网关,基于doip可以访问到对应域上目标芯片,进行诊断,刷写,OTA等任务。
2.1
三、DoIP的协议格式
2.23.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规定的。
5.1.3任何跨进程跨设备通信的起点其实就是在于某个约定,Doip就是约定13400端口,约定了announcement和VehicleIdentityRequest。
类比到android系统中binder,其实就是约定了都是通过ServiceManager,提供服务String-Binder的格式,查询服务String。
例如someip的协议,服务的发现,也是按照某个约定实现的,后续我会介绍someip的时候再细聊。
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的设计理念和汽车系统中用的很多设计理念都是互相借鉴的,甚至说可能是前者学习后者。
因为刚刚进入汽车领域,如果本文中有讲的不好的,请大佬指正。