add readme
This commit is contained in:
412
can_sniffer/README.md
Normal file
412
can_sniffer/README.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# 🚗 CAN Sniffer
|
||||||
|
|
||||||
|
[](https://www.python.org/downloads/)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com/psf/black)
|
||||||
|
[](https://github.com/python/mypy)
|
||||||
|
|
||||||
|
> Высокопроизводительный CAN bus sniffer для Raspberry Pi с плагинной архитектурой обработчиков, поддержкой параллельного чтения нескольких шин и интеграцией с InfluxDB.
|
||||||
|
|
||||||
|
## ✨ Особенности
|
||||||
|
|
||||||
|
- 🔌 **Параллельное чтение** - одновременная работа с несколькими CAN интерфейсами
|
||||||
|
- 🧩 **Плагинная архитектура** - легко добавлять новые обработчики (Kafka, MQTT, WebSocket)
|
||||||
|
- 📊 **InfluxDB интеграция** - отправка данных в InfluxDB с retry и health-check
|
||||||
|
- 💾 **SQLite хранилище** - локальное временное хранилище с WAL режимом
|
||||||
|
- ⚡ **Асинхронная обработка** - неблокирующая обработка сообщений через очередь
|
||||||
|
- 🔄 **Store-and-Forward** - надежная доставка данных с retry и backoff
|
||||||
|
- 📝 **Структурированное логирование** - детальное логирование с ротацией файлов
|
||||||
|
- ⚙️ **Гибкая конфигурация** - JSON файл + переменные окружения
|
||||||
|
- 🛡️ **Graceful shutdown** - корректное завершение работы всех компонентов
|
||||||
|
- 🎯 **Типобезопасность** - полная типизация с Pydantic
|
||||||
|
|
||||||
|
## 📋 Требования
|
||||||
|
|
||||||
|
- Python 3.11+
|
||||||
|
- Linux (для SocketCAN)
|
||||||
|
- CAN интерфейсы (например, MCP2515 на Raspberry Pi)
|
||||||
|
|
||||||
|
## 🚀 Установка
|
||||||
|
|
||||||
|
### 1. Клонирование репозитория
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd can_sniffer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Настройка CAN интерфейсов
|
||||||
|
|
||||||
|
Убедитесь, что CAN интерфейсы настроены и доступны:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка доступных интерфейсов
|
||||||
|
ip link show
|
||||||
|
|
||||||
|
# Настройка интерфейса (пример)
|
||||||
|
sudo ip link set can0 type can bitrate 500000
|
||||||
|
sudo ip link set can0 up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Конфигурация
|
||||||
|
|
||||||
|
Скопируйте и отредактируйте `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"can": {
|
||||||
|
"interfaces": ["can0", "can1"],
|
||||||
|
"listen_only": true,
|
||||||
|
"bitrate": 500000,
|
||||||
|
"filters": []
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"type": "sqlite",
|
||||||
|
"database_path": "can_offline.db",
|
||||||
|
"wal_mode": true,
|
||||||
|
"sync_mode": "NORMAL"
|
||||||
|
},
|
||||||
|
"influxdb": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "http://localhost:8086",
|
||||||
|
"token": "your-token",
|
||||||
|
"org": "automotive",
|
||||||
|
"bucket": "can_bus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎮 Использование
|
||||||
|
|
||||||
|
### Базовый запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск как модуль
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m can_sniffer.src.main
|
||||||
|
```
|
||||||
|
|
||||||
|
### С кастомными обработчиками
|
||||||
|
|
||||||
|
```python
|
||||||
|
from handlers import StorageHandler, InfluxDBHandler
|
||||||
|
from socket_can.message_processor import MessageProcessor
|
||||||
|
from socket_can import CANSniffer
|
||||||
|
|
||||||
|
# Создаем кастомный pipeline
|
||||||
|
handlers = [
|
||||||
|
StorageHandler(enabled=True),
|
||||||
|
InfluxDBHandler(enabled=True),
|
||||||
|
# Ваш кастомный обработчик
|
||||||
|
]
|
||||||
|
|
||||||
|
# Используем в MessageProcessor
|
||||||
|
processor = MessageProcessor(handlers=handlers)
|
||||||
|
sniffer = CANSniffer()
|
||||||
|
sniffer.start()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ CAN Bus │
|
||||||
|
│ (can0/can1)│
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ CANSniffer │ ← Параллельное чтение
|
||||||
|
│ (SocketCAN) │
|
||||||
|
└──────┬──────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ MessageProcessor │ ← Асинхронная очередь
|
||||||
|
│ (Queue) │
|
||||||
|
└──────┬───────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ Handler Pipeline │
|
||||||
|
├──────────────────┤
|
||||||
|
│ • StorageHandler │ → SQLite
|
||||||
|
│ • InfluxHandler │ → InfluxDB
|
||||||
|
│ • CustomHandler │ → Kafka/MQTT/WS
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Компоненты
|
||||||
|
|
||||||
|
- **CANFrame** - неизменяемая структура данных для CAN сообщений
|
||||||
|
- **CANSniffer** - параллельное чтение с нескольких CAN шин
|
||||||
|
- **MessageProcessor** - асинхронная обработка через очередь
|
||||||
|
- **BaseHandler** - интерфейс для плагинных обработчиков
|
||||||
|
- **StorageHandler** - сохранение в SQLite
|
||||||
|
- **InfluxDBHandler** - отправка в InfluxDB с retry
|
||||||
|
|
||||||
|
## 📦 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
can_sniffer/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.py # Точка входа
|
||||||
|
│ ├── config.py # Конфигурация (Pydantic)
|
||||||
|
│ ├── config.json # Файл конфигурации
|
||||||
|
│ ├── logger.py # Логирование
|
||||||
|
│ ├── can_frame.py # CANFrame dataclass
|
||||||
|
│ ├── socket_can/
|
||||||
|
│ │ ├── src.py # CANSniffer
|
||||||
|
│ │ └── message_processor.py # MessageProcessor
|
||||||
|
│ ├── handlers/
|
||||||
|
│ │ ├── base.py # BaseHandler
|
||||||
|
│ │ ├── storage_handler.py # SQLite handler
|
||||||
|
│ │ └── influxdb_handler.py # InfluxDB handler
|
||||||
|
│ ├── storage/
|
||||||
|
│ │ └── storage.py # SQLite storage
|
||||||
|
│ └── influxdb_handler/
|
||||||
|
│ └── influxdb_client.py # InfluxDB client
|
||||||
|
├── requirements.txt
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Конфигурация
|
||||||
|
|
||||||
|
### CAN настройки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"can": {
|
||||||
|
"interfaces": ["can0", "can1"], // Список интерфейсов
|
||||||
|
"listen_only": true, // Только чтение
|
||||||
|
"bitrate": 500000, // Скорость (бит/с)
|
||||||
|
"filters": [] // SocketCAN фильтры
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage настройки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"storage": {
|
||||||
|
"type": "sqlite",
|
||||||
|
"database_path": "can_offline.db",
|
||||||
|
"wal_mode": true, // WAL режим для производительности
|
||||||
|
"sync_mode": "NORMAL" // NORMAL/FULL/OFF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### InfluxDB настройки
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"influxdb": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "http://localhost:8086",
|
||||||
|
"token": "your-token",
|
||||||
|
"org": "automotive",
|
||||||
|
"bucket": "can_bus",
|
||||||
|
"batch_size": 1000, // Размер батча
|
||||||
|
"flush_interval": 5, // Интервал отправки (сек)
|
||||||
|
"max_retries": 3, // Количество повторов
|
||||||
|
"retry_backoff": 1.0, // Backoff интервал
|
||||||
|
"health_check_interval": 30 // Проверка здоровья (сек)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Логирование
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"level": "INFO", // DEBUG/INFO/WARNING/ERROR
|
||||||
|
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
"file": "can_edge.log",
|
||||||
|
"max_bytes": 10485760, // 10MB
|
||||||
|
"backup_count": 5 // Количество ротаций
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Переменные окружения
|
||||||
|
|
||||||
|
Конфигурацию можно переопределить через переменные окружения:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CAN_INTERFACES="can0,can1"
|
||||||
|
export INFLUXDB_URL="http://localhost:8086"
|
||||||
|
export INFLUXDB_TOKEN="your-token"
|
||||||
|
export LOGGING_LEVEL="DEBUG"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 Создание кастомного обработчика
|
||||||
|
|
||||||
|
```python
|
||||||
|
from handlers.base import BaseHandler
|
||||||
|
from can_frame import CANFrame
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
class MyCustomHandler(BaseHandler):
|
||||||
|
def __init__(self, enabled: bool = True):
|
||||||
|
super().__init__(name="my_handler", enabled=enabled)
|
||||||
|
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
# Инициализация (подключение к Kafka/MQTT/WebSocket)
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
# ... ваш код
|
||||||
|
self._initialized = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle(self, frame: CANFrame) -> bool:
|
||||||
|
# Обработка одного фрейма
|
||||||
|
# ... ваш код
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle_batch(self, frames: List[CANFrame]) -> int:
|
||||||
|
# Обработка батча
|
||||||
|
# ... ваш код
|
||||||
|
return len(frames)
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
# Принудительная отправка
|
||||||
|
pass
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
# Закрытие соединений
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"handler": self.name,
|
||||||
|
"enabled": self.enabled,
|
||||||
|
"initialized": self._initialized
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Статистика
|
||||||
|
|
||||||
|
Приложение выводит статистику каждые 10 секунд:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"processed_count": 12345,
|
||||||
|
"dropped_count": 0,
|
||||||
|
"queue_size": 0,
|
||||||
|
"running": true,
|
||||||
|
"handlers_count": 2,
|
||||||
|
"storage": {
|
||||||
|
"total_messages": 12345,
|
||||||
|
"database_path": "can_offline.db"
|
||||||
|
},
|
||||||
|
"influxdb": {
|
||||||
|
"enabled": true,
|
||||||
|
"sent_count": 12000,
|
||||||
|
"failed_count": 0,
|
||||||
|
"connection_status": "connected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Отладка
|
||||||
|
|
||||||
|
### Включение DEBUG логирования
|
||||||
|
|
||||||
|
Измените в `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"level": "DEBUG"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка CAN интерфейсов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка статуса
|
||||||
|
ip link show can0
|
||||||
|
|
||||||
|
# Тестовая отправка (если не listen_only)
|
||||||
|
cansend can0 123#DEADBEEF
|
||||||
|
|
||||||
|
# Мониторинг трафика
|
||||||
|
candump can0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка InfluxDB
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка соединения
|
||||||
|
curl -G http://localhost:8086/health
|
||||||
|
|
||||||
|
# Запрос данных
|
||||||
|
influx query 'from(bucket:"can_bus") |> range(start: -1h)'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Разработка
|
||||||
|
|
||||||
|
### Установка для разработки
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск тестов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# TODO: добавить тесты
|
||||||
|
pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Форматирование кода
|
||||||
|
|
||||||
|
```bash
|
||||||
|
black src/
|
||||||
|
isort src/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Лицензия
|
||||||
|
|
||||||
|
MIT License - см. [LICENSE](LICENSE) файл для деталей.
|
||||||
|
|
||||||
|
## 🤝 Вклад
|
||||||
|
|
||||||
|
Вклады приветствуются! Пожалуйста:
|
||||||
|
|
||||||
|
1. Fork проекта
|
||||||
|
2. Создайте feature branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit изменения (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push в branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Откройте Pull Request
|
||||||
|
|
||||||
|
## 📧 Контакты
|
||||||
|
|
||||||
|
- Issues: [GitHub Issues](https://github.com/yourusername/can_sniffer/issues)
|
||||||
|
- Discussions: [GitHub Discussions](https://github.com/yourusername/can_sniffer/discussions)
|
||||||
|
|
||||||
|
## 🙏 Благодарности
|
||||||
|
|
||||||
|
- [python-can](https://github.com/hardbyte/python-can) - библиотека для работы с CAN
|
||||||
|
- [InfluxDB](https://www.influxdata.com/) - time-series база данных
|
||||||
|
- [Pydantic](https://docs.pydantic.dev/) - валидация данных
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
⭐ Если проект полезен, поставьте звезду!
|
||||||
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
"""
|
|
||||||
Пример обработчика для демонстрации плагинной архитектуры.
|
|
||||||
|
|
||||||
Этот файл показывает, как легко добавить новый обработчик (например, Kafka, MQTT, WebSocket).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
from can_frame import CANFrame
|
|
||||||
from .base import BaseHandler
|
|
||||||
|
|
||||||
|
|
||||||
class ExampleHandler(BaseHandler):
|
|
||||||
"""
|
|
||||||
Пример обработчика для демонстрации.
|
|
||||||
|
|
||||||
Этот обработчик можно использовать как шаблон для создания новых:
|
|
||||||
- KafkaHandler
|
|
||||||
- MQTTHandler
|
|
||||||
- WebSocketHandler
|
|
||||||
- FileHandler
|
|
||||||
и т.д.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, enabled: bool = True):
|
|
||||||
"""Инициализация примера обработчика."""
|
|
||||||
super().__init__(name="example", enabled=enabled)
|
|
||||||
self.processed_count = 0
|
|
||||||
self.failed_count = 0
|
|
||||||
|
|
||||||
def initialize(self) -> bool:
|
|
||||||
"""Инициализация обработчика."""
|
|
||||||
if not self.enabled:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Здесь должна быть логика подключения к внешнему сервису
|
|
||||||
# Например: self.kafka_producer = KafkaProducer(...)
|
|
||||||
# Или: self.mqtt_client = mqtt.Client(...)
|
|
||||||
self._initialized = True
|
|
||||||
self.logger.info("Example handler initialized")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Failed to initialize example handler: {e}", exc_info=True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def handle(self, frame: CANFrame) -> bool:
|
|
||||||
"""Обработка одного CAN фрейма."""
|
|
||||||
if not self.enabled or not self._initialized:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Здесь должна быть логика отправки одного фрейма
|
|
||||||
# Например: self.kafka_producer.send('can-topic', frame.to_dict())
|
|
||||||
# Или: self.mqtt_client.publish('can/data', frame.to_dict())
|
|
||||||
|
|
||||||
self.processed_count += 1
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(
|
|
||||||
f"Failed to handle frame: {e}",
|
|
||||||
exc_info=True,
|
|
||||||
extra={"can_id": frame.can_id_hex}
|
|
||||||
)
|
|
||||||
self.failed_count += 1
|
|
||||||
return False
|
|
||||||
|
|
||||||
def handle_batch(self, frames: List[CANFrame]) -> int:
|
|
||||||
"""Обработка батча CAN фреймов."""
|
|
||||||
if not self.enabled or not self._initialized or not frames:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Здесь должна быть логика пакетной отправки
|
|
||||||
# Например: self.kafka_producer.send_batch([...])
|
|
||||||
# Или: self.mqtt_client.publish_batch([...])
|
|
||||||
|
|
||||||
processed = 0
|
|
||||||
for frame in frames:
|
|
||||||
if self.handle(frame):
|
|
||||||
processed += 1
|
|
||||||
|
|
||||||
return processed
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(
|
|
||||||
f"Failed to handle batch: {e}",
|
|
||||||
exc_info=True,
|
|
||||||
extra={"batch_size": len(frames)}
|
|
||||||
)
|
|
||||||
self.failed_count += len(frames)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def flush(self) -> None:
|
|
||||||
"""Принудительная отправка накопленных данных."""
|
|
||||||
if not self.enabled or not self._initialized:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Здесь должна быть логика принудительной отправки
|
|
||||||
# Например: self.kafka_producer.flush()
|
|
||||||
# Или: self.mqtt_client.flush()
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Failed to flush: {e}", exc_info=True)
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
"""Корректное завершение работы обработчика."""
|
|
||||||
if self._initialized:
|
|
||||||
try:
|
|
||||||
# Здесь должна быть логика закрытия соединений
|
|
||||||
# Например: self.kafka_producer.close()
|
|
||||||
# Или: self.mqtt_client.disconnect()
|
|
||||||
self.logger.info("Example handler closed")
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Error closing example handler: {e}", exc_info=True)
|
|
||||||
self._initialized = False
|
|
||||||
|
|
||||||
def get_stats(self) -> Dict[str, Any]:
|
|
||||||
"""Получение статистики обработчика."""
|
|
||||||
return {
|
|
||||||
"handler": self.name,
|
|
||||||
"enabled": self.enabled,
|
|
||||||
"initialized": self._initialized,
|
|
||||||
"processed_count": self.processed_count,
|
|
||||||
"failed_count": self.failed_count
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Пример использования:
|
|
||||||
#
|
|
||||||
# from handlers import BaseHandler, StorageHandler, InfluxDBHandler
|
|
||||||
# from handlers.example_handler import ExampleHandler
|
|
||||||
# from socket_can.message_processor import MessageProcessor
|
|
||||||
#
|
|
||||||
# # Создаем кастомный pipeline
|
|
||||||
# handlers = [
|
|
||||||
# StorageHandler(enabled=True),
|
|
||||||
# InfluxDBHandler(enabled=True),
|
|
||||||
# ExampleHandler(enabled=True), # Новый обработчик!
|
|
||||||
# ]
|
|
||||||
#
|
|
||||||
# # Используем в MessageProcessor
|
|
||||||
# processor = MessageProcessor(handlers=handlers)
|
|
||||||
# processor.start()
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user