/** * CAN Monitor for Flipper Zero * * Multi-page application with handshake protocol. * * 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 * * 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 #define TAG "CANMonitor" #define UART_BAUD 115200 #define RX_BUFFER_SIZE 256 #define IP_BUFFER_SIZE 32 // 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 data_received; uint32_t last_update_tick; } CanStats; // App context typedef struct { Gui* gui; ViewPort* view_port; FuriMutex* mutex; FuriHalSerialHandle* serial; FuriStreamBuffer* rx_stream; FuriThread* worker_thread; CanStats stats; AppPage current_page; ConnectionState conn_state; volatile bool running; volatile bool send_init; } CanMonitorApp; // 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); } } // 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; } 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* p = line + 6; // Parse ip= 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 = strstr(p, "total="); if(total) { stats->total_frames = strtoul(total + 6, NULL, 10); } // Parse pending= const char* pending = strstr(p, "pending="); if(pending) { stats->pending_frames = strtoul(pending + 8, NULL, 10); } // Parse processed= const char* processed = strstr(p, "processed="); if(processed) { stats->processed_frames = strtoul(processed + 10, NULL, 10); } stats->data_received = true; stats->last_update_tick = furi_get_tick(); return true; } // 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 byte = furi_hal_serial_async_rx(handle); furi_stream_buffer_send(app->rx_stream, &byte, 1, 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) { // 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) { last_rx_tick = furi_get_tick(); 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); 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 welcome page static void draw_welcome(Canvas* canvas, CanMonitorApp* app) { canvas_clear(canvas); // Title canvas_set_font(canvas, FontPrimary); canvas_draw_str_aligned(canvas, 64, 8, AlignCenter, AlignCenter, "CAN Monitor"); canvas_set_font(canvas, FontSecondary); // 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_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 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 frames: %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, 4, 62, buf); furi_mutex_release(app->mutex); } // 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; } } // 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); // Disable expansion protocol Expansion* expansion = furi_record_open(RECORD_EXPANSION); expansion_disable(expansion); furi_record_close(RECORD_EXPANSION); FURI_LOG_I(TAG, "Expansion disabled"); // Init UART app->rx_stream = furi_stream_buffer_alloc(RX_BUFFER_SIZE, 1); app->serial = furi_hal_serial_control_acquire(FuriHalSerialIdUsart); 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"); } // 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); FURI_LOG_I(TAG, "GUI initialized, waiting for user input"); // Main loop while(app->running) { furi_delay_ms(100); } 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 expansion = furi_record_open(RECORD_EXPANSION); expansion_enable(expansion); furi_record_close(RECORD_EXPANSION); 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_mutex_free(app->mutex); free(app); FURI_LOG_I(TAG, "Bye!"); return 0; }