Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import Timer from 'timer'
import { Vector3, Pose, Rotation, Maybe, noop, randomBetween } from 'stackchan-util'
import { type FaceContext, type Emotion, defaultFaceContext } from 'face-renderer'
import structuredClone from 'structuredClone'
import Digital from 'embedded:io/digital'
import Touch from 'touch'
const INTERVAL_FACE = 1000 / 30
const INTERVAL_POSE = 1000 / 10
/**
* The Driver for the actuator
*/
export type Driver = {
applyRotation: (ori: Rotation, time?: number) => Promise<void>
getRotation: () => Promise<Maybe<Rotation>>
setTorque: (torque: boolean) => Promise<void>
}
/**
* The text-to-speech engine
*/
export type TTS = {
stream: (text: string) => Promise<void>
onPlayed: (volume: number) => void
onDone: () => void
}
/**
* The display renderer
*/
export type Renderer = {
update: (interval: number, faceContext: FaceContext) => void
}
export type Button = {
onChanged: (this: Digital) => void
}
const buttonNames = ['a', 'b', 'c'] as const
type ButtonName = typeof buttonNames[number]
/**
* The constructor parameters of the robot.
*/
type RobotConstructorParam<T extends string> = {
driver: Driver
renderer: Renderer
tts: TTS
button: { [key in T]: Button }
pose?: {
body: Pose
eyes: {
left: Pose
right: Pose
}
}
touch?: Touch
}
export class Robot {
/**
* A Facade class that provides quick access for Stack-chan features
*
* @public
*/
#gazePoint: Vector3
#pose: {
body: Pose
eyes: {
left: Pose
right: Pose
}
}
#power: number
#tts: TTS
#driver: Driver
#button: { [key in ButtonName]: Button }
#touch: Touch
#isMoving: boolean
#renderer: Renderer
#updateFaceHandler: Timer
#updatePoseHandler: Timer
constructor(params: RobotConstructorParam<ButtonName>) {
this.useRenderer(params.renderer)
this.useDriver(params.driver)
this.useTTS(params.tts)
this.#isMoving = false
this.#power = 0
this.#button = params.button
this.#touch = params.touch
this.#pose = params.pose ?? {
body: {
position: {
x: 0.0,
y: 0.0,
z: 0.0,
},
rotation: {
y: 0.0,
p: 0.0,
r: 0.0,
},
},
eyes: {
left: {
position: {
x: 0.03,
y: 0.009,
z: 0,
},
rotation: {
r: 0.0,
p: 0.0,
y: 0.0,
},
},
right: {
position: {
x: 0.03,
y: -0.009,
z: 0,
},
rotation: {
r: 0.0,
p: 0.0,
y: 0.0,
},
},
},
}
this.#updatePoseHandler = Timer.repeat(this.updatePose.bind(this), INTERVAL_POSE)
this.#updateFaceHandler = Timer.repeat(this.updateFace.bind(this), INTERVAL_FACE)
}
/**
* set a TTS instance to Robot and register callbacks
*
* @param tts - TTS class instance
*/
useTTS(tts: TTS) {
if (this.#tts != null) {
this.#tts.onDone = noop
this.#tts.onPlayed = noop
}
this.#tts = tts
this.#tts.onPlayed = (volume: number) => {
this.#power = volume
}
this.#tts.onDone = () => {
this.#power = 0
}
}
/**
* set a Renderer instance to Robot and register callbacks
*
* @param renderer - Renderer class instance
*/
useRenderer(renderer: Renderer) {
this.#renderer = renderer
}
/**
* set a Driver instance to Robot and register callbacks
*
* @param driver - Driver class instance
*/
useDriver(driver: Driver) {
this.#driver = driver
}
/**
* get Buttons
*
* @returns Button instances
*/
get button() {
return this.#button
}
/**
* get Touch
*
* @returns Touch instances
*/
get touch() {
return this.#touch
}
/**
* get Pose
*
* @returns Button instances
*/
get pose() {
return this.#pose
}
/**
* let the robot say things
*
* @param text - the key or speech text itself to say
* @returns the text when speech finishes, otherwise the reason why it fails.
*/
async say(text: string): Promise<Maybe<string>> {
await this.#tts.stream(text).catch((reason) => {
return {
success: false,
message: reason,
}
})
return {
success: true,
value: text,
}
}
/**
* Move the focus point of the robot.
* When the robot looks somewhere, it moves its gaze or face direction
* toward that point.
* The function lookAt completes synchronously,
* and the function does not know when to start or finish moving the gaze.
*
* @param position - the position of the point to look at
*/
lookAt(position: Vector3) {
this.#gazePoint = position
}
/**
* Unregister the focus point.
*/
lookAway() {
this.#gazePoint = null
}
/**
* Set the pose.
*
* @returns void when the robot start moving
* @experimental
*/
async setPose(pose: Pose, time?: number): Promise<void> {
return this.#driver.applyRotation(pose.rotation, time)
}
/**
* Set the actuator torque.
*
* @returns void when the robot completes setting the torque
*/
async setTorque(torque: boolean): Promise<void> {
return this.#driver.setTorque(torque)
}
/**
* Set the emotion of the robot.
* The emotion may (or may not) affect the way the robot moves
* and its facial expressions.
*
* @param emotion - emotion
*/
setEmotion(emotion: Emotion) {
// TBD
}
/**
* Update the robot face.
* Process the robot's emotion, pose, gaze point and so on
* to modify the face context and passes it to Renderer#update
*/
updateFace() {
const face = structuredClone(defaultFaceContext)
if (this.#power != 0) {
face.mouth.open = Math.min(this.#power / 2000, 1.0)
}
if (this.#gazePoint != null) {
const relativeGazePoint = Vector3.rotate(this.#gazePoint, {
r: 0.0,
y: -this.#pose.body.rotation.y,
p: -this.#pose.body.rotation.p,
})
for (let key of ['left', 'right'] as const) {
const pos = this.#pose.eyes[key].position
const relative = Vector3.sub(relativeGazePoint, [pos.x, pos.y, pos.z])
const { y, p } = Rotation.fromVector3(relative)
face.eyes[key] = {
...face.eyes[key],
gazeX: Math.cos(y),
gazeY: Math.cos(p),
}
}
}
this.#renderer.update(INTERVAL_FACE, face)
}
/**
* Update the robot pose.
* Get the current pose from the Driver
* and trigger move if necessary to see the gaze point.
*/
async updatePose() {
const result = await this.#driver.getRotation()
if (result.success) {
this.#pose.body.rotation = result.value
}
if (!this.#isMoving && this.#gazePoint != null) {
const relativeGazePoint = Vector3.rotate(this.#gazePoint, {
r: 0.0,
y: -this.#pose.body.rotation.y,
p: -this.#pose.body.rotation.p,
})
const { y, p } = Rotation.fromVector3(relativeGazePoint)
if (y > Math.PI / 6 || y < -Math.PI / 6 || p > Math.PI / 6 || p < -Math.PI / 6) {
this.#isMoving = true
const time = randomBetween(0.5, 1.0)
await this.#driver.setTorque(true)
await this.#driver.applyRotation(Rotation.fromVector3(this.#gazePoint), time)
Timer.set(async () => {
await this.#driver.setTorque(false)
this.#isMoving = false
}, time * 1000 + 50)
}
}
}
}
bb719e551This file is pinned to the selected release. Download the original to open it in your design tool.