From 2dcdabe72f6b28a43414fe119fe9cedf60253e01 Mon Sep 17 00:00:00 2001 From: David Montero Date: Sun, 14 Jun 2026 03:11:54 +0200 Subject: [PATCH] fix(picow): web examples use relative URLs + a resilient server loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The served page loads under /api/gateway//, so absolute fetches like fetch('/on') hit velxio.dev/on instead of the chip — the LED/relay/servo controls did nothing. Use relative paths (fetch('on')) so they resolve under the gateway. The relay example now has real ON/OFF buttons and wraps its blocking accept loop in try/except so a dropped browser connection can't kill it. e2e now fires two sequential requests (first with a browser-sized header) against a non-resilient blocking server and asserts both are served. --- .../picow-gateway-e2e.investigate.test.ts | 65 +++++++++++-------- frontend/src/data/examples-picow-wifi.ts | 41 ++++++++---- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/frontend/src/__tests__/picow-gateway-e2e.investigate.test.ts b/frontend/src/__tests__/picow-gateway-e2e.investigate.test.ts index 54fdd982..1c4606f5 100644 --- a/frontend/src/__tests__/picow-gateway-e2e.investigate.test.ts +++ b/frontend/src/__tests__/picow-gateway-e2e.investigate.test.ts @@ -43,13 +43,17 @@ const SERVER_PY = [ 's.bind(("0.0.0.0", 80))', 's.listen(1)', 'print("LISTEN", ip)', + 'n = 0', + // Blocking server with NO per-client try/except (like the relay example): + // if the gateway ever forwarded an oversized request or RST a stale + // connection, recv()/the next accept() would raise ECONNRESET and kill + // this loop, so a second request would never be served. 'while True:', ' cl, addr = s.accept()', - ' try:', - ' cl.recv(512)', - ' cl.send(b"HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\nVELXIO-PICO-OK")', - ' except Exception as e:', - ' print("SRVERR", repr(e))', + ' cl.recv(512)', + ' n += 1', + ' print("REQ", n)', + ' cl.send(b"HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\nVELXIO-PICO-OK")', ' cl.close()', ].join('\n'); @@ -113,34 +117,43 @@ describe.skipIf(!process.env.CYW43_GATEWAY_E2E)('Pico W IoT gateway inbound', () } expect(serial).toMatch(/LISTEN 10\.13\.37\.42/); - // 2. Fire the gateway request and keep stepping the chip so it can - // accept the inbound connection and serve the page. + // 2. Fire TWO sequential gateway requests, the first carrying a large + // header (simulating a real browser's cookies/User-Agent). The chip's + // non-resilient blocking server must serve BOTH (proving the gateway + // forwards a lean request and never RSTs the chip on cleanup). const gwUrl = `${API_BASE}/gateway/${encodeURIComponent(clientId)}/`; - let result: { status: number; body: string } | null = null; - let err: unknown = null; - const fetchP = fetch(gwUrl) - .then(async (r) => { result = { status: r.status, body: await r.text() }; }) - .catch((e) => { err = e; }); + const bigHeader = { 'X-Browser-Junk': 'a'.repeat(4096) }; + const doFetch = async (path: string, headers?: Record) => { + let r: { status: number; body: string } | null = null; + let e: unknown = null; + const p = fetch(gwUrl + path, headers ? { headers } : undefined) + .then(async (resp) => { r = { status: resp.status, body: await resp.text() }; }) + .catch((x) => { e = x; }); + const dl = Date.now() + 25_000; + while (Date.now() < dl && r === null && e === null) { + sim.runFrameForTime(10); + await new Promise((res) => setTimeout(res, 0)); + } + await p; + return { r, e }; + }; - const reqDeadline = Date.now() + 30_000; - while (Date.now() < reqDeadline && result === null && err === null) { - sim.runFrameForTime(10); - await new Promise((r) => setTimeout(r, 0)); - } - await fetchP; + const first = await doFetch('', bigHeader); // page load (browser-sized) + const second = await doFetch('on'); // the toggle try { bridge.disconnect(); } catch { /* noop */ } try { sim.stop(); } catch { /* noop */ } - // eslint-disable-next-line no-console console.log('\n===== GATEWAY E2E =====\nclientId=' + clientId + - '\nserial(PY)=' + serial.split('\n').filter((l) => l.startsWith('PY') || l.startsWith('LISTEN') || l.startsWith('SRV')).join(' | ') + - '\nfetch=' + JSON.stringify(result) + (err ? ' err=' + String(err) : '') + - '\npkts=\n' + pktlog.join('\n')); + '\nserial=' + serial.split('\n').filter((l) => /^(PYIP|LISTEN|REQ|SRV|Traceback|OSError)/.test(l)).join(' | ') + + '\nfirst=' + JSON.stringify(first.r) + '\nsecond=' + JSON.stringify(second.r)); - expect(err).toBeNull(); - expect(result).not.toBeNull(); - expect(result!.status).toBe(200); - expect(result!.body).toContain('VELXIO-PICO-OK'); + expect(first.e).toBeNull(); + expect(first.r?.status).toBe(200); + expect(first.r?.body).toContain('VELXIO-PICO-OK'); + // The blocking server survived the first request and served the second. + expect(second.r?.status).toBe(200); + expect(serial).toContain('REQ 2'); + expect(serial).not.toMatch(/Traceback|ECONNRESET/); }, 140_000); }); diff --git a/frontend/src/data/examples-picow-wifi.ts b/frontend/src/data/examples-picow-wifi.ts index 5ff5d7f1..a73f1032 100644 --- a/frontend/src/data/examples-picow-wifi.ts +++ b/frontend/src/data/examples-picow-wifi.ts @@ -72,8 +72,8 @@ async def wifi_connect(): HTML = """

Pico W Async LED

- - + + """ async def handle_client(reader, writer): @@ -137,19 +137,32 @@ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((ip, 80)) s.listen(1) -print("Open browser: http://" + ip) +print("Open browser: http://%s/" % ip) +def page(): + state = "ON" if relay_state == 0 else "OFF" + return ("

Pico W Relay: %s

" + " " + "" + "") % state + +# Wrap each client in try/except: a browser that drops the connection +# mid-response would otherwise raise ECONNRESET and kill the server loop. while True: - conn, addr = s.accept() - request = str(conn.recv(1024)) - if "/on" in request: - relay_state = 0; relay.value(relay_state) - elif "/off" in request: - relay_state = 1; relay.value(relay_state) - conn.send("HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n") - conn.sendall("Relay: %s" % - ("ON" if relay_state == 0 else "OFF")) - conn.close() + try: + conn, addr = s.accept() + request = str(conn.recv(1024)) + if "/on" in request: + relay_state = 0; relay.value(relay_state) + elif "/off" in request: + relay_state = 1; relay.value(relay_state) + conn.send("HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n" + page()) + conn.close() + except OSError: + try: + conn.close() + except Exception: + pass `); const SERVO_WEB_PY = withVelxioGuest(`# Pico W Web Servo Controller — MicroPython @@ -183,7 +196,7 @@ def write_servo(angle): def webpage(pos): return ("

Servo {p}°

" "").format(p=pos) + "oninput=\\"fetch('?value='+this.value)\\">").format(p=pos) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)