fix(micropython): stop shadowing the real WiFi driver when a NIC exists
Reported in #262: on an emulated ESP32, MicroPython's WLAN.scan() always returned [], ifconfig() reported 10.0.2.15/10.0.2.2/10.0.2.3, and outbound sockets raised EHOSTUNREACH — while Arduino WiFi.h on the same board and container scanned, connected on 192.168.4.x and completed real HTTP requests. Two independent faults, both needed for the failure. 1. The MicroPython prelude injected a stub `network` module into sys.modules unconditionally, so user code never reached the firmware driver. The stub's hardcoded ifconfig() happens to be QEMU's SLIRP defaults, which is why the symptom read so convincingly as a routing bug. 2. hasWifi survived a language switch. It is a property of the compiled sketch, set only by arduino-cli, but setBoardLanguageMode kept it — so a board compiled once as Arduino carried that verdict into a later MicroPython run. The source-scanning fallback only fires when hasWifi is undefined, so it never got a chance, and the backend attaches the `-nic` device only when wifi_enabled is true. The stub is NOT removed, which is where this departs from the patch in the issue. It exists for a reason the report didn't have: without a NIC, `network.WLAN(STA_IF)` hangs forever waiting on peripheral status bits QEMU never sets, tripping the FreeRTOS watchdog after ~26s. Deleting it would trade a fake-but-working network for a hung board. It is now conditional — real driver when a NIC is attached, stub when none is — so both paths are honest. Also kept: the ntptime and urequests stubs, which the issue's diff would have dropped along with `network` (it removed the whole prelude block). Those back 29 call sites in the 100-days gallery examples, which have no emulated internet to reach regardless of the NIC. The detector is now one shared function instead of two copies that could disagree — the stub decision and the -nic decision MUST match. Folded in `from network import WLAN`, which matched neither previous pattern: such a sketch was judged WiFi-less and hung on the missing peripheral. The existing tests re-implemented the detection inline, so they only ever proved that copy self-consistent; the new ones call the shipped function.
This commit is contained in:
parent
2908726007
commit
1800f57225
|
|
@ -9,6 +9,7 @@
|
|||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { sketchUsesWifi } from '../store/useSimulatorStore';
|
||||
|
||||
// ── Mocks ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -357,6 +358,54 @@ void loop() { delay(1000); }
|
|||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 3b. The REAL detector (issue #262)
|
||||
//
|
||||
// The block above re-implements the detection inline, so it only ever
|
||||
// proves the copy in this file is self-consistent — which is how the
|
||||
// MicroPython gap below survived. These call the shipped function.
|
||||
//
|
||||
// Getting this wrong is not cosmetic: a false verdict means QEMU is
|
||||
// started without `-nic`, and MicroPython's `network.WLAN(STA_IF)` then
|
||||
// hangs ~26s on a peripheral that was never attached until the FreeRTOS
|
||||
// watchdog resets the board.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('sketchUsesWifi — the shipped detector', () => {
|
||||
const uses = (content: string) => sketchUsesWifi([{ content }]);
|
||||
|
||||
it('detects the Arduino forms', () => {
|
||||
expect(uses('#include <WiFi.h>')).toBe(true);
|
||||
expect(uses('#include "WiFi.h"')).toBe(true);
|
||||
expect(uses('#include <esp_wifi.h>')).toBe(true);
|
||||
expect(uses('void setup(){ WiFi.begin("s",""); }')).toBe(true);
|
||||
});
|
||||
|
||||
it('detects the MicroPython import forms', () => {
|
||||
expect(uses('import network')).toBe(true);
|
||||
expect(uses('import network as net')).toBe(true);
|
||||
expect(uses('wlan = network.WLAN(network.STA_IF)')).toBe(true);
|
||||
});
|
||||
|
||||
it('detects `from network import WLAN` (the #262 gap)', () => {
|
||||
// Reported as MicroPython WiFi being "broken": this form matched
|
||||
// neither /import\s+network\b/ nor /network\.WLAN/, so the sketch was
|
||||
// judged WiFi-less and never got a NIC.
|
||||
expect(uses('from network import WLAN\nw = WLAN(0)')).toBe(true);
|
||||
});
|
||||
|
||||
it('stays false for sketches that touch no network', () => {
|
||||
expect(uses('void setup(){ Serial.begin(115200); }')).toBe(false);
|
||||
expect(uses('from machine import Pin\nPin(2).on()')).toBe(false);
|
||||
});
|
||||
|
||||
it('scans every file, not just the first', () => {
|
||||
expect(
|
||||
sketchUsesWifi([{ content: 'from machine import Pin' }, { content: 'import network' }]),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 4. BLE detection from sketch files
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -963,6 +963,34 @@ function isEsp32Kind(kind: BoardKind): boolean {
|
|||
return getProBoard(kind)?.esp32Family !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this sketch need the emulated WiFi NIC?
|
||||
*
|
||||
* Single source of truth for two callers that must agree: startBoard(),
|
||||
* which decides whether QEMU gets `-nic` at all, and
|
||||
* loadMicroPythonProgram(), which decides whether to shadow `network`
|
||||
* with the offline stub. Disagreement between them is precisely the
|
||||
* failure reported in #262 — a real driver with no device behind it, or
|
||||
* a stub in front of a device that works.
|
||||
*
|
||||
* Arduino is detected from its includes; MicroPython from its import
|
||||
* forms. `from network import WLAN` is listed because omitting it left
|
||||
* hasWifi false for a sketch that genuinely uses WiFi, and the board
|
||||
* then hung for ~26s on a peripheral that was never attached.
|
||||
*/
|
||||
export function sketchUsesWifi(files: Array<{ content: string }>): boolean {
|
||||
return files.some(
|
||||
(f) =>
|
||||
f.content.includes('#include <WiFi.h>') ||
|
||||
f.content.includes('#include <esp_wifi.h>') ||
|
||||
f.content.includes('#include "WiFi.h"') ||
|
||||
f.content.includes('WiFi.begin(') ||
|
||||
/import\s+network\b/.test(f.content) ||
|
||||
/from\s+network\s+import\b/.test(f.content) ||
|
||||
/network\.WLAN/.test(f.content),
|
||||
);
|
||||
}
|
||||
|
||||
function isRiscVEsp32Kind(kind: BoardKind): boolean {
|
||||
const fam = getProBoard(kind)?.esp32Family;
|
||||
return ESP32_RISCV_KINDS.has(kind) || fam === 'esp32-c3' || fam === 'esp32-c6';
|
||||
|
|
@ -1944,13 +1972,33 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
return `with open(${path},'w') as _f:\n _f.write(${lit})`;
|
||||
});
|
||||
|
||||
// Does this run get a real NIC? Must match what startBoard()
|
||||
// will decide, because that is what actually attaches the
|
||||
// device — see sketchUsesWifi().
|
||||
const wifiOn = board.hasWifi ?? sketchUsesWifi(files);
|
||||
|
||||
// WiFi compat shim: replace `network`, `ntptime`, `urequests`
|
||||
// with smart stubs BEFORE user main.py imports them. The
|
||||
// picsimlab QEMU fork's esp32_wifi NIC emulation is sufficient
|
||||
// for Arduino's lightweight WiFi.h but not for MicroPython's
|
||||
// full esp_wifi_init path — calling `network.WLAN(STA_IF)`
|
||||
// hangs forever waiting for peripheral status bits QEMU never
|
||||
// sets, tripping the FreeRTOS task watchdog after ~26s.
|
||||
// with smart stubs BEFORE user main.py imports them.
|
||||
//
|
||||
// `network` is stubbed ONLY when no NIC is attached. With one
|
||||
// attached the real driver works (DHCP on 192.168.4.x, DNS,
|
||||
// outbound TCP), and shadowing it made MicroPython WiFi look
|
||||
// permanently broken while Arduino worked on the same board:
|
||||
// scan() returned [], ifconfig() reported QEMU's SLIRP
|
||||
// defaults, and sockets raised EHOSTUNREACH — issue #262.
|
||||
//
|
||||
// Without a NIC the stub still earns its place. The picsimlab
|
||||
// QEMU fork's esp32_wifi emulation is enough for Arduino's
|
||||
// lightweight WiFi.h but not for MicroPython's full
|
||||
// esp_wifi_init path: `network.WLAN(STA_IF)` then hangs
|
||||
// forever waiting for peripheral status bits QEMU never sets,
|
||||
// tripping the FreeRTOS task watchdog after ~26s. Deleting the
|
||||
// stub outright would trade a working-but-fake network for a
|
||||
// hung board, so it stays as the offline fallback.
|
||||
//
|
||||
// ntptime and urequests are stubbed unconditionally: they back
|
||||
// the gallery examples' clocks and weather panels, which have
|
||||
// no emulated internet to reach either way.
|
||||
//
|
||||
// Smart stub behaviour (so examples like smart-ui-eyes WORK
|
||||
// end-to-end, not just degrade gracefully):
|
||||
|
|
@ -1967,14 +2015,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
// useful data instead of "API Error".
|
||||
const now = new Date();
|
||||
const fakeWeatherCity = 'Simulator City';
|
||||
const wifiStub = [
|
||||
'import sys',
|
||||
'import json as _json',
|
||||
'try:',
|
||||
' import machine as _machine',
|
||||
'except ImportError:',
|
||||
' _machine = None',
|
||||
'',
|
||||
const networkStub = wifiOn ? [] : [
|
||||
'class _StubWLAN:',
|
||||
' def __init__(self, *a, **k):',
|
||||
' self._calls = 0',
|
||||
|
|
@ -1993,6 +2034,17 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
' AP_IF = 1',
|
||||
' WLAN = _StubWLAN',
|
||||
'sys.modules["network"] = _StubNetwork()',
|
||||
];
|
||||
|
||||
const wifiStub = [
|
||||
'import sys',
|
||||
'import json as _json',
|
||||
'try:',
|
||||
' import machine as _machine',
|
||||
'except ImportError:',
|
||||
' _machine = None',
|
||||
'',
|
||||
...networkStub,
|
||||
'',
|
||||
'# ntptime: pre-load RTC with host UTC so localtime() works.',
|
||||
`_VLX_BOOT_UTC = (${now.getUTCFullYear()}, ${now.getUTCMonth() + 1}, ${now.getUTCDate()}, ${now.getUTCDay() || 7}, ${now.getUTCHours()}, ${now.getUTCMinutes()}, ${now.getUTCSeconds()}, 0)`,
|
||||
|
|
@ -2089,10 +2141,18 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
// Stop any running simulation
|
||||
if (board.running) get().stopBoard(boardId);
|
||||
|
||||
// Clear compiled program since language changed
|
||||
// Clear compiled program since language changed. hasWifi goes with
|
||||
// it: it is a property of the compiled sketch, not of the board, and
|
||||
// only arduino-cli sets it. Keeping the old value meant a board that
|
||||
// had been compiled as Arduino carried that verdict into a later
|
||||
// MicroPython run, where the source-scanning fallback never fires
|
||||
// (it only runs when hasWifi is undefined) — so a MicroPython WiFi
|
||||
// sketch on a reused board silently got no NIC at all (#262).
|
||||
set((s) => ({
|
||||
boards: s.boards.map((b) =>
|
||||
b.id === boardId ? { ...b, languageMode: mode, compiledProgram: null } : b,
|
||||
b.id === boardId
|
||||
? { ...b, languageMode: mode, compiledProgram: null, hasWifi: undefined }
|
||||
: b,
|
||||
),
|
||||
}));
|
||||
|
||||
|
|
@ -2282,19 +2342,9 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
const editorState = useEditorStore.getState();
|
||||
const rawFiles = editorState.fileGroups[board.activeFileGroupId];
|
||||
const boardFiles = rawFiles && rawFiles.length > 0 ? rawFiles : editorState.files;
|
||||
hasWifi = boardFiles.some(
|
||||
(f) =>
|
||||
f.content.includes('#include <WiFi.h>') ||
|
||||
f.content.includes('#include <esp_wifi.h>') ||
|
||||
f.content.includes('#include "WiFi.h"') ||
|
||||
f.content.includes('WiFi.begin(') ||
|
||||
// MicroPython patterns — without these the WiFi NIC is never
|
||||
// passed to QEMU, and `network.WLAN(STA_IF)` hangs forever
|
||||
// trying to init a peripheral that doesn't exist, eventually
|
||||
// tripping the FreeRTOS task watchdog (TG1WDT_SYS_RESET).
|
||||
/import\s+network\b/.test(f.content) ||
|
||||
/network\.WLAN/.test(f.content),
|
||||
);
|
||||
// Shared with loadMicroPythonProgram's stub decision — the two
|
||||
// must not disagree about whether a NIC exists.
|
||||
hasWifi = sketchUsesWifi(boardFiles);
|
||||
}
|
||||
esp32Bridge.wifiEnabled = hasWifi;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue