Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
7b6aa454bThis file is pinned to the selected release. Download the original to open it in your design tool.
import Modules from 'modules'
import { Robot, type Driver, type TTS, type Renderer } from 'robot'
import { RS30XDriver } from 'rs30x-driver'
import { SCServoDriver } from 'scservo-driver'
import { PWMServoDriver } from 'sg90-driver'
import { DynamixelDriver } from 'dynamixel-driver'
import { NoneDriver } from 'none-driver'
import { TTS as LocalTTS } from 'tts-local'
import { TTS as RemoteTTS } from 'tts-remote'
import { TTS as VoiceVoxTTS } from 'tts-voicevox'
import { TTS as VoiceVoxWebTTS } from 'tts-voicevox-web'
import { TTS as ElevenLabsTTS } from 'tts-elevenlabs'
import { TTS as OpenAITTS } from 'tts-openai'
import defaultMod, { type StackchanMod } from 'default-mods/mod'
import { Renderer as SimpleRenderer } from 'renderer-simple'
import { Renderer as ImageFaceRenderer } from 'renderer-image'
import { Renderer as DogFaceRenderer } from 'renderer-dog'
import { Renderer as SmallFaceRenderer } from 'renderer-small'
import { NetworkService } from 'network-service'
import Microphone from 'microphone'
import Tone from 'tone'
import Timer from 'timer'
import { asyncWait } from 'stackchan-util'
import loadPreferences from 'loadPreference'
import Led from 'led'
import { StartupScreen } from 'startup-screen'
// wrapper button class for simulator
class SimButton {
#button
onChanged
constructor(button) {
const self = this
this.#button = new button({
onPush() {
self.onChanged?.()
},
})
}
read() {
return this.#button.read() ?? 1
}
}
function createRobot() {
const drivers = new Map<string, new (param: unknown) => Driver>([
['scservo', SCServoDriver],
['dynamixel', DynamixelDriver],
['pwm', PWMServoDriver],
['rs30x', RS30XDriver],
['none', NoneDriver],
])
const ttsEngines = new Map<string, new (param: unknown) => TTS>([
['local', LocalTTS],
['remote', RemoteTTS],
['voicevox', VoiceVoxTTS],
['voicevox-web', VoiceVoxWebTTS],
['elevenlabs', ElevenLabsTTS],
['openai', OpenAITTS],
])
const renderers = new Map<string, new (param: unknown) => Renderer>([
['dog', DogFaceRenderer],
['simple', SimpleRenderer],
['image', ImageFaceRenderer],
['small-face', SmallFaceRenderer],
])
const errors: string[] = []
// Servo Driver
const driverPrefs = loadPreferences('driver')
const driverKey = driverPrefs.type ?? 'scservo'
const Driver = drivers.get(driverKey)
// TTS
const ttsPrefs = loadPreferences('tts')
const ttsKey = ttsPrefs.type ?? 'local'
const TTS = ttsEngines.get(ttsKey)
// Renderer
const rendererPrefs = loadPreferences('renderer')
const rendererKey = rendererPrefs.type ?? 'simple'
const Renderer = renderers.get(rendererKey)
if (!Driver || !TTS || !Renderer) {
for (const [key, klass] of [
[driverKey, Driver],
[ttsKey, TTS],
[rendererKey, Renderer],
]) {
if (klass == null) {
errors.push(`type "${key}" does not exist`)
}
}
throw new Error(errors.join('\n'))
}
const driver = new Driver(driverPrefs)
const renderer = new Renderer(rendererPrefs)
const tts = new TTS(ttsPrefs)
const button = globalThis.button
// TODO(@meganetaaan): screen.touch does not exist under Commodetto context. Is this check necessary?
interface GlobalEnvironment {
screen?: {
touch?: unknown
}
device?: {
sensor?: {
Touch?: unknown
}
}
}
const _globalEnv = globalThis as unknown as GlobalEnvironment
// const TouchConstructor = _globalEnv.device?.sensor?.Touch
// const touch = TouchConstructor ? new Touch(TouchConstructor) : undefined
const touch = undefined
const microphone = Modules.has('embedded:io/audio/in') ? new Microphone() : undefined
const tone = new Tone({ volume: ttsPrefs.volume })
const modConfig = Modules.has('mod/config')
? (Modules.importNow('mod/config') as {
led?: Record<string, unknown>
})
: {}
const configLed = {
...loadPreferences('led'),
...(modConfig.led ?? {}),
}
const led = Object.fromEntries(
Object.entries(configLed).map(([key, ledConfig]) => [
key,
new Led(ledConfig as { pin: number; length?: number; order?: string }),
]),
)
return new Robot({
driver,
renderer,
tts,
button,
touch,
tone,
microphone,
led,
})
}
async function checkAndConnectWiFi(startupScreen: StartupScreen) {
const wifiPrefs = loadPreferences('wifi')
if (wifiPrefs.ssid == null || wifiPrefs.password == null) {
return null
}
return new Promise<boolean | 'setup'>((resolve, reject) => {
let settled = false
let setupWatchTimer: ReturnType<typeof Timer.repeat> | undefined
const finish = (callback: (value: boolean | 'setup') => void, value: boolean | 'setup') => {
if (settled) return
settled = true
if (setupWatchTimer !== undefined) {
Timer.clear(setupWatchTimer)
setupWatchTimer = undefined
}
callback(value)
}
globalThis.network = new NetworkService({
ssid: wifiPrefs.ssid,
password: wifiPrefs.password,
})
setupWatchTimer = Timer.repeat(() => {
if (!startupScreen.isSetupRequested()) return
globalThis.network?.close?.()
finish(resolve, 'setup')
}, 100)
globalThis.network.connect(
() => finish(resolve, true),
(error) => {
if (startupScreen.isSetupRequested()) {
finish(resolve, 'setup')
return
}
if (settled) return
settled = true
if (setupWatchTimer !== undefined) {
Timer.clear(setupWatchTimer)
setupWatchTimer = undefined
}
reject(error)
},
)
})
}
async function main(startupScreen: StartupScreen) {
startupScreen.setStatus('Preparing startup...')
if (globalThis.Host?.Button && !globalThis.button) {
globalThis.button = {
a: new SimButton(globalThis.Host.Button.a),
b: new SimButton(globalThis.Host.Button.b),
c: new SimButton(globalThis.Host.Button.c),
}
}
startupScreen.setStatus('Checking Wi-Fi settings...')
await asyncWait(100)
if (startupScreen.isSetupRequested()) {
startupScreen.setStatus('Skipping Wi-Fi for setup mode')
} else {
startupScreen.setStatus('Connecting to Wi-Fi...')
const wifiResult = await checkAndConnectWiFi(startupScreen).catch((msg) => {
startupScreen.setStatus(`Wi-Fi connection failed: ${String(msg)}`)
return 'failed'
})
if (wifiResult === true) {
startupScreen.setStatus('Wi-Fi connected')
} else if (wifiResult === 'setup') {
startupScreen.setStatus('Skipping Wi-Fi for setup mode')
} else if (wifiResult === null) {
startupScreen.setStatus('Wi-Fi skipped or unavailable')
}
}
startupScreen.setStatus('Loading mod...')
let { onRobotCreated, onLaunch } = defaultMod
if (Modules.has('mod')) {
const mod = Modules.importNow('mod') as StackchanMod
onRobotCreated = mod.onRobotCreated ?? onRobotCreated
onLaunch = mod.onLaunch ?? onLaunch
}
const forceSetup = startupScreen.isSetupRequested()
startupScreen.setStatus(forceSetup ? 'Opening setup screen...' : 'Running launch hooks...')
const shouldRobotCreate = await (onLaunch?.({ forceSetup }) ?? true)
if (shouldRobotCreate) {
startupScreen.setStatus('Initializing robot...')
const robot = createRobot()
startupScreen.setStatus('Starting robot callbacks...')
await onRobotCreated?.(robot, globalThis.device)
startupScreen.setStatus('Startup complete')
} else {
startupScreen.setStatus('Startup menu is active')
}
}
const startupScreen = new StartupScreen({
onOpenSetup: () => {
trace('[startup] setup screen requested from startup UI\n')
},
})
main(startupScreen).catch((error) => {
startupScreen.showError(error)
trace(`[startup] Fatal: ${String(error)}\n`)
})