fix(picow): open the IoT gateway in an in-app iframe, not a new tab
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.
This commit is contained in:
parent
3b8266a75f
commit
80d2086907
|
|
@ -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(
|
||||
<a
|
||||
key={i}
|
||||
href={gatewayUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={
|
||||
isPicoW
|
||||
? (e) => {
|
||||
// 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',
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -70,24 +70,27 @@ async def wifi_connect():
|
|||
print("IP:", ip)
|
||||
print("Open browser: http://%s/" % ip)
|
||||
|
||||
HTML = """<!DOCTYPE html>
|
||||
<html><body><h2>Pico W Async LED</h2>
|
||||
<button onclick="fetch('on')">ON</button>
|
||||
<button onclick="fetch('off')">OFF</button>
|
||||
</body></html>"""
|
||||
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 ("<!DOCTYPE html><html><body><h2>Pico W LED: %s</h2>"
|
||||
"<button onclick=\\"fetch('on').then(()=>location.reload())\\">ON</button> "
|
||||
"<button onclick=\\"fetch('off').then(()=>location.reload())\\">OFF</button>"
|
||||
"</body></html>") % 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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue