From 7cfeb7399d607be073f1961b88a52c7165d03be8 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Wed, 26 Aug 2026 20:42:48 +0200 Subject: [PATCH] feat(wifi): the SSID rewriter stands down when a project defines its own airspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A private overlay can install window.__velxio_custom_wifi_ssids__ — a nullary provider naming the WiFi networks the project's access-point parts broadcast. Three consumers, one seam: - the compile payload gains custom_wifi_ssids; when non-empty the ESP-IDF compiler skips SSID normalization entirely (no literal rewrite, no channel forcing) — the sketch connects to the network the user actually wrote, and a typo now fails truthfully. The dedup key hashes the list so with/without-APs builds never alias. - the MicroPython network shim redirects unknown SSIDs to the FIRST project network instead of the hardcoded Velxio-GUEST, and its tuple gains the trailing comma that made a one-network list a string. - the firmware-upload note lists the project's networks and says "add an access point part with this SSID" instead of "rebuild". No provider installed (OSS build) = exactly the old behavior; the issue-270 suite stays green (12/12). --- backend/app/api/routes/compile.py | 18 ++++++++++++++++++ backend/app/services/espidf_compiler.py | 12 ++++++++++-- frontend/src/services/compilation.ts | 8 ++++++++ frontend/src/store/useSimulatorStore.ts | 14 +++++++++++--- frontend/src/utils/firmwareWifiNote.ts | 19 +++++++++++++++++-- 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/backend/app/api/routes/compile.py b/backend/app/api/routes/compile.py index 8d46bf50..e2c32f2e 100644 --- a/backend/app/api/routes/compile.py +++ b/backend/app/api/routes/compile.py @@ -198,6 +198,7 @@ def _job_key( libraries: list[str] | None = None, owner_id: str | None = None, language: str | None = None, + custom_wifi_ssids: list[str] | None = None, ) -> str: """Stable content hash of (files, board, options, spiffs, libraries, owner) used as the deduplication key. @@ -232,6 +233,15 @@ def _job_key( import json h.update(json.dumps(board_options, sort_keys=True).encode()) h.update(b"\0") + if custom_wifi_ssids: + # Custom APs suppress the SSID rewrite, so the same source builds + # DIFFERENT bytes with vs without them — the key must see it. Only + # presence matters for the rewrite, but hash the sorted list so a + # rename also invalidates cleanly. + for ssid in sorted(custom_wifi_ssids): + h.update(ssid.encode()) + h.update(b"\0") + h.update(b"\0custom-aps\0") if spiffs_files: for f in sorted(spiffs_files, key=lambda x: x["name"]): h.update(f["name"].encode()) @@ -315,6 +325,12 @@ class CompileRequest(BaseModel): # backend deploy — espidf_compiler.compile validates known keys and # ignores the rest. None / missing on non-ESP32 boards. board_options: dict[str, str | int | bool] | None = None + # Optional: the SSIDs of the project's custom WiFi access points (the + # velxio-wifi-ap parts on the canvas, overlay feature). When non-empty + # the compiler does NOT rewrite the sketch's SSID literals — the project + # defines its own airspace, so what the user typed is what exists. Empty + # / missing keeps the legacy behavior (rewrite to the built-in networks). + custom_wifi_ssids: list[str] | None = None # User-uploaded files to bake into the SPIFFS partition (#162). Empty / # None means the SPIFFS region stays blank (current behaviour). spiffs_files: list[SpiffsFileBody] | None = None @@ -495,6 +511,7 @@ async def _run_compile( allowed_libraries=allowed_libraries, owner_id=owner_id, pure_idf=pure_idf, + custom_wifi_ssids=request.custom_wifi_ssids, ) return CompileResponse( success=result["success"], @@ -821,6 +838,7 @@ async def compile_start( sorted(allowed_libraries) if allowed_libraries else None, owner_id if allowed_libraries else None, language=request.language, + custom_wifi_ssids=request.custom_wifi_ssids, ) existing_id = JOB_BY_KEY.get(key) if existing_id is not None: diff --git a/backend/app/services/espidf_compiler.py b/backend/app/services/espidf_compiler.py index cade4d72..879ad2fa 100644 --- a/backend/app/services/espidf_compiler.py +++ b/backend/app/services/espidf_compiler.py @@ -3569,10 +3569,16 @@ class ESPIDFCompiler: allowed_libraries: set[str] | None = None, owner_id: str | None = None, pure_idf: bool = False, + custom_wifi_ssids: list[str] | None = None, ) -> dict: """ Compile Arduino sketch using ESP-IDF. + `custom_wifi_ssids` non-empty = the project carries its own access + point parts: the WiFi normalization below stands down entirely (no + SSID rewrite, no channel forcing) — the user's airspace is exactly + what they typed, typos included, which is now the truthful outcome. + Returns dict compatible with ArduinoCLIService.compile(): success, binary_content (base64), binary_type, stdout, stderr, error @@ -3967,7 +3973,8 @@ class ESPIDFCompiler: # Normalization still rewrites only the entry sketch: it edits # string literals in place, and the SSID that matters is the one # WiFi.begin() is called with. - main_content = self._normalize_wifi_for_qemu(main_content) + if not custom_wifi_ssids: + main_content = self._normalize_wifi_for_qemu(main_content) # Arduino-as-component only when compile() decided so: the core must # support the target (esp32c6 needs an arduino-esp32 3.x core the @@ -3996,7 +4003,8 @@ class ESPIDFCompiler: continue content = f.get('content', '') if has_wifi: - content = self._normalize_wifi_for_qemu_idf(content) + if not custom_wifi_ssids: + content = self._normalize_wifi_for_qemu_idf(content) (main_dir / name).write_text(content, encoding='utf-8') wrote_any = True if not wrote_any: diff --git a/frontend/src/services/compilation.ts b/frontend/src/services/compilation.ts index f4dcc966..5776eb4f 100644 --- a/frontend/src/services/compilation.ts +++ b/frontend/src/services/compilation.ts @@ -131,6 +131,13 @@ export async function compileCode( : null; // P2.3 — library manifest (resolution scope). null = legacy scan-all. const libraries = extras?.libraries && extras.libraries.length ? extras.libraries : null; + // Custom WiFi access points (overlay feature): when the project carries its + // own AP parts, their SSIDs ride along and the backend's SSID rewriter + // stands down — the sketch connects to the network the user actually wrote. + // OSS builds have no provider installed → null → legacy rewrite. + const customWifiSsids = + (window as { __velxio_custom_wifi_ssids__?: () => string[] | null }) + .__velxio_custom_wifi_ssids__?.() ?? null; let jobId: string; try { @@ -147,6 +154,7 @@ export async function compileCode( libraries, language: extras?.language ?? null, initiated_by: extras?.initiatedBy ?? null, + custom_wifi_ssids: customWifiSsids, }, { withCredentials: true, timeout: 30000 }, ); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 6244a5a5..2325f952 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -2131,17 +2131,25 @@ export const useSimulatorStore = create((set, get) => { // reviving the fake stub for them. Known SSIDs pass through // untouched, and the rewrite is announced on serial so nobody // debugs a connection they didn't make. + // Custom AP parts (overlay-provided window seam) replace the + // network list the shim knows about — and the redirect target + // becomes the FIRST project network instead of Velxio-GUEST. + // Without the provider (OSS build) the classic four stand. + const customSsids = + (window as { __velxio_custom_wifi_ssids__?: () => string[] | null }) + .__velxio_custom_wifi_ssids__?.() ?? null; + const shimSsids: readonly string[] = customSsids ?? EMULATED_WIFI_SSIDS; const networkStub = wifiOn ? [ 'import network as _vlx_net', 'import time as _vlx_time', - `_VLX_SSIDS = (${EMULATED_WIFI_SSIDS.map((s) => JSON.stringify(s)).join(', ')})`, + `_VLX_SSIDS = (${shimSsids.map((s) => JSON.stringify(s)).join(', ')},)`, 'class _VlxWLAN:', ' def __init__(self, *a, **k):', ' self._w = _vlx_net.WLAN(*a, **k)', ' def connect(self, ssid=None, key=None, **kw):', ' if ssid is not None and ssid not in _VLX_SSIDS:', - ' print("[velxio] SSID %r is not part of the emulated network; connecting to \'Velxio-GUEST\' instead" % ssid)', - ' ssid, key = "Velxio-GUEST", ""', + ` print("[velxio] SSID %r is not part of the emulated network; connecting to ${JSON.stringify(shimSsids[0]).replace(/"/g, "'")} instead" % ssid)`, + ` ssid, key = ${JSON.stringify(shimSsids[0])}, ""`, ' if ssid is None:', ' return self._w.connect()', ' return self._w.connect(ssid, key, **kw)', diff --git a/frontend/src/utils/firmwareWifiNote.ts b/frontend/src/utils/firmwareWifiNote.ts index f7852895..819c7f76 100644 --- a/frontend/src/utils/firmwareWifiNote.ts +++ b/frontend/src/utils/firmwareWifiNote.ts @@ -52,11 +52,26 @@ export async function wifiSsidNoteFor(file: File): Promise { 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; + // A project with its own AP parts (overlay window seam) defines the + // airspace; without them the classic four stand. Either way, firmware + // that names one of the on-air networks needs no note. + const custom = + (window as { __velxio_custom_wifi_ssids__?: () => string[] | null }) + .__velxio_custom_wifi_ssids__?.() ?? null; + const onAir: readonly string[] = custom ?? EMULATED_WIFI_SSIDS; + if (onAir.some((s) => containsAscii(bytes, s))) return null; + if (custom) { + return ( + 'This firmware uses WiFi but does not name any network in this project. ' + + `The project's access points broadcast: ${custom.join(', ')}. ` + + 'Add a WiFi Access Point part with the SSID the firmware expects and it will connect.' + ); + } 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. ' + + 'no password. Rebuild it with one of those as the SSID — or add a WiFi Access ' + + 'Point part with your SSID (paid feature) — and it will connect. ' + '(Sketches compiled here are rewritten automatically; an uploaded binary cannot be.)' ); }