Add UDS protocol support (ISO 14229) for ECU emulator
Implement UDS Service 0x22 (Read Data By Identifier) with support for: - Engine ECU (0x7E0): boost pressure, fuel rail pressure, lambda, torque, wastegate position, ignition timing, knock correction per cylinder - DSG Transmission (0x7E1): current gear, selector position, oil temp - Instrument Cluster (0x714): RPM, ambient light sensor Also adds ECU_EMULATOR_SPEC.md with full protocol documentation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
598
ECU_EMULATOR_SPEC.md
Normal file
598
ECU_EMULATOR_SPEC.md
Normal file
@@ -0,0 +1,598 @@
|
||||
# ECU Emulator Specification
|
||||
|
||||
Документация для создания эмулятора ECU для тестирования Carpibord.
|
||||
|
||||
## Общие сведения
|
||||
|
||||
- **CAN Bus Speed:** 500 kbps
|
||||
- **CAN ID Format:** 11-bit (standard)
|
||||
- **Протоколы:** OBD2 (ISO 15765-4) + UDS (ISO 14229)
|
||||
|
||||
---
|
||||
|
||||
## OBD2 Protocol (Mode 0x01)
|
||||
|
||||
### Формат запроса
|
||||
|
||||
```
|
||||
CAN ID: 0x7DF (broadcast) или 0x7E0 (ECU direct)
|
||||
Data: [Length, Mode, PID, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
|
||||
Пример запроса RPM:
|
||||
7DF [02 01 0C 00 00 00 00 00]
|
||||
│ │ │
|
||||
│ │ └── PID 0x0C (RPM)
|
||||
│ └───── Mode 0x01 (Current Data)
|
||||
└──────── Length: 2 bytes
|
||||
```
|
||||
|
||||
### Формат ответа
|
||||
|
||||
```
|
||||
CAN ID: 0x7E8 (ECU response)
|
||||
Data: [Length, Mode+0x40, PID, DataA, DataB, ...]
|
||||
|
||||
Пример ответа RPM = 2000:
|
||||
7E8 [04 41 0C 1F 40 00 00 00]
|
||||
│ │ │ │ │
|
||||
│ │ │ └───┴── Data: (0x1F * 256 + 0x40) / 4 = 2000 RPM
|
||||
│ │ └──────── PID 0x0C
|
||||
│ └─────────── Mode 0x41 (response to 0x01)
|
||||
└────────────── Length: 4 bytes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OBD2 PIDs — Полная спецификация
|
||||
|
||||
### PID 0x00 — Supported PIDs [01-20]
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 00 00 00 00 00 00]` |
|
||||
| Response | `7E8 [06 41 00 XX XX XX XX 00]` |
|
||||
| Formula | Bitmask of supported PIDs |
|
||||
|
||||
**Ответ для нашего набора PIDs:**
|
||||
```
|
||||
Поддерживаем: 0x04, 0x05, 0x0C, 0x0D, 0x0F, 0x10, 0x11
|
||||
Bitmask: 0x18 3F 80 00
|
||||
|
||||
7E8 [06 41 00 18 3F 80 00 00]
|
||||
```
|
||||
|
||||
### PID 0x04 — Engine Load
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 04 00 00 00 00 00]` |
|
||||
| Response | `7E8 [03 41 04 XX 00 00 00 00]` |
|
||||
| Formula | `A * 100 / 255` (%) |
|
||||
| Min | 0% |
|
||||
| Max | 100% |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Load = 0%: 7E8 [03 41 04 00 00 00 00 00]
|
||||
Load = 50%: 7E8 [03 41 04 80 00 00 00 00] (0x80 = 128 → 50.2%)
|
||||
Load = 100%: 7E8 [03 41 04 FF 00 00 00 00]
|
||||
```
|
||||
|
||||
### PID 0x05 — Coolant Temperature
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 05 00 00 00 00 00]` |
|
||||
| Response | `7E8 [03 41 05 XX 00 00 00 00]` |
|
||||
| Formula | `A - 40` (°C) |
|
||||
| Min | -40°C |
|
||||
| Max | 215°C |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Temp = -40°C: 7E8 [03 41 05 00 00 00 00 00]
|
||||
Temp = 0°C: 7E8 [03 41 05 28 00 00 00 00] (0x28 = 40)
|
||||
Temp = 90°C: 7E8 [03 41 05 82 00 00 00 00] (0x82 = 130)
|
||||
Temp = 105°C: 7E8 [03 41 05 91 00 00 00 00] (0x91 = 145)
|
||||
```
|
||||
|
||||
### PID 0x0C — Engine RPM
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 0C 00 00 00 00 00]` |
|
||||
| Response | `7E8 [04 41 0C XX YY 00 00 00]` |
|
||||
| Formula | `(A * 256 + B) / 4` (RPM) |
|
||||
| Min | 0 RPM |
|
||||
| Max | 16383.75 RPM |
|
||||
| Bytes | 2 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
RPM = 0: 7E8 [04 41 0C 00 00 00 00 00]
|
||||
RPM = 800: 7E8 [04 41 0C 0C 80 00 00 00] (0x0C80 = 3200, /4 = 800)
|
||||
RPM = 2000: 7E8 [04 41 0C 1F 40 00 00 00] (0x1F40 = 8000, /4 = 2000)
|
||||
RPM = 3500: 7E8 [04 41 0C 36 B0 00 00 00] (0x36B0 = 14000, /4 = 3500)
|
||||
RPM = 6000: 7E8 [04 41 0C 5D C0 00 00 00] (0x5DC0 = 24000, /4 = 6000)
|
||||
```
|
||||
|
||||
**Формула кодирования:** `raw = RPM * 4`, затем `A = raw >> 8`, `B = raw & 0xFF`
|
||||
|
||||
### PID 0x0D — Vehicle Speed
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 0D 00 00 00 00 00]` |
|
||||
| Response | `7E8 [03 41 0D XX 00 00 00 00]` |
|
||||
| Formula | `A` (km/h) |
|
||||
| Min | 0 km/h |
|
||||
| Max | 255 km/h |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Speed = 0: 7E8 [03 41 0D 00 00 00 00 00]
|
||||
Speed = 50: 7E8 [03 41 0D 32 00 00 00 00]
|
||||
Speed = 120: 7E8 [03 41 0D 78 00 00 00 00]
|
||||
Speed = 200: 7E8 [03 41 0D C8 00 00 00 00]
|
||||
```
|
||||
|
||||
### PID 0x0F — Intake Air Temperature
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 0F 00 00 00 00 00]` |
|
||||
| Response | `7E8 [03 41 0F XX 00 00 00 00]` |
|
||||
| Formula | `A - 40` (°C) |
|
||||
| Min | -40°C |
|
||||
| Max | 215°C |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Temp = 25°C: 7E8 [03 41 0F 41 00 00 00 00] (0x41 = 65)
|
||||
Temp = 40°C: 7E8 [03 41 0F 50 00 00 00 00] (0x50 = 80)
|
||||
```
|
||||
|
||||
### PID 0x10 — MAF Air Flow Rate
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 10 00 00 00 00 00]` |
|
||||
| Response | `7E8 [04 41 10 XX YY 00 00 00]` |
|
||||
| Formula | `(A * 256 + B) / 100` (g/s) |
|
||||
| Min | 0 g/s |
|
||||
| Max | 655.35 g/s |
|
||||
| Bytes | 2 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
MAF = 5 g/s: 7E8 [04 41 10 01 F4 00 00 00] (0x01F4 = 500, /100 = 5)
|
||||
MAF = 25 g/s: 7E8 [04 41 10 09 C4 00 00 00] (0x09C4 = 2500, /100 = 25)
|
||||
MAF = 100 g/s: 7E8 [04 41 10 27 10 00 00 00] (0x2710 = 10000, /100 = 100)
|
||||
```
|
||||
|
||||
### PID 0x11 — Throttle Position
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 11 00 00 00 00 00]` |
|
||||
| Response | `7E8 [03 41 11 XX 00 00 00 00]` |
|
||||
| Formula | `A * 100 / 255` (%) |
|
||||
| Min | 0% |
|
||||
| Max | 100% |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Throttle = 0%: 7E8 [03 41 11 00 00 00 00 00]
|
||||
Throttle = 15%: 7E8 [03 41 11 26 00 00 00 00] (0x26 = 38 → 14.9%)
|
||||
Throttle = 50%: 7E8 [03 41 11 80 00 00 00 00]
|
||||
Throttle = 100%: 7E8 [03 41 11 FF 00 00 00 00]
|
||||
```
|
||||
|
||||
### PID 0x20 — Supported PIDs [21-40]
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 20 00 00 00 00 00]` |
|
||||
| Response | `7E8 [06 41 20 XX XX XX XX 00]` |
|
||||
|
||||
**Ответ для поддержки 0x2F:**
|
||||
```
|
||||
7E8 [06 41 20 00 00 00 01 00]
|
||||
```
|
||||
|
||||
### PID 0x2F — Fuel Tank Level
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 2F 00 00 00 00 00]` |
|
||||
| Response | `7E8 [03 41 2F XX 00 00 00 00]` |
|
||||
| Formula | `A * 100 / 255` (%) |
|
||||
| Min | 0% |
|
||||
| Max | 100% |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Fuel = 25%: 7E8 [03 41 2F 40 00 00 00 00]
|
||||
Fuel = 50%: 7E8 [03 41 2F 80 00 00 00 00]
|
||||
Fuel = 75%: 7E8 [03 41 2F BF 00 00 00 00]
|
||||
```
|
||||
|
||||
### PID 0x40 — Supported PIDs [41-60]
|
||||
|
||||
```
|
||||
7E8 [06 41 40 00 00 00 01 00] (поддержка 0x5C)
|
||||
```
|
||||
|
||||
### PID 0x5C — Oil Temperature
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7DF [02 01 5C 00 00 00 00 00]` |
|
||||
| Response | `7E8 [03 41 5C XX 00 00 00 00]` |
|
||||
| Formula | `A - 40` (°C) |
|
||||
| Min | -40°C |
|
||||
| Max | 210°C |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Oil Temp = 90°C: 7E8 [03 41 5C 82 00 00 00 00] (0x82 = 130)
|
||||
Oil Temp = 110°C: 7E8 [03 41 5C 96 00 00 00 00] (0x96 = 150)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UDS Protocol (Mode 0x22)
|
||||
|
||||
### Формат запроса
|
||||
|
||||
```
|
||||
CAN ID: зависит от ECU (см. таблицу)
|
||||
Data: [03, 22, PID_H, PID_L, 55, 55, 55, 55]
|
||||
|
||||
Пример запроса Boost (PID 0x202A) к Engine ECU:
|
||||
7E0 [03 22 20 2A 55 55 55 55]
|
||||
│ │ │ │
|
||||
│ │ └───┴── PID 0x202A
|
||||
│ └───────── Service 0x22 (Read Data By ID)
|
||||
└──────────── Length: 3 bytes
|
||||
```
|
||||
|
||||
### Формат ответа
|
||||
|
||||
```
|
||||
CAN ID: response ID для ECU
|
||||
Data: [Length, 62, PID_H, PID_L, DataA, DataB, ...]
|
||||
|
||||
Пример ответа Boost = 185 kPa:
|
||||
7E8 [05 62 20 2A 07 3A 00 00]
|
||||
│ │ │ │ │ │
|
||||
│ │ └───┴──┴───┴── PID + Data: 0x073A = 1850, *0.1 = 185.0 kPa
|
||||
│ └────────────── Service 0x62 (positive response)
|
||||
└───────────────── Length: 5 bytes
|
||||
```
|
||||
|
||||
### Negative Response
|
||||
|
||||
Если PID не поддерживается:
|
||||
```
|
||||
7E8 [03 7F 22 31 00 00 00 00]
|
||||
│ │ │ │
|
||||
│ │ │ └── Error code 0x31 (Request Out of Range)
|
||||
│ │ └───── Service that failed (0x22)
|
||||
│ └──────── Negative response indicator (0x7F)
|
||||
└─────────── Length: 3 bytes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UDS PIDs — Engine ECU (0x7E0 → 0x7E8)
|
||||
|
||||
### PID 0x202A — Boost Pressure Actual
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E0 [03 22 20 2A 55 55 55 55]` |
|
||||
| Response | `7E8 [05 62 20 2A XX YY 00 00]` |
|
||||
| Formula | `(A * 256 + B) * 0.1` (kPa) |
|
||||
| Min | 0 kPa |
|
||||
| Max | 400 kPa |
|
||||
| Bytes | 2 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Boost = 100 kPa (атм): 7E8 [05 62 20 2A 03 E8 00 00] (0x03E8 = 1000)
|
||||
Boost = 150 kPa: 7E8 [05 62 20 2A 05 DC 00 00] (0x05DC = 1500)
|
||||
Boost = 200 kPa: 7E8 [05 62 20 2A 07 D0 00 00] (0x07D0 = 2000)
|
||||
Boost = 250 kPa (1.5 bar): 7E8 [05 62 20 2A 09 C4 00 00] (0x09C4 = 2500)
|
||||
```
|
||||
|
||||
### PID 0x2029 — Boost Pressure Target
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E0 [03 22 20 29 55 55 55 55]` |
|
||||
| Response | `7E8 [05 62 20 29 XX YY 00 00]` |
|
||||
| Formula | `(A * 256 + B) * 0.1` (kPa) |
|
||||
| Min | 0 kPa |
|
||||
| Max | 400 kPa |
|
||||
| Bytes | 2 |
|
||||
|
||||
### PID 0xF423 — Fuel Rail Pressure
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E0 [03 22 F4 23 55 55 55 55]` |
|
||||
| Response | `7E8 [05 62 F4 23 XX YY 00 00]` |
|
||||
| Formula | `(A * 256 + B) * 10` (kPa) |
|
||||
| Min | 0 kPa |
|
||||
| Max | 25000 kPa (250 bar) |
|
||||
| Bytes | 2 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
FRP = 5000 kPa (50 bar): 7E8 [05 62 F4 23 01 F4 00 00] (0x01F4 = 500)
|
||||
FRP = 15000 kPa (150 bar): 7E8 [05 62 F4 23 05 DC 00 00] (0x05DC = 1500)
|
||||
FRP = 20000 kPa (200 bar): 7E8 [05 62 F4 23 07 D0 00 00] (0x07D0 = 2000)
|
||||
```
|
||||
|
||||
### PID 0x10C0 — Lambda Actual
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E0 [03 22 10 C0 55 55 55 55]` |
|
||||
| Response | `7E8 [05 62 10 C0 XX YY 00 00]` |
|
||||
| Formula | `(A * 256 + B) * 0.000977` (λ) |
|
||||
| Min | 0.5 λ |
|
||||
| Max | 2.0 λ |
|
||||
| Bytes | 2 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Lambda = 1.0 (stoich): 7E8 [05 62 10 C0 04 00 00 00] (0x0400 = 1024 → 1.000)
|
||||
Lambda = 0.8 (rich): 7E8 [05 62 10 C0 03 33 00 00] (0x0333 = 819 → 0.800)
|
||||
Lambda = 1.1 (lean): 7E8 [05 62 10 C0 04 66 00 00] (0x0466 = 1126 → 1.100)
|
||||
```
|
||||
|
||||
**Формула кодирования:** `raw = lambda / 0.000977`
|
||||
|
||||
### PID 0x1456 — Lambda Target
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E0 [03 22 14 56 55 55 55 55]` |
|
||||
| Response | `7E8 [05 62 14 56 XX YY 00 00]` |
|
||||
| Formula | `(A * 256 + B) * 0.000977` (λ) |
|
||||
|
||||
### PID 0x437C — Torque Actual
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E0 [03 22 43 7C 55 55 55 55]` |
|
||||
| Response | `7E8 [05 62 43 7C XX YY 00 00]` |
|
||||
| Formula | `(A * 256 + B) * 0.1` (Nm) |
|
||||
| Min | 0 Nm |
|
||||
| Max | 500 Nm |
|
||||
| Bytes | 2 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Torque = 50 Nm: 7E8 [05 62 43 7C 01 F4 00 00] (0x01F4 = 500)
|
||||
Torque = 200 Nm: 7E8 [05 62 43 7C 07 D0 00 00] (0x07D0 = 2000)
|
||||
Torque = 350 Nm: 7E8 [05 62 43 7C 0D AC 00 00] (0x0DAC = 3500)
|
||||
```
|
||||
|
||||
### PID 0x39A2 — Wastegate Position Actual
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E0 [03 22 39 A2 55 55 55 55]` |
|
||||
| Response | `7E8 [05 62 39 A2 XX YY 00 00]` |
|
||||
| Formula | `(A * 256 + B) * 0.01` (%) |
|
||||
| Min | 0% |
|
||||
| Max | 100% |
|
||||
| Bytes | 2 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
WG = 0% (closed): 7E8 [05 62 39 A2 00 00 00 00]
|
||||
WG = 50%: 7E8 [05 62 39 A2 13 88 00 00] (0x1388 = 5000)
|
||||
WG = 100% (open): 7E8 [05 62 39 A2 27 10 00 00] (0x2710 = 10000)
|
||||
```
|
||||
|
||||
### PID 0x39A3 — Wastegate Position Target
|
||||
|
||||
Аналогично 0x39A2, Request: `7E0 [03 22 39 A3 55 55 55 55]`
|
||||
|
||||
### PID 0x2004 — Ignition Timing
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E0 [03 22 20 04 55 55 55 55]` |
|
||||
| Response | `7E8 [05 62 20 04 XX YY 00 00]` |
|
||||
| Formula | `signed(A * 256 + B) * 0.01` (°) |
|
||||
| Min | -10° |
|
||||
| Max | 50° |
|
||||
| Bytes | 2 (signed) |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Timing = 0°: 7E8 [05 62 20 04 00 00 00 00]
|
||||
Timing = 10°: 7E8 [05 62 20 04 03 E8 00 00] (0x03E8 = 1000)
|
||||
Timing = 25°: 7E8 [05 62 20 04 09 C4 00 00] (0x09C4 = 2500)
|
||||
Timing = -5°: 7E8 [05 62 20 04 FE 0C 00 00] (0xFE0C = -500 signed)
|
||||
```
|
||||
|
||||
**Формула кодирования (signed):**
|
||||
```python
|
||||
raw = int(timing * 100)
|
||||
if raw < 0:
|
||||
raw = 0x10000 + raw # Two's complement
|
||||
A = (raw >> 8) & 0xFF
|
||||
B = raw & 0xFF
|
||||
```
|
||||
|
||||
### PID 0x200A-0x200D — Timing Correction Cylinders 1-4
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E0 [03 22 20 0A 55 55 55 55]` (Cyl 1) |
|
||||
| Response | `7E8 [05 62 20 0A XX YY 00 00]` |
|
||||
| Formula | `signed(A * 256 + B) * 0.01` (°) |
|
||||
| Min | -15° |
|
||||
| Max | 5° |
|
||||
| Bytes | 2 (signed) |
|
||||
|
||||
**Примеры (Knock retard usually negative):**
|
||||
```
|
||||
Cyl1 TC = 0°: 7E8 [05 62 20 0A 00 00 00 00]
|
||||
Cyl1 TC = -2°: 7E8 [05 62 20 0A FF 38 00 00] (0xFF38 = -200 signed)
|
||||
Cyl1 TC = -5°: 7E8 [05 62 20 0A FE 0C 00 00] (0xFE0C = -500 signed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UDS PIDs — Instrument Cluster (0x714 → 0x77E)
|
||||
|
||||
### PID 0x22D1 — RPM from Cluster
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `714 [03 22 22 D1 55 55 55 55]` |
|
||||
| Response | `77E [05 62 22 D1 XX YY 00 00]` |
|
||||
| Formula | `(A * 256 + B) / 4` (RPM) |
|
||||
| Min | 0 RPM |
|
||||
| Max | 8000 RPM |
|
||||
| Bytes | 2 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
RPM = 850: 77E [05 62 22 D1 0D 48 00 00] (0x0D48 = 3400, /4 = 850)
|
||||
RPM = 3000: 77E [05 62 22 D1 2E E0 00 00] (0x2EE0 = 12000, /4 = 3000)
|
||||
```
|
||||
|
||||
### PID 0x224D — Ambient Light Sensor
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `714 [03 22 22 4D 55 55 55 55]` |
|
||||
| Response | `77E [04 62 22 4D XX 00 00 00]` |
|
||||
| Formula | `A` (0-255, 0=dark, 255=bright) |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Night: 77E [04 62 22 4D 10 00 00 00] (0x10 = 16)
|
||||
Daytime: 77E [04 62 22 4D E0 00 00 00] (0xE0 = 224)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UDS PIDs — DSG Transmission (0x7E1 → 0x7E9)
|
||||
|
||||
### PID 0x3816 — Current Gear
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E1 [03 22 38 16 55 55 55 55]` |
|
||||
| Response | `7E9 [04 62 38 16 XX 00 00 00]` |
|
||||
| Formula | Lookup table |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Gear mapping:**
|
||||
```
|
||||
0x00 = Neutral (0)
|
||||
0x0C = Reverse (-1)
|
||||
0x02 = 1st gear
|
||||
0x04 = 2nd gear
|
||||
0x06 = 3rd gear
|
||||
0x08 = 4th gear
|
||||
0x0A = 5th gear
|
||||
0x0E = 6th gear
|
||||
0x10 = 7th gear
|
||||
```
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Neutral: 7E9 [04 62 38 16 00 00 00 00]
|
||||
Reverse: 7E9 [04 62 38 16 0C 00 00 00]
|
||||
3rd gear: 7E9 [04 62 38 16 06 00 00 00]
|
||||
6th gear: 7E9 [04 62 38 16 0E 00 00 00]
|
||||
```
|
||||
|
||||
### PID 0x3815 — Gear Selector Position (PRNDM)
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E1 [03 22 38 15 55 55 55 55]` |
|
||||
| Response | `7E9 [04 62 38 15 XX 00 00 00]` |
|
||||
| Formula | Lookup table |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Mode mapping:**
|
||||
```
|
||||
0x00 = P (Park)
|
||||
0x01 = R (Reverse)
|
||||
0x02 = N (Neutral)
|
||||
0x03 = D (Drive)
|
||||
0x04 = S (Sport)
|
||||
0x05 = M (Manual)
|
||||
```
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Park: 7E9 [04 62 38 15 00 00 00 00]
|
||||
Drive: 7E9 [04 62 38 15 03 00 00 00]
|
||||
Sport: 7E9 [04 62 38 15 04 00 00 00]
|
||||
Manual: 7E9 [04 62 38 15 05 00 00 00]
|
||||
```
|
||||
|
||||
### PID 0x1940 — Transmission Oil Temperature
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Request | `7E1 [03 22 19 40 55 55 55 55]` |
|
||||
| Response | `7E9 [04 62 19 40 XX 00 00 00]` |
|
||||
| Formula | `A - 40` (°C) |
|
||||
| Min | -40°C |
|
||||
| Max | 150°C |
|
||||
| Bytes | 1 |
|
||||
|
||||
**Примеры:**
|
||||
```
|
||||
Temp = 80°C: 7E9 [04 62 19 40 78 00 00 00] (0x78 = 120)
|
||||
Temp = 100°C: 7E9 [04 62 19 40 8C 00 00 00] (0x8C = 140)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Таблица быстрого референса
|
||||
|
||||
### OBD2 Request/Response Summary
|
||||
|
||||
| PID | Request | Response Example | Value |
|
||||
|-----|---------|------------------|-------|
|
||||
| 0x04 | `7DF 02 01 04` | `7E8 03 41 04 80` | 50% load |
|
||||
| 0x05 | `7DF 02 01 05` | `7E8 03 41 05 82` | 90°C coolant |
|
||||
| 0x0C | `7DF 02 01 0C` | `7E8 04 41 0C 1F 40` | 2000 RPM |
|
||||
| 0x0D | `7DF 02 01 0D` | `7E8 03 41 0D 78` | 120 km/h |
|
||||
| 0x11 | `7DF 02 01 11` | `7E8 03 41 11 40` | 25% throttle |
|
||||
| 0x2F | `7DF 02 01 2F` | `7E8 03 41 2F 80` | 50% fuel |
|
||||
| 0x5C | `7DF 02 01 5C` | `7E8 03 41 5C 96` | 110°C oil |
|
||||
|
||||
### UDS Request/Response Summary
|
||||
|
||||
| ECU | PID | Request | Response Example | Value |
|
||||
|-----|-----|---------|------------------|-------|
|
||||
| Engine | 0x202A | `7E0 03 22 20 2A` | `7E8 05 62 20 2A 07 D0` | 200 kPa boost |
|
||||
| Engine | 0x437C | `7E0 03 22 43 7C` | `7E8 05 62 43 7C 0D AC` | 350 Nm torque |
|
||||
| Engine | 0x10C0 | `7E0 03 22 10 C0` | `7E8 05 62 10 C0 04 00` | λ=1.0 |
|
||||
| Cluster | 0x22D1 | `714 03 22 22 D1` | `77E 05 62 22 D1 1F 40` | 2000 RPM |
|
||||
| DSG | 0x3816 | `7E1 03 22 38 16` | `7E9 04 62 38 16 06` | 3rd gear |
|
||||
| DSG | 0x3815 | `7E1 03 22 38 15` | `7E9 04 62 38 15 03` | Drive mode |
|
||||
108
docs/UDS_PROTOCOL.md
Normal file
108
docs/UDS_PROTOCOL.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# UDS Protocol Support
|
||||
|
||||
Эмулятор поддерживает UDS (Unified Diagnostic Services) протокол согласно ISO 14229.
|
||||
|
||||
## Поддерживаемые ECU
|
||||
|
||||
| ECU | Request ID | Response ID | Описание |
|
||||
|-----|------------|-------------|----------|
|
||||
| Engine ECU | 0x7E0 | 0x7E8 | Блок управления двигателем |
|
||||
| DSG Transmission | 0x7E1 | 0x7E9 | Коробка передач DSG |
|
||||
| Instrument Cluster | 0x714 | 0x77E | Приборная панель |
|
||||
|
||||
## Формат запроса
|
||||
|
||||
```
|
||||
CAN ID: Request ID ECU
|
||||
Data: [03, 22, PID_H, PID_L, 55, 55, 55, 55]
|
||||
│ │ └───┴── Data Identifier (DID)
|
||||
│ └────────── Service 0x22 (Read Data By ID)
|
||||
└────────────── Length: 3 bytes
|
||||
```
|
||||
|
||||
## Формат ответа
|
||||
|
||||
### Положительный ответ
|
||||
```
|
||||
CAN ID: Response ID ECU
|
||||
Data: [Length, 62, PID_H, PID_L, DataA, DataB, ...]
|
||||
│ └───┴── Data Identifier
|
||||
└────────── Service 0x62 (positive response)
|
||||
```
|
||||
|
||||
### Отрицательный ответ
|
||||
```
|
||||
CAN ID: Response ID ECU
|
||||
Data: [03, 7F, 22, ErrorCode, 00, 00, 00, 00]
|
||||
│ │ └── Error code (0x31 = Request Out of Range)
|
||||
│ └────── Service that failed
|
||||
└────────── Negative Response Indicator
|
||||
```
|
||||
|
||||
## Поддерживаемые DIDs
|
||||
|
||||
### Engine ECU (0x7E0 → 0x7E8)
|
||||
|
||||
| DID | Название | Формула | Единицы | Диапазон |
|
||||
|-----|----------|---------|---------|----------|
|
||||
| 0x202A | Boost Pressure Actual | (A*256+B) * 0.1 | kPa | 0-400 |
|
||||
| 0x2029 | Boost Pressure Target | (A*256+B) * 0.1 | kPa | 0-400 |
|
||||
| 0xF423 | Fuel Rail Pressure | (A*256+B) * 10 | kPa | 0-25000 |
|
||||
| 0x10C0 | Lambda Actual | (A*256+B) * 0.000977 | λ | 0.5-2.0 |
|
||||
| 0x1456 | Lambda Target | (A*256+B) * 0.000977 | λ | 0.5-2.0 |
|
||||
| 0x437C | Torque Actual | (A*256+B) * 0.1 | Nm | 0-500 |
|
||||
| 0x39A2 | Wastegate Actual | (A*256+B) * 0.01 | % | 0-100 |
|
||||
| 0x39A3 | Wastegate Target | (A*256+B) * 0.01 | % | 0-100 |
|
||||
| 0x2004 | Ignition Timing | signed(A*256+B) * 0.01 | ° | -10 to 50 |
|
||||
| 0x200A | Timing Correction Cyl1 | signed(A*256+B) * 0.01 | ° | -15 to 5 |
|
||||
| 0x200B | Timing Correction Cyl2 | signed(A*256+B) * 0.01 | ° | -15 to 5 |
|
||||
| 0x200C | Timing Correction Cyl3 | signed(A*256+B) * 0.01 | ° | -15 to 5 |
|
||||
| 0x200D | Timing Correction Cyl4 | signed(A*256+B) * 0.01 | ° | -15 to 5 |
|
||||
|
||||
### DSG Transmission (0x7E1 → 0x7E9)
|
||||
|
||||
| DID | Название | Формула | Значения |
|
||||
|-----|----------|---------|----------|
|
||||
| 0x3816 | Current Gear | Lookup | 0x00=N, 0x0C=R, 0x02=1, 0x04=2, 0x06=3, 0x08=4, 0x0A=5, 0x0E=6, 0x10=7 |
|
||||
| 0x3815 | Gear Selector | Lookup | 0=P, 1=R, 2=N, 3=D, 4=S, 5=M |
|
||||
| 0x1940 | Trans Oil Temp | A - 40 | °C (-40 to 150) |
|
||||
|
||||
### Instrument Cluster (0x714 → 0x77E)
|
||||
|
||||
| DID | Название | Формула | Единицы |
|
||||
|-----|----------|---------|---------|
|
||||
| 0x22D1 | RPM | (A*256+B) / 4 | RPM |
|
||||
| 0x224D | Ambient Light | A | 0-255 (0=dark, 255=bright) |
|
||||
|
||||
## Примеры
|
||||
|
||||
### Запрос Boost Pressure
|
||||
```
|
||||
TX: 7E0 [03 22 20 2A 55 55 55 55]
|
||||
RX: 7E8 [05 62 20 2A 07 D0 00 00] → 0x07D0 = 2000 → 200.0 kPa
|
||||
```
|
||||
|
||||
### Запрос Current Gear (3rd)
|
||||
```
|
||||
TX: 7E1 [03 22 38 16 55 55 55 55]
|
||||
RX: 7E9 [04 62 38 16 06 00 00 00] → 0x06 = 3rd gear
|
||||
```
|
||||
|
||||
### Запрос неподдерживаемого DID
|
||||
```
|
||||
TX: 7E0 [03 22 FF FF 55 55 55 55]
|
||||
RX: 7E8 [03 7F 22 31 00 00 00 00] → Negative response: Request Out of Range
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
Используйте скрипт `scripts/test_uds.py`:
|
||||
|
||||
```bash
|
||||
python scripts/test_uds.py --interface vcan0
|
||||
```
|
||||
|
||||
Опции:
|
||||
- `--engine` - тестировать только Engine ECU
|
||||
- `--dsg` - тестировать только DSG
|
||||
- `--cluster` - тестировать только Instrument Cluster
|
||||
@@ -14,52 +14,70 @@ except ImportError:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# OBD2 PIDs для тестирования
|
||||
# OBD2 PIDs для тестирования (согласно ECU_EMULATOR_SPEC.md)
|
||||
TEST_PIDS = [
|
||||
# Supported PIDs
|
||||
(0x00, "Supported PIDs [01-20]"),
|
||||
(0x20, "Supported PIDs [21-40]"),
|
||||
(0x40, "Supported PIDs [41-60]"),
|
||||
# Engine
|
||||
(0x04, "Engine Load"),
|
||||
(0x05, "Coolant Temperature"),
|
||||
(0x0C, "Engine RPM"),
|
||||
(0x0D, "Vehicle Speed"),
|
||||
(0x0F, "Intake Air Temperature"),
|
||||
(0x10, "MAF Air Flow Rate"),
|
||||
(0x11, "Throttle Position"),
|
||||
# Fuel
|
||||
(0x2F, "Fuel Tank Level"),
|
||||
(0x5C, "Oil Temperature"),
|
||||
# Other
|
||||
(0x46, "Ambient Temperature"),
|
||||
]
|
||||
|
||||
|
||||
def decode_pid(pid: int, data: bytes) -> str:
|
||||
"""Декодировать значение PID."""
|
||||
"""Декодировать значение PID согласно ECU_EMULATOR_SPEC.md."""
|
||||
if len(data) < 1:
|
||||
return "No data"
|
||||
|
||||
if pid == 0x00:
|
||||
# Supported PIDs bitmap
|
||||
if pid in (0x00, 0x20, 0x40, 0x60):
|
||||
if len(data) >= 4:
|
||||
mask = int.from_bytes(data[:4], "big")
|
||||
supported = []
|
||||
base = pid + 1
|
||||
for i in range(32):
|
||||
if mask & (1 << (31 - i)):
|
||||
supported.append(f"{i+1:02X}")
|
||||
return f"PIDs: {', '.join(supported[:10])}..."
|
||||
supported.append(f"{base + i:02X}")
|
||||
if supported:
|
||||
return f"PIDs: {', '.join(supported)}"
|
||||
return "None"
|
||||
return f"Raw: {data.hex()}"
|
||||
|
||||
elif pid == 0x05 or pid == 0x0F or pid == 0x46:
|
||||
# Temperature: A - 40
|
||||
# Temperature PIDs: A - 40
|
||||
elif pid in (0x05, 0x0F, 0x46, 0x5C):
|
||||
return f"{data[0] - 40} °C"
|
||||
|
||||
elif pid == 0x0C:
|
||||
# Engine Load / Throttle / Fuel Level: A * 100 / 255
|
||||
elif pid in (0x04, 0x11, 0x2F):
|
||||
return f"{data[0] * 100 / 255:.1f} %"
|
||||
|
||||
# RPM: (A*256 + B) / 4
|
||||
elif pid == 0x0C:
|
||||
if len(data) >= 2:
|
||||
rpm = (data[0] * 256 + data[1]) / 4
|
||||
return f"{rpm:.0f} rpm"
|
||||
|
||||
elif pid == 0x0D:
|
||||
# Speed: A
|
||||
elif pid == 0x0D:
|
||||
return f"{data[0]} km/h"
|
||||
|
||||
elif pid == 0x11 or pid == 0x2F:
|
||||
# Percentage: A * 100 / 255
|
||||
return f"{data[0] * 100 / 255:.1f} %"
|
||||
# MAF: (A*256 + B) / 100
|
||||
elif pid == 0x10:
|
||||
if len(data) >= 2:
|
||||
maf = (data[0] * 256 + data[1]) / 100
|
||||
return f"{maf:.2f} g/s"
|
||||
|
||||
return f"Raw: {data.hex()}"
|
||||
|
||||
|
||||
250
scripts/test_uds.py
Normal file
250
scripts/test_uds.py
Normal file
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Тестовый скрипт для проверки UDS протокола эмулятора.
|
||||
Отправляет запросы UDS Service 0x22 и проверяет ответы.
|
||||
|
||||
Использование:
|
||||
python test_uds.py --interface vcan0
|
||||
|
||||
Требования:
|
||||
- Запущенный эмулятор на том же интерфейсе
|
||||
- pip install python-can
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
import can
|
||||
|
||||
|
||||
@dataclass
|
||||
class UDSTestCase:
|
||||
"""Тестовый случай для UDS."""
|
||||
name: str
|
||||
request_id: int
|
||||
response_id: int
|
||||
did: int
|
||||
description: str
|
||||
|
||||
|
||||
# Тестовые случаи согласно спецификации
|
||||
ENGINE_ECU_TESTS = [
|
||||
UDSTestCase("Boost Pressure Actual", 0x7E0, 0x7E8, 0x202A, "Boost in kPa"),
|
||||
UDSTestCase("Boost Pressure Target", 0x7E0, 0x7E8, 0x2029, "Target boost in kPa"),
|
||||
UDSTestCase("Fuel Rail Pressure", 0x7E0, 0x7E8, 0xF423, "FRP in kPa"),
|
||||
UDSTestCase("Lambda Actual", 0x7E0, 0x7E8, 0x10C0, "Lambda value"),
|
||||
UDSTestCase("Lambda Target", 0x7E0, 0x7E8, 0x1456, "Target lambda"),
|
||||
UDSTestCase("Torque Actual", 0x7E0, 0x7E8, 0x437C, "Torque in Nm"),
|
||||
UDSTestCase("Wastegate Actual", 0x7E0, 0x7E8, 0x39A2, "Wastegate %"),
|
||||
UDSTestCase("Wastegate Target", 0x7E0, 0x7E8, 0x39A3, "Target wastegate %"),
|
||||
UDSTestCase("Ignition Timing", 0x7E0, 0x7E8, 0x2004, "Timing in degrees"),
|
||||
UDSTestCase("Timing Correction Cyl1", 0x7E0, 0x7E8, 0x200A, "Knock retard"),
|
||||
UDSTestCase("Timing Correction Cyl2", 0x7E0, 0x7E8, 0x200B, "Knock retard"),
|
||||
UDSTestCase("Timing Correction Cyl3", 0x7E0, 0x7E8, 0x200C, "Knock retard"),
|
||||
UDSTestCase("Timing Correction Cyl4", 0x7E0, 0x7E8, 0x200D, "Knock retard"),
|
||||
]
|
||||
|
||||
DSG_TESTS = [
|
||||
UDSTestCase("Current Gear", 0x7E1, 0x7E9, 0x3816, "DSG gear"),
|
||||
UDSTestCase("Gear Selector", 0x7E1, 0x7E9, 0x3815, "PRNDM position"),
|
||||
UDSTestCase("Trans Oil Temp", 0x7E1, 0x7E9, 0x1940, "Temperature"),
|
||||
]
|
||||
|
||||
CLUSTER_TESTS = [
|
||||
UDSTestCase("Cluster RPM", 0x714, 0x77E, 0x22D1, "RPM from cluster"),
|
||||
UDSTestCase("Ambient Light", 0x714, 0x77E, 0x224D, "Light sensor"),
|
||||
]
|
||||
|
||||
|
||||
def build_uds_request(did: int) -> bytes:
|
||||
"""Построить UDS запрос Service 0x22."""
|
||||
did_h = (did >> 8) & 0xFF
|
||||
did_l = did & 0xFF
|
||||
# [length, service, did_h, did_l, padding...]
|
||||
return bytes([0x03, 0x22, did_h, did_l, 0x55, 0x55, 0x55, 0x55])
|
||||
|
||||
|
||||
def parse_uds_response(data: bytes) -> tuple[bool, int | None, bytes | None]:
|
||||
"""
|
||||
Распарсить UDS ответ.
|
||||
Returns: (is_positive, did, data) или (False, None, None) для negative response
|
||||
"""
|
||||
if len(data) < 4:
|
||||
return False, None, None
|
||||
|
||||
length = data[0]
|
||||
service = data[1]
|
||||
|
||||
if service == 0x62: # Positive response
|
||||
did = (data[2] << 8) | data[3]
|
||||
response_data = data[4:4 + length - 3]
|
||||
return True, did, response_data
|
||||
elif service == 0x7F: # Negative response
|
||||
error_code = data[3] if len(data) > 3 else 0
|
||||
return False, None, bytes([error_code])
|
||||
|
||||
return False, None, None
|
||||
|
||||
|
||||
def decode_value(did: int, data: bytes) -> str:
|
||||
"""Декодировать значение UDS ответа."""
|
||||
if len(data) == 0:
|
||||
return "No data"
|
||||
|
||||
if len(data) == 1:
|
||||
raw = data[0]
|
||||
# Temperature PIDs (offset -40)
|
||||
if did in (0x1940,):
|
||||
return f"{raw - 40}°C"
|
||||
# Gear
|
||||
if did == 0x3816:
|
||||
gear_map = {0x00: "N", 0x0C: "R", 0x02: "1", 0x04: "2", 0x06: "3",
|
||||
0x08: "4", 0x0A: "5", 0x0E: "6", 0x10: "7"}
|
||||
return gear_map.get(raw, f"Unknown ({raw:02X})")
|
||||
# Selector
|
||||
if did == 0x3815:
|
||||
pos_map = {0: "P", 1: "R", 2: "N", 3: "D", 4: "S", 5: "M"}
|
||||
return pos_map.get(raw, f"Unknown ({raw})")
|
||||
# Ambient light
|
||||
if did == 0x224D:
|
||||
return f"{raw} (0=dark, 255=bright)"
|
||||
return f"{raw}"
|
||||
|
||||
if len(data) >= 2:
|
||||
raw = (data[0] << 8) | data[1]
|
||||
|
||||
# Boost pressure (scale 0.1)
|
||||
if did in (0x202A, 0x2029):
|
||||
return f"{raw * 0.1:.1f} kPa"
|
||||
|
||||
# Fuel rail pressure (scale 10)
|
||||
if did == 0xF423:
|
||||
return f"{raw * 10} kPa ({raw * 10 / 100:.0f} bar)"
|
||||
|
||||
# Lambda (scale 0.000977)
|
||||
if did in (0x10C0, 0x1456):
|
||||
return f"{raw * 0.000977:.3f} λ"
|
||||
|
||||
# Torque (scale 0.1)
|
||||
if did == 0x437C:
|
||||
return f"{raw * 0.1:.1f} Nm"
|
||||
|
||||
# Wastegate (scale 0.01)
|
||||
if did in (0x39A2, 0x39A3):
|
||||
return f"{raw * 0.01:.1f}%"
|
||||
|
||||
# Signed values (timing)
|
||||
if did in (0x2004, 0x200A, 0x200B, 0x200C, 0x200D):
|
||||
if raw >= 0x8000:
|
||||
raw -= 0x10000
|
||||
return f"{raw * 0.01:.2f}°"
|
||||
|
||||
# RPM (scale /4)
|
||||
if did == 0x22D1:
|
||||
return f"{raw / 4:.0f} RPM"
|
||||
|
||||
return f"0x{raw:04X} ({raw})"
|
||||
|
||||
return data.hex().upper()
|
||||
|
||||
|
||||
def run_test(bus: can.Bus, test: UDSTestCase, timeout: float = 0.5) -> bool:
|
||||
"""Выполнить один тест UDS."""
|
||||
request_data = build_uds_request(test.did)
|
||||
|
||||
# Отправляем запрос
|
||||
msg = can.Message(
|
||||
arbitration_id=test.request_id,
|
||||
data=request_data,
|
||||
is_extended_id=False
|
||||
)
|
||||
bus.send(msg)
|
||||
|
||||
# Ждём ответ
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
response = bus.recv(timeout=0.1)
|
||||
if response is None:
|
||||
continue
|
||||
|
||||
if response.arbitration_id == test.response_id:
|
||||
is_positive, did, data = parse_uds_response(bytes(response.data))
|
||||
|
||||
if is_positive and did == test.did:
|
||||
decoded = decode_value(did, data)
|
||||
print(f" ✓ {test.name}: {decoded}")
|
||||
return True
|
||||
elif not is_positive:
|
||||
error_code = data[0] if data else 0
|
||||
error_names = {
|
||||
0x31: "Request Out of Range",
|
||||
0x22: "Conditions Not Correct",
|
||||
0x33: "Security Access Denied",
|
||||
0x10: "General Reject",
|
||||
}
|
||||
error_name = error_names.get(error_code, f"Unknown (0x{error_code:02X})")
|
||||
print(f" ✗ {test.name}: Negative response - {error_name}")
|
||||
return False
|
||||
|
||||
print(f" ✗ {test.name}: No response (timeout)")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Test UDS protocol of OBD2 emulator")
|
||||
parser.add_argument("-i", "--interface", default="vcan0", help="CAN interface")
|
||||
parser.add_argument("--timeout", type=float, default=0.5, help="Response timeout")
|
||||
parser.add_argument("--engine", action="store_true", help="Test Engine ECU only")
|
||||
parser.add_argument("--dsg", action="store_true", help="Test DSG only")
|
||||
parser.add_argument("--cluster", action="store_true", help="Test Cluster only")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Connecting to {args.interface}...")
|
||||
try:
|
||||
bus = can.Bus(interface="socketcan", channel=args.interface)
|
||||
except Exception as e:
|
||||
print(f"Error: Cannot connect to {args.interface}: {e}")
|
||||
return 1
|
||||
|
||||
print("=" * 60)
|
||||
print(" UDS PROTOCOL TEST")
|
||||
print("=" * 60)
|
||||
|
||||
# Определяем какие тесты запускать
|
||||
run_all = not (args.engine or args.dsg or args.cluster)
|
||||
|
||||
total_tests = 0
|
||||
passed_tests = 0
|
||||
|
||||
if run_all or args.engine:
|
||||
print("\n[Engine ECU - 0x7E0]")
|
||||
for test in ENGINE_ECU_TESTS:
|
||||
total_tests += 1
|
||||
if run_test(bus, test, args.timeout):
|
||||
passed_tests += 1
|
||||
|
||||
if run_all or args.dsg:
|
||||
print("\n[DSG Transmission - 0x7E1]")
|
||||
for test in DSG_TESTS:
|
||||
total_tests += 1
|
||||
if run_test(bus, test, args.timeout):
|
||||
passed_tests += 1
|
||||
|
||||
if run_all or args.cluster:
|
||||
print("\n[Instrument Cluster - 0x714]")
|
||||
for test in CLUSTER_TESTS:
|
||||
total_tests += 1
|
||||
if run_test(bus, test, args.timeout):
|
||||
passed_tests += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Results: {passed_tests}/{total_tests} tests passed")
|
||||
print("=" * 60)
|
||||
|
||||
bus.shutdown()
|
||||
return 0 if passed_tests == total_tests else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -33,6 +33,7 @@ class VehicleConfig(BaseModel):
|
||||
idle_rpm: int = Field(default=850, ge=600, le=1200)
|
||||
max_rpm: int = Field(default=6500, ge=4000, le=9000)
|
||||
redline_rpm: int = Field(default=6000, ge=3500, le=8500)
|
||||
max_torque_nm: float = Field(default=350.0, ge=100, le=800, description="Max torque in Nm")
|
||||
|
||||
# Температуры
|
||||
ambient_temp: float = Field(default=20.0, ge=-40, le=50)
|
||||
|
||||
42
src/main.py
42
src/main.py
@@ -13,6 +13,7 @@ from config import Config, CANConfig, VehicleConfig, SimulatorConfig, set_config
|
||||
from logger import get_logger, setup_logging
|
||||
from can_interface import CANInterface, CANFrame, CANInterfaceError
|
||||
from obd2 import OBD2Protocol
|
||||
from uds import UDSProtocol
|
||||
from simulator import VehicleSimulator, ScenarioManager
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -28,6 +29,7 @@ class OBD2Emulator:
|
||||
self.config = config
|
||||
self.can_interface = CANInterface(config.can)
|
||||
self.obd2_protocol = OBD2Protocol()
|
||||
self.uds_protocol = UDSProtocol()
|
||||
self.vehicle_simulator = VehicleSimulator(config.vehicle)
|
||||
self.scenario_manager = ScenarioManager(self.vehicle_simulator)
|
||||
|
||||
@@ -116,11 +118,14 @@ class OBD2Emulator:
|
||||
if frame is None:
|
||||
continue
|
||||
|
||||
# Обрабатываем как OBD2 запрос
|
||||
response_frame = self.obd2_protocol.process_frame(
|
||||
frame,
|
||||
self.vehicle_simulator.get_state()
|
||||
)
|
||||
vehicle_state = self.vehicle_simulator.get_state()
|
||||
|
||||
# Пробуем обработать как OBD2 запрос
|
||||
response_frame = self.obd2_protocol.process_frame(frame, vehicle_state)
|
||||
|
||||
# Если не OBD2, пробуем как UDS запрос
|
||||
if response_frame is None:
|
||||
response_frame = self.uds_protocol.process_frame(frame, vehicle_state)
|
||||
|
||||
# Отправляем ответ
|
||||
if response_frame:
|
||||
@@ -153,18 +158,23 @@ class OBD2Emulator:
|
||||
"coolant_temp": round(state.coolant_temp, 1),
|
||||
"throttle": round(state.throttle_pos, 1),
|
||||
"fuel_level": round(state.fuel_level, 1),
|
||||
"boost": round(state.boost_pressure_actual, 1),
|
||||
"lambda": round(state.lambda_actual, 3),
|
||||
"torque": round(state.torque_nm, 1),
|
||||
},
|
||||
"obd2_stats": self.obd2_protocol.get_stats(),
|
||||
"uds_stats": self.uds_protocol.get_stats(),
|
||||
}
|
||||
|
||||
def print_status(self) -> None:
|
||||
"""Вывести статус в консоль."""
|
||||
state = self.vehicle_simulator.get_state()
|
||||
stats = self.obd2_protocol.get_stats()
|
||||
obd2_stats = self.obd2_protocol.get_stats()
|
||||
uds_stats = self.uds_protocol.get_stats()
|
||||
|
||||
print("\033[2J\033[H") # Clear screen
|
||||
print("=" * 60)
|
||||
print(" OBD2 EMULATOR STATUS")
|
||||
print(" OBD2 + UDS EMULATOR STATUS")
|
||||
print("=" * 60)
|
||||
print(f" Interface: {self.config.can.interface}")
|
||||
print(f" Scenario: {self.scenario_manager.current_scenario.name if self.scenario_manager.current_scenario else 'None'}")
|
||||
@@ -180,20 +190,28 @@ class OBD2Emulator:
|
||||
print(f" Gear: {state.gear:>6}")
|
||||
print(f" Distance: {state.total_distance:>6.1f} km")
|
||||
print("-" * 60)
|
||||
print(" TURBO (UDS):")
|
||||
print(f" Boost: {state.boost_pressure_actual:>6.1f} kPa")
|
||||
print(f" Wastegate: {state.wastegate_actual:>6.1f} %")
|
||||
print(f" Lambda: {state.lambda_actual:>6.3f} λ")
|
||||
print(f" Torque: {state.torque_nm:>6.1f} Nm")
|
||||
print(f" Ign Timing: {state.ignition_timing:>6.1f} °")
|
||||
print("-" * 60)
|
||||
print(" TEMPERATURES:")
|
||||
print(f" Coolant: {state.coolant_temp:>6.1f} °C")
|
||||
print(f" Oil: {state.oil_temp:>6.1f} °C")
|
||||
print(f" Intake Air: {state.intake_temp:>6.1f} °C")
|
||||
print(f" Ambient: {state.ambient_temp:>6.1f} °C")
|
||||
print(f" Trans Oil: {state.transmission_oil_temp:>6.1f} °C")
|
||||
print("-" * 60)
|
||||
print(" FUEL:")
|
||||
print(f" Level: {state.fuel_level:>6.1f} %")
|
||||
print(f" Rate: {state.fuel_rate:>6.2f} L/h")
|
||||
print(f" Rail Press: {state.fuel_rail_pressure_uds:>6.0f} kPa")
|
||||
print("-" * 60)
|
||||
print(" OBD2 STATS:")
|
||||
print(f" Requests: {stats['requests_received']:>6}")
|
||||
print(f" Responses: {stats['responses_sent']:>6}")
|
||||
print(f" Unsupported: {stats['unsupported_requests']:>6}")
|
||||
print(" PROTOCOL STATS:")
|
||||
print(f" OBD2 Req: {obd2_stats['requests_received']:>6} | UDS Req: {uds_stats['requests_received']:>6}")
|
||||
print(f" OBD2 Resp: {obd2_stats['responses_sent']:>6} | UDS Resp: {uds_stats['responses_sent']:>6}")
|
||||
print(f" OBD2 Unsup: {obd2_stats['unsupported_requests']:>6} | UDS Neg: {uds_stats['negative_responses']:>6}")
|
||||
print("=" * 60)
|
||||
print(" Press Ctrl+C to stop")
|
||||
print("=" * 60)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Симулирует двигатель, трансмиссию, температуры и расход топлива.
|
||||
"""
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable
|
||||
@@ -57,6 +58,29 @@ class VehicleState:
|
||||
engine_running: bool = False
|
||||
gear: int = 0 # 0 = N/P, 1-6 = gears
|
||||
|
||||
# === UDS Engine ECU параметры ===
|
||||
boost_pressure_actual: float = 101.0 # kPa (атмосферное = 101)
|
||||
boost_pressure_target: float = 101.0 # kPa
|
||||
fuel_rail_pressure_uds: float = 5000.0 # kPa (UDS scale: /10)
|
||||
lambda_actual: float = 1.0 # λ (стехиометрия = 1.0)
|
||||
lambda_target: float = 1.0 # λ
|
||||
torque_nm: float = 0.0 # Nm (абсолютный момент)
|
||||
wastegate_actual: float = 0.0 # % (0 = closed, 100 = open)
|
||||
wastegate_target: float = 0.0 # %
|
||||
ignition_timing: float = 10.0 # ° (опережение зажигания)
|
||||
timing_correction_cyl1: float = 0.0 # ° (коррекция по детонации)
|
||||
timing_correction_cyl2: float = 0.0 # °
|
||||
timing_correction_cyl3: float = 0.0 # °
|
||||
timing_correction_cyl4: float = 0.0 # °
|
||||
|
||||
# === UDS DSG Transmission параметры ===
|
||||
current_gear_dsg: int = 0 # -1=R, 0=N, 1-7=gears
|
||||
gear_selector_position: int = 0 # 0=P, 1=R, 2=N, 3=D, 4=S, 5=M
|
||||
transmission_oil_temp: float = 20.0 # °C
|
||||
|
||||
# === UDS Instrument Cluster параметры ===
|
||||
ambient_light: int = 128 # 0-255 (0=dark, 255=bright)
|
||||
|
||||
def __post_init__(self):
|
||||
self._start_time = time.time()
|
||||
|
||||
@@ -385,8 +409,132 @@ class VehicleSimulator:
|
||||
self.state.total_distance += distance_km
|
||||
self.state.distance_since_clear += distance_km
|
||||
|
||||
# === UDS параметры ===
|
||||
self._update_uds_parameters(dt)
|
||||
|
||||
return self.state
|
||||
|
||||
def _update_uds_parameters(self, dt: float) -> None:
|
||||
"""Обновить UDS-специфичные параметры."""
|
||||
state = self.state
|
||||
|
||||
# --- Турбонаддув (boost) ---
|
||||
# Boost зависит от нагрузки и оборотов
|
||||
if state.engine_load > 30 and state.rpm > 2000:
|
||||
# Целевое давление наддува (макс 250 kPa при полной нагрузке)
|
||||
target_boost = 101 + (state.engine_load - 30) * 2.1
|
||||
state.boost_pressure_target = min(250, target_boost)
|
||||
# Фактическое давление следует за целевым с задержкой
|
||||
diff = state.boost_pressure_target - state.boost_pressure_actual
|
||||
state.boost_pressure_actual += diff * 0.3 * dt * 10
|
||||
else:
|
||||
# Атмосферное давление
|
||||
state.boost_pressure_target = 101.0
|
||||
diff = 101.0 - state.boost_pressure_actual
|
||||
state.boost_pressure_actual += diff * 0.5 * dt * 10
|
||||
|
||||
# --- Wastegate (вестгейт) ---
|
||||
# Вестгейт открывается для сброса избыточного давления
|
||||
if state.boost_pressure_actual > state.boost_pressure_target:
|
||||
state.wastegate_target = min(100, (state.boost_pressure_actual - state.boost_pressure_target) * 2)
|
||||
else:
|
||||
state.wastegate_target = max(0, 100 - state.engine_load)
|
||||
|
||||
diff = state.wastegate_target - state.wastegate_actual
|
||||
state.wastegate_actual += diff * 0.2 * dt * 10
|
||||
|
||||
# --- Lambda (AFR) ---
|
||||
# Стехиометрия = 1.0, богатая смесь при полной нагрузке
|
||||
if state.engine_load > 80:
|
||||
state.lambda_target = 0.85 # Богатая смесь для охлаждения
|
||||
elif state.engine_load > 50:
|
||||
state.lambda_target = 0.95
|
||||
else:
|
||||
state.lambda_target = 1.0 # Стехиометрия
|
||||
|
||||
# Фактическая lambda колеблется около целевой
|
||||
diff = state.lambda_target - state.lambda_actual
|
||||
state.lambda_actual += diff * 0.5 * dt * 10
|
||||
# Небольшие колебания
|
||||
state.lambda_actual += random.uniform(-0.01, 0.01)
|
||||
state.lambda_actual = max(0.7, min(1.3, state.lambda_actual))
|
||||
|
||||
# --- Torque (крутящий момент) ---
|
||||
# Максимальный момент зависит от RPM (кривая момента)
|
||||
max_torque = self.config.max_torque_nm
|
||||
if state.rpm < 1500:
|
||||
torque_factor = state.rpm / 1500 * 0.7
|
||||
elif state.rpm < 4500:
|
||||
torque_factor = 0.7 + (state.rpm - 1500) / 3000 * 0.3
|
||||
else:
|
||||
torque_factor = 1.0 - (state.rpm - 4500) / 2000 * 0.2
|
||||
|
||||
state.torque_nm = max_torque * torque_factor * (state.engine_load / 100)
|
||||
|
||||
# --- Fuel Rail Pressure (UDS) ---
|
||||
# Давление топлива зависит от нагрузки
|
||||
base_pressure = 5000 # kPa
|
||||
if state.engine_running:
|
||||
state.fuel_rail_pressure_uds = base_pressure + state.engine_load * 150
|
||||
else:
|
||||
state.fuel_rail_pressure_uds = 0
|
||||
|
||||
# --- Ignition Timing ---
|
||||
# Базовое опережение + коррекция от оборотов и нагрузки
|
||||
base_timing = 10.0
|
||||
rpm_advance = (state.rpm - 1000) / 500 # +2° на каждые 1000 RPM
|
||||
load_retard = state.engine_load * 0.15 # Уменьшение при нагрузке
|
||||
state.ignition_timing = base_timing + rpm_advance - load_retard
|
||||
state.ignition_timing = max(-5, min(40, state.ignition_timing))
|
||||
|
||||
# --- Timing Corrections (детонация) ---
|
||||
# При высокой нагрузке и температуре возможна детонация
|
||||
knock_probability = (state.engine_load / 100) * (state.coolant_temp / 100) * 0.3
|
||||
if random.random() < knock_probability:
|
||||
# Случайная коррекция на одном из цилиндров
|
||||
cyl = random.randint(1, 4)
|
||||
correction = random.uniform(-3, 0)
|
||||
if cyl == 1:
|
||||
state.timing_correction_cyl1 = correction
|
||||
elif cyl == 2:
|
||||
state.timing_correction_cyl2 = correction
|
||||
elif cyl == 3:
|
||||
state.timing_correction_cyl3 = correction
|
||||
else:
|
||||
state.timing_correction_cyl4 = correction
|
||||
else:
|
||||
# Постепенное восстановление
|
||||
state.timing_correction_cyl1 *= 0.95
|
||||
state.timing_correction_cyl2 *= 0.95
|
||||
state.timing_correction_cyl3 *= 0.95
|
||||
state.timing_correction_cyl4 *= 0.95
|
||||
|
||||
# --- DSG Gear ---
|
||||
state.current_gear_dsg = state.gear
|
||||
|
||||
# --- Gear Selector Position ---
|
||||
# 0=P, 1=R, 2=N, 3=D, 4=S, 5=M
|
||||
if state.speed < 1 and state.throttle_pos < 1:
|
||||
if state.gear == 0:
|
||||
state.gear_selector_position = 2 # Neutral
|
||||
else:
|
||||
state.gear_selector_position = 3 # Drive
|
||||
else:
|
||||
state.gear_selector_position = 3 # Drive
|
||||
|
||||
# --- Transmission Oil Temperature ---
|
||||
# Следует за температурой масла двигателя с задержкой
|
||||
target_trans_temp = state.oil_temp - 10
|
||||
diff = target_trans_temp - state.transmission_oil_temp
|
||||
state.transmission_oil_temp += diff * 0.01 * dt
|
||||
|
||||
# --- Ambient Light ---
|
||||
# Симулируем дневной свет (можно менять в сценариях)
|
||||
# По умолчанию: дневной свет = 200
|
||||
if not hasattr(self, '_ambient_light_target'):
|
||||
self._ambient_light_target = 200
|
||||
state.ambient_light = int(self._ambient_light_target)
|
||||
|
||||
def _update_speed(self, dt: float) -> None:
|
||||
"""Обновить скорость автомобиля."""
|
||||
current = self.state.speed
|
||||
|
||||
19
src/uds/__init__.py
Normal file
19
src/uds/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
UDS (Unified Diagnostic Services) протокол согласно ISO 14229.
|
||||
Поддержка Service 0x22 (Read Data By Identifier).
|
||||
"""
|
||||
from .protocol import UDSProtocol, UDSRequest, UDSResponse
|
||||
from .pids import UDSService, UDSPID, UDSPIDRegistry, get_uds_pid_registry
|
||||
from .ecu import ECUType, ECU_CONFIG
|
||||
|
||||
__all__ = [
|
||||
"UDSProtocol",
|
||||
"UDSRequest",
|
||||
"UDSResponse",
|
||||
"UDSService",
|
||||
"UDSPID",
|
||||
"UDSPIDRegistry",
|
||||
"get_uds_pid_registry",
|
||||
"ECUType",
|
||||
"ECU_CONFIG",
|
||||
]
|
||||
57
src/uds/ecu.py
Normal file
57
src/uds/ecu.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Конфигурация ECU для UDS протокола.
|
||||
Определяет CAN ID для каждого ECU.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class ECUType(Enum):
|
||||
"""Типы ECU в автомобиле."""
|
||||
ENGINE = auto() # Блок управления двигателем
|
||||
TRANSMISSION = auto() # Блок управления трансмиссией (DSG)
|
||||
INSTRUMENT_CLUSTER = auto() # Приборная панель
|
||||
|
||||
|
||||
@dataclass
|
||||
class ECUConfig:
|
||||
"""Конфигурация ECU."""
|
||||
name: str
|
||||
request_id: int # CAN ID для запросов
|
||||
response_id: int # CAN ID для ответов
|
||||
|
||||
|
||||
# Конфигурация ECU согласно спецификации
|
||||
ECU_CONFIG: dict[ECUType, ECUConfig] = {
|
||||
ECUType.ENGINE: ECUConfig(
|
||||
name="Engine ECU",
|
||||
request_id=0x7E0,
|
||||
response_id=0x7E8,
|
||||
),
|
||||
ECUType.TRANSMISSION: ECUConfig(
|
||||
name="DSG Transmission",
|
||||
request_id=0x7E1,
|
||||
response_id=0x7E9,
|
||||
),
|
||||
ECUType.INSTRUMENT_CLUSTER: ECUConfig(
|
||||
name="Instrument Cluster",
|
||||
request_id=0x714,
|
||||
response_id=0x77E,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_ecu_by_request_id(request_id: int) -> ECUType | None:
|
||||
"""Получить тип ECU по CAN ID запроса."""
|
||||
for ecu_type, config in ECU_CONFIG.items():
|
||||
if config.request_id == request_id:
|
||||
return ecu_type
|
||||
return None
|
||||
|
||||
|
||||
def get_response_id(request_id: int) -> int | None:
|
||||
"""Получить CAN ID ответа для заданного CAN ID запроса."""
|
||||
for config in ECU_CONFIG.values():
|
||||
if config.request_id == request_id:
|
||||
return config.response_id
|
||||
return None
|
||||
351
src/uds/pids.py
Normal file
351
src/uds/pids.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
Определения UDS PIDs (Data Identifiers) согласно ISO 14229.
|
||||
Поддержка Service 0x22 (Read Data By Identifier).
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Callable
|
||||
|
||||
from .ecu import ECUType
|
||||
|
||||
|
||||
class UDSService(IntEnum):
|
||||
"""UDS сервисы."""
|
||||
READ_DATA_BY_ID = 0x22 # Чтение данных по идентификатору
|
||||
READ_DATA_BY_ID_RESPONSE = 0x62 # Положительный ответ
|
||||
NEGATIVE_RESPONSE = 0x7F # Отрицательный ответ
|
||||
|
||||
|
||||
class UDSNegativeResponse(IntEnum):
|
||||
"""Коды отрицательных ответов UDS."""
|
||||
SERVICE_NOT_SUPPORTED = 0x11
|
||||
SUB_FUNCTION_NOT_SUPPORTED = 0x12
|
||||
INCORRECT_MESSAGE_LENGTH = 0x13
|
||||
CONDITIONS_NOT_CORRECT = 0x22
|
||||
REQUEST_OUT_OF_RANGE = 0x31
|
||||
SECURITY_ACCESS_DENIED = 0x33
|
||||
GENERAL_REJECT = 0x10
|
||||
|
||||
|
||||
@dataclass
|
||||
class UDSPID:
|
||||
"""Определение UDS PID (Data Identifier)."""
|
||||
did: int # Data Identifier (2 bytes)
|
||||
name: str
|
||||
description: str
|
||||
unit: str
|
||||
min_value: float
|
||||
max_value: float
|
||||
bytes_count: int
|
||||
ecu: ECUType # К какому ECU принадлежит
|
||||
encoder: Callable[[float], bytes]
|
||||
decoder: Callable[[bytes], float] | None = None
|
||||
signed: bool = False # Для знаковых значений (timing correction)
|
||||
|
||||
|
||||
class UDSPIDRegistry:
|
||||
"""Реестр всех поддерживаемых UDS PIDs."""
|
||||
|
||||
def __init__(self):
|
||||
self._pids: dict[tuple[ECUType, int], UDSPID] = {}
|
||||
self._register_engine_pids()
|
||||
self._register_cluster_pids()
|
||||
self._register_dsg_pids()
|
||||
|
||||
def register(self, pid: UDSPID) -> None:
|
||||
"""Зарегистрировать PID."""
|
||||
key = (pid.ecu, pid.did)
|
||||
self._pids[key] = pid
|
||||
|
||||
def get(self, ecu: ECUType, did: int) -> UDSPID | None:
|
||||
"""Получить PID по ECU и DID."""
|
||||
return self._pids.get((ecu, did))
|
||||
|
||||
def get_supported_dids(self, ecu: ECUType) -> list[int]:
|
||||
"""Получить список поддерживаемых DIDs для ECU."""
|
||||
return [did for (e, did) in self._pids.keys() if e == ecu]
|
||||
|
||||
def _encode_unsigned_2bytes(self, value: float, scale: float = 1.0) -> bytes:
|
||||
"""Кодировать unsigned 2-байтное значение."""
|
||||
raw = int(value / scale)
|
||||
raw = max(0, min(0xFFFF, raw))
|
||||
return raw.to_bytes(2, "big")
|
||||
|
||||
def _encode_signed_2bytes(self, value: float, scale: float = 1.0) -> bytes:
|
||||
"""Кодировать signed 2-байтное значение (two's complement)."""
|
||||
raw = int(value / scale)
|
||||
if raw < 0:
|
||||
raw = 0x10000 + raw # Two's complement
|
||||
raw = raw & 0xFFFF
|
||||
return raw.to_bytes(2, "big")
|
||||
|
||||
def _register_engine_pids(self) -> None:
|
||||
"""Регистрация PIDs для Engine ECU."""
|
||||
|
||||
# PID 0x202A - Boost Pressure Actual
|
||||
self.register(UDSPID(
|
||||
did=0x202A,
|
||||
name="BOOST_PRESSURE_ACTUAL",
|
||||
description="Boost Pressure Actual",
|
||||
unit="kPa",
|
||||
min_value=0,
|
||||
max_value=400,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_unsigned_2bytes(v, 0.1),
|
||||
decoder=lambda b: int.from_bytes(b, "big") * 0.1
|
||||
))
|
||||
|
||||
# PID 0x2029 - Boost Pressure Target
|
||||
self.register(UDSPID(
|
||||
did=0x2029,
|
||||
name="BOOST_PRESSURE_TARGET",
|
||||
description="Boost Pressure Target",
|
||||
unit="kPa",
|
||||
min_value=0,
|
||||
max_value=400,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_unsigned_2bytes(v, 0.1),
|
||||
decoder=lambda b: int.from_bytes(b, "big") * 0.1
|
||||
))
|
||||
|
||||
# PID 0xF423 - Fuel Rail Pressure
|
||||
self.register(UDSPID(
|
||||
did=0xF423,
|
||||
name="FUEL_RAIL_PRESSURE",
|
||||
description="Fuel Rail Pressure",
|
||||
unit="kPa",
|
||||
min_value=0,
|
||||
max_value=25000,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_unsigned_2bytes(v, 10.0),
|
||||
decoder=lambda b: int.from_bytes(b, "big") * 10
|
||||
))
|
||||
|
||||
# PID 0x10C0 - Lambda Actual
|
||||
self.register(UDSPID(
|
||||
did=0x10C0,
|
||||
name="LAMBDA_ACTUAL",
|
||||
description="Lambda Actual",
|
||||
unit="λ",
|
||||
min_value=0.5,
|
||||
max_value=2.0,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_unsigned_2bytes(v, 0.000977),
|
||||
decoder=lambda b: int.from_bytes(b, "big") * 0.000977
|
||||
))
|
||||
|
||||
# PID 0x1456 - Lambda Target
|
||||
self.register(UDSPID(
|
||||
did=0x1456,
|
||||
name="LAMBDA_TARGET",
|
||||
description="Lambda Target",
|
||||
unit="λ",
|
||||
min_value=0.5,
|
||||
max_value=2.0,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_unsigned_2bytes(v, 0.000977),
|
||||
decoder=lambda b: int.from_bytes(b, "big") * 0.000977
|
||||
))
|
||||
|
||||
# PID 0x437C - Torque Actual
|
||||
self.register(UDSPID(
|
||||
did=0x437C,
|
||||
name="TORQUE_ACTUAL",
|
||||
description="Torque Actual",
|
||||
unit="Nm",
|
||||
min_value=0,
|
||||
max_value=500,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_unsigned_2bytes(v, 0.1),
|
||||
decoder=lambda b: int.from_bytes(b, "big") * 0.1
|
||||
))
|
||||
|
||||
# PID 0x39A2 - Wastegate Position Actual
|
||||
self.register(UDSPID(
|
||||
did=0x39A2,
|
||||
name="WASTEGATE_ACTUAL",
|
||||
description="Wastegate Position Actual",
|
||||
unit="%",
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_unsigned_2bytes(v, 0.01),
|
||||
decoder=lambda b: int.from_bytes(b, "big") * 0.01
|
||||
))
|
||||
|
||||
# PID 0x39A3 - Wastegate Position Target
|
||||
self.register(UDSPID(
|
||||
did=0x39A3,
|
||||
name="WASTEGATE_TARGET",
|
||||
description="Wastegate Position Target",
|
||||
unit="%",
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_unsigned_2bytes(v, 0.01),
|
||||
decoder=lambda b: int.from_bytes(b, "big") * 0.01
|
||||
))
|
||||
|
||||
# PID 0x2004 - Ignition Timing
|
||||
self.register(UDSPID(
|
||||
did=0x2004,
|
||||
name="IGNITION_TIMING",
|
||||
description="Ignition Timing",
|
||||
unit="°",
|
||||
min_value=-10,
|
||||
max_value=50,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_signed_2bytes(v, 0.01),
|
||||
decoder=lambda b: self._decode_signed_2bytes(b, 0.01),
|
||||
signed=True
|
||||
))
|
||||
|
||||
# PIDs 0x200A-0x200D - Timing Correction Cylinders 1-4
|
||||
for i, did in enumerate([0x200A, 0x200B, 0x200C, 0x200D], start=1):
|
||||
self.register(UDSPID(
|
||||
did=did,
|
||||
name=f"TIMING_CORRECTION_CYL{i}",
|
||||
description=f"Timing Correction Cylinder {i}",
|
||||
unit="°",
|
||||
min_value=-15,
|
||||
max_value=5,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.ENGINE,
|
||||
encoder=lambda v: self._encode_signed_2bytes(v, 0.01),
|
||||
decoder=lambda b: self._decode_signed_2bytes(b, 0.01),
|
||||
signed=True
|
||||
))
|
||||
|
||||
def _register_cluster_pids(self) -> None:
|
||||
"""Регистрация PIDs для Instrument Cluster."""
|
||||
|
||||
# PID 0x22D1 - RPM from Cluster
|
||||
self.register(UDSPID(
|
||||
did=0x22D1,
|
||||
name="CLUSTER_RPM",
|
||||
description="RPM from Cluster",
|
||||
unit="RPM",
|
||||
min_value=0,
|
||||
max_value=8000,
|
||||
bytes_count=2,
|
||||
ecu=ECUType.INSTRUMENT_CLUSTER,
|
||||
encoder=lambda v: int(v * 4).to_bytes(2, "big"),
|
||||
decoder=lambda b: int.from_bytes(b, "big") / 4
|
||||
))
|
||||
|
||||
# PID 0x224D - Ambient Light Sensor
|
||||
self.register(UDSPID(
|
||||
did=0x224D,
|
||||
name="AMBIENT_LIGHT",
|
||||
description="Ambient Light Sensor",
|
||||
unit="",
|
||||
min_value=0,
|
||||
max_value=255,
|
||||
bytes_count=1,
|
||||
ecu=ECUType.INSTRUMENT_CLUSTER,
|
||||
encoder=lambda v: bytes([int(v)]),
|
||||
decoder=lambda b: b[0]
|
||||
))
|
||||
|
||||
def _register_dsg_pids(self) -> None:
|
||||
"""Регистрация PIDs для DSG Transmission."""
|
||||
|
||||
# PID 0x3816 - Current Gear
|
||||
self.register(UDSPID(
|
||||
did=0x3816,
|
||||
name="CURRENT_GEAR",
|
||||
description="Current Gear",
|
||||
unit="",
|
||||
min_value=-1,
|
||||
max_value=7,
|
||||
bytes_count=1,
|
||||
ecu=ECUType.TRANSMISSION,
|
||||
encoder=self._encode_gear,
|
||||
decoder=self._decode_gear
|
||||
))
|
||||
|
||||
# PID 0x3815 - Gear Selector Position
|
||||
self.register(UDSPID(
|
||||
did=0x3815,
|
||||
name="GEAR_SELECTOR",
|
||||
description="Gear Selector Position (PRNDM)",
|
||||
unit="",
|
||||
min_value=0,
|
||||
max_value=5,
|
||||
bytes_count=1,
|
||||
ecu=ECUType.TRANSMISSION,
|
||||
encoder=lambda v: bytes([int(v)]),
|
||||
decoder=lambda b: b[0]
|
||||
))
|
||||
|
||||
# PID 0x1940 - Transmission Oil Temperature
|
||||
self.register(UDSPID(
|
||||
did=0x1940,
|
||||
name="TRANS_OIL_TEMP",
|
||||
description="Transmission Oil Temperature",
|
||||
unit="°C",
|
||||
min_value=-40,
|
||||
max_value=150,
|
||||
bytes_count=1,
|
||||
ecu=ECUType.TRANSMISSION,
|
||||
encoder=lambda v: bytes([int(v + 40)]),
|
||||
decoder=lambda b: b[0] - 40
|
||||
))
|
||||
|
||||
def _decode_signed_2bytes(self, data: bytes, scale: float) -> float:
|
||||
"""Декодировать signed 2-байтное значение."""
|
||||
raw = int.from_bytes(data, "big")
|
||||
if raw >= 0x8000:
|
||||
raw -= 0x10000
|
||||
return raw * scale
|
||||
|
||||
def _encode_gear(self, gear: float) -> bytes:
|
||||
"""Кодировать передачу в DSG формат."""
|
||||
gear_map = {
|
||||
0: 0x00, # Neutral
|
||||
-1: 0x0C, # Reverse
|
||||
1: 0x02,
|
||||
2: 0x04,
|
||||
3: 0x06,
|
||||
4: 0x08,
|
||||
5: 0x0A,
|
||||
6: 0x0E,
|
||||
7: 0x10,
|
||||
}
|
||||
return bytes([gear_map.get(int(gear), 0x00)])
|
||||
|
||||
def _decode_gear(self, data: bytes) -> float:
|
||||
"""Декодировать передачу из DSG формата."""
|
||||
gear_map = {
|
||||
0x00: 0, # Neutral
|
||||
0x0C: -1, # Reverse
|
||||
0x02: 1,
|
||||
0x04: 2,
|
||||
0x06: 3,
|
||||
0x08: 4,
|
||||
0x0A: 5,
|
||||
0x0E: 6,
|
||||
0x10: 7,
|
||||
}
|
||||
return gear_map.get(data[0], 0)
|
||||
|
||||
|
||||
# Синглтон реестра
|
||||
_uds_pid_registry: UDSPIDRegistry | None = None
|
||||
|
||||
|
||||
def get_uds_pid_registry() -> UDSPIDRegistry:
|
||||
"""Получить глобальный реестр UDS PIDs."""
|
||||
global _uds_pid_registry
|
||||
if _uds_pid_registry is None:
|
||||
_uds_pid_registry = UDSPIDRegistry()
|
||||
return _uds_pid_registry
|
||||
250
src/uds/protocol.py
Normal file
250
src/uds/protocol.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
UDS протокол (ISO 14229) поверх CAN.
|
||||
Обработка запросов Service 0x22 (Read Data By Identifier).
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from can_interface import CANFrame
|
||||
from logger import get_logger
|
||||
from .ecu import ECUType, ECU_CONFIG, get_ecu_by_request_id, get_response_id
|
||||
from .pids import UDSService, UDSNegativeResponse, get_uds_pid_registry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from simulator.vehicle import VehicleState
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UDSRequest:
|
||||
"""Разобранный UDS запрос."""
|
||||
service: int
|
||||
did: int # Data Identifier (2 bytes)
|
||||
ecu: ECUType
|
||||
request_id: int # CAN ID запроса
|
||||
raw_data: bytes
|
||||
|
||||
@classmethod
|
||||
def from_can_frame(cls, frame: CANFrame) -> "UDSRequest | None":
|
||||
"""Распарсить CAN фрейм как UDS запрос."""
|
||||
# Проверяем, является ли это запросом к известному ECU
|
||||
ecu = get_ecu_by_request_id(frame.arbitration_id)
|
||||
if ecu is None:
|
||||
return None
|
||||
|
||||
data = frame.data
|
||||
if len(data) < 4:
|
||||
return None
|
||||
|
||||
# Первый байт - длина данных (PCI byte для Single Frame)
|
||||
length = data[0]
|
||||
if length < 3: # Минимум: service + DID (2 bytes)
|
||||
return None
|
||||
|
||||
# Второй байт - сервис
|
||||
service = data[1]
|
||||
|
||||
# Поддерживаем только Service 0x22
|
||||
if service != UDSService.READ_DATA_BY_ID:
|
||||
return None
|
||||
|
||||
# Третий и четвёртый байты - DID
|
||||
did = (data[2] << 8) | data[3]
|
||||
|
||||
return cls(
|
||||
service=service,
|
||||
did=did,
|
||||
ecu=ecu,
|
||||
request_id=frame.arbitration_id,
|
||||
raw_data=data
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
ecu_name = ECU_CONFIG[self.ecu].name
|
||||
return f"UDSRequest(ecu={ecu_name}, service={self.service:02X}, did={self.did:04X})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class UDSResponse:
|
||||
"""UDS ответ для отправки."""
|
||||
service: int # Response service (0x62 или 0x7F)
|
||||
did: int # Data Identifier
|
||||
data: bytes # Данные ответа
|
||||
response_id: int # CAN ID ответа
|
||||
is_negative: bool = False
|
||||
negative_code: int = 0
|
||||
|
||||
def to_can_frame(self) -> CANFrame:
|
||||
"""Преобразовать в CAN фрейм."""
|
||||
if self.is_negative:
|
||||
# Negative response: [length, 0x7F, service, error_code]
|
||||
payload = bytes([
|
||||
3, # Length
|
||||
UDSService.NEGATIVE_RESPONSE,
|
||||
UDSService.READ_DATA_BY_ID,
|
||||
self.negative_code,
|
||||
])
|
||||
else:
|
||||
# Positive response: [length, 0x62, DID_H, DID_L, data...]
|
||||
did_h = (self.did >> 8) & 0xFF
|
||||
did_l = self.did & 0xFF
|
||||
payload = bytes([
|
||||
len(self.data) + 3, # Length: service + DID + data
|
||||
UDSService.READ_DATA_BY_ID_RESPONSE,
|
||||
did_h,
|
||||
did_l,
|
||||
]) + self.data
|
||||
|
||||
# Дополняем до 8 байт
|
||||
payload = payload.ljust(8, b'\x00')
|
||||
|
||||
return CANFrame(
|
||||
arbitration_id=self.response_id,
|
||||
data=payload
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def negative(cls, did: int, response_id: int, error_code: int) -> "UDSResponse":
|
||||
"""Создать отрицательный ответ."""
|
||||
return cls(
|
||||
service=UDSService.NEGATIVE_RESPONSE,
|
||||
did=did,
|
||||
data=bytes(),
|
||||
response_id=response_id,
|
||||
is_negative=True,
|
||||
negative_code=error_code
|
||||
)
|
||||
|
||||
|
||||
class UDSProtocol:
|
||||
"""
|
||||
Обработчик UDS протокола.
|
||||
Получает запросы Service 0x22, формирует ответы на основе состояния автомобиля.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.pid_registry = get_uds_pid_registry()
|
||||
self._stats = {
|
||||
"requests_received": 0,
|
||||
"responses_sent": 0,
|
||||
"unsupported_requests": 0,
|
||||
"negative_responses": 0,
|
||||
}
|
||||
|
||||
def process_frame(self, frame: CANFrame, vehicle_state: "VehicleState") -> CANFrame | None:
|
||||
"""
|
||||
Обработать входящий CAN фрейм.
|
||||
Возвращает ответный фрейм или None, если фрейм не является UDS запросом.
|
||||
"""
|
||||
request = UDSRequest.from_can_frame(frame)
|
||||
if request is None:
|
||||
return None
|
||||
|
||||
self._stats["requests_received"] += 1
|
||||
logger.debug(f"UDS Received: {request}")
|
||||
|
||||
response = self._handle_request(request, vehicle_state)
|
||||
if response:
|
||||
self._stats["responses_sent"] += 1
|
||||
if response.is_negative:
|
||||
self._stats["negative_responses"] += 1
|
||||
return response.to_can_frame()
|
||||
|
||||
self._stats["unsupported_requests"] += 1
|
||||
return None
|
||||
|
||||
def _handle_request(self, request: UDSRequest, state: "VehicleState") -> UDSResponse | None:
|
||||
"""Обработать UDS запрос и вернуть ответ."""
|
||||
response_id = get_response_id(request.request_id)
|
||||
if response_id is None:
|
||||
return None
|
||||
|
||||
# Получаем определение PID
|
||||
pid_def = self.pid_registry.get(request.ecu, request.did)
|
||||
if pid_def is None:
|
||||
logger.debug(f"UDS DID {request.did:04X} not supported for {request.ecu}")
|
||||
return UDSResponse.negative(
|
||||
request.did,
|
||||
response_id,
|
||||
UDSNegativeResponse.REQUEST_OUT_OF_RANGE
|
||||
)
|
||||
|
||||
# Получаем значение из состояния автомобиля
|
||||
value = self._get_did_value(request.ecu, request.did, state)
|
||||
if value is None:
|
||||
logger.debug(f"UDS DID {request.did:04X} no value available")
|
||||
return UDSResponse.negative(
|
||||
request.did,
|
||||
response_id,
|
||||
UDSNegativeResponse.CONDITIONS_NOT_CORRECT
|
||||
)
|
||||
|
||||
# Кодируем значение
|
||||
try:
|
||||
encoded = pid_def.encoder(value)
|
||||
return UDSResponse(
|
||||
service=UDSService.READ_DATA_BY_ID_RESPONSE,
|
||||
did=request.did,
|
||||
data=encoded,
|
||||
response_id=response_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to encode UDS DID {request.did:04X}: {e}")
|
||||
return UDSResponse.negative(
|
||||
request.did,
|
||||
response_id,
|
||||
UDSNegativeResponse.GENERAL_REJECT
|
||||
)
|
||||
|
||||
def _get_did_value(self, ecu: ECUType, did: int, state: "VehicleState") -> float | None:
|
||||
"""Получить значение DID из состояния автомобиля."""
|
||||
|
||||
if ecu == ECUType.ENGINE:
|
||||
return self._get_engine_value(did, state)
|
||||
elif ecu == ECUType.TRANSMISSION:
|
||||
return self._get_transmission_value(did, state)
|
||||
elif ecu == ECUType.INSTRUMENT_CLUSTER:
|
||||
return self._get_cluster_value(did, state)
|
||||
|
||||
return None
|
||||
|
||||
def _get_engine_value(self, did: int, state: "VehicleState") -> float | None:
|
||||
"""Получить значение для Engine ECU."""
|
||||
mapping = {
|
||||
0x202A: state.boost_pressure_actual, # Boost Pressure Actual
|
||||
0x2029: state.boost_pressure_target, # Boost Pressure Target
|
||||
0xF423: state.fuel_rail_pressure_uds, # Fuel Rail Pressure (UDS scale)
|
||||
0x10C0: state.lambda_actual, # Lambda Actual
|
||||
0x1456: state.lambda_target, # Lambda Target
|
||||
0x437C: state.torque_nm, # Torque Actual (Nm)
|
||||
0x39A2: state.wastegate_actual, # Wastegate Position Actual
|
||||
0x39A3: state.wastegate_target, # Wastegate Position Target
|
||||
0x2004: state.ignition_timing, # Ignition Timing
|
||||
0x200A: state.timing_correction_cyl1, # Timing Correction Cyl 1
|
||||
0x200B: state.timing_correction_cyl2, # Timing Correction Cyl 2
|
||||
0x200C: state.timing_correction_cyl3, # Timing Correction Cyl 3
|
||||
0x200D: state.timing_correction_cyl4, # Timing Correction Cyl 4
|
||||
}
|
||||
return mapping.get(did)
|
||||
|
||||
def _get_transmission_value(self, did: int, state: "VehicleState") -> float | None:
|
||||
"""Получить значение для DSG Transmission."""
|
||||
mapping = {
|
||||
0x3816: state.current_gear_dsg, # Current Gear
|
||||
0x3815: state.gear_selector_position, # Gear Selector (PRNDM)
|
||||
0x1940: state.transmission_oil_temp, # Transmission Oil Temp
|
||||
}
|
||||
return mapping.get(did)
|
||||
|
||||
def _get_cluster_value(self, did: int, state: "VehicleState") -> float | None:
|
||||
"""Получить значение для Instrument Cluster."""
|
||||
mapping = {
|
||||
0x22D1: state.rpm, # RPM (same as OBD2)
|
||||
0x224D: state.ambient_light, # Ambient Light Sensor
|
||||
}
|
||||
return mapping.get(did)
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""Получить статистику протокола."""
|
||||
return self._stats.copy()
|
||||
Reference in New Issue
Block a user