mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
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>
464 lines
21 KiB
HTML
464 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<!--
|
|
Phase 2 minimal proof harness -- NOT the final website embed (that's a
|
|
later, separate design pass). Just enough of a page to manually verify:
|
|
|
|
1. the real boot splash + menu render on the canvas below (same content
|
|
Phase 1 proved renders as ASCII art in a terminal)
|
|
2. clicking the on-page buttons (or using arrow/enter/esc/n/p keys)
|
|
navigates the real menu
|
|
3. after changing something persistent (e.g. Settings > Name, or just
|
|
letting an identity get created on first boot) a REAL browser page
|
|
reload (F5 / Cmd-R -- not just re-running node) shows the same state
|
|
was preserved via IDBFS
|
|
|
|
Serve this directory with any static file server (must be http://, not
|
|
file:// -- the wasm/IndexedDB/module-script machinery needs a real
|
|
origin), e.g. from this directory:
|
|
|
|
python3 -m http.server 8080
|
|
|
|
then open http://localhost:8080/ .
|
|
-->
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>MeshCore companion_radio sim (Phase 2 -- wasm)</title>
|
|
<style>
|
|
body {
|
|
background: #1b1b1b;
|
|
color: #ddd;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 24px;
|
|
}
|
|
h1 { font-size: 16px; font-weight: 600; margin: 0; }
|
|
#sim-canvas {
|
|
background: #000;
|
|
border: 2px solid #444;
|
|
image-rendering: pixelated; /* keep the 128x64 backing store crisp when CSS-upscaled */
|
|
width: 512px;
|
|
height: 256px;
|
|
}
|
|
#status { font-size: 12px; color: #999; min-height: 1.2em; }
|
|
.dpad {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 56px);
|
|
grid-template-rows: repeat(3, 44px);
|
|
gap: 6px;
|
|
}
|
|
.dpad button, .row button {
|
|
background: #2a2a2a;
|
|
color: #eee;
|
|
border: 1px solid #555;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
}
|
|
.dpad button:active, .row button:active { background: #444; }
|
|
.dpad .up { grid-column: 2; grid-row: 1; }
|
|
.dpad .left { grid-column: 1; grid-row: 2; }
|
|
.dpad .sel { grid-column: 2; grid-row: 2; font-weight: 700; }
|
|
.dpad .right { grid-column: 3; grid-row: 2; }
|
|
.dpad .down { grid-column: 2; grid-row: 3; }
|
|
.row { display: flex; gap: 8px; }
|
|
.row button { padding: 8px 14px; }
|
|
.panel {
|
|
width: 512px;
|
|
box-sizing: border-box;
|
|
background: #222;
|
|
border: 1px solid #333;
|
|
border-radius: 6px;
|
|
padding: 8px 10px;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
}
|
|
.panel input {
|
|
background: #111;
|
|
color: #eee;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 4px 6px;
|
|
font-size: 12px;
|
|
}
|
|
.panel input[type=number] { width: 70px; }
|
|
.panel input[type=password] { width: 110px; }
|
|
.panel button {
|
|
background: #2a2a2a;
|
|
color: #eee;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
padding: 5px 10px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
.panel button:active { background: #444; }
|
|
.panel .reset-btn { background: #4a2222; border-color: #733; }
|
|
.panel .reset-btn:active { background: #622; }
|
|
#log {
|
|
width: 512px;
|
|
height: 90px;
|
|
overflow-y: auto;
|
|
background: #111;
|
|
border: 1px solid #333;
|
|
font: 11px/1.4 ui-monospace, monospace;
|
|
color: #7a7;
|
|
padding: 6px 8px;
|
|
white-space: pre-wrap;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>MeshCore companion_radio -- real firmware, in the browser (Phase 2 proof harness)</h1>
|
|
<canvas id="sim-canvas" width="128" height="64"></canvas>
|
|
<div id="status">loading...</div>
|
|
|
|
<div class="dpad">
|
|
<button class="up" data-key="0xB5">↑</button>
|
|
<button class="left" data-key="0xB4">←</button>
|
|
<button class="sel" data-key="13">OK</button>
|
|
<button class="right" data-key="0xB7">→</button>
|
|
<button class="down" data-key="0xB6">↓</button>
|
|
</div>
|
|
<div class="row">
|
|
<button data-key="27">Esc / Cancel</button>
|
|
<button data-key="0xF2">Prev (p)</button>
|
|
<button data-key="0xF1">Next (n)</button>
|
|
<button id="btn-reset" class="reset-btn">Reset</button>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<span>GPS:</span>
|
|
<input id="gps-lat" type="number" step="0.0001" placeholder="lat" value="51.5074">
|
|
<input id="gps-lon" type="number" step="0.0001" placeholder="lon" value="-0.1278">
|
|
<input id="gps-alt" type="number" step="1" placeholder="alt (m)" value="35">
|
|
<button id="btn-gps-set">Set GPS</button>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<span>Sensors:</span>
|
|
<label>Battery (mV) <input id="batt-mv" type="number" step="10" value="4000"></label>
|
|
<button id="btn-batt-set">Set</button>
|
|
<label>Temp (°C) <input id="env-temp" type="number" step="0.5" value="21"></label>
|
|
<button id="btn-temp-set">Set</button>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<span>Admin:</span>
|
|
<button id="btn-admin-open">Open Admin (first repeater)</button>
|
|
<input id="admin-pw" type="password" placeholder="password" value="password">
|
|
<button id="btn-admin-login">Login</button>
|
|
</div>
|
|
|
|
<div id="log"></div>
|
|
|
|
<script src="build/meshcore_sim.js"></script>
|
|
<script>
|
|
const statusEl = document.getElementById('status');
|
|
const logEl = document.getElementById('log');
|
|
function log(msg) {
|
|
const line = document.createElement('div');
|
|
line.textContent = msg;
|
|
logEl.appendChild(line);
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
}
|
|
|
|
// Buttons must never grab real browser keyboard focus -- without this a
|
|
// clicked button (e.g. Reset) stays focused, and a LATER physical
|
|
// Enter/Space keypress meant for the simulated device's own OK button
|
|
// instead re-activates that still-focused HTML button (a real browser
|
|
// default for <button> elements). preventDefault() on mousedown is the
|
|
// standard way to suppress focus-on-click without affecting the
|
|
// click/pointerdown/pointerup handlers below (separate event types).
|
|
document.querySelectorAll('button').forEach((btn) => {
|
|
btn.addEventListener('mousedown', (e) => e.preventDefault());
|
|
});
|
|
|
|
// console.log/.error from inside the wasm module (the sim_fs_mount_idbfs
|
|
// EM_ASM block, IDBFS internals, etc.) are useful to see in-page too,
|
|
// not just devtools -- forward them into #log without disturbing the
|
|
// real console.
|
|
for (const level of ['log', 'error', 'warn']) {
|
|
const orig = console[level].bind(console);
|
|
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 {
|
|
statusEl.textContent = 'booting wasm module...';
|
|
MeshCoreSim().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));
|
|
|
|
// A real MomentaryButton(pin, 1000, ...) fires either a short click OR
|
|
// a long-press event for one physical press, never both -- e.g.
|
|
// holding OK/Enter opens the real context menu (see
|
|
// UITask::handleLongPress() mapping KEY_ENTER -> KEY_CONTEXT_MENU)
|
|
// instead of the normal short-press action. LONG_PRESS_MS mirrors
|
|
// that same 1000ms threshold every real board's MomentaryButton uses.
|
|
const LONG_PRESS_MS = 1000;
|
|
|
|
function sendKey(code) {
|
|
if (Module && Module._sim_enqueue_key) Module._sim_enqueue_key(code);
|
|
}
|
|
function sendKeyLongPress(code) {
|
|
if (Module && Module._sim_enqueue_key_longpress) Module._sim_enqueue_key_longpress(code);
|
|
}
|
|
|
|
// Buzzer: a single Web Audio oscillator+gain standing in for the real
|
|
// piezo buzzer, polled against sim_buzzer_is_playing()/
|
|
// sim_buzzer_freq_hz() (see genericBuzzer's own #ifdef SIM_PLATFORM
|
|
// branch, src/helpers/ui/buzzer.cpp) -- those already track exactly
|
|
// which frequency (0 = silent) should be sounding right now, driven
|
|
// by the real RTTTL playback the notification/ringtone/startup code
|
|
// triggers unchanged. AudioContext can't be created before a real
|
|
// user gesture (browser autoplay policy), so this is created lazily
|
|
// on first pointerdown/keydown -- the boot startup jingle will
|
|
// already be over by the time that first gesture happens, same
|
|
// unavoidable limitation any embedded page with boot sound has.
|
|
let audioCtx = null, buzzOsc = null, buzzGain = null, buzzWasPlaying = false;
|
|
function ensureAudio() {
|
|
// 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;
|
|
buzzOsc.type = 'square'; // closer to a piezo buzzer's timbre than a sine
|
|
buzzOsc.frequency.value = 440;
|
|
buzzOsc.connect(buzzGain).connect(audioCtx.destination);
|
|
buzzOsc.start();
|
|
}
|
|
// Exposed for console poking / Playwright, same as window.Module above.
|
|
window.__buzzAudio = () => (audioCtx
|
|
? { state: audioCtx.state, gain: buzzGain.gain.value, freq: buzzOsc.frequency.value }
|
|
: null);
|
|
// Volume steps roughly mirror genericBuzzer's own real-hardware duty-
|
|
// cycle table (buzzer.cpp's PCT[]/duty[] -- ~6-8 dB per step); 0 is
|
|
// 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;
|
|
const now = audioCtx.currentTime;
|
|
if (playing) {
|
|
const freq = Module.ccall('sim_buzzer_freq_hz', 'number', [], []);
|
|
const vol = Module.ccall('sim_buzzer_get_volume', 'number', [], []);
|
|
// 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) {
|
|
rampGain(0, 0.01, now);
|
|
}
|
|
buzzWasPlaying = playing;
|
|
}
|
|
setInterval(pollBuzzer, 20);
|
|
|
|
// Press-and-hold for the on-page buttons: start a timer on press-down,
|
|
// fire the long-press call if still held past the threshold, and
|
|
// suppress the short click on release if it already fired.
|
|
document.querySelectorAll('button[data-key]').forEach((btn) => {
|
|
const code = Number(btn.dataset.key);
|
|
let timer = null, longFired = false;
|
|
btn.addEventListener('pointerdown', (e) => {
|
|
e.preventDefault();
|
|
ensureAudio();
|
|
longFired = false;
|
|
timer = setTimeout(() => { longFired = true; sendKeyLongPress(code); }, LONG_PRESS_MS);
|
|
});
|
|
btn.addEventListener('pointerup', () => {
|
|
if (timer) { clearTimeout(timer); timer = null; }
|
|
if (!longFired) sendKey(code);
|
|
});
|
|
const cancel = () => { if (timer) { clearTimeout(timer); timer = null; } };
|
|
btn.addEventListener('pointerleave', cancel);
|
|
btn.addEventListener('pointercancel', cancel);
|
|
});
|
|
|
|
// Keyboard passthrough: arrows/Enter/Escape/n/p, same mapping
|
|
// UITask.cpp's native SIM_PLATFORM stdin branch already uses, PLUS
|
|
// full printable-ASCII passthrough (letters/digits/punctuation) and
|
|
// a real Backspace -- this is what makes typing a node name, a chat
|
|
// message, or an admin password with a real keyboard work at all.
|
|
// KeyboardWidget.h's own comment on its "direct-typing passthrough"
|
|
// branch (0x20-0x7E insert, 0x08 backspace, KEY_KB_ENTER submits)
|
|
// confirms real hardware already accepts exactly these raw byte
|
|
// values from a literal-ASCII input source (its own example is
|
|
// CardKB) -- so this is that same input source, just fed from a
|
|
// real keyboard's keydown events instead of an I2C CardKB
|
|
// peripheral. Tab -> KEY_KB_ENTER ("submit the field") rather than
|
|
// Enter itself: Enter must keep meaning KEY_ENTER (13, ordinary
|
|
// menu-select) everywhere else in the UI, and Tab has no other
|
|
// meaning here to collide with. Backspace no longer doubles as
|
|
// Cancel (it did before this change) -- Escape already covers
|
|
// Cancel identically, and a working Backspace-while-typing matters
|
|
// more. WASD and space-as-Enter (both present before this change)
|
|
// are gone too, for the same reason -- 'w'/'a'/'s'/'d'/' ' must now
|
|
// type as themselves. Arrow keys still work exactly as before.
|
|
// Same press-and-hold logic as the buttons above, keyed by
|
|
// e.key so multiple keys can be held independently; the
|
|
// `keyHold.has()` guard ignores the browser's own keydown
|
|
// auto-repeat (which would otherwise restart the long-press timer
|
|
// on every repeat tick).
|
|
// NOTE: no 'n'/'p' Next/Prev shortcuts here (there were in earlier
|
|
// phases, before full ASCII passthrough existed) -- they collided
|
|
// with typing the literal letters 'n'/'p' into any text field. The
|
|
// on-screen Next(n)/Prev(p) buttons below still work via click.
|
|
const KEYMAP = {
|
|
ArrowUp: 0xB5, ArrowDown: 0xB6, ArrowLeft: 0xB4, ArrowRight: 0xB7,
|
|
Enter: 13,
|
|
Escape: 27,
|
|
Backspace: 0x08,
|
|
Tab: 0x05, // KEY_KB_ENTER -- submit the current text field
|
|
};
|
|
// Printable ASCII passthrough (KeyboardWidget.h's 0x20-0x7E direct-
|
|
// insert range) -- covers every letter/digit/punctuation key not
|
|
// already claimed by KEYMAP above. A single-character e.key IS the
|
|
// literal typed character already (browsers report the
|
|
// shift-applied glyph, e.g. 'A' or '!', not a raw scancode), so no
|
|
// separate shift-handling is needed here.
|
|
function resolveKey(e) {
|
|
if (e.key in KEYMAP) return KEYMAP[e.key];
|
|
if (e.key.length === 1) {
|
|
const c = e.key.charCodeAt(0);
|
|
if (c >= 0x20 && c <= 0x7E) return c;
|
|
}
|
|
return null;
|
|
}
|
|
// Don't hijack typing into the GPS/sensor/admin-password <input>
|
|
// fields below -- those are native HTML inputs the browser already
|
|
// handles; only forward keystrokes to the simulated device when
|
|
// focus is elsewhere (the canvas, the body, a button).
|
|
function focusedOnRealInput() {
|
|
const el = document.activeElement;
|
|
return el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
|
|
}
|
|
const keyHold = new Map();
|
|
window.addEventListener('keydown', (e) => {
|
|
if (focusedOnRealInput()) return;
|
|
const code = resolveKey(e);
|
|
if (code === null) return;
|
|
e.preventDefault();
|
|
ensureAudio();
|
|
if (keyHold.has(e.key)) return; // auto-repeat, not a new press
|
|
const state = { longFired: false };
|
|
state.timer = setTimeout(() => { state.longFired = true; sendKeyLongPress(code); }, LONG_PRESS_MS);
|
|
keyHold.set(e.key, state);
|
|
});
|
|
window.addEventListener('keyup', (e) => {
|
|
if (focusedOnRealInput()) return;
|
|
const state = keyHold.get(e.key);
|
|
if (!state) return;
|
|
clearTimeout(state.timer);
|
|
if (!state.longFired) sendKey(resolveKey(e));
|
|
keyHold.delete(e.key);
|
|
});
|
|
|
|
// Reset: a real reset button re-runs setup() from scratch on real
|
|
// hardware -- under Emscripten, board.reboot() (SimMainBoard.h)
|
|
// just exit(0)s, which is inert with -sEXIT_RUNTIME=0 (freezes the
|
|
// tab instead of resetting anything), so this is a real browser
|
|
// page reload instead. Identity/prefs already persist across this
|
|
// via IDBFS (see this file's own header comment) -- same proof
|
|
// Phase 2 established.
|
|
document.getElementById('btn-reset').addEventListener('click', () => {
|
|
location.reload();
|
|
});
|
|
|
|
// GPS: wires up the already-exported sim_location_set() (see
|
|
// SimLocationProvider.h) -- now that SimSensorManager actually
|
|
// plugs a LocationProvider into getLocationProvider(), the
|
|
// on-device Compass/Nearby screens will show a real fix after this.
|
|
document.getElementById('btn-gps-set').addEventListener('click', () => {
|
|
const lat = parseFloat(document.getElementById('gps-lat').value) || 0;
|
|
const lon = parseFloat(document.getElementById('gps-lon').value) || 0;
|
|
const alt = parseFloat(document.getElementById('gps-alt').value) || 0;
|
|
Module.ccall('sim_location_set', null, ['number','number','number'], [lat, lon, alt]);
|
|
log(`GPS set: ${lat}, ${lon}, ${alt}m`);
|
|
});
|
|
|
|
// Sensors: battery (SimMainBoard.h) and one representative
|
|
// environment channel (SimSensorManager.h) -- both JS-settable the
|
|
// same way sim_location_set() already was.
|
|
document.getElementById('btn-batt-set').addEventListener('click', () => {
|
|
const mv = parseInt(document.getElementById('batt-mv').value, 10) || 0;
|
|
Module.ccall('sim_battery_set_mv', null, ['number'], [mv]);
|
|
log(`battery set: ${mv}mV`);
|
|
});
|
|
document.getElementById('btn-temp-set').addEventListener('click', () => {
|
|
const c = parseFloat(document.getElementById('env-temp').value) || 0;
|
|
Module.ccall('sim_env_temperature_set', null, ['number'], [c]);
|
|
log(`env temperature set: ${c}C`);
|
|
});
|
|
|
|
// Admin: open the real AdminScreen for the first known repeater/
|
|
// room contact, then submit a login via the real sendRoomLogin()
|
|
// (see the sim_test_open_admin_with_first_repeater()/
|
|
// sim_test_login_first_repeater() hooks in
|
|
// examples/companion_radio/main.cpp) -- the sim's repeater/room
|
|
// server default to ADMIN_PASSWORD "password" (never overridden by
|
|
// variants/sim/), so that's this field's default value too.
|
|
document.getElementById('btn-admin-open').addEventListener('click', () => {
|
|
const ok = Module.ccall('sim_test_open_admin_with_first_repeater', 'number', [], []);
|
|
log(ok ? 'opened AdminScreen for first repeater/room contact' : 'no repeater/room contact known yet');
|
|
});
|
|
document.getElementById('btn-admin-login').addEventListener('click', () => {
|
|
const pw = document.getElementById('admin-pw').value;
|
|
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(reportBootFailure);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|