mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
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:
@@ -0,0 +1,171 @@
|
||||
#pragma once
|
||||
|
||||
// Minimal native Arduino-core shim for the SIM_PLATFORM (variants/sim)
|
||||
// native build. Scoped empirically (see the Phase-1 status report) by
|
||||
// grepping examples/companion_radio + ui-new + the src/helpers files the
|
||||
// sim build actually compiles for real Arduino API usage -- the app logic
|
||||
// turned out to use almost none of it: no Arduino String anywhere, no
|
||||
// PROGMEM/pgm_read, no digitalWrite/pinMode/analogRead/Wire/SPI reachable
|
||||
// (all gated behind board-specific PIN_* defines this build never sets),
|
||||
// and Serial. only in main.cpp's Serial.begin() plus MyMesh.cpp's
|
||||
// CLI-rescue debug command handler.
|
||||
//
|
||||
// Deliberately NOT defining the ARDUINO preprocessor macro: a couple of
|
||||
// vendored third-party libs (CayenneLPP.cpp, ArduinoJson) branch on
|
||||
// `#ifdef ARDUINO` to choose between Arduino String/Stream and plain
|
||||
// std::string/std::ostream -- leaving ARDUINO undefined routes them onto
|
||||
// their portable std:: path, which is exactly what we want and needs no
|
||||
// Arduino String implementation at all.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
#include <type_traits>
|
||||
|
||||
#include "Stream.h"
|
||||
|
||||
using std::isnan;
|
||||
using std::isinf;
|
||||
|
||||
// --- timing -----------------------------------------------------------
|
||||
|
||||
inline uint32_t _sim_millis_start_epoch_ms() {
|
||||
static const uint32_t t0 = (uint32_t)std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
return t0;
|
||||
}
|
||||
|
||||
inline unsigned long millis() {
|
||||
uint32_t now = (uint32_t)std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
return (unsigned long)(now - _sim_millis_start_epoch_ms());
|
||||
}
|
||||
|
||||
inline unsigned long micros() {
|
||||
uint64_t now = (uint64_t)std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
return (unsigned long)now;
|
||||
}
|
||||
|
||||
// Real delay(); MyMesh.cpp:2848 and UITask.cpp:2619's one-shot pre-reboot/
|
||||
// pre-shutdown pauses get their own SIM_PLATFORM branch that skips calling
|
||||
// this entirely (see the Phase-1 status report), so an actual sleep here
|
||||
// never blocks the sim's stdin-reader thread for long.
|
||||
inline void delay(unsigned long ms) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
||||
}
|
||||
|
||||
// --- randomSeed()/random() (Arduino's global RNG, used by StdRNG in
|
||||
// src/helpers/ArduinoHelpers.h -- NOT the same thing as mesh::RNG) ----
|
||||
inline void randomSeed(unsigned long seed) { if (seed != 0) ::srand((unsigned)seed); }
|
||||
inline long random(long howbig) {
|
||||
if (howbig <= 0) return 0;
|
||||
return (long)(::rand() % howbig);
|
||||
}
|
||||
inline long random(long howsmall, long howbig) {
|
||||
if (howsmall >= howbig) return howsmall;
|
||||
return howsmall + (long)(::rand() % (howbig - howsmall));
|
||||
}
|
||||
|
||||
// --- misc Arduino macros/helpers ---------------------------------------
|
||||
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM
|
||||
#endif
|
||||
#ifndef F
|
||||
#define F(x) (x)
|
||||
#endif
|
||||
#ifndef PSTR
|
||||
#define PSTR(x) (x)
|
||||
#endif
|
||||
|
||||
// Real Arduino cores define min()/max()/constrain() as untyped macros, which
|
||||
// works but is notorious for silently corrupting any <algorithm>/<vector>/etc
|
||||
// internals that use those same names if such a header is ever included
|
||||
// afterwards (this codebase's CayenneLPPPolyline.h uses std::vector/std::map,
|
||||
// and SimFS.h/etc use std::string) -- so these are real (mixed-argument-type)
|
||||
// templates instead, deducing a common value type via std::common_type
|
||||
// rather than decltype(a<b?a:b) (which can deduce a *reference* type when
|
||||
// both arguments are same-typed lvalues, and then fail to bind that
|
||||
// reference to the temporary a cast/mixed-type branch produces).
|
||||
#ifndef min
|
||||
template <typename T, typename U>
|
||||
typename std::common_type<T, U>::type min(T a, U b) { return a < b ? a : b; }
|
||||
#endif
|
||||
#ifndef max
|
||||
template <typename T, typename U>
|
||||
typename std::common_type<T, U>::type max(T a, U b) { return a > b ? a : b; }
|
||||
#endif
|
||||
#ifndef constrain
|
||||
template <typename T, typename U, typename V>
|
||||
typename std::common_type<T, U, V>::type constrain(T amt, U lo, V hi) {
|
||||
return amt < lo ? lo : (amt > hi ? hi : amt);
|
||||
}
|
||||
#endif
|
||||
#ifndef abs
|
||||
#define abs(x) ((x) > 0 ? (x) : -(x))
|
||||
#endif
|
||||
|
||||
// Arduino global itoa/ltoa/utoa/ultoa (AVR-libc-style, not in standard C++);
|
||||
// src/helpers/TxtDataHelpers.cpp's float-formatting code calls ltoa()
|
||||
// directly. glibc/libc++ don't provide these, so implement via snprintf's
|
||||
// base-10/16/8/2 support for 10, else a manual digit loop for other bases.
|
||||
inline char* ltoa(long value, char* result, int base) {
|
||||
if (base == 10) { snprintf(result, 32, "%ld", value); return result; }
|
||||
if (value == 0) { result[0] = '0'; result[1] = 0; return result; }
|
||||
bool neg = value < 0;
|
||||
unsigned long v = neg ? (unsigned long)(-value) : (unsigned long)value;
|
||||
char tmp[34]; int i = 0;
|
||||
while (v > 0) { int d = v % base; tmp[i++] = d < 10 ? ('0' + d) : ('a' + d - 10); v /= base; }
|
||||
int j = 0;
|
||||
if (neg) result[j++] = '-';
|
||||
while (i > 0) result[j++] = tmp[--i];
|
||||
result[j] = 0;
|
||||
return result;
|
||||
}
|
||||
inline char* itoa(int value, char* result, int base) { return ltoa((long)value, result, base); }
|
||||
|
||||
// Cooperative-multitasking yield point (ESP32/RP2040 cores feed this to their
|
||||
// scheduler/watchdog between blocking waits; a plain native process has none
|
||||
// of that, so it's a no-op). src/helpers/StreamUtils.h calls it directly.
|
||||
inline void yield() { }
|
||||
|
||||
// Opaque incomplete type used only for PROGMEM-string pointer typing on real
|
||||
// Arduino cores (F("...") normally returns a `const __FlashStringHelper*`).
|
||||
// Since F(x) is defined as a plain passthrough above (no real PROGMEM on a
|
||||
// native build), nothing ever actually constructs one of these -- it only
|
||||
// needs to exist as a type so a `print(const __FlashStringHelper*)` overload
|
||||
// (e.g. examples/companion_radio/ui-new/TrailScreen.h's BoundedSerialPrint)
|
||||
// declares cleanly, exactly like on a real board.
|
||||
class __FlashStringHelper;
|
||||
|
||||
// --- Serial -------------------------------------------------------------
|
||||
// Only ever used for Serial.begin() (main.cpp, ignored) and MyMesh.cpp's
|
||||
// CLI-rescue debug command handler (Serial.print/println/printf as output,
|
||||
// Serial.available()/read() as input). Output goes to real stdout; input
|
||||
// always reports "nothing available" -- CLI-rescue isn't reachable through
|
||||
// the sim's stdin-driven UI input path (see variants/sim/README) and isn't
|
||||
// one of the Phase-1 exit criteria, but every call still needs to compile.
|
||||
class SimSerialClass : public Stream {
|
||||
public:
|
||||
void begin(unsigned long baud) { }
|
||||
// Real HardwareSerial's operator bool() reports "is a USB/BLE host
|
||||
// actually connected" (some real boards genuinely gate on this, e.g.
|
||||
// TrailScreen.h's GPX-export-over-serial feature). stdout is always
|
||||
// "there" for a native process, so just always report ready.
|
||||
explicit operator bool() const { return true; }
|
||||
int available() override { return 0; }
|
||||
int read() override { return -1; }
|
||||
int peek() override { return -1; }
|
||||
size_t write(uint8_t b) override { putchar(b); return 1; }
|
||||
size_t write(const uint8_t *buffer, size_t size) override {
|
||||
fwrite(buffer, 1, size, stdout);
|
||||
return size;
|
||||
}
|
||||
void flush() override { fflush(stdout); }
|
||||
};
|
||||
|
||||
extern SimSerialClass Serial;
|
||||
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
// Minimal native stand-in for Arduino's Print class -- just enough of the
|
||||
// real API surface for MeshCore's app logic (which never uses Arduino
|
||||
// String) to link: byte/buffer write(), the numeric print() overloads, and
|
||||
// printf() (a genuine Arduino Print extension on ESP32/etc, used sparingly
|
||||
// by MyMesh.cpp's CLI-rescue debug path).
|
||||
//
|
||||
// Modelled on test/mocks/Stream.h's Print (same shape, same DEC/HEX/OCT/BIN
|
||||
// constants) but adds printf() since the real app needs it.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
#include <cstring>
|
||||
|
||||
#define DEC 10
|
||||
#define HEX 16
|
||||
#define OCT 8
|
||||
#define BIN 2
|
||||
|
||||
class Print {
|
||||
public:
|
||||
virtual ~Print() = default;
|
||||
|
||||
virtual size_t write(uint8_t b) { return 1; }
|
||||
|
||||
size_t write(const char *str) {
|
||||
if (str == NULL) return 0;
|
||||
return write((const uint8_t *)str, strlen(str));
|
||||
}
|
||||
|
||||
virtual size_t write(const uint8_t *buffer, size_t size) {
|
||||
size_t t = 0;
|
||||
for (size_t i = 0; i < size; i++) t += write(buffer[i]);
|
||||
return t;
|
||||
}
|
||||
|
||||
size_t write(const char *buffer, size_t size) {
|
||||
return write((const uint8_t *)buffer, size);
|
||||
}
|
||||
|
||||
virtual size_t print(unsigned char b, int base = DEC) { return printInt((unsigned long)b, base); }
|
||||
virtual size_t print(int v, int base = DEC) { return base == DEC ? printSigned((long)v) : printInt((unsigned long)v, base); }
|
||||
virtual size_t print(unsigned int v, int base = DEC) { return printInt((unsigned long)v, base); }
|
||||
virtual size_t print(long v, int base = DEC) { return base == DEC ? printSigned(v) : printInt((unsigned long)v, base); }
|
||||
virtual size_t print(unsigned long v, int base = DEC) { return printInt(v, base); }
|
||||
virtual size_t print(long long v, int base = DEC) { char buf[32]; snprintf(buf, sizeof(buf), "%lld", v); return write(buf); }
|
||||
virtual size_t print(unsigned long long v, int base = DEC) { char buf[32]; snprintf(buf, sizeof(buf), "%llu", v); return write(buf); }
|
||||
virtual size_t print(double v, int digits = 2) { char buf[64]; snprintf(buf, sizeof(buf), "%.*f", digits, v); return write(buf); }
|
||||
|
||||
size_t print(char c) { return write((uint8_t)c); }
|
||||
size_t print(const char* s) { return write(s); }
|
||||
|
||||
size_t println() { return write("\r\n"); }
|
||||
size_t println(const char* s) { size_t n = print(s); n += println(); return n; }
|
||||
size_t println(char c) { size_t n = print(c); n += println(); return n; }
|
||||
size_t println(int v, int base = DEC) { size_t n = print(v, base); n += println(); return n; }
|
||||
size_t println(unsigned int v, int base = DEC) { size_t n = print(v, base); n += println(); return n; }
|
||||
size_t println(long v, int base = DEC) { size_t n = print(v, base); n += println(); return n; }
|
||||
size_t println(unsigned long v, int base = DEC){ size_t n = print(v, base); n += println(); return n; }
|
||||
size_t println(double v, int digits = 2) { size_t n = print(v, digits); n += println(); return n; }
|
||||
|
||||
// Real Arduino cores (ESP32/NRF52) expose Print::printf(); used by
|
||||
// MyMesh.cpp's CLI-rescue debug command handler.
|
||||
size_t printf(const char* fmt, ...) {
|
||||
char buf[256];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
int n = vsnprintf(buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
if (n < 0) return 0;
|
||||
if ((size_t)n < sizeof(buf)) return write(buf);
|
||||
// truncated -- still write what fit
|
||||
return write(buf);
|
||||
}
|
||||
|
||||
virtual void flush() { }
|
||||
|
||||
private:
|
||||
size_t printInt(unsigned long v, int base) {
|
||||
char buf[34];
|
||||
if (base == DEC) { snprintf(buf, sizeof(buf), "%lu", v); }
|
||||
else if (base == HEX) { snprintf(buf, sizeof(buf), "%lx", v); }
|
||||
else if (base == OCT) { snprintf(buf, sizeof(buf), "%lo", v); }
|
||||
else {
|
||||
// generic base conversion (BIN etc.)
|
||||
char tmp[34]; int i = 0;
|
||||
unsigned long n = v;
|
||||
if (n == 0) tmp[i++] = '0';
|
||||
while (n > 0) { tmp[i++] = "0123456789abcdefghijklmnopqrstuvwxyz"[n % base]; n /= base; }
|
||||
int j = 0;
|
||||
while (i > 0) buf[j++] = tmp[--i];
|
||||
buf[j] = 0;
|
||||
}
|
||||
return write(buf);
|
||||
}
|
||||
size_t printSigned(long v) {
|
||||
char buf[34];
|
||||
snprintf(buf, sizeof(buf), "%ld", v);
|
||||
return write(buf);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
// Minimal native stand-in for Adafruit's RTClib.h. Real hardware boards pull
|
||||
// this in transitively for helpers/AutoDiscoverRTCClock.h (not used by the
|
||||
// sim build -- SimRTCClock.h reads the host wall clock directly) and for
|
||||
// DateTime, which src/helpers/CommonCLI.cpp's "clock"/"clock sync"/"time"
|
||||
// CLI commands genuinely construct and call hour()/minute()/day()/month()/
|
||||
// year() on. Nothing in companion_radio (root files or ui-new/) uses
|
||||
// DateTime, but CommonCLI.cpp (built for every board, including the sim) does.
|
||||
//
|
||||
// Implemented via the real C library's gmtime_r() (UTC calendar breakdown)
|
||||
// rather than hand-rolling RTClib's own unixtime<->y/m/d/h/m/s conversion --
|
||||
// same result, zero risk of a transcription bug in that math.
|
||||
#include <ctime>
|
||||
#include <cstdint>
|
||||
|
||||
class DateTime {
|
||||
uint32_t _unixtime;
|
||||
public:
|
||||
DateTime(uint32_t t = 0) : _unixtime(t) { }
|
||||
|
||||
uint16_t year() const { return 1900 + tmFields().tm_year; }
|
||||
uint8_t month() const { return (uint8_t)(tmFields().tm_mon + 1); }
|
||||
uint8_t day() const { return (uint8_t)tmFields().tm_mday; }
|
||||
uint8_t hour() const { return (uint8_t)tmFields().tm_hour; }
|
||||
uint8_t minute() const { return (uint8_t)tmFields().tm_min; }
|
||||
uint8_t second() const { return (uint8_t)tmFields().tm_sec; }
|
||||
uint32_t unixtime() const { return _unixtime; }
|
||||
|
||||
private:
|
||||
struct tm tmFields() const {
|
||||
time_t t = (time_t)_unixtime;
|
||||
struct tm out;
|
||||
gmtime_r(&t, &out);
|
||||
return out;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
// Minimal native stand-in for Arduino's Stream class. src/Utils.h includes
|
||||
// <Stream.h> directly (mesh::Utils::printHex(Stream&, ...)), and
|
||||
// src/Identity.cpp's readFrom/writeTo/printTo take a Stream& -- both real
|
||||
// hardware File objects (fs::File, Adafruit_LittleFS's File) and our own
|
||||
// SimFile (variants/sim/SimFS.h) derive from this, exactly like on real
|
||||
// boards.
|
||||
|
||||
#include "Print.h"
|
||||
|
||||
class Stream : public Print {
|
||||
public:
|
||||
virtual ~Stream() = default;
|
||||
|
||||
virtual int available() { return 0; }
|
||||
virtual int availableForWrite() { return 0; }
|
||||
virtual int read() { return -1; }
|
||||
virtual int peek() { return -1; }
|
||||
|
||||
virtual size_t readBytes(char *buffer, size_t length) {
|
||||
size_t i = 0;
|
||||
while (i < length) {
|
||||
int c = read();
|
||||
if (c < 0) break;
|
||||
buffer[i++] = (char)c;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
size_t readBytes(uint8_t *buffer, size_t length) {
|
||||
return readBytes((char *)buffer, length);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
// Minimal native stand-in for densaugeo/base64's base64.hpp. Real board envs
|
||||
// pull in the whole library via lib_deps; src/helpers/BaseChatMesh.cpp
|
||||
// (built for every board, including the sim, whenever MAX_GROUP_CHANNELS is
|
||||
// defined) only ever calls decode_base64() -- to decode a channel's
|
||||
// user-supplied PSK -- so that's the only function reproduced here, as a
|
||||
// plain standard base64 decoder (skips '=' padding/whitespace/invalid chars
|
||||
// rather than erroring, same permissive behaviour the real library has).
|
||||
|
||||
static inline int _sim_b64_val(unsigned char c) {
|
||||
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||||
if (c == '+') return 62;
|
||||
if (c == '/') return 63;
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline unsigned int decode_base64(const unsigned char input[], unsigned int inputLength, unsigned char output[]) {
|
||||
unsigned int out_len = 0;
|
||||
int val = 0, bits = -8;
|
||||
for (unsigned int i = 0; i < inputLength; i++) {
|
||||
unsigned char c = input[i];
|
||||
if (c == '=') break;
|
||||
int v = _sim_b64_val(c);
|
||||
if (v < 0) continue; // skip whitespace/invalid characters
|
||||
val = (val << 6) + v;
|
||||
bits += 6;
|
||||
if (bits >= 0) {
|
||||
output[out_len++] = (unsigned char)((val >> bits) & 0xFF);
|
||||
bits -= 8;
|
||||
}
|
||||
}
|
||||
return out_len;
|
||||
}
|
||||
Reference in New Issue
Block a user