Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
126616f26This file is pinned to the selected release. Download the original to open it in your design tool.
import { loadPreferenceConfig } from 'loadPreference'
import { DOMAIN, PREF_KEYS } from 'consts'
import { getLocalizationLanguage, normalizeLocale, type SupportedLocale, setLocalizationLanguage } from 'localization'
import { NetworkConnectionState, type NetworkConnectionState as NetworkState } from 'network-state'
import Preference from 'preference'
import { PreferenceServer } from 'preference-server'
import { createSettingsNetworkEntries, type RawWiFiScanResult, type SettingsNetworkEntry } from 'settings-network-list'
import { createInitialSettingsStatus } from 'settings-status'
import {
type SettingsApplication,
SettingsStatusValue,
type SettingsViewContext,
SettingsViewId,
type SettingsViewInstance,
settingsViews,
} from 'settings-view'
import Speaker from 'speaker'
import { clearStoredWiFiCredentials, connectStoredWiFi, stopStoredWiFiConnection } from 'stored-wifi'
import { applyTimezone as applySystemTimezone, normalizeTimezoneId, type TimezoneId } from 'timezone-settings'
import { canonicalizeVolume, resolveVolumePreference } from 'volume-model'
import { VolumePreviewQueue } from 'volume-preview'
import { scanWiFiNetworks } from 'wifi-scan'
import type { WiFiScanSession } from 'wifi-scan-types'
export type SetupModeResult = 'back' | 'boot'
const VOLUME_PREVIEW_FREQUENCY_HZ = 1000
const VOLUME_PREVIEW_DURATION_MS = 120
function settingsWifiStatusFromNetworkState(state: NetworkState): SettingsStatusValue {
switch (state) {
case NetworkConnectionState.SCANNING:
return SettingsStatusValue.SCANNING
case NetworkConnectionState.CONNECTING:
return SettingsStatusValue.CONNECTING
case NetworkConnectionState.SYNCING_TIME:
return SettingsStatusValue.SYNCING_TIME
case NetworkConnectionState.CONNECTED:
return SettingsStatusValue.CONNECTED
case NetworkConnectionState.RECONNECTING:
return SettingsStatusValue.RECONNECTING
case NetworkConnectionState.FAILED:
return SettingsStatusValue.FAILED
case NetworkConnectionState.CLOSED:
case NetworkConnectionState.IDLE:
return SettingsStatusValue.NOT_CONNECTED
}
return SettingsStatusValue.NOT_CONNECTED
}
export function startSetupMode(application: SettingsApplication): Promise<SetupModeResult> {
return new Promise((resolve) => {
const preferences = loadPreferenceConfig()
preferences.time.timezone = applySystemTimezone(preferences.time.timezone)
preferences.tts.volume = canonicalizeVolume(preferences.tts.volume)
const status = createInitialSettingsStatus(preferences)
const effectiveValues = Object.fromEntries(
PREF_KEYS.flatMap(([domain, key]) => {
const value = preferences[domain]?.[key]
return value == null ? [] : [[`${domain}.${key}`, value]]
}),
)
const viewState = {
status,
networks: [] as SettingsNetworkEntry[],
selectedSSID: '',
language: getLocalizationLanguage(),
timezone: preferences.time.timezone as TimezoneId,
volume: preferences.tts.volume as number,
}
const volumeSpeaker = new Speaker({ volume: viewState.volume })
const volumePreviewQueue = new VolumePreviewQueue({
play: (volume) => volumeSpeaker.tone(VOLUME_PREVIEW_FREQUENCY_HZ, VOLUME_PREVIEW_DURATION_MS, volume),
onError: (error) => trace(`[settings] volume preview failed: ${String(error)}\n`),
})
let currentView: SettingsViewInstance | undefined
let currentViewId: SettingsViewId = SettingsViewId.MENU
let scanSession: WiFiScanSession | undefined
let scanResults: RawWiFiScanResult[] = []
let preferenceServer: PreferenceServer | undefined
let finished = false
const viewContext: SettingsViewContext = {
state: viewState,
actions: {
exit: () => finish('back'),
boot: () => finish('boot'),
bootOffline: clearWiFiAndBootOffline,
navigate: showView,
scanWifi: scanNetworks,
cancelWifiScan,
selectWifiNetwork: selectNetwork,
submitWifiPassword,
selectLanguage: (locale: SupportedLocale) => applyLanguage(locale, true),
saveTimezone,
saveVolume,
},
}
function finish(result: SetupModeResult) {
if (finished) return
finished = true
currentView?.dispose?.()
currentView = undefined
preferenceServer?.close?.()
if (result === 'back') stopStoredWiFiConnection()
void volumePreviewQueue.close().then(
() => resolve(result),
() => resolve(result),
)
}
function clearWiFiAndBootOffline() {
clearStoredWiFiCredentials()
status['wifi.ssid'] = ''
status['wifi.password'] = ''
status.wifi = SettingsStatusValue.NOT_CONNECTED
trace('[network] saved Wi-Fi credentials cleared for offline boot\n')
finish('boot')
}
function showView(id: SettingsViewId) {
currentView?.dispose?.()
const nextView = settingsViews[id].create(viewContext)
application.empty()
application.add(nextView.content)
currentView = nextView
currentViewId = id
currentView.update?.()
}
function updateCurrentView() {
currentView?.update?.()
}
function updateNetworkList() {
viewState.networks = createSettingsNetworkEntries(scanResults)
updateCurrentView()
}
function cancelWifiScan() {
scanSession?.close()
scanSession = undefined
if (status.wifi === SettingsStatusValue.SCANNING) status.wifi = SettingsStatusValue.NOT_CONNECTED
}
function applyLanguage(value: unknown, persist: boolean) {
const locale = setLocalizationLanguage(value)
status['ui.language'] = locale
viewState.language = locale
if (persist || normalizeLocale(value) !== locale) Preference.set(DOMAIN.ui, 'language', locale)
showView(currentViewId)
}
function applyTimezone(value: unknown, persist: boolean) {
const timezone = applySystemTimezone(value)
status['time.timezone'] = timezone
viewState.timezone = timezone
if (persist || normalizeTimezoneId(value) !== value) Preference.set(DOMAIN.time, 'timezone', timezone)
return timezone
}
function saveTimezone(value: TimezoneId) {
applyTimezone(value, true)
showView(SettingsViewId.MENU)
}
function applyVolume(value: unknown, persist: boolean, preview: boolean) {
const { volume, storageValue, needsWrite } = resolveVolumePreference(value, viewState.volume)
status['tts.volume'] = volume
viewState.volume = volume
if (persist || needsWrite) Preference.set(DOMAIN.tts, 'volume', storageValue)
updateCurrentView()
if (preview) volumePreviewQueue.request(volume)
return volume
}
function saveVolume(value: number) {
applyVolume(value, true, true)
}
function scanNetworks() {
cancelWifiScan()
scanResults = []
status.wifi = SettingsStatusValue.SCANNING
updateCurrentView()
updateNetworkList()
let scanFinished = false
const nextScanSession = scanWiFiNetworks({
onFound: (result: RawWiFiScanResult) => {
scanResults.push(result)
updateNetworkList()
},
onComplete: () => {
scanFinished = true
scanSession = undefined
if (status.wifi === SettingsStatusValue.SCANNING) status.wifi = SettingsStatusValue.NOT_CONNECTED
updateCurrentView()
updateNetworkList()
},
onError: (message?: string) => {
scanFinished = true
scanSession = undefined
trace(`Wi-Fi scan failed${message ? `: ${message}` : ''}\n`)
status.wifi = SettingsStatusValue.FAILED
updateCurrentView()
},
})
scanSession = scanFinished ? undefined : nextScanSession
}
function selectNetwork(network: SettingsNetworkEntry) {
status['wifi.ssid'] = network.ssid
viewState.selectedSSID = network.ssid
Preference.set(DOMAIN.wifi, 'ssid', network.ssid)
showView(SettingsViewId.PASSWORD)
}
function submitWifiPassword(password: string) {
status['wifi.password'] = password
Preference.set(DOMAIN.wifi, 'password', password)
showView(SettingsViewId.WIFI)
testConnection()
}
function testConnection() {
if (status['wifi.ssid'].length === 0 || status['wifi.password'].length === 0) return
stopStoredWiFiConnection()
connectStoredWiFi({
ssid: status['wifi.ssid'],
password: status['wifi.password'],
onStateChanged: (state: NetworkState) => {
status.wifi = settingsWifiStatusFromNetworkState(state)
updateCurrentView()
},
onConnected: () => {
trace('connection complete\n')
status.wifi = SettingsStatusValue.CONNECTED
updateCurrentView()
},
onError: () => {
trace('connection failed\n')
status.wifi = SettingsStatusValue.FAILED
updateCurrentView()
},
})
}
showView(SettingsViewId.MENU)
preferenceServer = new PreferenceServer({
onPreferenceChanged: (key, value) => {
trace(`preference changed! ${key}\n`)
if (key === `${DOMAIN.ui}.language`) {
applyLanguage(value, false)
return
}
if (key === `${DOMAIN.time}.timezone`) {
applyTimezone(value, false)
showView(currentViewId)
return
}
if (key === `${DOMAIN.tts}.volume`) {
applyVolume(value, false, false)
return
}
status[key] = value
updateCurrentView()
},
onConnected: () => {
status.ble = SettingsStatusValue.CONNECTED
updateCurrentView()
},
onDisconnected: () => {
status.ble = SettingsStatusValue.NOT_CONNECTED
updateCurrentView()
},
keys: PREF_KEYS,
effectiveValues,
readOnlyKeys: preferences.driver.typeLocked === true ? ['driver.type'] : [],
})
})
}