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:
parent
68d062f526
commit
7cfeb7399d
|
|
@ -198,6 +198,7 @@ def _job_key(
|
||||||
libraries: list[str] | None = None,
|
libraries: list[str] | None = None,
|
||||||
owner_id: str | None = None,
|
owner_id: str | None = None,
|
||||||
language: str | None = None,
|
language: str | None = None,
|
||||||
|
custom_wifi_ssids: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Stable content hash of (files, board, options, spiffs, libraries, owner)
|
"""Stable content hash of (files, board, options, spiffs, libraries, owner)
|
||||||
used as the deduplication key.
|
used as the deduplication key.
|
||||||
|
|
@ -232,6 +233,15 @@ def _job_key(
|
||||||
import json
|
import json
|
||||||
h.update(json.dumps(board_options, sort_keys=True).encode())
|
h.update(json.dumps(board_options, sort_keys=True).encode())
|
||||||
h.update(b"\0")
|
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:
|
if spiffs_files:
|
||||||
for f in sorted(spiffs_files, key=lambda x: x["name"]):
|
for f in sorted(spiffs_files, key=lambda x: x["name"]):
|
||||||
h.update(f["name"].encode())
|
h.update(f["name"].encode())
|
||||||
|
|
@ -315,6 +325,12 @@ class CompileRequest(BaseModel):
|
||||||
# backend deploy — espidf_compiler.compile validates known keys and
|
# backend deploy — espidf_compiler.compile validates known keys and
|
||||||
# ignores the rest. None / missing on non-ESP32 boards.
|
# ignores the rest. None / missing on non-ESP32 boards.
|
||||||
board_options: dict[str, str | int | bool] | None = None
|
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 /
|
# User-uploaded files to bake into the SPIFFS partition (#162). Empty /
|
||||||
# None means the SPIFFS region stays blank (current behaviour).
|
# None means the SPIFFS region stays blank (current behaviour).
|
||||||
spiffs_files: list[SpiffsFileBody] | None = None
|
spiffs_files: list[SpiffsFileBody] | None = None
|
||||||
|
|
@ -495,6 +511,7 @@ async def _run_compile(
|
||||||
allowed_libraries=allowed_libraries,
|
allowed_libraries=allowed_libraries,
|
||||||
owner_id=owner_id,
|
owner_id=owner_id,
|
||||||
pure_idf=pure_idf,
|
pure_idf=pure_idf,
|
||||||
|
custom_wifi_ssids=request.custom_wifi_ssids,
|
||||||
)
|
)
|
||||||
return CompileResponse(
|
return CompileResponse(
|
||||||
success=result["success"],
|
success=result["success"],
|
||||||
|
|
@ -821,6 +838,7 @@ async def compile_start(
|
||||||
sorted(allowed_libraries) if allowed_libraries else None,
|
sorted(allowed_libraries) if allowed_libraries else None,
|
||||||
owner_id if allowed_libraries else None,
|
owner_id if allowed_libraries else None,
|
||||||
language=request.language,
|
language=request.language,
|
||||||
|
custom_wifi_ssids=request.custom_wifi_ssids,
|
||||||
)
|
)
|
||||||
existing_id = JOB_BY_KEY.get(key)
|
existing_id = JOB_BY_KEY.get(key)
|
||||||
if existing_id is not None:
|
if existing_id is not None:
|
||||||
|
|
|
||||||
|
|
@ -3569,10 +3569,16 @@ class ESPIDFCompiler:
|
||||||
allowed_libraries: set[str] | None = None,
|
allowed_libraries: set[str] | None = None,
|
||||||
owner_id: str | None = None,
|
owner_id: str | None = None,
|
||||||
pure_idf: bool = False,
|
pure_idf: bool = False,
|
||||||
|
custom_wifi_ssids: list[str] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Compile Arduino sketch using ESP-IDF.
|
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():
|
Returns dict compatible with ArduinoCLIService.compile():
|
||||||
success, binary_content (base64), binary_type, stdout, stderr, error
|
success, binary_content (base64), binary_type, stdout, stderr, error
|
||||||
|
|
||||||
|
|
@ -3967,6 +3973,7 @@ class ESPIDFCompiler:
|
||||||
# Normalization still rewrites only the entry sketch: it edits
|
# Normalization still rewrites only the entry sketch: it edits
|
||||||
# string literals in place, and the SSID that matters is the one
|
# string literals in place, and the SSID that matters is the one
|
||||||
# WiFi.begin() is called with.
|
# WiFi.begin() is called with.
|
||||||
|
if not custom_wifi_ssids:
|
||||||
main_content = self._normalize_wifi_for_qemu(main_content)
|
main_content = self._normalize_wifi_for_qemu(main_content)
|
||||||
|
|
||||||
# Arduino-as-component only when compile() decided so: the core must
|
# Arduino-as-component only when compile() decided so: the core must
|
||||||
|
|
@ -3996,6 +4003,7 @@ class ESPIDFCompiler:
|
||||||
continue
|
continue
|
||||||
content = f.get('content', '')
|
content = f.get('content', '')
|
||||||
if has_wifi:
|
if has_wifi:
|
||||||
|
if not custom_wifi_ssids:
|
||||||
content = self._normalize_wifi_for_qemu_idf(content)
|
content = self._normalize_wifi_for_qemu_idf(content)
|
||||||
(main_dir / name).write_text(content, encoding='utf-8')
|
(main_dir / name).write_text(content, encoding='utf-8')
|
||||||
wrote_any = True
|
wrote_any = True
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,13 @@ export async function compileCode(
|
||||||
: null;
|
: null;
|
||||||
// P2.3 — library manifest (resolution scope). null = legacy scan-all.
|
// P2.3 — library manifest (resolution scope). null = legacy scan-all.
|
||||||
const libraries = extras?.libraries && extras.libraries.length ? extras.libraries : null;
|
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;
|
let jobId: string;
|
||||||
try {
|
try {
|
||||||
|
|
@ -147,6 +154,7 @@ export async function compileCode(
|
||||||
libraries,
|
libraries,
|
||||||
language: extras?.language ?? null,
|
language: extras?.language ?? null,
|
||||||
initiated_by: extras?.initiatedBy ?? null,
|
initiated_by: extras?.initiatedBy ?? null,
|
||||||
|
custom_wifi_ssids: customWifiSsids,
|
||||||
},
|
},
|
||||||
{ withCredentials: true, timeout: 30000 },
|
{ withCredentials: true, timeout: 30000 },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2131,17 +2131,25 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
// reviving the fake stub for them. Known SSIDs pass through
|
// reviving the fake stub for them. Known SSIDs pass through
|
||||||
// untouched, and the rewrite is announced on serial so nobody
|
// untouched, and the rewrite is announced on serial so nobody
|
||||||
// debugs a connection they didn't make.
|
// 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 ? [
|
const networkStub = wifiOn ? [
|
||||||
'import network as _vlx_net',
|
'import network as _vlx_net',
|
||||||
'import time as _vlx_time',
|
'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:',
|
'class _VlxWLAN:',
|
||||||
' def __init__(self, *a, **k):',
|
' def __init__(self, *a, **k):',
|
||||||
' self._w = _vlx_net.WLAN(*a, **k)',
|
' self._w = _vlx_net.WLAN(*a, **k)',
|
||||||
' def connect(self, ssid=None, key=None, **kw):',
|
' def connect(self, ssid=None, key=None, **kw):',
|
||||||
' if ssid is not None and ssid not in _VLX_SSIDS:',
|
' 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)',
|
` print("[velxio] SSID %r is not part of the emulated network; connecting to ${JSON.stringify(shimSsids[0]).replace(/"/g, "'")} instead" % ssid)`,
|
||||||
' ssid, key = "Velxio-GUEST", ""',
|
` ssid, key = ${JSON.stringify(shimSsids[0])}, ""`,
|
||||||
' if ssid is None:',
|
' if ssid is None:',
|
||||||
' return self._w.connect()',
|
' return self._w.connect()',
|
||||||
' return self._w.connect(ssid, key, **kw)',
|
' return self._w.connect(ssid, key, **kw)',
|
||||||
|
|
|
||||||
|
|
@ -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
|
return null; // unreadable is the caller's problem, not ours to report twice
|
||||||
}
|
}
|
||||||
if (!WIFI_MARKERS.some((m) => containsAscii(bytes, m))) return null;
|
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 (
|
return (
|
||||||
'This firmware uses WiFi but does not name any network the emulator has. ' +
|
'This firmware uses WiFi but does not name any network the emulator has. ' +
|
||||||
`The emulated radio broadcasts only: ${EMULATED_WIFI_SSIDS.join(', ')} — all open, ` +
|
`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.)'
|
'(Sketches compiled here are rewritten automatically; an uploaded binary cannot be.)'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue