Create project for flipper zero integration
This commit is contained in:
@@ -156,11 +156,34 @@ class LoggingConfig(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class FlipperConfig(BaseModel):
|
||||
"""Конфигурация Flipper Zero UART."""
|
||||
|
||||
model_config = {"extra": "ignore"}
|
||||
|
||||
enabled: bool = Field(
|
||||
default=False,
|
||||
description="Включить отправку статистики на Flipper Zero"
|
||||
)
|
||||
device: str = Field(
|
||||
default="/dev/ttyAMA0",
|
||||
description="UART устройство для подключения Flipper Zero"
|
||||
)
|
||||
baudrate: int = Field(
|
||||
default=115200,
|
||||
description="Скорость UART (бод)"
|
||||
)
|
||||
send_interval: float = Field(
|
||||
default=1.0,
|
||||
description="Интервал отправки статистики (секунды)"
|
||||
)
|
||||
|
||||
|
||||
class GeneralConfig(BaseModel):
|
||||
"""Общие настройки."""
|
||||
|
||||
|
||||
model_config = {"extra": "ignore"}
|
||||
|
||||
|
||||
buffer_size: int = Field(
|
||||
default=10000,
|
||||
description="Размер буфера для данных"
|
||||
@@ -196,6 +219,7 @@ class Config(BaseSettings):
|
||||
can: CanConfig = Field(default_factory=CanConfig)
|
||||
storage: StorageConfig = Field(default_factory=StorageConfig)
|
||||
postgresql: PostgreSQLConfig = Field(default_factory=PostgreSQLConfig)
|
||||
flipper: FlipperConfig = Field(default_factory=FlipperConfig)
|
||||
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
||||
general: GeneralConfig = Field(default_factory=GeneralConfig)
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
from .base import BaseHandler
|
||||
from .storage_handler import StorageHandler
|
||||
from .postgresql_handler import PostgreSQLHandler
|
||||
from .flipper_handler import FlipperHandler
|
||||
|
||||
__all__ = [
|
||||
'BaseHandler',
|
||||
'StorageHandler',
|
||||
'PostgreSQLHandler',
|
||||
'FlipperHandler',
|
||||
]
|
||||
|
||||
|
||||
293
can_sniffer/src/handlers/flipper_handler.py
Normal file
293
can_sniffer/src/handlers/flipper_handler.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Flipper Zero UART Handler.
|
||||
|
||||
Sends CAN sniffer statistics to Flipper Zero via UART.
|
||||
Provides real-time monitoring on Flipper Zero display.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from handlers.base import BaseHandler
|
||||
from can_frame import CANFrame
|
||||
from config import config
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_ip_address() -> str:
|
||||
"""
|
||||
Get the primary IP address of this device.
|
||||
|
||||
Returns:
|
||||
IP address string or "0.0.0.0" if unable to determine
|
||||
"""
|
||||
try:
|
||||
# Create a socket to determine the outgoing IP
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(0.1)
|
||||
# Connect to a public address (doesn't actually send data)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: try to get any non-localhost IP
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
ip = socket.gethostbyname(hostname)
|
||||
if ip and not ip.startswith("127."):
|
||||
return ip
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "0.0.0.0"
|
||||
|
||||
|
||||
class FlipperHandler(BaseHandler):
|
||||
"""
|
||||
Handler that sends statistics to Flipper Zero via UART.
|
||||
|
||||
UART Configuration:
|
||||
- Device: /dev/ttyAMA0 (or configured device)
|
||||
- Baud: 115200
|
||||
- Format: 8N1
|
||||
|
||||
Protocol:
|
||||
Sends text line: STATS:ip=<ip>,total=<n>,pending=<n>,processed=<n>\n
|
||||
"""
|
||||
|
||||
def __init__(self, enabled: Optional[bool] = None):
|
||||
"""
|
||||
Initialize Flipper handler.
|
||||
|
||||
Args:
|
||||
enabled: Whether handler is enabled. If None, reads from config.
|
||||
"""
|
||||
# Check config for enabled status
|
||||
if enabled is None:
|
||||
enabled = getattr(config, "flipper", None) is not None
|
||||
if enabled:
|
||||
enabled = getattr(config.flipper, "enabled", False)
|
||||
|
||||
super().__init__(name="flipper_handler", enabled=enabled)
|
||||
|
||||
self.serial_port: Optional[Any] = None
|
||||
self.device = "/dev/ttyAMA0"
|
||||
self.baudrate = 115200
|
||||
self.send_interval = 1.0 # Send stats every 1 second
|
||||
|
||||
# Load config if available
|
||||
if hasattr(config, "flipper"):
|
||||
flipper_cfg = config.flipper
|
||||
self.device = getattr(flipper_cfg, "device", self.device)
|
||||
self.baudrate = getattr(flipper_cfg, "baudrate", self.baudrate)
|
||||
self.send_interval = getattr(flipper_cfg, "send_interval", self.send_interval)
|
||||
|
||||
# Statistics
|
||||
self._stats_lock = threading.Lock()
|
||||
self._total_frames = 0
|
||||
self._pending_frames = 0
|
||||
self._processed_frames = 0
|
||||
self._sent_count = 0
|
||||
self._error_count = 0
|
||||
|
||||
# Background sender thread
|
||||
self._sender_thread: Optional[threading.Thread] = None
|
||||
self._running = False
|
||||
|
||||
# IP address cache
|
||||
self._ip_address = "0.0.0.0"
|
||||
self._last_ip_check = 0
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
Initialize UART connection to Flipper Zero.
|
||||
|
||||
Returns:
|
||||
True if initialization successful
|
||||
"""
|
||||
try:
|
||||
import serial
|
||||
|
||||
self.serial_port = serial.Serial(
|
||||
port=self.device,
|
||||
baudrate=self.baudrate,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=0.1,
|
||||
)
|
||||
|
||||
# Get initial IP address
|
||||
self._ip_address = get_ip_address()
|
||||
self._last_ip_check = time.time()
|
||||
|
||||
self._initialized = True
|
||||
self.logger.info(
|
||||
f"Flipper handler initialized on {self.device} @ {self.baudrate} baud"
|
||||
)
|
||||
return True
|
||||
|
||||
except ImportError:
|
||||
self.logger.error("pyserial not installed. Run: pip install pyserial")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to initialize Flipper UART: {e}")
|
||||
return False
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the background sender thread."""
|
||||
if self._running:
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._sender_thread = threading.Thread(
|
||||
target=self._sender_loop, name="FlipperSender", daemon=True
|
||||
)
|
||||
self._sender_thread.start()
|
||||
self.logger.info("Flipper sender thread started")
|
||||
|
||||
def _sender_loop(self) -> None:
|
||||
"""Background loop that sends stats periodically."""
|
||||
while self._running:
|
||||
try:
|
||||
self._send_stats()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error sending stats to Flipper: {e}")
|
||||
with self._stats_lock:
|
||||
self._error_count += 1
|
||||
|
||||
time.sleep(self.send_interval)
|
||||
|
||||
def _send_stats(self) -> None:
|
||||
"""Send current statistics to Flipper Zero."""
|
||||
if not self.serial_port or not self.serial_port.is_open:
|
||||
return
|
||||
|
||||
# Refresh IP address every 60 seconds
|
||||
current_time = time.time()
|
||||
if current_time - self._last_ip_check > 60:
|
||||
self._ip_address = get_ip_address()
|
||||
self._last_ip_check = current_time
|
||||
|
||||
with self._stats_lock:
|
||||
total = self._total_frames
|
||||
pending = self._pending_frames
|
||||
processed = self._processed_frames
|
||||
|
||||
# Build stats message
|
||||
message = f"STATS:ip={self._ip_address},total={total},pending={pending},processed={processed}\n"
|
||||
|
||||
try:
|
||||
self.serial_port.write(message.encode("utf-8"))
|
||||
self.serial_port.flush()
|
||||
|
||||
with self._stats_lock:
|
||||
self._sent_count += 1
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"UART write error: {e}")
|
||||
with self._stats_lock:
|
||||
self._error_count += 1
|
||||
|
||||
def handle(self, frame: CANFrame) -> bool:
|
||||
"""
|
||||
Handle a single CAN frame.
|
||||
|
||||
Updates frame counters for statistics.
|
||||
|
||||
Args:
|
||||
frame: CANFrame to handle
|
||||
|
||||
Returns:
|
||||
True (always succeeds, just updates counters)
|
||||
"""
|
||||
with self._stats_lock:
|
||||
self._total_frames += 1
|
||||
self._pending_frames += 1
|
||||
return True
|
||||
|
||||
def handle_batch(self, frames: List[CANFrame]) -> int:
|
||||
"""
|
||||
Handle a batch of CAN frames.
|
||||
|
||||
Args:
|
||||
frames: List of CANFrame objects
|
||||
|
||||
Returns:
|
||||
Number of frames processed (all of them)
|
||||
"""
|
||||
count = len(frames)
|
||||
with self._stats_lock:
|
||||
self._total_frames += count
|
||||
# After batch processing, frames are processed
|
||||
self._processed_frames += count
|
||||
# Reduce pending by batch count
|
||||
self._pending_frames = max(0, self._pending_frames - count)
|
||||
return count
|
||||
|
||||
def update_pending(self, pending_count: int) -> None:
|
||||
"""
|
||||
Update pending frame count.
|
||||
|
||||
Called externally to sync with actual queue size.
|
||||
|
||||
Args:
|
||||
pending_count: Current number of pending frames
|
||||
"""
|
||||
with self._stats_lock:
|
||||
self._pending_frames = pending_count
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush - send immediate stats update."""
|
||||
try:
|
||||
self._send_stats()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error in flush: {e}")
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Shutdown the handler."""
|
||||
self.logger.info("Shutting down Flipper handler...")
|
||||
|
||||
self._running = False
|
||||
|
||||
if self._sender_thread and self._sender_thread.is_alive():
|
||||
self._sender_thread.join(timeout=2.0)
|
||||
|
||||
if self.serial_port and self.serial_port.is_open:
|
||||
try:
|
||||
# Send final "disconnected" message
|
||||
self.serial_port.write(b"STATS:ip=---,total=0,pending=0,processed=0\n")
|
||||
self.serial_port.flush()
|
||||
self.serial_port.close()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error closing serial port: {e}")
|
||||
|
||||
self._initialized = False
|
||||
self.logger.info("Flipper handler stopped")
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get handler statistics.
|
||||
|
||||
Returns:
|
||||
Dictionary with handler stats
|
||||
"""
|
||||
with self._stats_lock:
|
||||
return {
|
||||
"total_frames": self._total_frames,
|
||||
"pending_frames": self._pending_frames,
|
||||
"processed_frames": self._processed_frames,
|
||||
"sent_count": self._sent_count,
|
||||
"error_count": self._error_count,
|
||||
"device": self.device,
|
||||
"baudrate": self.baudrate,
|
||||
"connected": self.serial_port.is_open if self.serial_port else False,
|
||||
"ip_address": self._ip_address,
|
||||
}
|
||||
@@ -14,7 +14,7 @@ from typing import Optional, Dict, Any, List
|
||||
from logger import get_logger
|
||||
from config import config
|
||||
from can_frame import CANFrame
|
||||
from handlers import BaseHandler, StorageHandler, PostgreSQLHandler
|
||||
from handlers import BaseHandler, StorageHandler, PostgreSQLHandler, FlipperHandler
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -87,18 +87,21 @@ class MessageProcessor:
|
||||
def _create_default_handlers(self) -> List[BaseHandler]:
|
||||
"""
|
||||
Создание обработчиков по умолчанию из конфигурации.
|
||||
|
||||
|
||||
Returns:
|
||||
Список обработчиков
|
||||
"""
|
||||
handlers = []
|
||||
|
||||
|
||||
# Storage handler всегда включен
|
||||
handlers.append(StorageHandler(enabled=True))
|
||||
|
||||
|
||||
# PostgreSQL handler зависит от конфигурации
|
||||
handlers.append(PostgreSQLHandler(enabled=None)) # None = из config
|
||||
|
||||
|
||||
# Flipper Zero handler зависит от конфигурации
|
||||
handlers.append(FlipperHandler(enabled=None)) # None = из config
|
||||
|
||||
return handlers
|
||||
|
||||
def _init_handlers(self, handlers: List[BaseHandler]) -> None:
|
||||
@@ -347,9 +350,9 @@ class MessageProcessor:
|
||||
|
||||
self.running = True
|
||||
|
||||
# Запускаем специальные обработчики (например, PostgreSQL forwarder)
|
||||
# Запускаем специальные обработчики (например, PostgreSQL forwarder, Flipper sender)
|
||||
for handler in self.handlers:
|
||||
if isinstance(handler, PostgreSQLHandler) and handler.is_initialized():
|
||||
if isinstance(handler, (PostgreSQLHandler, FlipperHandler)) and handler.is_initialized():
|
||||
try:
|
||||
handler.start()
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user