From dc36a4b63f511f6f87ed078d780c0a9d0041808b Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Sun, 15 Feb 2026 16:16:42 +0100 Subject: [PATCH] Connect bridge telemetry to FlightLab with auto-advance triggers Map raw bridge fields (ias_kt, on_ground, etc.) to FlightLabTelemetryState format, add direct telemetry polling endpoint for solo mode, and show sim condition panel in sidebar regardless of auto-advance toggle state. Co-Authored-By: Claude Opus 4.6 --- app/pages/flightlab/takeoff.vue | 40 +++++++++++++++++- server/api/bridge/data.post.ts | 59 ++++++++++++--------------- server/api/flightlab/telemetry.get.ts | 20 +++++++++ 3 files changed, 85 insertions(+), 34 deletions(-) create mode 100644 server/api/flightlab/telemetry.get.ts diff --git a/app/pages/flightlab/takeoff.vue b/app/pages/flightlab/takeoff.vue index be567db..a715a64 100644 --- a/app/pages/flightlab/takeoff.vue +++ b/app/pages/flightlab/takeoff.vue @@ -90,7 +90,7 @@
@@ -451,6 +451,40 @@ const showRestartConfirm = ref(false) const showDetails = ref(false) const sidebarOpen = ref(false) +// --- Direct Bridge Telemetry Polling (solo mode, no WS session) --- +let telemetryPollInterval: ReturnType | null = null + +function startTelemetryPolling() { + stopTelemetryPolling() + if (!authStore.accessToken || sync.isConnected.value) return + telemetryPollInterval = setInterval(async () => { + try { + const res = await $fetch<{ telemetry: FlightLabTelemetryState | null }>('/api/flightlab/telemetry', { + headers: { Authorization: `Bearer ${authStore.accessToken}` }, + }) + if (res.telemetry) { + engine.updateTelemetry(res.telemetry) + } + } catch {} + }, 500) +} + +function stopTelemetryPolling() { + if (telemetryPollInterval) { + clearInterval(telemetryPollInterval) + telemetryPollInterval = null + } +} + +// Start polling when phase has simConditions (solo mode only) +watch(() => engine.currentPhase.value, (phase) => { + if (phase?.simConditions && !sync.isConnected.value) { + startTelemetryPolling() + } else { + stopTelemetryPolling() + } +}, { immediate: true }) + const currentPhase = computed(() => engine.currentPhase.value) // Main phase IDs for sidebar stepper @@ -663,7 +697,8 @@ async function handleJoin() { if (joinCode.value.length !== 4) return try { await sync.joinSession(joinCode.value) - // Subscribe to telemetry after joining + // Switch to WS-based telemetry + stopTelemetryPolling() if (authStore.user?.id) { sync.subscribeTelemetry(authStore.user.id) } @@ -736,6 +771,7 @@ onMounted(async () => { }) onBeforeUnmount(() => { + stopTelemetryPolling() engine.cleanup() audio.dispose() sync.disconnect() diff --git a/server/api/bridge/data.post.ts b/server/api/bridge/data.post.ts index f538e1b..037706d 100644 --- a/server/api/bridge/data.post.ts +++ b/server/api/bridge/data.post.ts @@ -2,41 +2,36 @@ import { createError, defineEventHandler, readBody } from 'h3' import { BridgeToken } from '../../models/BridgeToken' import { getBridgeTokenFromHeader } from '../../utils/bridge' import { flightlabTelemetryStore } from '../../utils/flightlabTelemetry' +import type { FlightLabTelemetryState } from '../../../shared/data/flightlab/types' /** - * Receives MSFS SimConnect telemetry data from an external bridge application. + * Receives telemetry data from an external bridge application. * - * The bridge should POST telemetry data with an x-bridge-token header so we - * can route the data to the correct FlightLab WebSocket session. - * - * ────────────────────────────────────────── - * EXAMPLE REQUEST (from SimBridge app): - * ────────────────────────────────────────── + * The bridge sends raw SimConnect-style fields which are mapped to the + * FlightLabTelemetryState format before storage. * * POST /api/bridge/data * x-bridge-token: - * Content-Type: application/json - * - * { - * "AIRSPEED_INDICATED": 145.2, - * "GROUND_VELOCITY": 142.8, - * "VERTICAL_SPEED": 0, - * "PLANE_ALTITUDE": 364, - * "PLANE_PITCH_DEGREES": 0.5, - * "TURB_ENG_N1_1": 87.3, - * "TURB_ENG_N1_2": 86.9, - * "SIM_ON_GROUND": true, - * "GEAR_HANDLE_POSITION": true, - * "FLAPS_HANDLE_INDEX": 2, - * "BRAKE_PARKING_POSITION": false, - * "AUTOPILOT_MASTER": false - * } - * - * ────────────────────────────────────────── - * RESPONSE: 204 No Content (success) - * 401 Unauthorized (missing/invalid token) - * ────────────────────────────────────────── */ + +/** Map raw bridge field names to FlightLabTelemetryState keys */ +function mapBridgeTelemetry(raw: Record): FlightLabTelemetryState { + return { + AIRSPEED_INDICATED: raw.ias_kt ?? raw.AIRSPEED_INDICATED ?? 0, + GROUND_VELOCITY: raw.groundspeed_kt ?? raw.GROUND_VELOCITY ?? 0, + VERTICAL_SPEED: raw.vertical_speed_fpm ?? raw.VERTICAL_SPEED ?? 0, + PLANE_ALTITUDE: raw.altitude_ft_indicated ?? raw.altitude_ft_true ?? raw.PLANE_ALTITUDE ?? 0, + PLANE_PITCH_DEGREES: raw.pitch_deg ?? raw.PLANE_PITCH_DEGREES ?? 0, + TURB_ENG_N1_1: raw.n1_pct ?? raw.TURB_ENG_N1_1 ?? 0, + TURB_ENG_N1_2: raw.n1_pct_2 ?? raw.TURB_ENG_N1_2 ?? 0, + SIM_ON_GROUND: raw.on_ground ?? raw.SIM_ON_GROUND ?? false, + GEAR_HANDLE_POSITION: raw.gear_handle ?? raw.GEAR_HANDLE_POSITION ?? false, + FLAPS_HANDLE_INDEX: raw.flaps_index ?? raw.FLAPS_HANDLE_INDEX ?? 0, + BRAKE_PARKING_POSITION: raw.parking_brake ?? raw.BRAKE_PARKING_POSITION ?? false, + AUTOPILOT_MASTER: raw.autopilot_master ?? raw.AUTOPILOT_MASTER ?? false, + } +} + export default defineEventHandler(async (event) => { const bridgeToken = getBridgeTokenFromHeader(event) if (!bridgeToken) { @@ -52,12 +47,12 @@ export default defineEventHandler(async (event) => { const body = await readBody(event) const telemetryKeys = body && typeof body === 'object' ? Object.keys(body as Record) : [] console.info( - `\x1b[35m[bridge:data]\x1b[0m token=\x1b[96m${bridgeToken.slice(0, 6)}...\x1b[0m user=\x1b[92m${userId}\x1b[0m telemetryKeys=\x1b[92m${telemetryKeys.length}\x1b[0m payload=`, - body, + `\x1b[35m[bridge:data]\x1b[0m token=\x1b[96m${bridgeToken.slice(0, 6)}...\x1b[0m user=\x1b[92m${userId}\x1b[0m telemetryKeys=\x1b[92m${telemetryKeys.length}\x1b[0m`, ) - // Store telemetry and broadcast to WebSocket subscribers - flightlabTelemetryStore.update(userId, body) + // Map raw bridge fields to FlightLab format and store + const mapped = mapBridgeTelemetry(body) + flightlabTelemetryStore.update(userId, mapped) event.node.res.statusCode = 204 }) diff --git a/server/api/flightlab/telemetry.get.ts b/server/api/flightlab/telemetry.get.ts new file mode 100644 index 0000000..f9a3138 --- /dev/null +++ b/server/api/flightlab/telemetry.get.ts @@ -0,0 +1,20 @@ +import { requireUserSession } from '../../utils/auth' +import { flightlabTelemetryStore } from '../../utils/flightlabTelemetry' + +/** + * Returns the latest bridge telemetry for the authenticated user. + * Used by FlightLab in solo mode (no WebSocket session) to poll telemetry. + * + * GET /api/flightlab/telemetry + * Authorization: Bearer + */ +export default defineEventHandler(async (event) => { + const user = await requireUserSession(event) + const userId = String(user._id) + const telemetry = flightlabTelemetryStore.get(userId) + + return { + telemetry, + timestamp: telemetry?.timestamp ?? null, + } +})