Update readme and add UDS
This commit is contained in:
149
README.md
149
README.md
@@ -0,0 +1,149 @@
|
|||||||
|
# Carpibord
|
||||||
|
|
||||||
|
Автомобильная диагностическая система для **Skoda Kodiaq 2021** на базе Raspberry Pi 5 и Flipper Zero.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ SKODA KODIAQ 2021 │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ Comfort │ │ OBD-II │ │
|
||||||
|
│ │ CAN Bus │ │ CAN Bus │ │
|
||||||
|
│ │ (двери, │ │ (двигатель,│ │
|
||||||
|
│ │ свет...) │ │ скорость) │ │
|
||||||
|
│ └──────┬──────┘ └──────┬──────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
└──────────┼───────────────────┼──────────────────────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┴──────────────┐
|
||||||
|
│ │ OBD-II Разъём │
|
||||||
|
│ │ Pin 6 (CANH) Pin 14 (CANL)│
|
||||||
|
│ └──────────────┬──────────────┘
|
||||||
|
│ │
|
||||||
|
┌──────────┴───────────────────┴──────────────────────────────────────────────┐
|
||||||
|
│ RASPBERRY PI 5 │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ WaveShare 2-CH CAN HAT │ │
|
||||||
|
│ │ (MCP2515, 500 kbps) │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ OBD2 Client (Python) │ │
|
||||||
|
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────────────────────┐ │ │
|
||||||
|
│ │ │ CAN │ │ OBD2 │ │ Vehicle │ │ Handler Pipeline │ │ │
|
||||||
|
│ │ │Interface │→│ Protocol │→│ Poller │→│ ┌────────┐ ┌────────────┐ │ │ │
|
||||||
|
│ │ └──────────┘ └──────────┘ └──────────┘ │ │ SQLite │→│ PostgreSQL │ │ │ │
|
||||||
|
│ │ │ └────────┘ └────────────┘ │ │ │
|
||||||
|
│ │ └────────────────────────────┘ │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────────────────┴──────────────────────────────────────────┐ │
|
||||||
|
│ │ UART (/dev/serial0, 115200) │ │
|
||||||
|
│ └─────────────────────────────┬──────────────────────────────────────────┘ │
|
||||||
|
│ ┌───────────────────────┴───────────────────────┐ │
|
||||||
|
│ │ X120x UPS HAT (опционально) │ │
|
||||||
|
│ │ I2C мониторинг батареи │ │
|
||||||
|
│ └───────────────────────────────────────────────┘ │
|
||||||
|
└────────────────────────────────┬────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────────────────┴────────────────────────────────────────────┐
|
||||||
|
│ FLIPPER ZERO │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ can_monitor.fap - Приложение для отображения данных │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Страницы: Live Data → Statistics → System → App Status → UPS → Actions│ │
|
||||||
|
│ └────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Компоненты
|
||||||
|
|
||||||
|
| Директория | Описание |
|
||||||
|
|------------|----------|
|
||||||
|
| `obd2_client/` | Python приложение для RPi5 — OBD2 мониторинг, storage, PostgreSQL sync |
|
||||||
|
| `flip_monitor/` | C приложение для Flipper Zero — GUI отображение данных |
|
||||||
|
| `systemd/` | Systemd сервисы для автозапуска |
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- **OBD2 мониторинг** — чтение данных двигателя через стандартный протокол
|
||||||
|
- **Offline-first хранение** — SQLite локально, синхронизация с PostgreSQL
|
||||||
|
- **Flipper Zero UI** — отображение данных на портативном устройстве
|
||||||
|
- **UPS мониторинг** — контроль батареи X120x HAT
|
||||||
|
- **Автозапуск** — systemd сервисы для работы в автомобиле
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### 1. Настройка RPi5
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# /boot/firmware/config.txt
|
||||||
|
dtparam=spi=on
|
||||||
|
dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25
|
||||||
|
enable_uart=1
|
||||||
|
dtoverlay=disable-bt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd obd2_client
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Настройка CAN
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ip link set can0 type can bitrate 500000
|
||||||
|
sudo ip link set can0 up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Базовый запуск
|
||||||
|
python -m src.main --interface can0
|
||||||
|
|
||||||
|
# С Flipper Zero
|
||||||
|
python -m src.main --interface can0 --flipper /dev/serial0
|
||||||
|
|
||||||
|
# С отладкой
|
||||||
|
python -m src.main --interface can0 --flipper /dev/serial0 --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Автозапуск (опционально)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd systemd
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Подключение
|
||||||
|
|
||||||
|
### OBD-II → CAN HAT
|
||||||
|
|
||||||
|
| OBD-II | CAN HAT |
|
||||||
|
|--------|---------|
|
||||||
|
| Pin 6 (CAN-H) | CH0 CANH |
|
||||||
|
| Pin 14 (CAN-L) | CH0 CANL |
|
||||||
|
| Pin 4/5 (GND) | GND |
|
||||||
|
|
||||||
|
### RPi5 → Flipper Zero
|
||||||
|
|
||||||
|
| RPi5 | Flipper Zero |
|
||||||
|
|------|--------------|
|
||||||
|
| GPIO14 (TX) | Pin 14 (RX) |
|
||||||
|
| GPIO15 (RX) | Pin 13 (TX) |
|
||||||
|
| GND | Pin 18 (GND) |
|
||||||
|
|
||||||
|
## Документация
|
||||||
|
|
||||||
|
- [OBD2 Client README](obd2_client/README.md) — детальная документация Python приложения
|
||||||
|
- [Flipper App README](flip_monitor/README.md) — документация приложения Flipper Zero
|
||||||
|
- [CLAUDE.md](CLAUDE.md) — техническое описание для AI-ассистентов
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
@@ -1,111 +1,95 @@
|
|||||||
# OBD2 Client для Skoda Kodiaq 2021 на RPi5
|
# OBD2 Client
|
||||||
|
|
||||||
Python приложение для чтения OBD2 данных через WaveShare 2-CH CAN HAT.
|
Python приложение для мониторинга OBD2 данных Skoda Kodiaq 2021 на Raspberry Pi 5.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- **OBD2 Protocol** — чтение стандартных PID (Mode 01)
|
||||||
|
- **Offline-first Storage** — SQLite с автосинхронизацией в PostgreSQL
|
||||||
|
- **Handler Pipeline** — модульная обработка данных
|
||||||
|
- **Flipper Zero UI** — отображение на портативном дисплее
|
||||||
|
- **UPS Monitoring** — мониторинг X120x UPS HAT
|
||||||
|
- **Systemd Services** — автозапуск при загрузке
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Основные зависимости
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Для PostgreSQL (опционально)
|
||||||
|
pip install psycopg2-binary
|
||||||
|
|
||||||
|
# Для UPS мониторинга (опционально, только RPi)
|
||||||
|
pip install smbus2 gpiozero
|
||||||
```
|
```
|
||||||
|
|
||||||
## Конфигурация RPi5
|
## Конфигурация RPi5
|
||||||
|
|
||||||
### /boot/config.txt
|
### /boot/firmware/config.txt
|
||||||
```
|
|
||||||
|
```ini
|
||||||
|
# CAN HAT (MCP2515)
|
||||||
dtparam=spi=on
|
dtparam=spi=on
|
||||||
dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=23
|
dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25
|
||||||
|
|
||||||
|
# UART для Flipper Zero
|
||||||
|
enable_uart=1
|
||||||
|
dtoverlay=disable-bt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Инициализация CAN интерфейса
|
### Инициализация CAN
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ip link set can0 up type can bitrate 500000
|
sudo ip link set can0 type can bitrate 500000
|
||||||
sudo ifconfig can0 txqueuelen 65536
|
sudo ip link set can0 up
|
||||||
```
|
```
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
### Запуск с реальным авто
|
### Базовый запуск
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m src.main --interface can0
|
python -m src.main --interface can0
|
||||||
```
|
```
|
||||||
|
|
||||||
### Только сканирование PID
|
### С Flipper Zero
|
||||||
```bash
|
|
||||||
python -m src.main --interface can0 --scan-only
|
|
||||||
```
|
|
||||||
|
|
||||||
### Тестирование с виртуальным CAN
|
|
||||||
```bash
|
|
||||||
# Создание виртуального интерфейса
|
|
||||||
sudo modprobe vcan
|
|
||||||
sudo ip link add dev vcan0 type vcan
|
|
||||||
sudo ip link set up vcan0
|
|
||||||
|
|
||||||
# Запуск клиента
|
|
||||||
python -m src.main --interface vcan0 --virtual
|
|
||||||
```
|
|
||||||
|
|
||||||
### Параметры командной строки
|
|
||||||
|
|
||||||
| Параметр | Описание |
|
|
||||||
|----------|----------|
|
|
||||||
| `-i, --interface` | CAN интерфейс (can0, vcan0) |
|
|
||||||
| `-c, --config` | Путь к config.json |
|
|
||||||
| `-v, --virtual` | Использовать виртуальный CAN |
|
|
||||||
| `--scan-only` | Только сканировать PID |
|
|
||||||
| `--flipper PORT` | Включить Flipper Zero сервер на указанном порту |
|
|
||||||
| `--debug` | Включить отладочный вывод |
|
|
||||||
|
|
||||||
## Интеграция с Flipper Zero
|
|
||||||
|
|
||||||
### Подключение
|
|
||||||
|
|
||||||
```
|
|
||||||
RPi5 Flipper Zero
|
|
||||||
GPIO14 (TX) --------> RX (pin 14)
|
|
||||||
GPIO15 (RX) <-------- TX (pin 13)
|
|
||||||
GND ---------- GND (pin 18)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Запуск с Flipper
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m src.main --interface can0 --flipper /dev/serial0
|
python -m src.main --interface can0 --flipper /dev/serial0
|
||||||
```
|
```
|
||||||
|
|
||||||
### Страницы на Flipper
|
### Только сканирование PID
|
||||||
|
|
||||||
| Страница | Тип | Описание |
|
```bash
|
||||||
|----------|-----|----------|
|
python -m src.main --interface can0 --scan-only
|
||||||
| Live Data | Info | RPM, Speed, Coolant, Throttle, Fuel |
|
```
|
||||||
| Statistics | Info | Queries, Success rate, Uptime |
|
|
||||||
| System Info | Info | IP, CPU temp, Memory, CAN interface |
|
|
||||||
| Actions | Menu | Reconnect, Clear cache, Reboot, Shutdown |
|
|
||||||
|
|
||||||
### Управление
|
### Тестовый режим (виртуальный CAN)
|
||||||
|
|
||||||
- **←/→** - переключение страниц
|
```bash
|
||||||
- **↑/↓** - выбор пункта меню / прокрутка
|
sudo modprobe vcan
|
||||||
- **OK** - подтверждение действия
|
sudo ip link add dev vcan0 type vcan
|
||||||
- **Back** - отмена / возврат
|
sudo ip link set up vcan0
|
||||||
|
|
||||||
## Поддерживаемые PID
|
python -m src.main --interface vcan0 --virtual
|
||||||
|
```
|
||||||
|
|
||||||
| PID | Параметр | Единицы |
|
### Параметры CLI
|
||||||
|-----|----------|---------|
|
|
||||||
| 0x04 | Engine Load | % |
|
| Параметр | Описание |
|
||||||
| 0x05 | Coolant Temp | °C |
|
|----------|----------|
|
||||||
| 0x0C | Engine RPM | RPM |
|
| `-i, --interface` | CAN интерфейс (can0, vcan0) |
|
||||||
| 0x0D | Vehicle Speed | km/h |
|
| `-c, --config` | Путь к config.json |
|
||||||
| 0x0F | Intake Air Temp | °C |
|
| `-v, --virtual` | Виртуальный CAN режим |
|
||||||
| 0x10 | MAF Rate | g/s |
|
| `--scan-only` | Только сканирование PID |
|
||||||
| 0x11 | Throttle Position | % |
|
| `--flipper PORT` | Включить Flipper Zero сервер |
|
||||||
| 0x2F | Fuel Level | % |
|
| `--no-monitor` | Отключить консольный вывод |
|
||||||
| 0x5C | Oil Temperature | °C |
|
| `--debug` | Отладочный режим |
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
Файл `config.json`:
|
### config.json
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -117,49 +101,335 @@ python -m src.main --interface can0 --flipper /dev/serial0
|
|||||||
"obd2": {
|
"obd2": {
|
||||||
"request_id": "0x7DF",
|
"request_id": "0x7DF",
|
||||||
"response_id": "0x7E8",
|
"response_id": "0x7E8",
|
||||||
"timeout": 0.1
|
"timeout": 0.2
|
||||||
},
|
},
|
||||||
"polling": {
|
"polling": {
|
||||||
"interval_fast": 0.1,
|
"interval_fast": 0.1,
|
||||||
"interval_slow": 1.0,
|
"interval_slow": 1.0,
|
||||||
"fast_pids": ["0x0C", "0x0D", "0x11"],
|
"fast_pids": ["0x0C", "0x0D", "0x11", "0x04"],
|
||||||
"slow_pids": ["0x05", "0x2F", "0x5C"]
|
"slow_pids": ["0x05", "0x5C", "0x0F", "0x2F"]
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"enabled": true,
|
||||||
|
"db_path": null,
|
||||||
|
"wal_mode": true,
|
||||||
|
"retention_days": 30,
|
||||||
|
"batch_size": 50,
|
||||||
|
"flush_interval": 1.0
|
||||||
|
},
|
||||||
|
"postgresql": {
|
||||||
|
"enabled": false,
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"database": "carpibord",
|
||||||
|
"user": "carpibord",
|
||||||
|
"password": "",
|
||||||
|
"sync_interval": 30.0,
|
||||||
|
"batch_size": 500
|
||||||
|
},
|
||||||
|
"flipper": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": "/dev/serial0",
|
||||||
|
"baudrate": 115200,
|
||||||
|
"show_ups": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Переменные окружения
|
### Переменные окружения
|
||||||
|
|
||||||
- `OBD2_CONFIG_PATH` - путь к config.json
|
```bash
|
||||||
- `OBD2_CAN_INTERFACE` - CAN интерфейс
|
# CAN
|
||||||
- `OBD2_CAN_BITRATE` - битрейт
|
export OBD2_CAN_INTERFACE=can0
|
||||||
- `OBD2_CAN_VIRTUAL` - использовать виртуальный CAN (true/false)
|
export OBD2_CAN_BITRATE=500000
|
||||||
- `OBD2_TIMEOUT` - таймаут ответа
|
|
||||||
|
|
||||||
## Структура проекта
|
# Storage
|
||||||
|
export OBD2_STORAGE_ENABLED=true
|
||||||
|
export OBD2_STORAGE_PATH=/data/obd2.db
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
export OBD2_PG_ENABLED=true
|
||||||
|
export OBD2_PG_HOST=postgres.example.com
|
||||||
|
export OBD2_PG_DATABASE=carpibord
|
||||||
|
export OBD2_PG_USER=carpibord
|
||||||
|
export OBD2_PG_PASSWORD=secret
|
||||||
|
|
||||||
|
# Flipper
|
||||||
|
export OBD2_FLIPPER_ENABLED=true
|
||||||
|
export OBD2_FLIPPER_PORT=/dev/serial0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
### Handler Pipeline (Offline-First)
|
||||||
|
|
||||||
|
```
|
||||||
|
VehiclePoller ──► HandlerPipeline
|
||||||
|
│
|
||||||
|
├── StorageHandler ──► SQLite (obd2_data.db)
|
||||||
|
│ │ │
|
||||||
|
│ │ [unsynced data]
|
||||||
|
│ │ │
|
||||||
|
│ └─────── background sync ───────┐
|
||||||
|
│ │
|
||||||
|
└── PostgreSQLHandler ◄──────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
PostgreSQL (remote)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Принцип работы:**
|
||||||
|
1. `VehiclePoller` опрашивает PID и вызывает callbacks
|
||||||
|
2. `StorageHandler` батчит данные и пишет в SQLite
|
||||||
|
3. `PostgreSQLHandler` в фоне синхронизирует unsynced записи в PostgreSQL
|
||||||
|
4. При потере связи данные накапливаются локально
|
||||||
|
5. При восстановлении — автоматическая досинхронизация
|
||||||
|
|
||||||
|
### Структура проекта
|
||||||
|
|
||||||
```
|
```
|
||||||
obd2_client/
|
obd2_client/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── __init__.py
|
│ ├── main.py # Точка входа, CLI
|
||||||
│ ├── main.py # Точка входа, CLI
|
│ ├── config.py # Конфигурация
|
||||||
│ ├── config.py # Конфигурация
|
│ ├── logger.py # Логирование
|
||||||
│ ├── logger.py # Логирование
|
│ │
|
||||||
│ ├── can/
|
│ ├── can/ # CAN абстракция
|
||||||
│ │ ├── frame.py # CAN фрейм dataclass
|
│ │ ├── frame.py # CANFrame dataclass
|
||||||
│ │ └── interface.py # Абстракция CAN шины
|
│ │ └── interface.py # CANInterface
|
||||||
│ ├── obd2/
|
│ │
|
||||||
│ │ ├── pids.py # Определения PID
|
│ ├── obd2/ # OBD2 протокол
|
||||||
│ │ ├── protocol.py # OBD2 запросы/ответы
|
│ │ ├── pids.py # Определения PID, декодеры
|
||||||
│ │ └── scanner.py # Автодетект PID
|
│ │ ├── protocol.py # Request/Response
|
||||||
│ ├── vehicle/
|
│ │ └── scanner.py # Автодетект PID
|
||||||
│ │ ├── state.py # Состояние авто
|
│ │
|
||||||
│ │ └── poller.py # Циклический опрос
|
│ ├── vehicle/ # Состояние автомобиля
|
||||||
│ └── flipper/
|
│ │ ├── state.py # VehicleState (thread-safe)
|
||||||
│ ├── protocol.py # UART протокол
|
│ │ └── poller.py # VehiclePoller (fast/slow)
|
||||||
│ ├── pages.py # Генераторы страниц
|
│ │
|
||||||
│ └── server.py # UART сервер
|
│ ├── handlers/ # Data Pipeline
|
||||||
|
│ │ ├── base.py # BaseHandler, OBD2Reading
|
||||||
|
│ │ ├── pipeline.py # HandlerPipeline
|
||||||
|
│ │ ├── storage_handler.py # SQLite handler
|
||||||
|
│ │ └── postgresql_handler.py # PostgreSQL sync
|
||||||
|
│ │
|
||||||
|
│ ├── storage/ # Persistence
|
||||||
|
│ │ └── storage.py # SQLite + sessions
|
||||||
|
│ │
|
||||||
|
│ └── flipper/ # Flipper Zero интеграция
|
||||||
|
│ ├── protocol.py # UART протокол
|
||||||
|
│ ├── server.py # FlipperServer
|
||||||
|
│ ├── pages.py # PageManager, страницы
|
||||||
|
│ └── providers/ # Data providers
|
||||||
|
│ ├── base.py # BaseProvider
|
||||||
|
│ └── ups_provider.py # X120x UPS
|
||||||
|
│
|
||||||
├── config.json
|
├── config.json
|
||||||
|
├── config.json.example
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Поддерживаемые PID
|
||||||
|
|
||||||
|
### OBD2 Standard (Mode 0x01)
|
||||||
|
|
||||||
|
| PID | Параметр | Единицы | Интервал |
|
||||||
|
|-----|----------|---------|----------|
|
||||||
|
| 0x04 | Engine Load | % | Fast |
|
||||||
|
| 0x05 | Coolant Temp | °C | Slow |
|
||||||
|
| 0x0C | Engine RPM | RPM | Fast |
|
||||||
|
| 0x0D | Vehicle Speed | km/h | Fast |
|
||||||
|
| 0x0F | Intake Air Temp | °C | Slow |
|
||||||
|
| 0x10 | MAF Rate | g/s | Slow |
|
||||||
|
| 0x11 | Throttle Position | % | Fast |
|
||||||
|
| 0x2F | Fuel Level | % | Slow |
|
||||||
|
| 0x5C | Oil Temperature | °C | Slow |
|
||||||
|
|
||||||
|
### UDS Extended (Mode 0x22) — MQB Platform
|
||||||
|
|
||||||
|
UDS (ISO 14229) позволяет читать расширенные данные напрямую из ECU.
|
||||||
|
**Это read-only протокол, не влияющий на работу автомобиля.**
|
||||||
|
|
||||||
|
#### Engine ECU (0x7E0 → 0x7E8)
|
||||||
|
|
||||||
|
| PID | Параметр | Единицы | Min | Max |
|
||||||
|
|-----|----------|---------|-----|-----|
|
||||||
|
| 0x202A | Boost Pressure Actual | kPa | 0 | 400 |
|
||||||
|
| 0x2029 | Boost Pressure Target | kPa | 0 | 400 |
|
||||||
|
| 0xF423 | Fuel Rail Pressure | kPa | 0 | 25000 |
|
||||||
|
| 0x10C0 | Lambda Actual | λ | 0.5 | 2.0 |
|
||||||
|
| 0x1456 | Lambda Target | λ | 0.5 | 2.0 |
|
||||||
|
| 0x437C | Torque Actual | Nm | 0 | 500 |
|
||||||
|
| 0x39A2 | Wastegate Position | % | 0 | 100 |
|
||||||
|
| 0x39A3 | Wastegate Target | % | 0 | 100 |
|
||||||
|
| 0x2004 | Ignition Timing | ° | -10 | 50 |
|
||||||
|
| 0x200A | Timing Correction Cyl 1 | ° | -15 | 5 |
|
||||||
|
| 0x200B | Timing Correction Cyl 2 | ° | -15 | 5 |
|
||||||
|
| 0x200C | Timing Correction Cyl 3 | ° | -15 | 5 |
|
||||||
|
| 0x200D | Timing Correction Cyl 4 | ° | -15 | 5 |
|
||||||
|
|
||||||
|
#### Instrument Cluster (0x714 → 0x77E)
|
||||||
|
|
||||||
|
| PID | Параметр | Единицы | Min | Max |
|
||||||
|
|-----|----------|---------|-----|-----|
|
||||||
|
| 0x22D1 | RPM (from cluster) | RPM | 0 | 8000 |
|
||||||
|
| 0x224D | Ambient Light Sensor | - | 0 | 255 |
|
||||||
|
|
||||||
|
#### DSG Transmission (0x7E1 → 0x7E9)
|
||||||
|
|
||||||
|
| PID | Параметр | Единицы | Min | Max |
|
||||||
|
|-----|----------|---------|-----|-----|
|
||||||
|
| 0x3816 | Current Gear | gear | -1 | 7 |
|
||||||
|
| 0x3815 | Gear Selector (PRNDM) | mode | 0 | 5 |
|
||||||
|
| 0x1940 | Transmission Temp | °C | -40 | 150 |
|
||||||
|
|
||||||
|
## UDS конфигурация
|
||||||
|
|
||||||
|
```json
|
||||||
|
"uds": {
|
||||||
|
"enabled": true,
|
||||||
|
"interval_fast": 0.2,
|
||||||
|
"interval_slow": 1.0,
|
||||||
|
"fast_pids": ["0x202A", "0x437C", "0x10C0", "0x2004"],
|
||||||
|
"slow_pids": ["0x39A2", "0x200A", "0x3816", "0x3815"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flipper Zero — страница UDS Data
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ UDS Data │
|
||||||
|
├──────────────────────┤
|
||||||
|
│ Boost: 185 kPa │
|
||||||
|
│ Torque: 280 Nm │
|
||||||
|
│ Lambda: 1.000 │
|
||||||
|
│ Timing: 12.5 deg │
|
||||||
|
│ Wastegate: 45% │
|
||||||
|
│ Gear: 4 │
|
||||||
|
└──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flipper Zero интеграция
|
||||||
|
|
||||||
|
### Подключение
|
||||||
|
|
||||||
|
```
|
||||||
|
RPi5 GPIO Flipper Zero
|
||||||
|
───────── ────────────
|
||||||
|
GPIO14 (TX) ────── Pin 14 (RX)
|
||||||
|
GPIO15 (RX) ────── Pin 13 (TX)
|
||||||
|
GND ────── Pin 18 (GND)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Страницы
|
||||||
|
|
||||||
|
| # | Страница | Тип | Описание |
|
||||||
|
|---|----------|-----|----------|
|
||||||
|
| 0 | Live Data | Info | RPM, Speed, Temps, Throttle, Fuel |
|
||||||
|
| 1 | Statistics | Info | Queries, Success rate, Uptime |
|
||||||
|
| 2 | System Info | Info | IP, CPU, Memory, CAN interface |
|
||||||
|
| 3 | App Status | Info | Pipeline, Storage, PostgreSQL sync |
|
||||||
|
| 4 | UPS Status | Info | Battery %, Voltage, Power status |
|
||||||
|
| 5 | Actions | Menu | Reconnect, Clear cache, Reboot, Shutdown |
|
||||||
|
|
||||||
|
### Управление
|
||||||
|
|
||||||
|
| Кнопка | Действие |
|
||||||
|
|--------|----------|
|
||||||
|
| ← / → | Переключение страниц |
|
||||||
|
| ↑ / ↓ | Прокрутка / выбор пункта меню |
|
||||||
|
| OK | Подтверждение |
|
||||||
|
| Back | Отмена |
|
||||||
|
|
||||||
|
## Systemd сервисы
|
||||||
|
|
||||||
|
### Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../systemd
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Управление
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск
|
||||||
|
sudo systemctl start carpibord
|
||||||
|
|
||||||
|
# Статус
|
||||||
|
sudo systemctl status carpibord
|
||||||
|
|
||||||
|
# Логи
|
||||||
|
journalctl -u carpibord -f
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
sudo systemctl stop carpibord
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сервисы
|
||||||
|
|
||||||
|
| Сервис | Описание |
|
||||||
|
|--------|----------|
|
||||||
|
| `can0-link.service` | Поднимает CAN интерфейс на 500 kbps |
|
||||||
|
| `carpibord.service` | Запускает OBD2 Client после CAN и сети |
|
||||||
|
|
||||||
|
## PostgreSQL Setup
|
||||||
|
|
||||||
|
### Создание базы
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE carpibord;
|
||||||
|
CREATE USER carpibord WITH PASSWORD 'your-password';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE carpibord TO carpibord;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Таблицы (создаются автоматически)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Readings
|
||||||
|
CREATE TABLE obd2_readings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
device_id TEXT DEFAULT 'rpi5',
|
||||||
|
session_id TEXT,
|
||||||
|
pid INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
value REAL NOT NULL,
|
||||||
|
unit TEXT NOT NULL,
|
||||||
|
timestamp TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Sessions
|
||||||
|
CREATE TABLE obd2_sessions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
device_id TEXT DEFAULT 'rpi5',
|
||||||
|
start_time TIMESTAMPTZ NOT NULL,
|
||||||
|
end_time TIMESTAMPTZ,
|
||||||
|
duration_seconds REAL,
|
||||||
|
reading_count INTEGER,
|
||||||
|
avg_speed REAL,
|
||||||
|
max_speed REAL,
|
||||||
|
avg_rpm REAL,
|
||||||
|
max_rpm REAL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## UPS мониторинг (X120x)
|
||||||
|
|
||||||
|
Поддерживаются UPS HAT на базе MAX17048 fuel gauge:
|
||||||
|
- X1201, X1202 и совместимые
|
||||||
|
- Подключение по I2C (адрес 0x36)
|
||||||
|
- GPIO 6 для детекции потери питания
|
||||||
|
|
||||||
|
### Отображаемые данные
|
||||||
|
|
||||||
|
- Battery % с визуализацией `[==== ]`
|
||||||
|
- Напряжение батареи (V)
|
||||||
|
- Статус: Charging / Full / Battery Mode
|
||||||
|
- Input voltage (V)
|
||||||
|
- Предупреждения при низком заряде
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
@@ -56,5 +56,14 @@
|
|||||||
"port": "/dev/serial0",
|
"port": "/dev/serial0",
|
||||||
"baudrate": 115200,
|
"baudrate": 115200,
|
||||||
"show_ups": true
|
"show_ups": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"uds": {
|
||||||
|
"_comment": "UDS extended diagnostics (MQB platform)",
|
||||||
|
"enabled": true,
|
||||||
|
"interval_fast": 0.2,
|
||||||
|
"interval_slow": 1.0,
|
||||||
|
"fast_pids": ["0x202A", "0x437C", "0x10C0", "0x2004"],
|
||||||
|
"slow_pids": ["0x39A2", "0x200A", "0x3816", "0x3815"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,19 @@ class FlipperConfig:
|
|||||||
show_ups: bool = True
|
show_ups: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UDSConfig:
|
||||||
|
"""UDS (extended diagnostics) configuration."""
|
||||||
|
|
||||||
|
enabled: bool = False
|
||||||
|
interval_fast: float = 0.2
|
||||||
|
interval_slow: float = 1.0
|
||||||
|
# Fast PIDs: boost, torque, lambda, timing
|
||||||
|
fast_pids: List[int] = field(default_factory=lambda: [0x202A, 0x437C, 0x10C0, 0x2004])
|
||||||
|
# Slow PIDs: wastegate, timing corrections, gear
|
||||||
|
slow_pids: List[int] = field(default_factory=lambda: [0x39A2, 0x200A, 0x3816, 0x3815])
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
"""Main configuration container."""
|
"""Main configuration container."""
|
||||||
@@ -93,6 +106,7 @@ class Config:
|
|||||||
storage: StorageConfig = field(default_factory=StorageConfig)
|
storage: StorageConfig = field(default_factory=StorageConfig)
|
||||||
postgresql: PostgreSQLConfig = field(default_factory=PostgreSQLConfig)
|
postgresql: PostgreSQLConfig = field(default_factory=PostgreSQLConfig)
|
||||||
flipper: FlipperConfig = field(default_factory=FlipperConfig)
|
flipper: FlipperConfig = field(default_factory=FlipperConfig)
|
||||||
|
uds: UDSConfig = field(default_factory=UDSConfig)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, config_path: Optional[str] = None) -> "Config":
|
def load(cls, config_path: Optional[str] = None) -> "Config":
|
||||||
@@ -204,6 +218,16 @@ class Config:
|
|||||||
self.flipper.baudrate = flipper_data.get("baudrate", self.flipper.baudrate)
|
self.flipper.baudrate = flipper_data.get("baudrate", self.flipper.baudrate)
|
||||||
self.flipper.show_ups = flipper_data.get("show_ups", self.flipper.show_ups)
|
self.flipper.show_ups = flipper_data.get("show_ups", self.flipper.show_ups)
|
||||||
|
|
||||||
|
if "uds" in data:
|
||||||
|
uds_data = data["uds"]
|
||||||
|
self.uds.enabled = uds_data.get("enabled", self.uds.enabled)
|
||||||
|
self.uds.interval_fast = uds_data.get("interval_fast", self.uds.interval_fast)
|
||||||
|
self.uds.interval_slow = uds_data.get("interval_slow", self.uds.interval_slow)
|
||||||
|
if "fast_pids" in uds_data:
|
||||||
|
self.uds.fast_pids = [self._parse_hex(p) for p in uds_data["fast_pids"]]
|
||||||
|
if "slow_pids" in uds_data:
|
||||||
|
self.uds.slow_pids = [self._parse_hex(p) for p in uds_data["slow_pids"]]
|
||||||
|
|
||||||
def _load_from_env(self) -> None:
|
def _load_from_env(self) -> None:
|
||||||
"""Load configuration from environment variables."""
|
"""Load configuration from environment variables."""
|
||||||
# CAN config
|
# CAN config
|
||||||
@@ -242,6 +266,10 @@ class Config:
|
|||||||
if "OBD2_FLIPPER_PORT" in os.environ:
|
if "OBD2_FLIPPER_PORT" in os.environ:
|
||||||
self.flipper.port = os.environ["OBD2_FLIPPER_PORT"]
|
self.flipper.port = os.environ["OBD2_FLIPPER_PORT"]
|
||||||
|
|
||||||
|
# UDS config
|
||||||
|
if "OBD2_UDS_ENABLED" in os.environ:
|
||||||
|
self.uds.enabled = os.environ["OBD2_UDS_ENABLED"].lower() == "true"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_hex(value) -> int:
|
def _parse_hex(value) -> int:
|
||||||
"""Parse hex string or int to int."""
|
"""Parse hex string or int to int."""
|
||||||
|
|||||||
@@ -78,14 +78,21 @@ class PageManager:
|
|||||||
generator=self._generate_app_status_page,
|
generator=self._generate_app_status_page,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Page 4: UPS Status
|
# Page 4: UDS Extended Data
|
||||||
|
self._pages.append(PageDefinition(
|
||||||
|
page_type=PageType.INFO,
|
||||||
|
title="UDS Data",
|
||||||
|
generator=self._generate_uds_page,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Page 5: UPS Status
|
||||||
self._pages.append(PageDefinition(
|
self._pages.append(PageDefinition(
|
||||||
page_type=PageType.INFO,
|
page_type=PageType.INFO,
|
||||||
title="UPS Status",
|
title="UPS Status",
|
||||||
generator=self._generate_ups_page,
|
generator=self._generate_ups_page,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Page 5: Actions Menu (last navigable page)
|
# Page 6: Actions Menu (last navigable page)
|
||||||
self._pages.append(PageDefinition(
|
self._pages.append(PageDefinition(
|
||||||
page_type=PageType.MENU,
|
page_type=PageType.MENU,
|
||||||
title="Actions",
|
title="Actions",
|
||||||
@@ -343,14 +350,14 @@ class PageManager:
|
|||||||
if self._pending_action:
|
if self._pending_action:
|
||||||
action = self._pending_action
|
action = self._pending_action
|
||||||
self._pending_action = None
|
self._pending_action = None
|
||||||
self._current_index = 5 # Back to actions menu
|
self._current_index = 6 # Back to actions menu
|
||||||
return self.execute_action(action)
|
return self.execute_action(action)
|
||||||
return False, "No pending action"
|
return False, "No pending action"
|
||||||
|
|
||||||
def cancel_action(self) -> None:
|
def cancel_action(self) -> None:
|
||||||
"""Cancel pending action."""
|
"""Cancel pending action."""
|
||||||
self._pending_action = None
|
self._pending_action = None
|
||||||
self._current_index = 5 # Back to actions menu
|
self._current_index = 6 # Back to actions menu
|
||||||
|
|
||||||
def _get_action_success_message(self, action_id: ActionID) -> str:
|
def _get_action_success_message(self, action_id: ActionID) -> str:
|
||||||
"""Get success message for action."""
|
"""Get success message for action."""
|
||||||
@@ -516,6 +523,62 @@ class PageManager:
|
|||||||
|
|
||||||
return Page(PageType.INFO, "System", lines)
|
return Page(PageType.INFO, "System", lines)
|
||||||
|
|
||||||
|
def _generate_uds_page(self, mgr: "PageManager") -> Page:
|
||||||
|
"""Generate UDS extended diagnostics page."""
|
||||||
|
uds_values = mgr.get_data("uds_values", {})
|
||||||
|
uds_stats = mgr.get_data("uds_stats", {})
|
||||||
|
|
||||||
|
if not uds_values and not uds_stats:
|
||||||
|
lines = ["UDS not enabled", "", "Enable in config.json:", '"uds": {"enabled": true}']
|
||||||
|
return Page(PageType.INFO, "UDS Data", lines)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
# Key UDS values
|
||||||
|
# Boost pressure (0x202A)
|
||||||
|
if 0x202A in uds_values:
|
||||||
|
v = uds_values[0x202A]
|
||||||
|
lines.append(f"Boost: {v.value:.0f} kPa")
|
||||||
|
|
||||||
|
# Torque (0x437C)
|
||||||
|
if 0x437C in uds_values:
|
||||||
|
v = uds_values[0x437C]
|
||||||
|
lines.append(f"Torque: {v.value:.0f} Nm")
|
||||||
|
|
||||||
|
# Lambda (0x10C0)
|
||||||
|
if 0x10C0 in uds_values:
|
||||||
|
v = uds_values[0x10C0]
|
||||||
|
lines.append(f"Lambda: {v.value:.3f}")
|
||||||
|
|
||||||
|
# Ignition timing (0x2004)
|
||||||
|
if 0x2004 in uds_values:
|
||||||
|
v = uds_values[0x2004]
|
||||||
|
lines.append(f"Timing: {v.value:.1f} deg")
|
||||||
|
|
||||||
|
# Wastegate (0x39A2)
|
||||||
|
if 0x39A2 in uds_values:
|
||||||
|
v = uds_values[0x39A2]
|
||||||
|
lines.append(f"Wastegate: {v.value:.0f}%")
|
||||||
|
|
||||||
|
# Gear (0x3816)
|
||||||
|
if 0x3816 in uds_values:
|
||||||
|
v = uds_values[0x3816]
|
||||||
|
gear = int(v.value)
|
||||||
|
gear_str = "R" if gear == -1 else ("N" if gear == 0 else str(gear))
|
||||||
|
lines.append(f"Gear: {gear_str}")
|
||||||
|
|
||||||
|
# Stats
|
||||||
|
if uds_stats:
|
||||||
|
queries = uds_stats.get("queries", 0)
|
||||||
|
successes = uds_stats.get("successes", 0)
|
||||||
|
rate = (successes / queries * 100) if queries > 0 else 0
|
||||||
|
lines.append(f"UDS: {rate:.0f}% success")
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
lines = ["Waiting for UDS data..."]
|
||||||
|
|
||||||
|
return Page(PageType.INFO, "UDS Data", lines)
|
||||||
|
|
||||||
def _generate_actions_page(self, mgr: "PageManager") -> Page:
|
def _generate_actions_page(self, mgr: "PageManager") -> Page:
|
||||||
"""Generate actions menu page."""
|
"""Generate actions menu page."""
|
||||||
actions = [
|
actions = [
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from .flipper.server import FlipperServer
|
|||||||
from .flipper.pages import ActionID
|
from .flipper.pages import ActionID
|
||||||
from .handlers import HandlerPipeline, StorageHandler, PostgreSQLHandler, OBD2Reading
|
from .handlers import HandlerPipeline, StorageHandler, PostgreSQLHandler, OBD2Reading
|
||||||
from .flipper.providers.ups_provider import UPSProvider
|
from .flipper.providers.ups_provider import UPSProvider
|
||||||
|
from .uds import UDSProtocol, UDSPoller, MQB_PIDS
|
||||||
|
|
||||||
|
|
||||||
class OBD2Client:
|
class OBD2Client:
|
||||||
@@ -65,6 +66,12 @@ class OBD2Client:
|
|||||||
if self.pipeline.get_handler_names():
|
if self.pipeline.get_handler_names():
|
||||||
self.poller.add_reading_callback(self._on_reading)
|
self.poller.add_reading_callback(self._on_reading)
|
||||||
|
|
||||||
|
# UDS Protocol and Poller (extended diagnostics)
|
||||||
|
self.uds_protocol = None
|
||||||
|
self.uds_poller = None
|
||||||
|
if config.uds.enabled:
|
||||||
|
self._setup_uds()
|
||||||
|
|
||||||
# Flipper Zero server
|
# Flipper Zero server
|
||||||
self.flipper_server = None
|
self.flipper_server = None
|
||||||
if flipper_port or config.flipper.enabled:
|
if flipper_port or config.flipper.enabled:
|
||||||
@@ -103,6 +110,53 @@ class OBD2Client:
|
|||||||
self.pipeline.register(pg_handler)
|
self.pipeline.register(pg_handler)
|
||||||
self._logger.info("PostgreSQL handler registered")
|
self._logger.info("PostgreSQL handler registered")
|
||||||
|
|
||||||
|
def _setup_uds(self) -> None:
|
||||||
|
"""Set up UDS extended diagnostics."""
|
||||||
|
self.uds_protocol = UDSProtocol(
|
||||||
|
can_interface=self.can_interface,
|
||||||
|
timeout=self.config.obd2.timeout,
|
||||||
|
pid_registry=MQB_PIDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.uds_poller = UDSPoller(
|
||||||
|
protocol=self.uds_protocol,
|
||||||
|
fast_interval=self.config.uds.interval_fast,
|
||||||
|
slow_interval=self.config.uds.interval_slow,
|
||||||
|
fast_pids=self.config.uds.fast_pids,
|
||||||
|
slow_pids=self.config.uds.slow_pids,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Connect UDS poller to pipeline
|
||||||
|
if self.pipeline.get_handler_names():
|
||||||
|
self.uds_poller.add_reading_callback(self._on_uds_reading)
|
||||||
|
|
||||||
|
self._logger.info("UDS extended diagnostics enabled")
|
||||||
|
|
||||||
|
def _on_uds_reading(
|
||||||
|
self,
|
||||||
|
pid: int,
|
||||||
|
name: str,
|
||||||
|
value: float,
|
||||||
|
unit: str,
|
||||||
|
timestamp: datetime,
|
||||||
|
ecu_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Handle new UDS reading - send to pipeline."""
|
||||||
|
if not self.pipeline.is_initialized():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prefix name with ECU for clarity
|
||||||
|
full_name = f"[{ecu_name}] {name}"
|
||||||
|
|
||||||
|
reading = OBD2Reading(
|
||||||
|
pid=pid,
|
||||||
|
name=full_name,
|
||||||
|
value=value,
|
||||||
|
unit=unit,
|
||||||
|
timestamp=timestamp,
|
||||||
|
)
|
||||||
|
self.pipeline.handle(reading)
|
||||||
|
|
||||||
def _on_reading(
|
def _on_reading(
|
||||||
self,
|
self,
|
||||||
pid: int,
|
pid: int,
|
||||||
@@ -136,6 +190,8 @@ class OBD2Client:
|
|||||||
pm.set_data_provider("pipeline_stats", lambda: self.pipeline.get_stats())
|
pm.set_data_provider("pipeline_stats", lambda: self.pipeline.get_stats())
|
||||||
pm.set_data_provider("config", lambda: self.config)
|
pm.set_data_provider("config", lambda: self.config)
|
||||||
pm.set_data_provider("session_id", self._get_session_id)
|
pm.set_data_provider("session_id", self._get_session_id)
|
||||||
|
pm.set_data_provider("uds_values", self._get_uds_values)
|
||||||
|
pm.set_data_provider("uds_stats", self._get_uds_stats)
|
||||||
|
|
||||||
# Add UPS status page if enabled
|
# Add UPS status page if enabled
|
||||||
if self.config.flipper.show_ups:
|
if self.config.flipper.show_ups:
|
||||||
@@ -160,6 +216,18 @@ class OBD2Client:
|
|||||||
return storage.get_current_session_id() or "No session"
|
return storage.get_current_session_id() or "No session"
|
||||||
return "N/A"
|
return "N/A"
|
||||||
|
|
||||||
|
def _get_uds_values(self) -> dict:
|
||||||
|
"""Get latest UDS values for Flipper display."""
|
||||||
|
if not self.uds_poller:
|
||||||
|
return {}
|
||||||
|
return self.uds_poller.get_all_values()
|
||||||
|
|
||||||
|
def _get_uds_stats(self) -> dict:
|
||||||
|
"""Get UDS poller stats."""
|
||||||
|
if not self.uds_poller:
|
||||||
|
return {}
|
||||||
|
return self.uds_poller.get_stats()
|
||||||
|
|
||||||
def _action_force_sync(self) -> bool:
|
def _action_force_sync(self) -> bool:
|
||||||
"""Force sync to PostgreSQL."""
|
"""Force sync to PostgreSQL."""
|
||||||
pg_handler = self.pipeline.get_handler("postgresql")
|
pg_handler = self.pipeline.get_handler("postgresql")
|
||||||
@@ -282,6 +350,11 @@ class OBD2Client:
|
|||||||
self._logger.info("Starting poller...")
|
self._logger.info("Starting poller...")
|
||||||
self.poller.start()
|
self.poller.start()
|
||||||
|
|
||||||
|
# Start UDS poller if enabled
|
||||||
|
if self.uds_poller:
|
||||||
|
self._logger.info("Starting UDS poller...")
|
||||||
|
self.uds_poller.start()
|
||||||
|
|
||||||
if monitor:
|
if monitor:
|
||||||
self._monitor_loop()
|
self._monitor_loop()
|
||||||
else:
|
else:
|
||||||
@@ -380,6 +453,9 @@ class OBD2Client:
|
|||||||
if self.poller.is_running:
|
if self.poller.is_running:
|
||||||
self.poller.stop()
|
self.poller.stop()
|
||||||
|
|
||||||
|
if self.uds_poller and self.uds_poller.is_running:
|
||||||
|
self.uds_poller.stop()
|
||||||
|
|
||||||
# Shutdown handler pipeline (flushes pending data)
|
# Shutdown handler pipeline (flushes pending data)
|
||||||
if self.pipeline.is_initialized():
|
if self.pipeline.is_initialized():
|
||||||
self._logger.info("Shutting down handler pipeline...")
|
self._logger.info("Shutting down handler pipeline...")
|
||||||
|
|||||||
25
obd2_client/src/uds/__init__.py
Normal file
25
obd2_client/src/uds/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
UDS (Unified Diagnostic Services) protocol implementation.
|
||||||
|
|
||||||
|
ISO 14229 - Read-only diagnostic access to ECU data.
|
||||||
|
|
||||||
|
SAFETY: This module only implements Service 0x22 (Read Data By Identifier).
|
||||||
|
It does NOT implement write services (0x2E) or control services (0x31).
|
||||||
|
Reading diagnostic data does not affect vehicle operation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .protocol import UDSProtocol, UDSResponse
|
||||||
|
from .pids import UDSPID, UDSPIDRegistry, MQB_PIDS, ECUType, ECU_ADDRESSES
|
||||||
|
from .poller import UDSPoller, UDSPollerState
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"UDSProtocol",
|
||||||
|
"UDSResponse",
|
||||||
|
"UDSPID",
|
||||||
|
"UDSPIDRegistry",
|
||||||
|
"MQB_PIDS",
|
||||||
|
"ECUType",
|
||||||
|
"ECU_ADDRESSES",
|
||||||
|
"UDSPoller",
|
||||||
|
"UDSPollerState",
|
||||||
|
]
|
||||||
404
obd2_client/src/uds/pids.py
Normal file
404
obd2_client/src/uds/pids.py
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
"""
|
||||||
|
UDS PID definitions for MQB platform vehicles.
|
||||||
|
|
||||||
|
Based on:
|
||||||
|
- https://github.com/bri3d/MQBSimosLogVariables
|
||||||
|
- https://github.com/mrfixpl/MQB-sniffer
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Callable, Dict, List, Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ECUType(Enum):
|
||||||
|
"""ECU module types."""
|
||||||
|
ENGINE = "engine" # ECU / Simos (0x7E0)
|
||||||
|
CLUSTER = "cluster" # Instrument Cluster (0x714)
|
||||||
|
TRANSMISSION = "transmission" # DSG Gearbox (0x7E1)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ECUAddress:
|
||||||
|
"""ECU CAN addresses."""
|
||||||
|
request_id: int
|
||||||
|
response_id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
# MQB ECU addresses
|
||||||
|
ECU_ADDRESSES: Dict[ECUType, ECUAddress] = {
|
||||||
|
ECUType.ENGINE: ECUAddress(0x7E0, 0x7E8, "Engine ECU"),
|
||||||
|
ECUType.CLUSTER: ECUAddress(0x714, 0x77E, "Instrument Cluster"),
|
||||||
|
ECUType.TRANSMISSION: ECUAddress(0x7E1, 0x7E9, "DSG Transmission"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UDSPID:
|
||||||
|
"""
|
||||||
|
UDS Parameter ID definition.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
pid: 2-byte PID code (e.g., 0x202A)
|
||||||
|
name: Human-readable name
|
||||||
|
short_name: Short identifier for logging
|
||||||
|
ecu: Target ECU type
|
||||||
|
decoder: Function to decode raw bytes to value
|
||||||
|
unit: Unit of measurement
|
||||||
|
min_value: Minimum expected value
|
||||||
|
max_value: Maximum expected value
|
||||||
|
description: Detailed description
|
||||||
|
"""
|
||||||
|
pid: int
|
||||||
|
name: str
|
||||||
|
short_name: str
|
||||||
|
ecu: ECUType
|
||||||
|
decoder: Callable[[bytes], float]
|
||||||
|
unit: str
|
||||||
|
min_value: float = 0
|
||||||
|
max_value: float = 100
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
def decode(self, data: bytes) -> float:
|
||||||
|
"""Decode raw bytes to value."""
|
||||||
|
return self.decoder(data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request_id(self) -> int:
|
||||||
|
"""Get CAN request ID for this PID's ECU."""
|
||||||
|
return ECU_ADDRESSES[self.ecu].request_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def response_id(self) -> int:
|
||||||
|
"""Get CAN response ID for this PID's ECU."""
|
||||||
|
return ECU_ADDRESSES[self.ecu].response_id
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"UDS[0x{self.pid:04X}] {self.name} ({self.unit})"
|
||||||
|
|
||||||
|
|
||||||
|
# Decoder functions
|
||||||
|
def _decode_uint16_div10(data: bytes) -> float:
|
||||||
|
"""Decode unsigned 16-bit, divide by 10."""
|
||||||
|
if len(data) < 2:
|
||||||
|
return 0.0
|
||||||
|
return (data[0] * 256 + data[1]) * 0.1
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_uint16_mul10(data: bytes) -> float:
|
||||||
|
"""Decode unsigned 16-bit, multiply by 10."""
|
||||||
|
if len(data) < 2:
|
||||||
|
return 0.0
|
||||||
|
return (data[0] * 256 + data[1]) * 10
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_uint16_div100(data: bytes) -> float:
|
||||||
|
"""Decode unsigned 16-bit, divide by 100."""
|
||||||
|
if len(data) < 2:
|
||||||
|
return 0.0
|
||||||
|
return (data[0] * 256 + data[1]) * 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_uint16_div4(data: bytes) -> float:
|
||||||
|
"""Decode unsigned 16-bit, divide by 4 (RPM from cluster)."""
|
||||||
|
if len(data) < 2:
|
||||||
|
return 0.0
|
||||||
|
return (data[0] * 256 + data[1]) / 4
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_lambda(data: bytes) -> float:
|
||||||
|
"""Decode lambda value."""
|
||||||
|
if len(data) < 2:
|
||||||
|
return 0.0
|
||||||
|
return (data[0] * 256 + data[1]) * 0.000977
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_signed16_div100(data: bytes) -> float:
|
||||||
|
"""Decode signed 16-bit, divide by 100 (timing)."""
|
||||||
|
if len(data) < 2:
|
||||||
|
return 0.0
|
||||||
|
value = data[0] * 256 + data[1]
|
||||||
|
if value > 32767:
|
||||||
|
value -= 65536
|
||||||
|
return value * 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_uint8(data: bytes) -> float:
|
||||||
|
"""Decode single unsigned byte."""
|
||||||
|
if len(data) < 1:
|
||||||
|
return 0.0
|
||||||
|
return float(data[0])
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_gear(data: bytes) -> float:
|
||||||
|
"""Decode DSG gear position."""
|
||||||
|
if len(data) < 1:
|
||||||
|
return 0.0
|
||||||
|
gear_map = {
|
||||||
|
0x00: 0, # Neutral
|
||||||
|
0x0C: -1, # Reverse
|
||||||
|
0x02: 1, # 1st
|
||||||
|
0x04: 2, # 2nd
|
||||||
|
0x06: 3, # 3rd
|
||||||
|
0x08: 4, # 4th
|
||||||
|
0x0A: 5, # 5th
|
||||||
|
0x0E: 6, # 6th
|
||||||
|
0x10: 7, # 7th
|
||||||
|
}
|
||||||
|
return float(gear_map.get(data[0], 0))
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_gear_mode(data: bytes) -> float:
|
||||||
|
"""Decode DSG gear mode (P/R/N/D/S/M)."""
|
||||||
|
if len(data) < 1:
|
||||||
|
return 0.0
|
||||||
|
# 0=P, 1=R, 2=N, 3=D, 4=S, 5=M
|
||||||
|
return float(data[0])
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_temperature(data: bytes) -> float:
|
||||||
|
"""Decode temperature (offset -40)."""
|
||||||
|
if len(data) < 1:
|
||||||
|
return 0.0
|
||||||
|
return float(data[0]) - 40
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_uint16(data: bytes) -> float:
|
||||||
|
"""Decode unsigned 16-bit raw."""
|
||||||
|
if len(data) < 2:
|
||||||
|
return 0.0
|
||||||
|
return float(data[0] * 256 + data[1])
|
||||||
|
|
||||||
|
|
||||||
|
class UDSPIDRegistry:
|
||||||
|
"""Registry of UDS PIDs."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._pids: Dict[int, UDSPID] = {}
|
||||||
|
self._by_ecu: Dict[ECUType, List[UDSPID]] = {
|
||||||
|
ECUType.ENGINE: [],
|
||||||
|
ECUType.CLUSTER: [],
|
||||||
|
ECUType.TRANSMISSION: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
def register(self, pid: UDSPID) -> None:
|
||||||
|
"""Register a PID."""
|
||||||
|
self._pids[pid.pid] = pid
|
||||||
|
self._by_ecu[pid.ecu].append(pid)
|
||||||
|
|
||||||
|
def get(self, pid_code: int) -> Optional[UDSPID]:
|
||||||
|
"""Get PID by code."""
|
||||||
|
return self._pids.get(pid_code)
|
||||||
|
|
||||||
|
def get_all(self) -> List[UDSPID]:
|
||||||
|
"""Get all registered PIDs."""
|
||||||
|
return list(self._pids.values())
|
||||||
|
|
||||||
|
def get_by_ecu(self, ecu: ECUType) -> List[UDSPID]:
|
||||||
|
"""Get PIDs for specific ECU."""
|
||||||
|
return self._by_ecu.get(ecu, [])
|
||||||
|
|
||||||
|
def __contains__(self, pid_code: int) -> bool:
|
||||||
|
return pid_code in self._pids
|
||||||
|
|
||||||
|
def __getitem__(self, pid_code: int) -> UDSPID:
|
||||||
|
return self._pids[pid_code]
|
||||||
|
|
||||||
|
|
||||||
|
# Create MQB PID registry
|
||||||
|
MQB_PIDS = UDSPIDRegistry()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ENGINE ECU (Simos) PIDs - 0x7E0
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x202A,
|
||||||
|
name="Boost Pressure Actual",
|
||||||
|
short_name="boost",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_uint16_div10,
|
||||||
|
unit="kPa",
|
||||||
|
min_value=0,
|
||||||
|
max_value=400,
|
||||||
|
description="Actual charge/boost pressure",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x2029,
|
||||||
|
name="Boost Pressure Target",
|
||||||
|
short_name="boost_tgt",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_uint16_div10,
|
||||||
|
unit="kPa",
|
||||||
|
min_value=0,
|
||||||
|
max_value=400,
|
||||||
|
description="Target charge/boost pressure",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0xF423,
|
||||||
|
name="Fuel Rail Pressure",
|
||||||
|
short_name="frp",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_uint16_mul10,
|
||||||
|
unit="kPa",
|
||||||
|
min_value=0,
|
||||||
|
max_value=25000,
|
||||||
|
description="High pressure fuel rail",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x10C0,
|
||||||
|
name="Lambda Actual",
|
||||||
|
short_name="lambda",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_lambda,
|
||||||
|
unit="λ",
|
||||||
|
min_value=0.5,
|
||||||
|
max_value=2.0,
|
||||||
|
description="Actual air-fuel ratio (1.0 = stoich)",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x1456,
|
||||||
|
name="Lambda Target",
|
||||||
|
short_name="lambda_tgt",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_lambda,
|
||||||
|
unit="λ",
|
||||||
|
min_value=0.5,
|
||||||
|
max_value=2.0,
|
||||||
|
description="Target air-fuel ratio",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x437C,
|
||||||
|
name="Torque Actual",
|
||||||
|
short_name="torque",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_uint16_div10,
|
||||||
|
unit="Nm",
|
||||||
|
min_value=0,
|
||||||
|
max_value=500,
|
||||||
|
description="Actual engine torque output",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x39A2,
|
||||||
|
name="Wastegate Position",
|
||||||
|
short_name="wg_pos",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_uint16_div100,
|
||||||
|
unit="%",
|
||||||
|
min_value=0,
|
||||||
|
max_value=100,
|
||||||
|
description="Turbo wastegate position",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x39A3,
|
||||||
|
name="Wastegate Target",
|
||||||
|
short_name="wg_tgt",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_uint16_div100,
|
||||||
|
unit="%",
|
||||||
|
min_value=0,
|
||||||
|
max_value=100,
|
||||||
|
description="Target wastegate position",
|
||||||
|
))
|
||||||
|
|
||||||
|
# Timing corrections per cylinder
|
||||||
|
for cyl, pid_offset in enumerate([0x200A, 0x200B, 0x200C, 0x200D], 1):
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=pid_offset,
|
||||||
|
name=f"Timing Correction Cyl {cyl}",
|
||||||
|
short_name=f"tc_c{cyl}",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_signed16_div100,
|
||||||
|
unit="°",
|
||||||
|
min_value=-15,
|
||||||
|
max_value=5,
|
||||||
|
description=f"Knock-related timing correction cylinder {cyl}",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x2004,
|
||||||
|
name="Ignition Timing",
|
||||||
|
short_name="ign_timing",
|
||||||
|
ecu=ECUType.ENGINE,
|
||||||
|
decoder=_decode_signed16_div100,
|
||||||
|
unit="°",
|
||||||
|
min_value=-10,
|
||||||
|
max_value=50,
|
||||||
|
description="Ignition timing advance",
|
||||||
|
))
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# INSTRUMENT CLUSTER PIDs - 0x714
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x22D1,
|
||||||
|
name="RPM (Cluster)",
|
||||||
|
short_name="rpm_cl",
|
||||||
|
ecu=ECUType.CLUSTER,
|
||||||
|
decoder=_decode_uint16_div4,
|
||||||
|
unit="RPM",
|
||||||
|
min_value=0,
|
||||||
|
max_value=8000,
|
||||||
|
description="Engine RPM from instrument cluster",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x224D,
|
||||||
|
name="Ambient Light",
|
||||||
|
short_name="amb_light",
|
||||||
|
ecu=ECUType.CLUSTER,
|
||||||
|
decoder=_decode_uint8,
|
||||||
|
unit="",
|
||||||
|
min_value=0,
|
||||||
|
max_value=255,
|
||||||
|
description="Ambient light sensor (0=dark, 255=bright)",
|
||||||
|
))
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DSG TRANSMISSION PIDs - 0x7E1
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x3816,
|
||||||
|
name="Current Gear",
|
||||||
|
short_name="gear",
|
||||||
|
ecu=ECUType.TRANSMISSION,
|
||||||
|
decoder=_decode_gear,
|
||||||
|
unit="",
|
||||||
|
min_value=-1,
|
||||||
|
max_value=7,
|
||||||
|
description="Current engaged gear (-1=R, 0=N, 1-7=gears)",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x3815,
|
||||||
|
name="Gear Selector",
|
||||||
|
short_name="prndm",
|
||||||
|
ecu=ECUType.TRANSMISSION,
|
||||||
|
decoder=_decode_gear_mode,
|
||||||
|
unit="",
|
||||||
|
min_value=0,
|
||||||
|
max_value=5,
|
||||||
|
description="Gear selector position (0=P, 1=R, 2=N, 3=D, 4=S, 5=M)",
|
||||||
|
))
|
||||||
|
|
||||||
|
MQB_PIDS.register(UDSPID(
|
||||||
|
pid=0x1940,
|
||||||
|
name="Transmission Temp",
|
||||||
|
short_name="trans_temp",
|
||||||
|
ecu=ECUType.TRANSMISSION,
|
||||||
|
decoder=_decode_temperature,
|
||||||
|
unit="°C",
|
||||||
|
min_value=-40,
|
||||||
|
max_value=150,
|
||||||
|
description="Transmission oil temperature",
|
||||||
|
))
|
||||||
314
obd2_client/src/uds/poller.py
Normal file
314
obd2_client/src/uds/poller.py
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
"""
|
||||||
|
UDS PID polling service.
|
||||||
|
|
||||||
|
Polls extended diagnostic data from MQB platform ECUs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional, Set, Dict, Callable, Any
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from .protocol import UDSProtocol, UDSResponse
|
||||||
|
from .pids import MQB_PIDS, ECUType, UDSPID
|
||||||
|
from ..logger import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
# Type alias for reading callback
|
||||||
|
UDSReadingCallback = Callable[[int, str, float, str, datetime, str], None]
|
||||||
|
|
||||||
|
|
||||||
|
class UDSPollerState(Enum):
|
||||||
|
"""Poller state enum."""
|
||||||
|
STOPPED = "stopped"
|
||||||
|
RUNNING = "running"
|
||||||
|
PAUSED = "paused"
|
||||||
|
|
||||||
|
|
||||||
|
class UDSPoller:
|
||||||
|
"""
|
||||||
|
Background poller for UDS PIDs.
|
||||||
|
|
||||||
|
Polls extended diagnostic data from Engine ECU, Cluster, and DSG.
|
||||||
|
Supports priority-based polling similar to OBD2 poller.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
protocol: UDSProtocol,
|
||||||
|
fast_interval: float = 0.2,
|
||||||
|
slow_interval: float = 1.0,
|
||||||
|
fast_pids: Optional[List[int]] = None,
|
||||||
|
slow_pids: Optional[List[int]] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize UDS poller.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
protocol: UDSProtocol instance
|
||||||
|
fast_interval: Polling interval for fast PIDs (seconds)
|
||||||
|
slow_interval: Polling interval for slow PIDs (seconds)
|
||||||
|
fast_pids: PIDs to poll frequently (boost, torque, timing)
|
||||||
|
slow_pids: PIDs to poll less frequently (temps, gear)
|
||||||
|
"""
|
||||||
|
self.protocol = protocol
|
||||||
|
self.fast_interval = fast_interval
|
||||||
|
self.slow_interval = slow_interval
|
||||||
|
|
||||||
|
# Default fast PIDs: boost, torque, lambda, timing
|
||||||
|
self.fast_pids: Set[int] = set(fast_pids or [
|
||||||
|
0x202A, # Boost Actual
|
||||||
|
0x437C, # Torque
|
||||||
|
0x10C0, # Lambda
|
||||||
|
0x2004, # Ignition Timing
|
||||||
|
])
|
||||||
|
|
||||||
|
# Default slow PIDs: wastegate, timing corrections, gear
|
||||||
|
self.slow_pids: Set[int] = set(slow_pids or [
|
||||||
|
0x39A2, # Wastegate Position
|
||||||
|
0x200A, # Timing Correction Cyl 1
|
||||||
|
0x3816, # Current Gear
|
||||||
|
0x3815, # Gear Selector (PRNDM)
|
||||||
|
])
|
||||||
|
|
||||||
|
self._state = UDSPollerState.STOPPED
|
||||||
|
self._thread: Optional[threading.Thread] = None
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._pause_event = threading.Event()
|
||||||
|
self._pause_event.set()
|
||||||
|
|
||||||
|
self._logger = get_logger("uds.poller")
|
||||||
|
self._stats = {
|
||||||
|
"queries": 0,
|
||||||
|
"successes": 0,
|
||||||
|
"failures": 0,
|
||||||
|
"by_ecu": {
|
||||||
|
ECUType.ENGINE.value: {"queries": 0, "successes": 0},
|
||||||
|
ECUType.CLUSTER.value: {"queries": 0, "successes": 0},
|
||||||
|
ECUType.TRANSMISSION.value: {"queries": 0, "successes": 0},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self._latest_values: Dict[int, UDSResponse] = {}
|
||||||
|
self._values_lock = threading.Lock()
|
||||||
|
self._reading_callbacks: List[UDSReadingCallback] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def poller_state(self) -> UDSPollerState:
|
||||||
|
"""Get current poller state."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""Check if poller is running."""
|
||||||
|
return self._state == UDSPollerState.RUNNING
|
||||||
|
|
||||||
|
def add_reading_callback(self, callback: UDSReadingCallback) -> None:
|
||||||
|
"""
|
||||||
|
Add callback for each successful reading.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback: Function(pid, name, value, unit, timestamp, ecu_name)
|
||||||
|
"""
|
||||||
|
self._reading_callbacks.append(callback)
|
||||||
|
|
||||||
|
def remove_reading_callback(self, callback: UDSReadingCallback) -> None:
|
||||||
|
"""Remove a reading callback."""
|
||||||
|
if callback in self._reading_callbacks:
|
||||||
|
self._reading_callbacks.remove(callback)
|
||||||
|
|
||||||
|
def set_pids(
|
||||||
|
self,
|
||||||
|
fast_pids: Optional[List[int]] = None,
|
||||||
|
slow_pids: Optional[List[int]] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Update PIDs to poll.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fast_pids: New fast PIDs (or None to keep current)
|
||||||
|
slow_pids: New slow PIDs (or None to keep current)
|
||||||
|
"""
|
||||||
|
if fast_pids is not None:
|
||||||
|
self.fast_pids = set(fast_pids)
|
||||||
|
if slow_pids is not None:
|
||||||
|
self.slow_pids = set(slow_pids)
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Start the polling thread."""
|
||||||
|
if self._thread is not None and self._thread.is_alive():
|
||||||
|
self._logger.warning("UDS poller already running")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._stop_event.clear()
|
||||||
|
self._pause_event.set()
|
||||||
|
self._state = UDSPollerState.RUNNING
|
||||||
|
|
||||||
|
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
self._logger.info("UDS poller started")
|
||||||
|
|
||||||
|
def stop(self, timeout: float = 2.0) -> None:
|
||||||
|
"""
|
||||||
|
Stop the polling thread.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeout: Maximum time to wait for thread to stop
|
||||||
|
"""
|
||||||
|
if self._thread is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._stop_event.set()
|
||||||
|
self._pause_event.set()
|
||||||
|
|
||||||
|
self._thread.join(timeout=timeout)
|
||||||
|
if self._thread.is_alive():
|
||||||
|
self._logger.warning("UDS poller thread did not stop cleanly")
|
||||||
|
|
||||||
|
self._thread = None
|
||||||
|
self._state = UDSPollerState.STOPPED
|
||||||
|
self._logger.info("UDS poller stopped")
|
||||||
|
|
||||||
|
def pause(self) -> None:
|
||||||
|
"""Pause polling."""
|
||||||
|
self._pause_event.clear()
|
||||||
|
self._state = UDSPollerState.PAUSED
|
||||||
|
self._logger.info("UDS poller paused")
|
||||||
|
|
||||||
|
def resume(self) -> None:
|
||||||
|
"""Resume polling after pause."""
|
||||||
|
self._pause_event.set()
|
||||||
|
self._state = UDSPollerState.RUNNING
|
||||||
|
self._logger.info("UDS poller resumed")
|
||||||
|
|
||||||
|
def get_stats(self) -> dict:
|
||||||
|
"""Get polling statistics."""
|
||||||
|
return dict(self._stats)
|
||||||
|
|
||||||
|
def get_value(self, pid_code: int) -> Optional[UDSResponse]:
|
||||||
|
"""
|
||||||
|
Get latest value for a PID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pid_code: PID to get
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Latest UDSResponse or None
|
||||||
|
"""
|
||||||
|
with self._values_lock:
|
||||||
|
return self._latest_values.get(pid_code)
|
||||||
|
|
||||||
|
def get_all_values(self) -> Dict[int, UDSResponse]:
|
||||||
|
"""Get all latest values."""
|
||||||
|
with self._values_lock:
|
||||||
|
return dict(self._latest_values)
|
||||||
|
|
||||||
|
def _poll_loop(self) -> None:
|
||||||
|
"""Main polling loop."""
|
||||||
|
last_slow_poll = 0.0
|
||||||
|
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
self._pause_event.wait()
|
||||||
|
|
||||||
|
if self._stop_event.is_set():
|
||||||
|
break
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Poll fast PIDs
|
||||||
|
for pid_code in self.fast_pids:
|
||||||
|
if self._stop_event.is_set():
|
||||||
|
break
|
||||||
|
self._poll_pid(pid_code)
|
||||||
|
|
||||||
|
# Poll slow PIDs at slower interval
|
||||||
|
if (current_time - last_slow_poll) >= self.slow_interval:
|
||||||
|
for pid_code in self.slow_pids:
|
||||||
|
if self._stop_event.is_set():
|
||||||
|
break
|
||||||
|
self._poll_pid(pid_code)
|
||||||
|
last_slow_poll = current_time
|
||||||
|
|
||||||
|
# Sleep for fast interval
|
||||||
|
sleep_time = self.fast_interval - (time.time() - current_time)
|
||||||
|
if sleep_time > 0:
|
||||||
|
self._stop_event.wait(sleep_time)
|
||||||
|
|
||||||
|
def _poll_pid(self, pid_code: int) -> bool:
|
||||||
|
"""
|
||||||
|
Poll a single UDS PID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pid_code: PID to poll
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful
|
||||||
|
"""
|
||||||
|
self._stats["queries"] += 1
|
||||||
|
|
||||||
|
pid = self.protocol.pids.get(pid_code)
|
||||||
|
if pid is None:
|
||||||
|
self._stats["failures"] += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
ecu_name = pid.ecu.value
|
||||||
|
self._stats["by_ecu"][ecu_name]["queries"] += 1
|
||||||
|
|
||||||
|
response = self.protocol.query_pid(pid_code)
|
||||||
|
if response is None:
|
||||||
|
self._stats["failures"] += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._stats["successes"] += 1
|
||||||
|
self._stats["by_ecu"][ecu_name]["successes"] += 1
|
||||||
|
|
||||||
|
timestamp = datetime.now()
|
||||||
|
|
||||||
|
# Store latest value
|
||||||
|
with self._values_lock:
|
||||||
|
self._latest_values[pid_code] = response
|
||||||
|
|
||||||
|
# Notify callbacks
|
||||||
|
for callback in self._reading_callbacks:
|
||||||
|
try:
|
||||||
|
callback(
|
||||||
|
pid_code,
|
||||||
|
response.pid.name,
|
||||||
|
response.value,
|
||||||
|
response.unit,
|
||||||
|
timestamp,
|
||||||
|
ecu_name,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.debug(f"UDS reading callback error: {e}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def create_uds_poller_from_config(
|
||||||
|
protocol: UDSProtocol,
|
||||||
|
config: Any,
|
||||||
|
) -> UDSPoller:
|
||||||
|
"""
|
||||||
|
Create UDS poller from config object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
protocol: UDSProtocol instance
|
||||||
|
config: Config object with uds section
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured UDSPoller
|
||||||
|
"""
|
||||||
|
uds_config = getattr(config, "uds", None)
|
||||||
|
|
||||||
|
if uds_config is None:
|
||||||
|
return UDSPoller(protocol)
|
||||||
|
|
||||||
|
return UDSPoller(
|
||||||
|
protocol=protocol,
|
||||||
|
fast_interval=getattr(uds_config, "interval_fast", 0.2),
|
||||||
|
slow_interval=getattr(uds_config, "interval_slow", 1.0),
|
||||||
|
fast_pids=getattr(uds_config, "fast_pids", None),
|
||||||
|
slow_pids=getattr(uds_config, "slow_pids", None),
|
||||||
|
)
|
||||||
293
obd2_client/src/uds/protocol.py
Normal file
293
obd2_client/src/uds/protocol.py
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
"""
|
||||||
|
UDS Protocol implementation (ISO 14229).
|
||||||
|
|
||||||
|
Read-only diagnostic access using Service 0x22 (Read Data By Identifier).
|
||||||
|
This is a SAFE, read-only protocol that does not modify vehicle behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from ..can.frame import CANFrame
|
||||||
|
from ..can.interface import CANInterface
|
||||||
|
from ..logger import get_logger
|
||||||
|
from .pids import UDSPID, UDSPIDRegistry, MQB_PIDS, ECUType, ECU_ADDRESSES
|
||||||
|
|
||||||
|
|
||||||
|
class UDSService(Enum):
|
||||||
|
"""UDS Service IDs (only safe read services)."""
|
||||||
|
READ_DATA_BY_ID = 0x22 # Read Data By Identifier
|
||||||
|
READ_DATA_BY_ID_RESPONSE = 0x62 # Positive response
|
||||||
|
|
||||||
|
|
||||||
|
class UDSError(Enum):
|
||||||
|
"""UDS Negative Response Codes."""
|
||||||
|
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
|
||||||
|
REQUEST_SEQUENCE_ERROR = 0x24
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UDSResponse:
|
||||||
|
"""
|
||||||
|
UDS response data.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
pid: PID that was queried
|
||||||
|
raw_data: Raw response data bytes
|
||||||
|
value: Decoded value
|
||||||
|
unit: Unit of measurement
|
||||||
|
ecu: Source ECU type
|
||||||
|
"""
|
||||||
|
pid: UDSPID
|
||||||
|
raw_data: bytes
|
||||||
|
value: float
|
||||||
|
unit: str
|
||||||
|
ecu: ECUType
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.pid.name}: {self.value:.2f} {self.unit}"
|
||||||
|
|
||||||
|
|
||||||
|
class UDSProtocol:
|
||||||
|
"""
|
||||||
|
UDS protocol handler for MQB platform vehicles.
|
||||||
|
|
||||||
|
Implements ISO 14229 Service 0x22 (Read Data By Identifier).
|
||||||
|
This is READ-ONLY and does not affect vehicle operation.
|
||||||
|
|
||||||
|
Safety notes:
|
||||||
|
- Only uses Service 0x22 (read)
|
||||||
|
- Does NOT use 0x2E (write), 0x31 (routine), 0x27 (security)
|
||||||
|
- Does NOT change diagnostic session (stays in default)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
can_interface: CANInterface,
|
||||||
|
timeout: float = 0.1,
|
||||||
|
pid_registry: Optional[UDSPIDRegistry] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize UDS protocol handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
can_interface: CAN interface to use
|
||||||
|
timeout: Response timeout in seconds
|
||||||
|
pid_registry: PID registry (default: MQB_PIDS)
|
||||||
|
"""
|
||||||
|
self.can = can_interface
|
||||||
|
self.timeout = timeout
|
||||||
|
self.pids = pid_registry or MQB_PIDS
|
||||||
|
self._logger = get_logger("uds.protocol")
|
||||||
|
|
||||||
|
def build_request(self, pid: int, ecu: ECUType) -> CANFrame:
|
||||||
|
"""
|
||||||
|
Build a UDS Read Data By Identifier request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pid: 2-byte PID code
|
||||||
|
ecu: Target ECU type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CANFrame ready to send
|
||||||
|
"""
|
||||||
|
ecu_addr = ECU_ADDRESSES[ecu]
|
||||||
|
pid_high = (pid >> 8) & 0xFF
|
||||||
|
pid_low = pid & 0xFF
|
||||||
|
|
||||||
|
# UDS frame format:
|
||||||
|
# [length, service_id, pid_high, pid_low, padding...]
|
||||||
|
data = bytes([
|
||||||
|
0x03, # Length: 3 bytes (service + 2-byte PID)
|
||||||
|
UDSService.READ_DATA_BY_ID.value, # 0x22
|
||||||
|
pid_high,
|
||||||
|
pid_low,
|
||||||
|
0x55, 0x55, 0x55, 0x55 # Padding
|
||||||
|
])
|
||||||
|
|
||||||
|
return CANFrame(arbitration_id=ecu_addr.request_id, data=data)
|
||||||
|
|
||||||
|
def parse_response(
|
||||||
|
self,
|
||||||
|
frame: CANFrame,
|
||||||
|
expected_pid: int,
|
||||||
|
ecu: ECUType,
|
||||||
|
) -> Optional[bytes]:
|
||||||
|
"""
|
||||||
|
Parse UDS response frame.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: Received CAN frame
|
||||||
|
expected_pid: PID we're expecting response for
|
||||||
|
ecu: Expected source ECU
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data bytes if valid positive response, None otherwise
|
||||||
|
"""
|
||||||
|
ecu_addr = ECU_ADDRESSES[ecu]
|
||||||
|
|
||||||
|
# Verify response is from correct ECU
|
||||||
|
if frame.arbitration_id != ecu_addr.response_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(frame.data) < 4:
|
||||||
|
return None
|
||||||
|
|
||||||
|
length = frame.data[0]
|
||||||
|
service = frame.data[1]
|
||||||
|
|
||||||
|
# Check for negative response (0x7F)
|
||||||
|
if service == 0x7F:
|
||||||
|
if len(frame.data) >= 4:
|
||||||
|
error_service = frame.data[2]
|
||||||
|
error_code = frame.data[3]
|
||||||
|
self._logger.debug(
|
||||||
|
f"UDS negative response: service=0x{error_service:02X}, "
|
||||||
|
f"error=0x{error_code:02X}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check for positive response (0x62)
|
||||||
|
if service != UDSService.READ_DATA_BY_ID_RESPONSE.value:
|
||||||
|
self._logger.debug(f"Unexpected service: 0x{service:02X}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Verify PID in response
|
||||||
|
response_pid = (frame.data[2] << 8) | frame.data[3]
|
||||||
|
if response_pid != expected_pid:
|
||||||
|
self._logger.debug(
|
||||||
|
f"PID mismatch: expected 0x{expected_pid:04X}, "
|
||||||
|
f"got 0x{response_pid:04X}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extract data bytes (after service + PID)
|
||||||
|
data_length = length - 3 # Subtract service(1) + pid(2)
|
||||||
|
if data_length <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return frame.data[4:4 + data_length]
|
||||||
|
|
||||||
|
def query_pid(self, pid_code: int) -> Optional[UDSResponse]:
|
||||||
|
"""
|
||||||
|
Query a single UDS PID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pid_code: PID code to query
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UDSResponse if successful, None otherwise
|
||||||
|
"""
|
||||||
|
pid = self.pids.get(pid_code)
|
||||||
|
if pid is None:
|
||||||
|
self._logger.warning(f"Unknown UDS PID: 0x{pid_code:04X}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
request = self.build_request(pid_code, pid.ecu)
|
||||||
|
|
||||||
|
if not self.can.send(request):
|
||||||
|
self._logger.error(f"Failed to send UDS request for 0x{pid_code:04X}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Wait for response from the specific ECU
|
||||||
|
response_id = pid.response_id
|
||||||
|
frame = self.can.receive_filtered([response_id], timeout=self.timeout)
|
||||||
|
|
||||||
|
if frame is None:
|
||||||
|
self._logger.debug(f"No response for UDS PID 0x{pid_code:04X}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
raw_data = self.parse_response(frame, pid_code, pid.ecu)
|
||||||
|
if raw_data is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = pid.decode(raw_data)
|
||||||
|
return UDSResponse(
|
||||||
|
pid=pid,
|
||||||
|
raw_data=raw_data,
|
||||||
|
value=value,
|
||||||
|
unit=pid.unit,
|
||||||
|
ecu=pid.ecu,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Failed to decode UDS PID 0x{pid_code:04X}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def query_ecu(self, ecu: ECUType) -> Dict[int, UDSResponse]:
|
||||||
|
"""
|
||||||
|
Query all known PIDs for a specific ECU.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ecu: ECU type to query
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping PID code to response
|
||||||
|
"""
|
||||||
|
results = {}
|
||||||
|
pids = self.pids.get_by_ecu(ecu)
|
||||||
|
|
||||||
|
for pid in pids:
|
||||||
|
response = self.query_pid(pid.pid)
|
||||||
|
if response is not None:
|
||||||
|
results[pid.pid] = response
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def scan_pid(self, pid_code: int, ecu: ECUType) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a PID is supported by sending a test query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pid_code: PID to test
|
||||||
|
ecu: Target ECU
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if ECU responds positively
|
||||||
|
"""
|
||||||
|
request = self.build_request(pid_code, ecu)
|
||||||
|
|
||||||
|
if not self.can.send(request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
response_id = ECU_ADDRESSES[ecu].response_id
|
||||||
|
frame = self.can.receive_filtered([response_id], timeout=self.timeout)
|
||||||
|
|
||||||
|
if frame is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for positive response
|
||||||
|
if len(frame.data) >= 2:
|
||||||
|
service = frame.data[1]
|
||||||
|
return service == UDSService.READ_DATA_BY_ID_RESPONSE.value
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_supported_pids(self, ecu: ECUType) -> List[int]:
|
||||||
|
"""
|
||||||
|
Find which registered PIDs are supported by ECU.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ecu: ECU to scan
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of supported PID codes
|
||||||
|
"""
|
||||||
|
supported = []
|
||||||
|
pids = self.pids.get_by_ecu(ecu)
|
||||||
|
|
||||||
|
self._logger.info(f"Scanning {len(pids)} PIDs for {ecu.value}...")
|
||||||
|
|
||||||
|
for pid in pids:
|
||||||
|
if self.scan_pid(pid.pid, ecu):
|
||||||
|
supported.append(pid.pid)
|
||||||
|
self._logger.debug(f" Found: 0x{pid.pid:04X} ({pid.name})")
|
||||||
|
|
||||||
|
self._logger.info(f"Found {len(supported)} supported PIDs for {ecu.value}")
|
||||||
|
return supported
|
||||||
Reference in New Issue
Block a user