diff --git a/can_sniffer/src/handlers/flipper_handler.py b/can_sniffer/src/handlers/flipper_handler.py index 06eae2d..b4096a0 100644 --- a/can_sniffer/src/handlers/flipper_handler.py +++ b/can_sniffer/src/handlers/flipper_handler.py @@ -1,8 +1,15 @@ """ -Flipper Zero UART Handler. +Flipper Zero UART Handler with Handshake Protocol. -Sends CAN sniffer statistics to Flipper Zero via UART. -Provides real-time monitoring on Flipper Zero display. +Waits for INIT command from Flipper Zero before sending statistics. +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 @@ -26,10 +33,8 @@ def get_ip_address() -> str: 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() @@ -37,7 +42,6 @@ def get_ip_address() -> str: except Exception: pass - # Fallback: try to get any non-localhost IP try: hostname = socket.gethostname() ip = socket.gethostbyname(hostname) @@ -51,15 +55,18 @@ def get_ip_address() -> str: 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: - Device: /dev/ttyAMA0 (or configured device) - Baud: 115200 - Format: 8N1 - - Protocol: - Sends text line: STATS:ip=,total=,pending=,processed=\n """ def __init__(self, enabled: Optional[bool] = None): @@ -69,7 +76,6 @@ class FlipperHandler(BaseHandler): 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: @@ -80,15 +86,18 @@ class FlipperHandler(BaseHandler): self.serial_port: Optional[Any] = None self.device = "/dev/ttyAMA0" 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"): 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) + # Connection state + self._connected = False + self._running = False + # Statistics self._stats_lock = threading.Lock() self._total_frames = 0 @@ -97,17 +106,16 @@ class FlipperHandler(BaseHandler): self._sent_count = 0 self._error_count = 0 - # Background sender thread - self._sender_thread: Optional[threading.Thread] = None - self._running = False + # Threads + self._rx_thread: Optional[threading.Thread] = None + self._tx_thread: Optional[threading.Thread] = None - # IP address cache + # IP address self._ip_address = "0.0.0.0" - self._last_ip_check = 0 def initialize(self) -> bool: """ - Initialize UART connection to Flipper Zero. + Initialize UART connection. Returns: True if initialization successful @@ -124,13 +132,12 @@ class FlipperHandler(BaseHandler): 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" + f"Flipper handler initialized on {self.device} @ {self.baudrate} baud, " + f"IP: {self._ip_address}" ) return True @@ -142,71 +149,145 @@ class FlipperHandler(BaseHandler): return False def start(self) -> None: - """Start the background sender thread.""" + """Start the RX listener and TX sender threads.""" 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") + self._connected = False + + # Start RX thread (listens for commands) + self._rx_thread = threading.Thread( + 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: 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: - self.logger.debug(f"Error sending stats to Flipper: {e}") + self.logger.debug(f"RX error: {e}") + time.sleep(0.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) + + except Exception as e: + self.logger.debug(f"TX error: {e}") with self._stats_lock: self._error_count += 1 - time.sleep(self.send_interval) + 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: """Send current statistics to Flipper Zero.""" - if not self.serial_port or not self.serial_port.is_open: + if not self._connected: 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() - + if self._send_raw(message): 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) + True (always succeeds) """ with self._stats_lock: self._total_frames += 1 @@ -221,14 +302,12 @@ class FlipperHandler(BaseHandler): frames: List of CANFrame objects Returns: - Number of frames processed (all of them) + Number of frames processed """ 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 @@ -236,8 +315,6 @@ class FlipperHandler(BaseHandler): """ Update pending frame count. - Called externally to sync with actual queue size. - Args: pending_count: Current number of pending frames """ @@ -245,26 +322,30 @@ class FlipperHandler(BaseHandler): 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}") + """Flush - send immediate stats if connected.""" + if self._connected: + try: + self._send_stats() + except Exception as e: + self.logger.debug(f"Flush error: {e}") def shutdown(self) -> None: """Shutdown the handler.""" self.logger.info("Shutting down Flipper handler...") self._running = False + self._connected = False - if self._sender_thread and self._sender_thread.is_alive(): - self._sender_thread.join(timeout=2.0) + # Wait for threads + 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: 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}") @@ -288,6 +369,10 @@ class FlipperHandler(BaseHandler): "error_count": self._error_count, "device": self.device, "baudrate": self.baudrate, - "connected": self.serial_port.is_open if self.serial_port else False, + "connected": self._connected, "ip_address": self._ip_address, } + + def is_connected(self) -> bool: + """Check if Flipper is connected.""" + return self._connected diff --git a/flip_monitor/can_monitor.c b/flip_monitor/can_monitor.c index 13341ac..d5dc3f6 100644 --- a/flip_monitor/can_monitor.c +++ b/flip_monitor/can_monitor.c @@ -1,206 +1,325 @@ /** * CAN Monitor for Flipper Zero * - * Receives CAN sniffer statistics from RPI5 via UART. - * Displays: IP address, total frames, pending frames, processed frames. + * Multi-page application with handshake protocol. * - * UART Configuration: - * - TX: GPIO 13 (pin 13) - * - RX: GPIO 14 (pin 14) - * - Baud: 115200 - * - 8N1 + * Handshake: + * 1. User presses OK on welcome screen + * 2. Flipper sends: INIT:flipper\n + * 3. RPI5 responds: ACK:rpi5,ip=x.x.x.x\n + * 4. Flipper allows navigation to stats page + * 5. RPI5 starts sending: STATS:ip=...,total=...,pending=...,processed=...\n * - * Protocol: Text-based - * Format: STATS:ip=,total=,pending=,processed=\n - * Example: STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245\n + * Wiring: + * RPI5 TX (GPIO14, Pin 8) -> Flipper RX (Pin 14) + * RPI5 RX (GPIO15, Pin 10) <- Flipper TX (Pin 13) + * RPI5 GND (Pin 6) -> Flipper GND (Pin 8/11/18) */ #include #include #include #include -#include -#include - #include -#include #include #include #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 +// Connection state +typedef enum { + StateDisconnected, + StateConnecting, + StateConnected, +} ConnectionState; + +// Pages +typedef enum { + PageWelcome, + PageStats, +} AppPage; + +// Statistics typedef struct { char ip_address[IP_BUFFER_SIZE]; uint32_t total_frames; uint32_t pending_frames; uint32_t processed_frames; - bool connected; + bool data_received; uint32_t last_update_tick; } CanStats; -// Application context +// App context typedef struct { Gui* gui; ViewPort* view_port; - FuriMessageQueue* event_queue; FuriMutex* mutex; - FuriHalSerialHandle* serial_handle; + FuriHalSerialHandle* serial; FuriStreamBuffer* rx_stream; - FuriThread* rx_thread; + FuriThread* worker_thread; CanStats stats; - bool running; + AppPage current_page; + ConnectionState conn_state; + volatile bool running; + volatile bool send_init; } CanMonitorApp; -// Event types -typedef enum { - EventTypeKey, - EventTypeStats, -} EventType; +// Send data via UART +static void uart_send(CanMonitorApp* app, const char* data) { + if(app->serial) { + furi_hal_serial_tx(app->serial, (uint8_t*)data, strlen(data)); + FURI_LOG_I(TAG, "TX: %s", data); + } +} -typedef struct { - EventType type; - InputEvent input; -} CanMonitorEvent; +// Parse ACK response: ACK:rpi5,ip=x.x.x.x +static bool parse_ack(const char* line, CanStats* stats) { + if(strncmp(line, "ACK:", 4) != 0) { + return false; + } -// 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 + const char* p = line + 4; + + // 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) { return false; } - const char* data = line + 6; + const char* p = 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'; - } + const char* ip = strstr(p, "ip="); + if(ip) { + ip += 3; + const char* end = strchr(ip, ','); + if(end && (size_t)(end - ip) < IP_BUFFER_SIZE) { + size_t len = end - ip; + memcpy(stats->ip_address, ip, 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); + const char* total = strstr(p, "total="); + if(total) { + stats->total_frames = strtoul(total + 6, NULL, 10); } // Parse pending= - const char* pending_start = strstr(data, "pending="); - if(pending_start) { - stats->pending_frames = strtoul(pending_start + 8, NULL, 10); + const char* pending = strstr(p, "pending="); + if(pending) { + stats->pending_frames = strtoul(pending + 8, NULL, 10); } // Parse processed= - const char* processed_start = strstr(data, "processed="); - if(processed_start) { - stats->processed_frames = strtoul(processed_start + 10, NULL, 10); + const char* processed = strstr(p, "processed="); + if(processed) { + stats->processed_frames = strtoul(processed + 10, NULL, 10); } - stats->connected = true; + stats->data_received = 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); +// Process received line +static void process_line(CanMonitorApp* app, const char* line) { + FURI_LOG_I(TAG, "RX: %s", line); + + furi_mutex_acquire(app->mutex, FuriWaitForever); + + 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) { - uint8_t data = furi_hal_serial_async_rx(handle); - furi_stream_buffer_send(app->rx_stream, &data, 1, 0); + uint8_t byte = furi_hal_serial_async_rx(handle); + furi_stream_buffer_send(app->rx_stream, &byte, 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; +// Worker thread +static int32_t worker_thread(void* ctx) { + CanMonitorApp* app = ctx; + char buffer[RX_BUFFER_SIZE]; + size_t idx = 0; + uint32_t last_rx_tick = 0; + + FURI_LOG_I(TAG, "Worker started"); while(app->running) { - uint8_t data; - size_t len = furi_stream_buffer_receive(app->rx_stream, &data, 1, 100); + // Check if we need to send INIT + 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(data == '\n' || data == '\r') { - if(rx_index > 0) { - rx_buffer[rx_index] = '\0'; + last_rx_tick = furi_get_tick(); - // 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; + if(byte == '\n' || byte == '\r') { + if(idx > 0) { + buffer[idx] = '\0'; + process_line(app, buffer); + idx = 0; } - } else if(rx_index < RX_BUFFER_SIZE - 1) { - rx_buffer[rx_index++] = data; + } 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 connection timeout (5 seconds) + // Check data timeout (5 sec) - only when connected 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); + if(app->conn_state == StateConnected && app->stats.data_received) { + if((furi_get_tick() - app->stats.last_update_tick) > 5000) { + app->stats.data_received = false; + view_port_update(app->view_port); + } } furi_mutex_release(app->mutex); } + FURI_LOG_I(TAG, "Worker stopped"); return 0; } -// Draw callback -static void draw_callback(Canvas* canvas, void* context) { - CanMonitorApp* app = context; - - furi_mutex_acquire(app->mutex, FuriWaitForever); - +// Draw welcome page +static void draw_welcome(Canvas* canvas, CanMonitorApp* app) { canvas_clear(canvas); - canvas_set_font(canvas, FontPrimary); // 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); - // Connection status - if(app->stats.connected) { - canvas_draw_str(canvas, 0, 14, "Status: Connected"); + // Subtitle + canvas_draw_str_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "RPI5 CAN Sniffer"); + + // 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 { - 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 - char buf[64]; if(strlen(app->stats.ip_address) > 0) { snprintf(buf, sizeof(buf), "IP: %s", app->stats.ip_address); } else { @@ -209,7 +328,7 @@ static void draw_callback(Canvas* canvas, void* context) { canvas_draw_str(canvas, 0, 26, buf); // 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); // Pending frames @@ -218,110 +337,158 @@ static void draw_callback(Canvas* canvas, void* context) { // 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); } -// 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); +// Draw callback +static void draw_callback(Canvas* canvas, void* ctx) { + CanMonitorApp* app = ctx; + + switch(app->current_page) { + case PageWelcome: + draw_welcome(canvas, app); + break; + case PageStats: + draw_stats(canvas, app); + break; + } } -// Initialize application -static CanMonitorApp* can_monitor_app_alloc(void) { +// Input callback +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)); memset(app, 0, sizeof(CanMonitorApp)); - app->running = true; + app->current_page = PageWelcome; + app->conn_state = StateDisconnected; + app->send_init = false; + + // Mutex 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 + // Disable expansion protocol Expansion* expansion = furi_record_open(RECORD_EXPANSION); expansion_disable(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->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); + app->serial = furi_hal_serial_control_acquire(FuriHalSerialIdUsart); - // Start RX thread - app->rx_thread = furi_thread_alloc_ex("CANMonitorRx", 1024, uart_rx_thread, app); - furi_thread_start(app->rx_thread); + if(app->serial) { + furi_hal_serial_init(app->serial, UART_BAUD); + 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->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; -} + FURI_LOG_I(TAG, "GUI initialized, waiting for user input"); -// 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); + // Main loop + while(app->running) { + furi_delay_ms(100); + } - // 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_LOG_I(TAG, "Shutting down"); + + // Send disconnect signal + 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); - // Re-enable expansion protocol - Expansion* expansion = furi_record_open(RECORD_EXPANSION); + // Re-enable 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); + FURI_LOG_I(TAG, "Bye!"); return 0; }