feat(sim): add variants/sim/ — real companion_radio firmware on native + Emscripten

New board variant compiling the unmodified MyMesh/UITask/DataStore app
logic against real mesh::Radio/MainBoard/RTCClock/RNG interfaces, for
running the actual firmware outside embedded hardware:

- Native (plain g++, platform = native): ASCII-art display over stdout,
  stdin-driven input, local-disk-backed DataStore/IdentityStore.
- Emscripten/WASM (variants/sim/build_wasm.sh, since PlatformIO's native
  platform force-overrides any CC/CXX toolchain override back to system
  clang++): canvas-backed display, IDBFS-backed persistence across page
  reloads, JS-callable input via sim_enqueue_key(), emscripten_set_main_loop.

Real rweather/Crypto (AES128/SHA256/Ed25519) vendored unmodified and
proven working on both targets. variants/sim/web/index.html is a bare
verification harness, not the polished website embed.
This commit is contained in:
Jakub
2026-09-03 00:46:47 +02:00
parent bbf107d62c
commit 8f4c92a217
197 changed files with 23070 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
# Phase 2 (Emscripten) build script for the companion_radio sim.
#
# Why a shell script instead of a PlatformIO env: PlatformIO's `platform =
# native` env (see variants/sim/platformio.ini's [env:sim_companion_radio])
# was tried first, by pointing a `pre:` extra_script at this same source
# list and overriding env['CC']/env['CXX'] to em++/emcc via env.Replace().
# That override *did* take effect (confirmed: the extra_script's own print()
# showed the correct em++ path) but was silently discarded before any file
# was actually compiled -- PlatformIO's native platform package
# (~/.platformio/platforms/native/builder/main.py) unconditionally calls
# env.Tool("gcc") / env.Tool("g++") to (re-)detect the toolchain, and that
# happens to run *after* extra_scripts regardless of pre:/post: ordering,
# re-overwriting CC/CXX back to the real system clang++ every time. Every
# object file in that experiment was still compiled by Xcode's clang++, not
# em++ -- a genuine, reproducible wall (not a config typo), and PlatformIO's
# native platform has no supported hook to stop it from re-detecting the
# toolchain like that. Rather than fight PlatformIO's SCons integration
# further, this script just invokes em++ directly -- it mirrors
# platformio.ini's build_flags/build_src_filter/-I list by hand (see the
# SRCS/INCLUDES/DEFINES arrays below), so if that .ini file's source list
# ever changes, this script's arrays need the same edit alongside it.
#
# Usage:
# variants/sim/build_wasm.sh # release-ish build (-O2)
# variants/sim/build_wasm.sh debug # -O0 -g, easier to debug in devtools
#
# Requires emsdk 6.0.9 (pinned; see variants/sim/tools/emsdk/ -- installed by
# this same task, see the Phase 2 report for the exact activation command).
# This script finds em++ itself via a fixed relative path, so `source
# emsdk_env.sh` first is NOT required to run it (but IS required for
# interactive use of emcc/em++/emrun directly on the command line).
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"
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 build_src_filter, just spelled
# as real paths from the repo root instead of PlatformIO's src-relative
# "+<../x>" syntax. Keep in sync with that file by hand.
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/companion_radio/main.cpp
examples/companion_radio/MyMesh.cpp
examples/companion_radio/DataStore.cpp
examples/companion_radio/ui-new/UITask.cpp
)
INCLUDES=(
-Ivariants/sim/arduino
-Ivariants/sim
-Ivariants/sim/thirdparty/crypto
-Ivariants/sim/thirdparty/cayennelpp
-Ivariants/sim/thirdparty/arduinojson
-Ilib/ed25519
-Isrc
-Iexamples/companion_radio
-Iexamples/companion_radio/ui-new
)
DEFINES=(
-DSIM_PLATFORM
-DMESH_DEBUG=0
# The one difference from platformio.ini's native env: the canvas-backed
# DisplayDriver (variants/sim/SimDisplayDriver.h's __EMSCRIPTEN__-guarded
# SimDisplayDriverCanvas class) instead of the ASCII/stdout one.
-DDISPLAY_CLASS=SimDisplayDriverCanvas
-DMAX_CONTACTS=100
-DMAX_GROUP_CHANNELS=8
)
# -funsigned-char: carried over from Phase 1 verbatim -- real ARM cores
# default `char` to unsigned; em++'s target (wasm32) defaults it to signed,
# same mismatch Phase 1 hit on a native x86/ARM64 host, for the same reason
# (KEY_* codes up to 0xF3 compared as plain `char` throughout UIScreen.h/
# KeyboardWidget.h/PopupMenu.h) -- without it keyboard input compiles but
# silently never matches.
COMMON_FLAGS=(-std=c++17 -funsigned-char "${OPT_FLAGS[@]}" "${DEFINES[@]}" "${INCLUDES[@]}")
# Compile each source to its own object file, one em++ invocation per file,
# with the object path mirroring the source's own directory (obj/<same
# relative path>.o) rather than every object landing in one flat directory.
# This isn't just tidiness: a first attempt passed every source straight to
# a single em++ invocation and let it manage its own (flat) temp object
# directory internally, which broke on this specific source tree --
# lib/ed25519/sha512.c (ed25519's own plain-C SHA512, unrelated to the
# rweather/Crypto library) and variants/sim/thirdparty/crypto/SHA512.cpp
# (rweather's C++ SHA512 class) both produce a "sha512.o"/"SHA512.o" object,
# which collided as the SAME file on macOS's case-insensitive-by-default
# APFS -- wasm-ld then reported duplicate symbols for the *second* file's
# whole contents, because it was quite literally linking the first file's
# object twice under two different names. PlatformIO's native build never
# hits this because SCons mirrors each source's own directory under
# .pio/build/<env>/ -- that's exactly what this does too, by hand.
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
# FS is exported so a host page (or a manual verification script) can
# directly inspect what DataStore/IdentityStore actually wrote -- e.g.
# 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.
"$EMXX" \
"${OBJS[@]}" \
-lidbfs.js \
-sALLOW_MEMORY_GROWTH=1 \
-sFORCE_FILESYSTEM=1 \
-sMODULARIZE=1 \
-sEXPORT_NAME=MeshCoreSim \
-sENVIRONMENT=web \
-sEXIT_RUNTIME=0 \
-sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap \
-o "$OUT_DIR/meshcore_sim.js"
echo ""
echo "Built: $OUT_DIR/meshcore_sim.js (+ .wasm alongside it)"
echo "Serve variants/sim/web/ locally and open index.html, e.g.:"
echo " cd $SCRIPT_DIR/web && python3 -m http.server 8080"