feat(firmware): say why an uploaded binary cannot find any network (#270)

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.
This commit is contained in:
David Montero Crespo 2026-08-22 07:18:01 +02:00
parent adb3180ac4
commit d64b679604
5 changed files with 147 additions and 2 deletions

View File

@ -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();
});
});

View File

@ -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();

View File

@ -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<SimulatorState>((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)',

View File

@ -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<BoardKind, string> = {
'arduino-uno': 'Arduino Uno',
'arduino-nano': 'Arduino Nano',

View File

@ -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<string | null> {
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.)'
);
}