Update flipper and can_sniffer

This commit is contained in:
2026-01-26 15:32:23 +03:00
parent 9044e96685
commit 9f38bbcf9d
2 changed files with 495 additions and 243 deletions

View File

@@ -1,8 +1,15 @@
""" """
Flipper Zero UART Handler. Flipper Zero UART Handler with Handshake Protocol.
Sends CAN sniffer statistics to Flipper Zero via UART. Waits for INIT command from Flipper Zero before sending statistics.
Provides real-time monitoring on Flipper Zero display. Supports handshake protocol for secure connection establishment.
Protocol:
1. RPI5 waits in passive mode, listening for commands
2. Flipper sends: INIT:flipper\n
3. RPI5 responds: ACK:rpi5,ip=x.x.x.x\n
4. RPI5 starts sending: STATS:ip=...,total=...,pending=...,processed=...\n
5. Flipper sends: STOP:flipper\n to disconnect
""" """
import socket import socket
@@ -26,10 +33,8 @@ def get_ip_address() -> str:
IP address string or "0.0.0.0" if unable to determine IP address string or "0.0.0.0" if unable to determine
""" """
try: try:
# Create a socket to determine the outgoing IP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(0.1) s.settimeout(0.1)
# Connect to a public address (doesn't actually send data)
s.connect(("8.8.8.8", 80)) s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0] ip = s.getsockname()[0]
s.close() s.close()
@@ -37,7 +42,6 @@ def get_ip_address() -> str:
except Exception: except Exception:
pass pass
# Fallback: try to get any non-localhost IP
try: try:
hostname = socket.gethostname() hostname = socket.gethostname()
ip = socket.gethostbyname(hostname) ip = socket.gethostbyname(hostname)
@@ -51,15 +55,18 @@ def get_ip_address() -> str:
class FlipperHandler(BaseHandler): class FlipperHandler(BaseHandler):
""" """
Handler that sends statistics to Flipper Zero via UART. Handler that communicates with Flipper Zero via UART.
Implements handshake protocol:
- Waits for INIT:flipper command
- Responds with ACK:rpi5,ip=x.x.x.x
- Sends STATS periodically while connected
- Stops on STOP:flipper command
UART Configuration: UART Configuration:
- Device: /dev/ttyAMA0 (or configured device) - Device: /dev/ttyAMA0 (or configured device)
- Baud: 115200 - Baud: 115200
- Format: 8N1 - Format: 8N1
Protocol:
Sends text line: STATS:ip=<ip>,total=<n>,pending=<n>,processed=<n>\n
""" """
def __init__(self, enabled: Optional[bool] = None): def __init__(self, enabled: Optional[bool] = None):
@@ -69,7 +76,6 @@ class FlipperHandler(BaseHandler):
Args: Args:
enabled: Whether handler is enabled. If None, reads from config. enabled: Whether handler is enabled. If None, reads from config.
""" """
# Check config for enabled status
if enabled is None: if enabled is None:
enabled = getattr(config, "flipper", None) is not None enabled = getattr(config, "flipper", None) is not None
if enabled: if enabled:
@@ -80,15 +86,18 @@ class FlipperHandler(BaseHandler):
self.serial_port: Optional[Any] = None self.serial_port: Optional[Any] = None
self.device = "/dev/ttyAMA0" self.device = "/dev/ttyAMA0"
self.baudrate = 115200 self.baudrate = 115200
self.send_interval = 1.0 # Send stats every 1 second self.send_interval = 1.0
# Load config if available
if hasattr(config, "flipper"): if hasattr(config, "flipper"):
flipper_cfg = config.flipper flipper_cfg = config.flipper
self.device = getattr(flipper_cfg, "device", self.device) self.device = getattr(flipper_cfg, "device", self.device)
self.baudrate = getattr(flipper_cfg, "baudrate", self.baudrate) self.baudrate = getattr(flipper_cfg, "baudrate", self.baudrate)
self.send_interval = getattr(flipper_cfg, "send_interval", self.send_interval) self.send_interval = getattr(flipper_cfg, "send_interval", self.send_interval)
# Connection state
self._connected = False
self._running = False
# Statistics # Statistics
self._stats_lock = threading.Lock() self._stats_lock = threading.Lock()
self._total_frames = 0 self._total_frames = 0
@@ -97,17 +106,16 @@ class FlipperHandler(BaseHandler):
self._sent_count = 0 self._sent_count = 0
self._error_count = 0 self._error_count = 0
# Background sender thread # Threads
self._sender_thread: Optional[threading.Thread] = None self._rx_thread: Optional[threading.Thread] = None
self._running = False self._tx_thread: Optional[threading.Thread] = None
# IP address cache # IP address
self._ip_address = "0.0.0.0" self._ip_address = "0.0.0.0"
self._last_ip_check = 0
def initialize(self) -> bool: def initialize(self) -> bool:
""" """
Initialize UART connection to Flipper Zero. Initialize UART connection.
Returns: Returns:
True if initialization successful True if initialization successful
@@ -124,13 +132,12 @@ class FlipperHandler(BaseHandler):
timeout=0.1, timeout=0.1,
) )
# Get initial IP address
self._ip_address = get_ip_address() self._ip_address = get_ip_address()
self._last_ip_check = time.time()
self._initialized = True self._initialized = True
self.logger.info( self.logger.info(
f"Flipper handler initialized on {self.device} @ {self.baudrate} baud" f"Flipper handler initialized on {self.device} @ {self.baudrate} baud, "
f"IP: {self._ip_address}"
) )
return True return True
@@ -142,71 +149,145 @@ class FlipperHandler(BaseHandler):
return False return False
def start(self) -> None: def start(self) -> None:
"""Start the background sender thread.""" """Start the RX listener and TX sender threads."""
if self._running: if self._running:
return return
self._running = True self._running = True
self._sender_thread = threading.Thread( self._connected = False
target=self._sender_loop, name="FlipperSender", daemon=True
) # Start RX thread (listens for commands)
self._sender_thread.start() self._rx_thread = threading.Thread(
self.logger.info("Flipper sender thread started") target=self._rx_loop, name="FlipperRX", daemon=True
)
self._rx_thread.start()
# Start TX thread (sends stats when connected)
self._tx_thread = threading.Thread(
target=self._tx_loop, name="FlipperTX", daemon=True
)
self._tx_thread.start()
self.logger.info("Flipper handler started, waiting for connection...")
def _rx_loop(self) -> None:
"""Receive loop - listens for commands from Flipper."""
buffer = ""
def _sender_loop(self) -> None:
"""Background loop that sends stats periodically."""
while self._running: while self._running:
try: try:
self._send_stats() if not self.serial_port or not self.serial_port.is_open:
time.sleep(0.1)
continue
# Read available data
if self.serial_port.in_waiting > 0:
data = self.serial_port.read(self.serial_port.in_waiting)
buffer += data.decode("utf-8", errors="ignore")
# Process complete lines
while "\n" in buffer:
line, buffer = buffer.split("\n", 1)
line = line.strip()
if line:
self._process_command(line)
else:
time.sleep(0.05)
except Exception as e: except Exception as e:
self.logger.debug(f"Error sending stats to Flipper: {e}") self.logger.debug(f"RX error: {e}")
with self._stats_lock: time.sleep(0.1)
self._error_count += 1
def _process_command(self, command: str) -> None:
"""
Process received command from Flipper.
Args:
command: Received command string
"""
self.logger.info(f"Received command: {command}")
if command.startswith("INIT:"):
# Handshake initiation
client_id = command[5:].strip()
self.logger.info(f"Handshake request from: {client_id}")
# Send ACK with IP address
self._ip_address = get_ip_address()
ack_msg = f"ACK:rpi5,ip={self._ip_address}\n"
self._send_raw(ack_msg)
self._connected = True
self.logger.info(f"Connected to Flipper, IP: {self._ip_address}")
elif command.startswith("STOP:"):
# Disconnect request
client_id = command[5:].strip()
self.logger.info(f"Disconnect request from: {client_id}")
self._connected = False
self.logger.info("Disconnected from Flipper")
def _tx_loop(self) -> None:
"""Transmit loop - sends stats when connected."""
while self._running:
try:
if self._connected:
self._send_stats()
time.sleep(self.send_interval) time.sleep(self.send_interval)
except Exception as e:
self.logger.debug(f"TX error: {e}")
with self._stats_lock:
self._error_count += 1
def _send_raw(self, message: str) -> bool:
"""
Send raw message via UART.
Args:
message: Message to send
Returns:
True if sent successfully
"""
if not self.serial_port or not self.serial_port.is_open:
return False
try:
self.serial_port.write(message.encode("utf-8"))
self.serial_port.flush()
self.logger.debug(f"TX: {message.strip()}")
return True
except Exception as e:
self.logger.debug(f"Send error: {e}")
return False
def _send_stats(self) -> None: def _send_stats(self) -> None:
"""Send current statistics to Flipper Zero.""" """Send current statistics to Flipper Zero."""
if not self.serial_port or not self.serial_port.is_open: if not self._connected:
return 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: with self._stats_lock:
total = self._total_frames total = self._total_frames
pending = self._pending_frames pending = self._pending_frames
processed = self._processed_frames processed = self._processed_frames
# Build stats message
message = f"STATS:ip={self._ip_address},total={total},pending={pending},processed={processed}\n" message = f"STATS:ip={self._ip_address},total={total},pending={pending},processed={processed}\n"
try: if self._send_raw(message):
self.serial_port.write(message.encode("utf-8"))
self.serial_port.flush()
with self._stats_lock: with self._stats_lock:
self._sent_count += 1 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: def handle(self, frame: CANFrame) -> bool:
""" """
Handle a single CAN frame. Handle a single CAN frame.
Updates frame counters for statistics.
Args: Args:
frame: CANFrame to handle frame: CANFrame to handle
Returns: Returns:
True (always succeeds, just updates counters) True (always succeeds)
""" """
with self._stats_lock: with self._stats_lock:
self._total_frames += 1 self._total_frames += 1
@@ -221,14 +302,12 @@ class FlipperHandler(BaseHandler):
frames: List of CANFrame objects frames: List of CANFrame objects
Returns: Returns:
Number of frames processed (all of them) Number of frames processed
""" """
count = len(frames) count = len(frames)
with self._stats_lock: with self._stats_lock:
self._total_frames += count self._total_frames += count
# After batch processing, frames are processed
self._processed_frames += count self._processed_frames += count
# Reduce pending by batch count
self._pending_frames = max(0, self._pending_frames - count) self._pending_frames = max(0, self._pending_frames - count)
return count return count
@@ -236,8 +315,6 @@ class FlipperHandler(BaseHandler):
""" """
Update pending frame count. Update pending frame count.
Called externally to sync with actual queue size.
Args: Args:
pending_count: Current number of pending frames pending_count: Current number of pending frames
""" """
@@ -245,26 +322,30 @@ class FlipperHandler(BaseHandler):
self._pending_frames = pending_count self._pending_frames = pending_count
def flush(self) -> None: def flush(self) -> None:
"""Flush - send immediate stats update.""" """Flush - send immediate stats if connected."""
if self._connected:
try: try:
self._send_stats() self._send_stats()
except Exception as e: except Exception as e:
self.logger.debug(f"Error in flush: {e}") self.logger.debug(f"Flush error: {e}")
def shutdown(self) -> None: def shutdown(self) -> None:
"""Shutdown the handler.""" """Shutdown the handler."""
self.logger.info("Shutting down Flipper handler...") self.logger.info("Shutting down Flipper handler...")
self._running = False self._running = False
self._connected = False
if self._sender_thread and self._sender_thread.is_alive(): # Wait for threads
self._sender_thread.join(timeout=2.0) if self._rx_thread and self._rx_thread.is_alive():
self._rx_thread.join(timeout=2.0)
if self._tx_thread and self._tx_thread.is_alive():
self._tx_thread.join(timeout=2.0)
# Close serial port
if self.serial_port and self.serial_port.is_open: if self.serial_port and self.serial_port.is_open:
try: 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() self.serial_port.close()
except Exception as e: except Exception as e:
self.logger.debug(f"Error closing serial port: {e}") self.logger.debug(f"Error closing serial port: {e}")
@@ -288,6 +369,10 @@ class FlipperHandler(BaseHandler):
"error_count": self._error_count, "error_count": self._error_count,
"device": self.device, "device": self.device,
"baudrate": self.baudrate, "baudrate": self.baudrate,
"connected": self.serial_port.is_open if self.serial_port else False, "connected": self._connected,
"ip_address": self._ip_address, "ip_address": self._ip_address,
} }
def is_connected(self) -> bool:
"""Check if Flipper is connected."""
return self._connected

View File

@@ -1,206 +1,325 @@
/** /**
* CAN Monitor for Flipper Zero * CAN Monitor for Flipper Zero
* *
* Receives CAN sniffer statistics from RPI5 via UART. * Multi-page application with handshake protocol.
* Displays: IP address, total frames, pending frames, processed frames.
* *
* UART Configuration: * Handshake:
* - TX: GPIO 13 (pin 13) * 1. User presses OK on welcome screen
* - RX: GPIO 14 (pin 14) * 2. Flipper sends: INIT:flipper\n
* - Baud: 115200 * 3. RPI5 responds: ACK:rpi5,ip=x.x.x.x\n
* - 8N1 * 4. Flipper allows navigation to stats page
* 5. RPI5 starts sending: STATS:ip=...,total=...,pending=...,processed=...\n
* *
* Protocol: Text-based * Wiring:
* Format: STATS:ip=<ip>,total=<n>,pending=<n>,processed=<n>\n * RPI5 TX (GPIO14, Pin 8) -> Flipper RX (Pin 14)
* Example: STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245\n * RPI5 RX (GPIO15, Pin 10) <- Flipper TX (Pin 13)
* RPI5 GND (Pin 6) -> Flipper GND (Pin 8/11/18)
*/ */
#include <furi.h> #include <furi.h>
#include <furi_hal.h> #include <furi_hal.h>
#include <gui/gui.h> #include <gui/gui.h>
#include <gui/view_port.h> #include <gui/view_port.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <expansion/expansion.h> #include <expansion/expansion.h>
#include <furi_hal_serial.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#define TAG "CANMonitor" #define TAG "CANMonitor"
// UART configuration
#define UART_CH FuriHalSerialIdUsart
#define UART_BAUD 115200 #define UART_BAUD 115200
// Buffer sizes
#define RX_BUFFER_SIZE 256 #define RX_BUFFER_SIZE 256
#define IP_BUFFER_SIZE 32 #define IP_BUFFER_SIZE 32
// Statistics structure // Connection state
typedef enum {
StateDisconnected,
StateConnecting,
StateConnected,
} ConnectionState;
// Pages
typedef enum {
PageWelcome,
PageStats,
} AppPage;
// Statistics
typedef struct { typedef struct {
char ip_address[IP_BUFFER_SIZE]; char ip_address[IP_BUFFER_SIZE];
uint32_t total_frames; uint32_t total_frames;
uint32_t pending_frames; uint32_t pending_frames;
uint32_t processed_frames; uint32_t processed_frames;
bool connected; bool data_received;
uint32_t last_update_tick; uint32_t last_update_tick;
} CanStats; } CanStats;
// Application context // App context
typedef struct { typedef struct {
Gui* gui; Gui* gui;
ViewPort* view_port; ViewPort* view_port;
FuriMessageQueue* event_queue;
FuriMutex* mutex; FuriMutex* mutex;
FuriHalSerialHandle* serial_handle; FuriHalSerialHandle* serial;
FuriStreamBuffer* rx_stream; FuriStreamBuffer* rx_stream;
FuriThread* rx_thread; FuriThread* worker_thread;
CanStats stats; CanStats stats;
bool running; AppPage current_page;
ConnectionState conn_state;
volatile bool running;
volatile bool send_init;
} CanMonitorApp; } CanMonitorApp;
// Event types // Send data via UART
typedef enum { static void uart_send(CanMonitorApp* app, const char* data) {
EventTypeKey, if(app->serial) {
EventTypeStats, furi_hal_serial_tx(app->serial, (uint8_t*)data, strlen(data));
} EventType; FURI_LOG_I(TAG, "TX: %s", data);
}
}
typedef struct { // Parse ACK response: ACK:rpi5,ip=x.x.x.x
EventType type; static bool parse_ack(const char* line, CanStats* stats) {
InputEvent input; if(strncmp(line, "ACK:", 4) != 0) {
} CanMonitorEvent; return false;
}
// Parse statistics from received line const char* p = line + 4;
static bool parse_stats_line(const char* line, CanStats* stats) {
// Format: STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245 // Check for rpi5 identifier
if(strncmp(p, "rpi5", 4) != 0) {
return false;
}
// Parse ip=
const char* ip = strstr(p, "ip=");
if(ip) {
ip += 3;
// Find end (comma, newline, or end of string)
size_t len = 0;
while(ip[len] && ip[len] != ',' && ip[len] != '\n' && ip[len] != '\r') {
len++;
}
if(len > 0 && len < IP_BUFFER_SIZE) {
memcpy(stats->ip_address, ip, len);
stats->ip_address[len] = '\0';
}
}
return true;
}
// Parse STATS: STATS:ip=x.x.x.x,total=N,pending=N,processed=N
static bool parse_stats(const char* line, CanStats* stats) {
if(strncmp(line, "STATS:", 6) != 0) { if(strncmp(line, "STATS:", 6) != 0) {
return false; return false;
} }
const char* data = line + 6; const char* p = line + 6;
// Parse ip= // Parse ip=
const char* ip_start = strstr(data, "ip="); const char* ip = strstr(p, "ip=");
if(ip_start) { if(ip) {
ip_start += 3; ip += 3;
const char* ip_end = strchr(ip_start, ','); const char* end = strchr(ip, ',');
if(ip_end) { if(end && (size_t)(end - ip) < IP_BUFFER_SIZE) {
size_t len = ip_end - ip_start; size_t len = end - ip;
if(len < IP_BUFFER_SIZE) { memcpy(stats->ip_address, ip, len);
strncpy(stats->ip_address, ip_start, len);
stats->ip_address[len] = '\0'; stats->ip_address[len] = '\0';
} }
} }
}
// Parse total= // Parse total=
const char* total_start = strstr(data, "total="); const char* total = strstr(p, "total=");
if(total_start) { if(total) {
stats->total_frames = strtoul(total_start + 6, NULL, 10); stats->total_frames = strtoul(total + 6, NULL, 10);
} }
// Parse pending= // Parse pending=
const char* pending_start = strstr(data, "pending="); const char* pending = strstr(p, "pending=");
if(pending_start) { if(pending) {
stats->pending_frames = strtoul(pending_start + 8, NULL, 10); stats->pending_frames = strtoul(pending + 8, NULL, 10);
} }
// Parse processed= // Parse processed=
const char* processed_start = strstr(data, "processed="); const char* processed = strstr(p, "processed=");
if(processed_start) { if(processed) {
stats->processed_frames = strtoul(processed_start + 10, NULL, 10); stats->processed_frames = strtoul(processed + 10, NULL, 10);
} }
stats->connected = true; stats->data_received = true;
stats->last_update_tick = furi_get_tick(); stats->last_update_tick = furi_get_tick();
return true; return true;
} }
// UART receive callback // Process received line
static void uart_rx_callback( static void process_line(CanMonitorApp* app, const char* line) {
FuriHalSerialHandle* handle, FURI_LOG_I(TAG, "RX: %s", line);
FuriHalSerialRxEvent event,
void* context) { furi_mutex_acquire(app->mutex, FuriWaitForever);
CanMonitorApp* app = context;
UNUSED(handle); if(app->conn_state == StateConnecting) {
// Waiting for ACK
if(parse_ack(line, &app->stats)) {
FURI_LOG_I(TAG, "ACK received, IP: %s", app->stats.ip_address);
app->conn_state = StateConnected;
}
} else if(app->conn_state == StateConnected) {
// Parse stats
parse_stats(line, &app->stats);
}
furi_mutex_release(app->mutex);
view_port_update(app->view_port);
}
// UART RX callback
static void uart_callback(FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* ctx) {
CanMonitorApp* app = ctx;
if(event == FuriHalSerialRxEventData) { if(event == FuriHalSerialRxEventData) {
uint8_t data = furi_hal_serial_async_rx(handle); uint8_t byte = furi_hal_serial_async_rx(handle);
furi_stream_buffer_send(app->rx_stream, &data, 1, 0); furi_stream_buffer_send(app->rx_stream, &byte, 1, 0);
} }
} }
// UART receive thread // Worker thread
static int32_t uart_rx_thread(void* context) { static int32_t worker_thread(void* ctx) {
CanMonitorApp* app = context; CanMonitorApp* app = ctx;
char rx_buffer[RX_BUFFER_SIZE]; char buffer[RX_BUFFER_SIZE];
size_t rx_index = 0; size_t idx = 0;
uint32_t last_rx_tick = 0;
FURI_LOG_I(TAG, "Worker started");
while(app->running) { while(app->running) {
uint8_t data; // Check if we need to send INIT
size_t len = furi_stream_buffer_receive(app->rx_stream, &data, 1, 100); if(app->send_init) {
app->send_init = false;
uart_send(app, "INIT:flipper\n");
furi_mutex_acquire(app->mutex, FuriWaitForever);
app->conn_state = StateConnecting;
furi_mutex_release(app->mutex);
view_port_update(app->view_port);
}
// Receive data
uint8_t byte;
size_t len = furi_stream_buffer_receive(app->rx_stream, &byte, 1, 50);
if(len > 0) { if(len > 0) {
if(data == '\n' || data == '\r') { last_rx_tick = furi_get_tick();
if(rx_index > 0) {
rx_buffer[rx_index] = '\0';
// Parse the received line if(byte == '\n' || byte == '\r') {
if(idx > 0) {
buffer[idx] = '\0';
process_line(app, buffer);
idx = 0;
}
} else if(idx < RX_BUFFER_SIZE - 1) {
buffer[idx++] = byte;
}
} else {
// Timeout: parse partial data after 500ms
if(idx > 0 && (furi_get_tick() - last_rx_tick) > 500) {
buffer[idx] = '\0';
process_line(app, buffer);
idx = 0;
}
}
// Check data timeout (5 sec) - only when connected
furi_mutex_acquire(app->mutex, FuriWaitForever); furi_mutex_acquire(app->mutex, FuriWaitForever);
if(parse_stats_line(rx_buffer, &app->stats)) { if(app->conn_state == StateConnected && app->stats.data_received) {
// Notify view to redraw if((furi_get_tick() - app->stats.last_update_tick) > 5000) {
app->stats.data_received = false;
view_port_update(app->view_port); 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); furi_mutex_release(app->mutex);
} }
FURI_LOG_I(TAG, "Worker stopped");
return 0; return 0;
} }
// Draw callback // Draw welcome page
static void draw_callback(Canvas* canvas, void* context) { static void draw_welcome(Canvas* canvas, CanMonitorApp* app) {
CanMonitorApp* app = context;
furi_mutex_acquire(app->mutex, FuriWaitForever);
canvas_clear(canvas); canvas_clear(canvas);
canvas_set_font(canvas, FontPrimary);
// Title // Title
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, "CAN Monitor"); canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 64, 8, AlignCenter, AlignCenter, "CAN Monitor");
canvas_set_font(canvas, FontSecondary); canvas_set_font(canvas, FontSecondary);
// Connection status // Subtitle
if(app->stats.connected) { canvas_draw_str_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "RPI5 CAN Sniffer");
canvas_draw_str(canvas, 0, 14, "Status: Connected");
// Status based on connection state
furi_mutex_acquire(app->mutex, FuriWaitForever);
ConnectionState state = app->conn_state;
char ip_buf[IP_BUFFER_SIZE];
strncpy(ip_buf, app->stats.ip_address, IP_BUFFER_SIZE);
furi_mutex_release(app->mutex);
switch(state) {
case StateDisconnected:
canvas_draw_str_aligned(canvas, 64, 38, AlignCenter, AlignCenter, "Press OK to connect");
canvas_draw_str_aligned(canvas, 64, 50, AlignCenter, AlignCenter, "[OK]");
break;
case StateConnecting:
canvas_draw_str_aligned(canvas, 64, 38, AlignCenter, AlignCenter, "Connecting...");
canvas_draw_str_aligned(canvas, 64, 50, AlignCenter, AlignCenter, "Waiting for RPI5");
break;
case StateConnected:
canvas_draw_str_aligned(canvas, 64, 34, AlignCenter, AlignCenter, "Connected!");
if(strlen(ip_buf) > 0) {
char buf[48];
snprintf(buf, sizeof(buf), "RPI5: %s", ip_buf);
canvas_draw_str_aligned(canvas, 64, 46, AlignCenter, AlignCenter, buf);
}
canvas_draw_str_aligned(canvas, 64, 58, AlignCenter, AlignCenter, "Press RIGHT >");
break;
}
}
// Draw stats page
static void draw_stats(Canvas* canvas, CanMonitorApp* app) {
char buf[64];
canvas_clear(canvas);
// Header
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 10, 10, "CAN Monitor");
// Navigation hint
canvas_draw_str(canvas, 0, 10, "<");
// Connection indicator
furi_mutex_acquire(app->mutex, FuriWaitForever);
bool data_ok = app->stats.data_received;
furi_mutex_release(app->mutex);
if(data_ok) {
canvas_draw_disc(canvas, 120, 6, 4);
} else { } else {
canvas_draw_str(canvas, 0, 14, "Status: Waiting..."); canvas_draw_circle(canvas, 120, 6, 4);
} }
// Separator
canvas_draw_line(canvas, 0, 14, 128, 14);
canvas_set_font(canvas, FontSecondary);
furi_mutex_acquire(app->mutex, FuriWaitForever);
// IP Address // IP Address
char buf[64];
if(strlen(app->stats.ip_address) > 0) { if(strlen(app->stats.ip_address) > 0) {
snprintf(buf, sizeof(buf), "IP: %s", app->stats.ip_address); snprintf(buf, sizeof(buf), "IP: %s", app->stats.ip_address);
} else { } else {
@@ -209,7 +328,7 @@ static void draw_callback(Canvas* canvas, void* context) {
canvas_draw_str(canvas, 0, 26, buf); canvas_draw_str(canvas, 0, 26, buf);
// Total frames // Total frames
snprintf(buf, sizeof(buf), "Total: %lu", (unsigned long)app->stats.total_frames); snprintf(buf, sizeof(buf), "Total frames: %lu", (unsigned long)app->stats.total_frames);
canvas_draw_str(canvas, 0, 38, buf); canvas_draw_str(canvas, 0, 38, buf);
// Pending frames // Pending frames
@@ -218,110 +337,158 @@ static void draw_callback(Canvas* canvas, void* context) {
// Processed frames // Processed frames
snprintf(buf, sizeof(buf), "Processed: %lu", (unsigned long)app->stats.processed_frames); snprintf(buf, sizeof(buf), "Processed: %lu", (unsigned long)app->stats.processed_frames);
canvas_draw_str(canvas, 0, 62, buf); canvas_draw_str(canvas, 4, 62, buf);
furi_mutex_release(app->mutex); furi_mutex_release(app->mutex);
} }
// Input callback // Draw callback
static void input_callback(InputEvent* input_event, void* context) { static void draw_callback(Canvas* canvas, void* ctx) {
CanMonitorApp* app = context; CanMonitorApp* app = ctx;
CanMonitorEvent event = {.type = EventTypeKey, .input = *input_event};
furi_message_queue_put(app->event_queue, &event, FuriWaitForever); switch(app->current_page) {
case PageWelcome:
draw_welcome(canvas, app);
break;
case PageStats:
draw_stats(canvas, app);
break;
}
} }
// Initialize application // Input callback
static CanMonitorApp* can_monitor_app_alloc(void) { static void input_callback(InputEvent* event, void* ctx) {
CanMonitorApp* app = ctx;
if(event->type == InputTypeShort) {
furi_mutex_acquire(app->mutex, FuriWaitForever);
switch(event->key) {
case InputKeyOk:
if(app->current_page == PageWelcome && app->conn_state == StateDisconnected) {
// Start handshake
app->send_init = true;
}
break;
case InputKeyRight:
// Only allow if connected
if(app->current_page == PageWelcome && app->conn_state == StateConnected) {
app->current_page = PageStats;
view_port_update(app->view_port);
}
break;
case InputKeyLeft:
if(app->current_page == PageStats) {
app->current_page = PageWelcome;
view_port_update(app->view_port);
}
break;
case InputKeyBack:
if(app->current_page == PageWelcome) {
app->running = false;
} else {
app->current_page = PageWelcome;
view_port_update(app->view_port);
}
break;
default:
break;
}
furi_mutex_release(app->mutex);
}
}
// Main entry point
int32_t can_monitor_app(void* p) {
UNUSED(p);
FURI_LOG_I(TAG, "Starting CAN Monitor");
// Allocate app
CanMonitorApp* app = malloc(sizeof(CanMonitorApp)); CanMonitorApp* app = malloc(sizeof(CanMonitorApp));
memset(app, 0, sizeof(CanMonitorApp)); memset(app, 0, sizeof(CanMonitorApp));
app->running = true; app->running = true;
app->current_page = PageWelcome;
app->conn_state = StateDisconnected;
app->send_init = false;
// Mutex
app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
app->event_queue = furi_message_queue_alloc(8, sizeof(CanMonitorEvent));
// Initialize stats // Disable expansion protocol
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* expansion = furi_record_open(RECORD_EXPANSION);
expansion_disable(expansion); expansion_disable(expansion);
furi_record_close(RECORD_EXPANSION); furi_record_close(RECORD_EXPANSION);
// Initialize UART FURI_LOG_I(TAG, "Expansion disabled");
// Init UART
app->rx_stream = furi_stream_buffer_alloc(RX_BUFFER_SIZE, 1); app->rx_stream = furi_stream_buffer_alloc(RX_BUFFER_SIZE, 1);
app->serial_handle = furi_hal_serial_control_acquire(UART_CH); app->serial = furi_hal_serial_control_acquire(FuriHalSerialIdUsart);
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 if(app->serial) {
app->rx_thread = furi_thread_alloc_ex("CANMonitorRx", 1024, uart_rx_thread, app); furi_hal_serial_init(app->serial, UART_BAUD);
furi_thread_start(app->rx_thread); furi_hal_serial_async_rx_start(app->serial, uart_callback, app, false);
FURI_LOG_I(TAG, "UART initialized at %d baud", UART_BAUD);
} else {
FURI_LOG_E(TAG, "Failed to acquire UART");
}
// Initialize GUI // Start worker
app->worker_thread = furi_thread_alloc_ex("CanMonitorWorker", 1024, worker_thread, app);
furi_thread_start(app->worker_thread);
// Init GUI
app->gui = furi_record_open(RECORD_GUI); app->gui = furi_record_open(RECORD_GUI);
app->view_port = view_port_alloc(); app->view_port = view_port_alloc();
view_port_draw_callback_set(app->view_port, draw_callback, app); view_port_draw_callback_set(app->view_port, draw_callback, app);
view_port_input_callback_set(app->view_port, input_callback, app); view_port_input_callback_set(app->view_port, input_callback, app);
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
return app; FURI_LOG_I(TAG, "GUI initialized, waiting for user input");
}
// Free application // Main loop
static void can_monitor_app_free(CanMonitorApp* app) { while(app->running) {
// Stop RX thread furi_delay_ms(100);
app->running = false; }
furi_thread_join(app->rx_thread);
furi_thread_free(app->rx_thread);
// Deinitialize UART FURI_LOG_I(TAG, "Shutting down");
furi_hal_serial_async_rx_stop(app->serial_handle);
furi_hal_serial_deinit(app->serial_handle); // Send disconnect signal
furi_hal_serial_control_release(app->serial_handle); if(app->conn_state == StateConnected) {
uart_send(app, "STOP:flipper\n");
}
// Cleanup
furi_thread_join(app->worker_thread);
furi_thread_free(app->worker_thread);
if(app->serial) {
furi_hal_serial_async_rx_stop(app->serial);
furi_hal_serial_deinit(app->serial);
furi_hal_serial_control_release(app->serial);
}
furi_stream_buffer_free(app->rx_stream); furi_stream_buffer_free(app->rx_stream);
// Re-enable expansion protocol // Re-enable expansion
Expansion* expansion = furi_record_open(RECORD_EXPANSION); expansion = furi_record_open(RECORD_EXPANSION);
expansion_enable(expansion); expansion_enable(expansion);
furi_record_close(RECORD_EXPANSION); furi_record_close(RECORD_EXPANSION);
// Free GUI
view_port_enabled_set(app->view_port, false); view_port_enabled_set(app->view_port, false);
gui_remove_view_port(app->gui, app->view_port); gui_remove_view_port(app->gui, app->view_port);
view_port_free(app->view_port); view_port_free(app->view_port);
furi_record_close(RECORD_GUI); furi_record_close(RECORD_GUI);
furi_message_queue_free(app->event_queue);
furi_mutex_free(app->mutex); furi_mutex_free(app->mutex);
free(app); free(app);
}
// Main application entry point FURI_LOG_I(TAG, "Bye!");
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; return 0;
} }