diff --git a/Gui.h b/Gui.h index 1ba7f9e..b85d6d6 100644 --- a/Gui.h +++ b/Gui.h @@ -685,13 +685,94 @@ static void gui_create_gps_screen(lv_obj_t *parent) { } // --------------------------------------------------------------------------- -// Screen: Messages (left tile — swipe left from watch face) +// Screen: Messages (right tile — swipe left from watch face) // --------------------------------------------------------------------------- +static lv_obj_t *gui_msg_title = NULL; +static lv_obj_t *gui_msg_empty = NULL; +static lv_obj_t *gui_msg_cont = NULL; +#define GUI_MSG_ROWS 8 // max visible message rows +static lv_obj_t *gui_msg_name[GUI_MSG_ROWS]; +static lv_obj_t *gui_msg_rssi[GUI_MSG_ROWS]; +static lv_obj_t *gui_msg_time[GUI_MSG_ROWS]; +static lv_obj_t *gui_msg_row[GUI_MSG_ROWS]; +static uint32_t gui_msg_last_rebuild = 0; + +// Message log types and forward declarations (defined in MessageLog.h) +#ifndef MSG_LOG_SIZE +#define MSG_LOG_SIZE 16 +#define MSG_NAME_LEN 24 +#endif +struct msg_entry_t { + uint32_t timestamp; + int16_t rssi; + int8_t snr; + uint8_t pkt_type; + uint8_t sender_hash[16]; + char display_name[MSG_NAME_LEN]; + uint16_t pkt_len; + bool is_announce; +}; +extern msg_entry_t msg_log[]; +extern uint8_t msg_log_head; +extern uint8_t msg_log_count; +extern uint32_t msg_log_last_change; + 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); + gui_msg_title = gui_label_at(parent, &lv_font_montserrat_14, GUI_COL_DIM, "MESSAGES", GUI_PAD, 12); + + gui_msg_empty = gui_label(parent, &lv_font_montserrat_14, GUI_COL_DIM, "Listening..."); + lv_obj_align(gui_msg_empty, LV_ALIGN_CENTER, 0, 0); + + // Scrollable message list container + gui_msg_cont = lv_obj_create(parent); + lv_obj_remove_style_all(gui_msg_cont); + lv_obj_set_size(gui_msg_cont, GUI_W, GUI_H - 40); + lv_obj_set_pos(gui_msg_cont, 0, 35); + lv_obj_set_style_bg_opa(gui_msg_cont, LV_OPA_TRANSP, 0); + lv_obj_clear_flag(gui_msg_cont, LV_OBJ_FLAG_SCROLLABLE); + + // Pre-create row widgets + for (int i = 0; i < GUI_MSG_ROWS; i++) { + int y = i * 55; + gui_msg_row[i] = lv_obj_create(gui_msg_cont); + lv_obj_remove_style_all(gui_msg_row[i]); + lv_obj_set_size(gui_msg_row[i], GUI_W - 2 * GUI_PAD, 50); + lv_obj_set_pos(gui_msg_row[i], GUI_PAD, y); + lv_obj_add_flag(gui_msg_row[i], LV_OBJ_FLAG_HIDDEN); + + // Display name (left) + gui_msg_name[i] = lv_label_create(gui_msg_row[i]); + lv_obj_set_style_text_font(gui_msg_name[i], &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(gui_msg_name[i], lv_color_hex(GUI_COL_WHITE), 0); + lv_obj_set_pos(gui_msg_name[i], 0, 0); + lv_obj_set_width(gui_msg_name[i], GUI_W - 2 * GUI_PAD - 70); + lv_label_set_long_mode(gui_msg_name[i], LV_LABEL_LONG_CLIP); + + // RSSI (right) + gui_msg_rssi[i] = lv_label_create(gui_msg_row[i]); + lv_obj_set_style_text_font(gui_msg_rssi[i], &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(gui_msg_rssi[i], lv_color_hex(GUI_COL_AMBER), 0); + lv_obj_set_pos(gui_msg_rssi[i], GUI_W - 2 * GUI_PAD - 65, 0); + lv_obj_set_width(gui_msg_rssi[i], 65); + lv_obj_set_style_text_align(gui_msg_rssi[i], LV_TEXT_ALIGN_RIGHT, 0); + + // Time ago (below name) + gui_msg_time[i] = lv_label_create(gui_msg_row[i]); + lv_obj_set_style_text_font(gui_msg_time[i], &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(gui_msg_time[i], lv_color_hex(GUI_COL_DIM), 0); + lv_obj_set_pos(gui_msg_time[i], 0, 20); + + // Divider line at bottom of row + if (i > 0) { + lv_obj_t *div = lv_obj_create(gui_msg_row[i]); + lv_obj_remove_style_all(div); + lv_obj_set_size(div, GUI_W - 2 * GUI_PAD, 1); + lv_obj_set_pos(div, 0, -3); + lv_obj_set_style_bg_color(div, lv_color_hex(0x1A1A1A), 0); + lv_obj_set_style_bg_opa(div, LV_OPA_COVER, 0); + } + } } // --------------------------------------------------------------------------- @@ -901,12 +982,13 @@ static void gui_update_data() { bool on_radio = (cur_tile == gui_tile_radio); bool on_gps = (cur_tile == gui_tile_gps); bool on_settings = (cur_tile == gui_tile_set); + bool on_msg = (cur_tile == gui_tile_msg); // ---- 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; + if (!on_watch && !on_radio && !on_gps && !on_settings && !on_msg) return; // ---- Watch face details (only when visible) ---- if (on_watch) { @@ -1371,6 +1453,50 @@ static void gui_update_data() { lv_label_set_text(gui_set_log_status, "No SD card"); #endif } + + // ---- Messages screen ---- + if (on_msg && gui_msg_cont) { + if (msg_log_count == 0) { + lv_obj_clear_flag(gui_msg_empty, LV_OBJ_FLAG_HIDDEN); + for (int i = 0; i < GUI_MSG_ROWS; i++) + lv_obj_add_flag(gui_msg_row[i], LV_OBJ_FLAG_HIDDEN); + lv_label_set_text_fmt(gui_msg_title, "MESSAGES"); + } else { + lv_obj_add_flag(gui_msg_empty, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text_fmt(gui_msg_title, "MESSAGES (%d)", msg_log_count); + + // Display messages newest-first + int shown = 0; + for (int i = 0; i < msg_log_count && shown < GUI_MSG_ROWS; i++) { + int idx = (msg_log_head - 1 - i + MSG_LOG_SIZE) % MSG_LOG_SIZE; + msg_entry_t &m = msg_log[idx]; + + lv_obj_clear_flag(gui_msg_row[shown], LV_OBJ_FLAG_HIDDEN); + + // Name — amber for announces, white for data + lv_label_set_text(gui_msg_name[shown], m.display_name); + lv_obj_set_style_text_color(gui_msg_name[shown], + lv_color_hex(m.is_announce ? GUI_COL_AMBER : GUI_COL_WHITE), 0); + + // RSSI with colour coding + lv_label_set_text_fmt(gui_msg_rssi[shown], "%d dBm", m.rssi); + uint32_t rssi_col = (m.rssi > -80) ? GUI_COL_GREEN : + (m.rssi > -100) ? GUI_COL_AMBER : GUI_COL_RED; + lv_obj_set_style_text_color(gui_msg_rssi[shown], lv_color_hex(rssi_col), 0); + + // Time ago + uint32_t ago = (millis() - m.timestamp) / 1000; + if (ago < 60) lv_label_set_text_fmt(gui_msg_time[shown], "%lus ago %dB", ago, m.pkt_len); + else if (ago < 3600) lv_label_set_text_fmt(gui_msg_time[shown], "%lum ago %dB", ago / 60, m.pkt_len); + else lv_label_set_text_fmt(gui_msg_time[shown], "%luh ago %dB", ago / 3600, m.pkt_len); + + shown++; + } + // Hide unused rows + for (int i = shown; i < GUI_MSG_ROWS; i++) + lv_obj_add_flag(gui_msg_row[i], LV_OBJ_FLAG_HIDDEN); + } + } } // --------------------------------------------------------------------------- diff --git a/MessageLog.h b/MessageLog.h new file mode 100644 index 0000000..0244908 --- /dev/null +++ b/MessageLog.h @@ -0,0 +1,148 @@ +// MessageLog.h — Received packet log and RNS announce parser +// Stores recent LoRa packets for the Messages screen viewer + +#ifndef MESSAGELOG_H +#define MESSAGELOG_H + +#if BOARD_MODEL == BOARD_TWATCH_ULT + +#include "mbedtls/sha256.h" + +// RNS packet type bits (flags byte, bits 1-0) +#define RNS_TYPE_DATA 0x00 +#define RNS_TYPE_ANNOUNCE 0x01 +// RNS header type (flags byte, bit 6) +#define RNS_HEADER_1 0x00 +#define RNS_HEADER_2 0x40 +// IFAC flag (flags byte, bit 7) +#define RNS_IFAC_FLAG 0x80 + +// msg_entry_t struct defined in Gui.h (included earlier) +// MSG_LOG_SIZE and MSG_NAME_LEN also defined there +#define MSG_DEDUP_MS 60000 // ignore same sender within 60s + +msg_entry_t msg_log[MSG_LOG_SIZE]; +uint8_t msg_log_head = 0; +uint8_t msg_log_count = 0; +uint32_t msg_log_last_change = 0; + +// Find existing entry for this sender (for de-dup / update) +static int msg_log_find_sender(const uint8_t *hash) { + for (int i = 0; i < msg_log_count; i++) { + int idx = (msg_log_head - 1 - i + MSG_LOG_SIZE) % MSG_LOG_SIZE; + if (memcmp(msg_log[idx].sender_hash, hash, 16) == 0) return idx; + } + return -1; +} + +// Add or update a message entry +static void msg_log_add(const uint8_t *sender_hash, const char *name, + int16_t rssi, int8_t snr, uint8_t pkt_type, + uint16_t pkt_len, bool is_announce) { + // De-dup: update existing entry for same sender + int existing = msg_log_find_sender(sender_hash); + if (existing >= 0) { + msg_entry_t &e = msg_log[existing]; + if (millis() - e.timestamp < MSG_DEDUP_MS && !is_announce) return; + // Update existing + e.timestamp = millis(); + e.rssi = rssi; + e.snr = snr; + e.pkt_type = pkt_type; + e.pkt_len = pkt_len; + if (is_announce && name[0]) { + strncpy(e.display_name, name, MSG_NAME_LEN - 1); + e.display_name[MSG_NAME_LEN - 1] = '\0'; + e.is_announce = true; + } + msg_log_last_change = millis(); + return; + } + + // New entry + msg_entry_t &e = msg_log[msg_log_head]; + e.timestamp = millis(); + e.rssi = rssi; + e.snr = snr; + e.pkt_type = pkt_type; + e.pkt_len = pkt_len; + e.is_announce = is_announce; + memcpy(e.sender_hash, sender_hash, 16); + if (name && name[0]) { + strncpy(e.display_name, name, MSG_NAME_LEN - 1); + e.display_name[MSG_NAME_LEN - 1] = '\0'; + } else { + // Short hex of sender hash + snprintf(e.display_name, MSG_NAME_LEN, "%02x%02x%02x..%02x%02x", + sender_hash[0], sender_hash[1], sender_hash[2], + sender_hash[14], sender_hash[15]); + } + msg_log_head = (msg_log_head + 1) % MSG_LOG_SIZE; + if (msg_log_count < MSG_LOG_SIZE) msg_log_count++; + msg_log_last_change = millis(); +} + +// Parse an RNS packet and log it +// Called from main loop after dequeue, before kiss_write_packet +// pkt points to raw LoRa payload (after split reassembly), len is total length +static void msg_log_parse_packet(const uint8_t *pkt, uint16_t len, + int16_t rssi, int8_t snr) { + if (len < 19) return; // too short for RNS header + + uint8_t flags = pkt[0]; + bool has_ifac = (flags & RNS_IFAC_FLAG) != 0; + uint8_t pkt_type = flags & 0x03; + + // IFAC packets: the real header starts after unmasking, but we can + // still extract the dest_hash from bytes 2-17 (masked but deterministic position) + // For announces, the IFAC is stripped by the receiver's IFAC handler. + // For our purposes, we parse what we can. + + int hdr_offset = 0; // where the header starts after IFAC + if (has_ifac) { + // IFAC-wrapped: bytes 0-1 are masked header, bytes 2-9 are IFAC signature, + // rest is masked payload. We can't parse without unmasking. + // But we still log the packet with the dest_hash from bytes 2-17 + // (these are the IFAC sig bytes, not the actual dest_hash) + // For now, just log as generic packet with flags info + uint8_t generic_hash[16]; + memcpy(generic_hash, pkt + 2, 16); + msg_log_add(generic_hash, NULL, rssi, snr, pkt_type, len, false); + return; + } + + // Non-IFAC packet — parse RNS header directly + const uint8_t *dest_hash = pkt + 2; // bytes 2-17 + + if (pkt_type == RNS_TYPE_ANNOUNCE && len > 167) { + // ANNOUNCE packet structure (HEADER_1, 19-byte header): + // [flags(1) hops(1) dest_hash(16) context(1)] + // [pub_keys(64) = x25519(32) + ed25519(32)] + // [name_hash(10)] + // [random_hash(10)] + // [signature(64)] + // [app_data(variable) = display name] + + // Compute identity hash from public keys + const uint8_t *pub_keys = pkt + 19; // 64 bytes + uint8_t identity_hash[32]; + mbedtls_sha256(pub_keys, 64, identity_hash, 0); + + // Extract display name from app_data + char name[MSG_NAME_LEN] = {0}; + int app_data_offset = 19 + 64 + 10 + 10 + 64; // = 167 + int app_data_len = len - app_data_offset; + if (app_data_len > 0 && app_data_len < MSG_NAME_LEN) { + memcpy(name, pkt + app_data_offset, app_data_len); + name[app_data_len] = '\0'; + } + + msg_log_add(identity_hash, name, rssi, snr, pkt_type, len, true); + } else { + // DATA or short packet — log with dest_hash + msg_log_add(dest_hash, NULL, rssi, snr, pkt_type, len, false); + } +} + +#endif // BOARD_MODEL == BOARD_TWATCH_ULT +#endif // MESSAGELOG_H diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino index 27ce0c4..85e3327 100644 --- a/RNode_Firmware.ino +++ b/RNode_Firmware.ino @@ -61,6 +61,9 @@ // Sensor data logger to SD card (must be before callbacks that call sensor_log_*) #include "IMULogger.h" + // Message log for received packet viewer + #include "MessageLog.h" + // IMU sensor callbacks void imu_step_cb(uint8_t sensor_id, uint8_t *data, uint32_t size, uint64_t *timestamp, void *user_data) { if (size >= 4) { @@ -2062,6 +2065,12 @@ void loop() { free(modem_packet); modem_packet = NULL; + // Log received packet for message viewer + #if BOARD_MODEL == BOARD_TWATCH_ULT + msg_log_parse_packet(pbuf, host_write_len, last_rssi, + (int8_t)(last_snr_raw > 127 ? last_snr_raw - 256 : last_snr_raw)); + #endif + response_channel = data_channel; kiss_indicate_stat_rssi(); kiss_indicate_stat_snr();