From d64b67960481b015d1f19584101928774c0858d3 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sat, 22 Aug 2026 07:18:01 +0200 Subject: [PATCH] feat(firmware): say why an uploaded binary cannot find any network (#270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Visuino-built `Generated.ino.merged.bin` was reported as failing to run in Velxio. It runs fine. The screenshot shows the ESP32 bootloader completing and the sketch executing; what it also shows is [E][STA.cpp:357] connect(): STA connect failed! 0x300a: ESP_ERR_WIFI_SSID [W][WiFiGeneric.cpp:900] setTxPower(): Neither AP or STA has been started which is WiFi association, not firmware loading — and reads as "the emulator is broken" to someone who has no reason to know the difference. The emulated radio broadcasts four networks and nothing else: Velxio-GUEST, PICSimLabWifi, Espressif, MasseyWifi, all open. Sketches compiled here never have to know, because espidf_compiler rewrites their SSID literal on the way to the emulator. Firmware that arrives already built skips that step by construction — there is no compile to rewrite anything — so it hunts for the network it was built for and never finds it. Confirmed on the real emulator rather than reasoned about: the same sketch built with SSID "MyHomeNet" reports FAILED, and with "Velxio-GUEST" connects on 192.168.4.x. Same board, same path, only the string differs. So the upload path now reads the image and, when it carries the Arduino WiFi stack but names none of the reachable networks, prints one line saying which networks exist and why a compiled-elsewhere binary cannot be fixed up automatically. When the image already names one it says nothing: a note nobody needs is noise, and noise is how people learn to ignore the console. The markers are deliberately narrow for the same reason — `esp_wifi` alone appears in images that never touch the radio. The SSID list moves to EMULATED_WIFI_SSIDS in types/board.ts. It was written by hand inside the MicroPython prelude string, and a second hand-written copy is precisely what #268 was made of. Not closed by this: the reporter's exact error is ESP_ERR_WIFI_SSID from esp_wifi_connect, meaning the SSID handed to the stack was invalid rather than merely absent from the air — forcing an empty SSID here produces a different message from a different line (STA.cpp:387, "SSID too long or missing!"). That may be his Visuino configuration or his core version; it needs his binary to tell, which he offered to send. --- .../issue-270-firmware-wifi-note.test.ts | 62 +++++++++++++++++++ .../src/components/editor/EditorToolbar.tsx | 11 ++++ frontend/src/store/useSimulatorStore.ts | 4 +- frontend/src/types/board.ts | 18 ++++++ frontend/src/utils/firmwareWifiNote.ts | 54 ++++++++++++++++ 5 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 frontend/src/__tests__/issue-270-firmware-wifi-note.test.ts create mode 100644 frontend/src/utils/firmwareWifiNote.ts diff --git a/frontend/src/__tests__/issue-270-firmware-wifi-note.test.ts b/frontend/src/__tests__/issue-270-firmware-wifi-note.test.ts new file mode 100644 index 00000000..49cd711f --- /dev/null +++ b/frontend/src/__tests__/issue-270-firmware-wifi-note.test.ts @@ -0,0 +1,62 @@ +// @vitest-environment jsdom +/** + * Issue #270 — a Visuino-built .bin boots and runs in Velxio and then reports + * `STA connect failed! 0x300a: ESP_ERR_WIFI_SSID`, which reads as the emulator + * being broken. It is not: the emulated radio broadcasts four networks and + * nothing else, and a binary built outside Velxio keeps whatever SSID it was + * built with, because the rewrite that saves compiled sketches happens in the + * compiler this file never went through. + * + * Verified against the real emulator while writing this: the same sketch built + * with SSID "MyHomeNet" fails to associate, and with "Velxio-GUEST" connects + * on 192.168.4.x. + */ +import { describe, it, expect } from 'vitest'; +import { wifiSsidNoteFor } from '../utils/firmwareWifiNote'; +import { EMULATED_WIFI_SSIDS } from '../types/board'; + +/** A fake image: some binary noise with the given ASCII strings buried in it. */ +function image(strings: string[]): File { + const parts: number[] = []; + for (let i = 0; i < 512; i++) parts.push(i & 0xff); + for (const s of strings) { + for (let i = 0; i < s.length; i++) parts.push(s.charCodeAt(i)); + parts.push(0); + for (let i = 0; i < 64; i++) parts.push(0xa5); + } + return new File([new Uint8Array(parts)], 'Generated.ino.merged.bin'); +} + +describe('issue #270 — uploaded firmware that cannot reach any network', () => { + it('warns when the image uses WiFi and names no reachable network', async () => { + const note = await wifiSsidNoteFor(image(['WiFiGeneric', 'MyHomeNet'])); + expect(note).toBeTruthy(); + for (const ssid of EMULATED_WIFI_SSIDS) expect(note).toContain(ssid); + }); + + it('stays quiet when the image already names one', async () => { + for (const ssid of EMULATED_WIFI_SSIDS) { + expect(await wifiSsidNoteFor(image(['WiFiGeneric', ssid]))).toBeNull(); + } + }); + + it('stays quiet for firmware that does not use WiFi', async () => { + // A note nobody needs is noise, and noise is how people learn to ignore + // the console. + expect(await wifiSsidNoteFor(image(['blink', 'digitalWrite']))).toBeNull(); + }); + + it('recognises the WiFi stack by more than one marker', async () => { + for (const marker of ['WiFiGeneric', 'STA.cpp', 'esp_wifi_connect', 'wifi_station']) { + expect(await wifiSsidNoteFor(image([marker, 'SomeHomeNet']))).toBeTruthy(); + } + }); + + it('finds a marker that straddles the very end of the image', async () => { + const bytes: number[] = []; + for (let i = 0; i < 100; i++) bytes.push(0xff); + for (const ch of 'WiFiGeneric') bytes.push(ch.charCodeAt(0)); + const note = await wifiSsidNoteFor(new File([new Uint8Array(bytes)], 'fw.bin')); + expect(note).toBeTruthy(); + }); +}); diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 1303bc9e..111d1b6e 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -30,6 +30,7 @@ import { mergeSuggestedLibraries } from '../../utils/libraryManifest'; import { parseCompileResult, isNoiseBuildLine } from '../../utils/compilationLogger'; import type { CompilationLog, CompileTarget } from '../../utils/compilationLogger'; import { exportToWokwiZip, retargetBoardWires } from '../../utils/wokwiZip'; +import { wifiSsidNoteFor } from '../../utils/firmwareWifiNote'; import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject'; import { readFirmwareFile } from '../../utils/firmwareLoader'; import { @@ -1399,6 +1400,16 @@ export const EditorToolbar = ({ }); } + // A binary built elsewhere never passed through our compiler, so its + // WiFi SSID was never rewritten for the emulator — and the emulated + // radio only ever broadcasts EMULATED_WIFI_SSIDS. The firmware boots + // and runs perfectly and then sits there failing to associate, which + // reads as "the emulator is broken" (issue #270). Say it once, here, + // and only when the binary actually looks like it wants WiFi and names + // none of the networks it could reach. + const note = await wifiSsidNoteFor(file); + if (note) addLog({ timestamp: new Date(), type: 'info', message: note }); + if (activeBoardId) { compileBoardProgram(activeBoardId, result.program); markCompiled(); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index a0049d27..4b8bc397 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -27,7 +27,7 @@ import type { I2CDevice } from '../simulation/I2CBusManager'; import type { RP2040I2CDevice } from '../simulation/RP2040Simulator'; import type { Wire, WireInProgress, WireEndpoint } from '../types/wire'; import type { BoardKind, BoardInstance, LanguageMode, WifiStatus } from '../types/board'; -import { BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, isStm32BoardKind } from '../types/board'; +import { BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, EMULATED_WIFI_SSIDS, isPiBoardKind, isStm32BoardKind } from '../types/board'; import { boardGateDecision, proBoardFeatureName, triggerProUpgradePrompt } from '../lib/proBoardGate'; import { getSerialTxInterceptor } from '../lib/proHardwareSerial'; import { calculatePinPosition } from '../utils/pinPositionCalculator'; @@ -2118,7 +2118,7 @@ export const useSimulatorStore = create((set, get) => { const networkStub = wifiOn ? [ 'import network as _vlx_net', 'import time as _vlx_time', - '_VLX_SSIDS = ("Velxio-GUEST", "PICSimLabWifi", "Espressif", "MasseyWifi")', + `_VLX_SSIDS = (${EMULATED_WIFI_SSIDS.map((s) => JSON.stringify(s)).join(', ')})`, 'class _VlxWLAN:', ' def __init__(self, *a, **k):', ' self._w = _vlx_net.WLAN(*a, **k)', diff --git a/frontend/src/types/board.ts b/frontend/src/types/board.ts index 77d38fa4..a31585ba 100644 --- a/frontend/src/types/board.ts +++ b/frontend/src/types/board.ts @@ -194,6 +194,24 @@ export function isKnownBoardKind(kind: string): kind is BoardKind { ); } +/** + * The networks the emulated radio broadcasts — all open, no password. + * + * Both backends beacon the same four (the QEMU fork's esp32_wifi_ap.c and the + * in-browser engines' ACCESS_POINTS), and a station can only ever associate + * with one of them. Sketches COMPILED in Velxio never have to know: the + * compiler rewrites their SSID literal on the way to the emulator. Firmware + * that arrives already built does not pass through that step, so its own SSID + * is what it hunts for — which is why an otherwise-working binary sits there + * failing to connect (issue #270). + */ +export const EMULATED_WIFI_SSIDS = [ + 'Velxio-GUEST', + 'PICSimLabWifi', + 'Espressif', + 'MasseyWifi', +] as const; + export const BOARD_KIND_LABELS: Record = { 'arduino-uno': 'Arduino Uno', 'arduino-nano': 'Arduino Nano', diff --git a/frontend/src/utils/firmwareWifiNote.ts b/frontend/src/utils/firmwareWifiNote.ts new file mode 100644 index 00000000..02c57536 --- /dev/null +++ b/frontend/src/utils/firmwareWifiNote.ts @@ -0,0 +1,54 @@ +/** + * Tell someone their uploaded firmware will never find a network (issue #270). + * + * A sketch compiled inside Velxio has its WiFi SSID rewritten on the way to + * the emulator, because the emulated radio broadcasts a fixed set of networks + * and nothing else. Firmware that arrives already built — from the Arduino + * IDE, from Visuino, from anywhere — skips that step by construction: there is + * no compile to rewrite anything. So it boots, runs, and hunts for a network + * that does not exist here, which looks like the emulator failing rather than + * a configuration mismatch. The reporter of #270 lost an afternoon to exactly + * that with a binary that was, in every other respect, working. + */ +import { EMULATED_WIFI_SSIDS } from '../types/board'; + +/** + * Markers that the image contains the Arduino WiFi stack. Kept narrow on + * purpose: `esp_wifi` alone appears in images that never touch the radio, and + * a note shown to someone who does not need it is noise that teaches people to + * ignore the console. + */ +const WIFI_MARKERS = ['WiFiGeneric', 'STA.cpp', 'esp_wifi_connect', 'wifi_station']; + +/** Search a binary for an ASCII string, without decoding the whole image. */ +function containsAscii(bytes: Uint8Array, needle: string): boolean { + const pat = new Uint8Array(needle.length); + for (let i = 0; i < needle.length; i++) pat[i] = needle.charCodeAt(i); + outer: for (let i = 0; i + pat.length <= bytes.length; i++) { + for (let j = 0; j < pat.length; j++) if (bytes[i + j] !== pat[j]) continue outer; + return true; + } + return false; +} + +/** + * The line to print for this firmware, or null when there is nothing to say — + * either it does not use WiFi, or it already names a network the emulator + * broadcasts and will connect on its own. + */ +export async function wifiSsidNoteFor(file: File): Promise { + let bytes: Uint8Array; + try { + bytes = new Uint8Array(await file.arrayBuffer()); + } catch { + return null; // unreadable is the caller's problem, not ours to report twice + } + if (!WIFI_MARKERS.some((m) => containsAscii(bytes, m))) return null; + if (EMULATED_WIFI_SSIDS.some((s) => containsAscii(bytes, s))) return null; + return ( + 'This firmware uses WiFi but does not name any network the emulator has. ' + + `The emulated radio broadcasts only: ${EMULATED_WIFI_SSIDS.join(', ')} — all open, ` + + 'no password. Rebuild it with one of those as the SSID and it will connect. ' + + '(Sketches compiled here are rewritten automatically; an uploaded binary cannot be.)' + ); +}