Create project for flipper zero integration
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -87,3 +87,4 @@ can_edge.log*
|
|||||||
config.json
|
config.json
|
||||||
|
|
||||||
.cursor/
|
.cursor/
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -156,6 +156,29 @@ 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):
|
class GeneralConfig(BaseModel):
|
||||||
"""Общие настройки."""
|
"""Общие настройки."""
|
||||||
|
|
||||||
@@ -196,6 +219,7 @@ class Config(BaseSettings):
|
|||||||
can: CanConfig = Field(default_factory=CanConfig)
|
can: CanConfig = Field(default_factory=CanConfig)
|
||||||
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)
|
||||||
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
||||||
general: GeneralConfig = Field(default_factory=GeneralConfig)
|
general: GeneralConfig = Field(default_factory=GeneralConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
from .base import BaseHandler
|
from .base import BaseHandler
|
||||||
from .storage_handler import StorageHandler
|
from .storage_handler import StorageHandler
|
||||||
from .postgresql_handler import PostgreSQLHandler
|
from .postgresql_handler import PostgreSQLHandler
|
||||||
|
from .flipper_handler import FlipperHandler
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'BaseHandler',
|
'BaseHandler',
|
||||||
'StorageHandler',
|
'StorageHandler',
|
||||||
'PostgreSQLHandler',
|
'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 logger import get_logger
|
||||||
from config import config
|
from config import config
|
||||||
from can_frame import CANFrame
|
from can_frame import CANFrame
|
||||||
from handlers import BaseHandler, StorageHandler, PostgreSQLHandler
|
from handlers import BaseHandler, StorageHandler, PostgreSQLHandler, FlipperHandler
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -99,6 +99,9 @@ class MessageProcessor:
|
|||||||
# PostgreSQL handler зависит от конфигурации
|
# PostgreSQL handler зависит от конфигурации
|
||||||
handlers.append(PostgreSQLHandler(enabled=None)) # None = из config
|
handlers.append(PostgreSQLHandler(enabled=None)) # None = из config
|
||||||
|
|
||||||
|
# Flipper Zero handler зависит от конфигурации
|
||||||
|
handlers.append(FlipperHandler(enabled=None)) # None = из config
|
||||||
|
|
||||||
return handlers
|
return handlers
|
||||||
|
|
||||||
def _init_handlers(self, handlers: List[BaseHandler]) -> None:
|
def _init_handlers(self, handlers: List[BaseHandler]) -> None:
|
||||||
@@ -347,9 +350,9 @@ class MessageProcessor:
|
|||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
# Запускаем специальные обработчики (например, PostgreSQL forwarder)
|
# Запускаем специальные обработчики (например, PostgreSQL forwarder, Flipper sender)
|
||||||
for handler in self.handlers:
|
for handler in self.handlers:
|
||||||
if isinstance(handler, PostgreSQLHandler) and handler.is_initialized():
|
if isinstance(handler, (PostgreSQLHandler, FlipperHandler)) and handler.is_initialized():
|
||||||
try:
|
try:
|
||||||
handler.start()
|
handler.start()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
170
flip_monitor/README.md
Normal file
170
flip_monitor/README.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# CAN Monitor for Flipper Zero
|
||||||
|
|
||||||
|
Flipper Zero application for monitoring CAN sniffer statistics from Raspberry Pi 5 via UART.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Real-time display of CAN sniffer statistics
|
||||||
|
- Shows: IP address, total frames, pending frames, processed frames
|
||||||
|
- Connection status indicator
|
||||||
|
- Compatible with Unleashed firmware (0.84e and later)
|
||||||
|
|
||||||
|
## Hardware Connection
|
||||||
|
|
||||||
|
### Wiring (RPI5 <-> Flipper Zero)
|
||||||
|
|
||||||
|
```
|
||||||
|
RPI5 GPIO Flipper Zero GPIO
|
||||||
|
----------- -----------------
|
||||||
|
TX (GPIO 14) --> RX (Pin 14)
|
||||||
|
RX (GPIO 15) <-- TX (Pin 13)
|
||||||
|
GND --> GND
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Cross TX/RX connections (RPI TX -> Flipper RX, RPI RX -> Flipper TX)
|
||||||
|
|
||||||
|
### Flipper Zero GPIO Pinout
|
||||||
|
|
||||||
|
```
|
||||||
|
Pin 13 = TX (USART)
|
||||||
|
Pin 14 = RX (USART)
|
||||||
|
Pin 8/11/18 = GND
|
||||||
|
```
|
||||||
|
|
||||||
|
### RPI5 GPIO (using /dev/ttyAMA0)
|
||||||
|
|
||||||
|
```
|
||||||
|
GPIO 14 = TX (Pin 8)
|
||||||
|
GPIO 15 = RX (Pin 10)
|
||||||
|
GND = Pin 6, 9, 14, 20, 25, 30, 34, 39
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building the Application
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. Clone Flipper Zero firmware:
|
||||||
|
```bash
|
||||||
|
git clone --recursive https://github.com/DarkFlippers/unleashed-firmware.git
|
||||||
|
cd unleashed-firmware
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Copy the `flip_monitor` folder to `applications_user/`:
|
||||||
|
```bash
|
||||||
|
cp -r /path/to/carpibord/flip_monitor applications_user/can_monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create icon (10x10 PNG, 1-bit):
|
||||||
|
```bash
|
||||||
|
# Create icons/can_monitor.png (10x10 pixels, black & white)
|
||||||
|
# You can use any image editor or online tool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the FAP
|
||||||
|
./fbt fap_can_monitor
|
||||||
|
|
||||||
|
# Or build all external apps
|
||||||
|
./fbt fap_dist
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiled `.fap` file will be in `build/f7-firmware-D/.extapps/can_monitor.fap`
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
Copy the `.fap` file to your Flipper Zero SD card:
|
||||||
|
```
|
||||||
|
SD Card/apps/GPIO/can_monitor.fap
|
||||||
|
```
|
||||||
|
|
||||||
|
## RPI5 Configuration
|
||||||
|
|
||||||
|
### 1. Enable UART
|
||||||
|
|
||||||
|
Add to `/boot/config.txt`:
|
||||||
|
```
|
||||||
|
enable_uart=1
|
||||||
|
dtoverlay=uart0
|
||||||
|
```
|
||||||
|
|
||||||
|
Reboot after changes.
|
||||||
|
|
||||||
|
### 2. Install pyserial
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyserial
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure CAN Sniffer
|
||||||
|
|
||||||
|
Add to `can_sniffer/config.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"flipper": {
|
||||||
|
"enabled": true,
|
||||||
|
"device": "/dev/ttyAMA0",
|
||||||
|
"baudrate": 115200,
|
||||||
|
"send_interval": 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use environment variables:
|
||||||
|
```bash
|
||||||
|
export CAN_SNIFFER_FLIPPER__ENABLED=true
|
||||||
|
export CAN_SNIFFER_FLIPPER__DEVICE=/dev/ttyAMA0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Run CAN Sniffer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd can_sniffer/src
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protocol
|
||||||
|
|
||||||
|
The RPI5 sends text-based statistics over UART:
|
||||||
|
|
||||||
|
```
|
||||||
|
STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245\n
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `ip` - RPI5 IP address
|
||||||
|
- `total` - Total CAN frames received
|
||||||
|
- `pending` - Frames in processing queue
|
||||||
|
- `processed` - Successfully processed frames
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### No connection
|
||||||
|
|
||||||
|
1. Check wiring (TX/RX crossed correctly)
|
||||||
|
2. Verify UART is enabled on RPI5: `ls -la /dev/ttyAMA0`
|
||||||
|
3. Check config: `flipper.enabled = true`
|
||||||
|
4. Test UART manually:
|
||||||
|
```bash
|
||||||
|
# On RPI5
|
||||||
|
echo "STATS:ip=test,total=1,pending=0,processed=1" > /dev/ttyAMA0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission denied on /dev/ttyAMA0
|
||||||
|
|
||||||
|
Add user to dialout group:
|
||||||
|
```bash
|
||||||
|
sudo usermod -a -G dialout $USER
|
||||||
|
# Then logout and login again
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flipper shows "Waiting..."
|
||||||
|
|
||||||
|
- Stats are sent every 1 second (configurable)
|
||||||
|
- Connection timeout is 5 seconds
|
||||||
|
- Check if CAN sniffer is running
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
11
flip_monitor/application.fam
Normal file
11
flip_monitor/application.fam
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
App(
|
||||||
|
appid="can_monitor",
|
||||||
|
name="CAN Monitor",
|
||||||
|
apptype=FlipperAppType.EXTERNAL,
|
||||||
|
entry_point="can_monitor_app",
|
||||||
|
stack_size=2 * 1024,
|
||||||
|
fap_category="GPIO",
|
||||||
|
fap_author="carpibord",
|
||||||
|
fap_version="1.0",
|
||||||
|
fap_description="CAN Sniffer monitor via UART from RPI5",
|
||||||
|
)
|
||||||
327
flip_monitor/can_monitor.c
Normal file
327
flip_monitor/can_monitor.c
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
/**
|
||||||
|
* CAN Monitor for Flipper Zero
|
||||||
|
*
|
||||||
|
* Receives CAN sniffer statistics from RPI5 via UART.
|
||||||
|
* Displays: IP address, total frames, pending frames, processed frames.
|
||||||
|
*
|
||||||
|
* UART Configuration:
|
||||||
|
* - TX: GPIO 13 (pin 13)
|
||||||
|
* - RX: GPIO 14 (pin 14)
|
||||||
|
* - Baud: 115200
|
||||||
|
* - 8N1
|
||||||
|
*
|
||||||
|
* Protocol: Text-based
|
||||||
|
* Format: STATS:ip=<ip>,total=<n>,pending=<n>,processed=<n>\n
|
||||||
|
* Example: STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245\n
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <furi.h>
|
||||||
|
#include <furi_hal.h>
|
||||||
|
#include <gui/gui.h>
|
||||||
|
#include <gui/view_port.h>
|
||||||
|
#include <notification/notification.h>
|
||||||
|
#include <notification/notification_messages.h>
|
||||||
|
|
||||||
|
#include <expansion/expansion.h>
|
||||||
|
#include <furi_hal_serial.h>
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define TAG "CANMonitor"
|
||||||
|
|
||||||
|
// UART configuration
|
||||||
|
#define UART_CH FuriHalSerialIdUsart
|
||||||
|
#define UART_BAUD 115200
|
||||||
|
|
||||||
|
// Buffer sizes
|
||||||
|
#define RX_BUFFER_SIZE 256
|
||||||
|
#define IP_BUFFER_SIZE 32
|
||||||
|
|
||||||
|
// Statistics structure
|
||||||
|
typedef struct {
|
||||||
|
char ip_address[IP_BUFFER_SIZE];
|
||||||
|
uint32_t total_frames;
|
||||||
|
uint32_t pending_frames;
|
||||||
|
uint32_t processed_frames;
|
||||||
|
bool connected;
|
||||||
|
uint32_t last_update_tick;
|
||||||
|
} CanStats;
|
||||||
|
|
||||||
|
// Application context
|
||||||
|
typedef struct {
|
||||||
|
Gui* gui;
|
||||||
|
ViewPort* view_port;
|
||||||
|
FuriMessageQueue* event_queue;
|
||||||
|
FuriMutex* mutex;
|
||||||
|
|
||||||
|
FuriHalSerialHandle* serial_handle;
|
||||||
|
FuriStreamBuffer* rx_stream;
|
||||||
|
FuriThread* rx_thread;
|
||||||
|
|
||||||
|
CanStats stats;
|
||||||
|
bool running;
|
||||||
|
} CanMonitorApp;
|
||||||
|
|
||||||
|
// Event types
|
||||||
|
typedef enum {
|
||||||
|
EventTypeKey,
|
||||||
|
EventTypeStats,
|
||||||
|
} EventType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
EventType type;
|
||||||
|
InputEvent input;
|
||||||
|
} CanMonitorEvent;
|
||||||
|
|
||||||
|
// Parse statistics from received line
|
||||||
|
static bool parse_stats_line(const char* line, CanStats* stats) {
|
||||||
|
// Format: STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245
|
||||||
|
if(strncmp(line, "STATS:", 6) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* data = line + 6;
|
||||||
|
|
||||||
|
// Parse ip=
|
||||||
|
const char* ip_start = strstr(data, "ip=");
|
||||||
|
if(ip_start) {
|
||||||
|
ip_start += 3;
|
||||||
|
const char* ip_end = strchr(ip_start, ',');
|
||||||
|
if(ip_end) {
|
||||||
|
size_t len = ip_end - ip_start;
|
||||||
|
if(len < IP_BUFFER_SIZE) {
|
||||||
|
strncpy(stats->ip_address, ip_start, len);
|
||||||
|
stats->ip_address[len] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse total=
|
||||||
|
const char* total_start = strstr(data, "total=");
|
||||||
|
if(total_start) {
|
||||||
|
stats->total_frames = strtoul(total_start + 6, NULL, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pending=
|
||||||
|
const char* pending_start = strstr(data, "pending=");
|
||||||
|
if(pending_start) {
|
||||||
|
stats->pending_frames = strtoul(pending_start + 8, NULL, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse processed=
|
||||||
|
const char* processed_start = strstr(data, "processed=");
|
||||||
|
if(processed_start) {
|
||||||
|
stats->processed_frames = strtoul(processed_start + 10, NULL, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
stats->connected = true;
|
||||||
|
stats->last_update_tick = furi_get_tick();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UART receive callback
|
||||||
|
static void uart_rx_callback(
|
||||||
|
FuriHalSerialHandle* handle,
|
||||||
|
FuriHalSerialRxEvent event,
|
||||||
|
void* context) {
|
||||||
|
CanMonitorApp* app = context;
|
||||||
|
UNUSED(handle);
|
||||||
|
|
||||||
|
if(event == FuriHalSerialRxEventData) {
|
||||||
|
uint8_t data = furi_hal_serial_async_rx(handle);
|
||||||
|
furi_stream_buffer_send(app->rx_stream, &data, 1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UART receive thread
|
||||||
|
static int32_t uart_rx_thread(void* context) {
|
||||||
|
CanMonitorApp* app = context;
|
||||||
|
char rx_buffer[RX_BUFFER_SIZE];
|
||||||
|
size_t rx_index = 0;
|
||||||
|
|
||||||
|
while(app->running) {
|
||||||
|
uint8_t data;
|
||||||
|
size_t len = furi_stream_buffer_receive(app->rx_stream, &data, 1, 100);
|
||||||
|
|
||||||
|
if(len > 0) {
|
||||||
|
if(data == '\n' || data == '\r') {
|
||||||
|
if(rx_index > 0) {
|
||||||
|
rx_buffer[rx_index] = '\0';
|
||||||
|
|
||||||
|
// Parse the received line
|
||||||
|
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||||
|
if(parse_stats_line(rx_buffer, &app->stats)) {
|
||||||
|
// Notify view to redraw
|
||||||
|
view_port_update(app->view_port);
|
||||||
|
}
|
||||||
|
furi_mutex_release(app->mutex);
|
||||||
|
|
||||||
|
rx_index = 0;
|
||||||
|
}
|
||||||
|
} else if(rx_index < RX_BUFFER_SIZE - 1) {
|
||||||
|
rx_buffer[rx_index++] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check connection timeout (5 seconds)
|
||||||
|
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||||
|
if(app->stats.connected &&
|
||||||
|
(furi_get_tick() - app->stats.last_update_tick) > 5000) {
|
||||||
|
app->stats.connected = false;
|
||||||
|
view_port_update(app->view_port);
|
||||||
|
}
|
||||||
|
furi_mutex_release(app->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw callback
|
||||||
|
static void draw_callback(Canvas* canvas, void* context) {
|
||||||
|
CanMonitorApp* app = context;
|
||||||
|
|
||||||
|
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||||
|
|
||||||
|
canvas_clear(canvas);
|
||||||
|
canvas_set_font(canvas, FontPrimary);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, "CAN Monitor");
|
||||||
|
|
||||||
|
canvas_set_font(canvas, FontSecondary);
|
||||||
|
|
||||||
|
// Connection status
|
||||||
|
if(app->stats.connected) {
|
||||||
|
canvas_draw_str(canvas, 0, 14, "Status: Connected");
|
||||||
|
} else {
|
||||||
|
canvas_draw_str(canvas, 0, 14, "Status: Waiting...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP Address
|
||||||
|
char buf[64];
|
||||||
|
if(strlen(app->stats.ip_address) > 0) {
|
||||||
|
snprintf(buf, sizeof(buf), "IP: %s", app->stats.ip_address);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, sizeof(buf), "IP: ---");
|
||||||
|
}
|
||||||
|
canvas_draw_str(canvas, 0, 26, buf);
|
||||||
|
|
||||||
|
// Total frames
|
||||||
|
snprintf(buf, sizeof(buf), "Total: %lu", (unsigned long)app->stats.total_frames);
|
||||||
|
canvas_draw_str(canvas, 0, 38, buf);
|
||||||
|
|
||||||
|
// Pending frames
|
||||||
|
snprintf(buf, sizeof(buf), "Pending: %lu", (unsigned long)app->stats.pending_frames);
|
||||||
|
canvas_draw_str(canvas, 0, 50, buf);
|
||||||
|
|
||||||
|
// Processed frames
|
||||||
|
snprintf(buf, sizeof(buf), "Processed: %lu", (unsigned long)app->stats.processed_frames);
|
||||||
|
canvas_draw_str(canvas, 0, 62, buf);
|
||||||
|
|
||||||
|
furi_mutex_release(app->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input callback
|
||||||
|
static void input_callback(InputEvent* input_event, void* context) {
|
||||||
|
CanMonitorApp* app = context;
|
||||||
|
CanMonitorEvent event = {.type = EventTypeKey, .input = *input_event};
|
||||||
|
furi_message_queue_put(app->event_queue, &event, FuriWaitForever);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize application
|
||||||
|
static CanMonitorApp* can_monitor_app_alloc(void) {
|
||||||
|
CanMonitorApp* app = malloc(sizeof(CanMonitorApp));
|
||||||
|
memset(app, 0, sizeof(CanMonitorApp));
|
||||||
|
|
||||||
|
app->running = true;
|
||||||
|
app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||||
|
app->event_queue = furi_message_queue_alloc(8, sizeof(CanMonitorEvent));
|
||||||
|
|
||||||
|
// Initialize stats
|
||||||
|
memset(&app->stats, 0, sizeof(CanStats));
|
||||||
|
strcpy(app->stats.ip_address, "");
|
||||||
|
|
||||||
|
// Disable expansion protocol to use UART
|
||||||
|
Expansion* expansion = furi_record_open(RECORD_EXPANSION);
|
||||||
|
expansion_disable(expansion);
|
||||||
|
furi_record_close(RECORD_EXPANSION);
|
||||||
|
|
||||||
|
// Initialize UART
|
||||||
|
app->rx_stream = furi_stream_buffer_alloc(RX_BUFFER_SIZE, 1);
|
||||||
|
app->serial_handle = furi_hal_serial_control_acquire(UART_CH);
|
||||||
|
furi_hal_serial_init(app->serial_handle, UART_BAUD);
|
||||||
|
furi_hal_serial_async_rx_start(app->serial_handle, uart_rx_callback, app, false);
|
||||||
|
|
||||||
|
// Start RX thread
|
||||||
|
app->rx_thread = furi_thread_alloc_ex("CANMonitorRx", 1024, uart_rx_thread, app);
|
||||||
|
furi_thread_start(app->rx_thread);
|
||||||
|
|
||||||
|
// Initialize GUI
|
||||||
|
app->gui = furi_record_open(RECORD_GUI);
|
||||||
|
app->view_port = view_port_alloc();
|
||||||
|
view_port_draw_callback_set(app->view_port, draw_callback, app);
|
||||||
|
view_port_input_callback_set(app->view_port, input_callback, app);
|
||||||
|
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free application
|
||||||
|
static void can_monitor_app_free(CanMonitorApp* app) {
|
||||||
|
// Stop RX thread
|
||||||
|
app->running = false;
|
||||||
|
furi_thread_join(app->rx_thread);
|
||||||
|
furi_thread_free(app->rx_thread);
|
||||||
|
|
||||||
|
// Deinitialize UART
|
||||||
|
furi_hal_serial_async_rx_stop(app->serial_handle);
|
||||||
|
furi_hal_serial_deinit(app->serial_handle);
|
||||||
|
furi_hal_serial_control_release(app->serial_handle);
|
||||||
|
furi_stream_buffer_free(app->rx_stream);
|
||||||
|
|
||||||
|
// Re-enable expansion protocol
|
||||||
|
Expansion* expansion = furi_record_open(RECORD_EXPANSION);
|
||||||
|
expansion_enable(expansion);
|
||||||
|
furi_record_close(RECORD_EXPANSION);
|
||||||
|
|
||||||
|
// Free GUI
|
||||||
|
view_port_enabled_set(app->view_port, false);
|
||||||
|
gui_remove_view_port(app->gui, app->view_port);
|
||||||
|
view_port_free(app->view_port);
|
||||||
|
furi_record_close(RECORD_GUI);
|
||||||
|
|
||||||
|
furi_message_queue_free(app->event_queue);
|
||||||
|
furi_mutex_free(app->mutex);
|
||||||
|
|
||||||
|
free(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main application entry point
|
||||||
|
int32_t can_monitor_app(void* p) {
|
||||||
|
UNUSED(p);
|
||||||
|
|
||||||
|
CanMonitorApp* app = can_monitor_app_alloc();
|
||||||
|
|
||||||
|
CanMonitorEvent event;
|
||||||
|
bool running = true;
|
||||||
|
|
||||||
|
// Main event loop
|
||||||
|
while(running) {
|
||||||
|
if(furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
|
||||||
|
if(event.type == EventTypeKey) {
|
||||||
|
if(event.input.type == InputTypeShort ||
|
||||||
|
event.input.type == InputTypeLong) {
|
||||||
|
if(event.input.key == InputKeyBack) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
can_monitor_app_free(app);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user