Create new module for flipper and update UI flipper zero

This commit is contained in:
2026-01-27 14:52:06 +03:00
parent 434feb2a1d
commit dab72ac1b1
18 changed files with 2659 additions and 254 deletions

View File

@@ -1,27 +1,104 @@
# CAN Monitor for Flipper Zero
Flipper Zero application for monitoring CAN sniffer statistics from Raspberry Pi 5 via UART.
Dynamic multi-page monitor application for Raspberry Pi 5 via UART.
## Features
- Real-time display of CAN sniffer statistics
- Shows: IP address, total frames, pending frames, processed frames
- Connection status indicator
- Compatible with Unleashed firmware (0.84e and later)
- **Dynamic Page System** - Pages are fully controlled by RPi5
- **Multiple Page Types**:
- **Info** - Read-only status display
- **Menu** - Selectable action items
- **Confirm** - Yes/No confirmation dialogs
- **Real-time Updates** - Content refreshes every second
- **Bidirectional Communication** - Send commands back to RPi5
- **Result Notifications** - Action results displayed as overlay
## Default Pages
When connected to the CAN Sniffer system:
| Page | Type | Description |
|------|------|-------------|
| CAN Statistics | Info | Frame counts, queue status, per-interface stats |
| UPS Status | Info | Battery level, voltage, charging state (X120x) |
| System Info | Info | CPU temperature, power consumption, fan RPM |
| Actions | Menu | Shutdown, Reboot, Cancel shutdown |
## Screen Layout
```
128x64 pixels (Flipper Zero display)
+----------------------------------+
| [1/4] Page Title (*) | <- Header: page indicator, title, connection dot
+----------------------------------+
| |
| Content Line 1 | <- Content area: 4-5 lines
| Content Line 2 |
| Content Line 3 |
| Content Line 4 |
| |
+----------------------------------+
| < Hint Text > | <- Footer: nav arrows, action hint
+----------------------------------+
```
## Controls
| Key | Info Page | Menu Page | Confirm Page |
|-----|-----------|-----------|--------------|
| **Left/Right** | Navigate pages | Navigate pages | Navigate pages |
| **Up/Down** | - | Select item | Switch Yes/No |
| **OK** | - | Execute action | Confirm selection |
| **Back** | Disconnect (pg 0) / Prev page | Same | Cancel dialog |
## Protocol
### RPi5 -> Flipper
```
PAGE:<idx>/<total>|<type>|<title>|<lines>|<actions>|<selected>
ACK:<device>,ip=<ip>
RESULT:<OK|ERROR>|<message>
```
**Examples:**
```
PAGE:0/4|info|CAN Statistics|Total: 12.3K;Processed: 12.1K;Queue: 142||0
PAGE:1/4|info|UPS Status|Bat: 85.2% [====];Voltage: 4.12V;Status: Charging||0
PAGE:2/4|info|System Info|CPU Temp: 52.1C;Power: 4.2W;Fan: 3200 RPM||0
PAGE:3/4|menu|Actions|Shutdown;Reboot;Cancel Shutdown||0
PAGE:3/4|confirm|Confirm Shutdown?|Are you sure?|Yes;No|1
ACK:rpi5,ip=192.168.1.100
RESULT:OK|Shutdown in 1 min
```
### Flipper -> RPi5
```
INIT:flipper - Start handshake
STOP:flipper - Disconnect
CMD:NAV:next - Next page
CMD:NAV:prev - Previous page
CMD:SELECT:<index> - Select menu item
CMD:CONFIRM - Confirm action
CMD:CANCEL - Cancel action
CMD:REFRESH - Request page update
```
## Hardware Connection
### Wiring (RPI5 <-> Flipper Zero)
### Wiring Diagram
```
RPI5 GPIO Flipper Zero GPIO
----------- -----------------
TX (GPIO 14) --> RX (Pin 14)
RX (GPIO 15) <-- TX (Pin 13)
GND --> GND
Flipper Zero Raspberry Pi 5
----------- ---------------
Pin 13 (TX) ----> GPIO 15 (RX) - Pin 10
Pin 14 (RX) <---- GPIO 14 (TX) - Pin 8
Pin 8/11/18 (GND) ---- GND - Pin 6
```
**Note:** Cross TX/RX connections (RPI TX -> Flipper RX, RPI RX -> Flipper TX)
**Note:** Cross TX/RX connections (Flipper TX -> RPi RX)
### Flipper Zero GPIO Pinout
@@ -54,12 +131,6 @@ cd unleashed-firmware
cp -r /path/to/carpibord/flip_monitor applications_user/can_monitor
```
3. Create icon (10x10 PNG, 1-bit):
```bash
# Create icons/can_monitor.png (10x10 pixels, black & white)
# You can use any image editor or online tool
```
### Build
```bash
@@ -83,23 +154,29 @@ SD Card/apps/GPIO/can_monitor.fap
### 1. Enable UART
Add to `/boot/config.txt`:
Add to `/boot/firmware/config.txt`:
```
enable_uart=1
dtoverlay=uart0
```
Disable serial console:
```bash
sudo raspi-config
# Interface Options -> Serial Port -> No (login shell) -> Yes (hardware)
```
Reboot after changes.
### 2. Install pyserial
### 2. Install Dependencies
```bash
pip install pyserial
pip install pyserial smbus2 gpiozero
```
### 3. Configure CAN Sniffer
Add to `can_sniffer/config.json`:
Add to `can_sniffer/src/config.json`:
```json
{
"flipper": {
@@ -124,19 +201,38 @@ cd can_sniffer/src
python main.py
```
## Protocol
## Adding Custom Pages
The RPI5 sends text-based statistics over UART:
On RPi5 side, create a new page class:
```
STATS:ip=192.168.1.100,total=12345,pending=100,processed=12245\n
```python
from flipper.pages.base import InfoPage
class MyCustomPage(InfoPage):
def __init__(self):
super().__init__(
name="my_page",
title="My Page",
icon="custom"
)
def get_lines(self) -> list[str]:
return [
"Custom line 1",
"Custom line 2",
f"Value: {self.get_value()}"
]
```
Fields:
- `ip` - RPI5 IP address
- `total` - Total CAN frames received
- `pending` - Frames in processing queue
- `processed` - Successfully processed frames
Register in FlipperHandler:
```python
# In flipper_handler.py
from flipper.pages.my_custom import MyCustomPage
# In _setup_pages():
self._page_manager.register_page(MyCustomPage())
```
## Troubleshooting
@@ -148,7 +244,7 @@ Fields:
4. Test UART manually:
```bash
# On RPI5
echo "STATS:ip=test,total=1,pending=0,processed=1" > /dev/ttyAMA0
echo "ACK:rpi5,ip=test" > /dev/ttyAMA0
```
### Permission denied on /dev/ttyAMA0
@@ -159,11 +255,23 @@ sudo usermod -a -G dialout $USER
# Then logout and login again
```
### Flipper shows "Waiting..."
### Flipper shows "Waiting for data..."
- Stats are sent every 1 second (configurable)
- Data is sent every 1 second (configurable)
- Connection timeout is 5 seconds
- Check if CAN sniffer is running
- Check UART logs: `CAN_SNIFFER_LOGGING__LEVEL=DEBUG python main.py`
### UPS page shows "not available"
- X120x UPS must be connected via I2C
- Install smbus2: `pip install smbus2`
- Check I2C: `i2cdetect -y 1` (should show device at 0x36)
## Version History
- **v2.0** - Dynamic page system, bidirectional commands, UPS/System pages
- **v1.0** - Basic CAN statistics display
## License

View File

@@ -3,9 +3,10 @@ App(
name="CAN Monitor",
apptype=FlipperAppType.EXTERNAL,
entry_point="can_monitor_app",
stack_size=2 * 1024,
stack_size=4 * 1024,
fap_category="GPIO",
fap_author="carpibord",
fap_version="1.0",
fap_description="CAN Sniffer monitor via UART from RPI5",
fap_version="2.0",
fap_description="Dynamic multi-page RPI5 monitor with CAN stats, UPS, system info and actions",
fap_icon="A:/icons/gpio_50.png",
)

View File

@@ -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");
}