mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
fix(sim): text-width/render bugs, splash version, wasm-fetch error, battery lag
Code-review pass over the buzzer/sim commits turned up several real bugs,
plus two issues found afterward from manual browser testing:
Rendering (SimDisplayDriverCanvas, variants/sim/SimDisplayDriver.h + target.cpp):
- getTextWidth() measured UTF-8 BYTES (strlen()*6), not codepoints. Since
b067e95b stopped stripping accents, any accented string now measures
double its real width -- mis-centred titles, premature ellipsis/marquee,
badges pushed off-screen. Now uses the real MiscFixedRenderer measurement
(miscFixedTextWidth()), same as SH1106Display/SSD1306Display.
- Added the matching getCodepointWidth() override (O(1) single-glyph
advance), same pattern as SSD1306Display.
- isSingleFont() was left at the base class's `false`, though this backend
only ever renders MiscFixed -- UITask.cpp's status-bar indicator height
keys off this (`lh-2` vs `lh`), so the sim drew it 2px taller than a real
board.
- print() blitted the full 128x64 canvas on every call (dozens per frame,
60fps) -- now tracks a dirty bounding box and only clears/blits the
region actually touched.
Web Audio (buzzer bridge, index.html + mesh.html):
- No AudioContext.resume() -- a context created (or later suspended) in the
'suspended' state (Safari/Firefox, or any browser backgrounding the tab)
stayed silent forever. Now resumed on every gesture.
- linearRampToValueAtTime with no anchoring setValueAtTime interpolates
from the LAST scheduled event, not "now" -- so the anti-click ramps could
effectively snap instead of fading. Fixed with cancelScheduledValues +
setValueAtTime(current) before each ramp.
- mesh.html: a gesture only armed the clicked instance's audio. Click A,
send A->B, and B (the one actually meant to beep on receipt) stayed
silent. Now any gesture arms both A and B.
- RTTTL rests (freq=0, still "playing") now explicitly hold pitch and drop
gain instead of it happening to work by coincidence.
Misc: sim_test_get_num_contacts() was missing the g_sim_ready gate every
other sim_test_* hook has, so it could return a bogus negative count before
setup() finishes seeding num_contacts.
Splash screen missing "Solo <version>" bar: variants/sim never defined
FIRMWARE_SOLO_BUILD (every real Solo board does), so SplashScreen silently
skipped that whole line -- the sim looked like a plain non-Solo companion
build. Added -D FIRMWARE_SOLO_BUILD=1 to platformio.ini and build_wasm.sh.
Verified on a real canvas screenshot: "MESHCORE 1.17.1 / 19 Aug 2026 /
Solo v1.27".
Wasm-fetch error message: "failed to start: RuntimeError: Aborted(both
async and sync fetching of the wasm failed)" is Emscripten's own opaque
message for the single most common real cause -- the page opened via
file://...index.html instead of served over http(s) (fetch() on a local
file is blocked by CORS in both Chrome and Safari, confirmed by reproducing
the exact same error/stack via file://). Both harnesses now detect
location.protocol === 'file:' and show an actionable message with the
one-line fix instead of the raw stack trace.
Battery-set latency: SimMainBoard's battery value is an exact, instantaneous
JS-set integer (see sim_battery_set_mv()), but UITask's battery-check code
polls it every 8s and runs it through an EMA (alpha=0.2) meant to smooth a
REAL board's noisy ADC -- so a value typed into the demo UI could take tens
of seconds to visibly settle. SIM_PLATFORM now checks every 250ms and skips
the EMA (nothing to smooth), since the reading is already clean. Measured
on real canvas pixels: indicator update now lands within one screen-refresh
cycle instead of up to 8s+.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+45
-15
@@ -83,17 +83,44 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
class SimGfxCanvas : public Adafruit_GFX {
|
||||
public:
|
||||
uint8_t px[128 * 64];
|
||||
SimGfxCanvas() : Adafruit_GFX(128, 64) { memset(px, 0, sizeof(px)); }
|
||||
// Inclusive bounding box of everything plotted since the last resetDirty().
|
||||
// Without it, print() blitted all 8192 cells on every single call -- and a
|
||||
// busy screen makes dozens of print() calls per frame, at 60fps, so the
|
||||
// JS-side pixel loop dominated the whole frame budget for what is usually
|
||||
// one short row of text.
|
||||
int dx0, dy0, dx1, dy1;
|
||||
SimGfxCanvas() : Adafruit_GFX(128, 64) { memset(px, 0, sizeof(px)); resetDirty(); }
|
||||
void resetDirty() { dx0 = 128; dy0 = 64; dx1 = -1; dy1 = -1; }
|
||||
bool isDirty() const { return dx1 >= dx0 && dy1 >= dy0; }
|
||||
void drawPixel(int16_t x, int16_t y, uint16_t color) override {
|
||||
if ((unsigned)x >= 128 || (unsigned)y >= 64) return;
|
||||
px[y * 128 + x] = (color != 0) ? 1 : 0;
|
||||
if (x < dx0) dx0 = x;
|
||||
if (x > dx1) dx1 = x;
|
||||
if (y < dy0) dy0 = y;
|
||||
if (y > dy1) dy1 = y;
|
||||
}
|
||||
};
|
||||
|
||||
// Real MiscFixed metrics, same source of truth the glyph plotting above
|
||||
// uses -- see SimDisplayDriver.h for why these are here and not inline.
|
||||
uint16_t SimDisplayDriverCanvas::getTextWidth(const char* str) {
|
||||
return str ? miscFixedTextWidth(str, _text_sz) : 0;
|
||||
}
|
||||
|
||||
uint16_t SimDisplayDriverCanvas::getCodepointWidth(uint32_t cp) {
|
||||
return miscFixedXAdvance(cp, _text_sz);
|
||||
}
|
||||
|
||||
void SimDisplayDriverCanvas::print(const char* str) {
|
||||
if (!str) return;
|
||||
static SimGfxCanvas gfx;
|
||||
memset(gfx.px, 0, sizeof(gfx.px));
|
||||
// Only the previously-dirtied region needs clearing, not all 8 KB.
|
||||
if (gfx.isDirty()) {
|
||||
for (int y = gfx.dy0; y <= gfx.dy1; y++)
|
||||
memset(&gfx.px[y * 128 + gfx.dx0], 0, (size_t)(gfx.dx1 - gfx.dx0 + 1));
|
||||
}
|
||||
gfx.resetDirty();
|
||||
gfx.setCursor(_cursor_x, _cursor_y);
|
||||
// color arg is just our own internal "lit" marker (1) -- the real on-screen
|
||||
// amber/black choice is applied once at blit time below, from _color, same
|
||||
@@ -105,20 +132,23 @@ void SimDisplayDriverCanvas::print(const char* str) {
|
||||
|
||||
// startFrame() already blanks the whole canvas to black every frame, so
|
||||
// only the lit pixels need drawing here -- unlit buffer cells are already
|
||||
// correct background. One EM_ASM call blits the whole 128x64 buffer
|
||||
// (reading it directly out of wasm memory, same pattern as drawXbm()
|
||||
// below) rather than one call per glyph pixel.
|
||||
EM_ASM({
|
||||
if (!Module.__simCtx) return;
|
||||
var ctx = Module.__simCtx;
|
||||
var buf = $0;
|
||||
ctx.fillStyle = UTF8ToString($1) === 'L' ? '#ffb000' : '#000';
|
||||
for (var y = 0; y < 64; y++) {
|
||||
for (var x = 0; x < 128; x++) {
|
||||
if (HEAPU8[buf + y * 128 + x]) ctx.fillRect(x, y, 1, 1);
|
||||
// correct background. One EM_ASM call blits the buffer (reading it directly
|
||||
// out of wasm memory, same pattern as drawXbm() below) rather than one call
|
||||
// per glyph pixel, and only over the rows/columns this string actually
|
||||
// touched rather than the full 128x64.
|
||||
if (gfx.isDirty()) {
|
||||
EM_ASM({
|
||||
if (!Module.__simCtx) return;
|
||||
var ctx = Module.__simCtx;
|
||||
var buf = $0;
|
||||
ctx.fillStyle = UTF8ToString($5) === 'L' ? '#ffb000' : '#000';
|
||||
for (var y = $2; y <= $4; y++) {
|
||||
for (var x = $1; x <= $3; x++) {
|
||||
if (HEAPU8[buf + y * 128 + x]) ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, gfx.px, (_color != DARK) ? "L" : "D");
|
||||
}, gfx.px, gfx.dx0, gfx.dy0, gfx.dx1, gfx.dy1, (_color != DARK) ? "L" : "D");
|
||||
}
|
||||
|
||||
// Same external contract as every other DisplayDriver backend here (see
|
||||
// SimDisplayDriver's own ASCII print()): only _cursor_x advances by the
|
||||
|
||||
Reference in New Issue
Block a user