diff --git a/.gitignore b/.gitignore index 2ef3dcea..2168bd08 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ platformio.local.ini CODE_REVIEW.md tools/pngs/ tools/gpx/ +sim_data/ +variants/sim/tools/emsdk/ +variants/sim/web/build/ # graphify graphify-out/ diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 92b88cb3..fa3c65ad 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -201,6 +201,8 @@ bool DataStore::formatFileSystem() { bool fs_success = ((fs::SPIFFSFS *)_fs)->format(); esp_err_t nvs_err = nvs_flash_erase(); // no need to reinit, will be done by reboot return fs_success && (nvs_err == ESP_OK); +#elif defined(SIM_PLATFORM) + return _fs->format(); #else #error "need to implement format()" #endif diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2c06b86f..23d96a19 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2845,7 +2845,12 @@ void MyMesh::handleCmdFrame(size_t len) { bool success = _store->formatFileSystem(); if (success) { writeOKFrame(); +#ifdef SIM_PLATFORM + // Skip the pre-reboot UX pause -- board.reboot() just exits the + // process in the sim (see SimMainBoard::reboot()). +#else delay(1000); +#endif board.reboot(); // doesn't return } else { writeErrFrame(ERR_CODE_FILE_IO_ERROR); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 8b48366d..008cb8e2 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -33,6 +33,13 @@ static uint32_t _atoi(const char* sp) { #elif defined(ESP32) #include DataStore store(SPIFFS, rtc_clock); +#elif defined(SIM_PLATFORM) + #include + // Real files under ./sim_data/ (relative to the process's cwd) so + // NodePrefs/contacts/identity genuinely round-trip across process + // restarts -- see SimFS.h. + SimFS sim_fs("./sim_data"); + DataStore store(sim_fs, rtc_clock); #endif #ifdef ESP32 @@ -88,6 +95,11 @@ static uint32_t _atoi(const char* sp) { #elif defined(STM32_PLATFORM) #include ArduinoSerialInterface serial_interface; +#elif defined(SIM_PLATFORM) + // No real BLE/USB companion-app transport in Phase 1 -- always reports + // "not connected". See variants/sim/SimSerialInterface.h. + #include + SimSerialInterface serial_interface; #else #error "need to define a serial interface" #endif @@ -239,6 +251,18 @@ void setup() { serial_interface.begin(Serial); #endif the_mesh.startInterface(serial_interface); +#elif defined(SIM_PLATFORM) + // sim_fs already exists/mkdir'd itself in its constructor above. + store.begin(); + the_mesh.begin( + #ifdef DISPLAY_CLASS + disp != NULL + #else + false + #endif + ); + serial_interface.begin(); + the_mesh.startInterface(serial_interface); #else #error "need to define filesystem" #endif diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index dc54fe13..c21ca03b 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -9,6 +9,22 @@ #ifdef WIFI_SSID #include #endif +#ifdef SIM_PLATFORM + #include + #include + #ifdef __EMSCRIPTEN__ + #include + // The single UITask instance is a file-scope global in + // examples/companion_radio/main.cpp (`UITask ui_task(...)`, only under + // `#ifdef DISPLAY_CLASS`, which the sim build always defines) -- not + // reachable from here by name, so UITask::begin() stashes `this` here + // (see below) the same way every other single-instance sim glue point + // does. Declared up here (rather than next to its use near enqueueKey(), + // further down this file) since UITask::begin() -- also further down, + // but earlier in the file -- needs it too. + static UITask* g_sim_ui_task_for_js = nullptr; + #endif +#endif #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds @@ -1361,6 +1377,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no uint32_t aoff = autoOffMillis(); _auto_off = millis() + (aoff > 0 ? aoff : AUTO_OFF_MILLIS); +#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__) + g_sim_ui_task_for_js = this; // see sim_enqueue_key() below +#endif + #if defined(CARDKB_I2C) // On the ENV_PIN_SDA/SCL path, CARDKB_I2C is Wire1, already brought up by // sensors.begin() (EnvironmentSensorManager), which runs before this. On @@ -2099,6 +2119,23 @@ void UITask::enqueueKey(char c) { _kq_head = next; } +#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__) +void UITask::injectSimKey(char c) { + enqueueKey(checkDisplayOn(c)); +} + +// Called directly from a host HTML page's JS (button onclick / keydown +// listener) -- e.g. `Module._sim_enqueue_key(keyCode)` -- to drive the real +// on-device menu. `c` is one of the KEY_* codes in src/helpers/ui/ +// UIScreen.h (KEY_UP/DOWN/LEFT/RIGHT/ENTER/CANCEL/NEXT/PREV/SELECT), the +// exact same values the native build's stdin-poll branch above already +// enqueues -- so the host page owns key-mapping (arrow keys, on-screen +// D-pad buttons, whatever), not this function. +extern "C" EMSCRIPTEN_KEEPALIVE void sim_enqueue_key(char c) { + if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->injectSimKey(c); +} +#endif + bool UITask::dequeueKey(char& c) { if (_kq_tail == _kq_head) return false; c = _key_queue[_kq_tail]; @@ -2364,6 +2401,66 @@ void UITask::loop() { } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { if (!_locked) enqueueKey(handleTripleClick(KEY_SELECT)); } +#elif defined(SIM_PLATFORM) + // Native terminal input: stdin is put into raw/non-canonical mode by + // variants/sim/sim_main.cpp's main(), so keys arrive here one at a time + // with no Enter-to-submit line buffering. Non-blocking select() on fd 0 + // (VMIN=0/VTIME=0 on the fd itself would also work, but select() keeps + // the intent -- "is there a key waiting?" -- explicit) takes the place of + // every concrete MomentaryButton/GPIO poll above. Every real board maps + // its own physical buttons down to the same enqueueKey() choke point; + // this is the sim's one input source instead. + // Arrow keys -> KEY_UP/DOWN/LEFT/RIGHT + // Enter/Space -> KEY_ENTER + // Esc/Backspace-> KEY_CANCEL + // w/a/s/d -> up/left/down/right (arrow keys need a real terminal; + // WASD works even through a dumb pipe/redirected stdin) + // n / p -> KEY_NEXT / KEY_PREV + { + fd_set fds; + FD_ZERO(&fds); + FD_SET(0, &fds); + struct timeval tv = {0, 0}; + if (select(1, &fds, NULL, NULL, &tv) > 0) { + uint8_t buf[16]; + int n = (int)read(0, buf, sizeof(buf)); + int i = 0; + while (i < n) { + uint8_t c = buf[i++]; + char key = 0; + if (c == 0x1b && i + 1 < n && buf[i] == '[') { + uint8_t code = buf[i + 1]; + i += 2; + switch (code) { + case 'A': key = KEY_UP; break; + case 'B': key = KEY_DOWN; break; + case 'C': key = KEY_RIGHT; break; + case 'D': key = KEY_LEFT; break; + default: key = 0; break; + } + } else if (c == 0x1b) { + key = KEY_CANCEL; + } else if (c == '\r' || c == '\n' || c == ' ') { + key = KEY_ENTER; + } else if (c == 127 || c == 8) { + key = KEY_CANCEL; + } else if (c == 'w' || c == 'W') { + key = KEY_UP; + } else if (c == 's' || c == 'S') { + key = KEY_DOWN; + } else if (c == 'a' || c == 'A') { + key = KEY_LEFT; + } else if (c == 'd' || c == 'D') { + key = KEY_RIGHT; + } else if (c == 'n') { + key = KEY_NEXT; + } else if (c == 'p') { + key = KEY_PREV; + } + if (key) enqueueKey(checkDisplayOn(key)); + } + } + } #endif #if defined(PIN_USER_BTN_ANA) if (millis() - _analogue_pin_read_millis > 10) { @@ -2616,7 +2713,11 @@ void UITask::loop() { _display->drawTextCentered(_display->width() / 2, mid - step, "Low Battery"); _display->drawTextCentered(_display->width() / 2, mid, "Shutting down"); _display->endFrame(); +#ifdef SIM_PLATFORM + // Skip the pre-shutdown UX pause in the sim. +#else if (_display->isEink() == false) { delay(2000); } +#endif } shutdown(); } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 26ff28c0..e16c57d0 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -208,6 +208,20 @@ class UITask : public AbstractUITask { void enqueueKey(char c); bool dequeueKey(char& c); +#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__) +public: + // JS-callable input entry point for the Phase 2 (Emscripten) sim build -- + // see the sim_enqueue_key() EMSCRIPTEN_KEEPALIVE wrapper defined at the + // bottom of UITask.cpp, which is what a host HTML page's buttons/keyboard + // listener actually calls. Routes through checkDisplayOn() (wake-on-any-key, + // same as every other input source below) then the same enqueueKey() + // choke point every real board's button poll already uses -- this is the + // sim's substitute for a real board's GPIO/joystick poll, not a new input + // path of its own. + void injectSimKey(char c); +private: +#endif + // Optional M5Stack CardKB (I2C keyboard, addr 0x5F). See the CARDKB_I2C // definition near the top of this file for which bus it's on and why. #if defined(CARDKB_I2C) diff --git a/src/helpers/IdentityStore.h b/src/helpers/IdentityStore.h index d0d7ee45..c5578947 100644 --- a/src/helpers/IdentityStore.h +++ b/src/helpers/IdentityStore.h @@ -8,6 +8,11 @@ #define FILESYSTEM Adafruit_LittleFS using namespace Adafruit_LittleFS_Namespace; +#elif defined(SIM_PLATFORM) + #include + #define FILESYSTEM SimFS + // SimFS.h already typedefs File itself (matches the ESP32/NRF52 pattern of + // a bare, unqualified `File` symbol coming from the platform's own core). #endif #include diff --git a/variants/sim/SimDisplayDriver.h b/variants/sim/SimDisplayDriver.h new file mode 100644 index 00000000..41083ee1 --- /dev/null +++ b/variants/sim/SimDisplayDriver.h @@ -0,0 +1,305 @@ +#pragma once + +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +// DisplayDriver implementation for the native sim build. Shaped like +// src/helpers/ui/NullDisplayDriver.h (same pure-virtual overrides -- start +// from that file, per the Phase-1 plan) but instead of no-ops, maintains an +// in-memory framebuffer and prints it to stdout as ASCII/block-art on +// endFrame(), so the real UITask/menu system's actual draw calls are +// visible in a terminal. +// +// Logical canvas is 128x64 (matches NullDisplayDriver / a typical SSD1306 +// OLED, so layout math in the real screens behaves exactly as on that +// hardware). For terminal rendering it's downsampled onto a coarser +// CELL_W x CELL_H-pixel grid: +// - fillRect()/drawRect()/drawXbm() mark the cells they cover as "filled" +// (drawXbm -- icons -- has no real bitmap to rasterize in ASCII, so it's +// approximated as a solid block, same as a fillRect over that area). +// - print() does NOT rasterize a bitmap font -- it places the real +// characters of the real string into the grid at the (approximate) +// cursor cell, which is what actually makes the output legible. Real +// text always wins over a "filled" block in the same cell. +class SimDisplayDriver : public DisplayDriver { + static const int CELL_W = 2; // pixels per terminal column + static const int CELL_H = 2; // pixels per terminal row + static const int COLS = 128 / CELL_W; // 64 + static const int ROWS = 64 / CELL_H; // 32 + + bool _on = false; + int _cursor_x = 0, _cursor_y = 0; + Color _color = LIGHT; + bool _filled[ROWS][COLS]; + char _text[ROWS][COLS]; // 0 = no character placed + bool _dirty = false; + int _frame_no = 0; + + void cellOf(int px, int py, int& cx, int& cy) const { + cx = px / CELL_W; cy = py / CELL_H; + } + +public: + SimDisplayDriver() : DisplayDriver(128, 64) { clearBuffers(); } + + bool begin() { _on = true; return true; } + + void clearBuffers() { + memset(_filled, 0, sizeof(_filled)); + memset(_text, 0, sizeof(_text)); + } + + bool isOn() override { return _on; } + void turnOn() override { _on = true; } + void turnOff() override { _on = false; } + void clear() override { clearBuffers(); } + + void startFrame(Color bkg = DARK) override { + clearBuffers(); + _color = LIGHT; + } + + void setTextSize(int sz) override { /* one fixed size in ASCII output */ } + + void setColor(Color c) override { _color = c; } + + void setCursor(int x, int y) override { _cursor_x = x; _cursor_y = y; } + + void print(const char* str) override { + if (!str) return; + int cx, cy; + cellOf(_cursor_x, _cursor_y, cx, cy); + int col = cx; + for (const char* p = str; *p; p++) { + if (*p == '\n') { cy++; col = cx; continue; } + if (col >= 0 && col < COLS && cy >= 0 && cy < ROWS) { + _text[cy][col] = (*p >= 32 && *p < 127) ? *p : '?'; + } + col++; + } + // advance cursor horizontally by the printed width, like a real display + _cursor_x += getTextWidth(str); + } + + void fillRect(int x, int y, int w, int h) override { markRect(x, y, w, h); } + void drawRect(int x, int y, int w, int h) override { + markRect(x, y, w, 1); + markRect(x, y + h - 1, w, 1); + markRect(x, y, 1, h); + markRect(x + w - 1, y, 1, h); + } + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { + markRect(x, y, w, h); // icon placeholder: solid block (see class comment) + } + + uint16_t getTextWidth(const char* str) override { + return str ? (uint16_t)(strlen(str) * getCharWidth()) : 0; + } + + void endFrame() { + printf("\n===== SimDisplayDriver frame #%d =====\n", _frame_no++); + printf("+"); + for (int c = 0; c < COLS; c++) printf("-"); + printf("+\n"); + for (int r = 0; r < ROWS; r++) { + printf("|"); + for (int c = 0; c < COLS; c++) { + char ch = _text[r][c]; + if (ch) putchar(ch); + else if (_filled[r][c]) putchar('#'); + else putchar(' '); + } + printf("|\n"); + } + printf("+"); + for (int c = 0; c < COLS; c++) printf("-"); + printf("+\n"); + fflush(stdout); + } + +private: + void markRect(int x, int y, int w, int h) { + bool lit = (_color != DARK); + int cx0, cy0, cx1, cy1; + cellOf(x, y, cx0, cy0); + cellOf(x + w - 1, y + h - 1, cx1, cy1); + for (int r = cy0; r <= cy1; r++) { + if (r < 0 || r >= ROWS) continue; + for (int c = cx0; c <= cx1; c++) { + if (c < 0 || c >= COLS) continue; + _filled[r][c] = lit; + } + } + } +}; + +#ifdef __EMSCRIPTEN__ +// --------------------------------------------------------------------- +// Phase 2 (Emscripten): DisplayDriver backend that draws to a real +// HTML5 instead of dumping ASCII art to stdout. SimDisplayDriver +// above is left completely untouched -- the native build still links that +// class (see variants/sim/platformio.ini's DISPLAY_CLASS=SimDisplayDriver); +// this class is only selected when DISPLAY_CLASS=SimDisplayDriverCanvas is +// set by the Emscripten build (variants/sim/build_wasm.sh). +// +// Design choice: draw straight through the browser's canvas 2D API on every +// draw call (fillRect/strokeRect/fillText), rather than building an offscreen +// RGBA framebuffer in linear memory and blitting it with putImageData(). The +// canvas 2D approach was simpler and more robust for this app's actual draw +// call shape: +// - print() needs real text rendering (variable glyphs, marquee/ellipsis +// logic in DisplayDriver.h measures via getTextWidth()) -- letting the +// browser's own font rasterizer draw it is both less code and crisper +// than hand-rolling a bitmap font + blit. +// - Every draw happens synchronously within one call to startFrame().. +// endFrame() inside a single JS "tick" (called from the Emscripten main +// loop -- see sim_main.cpp) -- the browser never paints a partial canvas +// mid-tick, so there's no tearing/flicker risk from not double-buffering +/// in C++ first. +// - putImageData() would still need *something* to rasterize text and +// icons into an RGBA buffer first -- it doesn't remove that work, it +/// only relocates it into C++ for no real benefit here. +// +// 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(). +class SimDisplayDriverCanvas : public DisplayDriver { + bool _on = false; + int _cursor_x = 0, _cursor_y = 0; + Color _color = LIGHT; + +public: + SimDisplayDriverCanvas() : DisplayDriver(128, 64) { } + + 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; + }); + return true; + } + + bool isOn() override { return _on; } + void turnOn() override { _on = true; } + void turnOff() override { + _on = false; + EM_ASM({ + if (!window.__simCtx) return; + window.__simCtx.fillStyle = '#000'; + window.__simCtx.fillRect(0, 0, 128, 64); + }); + } + void clear() override { turnOff(); _on = true; } + + 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); + }); + } + + void setTextSize(int sz) override { /* one fixed size, like the native ASCII backend */ } + void setColor(Color c) override { _color = c; } + void setCursor(int x, int y) override { _cursor_x = x; _cursor_y = y; } + + // Amber-on-black palette (a common OLED look) for LIGHT/DARK; the other + // Color enumerators (RED/GREEN/BLUE/YELLOW/ORANGE) aren't used on the real + // monochrome OLED boards this sim mirrors either (DisplayDriver.h's own + // comment: "on b/w screen, colors will be !=0 synonym of light"). + static const char* jsColor(Color c) { return c == DARK ? "#000" : "#ffb000"; } + + void print(const char* str) override { + if (!str) return; + EM_ASM({ + if (!window.__simCtx) return; + var ctx = window.__simCtx; + ctx.fillStyle = UTF8ToString($3) === 'L' ? '#ffb000' : '#000'; + ctx.font = '8px monospace'; + ctx.textBaseline = 'top'; + // Advance width is fixed (getCharWidth()==6, DisplayDriver.h default) -- + // draw one character per cell so glyph spacing matches the layout math + // every screen already does off getTextWidth()'s strlen()*6 estimate, + // instead of leaving it to the font's own (proportional) metrics. + // NB: EM_ASM's argument-splitting only understands parens, not + // braces -- an unparenthesized top-level comma (e.g. a multi-name + // `var a, b;`) gets misread as separating this macro's own C++ + // arguments and breaks the whole block. Every declaration below is + // therefore its own separate `var` statement. + var s = UTF8ToString($0); + var x = $1; + var y = $2; + for (var i = 0; i < s.length; i++) { + ctx.fillText(s[i], x + i * 6, y); + } + }, str, _cursor_x, _cursor_y, (_color != DARK) ? "L" : "D"); + _cursor_x += getTextWidth(str); + } + + 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); + }, 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; + ctx.strokeStyle = UTF8ToString($4) === 'L' ? '#ffb000' : '#000'; + ctx.lineWidth = 1; + ctx.strokeRect($0 + 0.5, $1 + 0.5, $2 - 1, $3 - 1); + }, x, y, w, h, (_color != DARK) ? "L" : "D"); + } + + // Real XBM bit-unpacking (row-major, MSB-first, rows padded to whole + // bytes) -- same convention every real DisplayDriver's drawXbm() already + // assumes (see e.g. src/helpers/ui/ST7789Display.cpp's own drawXbm(), + // `0x80 >> (bx & 7)` against `widthInBytes = (w+7)/8`). A canvas can + // afford to rasterize the real icon pixels cheaply, unlike the native + // ASCII backend's solid-block placeholder (no ASCII resolution for that). + // `bits` is a pointer into wasm linear memory; EM_ASM passes it through as + // 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; + var x0 = $0; + var y0 = $1; + var w = $2; + var h = $3; + var bits = $4; + var lit = UTF8ToString($5) === 'L'; + var widthInBytes = (w + 7) >> 3; + ctx.fillStyle = lit ? '#ffb000' : '#000'; + for (var ry = 0; ry < h; ry++) { + for (var rx = 0; rx < w; rx++) { + var byteOff = bits + ry * widthInBytes + (rx >> 3); + var mask = 0x80 >> (rx & 7); + if (HEAPU8[byteOff] & mask) ctx.fillRect(x0 + rx, y0 + ry, 1, 1); + } + } + }, x, y, w, h, bits, (_color != DARK) ? "L" : "D"); + } + + uint16_t getTextWidth(const char* str) override { + return str ? (uint16_t)(strlen(str) * getCharWidth()) : 0; + } + + // Every draw call above already lands directly on the visible canvas + // (see the class comment) -- nothing left to flush. + void endFrame() override { } +}; +#endif // __EMSCRIPTEN__ diff --git a/variants/sim/SimFS.h b/variants/sim/SimFS.h new file mode 100644 index 00000000..54d33bd2 --- /dev/null +++ b/variants/sim/SimFS.h @@ -0,0 +1,298 @@ +#pragma once + +// Native-disk-backed FILESYSTEM/File shim for the sim build's DataStore +// persistence (examples/companion_radio/DataStore.cpp / .h, +// src/helpers/IdentityStore.h/.cpp). Modelled on the ESP32 fs::FS/fs::File +// shape (DataStore.cpp's own "#else" fallback branch already calls +// fs->open(path, "r"/"w", create_bool) -- matching that signature here +// means SIM_PLATFORM falls straight into those existing branches with no +// DataStore.cpp/IdentityStore.cpp edits needed beyond the FILESYSTEM/File +// typedefs in IdentityStore.h), but backed by plain fopen/fread/fwrite +// under a local directory (./sim_data/ by default) so NodePrefs/contacts/ +// identity genuinely round-trip across process restarts. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class SimFile : public Stream { + friend class SimFS; + FILE* _fp = nullptr; + DIR* _dir = nullptr; + bool _is_dir = false; + std::string _dirpath; // full on-disk path, directory mode only + std::string _name; // basename + size_t _size = 0; + + SimFile(FILE* fp, const std::string& name, size_t size) : _fp(fp), _name(name), _size(size) { } + SimFile(DIR* dir, const std::string& dirpath, const std::string& name) + : _dir(dir), _is_dir(true), _dirpath(dirpath), _name(name) { } + +public: + SimFile() { } + + operator bool() const { return _fp != nullptr || _dir != nullptr; } + + bool isDirectory() { return _is_dir; } + const char* name() { return _name.c_str(); } + size_t size() { return _size; } + + // directory iteration (MyMesh.cpp's CLI-rescue "ls" debug command) + SimFile openNextFile() { + if (!_dir) return SimFile(); + struct dirent* ent; + while ((ent = readdir(_dir)) != nullptr) { + std::string nm = ent->d_name; + if (nm == "." || nm == "..") continue; + std::string full = _dirpath + "/" + nm; + struct stat st; + if (stat(full.c_str(), &st) != 0) continue; + SimFile f; + f._name = nm; + f._is_dir = S_ISDIR(st.st_mode); + f._size = (size_t)st.st_size; + return f; + } + return SimFile(); + } + + size_t read(uint8_t* buf, size_t len) { + if (!_fp) return 0; + return fread(buf, 1, len, _fp); + } + int read() override { + if (!_fp) return -1; + int c = fgetc(_fp); + return c; + } + int peek() override { + if (!_fp) return -1; + int c = fgetc(_fp); + if (c != EOF) ungetc(c, _fp); + return c; + } + int available() override { + if (!_fp) return 0; + long cur = ftell(_fp); + if (cur < 0) return 0; + return (int)((long)_size - cur); + } + + size_t write(uint8_t b) override { return _fp ? fwrite(&b, 1, 1, _fp) : 0; } + size_t write(const uint8_t* buf, size_t len) override { + return _fp ? fwrite(buf, 1, len, _fp) : 0; + } + + bool seek(uint32_t pos) { + if (!_fp) return false; + return fseek(_fp, (long)pos, SEEK_SET) == 0; + } + + void close() { + if (_fp) { fclose(_fp); _fp = nullptr; } + if (_dir) { closedir(_dir); _dir = nullptr; } + } +}; + +typedef SimFile File; + +class SimFS { + std::string _root; + + // Creates every path component of `path`, whether it's absolute ("/a/b") + // or -- as SimFS's own root ("./sim_data") and every path built from it + // always are -- relative ("./a/b", "a/b"). A relative path must accumulate + // starting from "." (not ""), otherwise the first component gets + // mkdir()'d as if it were rooted at the real filesystem's "/" (a real bug + // this had: "./sim_data" was being split into a leading "." component and + // then prefixed with "/", trying -- and silently failing, permission + // denied -- to mkdir "/./sim_data" instead of "./sim_data"). + static void mkdirsRecursive(const std::string& path) { + if (path.empty() || path == "/" || path == ".") return; + bool absolute = (path[0] == '/'); + std::string cur = absolute ? "" : "."; + size_t pos = absolute ? 1 : 0; + while (pos <= path.size()) { + size_t slash = path.find('/', pos); + std::string part = (slash == std::string::npos) ? path.substr(pos) : path.substr(pos, slash - pos); + if (!part.empty() && part != ".") { + cur += "/" + part; + ::mkdir(cur.c_str(), 0755); + } + if (slash == std::string::npos) break; + pos = slash + 1; + } + } + + std::string fullPath(const char* path) const { + if (path && path[0] == '/') return _root + path; + return _root + "/" + (path ? path : ""); + } + + static std::string basenameOf(const char* path) { + if (!path) return ""; + const char* slash = strrchr(path, '/'); + return slash ? std::string(slash + 1) : std::string(path); + } + + void ensureParentDir(const std::string& fullFilePath) const { + size_t slash = fullFilePath.find_last_of('/'); + if (slash == std::string::npos) return; + std::string dir = fullFilePath.substr(0, slash); + mkdirsRecursive(dir); + } + + static bool rmrf(const std::string& path) { + DIR* d = opendir(path.c_str()); + if (!d) { return ::remove(path.c_str()) == 0; } + struct dirent* ent; + bool ok = true; + while ((ent = readdir(d)) != nullptr) { + std::string nm = ent->d_name; + if (nm == "." || nm == "..") continue; + ok = rmrf(path + "/" + nm) && ok; + } + closedir(d); + ok = (::rmdir(path.c_str()) == 0) && ok; + return ok; + } + +public: + explicit SimFS(const std::string& root) : _root(root) { mkdirsRecursive(root); } + + bool mkdir(const char* path) { + mkdirsRecursive(fullPath(path)); + return true; + } + + bool exists(const char* path) { + struct stat st; + return stat(fullPath(path).c_str(), &st) == 0; + } + + bool remove(const char* path) { + return ::remove(fullPath(path).c_str()) == 0; + } + + bool rename(const char* oldPath, const char* newPath) { + return ::rename(fullPath(oldPath).c_str(), fullPath(newPath).c_str()) == 0; + } + + bool format() { + // Wipe the sim device's whole data directory and recreate it empty -- + // mirrors a real LittleFS/SPIFFS format(). + rmrf(_root); + mkdirsRecursive(_root); + return true; + } + + // Matches the ESP32 fs::FS::open(path, mode, create) shape that + // DataStore.cpp/IdentityStore.cpp's platform-agnostic "#else" fallback + // branches already call. + SimFile open(const char* path, const char* mode = "r", bool create = false) { + std::string full = fullPath(path); + struct stat st; + bool file_exists = (stat(full.c_str(), &st) == 0); + + if (file_exists && S_ISDIR(st.st_mode)) { + DIR* d = opendir(full.c_str()); + if (!d) return SimFile(); + return SimFile(d, full, basenameOf(path)); + } + + bool want_write = (mode && mode[0] == 'w'); + if (want_write) { + ensureParentDir(full); + FILE* fp = fopen(full.c_str(), "wb"); + if (!fp) return SimFile(); + return SimFile(fp, basenameOf(path), 0); + } + + if (!file_exists) return SimFile(); + FILE* fp = fopen(full.c_str(), "rb"); + if (!fp) return SimFile(); + return SimFile(fp, basenameOf(path), (size_t)st.st_size); + } +}; + +#ifdef __EMSCRIPTEN__ +#include + +// Phase 2 (Emscripten) IDBFS backend. +// +// Important finding from actually reading SimFile/SimFS above before writing +// this: NEITHER class needs a single line changed for Emscripten. Every op +// here (fopen/fread/fwrite/fclose, mkdir, opendir/readdir, stat, rename, +// remove) is a plain libc call, and Emscripten's C runtime already +// implements all of those against its own virtual filesystem (MEMFS by +// default) -- that's the whole point of Emscripten's libc port, and it's +// why this shim was written directly against fopen()-shaped calls in Phase +// 1 rather than some native-only API. So SimFS's constructor (mkdirsRecursive +// on "./sim_data") and every DataStore/IdentityStore read/write already work +// unmodified under Emscripten, exactly as they do natively -- just against +// MEMFS (in-memory, gone on refresh) instead of the real disk. +// +// The only thing actually missing for the browser is durability: MEMFS +// alone doesn't survive a page reload. IDBFS is Emscripten's IndexedDB- +// backed filesystem type that mirrors a MEMFS directory to/from IndexedDB. +// Mounting it is the one piece of real Emscripten-specific code needed, so +// it lives here (colocated with the FS shim it backs) rather than in +// sim_main.cpp: +// +// 1. FS.mount(IDBFS, {autoPersist: true}, root) -- autoPersist is a +// built-in IDBFS option (see this SDK's upstream/emscripten/src/lib/ +// libidbfs.js) that hooks every file close() following a write and +// queues a debounced FS.syncfs(false, cb) push to IndexedDB +// automatically (batched to one push per JS event-loop tick, so a +// DataStore save that opens/writes/closes several files in a row still +// costs one IndexedDB commit, not several). This was chosen over a +// hand-rolled periodic timer in the main loop: it can't drift out of +// sync with what was actually written (a timer firing between saves +// could persist a half-written state; a close()-triggered persist +// never can), and it does nothing at all when nothing changed instead +// of a timer's fixed idle-polling cost. +// 2. FS.syncfs(true, cb) -- the reverse direction, a one-time pull *from* +// IndexedDB into the MEMFS mirror. This must complete, and its callback +// must fire, before the app ever reads a file -- i.e. before setup() +// runs -- since IndexedDB has no synchronous API. sim_main.cpp's +/// Emscripten main() calls this and only calls setup() from the +// callback (sim_idbfs_ready(), EMSCRIPTEN_KEEPALIVE'd below so the +// generated JS glue can call it back by name). +// +// root must be an absolute path ("/sim_data") that already exists as a +// plain MEMFS directory by the time this runs -- true here because SimFS's +// own constructor (a file-scope global in examples/companion_radio/main.cpp, +// so it runs during static init, before this function's caller in +// sim_main.cpp's main()) already mkdir'd "./sim_data", which resolves to the +// same node as "/sim_data" since Emscripten's cwd defaults to "/". Mounting +// IDBFS onto an existing *empty* directory is the exact pattern Emscripten's +// own test suite uses (test/fs/test_idbfs_sync.c: `FS.mkdir(...); +// FS.mount(IDBFS, ..., ...)`) -- BUT unlike the real ::mkdir() syscall +// SimFS's own C++ constructor already called (which returns EEXIST quietly +// and is ignored, POSIX mkdir -p style), JS's own `FS.mkdir()` throws a hard +// exception if the directory is already there, which it always will be by +// this point. Confirmed by actually running this in a real browser before +// trusting the comment that used to be here: first load produced an +// uncaught `ErrnoError: File exists` right here, which silently aborted +// 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). +inline void sim_fs_mount_idbfs(const char* root) { + EM_ASM({ + var root = UTF8ToString($0); + 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...'); + _sim_idbfs_ready(); + }); + }, root); +} +#endif // __EMSCRIPTEN__ diff --git a/variants/sim/SimLocationProvider.h b/variants/sim/SimLocationProvider.h new file mode 100644 index 00000000..56879fea --- /dev/null +++ b/variants/sim/SimLocationProvider.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +// 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. +class SimLocationProvider : public LocationProvider { +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 { } + void begin() override { } + void stop() override { } + void loop() override { } + bool isEnabled() override { return false; } +}; diff --git a/variants/sim/SimMainBoard.h b/variants/sim/SimMainBoard.h new file mode 100644 index 00000000..86398ea4 --- /dev/null +++ b/variants/sim/SimMainBoard.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +// mesh::MainBoard implementation for the native sim build -- no real +// hardware, so battery/manufacturer/reboot are all just plausible fakes. +class SimMainBoard : public mesh::MainBoard { +public: + void begin() { } + + uint16_t getBattMilliVolts() override { return 4000; } // pretend full battery + const char* getManufacturerName() const override { return "MeshCore Sim (native)"; } + + void reboot() override { + printf("\n[sim] reboot() requested -- exiting process (rerun the binary to simulate a reboot)\n"); + fflush(stdout); + exit(0); + } + + uint8_t getStartupReason() const override { return BD_STARTUP_NORMAL; } + + void onBootComplete() override { } + void sleep(uint32_t secs) override { } // no-op: native process never actually sleeps the CPU +}; diff --git a/variants/sim/SimRNG.h b/variants/sim/SimRNG.h new file mode 100644 index 00000000..88cd4228 --- /dev/null +++ b/variants/sim/SimRNG.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +// 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. +class SimRNG : public mesh::RNG { +public: + SimRNG() { } + void begin() { + unsigned seed = (unsigned)time(NULL) ^ (unsigned)(uintptr_t)this; + srand(seed); + } + void random(uint8_t* dest, size_t sz) override { + for (size_t i = 0; i < sz; i++) dest[i] = (uint8_t)rand(); + } +}; diff --git a/variants/sim/SimRTCClock.h b/variants/sim/SimRTCClock.h new file mode 100644 index 00000000..c0d7efc2 --- /dev/null +++ b/variants/sim/SimRTCClock.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +// mesh::RTCClock backed by the real host wall clock (time(nullptr)), with +// setCurrentTime() applying an offset so DataStore::restoreRTCTime() +// (persisted last-known time) and the CLI's `time` command still work. +class SimRTCClock : public mesh::RTCClock { + long _offset = 0; // added to real wall-clock time() +public: + uint32_t getCurrentTime() override { + return (uint32_t)((long)time(NULL) + _offset); + } + void setCurrentTime(uint32_t t) override { + _offset = (long)t - (long)time(NULL); + } +}; diff --git a/variants/sim/SimRadio.h b/variants/sim/SimRadio.h new file mode 100644 index 00000000..3a4b8899 --- /dev/null +++ b/variants/sim/SimRadio.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include + +// mesh::Radio implementation for the native sim build (Phase 1). 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. +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; + +public: + void begin() override { } + + int recvRaw(uint8_t* bytes, int sz) override { + return 0; // never any incoming data yet (Phase 3: real ether) + } + + uint32_t getEstAirtimeFor(int len_bytes) override { + // Rough LoRa-ish estimate so anything that logs/uses airtime for + // scheduling doesn't see nonsense; not calibrated to any real profile. + return (uint32_t)(len_bytes * 3 + 50); + } + + float packetScore(float snr, int packet_len) override { + return 100.0f; // pretend every packet we'd send is a clean, high-quality one + } + + bool startSendRaw(const uint8_t* bytes, int len) override { + n_sent++; + return true; // instantly "succeeds" -- nothing is actually transmitted yet + } + + bool isSendComplete() override { return true; } + void onSendFinished() override { } + + bool isInRecvMode() const override { return true; } + + // --- 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 + // same way they'd call them on a RadioLibWrapper subclass (see + // src/helpers/radiolib/RadioLibWrappers.h, which every one of these + // mirrors). No real chip underneath, so these just report plausible + // static/no-op values. + + uint32_t getRngSeed() { + return (uint32_t)time(NULL) ^ (uint32_t)(uintptr_t)this ^ (uint32_t)rand(); + } + + void getFreqBounds(float& min_mhz, float& max_mhz) const { + min_mhz = 150.0f; + max_mhz = 2500.0f; + } + + void setParams(float freq, float bw, uint8_t sf, uint8_t cr) { } + void powerOff() { } + + void setPowerSaving(bool en) { _power_save = en; } + bool getPowerSaving() const { return _power_save; } + + void setTxPower(int8_t dbm) { _tx_dbm = dbm; } + int8_t getTxPower() const { return _tx_dbm; } + + bool setRxBoostedGainMode(bool en) { _rx_boosted_gain = en; return true; } + bool getRxBoostedGainMode() const { return _rx_boosted_gain; } + + uint32_t getPacketsRecv() const { return n_recv; } + uint32_t getPacketsRecvErrors() const { return n_recv_errors; } + uint32_t getPacketsSent() const { return n_sent; } + uint32_t getRxPsWatchdogSoftCount() const { return 0; } + uint32_t getRxPsWatchdogHardCount() const { return 0; } + void resetStats() { n_recv = n_sent = n_recv_errors = 0; } + + static float snrFloorForSF(uint8_t sf) { + if (sf < 7) sf = 7; else if (sf > 12) sf = 12; + return -7.5f - 2.5f * (float)(sf - 7); + } +}; diff --git a/variants/sim/SimSerialInterface.h b/variants/sim/SimSerialInterface.h new file mode 100644 index 00000000..6bb563c7 --- /dev/null +++ b/variants/sim/SimSerialInterface.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +// BaseSerialInterface stub for the native sim build: no companion app can +// connect over BLE/USB in Phase 1 (there's no real transport), so this +// always reports "not connected, nothing to do". Enough to let MyMesh's +// startInterface()/loop() run unmodified. +class SimSerialInterface : public BaseSerialInterface { +public: + void begin() { } + + void enable() override { } + void disable() override { } + bool isEnabled() const override { return false; } + bool isConnected() const override { return false; } + bool isWriteBusy() const override { return false; } + size_t writeFrame(const uint8_t src[], size_t len) override { return 0; } + size_t checkRecvFrame(uint8_t dest[]) override { return 0; } +}; diff --git a/variants/sim/arduino/Arduino.h b/variants/sim/arduino/Arduino.h new file mode 100644 index 00000000..350bf42a --- /dev/null +++ b/variants/sim/arduino/Arduino.h @@ -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 +#include +#include +#include +#include +#include +#include + +#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::steady_clock::now().time_since_epoch()).count(); + return t0; +} + +inline unsigned long millis() { + uint32_t now = (uint32_t)std::chrono::duration_cast( + 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::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 //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 +typename std::common_type::type min(T a, U b) { return a < b ? a : b; } +#endif +#ifndef max +template +typename std::common_type::type max(T a, U b) { return a > b ? a : b; } +#endif +#ifndef constrain +template +typename std::common_type::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; diff --git a/variants/sim/arduino/Print.h b/variants/sim/arduino/Print.h new file mode 100644 index 00000000..53d4790c --- /dev/null +++ b/variants/sim/arduino/Print.h @@ -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 +#include +#include +#include +#include + +#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); + } +}; diff --git a/variants/sim/arduino/RTClib.h b/variants/sim/arduino/RTClib.h new file mode 100644 index 00000000..90f6632b --- /dev/null +++ b/variants/sim/arduino/RTClib.h @@ -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 +#include + +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; + } +}; diff --git a/variants/sim/arduino/Stream.h b/variants/sim/arduino/Stream.h new file mode 100644 index 00000000..672b37dc --- /dev/null +++ b/variants/sim/arduino/Stream.h @@ -0,0 +1,33 @@ +#pragma once + +// Minimal native stand-in for Arduino's Stream class. src/Utils.h includes +// 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); + } +}; diff --git a/variants/sim/arduino/base64.hpp b/variants/sim/arduino/base64.hpp new file mode 100644 index 00000000..63fb4228 --- /dev/null +++ b/variants/sim/arduino/base64.hpp @@ -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; +} diff --git a/variants/sim/build_wasm.sh b/variants/sim/build_wasm.sh new file mode 100755 index 00000000..2308350b --- /dev/null +++ b/variants/sim/build_wasm.sh @@ -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/.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// -- 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" diff --git a/variants/sim/platformio.ini b/variants/sim/platformio.ini new file mode 100644 index 00000000..07ba08eb --- /dev/null +++ b/variants/sim/platformio.ini @@ -0,0 +1,64 @@ +; variants/sim -- native (host) build of the real companion_radio app logic +; (MyMesh/UITask/DataStore), for Phase 1 of the in-house browser-sim project +; (see the plan doc / project memory for the full Emscripten roadmap this is +; step one of). No Emscripten here yet -- plain g++, a terminal ASCII-art +; "display", stdin for input. Deliberately the easiest possible target so the +; from-scratch port can be debugged without WASM complexity on top. +; +; Not `extends`-ing arduino_base/esp32_base etc: those pull in RadioLib, Wire, +; SPI and a real Arduino framework, none of which this build wants -- it's +; written against platform=native from scratch, closer in spirit to +; [env:native]/[env:native_kiss_modem] below than to a real board env. +[env:sim_companion_radio] +platform = native +build_type = debug +build_flags = + -std=c++17 + -g + ; ARM toolchains (every real board target) default `char` to UNSIGNED; + ; x86/ARM64 host compilers default it to signed. The UI's KEY_* codes + ; (src/helpers/ui/UIScreen.h) go up to 0xF3 and get compared/stored as + ; plain `char` throughout (KeyboardWidget.h, PopupMenu.h, UITask.cpp) -- + ; without this flag those comparisons are silently always-false on a + ; signed-char host (a negative char never equals a positive KEY_* int + ; literal), which would make keyboard input compile cleanly but never + ; actually navigate anything. + -funsigned-char + -D SIM_PLATFORM + -D MESH_DEBUG=0 + -D DISPLAY_CLASS=SimDisplayDriver + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -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/companion_radio + -I examples/companion_radio/ui-new +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/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/sim/sim_main.cpp b/variants/sim/sim_main.cpp new file mode 100644 index 00000000..e9274b04 --- /dev/null +++ b/variants/sim/sim_main.cpp @@ -0,0 +1,112 @@ +// Process/runtime entry point for the sim build. Real Arduino cores provide +// their own main() (call setup() once, then loop() forever); platform=native +// has no such core, so this is that main() for both Phase 1's native target +// and Phase 2's Emscripten target -- the two are different enough (a native +// process owns its own loop and a real stdin tty; a wasm module in a browser +// tab must hand control back to the browser's event loop between ticks, and +// has no stdin at all) that they get fully separate #ifdef __EMSCRIPTEN__ +// branches below rather than one branch trying to cover both. +#include +#include +#include + +// Defined by examples/companion_radio/main.cpp (Arduino sketch convention: +// no header declares these, every board's own entry point just forward- +// declares and calls them). +void setup(); +void loop(); + +#ifdef __EMSCRIPTEN__ +// --------------------------------------------------------------------- +// Phase 2: Emscripten/browser entry point. +#include +#include "SimFS.h" // sim_fs_mount_idbfs() + +// One tick of the real app's cooperative loop -- identical body to +// examples/companion_radio/main.cpp's own loop() (the_mesh.loop(); +// sensors.loop(); ui_task.loop(); rtc_clock.tick(); ...), so this wrapper +// adds nothing of its own; it exists only because emscripten_set_main_loop +// wants a void(void) function pointer, and passing `loop` directly would +// also work here, but naming it makes the call below self-documenting. +static void sim_main_loop_tick() { + loop(); +} + +// Called back from JS (see SimFS.h's sim_fs_mount_idbfs()) once the async +// IndexedDB -> MEMFS pull finishes. This is where the app's boot sequence +// actually starts -- deliberately NOT in main() itself, since main() must +// return (or hand off via emscripten_exit_with_live_runtime()) long before +// this callback ever fires. +extern "C" EMSCRIPTEN_KEEPALIVE void sim_idbfs_ready() { + setup(); + // fps=0, simulate_infinite_loop=1: let the browser's own + // requestAnimationFrame cadence drive ticks (Emscripten's documented + // recommendation for anything drawing to a ) rather than a fixed + // interval -- ties the sim's tick rate to the actual display refresh, and + // it's throttled/paused for free by the browser when the tab is hidden or + // backgrounded, which a manual setInterval(..., fixed_ms) wouldn't get. + emscripten_set_main_loop(sim_main_loop_tick, 0, 1); +} + +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"); + // 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() + // above, an async JS callback that fires well after main() itself is done. + emscripten_exit_with_live_runtime(); + return 0; +} + +#else +// --------------------------------------------------------------------- +// Phase 1: native (host process) entry point -- unchanged from the +// original Phase 1 implementation. Puts the terminal into raw/ +// non-canonical mode so UITask.cpp's SIM_PLATFORM stdin-poll branch (see +// UITask::loop()) receives individual keystrokes immediately instead of +// once per Enter-terminated line. +#include +#include + +static struct termios g_orig_termios; +static bool g_termios_saved = false; + +static void restoreTerminal() { + if (g_termios_saved) tcsetattr(STDIN_FILENO, TCSANOW, &g_orig_termios); +} + +static void setupTerminal() { + if (!isatty(STDIN_FILENO)) return; // piped/redirected stdin -- leave alone + if (tcgetattr(STDIN_FILENO, &g_orig_termios) != 0) return; + g_termios_saved = true; + atexit(restoreTerminal); + + struct termios raw = g_orig_termios; + raw.c_lflag &= ~(ICANON | ECHO); // no line buffering, no local echo + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSANOW, &raw); +} + +int main() { + setupTerminal(); + printf("MeshCore sim (native) -- Ctrl-C to quit.\n"); + printf("Keys: arrows/WASD move, Enter/Space select, Esc/Backspace cancel, n/p next/prev.\n\n"); + + setup(); + for (;;) { + loop(); + // Real boards spin their loop() as fast as the hardware allows too; + // this just keeps a native process from pegging a CPU core at 100% + // for no benefit -- 1ms is well under any UI timing this app cares + // about (refresh/animation intervals are tens-to-hundreds of ms). + usleep(1000); + } + return 0; +} +#endif // __EMSCRIPTEN__ diff --git a/variants/sim/target.cpp b/variants/sim/target.cpp new file mode 100644 index 00000000..f7d35df7 --- /dev/null +++ b/variants/sim/target.cpp @@ -0,0 +1,28 @@ +#include +#include "target.h" +#include "SimRNG.h" + +// Global stdout-backed Serial object, declared extern in Arduino.h. +SimSerialClass Serial; + +SimMainBoard board; +SimRadio radio_driver; +SimRTCClock rtc_clock; +SensorManager sensors; // base class: no real sensors in Phase 1 + +#ifdef DISPLAY_CLASS +DISPLAY_CLASS display; +#endif + +bool radio_init() { + // No real radio hardware to initialise -- always succeeds (see + // variants/sim/SimRadio.h; Phase 3 of the sim plan is where two SimRadio + // instances actually exchange bytes through a shared in-memory "ether"). + return true; +} + +mesh::LocalIdentity radio_new_identity() { + static SimRNG rng; + rng.begin(); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/sim/target.h b/variants/sim/target.h new file mode 100644 index 00000000..7300f40e --- /dev/null +++ b/variants/sim/target.h @@ -0,0 +1,29 @@ +#pragma once + +// variants/sim -- native (host) "board" for running the real companion_radio +// app logic (MyMesh/UITask/DataStore) as a plain terminal program, with no +// real radio/display/BLE hardware. Follows the same target.h/target.cpp/ +// platformio.ini triplet convention as every other board variant (see +// variants/generic-e22/ for the template this was modelled on). + +#include +#include "SimRadio.h" +#include "SimMainBoard.h" +#include "SimRTCClock.h" +#include + +#ifdef DISPLAY_CLASS + #include "SimDisplayDriver.h" +#endif + +extern SimMainBoard board; +extern SimRadio radio_driver; +extern SimRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson.h b/variants/sim/thirdparty/arduinojson/ArduinoJson.h new file mode 100644 index 00000000..9d5927d4 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson.h @@ -0,0 +1,17 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#ifdef __cplusplus + +# include "ArduinoJson.hpp" + +using namespace ArduinoJson; + +#else + +#error ArduinoJson requires a C++ compiler, please change file extension to .cc or .cpp + +#endif diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson.hpp new file mode 100644 index 00000000..fdaf7863 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson.hpp @@ -0,0 +1,65 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#if __cplusplus < 201103L && (!defined(_MSC_VER) || _MSC_VER < 1910) +# error ArduinoJson requires C++11 or newer. Configure your compiler for C++11 or downgrade ArduinoJson to 6.20. +#endif + +#include "ArduinoJson/Configuration.hpp" + +// Include Arduino.h before stdlib.h to avoid conflict with atexit() +// https://github.com/bblanchon/ArduinoJson/pull/1693#issuecomment-1001060240 +#if ARDUINOJSON_ENABLE_ARDUINO_STRING || ARDUINOJSON_ENABLE_ARDUINO_STREAM || \ + ARDUINOJSON_ENABLE_ARDUINO_PRINT || \ + (ARDUINOJSON_ENABLE_PROGMEM && defined(ARDUINO)) +# include +#endif + +#if !ARDUINOJSON_DEBUG +# ifdef __clang__ +# pragma clang system_header +# elif defined __GNUC__ +# pragma GCC system_header +# endif +#endif + +// Remove true and false macros defined by some cores, such as Arduino Due's +// See issues #2181 and arduino/ArduinoCore-sam#50 +#ifdef true +# undef true +#endif +#ifdef false +# undef false +#endif + +#include "ArduinoJson/Array/JsonArray.hpp" +#include "ArduinoJson/Object/JsonObject.hpp" +#include "ArduinoJson/Variant/JsonVariantConst.hpp" + +#include "ArduinoJson/Document/JsonDocument.hpp" + +#include "ArduinoJson/Array/ArrayImpl.hpp" +#include "ArduinoJson/Array/ElementProxy.hpp" +#include "ArduinoJson/Array/Utilities.hpp" +#include "ArduinoJson/Collection/CollectionImpl.hpp" +#include "ArduinoJson/Memory/ResourceManagerImpl.hpp" +#include "ArduinoJson/Object/MemberProxy.hpp" +#include "ArduinoJson/Object/ObjectImpl.hpp" +#include "ArduinoJson/Variant/ConverterImpl.hpp" +#include "ArduinoJson/Variant/JsonVariantCopier.hpp" +#include "ArduinoJson/Variant/VariantCompare.hpp" +#include "ArduinoJson/Variant/VariantImpl.hpp" +#include "ArduinoJson/Variant/VariantRefBaseImpl.hpp" + +#include "ArduinoJson/Json/JsonDeserializer.hpp" +#include "ArduinoJson/Json/JsonSerializer.hpp" +#include "ArduinoJson/Json/PrettyJsonSerializer.hpp" +#include "ArduinoJson/MsgPack/MsgPackBinary.hpp" +#include "ArduinoJson/MsgPack/MsgPackDeserializer.hpp" +#include "ArduinoJson/MsgPack/MsgPackExtension.hpp" +#include "ArduinoJson/MsgPack/MsgPackSerializer.hpp" + +#include "ArduinoJson/compatibility.hpp" diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/ArrayData.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/ArrayData.hpp new file mode 100644 index 00000000..c386950d --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/ArrayData.hpp @@ -0,0 +1,66 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +class ArrayData : public CollectionData { + public: + VariantData* addElement(ResourceManager* resources); + + static VariantData* addElement(ArrayData* array, ResourceManager* resources) { + if (!array) + return nullptr; + return array->addElement(resources); + } + + template + bool addValue(const T& value, ResourceManager* resources); + + template + static bool addValue(ArrayData* array, const T& value, + ResourceManager* resources) { + if (!array) + return false; + return array->addValue(value, resources); + } + + VariantData* getOrAddElement(size_t index, ResourceManager* resources); + + VariantData* getElement(size_t index, const ResourceManager* resources) const; + + static VariantData* getElement(const ArrayData* array, size_t index, + const ResourceManager* resources) { + if (!array) + return nullptr; + return array->getElement(index, resources); + } + + void removeElement(size_t index, ResourceManager* resources); + + static void removeElement(ArrayData* array, size_t index, + ResourceManager* resources) { + if (!array) + return; + array->removeElement(index, resources); + } + + void remove(iterator it, ResourceManager* resources) { + CollectionData::removeOne(it, resources); + } + + static void remove(ArrayData* array, iterator it, + ResourceManager* resources) { + if (array) + return array->remove(it, resources); + } + + private: + iterator at(size_t index, const ResourceManager* resources) const; +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/ArrayImpl.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/ArrayImpl.hpp new file mode 100644 index 00000000..044064fa --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/ArrayImpl.hpp @@ -0,0 +1,79 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +inline ArrayData::iterator ArrayData::at( + size_t index, const ResourceManager* resources) const { + auto it = createIterator(resources); + while (!it.done() && index) { + it.next(resources); + --index; + } + return it; +} + +inline VariantData* ArrayData::addElement(ResourceManager* resources) { + auto slot = resources->allocVariant(); + if (!slot) + return nullptr; + CollectionData::appendOne(slot, resources); + return slot.ptr(); +} + +inline VariantData* ArrayData::getOrAddElement(size_t index, + ResourceManager* resources) { + auto it = createIterator(resources); + while (!it.done() && index > 0) { + it.next(resources); + index--; + } + if (it.done()) + index++; + VariantData* element = it.data(); + while (index > 0) { + element = addElement(resources); + if (!element) + return nullptr; + index--; + } + return element; +} + +inline VariantData* ArrayData::getElement( + size_t index, const ResourceManager* resources) const { + return at(index, resources).data(); +} + +inline void ArrayData::removeElement(size_t index, ResourceManager* resources) { + remove(at(index, resources), resources); +} + +template +inline bool ArrayData::addValue(const T& value, ResourceManager* resources) { + ARDUINOJSON_ASSERT(resources != nullptr); + auto slot = resources->allocVariant(); + if (!slot) + return false; + JsonVariant variant(slot.ptr(), resources); + if (!variant.set(value)) { + resources->freeVariant(slot); + return false; + } + CollectionData::appendOne(slot, resources); + return true; +} + +// Returns the size (in bytes) of an array with n elements. +constexpr size_t sizeofArray(size_t n) { + return n * ResourceManager::slotSize; +} + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/ElementProxy.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/ElementProxy.hpp new file mode 100644 index 00000000..d0d8b417 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/ElementProxy.hpp @@ -0,0 +1,75 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +// A proxy class to get or set an element of an array. +// https://arduinojson.org/v7/api/jsonarray/subscript/ +template +class ElementProxy : public VariantRefBase>, + public VariantOperators> { + friend class VariantAttorney; + + friend class VariantRefBase>; + + template + friend class MemberProxy; + + template + friend class ElementProxy; + + public: + ElementProxy(TUpstream upstream, size_t index) + : upstream_(upstream), index_(index) {} + + ElementProxy& operator=(const ElementProxy& src) { + this->set(src); + return *this; + } + + template + ElementProxy& operator=(const T& src) { + this->set(src); + return *this; + } + + template + ElementProxy& operator=(T* src) { + this->set(src); + return *this; + } + + private: + // clang-format off + ElementProxy(const ElementProxy& src) // Error here? See https://arduinojson.org/v7/proxy-non-copyable/ + : upstream_(src.upstream_), index_(src.index_) {} + // clang-format on + + ResourceManager* getResourceManager() const { + return VariantAttorney::getResourceManager(upstream_); + } + + FORCE_INLINE VariantData* getData() const { + return VariantData::getElement( + VariantAttorney::getData(upstream_), index_, + VariantAttorney::getResourceManager(upstream_)); + } + + VariantData* getOrCreateData() const { + auto data = VariantAttorney::getOrCreateData(upstream_); + if (!data) + return nullptr; + return data->getOrAddElement( + index_, VariantAttorney::getResourceManager(upstream_)); + } + + TUpstream upstream_; + size_t index_; +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/JsonArray.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/JsonArray.hpp new file mode 100644 index 00000000..0441beea --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/JsonArray.hpp @@ -0,0 +1,219 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include + +ARDUINOJSON_BEGIN_PUBLIC_NAMESPACE + +class JsonObject; + +// A reference to an array in a JsonDocument +// https://arduinojson.org/v7/api/jsonarray/ +class JsonArray : public detail::VariantOperators { + friend class detail::VariantAttorney; + + public: + using iterator = JsonArrayIterator; + + // Constructs an unbound reference. + JsonArray() : data_(0), resources_(0) {} + + // INTERNAL USE ONLY + JsonArray(detail::ArrayData* data, detail::ResourceManager* resources) + : data_(data), resources_(resources) {} + + // Returns a JsonVariant pointing to the array. + // https://arduinojson.org/v7/api/jsonvariant/ + operator JsonVariant() { + void* data = data_; // prevent warning cast-align + return JsonVariant(reinterpret_cast(data), + resources_); + } + + // Returns a read-only reference to the array. + // https://arduinojson.org/v7/api/jsonarrayconst/ + operator JsonArrayConst() const { + return JsonArrayConst(data_, resources_); + } + + // Appends a new (empty) element to the array. + // Returns a reference to the new element. + // https://arduinojson.org/v7/api/jsonarray/add/ + template ::value, int> = 0> + T add() const { + return add().to(); + } + + // Appends a new (null) element to the array. + // Returns a reference to the new element. + // https://arduinojson.org/v7/api/jsonarray/add/ + template ::value, int> = 0> + JsonVariant add() const { + return JsonVariant(detail::ArrayData::addElement(data_, resources_), + resources_); + } + + // Appends a value to the array. + // https://arduinojson.org/v7/api/jsonarray/add/ + template + bool add(const T& value) const { + return detail::ArrayData::addValue(data_, value, resources_); + } + + // Appends a value to the array. + // https://arduinojson.org/v7/api/jsonarray/add/ + template ::value, int> = 0> + bool add(T* value) const { + return detail::ArrayData::addValue(data_, value, resources_); + } + + // Returns an iterator to the first element of the array. + // https://arduinojson.org/v7/api/jsonarray/begin/ + iterator begin() const { + if (!data_) + return iterator(); + return iterator(data_->createIterator(resources_), resources_); + } + + // Returns an iterator following the last element of the array. + // https://arduinojson.org/v7/api/jsonarray/end/ + iterator end() const { + return iterator(); + } + + // Copies an array. + // https://arduinojson.org/v7/api/jsonarray/set/ + bool set(JsonArrayConst src) const { + if (!data_) + return false; + + clear(); + for (auto element : src) { + if (!add(element)) + return false; + } + + return true; + } + + // Removes the element at the specified iterator. + // https://arduinojson.org/v7/api/jsonarray/remove/ + void remove(iterator it) const { + detail::ArrayData::remove(data_, it.iterator_, resources_); + } + + // Removes the element at the specified index. + // https://arduinojson.org/v7/api/jsonarray/remove/ + void remove(size_t index) const { + detail::ArrayData::removeElement(data_, index, resources_); + } + + // Removes the element at the specified index. + // https://arduinojson.org/v7/api/jsonarray/remove/ + template ::value, int> = 0> + void remove(const TVariant& variant) const { + if (variant.template is()) + remove(variant.template as()); + } + + // Removes all the elements of the array. + // https://arduinojson.org/v7/api/jsonarray/clear/ + void clear() const { + detail::ArrayData::clear(data_, resources_); + } + + // Gets or sets the element at the specified index. + // https://arduinojson.org/v7/api/jsonarray/subscript/ + template ::value, int> = 0> + detail::ElementProxy operator[](T index) const { + return {*this, size_t(index)}; + } + + // Gets or sets the element at the specified index. + // https://arduinojson.org/v7/api/jsonarray/subscript/ + template ::value, int> = 0> + detail::ElementProxy operator[](const TVariant& variant) const { + if (variant.template is()) + return {*this, variant.template as()}; + else + return {*this, size_t(-1)}; + } + + operator JsonVariantConst() const { + return JsonVariantConst(collectionToVariant(data_), resources_); + } + + // Returns true if the reference is unbound. + // https://arduinojson.org/v7/api/jsonarray/isnull/ + bool isNull() const { + return data_ == 0; + } + + // Returns true if the reference is bound. + // https://arduinojson.org/v7/api/jsonarray/isnull/ + operator bool() const { + return data_ != 0; + } + + // Returns the depth (nesting level) of the array. + // https://arduinojson.org/v7/api/jsonarray/nesting/ + size_t nesting() const { + return detail::VariantData::nesting(collectionToVariant(data_), resources_); + } + + // Returns the number of elements in the array. + // https://arduinojson.org/v7/api/jsonarray/size/ + size_t size() const { + return data_ ? data_->size(resources_) : 0; + } + + // DEPRECATED: use add() instead + ARDUINOJSON_DEPRECATED("use add() instead") + JsonVariant add() const { + return add(); + } + + // DEPRECATED: use add() instead + ARDUINOJSON_DEPRECATED("use add() instead") + JsonArray createNestedArray() const { + return add(); + } + + // DEPRECATED: use add() instead + ARDUINOJSON_DEPRECATED("use add() instead") + JsonObject createNestedObject() const; + + // DEPRECATED: always returns zero + ARDUINOJSON_DEPRECATED("always returns zero") + size_t memoryUsage() const { + return 0; + } + + private: + detail::ResourceManager* getResourceManager() const { + return resources_; + } + + detail::VariantData* getData() const { + return collectionToVariant(data_); + } + + detail::VariantData* getOrCreateData() const { + return collectionToVariant(data_); + } + + detail::ArrayData* data_; + detail::ResourceManager* resources_; +}; + +ARDUINOJSON_END_PUBLIC_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/JsonArrayConst.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/JsonArrayConst.hpp new file mode 100644 index 00000000..88762d34 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/JsonArrayConst.hpp @@ -0,0 +1,133 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include +#include + +ARDUINOJSON_BEGIN_PUBLIC_NAMESPACE + +class JsonObject; + +// A read-only reference to an array in a JsonDocument +// https://arduinojson.org/v7/api/jsonarrayconst/ +class JsonArrayConst : public detail::VariantOperators { + friend class JsonArray; + friend class detail::VariantAttorney; + + public: + using iterator = JsonArrayConstIterator; + + // Returns an iterator to the first element of the array. + // https://arduinojson.org/v7/api/jsonarrayconst/begin/ + iterator begin() const { + if (!data_) + return iterator(); + return iterator(data_->createIterator(resources_), resources_); + } + + // Returns an iterator to the element following the last element of the array. + // https://arduinojson.org/v7/api/jsonarrayconst/end/ + iterator end() const { + return iterator(); + } + + // Creates an unbound reference. + JsonArrayConst() : data_(0), resources_(0) {} + + // INTERNAL USE ONLY + JsonArrayConst(const detail::ArrayData* data, + const detail::ResourceManager* resources) + : data_(data), resources_(resources) {} + + // Returns the element at the specified index. + // https://arduinojson.org/v7/api/jsonarrayconst/subscript/ + template ::value, int> = 0> + JsonVariantConst operator[](T index) const { + return JsonVariantConst( + detail::ArrayData::getElement(data_, size_t(index), resources_), + resources_); + } + + // Returns the element at the specified index. + // https://arduinojson.org/v7/api/jsonarrayconst/subscript/ + template ::value, int> = 0> + JsonVariantConst operator[](const TVariant& variant) const { + if (variant.template is()) + return operator[](variant.template as()); + else + return JsonVariantConst(); + } + + operator JsonVariantConst() const { + return JsonVariantConst(getData(), resources_); + } + + // Returns true if the reference is unbound. + // https://arduinojson.org/v7/api/jsonarrayconst/isnull/ + bool isNull() const { + return data_ == 0; + } + + // Returns true if the reference is bound. + // https://arduinojson.org/v7/api/jsonarrayconst/isnull/ + operator bool() const { + return data_ != 0; + } + + // Returns the depth (nesting level) of the array. + // https://arduinojson.org/v7/api/jsonarrayconst/nesting/ + size_t nesting() const { + return detail::VariantData::nesting(getData(), resources_); + } + + // Returns the number of elements in the array. + // https://arduinojson.org/v7/api/jsonarrayconst/size/ + size_t size() const { + return data_ ? data_->size(resources_) : 0; + } + + // DEPRECATED: always returns zero + ARDUINOJSON_DEPRECATED("always returns zero") + size_t memoryUsage() const { + return 0; + } + + private: + const detail::VariantData* getData() const { + return collectionToVariant(data_); + } + + const detail::ArrayData* data_; + const detail::ResourceManager* resources_; +}; + +// Compares the content of two arrays. +// Returns true if the two arrays are equal. +inline bool operator==(JsonArrayConst lhs, JsonArrayConst rhs) { + if (!lhs && !rhs) + return true; + if (!lhs || !rhs) + return false; + + auto a = lhs.begin(); + auto b = rhs.begin(); + + for (;;) { + if (a == b) // same pointer or both null + return true; + if (a == lhs.end() || b == rhs.end()) + return false; + if (*a != *b) + return false; + ++a; + ++b; + } +} + +ARDUINOJSON_END_PUBLIC_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/JsonArrayIterator.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/JsonArrayIterator.hpp new file mode 100644 index 00000000..48daf8b9 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/JsonArrayIterator.hpp @@ -0,0 +1,96 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include + +ARDUINOJSON_BEGIN_PUBLIC_NAMESPACE + +template +class Ptr { + public: + Ptr(T value) : value_(value) {} + + T* operator->() { + return &value_; + } + + T& operator*() { + return value_; + } + + private: + T value_; +}; + +class JsonArrayIterator { + friend class JsonArray; + + public: + JsonArrayIterator() {} + explicit JsonArrayIterator(detail::ArrayData::iterator iterator, + detail::ResourceManager* resources) + : iterator_(iterator), resources_(resources) {} + + JsonVariant operator*() { + return JsonVariant(iterator_.data(), resources_); + } + Ptr operator->() { + return operator*(); + } + + bool operator==(const JsonArrayIterator& other) const { + return iterator_ == other.iterator_; + } + + bool operator!=(const JsonArrayIterator& other) const { + return iterator_ != other.iterator_; + } + + JsonArrayIterator& operator++() { + iterator_.next(resources_); + return *this; + } + + private: + detail::ArrayData::iterator iterator_; + detail::ResourceManager* resources_; +}; + +class JsonArrayConstIterator { + friend class JsonArray; + + public: + JsonArrayConstIterator() {} + explicit JsonArrayConstIterator(detail::ArrayData::iterator iterator, + const detail::ResourceManager* resources) + : iterator_(iterator), resources_(resources) {} + + JsonVariantConst operator*() const { + return JsonVariantConst(iterator_.data(), resources_); + } + Ptr operator->() { + return operator*(); + } + + bool operator==(const JsonArrayConstIterator& other) const { + return iterator_ == other.iterator_; + } + + bool operator!=(const JsonArrayConstIterator& other) const { + return iterator_ != other.iterator_; + } + + JsonArrayConstIterator& operator++() { + iterator_.next(resources_); + return *this; + } + + private: + detail::ArrayData::iterator iterator_; + const detail::ResourceManager* resources_; +}; + +ARDUINOJSON_END_PUBLIC_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/Utilities.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/Utilities.hpp new file mode 100644 index 00000000..9f404dea --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Array/Utilities.hpp @@ -0,0 +1,112 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include + +ARDUINOJSON_BEGIN_PUBLIC_NAMESPACE + +// Copies a value to a JsonVariant. +// This is a degenerated form of copyArray() to stop the recursion. +template ::value, int> = 0> +inline bool copyArray(const T& src, JsonVariant dst) { + return dst.set(src); +} + +// Copies values from an array to a JsonArray or a JsonVariant. +// https://arduinojson.org/v7/api/misc/copyarray/ +template ::value, int> = 0> +inline bool copyArray(T (&src)[N], const TDestination& dst) { + return copyArray(src, N, dst); +} + +// Copies values from an array to a JsonArray or a JsonVariant. +// https://arduinojson.org/v7/api/misc/copyarray/ +template ::value, int> = 0> +inline bool copyArray(const T* src, size_t len, const TDestination& dst) { + bool ok = true; + for (size_t i = 0; i < len; i++) { + ok &= copyArray(src[i], dst.template add()); + } + return ok; +} + +// Copies a string to a JsonVariant. +// This is a degenerated form of copyArray() to handle strings. +template +inline bool copyArray(const char* src, size_t, const TDestination& dst) { + return dst.set(src); +} + +// Copies values from an array to a JsonDocument. +// https://arduinojson.org/v7/api/misc/copyarray/ +template +inline bool copyArray(const T& src, JsonDocument& dst) { + return copyArray(src, dst.to()); +} + +// Copies an array to a JsonDocument. +// https://arduinojson.org/v7/api/misc/copyarray/ +template +inline bool copyArray(const T* src, size_t len, JsonDocument& dst) { + return copyArray(src, len, dst.to()); +} + +// Copies a value from a JsonVariant. +// This is a degenerated form of copyArray() to stop the recursion. +template ::value, int> = 0> +inline size_t copyArray(JsonVariantConst src, T& dst) { + dst = src.as(); + return 1; +} + +// Copies values from a JsonArray or JsonVariant to an array. +// https://arduinojson.org/v7/api/misc/copyarray/ +template +inline size_t copyArray(JsonArrayConst src, T (&dst)[N]) { + return copyArray(src, dst, N); +} + +// Copies values from a JsonArray or JsonVariant to an array. +// https://arduinojson.org/v7/api/misc/copyarray/ +template +inline size_t copyArray(JsonArrayConst src, T* dst, size_t len) { + size_t i = 0; + for (JsonArrayConst::iterator it = src.begin(); it != src.end() && i < len; + ++it) + copyArray(*it, dst[i++]); + return i; +} + +// Copies a string from a JsonVariant. +// This is a degenerated form of copyArray() to handle strings. +template +inline size_t copyArray(JsonVariantConst src, char (&dst)[N]) { + JsonString s = src; + size_t len = N - 1; + if (len > s.size()) + len = s.size(); + memcpy(dst, s.c_str(), len); + dst[len] = 0; + return 1; +} + +// Copies values from a JsonDocument to an array. +// https://arduinojson.org/v7/api/misc/copyarray/ +template < + typename TSource, typename T, + detail::enable_if_t::value && + detail::is_base_of::value, + int> = 0> +inline size_t copyArray(const TSource& src, T& dst) { + return copyArray(src.template as(), dst); +} + +ARDUINOJSON_END_PUBLIC_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Collection/CollectionData.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Collection/CollectionData.hpp new file mode 100644 index 00000000..88215998 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Collection/CollectionData.hpp @@ -0,0 +1,122 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include +#include + +#include // size_t + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +class VariantData; +class ResourceManager; + +class CollectionIterator { + friend class CollectionData; + + public: + CollectionIterator() : slot_(nullptr), currentId_(NULL_SLOT) {} + + void next(const ResourceManager* resources); + + bool done() const { + return slot_ == nullptr; + } + + bool operator==(const CollectionIterator& other) const { + return slot_ == other.slot_; + } + + bool operator!=(const CollectionIterator& other) const { + return slot_ != other.slot_; + } + + VariantData* operator->() { + ARDUINOJSON_ASSERT(slot_ != nullptr); + return data(); + } + + VariantData& operator*() { + ARDUINOJSON_ASSERT(slot_ != nullptr); + return *data(); + } + + const VariantData& operator*() const { + ARDUINOJSON_ASSERT(slot_ != nullptr); + return *data(); + } + + VariantData* data() { + return reinterpret_cast(slot_); + } + + const VariantData* data() const { + return reinterpret_cast(slot_); + } + + private: + CollectionIterator(VariantData* slot, SlotId slotId); + + VariantData* slot_; + SlotId currentId_, nextId_; +}; + +class CollectionData { + SlotId head_ = NULL_SLOT; + SlotId tail_ = NULL_SLOT; + + public: + // Placement new + static void* operator new(size_t, void* p) noexcept { + return p; + } + + static void operator delete(void*, void*) noexcept {} + + using iterator = CollectionIterator; + + iterator createIterator(const ResourceManager* resources) const; + + size_t size(const ResourceManager*) const; + size_t nesting(const ResourceManager*) const; + + void clear(ResourceManager* resources); + + static void clear(CollectionData* collection, ResourceManager* resources) { + if (!collection) + return; + collection->clear(resources); + } + + SlotId head() const { + return head_; + } + + protected: + void appendOne(Slot slot, const ResourceManager* resources); + void appendPair(Slot key, Slot value, + const ResourceManager* resources); + + void removeOne(iterator it, ResourceManager* resources); + void removePair(iterator it, ResourceManager* resources); + + private: + Slot getPreviousSlot(VariantData*, const ResourceManager*) const; +}; + +inline const VariantData* collectionToVariant( + const CollectionData* collection) { + const void* data = collection; // prevent warning cast-align + return reinterpret_cast(data); +} + +inline VariantData* collectionToVariant(CollectionData* collection) { + void* data = collection; // prevent warning cast-align + return reinterpret_cast(data); +} + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Collection/CollectionImpl.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Collection/CollectionImpl.hpp new file mode 100644 index 00000000..7b430530 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Collection/CollectionImpl.hpp @@ -0,0 +1,137 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include +#include +#include +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +inline CollectionIterator::CollectionIterator(VariantData* slot, SlotId slotId) + : slot_(slot), currentId_(slotId) { + nextId_ = slot_ ? slot_->next() : NULL_SLOT; +} + +inline void CollectionIterator::next(const ResourceManager* resources) { + ARDUINOJSON_ASSERT(currentId_ != NULL_SLOT); + slot_ = resources->getVariant(nextId_); + currentId_ = nextId_; + if (slot_) + nextId_ = slot_->next(); +} + +inline CollectionData::iterator CollectionData::createIterator( + const ResourceManager* resources) const { + return iterator(resources->getVariant(head_), head_); +} + +inline void CollectionData::appendOne(Slot slot, + const ResourceManager* resources) { + if (tail_ != NULL_SLOT) { + auto tail = resources->getVariant(tail_); + tail->setNext(slot.id()); + tail_ = slot.id(); + } else { + head_ = slot.id(); + tail_ = slot.id(); + } +} + +inline void CollectionData::appendPair(Slot key, + Slot value, + const ResourceManager* resources) { + key->setNext(value.id()); + + if (tail_ != NULL_SLOT) { + auto tail = resources->getVariant(tail_); + tail->setNext(key.id()); + tail_ = value.id(); + } else { + head_ = key.id(); + tail_ = value.id(); + } +} + +inline void CollectionData::clear(ResourceManager* resources) { + auto next = head_; + while (next != NULL_SLOT) { + auto currId = next; + auto slot = resources->getVariant(next); + next = slot->next(); + resources->freeVariant({slot, currId}); + } + + head_ = NULL_SLOT; + tail_ = NULL_SLOT; +} + +inline Slot CollectionData::getPreviousSlot( + VariantData* target, const ResourceManager* resources) const { + auto prev = Slot(); + auto currentId = head_; + while (currentId != NULL_SLOT) { + auto currentSlot = resources->getVariant(currentId); + if (currentSlot == target) + break; + prev = Slot(currentSlot, currentId); + currentId = currentSlot->next(); + } + return prev; +} + +inline void CollectionData::removeOne(iterator it, ResourceManager* resources) { + if (it.done()) + return; + auto curr = it.slot_; + auto prev = getPreviousSlot(curr, resources); + auto next = curr->next(); + if (prev) + prev->setNext(next); + else + head_ = next; + if (next == NULL_SLOT) + tail_ = prev.id(); + resources->freeVariant({it.slot_, it.currentId_}); +} + +inline void CollectionData::removePair(ObjectData::iterator it, + ResourceManager* resources) { + if (it.done()) + return; + + auto keySlot = it.slot_; + + auto valueId = it.nextId_; + auto valueSlot = resources->getVariant(valueId); + + // remove value slot + keySlot->setNext(valueSlot->next()); + resources->freeVariant({valueSlot, valueId}); + + // remove key slot + removeOne(it, resources); +} + +inline size_t CollectionData::nesting(const ResourceManager* resources) const { + size_t maxChildNesting = 0; + for (auto it = createIterator(resources); !it.done(); it.next(resources)) { + size_t childNesting = it->nesting(resources); + if (childNesting > maxChildNesting) + maxChildNesting = childNesting; + } + return maxChildNesting + 1; +} + +inline size_t CollectionData::size(const ResourceManager* resources) const { + size_t count = 0; + for (auto it = createIterator(resources); !it.done(); it.next(resources)) + count++; + return count; +} + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Configuration.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Configuration.hpp new file mode 100644 index 00000000..801202c4 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Configuration.hpp @@ -0,0 +1,285 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +// Support std::istream and std::ostream +// https://arduinojson.org/v7/config/enable_std_stream/ +#ifndef ARDUINOJSON_ENABLE_STD_STREAM +# ifdef __has_include +# if __has_include() && \ + __has_include() && \ + !defined(min) && \ + !defined(max) +# define ARDUINOJSON_ENABLE_STD_STREAM 1 +# else +# define ARDUINOJSON_ENABLE_STD_STREAM 0 +# endif +# else +# ifdef ARDUINO +# define ARDUINOJSON_ENABLE_STD_STREAM 0 +# else +# define ARDUINOJSON_ENABLE_STD_STREAM 1 +# endif +# endif +#endif + +// Support std::string +// https://arduinojson.org/v7/config/enable_std_string/ +#ifndef ARDUINOJSON_ENABLE_STD_STRING +# ifdef __has_include +# if __has_include() && !defined(min) && !defined(max) +# define ARDUINOJSON_ENABLE_STD_STRING 1 +# else +# define ARDUINOJSON_ENABLE_STD_STRING 0 +# endif +# else +# ifdef ARDUINO +# define ARDUINOJSON_ENABLE_STD_STRING 0 +# else +# define ARDUINOJSON_ENABLE_STD_STRING 1 +# endif +# endif +#endif + +// Support for std::string_view +#ifndef ARDUINOJSON_ENABLE_STRING_VIEW +# ifdef __has_include +# if __has_include() && __cplusplus >= 201703L +# define ARDUINOJSON_ENABLE_STRING_VIEW 1 +# else +# define ARDUINOJSON_ENABLE_STRING_VIEW 0 +# endif +# else +# define ARDUINOJSON_ENABLE_STRING_VIEW 0 +# endif +#endif + +// Pointer size: a heuristic to set sensible defaults +#ifndef ARDUINOJSON_SIZEOF_POINTER +# if defined(__SIZEOF_POINTER__) +# define ARDUINOJSON_SIZEOF_POINTER __SIZEOF_POINTER__ +# elif defined(_WIN64) && _WIN64 +# define ARDUINOJSON_SIZEOF_POINTER 8 // 64 bits +# else +# define ARDUINOJSON_SIZEOF_POINTER 4 // assume 32 bits otherwise +# endif +#endif + +// Store floating-point values with float (0) or double (1) +// https://arduinojson.org/v7/config/use_double/ +#ifndef ARDUINOJSON_USE_DOUBLE +# if ARDUINOJSON_SIZEOF_POINTER >= 4 // 32 & 64 bits systems +# define ARDUINOJSON_USE_DOUBLE 1 +# else +# define ARDUINOJSON_USE_DOUBLE 0 +# endif +#endif + +// Store integral values with long (0) or long long (1) +// https://arduinojson.org/v7/config/use_long_long/ +#ifndef ARDUINOJSON_USE_LONG_LONG +# if ARDUINOJSON_SIZEOF_POINTER >= 4 // 32 & 64 bits systems +# define ARDUINOJSON_USE_LONG_LONG 1 +# else +# define ARDUINOJSON_USE_LONG_LONG 0 +# endif +#endif + +// Limit nesting as the stack is likely to be small +// https://arduinojson.org/v7/config/default_nesting_limit/ +#ifndef ARDUINOJSON_DEFAULT_NESTING_LIMIT +# define ARDUINOJSON_DEFAULT_NESTING_LIMIT 10 +#endif + +// Number of bytes to store a slot id +// https://arduinojson.org/v7/config/slot_id_size/ +#ifndef ARDUINOJSON_SLOT_ID_SIZE +# if ARDUINOJSON_SIZEOF_POINTER <= 2 +// 8-bit and 16-bit archs => up to 255 slots +# define ARDUINOJSON_SLOT_ID_SIZE 1 +# elif ARDUINOJSON_SIZEOF_POINTER == 4 +// 32-bit arch => up to 65535 slots +# define ARDUINOJSON_SLOT_ID_SIZE 2 +# else +// 64-bit arch => up to 4294967295 slots +# define ARDUINOJSON_SLOT_ID_SIZE 4 +# endif +#endif + +// Capacity of each variant pool (in slots) +#ifndef ARDUINOJSON_POOL_CAPACITY +# if ARDUINOJSON_SLOT_ID_SIZE == 1 +# define ARDUINOJSON_POOL_CAPACITY 16 // 96 bytes +# elif ARDUINOJSON_SLOT_ID_SIZE == 2 +# define ARDUINOJSON_POOL_CAPACITY 128 // 1024 bytes +# else +# define ARDUINOJSON_POOL_CAPACITY 256 // 4096 bytes +# endif +#endif + +// Initial capacity of the pool list +#ifndef ARDUINOJSON_INITIAL_POOL_COUNT +# define ARDUINOJSON_INITIAL_POOL_COUNT 4 +#endif + +// Automatically call shrinkToFit() from deserializeXxx() +// Disabled by default on 8-bit platforms because it's not worth the increase in +// code size +#ifndef ARDUINOJSON_AUTO_SHRINK +# if ARDUINOJSON_SIZEOF_POINTER <= 2 +# define ARDUINOJSON_AUTO_SHRINK 0 +# else +# define ARDUINOJSON_AUTO_SHRINK 1 +# endif +#endif + +// Number of bytes to store the length of a string +// https://arduinojson.org/v7/config/string_length_size/ +#ifndef ARDUINOJSON_STRING_LENGTH_SIZE +# if ARDUINOJSON_SIZEOF_POINTER <= 2 +# define ARDUINOJSON_STRING_LENGTH_SIZE 1 // up to 255 characters +# else +# define ARDUINOJSON_STRING_LENGTH_SIZE 2 // up to 65535 characters +# endif +#endif + +#ifdef ARDUINO + +// Enable support for Arduino's String class +// https://arduinojson.org/v7/config/enable_arduino_string/ +# ifndef ARDUINOJSON_ENABLE_ARDUINO_STRING +# define ARDUINOJSON_ENABLE_ARDUINO_STRING 1 +# endif + +// Enable support for Arduino's Stream class +// https://arduinojson.org/v7/config/enable_arduino_stream/ +# ifndef ARDUINOJSON_ENABLE_ARDUINO_STREAM +# define ARDUINOJSON_ENABLE_ARDUINO_STREAM 1 +# endif + +// Enable support for Arduino's Print class +# ifndef ARDUINOJSON_ENABLE_ARDUINO_PRINT +# define ARDUINOJSON_ENABLE_ARDUINO_PRINT 1 +# endif + +// Enable support for PROGMEM +// https://arduinojson.org/v7/config/enable_progmem/ +# ifndef ARDUINOJSON_ENABLE_PROGMEM +# define ARDUINOJSON_ENABLE_PROGMEM 1 +# endif + +#else // ARDUINO + +// Disable support for Arduino's String class +// https://arduinojson.org/v7/config/enable_arduino_string/ +# ifndef ARDUINOJSON_ENABLE_ARDUINO_STRING +# define ARDUINOJSON_ENABLE_ARDUINO_STRING 0 +# endif + +// Disable support for Arduino's Stream class +// https://arduinojson.org/v7/config/enable_arduino_stream/ +# ifndef ARDUINOJSON_ENABLE_ARDUINO_STREAM +# define ARDUINOJSON_ENABLE_ARDUINO_STREAM 0 +# endif + +// Disable support for Arduino's Print class +# ifndef ARDUINOJSON_ENABLE_ARDUINO_PRINT +# define ARDUINOJSON_ENABLE_ARDUINO_PRINT 0 +# endif + +// Enable PROGMEM support on AVR only +// https://arduinojson.org/v7/config/enable_progmem/ +# ifndef ARDUINOJSON_ENABLE_PROGMEM +# ifdef __AVR__ +# define ARDUINOJSON_ENABLE_PROGMEM 1 +# else +# define ARDUINOJSON_ENABLE_PROGMEM 0 +# endif +# endif + +#endif // ARDUINO + +// Convert unicode escape sequence (\u0123) to UTF-8 +// https://arduinojson.org/v7/config/decode_unicode/ +#ifndef ARDUINOJSON_DECODE_UNICODE +# define ARDUINOJSON_DECODE_UNICODE 1 +#endif + +// Ignore comments in input +// https://arduinojson.org/v7/config/enable_comments/ +#ifndef ARDUINOJSON_ENABLE_COMMENTS +# define ARDUINOJSON_ENABLE_COMMENTS 0 +#endif + +// Support NaN in JSON +// https://arduinojson.org/v7/config/enable_nan/ +#ifndef ARDUINOJSON_ENABLE_NAN +# define ARDUINOJSON_ENABLE_NAN 0 +#endif + +// Support Infinity in JSON +// https://arduinojson.org/v7/config/enable_infinity/ +#ifndef ARDUINOJSON_ENABLE_INFINITY +# define ARDUINOJSON_ENABLE_INFINITY 0 +#endif + +// Control the exponentiation threshold for big numbers +// CAUTION: cannot be more that 1e9 !!!! +// https://arduinojson.org/v7/config/positive_exponentiation_threshold/ +#ifndef ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD +# define ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD 1e7 +#endif + +// Control the exponentiation threshold for small numbers +// https://arduinojson.org/v7/config/negative_exponentiation_threshold/ +#ifndef ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD +# define ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD 1e-5 +#endif + +#ifndef ARDUINOJSON_LITTLE_ENDIAN +# if defined(_MSC_VER) || \ + (defined(__BYTE_ORDER__) && \ + __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || \ + defined(__LITTLE_ENDIAN__) || defined(__i386) || defined(__x86_64) +# define ARDUINOJSON_LITTLE_ENDIAN 1 +# else +# define ARDUINOJSON_LITTLE_ENDIAN 0 +# endif +#endif + +#ifndef ARDUINOJSON_ENABLE_ALIGNMENT +# if defined(__AVR) +# define ARDUINOJSON_ENABLE_ALIGNMENT 0 +# else +# define ARDUINOJSON_ENABLE_ALIGNMENT 1 +# endif +#endif + +#ifndef ARDUINOJSON_TAB +# define ARDUINOJSON_TAB " " +#endif + +#ifndef ARDUINOJSON_STRING_BUFFER_SIZE +# define ARDUINOJSON_STRING_BUFFER_SIZE 32 +#endif + +#ifndef ARDUINOJSON_DEBUG +# ifdef __PLATFORMIO_BUILD_DEBUG__ +# define ARDUINOJSON_DEBUG 1 +# else +# define ARDUINOJSON_DEBUG 0 +# endif +#endif + +#if ARDUINOJSON_USE_LONG_LONG || ARDUINOJSON_USE_DOUBLE +# define ARDUINOJSON_USE_EXTENSIONS 1 +#else +# define ARDUINOJSON_USE_EXTENSIONS 0 +#endif + +#if defined(nullptr) +# error nullptr is defined as a macro. Remove the faulty #define or #undef nullptr +// See https://github.com/bblanchon/ArduinoJson/issues/1355 +#endif diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/DeserializationError.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/DeserializationError.hpp new file mode 100644 index 00000000..c498b07b --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/DeserializationError.hpp @@ -0,0 +1,106 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include +#include + +#if ARDUINOJSON_ENABLE_STD_STREAM +# include +#endif + +ARDUINOJSON_BEGIN_PUBLIC_NAMESPACE + +class DeserializationError { + public: + enum Code { + Ok, + EmptyInput, + IncompleteInput, + InvalidInput, + NoMemory, + TooDeep + }; + + DeserializationError() {} + DeserializationError(Code c) : code_(c) {} + + // Compare with DeserializationError + friend bool operator==(const DeserializationError& lhs, + const DeserializationError& rhs) { + return lhs.code_ == rhs.code_; + } + friend bool operator!=(const DeserializationError& lhs, + const DeserializationError& rhs) { + return lhs.code_ != rhs.code_; + } + + // Compare with Code + friend bool operator==(const DeserializationError& lhs, Code rhs) { + return lhs.code_ == rhs; + } + friend bool operator==(Code lhs, const DeserializationError& rhs) { + return lhs == rhs.code_; + } + friend bool operator!=(const DeserializationError& lhs, Code rhs) { + return lhs.code_ != rhs; + } + friend bool operator!=(Code lhs, const DeserializationError& rhs) { + return lhs != rhs.code_; + } + + // Returns true if there is an error + explicit operator bool() const { + return code_ != Ok; + } + + // Returns internal enum, useful for switch statement + Code code() const { + return code_; + } + + const char* c_str() const { + static const char* messages[] = { + "Ok", "EmptyInput", "IncompleteInput", + "InvalidInput", "NoMemory", "TooDeep"}; + ARDUINOJSON_ASSERT(static_cast(code_) < + sizeof(messages) / sizeof(messages[0])); + return messages[code_]; + } + +#if ARDUINOJSON_ENABLE_PROGMEM + const __FlashStringHelper* f_str() const { + ARDUINOJSON_DEFINE_PROGMEM_ARRAY(char, s0, "Ok"); + ARDUINOJSON_DEFINE_PROGMEM_ARRAY(char, s1, "EmptyInput"); + ARDUINOJSON_DEFINE_PROGMEM_ARRAY(char, s2, "IncompleteInput"); + ARDUINOJSON_DEFINE_PROGMEM_ARRAY(char, s3, "InvalidInput"); + ARDUINOJSON_DEFINE_PROGMEM_ARRAY(char, s4, "NoMemory"); + ARDUINOJSON_DEFINE_PROGMEM_ARRAY(char, s5, "TooDeep"); + ARDUINOJSON_DEFINE_PROGMEM_ARRAY(const char*, messages, + {s0, s1, s2, s3, s4, s5}); + return reinterpret_cast( + detail::pgm_read(messages + code_)); + } +#endif + + private: + Code code_; +}; + +#if ARDUINOJSON_ENABLE_STD_STREAM +inline std::ostream& operator<<(std::ostream& s, + const DeserializationError& e) { + s << e.c_str(); + return s; +} + +inline std::ostream& operator<<(std::ostream& s, DeserializationError::Code c) { + s << DeserializationError(c).c_str(); + return s; +} +#endif + +ARDUINOJSON_END_PUBLIC_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/DeserializationOptions.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/DeserializationOptions.hpp new file mode 100644 index 00000000..b711d716 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/DeserializationOptions.hpp @@ -0,0 +1,35 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +template +struct DeserializationOptions { + TFilter filter; + DeserializationOption::NestingLimit nestingLimit; +}; + +template +inline DeserializationOptions makeDeserializationOptions( + TFilter filter, DeserializationOption::NestingLimit nestingLimit = {}) { + return {filter, nestingLimit}; +} + +template +inline DeserializationOptions makeDeserializationOptions( + DeserializationOption::NestingLimit nestingLimit, TFilter filter) { + return {filter, nestingLimit}; +} + +inline DeserializationOptions makeDeserializationOptions( + DeserializationOption::NestingLimit nestingLimit = {}) { + return {{}, nestingLimit}; +} + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Filter.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Filter.hpp new file mode 100644 index 00000000..564893b8 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Filter.hpp @@ -0,0 +1,77 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include + +ARDUINOJSON_BEGIN_PUBLIC_NAMESPACE + +namespace DeserializationOption { +class Filter { + public: +#if ARDUINOJSON_AUTO_SHRINK + explicit Filter(JsonDocument& doc) : variant_(doc) { + doc.shrinkToFit(); + } +#endif + + explicit Filter(JsonVariantConst variant) : variant_(variant) {} + + bool allow() const { + return variant_; + } + + bool allowArray() const { + return variant_ == true || variant_.is(); + } + + bool allowObject() const { + return variant_ == true || variant_.is(); + } + + bool allowValue() const { + return variant_ == true; + } + + template + Filter operator[](const TKey& key) const { + if (variant_ == true) // "true" means "allow recursively" + return *this; + JsonVariantConst member = variant_[key]; + return Filter(member.isNull() ? variant_["*"] : member); + } + + private: + JsonVariantConst variant_; +}; +} // namespace DeserializationOption + +namespace detail { +struct AllowAllFilter { + bool allow() const { + return true; + } + + bool allowArray() const { + return true; + } + + bool allowObject() const { + return true; + } + + bool allowValue() const { + return true; + } + + template + AllowAllFilter operator[](const TKey&) const { + return AllowAllFilter(); + } +}; +} // namespace detail + +ARDUINOJSON_END_PUBLIC_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/NestingLimit.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/NestingLimit.hpp new file mode 100644 index 00000000..11aba7ea --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/NestingLimit.hpp @@ -0,0 +1,32 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include + +ARDUINOJSON_BEGIN_PUBLIC_NAMESPACE + +namespace DeserializationOption { +class NestingLimit { + public: + NestingLimit() : value_(ARDUINOJSON_DEFAULT_NESTING_LIMIT) {} + explicit NestingLimit(uint8_t n) : value_(n) {} + + NestingLimit decrement() const { + ARDUINOJSON_ASSERT(value_ > 0); + return NestingLimit(static_cast(value_ - 1)); + } + + bool reached() const { + return value_ == 0; + } + + private: + uint8_t value_; +}; +} // namespace DeserializationOption + +ARDUINOJSON_END_PUBLIC_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Reader.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Reader.hpp new file mode 100644 index 00000000..46103122 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Reader.hpp @@ -0,0 +1,74 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include + +#include // for size_t + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +// The default reader is a simple wrapper for Readers that are not copyable +template +struct Reader { + public: + Reader(TSource& source) : source_(&source) {} + + int read() { + // clang-format off + return source_->read(); // Error here? See https://arduinojson.org/v7/invalid-input/ + // clang-format on + } + + size_t readBytes(char* buffer, size_t length) { + return source_->readBytes(buffer, length); + } + + private: + TSource* source_; +}; + +template +struct BoundedReader { + // no default implementation because we need to pass the size to the + // constructor +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE + +#include +#include +#include + +#if ARDUINOJSON_ENABLE_ARDUINO_STREAM +# include +#endif + +#if ARDUINOJSON_ENABLE_ARDUINO_STRING +# include +#endif + +#if ARDUINOJSON_ENABLE_PROGMEM +# include +#endif + +#if ARDUINOJSON_ENABLE_STD_STREAM +# include +#endif + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +template +Reader> makeReader(TInput&& input) { + return Reader>{detail::forward(input)}; +} + +template +BoundedReader makeReader(TChar* input, size_t inputSize) { + return BoundedReader{input, inputSize}; +} + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/ArduinoStreamReader.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/ArduinoStreamReader.hpp new file mode 100644 index 00000000..7c4d6b85 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/ArduinoStreamReader.hpp @@ -0,0 +1,30 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +template +struct Reader::value>> { + public: + explicit Reader(Stream& stream) : stream_(&stream) {} + + int read() { + // don't use stream_->read() as it ignores the timeout + char c; + return stream_->readBytes(&c, 1) ? static_cast(c) : -1; + } + + size_t readBytes(char* buffer, size_t length) { + return stream_->readBytes(buffer, length); + } + + private: + Stream* stream_; +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/ArduinoStringReader.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/ArduinoStringReader.hpp new file mode 100644 index 00000000..ba3d7b76 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/ArduinoStringReader.hpp @@ -0,0 +1,18 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +template +struct Reader::value>> + : BoundedReader { + explicit Reader(const ::String& s) + : BoundedReader(s.c_str(), s.length()) {} +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/FlashReader.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/FlashReader.hpp new file mode 100644 index 00000000..80c0da84 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/FlashReader.hpp @@ -0,0 +1,56 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +template <> +struct Reader { + const char* ptr_; + + public: + explicit Reader(const __FlashStringHelper* ptr) + : ptr_(reinterpret_cast(ptr)) {} + + int read() { + return pgm_read_byte(ptr_++); + } + + size_t readBytes(char* buffer, size_t length) { + memcpy_P(buffer, ptr_, length); + ptr_ += length; + return length; + } +}; + +template <> +struct BoundedReader { + const char* ptr_; + const char* end_; + + public: + explicit BoundedReader(const __FlashStringHelper* ptr, size_t size) + : ptr_(reinterpret_cast(ptr)), end_(ptr_ + size) {} + + int read() { + if (ptr_ < end_) + return pgm_read_byte(ptr_++); + else + return -1; + } + + size_t readBytes(char* buffer, size_t length) { + size_t available = static_cast(end_ - ptr_); + if (available < length) + length = available; + memcpy_P(buffer, ptr_, length); + ptr_ += length; + return length; + } +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/IteratorReader.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/IteratorReader.hpp new file mode 100644 index 00000000..428d3f50 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/IteratorReader.hpp @@ -0,0 +1,42 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +template +class IteratorReader { + TIterator ptr_, end_; + + public: + explicit IteratorReader(TIterator begin, TIterator end) + : ptr_(begin), end_(end) {} + + int read() { + if (ptr_ < end_) + return static_cast(*ptr_++); + else + return -1; + } + + size_t readBytes(char* buffer, size_t length) { + size_t i = 0; + while (i < length && ptr_ < end_) + buffer[i++] = *ptr_++; + return i; + } +}; + +template +struct Reader> + : IteratorReader { + explicit Reader(const TSource& source) + : IteratorReader(source.begin(), + source.end()) {} +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/RamReader.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/RamReader.hpp new file mode 100644 index 00000000..30ddd8f3 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/RamReader.hpp @@ -0,0 +1,49 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +template +struct IsCharOrVoid { + static const bool value = + is_same::value || is_same::value || + is_same::value || is_same::value; +}; + +template +struct IsCharOrVoid : IsCharOrVoid {}; + +template +struct Reader::value>> { + const char* ptr_; + + public: + explicit Reader(const void* ptr) + : ptr_(ptr ? reinterpret_cast(ptr) : "") {} + + int read() { + return static_cast(*ptr_++); + } + + size_t readBytes(char* buffer, size_t length) { + for (size_t i = 0; i < length; i++) + buffer[i] = *ptr_++; + return length; + } +}; + +template +struct BoundedReader::value>> + : public IteratorReader { + public: + explicit BoundedReader(const void* ptr, size_t len) + : IteratorReader(reinterpret_cast(ptr), + reinterpret_cast(ptr) + len) {} +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/StdStreamReader.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/StdStreamReader.hpp new file mode 100644 index 00000000..5de22c01 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/StdStreamReader.hpp @@ -0,0 +1,29 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +template +struct Reader::value>> { + public: + explicit Reader(std::istream& stream) : stream_(&stream) {} + + int read() { + return stream_->get(); + } + + size_t readBytes(char* buffer, size_t length) { + stream_->read(buffer, static_cast(length)); + return static_cast(stream_->gcount()); + } + + private: + std::istream* stream_; +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/VariantReader.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/VariantReader.hpp new file mode 100644 index 00000000..6b915175 --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/Readers/VariantReader.hpp @@ -0,0 +1,19 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +template +struct Reader::value>> + : Reader { + explicit Reader(const TVariant& x) + : Reader(x.template as()) {} +}; + +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/deserialize.hpp b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/deserialize.hpp new file mode 100644 index 00000000..a23e093e --- /dev/null +++ b/variants/sim/thirdparty/arduinojson/ArduinoJson/Deserialization/deserialize.hpp @@ -0,0 +1,79 @@ +// ArduinoJson - https://arduinojson.org +// Copyright © 2014-2026, Benoit BLANCHON +// MIT License + +#pragma once + +#include +#include +#include +#include + +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE + +// A meta-function that returns the first type of the parameter pack +// or void if empty +template +struct first_or_void { + using type = void; +}; +template +struct first_or_void { + using type = T; +}; + +// A meta-function that returns true if T is a valid destination type for +// deserialize() +template +using is_deserialize_destination = + bool_constant>::value || + IsVariant::value>; + +template +inline void shrinkJsonDocument(TDestination&) { + // no-op by default +} + +#if ARDUINOJSON_AUTO_SHRINK +inline void shrinkJsonDocument(JsonDocument& doc) { + doc.shrinkToFit(); +} +#endif + +template