@@ -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,
+ }
+})