fix(picow): web examples use relative URLs + a resilient server loop
The served page loads under /api/gateway/<id>/, 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.
This commit is contained in:
parent
bb4d06cc7a
commit
2dcdabe72f
|
|
@ -43,13 +43,17 @@ const SERVER_PY = [
|
||||||
's.bind(("0.0.0.0", 80))',
|
's.bind(("0.0.0.0", 80))',
|
||||||
's.listen(1)',
|
's.listen(1)',
|
||||||
'print("LISTEN", ip)',
|
'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:',
|
'while True:',
|
||||||
' cl, addr = s.accept()',
|
' cl, addr = s.accept()',
|
||||||
' try:',
|
' cl.recv(512)',
|
||||||
' cl.recv(512)',
|
' n += 1',
|
||||||
' cl.send(b"HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n<html><body>VELXIO-PICO-OK</body></html>")',
|
' print("REQ", n)',
|
||||||
' except Exception as e:',
|
' cl.send(b"HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n<html><body>VELXIO-PICO-OK</body></html>")',
|
||||||
' print("SRVERR", repr(e))',
|
|
||||||
' cl.close()',
|
' cl.close()',
|
||||||
].join('\n');
|
].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/);
|
expect(serial).toMatch(/LISTEN 10\.13\.37\.42/);
|
||||||
|
|
||||||
// 2. Fire the gateway request and keep stepping the chip so it can
|
// 2. Fire TWO sequential gateway requests, the first carrying a large
|
||||||
// accept the inbound connection and serve the page.
|
// 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)}/`;
|
const gwUrl = `${API_BASE}/gateway/${encodeURIComponent(clientId)}/`;
|
||||||
let result: { status: number; body: string } | null = null;
|
const bigHeader = { 'X-Browser-Junk': 'a'.repeat(4096) };
|
||||||
let err: unknown = null;
|
const doFetch = async (path: string, headers?: Record<string, string>) => {
|
||||||
const fetchP = fetch(gwUrl)
|
let r: { status: number; body: string } | null = null;
|
||||||
.then(async (r) => { result = { status: r.status, body: await r.text() }; })
|
let e: unknown = null;
|
||||||
.catch((e) => { err = e; });
|
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;
|
const first = await doFetch('', bigHeader); // page load (browser-sized)
|
||||||
while (Date.now() < reqDeadline && result === null && err === null) {
|
const second = await doFetch('on'); // the toggle
|
||||||
sim.runFrameForTime(10);
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
}
|
|
||||||
await fetchP;
|
|
||||||
|
|
||||||
try { bridge.disconnect(); } catch { /* noop */ }
|
try { bridge.disconnect(); } catch { /* noop */ }
|
||||||
try { sim.stop(); } catch { /* noop */ }
|
try { sim.stop(); } catch { /* noop */ }
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('\n===== GATEWAY E2E =====\nclientId=' + clientId +
|
console.log('\n===== GATEWAY E2E =====\nclientId=' + clientId +
|
||||||
'\nserial(PY)=' + serial.split('\n').filter((l) => l.startsWith('PY') || l.startsWith('LISTEN') || l.startsWith('SRV')).join(' | ') +
|
'\nserial=' + serial.split('\n').filter((l) => /^(PYIP|LISTEN|REQ|SRV|Traceback|OSError)/.test(l)).join(' | ') +
|
||||||
'\nfetch=' + JSON.stringify(result) + (err ? ' err=' + String(err) : '') +
|
'\nfirst=' + JSON.stringify(first.r) + '\nsecond=' + JSON.stringify(second.r));
|
||||||
'\npkts=\n' + pktlog.join('\n'));
|
|
||||||
|
|
||||||
expect(err).toBeNull();
|
expect(first.e).toBeNull();
|
||||||
expect(result).not.toBeNull();
|
expect(first.r?.status).toBe(200);
|
||||||
expect(result!.status).toBe(200);
|
expect(first.r?.body).toContain('VELXIO-PICO-OK');
|
||||||
expect(result!.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);
|
}, 140_000);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,8 @@ async def wifi_connect():
|
||||||
|
|
||||||
HTML = """<!DOCTYPE html>
|
HTML = """<!DOCTYPE html>
|
||||||
<html><body><h2>Pico W Async LED</h2>
|
<html><body><h2>Pico W Async LED</h2>
|
||||||
<button onclick="fetch('/on')">ON</button>
|
<button onclick="fetch('on')">ON</button>
|
||||||
<button onclick="fetch('/off')">OFF</button>
|
<button onclick="fetch('off')">OFF</button>
|
||||||
</body></html>"""
|
</body></html>"""
|
||||||
|
|
||||||
async def handle_client(reader, writer):
|
async def handle_client(reader, writer):
|
||||||
|
|
@ -137,19 +137,32 @@ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
s.bind((ip, 80))
|
s.bind((ip, 80))
|
||||||
s.listen(1)
|
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 ("<html><body><h2>Pico W Relay: %s</h2>"
|
||||||
|
"<button onclick=\\"fetch('on').then(()=>location.reload())\\">ON</button> "
|
||||||
|
"<button onclick=\\"fetch('off').then(()=>location.reload())\\">OFF</button>"
|
||||||
|
"</body></html>") % 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:
|
while True:
|
||||||
conn, addr = s.accept()
|
try:
|
||||||
request = str(conn.recv(1024))
|
conn, addr = s.accept()
|
||||||
if "/on" in request:
|
request = str(conn.recv(1024))
|
||||||
relay_state = 0; relay.value(relay_state)
|
if "/on" in request:
|
||||||
elif "/off" in request:
|
relay_state = 0; relay.value(relay_state)
|
||||||
relay_state = 1; relay.value(relay_state)
|
elif "/off" in request:
|
||||||
conn.send("HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n")
|
relay_state = 1; relay.value(relay_state)
|
||||||
conn.sendall("<html><body>Relay: %s</body></html>" %
|
conn.send("HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\n\\r\\n" + page())
|
||||||
("ON" if relay_state == 0 else "OFF"))
|
conn.close()
|
||||||
conn.close()
|
except OSError:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const SERVO_WEB_PY = withVelxioGuest(`# Pico W Web Servo Controller — MicroPython
|
const SERVO_WEB_PY = withVelxioGuest(`# Pico W Web Servo Controller — MicroPython
|
||||||
|
|
@ -183,7 +196,7 @@ def write_servo(angle):
|
||||||
def webpage(pos):
|
def webpage(pos):
|
||||||
return ("<html><body><h1>Servo {p}°</h1>"
|
return ("<html><body><h1>Servo {p}°</h1>"
|
||||||
"<input type=range min=0 max=180 value={p} "
|
"<input type=range min=0 max=180 value={p} "
|
||||||
"oninput=\\"fetch('/?value='+this.value)\\"></body></html>").format(p=pos)
|
"oninput=\\"fetch('?value='+this.value)\\"></body></html>").format(p=pos)
|
||||||
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue