Loading files
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'] : [],
})
})
}
e14740673This file is pinned to the selected release. Download the original to open it in your design tool.