Create new module for flipper and update UI flipper zero
This commit is contained in:
@@ -1,14 +1,21 @@
|
||||
/**
|
||||
* CAN Monitor for Flipper Zero
|
||||
* CAN Monitor for Flipper Zero - Dynamic UI Version
|
||||
*
|
||||
* Multi-page application with handshake protocol.
|
||||
* Multi-page application with dynamic content from RPI5.
|
||||
*
|
||||
* 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:
|
||||
* RPi -> Flipper:
|
||||
* PAGE:<idx>/<total>|<type>|<title>|<lines>|<actions>|<selected>
|
||||
* ACK:<device>,ip=<ip>
|
||||
* RESULT:<status>|<message>
|
||||
*
|
||||
* Flipper -> RPi:
|
||||
* INIT:flipper
|
||||
* STOP:flipper
|
||||
* CMD:NAV:next / CMD:NAV:prev
|
||||
* CMD:SELECT:<index>
|
||||
* CMD:CONFIRM / CMD:CANCEL
|
||||
* CMD:REFRESH
|
||||
*
|
||||
* Wiring:
|
||||
* RPI5 TX (GPIO14, Pin 8) -> Flipper RX (Pin 14)
|
||||
@@ -20,6 +27,7 @@
|
||||
#include <furi_hal.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_port.h>
|
||||
#include <gui/elements.h>
|
||||
#include <expansion/expansion.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
@@ -27,8 +35,11 @@
|
||||
|
||||
#define TAG "CANMonitor"
|
||||
#define UART_BAUD 115200
|
||||
#define RX_BUFFER_SIZE 256
|
||||
#define IP_BUFFER_SIZE 32
|
||||
#define RX_BUFFER_SIZE 512
|
||||
#define MAX_LINES 5
|
||||
#define MAX_ACTIONS 4
|
||||
#define MAX_LINE_LENGTH 32
|
||||
#define MAX_TITLE_LENGTH 24
|
||||
|
||||
// Connection state
|
||||
typedef enum {
|
||||
@@ -37,21 +48,35 @@ typedef enum {
|
||||
StateConnected,
|
||||
} ConnectionState;
|
||||
|
||||
// Pages
|
||||
// Page types
|
||||
typedef enum {
|
||||
PageWelcome,
|
||||
PageStats,
|
||||
} AppPage;
|
||||
PageTypeInfo,
|
||||
PageTypeMenu,
|
||||
PageTypeConfirm,
|
||||
} PageType;
|
||||
|
||||
// Statistics
|
||||
// Page content received from RPi
|
||||
typedef struct {
|
||||
char ip_address[IP_BUFFER_SIZE];
|
||||
uint32_t total_frames;
|
||||
uint32_t pending_frames;
|
||||
uint32_t processed_frames;
|
||||
bool data_received;
|
||||
uint8_t page_index;
|
||||
uint8_t total_pages;
|
||||
PageType page_type;
|
||||
char title[MAX_TITLE_LENGTH];
|
||||
char lines[MAX_LINES][MAX_LINE_LENGTH];
|
||||
uint8_t line_count;
|
||||
char actions[MAX_ACTIONS][MAX_LINE_LENGTH];
|
||||
uint8_t action_count;
|
||||
uint8_t selected_index;
|
||||
bool data_valid;
|
||||
uint32_t last_update_tick;
|
||||
} CanStats;
|
||||
} PageContent;
|
||||
|
||||
// Result message
|
||||
typedef struct {
|
||||
bool has_result;
|
||||
bool success;
|
||||
char message[MAX_LINE_LENGTH];
|
||||
uint32_t show_until_tick;
|
||||
} ResultMessage;
|
||||
|
||||
// App context
|
||||
typedef struct {
|
||||
@@ -63,13 +88,19 @@ typedef struct {
|
||||
FuriStreamBuffer* rx_stream;
|
||||
FuriThread* worker_thread;
|
||||
|
||||
CanStats stats;
|
||||
AppPage current_page;
|
||||
PageContent page;
|
||||
ResultMessage result;
|
||||
ConnectionState conn_state;
|
||||
char ip_address[32];
|
||||
|
||||
volatile bool running;
|
||||
volatile bool send_init;
|
||||
} CanMonitorApp;
|
||||
|
||||
// Forward declarations
|
||||
static void draw_callback(Canvas* canvas, void* ctx);
|
||||
static void input_callback(InputEvent* event, void* ctx);
|
||||
|
||||
// Send data via UART
|
||||
static void uart_send(CanMonitorApp* app, const char* data) {
|
||||
if(app->serial) {
|
||||
@@ -78,77 +109,200 @@ static void uart_send(CanMonitorApp* app, const char* data) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse ACK response: ACK:rpi5,ip=x.x.x.x
|
||||
static bool parse_ack(const char* line, CanStats* stats) {
|
||||
// Clear page content
|
||||
static void clear_page_content(PageContent* page) {
|
||||
page->page_index = 0;
|
||||
page->total_pages = 0;
|
||||
page->page_type = PageTypeInfo;
|
||||
page->title[0] = '\0';
|
||||
page->line_count = 0;
|
||||
page->action_count = 0;
|
||||
page->selected_index = 0;
|
||||
page->data_valid = false;
|
||||
|
||||
for(int i = 0; i < MAX_LINES; i++) {
|
||||
page->lines[i][0] = '\0';
|
||||
}
|
||||
for(int i = 0; i < MAX_ACTIONS; i++) {
|
||||
page->actions[i][0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
// Parse semicolon-separated items into array
|
||||
static uint8_t parse_items(const char* str, char items[][MAX_LINE_LENGTH], uint8_t max_items) {
|
||||
uint8_t count = 0;
|
||||
|
||||
if(!str || str[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* start = str;
|
||||
const char* end;
|
||||
|
||||
while(count < max_items && start && *start) {
|
||||
end = strchr(start, ';');
|
||||
|
||||
size_t len;
|
||||
if(end) {
|
||||
len = end - start;
|
||||
} else {
|
||||
len = strlen(start);
|
||||
}
|
||||
|
||||
if(len >= MAX_LINE_LENGTH) {
|
||||
len = MAX_LINE_LENGTH - 1;
|
||||
}
|
||||
|
||||
if(len > 0) {
|
||||
memcpy(items[count], start, len);
|
||||
items[count][len] = '\0';
|
||||
count++;
|
||||
}
|
||||
|
||||
if(end) {
|
||||
start = end + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// Parse PAGE message
|
||||
// Format: PAGE:<idx>/<total>|<type>|<title>|<lines>|<actions>|<selected>
|
||||
static bool parse_page(const char* line, PageContent* page) {
|
||||
if(strncmp(line, "PAGE:", 5) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* p = line + 5;
|
||||
char* end;
|
||||
|
||||
// Parse page index and total
|
||||
page->page_index = (uint8_t)strtoul(p, &end, 10);
|
||||
if(*end != '/') return false;
|
||||
p = end + 1;
|
||||
|
||||
page->total_pages = (uint8_t)strtoul(p, &end, 10);
|
||||
if(*end != '|') return false;
|
||||
p = end + 1;
|
||||
|
||||
// Parse page type
|
||||
const char* type_end = strchr(p, '|');
|
||||
if(!type_end) return false;
|
||||
|
||||
if(strncmp(p, "info", type_end - p) == 0) {
|
||||
page->page_type = PageTypeInfo;
|
||||
} else if(strncmp(p, "menu", type_end - p) == 0) {
|
||||
page->page_type = PageTypeMenu;
|
||||
} else if(strncmp(p, "confirm", type_end - p) == 0) {
|
||||
page->page_type = PageTypeConfirm;
|
||||
} else {
|
||||
page->page_type = PageTypeInfo;
|
||||
}
|
||||
p = type_end + 1;
|
||||
|
||||
// Parse title
|
||||
const char* title_end = strchr(p, '|');
|
||||
if(!title_end) return false;
|
||||
|
||||
size_t title_len = title_end - p;
|
||||
if(title_len >= MAX_TITLE_LENGTH) {
|
||||
title_len = MAX_TITLE_LENGTH - 1;
|
||||
}
|
||||
memcpy(page->title, p, title_len);
|
||||
page->title[title_len] = '\0';
|
||||
p = title_end + 1;
|
||||
|
||||
// Parse lines
|
||||
const char* lines_end = strchr(p, '|');
|
||||
if(!lines_end) return false;
|
||||
|
||||
char lines_buf[256];
|
||||
size_t lines_len = lines_end - p;
|
||||
if(lines_len >= sizeof(lines_buf)) {
|
||||
lines_len = sizeof(lines_buf) - 1;
|
||||
}
|
||||
memcpy(lines_buf, p, lines_len);
|
||||
lines_buf[lines_len] = '\0';
|
||||
page->line_count = parse_items(lines_buf, page->lines, MAX_LINES);
|
||||
p = lines_end + 1;
|
||||
|
||||
// Parse actions
|
||||
const char* actions_end = strchr(p, '|');
|
||||
if(!actions_end) return false;
|
||||
|
||||
char actions_buf[128];
|
||||
size_t actions_len = actions_end - p;
|
||||
if(actions_len >= sizeof(actions_buf)) {
|
||||
actions_len = sizeof(actions_buf) - 1;
|
||||
}
|
||||
memcpy(actions_buf, p, actions_len);
|
||||
actions_buf[actions_len] = '\0';
|
||||
page->action_count = parse_items(actions_buf, page->actions, MAX_ACTIONS);
|
||||
p = actions_end + 1;
|
||||
|
||||
// Parse selected index
|
||||
page->selected_index = (uint8_t)strtoul(p, NULL, 10);
|
||||
|
||||
page->data_valid = true;
|
||||
page->last_update_tick = furi_get_tick();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse ACK message
|
||||
static bool parse_ack(const char* line, char* ip_out, size_t ip_size) {
|
||||
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=");
|
||||
const char* ip = strstr(line, "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';
|
||||
if(len > 0 && len < ip_size) {
|
||||
memcpy(ip_out, ip, len);
|
||||
ip_out[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) {
|
||||
// Parse RESULT message
|
||||
static bool parse_result(const char* line, ResultMessage* result) {
|
||||
if(strncmp(line, "RESULT:", 7) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* p = line + 6;
|
||||
const char* p = line + 7;
|
||||
|
||||
// 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 status
|
||||
if(strncmp(p, "OK|", 3) == 0) {
|
||||
result->success = true;
|
||||
p += 3;
|
||||
} else if(strncmp(p, "ERROR|", 6) == 0) {
|
||||
result->success = false;
|
||||
p += 6;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse total=
|
||||
const char* total = strstr(p, "total=");
|
||||
if(total) {
|
||||
stats->total_frames = strtoul(total + 6, NULL, 10);
|
||||
// Parse message
|
||||
size_t len = strlen(p);
|
||||
if(len >= MAX_LINE_LENGTH) {
|
||||
len = MAX_LINE_LENGTH - 1;
|
||||
}
|
||||
memcpy(result->message, p, len);
|
||||
result->message[len] = '\0';
|
||||
|
||||
// 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();
|
||||
result->has_result = true;
|
||||
result->show_until_tick = furi_get_tick() + 3000; // Show for 3 seconds
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -161,13 +315,17 @@ static void process_line(CanMonitorApp* app, const char* line) {
|
||||
|
||||
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);
|
||||
if(parse_ack(line, app->ip_address, sizeof(app->ip_address))) {
|
||||
FURI_LOG_I(TAG, "ACK received, IP: %s", app->ip_address);
|
||||
app->conn_state = StateConnected;
|
||||
}
|
||||
} else if(app->conn_state == StateConnected) {
|
||||
// Parse stats
|
||||
parse_stats(line, &app->stats);
|
||||
// Parse PAGE or RESULT
|
||||
if(line[0] == 'P') {
|
||||
parse_page(line, &app->page);
|
||||
} else if(line[0] == 'R') {
|
||||
parse_result(line, &app->result);
|
||||
}
|
||||
}
|
||||
|
||||
furi_mutex_release(app->mutex);
|
||||
@@ -201,6 +359,7 @@ static int32_t worker_thread(void* ctx) {
|
||||
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
app->conn_state = StateConnecting;
|
||||
clear_page_content(&app->page);
|
||||
furi_mutex_release(app->mutex);
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
@@ -232,12 +391,18 @@ static int32_t worker_thread(void* ctx) {
|
||||
|
||||
// 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;
|
||||
if(app->conn_state == StateConnected && app->page.data_valid) {
|
||||
if((furi_get_tick() - app->page.last_update_tick) > 5000) {
|
||||
app->page.data_valid = false;
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear result message after timeout
|
||||
if(app->result.has_result && furi_get_tick() > app->result.show_until_tick) {
|
||||
app->result.has_result = false;
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
furi_mutex_release(app->mutex);
|
||||
}
|
||||
|
||||
@@ -245,24 +410,166 @@ static int32_t worker_thread(void* ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Draw welcome page
|
||||
// Draw header with page indicator
|
||||
static void draw_header(Canvas* canvas, CanMonitorApp* app) {
|
||||
// Page indicator
|
||||
char page_str[16];
|
||||
snprintf(
|
||||
page_str,
|
||||
sizeof(page_str),
|
||||
"[%d/%d]",
|
||||
app->page.page_index + 1,
|
||||
app->page.total_pages);
|
||||
canvas_draw_str(canvas, 0, 8, page_str);
|
||||
|
||||
// Title (centered)
|
||||
uint16_t title_width = canvas_string_width(canvas, app->page.title);
|
||||
uint16_t title_x = (128 - title_width) / 2;
|
||||
canvas_draw_str(canvas, title_x, 8, app->page.title);
|
||||
|
||||
// Connection indicator (right side)
|
||||
if(app->page.data_valid) {
|
||||
canvas_draw_disc(canvas, 122, 4, 3);
|
||||
} else {
|
||||
canvas_draw_circle(canvas, 122, 4, 3);
|
||||
}
|
||||
|
||||
// Separator line
|
||||
canvas_draw_line(canvas, 0, 11, 128, 11);
|
||||
}
|
||||
|
||||
// Draw footer with navigation hints
|
||||
static void draw_footer(Canvas* canvas, PageType page_type, uint8_t total_pages, uint8_t current_page) {
|
||||
canvas_draw_line(canvas, 0, 54, 128, 54);
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
// Left arrow (if not first page)
|
||||
if(current_page > 0) {
|
||||
canvas_draw_str(canvas, 2, 63, "<");
|
||||
}
|
||||
|
||||
// Center action hint
|
||||
const char* hint = "";
|
||||
switch(page_type) {
|
||||
case PageTypeInfo:
|
||||
hint = "";
|
||||
break;
|
||||
case PageTypeMenu:
|
||||
hint = "OK=Select";
|
||||
break;
|
||||
case PageTypeConfirm:
|
||||
hint = "OK=Yes Back=No";
|
||||
break;
|
||||
}
|
||||
uint16_t hint_width = canvas_string_width(canvas, hint);
|
||||
canvas_draw_str(canvas, (128 - hint_width) / 2, 63, hint);
|
||||
|
||||
// Right arrow (if not last page)
|
||||
if(current_page < total_pages - 1) {
|
||||
canvas_draw_str(canvas, 122, 63, ">");
|
||||
}
|
||||
}
|
||||
|
||||
// Draw info page
|
||||
static void draw_info_page(Canvas* canvas, CanMonitorApp* app) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
uint8_t y = 22;
|
||||
for(uint8_t i = 0; i < app->page.line_count && i < MAX_LINES; i++) {
|
||||
canvas_draw_str(canvas, 2, y, app->page.lines[i]);
|
||||
y += 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw menu page
|
||||
static void draw_menu_page(Canvas* canvas, CanMonitorApp* app) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
uint8_t y = 22;
|
||||
for(uint8_t i = 0; i < app->page.action_count && i < MAX_ACTIONS; i++) {
|
||||
// Selection indicator
|
||||
if(i == app->page.selected_index) {
|
||||
canvas_draw_box(canvas, 0, y - 8, 128, 10);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
|
||||
canvas_draw_str(canvas, 4, y, app->page.actions[i]);
|
||||
|
||||
if(i == app->page.selected_index) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
y += 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw confirm page
|
||||
static void draw_confirm_page(Canvas* canvas, CanMonitorApp* app) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
// Message (centered)
|
||||
if(app->page.line_count > 0) {
|
||||
uint16_t msg_width = canvas_string_width(canvas, app->page.lines[0]);
|
||||
canvas_draw_str(canvas, (128 - msg_width) / 2, 30, app->page.lines[0]);
|
||||
}
|
||||
|
||||
// Yes/No options
|
||||
uint8_t btn_y = 44;
|
||||
uint8_t yes_x = 32;
|
||||
uint8_t no_x = 80;
|
||||
|
||||
// Draw Yes
|
||||
if(app->page.selected_index == 0) {
|
||||
canvas_draw_box(canvas, yes_x - 2, btn_y - 8, 24, 12);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_str(canvas, yes_x, btn_y, "Yes");
|
||||
if(app->page.selected_index == 0) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
// Draw No
|
||||
if(app->page.selected_index == 1) {
|
||||
canvas_draw_box(canvas, no_x - 2, btn_y - 8, 20, 12);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_str(canvas, no_x, btn_y, "No");
|
||||
if(app->page.selected_index == 1) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw result overlay
|
||||
static void draw_result_overlay(Canvas* canvas, ResultMessage* result) {
|
||||
// Semi-transparent background
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_box(canvas, 10, 20, 108, 28);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_frame(canvas, 10, 20, 108, 28);
|
||||
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
const char* status = result->success ? "OK" : "Error";
|
||||
canvas_draw_str(canvas, 14, 32, status);
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str(canvas, 14, 44, result->message);
|
||||
}
|
||||
|
||||
// Draw welcome/connecting screen
|
||||
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);
|
||||
canvas_draw_str_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "RPI5 Dynamic UI");
|
||||
|
||||
// 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);
|
||||
char ip_buf[32];
|
||||
strncpy(ip_buf, app->ip_address, sizeof(ip_buf));
|
||||
furi_mutex_release(app->mutex);
|
||||
|
||||
switch(state) {
|
||||
@@ -283,139 +590,167 @@ static void draw_welcome(Canvas* canvas, CanMonitorApp* app) {
|
||||
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 >");
|
||||
canvas_draw_str_aligned(canvas, 64, 58, AlignCenter, AlignCenter, "Waiting for data...");
|
||||
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);
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
|
||||
// Check if we should show welcome screen
|
||||
if(app->conn_state != StateConnected || !app->page.data_valid) {
|
||||
furi_mutex_release(app->mutex);
|
||||
draw_welcome(canvas, app);
|
||||
return;
|
||||
}
|
||||
|
||||
canvas_clear(canvas);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
|
||||
// Draw header
|
||||
draw_header(canvas, app);
|
||||
|
||||
// Draw content based on page type
|
||||
switch(app->page.page_type) {
|
||||
case PageTypeInfo:
|
||||
draw_info_page(canvas, app);
|
||||
break;
|
||||
case PageStats:
|
||||
draw_stats(canvas, app);
|
||||
case PageTypeMenu:
|
||||
draw_menu_page(canvas, app);
|
||||
break;
|
||||
case PageTypeConfirm:
|
||||
draw_confirm_page(canvas, app);
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw footer
|
||||
draw_footer(canvas, app->page.page_type, app->page.total_pages, app->page.page_index);
|
||||
|
||||
// Draw result overlay if present
|
||||
if(app->result.has_result) {
|
||||
draw_result_overlay(canvas, &app->result);
|
||||
}
|
||||
|
||||
furi_mutex_release(app->mutex);
|
||||
}
|
||||
|
||||
// Input callback
|
||||
static void input_callback(InputEvent* event, void* ctx) {
|
||||
CanMonitorApp* app = ctx;
|
||||
|
||||
if(event->type == InputTypeShort) {
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
if(event->type != InputTypeShort && event->type != InputTypeRepeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
||||
|
||||
// Handle welcome screen input
|
||||
if(app->conn_state == StateDisconnected) {
|
||||
if(event->key == InputKeyOk) {
|
||||
app->send_init = true;
|
||||
} else if(event->key == InputKeyBack) {
|
||||
app->running = false;
|
||||
}
|
||||
furi_mutex_release(app->mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle connected state input
|
||||
if(app->conn_state == StateConnected && app->page.data_valid) {
|
||||
switch(event->key) {
|
||||
case InputKeyOk:
|
||||
if(app->current_page == PageWelcome && app->conn_state == StateDisconnected) {
|
||||
// Start handshake
|
||||
app->send_init = true;
|
||||
case InputKeyUp:
|
||||
if(app->page.page_type == PageTypeMenu && app->page.selected_index > 0) {
|
||||
app->page.selected_index--;
|
||||
view_port_update(app->view_port);
|
||||
} else if(app->page.page_type == PageTypeConfirm) {
|
||||
app->page.selected_index = 0; // Yes
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
break;
|
||||
|
||||
case InputKeyRight:
|
||||
// Only allow if connected
|
||||
if(app->current_page == PageWelcome && app->conn_state == StateConnected) {
|
||||
app->current_page = PageStats;
|
||||
case InputKeyDown:
|
||||
if(app->page.page_type == PageTypeMenu &&
|
||||
app->page.selected_index < app->page.action_count - 1) {
|
||||
app->page.selected_index++;
|
||||
view_port_update(app->view_port);
|
||||
} else if(app->page.page_type == PageTypeConfirm) {
|
||||
app->page.selected_index = 1; // No
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
break;
|
||||
|
||||
case InputKeyLeft:
|
||||
if(app->current_page == PageStats) {
|
||||
app->current_page = PageWelcome;
|
||||
view_port_update(app->view_port);
|
||||
// Navigate to previous page
|
||||
uart_send(app, "CMD:NAV:prev\n");
|
||||
break;
|
||||
|
||||
case InputKeyRight:
|
||||
// Navigate to next page
|
||||
uart_send(app, "CMD:NAV:next\n");
|
||||
break;
|
||||
|
||||
case InputKeyOk:
|
||||
if(app->page.page_type == PageTypeMenu) {
|
||||
// Send selection
|
||||
char cmd[32];
|
||||
snprintf(cmd, sizeof(cmd), "CMD:SELECT:%d\n", app->page.selected_index);
|
||||
uart_send(app, cmd);
|
||||
} else if(app->page.page_type == PageTypeConfirm) {
|
||||
if(app->page.selected_index == 0) {
|
||||
uart_send(app, "CMD:CONFIRM\n");
|
||||
} else {
|
||||
uart_send(app, "CMD:CANCEL\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case InputKeyBack:
|
||||
if(app->current_page == PageWelcome) {
|
||||
app->running = false;
|
||||
} else {
|
||||
app->current_page = PageWelcome;
|
||||
if(app->page.page_type == PageTypeConfirm) {
|
||||
// Cancel confirmation
|
||||
uart_send(app, "CMD:CANCEL\n");
|
||||
} else if(app->page.page_index == 0) {
|
||||
// On first page, disconnect
|
||||
uart_send(app, "STOP:flipper\n");
|
||||
app->conn_state = StateDisconnected;
|
||||
clear_page_content(&app->page);
|
||||
view_port_update(app->view_port);
|
||||
} else {
|
||||
// Go to previous page
|
||||
uart_send(app, "CMD:NAV:prev\n");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
furi_mutex_release(app->mutex);
|
||||
} else if(event->key == InputKeyBack) {
|
||||
// Disconnect and exit
|
||||
if(app->conn_state != StateDisconnected) {
|
||||
uart_send(app, "STOP:flipper\n");
|
||||
}
|
||||
app->running = false;
|
||||
}
|
||||
|
||||
furi_mutex_release(app->mutex);
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
int32_t can_monitor_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
FURI_LOG_I(TAG, "Starting CAN Monitor");
|
||||
FURI_LOG_I(TAG, "Starting CAN Monitor (Dynamic UI)");
|
||||
|
||||
// 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;
|
||||
clear_page_content(&app->page);
|
||||
|
||||
// Mutex
|
||||
app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
@@ -440,7 +775,7 @@ int32_t can_monitor_app(void* p) {
|
||||
}
|
||||
|
||||
// Start worker
|
||||
app->worker_thread = furi_thread_alloc_ex("CanMonitorWorker", 1024, worker_thread, app);
|
||||
app->worker_thread = furi_thread_alloc_ex("CanMonitorWorker", 2048, worker_thread, app);
|
||||
furi_thread_start(app->worker_thread);
|
||||
|
||||
// Init GUI
|
||||
@@ -450,7 +785,7 @@ int32_t can_monitor_app(void* p) {
|
||||
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");
|
||||
FURI_LOG_I(TAG, "GUI initialized");
|
||||
|
||||
// Main loop
|
||||
while(app->running) {
|
||||
@@ -460,7 +795,7 @@ int32_t can_monitor_app(void* p) {
|
||||
FURI_LOG_I(TAG, "Shutting down");
|
||||
|
||||
// Send disconnect signal
|
||||
if(app->conn_state == StateConnected) {
|
||||
if(app->conn_state != StateDisconnected) {
|
||||
uart_send(app, "STOP:flipper\n");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user