feat(wifi): the SSID rewriter stands down when a project defines its own airspace

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).
This commit is contained in:
David Montero Crespo 2026-08-26 20:42:48 +02:00
parent 68d062f526
commit 7cfeb7399d
5 changed files with 64 additions and 7 deletions

View File

@ -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:

View File

@ -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,6 +3973,7 @@ 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.
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
@ -3996,6 +4003,7 @@ class ESPIDFCompiler:
continue
content = f.get('content', '')
if has_wifi:
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

View File

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

View File

@ -2131,17 +2131,25 @@ export const useSimulatorStore = create<SimulatorState>((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)',

View File

@ -52,11 +52,26 @@ export async function wifiSsidNoteFor(file: File): Promise<string | null> {
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.)'
);
}