feat(live-atc): optional auto-tune after a frequency handoff

Tuning is manual, so after a handoff nothing the pilot says goes through until
they dial the new frequency in. With the setting on, the radio does it for them
three seconds after the handoff was accepted, announcing "OpenSquawk changing
frequency to …" first so it is never a surprise.

Whether a change is due is decided from state rather than from an event, which
is what makes the two must-not-tune cases safe without a special case: a
frequency readback that was wrong and one not yet given both leave the session
on a state that still expects the frequency already dialled in, so nothing is
due. A pending change is dropped if the session ends or the pilot reaches for
the radio themselves — theirs wins.

Off by default: working the radio is part of what is being practised, so
handing it to the aircraft has to be a deliberate choice. The decision itself
is a pure function and covered by tests; the announcement goes through the same
speech and comm-log path as everything else, so the browser sim and the bridge
both see the tuned frequency the way they already do for a manual change.

Also folds the two copies of normalizedFrequencyValue into one in shared/, so
the auto-tune logic can compare frequencies without a composable import.
This commit is contained in:
itsrubberduck
2026-07-27 00:51:53 +02:00
parent a40ce0a7eb
commit 1bd1aac76d
11 changed files with 239 additions and 10 deletions

View File

@@ -5,9 +5,9 @@ import { normalizeManualFreq } from '../../../../shared/utils/frequency'
import {
FREQUENCY_PLACEHOLDER,
FREQ_ROLE_LABEL,
normalizedFrequencyValue,
type DisplayAirportFrequencyEntry,
} from '~/composables/useFrequencyPresets'
import { normalizedFrequencyValue } from '~~/shared/utils/frequency'
const props = defineProps<{
active: string

View File

@@ -20,6 +20,7 @@ const debugMode = defineModel<boolean>('debugMode', { required: true })
const prerecEnabled = defineModel<boolean>('prerecEnabled', { required: true })
const prerecSeconds = defineModel<number>('prerecSeconds', { required: true })
const aiTrafficEnabled = defineModel<boolean>('aiTrafficEnabled', { required: true })
const autoTuneEnabled = defineModel<boolean>('autoTuneEnabled', { required: true })
/**
* Shown while AI traffic is on. These are deliberate v1 boundaries from the
@@ -138,6 +139,17 @@ const AI_TRAFFIC_LIMITS = [
band sounds alive while you fly.
</p>
</div>
<v-switch
v-model="autoTuneEnabled"
color="cyan"
inset
label="Auto-tune after handoff"
hide-details
/>
<p class="text-[11px] text-white/50 -mt-1">
Dials in the new frequency a few seconds after a handoff, announcing it
first. Tune the radio yourself at any point and that takes precedence.
</p>
<v-switch
v-model="aiTrafficEnabled"
color="cyan"