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.
@@ -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
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}')