mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(sim): e-ink simulator builds
SIM_EINK=landscape|portrait builds the e-ink firmware for the browser, drawn by the same EinkGfxDisplay as the panel. index.html and mesh.html pick a build from the URL. The simulated sensors get a GPS switch, so the UI's GPS toggle works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -462,6 +462,32 @@ public:
|
||||
void endFrame() override { }
|
||||
};
|
||||
|
||||
#ifdef SIM_EINK_PANEL_W
|
||||
// An e-ink panel (the Wio Tracker L1 E-ink's 2.13", 122x250 before rotation):
|
||||
// EinkGfxDisplay draws, exactly as GxEPDDisplay does on the board, into a 1-bit
|
||||
// canvas, and a frame that changed is put on the page in paper and ink. The
|
||||
// host page counts refreshes from Module.__einkRefreshes. Bodies in target.cpp,
|
||||
// the one TU that pulls in the font tables.
|
||||
#include <helpers/ui/EinkGfxDisplay.h>
|
||||
#ifndef DISPLAY_ROTATION
|
||||
#define DISPLAY_ROTATION 0
|
||||
#endif
|
||||
class SimEinkDisplay : public EinkGfxDisplay {
|
||||
bool _on = false;
|
||||
static Adafruit_GFX& canvas();
|
||||
public:
|
||||
static const int PANEL_W = SIM_EINK_PANEL_W, PANEL_H = SIM_EINK_PANEL_H;
|
||||
SimEinkDisplay();
|
||||
bool begin();
|
||||
bool isOn() override { return _on; }
|
||||
void turnOn() override { _on = true; }
|
||||
void turnOff() override { _on = false; }
|
||||
void clear() override;
|
||||
void setDisplayRotation(uint8_t rot) override;
|
||||
void endFrame() override;
|
||||
};
|
||||
#endif
|
||||
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE inline void sim_lcd_touch(int x, int y, int down) {
|
||||
SimLcdDisplay::Touch& t = SimLcdDisplay::touchState();
|
||||
t.x = x; t.y = y; t.down = down != 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <string.h>
|
||||
|
||||
#include <helpers/SensorManager.h>
|
||||
|
||||
@@ -35,11 +36,21 @@
|
||||
// type turns out to matter for a demo.
|
||||
class SimSensorManager : public SensorManager {
|
||||
static float& envTemperatureRef() { static float t = 21.0f; return t; }
|
||||
bool _gps = false; // the "gps" setting, as a real board's GPS switch: the UI's toggle needs it
|
||||
|
||||
public:
|
||||
LocationProvider* getLocationProvider() override;
|
||||
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
|
||||
|
||||
int getNumSettings() const override { return 1; }
|
||||
const char* getSettingName(int i) const override { return i == 0 ? "gps" : NULL; }
|
||||
const char* getSettingValue(int i) const override { return i == 0 ? (_gps ? "1" : "0") : NULL; }
|
||||
bool setSettingValue(const char* name, const char* value) override {
|
||||
if (strcmp(name, "gps") != 0) return false;
|
||||
_gps = strcmp(value, "0") != 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void setEnvTemperature(float celsius) { envTemperatureRef() = celsius; }
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
# Usage:
|
||||
# variants/sim/build_wasm.sh # release-ish build (-O2)
|
||||
# variants/sim/build_wasm.sh debug # -O0 -g, easier to debug in devtools
|
||||
# SIM_UI=lvgl variants/sim/build_wasm.sh # the L2 (web/lvgl.html)
|
||||
# SIM_EINK=landscape variants/sim/build_wasm.sh # the L1 E-ink, also portrait
|
||||
#
|
||||
# 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).
|
||||
@@ -192,6 +194,28 @@ if [ "$SIM_UI" = "lvgl" ]; then
|
||||
LINK_EXTRA=(-sFETCH=1) # map downloads (LvglPort.h) go through emscripten_fetch
|
||||
fi
|
||||
|
||||
# SIM_EINK=landscape|portrait: ui-new on the Wio Tracker L1 E-ink's 2.13" panel
|
||||
# (122x250, solo/wio-tracker-l1-eink's two builds) instead of the OLED ->
|
||||
# web/index.html?build=meshcore_sim_eink_landscape (or _portrait).
|
||||
SIM_EINK="${SIM_EINK:-}"
|
||||
if [ -n "$SIM_EINK" ]; then
|
||||
case "$SIM_EINK" in
|
||||
landscape) EINK_DEFS=(-DDISPLAY_ROTATION=1 -DEINK_LARGE_FONT=1) ;;
|
||||
portrait) EINK_DEFS=(-DDISPLAY_ROTATION=0) ;;
|
||||
*) echo "error: SIM_EINK must be landscape or portrait" >&2; exit 1 ;;
|
||||
esac
|
||||
OUT_NAME="meshcore_sim_eink_$SIM_EINK"
|
||||
EXPORT_NAME=MeshCoreSimEink # distinct factory: web/mesh.html?a=... loads it beside MeshCoreSim
|
||||
kept=()
|
||||
for d in "${DEFINES[@]}"; do
|
||||
case "$d" in -DDISPLAY_CLASS=*|-DMAX_GROUP_CHANNELS=*) ;; *) kept+=("$d") ;; esac
|
||||
done
|
||||
# EINK_DISPLAY_MODEL is only tested for presence outside GxEPDDisplay
|
||||
# (Features.h: the e-ink settings and timings), which the sim doesn't build.
|
||||
DEFINES=("${kept[@]}" -DDISPLAY_CLASS=SimEinkDisplay -DSIM_EINK_PANEL_W=122 -DSIM_EINK_PANEL_H=250
|
||||
-DEINK_DISPLAY_MODEL=SimEinkPanel -DMAX_GROUP_CHANNELS=40 "${EINK_DEFS[@]}")
|
||||
fi
|
||||
|
||||
# -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
|
||||
|
||||
@@ -157,4 +157,82 @@ void SimDisplayDriverCanvas::print(const char* str) {
|
||||
// real y position across embedded '\n's internally.
|
||||
_cursor_x += getTextWidth(str);
|
||||
}
|
||||
|
||||
#ifdef SIM_EINK_PANEL_W
|
||||
#include <helpers/ui/EinkGfxDisplayImpl.h>
|
||||
|
||||
// The panel's RAM: 1 bit a pixel, ink = 1. GFXcanvas1 rotates like GxEPD2 does.
|
||||
Adafruit_GFX& SimEinkDisplay::canvas() {
|
||||
static GFXcanvas1 c(PANEL_W, PANEL_H);
|
||||
return c;
|
||||
}
|
||||
|
||||
SimEinkDisplay::SimEinkDisplay()
|
||||
: EinkGfxDisplay((DISPLAY_ROTATION & 1) ? PANEL_H : PANEL_W,
|
||||
(DISPLAY_ROTATION & 1) ? PANEL_W : PANEL_H, canvas(), 1, 0) {
|
||||
canvas().setRotation(DISPLAY_ROTATION);
|
||||
}
|
||||
|
||||
// Sizes the page's canvas to the panel as it's turned, at Module.simEinkScale
|
||||
// (default 3) CSS pixels a pixel.
|
||||
static void simEinkFitCanvas(int w, int h) {
|
||||
EM_ASM({
|
||||
var tag = (typeof Module !== 'undefined' && Module['simInstanceTag']) ? Module['simInstanceTag'] : '';
|
||||
var c = document.getElementById(tag ? ('sim-canvas-' + tag) : 'sim-canvas');
|
||||
if (!c) { console.error('[sim] e-ink canvas not found in the host page'); return; }
|
||||
var k = Module.simEinkScale || 3;
|
||||
c.width = $0; c.height = $1;
|
||||
c.style.width = ($0 * k) + 'px'; c.style.height = ($1 * k) + 'px';
|
||||
Module.__simCtx = c.getContext('2d');
|
||||
Module.__simCtx.imageSmoothingEnabled = false;
|
||||
Module.__einkRefreshes = Module.__einkRefreshes || 0;
|
||||
}, w, h);
|
||||
}
|
||||
|
||||
bool SimEinkDisplay::begin() {
|
||||
_on = true;
|
||||
simEinkFitCanvas(width(), height());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SimEinkDisplay::clear() {
|
||||
canvas().fillScreen(0);
|
||||
forceRedraw();
|
||||
}
|
||||
|
||||
void SimEinkDisplay::setDisplayRotation(uint8_t rot) {
|
||||
canvas().setRotation(rot & 3);
|
||||
setDimensions(canvas().width(), canvas().height());
|
||||
updateLayout();
|
||||
simEinkFitCanvas(width(), height());
|
||||
}
|
||||
|
||||
// Only a frame that differs reaches the page, as only that one costs the
|
||||
// board a refresh. Read back through getPixel(), which applies the rotation.
|
||||
void SimEinkDisplay::endFrame() {
|
||||
if (!frameChanged()) return;
|
||||
const int w = width(), h = height();
|
||||
static uint8_t px[SIM_EINK_PANEL_W * SIM_EINK_PANEL_H];
|
||||
const GFXcanvas1& c = (const GFXcanvas1&)canvas();
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++) px[y * w + x] = c.getPixel(x, y);
|
||||
EM_ASM({
|
||||
if (!Module.__simCtx) return;
|
||||
var w = $1;
|
||||
var h = $2;
|
||||
var img = Module.__simCtx.createImageData(w, h);
|
||||
var d = img.data;
|
||||
for (var i = 0; i < w * h; i++) {
|
||||
var ink = HEAPU8[$0 + i];
|
||||
var o = i * 4;
|
||||
d[o] = ink ? 0x1a : 0xdc;
|
||||
d[o + 1] = ink ? 0x1a : 0xdc;
|
||||
d[o + 2] = ink ? 0x1a : 0xd2;
|
||||
d[o + 3] = 255;
|
||||
}
|
||||
Module.__simCtx.putImageData(img, 0, 0);
|
||||
Module.__einkRefreshes++;
|
||||
}, px, w, h);
|
||||
}
|
||||
#endif // SIM_EINK_PANEL_W
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
@@ -158,7 +158,13 @@
|
||||
|
||||
<div id="log"></div>
|
||||
|
||||
<script src="build/meshcore_sim.js"></script>
|
||||
<!-- ?build=meshcore_sim_eink_landscape (or _portrait) loads another build of
|
||||
the same UI; the e-ink driver sizes the canvas itself. -->
|
||||
<script>
|
||||
document.write('<script src="build/' +
|
||||
(new URLSearchParams(location.search).get('build') || 'meshcore_sim').replace(/[^\w]/g, '') +
|
||||
'.js"><\/script>');
|
||||
</script>
|
||||
<script>
|
||||
const statusEl = document.getElementById('status');
|
||||
const logEl = document.getElementById('log');
|
||||
@@ -209,11 +215,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof MeshCoreSim !== 'function') {
|
||||
const SimFactory = window.MeshCoreSimEink || window.MeshCoreSim;
|
||||
if (typeof SimFactory !== 'function') {
|
||||
statusEl.textContent = 'error: build/meshcore_sim.js missing or failed to load -- run variants/sim/build_wasm.sh first.';
|
||||
} else {
|
||||
statusEl.textContent = 'booting wasm module...';
|
||||
MeshCoreSim().then((Module) => {
|
||||
SimFactory().then((Module) => {
|
||||
window.Module = Module; // exposed for console poking while testing
|
||||
statusEl.textContent = 'running -- IDBFS-backed identity/prefs persist across a real reload.';
|
||||
log('module ready; sim_enqueue_key = ' + (typeof Module._sim_enqueue_key));
|
||||
|
||||
@@ -231,8 +231,14 @@
|
||||
|
||||
<script src="build/meshcore_sim.js"></script>
|
||||
<script src="build/repeater/meshcore_sim_repeater.js"></script>
|
||||
<!-- ?a=meshcore_sim_eink_landscape (or _portrait): A is that e-ink build. -->
|
||||
<script>
|
||||
const A_BUILD = (new URLSearchParams(location.search).get('a') || '').replace(/[^\w]/g, '');
|
||||
if (A_BUILD) document.write('<script src="build/' + A_BUILD + '.js"><\/script>');
|
||||
</script>
|
||||
<script>
|
||||
const logEl = document.getElementById('log');
|
||||
const FactoryA = (opts) => (A_BUILD ? MeshCoreSimEink : MeshCoreSim)(opts);
|
||||
function log(msg) {
|
||||
const line = document.createElement('div');
|
||||
line.textContent = '[' + new Date().toISOString().substr(11, 12) + '] ' + msg;
|
||||
@@ -384,7 +390,7 @@
|
||||
async function resetInstance(key) {
|
||||
log(`resetting instance ${key}...`);
|
||||
document.getElementById('status-' + key).textContent = 'resetting...';
|
||||
if (key === 'A') A = wrapInstance(await MeshCoreSim({ simInstanceTag: 'A' }), 'A');
|
||||
if (key === 'A') A = wrapInstance(await FactoryA({ simInstanceTag: 'A' }), 'A');
|
||||
else if (key === 'B') B = wrapInstance(await MeshCoreSim({ simInstanceTag: 'B' }), 'B');
|
||||
else if (key === 'R') R = wrapInstance(await MeshCoreSimRepeater({ simInstanceTag: 'R' }), 'R');
|
||||
await waitReady(key === 'A' ? A : key === 'B' ? B : R, 'status-' + key);
|
||||
@@ -394,7 +400,7 @@
|
||||
|
||||
async function main() {
|
||||
log('booting instance A (companion_radio)...');
|
||||
const modA = await MeshCoreSim({ simInstanceTag: 'A' });
|
||||
const modA = await FactoryA({ simInstanceTag: 'A' });
|
||||
log('booting instance B (companion_radio)...');
|
||||
const modB = await MeshCoreSim({ simInstanceTag: 'B' });
|
||||
log('booting instance R (simple_repeater)...');
|
||||
|
||||
Reference in New Issue
Block a user