Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
87fab9c0aThis file is pinned to the selected release. Download the original to open it in your design tool.
import loadPreferences from 'loadPreference'
import { createAppControllerApplication } from 'app-controller'
import { DogFace, SimpleFace, SmallFace } from 'behaviors/face'
import Camera from 'camera'
import { ChatStatusBar } from 'chat-status-bar'
import defaultMod, { type StackchanMod } from 'default-mods/mod'
import type { DrawerButtonSpec } from 'drawer'
import { DynamixelDriver } from 'dynamixel-driver'
import IMU from 'imu'
import Led from 'led'
import { M5StackChanServoDriver } from 'm5stackchan-servo-driver'
import config from 'mc/config'
import Microphone from 'microphone'
import Modules from 'modules'
import { NetworkService } from 'network-service'
import { NoneDriver } from 'none-driver'
import { ImageAvatarFace } from 'parts/image/image-avatar-face'
import type { Container as PiuContainer } from 'piu/MC'
import PY32Led from 'py32-led'
import { type Driver, type RobotUI, Robot, type Button as RobotButton, type TTS } from 'robot'
import { RS30XDriver } from 'rs30x-driver'
import { SCServoDriver } from 'scservo-driver'
import { PWMServoDriver } from 'sg90-driver'
import { asyncWait } from 'stackchan-util'
import Tone from 'tone'
import Touch from 'touch'
import TouchPanel from 'touch-panel'
import { TTS as ElevenLabsTTS } from 'tts-elevenlabs'
import { TTS as LocalTTS } from 'tts-local'
import { TTS as OpenAITTS } from 'tts-openai'
import { TTS as RemoteTTS } from 'tts-remote'
import { TTS as VoiceVoxTTS } from 'tts-voicevox'
import { TTS as VoiceVoxWebTTS } from 'tts-voicevox-web'
type DeviceButton = RobotButton & {
read: () => number
}
type SimulatorButtonCtor = new (options: {
onPush?: () => void
}) => {
read: () => number | undefined
}
type RobotLed = Pick<Led, 'on' | 'off' | 'blink' | 'rainbow'>
type UIOptions = {
avatar?: string
drawerButtons?: DrawerButtonSpec[]
displayListLength?: number
}
type GlobalEnvironment = {
button?: Partial<Record<'a' | 'b' | 'c' | 'power', DeviceButton>>
network?: NetworkService
device?: {
sensor?: {
TouchPanel?: new (options: unknown) => unknown
IMU?: new (options: unknown) => unknown
}
}
Host?: {
Button?: Partial<Record<'a' | 'b' | 'c', SimulatorButtonCtor>>
}
}
const globalEnv = globalThis as typeof globalThis & GlobalEnvironment
function isThenable<T>(value: T | Promise<T>): value is Promise<T> {
return value != null && typeof (value as { then?: unknown }).then === 'function'
}
function asUIOptions(param: unknown): UIOptions {
return (param ?? {}) as UIOptions
}
function createStackchanUI(face: PiuContainer, options: UIOptions = {}, displayListLength = 2048): RobotUI {
return createAppControllerApplication(
{
face,
appBar: new ChatStatusBar(),
drawerButtons: options.drawerButtons,
},
{ displayListLength },
)
}
// wrapper button class for simulator
class SimButton {
#button: { read: () => number | undefined }
onChanged = () => {}
constructor(button: SimulatorButtonCtor) {
const self = this
this.#button = new button({
onPush() {
self.onChanged()
},
})
}
read() {
return this.#button.read() ?? 1
}
}
function createRobot() {
const drivers = new Map<string, (param: unknown) => Driver>([
['scservo', (param) => new SCServoDriver(param as ConstructorParameters<typeof SCServoDriver>[0])],
[
'm5stackchan',
(param) => new M5StackChanServoDriver(param as ConstructorParameters<typeof M5StackChanServoDriver>[0]),
],
['dynamixel', (param) => new DynamixelDriver(param as ConstructorParameters<typeof DynamixelDriver>[0])],
['pwm', (param) => new PWMServoDriver(param as ConstructorParameters<typeof PWMServoDriver>[0])],
['rs30x', (param) => new RS30XDriver(param as ConstructorParameters<typeof RS30XDriver>[0])],
['none', () => new NoneDriver()],
])
const ttsEngines = new Map<string, (param: unknown) => TTS>([
['local', (param) => new LocalTTS(param as ConstructorParameters<typeof LocalTTS>[0])],
['remote', (param) => new RemoteTTS(param as ConstructorParameters<typeof RemoteTTS>[0])],
['voicevox', (param) => new VoiceVoxTTS(param as ConstructorParameters<typeof VoiceVoxTTS>[0])],
['voicevox-web', (param) => new VoiceVoxWebTTS(param as ConstructorParameters<typeof VoiceVoxWebTTS>[0])],
['elevenlabs', (param) => new ElevenLabsTTS(param as ConstructorParameters<typeof ElevenLabsTTS>[0])],
['openai', (param) => new OpenAITTS(param as ConstructorParameters<typeof OpenAITTS>[0])],
])
const uiControllers = new Map<string, (param: unknown) => RobotUI>([
['dog', (param) => createStackchanUI(new DogFace(), asUIOptions(param))],
['simple', (param) => createStackchanUI(new SimpleFace(), asUIOptions(param))],
[
'image',
(param) => {
const options = asUIOptions(param)
return createStackchanUI(
new ImageAvatarFace({ pack: options.avatar }),
options,
options.displayListLength ?? 4096,
)
},
],
['small-face', (param) => createStackchanUI(new SmallFace(), asUIOptions(param))],
])
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)
// UI
const uiPrefs = loadPreferences('ui')
const uiKey = uiPrefs.type ?? 'simple'
const UI = uiControllers.get(uiKey)
if (!Driver || !TTS || !UI) {
for (const [key, klass] of [
[driverKey, Driver],
[ttsKey, TTS],
[uiKey, UI],
]) {
if (klass == null) {
errors.push(`type "${key}" does not exist`)
}
}
throw new Error(errors.join('\n'))
}
const driver = Driver(driverPrefs)
const ui = UI(uiPrefs)
const tts = TTS(ttsPrefs)
const touch = config.Touch ? new Touch(config.Touch) : undefined
const touchPanel = globalEnv.device?.sensor?.TouchPanel
? new TouchPanel(globalEnv.device.sensor.TouchPanel as ConstructorParameters<typeof TouchPanel>[0])
: undefined
const imu = globalEnv.device?.sensor?.IMU
? new IMU(globalEnv.device.sensor.IMU as ConstructorParameters<typeof IMU>[0])
: undefined
const microphone = Modules.has('embedded:io/audio/in') ? new Microphone() : undefined
const camera = new Camera()
const tone = new Tone({ volume: ttsPrefs.volume })
const configLed = loadPreferences('led')
const ledEntries: [string, RobotLed][] = Object.entries(configLed).flatMap(
([key, ledConfig]): [string, RobotLed][] => {
const candidate = ledConfig as {
type?: unknown
pin?: unknown
length?: unknown
order?: unknown
ledPin?: unknown
address?: unknown
}
if (
typeof ledConfig !== 'object' ||
ledConfig == null ||
(candidate.length !== undefined && typeof candidate.length !== 'number') ||
(candidate.order !== undefined && typeof candidate.order !== 'string') ||
(candidate.ledPin !== undefined && typeof candidate.ledPin !== 'number') ||
(candidate.address !== undefined && typeof candidate.address !== 'number')
) {
trace(`[main] skip led config (invalid shape): ${key}\n`)
return []
}
if (candidate.type === 'py32') {
if (typeof candidate.ledPin !== 'number') {
trace(`[main] skip py32 led config (missing/invalid ledPin): ${key}\n`)
return []
}
return [[key, new PY32Led(candidate as { length?: number; ledPin?: number; address?: number })]]
}
if (typeof candidate.pin !== 'number') {
trace(`[main] skip led config (missing/invalid pin): ${key}\n`)
return []
}
return [[key, new Led(candidate as { pin: number; length?: number; order?: string })]]
},
)
const led = Object.fromEntries(ledEntries)
return new Robot({
driver,
ui,
tts,
button: globalEnv.button,
touch,
touchPanel,
imu,
tone,
microphone,
camera,
led: led as ConstructorParameters<typeof Robot>[0]['led'],
} as ConstructorParameters<typeof Robot>[0])
}
async function checkAndConnectWiFi() {
const wifiPrefs = loadPreferences('wifi')
if (wifiPrefs.ssid == null || wifiPrefs.password == null) {
return
}
return new Promise<void>((resolve, reject) => {
globalEnv.network = new NetworkService({
ssid: wifiPrefs.ssid,
password: wifiPrefs.password,
})
globalEnv.network.connect(resolve, reject)
})
}
async function main() {
trace(`[main] start wasm=${Boolean(config.wasm)}\n`)
if (globalEnv.Host?.Button && !globalEnv.button) {
trace('[main] installing simulator buttons\n')
const { a, b, c } = globalEnv.Host.Button
globalEnv.button = {
...(a && { a: new SimButton(a) }),
...(b && { b: new SimButton(b) }),
...(c && { c: new SimButton(c) }),
}
}
if (config.wasm) {
trace('[main] wasm path start\n')
const wasmDefaultMod = Modules.importNow('default-mods/wasm/mod') as StackchanMod
let { onRobotCreated, onLaunch } = wasmDefaultMod
trace(`[main] wasm defaultMod onLaunch=${onLaunch != null} onRobotCreated=${onRobotCreated != null}\n`)
if (Modules.has('mod')) {
trace('[main] wasm loading mod override\n')
const mod = Modules.importNow('mod') as StackchanMod
onRobotCreated = mod.onRobotCreated ?? onRobotCreated
onLaunch = mod.onLaunch ?? onLaunch
}
const launchResult = onLaunch?.() ?? true
const continueWasm = (shouldRobotCreate: boolean) => {
trace(`[main] wasm onLaunch shouldRobotCreate=${shouldRobotCreate}\n`)
if (shouldRobotCreate) {
const robot = createRobot()
trace('[main] wasm robot created\n')
const robotCreatedResult = onRobotCreated?.(robot, globalEnv.device)
if (isThenable(robotCreatedResult)) {
robotCreatedResult
.then(() => {
trace('[main] wasm onRobotCreated complete\n')
})
.catch((error) => {
trace(`[main] wasm onRobotCreated error ${error?.message ?? error}\n`)
})
} else {
trace('[main] wasm onRobotCreated complete\n')
}
}
trace('[main] wasm path complete\n')
}
if (isThenable(launchResult)) {
launchResult.then(continueWasm).catch((error) => {
trace(`[main] wasm onLaunch error ${error?.message ?? error}\n`)
})
} else {
continueWasm(launchResult)
}
return
}
await asyncWait(100)
trace('[main] check Wi-Fi start\n')
await checkAndConnectWiFi().catch((msg) => {
trace(`WiFi connection failed: ${msg}`)
})
trace('[main] check Wi-Fi complete\n')
trace('[main] loading default mod\n')
let { onRobotCreated, onLaunch } = defaultMod
trace('[main] checking mod override\n')
if (Modules.has('mod')) {
const mod = Modules.importNow('mod') as StackchanMod
onRobotCreated = mod.onRobotCreated ?? onRobotCreated
onLaunch = mod.onLaunch ?? onLaunch
}
const shouldRobotCreate = await (onLaunch?.() ?? true)
trace(`[main] onLaunch shouldRobotCreate=${shouldRobotCreate}\n`)
if (shouldRobotCreate) {
const robot = createRobot()
trace('[main] robot created\n')
await onRobotCreated?.(robot, globalEnv.device)
trace('[main] onRobotCreated complete\n')
}
}
main().catch((error) => {
trace(`[main] error ${error?.message ?? error}\n`)
})