mirror of
https://github.com/markqvist/RNode_Firmware.git
synced 2026-04-27 22:35:36 +00:00
Refactors IMULogger to a general sensor data logger with tagged CSV format: ms,type,d0,d1,...,d8 Types: A=accel, G=gyro, M=mag, S=step, W=wrist_tilt, P=GPS, T=touch Each sensor pushes its own tagged sample independently instead of the old combined IMU-only row format. GPS logged at 1Hz when active, step/tilt events logged on occurrence, touch logged on press. Adds Data Logger switch to the Settings screen with live sample count and duration display. Reorders includes so IMULogger.h is available to sensor callbacks.
1445 lines
60 KiB
C++
1445 lines
60 KiB
C++
// R-Watch GUI — LVGL integration for T-Watch Ultimate
|
|
// Tileview navigation with watch face, radio status, GPS, messages, settings
|
|
// Requires: CO5300.h (display), XL9555.h (power control), DRV2605.h (haptic)
|
|
|
|
#ifndef GUI_H
|
|
#define GUI_H
|
|
|
|
#if BOARD_MODEL == BOARD_TWATCH_ULT
|
|
|
|
#include <lvgl.h>
|
|
|
|
// Custom fonts: generated with lv_font_conv --no-compress from Montserrat Bold
|
|
// IMPORTANT: must use --no-compress or set LV_USE_FONT_COMPRESSED=1 in lv_conf.h
|
|
namespace _f96 {
|
|
#include "Fonts/montserrat_bold_96.c"
|
|
}
|
|
namespace _f28 {
|
|
#include "Fonts/montserrat_bold_28.c"
|
|
}
|
|
static const lv_font_t &font_time = _f96::montserrat_bold_96;
|
|
static const lv_font_t &font_mid = _f28::montserrat_bold_28;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Color palette (24-bit hex for lv_color_hex)
|
|
// ---------------------------------------------------------------------------
|
|
#define GUI_COL_BLACK 0x000000
|
|
#define GUI_COL_WHITE 0xFFFFEF // Bone white
|
|
#define GUI_COL_DIM 0x404040 // Dividers, inactive labels
|
|
#define GUI_COL_MID 0x808080 // Secondary text
|
|
#define GUI_COL_AMBER 0xFFA500 // LoRa / radio accent
|
|
#define GUI_COL_TEAL 0x00FFC0 // GPS accent
|
|
#define GUI_COL_BLUE 0x4080FF // BLE accent
|
|
#define GUI_COL_RED 0xFF0000 // Warnings
|
|
#define GUI_COL_GREEN 0x00FF00 // Confirmations
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Layout constants (410x502 display)
|
|
// ---------------------------------------------------------------------------
|
|
#define GUI_W CO5300_WIDTH
|
|
#define GUI_H CO5300_HEIGHT
|
|
#define GUI_PAD 24
|
|
|
|
// Watch face zones (adjusted for 96px time font)
|
|
#define GUI_STATUS_Y 10
|
|
#define GUI_TIME_Y 40
|
|
#define GUI_DATE_Y 150
|
|
#define GUI_RULE1_Y 190
|
|
#define GUI_COMP_Y 200
|
|
#define GUI_COMP_H 90
|
|
#define GUI_RULE2_Y 295
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LVGL core objects
|
|
// ---------------------------------------------------------------------------
|
|
static lv_display_t *gui_display = NULL;
|
|
static lv_indev_t *gui_indev = NULL;
|
|
|
|
// Full-frame double buffer in PSRAM — tear-free rendering.
|
|
// LVGL only re-renders dirty areas within the buffer but always
|
|
// flushes the complete frame (~18ms via DMA SPI, CPU yields).
|
|
// Two buffers: 410*502*2 = 411,640 bytes each (823KB total).
|
|
#define GUI_BUF_LINES GUI_H
|
|
static uint8_t *gui_buf1 = NULL;
|
|
static uint8_t *gui_buf2 = NULL;
|
|
|
|
// Tileview and tiles
|
|
static lv_obj_t *gui_tileview = NULL;
|
|
static lv_obj_t *gui_tile_watch = NULL;
|
|
static lv_obj_t *gui_tile_radio = NULL;
|
|
static lv_obj_t *gui_tile_gps = NULL;
|
|
static lv_obj_t *gui_tile_msg = NULL;
|
|
static lv_obj_t *gui_tile_set = NULL;
|
|
|
|
// Convenience alias for display_unblank invalidation
|
|
static lv_obj_t *gui_screen = NULL;
|
|
|
|
// Watch face widgets
|
|
static lv_obj_t *gui_mode_label = NULL;
|
|
static lv_obj_t *gui_batt_label = NULL;
|
|
static lv_obj_t *gui_time_label = NULL;
|
|
static lv_obj_t *gui_date_label = NULL;
|
|
static lv_obj_t *gui_lora_value = NULL;
|
|
static lv_obj_t *gui_lora_label = NULL;
|
|
static lv_obj_t *gui_gps_value = NULL;
|
|
static lv_obj_t *gui_gps_label = NULL;
|
|
static lv_obj_t *gui_batt_value = NULL; // battery detail in complications
|
|
static lv_obj_t *gui_batt_detail = NULL;
|
|
static lv_obj_t *gui_step_label = NULL; // step count below complications
|
|
|
|
// Radio status widgets
|
|
static lv_obj_t *gui_radio_freq = NULL;
|
|
static lv_obj_t *gui_radio_params = NULL;
|
|
static lv_obj_t *gui_radio_rssi_bar = NULL;
|
|
static lv_obj_t *gui_radio_rssi_lbl = NULL;
|
|
static lv_obj_t *gui_radio_util = NULL;
|
|
static lv_obj_t *gui_radio_ble = NULL;
|
|
static lv_obj_t *gui_radio_pkts = NULL;
|
|
|
|
// Radio screen additional widgets
|
|
static lv_obj_t *gui_radio_temp = NULL;
|
|
static lv_obj_t *gui_radio_batt = NULL;
|
|
|
|
// GPS screen widgets
|
|
static lv_obj_t *gui_gps_coords = NULL;
|
|
static lv_obj_t *gui_gps_fix = NULL;
|
|
static lv_obj_t *gui_gps_alt = NULL;
|
|
static lv_obj_t *gui_gps_beacon = NULL;
|
|
|
|
// Touch input via function pointer (set by .ino after touch init)
|
|
typedef bool (*gui_touch_fn_t)(int16_t *x, int16_t *y);
|
|
static gui_touch_fn_t gui_touch_fn = NULL;
|
|
|
|
// Data update throttle
|
|
static uint32_t gui_last_data_update = 0;
|
|
#define GUI_DATA_UPDATE_MS 500
|
|
|
|
// Track current tile for haptic feedback
|
|
static uint8_t gui_last_tile_col = 1;
|
|
static uint8_t gui_last_tile_row = 1;
|
|
static bool gui_was_scrolling = false;
|
|
|
|
// Frame timing metrics
|
|
static uint32_t gui_frame_count = 0;
|
|
|
|
// Main loop profiling (set from .ino, read by metrics command)
|
|
uint32_t prof_radio_us = 0;
|
|
uint32_t prof_serial_us = 0;
|
|
uint32_t prof_display_us = 0;
|
|
uint32_t prof_pmu_us = 0;
|
|
uint32_t prof_gps_us = 0;
|
|
uint32_t prof_bt_us = 0;
|
|
uint32_t prof_imu_us = 0;
|
|
uint32_t prof_other_us = 0;
|
|
static uint32_t gui_flush_us_total = 0;
|
|
static uint32_t gui_flush_us_last = 0;
|
|
static uint32_t gui_render_us_last = 0;
|
|
static uint32_t gui_render_start = 0;
|
|
static uint32_t gui_loop_us_last = 0; // time between gui_update() calls
|
|
static uint32_t gui_loop_us_max = 0; // worst case loop time
|
|
static uint32_t gui_last_update_us = 0;
|
|
|
|
// Remote touch injection
|
|
static int16_t gui_inject_x = -1;
|
|
static int16_t gui_inject_y = -1;
|
|
static bool gui_inject_pressed = false;
|
|
static uint32_t gui_inject_until = 0; // millis() deadline for injected touch
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LVGL display flush callback
|
|
// ---------------------------------------------------------------------------
|
|
// Shadow framebuffer for screenshots (RGB565 swapped / big-endian — same as display)
|
|
uint16_t *gui_screenshot_buf = NULL;
|
|
|
|
// Forward declarations — defined in Display.h / Power.h / .ino after Gui.h is included
|
|
void display_unblank();
|
|
extern float pmu_temperature;
|
|
extern volatile uint32_t imu_step_count;
|
|
// Sensor logger toggle — set by .ino after IMULogger.h is included
|
|
typedef bool (*gui_log_toggle_fn_t)();
|
|
static gui_log_toggle_fn_t gui_log_toggle_fn = NULL;
|
|
static bool gui_imu_logging = false;
|
|
// Forward declarations for IMULogger.h variables (defined later in compilation)
|
|
extern bool imu_logging;
|
|
extern uint32_t imu_log_samples;
|
|
extern uint32_t imu_log_start_ms;
|
|
// Forward declaration for touch logging (defined in IMULogger.h)
|
|
void sensor_log_touch(int16_t x, int16_t y, bool pressed);
|
|
#ifndef PMU_TEMP_MIN
|
|
#define PMU_TEMP_MIN -30
|
|
#endif
|
|
static volatile bool gui_screenshot_pending = false; // set true to capture next frame
|
|
|
|
static void gui_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
|
|
uint16_t x1 = area->x1;
|
|
uint16_t y1 = area->y1;
|
|
uint16_t w = area->x2 - area->x1 + 1;
|
|
uint16_t h = area->y2 - area->y1 + 1;
|
|
uint16_t *pixels = (uint16_t *)px_map;
|
|
|
|
// Copy to shadow framebuffer when screenshot capture is active
|
|
if (gui_screenshot_buf && gui_screenshot_pending) {
|
|
for (uint16_t row = 0; row < h; row++) {
|
|
memcpy(&gui_screenshot_buf[(y1 + row) * GUI_W + x1],
|
|
&pixels[row * w], w * sizeof(uint16_t));
|
|
}
|
|
}
|
|
|
|
uint32_t t0 = micros();
|
|
co5300_push_pixels(x1, y1, w, h, pixels);
|
|
gui_flush_us_last = micros() - t0;
|
|
gui_flush_us_total += gui_flush_us_last;
|
|
gui_frame_count++;
|
|
lv_display_flush_ready(disp);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LVGL touch input read callback
|
|
// ---------------------------------------------------------------------------
|
|
static void gui_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
|
|
// Check for injected remote touch first
|
|
if (gui_inject_pressed && millis() < gui_inject_until) {
|
|
data->point.x = gui_inject_x;
|
|
data->point.y = gui_inject_y;
|
|
data->state = LV_INDEV_STATE_PRESSED;
|
|
last_unblank_event = millis();
|
|
return;
|
|
}
|
|
gui_inject_pressed = false;
|
|
|
|
// Real touch hardware
|
|
int16_t tx, ty;
|
|
if (gui_touch_fn && gui_touch_fn(&tx, &ty)) {
|
|
data->point.x = tx;
|
|
data->point.y = ty;
|
|
data->state = LV_INDEV_STATE_PRESSED;
|
|
last_unblank_event = millis();
|
|
#if HAS_SD
|
|
sensor_log_touch(tx, ty, true);
|
|
#endif
|
|
} else {
|
|
data->state = LV_INDEV_STATE_RELEASED;
|
|
}
|
|
}
|
|
|
|
void gui_set_touch_handler(gui_touch_fn_t fn) {
|
|
gui_touch_fn = fn;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
static void gui_style_black_container(lv_obj_t *obj) {
|
|
lv_obj_set_style_bg_color(obj, lv_color_hex(GUI_COL_BLACK), 0);
|
|
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_width(obj, 0, 0);
|
|
lv_obj_set_style_pad_all(obj, 0, 0);
|
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
|
}
|
|
|
|
static lv_obj_t *gui_create_rule(lv_obj_t *parent, lv_coord_t y) {
|
|
static lv_point_precise_t rule_pts[] = {
|
|
{GUI_PAD, 0}, {GUI_W - GUI_PAD, 0}
|
|
};
|
|
lv_obj_t *line = lv_line_create(parent);
|
|
lv_line_set_points(line, rule_pts, 2);
|
|
lv_obj_set_style_line_color(line, lv_color_hex(GUI_COL_DIM), 0);
|
|
lv_obj_set_style_line_width(line, 1, 0);
|
|
lv_obj_set_pos(line, 0, y);
|
|
return line;
|
|
}
|
|
|
|
static lv_obj_t *gui_label(lv_obj_t *parent, const lv_font_t *font,
|
|
uint32_t color, const char *text) {
|
|
lv_obj_t *lbl = lv_label_create(parent);
|
|
lv_obj_set_style_text_font(lbl, font, 0);
|
|
lv_obj_set_style_text_color(lbl, lv_color_hex(color), 0);
|
|
lv_label_set_text(lbl, text);
|
|
return lbl;
|
|
}
|
|
|
|
static lv_obj_t *gui_label_at(lv_obj_t *parent, const lv_font_t *font,
|
|
uint32_t color, const char *text,
|
|
lv_coord_t x, lv_coord_t y) {
|
|
lv_obj_t *lbl = gui_label(parent, font, color, text);
|
|
lv_obj_set_pos(lbl, x, y);
|
|
return lbl;
|
|
}
|
|
|
|
static void gui_create_complication(lv_obj_t *parent, lv_coord_t x, lv_coord_t w,
|
|
uint32_t color, const char *label_text,
|
|
lv_obj_t **value_out, lv_obj_t **label_out) {
|
|
lv_obj_t *cell = lv_obj_create(parent);
|
|
lv_obj_remove_style_all(cell);
|
|
lv_obj_set_size(cell, w, GUI_COMP_H);
|
|
lv_obj_set_pos(cell, x, 0);
|
|
|
|
lv_obj_t *val = gui_label(cell, &font_mid, color, "--");
|
|
lv_obj_set_width(val, w);
|
|
lv_obj_set_style_text_align(val, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(val, LV_ALIGN_TOP_MID, 0, 4);
|
|
|
|
lv_obj_t *lbl = gui_label(cell, &lv_font_montserrat_14, GUI_COL_DIM, label_text);
|
|
lv_obj_set_width(lbl, w);
|
|
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 46);
|
|
|
|
if (value_out) *value_out = val;
|
|
if (label_out) *label_out = lbl;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Screen: Watch Face (center tile)
|
|
// ---------------------------------------------------------------------------
|
|
static void gui_create_watchface(lv_obj_t *parent) {
|
|
gui_style_black_container(parent);
|
|
|
|
// Status bar
|
|
gui_mode_label = gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_MID, "IDLE", GUI_PAD, GUI_STATUS_Y);
|
|
gui_batt_label = gui_label(parent, &lv_font_montserrat_14, GUI_COL_MID, "--%");
|
|
lv_obj_align(gui_batt_label, LV_ALIGN_TOP_RIGHT, -GUI_PAD, GUI_STATUS_Y);
|
|
|
|
// Time (96px custom font — digits and colon only)
|
|
gui_time_label = gui_label(parent, &font_time, GUI_COL_WHITE, "00:00");
|
|
lv_obj_set_style_text_letter_space(gui_time_label, 2, 0);
|
|
lv_obj_set_width(gui_time_label, GUI_W);
|
|
lv_obj_set_style_text_align(gui_time_label, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_pos(gui_time_label, 0, GUI_TIME_Y);
|
|
|
|
// Date
|
|
gui_date_label = gui_label(parent, &font_mid, GUI_COL_MID, "--- -- ---");
|
|
lv_obj_set_width(gui_date_label, GUI_W);
|
|
lv_obj_set_style_text_align(gui_date_label, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_pos(gui_date_label, 0, GUI_DATE_Y);
|
|
|
|
// Rule 1
|
|
gui_create_rule(parent, GUI_RULE1_Y);
|
|
|
|
// Complications
|
|
lv_obj_t *comp = lv_obj_create(parent);
|
|
lv_obj_remove_style_all(comp);
|
|
lv_obj_set_size(comp, GUI_W, GUI_COMP_H);
|
|
lv_obj_set_pos(comp, 0, GUI_COMP_Y);
|
|
lv_obj_clear_flag(comp, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
int cw = (GUI_W - GUI_PAD * 2) / 3;
|
|
gui_create_complication(comp, GUI_PAD, cw, GUI_COL_AMBER, "LoRa", &gui_lora_value, &gui_lora_label);
|
|
gui_create_complication(comp, GUI_PAD + cw, cw, GUI_COL_TEAL, "GPS", &gui_gps_value, &gui_gps_label);
|
|
gui_create_complication(comp, GUI_PAD + cw * 2, cw, GUI_COL_WHITE, "Batt", &gui_batt_value, &gui_batt_detail);
|
|
|
|
// Rule 2
|
|
gui_create_rule(parent, GUI_RULE2_Y);
|
|
|
|
// Step counter below complications
|
|
gui_step_label = gui_label(parent, &lv_font_montserrat_20, GUI_COL_DIM, "");
|
|
lv_obj_set_width(gui_step_label, GUI_W);
|
|
lv_obj_set_style_text_align(gui_step_label, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_pos(gui_step_label, 0, GUI_RULE2_Y + 15);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Screen: Radio Status (top tile — swipe down from watch face)
|
|
// ---------------------------------------------------------------------------
|
|
static void gui_create_radio_screen(lv_obj_t *parent) {
|
|
gui_style_black_container(parent);
|
|
|
|
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "RADIO STATUS", GUI_PAD, 12);
|
|
|
|
// Frequency
|
|
gui_radio_freq = gui_label_at(parent, &font_mid, GUI_COL_AMBER, "--- MHz", GUI_PAD, 40);
|
|
|
|
// LoRa parameters
|
|
gui_radio_params = gui_label_at(parent, &font_mid, GUI_COL_MID, "SF- BW- CR-", GUI_PAD, 80);
|
|
|
|
// RSSI
|
|
gui_create_rule(parent, 115);
|
|
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "RSSI", GUI_PAD, 125);
|
|
gui_radio_rssi_lbl = gui_label(parent, &font_mid, GUI_COL_AMBER, "---");
|
|
lv_obj_align(gui_radio_rssi_lbl, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 122);
|
|
|
|
lv_obj_t *bar = lv_bar_create(parent);
|
|
lv_obj_set_size(bar, GUI_W - GUI_PAD * 2, 20);
|
|
lv_obj_set_pos(bar, GUI_PAD, 150);
|
|
lv_bar_set_range(bar, -140, -40);
|
|
lv_bar_set_value(bar, -140, LV_ANIM_OFF);
|
|
lv_obj_set_style_bg_color(bar, lv_color_hex(GUI_COL_DIM), 0);
|
|
lv_obj_set_style_bg_color(bar, lv_color_hex(GUI_COL_AMBER), LV_PART_INDICATOR);
|
|
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_INDICATOR);
|
|
gui_radio_rssi_bar = bar;
|
|
|
|
// Channel utilization
|
|
gui_create_rule(parent, 185);
|
|
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "CHANNEL", GUI_PAD, 198);
|
|
gui_radio_util = gui_label(parent, &font_mid, GUI_COL_MID, "-- %");
|
|
lv_obj_align(gui_radio_util, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 195);
|
|
|
|
// BLE status
|
|
gui_create_rule(parent, 230);
|
|
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "BLE", GUI_PAD, 243);
|
|
gui_radio_ble = gui_label(parent, &font_mid, GUI_COL_BLUE, "---");
|
|
lv_obj_align(gui_radio_ble, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 240);
|
|
|
|
// Packet counts
|
|
gui_create_rule(parent, 275);
|
|
gui_radio_pkts = gui_label_at(parent, &font_mid, GUI_COL_MID,
|
|
"RX: 0 TX: 0", GUI_PAD, 290);
|
|
|
|
// Battery and temperature
|
|
gui_create_rule(parent, 325);
|
|
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "BATTERY", GUI_PAD, 338);
|
|
gui_radio_batt = gui_label(parent, &font_mid, GUI_COL_MID, "---");
|
|
lv_obj_align(gui_radio_batt, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 335);
|
|
|
|
gui_create_rule(parent, 370);
|
|
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "TEMPERATURE", GUI_PAD, 383);
|
|
gui_radio_temp = gui_label(parent, &font_mid, GUI_COL_MID, "---");
|
|
lv_obj_align(gui_radio_temp, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 380);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Screen: GPS (right tile — swipe right from watch face)
|
|
// ---------------------------------------------------------------------------
|
|
static void gui_create_gps_screen(lv_obj_t *parent) {
|
|
gui_style_black_container(parent);
|
|
|
|
lv_obj_t *gps_title = gui_label(parent, &lv_font_montserrat_14, GUI_COL_DIM, "GPS");
|
|
lv_obj_set_width(gps_title, GUI_W);
|
|
lv_obj_set_style_text_align(gps_title, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_pos(gps_title, 0, 12);
|
|
|
|
// Coordinates — centered for rounded corner clearance
|
|
gui_gps_coords = gui_label(parent, &font_mid, GUI_COL_TEAL, "-- --");
|
|
lv_obj_set_width(gui_gps_coords, GUI_W);
|
|
lv_obj_set_style_text_align(gui_gps_coords, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_pos(gui_gps_coords, 0, 40);
|
|
|
|
// Fix quality
|
|
gui_create_rule(parent, 110);
|
|
gui_gps_fix = gui_label_at(parent, &font_mid, GUI_COL_MID,
|
|
"Sats: -- HDOP: --", GUI_PAD, 125);
|
|
|
|
// Altitude and speed
|
|
gui_create_rule(parent, 160);
|
|
gui_gps_alt = gui_label_at(parent, &font_mid, GUI_COL_MID,
|
|
"Alt: -- Spd: --", GUI_PAD, 175);
|
|
|
|
// Beacon status
|
|
gui_create_rule(parent, 215);
|
|
gui_gps_beacon = gui_label_at(parent, &font_mid, GUI_COL_AMBER,
|
|
"Beacon: --", GUI_PAD, 230);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Screen: Messages (left tile — swipe left from watch face)
|
|
// ---------------------------------------------------------------------------
|
|
static void gui_create_msg_screen(lv_obj_t *parent) {
|
|
gui_style_black_container(parent);
|
|
gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "MESSAGES", GUI_PAD, 12);
|
|
lv_obj_t *lbl = gui_label(parent, &font_mid, GUI_COL_DIM, "No messages");
|
|
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Screen: Settings (bottom tile — swipe up from watch face)
|
|
// ---------------------------------------------------------------------------
|
|
static lv_obj_t *gui_set_disp_slider = NULL;
|
|
static lv_obj_t *gui_set_disp_val = NULL;
|
|
static lv_obj_t *gui_set_bcn_roller = NULL;
|
|
static lv_obj_t *gui_set_gps_roller = NULL;
|
|
static lv_obj_t *gui_set_bcn_sw = NULL;
|
|
static lv_obj_t *gui_set_log_sw = NULL;
|
|
static lv_obj_t *gui_set_log_status = NULL;
|
|
|
|
static void gui_set_disp_cb(lv_event_t *e) {
|
|
lv_obj_t *slider = (lv_obj_t *)lv_event_get_target(e);
|
|
int32_t val = lv_slider_get_value(slider);
|
|
display_blanking_timeout = (uint32_t)val * 1000;
|
|
char buf[8]; snprintf(buf, sizeof(buf), "%lds", (long)val);
|
|
lv_label_set_text(gui_set_disp_val, buf);
|
|
EEPROM.write(config_addr(ADDR_CONF_DISP_TIMEOUT), (uint8_t)val);
|
|
EEPROM.commit();
|
|
}
|
|
|
|
static void gui_set_bcn_en_cb(lv_event_t *e) {
|
|
lv_obj_t *sw = (lv_obj_t *)lv_event_get_target(e);
|
|
beacon_enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
|
EEPROM.write(config_addr(ADDR_CONF_BCN_EN), beacon_enabled ? 1 : 0);
|
|
EEPROM.commit();
|
|
}
|
|
|
|
static void gui_set_bcn_int_cb(lv_event_t *e) {
|
|
lv_obj_t *roller = (lv_obj_t *)lv_event_get_target(e);
|
|
uint16_t idx = lv_roller_get_selected(roller);
|
|
if (idx < BEACON_INTERVAL_OPTIONS_COUNT) {
|
|
beacon_interval_ms = beacon_interval_options[idx];
|
|
EEPROM.write(config_addr(ADDR_CONF_BCN_INT), (uint8_t)idx);
|
|
EEPROM.commit();
|
|
}
|
|
}
|
|
|
|
static void gui_set_gps_model_cb(lv_event_t *e) {
|
|
lv_obj_t *roller = (lv_obj_t *)lv_event_get_target(e);
|
|
uint16_t idx = lv_roller_get_selected(roller);
|
|
if (idx < GPS_MODEL_OPTIONS_COUNT) {
|
|
gps_set_dynamic_model(idx);
|
|
EEPROM.write(config_addr(ADDR_CONF_GPS_MODEL), (uint8_t)idx);
|
|
EEPROM.commit();
|
|
}
|
|
}
|
|
|
|
static void gui_set_log_cb(lv_event_t *e) {
|
|
bool on = lv_obj_has_state(gui_set_log_sw, LV_STATE_CHECKED);
|
|
if (on) {
|
|
if (gui_log_toggle_fn) gui_log_toggle_fn();
|
|
} else {
|
|
if (gui_log_toggle_fn && imu_logging) gui_log_toggle_fn();
|
|
}
|
|
}
|
|
|
|
static void gui_create_settings_screen(lv_obj_t *parent) {
|
|
gui_style_black_container(parent);
|
|
|
|
// Child container for settings content — do NOT set flex on the tile itself
|
|
lv_obj_t *cont = lv_obj_create(parent);
|
|
lv_obj_remove_style_all(cont);
|
|
lv_obj_set_size(cont, GUI_W, GUI_H);
|
|
lv_obj_set_style_bg_color(cont, lv_color_hex(GUI_COL_BLACK), 0);
|
|
lv_obj_set_style_bg_opa(cont, LV_OPA_COVER, 0);
|
|
lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
// Title
|
|
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_DIM, "SETTINGS", GUI_PAD, 12);
|
|
|
|
// --- Row 1: Display timeout (y=50) ---
|
|
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Display timeout", GUI_PAD, 55);
|
|
gui_set_disp_slider = lv_slider_create(cont);
|
|
lv_obj_set_size(gui_set_disp_slider, 180, 12);
|
|
lv_obj_set_pos(gui_set_disp_slider, GUI_PAD, 80);
|
|
lv_slider_set_range(gui_set_disp_slider, 5, 60);
|
|
lv_slider_set_value(gui_set_disp_slider, (int32_t)(display_blanking_timeout / 1000), LV_ANIM_OFF);
|
|
lv_obj_set_style_bg_color(gui_set_disp_slider, lv_color_hex(GUI_COL_DIM), 0);
|
|
lv_obj_set_style_bg_color(gui_set_disp_slider, lv_color_hex(GUI_COL_AMBER), LV_PART_INDICATOR);
|
|
lv_obj_set_style_bg_color(gui_set_disp_slider, lv_color_hex(GUI_COL_WHITE), LV_PART_KNOB);
|
|
lv_obj_set_style_pad_all(gui_set_disp_slider, 4, LV_PART_KNOB);
|
|
lv_obj_add_event_cb(gui_set_disp_slider, gui_set_disp_cb, LV_EVENT_VALUE_CHANGED, NULL);
|
|
char disp_buf[8]; snprintf(disp_buf, sizeof(disp_buf), "%lds", (long)(display_blanking_timeout / 1000));
|
|
gui_set_disp_val = gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_WHITE, disp_buf, GUI_PAD + 200, 75);
|
|
|
|
gui_create_rule(cont, 110);
|
|
|
|
// --- Row 2: Beacon enable (y=120) ---
|
|
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Beacon", GUI_PAD, 125);
|
|
gui_set_bcn_sw = lv_switch_create(cont);
|
|
lv_obj_set_pos(gui_set_bcn_sw, GUI_W - GUI_PAD - 50, 120);
|
|
lv_obj_set_size(gui_set_bcn_sw, 50, 26);
|
|
if (beacon_enabled) lv_obj_add_state(gui_set_bcn_sw, LV_STATE_CHECKED);
|
|
lv_obj_set_style_bg_color(gui_set_bcn_sw, lv_color_hex(GUI_COL_DIM), 0);
|
|
lv_obj_set_style_bg_color(gui_set_bcn_sw, lv_color_hex(GUI_COL_AMBER), LV_PART_INDICATOR | LV_STATE_CHECKED);
|
|
lv_obj_add_event_cb(gui_set_bcn_sw, gui_set_bcn_en_cb, LV_EVENT_VALUE_CHANGED, NULL);
|
|
|
|
gui_create_rule(cont, 160);
|
|
|
|
// --- Row 3: Beacon interval (y=170) ---
|
|
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Beacon interval", GUI_PAD, 175);
|
|
gui_set_bcn_roller = lv_roller_create(cont);
|
|
lv_roller_set_options(gui_set_bcn_roller, "10s\n30s\n1min\n5min\n10min", LV_ROLLER_MODE_NORMAL);
|
|
lv_obj_set_pos(gui_set_bcn_roller, GUI_W - GUI_PAD - 100, 170);
|
|
lv_obj_set_size(gui_set_bcn_roller, 100, 60);
|
|
lv_obj_set_style_bg_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_BLACK), 0);
|
|
lv_obj_set_style_text_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_WHITE), 0);
|
|
lv_obj_set_style_text_font(gui_set_bcn_roller, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_bg_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_AMBER), LV_PART_SELECTED);
|
|
lv_obj_set_style_text_color(gui_set_bcn_roller, lv_color_hex(GUI_COL_BLACK), LV_PART_SELECTED);
|
|
// Set initial selection from current beacon_interval_ms
|
|
for (uint8_t i = 0; i < BEACON_INTERVAL_OPTIONS_COUNT; i++) {
|
|
if (beacon_interval_options[i] == beacon_interval_ms) {
|
|
lv_roller_set_selected(gui_set_bcn_roller, i, LV_ANIM_OFF);
|
|
break;
|
|
}
|
|
}
|
|
lv_obj_add_event_cb(gui_set_bcn_roller, gui_set_bcn_int_cb, LV_EVENT_VALUE_CHANGED, NULL);
|
|
|
|
gui_create_rule(cont, 245);
|
|
|
|
// --- Row 4: GPS dynamic model (y=255) ---
|
|
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "GPS model", GUI_PAD, 260);
|
|
gui_set_gps_roller = lv_roller_create(cont);
|
|
lv_roller_set_options(gui_set_gps_roller, "Portable\nStationary\nPedestrian\nAutomotive", LV_ROLLER_MODE_NORMAL);
|
|
lv_obj_set_pos(gui_set_gps_roller, GUI_W - GUI_PAD - 140, 255);
|
|
lv_obj_set_size(gui_set_gps_roller, 140, 60);
|
|
lv_obj_set_style_bg_color(gui_set_gps_roller, lv_color_hex(GUI_COL_BLACK), 0);
|
|
lv_obj_set_style_text_color(gui_set_gps_roller, lv_color_hex(GUI_COL_WHITE), 0);
|
|
lv_obj_set_style_text_font(gui_set_gps_roller, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_bg_color(gui_set_gps_roller, lv_color_hex(GUI_COL_AMBER), LV_PART_SELECTED);
|
|
lv_obj_set_style_text_color(gui_set_gps_roller, lv_color_hex(GUI_COL_BLACK), LV_PART_SELECTED);
|
|
lv_roller_set_selected(gui_set_gps_roller, gps_dynamic_model, LV_ANIM_OFF);
|
|
lv_obj_add_event_cb(gui_set_gps_roller, gui_set_gps_model_cb, LV_EVENT_VALUE_CHANGED, NULL);
|
|
|
|
// --- Sensor logger ---
|
|
gui_create_rule(cont, 320);
|
|
gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_MID, "Data logger", GUI_PAD, 335);
|
|
gui_set_log_sw = lv_switch_create(cont);
|
|
lv_obj_set_pos(gui_set_log_sw, GUI_W - GUI_PAD - 50, 330);
|
|
lv_obj_set_size(gui_set_log_sw, 50, 26);
|
|
lv_obj_set_style_bg_color(gui_set_log_sw, lv_color_hex(GUI_COL_DIM), 0);
|
|
lv_obj_set_style_bg_color(gui_set_log_sw, lv_color_hex(GUI_COL_GREEN), LV_PART_INDICATOR | LV_STATE_CHECKED);
|
|
lv_obj_add_event_cb(gui_set_log_sw, gui_set_log_cb, LV_EVENT_VALUE_CHANGED, NULL);
|
|
gui_set_log_status = gui_label_at(cont, &lv_font_montserrat_14, GUI_COL_DIM, "", GUI_PAD, 365);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tileview change event — haptic feedback
|
|
// ---------------------------------------------------------------------------
|
|
static void gui_tile_change_cb(lv_event_t *e) {
|
|
lv_obj_t *tv = (lv_obj_t *)lv_event_get_target(e);
|
|
lv_obj_t *tile = (lv_obj_t *)lv_tileview_get_tile_active(tv);
|
|
if (!tile) return;
|
|
|
|
// Get tile position
|
|
lv_coord_t col = lv_obj_get_x(tile) / GUI_W;
|
|
lv_coord_t row = lv_obj_get_y(tile) / GUI_H;
|
|
|
|
if (col != gui_last_tile_col || row != gui_last_tile_row) {
|
|
gui_last_tile_col = col;
|
|
gui_last_tile_row = row;
|
|
#if defined(DRV2605_H)
|
|
if (drv2605_ready) drv2605_play(HAPTIC_TRANSITION);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Update all screen data from firmware globals
|
|
// ---------------------------------------------------------------------------
|
|
static const char *gui_month_names[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN",
|
|
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
|
|
|
|
static bool gui_is_scrolling() {
|
|
if (!gui_tileview) return false;
|
|
lv_obj_t *tile = (lv_obj_t *)lv_tileview_get_tile_active(gui_tileview);
|
|
if (!tile) return false;
|
|
// Check if scroll position doesn't match tile alignment
|
|
lv_coord_t sx = lv_obj_get_scroll_x(gui_tileview);
|
|
lv_coord_t sy = lv_obj_get_scroll_y(gui_tileview);
|
|
lv_coord_t tx = lv_obj_get_x(tile);
|
|
lv_coord_t ty = lv_obj_get_y(tile);
|
|
return (sx != tx || sy != ty);
|
|
}
|
|
|
|
static void gui_update_data() {
|
|
if (!gui_time_label) return;
|
|
|
|
// Skip data updates during scroll animation — frees CPU for rendering
|
|
if (gui_is_scrolling()) return;
|
|
|
|
uint32_t now = millis();
|
|
if (now - gui_last_data_update < GUI_DATA_UPDATE_MS) return;
|
|
gui_last_data_update = now;
|
|
|
|
// Detect current tile — only update visible screen's labels
|
|
lv_obj_t *cur_tile = (lv_obj_t *)lv_tileview_get_tile_active(gui_tileview);
|
|
bool on_watch = (cur_tile == gui_tile_watch);
|
|
bool on_radio = (cur_tile == gui_tile_radio);
|
|
bool on_gps = (cur_tile == gui_tile_gps);
|
|
bool on_settings = (cur_tile == gui_tile_set);
|
|
|
|
// ---- Watch face ----
|
|
// Time always updates (cheap, changes rarely)
|
|
lv_label_set_text_fmt(gui_time_label, "%02d:%02d", rtc_hour, rtc_minute);
|
|
|
|
if (!on_watch && !on_radio && !on_gps && !on_settings) return;
|
|
|
|
// ---- Watch face details (only when visible) ----
|
|
if (on_watch) {
|
|
|
|
#if HAS_RTC == true
|
|
if (rtc_year > 0) {
|
|
const char *mon = (rtc_month >= 1 && rtc_month <= 12) ? gui_month_names[rtc_month - 1] : "---";
|
|
lv_label_set_text_fmt(gui_date_label, "%d %s %d", rtc_day, mon, rtc_year);
|
|
}
|
|
#endif
|
|
|
|
// Mode indicator
|
|
if (bt_state == BT_STATE_CONNECTED) {
|
|
lv_label_set_text(gui_mode_label, "MODEM");
|
|
lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_BLUE), 0);
|
|
}
|
|
#if HAS_GPS == true
|
|
else if (beacon_mode_active) {
|
|
lv_label_set_text(gui_mode_label, "BEACON");
|
|
lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_AMBER), 0);
|
|
}
|
|
#endif
|
|
else if (radio_online) {
|
|
lv_label_set_text(gui_mode_label, "RADIO");
|
|
lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_MID), 0);
|
|
} else {
|
|
lv_label_set_text(gui_mode_label, "IDLE");
|
|
lv_obj_set_style_text_color(gui_mode_label, lv_color_hex(GUI_COL_DIM), 0);
|
|
}
|
|
|
|
// Battery
|
|
if (battery_state == BATTERY_STATE_CHARGING) {
|
|
lv_label_set_text_fmt(gui_batt_label, "%d%% +", (int)battery_percent);
|
|
} else {
|
|
lv_label_set_text_fmt(gui_batt_label, "%d%%", (int)battery_percent);
|
|
}
|
|
lv_obj_align(gui_batt_label, LV_ALIGN_TOP_RIGHT, -GUI_PAD, GUI_STATUS_Y);
|
|
|
|
// LoRa complication
|
|
if (radio_online) {
|
|
if (last_rssi > -292) {
|
|
lv_label_set_text_fmt(gui_lora_value, "%d", last_rssi);
|
|
} else {
|
|
lv_label_set_text(gui_lora_value, "---");
|
|
}
|
|
lv_obj_set_style_text_color(gui_lora_value, lv_color_hex(GUI_COL_AMBER), 0);
|
|
} else {
|
|
lv_label_set_text(gui_lora_value, "OFF");
|
|
lv_obj_set_style_text_color(gui_lora_value, lv_color_hex(GUI_COL_DIM), 0);
|
|
}
|
|
|
|
// GPS complication — color by fix quality
|
|
#if HAS_GPS == true
|
|
if (gps_sats > 0) {
|
|
lv_label_set_text_fmt(gui_gps_value, "%d sats", gps_sats);
|
|
uint32_t gps_col = (gps_hdop < 5.0) ? GUI_COL_TEAL :
|
|
(gps_hdop < 15.0) ? GUI_COL_AMBER : GUI_COL_MID;
|
|
lv_obj_set_style_text_color(gui_gps_value, lv_color_hex(gps_col), 0);
|
|
} else {
|
|
lv_label_set_text(gui_gps_value, "no fix");
|
|
lv_obj_set_style_text_color(gui_gps_value, lv_color_hex(GUI_COL_DIM), 0);
|
|
}
|
|
#endif
|
|
|
|
// Step counter
|
|
if (gui_step_label) {
|
|
if (imu_step_count > 0) {
|
|
lv_label_set_text_fmt(gui_step_label, "%lu steps", imu_step_count);
|
|
lv_obj_set_style_text_color(gui_step_label, lv_color_hex(GUI_COL_MID), 0);
|
|
} else {
|
|
lv_label_set_text(gui_step_label, "");
|
|
}
|
|
}
|
|
|
|
// Battery complication — voltage and state
|
|
if (battery_state == BATTERY_STATE_CHARGING) {
|
|
lv_label_set_text_fmt(gui_batt_value, "%.2fV", battery_voltage);
|
|
lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(GUI_COL_GREEN), 0);
|
|
lv_label_set_text(gui_batt_detail, "Charging");
|
|
lv_obj_set_style_text_color(gui_batt_detail, lv_color_hex(GUI_COL_GREEN), 0);
|
|
} else if (battery_state == BATTERY_STATE_CHARGED) {
|
|
lv_label_set_text_fmt(gui_batt_value, "%.2fV", battery_voltage);
|
|
lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(GUI_COL_GREEN), 0);
|
|
lv_label_set_text(gui_batt_detail, "Full");
|
|
lv_obj_set_style_text_color(gui_batt_detail, lv_color_hex(GUI_COL_GREEN), 0);
|
|
} else if (battery_percent < 15) {
|
|
lv_label_set_text_fmt(gui_batt_value, "%.2fV", battery_voltage);
|
|
lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(GUI_COL_RED), 0);
|
|
lv_label_set_text(gui_batt_detail, "Low");
|
|
lv_obj_set_style_text_color(gui_batt_detail, lv_color_hex(GUI_COL_RED), 0);
|
|
} else {
|
|
lv_label_set_text_fmt(gui_batt_value, "%.2fV", battery_voltage);
|
|
lv_obj_set_style_text_color(gui_batt_value, lv_color_hex(GUI_COL_WHITE), 0);
|
|
lv_label_set_text(gui_batt_detail, "Batt");
|
|
lv_obj_set_style_text_color(gui_batt_detail, lv_color_hex(GUI_COL_DIM), 0);
|
|
}
|
|
|
|
} // end on_watch
|
|
|
|
// ---- Radio status screen (only when visible) ----
|
|
if (on_radio && gui_radio_freq) {
|
|
if (lora_freq > 0) {
|
|
lv_label_set_text_fmt(gui_radio_freq, "%.3f MHz", (float)lora_freq / 1000000.0);
|
|
} else {
|
|
lv_label_set_text(gui_radio_freq, "--- MHz");
|
|
}
|
|
|
|
lv_label_set_text_fmt(gui_radio_params, "SF%d BW%lu CR4/%d",
|
|
lora_sf, lora_bw / 1000, lora_cr);
|
|
|
|
if (radio_online && last_rssi > -292) {
|
|
lv_bar_set_value(gui_radio_rssi_bar, last_rssi, LV_ANIM_ON);
|
|
lv_label_set_text_fmt(gui_radio_rssi_lbl, "%d dBm", last_rssi);
|
|
} else {
|
|
lv_bar_set_value(gui_radio_rssi_bar, -140, LV_ANIM_OFF);
|
|
lv_label_set_text(gui_radio_rssi_lbl, "---");
|
|
}
|
|
|
|
lv_label_set_text_fmt(gui_radio_util, "%.1f%%",
|
|
(float)local_channel_util / 100.0);
|
|
|
|
if (bt_state == BT_STATE_CONNECTED) {
|
|
lv_label_set_text(gui_radio_ble, "Connected");
|
|
lv_obj_set_style_text_color(gui_radio_ble, lv_color_hex(GUI_COL_BLUE), 0);
|
|
} else if (bt_state == BT_STATE_ON) {
|
|
lv_label_set_text(gui_radio_ble, "Advertising");
|
|
lv_obj_set_style_text_color(gui_radio_ble, lv_color_hex(GUI_COL_MID), 0);
|
|
} else {
|
|
lv_label_set_text(gui_radio_ble, "Off");
|
|
lv_obj_set_style_text_color(gui_radio_ble, lv_color_hex(GUI_COL_DIM), 0);
|
|
}
|
|
|
|
lv_label_set_text_fmt(gui_radio_pkts, "RX: %lu TX: %lu", stat_rx, stat_tx);
|
|
|
|
// Battery detail
|
|
if (gui_radio_batt) {
|
|
lv_label_set_text_fmt(gui_radio_batt, "%.2fV %d%%", battery_voltage, (int)battery_percent);
|
|
lv_obj_align(gui_radio_batt, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 335);
|
|
}
|
|
|
|
// Temperature
|
|
if (gui_radio_temp) {
|
|
if (pmu_temperature > (PMU_TEMP_MIN - 1)) {
|
|
lv_label_set_text_fmt(gui_radio_temp, "%.1f C", pmu_temperature);
|
|
} else {
|
|
lv_label_set_text(gui_radio_temp, "---");
|
|
}
|
|
lv_obj_align(gui_radio_temp, LV_ALIGN_TOP_RIGHT, -GUI_PAD, 380);
|
|
}
|
|
}
|
|
|
|
// ---- GPS screen (only when visible — float formatting is expensive) ----
|
|
#if HAS_GPS == true
|
|
if (on_gps && gui_gps_coords) {
|
|
bool good_fix = (gps_sats >= 4 && gps_hdop < 10.0 && gps_lat != 0.0);
|
|
bool any_fix = (gps_sats > 0 && gps_lat != 0.0);
|
|
|
|
// Coordinates — show when any fix, but dim when HDOP is poor
|
|
if (any_fix) {
|
|
lv_label_set_text_fmt(gui_gps_coords, "%.6f\n%.6f", gps_lat, gps_lon);
|
|
lv_obj_set_style_text_color(gui_gps_coords,
|
|
lv_color_hex(good_fix ? GUI_COL_TEAL : GUI_COL_MID), 0);
|
|
} else {
|
|
lv_label_set_text(gui_gps_coords, "No fix");
|
|
lv_obj_set_style_text_color(gui_gps_coords, lv_color_hex(GUI_COL_DIM), 0);
|
|
}
|
|
|
|
// Fix quality — color HDOP by quality
|
|
uint32_t hdop_col = (gps_hdop < 2.0) ? GUI_COL_GREEN :
|
|
(gps_hdop < 5.0) ? GUI_COL_TEAL :
|
|
(gps_hdop < 10.0) ? GUI_COL_AMBER : GUI_COL_RED;
|
|
lv_label_set_text_fmt(gui_gps_fix, "Sats: %d HDOP: %.1f", gps_sats, gps_hdop);
|
|
lv_obj_set_style_text_color(gui_gps_fix, lv_color_hex(hdop_col), 0);
|
|
|
|
// Alt/Speed — suppress speed when HDOP is poor (it's just noise)
|
|
if (good_fix) {
|
|
lv_label_set_text_fmt(gui_gps_alt, "Alt: %.0fm Spd: %.1fkm/h", gps_alt, gps_speed);
|
|
lv_obj_set_style_text_color(gui_gps_alt, lv_color_hex(GUI_COL_MID), 0);
|
|
} else if (any_fix) {
|
|
lv_label_set_text_fmt(gui_gps_alt, "Alt: %.0fm Spd: ---", gps_alt);
|
|
lv_obj_set_style_text_color(gui_gps_alt, lv_color_hex(GUI_COL_DIM), 0);
|
|
} else {
|
|
lv_label_set_text(gui_gps_alt, "Alt: --- Spd: ---");
|
|
lv_obj_set_style_text_color(gui_gps_alt, lv_color_hex(GUI_COL_DIM), 0);
|
|
}
|
|
|
|
if (beacon_mode_active) {
|
|
lv_label_set_text(gui_gps_beacon, "Beacon: active");
|
|
lv_obj_set_style_text_color(gui_gps_beacon, lv_color_hex(GUI_COL_AMBER), 0);
|
|
} else {
|
|
lv_label_set_text(gui_gps_beacon, "Beacon: off");
|
|
lv_obj_set_style_text_color(gui_gps_beacon, lv_color_hex(GUI_COL_DIM), 0);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// ---- Settings screen (logger status) ----
|
|
if (on_settings && gui_set_log_status) {
|
|
#if HAS_SD
|
|
if (imu_logging) {
|
|
uint32_t dur = (millis() - imu_log_start_ms) / 1000;
|
|
lv_label_set_text_fmt(gui_set_log_status, "%lu samples %lus", imu_log_samples, dur);
|
|
lv_obj_set_style_text_color(gui_set_log_status, lv_color_hex(GUI_COL_GREEN), 0);
|
|
if (!lv_obj_has_state(gui_set_log_sw, LV_STATE_CHECKED))
|
|
lv_obj_add_state(gui_set_log_sw, LV_STATE_CHECKED);
|
|
} else {
|
|
lv_label_set_text(gui_set_log_status, "SD card ready");
|
|
lv_obj_set_style_text_color(gui_set_log_status, lv_color_hex(GUI_COL_DIM), 0);
|
|
if (lv_obj_has_state(gui_set_log_sw, LV_STATE_CHECKED))
|
|
lv_obj_clear_state(gui_set_log_sw, LV_STATE_CHECKED);
|
|
}
|
|
#else
|
|
lv_label_set_text(gui_set_log_status, "No SD card");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Initialize LVGL and create all screens
|
|
// ---------------------------------------------------------------------------
|
|
bool gui_init() {
|
|
lv_init();
|
|
|
|
// --- Display driver ---
|
|
gui_display = lv_display_create(GUI_W, GUI_H);
|
|
if (!gui_display) return false;
|
|
lv_display_set_color_format(gui_display, LV_COLOR_FORMAT_RGB565_SWAPPED);
|
|
lv_display_set_flush_cb(gui_display, gui_flush_cb);
|
|
|
|
uint32_t buf_size = GUI_W * GUI_BUF_LINES * sizeof(uint16_t);
|
|
gui_buf1 = (uint8_t *)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
gui_buf2 = (uint8_t *)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
if (!gui_buf1 || !gui_buf2) {
|
|
if (gui_buf1) free(gui_buf1);
|
|
if (gui_buf2) free(gui_buf2);
|
|
gui_buf2 = NULL;
|
|
gui_buf1 = (uint8_t *)malloc(buf_size);
|
|
if (!gui_buf1) return false;
|
|
}
|
|
lv_display_set_buffers(gui_display, gui_buf1, gui_buf2, buf_size,
|
|
LV_DISPLAY_RENDER_MODE_FULL);
|
|
|
|
// Shadow framebuffer for screenshots (410*502*2 = 411,640 bytes)
|
|
gui_screenshot_buf = (uint16_t *)heap_caps_malloc(GUI_W * GUI_H * sizeof(uint16_t),
|
|
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
if (gui_screenshot_buf) {
|
|
memset(gui_screenshot_buf, 0, GUI_W * GUI_H * sizeof(uint16_t));
|
|
Serial.printf("[gui] screenshot buf @ %p (%u bytes)\n",
|
|
gui_screenshot_buf, GUI_W * GUI_H * 2);
|
|
}
|
|
|
|
// --- Input driver ---
|
|
gui_indev = lv_indev_create();
|
|
if (gui_indev) {
|
|
lv_indev_set_type(gui_indev, LV_INDEV_TYPE_POINTER);
|
|
lv_indev_set_read_cb(gui_indev, gui_touch_read_cb);
|
|
lv_indev_set_scroll_throw(gui_indev, 20); // Moderate friction: decelerates into snap within ~1s at 10fps
|
|
}
|
|
|
|
// --- Screen setup ---
|
|
gui_screen = lv_screen_active();
|
|
gui_style_black_container(gui_screen);
|
|
|
|
// --- Tileview: 3x3 grid, 5 populated tiles ---
|
|
// (1,0) Radio
|
|
// (0,1) GPS (1,1) Watch (2,1) Messages
|
|
// (1,2) Settings
|
|
gui_tileview = lv_tileview_create(gui_screen);
|
|
lv_obj_set_style_bg_color(gui_tileview, lv_color_hex(GUI_COL_BLACK), 0);
|
|
lv_obj_set_style_bg_opa(gui_tileview, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_width(gui_tileview, 0, 0);
|
|
lv_obj_set_style_pad_all(gui_tileview, 0, 0);
|
|
lv_obj_set_scrollbar_mode(gui_tileview, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_set_style_anim_duration(gui_tileview, 150, 0); // Snappy 150ms scroll snap
|
|
lv_obj_clear_flag(gui_tileview, LV_OBJ_FLAG_SCROLL_ELASTIC); // No bounce at tile edges
|
|
lv_obj_set_size(gui_tileview, GUI_W, GUI_H);
|
|
|
|
gui_tile_watch = lv_tileview_add_tile(gui_tileview, 1, 1, LV_DIR_ALL);
|
|
gui_tile_radio = lv_tileview_add_tile(gui_tileview, 1, 0, LV_DIR_BOTTOM);
|
|
gui_tile_gps = lv_tileview_add_tile(gui_tileview, 0, 1, LV_DIR_RIGHT);
|
|
gui_tile_msg = lv_tileview_add_tile(gui_tileview, 2, 1, LV_DIR_LEFT);
|
|
gui_tile_set = lv_tileview_add_tile(gui_tileview, 1, 2, LV_DIR_TOP);
|
|
|
|
// Start on watch face
|
|
lv_tileview_set_tile(gui_tileview, gui_tile_watch, LV_ANIM_OFF);
|
|
|
|
// Haptic feedback on tile change
|
|
lv_obj_add_event_cb(gui_tileview, gui_tile_change_cb, LV_EVENT_VALUE_CHANGED, NULL);
|
|
|
|
// --- Create screen content ---
|
|
gui_create_watchface(gui_tile_watch);
|
|
gui_create_radio_screen(gui_tile_radio);
|
|
gui_create_gps_screen(gui_tile_gps);
|
|
gui_create_msg_screen(gui_tile_msg);
|
|
gui_create_settings_screen(gui_tile_set);
|
|
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Screenshot: dump framebuffer as raw RGB565 to a file on SPIFFS,
|
|
// or output dimensions to serial for external tools.
|
|
// Call gui_screenshot() to write /screenshot.raw to SPIFFS (if mounted),
|
|
// or read gui_screenshot_buf directly via debugger.
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// Remote debug protocol over serial
|
|
// ---------------------------------------------------------------------------
|
|
// Trigger: 3-byte prefix [0x52, 0x57, 0x53] ("RWS") + command byte + optional payload
|
|
//
|
|
// Commands:
|
|
// 'S' (0x53) — Screenshot: captures next frame, responds RWSS + u16 w + u16 h + pixels
|
|
// 'T' (0x54) — Touch inject: reads 5 bytes (u16 x, u16 y, u8 duration_100ms)
|
|
// 'N' (0x4E) — Navigate: reads 2 bytes (u8 col, u8 row) — jump to tile
|
|
// 'M' (0x4D) — Metrics: responds RWSM + JSON stats
|
|
// 'I' (0x49) — Invalidate: force full screen redraw
|
|
// 'L' (0x4C) — Log toggle: start/stop IMU logging to SD card
|
|
// 'F' (0x46) — File list: lists files on SD card
|
|
// 'P' (0x50) — Profile: runs standardized performance test, reports JSON results
|
|
// 'B' (0x42) — Beacon dump: dumps last beacon packet pre/post IFAC
|
|
// 'C' (0x43) — Crypto test: runs IFAC test vectors, reports pass/fail
|
|
|
|
#define GUI_CMD_PREFIX_LEN 3
|
|
static const uint8_t gui_cmd_prefix[] = {0x52, 0x57, 0x53}; // "RWS"
|
|
static uint8_t gui_cmd_state = 0;
|
|
static uint8_t gui_cmd_id = 0;
|
|
static uint8_t gui_cmd_payload[8];
|
|
static uint8_t gui_cmd_payload_pos = 0;
|
|
static uint8_t gui_cmd_payload_len = 0;
|
|
|
|
static void gui_cmd_execute();
|
|
|
|
void gui_process_serial_byte(uint8_t b) {
|
|
// Match prefix
|
|
if (gui_cmd_state < GUI_CMD_PREFIX_LEN) {
|
|
if (b == gui_cmd_prefix[gui_cmd_state]) {
|
|
gui_cmd_state++;
|
|
} else {
|
|
gui_cmd_state = (b == gui_cmd_prefix[0]) ? 1 : 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Prefix matched — next byte is command
|
|
if (gui_cmd_state == GUI_CMD_PREFIX_LEN) {
|
|
gui_cmd_id = b;
|
|
gui_cmd_payload_pos = 0;
|
|
switch (b) {
|
|
case 'T': gui_cmd_payload_len = 5; break; // x(2) + y(2) + duration(1)
|
|
case 'N': gui_cmd_payload_len = 2; break; // col(1) + row(1)
|
|
default: gui_cmd_payload_len = 0; break; // S, M, I, F, L — no payload
|
|
}
|
|
gui_cmd_state++;
|
|
if (gui_cmd_payload_len == 0) {
|
|
gui_cmd_execute();
|
|
gui_cmd_state = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Collecting payload
|
|
if (gui_cmd_payload_pos < gui_cmd_payload_len) {
|
|
gui_cmd_payload[gui_cmd_payload_pos++] = b;
|
|
if (gui_cmd_payload_pos >= gui_cmd_payload_len) {
|
|
gui_cmd_execute();
|
|
gui_cmd_state = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void gui_cmd_execute() {
|
|
const uint8_t hdr[] = {'R', 'W', 'S', gui_cmd_id};
|
|
|
|
switch (gui_cmd_id) {
|
|
case 'S': { // Screenshot
|
|
if (gui_screenshot_buf) {
|
|
// Unblank and force full redraw so screenshot captures entire screen
|
|
if (display_blanked) display_unblank();
|
|
lv_obj_invalidate(lv_screen_active());
|
|
gui_screenshot_pending = true;
|
|
// Force full-screen render into screenshot buffer
|
|
// Advance tick past the refresh period to ensure render fires
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
gui_update_data();
|
|
lv_timer_handler();
|
|
gui_screenshot_pending = false;
|
|
Serial.write(hdr, 4);
|
|
uint16_t w = GUI_W, h = GUI_H;
|
|
Serial.write((uint8_t *)&w, 2);
|
|
Serial.write((uint8_t *)&h, 2);
|
|
Serial.write((uint8_t *)gui_screenshot_buf, GUI_W * GUI_H * 2);
|
|
Serial.flush();
|
|
} else {
|
|
Serial.write(hdr, 4);
|
|
uint16_t z = 0;
|
|
Serial.write((uint8_t *)&z, 2);
|
|
Serial.write((uint8_t *)&z, 2);
|
|
Serial.flush();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'T': { // Touch inject
|
|
gui_inject_x = gui_cmd_payload[0] | (gui_cmd_payload[1] << 8);
|
|
gui_inject_y = gui_cmd_payload[2] | (gui_cmd_payload[3] << 8);
|
|
uint32_t dur = gui_cmd_payload[4] * 100; // duration in 100ms units
|
|
if (dur == 0) dur = 200;
|
|
gui_inject_pressed = true;
|
|
gui_inject_until = millis() + dur;
|
|
// Unblank display on injected touch
|
|
if (display_blanked) display_unblank();
|
|
break;
|
|
}
|
|
|
|
case 'N': { // Navigate to tile
|
|
uint8_t col = gui_cmd_payload[0];
|
|
uint8_t row = gui_cmd_payload[1];
|
|
if (gui_tileview) {
|
|
// Find tile at position
|
|
lv_obj_t *target = NULL;
|
|
if (col == 1 && row == 1) target = gui_tile_watch;
|
|
else if (col == 1 && row == 0) target = gui_tile_radio;
|
|
else if (col == 0 && row == 1) target = gui_tile_gps;
|
|
else if (col == 2 && row == 1) target = gui_tile_msg;
|
|
else if (col == 1 && row == 2) target = gui_tile_set;
|
|
if (target) {
|
|
lv_tileview_set_tile(gui_tileview, target, LV_ANIM_ON);
|
|
}
|
|
}
|
|
if (display_blanked) display_unblank();
|
|
break;
|
|
}
|
|
|
|
case 'M': { // Metrics
|
|
Serial.write(hdr, 4);
|
|
char buf[192];
|
|
uint32_t avg_flush = gui_frame_count > 0 ? gui_flush_us_total / gui_frame_count : 0;
|
|
snprintf(buf, sizeof(buf),
|
|
"{\"build\":\"%s %s\",\"loop\":%lu,\"radio\":%lu,"
|
|
"\"serial\":%lu,\"disp\":%lu,\"pmu\":%lu,"
|
|
"\"gps\":%lu,\"bt\":%lu,\"imu\":%lu,"
|
|
"\"bcn_gate\":%d,\"hw_ready\":%d,"
|
|
"\"lxmf_id\":%d,\"bcn_crypto\":%d}\n",
|
|
__DATE__, __TIME__,
|
|
gui_loop_us_last, prof_radio_us, prof_serial_us,
|
|
prof_display_us, prof_pmu_us, prof_gps_us,
|
|
prof_bt_us, prof_imu_us,
|
|
beacon_gate, hw_ready ? 1 : 0,
|
|
lxmf_identity_configured ? 1 : 0,
|
|
beacon_crypto_configured ? 1 : 0);
|
|
gui_loop_us_max = 0;
|
|
Serial.write((uint8_t *)buf, strlen(buf));
|
|
Serial.flush();
|
|
break;
|
|
}
|
|
|
|
case 'P': { // Standardized performance profile test
|
|
Serial.write(hdr, 4);
|
|
if (display_blanked) display_unblank();
|
|
|
|
uint32_t p_t0, p_t1;
|
|
uint32_t p_idle_render = 0, p_idle_flush = 0;
|
|
uint32_t p_full_render = 0, p_full_flush = 0;
|
|
uint32_t p_nav_total = 0;
|
|
uint32_t p_data_update = 0;
|
|
uint32_t p_frames = 0;
|
|
uint32_t p_multi_total = 0;
|
|
|
|
// Test 1: Idle frame (nothing dirty)
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
lv_timer_handler(); // clear any pending
|
|
gui_flush_us_last = 0;
|
|
gui_frame_count = 0;
|
|
p_t0 = micros();
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
lv_timer_handler();
|
|
p_t1 = micros();
|
|
p_idle_render = p_t1 - p_t0;
|
|
p_idle_flush = gui_flush_us_last;
|
|
|
|
// Test 2: Full invalidation + render
|
|
lv_obj_invalidate(lv_screen_active());
|
|
gui_flush_us_last = 0;
|
|
p_t0 = micros();
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
lv_timer_handler();
|
|
p_t1 = micros();
|
|
p_full_render = p_t1 - p_t0;
|
|
p_full_flush = gui_flush_us_last;
|
|
|
|
// Test 3: Data update cycle
|
|
gui_last_data_update = 0; // force update
|
|
p_t0 = micros();
|
|
gui_update_data();
|
|
p_t1 = micros();
|
|
p_data_update = p_t1 - p_t0;
|
|
|
|
// Test 4: Navigate to each tile and back (5 transitions)
|
|
p_t0 = micros();
|
|
lv_tileview_set_tile(gui_tileview, gui_tile_radio, LV_ANIM_OFF);
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
lv_timer_handler();
|
|
lv_tileview_set_tile(gui_tileview, gui_tile_gps, LV_ANIM_OFF);
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
lv_timer_handler();
|
|
lv_tileview_set_tile(gui_tileview, gui_tile_msg, LV_ANIM_OFF);
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
lv_timer_handler();
|
|
lv_tileview_set_tile(gui_tileview, gui_tile_set, LV_ANIM_OFF);
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
lv_timer_handler();
|
|
lv_tileview_set_tile(gui_tileview, gui_tile_watch, LV_ANIM_OFF);
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
lv_timer_handler();
|
|
p_t1 = micros();
|
|
p_nav_total = p_t1 - p_t0;
|
|
|
|
// Test 5: Rapid frame burst (10 full frames)
|
|
p_t0 = micros();
|
|
for (int i = 0; i < 10; i++) {
|
|
lv_obj_invalidate(lv_screen_active());
|
|
lv_tick_inc(LV_DEF_REFR_PERIOD + 1);
|
|
lv_timer_handler();
|
|
}
|
|
p_t1 = micros();
|
|
p_multi_total = p_t1 - p_t0;
|
|
|
|
Serial.printf("{\"test\":\"profile\",\"build\":\"%s %s\","
|
|
"\"idle_us\":%lu,\"idle_flush_us\":%lu,"
|
|
"\"full_us\":%lu,\"full_flush_us\":%lu,"
|
|
"\"data_update_us\":%lu,"
|
|
"\"nav_5tile_us\":%lu,"
|
|
"\"burst_10frame_us\":%lu,"
|
|
"\"avg_frame_us\":%lu,"
|
|
"\"loop_us\":%lu,"
|
|
"\"heap\":%lu,\"psram\":%lu}\n",
|
|
__DATE__, __TIME__,
|
|
p_idle_render, p_idle_flush,
|
|
p_full_render, p_full_flush,
|
|
p_data_update,
|
|
p_nav_total,
|
|
p_multi_total, p_multi_total / 10,
|
|
gui_loop_us_last,
|
|
(uint32_t)esp_get_free_heap_size(),
|
|
(uint32_t)heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
|
|
Serial.flush();
|
|
break;
|
|
}
|
|
|
|
case 'B': { // Beacon packet dump
|
|
extern uint8_t diag_beacon_pre[];
|
|
extern uint16_t diag_beacon_pre_len;
|
|
extern uint8_t diag_beacon_post[];
|
|
extern uint16_t diag_beacon_post_len;
|
|
|
|
Serial.write(hdr, 4);
|
|
Serial.printf("{\"pre_len\":%d,\"post_len\":%d", diag_beacon_pre_len, diag_beacon_post_len);
|
|
if (diag_beacon_pre_len > 0) {
|
|
Serial.printf(",\"pre\":\"");
|
|
for (int i = 0; i < diag_beacon_pre_len; i++) Serial.printf("%02x", diag_beacon_pre[i]);
|
|
Serial.printf("\"");
|
|
}
|
|
if (diag_beacon_post_len > 0) {
|
|
Serial.printf(",\"post\":\"");
|
|
for (int i = 0; i < diag_beacon_post_len; i++) Serial.printf("%02x", diag_beacon_post[i]);
|
|
Serial.printf("\"");
|
|
}
|
|
Serial.println("}");
|
|
Serial.flush();
|
|
break;
|
|
}
|
|
|
|
case 'C': { // Crypto test — IFAC test vectors
|
|
#if HAS_GPS == true
|
|
Serial.write(hdr, 4);
|
|
|
|
// Test vectors from scripts/test_ifac.py
|
|
const uint8_t tv_key[64] = {0x3a, 0xc2, 0xe0, 0x12, 0xa0, 0x86, 0x04, 0x3c, 0x67, 0xcc, 0xef, 0x40, 0x6a, 0x0b, 0xdb, 0x38, 0xc0, 0x66, 0xb2, 0xee, 0x0a, 0x7f, 0x18, 0x27, 0xfa, 0x1c, 0xb9, 0xdc, 0xcf, 0xbb, 0x8e, 0x9d, 0x53, 0x48, 0xc5, 0x56, 0xf0, 0x8e, 0xed, 0xf3, 0x0b, 0xce, 0x46, 0x2b, 0xb2, 0x09, 0x6b, 0x99, 0x26, 0x08, 0xf4, 0xfc, 0xfd, 0x12, 0x32, 0x4b, 0xb2, 0x45, 0x86, 0x2b, 0x59, 0xd6, 0x11, 0xc7};
|
|
const uint8_t tv_pk[32] = {0x1a, 0x54, 0x5d, 0x78, 0x34, 0xc3, 0xe1, 0x6c, 0x53, 0x9d, 0xd5, 0xf5, 0x3a, 0xd1, 0x5b, 0x67, 0xae, 0x57, 0x5e, 0x97, 0x06, 0x05, 0x38, 0x5b, 0xeb, 0x76, 0xe9, 0x85, 0x2e, 0xf9, 0xe1, 0xdf};
|
|
const uint8_t tv_msg[19] = {0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99};
|
|
const uint8_t tv_sig[64] = {0x99, 0x80, 0x27, 0x05, 0xaf, 0xde, 0xb0, 0xe6, 0xfe, 0xe5, 0x2b, 0xbc, 0x35, 0x4a, 0x87, 0x93, 0xd8, 0xc2, 0x9c, 0x77, 0x41, 0x6c, 0x5c, 0x54, 0x62, 0x7e, 0x66, 0xc6, 0x50, 0x05, 0xe5, 0x0a, 0x02, 0x48, 0x94, 0x4b, 0xb1, 0x02, 0x5b, 0x3a, 0xaa, 0xa2, 0x9b, 0x26, 0xc4, 0x7f, 0x49, 0x4b, 0xa2, 0x1a, 0xf0, 0xb5, 0xd0, 0x08, 0x8f, 0x9b, 0x49, 0x5b, 0xf2, 0xc7, 0xe1, 0x83, 0x99, 0x01};
|
|
const uint8_t tv_mask[27] = {0x3b, 0x8f, 0x16, 0xab, 0xe6, 0x0b, 0x8e, 0x35, 0xcb, 0x47, 0x5a, 0x3d, 0x13, 0x00, 0x05, 0xe6, 0x79, 0x79, 0x99, 0x23, 0x35, 0x24, 0x64, 0xd8, 0x4b, 0xf5, 0x3c};
|
|
const uint8_t tv_result[27] = {0xbb, 0x8f, 0x49, 0x5b, 0xf2, 0xc7, 0xe1, 0x83, 0x99, 0x01, 0x48, 0x09, 0x45, 0x78, 0x9f, 0x5a, 0xa7, 0x89, 0x88, 0x01, 0x06, 0x60, 0x31, 0xbe, 0x3c, 0x7d, 0xa5};
|
|
|
|
// Test 1: Keypair derivation
|
|
uint8_t pk[32], sk[64];
|
|
crypto_sign_ed25519_seed_keypair(pk, sk, tv_key + 32);
|
|
bool pk_match = (memcmp(pk, tv_pk, 32) == 0);
|
|
|
|
// Test 2: Signature
|
|
uint8_t sig[64];
|
|
unsigned long long sig_len;
|
|
crypto_sign_ed25519_detached(sig, &sig_len, tv_msg, 19, sk);
|
|
bool sig_match = (memcmp(sig, tv_sig, 64) == 0);
|
|
|
|
// Test 3: HKDF
|
|
uint8_t mask[27];
|
|
rns_hkdf_var(sig + 56, 8, tv_key, 64, mask, 27);
|
|
bool mask_match = (memcmp(mask, tv_mask, 27) == 0);
|
|
|
|
// Test 4: Full IFAC apply
|
|
uint8_t pkt[64];
|
|
memcpy(pkt, tv_msg, 19);
|
|
// Save original ifac state and substitute test key
|
|
uint8_t saved_key[64]; bool saved_configured;
|
|
memcpy(saved_key, ifac_key, 64);
|
|
saved_configured = ifac_configured;
|
|
memcpy(ifac_key, tv_key, 64);
|
|
ifac_derive_keypair();
|
|
ifac_configured = true;
|
|
uint16_t result_len = ifac_apply(pkt, 19);
|
|
bool result_match = (result_len == 27) && (memcmp(pkt, tv_result, 27) == 0);
|
|
// Restore
|
|
memcpy(ifac_key, saved_key, 64);
|
|
if (saved_configured) ifac_derive_keypair();
|
|
ifac_configured = saved_configured;
|
|
|
|
// Report
|
|
// Dump the live IFAC key for verification
|
|
Serial.printf("{\"pk\":%s,\"sig\":%s,\"hkdf\":%s,\"ifac\":%s,\"configured\":%s",
|
|
pk_match ? "true" : "false",
|
|
sig_match ? "true" : "false",
|
|
mask_match ? "true" : "false",
|
|
result_match ? "true" : "false",
|
|
ifac_configured ? "true" : "false");
|
|
Serial.printf(",\"live_key\":\"");
|
|
for (int i = 0; i < 64; i++) Serial.printf("%02x", ifac_key[i]);
|
|
Serial.printf("\"");
|
|
|
|
// Dump actual values on failure for debugging
|
|
if (!sig_match) {
|
|
Serial.printf(",\"actual_sig\":\"");
|
|
for (int i = 0; i < 64; i++) Serial.printf("%02x", sig[i]);
|
|
Serial.printf("\"");
|
|
}
|
|
if (!mask_match) {
|
|
Serial.printf(",\"actual_mask\":\"");
|
|
for (int i = 0; i < 27; i++) Serial.printf("%02x", mask[i]);
|
|
Serial.printf("\"");
|
|
}
|
|
if (!result_match) {
|
|
Serial.printf(",\"actual_result\":\"");
|
|
for (int i = 0; i < (int)result_len; i++) Serial.printf("%02x", pkt[i]);
|
|
Serial.printf("\",\"result_len\":%d", result_len);
|
|
}
|
|
Serial.println("}");
|
|
Serial.flush();
|
|
#else
|
|
Serial.write(hdr, 4);
|
|
Serial.println("{\"error\":\"no_gps\"}");
|
|
Serial.flush();
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
case 'I': { // Invalidate — force full redraw
|
|
if (gui_screen) lv_obj_invalidate(gui_screen);
|
|
if (display_blanked) display_unblank();
|
|
break;
|
|
}
|
|
|
|
case 'L': { // Toggle IMU logging
|
|
Serial.write(hdr, 4);
|
|
if (gui_log_toggle_fn) {
|
|
gui_imu_logging = gui_log_toggle_fn();
|
|
Serial.printf("{\"logging\":%s}\n", gui_imu_logging ? "true" : "false");
|
|
} else {
|
|
Serial.println("{\"logging\":false,\"error\":\"not_available\"}");
|
|
}
|
|
Serial.flush();
|
|
break;
|
|
}
|
|
|
|
case 'F': { // List files on SD card (handled via function pointer)
|
|
Serial.write(hdr, 4);
|
|
if (gui_log_toggle_fn) {
|
|
// Reuse the SD infrastructure from IMU logger
|
|
Serial.println("{\"error\":\"use_log_command\"}");
|
|
} else {
|
|
Serial.println("{\"error\":\"not_available\"}");
|
|
}
|
|
Serial.flush();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void gui_screenshot_info() {
|
|
if (gui_screenshot_buf) {
|
|
Serial.printf("[screenshot] addr=%p size=%u w=%d h=%d\n",
|
|
gui_screenshot_buf, GUI_W * GUI_H * 2, GUI_W, GUI_H);
|
|
} else {
|
|
Serial.println("[screenshot] buffer not allocated");
|
|
}
|
|
}
|
|
|
|
// Write screenshot to SD card as raw RGB565 + BMP header
|
|
#if HAS_SD
|
|
#include <SD.h>
|
|
#include "SharedSPI.h"
|
|
bool gui_screenshot_sd(const char *path = "/screenshot.bmp") {
|
|
if (!gui_screenshot_buf) return false;
|
|
|
|
// Acquire shared SPI mutex for SD access
|
|
if (shared_spi_mutex) xSemaphoreTake(shared_spi_mutex, portMAX_DELAY);
|
|
SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS);
|
|
if (!SD.begin(SD_CS, SPI, 4000000, "/sd", 5)) {
|
|
if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
|
|
Serial.println("[screenshot] SD init failed");
|
|
return false;
|
|
}
|
|
|
|
File f = SD.open(path, FILE_WRITE);
|
|
if (!f) {
|
|
if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
|
|
Serial.println("[screenshot] file open failed");
|
|
return false;
|
|
}
|
|
|
|
// Write BMP header (RGB565 LE, top-down)
|
|
uint32_t img_size = GUI_W * GUI_H * 2;
|
|
uint32_t file_size = 14 + 40 + 12 + img_size; // file + info + masks + pixels
|
|
uint32_t data_offset = 14 + 40 + 12;
|
|
|
|
// BMP file header (14 bytes)
|
|
uint8_t bmp_hdr[14] = {'B', 'M'};
|
|
memcpy(&bmp_hdr[2], &file_size, 4);
|
|
memset(&bmp_hdr[6], 0, 4); // reserved
|
|
memcpy(&bmp_hdr[10], &data_offset, 4);
|
|
f.write(bmp_hdr, 14);
|
|
|
|
// DIB header (BITMAPINFOHEADER, 40 bytes)
|
|
uint8_t dib[40] = {};
|
|
uint32_t dib_size = 40;
|
|
int32_t bmp_w = GUI_W;
|
|
int32_t bmp_h = -GUI_H; // negative = top-down
|
|
uint16_t planes = 1;
|
|
uint16_t bpp = 16;
|
|
uint32_t compression = 3; // BI_BITFIELDS
|
|
memcpy(&dib[0], &dib_size, 4);
|
|
memcpy(&dib[4], &bmp_w, 4);
|
|
memcpy(&dib[8], &bmp_h, 4);
|
|
memcpy(&dib[12], &planes, 2);
|
|
memcpy(&dib[14], &bpp, 2);
|
|
memcpy(&dib[16], &compression, 4);
|
|
memcpy(&dib[20], &img_size, 4);
|
|
f.write(dib, 40);
|
|
|
|
// RGB565 bitmasks (R, G, B)
|
|
uint32_t mask_r = 0xF800, mask_g = 0x07E0, mask_b = 0x001F;
|
|
f.write((uint8_t *)&mask_r, 4);
|
|
f.write((uint8_t *)&mask_g, 4);
|
|
f.write((uint8_t *)&mask_b, 4);
|
|
|
|
// Pixel data (RGB565 LE, row by row)
|
|
// BMP rows must be 4-byte aligned; 410*2=820 bytes per row, 820%4=0, no padding needed
|
|
f.write((uint8_t *)gui_screenshot_buf, img_size);
|
|
|
|
f.close();
|
|
if (shared_spi_mutex) xSemaphoreGive(shared_spi_mutex);
|
|
|
|
Serial.printf("[screenshot] saved %s (%u bytes)\n", path, file_size);
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main GUI update — called from update_display()
|
|
// ---------------------------------------------------------------------------
|
|
void gui_update() {
|
|
static uint32_t last_tick = 0;
|
|
uint32_t now = millis();
|
|
lv_tick_inc(now - last_tick);
|
|
last_tick = now;
|
|
|
|
// Measure loop interval
|
|
uint32_t now_us = micros();
|
|
if (gui_last_update_us > 0) {
|
|
gui_loop_us_last = now_us - gui_last_update_us;
|
|
if (gui_loop_us_last > gui_loop_us_max) gui_loop_us_max = gui_loop_us_last;
|
|
}
|
|
gui_last_update_us = now_us;
|
|
|
|
gui_update_data();
|
|
|
|
gui_render_start = micros();
|
|
lv_timer_handler();
|
|
gui_render_us_last = micros() - gui_render_start;
|
|
// After lv_timer_handler, a new DMA may be queued via gui_flush_cb.
|
|
// It runs in background on SPI3 until the next gui_update() call.
|
|
}
|
|
|
|
#endif // BOARD_MODEL == BOARD_TWATCH_ULT
|
|
#endif // GUI_H
|