Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pr-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
- wio_wm1110_repeater
# SX1276 (ESP32)
- Tbeam_SX1276_repeater
# Seeed Wio Tracker L2 Pro (ESP32-S3, LovyanGFX display)
- Wio_Tracker_L2_companion_radio_ble
- Wio_Tracker_L2_standalone_lvgl

steps:
- name: Clone Repo
Expand Down
43 changes: 43 additions & 0 deletions boards/seeed_wio_tracker_l2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi",
"partitions": "default_16MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino"],
"name": "Seeed Wio Tracker L2 (16 MB flash, 8 MB OPI PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.seeedstudio.com/",
"vendor": "Seeed Studio"
}
10 changes: 8 additions & 2 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2433,15 +2433,21 @@ void MyMesh::loop() {
}
}

bool MyMesh::advert() {
bool MyMesh::advert(bool flood) {
mesh::Packet* pkt;
if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) {
pkt = createSelfAdvert(_prefs.node_name);
} else {
pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon);
}
if (pkt) {
sendZeroHop(pkt);
if (flood) {
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
sendFloodScoped(default_scope, pkt, 0);
} else {
sendZeroHop(pkt);
}
return true;
} else {
return false;
Expand Down
13 changes: 7 additions & 6 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {

void loop();
void handleCmdFrame(size_t len);
bool advert();
bool advert(bool flood = false);
void enterCLIRescue();

int getRecentlyHeard(AdvertPath dest[], int max_num);
Expand Down Expand Up @@ -215,6 +215,12 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
// To check if there is pending work
bool hasPendingWork() const;

bool isValidClientRepeatFreq(uint32_t f) const;

// helpers, short-cuts
void saveChannels() { _store->saveChannels(this); }
void saveContacts();

private:
void writeOKFrame();
void writeErrFrame(uint8_t err_code);
Expand All @@ -233,11 +239,6 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
void checkCLIRescueCmd();
bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply);
void checkSerialInterface();
bool isValidClientRepeatFreq(uint32_t f) const;

// helpers, short-cuts
void saveChannels() { _store->saveChannels(this); }
void saveContacts();

DataStore* _store;
NodePrefs _prefs;
Expand Down
183 changes: 183 additions & 0 deletions examples/companion_radio/ui-lvgl/ChatStore.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#include "ChatStore.h"
#include <helpers/TxtDataHelpers.h>

#ifdef ESP32
#include <SPIFFS.h>
#endif

#define CHAT_HIST_FILE "/chat_hist.bin"
#define CHAT_HIST_MAGIC 0x43483032 // "CH02"

static ChatMsg chat_store[CHAT_STORE_SIZE];
static int chat_head = 0;
static bool chat_dirty = false;
static unsigned long chat_flush_at = 0;

void makeChannelKey(int idx, uint8_t key[6]) {
key[0] = 0xFE; key[1] = 0xC4; key[2] = (uint8_t)idx;
key[3] = 0x00; key[4] = 0xBE; key[5] = 0xEF;
}

bool isChannelKey(const uint8_t key[6], int* idx_out) {
if (key[0] == 0xFE && key[1] == 0xC4 && key[3] == 0x00 && key[4] == 0xBE && key[5] == 0xEF) {
if (idx_out) *idx_out = key[2];
return true;
}
return false;
}

void chatStoreMarkDirty() {
chat_dirty = true;
chat_flush_at = millis() + 4000;
}

ChatMsg* chatStorePush(const uint8_t* pub_key, bool outgoing, uint32_t timestamp, const char* text) {
auto m = &chat_store[chat_head];
chat_head = (chat_head + 1) % CHAT_STORE_SIZE;
memcpy(m->prefix, pub_key, 6);
m->outgoing = outgoing ? 1 : 0;
m->valid = 1;
m->status = MSG_STATUS_NONE;
m->expected_ack = 0;
m->timeout_at = 0;
m->timestamp = timestamp;
StrHelper::strncpy(m->text, text, sizeof(m->text));
chatStoreMarkDirty();
return m;
}

ChatMsg* chatStoreGet(const uint8_t* pub_key, int k) {
int found = 0;
for (int i = 0; i < CHAT_STORE_SIZE; i++) {
int idx = (chat_head - 1 - i + CHAT_STORE_SIZE * 2) % CHAT_STORE_SIZE;
auto m = &chat_store[idx];
if (!m->valid || memcmp(m->prefix, pub_key, 6) != 0) continue;
if (found == k) return m;
found++;
}
return NULL;
}

bool chatStoreAck(uint32_t ack_crc) {
bool changed = false;
for (int i = 0; i < CHAT_STORE_SIZE; i++) {
auto m = &chat_store[i];
if (m->valid && m->status == MSG_STATUS_PENDING && m->expected_ack == ack_crc) {
m->status = MSG_STATUS_DELIVERED;
m->expected_ack = 0;
changed = true;
}
}
if (changed) chatStoreMarkDirty();
return changed;
}

int chatStoreThreads(uint8_t keys[][6], int max) {
int count = 0;
for (int i = 0; i < CHAT_STORE_SIZE && count < max; i++) {
int idx = (chat_head - 1 - i + CHAT_STORE_SIZE * 2) % CHAT_STORE_SIZE;
auto m = &chat_store[idx];
if (!m->valid) continue;
bool seen = false;
for (int j = 0; j < count; j++) {
if (memcmp(keys[j], m->prefix, 6) == 0) { seen = true; break; }
}
if (!seen) memcpy(keys[count++], m->prefix, 6);
}
return count;
}

void chatStoreLoad() {
#ifdef ESP32
if (!SPIFFS.begin(true)) return;
File f = SPIFFS.open(CHAT_HIST_FILE, "r");
if (!f) return;
uint32_t magic = 0;
int32_t head = 0;
bool ok = f.read((uint8_t *)&magic, 4) == 4 && magic == CHAT_HIST_MAGIC
&& f.read((uint8_t *)&head, 4) == 4
&& f.read((uint8_t *)chat_store, sizeof(chat_store)) == sizeof(chat_store)
&& head >= 0 && head < CHAT_STORE_SIZE;
f.close();
if (ok) {
chat_head = head;
for (int i = 0; i < CHAT_STORE_SIZE; i++) {
chat_store[i].text[sizeof(chat_store[i].text) - 1] = 0; // guard vs flash corruption
if (chat_store[i].valid && chat_store[i].status == MSG_STATUS_PENDING) {
chat_store[i].status = MSG_STATUS_NO_ACK; // acks can't match across reboot
}
}
} else {
memset(chat_store, 0, sizeof(chat_store));
chat_head = 0;
}
#endif
}

static void chatStoreSave() {
chat_dirty = false;
#ifdef ESP32
File f = SPIFFS.open(CHAT_HIST_FILE, "w");
if (!f) return;
uint32_t magic = CHAT_HIST_MAGIC;
int32_t head = chat_head;
f.write((const uint8_t *)&magic, 4);
f.write((const uint8_t *)&head, 4);
f.write((const uint8_t *)chat_store, sizeof(chat_store));
f.close();
#endif
}

void chatStoreFlushLoop() {
if (chat_dirty && millis() > chat_flush_at) {
chatStoreSave();
}
}

void chatStoreClearThread(const uint8_t key[6]) {
for (int i = 0; i < CHAT_STORE_SIZE; i++) {
if (chat_store[i].valid && memcmp(chat_store[i].prefix, key, 6) == 0) {
chat_store[i].valid = 0;
}
}
chatStoreMarkDirty();
}

// --- unread counters ---
#define UNREAD_SLOTS 24
struct UnreadSlot { uint8_t key[6]; int count; };
static UnreadSlot unread[UNREAD_SLOTS];

void chatStoreUnreadBump(const uint8_t key[6]) {
int free_slot = -1;
for (int i = 0; i < UNREAD_SLOTS; i++) {
if (unread[i].count > 0 && memcmp(unread[i].key, key, 6) == 0) {
unread[i].count++;
return;
}
if (unread[i].count == 0 && free_slot < 0) free_slot = i;
}
if (free_slot >= 0) {
memcpy(unread[free_slot].key, key, 6);
unread[free_slot].count = 1;
}
}

void chatStoreUnreadClear(const uint8_t key[6]) {
for (int i = 0; i < UNREAD_SLOTS; i++) {
if (unread[i].count > 0 && memcmp(unread[i].key, key, 6) == 0) unread[i].count = 0;
}
}

int chatStoreUnreadGet(const uint8_t key[6]) {
for (int i = 0; i < UNREAD_SLOTS; i++) {
if (unread[i].count > 0 && memcmp(unread[i].key, key, 6) == 0) return unread[i].count;
}
return 0;
}

int chatStoreUnreadTotal() {
int total = 0;
for (int i = 0; i < UNREAD_SLOTS; i++) total += unread[i].count;
return total;
}
48 changes: 48 additions & 0 deletions examples/companion_radio/ui-lvgl/ChatStore.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once

#include <Arduino.h>

// On-device chat history, persisted to /chat_hist.bin so threads survive a
// reboot. Fixed-size ring: CHAT_STORE_SIZE messages across all threads.

#define MSG_STATUS_NONE 0
#define MSG_STATUS_PENDING 1
#define MSG_STATUS_DELIVERED 2
#define MSG_STATUS_NO_ACK 3

struct ChatMsg {
uint8_t prefix[6]; // contact pub_key prefix; identifies the thread
uint8_t outgoing; // 1 = sent from this device
uint8_t valid;
uint8_t status; // MSG_STATUS_*
uint32_t timestamp;
uint32_t expected_ack; // ack CRC we're waiting for (pending only)
uint32_t timeout_at; // millis deadline for pending -> no_ack
char text[120];
};

#ifndef CHAT_STORE_SIZE
#define CHAT_STORE_SIZE 40
#endif

// synthetic 6-byte thread key for channel slot idx (vs contact pub_key prefix)
void makeChannelKey(int idx, uint8_t key[6]);
bool isChannelKey(const uint8_t key[6], int* idx_out);

ChatMsg* chatStorePush(const uint8_t* pub_key, bool outgoing, uint32_t timestamp, const char* text);
ChatMsg* chatStoreGet(const uint8_t* pub_key, int k); // k-th newest in thread; NULL when exhausted
void chatStoreLoad();
void chatStoreFlushLoop(); // call from loop(); debounced save when dirty
void chatStoreMarkDirty();
bool chatStoreAck(uint32_t ack_crc); // mark matching pending msg delivered

// distinct thread keys, newest activity first; returns count (<= max)
int chatStoreThreads(uint8_t keys[][6], int max);

void chatStoreClearThread(const uint8_t key[6]); // delete a thread's messages

// per-thread unread counters (RAM only)
void chatStoreUnreadBump(const uint8_t key[6]);
void chatStoreUnreadClear(const uint8_t key[6]);
int chatStoreUnreadGet(const uint8_t key[6]);
int chatStoreUnreadTotal();
Loading