mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
feat(sim): two-device messaging + repeater relay over a JS ether
Ports examples/simple_repeater to variants/sim/ (new sim_simple_repeater native env + build_wasm_repeater.sh) and adds a JS "ether" (variants/sim/web/mesh.html) that bridges two real companion_radio WASM instances through a real simple_repeater instance in a strict A<->R<->B topology (no direct A-B link), proving genuine relay routing rather than a shortcut. Also fixes multi-instance issues Phase 2's single-instance design never surfaced: SimDisplayDriver's canvas context/id caching was keyed on a single global instead of per-instance, and both wasm builds were missing _malloc/_free/HEAPU8 runtime exports needed for the ether to poke bytes into an instance's memory. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -167,8 +167,31 @@ private:
|
||||
//
|
||||
// Each call reaches into the DOM via EM_ASM (synchronous, main-thread JS --
|
||||
// fine since this build has no pthreads/proxying). The canvas is looked up
|
||||
// by id once in begin() and cached on a JS global (window.__simCtx) so every
|
||||
// later call is one property read, not a fresh getElementById().
|
||||
// by id once in begin() and cached on a per-instance JS property (see below)
|
||||
// so every later call is one property read, not a fresh getElementById().
|
||||
//
|
||||
// Phase 3 addendum: cached on Module.__simCtx, NOT window.__simCtx as this
|
||||
// class originally did in Phase 2. Phase 2 only ever ran one instance on a
|
||||
// page, so a plain `window` global was invisible/harmless as a design smell;
|
||||
// Phase 3 loads multiple MeshCoreSim()/MeshCoreSimRepeater() instances on
|
||||
// ONE page, and `window` is the single real browser global shared by every
|
||||
// one of them (MODULARIZE isolates each instance's own Module/wasm linear
|
||||
// memory, but NOT the DOM/window) -- two instances' begin() calls would
|
||||
// stomp the same window.__simCtx in turn, and both would end up drawing
|
||||
// through whichever one won. `Module` itself, by contrast, IS a distinct
|
||||
// object per instance (that's the whole point of MODULARIZE) and is already
|
||||
// reachable from inside EM_ASM here as the current instance's own Module
|
||||
// (same access pattern SimFS.h's sim_fs_mount_idbfs()/SimInstance.h's
|
||||
// sim_instance_salt() already rely on for Module['simInstanceTag']), so
|
||||
// storing it there instead scopes it correctly per instance for free.
|
||||
//
|
||||
// The canvas element id is ALSO made per-instance the same way: an untagged
|
||||
// instance (no Module['simInstanceTag'], e.g. Phase 2's original
|
||||
// single-instance web/index.html harness) still looks for plain
|
||||
// "sim-canvas", byte-for-byte the pre-Phase-3 behavior; a tagged instance
|
||||
// (Module['simInstanceTag'] = 'A', from a Phase 3 multi-instance host page
|
||||
// like web/mesh.html) looks for "sim-canvas-A" instead, so two instances on
|
||||
// one page never fight over the same <canvas> element either.
|
||||
class SimDisplayDriverCanvas : public DisplayDriver {
|
||||
bool _on = false;
|
||||
int _cursor_x = 0, _cursor_y = 0;
|
||||
@@ -180,10 +203,12 @@ public:
|
||||
bool begin() {
|
||||
_on = true;
|
||||
EM_ASM({
|
||||
var c = document.getElementById('sim-canvas');
|
||||
if (!c) { console.error('[sim] #sim-canvas not found in the host page'); return; }
|
||||
window.__simCtx = c.getContext('2d');
|
||||
window.__simCtx.imageSmoothingEnabled = false;
|
||||
var tag = (typeof Module !== 'undefined' && Module['simInstanceTag']) ? Module['simInstanceTag'] : '';
|
||||
var id = tag ? ('sim-canvas-' + tag) : 'sim-canvas';
|
||||
var c = document.getElementById(id);
|
||||
if (!c) { console.error('[sim] #' + id + ' not found in the host page'); return; }
|
||||
Module.__simCtx = c.getContext('2d');
|
||||
Module.__simCtx.imageSmoothingEnabled = false;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -193,9 +218,9 @@ public:
|
||||
void turnOff() override {
|
||||
_on = false;
|
||||
EM_ASM({
|
||||
if (!window.__simCtx) return;
|
||||
window.__simCtx.fillStyle = '#000';
|
||||
window.__simCtx.fillRect(0, 0, 128, 64);
|
||||
if (!Module.__simCtx) return;
|
||||
Module.__simCtx.fillStyle = '#000';
|
||||
Module.__simCtx.fillRect(0, 0, 128, 64);
|
||||
});
|
||||
}
|
||||
void clear() override { turnOff(); _on = true; }
|
||||
@@ -203,9 +228,9 @@ public:
|
||||
void startFrame(Color bkg = DARK) override {
|
||||
_color = LIGHT;
|
||||
EM_ASM({
|
||||
if (!window.__simCtx) return;
|
||||
window.__simCtx.fillStyle = '#000';
|
||||
window.__simCtx.fillRect(0, 0, 128, 64);
|
||||
if (!Module.__simCtx) return;
|
||||
Module.__simCtx.fillStyle = '#000';
|
||||
Module.__simCtx.fillRect(0, 0, 128, 64);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -222,8 +247,8 @@ public:
|
||||
void print(const char* str) override {
|
||||
if (!str) return;
|
||||
EM_ASM({
|
||||
if (!window.__simCtx) return;
|
||||
var ctx = window.__simCtx;
|
||||
if (!Module.__simCtx) return;
|
||||
var ctx = Module.__simCtx;
|
||||
ctx.fillStyle = UTF8ToString($3) === 'L' ? '#ffb000' : '#000';
|
||||
ctx.font = '8px monospace';
|
||||
ctx.textBaseline = 'top';
|
||||
@@ -248,16 +273,16 @@ public:
|
||||
|
||||
void fillRect(int x, int y, int w, int h) override {
|
||||
EM_ASM({
|
||||
if (!window.__simCtx) return;
|
||||
window.__simCtx.fillStyle = UTF8ToString($4) === 'L' ? '#ffb000' : '#000';
|
||||
window.__simCtx.fillRect($0, $1, $2, $3);
|
||||
if (!Module.__simCtx) return;
|
||||
Module.__simCtx.fillStyle = UTF8ToString($4) === 'L' ? '#ffb000' : '#000';
|
||||
Module.__simCtx.fillRect($0, $1, $2, $3);
|
||||
}, x, y, w, h, (_color != DARK) ? "L" : "D");
|
||||
}
|
||||
|
||||
void drawRect(int x, int y, int w, int h) override {
|
||||
EM_ASM({
|
||||
if (!window.__simCtx) return;
|
||||
var ctx = window.__simCtx;
|
||||
if (!Module.__simCtx) return;
|
||||
var ctx = Module.__simCtx;
|
||||
ctx.strokeStyle = UTF8ToString($4) === 'L' ? '#ffb000' : '#000';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect($0 + 0.5, $1 + 0.5, $2 - 1, $3 - 1);
|
||||
@@ -274,8 +299,8 @@ public:
|
||||
// a plain integer and the JS side indexes HEAPU8 with it directly.
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override {
|
||||
EM_ASM({
|
||||
if (!window.__simCtx) return;
|
||||
var ctx = window.__simCtx;
|
||||
if (!Module.__simCtx) return;
|
||||
var ctx = Module.__simCtx;
|
||||
var x0 = $0;
|
||||
var y0 = $1;
|
||||
var w = $2;
|
||||
|
||||
+32
-1
@@ -283,14 +283,45 @@ public:
|
||||
// main() before sim_idbfs_ready() ever ran -- so this needs its own
|
||||
// try/catch, unlike Emscripten's own test (which never pre-creates the dir
|
||||
// through a second path first).
|
||||
// Phase 3 addendum: when two module instances mount IDBFS at the exact same
|
||||
// path (both companion_radio instances always do -- SimFS's own root is a
|
||||
// hardcoded literal, "./sim_data", chosen once at static-init time long
|
||||
// before any JS-supplied per-instance config is reachable -- see
|
||||
// SimInstance.h's header comment for why runtime differentiation had to
|
||||
// happen at a different layer), they'd naively share ONE real IndexedDB
|
||||
// database: this SDK's IDBFS (src/lib/libidbfs.js, IDBFS.getDB) keys the
|
||||
// actual browser-level database by the mount path string ITSELF, with no
|
||||
// mount-option override for that. Since both instances run in the same
|
||||
// browser tab (same origin), that's a real collision, not a hypothetical
|
||||
// one -- confirmed by reading the vendored emsdk's own libidbfs.js during
|
||||
// this phase.
|
||||
//
|
||||
// Fix: monkey-patch IDBFS.getDB (only when the host page opted in via
|
||||
// Module['simInstanceTag'], e.g. MeshCoreSim({simInstanceTag: 'A'})) so the
|
||||
// REAL database name gets the tag appended, while the mount PATH stays
|
||||
// "/sim_data" for every instance -- it has to, since that's the exact path
|
||||
// SimFS's own fopen()-shaped calls resolve to, and only files actually
|
||||
// living under the mounted path get persisted at all. This only touches
|
||||
// this module instance's own IDBFS global (each MeshCoreSim() call has its
|
||||
// own independent copy, per MODULARIZE), never the vendored emsdk source
|
||||
// itself. Untagged instances (Module['simInstanceTag'] unset, e.g. the
|
||||
// original Phase 2 single-instance web/index.html harness) get IDBFS.getDB
|
||||
// completely unpatched -- byte-for-byte the pre-Phase-3 behavior.
|
||||
inline void sim_fs_mount_idbfs(const char* root) {
|
||||
EM_ASM({
|
||||
var root = UTF8ToString($0);
|
||||
var tag = (typeof Module !== 'undefined' && Module['simInstanceTag']) ? Module['simInstanceTag'] : '';
|
||||
if (tag) {
|
||||
var origGetDB = IDBFS.getDB;
|
||||
IDBFS.getDB = function(name, callback) {
|
||||
return origGetDB.call(IDBFS, name + '#' + tag, callback);
|
||||
};
|
||||
}
|
||||
try { FS.mkdir(root); } catch (e) { /* already exists -- see comment above */ }
|
||||
FS.mount(IDBFS, { autoPersist: true }, root);
|
||||
FS.syncfs(true, function(err) {
|
||||
if (err) console.error('[sim] IDBFS initial syncfs(true) failed:', err);
|
||||
else console.log('[sim] IDBFS pull from IndexedDB complete, booting app...');
|
||||
else console.log('[sim] IDBFS pull from IndexedDB complete, booting app...' + (tag ? (' (instance ' + tag + ')') : ''));
|
||||
_sim_idbfs_ready();
|
||||
});
|
||||
}, root);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Phase 3: per-instance "salt" for the sim build.
|
||||
//
|
||||
// The two-companion-radio + one-repeater browser demo loads the SAME
|
||||
// compiled companion_radio.wasm/js twice (see build_wasm.sh -- there is no
|
||||
// separate "instance A" vs "instance B" build, by design: MODULARIZE gives
|
||||
// each MeshCoreSim() call its own independent globals/linear memory, so one
|
||||
// binary genuinely serves both roles). That means nothing can be baked in
|
||||
// at compile time to tell the two apart -- any per-instance difference has
|
||||
// to come from the host page, passed in as a plain JS value on the Module
|
||||
// config object *before* that instance's factory promise resolves (e.g.
|
||||
// MeshCoreSim({ simInstanceTag: 'A' })), and read back from C++ only at
|
||||
// *runtime* (never at static-init time -- global C++ constructors run
|
||||
// before any JS-supplied Module config is reachable from generated code,
|
||||
// confirmed by a standalone getenv()-via-Module.ENV experiment during this
|
||||
// phase that came back null; Module.arguments/argv has the same timing
|
||||
// problem for the same reason). Every call site below only ever runs from
|
||||
// setup()-time code (never a global/static initializer), so this is safe.
|
||||
//
|
||||
// Used for two unrelated purposes that both care about the two instances
|
||||
// NOT looking identical to each other:
|
||||
// 1. SimRNG::begin() / SimRadio::getRngSeed() -- without this, two
|
||||
// instances started in the same browser tick could produce the exact
|
||||
// same seed (same wall-clock second, same `rand()` state, and often
|
||||
// the same `this` pointer value across independent-but-identically-
|
||||
// laid-out linear memories), which would hand both simulated devices
|
||||
// the same Ed25519 identity. Mixing in a per-instance tag makes that
|
||||
// collision a non-issue regardless of whether the timing/address
|
||||
// coincidence happens.
|
||||
// 2. SimFS.h's sim_fs_mount_idbfs() namespacing the real IndexedDB
|
||||
// database per instance so instance A/B don't silently share
|
||||
// persisted storage on a page reload (see the long comment there).
|
||||
//
|
||||
// Native (and any wasm build that never sets simInstanceTag) gets salt 0,
|
||||
// i.e. exactly the pre-Phase-3 behavior -- single-instance builds are
|
||||
// unaffected.
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
|
||||
inline uint32_t sim_instance_salt() {
|
||||
return (uint32_t)EM_ASM_INT({
|
||||
var t = (typeof Module !== 'undefined' && Module['simInstanceTag']) ? Module['simInstanceTag'] : '';
|
||||
var h = 2166136261; // FNV-1a, plain JS numbers (>>> 0 keeps it unsigned 32-bit)
|
||||
for (var i = 0; i < t.length; i++) {
|
||||
h = h ^ t.charCodeAt(i);
|
||||
h = (h * 16777619) >>> 0;
|
||||
}
|
||||
return h >>> 0;
|
||||
});
|
||||
}
|
||||
#else
|
||||
inline uint32_t sim_instance_salt() { return 0; }
|
||||
#endif
|
||||
@@ -1,20 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/sensors/LocationProvider.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <ctime>
|
||||
|
||||
// LocationProvider stub for the native sim build: no real GPS, always
|
||||
// reports "no fix". A settable lat/lon (JS-driven) is a Phase-2/3 concern.
|
||||
// LocationProvider stub for the sim build. Phase 1/2: no real GPS, always
|
||||
// reports "no fix" (default-constructed state below reproduces that
|
||||
// exactly). Phase 3: JS-settable per instance via sim_location_set(),
|
||||
// declared at the bottom of this file.
|
||||
//
|
||||
// Note on how this actually reaches the advertised location: companion_radio
|
||||
// itself never calls a LocationProvider through this class -- it reads
|
||||
// `sensors.node_lat`/`node_lon` directly (see e.g.
|
||||
// examples/companion_radio/MyMesh.cpp's createSelfAdvert() call sites), and
|
||||
// `sensors` (declared in target.h) is a plain base `SensorManager`, not a
|
||||
// subclass wired to any LocationProvider -- there was never a real GPS
|
||||
// object plugged in here to begin with (see SensorManager.h: "modify
|
||||
// node_lat/node_lon directly, if you want to affect Advert location"). So
|
||||
// sim_location_set() below writes BOTH this class's own state (satisfying
|
||||
// the LocationProvider interface faithfully, in case future sim code wants
|
||||
// a real one) AND `sensors.node_lat/node_lon` directly (the field
|
||||
// companion_radio's advert path actually reads) -- belt and suspenders,
|
||||
// same underlying values either way.
|
||||
class SimLocationProvider : public LocationProvider {
|
||||
long _lat_e6 = 0, _lon_e6 = 0, _alt_mm = 0;
|
||||
bool _valid = false;
|
||||
|
||||
public:
|
||||
long getLatitude() override { return 0; }
|
||||
long getLongitude() override { return 0; }
|
||||
long getAltitude() override { return 0; }
|
||||
long satellitesCount() override { return 0; }
|
||||
bool isValid() override { return false; }
|
||||
long getTimestamp() override { return 0; }
|
||||
void reset() override { }
|
||||
long getLatitude() override { return _lat_e6; }
|
||||
long getLongitude() override { return _lon_e6; }
|
||||
long getAltitude() override { return _alt_mm; }
|
||||
long satellitesCount() override { return _valid ? 8 : 0; }
|
||||
bool isValid() override { return _valid; }
|
||||
long getTimestamp() override { return _valid ? (long)time(NULL) : 0; }
|
||||
void reset() override { _valid = false; }
|
||||
void begin() override { }
|
||||
void stop() override { }
|
||||
void loop() override { }
|
||||
bool isEnabled() override { return false; }
|
||||
bool isEnabled() override { return _valid; }
|
||||
void sendSentence(const char* sentence) override { }
|
||||
|
||||
void set(float lat_deg, float lon_deg, float alt_m = 0.0f) {
|
||||
_lat_e6 = (long)(lat_deg * 1000000.0f);
|
||||
_lon_e6 = (long)(lon_deg * 1000000.0f);
|
||||
_alt_mm = (long)(alt_m * 1000.0f);
|
||||
_valid = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Single instance, mirroring the pattern of every other Sim* global
|
||||
// (board/radio_driver/rtc_clock/sensors) declared in target.h/target.cpp --
|
||||
// this one isn't wired into target.h/.cpp itself since nothing on the
|
||||
// SIM_PLATFORM boot path currently constructs a LocationProvider at all
|
||||
// (see the class comment above), so it lives here instead as a
|
||||
// self-contained, opt-in addition.
|
||||
extern SensorManager sensors; // defined in variants/sim/target.cpp
|
||||
inline SimLocationProvider& sim_location_provider() {
|
||||
static SimLocationProvider instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
inline void sim_location_apply(float lat_deg, float lon_deg, float alt_m = 0.0f) {
|
||||
sim_location_provider().set(lat_deg, lon_deg, alt_m);
|
||||
sensors.node_lat = lat_deg;
|
||||
sensors.node_lon = lon_deg;
|
||||
sensors.node_altitude = alt_m;
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
// C linkage (no default args, unlike sim_location_apply() above) so JS's
|
||||
// ccall('sim_location_set', ...) can find it by its exact, unmangled name.
|
||||
extern "C" inline EMSCRIPTEN_KEEPALIVE void sim_location_set(float lat_deg, float lon_deg, float alt_m) {
|
||||
sim_location_apply(lat_deg, lon_deg, alt_m);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
#include <Utils.h>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include "SimInstance.h"
|
||||
|
||||
// mesh::RNG implementation for the native sim build. Not cryptographically
|
||||
// strong (rand() under the hood) -- fine for a terminal demo; a real
|
||||
// two-device-messaging phase (Phase 3 of the sim plan) may want to swap
|
||||
// this for something seeded from the OS CSPRNG.
|
||||
// strong (rand() under the hood) -- fine for a terminal demo. Phase 3 mixes
|
||||
// in sim_instance_salt() so that two module instances of the SAME compiled
|
||||
// binary (the browser's two companion_radio instances) can't end up with
|
||||
// the same seed -- see SimInstance.h for why that's a real risk here, not
|
||||
// a hypothetical one.
|
||||
class SimRNG : public mesh::RNG {
|
||||
public:
|
||||
SimRNG() { }
|
||||
void begin() {
|
||||
unsigned seed = (unsigned)time(NULL) ^ (unsigned)(uintptr_t)this;
|
||||
unsigned seed = (unsigned)time(NULL) ^ (unsigned)(uintptr_t)this ^ sim_instance_salt();
|
||||
srand(seed);
|
||||
}
|
||||
void random(uint8_t* dest, size_t sz) override {
|
||||
|
||||
+139
-10
@@ -1,32 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <Dispatcher.h>
|
||||
#include <MeshCore.h> // MAX_TRANS_UNIT
|
||||
#include <ctime>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include "SimInstance.h"
|
||||
|
||||
// mesh::Radio implementation for the native sim build (Phase 1). Mirrors
|
||||
// the FakeRadio in test/test_kiss_modem/test_tx_backpressure.cpp in spirit
|
||||
// mesh::Radio implementation for the native sim build. Mirrors the
|
||||
// FakeRadio in test/test_kiss_modem/test_tx_backpressure.cpp in spirit
|
||||
// (always-succeed send, no real RF) but is written directly against the
|
||||
// REAL mesh::Radio interface in src/Dispatcher.h -- that test mock is for a
|
||||
// different, out-of-date mocked Mesh.h (see the Phase-1 plan) and must not
|
||||
// be copied.
|
||||
//
|
||||
// Phase 1 has exactly one logical device, so there is nothing to actually
|
||||
// exchange packets with: recvRaw() always reports "nothing received",
|
||||
// startSendRaw()/isSendComplete() always report success instantly. Phase 3
|
||||
// of the sim plan (two simulated devices + a repeater) is where this class
|
||||
// grows a real in-memory "ether" so two instances can actually talk.
|
||||
// Phase 1/2 had exactly one logical device, so there was nothing to
|
||||
// actually exchange packets with: recvRaw() always reported "nothing
|
||||
// received", startSendRaw()/isSendComplete() always reported success
|
||||
// instantly. Phase 3 adds a real in-memory "ether": a bounded FIFO of whole
|
||||
// raw packets in each direction, drained/filled by the JS-facing functions
|
||||
// at the bottom of this file. Dispatcher::checkRecv()/checkSend() only ever
|
||||
// deal in whole packets (recvRaw() returns 0-or-a-whole-packet in one call;
|
||||
// startSendRaw() is handed one whole packet to send) -- see
|
||||
// src/Dispatcher.cpp -- so queueing whole packets (not a byte stream)
|
||||
// matches that contract exactly, no framing/reassembly needed on either side.
|
||||
class SimRadio : public mesh::Radio {
|
||||
uint32_t n_recv = 0, n_sent = 0, n_recv_errors = 0;
|
||||
bool _power_save = false;
|
||||
bool _rx_boosted_gain = false;
|
||||
int8_t _tx_dbm = 0;
|
||||
|
||||
// A "clean, high-quality" fake link by default -- packetScore() below is
|
||||
// already a flat 100.0, these back getLastRSSI()/getLastSNR() (read by
|
||||
// Dispatcher for scoring/logging and by MyMesh for the advert path's SNR
|
||||
// display) with plausible non-zero numbers instead of the base class's
|
||||
// default 0/0.
|
||||
float _last_snr = 40.0f; // Packet::_snr stores this * 4 as an int8_t (see Dispatcher.cpp)
|
||||
float _last_rssi = -60.0f;
|
||||
|
||||
struct QueuedPacket {
|
||||
uint8_t data[MAX_TRANS_UNIT];
|
||||
int len = 0;
|
||||
};
|
||||
static const int QUEUE_CAP = 16;
|
||||
QueuedPacket _tx_queue[QUEUE_CAP];
|
||||
int _tx_head = 0, _tx_count = 0;
|
||||
QueuedPacket _rx_queue[QUEUE_CAP];
|
||||
int _rx_head = 0, _rx_count = 0;
|
||||
|
||||
public:
|
||||
void begin() override { }
|
||||
|
||||
int recvRaw(uint8_t* bytes, int sz) override {
|
||||
return 0; // never any incoming data yet (Phase 3: real ether)
|
||||
if (_rx_count == 0) return 0;
|
||||
QueuedPacket& p = _rx_queue[_rx_head];
|
||||
int n = p.len < sz ? p.len : sz;
|
||||
memcpy(bytes, p.data, n);
|
||||
_rx_head = (_rx_head + 1) % QUEUE_CAP;
|
||||
_rx_count--;
|
||||
n_recv++;
|
||||
return n;
|
||||
}
|
||||
|
||||
uint32_t getEstAirtimeFor(int len_bytes) override {
|
||||
@@ -41,7 +74,23 @@ public:
|
||||
|
||||
bool startSendRaw(const uint8_t* bytes, int len) override {
|
||||
n_sent++;
|
||||
return true; // instantly "succeeds" -- nothing is actually transmitted yet
|
||||
if (len > 0) {
|
||||
int n = len > MAX_TRANS_UNIT ? MAX_TRANS_UNIT : len;
|
||||
if (_tx_count == QUEUE_CAP) {
|
||||
// Nobody (no JS ether tick) is draining the outbox -- true for the
|
||||
// Phase 1/2 single-instance builds, since nothing there ever polls
|
||||
// sim_radio_poll_tx(). Drop the oldest queued TX rather than growing
|
||||
// unboundedly; a long-running single-instance sim just silently
|
||||
// "transmits into the void" exactly as it always did pre-Phase-3.
|
||||
_tx_head = (_tx_head + 1) % QUEUE_CAP;
|
||||
_tx_count--;
|
||||
}
|
||||
int idx = (_tx_head + _tx_count) % QUEUE_CAP;
|
||||
memcpy(_tx_queue[idx].data, bytes, n);
|
||||
_tx_queue[idx].len = n;
|
||||
_tx_count++;
|
||||
}
|
||||
return true; // instantly "succeeds" -- matches every real RadioLib wrapper's fire-and-forget startSendRaw()
|
||||
}
|
||||
|
||||
bool isSendComplete() override { return true; }
|
||||
@@ -49,6 +98,9 @@ public:
|
||||
|
||||
bool isInRecvMode() const override { return true; }
|
||||
|
||||
float getLastRSSI() const override { return _last_rssi; }
|
||||
float getLastSNR() const override { return _last_snr; }
|
||||
|
||||
// --- Extra methods below (not part of mesh::Radio) -------------------
|
||||
// MyMesh.cpp/DataStore.cpp/the Settings/Diagnostics UI screens call these
|
||||
// directly on the concrete radio_driver object on every real board, the
|
||||
@@ -58,7 +110,13 @@ public:
|
||||
// static/no-op values.
|
||||
|
||||
uint32_t getRngSeed() {
|
||||
return (uint32_t)time(NULL) ^ (uint32_t)(uintptr_t)this ^ (uint32_t)rand();
|
||||
// sim_instance_salt(): see SimInstance.h -- without it, two module
|
||||
// instances of the same compiled binary started in the same browser
|
||||
// tick could plausibly compute the exact same seed here (same
|
||||
// time(NULL) second, same `rand()` process state, often the same
|
||||
// `this` address across independent-but-identically-laid-out linear
|
||||
// memories) and end up with correlated "random" behaviour.
|
||||
return (uint32_t)time(NULL) ^ (uint32_t)(uintptr_t)this ^ (uint32_t)rand() ^ sim_instance_salt();
|
||||
}
|
||||
|
||||
void getFreqBounds(float& min_mhz, float& max_mhz) const {
|
||||
@@ -89,4 +147,75 @@ public:
|
||||
if (sf < 7) sf = 7; else if (sf > 12) sf = 12;
|
||||
return -7.5f - 2.5f * (float)(sf - 7);
|
||||
}
|
||||
|
||||
// --- Ether hooks (Phase 3) ---------------------------------------------
|
||||
// Called from the JS-facing extern "C" wrappers below (and reusable from
|
||||
// a native test harness, since neither depends on Emscripten). These are
|
||||
// the ONLY way bytes cross between two SimRadio instances -- there is no
|
||||
// shared C++ state between module instances, on purpose (see the plan's
|
||||
// "never run two logical devices in one process" decision).
|
||||
|
||||
// Pop one queued outbound packet (FIFO) into `out`, truncated to
|
||||
// `max_len`. Returns bytes written, or 0 if nothing is queued. A JS ether
|
||||
// tick calls this once per instance per tick to drain whatever this
|
||||
// device tried to transmit since the last tick.
|
||||
int pollTx(uint8_t* out, int max_len) {
|
||||
if (_tx_count == 0) return 0;
|
||||
QueuedPacket& p = _tx_queue[_tx_head];
|
||||
int n = p.len < max_len ? p.len : max_len;
|
||||
memcpy(out, p.data, n);
|
||||
_tx_head = (_tx_head + 1) % QUEUE_CAP;
|
||||
_tx_count--;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Push one raw packet into this device's inbox for recvRaw() to pick up
|
||||
// on Dispatcher's next checkRecv() poll. Returns false (no-op) if `len`
|
||||
// is out of range or the inbox is already full (oldest entry dropped to
|
||||
// make room rather than blocking -- a real radio would just drop an
|
||||
// over-the-air packet it couldn't buffer either).
|
||||
bool injectRx(const uint8_t* data, int len) {
|
||||
if (len <= 0 || len > MAX_TRANS_UNIT) return false;
|
||||
if (_rx_count == QUEUE_CAP) {
|
||||
_rx_head = (_rx_head + 1) % QUEUE_CAP;
|
||||
_rx_count--;
|
||||
n_recv_errors++;
|
||||
}
|
||||
int idx = (_rx_head + _rx_count) % QUEUE_CAP;
|
||||
memcpy(_rx_queue[idx].data, data, len);
|
||||
_rx_queue[idx].len = len;
|
||||
_rx_count++;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
|
||||
// JS-facing ether bridge. `radio_driver` is a file-scope global defined in
|
||||
// variants/sim/target.cpp (one instance per compiled module -- see
|
||||
// target.h's `extern SimRadio radio_driver;`), so these two functions
|
||||
// always operate on THIS module instance's own radio, never any other's.
|
||||
// Because -sMODULARIZE=1 -sEXPORT_NAME=MeshCoreSim gives every
|
||||
// MeshCoreSim() call its own independent Module/globals/linear memory
|
||||
// (verified empirically for this phase, not just assumed from the build
|
||||
// flags -- see the Phase 3 report), calling instanceA.ccall('sim_radio_poll_tx', ...)
|
||||
// and instanceB.ccall('sim_radio_poll_tx', ...) really do reach two
|
||||
// separate SimRadio objects with no way to cross-talk except through
|
||||
// whatever the host page's ether loop explicitly wires together by
|
||||
// shuttling bytes from one instance's poll_tx into another's inject_rx.
|
||||
//
|
||||
// `inline` (not just EMSCRIPTEN_KEEPALIVE'd) because this header is
|
||||
// included from several .cpp translation units (via target.h) -- without
|
||||
// it, each would emit its own non-inline definition and the link would
|
||||
// fail with duplicate symbols, same reasoning as sim_fs_mount_idbfs() in
|
||||
// SimFS.h.
|
||||
extern SimRadio radio_driver;
|
||||
|
||||
extern "C" inline EMSCRIPTEN_KEEPALIVE int sim_radio_poll_tx(uint8_t* out_buf, int max_len) {
|
||||
return radio_driver.pollTx(out_buf, max_len);
|
||||
}
|
||||
extern "C" inline EMSCRIPTEN_KEEPALIVE void sim_radio_inject_rx(const uint8_t* data, int len) {
|
||||
radio_driver.injectRx(data, len);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -167,6 +167,18 @@ done
|
||||
# Module.FS.readFile('/sim_data/identity/_main.id') -- to prove IDBFS
|
||||
# persistence with a real file-content comparison across a reload, not just
|
||||
# "the app didn't crash". Not required for the app itself.
|
||||
#
|
||||
# EXPORTED_FUNCTIONS=_main,_malloc,_free (Phase 3 addition): every
|
||||
# EMSCRIPTEN_KEEPALIVE-attributed function (all the sim_*() hooks across
|
||||
# variants/sim/ and examples/companion_radio/) is exported regardless of
|
||||
# this list -- that's what the attribute is FOR -- so this only adds
|
||||
# malloc()/free() themselves, needed by web/mesh.html's JS "ether" to
|
||||
# allocate a scratch buffer per instance for sim_radio_poll_tx()/
|
||||
# sim_radio_inject_rx() (see variants/sim/SimRadio.h). Without this,
|
||||
# Module._malloc() would abort at runtime with "malloc() called but not
|
||||
# included in the build" -- confirmed by hitting exactly that during Phase
|
||||
# 3. Purely additive: nothing Phase 2's web/index.html already does
|
||||
# (sim_enqueue_key() with a plain number, no buffer marshaling) is affected.
|
||||
"$EMXX" \
|
||||
"${OBJS[@]}" \
|
||||
-lidbfs.js \
|
||||
@@ -176,7 +188,8 @@ done
|
||||
-sEXPORT_NAME=MeshCoreSim \
|
||||
-sENVIRONMENT=web \
|
||||
-sEXIT_RUNTIME=0 \
|
||||
-sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap \
|
||||
-sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap,HEAPU8 \
|
||||
-sEXPORTED_FUNCTIONS=_main,_malloc,_free \
|
||||
-o "$OUT_DIR/meshcore_sim.js"
|
||||
|
||||
echo ""
|
||||
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 3 (Emscripten) build script for the examples/simple_repeater sim --
|
||||
# sibling of build_wasm.sh (the companion_radio one), NOT a modification of
|
||||
# it, so the existing companion_radio build stays byte-for-byte untouched
|
||||
# (see the "hard constraints" in the Phase 3 plan: build_wasm.sh must keep
|
||||
# working exactly as before).
|
||||
#
|
||||
# Differences from build_wasm.sh, all deliberate:
|
||||
# - examples/simple_repeater/{main,MyMesh}.cpp instead of
|
||||
# examples/companion_radio/{main,MyMesh,DataStore}.cpp +
|
||||
# ui-new/UITask.cpp -- no UI/display/BLE in this app at all, and
|
||||
# UITask.cpp specifically does NOT compile cleanly with no display
|
||||
# defined (real Arduino GPIO constants used unconditionally -- see
|
||||
# variants/sim/platformio.ini's [env:sim_simple_repeater] comment for
|
||||
# the same issue hit there), so it's excluded, matching that env.
|
||||
# - No -DDISPLAY_CLASS -- headless, matching every real hardware
|
||||
# repeater env's convention when no display is fitted.
|
||||
# - -DSIM_FS_ROOT=\"/sim_data_repeater\" -- must match the
|
||||
# "./sim_data_repeater" SimFS root examples/simple_repeater/main.cpp's
|
||||
# own SIM_PLATFORM branch constructs (see variants/sim/sim_main.cpp),
|
||||
# kept DIFFERENT from companion_radio's "/sim_data" so a repeater
|
||||
# instance's IDBFS-backed identity storage can never collide with a
|
||||
# companion instance's, even on the same page/origin.
|
||||
# - -sEXPORT_NAME=MeshCoreSimRepeater (not MeshCoreSim) -- the host page
|
||||
# loads both build/meshcore_sim.js (build/) and
|
||||
# build/repeater/meshcore_sim_repeater.js side by side; each is its own
|
||||
# MODULARIZE factory function under a distinct global name so loading
|
||||
# both on one page can't collide.
|
||||
# - Output lands under web/build/repeater/, not web/build/, so the two
|
||||
# builds' .wasm/.js/obj/ trees never share a directory.
|
||||
#
|
||||
# Usage:
|
||||
# variants/sim/build_wasm_repeater.sh # release-ish build (-O2)
|
||||
# variants/sim/build_wasm_repeater.sh debug # -O0 -g
|
||||
#
|
||||
# Same emsdk 6.0.9 requirement as build_wasm.sh -- see that script's header
|
||||
# comment for the install command if variants/sim/tools/emsdk/ is missing.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
EMSDK_DIR="$SCRIPT_DIR/tools/emsdk"
|
||||
EMXX="$EMSDK_DIR/upstream/emscripten/em++"
|
||||
OUT_DIR="$SCRIPT_DIR/web/build/repeater"
|
||||
|
||||
if [ ! -x "$EMXX" ]; then
|
||||
echo "error: em++ not found at $EMXX" >&2
|
||||
echo "Install it first:" >&2
|
||||
echo " cd $EMSDK_DIR && python3 ./emsdk.py install 6.0.9 && python3 ./emsdk.py activate 6.0.9" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_MODE="${1:-release}"
|
||||
if [ "$BUILD_MODE" = "debug" ]; then
|
||||
OPT_FLAGS=(-O0 -g)
|
||||
else
|
||||
OPT_FLAGS=(-O2)
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Same list as variants/sim/platformio.ini's [env:sim_simple_repeater]
|
||||
# build_src_filter, spelled as real paths. Keep in sync with that file by
|
||||
# hand (same convention build_wasm.sh already established for the
|
||||
# companion_radio side).
|
||||
SRCS=(
|
||||
src/Dispatcher.cpp
|
||||
src/Identity.cpp
|
||||
src/Mesh.cpp
|
||||
src/Packet.cpp
|
||||
src/Utils.cpp
|
||||
src/helpers/AdvertDataHelpers.cpp
|
||||
src/helpers/BaseChatMesh.cpp
|
||||
src/helpers/ClientACL.cpp
|
||||
src/helpers/CommonCLI.cpp
|
||||
src/helpers/ConfigSerializer.cpp
|
||||
src/helpers/DeviceDiag.cpp
|
||||
src/helpers/IdentityStore.cpp
|
||||
src/helpers/RegionMap.cpp
|
||||
src/helpers/StaticPoolPacketManager.cpp
|
||||
src/helpers/TransportKeyStore.cpp
|
||||
src/helpers/TxtDataHelpers.cpp
|
||||
lib/ed25519/add_scalar.c
|
||||
lib/ed25519/fe.c
|
||||
lib/ed25519/ge.c
|
||||
lib/ed25519/key_exchange.c
|
||||
lib/ed25519/keypair.c
|
||||
lib/ed25519/sc.c
|
||||
lib/ed25519/seed.c
|
||||
lib/ed25519/sha512.c
|
||||
lib/ed25519/sign.c
|
||||
lib/ed25519/verify.c
|
||||
variants/sim/sim_main.cpp
|
||||
variants/sim/target.cpp
|
||||
variants/sim/thirdparty/crypto/AES128.cpp
|
||||
variants/sim/thirdparty/crypto/AESCommon.cpp
|
||||
variants/sim/thirdparty/crypto/BigNumberUtil.cpp
|
||||
variants/sim/thirdparty/crypto/BlockCipher.cpp
|
||||
variants/sim/thirdparty/crypto/Crypto.cpp
|
||||
variants/sim/thirdparty/crypto/Curve25519.cpp
|
||||
variants/sim/thirdparty/crypto/Ed25519.cpp
|
||||
variants/sim/thirdparty/crypto/Hash.cpp
|
||||
variants/sim/thirdparty/crypto/rng_stub.cpp
|
||||
variants/sim/thirdparty/crypto/SHA256.cpp
|
||||
variants/sim/thirdparty/crypto/SHA512.cpp
|
||||
variants/sim/thirdparty/cayennelpp/CayenneLPP.cpp
|
||||
variants/sim/thirdparty/cayennelpp/CayenneLPPPolyline.cpp
|
||||
examples/simple_repeater/main.cpp
|
||||
examples/simple_repeater/MyMesh.cpp
|
||||
)
|
||||
|
||||
INCLUDES=(
|
||||
-Ivariants/sim/arduino
|
||||
-Ivariants/sim
|
||||
-Ivariants/sim/thirdparty/crypto
|
||||
-Ivariants/sim/thirdparty/cayennelpp
|
||||
-Ivariants/sim/thirdparty/arduinojson
|
||||
-Ilib/ed25519
|
||||
-Isrc
|
||||
-Iexamples/simple_repeater
|
||||
)
|
||||
|
||||
DEFINES=(
|
||||
-DSIM_PLATFORM
|
||||
-DMESH_DEBUG=0
|
||||
-DENABLE_ADVERT_ON_BOOT=1
|
||||
-DSIM_FS_ROOT="\"/sim_data_repeater\""
|
||||
)
|
||||
|
||||
# -funsigned-char: same reasoning as build_wasm.sh (KEY_* codes aren't
|
||||
# actually used by this app at all -- no UI -- but every other sim TU is
|
||||
# compiled with this flag and mixing char signedness across TUs that share
|
||||
# struct layouts/bitfields is asking for trouble, so keep it uniform).
|
||||
COMMON_FLAGS=(-std=c++17 -funsigned-char "${OPT_FLAGS[@]}" "${DEFINES[@]}" "${INCLUDES[@]}")
|
||||
|
||||
# Mirror each source's own directory under obj/ (see build_wasm.sh's own
|
||||
# comment on this -- the sha512.o/SHA512.o macOS case-collision reason
|
||||
# applies identically here since it's the same source tree).
|
||||
OBJ_DIR="$OUT_DIR/obj"
|
||||
rm -rf "$OBJ_DIR"
|
||||
OBJS=()
|
||||
for src in "${SRCS[@]}"; do
|
||||
obj="$OBJ_DIR/${src%.*}.o"
|
||||
mkdir -p "$(dirname "$obj")"
|
||||
"$EMXX" -c "${COMMON_FLAGS[@]}" "$src" -o "$obj"
|
||||
OBJS+=("$obj")
|
||||
done
|
||||
|
||||
"$EMXX" \
|
||||
"${OBJS[@]}" \
|
||||
-lidbfs.js \
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sFORCE_FILESYSTEM=1 \
|
||||
-sMODULARIZE=1 \
|
||||
-sEXPORT_NAME=MeshCoreSimRepeater \
|
||||
-sENVIRONMENT=web \
|
||||
-sEXIT_RUNTIME=0 \
|
||||
-sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap,HEAPU8 \
|
||||
-sEXPORTED_FUNCTIONS=_main,_malloc,_free \
|
||||
-o "$OUT_DIR/meshcore_sim_repeater.js"
|
||||
|
||||
echo ""
|
||||
echo "Built: $OUT_DIR/meshcore_sim_repeater.js (+ .wasm alongside it)"
|
||||
@@ -62,3 +62,79 @@ build_src_filter =
|
||||
+<../variants/sim/thirdparty/cayennelpp/*.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
|
||||
; Phase 3: native build of the real examples/simple_repeater app logic
|
||||
; (MyMesh/no UI) against the same variants/sim/ interfaces as
|
||||
; [env:sim_companion_radio] above. No DISPLAY_CLASS defined -- a repeater
|
||||
; doesn't need one (examples/simple_repeater/main.cpp already gates its
|
||||
; UITask/display use behind #ifdef DISPLAY_CLASS, same as every real
|
||||
; headless-repeater hardware env does), which also means UITask.cpp/.h
|
||||
; never need to be ported/compiled for this target at all.
|
||||
;
|
||||
; Everything else mirrors [env:sim_companion_radio] deliberately closely
|
||||
; (same src/*.cpp list, same real hardware envs' own convention of sharing
|
||||
; one broad src/helpers/*.cpp filter across companion_radio/simple_repeater/
|
||||
; room_server -- see e.g. variants/ebyte_eora_s3/platformio.ini's
|
||||
; [env:Ebyte_EoRa-S3] base + its "_Repeater" env only adding
|
||||
; "+<../examples/simple_repeater>" on top) -- a few of those .cpp files
|
||||
; (BaseChatMesh.cpp, ConfigSerializer.cpp, DeviceDiag.cpp) aren't actually
|
||||
; reachable from simple_repeater's own code, but compiling them in unused is
|
||||
; harmless and keeps this env a straightforward diff against the one above
|
||||
; rather than a hand-pruned, easy-to-drift-out-of-sync list.
|
||||
[env:sim_simple_repeater]
|
||||
platform = native
|
||||
build_type = debug
|
||||
build_flags =
|
||||
-std=c++17
|
||||
-g
|
||||
-funsigned-char
|
||||
-D SIM_PLATFORM
|
||||
-D MESH_DEBUG=0
|
||||
; Real hardware repeater envs get this from [arduino_base] (which this
|
||||
; from-scratch platform=native env deliberately doesn't extend -- see the
|
||||
; header comment on [env:sim_companion_radio] above); set it explicitly so
|
||||
; the repeater actually broadcasts its own identity on boot, same as a
|
||||
; real one would.
|
||||
-D ENABLE_ADVERT_ON_BOOT=1
|
||||
-I variants/sim/arduino
|
||||
-I variants/sim
|
||||
-I variants/sim/thirdparty/crypto
|
||||
-I variants/sim/thirdparty/cayennelpp
|
||||
-I variants/sim/thirdparty/arduinojson
|
||||
-I lib/ed25519
|
||||
-I src
|
||||
-I examples/simple_repeater
|
||||
build_src_filter =
|
||||
-<*>
|
||||
+<../src/Dispatcher.cpp>
|
||||
+<../src/Identity.cpp>
|
||||
+<../src/Mesh.cpp>
|
||||
+<../src/Packet.cpp>
|
||||
+<../src/Utils.cpp>
|
||||
+<../src/helpers/AdvertDataHelpers.cpp>
|
||||
+<../src/helpers/BaseChatMesh.cpp>
|
||||
+<../src/helpers/ClientACL.cpp>
|
||||
+<../src/helpers/CommonCLI.cpp>
|
||||
+<../src/helpers/ConfigSerializer.cpp>
|
||||
+<../src/helpers/DeviceDiag.cpp>
|
||||
+<../src/helpers/IdentityStore.cpp>
|
||||
+<../src/helpers/RegionMap.cpp>
|
||||
+<../src/helpers/StaticPoolPacketManager.cpp>
|
||||
+<../src/helpers/TransportKeyStore.cpp>
|
||||
+<../src/helpers/TxtDataHelpers.cpp>
|
||||
+<../lib/ed25519/*.c>
|
||||
+<../variants/sim/*.cpp>
|
||||
+<../variants/sim/thirdparty/crypto/*.cpp>
|
||||
+<../variants/sim/thirdparty/cayennelpp/*.cpp>
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
; UITask.cpp uses real Arduino GPIO constants (HIGH/LOW/digitalRead(),
|
||||
; gated on real #if PIN_USER_BTN/UI_HAS_JOYSTICK defines this env never
|
||||
; sets) directly at file scope in its button-polling code, unconditionally
|
||||
; -- unlike companion_radio's ui-new/UITask.cpp, it isn't written to
|
||||
; compile cleanly with no display at all. Since DISPLAY_CLASS is
|
||||
; deliberately undefined for this headless repeater env (main.cpp's own
|
||||
; `#ifdef DISPLAY_CLASS` already skips constructing a UITask instance
|
||||
; entirely), this translation unit is simply never needed -- exclude it
|
||||
; rather than porting GPIO stubs into variants/sim/arduino/Arduino.h for
|
||||
; code that would never run.
|
||||
-<../examples/simple_repeater/UITask.cpp>
|
||||
|
||||
@@ -48,13 +48,23 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_idbfs_ready() {
|
||||
emscripten_set_main_loop(sim_main_loop_tick, 0, 1);
|
||||
}
|
||||
|
||||
// The app-level SimFS root to mount IDBFS at -- must match whatever
|
||||
// relative "./sim_data..." path that app's own main.cpp constructs its
|
||||
// SimFS with (see the long comment on sim_fs_mount_idbfs() in SimFS.h for
|
||||
// why those are the same filesystem node under Emscripten's default cwd,
|
||||
// "/"). Defaults to companion_radio's root, unchanged from Phase 2 --
|
||||
// Phase 3's simple_repeater build (variants/sim/build_wasm_repeater.sh)
|
||||
// overrides this via -DSIM_FS_ROOT so its identity storage lands under a
|
||||
// DIFFERENT IDBFS-backed root than a companion instance's, same reasoning
|
||||
// as examples/simple_repeater/main.cpp's SIM_PLATFORM branch using
|
||||
// "./sim_data_repeater" instead of "./sim_data" for its SimFS.
|
||||
#ifndef SIM_FS_ROOT
|
||||
#define SIM_FS_ROOT "/sim_data"
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
printf("MeshCore sim (wasm) starting -- mounting IDBFS at /sim_data...\n");
|
||||
// "/sim_data" must match the relative "./sim_data" DataStore store(sim_fs,
|
||||
// ...) in examples/companion_radio/main.cpp resolves to -- see the long
|
||||
// comment on sim_fs_mount_idbfs() in SimFS.h for why those are the same
|
||||
// filesystem node under Emscripten's default cwd ("/").
|
||||
sim_fs_mount_idbfs("/sim_data");
|
||||
printf("MeshCore sim (wasm) starting -- mounting IDBFS at " SIM_FS_ROOT "...\n");
|
||||
sim_fs_mount_idbfs(SIM_FS_ROOT);
|
||||
// Keep the runtime alive after main() returns instead of tearing it down
|
||||
// (the default for a `main()` that returns under Emscripten) -- the real
|
||||
// boot sequence hasn't happened yet, it's waiting on sim_idbfs_ready()
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
#include "SimMainBoard.h"
|
||||
#include "SimRTCClock.h"
|
||||
#include <helpers/SensorManager.h>
|
||||
// Included here (rather than only where it's used) so its JS-facing
|
||||
// sim_location_set() EMSCRIPTEN_KEEPALIVE export actually gets compiled
|
||||
// into every SIM_PLATFORM target that includes target.h (companion_radio
|
||||
// AND simple_repeater) -- an inline function nobody #includes never gets
|
||||
// emitted at all, KEEPALIVE or not.
|
||||
#include "SimLocationProvider.h"
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "SimDisplayDriver.h"
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
Phase 3 proof harness -- two real companion_radio instances (A, B) and one
|
||||
real simple_repeater instance (R), all three the SAME compiled binaries as
|
||||
build_wasm.sh/build_wasm_repeater.sh produce (no special "multi" build),
|
||||
loaded TWICE (A/B) and once (R) via their MODULARIZE factory functions
|
||||
(MeshCoreSim()/MeshCoreSimRepeater()) -- each call returns an independent
|
||||
Module instance with its own linear memory/globals, confirmed empirically
|
||||
(see the Phase 3 report). They are bridged ONLY by the plain JS "ether"
|
||||
loop below, which shuttles real raw packet bytes between each instance's
|
||||
SimRadio TX/RX queues (sim_radio_poll_tx()/sim_radio_inject_rx(), see
|
||||
variants/sim/SimRadio.h) -- no protocol logic is reimplemented here, this
|
||||
is pure transport plumbing.
|
||||
|
||||
Topology (deliberately NOT a full mesh): A <-> R <-> B only. A and B are
|
||||
never wired directly to each other, so any message that reaches the other
|
||||
side MUST have been relayed by R -- this is what proves A->R->B routing
|
||||
rather than a direct A->B shortcut.
|
||||
|
||||
Serve this directory with a real static file server (http://, not
|
||||
file://), e.g. from this directory:
|
||||
python3 -m http.server 8080
|
||||
then open http://localhost:8080/mesh.html .
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MeshCore sim -- two devices + repeater (Phase 3)</title>
|
||||
<style>
|
||||
body {
|
||||
background: #1b1b1b;
|
||||
color: #ddd;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { font-size: 16px; font-weight: 600; margin: 0 0 4px; }
|
||||
.sub { font-size: 12px; color: #999; margin-bottom: 16px; }
|
||||
.devices { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; }
|
||||
.device { display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||
.device h2 { font-size: 13px; margin: 0; color: #ffb000; }
|
||||
canvas.sim-canvas {
|
||||
background: #000;
|
||||
border: 2px solid #444;
|
||||
image-rendering: pixelated;
|
||||
width: 384px;
|
||||
height: 192px;
|
||||
}
|
||||
.status { font-size: 11px; color: #999; min-height: 1.2em; text-align: center; }
|
||||
.row { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
|
||||
button {
|
||||
background: #2a2a2a;
|
||||
color: #eee;
|
||||
border: 1px solid #555;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:active { background: #444; }
|
||||
button:disabled { opacity: 0.4; cursor: default; }
|
||||
.repeater {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 18px;
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
min-width: 160px;
|
||||
justify-content: center;
|
||||
}
|
||||
.repeater h2 { font-size: 13px; margin: 0; color: #7fd0ff; }
|
||||
.tower {
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
filter: grayscale(1) brightness(0.6);
|
||||
transition: filter 0.15s, transform 0.15s;
|
||||
}
|
||||
.tower.relaying {
|
||||
filter: grayscale(0) brightness(1.3);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
.relay-count { font-size: 12px; color: #7fd0ff; }
|
||||
#controls { margin-top: 20px; display: flex; flex-direction: column; gap: 10px; }
|
||||
#controls .row { justify-content: flex-start; }
|
||||
#log {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
height: 220px;
|
||||
overflow-y: auto;
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
font: 11px/1.5 ui-monospace, monospace;
|
||||
color: #7a7;
|
||||
padding: 8px 10px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.ether-config { font-size: 11px; color: #999; display: flex; gap: 14px; align-items: center; }
|
||||
.ether-config input[type=number] { width: 56px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MeshCore sim -- two devices + repeater, bridged by a JS "ether"</h1>
|
||||
<div class="sub">Real companion_radio x2 (A, B) + real simple_repeater (R), same compiled wasm as the single-instance harness -- topology is A <-> R <-> B only (no direct A-B link), so any delivered message proves real relay routing.</div>
|
||||
|
||||
<div class="devices">
|
||||
<div class="device">
|
||||
<h2>Instance A (companion_radio)</h2>
|
||||
<canvas id="sim-canvas-A" class="sim-canvas" width="128" height="64"></canvas>
|
||||
<div class="status" id="status-A">loading...</div>
|
||||
<div class="row">
|
||||
<button data-instance="A" data-key="0xB5">↑</button>
|
||||
<button data-instance="A" data-key="0xB4">←</button>
|
||||
<button data-instance="A" data-key="13">OK</button>
|
||||
<button data-instance="A" data-key="0xB7">→</button>
|
||||
<button data-instance="A" data-key="0xB6">↓</button>
|
||||
<button data-instance="A" data-key="27">Esc</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repeater">
|
||||
<h2>Instance R (simple_repeater)</h2>
|
||||
<div class="tower" id="tower">📡</div>
|
||||
<div class="relay-count" id="relay-count">relayed: 0</div>
|
||||
<div class="status" id="status-R">loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="device">
|
||||
<h2>Instance B (companion_radio)</h2>
|
||||
<canvas id="sim-canvas-B" class="sim-canvas" width="128" height="64"></canvas>
|
||||
<div class="status" id="status-B">loading...</div>
|
||||
<div class="row">
|
||||
<button data-instance="B" data-key="0xB5">↑</button>
|
||||
<button data-instance="B" data-key="0xB4">←</button>
|
||||
<button data-instance="B" data-key="13">OK</button>
|
||||
<button data-instance="B" data-key="0xB7">→</button>
|
||||
<button data-instance="B" data-key="0xB6">↓</button>
|
||||
<button data-instance="B" data-key="27">Esc</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="controls">
|
||||
<div class="ether-config">
|
||||
<label>ether delay (ms): <input type="number" id="ether-delay" value="150" min="0" max="5000"></label>
|
||||
<label>drop probability (0-1): <input type="number" id="ether-drop" value="0" min="0" max="1" step="0.05"></label>
|
||||
<span id="ether-status">ether: stopped</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="btn-advert-a">Send Advert (A, flood)</button>
|
||||
<button id="btn-advert-b">Send Advert (B, flood)</button>
|
||||
<button id="btn-dm-ab">Send DM A→B</button>
|
||||
<button id="btn-dm-ba">Send DM B→A</button>
|
||||
<button id="btn-open-dm-a">Open DM screen on A</button>
|
||||
<button id="btn-open-dm-b">Open DM screen on B</button>
|
||||
<button id="btn-run-demo">Run full demo (advert both ways, send A→B, open B's DM)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="log"></div>
|
||||
|
||||
<script src="build/meshcore_sim.js"></script>
|
||||
<script src="build/repeater/meshcore_sim_repeater.js"></script>
|
||||
<script>
|
||||
const logEl = document.getElementById('log');
|
||||
function log(msg) {
|
||||
const line = document.createElement('div');
|
||||
line.textContent = '[' + new Date().toISOString().substr(11, 12) + '] ' + msg;
|
||||
logEl.appendChild(line);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
const MAX_PKT = 256; // MAX_TRANS_UNIT (255) rounded up, src/MeshCore.h
|
||||
|
||||
// Per-instance wrapper: allocates a persistent scratch TX-poll buffer,
|
||||
// exposes small ccall-based helpers. `mod` is a ready Emscripten Module
|
||||
// instance (post-await on the MODULARIZE factory promise).
|
||||
function wrapInstance(mod, label) {
|
||||
const txBuf = mod._malloc(MAX_PKT);
|
||||
return {
|
||||
mod, label, txBuf,
|
||||
isReady() { return mod.ccall('sim_is_ready', 'number', [], []) === 1; },
|
||||
pollTx() {
|
||||
// Drain every queued outbound packet this tick (SimRadio's queue
|
||||
// is a bounded FIFO -- see variants/sim/SimRadio.h -- so a single
|
||||
// poll might not be enough if several packets queued up between
|
||||
// ether ticks).
|
||||
const packets = [];
|
||||
for (;;) {
|
||||
const n = mod.ccall('sim_radio_poll_tx', 'number', ['number', 'number'], [txBuf, MAX_PKT]);
|
||||
if (n <= 0) break;
|
||||
packets.push(mod.HEAPU8.slice(txBuf, txBuf + n));
|
||||
}
|
||||
return packets;
|
||||
},
|
||||
injectRx(bytes) {
|
||||
const ptr = mod._malloc(bytes.length);
|
||||
mod.HEAPU8.set(bytes, ptr);
|
||||
mod.ccall('sim_radio_inject_rx', null, ['number', 'number'], [ptr, bytes.length]);
|
||||
mod._free(ptr);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// --- The "ether": fixed delay + optional flat drop probability, per
|
||||
// the plan's "keep it simple, not a full RF model" instruction. Each
|
||||
// link below is one-directional; the topology array at the bottom
|
||||
// encodes A<->R<->B with no direct A<->B link.
|
||||
let etherLinks = []; // [{from, to}]
|
||||
let etherTimer = null;
|
||||
|
||||
function etherTick() {
|
||||
const delayMs = Number(document.getElementById('ether-delay').value) || 0;
|
||||
const dropProb = Math.min(1, Math.max(0, Number(document.getElementById('ether-drop').value) || 0));
|
||||
for (const link of etherLinks) {
|
||||
const packets = link.from.pollTx();
|
||||
for (const pkt of packets) {
|
||||
for (const to of link.to) {
|
||||
if (Math.random() < dropProb) {
|
||||
log(`ether: dropped ${pkt.length}B ${link.from.label} -> ${to.label}`);
|
||||
continue;
|
||||
}
|
||||
setTimeout(() => {
|
||||
to.injectRx(pkt);
|
||||
}, delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startEther() {
|
||||
if (etherTimer) return;
|
||||
etherTimer = setInterval(etherTick, 60);
|
||||
document.getElementById('ether-status').textContent = 'ether: running';
|
||||
}
|
||||
|
||||
let lastRelayCount = 0;
|
||||
function pollRelay(R) {
|
||||
if (!R.isReady()) return;
|
||||
const n = R.mod.ccall('sim_repeater_get_relay_count', 'number', [], []);
|
||||
document.getElementById('relay-count').textContent = 'relayed: ' + n;
|
||||
if (n > lastRelayCount) {
|
||||
log(`*** Repeater R relayed a packet (count ${lastRelayCount} -> ${n}) ***`);
|
||||
const tower = document.getElementById('tower');
|
||||
tower.classList.add('relaying');
|
||||
setTimeout(() => tower.classList.remove('relaying'), 400);
|
||||
}
|
||||
lastRelayCount = n;
|
||||
}
|
||||
|
||||
function sendKey(inst, code) {
|
||||
inst.mod.ccall('sim_enqueue_key', null, ['number'], [code]);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
log('booting instance A (companion_radio)...');
|
||||
const modA = await MeshCoreSim({ simInstanceTag: 'A' });
|
||||
log('booting instance B (companion_radio)...');
|
||||
const modB = await MeshCoreSim({ simInstanceTag: 'B' });
|
||||
log('booting instance R (simple_repeater)...');
|
||||
const modR = await MeshCoreSimRepeater({ simInstanceTag: 'R' });
|
||||
|
||||
const A = wrapInstance(modA, 'A');
|
||||
const B = wrapInstance(modB, 'B');
|
||||
const R = wrapInstance(modR, 'R');
|
||||
|
||||
// Wait for each instance's real setup() to finish (see sim_is_ready()
|
||||
// in examples/companion_radio/main.cpp + examples/simple_repeater/main.cpp)
|
||||
// -- the MODULARIZE factory promise resolves once the wasm module is
|
||||
// instantiated, well before the async IDBFS-ready callback actually
|
||||
// invokes setup().
|
||||
async function waitReady(inst, statusElId) {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
if (inst.isReady()) {
|
||||
document.getElementById(statusElId).textContent = 'ready';
|
||||
return;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
}
|
||||
document.getElementById(statusElId).textContent = 'TIMED OUT waiting for boot';
|
||||
log(`WARNING: instance ${inst.label} never became ready`);
|
||||
}
|
||||
await Promise.all([
|
||||
waitReady(A, 'status-A'),
|
||||
waitReady(B, 'status-B'),
|
||||
waitReady(R, 'status-R'),
|
||||
]);
|
||||
log('all instances ready.');
|
||||
|
||||
// Topology: A <-> R <-> B. No direct A<->B link -- see file header.
|
||||
etherLinks = [
|
||||
{ from: A, to: [R] },
|
||||
{ from: B, to: [R] },
|
||||
{ from: R, to: [A, B] },
|
||||
];
|
||||
startEther();
|
||||
setInterval(() => pollRelay(R), 100);
|
||||
|
||||
document.querySelectorAll('button[data-instance]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const inst = btn.dataset.instance === 'A' ? A : B;
|
||||
sendKey(inst, Number(btn.dataset.key));
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('btn-advert-a').addEventListener('click', () => {
|
||||
const ok = A.mod.ccall('sim_test_advert_flood', 'number', [], []);
|
||||
log(`A: sent flood self-advert (ok=${ok})`);
|
||||
});
|
||||
document.getElementById('btn-advert-b').addEventListener('click', () => {
|
||||
const ok = B.mod.ccall('sim_test_advert_flood', 'number', [], []);
|
||||
log(`B: sent flood self-advert (ok=${ok})`);
|
||||
});
|
||||
document.getElementById('btn-dm-ab').addEventListener('click', () => {
|
||||
const text = 'Hello B, this is A! (' + new Date().toLocaleTimeString() + ')';
|
||||
const result = A.mod.ccall('sim_test_send_msg_to_first_contact', 'number', ['string'], [text]);
|
||||
log(`A -> B: sendMessage() result=${result} (1=flood,2=direct,0=failed,-1=no contact yet) text="${text}"`);
|
||||
});
|
||||
document.getElementById('btn-dm-ba').addEventListener('click', () => {
|
||||
const text = 'Hello A, this is B! (' + new Date().toLocaleTimeString() + ')';
|
||||
const result = B.mod.ccall('sim_test_send_msg_to_first_contact', 'number', ['string'], [text]);
|
||||
log(`B -> A: sendMessage() result=${result} (1=flood,2=direct,0=failed,-1=no contact yet) text="${text}"`);
|
||||
});
|
||||
document.getElementById('btn-open-dm-a').addEventListener('click', () => {
|
||||
const ok = A.mod.ccall('sim_test_open_dm_with_first_contact', 'number', [], []);
|
||||
log(`A: opened DM screen (ok=${ok})`);
|
||||
});
|
||||
document.getElementById('btn-open-dm-b').addEventListener('click', () => {
|
||||
const ok = B.mod.ccall('sim_test_open_dm_with_first_contact', 'number', [], []);
|
||||
log(`B: opened DM screen (ok=${ok})`);
|
||||
});
|
||||
|
||||
async function waitForContacts(inst, minCount, timeoutMs) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const n = inst.mod.ccall('sim_test_get_num_contacts', 'number', [], []);
|
||||
if (n >= minCount) return true;
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
document.getElementById('btn-run-demo').addEventListener('click', async () => {
|
||||
log('=== running full demo sequence ===');
|
||||
A.mod.ccall('sim_test_advert_flood', 'number', [], []);
|
||||
log('A: sent flood advert');
|
||||
B.mod.ccall('sim_test_advert_flood', 'number', [], []);
|
||||
log('B: sent flood advert');
|
||||
const gotA = await waitForContacts(A, 1, 5000);
|
||||
const gotB = await waitForContacts(B, 1, 5000);
|
||||
log(`A has contact: ${gotA}, B has contact: ${gotB}`);
|
||||
if (!gotA || !gotB) {
|
||||
log('demo aborted: advert propagation through repeater did not complete in time');
|
||||
return;
|
||||
}
|
||||
const text = 'Hello B, this is A! (' + new Date().toLocaleTimeString() + ')';
|
||||
const result = A.mod.ccall('sim_test_send_msg_to_first_contact', 'number', ['string'], [text]);
|
||||
log(`A -> B: sendMessage() result=${result}, text="${text}"`);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
const ok = B.mod.ccall('sim_test_open_dm_with_first_contact', 'number', [], []);
|
||||
log(`B: opened DM screen (ok=${ok}) -- check B's canvas for the received text`);
|
||||
log('=== demo sequence complete ===');
|
||||
});
|
||||
|
||||
window.__meshSim = { A, B, R }; // exposed for console poking / Playwright
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log('FATAL: ' + err);
|
||||
console.error(err);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user