mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-06 01:06:28 +08:00
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:
67
tests/shared/autoTune.test.ts
Normal file
67
tests/shared/autoTune.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { AUTO_TUNE_DELAY_MS, planAutoTune } from '~~/shared/utils/autoTune'
|
||||
|
||||
const plan = (over: Partial<Parameters<typeof planAutoTune>[0]> = {}) =>
|
||||
planAutoTune({ enabled: true, active: '118.700', expected: '121.800', ...over })
|
||||
|
||||
describe('planAutoTune', () => {
|
||||
it('tunes to the frequency the new state expects', () => {
|
||||
const result = plan()
|
||||
assert.equal(result?.frequency, '121.800')
|
||||
assert.equal(result?.delayMs, AUTO_TUNE_DELAY_MS)
|
||||
})
|
||||
|
||||
it('announces the change before making it', () => {
|
||||
assert.equal(plan()?.announcement, 'OpenSquawk changing frequency to 121.800')
|
||||
})
|
||||
|
||||
it('does nothing when the setting is off', () => {
|
||||
assert.equal(plan({ enabled: false }), null)
|
||||
})
|
||||
|
||||
it('does nothing when already on the expected frequency', () => {
|
||||
assert.equal(plan({ active: '121.800' }), null)
|
||||
})
|
||||
|
||||
it('treats a comma decimal and stray spacing as the same frequency', () => {
|
||||
assert.equal(plan({ active: '121,800', expected: ' 121.800 ' }), null)
|
||||
})
|
||||
|
||||
it('leaves the pilot alone on any frequency the position publishes', () => {
|
||||
const result = plan({
|
||||
active: '118.500',
|
||||
expected: '118.700',
|
||||
accepted: ['118.700', '118.500'],
|
||||
})
|
||||
assert.equal(result, null)
|
||||
})
|
||||
|
||||
it('tunes when on none of the published frequencies', () => {
|
||||
const result = plan({
|
||||
active: '121.800',
|
||||
expected: '118.700',
|
||||
accepted: ['118.700', '118.500'],
|
||||
})
|
||||
assert.equal(result?.frequency, '118.700')
|
||||
})
|
||||
|
||||
it('does nothing where the state expects no frequency', () => {
|
||||
assert.equal(plan({ expected: undefined }), null)
|
||||
assert.equal(plan({ expected: '' }), null)
|
||||
})
|
||||
|
||||
// The two cases that must never tune. Both leave the session resting on the
|
||||
// state that still expects the frequency already dialled in, so "expected
|
||||
// equals active" is what actually guards them.
|
||||
it('does not tune while the frequency readback is still owed', () => {
|
||||
// Still on the handoff readback state: it belongs to the current position.
|
||||
assert.equal(plan({ active: '118.700', expected: '118.700' }), null)
|
||||
})
|
||||
|
||||
it('does not tune after a wrong frequency readback', () => {
|
||||
// A wrong readback loops back to the same state — same frequency, no change.
|
||||
assert.equal(plan({ active: '118.700', expected: '118.700' }), null)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user