* Add ARCH_PORTDUINO_WASM build: meshtasticd in WebAssembly over WebUSB
Compile the full portduino firmware to WebAssembly (Emscripten) so a real node runs in a browser tab or headless Node, driving a LoRa radio over WebUSB through a CH341 — the desktop Ch341Hal path with its libusb backend swapped for a WebUSB one. The native/desktop portduino build is unchanged.
New build env under src/platform/portduino/wasm/ (excluded from the native build_src_filter): WebUSB libpinedio backend, config/FS/region/MAC/PhoneAPI glue, wasm setup/loop, JS WebUSB runtime, and build stubs. bin/build-portduino-wasm.sh runs a standalone cached emcc build to build/wasm/meshnode.{mjs,wasm}.
Six firmware sources gain #ifdef ARCH_PORTDUINO_WASM guards (single-threaded cooperative emscripten_sleep loop, continuous RX, US region default, std RNG, no-popen exec); none affect non-wasm builds.
* PortDuino WASM: CI build job, cross-platform libdeps, configurable adapter
bin/build-portduino-wasm.sh: auto-detect native-macos (macOS) vs native (Linux/CI) libdeps with a NATIVE_ENV override, and use the SAME env's Crypto with an XEdDSA-present guard. Drops the heltec-v3/Crypto borrow — the meshtastic/Crypto pin already ships XEdDSA; the native libdeps cache was just stale. No env pin change.
Add .github/workflows/build_portduino_wasm.yml: build the ARCH_PORTDUINO_WASM target in CI (ubuntu + emsdk, native libdeps) so it can't silently bit-rot; asserts build/wasm/meshnode.{mjs,wasm}.
src/platform/portduino/wasm: add wasm_set_lora_* setters so the JS host can configure any CH341 LoRa adapter (module, USB ids, DIO/TCXO, SPI speed, pins); wasm_config_apply falls back to the MeshToad defaults when unset.
* WASM: build as a first-class [env:wasm] via platform-wasm (retire emcc script)
Replace the standalone emcc build (bin/build-portduino-wasm.sh) with a normal
PlatformIO env, `pio run -e wasm`, using the new meshtastic/platform-wasm
platform (emcc/em++ + Asyncify, the WASM sibling of platform-native). The
portduino WebAssembly node is now built the same way as every other target.
- variants/native/portduino/platformio.ini: add [env:wasm] (platform pinned to
platform-wasm, board wasm). Translates the script's source set into a curated
build_src_filter, the ~30 EXCLUDE_* defines, and the lib set; defines
ARCH_PORTDUINO_WASM in-repo so correctness doesn't hinge on the platform's
board.json.
- extra_scripts/wasm_link_flags.py: the firmware-specific emcc *link* settings
(EXPORT_NAME, EXPORTED_RUNTIME_METHODS, EXPORTED_FUNCTIONS, ASYNCIFY_IMPORTS).
PlatformIO feeds build_flags to compile only, so these must ride LINKFLAGS;
without it the WebUSB Asyncify seam and the JS host's runtime methods are
dropped (the _wasm_* exports survive only via EMSCRIPTEN_KEEPALIVE).
- PortduinoGlue.{h,cpp}: guard the yaml-cpp dependency out of the WASM build
(#ifndef ARCH_PORTDUINO_WASM around the include, emit_yaml/loadConfig/
readGPIOFromYaml). The browser node configures via the wasm_set_lora_* setters
and dead-strips the YAML path; this drops the host yaml-cpp build dependency
entirely. Native is unchanged (guards are inert there).
- portduino_glue_wasm.cpp / portduino_main_wasm.cpp: repair EM_ASM JS that a
formatter had mangled (!== -> != =, regex split) in the prior landing; the
emcc link succeeds regardless, so CI now runs `node --check meshnode.mjs`.
- .github/workflows/build_portduino_wasm.yml: build via `pio run -e wasm`
(artifacts under .pio/build/wasm/), trigger on the shared inputs the env
inherits (root platformio.ini, bin/platformio-*.py).
- NodeDB.cpp: drop the dead ARCH_PORTDUINO_WASM region-default branch (region
now defaults the same as native).
- Crypto renovate pins: add the missing gitBranch so they track upstream.
Output: .pio/build/wasm/meshnode.{mjs,wasm} (ES module, factory createMeshNode).
Verified: pio run -e wasm (against the published platform archive), node --check,
module instantiates in Node with all exports; native-macos + Docker native unit
tests (450/450) still pass.
* Fix name on the Piggystick
* wasm: pin platform-wasm at the GPL-3.0-relicensed commit
platform-wasm's LICENSE was always GPLv3 (matching this firmware), but its
platform.json/README still declared Apache-2.0 (mis-copied from platform-native).
That's fixed upstream in b83fa5b; bump the [env:wasm] pin to it. Build output is
unchanged (license metadata only). Verified: pio run -e wasm against b83fa5b.
* wasm: make reboot() actually restart the node (was a no-op)
In wasm the reboot path is live (main.cpp -> Power::powerCommandsCheck ->
Power::reboot), but Power::reboot's ARCH_PORTDUINO arm tore down SPI/Wire/Serial
and then called the no-op ::reboot() stub — leaving the node running with a dead
radio until the tab was manually reloaded. Triggers include an admin/phone
reboot, factory reset, the "reconfigure failed" path, and the 60 s stuck-TX
hardware watchdog (RadioLibInterface).
- Power::reboot(): add an ARCH_PORTDUINO_WASM arm (before ARCH_PORTDUINO, since
the wasm build defines both) that skips the host teardown and just calls
::reboot(). notifyReboot already let modules persist.
- ::reboot() (glue): hand off to the JS host — browser reloads the tab (NodeDB
state survives via IDBFS, same identity returns); headless calls Module.onReboot
if provided, else logs. Loose !=/== so clang-format doesn't mangle the EM_ASM JS.
- README: document the reboot handoff + the Module.onReboot hook.
Verified: pio run -e wasm + node --check (EM_ASM intact); native-macos unaffected.
* wasm: rename env to native-wasm and run it in the main CI matrix
Rename [env:wasm] -> [env:native-wasm] for consistency with the portduino
native family (native, native-macos, native-tft). The build dir follows to
.pio/build/native-wasm/ (artifact is still meshnode.{mjs,wasm}); the PIOENV
guard in extra_scripts/wasm_link_flags.py, the README, and the companion wrapper
move with it. The board stays `wasm`.
Also wire the build into normal CI: build_portduino_wasm.yml becomes a reusable
workflow (workflow_call) invoked as the `build-wasm` job of main_matrix.yml, so
the WebAssembly node is built like every other platform instead of on a separate
path trigger.
* native-wasm: auto-locate the Emscripten SDK (pre-build script)
`pio run -e native-wasm` failed with "emcc not found" whenever it was invoked
from a shell that hadn't sourced emsdk_env.sh — a VS Code task, an IDE build
button, a bare terminal. Add a pre: extra script that probes the usual emsdk
locations ($EMSDK_ENV, $EMSDK, ~/emsdk, ./.emsdk, the sibling companion
checkout), sources emsdk_env.sh, and imports the resulting environment so the
platform builder and emcc see PATH/EMSDK/EM_CONFIG. No-op when emcc is already
reachable (CI), silent when no SDK is found (the platform emits its own error).
* wasm: address PR review feedback
- js/bridge.js: import CH341 from "./ch341.js" (sibling in this layout), not
"../src/ch341.js" which doesn't resolve here.
- js/ch341.js: a zero-length transferIn while MISO bytes are still outstanding
now throws instead of breaking out with a partially-filled buffer — silent SPI
corruption becomes a loud error, matching the comment above it.
- libpinedio_webusb.c: webusb_set_auto_cs honors the AUTO_CS option (? 1 : 0)
instead of the always-on ? 1 : 1. Runtime behavior is unchanged — Ch341Hal sets
AUTO_CS=0 right after pinedio_init (RadioLib drives the active-low NSS); the
option just isn't set yet at init, so this now correctly defaults off.
- SX126xInterface.cpp: the RX-start error log now names the method actually
called (startReceive vs startReceiveDutyCycleAuto) instead of hardcoding the
duty-cycle name in the WASM branch.
* native-wasm: drop the emsdk bootstrap shim (now in platform-wasm)
The Emscripten SDK auto-location moved into the platform-wasm builder, so the
firmware no longer needs its own pre: extra script. Remove
extra_scripts/wasm_emsdk_env.py and bump the platform pin to the build that
carries the bootstrap. The wasm_link_flags.py post script stays — those exported
fns / runtime methods / Asyncify import seam are firmware-app-specific.
* wasm: use the canonical companion name (meshtasticd-wasm-node)
The companion repo was renamed meshtastic-web-node -> meshtasticd-wasm-node; fix
the stale name in the wasm README and bump the platform pin to the build that
promotes the canonical name in its emsdk auto-location.
* wasm: re-entrancy guard for the API/region entry points + flaky-open retry
Two robustness fixes for the browser node:
- Re-entrancy guard. The node is single-threaded + Asyncify: while setup()/loop()
is suspended inside a WebUSB transfer, the JS event loop is free, so a stray
DOM/timer callback that re-enters a wasm_* entry point starts a second Asyncify
unwind ("async operation already in flight" abort) or clobbers shared PhoneAPI
state (observed as a "PhoneAPI::available unexpected state" flood). Add a
g_wasm_in_firmware flag set around setup()/loop() (portduino_main_wasm.cpp); the
wasm_set_region / wasm_api_to_radio / wasm_api_from_radio / wasm_api_available
entry points now reject a mid-tick call (return busy) instead of corrupting or
aborting. The host must still call them between ticks — this is the safety net
the design lacked, not a substitute for the JS queue.
- CH341 open retry (js/bridge.js). First-connect WebUSB is flaky — the interface
is briefly unclaimable right after the grant, or held by a prior session,
giving a transient "Could not open SPI: -1". Retry the open with a short
backoff, closing the device between attempts so claimInterface starts clean.
* wasm: exclude emscripten-only sources from cppcheck
The `check` board matrix runs `pio check` (cppcheck) over all of src/,
including src/platform/portduino/wasm/. cppcheck can't parse the EM_ASYNC_JS/
EM_JS macros (Syntax Error: AST broken at libpinedio_webusb.c:39,
internalAstError) and these sources are not part of any checked board build
([env:native-wasm] is board_level=extra, compiled by the build-wasm CI job).
Suppress the wasm dir in suppressions.txt, the same way generated/ and .pio/
are already excluded.
* wasm: coalesce FS.syncfs so two never run at once
IDBFS syncfs is async; the explicit wasm_fs_sync (5s timer + post-save +
beforeunload) could overlap a prior in-flight sync, warning "2 FS.syncfs
operations in flight at once". Serialize: if a sync is running, mark a pending
re-sync and let the in-flight one chain it on completion — at most one in flight,
trailing writes still flushed. (Companion drops IDBFS autoPersist so this is the
single persistence path.)
* wasm: silence false-positive SAST on the emscripten glue
- extra_scripts/wasm_link_flags.py: restore the trunk-ignore-all(ruff/F821,
flake8/F821) header every other SCons extra_script carries; Import/env are
SConscript-injected globals, so ruff/flake8 flag them as undefined.
- .semgrepignore: exclude src/platform/portduino/wasm/js/ (browser WebUSB glue,
not part of the firmware binary). The unsafe-formatstring rule false-positives
on its benign retry/diagnostic console logs.
* Update .github/workflows/build_portduino_wasm.yml
Co-authored-by: Austin <vidplace7@gmail.com>
---------
Co-authored-by: Austin <vidplace7@gmail.com>
177 lines
5.0 KiB
C++
177 lines
5.0 KiB
C++
#include "HardwareRNG.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <random>
|
|
|
|
#include "configuration.h"
|
|
|
|
#if HAS_RADIO
|
|
#include "RadioLibInterface.h"
|
|
#endif
|
|
|
|
#if defined(ARCH_NRF52)
|
|
#include <Adafruit_nRFCrypto.h>
|
|
extern Adafruit_nRFCrypto nRFCrypto;
|
|
#elif defined(ARCH_ESP32)
|
|
#include <esp_system.h>
|
|
#elif defined(ARCH_RP2040)
|
|
#include <Arduino.h>
|
|
#elif defined(ARCH_PORTDUINO)
|
|
#include <random>
|
|
#include <unistd.h>
|
|
#ifdef __linux__
|
|
#include <sys/random.h> // getrandom()
|
|
#else
|
|
#include <stdlib.h> // arc4random_buf() on Darwin/BSD
|
|
#endif
|
|
#endif
|
|
|
|
namespace HardwareRNG
|
|
{
|
|
|
|
namespace
|
|
{
|
|
void fillWithRandomDevice(uint8_t *buffer, size_t length)
|
|
{
|
|
std::random_device rd;
|
|
size_t offset = 0;
|
|
while (offset < length) {
|
|
uint32_t value = rd();
|
|
size_t toCopy = std::min(length - offset, sizeof(value));
|
|
memcpy(buffer + offset, &value, toCopy);
|
|
offset += toCopy;
|
|
}
|
|
}
|
|
|
|
#if HAS_RADIO
|
|
bool mixWithLoRaEntropy(uint8_t *buffer, size_t length)
|
|
{
|
|
// Only attempt to pull entropy from the modem if it is initialized and exposes the helper.
|
|
// When the radio stack is disabled or has not yet been configured, we simply skip this step
|
|
// and return false so callers know no extra mixing occurred.
|
|
RadioLibInterface *radio = RadioLibInterface::instance;
|
|
if (!radio) {
|
|
// This path can run during portduinoSetup() before the console is initialized,
|
|
// both for unit-test binaries and the simulator's meshtasticd; LOG_* dereferences `console`.
|
|
if (console) {
|
|
LOG_ERROR("No radio instance available to provide entropy");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
constexpr size_t chunkSize = 16;
|
|
uint8_t scratch[chunkSize];
|
|
size_t offset = 0;
|
|
bool mixed = false;
|
|
|
|
while (offset < length) {
|
|
size_t toCopy = std::min(length - offset, chunkSize);
|
|
|
|
// randomBytes() returns false if the modem does not support it or is not ready
|
|
// (for instance, when the radio is powered down). We break immediately to avoid
|
|
// blocking or returning partially-filled entropy and simply report failure.
|
|
if (!radio->randomBytes(scratch, toCopy)) {
|
|
break;
|
|
}
|
|
|
|
for (size_t i = 0; i < toCopy; ++i) {
|
|
buffer[offset + i] ^= scratch[i];
|
|
}
|
|
|
|
mixed = true;
|
|
offset += toCopy;
|
|
}
|
|
|
|
// Avoid leaving the modem-sourced bytes sitting on the stack longer than needed.
|
|
if (mixed) {
|
|
memset(scratch, 0, sizeof(scratch));
|
|
}
|
|
|
|
return mixed;
|
|
}
|
|
#endif
|
|
} // namespace
|
|
|
|
bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
|
|
{
|
|
if (!buffer || length == 0) {
|
|
return false;
|
|
}
|
|
|
|
bool filled = false;
|
|
|
|
#if defined(ARCH_NRF52)
|
|
// The Nordic SDK RNG provides cryptographic-quality randomness backed by hardware.
|
|
nRFCrypto.begin();
|
|
auto result = nRFCrypto.Random.generate(buffer, length);
|
|
nRFCrypto.end();
|
|
filled = result;
|
|
#elif defined(ARCH_ESP32)
|
|
// ESP32 exposes a true RNG via esp_fill_random().
|
|
esp_fill_random(buffer, length);
|
|
filled = true;
|
|
#elif defined(ARCH_RP2040)
|
|
// RP2040 has a hardware random number generator accessible through the Arduino core.
|
|
size_t offset = 0;
|
|
while (offset < length) {
|
|
uint32_t value = rp2040.hwrand32();
|
|
size_t toCopy = std::min(length - offset, sizeof(value));
|
|
memcpy(buffer + offset, &value, toCopy);
|
|
offset += toCopy;
|
|
}
|
|
filled = true;
|
|
#elif defined(ARCH_PORTDUINO)
|
|
// Prefer the host OS RNG first when running under Portduino.
|
|
#ifdef __linux__
|
|
ssize_t generated = ::getrandom(buffer, length, 0);
|
|
if (generated == static_cast<ssize_t>(length)) {
|
|
filled = true;
|
|
}
|
|
#elif defined(__EMSCRIPTEN__)
|
|
// Browser/wasm: no getrandom/arc4random — fall through to std::random_device,
|
|
// which emscripten backs with crypto.getRandomValues().
|
|
#else
|
|
// arc4random_buf is available on Darwin/BSD and cannot fail.
|
|
::arc4random_buf(buffer, length);
|
|
filled = true;
|
|
#endif
|
|
|
|
if (!filled) {
|
|
fillWithRandomDevice(buffer, length);
|
|
filled = true;
|
|
}
|
|
#endif
|
|
|
|
if (!filled) {
|
|
// As a last resort, fall back to std::random_device. This should only be reached
|
|
// if a platform-specific source was unavailable.
|
|
fillWithRandomDevice(buffer, length);
|
|
filled = true;
|
|
}
|
|
|
|
#if HAS_RADIO
|
|
if (useRadioEntropy) {
|
|
// Best-effort: if the radio is active and can provide modem entropy, XOR it over the
|
|
// buffer to improve overall quality. We consider the filling a success if either a
|
|
// good platform RNG or the modem RNG provided data, so we return true as long as at
|
|
// least one of those steps succeeded.
|
|
filled = mixWithLoRaEntropy(buffer, length) || filled;
|
|
}
|
|
#endif
|
|
|
|
return filled;
|
|
}
|
|
|
|
bool seed(uint32_t &seedOut)
|
|
{
|
|
uint32_t candidate = 0;
|
|
if (!fill(reinterpret_cast<uint8_t *>(&candidate), sizeof(candidate), true)) {
|
|
return false;
|
|
}
|
|
seedOut = candidate;
|
|
return true;
|
|
}
|
|
|
|
} // namespace HardwareRNG
|