autouds 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autouds-0.1.0/.gitignore +51 -0
- autouds-0.1.0/.python-version +1 -0
- autouds-0.1.0/PKG-INFO +15 -0
- autouds-0.1.0/README.md +0 -0
- autouds-0.1.0/pyproject.toml +38 -0
- autouds-0.1.0/src/autouds/__init__.py +13 -0
- autouds-0.1.0/src/autouds/__main__.py +44 -0
- autouds-0.1.0/src/autouds/_handler.py +106 -0
- autouds-0.1.0/src/autouds/_models.py +226 -0
- autouds-0.1.0/src/autouds/_tables.py +441 -0
- autouds-0.1.0/src/autouds/app.py +270 -0
- autouds-0.1.0/uv.lock +594 -0
autouds-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
*.egg
|
|
14
|
+
wheels/
|
|
15
|
+
|
|
16
|
+
# Virtual environments
|
|
17
|
+
.venv/
|
|
18
|
+
venv/
|
|
19
|
+
env/
|
|
20
|
+
.venv-*/
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
*.swp
|
|
26
|
+
*.swo
|
|
27
|
+
*~
|
|
28
|
+
|
|
29
|
+
# OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
|
32
|
+
Desktop.ini
|
|
33
|
+
|
|
34
|
+
# Environment
|
|
35
|
+
.env
|
|
36
|
+
.env.local
|
|
37
|
+
.env.*
|
|
38
|
+
|
|
39
|
+
# Testing
|
|
40
|
+
.pytest_cache/
|
|
41
|
+
.coverage
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
|
|
46
|
+
# Type checking
|
|
47
|
+
.mypy_cache/
|
|
48
|
+
.ruff_cache/
|
|
49
|
+
|
|
50
|
+
# Jupyter
|
|
51
|
+
.ipynb_checkpoints/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
autouds-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autouds
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: UDS (Unified Diagnostic Services) diagnostic tool over DoIP — ISO 14229-1
|
|
5
|
+
Project-URL: Homepage, https://github.com/leno166/autouds
|
|
6
|
+
Project-URL: Source, https://github.com/leno166/autouds
|
|
7
|
+
Author: leno augenstern
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: System :: Networking
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
|
+
Requires-Dist: autodoip>=0.1.4
|
|
15
|
+
Requires-Dist: pydantic>=2.0
|
autouds-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "autouds"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "UDS (Unified Diagnostic Services) diagnostic tool over DoIP — ISO 14229-1"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "leno augenstern"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Topic :: System :: Networking",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"autodoip>=0.1.4",
|
|
23
|
+
"pydantic>=2.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/leno166/autouds"
|
|
28
|
+
Source = "https://github.com/leno166/autouds"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
autouds = "autouds.__main__:main"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"build>=1.5.0",
|
|
36
|
+
"pytest>=9.0.3",
|
|
37
|
+
"twine>=6.2.0",
|
|
38
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@文件: __init__.py
|
|
3
|
+
@作者: 雷小鸥
|
|
4
|
+
@日期: 2026/6/9 18:58
|
|
5
|
+
@许可: MIT License
|
|
6
|
+
@描述:
|
|
7
|
+
@版本: Version 0.1
|
|
8
|
+
"""
|
|
9
|
+
from ._handler import handler, set_send_impl
|
|
10
|
+
from ._models import Response, Request, UdsError
|
|
11
|
+
from .app import App
|
|
12
|
+
|
|
13
|
+
__all__ = ['handler', 'set_send_impl', 'Response', 'Request', 'UdsError', 'App']
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@文件: __main__.py
|
|
3
|
+
@作者: 雷小鸥
|
|
4
|
+
@日期: 2026/6/10 08:51
|
|
5
|
+
@许可: MIT License
|
|
6
|
+
@描述: UDS 诊断入口
|
|
7
|
+
@版本: Version 0.1
|
|
8
|
+
"""
|
|
9
|
+
import logging
|
|
10
|
+
from . import App
|
|
11
|
+
|
|
12
|
+
logging.basicConfig(
|
|
13
|
+
level=logging.INFO,
|
|
14
|
+
format='%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(funcName)s - %(message)s',
|
|
15
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
app = App(
|
|
21
|
+
ip='<tester-ip>',
|
|
22
|
+
ecus={
|
|
23
|
+
'mcu': (0x1001, '<ecu-ip>', 0),
|
|
24
|
+
'soc': (0x1002, '<ecu-ip>', 0),
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# 诊断会话控制
|
|
29
|
+
p2, p2_star = app.default()
|
|
30
|
+
print(f'default: P2={p2}, P2*={p2_star}')
|
|
31
|
+
|
|
32
|
+
# 切换到 soc
|
|
33
|
+
app.on('soc')
|
|
34
|
+
|
|
35
|
+
# 安全访问
|
|
36
|
+
seed = app.l1()
|
|
37
|
+
print(f'L1 seed: {seed.hex()}')
|
|
38
|
+
|
|
39
|
+
app.l1_ex(seed)
|
|
40
|
+
print('L1 unlock done')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == '__main__':
|
|
44
|
+
main()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@文件: _handler.py
|
|
3
|
+
@作者: 雷小鸥
|
|
4
|
+
@日期: 2026/6/11 12:01
|
|
5
|
+
@许可: MIT License
|
|
6
|
+
@描述: handler 装饰器 + 发送桥接。
|
|
7
|
+
@版本: Version 0.1
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
from typing import Callable, Generator, ParamSpec, TypeVar
|
|
13
|
+
from ._models import Request, Response, UdsError
|
|
14
|
+
|
|
15
|
+
P = ParamSpec("P")
|
|
16
|
+
R = TypeVar("R")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def handler(*args, **kwargs):
|
|
20
|
+
"""装饰器工厂:预建 Request,驱动用户生成器完成 UDS 交互。"""
|
|
21
|
+
fn = args[0] if len(args) == 1 else None
|
|
22
|
+
|
|
23
|
+
def decorator(f: Callable[..., Generator[bytes | None, Response, R]]) -> Callable[..., R]:
|
|
24
|
+
@functools.wraps(f)
|
|
25
|
+
def wrapper(self, *inner_args, **inner_kwargs):
|
|
26
|
+
# 1. 从装饰器参数取初始值(可能为 None)
|
|
27
|
+
s_name = kwargs.get('service_name', None)
|
|
28
|
+
s_id = kwargs.get('s_id', None)
|
|
29
|
+
fn_name = kwargs.get('sub_fn_name', None)
|
|
30
|
+
sub_fn = kwargs.get('sub_fn', None)
|
|
31
|
+
supp = kwargs.get('suppress_positive_response', None)
|
|
32
|
+
|
|
33
|
+
# 2. 调用时关键字覆盖:不传则保留装饰器参数
|
|
34
|
+
s_name = inner_kwargs.pop('service_name', s_name)
|
|
35
|
+
s_id = inner_kwargs.pop('s_id', s_id)
|
|
36
|
+
fn_name = inner_kwargs.pop('sub_fn_name', fn_name)
|
|
37
|
+
sub_fn = inner_kwargs.pop('sub_fn', sub_fn)
|
|
38
|
+
supp = inner_kwargs.pop('suppress_positive_response', supp)
|
|
39
|
+
|
|
40
|
+
# 3. 最终默认值
|
|
41
|
+
if supp is None:
|
|
42
|
+
supp = False
|
|
43
|
+
|
|
44
|
+
# 4. 工厂形式:驱动生成器获取 params
|
|
45
|
+
gen = f(self, *inner_args, **inner_kwargs)
|
|
46
|
+
params = next(gen) # 驱动生成器到第一个 yield
|
|
47
|
+
if params is not None and not isinstance(params, bytes):
|
|
48
|
+
raise TypeError("生成器第一个 yield 必须为 bytes 或 None")
|
|
49
|
+
|
|
50
|
+
request = Request.model_validate({
|
|
51
|
+
'service_name' : s_name,
|
|
52
|
+
'service_id' : s_id,
|
|
53
|
+
'sub_fn_name' : fn_name,
|
|
54
|
+
'sub_fn' : sub_fn,
|
|
55
|
+
'suppress_positive_response': supp,
|
|
56
|
+
'params' : params
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
# 6. 发送请求、收集响应
|
|
60
|
+
resp: Response | None = None
|
|
61
|
+
for raw in send(request.raw):
|
|
62
|
+
resp = Response.model_validate({
|
|
63
|
+
'raw': raw, 'request': request, 'father': resp
|
|
64
|
+
})
|
|
65
|
+
if not (resp.is_negative and resp.nrc == 0x78):
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
if resp is None:
|
|
69
|
+
resp = Response.model_validate({'request': request})
|
|
70
|
+
|
|
71
|
+
if resp.is_negative:
|
|
72
|
+
raise UdsError(resp)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
gen.send(resp)
|
|
76
|
+
except StopIteration as e:
|
|
77
|
+
return e.value
|
|
78
|
+
finally:
|
|
79
|
+
gen.close()
|
|
80
|
+
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"generator must return immediately after receiving Response"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
return decorator(fn) if fn else decorator
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
91
|
+
# 发送桥接
|
|
92
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
93
|
+
|
|
94
|
+
_send_impl = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def set_send_impl(func):
|
|
98
|
+
"""注入真正的 send 实现(生成器函数,接收 bytes 载荷,yield 原始响应字节)。"""
|
|
99
|
+
global _send_impl
|
|
100
|
+
_send_impl = func
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def send(payload: bytes):
|
|
104
|
+
if _send_impl is None:
|
|
105
|
+
raise RuntimeError("请先调用 set_send_impl() 设置发送函数")
|
|
106
|
+
yield from _send_impl(payload)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@文件: _models.py
|
|
3
|
+
@作者: 雷小鸥
|
|
4
|
+
@日期: 2026/6/11 12:02
|
|
5
|
+
@许可: MIT License
|
|
6
|
+
@描述:
|
|
7
|
+
UDS Request / Response 模型
|
|
8
|
+
|
|
9
|
+
Request — 构造时可用 model_validate 传入语义名,内部查表转换:
|
|
10
|
+
- Request.model_validate({'service_name': 'session', 'sub_fn_name': 'default'})
|
|
11
|
+
- Request.model_validate({'service_id': 0x10, 'sub_fn': 0x01})
|
|
12
|
+
|
|
13
|
+
Response — 两阶段:先创建空壳,收到 raw 后 _parse 填充
|
|
14
|
+
|
|
15
|
+
@版本: Version 0.1
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field, model_validator, PrivateAttr
|
|
21
|
+
from ._tables import SERVICE_MAP, SUB_FN_MAP, NRC_MAP
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
25
|
+
# Request
|
|
26
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
27
|
+
|
|
28
|
+
class Request(BaseModel):
|
|
29
|
+
service_id: int = Field(..., ge=0x00, le=0xFF)
|
|
30
|
+
sub_fn: int | None = Field(None, ge=0x00, le=0x7F)
|
|
31
|
+
suppress_positive_response: bool = False
|
|
32
|
+
|
|
33
|
+
params: bytes | None = Field(None)
|
|
34
|
+
|
|
35
|
+
_fn_byte: bytes | None = PrivateAttr(None)
|
|
36
|
+
|
|
37
|
+
# noinspection PyPropertyDefinition
|
|
38
|
+
@property
|
|
39
|
+
def raw(self) -> bytes:
|
|
40
|
+
return self.service_id.to_bytes(1, 'big') + (self._fn_byte or b'') + (self.params or b'')
|
|
41
|
+
|
|
42
|
+
# noinspection PyPropertyDefinition
|
|
43
|
+
@property
|
|
44
|
+
def hex(self) -> str:
|
|
45
|
+
return ' '.join(f'{b:02X}' for b in self.raw)
|
|
46
|
+
|
|
47
|
+
# noinspection PyNestedDecorators
|
|
48
|
+
@model_validator(mode='before')
|
|
49
|
+
@classmethod
|
|
50
|
+
def _preprocess(cls, data):
|
|
51
|
+
"""统一处理 service_name / sub_fn_name,并按依赖顺序验证。"""
|
|
52
|
+
if not isinstance(data, dict):
|
|
53
|
+
return data
|
|
54
|
+
|
|
55
|
+
# 1. 解析 service_name -> service_id
|
|
56
|
+
s_name = data.pop('service_name', None)
|
|
57
|
+
if s_name is not None:
|
|
58
|
+
resolved_sid = SERVICE_MAP.get(s_name)
|
|
59
|
+
if resolved_sid is None:
|
|
60
|
+
raise ValueError(f'未知服务名: {s_name!r}')
|
|
61
|
+
existing_sid = data.get('service_id')
|
|
62
|
+
if existing_sid is not None and existing_sid != resolved_sid:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f'service_name({s_name!r}) 与 service_id(0x{existing_sid:02X}) 冲突'
|
|
65
|
+
)
|
|
66
|
+
data['service_id'] = resolved_sid
|
|
67
|
+
|
|
68
|
+
# 2. 尝试获取 service_id(可能上一步已设置,或用户直接提供)
|
|
69
|
+
s_id = data.get('service_id')
|
|
70
|
+
|
|
71
|
+
# 3. 解析 sub_fn_name -> sub_fn(此时必须已知 service_id)
|
|
72
|
+
sub_fn_name = data.pop('sub_fn_name', None)
|
|
73
|
+
if sub_fn_name is not None:
|
|
74
|
+
if s_id is None:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
'提供了 sub_fn_name 但未指定服务,请同时提供 service_id 或 service_name'
|
|
77
|
+
)
|
|
78
|
+
table = SUB_FN_MAP.get(s_id)
|
|
79
|
+
if table is None or not table.get('enable'):
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f'服务 0x{s_id:02X} 不支持子功能,却提供了 sub_fn_name: {sub_fn_name!r}'
|
|
82
|
+
)
|
|
83
|
+
resolved_fn = table.get(sub_fn_name)
|
|
84
|
+
if resolved_fn is None:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f'未知子功能名: {sub_fn_name!r} (服务 0x{s_id:02X})'
|
|
87
|
+
)
|
|
88
|
+
existing_fn = data.get('sub_fn')
|
|
89
|
+
if existing_fn is not None and existing_fn != resolved_fn:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f'sub_fn_name({sub_fn_name!r}) 与 sub_fn(0x{existing_fn:02X}) 冲突'
|
|
92
|
+
)
|
|
93
|
+
data['sub_fn'] = resolved_fn
|
|
94
|
+
|
|
95
|
+
# 4. 验证直接传入的 sub_fn 值(如果存在)
|
|
96
|
+
fn = data.get('sub_fn')
|
|
97
|
+
if fn is not None:
|
|
98
|
+
if not isinstance(fn, int) or not (0x00 <= fn <= 0x7F):
|
|
99
|
+
raise ValueError('sub_fn 必须是 0x00~0x7F 的整数')
|
|
100
|
+
if s_id is None:
|
|
101
|
+
raise ValueError('提供了 sub_fn 但未指定服务,请同时提供 service_id 或 service_name')
|
|
102
|
+
table = SUB_FN_MAP.get(s_id)
|
|
103
|
+
if table is None or not table.get('enable'):
|
|
104
|
+
raise ValueError(f'服务 0x{s_id:02X} 不支持子功能,却提供了 sub_fn=0x{fn:02X}')
|
|
105
|
+
|
|
106
|
+
allowed_values = {v for k, v in table.items() if k != 'enable'}
|
|
107
|
+
if fn not in allowed_values:
|
|
108
|
+
raise ValueError(f'sub_fn=0x{fn:02X} 对于服务 0x{s_id:02X} 无效')
|
|
109
|
+
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
@model_validator(mode='after')
|
|
113
|
+
def _build_fn_byte(self):
|
|
114
|
+
"""根据 sub_fn 和 suppress_positive_response 构造功能字节。"""
|
|
115
|
+
if self.sub_fn is None:
|
|
116
|
+
self._fn_byte = b''
|
|
117
|
+
else:
|
|
118
|
+
fn_byte = self.sub_fn
|
|
119
|
+
if self.suppress_positive_response:
|
|
120
|
+
fn_byte |= 0x80
|
|
121
|
+
self._fn_byte = fn_byte.to_bytes(1, 'big')
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
126
|
+
# Response
|
|
127
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
128
|
+
|
|
129
|
+
class Response(BaseModel):
|
|
130
|
+
father: Response | None = None
|
|
131
|
+
request: Request
|
|
132
|
+
raw: bytes = b''
|
|
133
|
+
|
|
134
|
+
# ── 解析后的字段 ──
|
|
135
|
+
ok: bool = False
|
|
136
|
+
is_negative: bool = False
|
|
137
|
+
|
|
138
|
+
service_id: int = 0
|
|
139
|
+
sub_fn: int | None = None
|
|
140
|
+
params: bytes = b''
|
|
141
|
+
|
|
142
|
+
request_id: int = 0
|
|
143
|
+
nrc: int = 0
|
|
144
|
+
nrc_desc: str = ''
|
|
145
|
+
|
|
146
|
+
# noinspection PyPropertyDefinition
|
|
147
|
+
@property
|
|
148
|
+
def hex(self) -> str:
|
|
149
|
+
return ' '.join(f'{b:02X}' for b in self.raw)
|
|
150
|
+
|
|
151
|
+
@model_validator(mode='after')
|
|
152
|
+
def _parse(self):
|
|
153
|
+
if not self.raw:
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
raw = self.raw
|
|
157
|
+
s_id = raw[0]
|
|
158
|
+
|
|
159
|
+
match s_id:
|
|
160
|
+
case 0x7F:
|
|
161
|
+
self.__handle_negative(raw)
|
|
162
|
+
case _:
|
|
163
|
+
self.__handle_positive(raw)
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def __handle_negative(self, raw: bytes):
|
|
167
|
+
if len(raw) < 3:
|
|
168
|
+
raise ValueError("负响应长度不足,至少需要 3 字节")
|
|
169
|
+
request_id = raw[1]
|
|
170
|
+
if request_id != self.request.service_id:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"负响应的请求 SID 不匹配:期望 0x{self.request.service_id:02X},"
|
|
173
|
+
f"收到 0x{request_id:02X}"
|
|
174
|
+
)
|
|
175
|
+
self.request_id = request_id
|
|
176
|
+
self.is_negative = True
|
|
177
|
+
self.service_id = raw[0]
|
|
178
|
+
self.nrc = raw[2]
|
|
179
|
+
self.nrc_desc = NRC_MAP.get(self.nrc, "Unknown NRC")
|
|
180
|
+
|
|
181
|
+
def __handle_positive(self, raw: bytes):
|
|
182
|
+
expected_sid = self.request.service_id | 0x40
|
|
183
|
+
s_id = raw[0]
|
|
184
|
+
if s_id != expected_sid:
|
|
185
|
+
raise ValueError(f"无效响应:期望 SID 0x{expected_sid:02X},收到 0x{s_id:02X}")
|
|
186
|
+
|
|
187
|
+
self.ok = True
|
|
188
|
+
self.is_negative = False
|
|
189
|
+
self.service_id = s_id
|
|
190
|
+
|
|
191
|
+
# 判断服务是否需要子功能字节
|
|
192
|
+
table = SUB_FN_MAP.get(self.request.service_id)
|
|
193
|
+
need_sub_fn = (table is not None and table.get('enable', False))
|
|
194
|
+
|
|
195
|
+
if not need_sub_fn:
|
|
196
|
+
self.sub_fn = None
|
|
197
|
+
self.params = raw[1:] if len(raw) > 1 else b''
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if len(raw) < 2:
|
|
201
|
+
raise ValueError(f"服务 0x{self.request.service_id:02X} 需要子功能,但响应缺少子功能字节")
|
|
202
|
+
|
|
203
|
+
if self.request.sub_fn is None:
|
|
204
|
+
raise ValueError("服务需要子功能但请求未提供子功能")
|
|
205
|
+
|
|
206
|
+
response_sub_fn = raw[1]
|
|
207
|
+
if response_sub_fn != self.request.sub_fn:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"响应子功能不匹配:期望 0x{self.request.sub_fn:02X},"
|
|
210
|
+
f"收到 0x{response_sub_fn:02X}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
self.sub_fn = response_sub_fn
|
|
214
|
+
self.params = raw[2:] if len(raw) > 2 else b''
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
218
|
+
# UdsError
|
|
219
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
220
|
+
|
|
221
|
+
class UdsError(Exception):
|
|
222
|
+
"""UDS 负响应异常,通过 resp 属性获取完整 Response。"""
|
|
223
|
+
|
|
224
|
+
def __init__(self, resp: Response):
|
|
225
|
+
self.resp = resp
|
|
226
|
+
super().__init__(f'{resp.nrc_desc} 0x{resp.nrc:02X}')
|