Fix OBS display and Start button not working
Three issues fixed:
1. Port mismatch: The sidecar reported the OBS port (8080) in the
ready event but the frontend needs the API port (8081). Now reports
the API port so WebSocket/REST connects to the right place.
2. Broadcast from wrong thread: Engine init fires state_changed from
a background thread, but _broadcast_control used get_event_loop()
which returns the wrong loop. Now captures the uvicorn event loop
at startup via on_event("startup").
3. Missed ready state: If the engine finishes before the WebSocket
client connects, the "ready" state_changed was never received.
Added status polling (GET /api/status) on WebSocket connect that
retries every 2s while appState is "initializing".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,11 +99,19 @@ class APIServer:
|
|||||||
|
|
||||||
self.controller.on_credits_low = on_credits_low
|
self.controller.on_credits_low = on_credits_low
|
||||||
|
|
||||||
|
def set_event_loop(self, loop: asyncio.AbstractEventLoop):
|
||||||
|
"""Set the event loop used for broadcasting (call from uvicorn startup)."""
|
||||||
|
self._event_loop = loop
|
||||||
|
|
||||||
def _broadcast_control(self, data: dict):
|
def _broadcast_control(self, data: dict):
|
||||||
"""Send a message to all connected /ws/control clients."""
|
"""Send a message to all connected /ws/control clients."""
|
||||||
if not self.control_connections:
|
if not self.control_connections:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
loop = getattr(self, '_event_loop', None)
|
||||||
|
if loop is None:
|
||||||
|
return
|
||||||
|
|
||||||
message = json.dumps(data)
|
message = json.dumps(data)
|
||||||
disconnected = []
|
disconnected = []
|
||||||
|
|
||||||
@@ -111,7 +119,7 @@ class APIServer:
|
|||||||
try:
|
try:
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
ws.send_text(message),
|
ws.send_text(message),
|
||||||
asyncio.get_event_loop(),
|
loop,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
disconnected.append(ws)
|
disconnected.append(ws)
|
||||||
@@ -124,6 +132,10 @@ class APIServer:
|
|||||||
app = self.app
|
app = self.app
|
||||||
ctrl = self.controller
|
ctrl = self.controller
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def on_startup():
|
||||||
|
self.set_event_loop(asyncio.get_event_loop())
|
||||||
|
|
||||||
# ── Status ─────────────────────────────────────────────
|
# ── Status ─────────────────────────────────────────────
|
||||||
|
|
||||||
@app.get("/api/status")
|
@app.get("/api/status")
|
||||||
|
|||||||
@@ -88,11 +88,16 @@ def main():
|
|||||||
# Create API server wrapping the controller
|
# Create API server wrapping the controller
|
||||||
api_server = APIServer(controller)
|
api_server = APIServer(controller)
|
||||||
|
|
||||||
# Determine actual port (web server may have shifted if port was in use)
|
# OBS display runs on the configured port, API server on port+1
|
||||||
actual_port = controller.actual_web_port or args.port
|
obs_port = controller.actual_web_port or args.port
|
||||||
|
api_port = obs_port + 1
|
||||||
|
|
||||||
# Print ready event so Tauri can discover the port
|
# Print ready event so Tauri can discover the API port
|
||||||
print(json.dumps({"event": "ready", "port": actual_port}), flush=True)
|
print(json.dumps({
|
||||||
|
"event": "ready",
|
||||||
|
"port": api_port,
|
||||||
|
"obs_port": obs_port,
|
||||||
|
}), flush=True)
|
||||||
|
|
||||||
# Run the API server (blocks)
|
# Run the API server (blocks)
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -104,7 +109,7 @@ def main():
|
|||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
api_server.app,
|
api_server.app,
|
||||||
host=args.host,
|
host=args.host,
|
||||||
port=actual_port + 1, # API on port+1, OBS display on the main port
|
port=api_port,
|
||||||
log_level="error",
|
log_level="error",
|
||||||
access_log=False,
|
access_log=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,6 +54,35 @@ async function apiFetch(path: string, options?: RequestInit): Promise<Response>
|
|||||||
return fetch(url, { ...options, headers });
|
return fetch(url, { ...options, headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Status polling ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let statusPollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
async function pollStatus() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(apiUrl("/api/status"));
|
||||||
|
if (resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.state) {
|
||||||
|
state.appState = data.state as AppState;
|
||||||
|
}
|
||||||
|
if (data.engine_device) {
|
||||||
|
state.deviceInfo = data.engine_device;
|
||||||
|
}
|
||||||
|
if (data.version) {
|
||||||
|
state.version = data.version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// API not ready yet, will retry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep polling every 2s while still initializing
|
||||||
|
if (state.appState === "initializing" && state.connectionState === "connected") {
|
||||||
|
statusPollTimer = setTimeout(pollStatus, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── WebSocket management ─────────────────────────────────────────────
|
// ── WebSocket management ─────────────────────────────────────────────
|
||||||
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
@@ -80,6 +109,9 @@ function _openSocket() {
|
|||||||
clearTimeout(reconnectTimer);
|
clearTimeout(reconnectTimer);
|
||||||
reconnectTimer = null;
|
reconnectTimer = null;
|
||||||
}
|
}
|
||||||
|
// Poll status to catch engine ready state that may have been
|
||||||
|
// missed (engine can finish before WebSocket connects)
|
||||||
|
pollStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
@@ -132,6 +164,10 @@ function _scheduleReconnect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function disconnect() {
|
function disconnect() {
|
||||||
|
if (statusPollTimer) {
|
||||||
|
clearTimeout(statusPollTimer);
|
||||||
|
statusPollTimer = null;
|
||||||
|
}
|
||||||
if (reconnectTimer) {
|
if (reconnectTimer) {
|
||||||
clearTimeout(reconnectTimer);
|
clearTimeout(reconnectTimer);
|
||||||
reconnectTimer = null;
|
reconnectTimer = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user