mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-27 21:26:37 +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:
@@ -189,6 +189,26 @@
|
||||
console[level] = (...args) => { orig(...args); log('[js] ' + args.join(' ')); };
|
||||
}
|
||||
|
||||
// Emscripten's own "both async and sync fetching of the wasm failed" is
|
||||
// near-unreadable to anyone who hasn't debugged this exact runtime
|
||||
// before. Its single most common real-world cause: the page was opened
|
||||
// directly (double-clicked, file://...index.html) instead of served
|
||||
// over http -- `fetch()` on a local file is blocked by CORS in both
|
||||
// Chrome and Safari, with no server-side fix possible (it's the browser
|
||||
// refusing the request, not a missing/misnamed file). Detect that
|
||||
// specific case and say so plainly; anything else, show the raw error
|
||||
// so it's at least visible instead of silently swallowed.
|
||||
function reportBootFailure(err) {
|
||||
console.error(err);
|
||||
if (location.protocol === 'file:') {
|
||||
statusEl.textContent = 'error: this page must be served over http(s), not opened as a file:// URL '
|
||||
+ '(the browser blocks the wasm fetch either way). Run e.g. '
|
||||
+ '"cd variants/sim/web && python3 -m http.server 8080" and open http://localhost:8080/index.html instead.';
|
||||
} else {
|
||||
statusEl.textContent = 'error: failed to start the wasm module -- ' + err + ' (see devtools console for detail).';
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof MeshCoreSim !== 'function') {
|
||||
statusEl.textContent = 'error: build/meshcore_sim.js missing or failed to load -- run variants/sim/build_wasm.sh first.';
|
||||
} else {
|
||||
@@ -226,8 +246,17 @@
|
||||
// unavoidable limitation any embedded page with boot sound has.
|
||||
let audioCtx = null, buzzOsc = null, buzzGain = null, buzzWasPlaying = false;
|
||||
function ensureAudio() {
|
||||
if (audioCtx) return;
|
||||
// Not just "create once": a context can also be created in, or later
|
||||
// fall back to, the 'suspended' state (Safari/Firefox start it
|
||||
// suspended even inside a gesture handler; any browser may suspend
|
||||
// it again when the tab is backgrounded). Without resuming it here
|
||||
// on every gesture, the oscillator keeps running silently forever.
|
||||
if (audioCtx) {
|
||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
return;
|
||||
}
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
buzzOsc = audioCtx.createOscillator();
|
||||
buzzGain = audioCtx.createGain();
|
||||
buzzGain.gain.value = 0;
|
||||
@@ -245,6 +274,17 @@
|
||||
// silence regardless of what's "playing" (mutes without touching the
|
||||
// sim's own note-advance timing).
|
||||
const BUZZER_GAIN = [0, 0.02, 0.05, 0.09, 0.15];
|
||||
// A linear ramp interpolates from the PREVIOUS automation event, not
|
||||
// from "now" -- with no event anchoring the curve's start, a ramp
|
||||
// scheduled long after the last one effectively snaps to its target
|
||||
// instead of easing over the requested few ms, which is exactly the
|
||||
// click the ramp was meant to avoid. Anchoring at the current value
|
||||
// first makes the short fade real.
|
||||
function rampGain(target, seconds, now) {
|
||||
buzzGain.gain.cancelScheduledValues(now);
|
||||
buzzGain.gain.setValueAtTime(buzzGain.gain.value, now);
|
||||
buzzGain.gain.linearRampToValueAtTime(target, now + seconds);
|
||||
}
|
||||
function pollBuzzer() {
|
||||
if (!audioCtx || !Module || !Module._sim_buzzer_is_playing) return;
|
||||
const playing = Module.ccall('sim_buzzer_is_playing', 'number', [], []) === 1;
|
||||
@@ -252,10 +292,12 @@
|
||||
if (playing) {
|
||||
const freq = Module.ccall('sim_buzzer_freq_hz', 'number', [], []);
|
||||
const vol = Module.ccall('sim_buzzer_get_volume', 'number', [], []);
|
||||
buzzOsc.frequency.setValueAtTime(freq > 0 ? freq : 440, now);
|
||||
buzzGain.gain.linearRampToValueAtTime(freq > 0 ? BUZZER_GAIN[vol] || 0 : 0, now + 0.005);
|
||||
// freq 0 is a real RTTTL rest ('p') -- still "playing", just
|
||||
// silent, so hold the last pitch and drop the gain instead.
|
||||
if (freq > 0) buzzOsc.frequency.setValueAtTime(freq, now);
|
||||
rampGain(freq > 0 ? (BUZZER_GAIN[vol] || 0) : 0, 0.005, now);
|
||||
} else if (buzzWasPlaying) {
|
||||
buzzGain.gain.linearRampToValueAtTime(0, now + 0.01);
|
||||
rampGain(0, 0.01, now);
|
||||
}
|
||||
buzzWasPlaying = playing;
|
||||
}
|
||||
@@ -414,10 +456,7 @@
|
||||
const result = Module.ccall('sim_test_login_first_repeater', 'number', ['string'], [pw]);
|
||||
log(`login request result=${result} (1=sent,0=send failed,-1=no contact yet; actual accept/reject is async -- check the Admin screen)`);
|
||||
});
|
||||
}).catch((err) => {
|
||||
statusEl.textContent = 'failed to start: ' + err;
|
||||
console.error(err);
|
||||
});
|
||||
}).catch(reportBootFailure);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user