Add Messages screen with received packet log and announce parser

New MessageLog.h parses incoming RNS packets:
- Announces: extracts display name, identity hash, public keys
- Data packets: logs dest hash, RSSI, SNR, size
- De-duplicates by sender hash within 60s

Messages screen (swipe right from watch face) shows:
- "Listening..." when no packets received
- Chronological list of peers with name, RSSI (colour-coded), time ago
- Announces shown in amber, data packets in white
- Up to 16 entries in ring buffer, 8 visible rows

Packet interception hooks into main loop dequeue path, before
kiss_write_packet(), so host forwarding is unaffected.
This commit is contained in:
GlassOnTin 2026-04-07 13:56:49 +01:00
commit 50e81d0f1c
3 changed files with 288 additions and 5 deletions

136
Gui.h
View file

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

148
MessageLog.h Normal file
View file

@ -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

View file

@ -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();