495 lines
14 KiB
C
495 lines
14 KiB
C
/**
|
|
* 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 <furi.h>
|
|
#include <furi_hal.h>
|
|
#include <gui/gui.h>
|
|
#include <gui/view_port.h>
|
|
#include <expansion/expansion.h>
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#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;
|
|
}
|