mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-04 16:22:48 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -90,7 +90,7 @@
|
||||
|
||||
<!-- SimBridge Conditions Panel -->
|
||||
<div
|
||||
v-if="engine.autoAdvanceEnabled.value && currentPhase?.simConditions"
|
||||
v-if="currentPhase?.simConditions"
|
||||
class="border-t border-white/5 p-4 shrink-0"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
@@ -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<typeof setInterval> | 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()
|
||||
|
||||
@@ -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: <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<string, any>): 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<string, unknown>) : []
|
||||
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
|
||||
})
|
||||
|
||||
20
server/api/flightlab/telemetry.get.ts
Normal file
20
server/api/flightlab/telemetry.get.ts
Normal file
@@ -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 <access-token>
|
||||
*/
|
||||
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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user