From 80d20869071d5a84b4f5411ea4a2753a232c582f Mon Sep 17 00:00:00 2001 From: David Montero Date: Sun, 14 Jun 2026 04:19:48 +0200 Subject: [PATCH] fix(picow): open the IoT gateway in an in-app iframe, not a new tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pico W emulation runs in THIS browser tab via requestAnimationFrame. Opening the gateway with target=_blank / window.open backgrounds the emulation tab; the browser then pauses its rAF, the simulated chip freezes, and the gateway can no longer reach the server on it — the request times out (502) and toggles do nothing. Render the served page in a same-tab iframe panel (openDeviceGateway) for the Pico W so the emulation stays in the foreground and keeps answering. The ESP32 is unchanged (its server runs in QEMU on the backend, immune to tab visibility), so it keeps opening in a new tab. Also: the async-led page now shows the LED state (the board's onboard LED isn't drawn on the canvas) so the toggle has visible feedback. --- .../components/simulator/SerialMonitor.tsx | 12 +++ .../components/simulator/SimulatorCanvas.tsx | 9 +- frontend/src/data/examples-picow-wifi.ts | 27 +++--- frontend/src/lib/openDeviceGateway.ts | 83 +++++++++++++++++++ 4 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 frontend/src/lib/openDeviceGateway.ts diff --git a/frontend/src/components/simulator/SerialMonitor.tsx b/frontend/src/components/simulator/SerialMonitor.tsx index b36021e5..7b7327f3 100644 --- a/frontend/src/components/simulator/SerialMonitor.tsx +++ b/frontend/src/components/simulator/SerialMonitor.tsx @@ -7,6 +7,7 @@ import React, { useRef, useEffect, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useSimulatorStore } from '../../store/useSimulatorStore'; import { getTabSessionId } from '../../simulation/Esp32Bridge'; +import { openDeviceGateway } from '../../lib/openDeviceGateway'; import type { BoardKind } from '../../types/board'; import { boardDisplayName } from '../../types/board'; @@ -283,12 +284,23 @@ export const SerialMonitor: React.FC = () => { const gatewayUrl = `${backendBase}/gateway/${clientId}${path}`; parts.push(text.slice(lastIdx, start)); + const isPicoW = activeBoard.boardKind === 'pi-pico-w'; parts.push( { + // Pico W runs in this tab; a new tab freezes the + // emulation. Show the page in an in-app iframe. + e.preventDefault(); + openDeviceGateway(gatewayUrl); + } + : undefined + } style={{ color: '#4fc3f7', textDecoration: 'underline', diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index a88b03be..e2682e13 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -1,5 +1,6 @@ import { useSimulatorStore, getEsp32Bridge } from '../../store/useSimulatorStore'; import { useElectricalStore } from '../../store/useElectricalStore'; +import { openDeviceGateway } from '../../lib/openDeviceGateway'; import React, { useEffect, useState, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { Undo2, Redo2 } from 'lucide-react'; @@ -2202,7 +2203,13 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { __velxio_iot_gateway_open_gate__?: () => boolean; }).__velxio_iot_gateway_open_gate__; if (gate && gate()) return; - window.open(gatewayUrl, '_blank'); + // Pico W runs in THIS tab — a new tab would background and + // freeze the emulation. Show the page in an in-app iframe. + if (activeBoard.boardKind === 'pi-pico-w') { + openDeviceGateway(gatewayUrl); + } else { + window.open(gatewayUrl, '_blank'); + } }; return ( -

Pico W Async LED

- - -""" +led_on = False + +def page(): + # The board's onboard LED isn't drawn on the canvas, so show the state + # here; the buttons reload the page so you can see it flip. + state = "ON" if led_on else "OFF" + return ("

Pico W LED: %s

" + " " + "" + "") % state async def handle_client(reader, writer): - request_line = await reader.readline() - request = request_line.decode() + global led_on + request = (await reader.readline()).decode() while await reader.readline() != b"\\r\\n": pass if "GET /on" in request: - led.on(); body = "ON" + led.on(); led_on = True elif "GET /off" in request: - led.off(); body = "OFF" - else: - body = HTML - writer.write(("HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\nConnection: close\\r\\n\\r\\n" + body).encode()) + led.off(); led_on = False + writer.write(("HTTP/1.1 200 OK\\r\\nContent-Type: text/html\\r\\nConnection: close\\r\\n\\r\\n" + page()).encode()) await writer.drain() await writer.wait_closed() diff --git a/frontend/src/lib/openDeviceGateway.ts b/frontend/src/lib/openDeviceGateway.ts new file mode 100644 index 00000000..8c31c831 --- /dev/null +++ b/frontend/src/lib/openDeviceGateway.ts @@ -0,0 +1,83 @@ +/** + * openDeviceGateway + * + * Opens an emulated board's IoT-gateway page inside an in-app iframe panel + * (same tab) instead of a new browser tab. + * + * Why: the Raspberry Pi Pico W emulation runs in THIS browser tab, driven by + * requestAnimationFrame. Opening the gateway in a new tab backgrounds the + * emulation tab, the browser pauses its rAF, the simulated chip freezes, and + * the gateway can no longer reach the server running on it (the request times + * out / returns 502). Rendering the served page in an iframe keeps the + * emulation tab in the foreground, so the chip keeps running and answers. + * + * (The ESP32 doesn't need this — its server runs in QEMU on the backend, which + * is unaffected by browser tab visibility.) + * + * Plain DOM, no React state, so it can be invoked from anywhere (serial-monitor + * link, canvas WiFi badge) without threading props. + */ + +const OVERLAY_ID = 'velxio-device-gateway-overlay'; + +export function openDeviceGateway(url: string): void { + if (typeof document === 'undefined') return; + document.getElementById(OVERLAY_ID)?.remove(); + + const overlay = document.createElement('div'); + overlay.id = OVERLAY_ID; + overlay.style.cssText = + 'position:fixed;inset:0;z-index:100000;background:rgba(0,0,0,0.55);' + + 'display:flex;align-items:center;justify-content:center;'; + + const panel = document.createElement('div'); + panel.style.cssText = + 'background:#1e1e1e;border:1px solid #3a3a3a;border-radius:10px;' + + 'width:min(440px,94vw);height:min(640px,90vh);display:flex;' + + 'flex-direction:column;overflow:hidden;box-shadow:0 12px 48px rgba(0,0,0,0.5);'; + + const bar = document.createElement('div'); + bar.style.cssText = + 'display:flex;align-items:center;justify-content:space-between;gap:12px;' + + 'padding:8px 12px;background:#252526;color:#ddd;' + + 'font:13px -apple-system,BlinkMacSystemFont,sans-serif;border-bottom:1px solid #3a3a3a;'; + + const title = document.createElement('span'); + title.textContent = 'Device web page (IoT Gateway)'; + title.style.cssText = 'font-weight:600;'; + + const right = document.createElement('div'); + right.style.cssText = 'display:flex;align-items:center;gap:14px;'; + + const reload = document.createElement('button'); + reload.type = 'button'; + reload.textContent = 'Reload'; + reload.style.cssText = 'background:none;border:none;color:#4fc3f7;cursor:pointer;font-size:13px;padding:0;'; + + const openTab = document.createElement('a'); + openTab.textContent = 'Open in tab'; + openTab.href = url; + openTab.target = '_blank'; + openTab.rel = 'noreferrer'; + openTab.style.cssText = 'color:#4fc3f7;text-decoration:none;font-size:13px;'; + + const close = document.createElement('button'); + close.type = 'button'; + close.textContent = 'Close'; + close.style.cssText = 'background:#3a3a3a;border:none;color:#fff;cursor:pointer;font-size:13px;border-radius:5px;padding:4px 10px;'; + + const iframe = document.createElement('iframe'); + iframe.src = url; + iframe.style.cssText = 'flex:1;border:none;width:100%;background:#fff;'; + + reload.onclick = () => { iframe.src = url; }; + const dismiss = () => overlay.remove(); + close.onclick = dismiss; + overlay.onclick = (e) => { if (e.target === overlay) dismiss(); }; + + right.append(reload, openTab, close); + bar.append(title, right); + panel.append(bar, iframe); + overlay.append(panel); + document.body.append(overlay); +}