Metadata-Version: 2.4
Name: autouds
Version: 0.3.0
Summary: UDS (Unified Diagnostic Services) diagnostic tool over DoIP — ISO 14229-1
Project-URL: Homepage, https://github.com/leno166/autouds
Project-URL: Source, https://github.com/leno166/autouds
Author: leno augenstern
License: MIT
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Networking
Requires-Python: >=3.14
Requires-Dist: autodoip>=0.3.0
Requires-Dist: pydantic>=2.0
Description-Content-Type: text/markdown

# autouds

**autouds** 是一个基于 Python 的 UDS (Unified Diagnostic Services) 诊断工具，遵循 ISO 14229-1 标准，通过 [autodoip](https://github.com/leno166/autodoip) (ISO 13400) 传输层与汽车 ECU 进行诊断通信。

采用生成器驱动的装饰器模式，将 Python 方法映射为 UDS 服务调用，自动处理请求构造、响应解析和负响应异常。

## 特性

- 覆盖 ISO 14229-1 主要诊断服务：会话控制、安全访问、DTC 读写、数据标识符读写、例行程序控制等
- 装饰器驱动：`@handler(service_name='session')` 将生成器方法映射到 UDS 服务
- 生成器模式：`yield` 发送请求，接收 `Response` 后继续处理；负响应通过 `gen.throw(UdsError)` 注入生成器
- 逃生舱：`request(raw)` 接受原始 hex 字符串，绕过校验链直接发送任意报文
- 智能请求构造：支持中文/英文/短键名解析服务 ID 和子功能
- 自动处理延迟响应 (NRC 0x78) 的轮询等待
- 分级日志标签：`[意图保证]` / `[涌现能力]` / `[约束补偿]` / `[隐含依赖]` 覆盖完整交互链路
- 基于 [autodoip](https://github.com/leno166/autodoip) 零依赖传输层

## 安装

```bash
pip install autouds
```

`autodoip` 和 `pydantic` 会作为依赖自动安装。要求 Python ≥ 3.14。

## 快速开始

```python
from autouds import App

# 创建诊断应用：监听指定 IP，预定义 ECU 列表
app = App(
    ip='192.168.10.1',
    ecus={
        'mcu': (0x1001, '192.168.10.10', 13400),
        'soc': (0x1002, '192.168.10.20', 13400),
    },
)

# 进入默认会话，获取 P2/P2* 定时参数
p2, p2_star = app.session(sub_fn_name='default')
print(f'P2={p2}ms, P2*={p2_star}ms')

# 安全访问 — 请求种子
seed = app.unlock(0x01)
print(f'seed: {seed.hex()}')
# ... 计算密钥 ...
app.unlock_ex(0x01, key)

# 读取数据标识符
data = app.read_did(0xFF00)
print(f'DID 0xFF00: {data.hex()}')

# 切换到另一个 ECU
app.on('soc')

# 逃生舱：发送任意 hex 报文
resp = app.request('10 01')
print(resp.hex)
```

## 命令行

```bash
python -m autouds [ECU_IP]
```

## 支持的 UDS 服务

### 通用请求（逃生舱）

| 方法 | 说明 |
|------|------|
| `app.request(raw)` | 发送 hex 字符串报文，首字节 = SID，剩余 = params。绕过校验链 |

`request()` 不校验服务名/子功能名，sub_fn 需自行编码在 raw 中。适合调试、发送未封装的服务。

### 诊断会话控制 (0x10)

```python
app.session(*, sub_fn_name='default', suppress_positive_response=False) -> tuple[int, int]
```

| sub_fn_name | 子功能 | 说明 |
|-------------|--------|------|
| `'default'` | 0x01 | 默认会话，返回 `(P2, P2*)` |
| `'program'` | 0x02 | 编程会话 |
| `'extend'`  | 0x03 | 扩展会话 |
| `'safety'`  | 0x04 | 安全系统会话 |

### ECU 复位 (0x11)

```python
app.reset(*, sub_fn_name='hard', suppress_positive_response=False) -> Response
```

| sub_fn_name | 子功能 | 说明 |
|-------------|--------|------|
| `'hard'` | 0x01 | 硬复位 |
| `'key'`  | 0x02 | 钥匙复位 |
| `'soft'` | 0x03 | 软复位 |
| `'on'`   | 0x04 | 启用快速断电 |
| `'off'`  | 0x05 | 禁用快速断电 |

### 安全访问 (0x27)

```python
app.unlock(level, suppress_positive_response=False)            -> bytes   # 请求种子
app.unlock_ex(level, payload, suppress_positive_response=False)           # 发送密钥
```

`level` 为安全等级（0x01 / 0x05 / 0x19），`unlock_ex` 内部自动 `level+1` 作为密钥发送的子功能。

| 调用 | 报文 | 说明 |
|------|------|------|
| `app.unlock(0x01)` | `27 01` | 请求 L1 种子，返回 seed bytes |
| `app.unlock_ex(0x01, key)` | `27 02 <key>` | 发送 L1 密钥 |
| `app.unlock(0x05)` | `27 05` | 请求 L5 种子 |
| `app.unlock_ex(0x05, key)` | `27 06 <key>` | 发送 L5 密钥 |
| `app.unlock(0x19)` | `27 19` | 请求 L19 种子 |
| `app.unlock_ex(0x19, key)` | `27 1A <key>` | 发送 L19 密钥 |

抑制肯定响应时 `unlock()` 返回 `b''`。

### 通讯控制 (0x28)

```python
app.comm_control(rx=True, tx=True, plus=False, type='app', sub_net=0x0,
                 suppress_positive_response=False) -> Response
```

用三个布尔开关选择子功能：

| rx | tx | plus | 子功能 | 说明 |
|----|----|------|--------|------|
| T  | T  | F    | 0x00 | 启用接收和发送 |
| T  | F  | F    | 0x01 | 启用接收，禁用发送 |
| F  | T  | F    | 0x02 | 禁用接收，启用发送 |
| F  | F  | F    | 0x03 | 禁用接收和发送 |
| T  | F  | T    | 0x04 | 启用接收（增强），禁用发送 |
| T  | T  | T    | 0x05 | 启用接收和发送（增强） |

`type` 指定报文类型：`'app'`（应用报文）、`'nm'`（网络管理）、`'all'`（两者）。`sub_net` 为子网号（0x0~0xF，0xF = 所有子网）。

### 待机握手 (0x3E)

```python
app.present(*, suppress_positive_response=True) -> Response
```

默认抑制肯定响应（`3E 80`），传 `False` 得 `3E 00`。

### 控制 DTC 设置 (0x85)

```python
app.dtc_on(*, suppress_positive_response=False)  -> bool
app.dtc_off(*, suppress_positive_response=False) -> bool
```

| 方法 | 报文 | 说明 |
|------|------|------|
| `app.dtc_on()`  | `85 01` → `C5 01` | 允许 DTC 更新，返回 `True`/`False` |
| `app.dtc_off()` | `85 02` → `C5 02` | 禁止 DTC 更新，返回 `True`/`False` |

负响应时内部捕获 `UdsError`，返回 `False`。

### 清除诊断信息 (0x14)

```python
app.clear_dtc(mask='FFFFFF') -> bool
```

`mask` 为 3 字节 hex 字符串，指定要清除的 DTC 分组（默认 `'FFFFFF'` 全部清除）。负响应返回 `False`。

### 读取 DTC 信息 (0x19)

> ⚠️ 以下方法尚未实现，仅保留签名预留。

```python
app.read_dtc_count()     # TODO: 需要 status mask 参数
app.read_dtc_by_mask()   # TODO: 需要 status mask、DTC mask 参数
```

### 数据读写 (0x22 / 0x2E)

```python
app.read_did(did)              -> bytes   # 按标识符读取数据
app.write_did(did, payload)    -> None    # 按标识符写入数据
```

`read_did` 自动校验响应中的 DID 回显，返回 `params[2:]`（去掉回显 DID 后的数据体）。

### 例行程序控制 (0x31)

```python
app.routine(*, routine_id, sub_fn_name, data=b'',
            suppress_positive_response=False) -> Response
```

| sub_fn_name | 子功能 | 说明 |
|-------------|--------|------|
| `'start'` | 0x01 | 启动例行程序 |
| `'stop'`  | 0x02 | 停止例行程序 |
| `'poll'`  | 0x03 | 请求执行结果 |

例：`app.routine(routine_id=0x0237, sub_fn_name='start', data=glid)` → `31 01 02 37 <glid>`

### 未实现的服务

> ⚠️ 以下方法标记为 TODO，调用会返回 `None`（生成器体为 `...`）。

| 方法 | 服务 | 说明 |
|------|------|------|
| `app.io_control()` | 0x2F | I/O 控制 — 需要 DID、控制参数 |
| `app.request_download()` | 0x34 | 请求下载 — 需要内存地址、大小 |
| `app.request_upload()` | 0x35 | 请求上传 — 需要内存地址、大小 |
| `app.transfer_data()` | 0x36 | 数据传输 — 需要 block_counter、数据 |
| `app.exit_transfer()` | 0x37 | 退出传输 — 可能需要参数 |

## API 概览

| 类/函数 | 说明 |
|---------|------|
| `App(ip, ecus, port=13400, tester=0x0E80, doip_config=None)` | 诊断应用主类 |
| `App.on(name)` | 切换到指定 ECU |
| `App.close()` | 关闭 DoIP 连接 |
| `App.request(raw)` | 逃生舱，发送任意 hex 报文 |
| `handler(service_name=..., sub_fn_name=..., s_id=..., sub_fn=..., suppress_positive_response=...)` | 装饰器，将生成器方法映射为 UDS 服务 |
| `Request` | UDS 请求模型，支持语义名或原始 ID 构造 |
| `Response` | UDS 响应模型，自动解析正/负响应，支持多帧串联 |
| `UdsError` | 负响应异常，包含 `resp.nrc` 和 `resp.nrc_desc` |

## 自定义服务

通过 `@handler` 装饰器和 `App` 子类可以扩展自定义 UDS 服务：

```python
from autouds import App, handler, Response
from typing import Generator


class MyApp(App):
    @handler(service_name='read_did')
    def read_vin(self, did: int) -> Generator[bytes, Response, bytes]:
        resp: Response = yield did.to_bytes(2, 'big')
        return resp.params
```

装饰器支持直接指定 service_id / sub_fn：

```python
@handler(s_id=0x22)
def custom_service(self, did: int) -> Generator[bytes, Response, bytes]:
    resp: Response = yield did.to_bytes(2, 'big')
    return resp.params
```

## 依赖

- [autodoip](https://github.com/leno166/autodoip) — DoIP 传输层 (ISO 13400)
- [pydantic](https://github.com/pydantic/pydantic) — 数据验证

## 许可证

MIT License
