mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-11-12 04:37:33 +00:00
The touch UI (src/ui-touch, unmodified apart from WADA_WEB board guards) builds for macOS with CMake and runs in an SDL2 window: a compat layer for the Arduino/ESP-IDF/FreeRTOS surface, a MyMesh stand-in with a MeshBackend interface (DemoBackend now), and the web board's SDL display/touch/keyboard drivers. WEB_BUILD.md follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
298 lines
14 KiB
C++
298 lines
14 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// DemoBackend: a small believable neighbourhood for the website demo and for
|
|
// developing the web build without a radio. The cast, places and conversations
|
|
// are scripts/doc/web-demo/demo.py's, so the browser demo and the documentation
|
|
// screenshots tell the same story.
|
|
//
|
|
// What it does:
|
|
// * reports a node identity, radio settings, 8 contacts (chat, repeater, room),
|
|
// 2 channels and 4 heard-but-not-added nodes;
|
|
// * on the FIRST boot of a data directory, plays the demo conversations into the UI
|
|
// (a marker file keeps them from being added again on every start);
|
|
// * DMs you send are ACKed after a short, varying delay, sometimes answered;
|
|
// channel posts are "heard repeated"; status pings to the repeater answer;
|
|
// * every so often someone says something on Public, and a node adverts.
|
|
// Everything is deterministic per run except the timings' small jitter.
|
|
#include "MeshBackend.h"
|
|
#include <Arduino.h>
|
|
#include <SPIFFS.h>
|
|
#include <Utils.h>
|
|
#include <helpers/AdvertDataHelpers.h>
|
|
#include <helpers/BaseChatMesh.h>
|
|
#include <helpers/TxtDataHelpers.h>
|
|
#include <vector>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
// demo.py: self node (Amsterdam centre) + radio.
|
|
const int32_t SELF_LAT = 52372800, SELF_LON = 4893400;
|
|
const char* NODE_NAME = "Kaj T-Deck";
|
|
|
|
struct DemoContact { const char* name; uint8_t type; bool fav; bool direct; uint32_t age_s; double dlat, dlon; };
|
|
const DemoContact k_contacts[] = {
|
|
{"Sanne", ADV_TYPE_CHAT, true, true, 95, 0.021, -0.015},
|
|
{"Diego", ADV_TYPE_CHAT, false, false, 1500, -0.034, 0.052},
|
|
{"Marta", ADV_TYPE_CHAT, false, true, 320, 0.0, 0.0},
|
|
{"Hilltop Repeater", ADV_TYPE_REPEATER, false, false, 210, 0.145, 0.088},
|
|
{"Harbor Room", ADV_TYPE_ROOM, false, false, 640, -0.011, 0.031},
|
|
{"Noah", ADV_TYPE_CHAT, false, false, 18000, 0.0, 0.0},
|
|
{"Femke", ADV_TYPE_CHAT, false, true, 86400 * 3, -0.062, -0.044},
|
|
{"Old Node", ADV_TYPE_CHAT, false, false, 86400 * 9, 0.0, 0.0},
|
|
};
|
|
struct DemoDiscovered { const char* name; uint8_t type; uint8_t path_len; uint32_t age_s; double dlat, dlon; };
|
|
const DemoDiscovered k_discovered[] = {
|
|
{"Lars", ADV_TYPE_CHAT, 2, 140, 0.012, 0.077},
|
|
{"Ridge Repeater", ADV_TYPE_REPEATER, 1, 380, 0.191, -0.02},
|
|
{"Cafe Beacon", ADV_TYPE_CHAT, 3, 900, -0.008, 0.006},
|
|
{"Yuki", ADV_TYPE_CHAT, 2, 2600, 0.0, 0.0},
|
|
};
|
|
// demo.py _MESSAGES, oldest first. out=true: sent by this node (from the "phone app").
|
|
struct DemoMsg { const char* thread; bool out; const char* sender; const char* text; uint8_t path; int8_t snr; int8_t rssi; };
|
|
const DemoMsg k_history[] = {
|
|
{"Sanne", false, nullptr, "Morning! You around for the hike this weekend?", 0, 8, -64},
|
|
{"Sanne", true, nullptr, "Yeah! Saturday works. Meshing the whole way up.", 0, 0, 0},
|
|
{"Sanne", false, nullptr, "Perfect. I'll bring the spare T-Deck as a relay.", 0, 7, -66},
|
|
{"Sanne", true, nullptr, "Just tested range from the ridge: 6 km line of sight.", 0, 0, 0},
|
|
{"Sanne", false, nullptr, "Nice, that covers the valley. See you at 8.", 0, 9, -61},
|
|
{"Diego", false, nullptr, "Did the firmware update land on your node?", 2, 2, -98},
|
|
{"Diego", true, nullptr, "Flashing now. beta looks solid so far.", 2, 0, 0},
|
|
{"Diego", false, nullptr, "Great. Ping me once the repeater picks you up.", 2, 1, -101},
|
|
{"Diego", true, nullptr, "Will do \xF0\x9F\x91\x8D", 2, 0, 0},
|
|
{"Marta", true, nullptr, "Map tiles are caching fine on the new build.", 0, 0, 0},
|
|
{"Marta", false, nullptr, "Awesome, offline maps at the campsite then.", 0, 6, -70},
|
|
{"Marta", true, nullptr, "Exactly. Sending you my GPS pin now.", 0, 0, 0},
|
|
{"Marta", false, nullptr, "Didn't come through, try again?", 0, 5, -72},
|
|
{"Harbor Room", false, "Diego", "Anyone near the north dock?", 1, 3, -92},
|
|
{"Harbor Room", false, "Sanne", "I'm 300 m out, what's up?", 1, 7, -68},
|
|
{"Harbor Room", false, "Femke", "Copy. Battery at 82% on the solar node.", 2, 1, -104},
|
|
{"Harbor Room", false, "Diego", "Meeting at the boathouse in 10.", 1, 4, -88},
|
|
{"#Public", false, nullptr, "Hilltop Repeater: back online after the storm, all good.", 1, 4, -90},
|
|
{"#Public", false, nullptr, "Lars: anyone else hearing the Ridge repeater today?", 2, 2, -97},
|
|
};
|
|
// Live chatter on Public, one line every minute or so.
|
|
const char* k_public_chatter[] = {
|
|
"Diego: testing from the tram, 2 hops",
|
|
"Femke: solar node at 85% and climbing",
|
|
"Lars: is there a meetup this month?",
|
|
"Sanne: the ridge link is solid today",
|
|
"Noah: new antenna on the roof, let me know how I sound",
|
|
};
|
|
// What a DM partner might say back.
|
|
const char* k_replies[] = {
|
|
"Got it, thanks!",
|
|
"Copy that.",
|
|
"Sounds good \xF0\x9F\x91\x8C",
|
|
"Received loud and clear.",
|
|
};
|
|
|
|
// A stable public key per name: sha256("wadamesh-demo:" + name). Not a real keypair,
|
|
// which is fine: the demo never encrypts.
|
|
void demoKey(const char* name, uint8_t out[PUB_KEY_SIZE]) {
|
|
std::string s = std::string("wadamesh-demo:") + name;
|
|
mesh::Utils::sha256(out, PUB_KEY_SIZE, (const uint8_t*)s.data(), (int)s.size());
|
|
}
|
|
int32_t absCoord(int32_t base, double d) { return d == 0.0 ? 0 : (int32_t)(base + d * 1000000.0); }
|
|
|
|
struct Pending {
|
|
uint32_t due_ms;
|
|
enum Kind { Ack, Reply, Repeat, Status, Trace } kind;
|
|
uint8_t key[PUB_KEY_SIZE];
|
|
uint32_t value;
|
|
};
|
|
|
|
class DemoBackend : public MeshBackend {
|
|
public:
|
|
const char* name() const override { return "demo"; }
|
|
|
|
void begin(MeshBackendEvents& ev, NodePrefs& prefs, uint8_t self_pub_key[32]) override {
|
|
_ev = &ev;
|
|
demoKey(NODE_NAME, self_pub_key);
|
|
StrHelper::strncpy(prefs.node_name, NODE_NAME, sizeof(prefs.node_name));
|
|
prefs.freq = 869.525f; prefs.bw = 250.0f; prefs.sf = 11; prefs.cr = 5; prefs.tx_power_dbm = 22; // demo.py RADIO
|
|
const uint32_t now = (uint32_t)time(nullptr);
|
|
for (const DemoContact& d : k_contacts) {
|
|
ContactInfo c{};
|
|
demoKey(d.name, c.id.pub_key);
|
|
StrHelper::strncpy(c.name, d.name, sizeof(c.name));
|
|
c.type = d.type;
|
|
c.flags = d.fav ? 0x01 : 0;
|
|
c.out_path_len = d.direct ? 0 : OUT_PATH_UNKNOWN;
|
|
c.last_advert_timestamp = now - d.age_s;
|
|
c.lastmod = now - d.age_s;
|
|
c.gps_lat = absCoord(SELF_LAT, d.dlat);
|
|
c.gps_lon = absCoord(SELF_LON, d.dlon);
|
|
ev.beContact(c, false, 0);
|
|
}
|
|
addChannel(0, "Public", nullptr);
|
|
addChannel(1, "#emergency", "#emergency");
|
|
}
|
|
|
|
void loop(uint32_t now_ms) override {
|
|
if (!_started) { _started = true; _t0 = now_ms; }
|
|
// After the splash: the heard-not-added nodes, then (first boot only) the history.
|
|
if (!_seeded && now_ms - _t0 > 1500) {
|
|
_seeded = true;
|
|
const uint32_t now = (uint32_t)time(nullptr);
|
|
for (const DemoDiscovered& d : k_discovered) {
|
|
ContactInfo c{};
|
|
demoKey(d.name, c.id.pub_key);
|
|
StrHelper::strncpy(c.name, d.name, sizeof(c.name));
|
|
c.type = d.type;
|
|
c.out_path_len = OUT_PATH_UNKNOWN;
|
|
c.last_advert_timestamp = now - d.age_s;
|
|
c.gps_lat = absCoord(SELF_LAT, d.dlat);
|
|
c.gps_lon = absCoord(SELF_LON, d.dlon);
|
|
_ev->beDiscovered(c, d.path_len);
|
|
}
|
|
if (!SPIFFS.exists(k_seed_marker)) {
|
|
playHistory();
|
|
File f = SPIFFS.open(k_seed_marker, "w");
|
|
if (f) { f.print("1"); f.close(); }
|
|
}
|
|
_next_chatter = now_ms + 45000;
|
|
_next_advert = now_ms + 20000;
|
|
}
|
|
if (_seeded && (int32_t)(now_ms - _next_chatter) >= 0) {
|
|
const char* line = k_public_chatter[_chatter_i++ % (sizeof k_public_chatter / sizeof *k_public_chatter)];
|
|
_ev->beChannelMessage(0, 0, line, 1 + (uint8_t)random(3), (int8_t)(4 * (random(12) - 3)), (int8_t)(-60 - random(45)));
|
|
_next_chatter = now_ms + 60000 + (uint32_t)random(30000);
|
|
}
|
|
if (_seeded && (int32_t)(now_ms - _next_advert) >= 0) {
|
|
const DemoContact& d = k_contacts[_advert_i++ % 5]; // the active five re-advert
|
|
ContactInfo c{};
|
|
demoKey(d.name, c.id.pub_key);
|
|
StrHelper::strncpy(c.name, d.name, sizeof(c.name));
|
|
c.type = d.type; c.flags = d.fav ? 0x01 : 0;
|
|
c.out_path_len = d.direct ? 0 : OUT_PATH_UNKNOWN;
|
|
c.last_advert_timestamp = c.lastmod = (uint32_t)time(nullptr);
|
|
c.gps_lat = absCoord(SELF_LAT, d.dlat);
|
|
c.gps_lon = absCoord(SELF_LON, d.dlon);
|
|
_ev->beContact(c, true, d.direct ? 0 : 2);
|
|
_ev->beSignal((int8_t)(4 * (random(14) - 4)), (int8_t)(-55 - random(50)));
|
|
_next_advert = now_ms + 40000 + (uint32_t)random(40000);
|
|
}
|
|
for (size_t i = 0; i < _pending.size();) {
|
|
if ((int32_t)(now_ms - _pending[i].due_ms) < 0) { ++i; continue; }
|
|
Pending p = _pending[i];
|
|
_pending.erase(_pending.begin() + (long)i);
|
|
fire(p);
|
|
}
|
|
}
|
|
|
|
int sendDirect(const ContactInfo& to, uint32_t ts, const char* text, uint32_t& expected_ack,
|
|
uint32_t& est_timeout_ms, uint32_t& sent_fp) override {
|
|
(void)ts; (void)text;
|
|
expected_ack = nextTag();
|
|
est_timeout_ms = 4000;
|
|
const bool direct = to.out_path_len != OUT_PATH_UNKNOWN;
|
|
sent_fp = direct ? 0 : nextTag();
|
|
queue(Pending::Ack, to.id.pub_key, expected_ack, 700 + (uint32_t)random(1500));
|
|
if (sent_fp) queue(Pending::Repeat, to.id.pub_key, sent_fp, 400 + (uint32_t)random(600));
|
|
if (to.type == ADV_TYPE_CHAT && random(100) < 60)
|
|
queue(Pending::Reply, to.id.pub_key, 0, 3500 + (uint32_t)random(4000));
|
|
return direct ? MSG_SEND_SENT_DIRECT : MSG_SEND_SENT_FLOOD;
|
|
}
|
|
bool sendChannel(int channel_idx, const ChannelDetails& ch, uint32_t ts, const char* sender_name,
|
|
const char* text, uint32_t& sent_fp) override {
|
|
(void)channel_idx; (void)ch; (void)ts; (void)sender_name; (void)text;
|
|
sent_fp = nextTag();
|
|
uint8_t none[PUB_KEY_SIZE] = {0};
|
|
const int repeats = 1 + (int)random(3);
|
|
for (int i = 0; i < repeats; ++i) queue(Pending::Repeat, none, sent_fp, 350 + (uint32_t)random(1800));
|
|
return true;
|
|
}
|
|
bool sendAdvert(bool flood) override { (void)flood; return true; }
|
|
|
|
int requestStatus(const ContactInfo& c) override {
|
|
if (c.type != ADV_TYPE_REPEATER && c.type != ADV_TYPE_ROOM) return MSG_SEND_FAILED;
|
|
queue(Pending::Status, c.id.pub_key, 0, 900 + (uint32_t)random(800));
|
|
return c.out_path_len == OUT_PATH_UNKNOWN ? MSG_SEND_SENT_FLOOD : MSG_SEND_SENT_DIRECT;
|
|
}
|
|
uint32_t tracePing(const uint8_t pub_key[32]) override {
|
|
const uint32_t tag = nextTag();
|
|
queue(Pending::Trace, pub_key, tag, 600 + (uint32_t)random(700));
|
|
return tag;
|
|
}
|
|
uint32_t packetsRecv() const override { return 0; }
|
|
|
|
private:
|
|
static constexpr const char* k_seed_marker = "/web_demo_seeded";
|
|
|
|
void addChannel(int idx, const char* name, const char* hashtag_key) {
|
|
ChannelDetails ch{};
|
|
StrHelper::strncpy(ch.name, name, sizeof(ch.name));
|
|
if (hashtag_key) { // hashtag channels: the key is SHA256("#name") (16 bytes)
|
|
mesh::Utils::sha256(ch.channel.secret, 16, (const uint8_t*)hashtag_key, (int)strlen(hashtag_key));
|
|
} else { // MyMesh.cpp PUBLIC_GROUP_PSK, izOH6cXN6mrJ5e26oRXNcg==
|
|
static const uint8_t k_public[16] = {0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
|
|
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72};
|
|
memcpy(ch.channel.secret, k_public, 16);
|
|
}
|
|
_ev->beChannel(idx, ch);
|
|
}
|
|
void playHistory() {
|
|
for (const DemoMsg& m : k_history) {
|
|
const bool is_channel = m.thread[0] == '#';
|
|
if (is_channel) {
|
|
_ev->beChannelMessage(0, 0, m.text, m.path, (int8_t)(m.snr * 4), m.rssi);
|
|
continue;
|
|
}
|
|
uint8_t key[PUB_KEY_SIZE];
|
|
demoKey(m.thread, key);
|
|
if (m.out) {
|
|
const uint32_t ack = nextTag();
|
|
_ev->beOutgoingDirect(key, m.text, ack);
|
|
_ev->beAck(ack); // history: already delivered
|
|
} else if (m.sender) {
|
|
_ev->beRoomMessage(key, m.sender, 0, m.text, m.path, (int8_t)(m.snr * 4), m.rssi);
|
|
} else {
|
|
_ev->beDirectMessage(key, 0, m.text, m.path ? m.path : 0xFF, m.path != 0, (int8_t)(m.snr * 4), m.rssi);
|
|
}
|
|
}
|
|
}
|
|
void fire(const Pending& p) {
|
|
switch (p.kind) {
|
|
case Pending::Ack: _ev->beAck(p.value); break;
|
|
case Pending::Repeat: _ev->beRepeatHeard(p.value); _ev->beSignal((int8_t)(4 * (random(10) - 2)), (int8_t)(-70 - random(30))); break;
|
|
case Pending::Reply: {
|
|
const char* r = k_replies[_reply_i++ % (sizeof k_replies / sizeof *k_replies)];
|
|
_ev->beDirectMessage(p.key, 0, r, 0xFF, false, (int8_t)(4 * (3 + random(6))), (int8_t)(-60 - random(30)));
|
|
break;
|
|
}
|
|
case Pending::Status: {
|
|
// RepeaterStats, packed little-endian like the repeater firmware sends it.
|
|
uint8_t s[56] = {0};
|
|
auto put16 = [&](int o, uint16_t v) { s[o] = (uint8_t)v; s[o + 1] = (uint8_t)(v >> 8); };
|
|
auto put32 = [&](int o, uint32_t v) { for (int i = 0; i < 4; ++i) s[o + i] = (uint8_t)(v >> (8 * i)); };
|
|
put16(0, 4012); // batt_mv
|
|
put16(2, 0); // queue_len
|
|
put16(4, (uint16_t)(int16_t)-118); // noise_floor
|
|
put16(6, (uint16_t)(int16_t)-87); // last_rssi
|
|
put32(8, 18234); put32(12, 9120); // n_recv, n_sent
|
|
put32(16, 6 * 86400 + 4 * 3600 + 17 * 60); // uptime_secs (at offset 16 in the UI's reader)
|
|
_ev->beStatusReply(p.key, s, sizeof s);
|
|
break;
|
|
}
|
|
case Pending::Trace: _ev->beTraceResult(p.value, (int8_t)(5 + random(4)), (int8_t)(6 + random(4))); break;
|
|
}
|
|
}
|
|
void queue(Pending::Kind k, const uint8_t key[PUB_KEY_SIZE], uint32_t value, uint32_t delay_ms) {
|
|
Pending p{};
|
|
p.due_ms = millis() + delay_ms; p.kind = k; p.value = value;
|
|
memcpy(p.key, key, PUB_KEY_SIZE);
|
|
_pending.push_back(p);
|
|
}
|
|
uint32_t nextTag() { uint32_t t = 0; while (!t) t = (uint32_t)random(0x7fffffff) ^ 0x5741d0e0u; return t; }
|
|
|
|
MeshBackendEvents* _ev = nullptr;
|
|
bool _started = false, _seeded = false;
|
|
uint32_t _t0 = 0, _next_chatter = 0, _next_advert = 0;
|
|
unsigned _chatter_i = 0, _advert_i = 0, _reply_i = 0;
|
|
std::vector<Pending> _pending;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
MeshBackend* wadaCreateDemoBackend() { return new DemoBackend(); }
|