diff --git a/frontend/src/__tests__/issue-270-firmware-wifi-note.test.ts b/frontend/src/__tests__/issue-270-firmware-wifi-note.test.ts index 49cd711f..4ba1b8c0 100644 --- a/frontend/src/__tests__/issue-270-firmware-wifi-note.test.ts +++ b/frontend/src/__tests__/issue-270-firmware-wifi-note.test.ts @@ -29,14 +29,14 @@ function image(strings: string[]): File { 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'])); + const note = await wifiSsidNoteFor(image(['net80211', 'esp_wifi', '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(); + expect(await wifiSsidNoteFor(image(['net80211', 'esp_wifi', ssid]))).toBeNull(); } }); @@ -46,17 +46,74 @@ describe('issue #270 — uploaded firmware that cannot reach any network', () => expect(await wifiSsidNoteFor(image(['blink', 'digitalWrite']))).toBeNull(); }); + it('stays quiet for the reporter\'s own binary, which names the right network', async () => { + // His SSID was correct all along — the note must not accuse him of a + // problem he does not have. + expect(await wifiSsidNoteFor(image(['net80211', 'esp_wifi', 'Velxio-GUEST']))).toBeNull(); + }); + it('recognises the WiFi stack by more than one marker', async () => { - for (const marker of ['WiFiGeneric', 'STA.cpp', 'esp_wifi_connect', 'wifi_station']) { + for (const marker of ['net80211', 'phy_init', 'esp_wifi', 'wifi_init']) { expect(await wifiSsidNoteFor(image([marker, 'SomeHomeNet']))).toBeTruthy(); } }); + it('still recognises it when the build stripped its log strings', async () => { + // The reporter's second binary has no 'STA.cpp' and no 'WiFiGeneric' at + // all — a lower debug level drops them — while the driver names remain. + // Keying on the log strings meant the note stayed silent on exactly the + // firmware it exists for. + const stripped = image(['net80211', 'phy_init', 'esp_wifi', 'MyHomeNet']); + expect(await wifiSsidNoteFor(stripped)).toBeTruthy(); + }); + + it('does not fire on a name that a blink image also carries', async () => { + // WIFI_INIT is in both a blink build and a WiFi build, measured. + expect(await wifiSsidNoteFor(image(['WIFI_INIT_CONFIG_DEFAULT', 'blink']))).toBeNull(); + }); + 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)); + for (const ch of 'net80211') bytes.push(ch.charCodeAt(0)); const note = await wifiSsidNoteFor(new File([new Uint8Array(bytes)], 'fw.bin')); expect(note).toBeTruthy(); }); }); + +describe('issue #270 — the serial note for a blank-NVS connect', () => { + // Verified against the emulator: WiFi.begin() with no arguments on the + // simulator's blank flash produces exactly `STA.cpp:357 ... 0x300a: + // ESP_ERR_WIFI_SSID` — the reporter's error, timestamps and all. On his + // real DevKit the same binary worked because NVS still held credentials + // some earlier firmware had saved. + const MARKER = '0x300a: ESP_ERR_WIFI_SSID'; + + it('explains the error once', async () => { + const { annotateSerialChunk } = await import('../utils/serialDiagnostics'); + const chunk = `[ 40][E][STA.cpp:357] connect(): STA connect failed! ${MARKER}\r\n`; + const out = annotateSerialChunk('', chunk); + expect(out).toContain('NVS'); + expect(out).toContain('Velxio-GUEST'); + }); + + it('does not repeat itself while the same run keeps failing', async () => { + const { annotateSerialChunk } = await import('../utils/serialDiagnostics'); + const chunk = `STA connect failed! ${MARKER}\r\n`; + const first = annotateSerialChunk('', chunk); + const second = annotateSerialChunk(first, chunk); + expect(second).not.toContain('NVS'); + }); + + it('re-arms on a fresh run, whose log starts empty', async () => { + const { annotateSerialChunk } = await import('../utils/serialDiagnostics'); + const chunk = `STA connect failed! ${MARKER}\r\n`; + annotateSerialChunk('', chunk); + expect(annotateSerialChunk('', chunk)).toContain('NVS'); + }); + + it('leaves ordinary output alone', async () => { + const { annotateSerialChunk } = await import('../utils/serialDiagnostics'); + expect(annotateSerialChunk('', 'TICK 1\r\n')).toBe('TICK 1\r\n'); + }); +}); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 030f9657..6244a5a5 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -28,6 +28,7 @@ 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_KIND_FQBN, BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, EMULATED_WIFI_SSIDS, isPiBoardKind, isStm32BoardKind } from '../types/board'; +import { annotateSerialChunk } from '../utils/serialDiagnostics'; import { boardGateDecision, proBoardFeatureName, triggerProUpgradePrompt } from '../lib/proBoardGate'; import { getSerialTxInterceptor } from '../lib/proHardwareSerial'; import { calculatePinPosition } from '../utils/pinPositionCalculator'; @@ -1312,8 +1313,11 @@ const { append: appendSerial } = createSerialBatcher((perBoard) => { useSimulatorStore.setState((s) => { let globalOut = s.serialOutput; const boards = s.boards.map((b) => { - const chunk = perBoard.get(b.id); - if (!chunk) return b; + const raw = perBoard.get(b.id); + if (!raw) return b; + // Attach the one-line explanation when a known cryptic firmware error + // scrolls past (issue #270); a no-op for ordinary output. + const chunk = annotateSerialChunk(b.serialOutput, raw); if (s.activeBoardId === b.id) globalOut += chunk; return { ...b, serialOutput: b.serialOutput + chunk }; }); diff --git a/frontend/src/utils/firmwareWifiNote.ts b/frontend/src/utils/firmwareWifiNote.ts index 02c57536..f7852895 100644 --- a/frontend/src/utils/firmwareWifiNote.ts +++ b/frontend/src/utils/firmwareWifiNote.ts @@ -13,12 +13,20 @@ 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. + * Markers that the image links the WiFi stack. + * + * Chosen by measurement, against two real builds of the same board: a blink + * sketch and a WiFi one. Every name here is absent from the blink image and + * present in the WiFi one. `WIFI_INIT` looked obvious and is in BOTH, so it is + * not here. + * + * They are IDF-level names on purpose. The first version of this list used the + * Arduino log strings ('WiFiGeneric', 'STA.cpp'), which vanish when the build + * lowers its debug level — the reporter's own second binary has neither, and + * the note would have stayed silent on the exact firmware it was written for. + * These survive, because they are the driver, not a message about it. */ -const WIFI_MARKERS = ['WiFiGeneric', 'STA.cpp', 'esp_wifi_connect', 'wifi_station']; +const WIFI_MARKERS = ['net80211', 'phy_init', 'esp_wifi', 'wifi_init']; /** Search a binary for an ASCII string, without decoding the whole image. */ function containsAscii(bytes: Uint8Array, needle: string): boolean { diff --git a/frontend/src/utils/serialDiagnostics.ts b/frontend/src/utils/serialDiagnostics.ts new file mode 100644 index 00000000..1f430ae1 --- /dev/null +++ b/frontend/src/utils/serialDiagnostics.ts @@ -0,0 +1,39 @@ +/** + * One-line explanations for serial errors that read as emulator bugs. + * + * The firmware's own message is written for someone holding real hardware. + * `ESP_ERR_WIFI_SSID` cost the reporter of issue #270 days: his Visuino build + * called WiFi.begin() with no credentials, which on a real DevKit quietly + * reuses the network saved in the chip's flash (NVS) by some earlier firmware + * — and the simulator's flash starts blank, so the same binary fails at boot + * with a hex code. Nothing was wrong with the emulation; nothing in the output + * said so. + * + * Each entry appends its note once per run: the note re-arms when the serial + * log is cleared (every Run starts a fresh log), because the presence of the + * note in the existing output is itself the marker. + */ +const NOTES: ReadonlyArray<{ marker: string; note: string }> = [ + { + marker: '0x300a: ESP_ERR_WIFI_SSID', + note: + '[velxio] This firmware asked to connect with no SSID (or an invalid one). ' + + 'WiFi.begin() without arguments reuses credentials saved in the chip’s flash (NVS) ' + + 'by a previous firmware — the simulator’s flash starts blank, so there is nothing ' + + 'to reuse. Pass them explicitly: WiFi.begin("Velxio-GUEST", "").', + }, +]; + +/** + * The chunk to actually append, with any due explanation attached. Pure so it + * can be tested without the frame batcher around it. + */ +export function annotateSerialChunk(existingOutput: string, chunk: string): string { + let out = chunk; + for (const { marker, note } of NOTES) { + if (out.includes(marker) && !existingOutput.includes(note) && !out.includes(note)) { + out += `\r\n${note}\r\n`; + } + } + return out; +}