mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-11 12:05:33 +08:00
Bridge dome light mode mapping and flightlab seat belt flow
This commit is contained in:
@@ -8,10 +8,8 @@ import type {FlightLabTelemetryState} from '../../../shared/data/flightlab/types
|
|||||||
type DomeLightMode = 'off' | 'white' | 'amber'
|
type DomeLightMode = 'off' | 'white' | 'amber'
|
||||||
|
|
||||||
const DOME_LIGHT_WEBHOOK_FALLBACK_URL = 'https://home.io.faktorxmensch.com/api/webhook/lidl_stab_3modi_8492'
|
const DOME_LIGHT_WEBHOOK_FALLBACK_URL = 'https://home.io.faktorxmensch.com/api/webhook/lidl_stab_3modi_8492'
|
||||||
const DOME_LIGHT_ON_MODES: DomeLightMode[] = ['white', 'amber']
|
|
||||||
|
|
||||||
const nextDomeLightOnModeIndexByToken = new Map<string, number>()
|
const lastDomeLightModeByToken = new Map<string, DomeLightMode | null>()
|
||||||
const lastDomeLightStateByToken = new Map<string, boolean | null>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives telemetry data from an external bridge application.
|
* Receives telemetry data from an external bridge application.
|
||||||
@@ -24,7 +22,22 @@ const lastDomeLightStateByToken = new Map<string, boolean | null>()
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** Map raw bridge field names to FlightLabTelemetryState keys */
|
/** Map raw bridge field names to FlightLabTelemetryState keys */
|
||||||
|
function parseBooleanTelemetryValue(value: unknown): boolean | null {
|
||||||
|
if (typeof value === 'boolean') return value
|
||||||
|
if (typeof value === 'number') return value !== 0
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toLowerCase()
|
||||||
|
if (['1', 'true', 'on', 'yes'].includes(normalized)) return true
|
||||||
|
if (['0', 'false', 'off', 'no'].includes(normalized)) return false
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function mapBridgeTelemetry(raw: Record<string, any>): FlightLabTelemetryState {
|
function mapBridgeTelemetry(raw: Record<string, any>): FlightLabTelemetryState {
|
||||||
|
const seatBeltSignsValue = parseBooleanTelemetryValue(
|
||||||
|
raw.seat_belt_signs ?? raw.SEAT_BELT_SIGNS ?? raw.seatbelt_signs ?? raw.seat_belts_signs,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
AIRSPEED_INDICATED: raw.ias_kt ?? raw.AIRSPEED_INDICATED ?? 0,
|
AIRSPEED_INDICATED: raw.ias_kt ?? raw.AIRSPEED_INDICATED ?? 0,
|
||||||
AIRSPEED_TRUE: raw.tas_kt ?? raw.AIRSPEED_TRUE ?? 0,
|
AIRSPEED_TRUE: raw.tas_kt ?? raw.AIRSPEED_TRUE ?? 0,
|
||||||
@@ -39,6 +52,7 @@ function mapBridgeTelemetry(raw: Record<string, any>): FlightLabTelemetryState {
|
|||||||
GEAR_HANDLE_POSITION: !!(raw.gear_handle ?? raw.GEAR_HANDLE_POSITION ?? false),
|
GEAR_HANDLE_POSITION: !!(raw.gear_handle ?? raw.GEAR_HANDLE_POSITION ?? false),
|
||||||
FLAPS_HANDLE_INDEX: raw.flaps_index ?? raw.FLAPS_HANDLE_INDEX ?? 0,
|
FLAPS_HANDLE_INDEX: raw.flaps_index ?? raw.FLAPS_HANDLE_INDEX ?? 0,
|
||||||
BRAKE_PARKING_POSITION: !!(raw.parking_brake ?? raw.BRAKE_PARKING_POSITION ?? false),
|
BRAKE_PARKING_POSITION: !!(raw.parking_brake ?? raw.BRAKE_PARKING_POSITION ?? false),
|
||||||
|
...(seatBeltSignsValue === null ? {} : {SEAT_BELT_SIGNS: seatBeltSignsValue}),
|
||||||
AUTOPILOT_MASTER: !!(raw.autopilot_master ?? raw.AUTOPILOT_MASTER ?? false),
|
AUTOPILOT_MASTER: !!(raw.autopilot_master ?? raw.AUTOPILOT_MASTER ?? false),
|
||||||
TRANSPONDER_CODE: raw.transponder_code ?? raw.TRANSPONDER_CODE ?? 0,
|
TRANSPONDER_CODE: raw.transponder_code ?? raw.TRANSPONDER_CODE ?? 0,
|
||||||
ADF_ACTIVE_FREQUENCY: raw.adf_active_freq ?? raw.ADF_ACTIVE_FREQUENCY ?? 0,
|
ADF_ACTIVE_FREQUENCY: raw.adf_active_freq ?? raw.ADF_ACTIVE_FREQUENCY ?? 0,
|
||||||
@@ -46,42 +60,47 @@ function mapBridgeTelemetry(raw: Record<string, any>): FlightLabTelemetryState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveBooleanValue(value: unknown): boolean | null {
|
||||||
|
if (typeof value === 'boolean') return value
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
if (value === 1) return true
|
||||||
|
if (value === 0) return false
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toLowerCase()
|
||||||
|
if (normalized === 'true' || normalized === '1') return true
|
||||||
|
if (normalized === 'false' || normalized === '0') return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function resolveDomeLightValue(raw: Record<string, any>): boolean | null {
|
function resolveDomeLightValue(raw: Record<string, any>): boolean | null {
|
||||||
const value = raw.dome_light ?? raw.DOME_LIGHT
|
return resolveBooleanValue(raw.dome_light ?? raw.DOME_LIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof value === 'boolean') return value
|
function resolveNavLogoLightsValue(raw: Record<string, any>): boolean | null {
|
||||||
if (typeof value === 'number') {
|
return resolveBooleanValue(raw.nav_logo_lights ?? raw.NAV_LOGO_LIGHTS)
|
||||||
if (value === 1) return true
|
}
|
||||||
if (value === 0) return false
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const normalized = value.trim().toLowerCase()
|
|
||||||
if (normalized === 'true' || normalized === '1') return true
|
|
||||||
if (normalized === 'false' || normalized === '0') return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
export function resolveDomeLightMode(raw: Record<string, any>): DomeLightMode | null {
|
||||||
|
const domeLight = resolveDomeLightValue(raw)
|
||||||
|
if (domeLight === null) return null
|
||||||
|
if (!domeLight) return 'off'
|
||||||
|
|
||||||
|
const navLogoLights = resolveNavLogoLightsValue(raw)
|
||||||
|
return navLogoLights ? 'white' : 'amber'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function forwardDomeLightToWebhook(raw: Record<string, any>, webhookUrl: string, stateKey: string) {
|
async function forwardDomeLightToWebhook(raw: Record<string, any>, webhookUrl: string, stateKey: string) {
|
||||||
const domeLight = resolveDomeLightValue(raw)
|
const mode = resolveDomeLightMode(raw)
|
||||||
const lastDomeLightState = lastDomeLightStateByToken.get(stateKey) ?? null
|
const lastMode = lastDomeLightModeByToken.get(stateKey) ?? null
|
||||||
|
|
||||||
if (domeLight === null) return
|
if (mode === null) return
|
||||||
if (lastDomeLightState === domeLight) return
|
if (lastMode === mode) return
|
||||||
|
|
||||||
let nextDomeLightOnModeIndex = nextDomeLightOnModeIndexByToken.get(stateKey) ?? 0
|
try {
|
||||||
|
const response = await fetch(webhookUrl, {
|
||||||
const mode: DomeLightMode = domeLight
|
|
||||||
? DOME_LIGHT_ON_MODES[nextDomeLightOnModeIndex % DOME_LIGHT_ON_MODES.length]!
|
|
||||||
: 'off'
|
|
||||||
|
|
||||||
if (domeLight) {
|
|
||||||
nextDomeLightOnModeIndex = (nextDomeLightOnModeIndex + 1) % DOME_LIGHT_ON_MODES.length
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(webhookUrl, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
@@ -89,27 +108,26 @@ async function forwardDomeLightToWebhook(raw: Record<string, any>, webhookUrl: s
|
|||||||
body: JSON.stringify({mode}),
|
body: JSON.stringify({mode}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const responseBody = await response.text().catch(() => '')
|
const responseBody = await response.text().catch(() => '')
|
||||||
console.error(
|
console.error(
|
||||||
`\x1b[31m[bridge:dome]\x1b[0m webhook failed status=\x1b[91m${response.status}\x1b[0m mode=\x1b[93m${mode}\x1b[0m body=${responseBody.slice(0, 180)}`,
|
`\x1b[31m[bridge:dome]\x1b[0m webhook failed status=\x1b[91m${response.status}\x1b[0m mode=\x1b[93m${mode}\x1b[0m body=${responseBody.slice(0, 180)}`,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
console.info(
|
const domeLight = resolveDomeLightValue(raw)
|
||||||
`\x1b[36m[bridge:dome]\x1b[0m dome_light=\x1b[92m${String(domeLight)}\x1b[0m mode=\x1b[93m${mode}\x1b[0m`,
|
const navLogoLights = resolveNavLogoLightsValue(raw)
|
||||||
)
|
console.info(
|
||||||
}
|
`\x1b[36m[bridge:dome]\x1b[0m dome_light=\x1b[92m${String(domeLight)}\x1b[0m nav_logo_lights=\x1b[92m${String(navLogoLights)}\x1b[0m mode=\x1b[93m${mode}\x1b[0m`,
|
||||||
} catch (error) {
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`\x1b[31m[bridge:dome]\x1b[0m webhook request failed mode=\x1b[93m${mode}\x1b[0m`,
|
`\x1b[31m[bridge:dome]\x1b[0m webhook request failed mode=\x1b[93m${mode}\x1b[0m`,
|
||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
lastDomeLightStateByToken.set(stateKey, domeLight)
|
lastDomeLightModeByToken.set(stateKey, mode)
|
||||||
if (domeLight) {
|
}
|
||||||
nextDomeLightOnModeIndexByToken.set(stateKey, nextDomeLightOnModeIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
@@ -128,7 +146,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const telemetry = body && typeof body === 'object' ? (body as Record<string, any>) : {}
|
const telemetry = body && typeof body === 'object' ? (body as Record<string, any>) : {}
|
||||||
const telemetryKeys = Object.keys(telemetry)
|
const telemetryKeys = Object.keys(telemetry)
|
||||||
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`)
|
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`)
|
||||||
// console.table( telemetryKeys.reduce((acc, key) => { acc[key] = telemetry[key]; return acc }, {} as Record<string, any>) )
|
console.table( telemetryKeys.reduce((acc, key) => { acc[key] = telemetry[key]; return acc }, {} as Record<string, any>) )
|
||||||
|
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const domeLightWebhookUrl = String(runtimeConfig.domeLightWebhookUrl || '').trim() || DOME_LIGHT_WEBHOOK_FALLBACK_URL
|
const domeLightWebhookUrl = String(runtimeConfig.domeLightWebhookUrl || '').trim() || DOME_LIGHT_WEBHOOK_FALLBACK_URL
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface FlightLabTelemetryState {
|
|||||||
GEAR_HANDLE_POSITION: boolean // true = down, false = up
|
GEAR_HANDLE_POSITION: boolean // true = down, false = up
|
||||||
FLAPS_HANDLE_INDEX: number // 0-4 for A320
|
FLAPS_HANDLE_INDEX: number // 0-4 for A320
|
||||||
BRAKE_PARKING_POSITION: boolean // true = set, false = released
|
BRAKE_PARKING_POSITION: boolean // true = set, false = released
|
||||||
|
SEAT_BELT_SIGNS?: boolean // true = on, false = off (if provided by bridge)
|
||||||
AUTOPILOT_MASTER: boolean
|
AUTOPILOT_MASTER: boolean
|
||||||
TRANSPONDER_CODE: number // squawk code (0-7777 octal)
|
TRANSPONDER_CODE: number // squawk code (0-7777 octal)
|
||||||
ADF_ACTIVE_FREQUENCY: number // Hz
|
ADF_ACTIVE_FREQUENCY: number // Hz
|
||||||
|
|||||||
24
tests/server/bridgeDataDomeLight.test.ts
Normal file
24
tests/server/bridgeDataDomeLight.test.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, it } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
import { resolveDomeLightMode } from '~~/server/api/bridge/data.post'
|
||||||
|
|
||||||
|
describe('bridge dome light mode mapping', () => {
|
||||||
|
it('maps all dome/nav combinations to expected mode', () => {
|
||||||
|
assert.equal(resolveDomeLightMode({ dome_light: false, nav_logo_lights: false }), 'off')
|
||||||
|
assert.equal(resolveDomeLightMode({ dome_light: false, nav_logo_lights: true }), 'off')
|
||||||
|
assert.equal(resolveDomeLightMode({ dome_light: true, nav_logo_lights: false }), 'amber')
|
||||||
|
assert.equal(resolveDomeLightMode({ dome_light: true, nav_logo_lights: true }), 'white')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('supports numeric and string boolean payloads', () => {
|
||||||
|
assert.equal(resolveDomeLightMode({ dome_light: 1, nav_logo_lights: 0 }), 'amber')
|
||||||
|
assert.equal(resolveDomeLightMode({ dome_light: '1', nav_logo_lights: '1' }), 'white')
|
||||||
|
assert.equal(resolveDomeLightMode({ dome_light: 'false', nav_logo_lights: 'true' }), 'off')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null when dome_light is missing or invalid', () => {
|
||||||
|
assert.equal(resolveDomeLightMode({ nav_logo_lights: true }), null)
|
||||||
|
assert.equal(resolveDomeLightMode({ dome_light: 'invalid', nav_logo_lights: true }), null)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user