tjbot-ce 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +382 -0
- package/dist/camera/camera.d.ts +62 -0
- package/dist/camera/camera.d.ts.map +1 -0
- package/dist/camera/camera.js +155 -0
- package/dist/camera/camera.js.map +1 -0
- package/dist/camera/index.d.ts +18 -0
- package/dist/camera/index.d.ts.map +1 -0
- package/dist/camera/index.js +18 -0
- package/dist/camera/index.js.map +1 -0
- package/dist/config/config-types.d.ts +75 -0
- package/dist/config/config-types.d.ts.map +1 -0
- package/dist/config/config-types.generated.d.ts +495 -0
- package/dist/config/config-types.generated.d.ts.map +1 -0
- package/dist/config/config-types.generated.js +2 -0
- package/dist/config/config-types.generated.js.map +1 -0
- package/dist/config/config-types.js +175 -0
- package/dist/config/config-types.js.map +1 -0
- package/dist/config/index.d.ts +20 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +19 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/tjbot-config.d.ts +98 -0
- package/dist/config/tjbot-config.d.ts.map +1 -0
- package/dist/config/tjbot-config.js +309 -0
- package/dist/config/tjbot-config.js.map +1 -0
- package/dist/config/vendor/colors.yaml +61 -0
- package/dist/config/vendor/model-registry.yaml +275 -0
- package/dist/config/vendor/tjbot-config.schema.yaml +792 -0
- package/dist/config/vendor/tjbot.default.toml +452 -0
- package/dist/led/index.d.ts +20 -0
- package/dist/led/index.d.ts.map +1 -0
- package/dist/led/index.js +20 -0
- package/dist/led/index.js.map +1 -0
- package/dist/led/led-common-anode.d.ts +38 -0
- package/dist/led/led-common-anode.d.ts.map +1 -0
- package/dist/led/led-common-anode.js +79 -0
- package/dist/led/led-common-anode.js.map +1 -0
- package/dist/led/led-neopixel-spi.d.ts +60 -0
- package/dist/led/led-neopixel-spi.d.ts.map +1 -0
- package/dist/led/led-neopixel-spi.js +216 -0
- package/dist/led/led-neopixel-spi.js.map +1 -0
- package/dist/led/led-neopixel-ws281x.js +186 -0
- package/dist/led/led-neopixel.d.ts +57 -0
- package/dist/led/led-neopixel.d.ts.map +1 -0
- package/dist/led/led-neopixel.js +235 -0
- package/dist/led/led-neopixel.js.map +1 -0
- package/dist/microphone/index.d.ts +18 -0
- package/dist/microphone/index.d.ts.map +1 -0
- package/dist/microphone/index.js +18 -0
- package/dist/microphone/index.js.map +1 -0
- package/dist/microphone/microphone.d.ts +65 -0
- package/dist/microphone/microphone.d.ts.map +1 -0
- package/dist/microphone/microphone.js +179 -0
- package/dist/microphone/microphone.js.map +1 -0
- package/dist/rpi-drivers/index.d.ts +22 -0
- package/dist/rpi-drivers/index.d.ts.map +1 -0
- package/dist/rpi-drivers/index.js +22 -0
- package/dist/rpi-drivers/index.js.map +1 -0
- package/dist/rpi-drivers/rpi-detect.d.ts +24 -0
- package/dist/rpi-drivers/rpi-detect.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi-detect.js +49 -0
- package/dist/rpi-drivers/rpi-detect.js.map +1 -0
- package/dist/rpi-drivers/rpi-driver.d.ts +116 -0
- package/dist/rpi-drivers/rpi-driver.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi-driver.js +261 -0
- package/dist/rpi-drivers/rpi-driver.js.map +1 -0
- package/dist/rpi-drivers/rpi3-driver.d.ts +47 -0
- package/dist/rpi-drivers/rpi3-driver.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi3-driver.js +145 -0
- package/dist/rpi-drivers/rpi3-driver.js.map +1 -0
- package/dist/rpi-drivers/rpi4-driver.d.ts +35 -0
- package/dist/rpi-drivers/rpi4-driver.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi4-driver.js +101 -0
- package/dist/rpi-drivers/rpi4-driver.js.map +1 -0
- package/dist/rpi-drivers/rpi5-driver.d.ts +33 -0
- package/dist/rpi-drivers/rpi5-driver.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi5-driver.js +78 -0
- package/dist/rpi-drivers/rpi5-driver.js.map +1 -0
- package/dist/servo/index.d.ts +19 -0
- package/dist/servo/index.d.ts.map +1 -0
- package/dist/servo/index.js +19 -0
- package/dist/servo/index.js.map +1 -0
- package/dist/servo/servo-constants.d.ts +33 -0
- package/dist/servo/servo-constants.d.ts.map +1 -0
- package/dist/servo/servo-constants.js +34 -0
- package/dist/servo/servo-constants.js.map +1 -0
- package/dist/servo/servo-lgpio.d.ts +82 -0
- package/dist/servo/servo-lgpio.d.ts.map +1 -0
- package/dist/servo/servo-lgpio.js +178 -0
- package/dist/servo/servo-lgpio.js.map +1 -0
- package/dist/speaker/audio-player.d.ts +30 -0
- package/dist/speaker/audio-player.d.ts.map +1 -0
- package/dist/speaker/audio-player.js +68 -0
- package/dist/speaker/audio-player.js.map +1 -0
- package/dist/speaker/index.d.ts +18 -0
- package/dist/speaker/index.d.ts.map +1 -0
- package/dist/speaker/index.js +18 -0
- package/dist/speaker/index.js.map +1 -0
- package/dist/speaker/speaker.d.ts +53 -0
- package/dist/speaker/speaker.d.ts.map +1 -0
- package/dist/speaker/speaker.js +125 -0
- package/dist/speaker/speaker.js.map +1 -0
- package/dist/stt/backends/azure-stt.d.ts +32 -0
- package/dist/stt/backends/azure-stt.d.ts.map +1 -0
- package/dist/stt/backends/azure-stt.js +227 -0
- package/dist/stt/backends/azure-stt.js.map +1 -0
- package/dist/stt/backends/google-cloud-stt.d.ts +31 -0
- package/dist/stt/backends/google-cloud-stt.d.ts.map +1 -0
- package/dist/stt/backends/google-cloud-stt.js +371 -0
- package/dist/stt/backends/google-cloud-stt.js.map +1 -0
- package/dist/stt/backends/ibm-watson-stt.d.ts +32 -0
- package/dist/stt/backends/ibm-watson-stt.d.ts.map +1 -0
- package/dist/stt/backends/ibm-watson-stt.js +190 -0
- package/dist/stt/backends/ibm-watson-stt.js.map +1 -0
- package/dist/stt/backends/sherpa-onnx-stt.d.ts +117 -0
- package/dist/stt/backends/sherpa-onnx-stt.d.ts.map +1 -0
- package/dist/stt/backends/sherpa-onnx-stt.js +694 -0
- package/dist/stt/backends/sherpa-onnx-stt.js.map +1 -0
- package/dist/stt/index.d.ts +20 -0
- package/dist/stt/index.d.ts.map +1 -0
- package/dist/stt/index.js +21 -0
- package/dist/stt/index.js.map +1 -0
- package/dist/stt/stt-engine.d.ts +68 -0
- package/dist/stt/stt-engine.d.ts.map +1 -0
- package/dist/stt/stt-engine.js +99 -0
- package/dist/stt/stt-engine.js.map +1 -0
- package/dist/stt/stt-utils.d.ts +36 -0
- package/dist/stt/stt-utils.d.ts.map +1 -0
- package/dist/stt/stt-utils.js +112 -0
- package/dist/stt/stt-utils.js.map +1 -0
- package/dist/stt/stt.d.ts +52 -0
- package/dist/stt/stt.d.ts.map +1 -0
- package/dist/stt/stt.js +100 -0
- package/dist/stt/stt.js.map +1 -0
- package/dist/tjbot.d.ts +317 -0
- package/dist/tjbot.d.ts.map +1 -0
- package/dist/tjbot.js +736 -0
- package/dist/tjbot.js.map +1 -0
- package/dist/tts/backends/azure-tts.d.ts +30 -0
- package/dist/tts/backends/azure-tts.d.ts.map +1 -0
- package/dist/tts/backends/azure-tts.js +92 -0
- package/dist/tts/backends/azure-tts.js.map +1 -0
- package/dist/tts/backends/google-cloud-tts.d.ts +38 -0
- package/dist/tts/backends/google-cloud-tts.d.ts.map +1 -0
- package/dist/tts/backends/google-cloud-tts.js +116 -0
- package/dist/tts/backends/google-cloud-tts.js.map +1 -0
- package/dist/tts/backends/ibm-watson-tts.d.ts +42 -0
- package/dist/tts/backends/ibm-watson-tts.d.ts.map +1 -0
- package/dist/tts/backends/ibm-watson-tts.js +99 -0
- package/dist/tts/backends/ibm-watson-tts.js.map +1 -0
- package/dist/tts/backends/sherpa-onnx-tts.d.ts +80 -0
- package/dist/tts/backends/sherpa-onnx-tts.d.ts.map +1 -0
- package/dist/tts/backends/sherpa-onnx-tts.js +237 -0
- package/dist/tts/backends/sherpa-onnx-tts.js.map +1 -0
- package/dist/tts/index.d.ts +19 -0
- package/dist/tts/index.d.ts.map +1 -0
- package/dist/tts/index.js +20 -0
- package/dist/tts/index.js.map +1 -0
- package/dist/tts/tts-engine.d.ts +67 -0
- package/dist/tts/tts-engine.d.ts.map +1 -0
- package/dist/tts/tts-engine.js +109 -0
- package/dist/tts/tts-engine.js.map +1 -0
- package/dist/tts/tts.d.ts +47 -0
- package/dist/tts/tts.d.ts.map +1 -0
- package/dist/tts/tts.js +101 -0
- package/dist/tts/tts.js.map +1 -0
- package/dist/utils/colors.d.ts +39 -0
- package/dist/utils/colors.d.ts.map +1 -0
- package/dist/utils/colors.js +155 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/utils/constants.d.ts +41 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +43 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/credentials.d.ts +43 -0
- package/dist/utils/credentials.d.ts.map +1 -0
- package/dist/utils/credentials.js +121 -0
- package/dist/utils/credentials.js.map +1 -0
- package/dist/utils/errors.d.ts +26 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +32 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +25 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +23 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logging.d.ts +44 -0
- package/dist/utils/logging.d.ts.map +1 -0
- package/dist/utils/logging.js +113 -0
- package/dist/utils/logging.js.map +1 -0
- package/dist/utils/model-registry.d.ts +142 -0
- package/dist/utils/model-registry.d.ts.map +1 -0
- package/dist/utils/model-registry.js +391 -0
- package/dist/utils/model-registry.js.map +1 -0
- package/dist/utils/utils.d.ts +33 -0
- package/dist/utils/utils.d.ts.map +1 -0
- package/dist/utils/utils.js +50 -0
- package/dist/utils/utils.js.map +1 -0
- package/dist/vision/backends/azure-vision.d.ts +33 -0
- package/dist/vision/backends/azure-vision.d.ts.map +1 -0
- package/dist/vision/backends/azure-vision.js +151 -0
- package/dist/vision/backends/azure-vision.js.map +1 -0
- package/dist/vision/backends/google-cloud-vision.d.ts +32 -0
- package/dist/vision/backends/google-cloud-vision.d.ts.map +1 -0
- package/dist/vision/backends/google-cloud-vision.js +193 -0
- package/dist/vision/backends/google-cloud-vision.js.map +1 -0
- package/dist/vision/backends/onnx.d.ts +116 -0
- package/dist/vision/backends/onnx.d.ts.map +1 -0
- package/dist/vision/backends/onnx.js +781 -0
- package/dist/vision/backends/onnx.js.map +1 -0
- package/dist/vision/index.d.ts +19 -0
- package/dist/vision/index.d.ts.map +1 -0
- package/dist/vision/index.js +20 -0
- package/dist/vision/index.js.map +1 -0
- package/dist/vision/vision-engine.d.ts +131 -0
- package/dist/vision/vision-engine.d.ts.map +1 -0
- package/dist/vision/vision-engine.js +97 -0
- package/dist/vision/vision-engine.js.map +1 -0
- package/dist/vision/vision.d.ts +48 -0
- package/dist/vision/vision.d.ts.map +1 -0
- package/dist/vision/vision.js +83 -0
- package/dist/vision/vision.js.map +1 -0
- package/package.json +124 -0
package/dist/tjbot.js
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2016-2025 IBM Corp. All Rights Reserved.
|
|
3
|
+
* Copyright 2026-present TJBot Contributors. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { TJBotConfig } from './config/tjbot-config.js';
|
|
18
|
+
import { RPi3Driver, RPi4Driver, RPi5Driver, RPiDetect } from './rpi-drivers/index.js';
|
|
19
|
+
import { ServoPosition } from './servo/index.js';
|
|
20
|
+
import { inferSTTMode } from './stt/stt-utils.js';
|
|
21
|
+
import { Capability, getShineColors, Hardware, initWinston, ModelRegistry, normalizeColor, sleep as asyncSleep, TJBotError, } from './utils/index.js';
|
|
22
|
+
import { getLogger } from './utils/logging.js';
|
|
23
|
+
// node modules
|
|
24
|
+
import cm from 'color-model';
|
|
25
|
+
import { promises as fsPromises, readFileSync } from 'fs';
|
|
26
|
+
import { easeInOutQuad } from 'js-easing-functions';
|
|
27
|
+
import { dirname, join } from 'path';
|
|
28
|
+
import temp from 'temp';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
const logger = getLogger(import.meta.url);
|
|
31
|
+
// Read version from package.json
|
|
32
|
+
const DIRNAME = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const PACKAGE_JSON = JSON.parse(readFileSync(join(DIRNAME, '../package.json'), 'utf-8'));
|
|
34
|
+
// Configure winston logging at module load time so all internals share one logger format.
|
|
35
|
+
initWinston('info');
|
|
36
|
+
/**
|
|
37
|
+
* Class representing a TJBot
|
|
38
|
+
*/
|
|
39
|
+
class TJBot {
|
|
40
|
+
/**
|
|
41
|
+
* TJBot library version
|
|
42
|
+
* @readonly
|
|
43
|
+
*/
|
|
44
|
+
static VERSION = `v${PACKAGE_JSON.version}`;
|
|
45
|
+
/**
|
|
46
|
+
* Singleton instance
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
static instance;
|
|
50
|
+
/**
|
|
51
|
+
* Hardware list
|
|
52
|
+
* @readonly
|
|
53
|
+
*/
|
|
54
|
+
static Hardware = Hardware;
|
|
55
|
+
/**
|
|
56
|
+
* TJBot configuration
|
|
57
|
+
*/
|
|
58
|
+
config;
|
|
59
|
+
/**
|
|
60
|
+
* Raspberry Pi model on which TJBot is running
|
|
61
|
+
* @example "Raspberry Pi 5"
|
|
62
|
+
*/
|
|
63
|
+
rpiModel;
|
|
64
|
+
/**
|
|
65
|
+
* Raspberry Pi hardware driver
|
|
66
|
+
*/
|
|
67
|
+
rpiDriver;
|
|
68
|
+
/**
|
|
69
|
+
* Cache of the colors recognized by TJBot
|
|
70
|
+
*/
|
|
71
|
+
_shineColors = [];
|
|
72
|
+
/**
|
|
73
|
+
* Flag to track if TJBot has been initialized
|
|
74
|
+
*/
|
|
75
|
+
_initialized = false;
|
|
76
|
+
/**
|
|
77
|
+
* Promise for in-flight cleanup operation, if any.
|
|
78
|
+
*/
|
|
79
|
+
_cleanupPromise = null;
|
|
80
|
+
/**
|
|
81
|
+
* Guard to ensure process lifecycle hooks are installed only once.
|
|
82
|
+
*/
|
|
83
|
+
_processHooksInstalled = false;
|
|
84
|
+
/**
|
|
85
|
+
* Private constructor.
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
constructor() {
|
|
89
|
+
// automatically track and clean up temporary files
|
|
90
|
+
temp.track();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the singleton instance of TJBot.
|
|
94
|
+
* @returns {TJBot} The singleton TJBot instance
|
|
95
|
+
* @public
|
|
96
|
+
*/
|
|
97
|
+
static getInstance() {
|
|
98
|
+
if (!TJBot.instance) {
|
|
99
|
+
TJBot.instance = new TJBot();
|
|
100
|
+
}
|
|
101
|
+
return TJBot.instance;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get recipe-specific configuration. This method can be used before calling `TJBot.getInstance().initialize()`
|
|
105
|
+
* in case a recipe needs to dynamically determine which hardware components should be configured.
|
|
106
|
+
* @param {string=} recipeConfigPath (optional) Path to recipe configuration file (default: recipe.toml in current working directory)
|
|
107
|
+
* @return {Record<string, unknown>} The recipe configuration as a key-value object. If no recipe configuration file is found, returns an empty object.
|
|
108
|
+
*
|
|
109
|
+
*/
|
|
110
|
+
static getRecipeConfig(recipeConfigPath = 'recipe.toml') {
|
|
111
|
+
const config = new TJBotConfig(undefined, recipeConfigPath);
|
|
112
|
+
return config.recipe;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Initialize TJBot with configuration. Can be called multiple times to reconfigure.
|
|
116
|
+
* Performs cleanup of previous initialization, loads configuration, detects hardware,
|
|
117
|
+
* initializes all configured hardware and AI models eagerly.
|
|
118
|
+
* @param {Partial<TJBotConfigSchema>=} overrideConfig (optional) Configuration object to overlay on top of loaded config.
|
|
119
|
+
* @param {string=} recipeConfigPath (optional) Path to recipe configuration file (default: recipe.toml in current working directory)
|
|
120
|
+
* @throws {TJBotError} if configuration file cannot be loaded, is invalid, or cleanup fails
|
|
121
|
+
* @public
|
|
122
|
+
*/
|
|
123
|
+
async initialize(overrideConfig, recipeConfigPath) {
|
|
124
|
+
logger.info('Initializing TJBot...');
|
|
125
|
+
this.installProcessCleanupHooks();
|
|
126
|
+
// Cleanup previous initialization if any
|
|
127
|
+
if (this._initialized) {
|
|
128
|
+
logger.info('Cleaning up previous initialization...');
|
|
129
|
+
await this.cleanup();
|
|
130
|
+
}
|
|
131
|
+
// Load configuration
|
|
132
|
+
this.config = new TJBotConfig(overrideConfig, recipeConfigPath);
|
|
133
|
+
// Update log level from config
|
|
134
|
+
const logConfig = this.config.log;
|
|
135
|
+
if (logConfig && logConfig.level) {
|
|
136
|
+
logger.level = logConfig.level;
|
|
137
|
+
}
|
|
138
|
+
// Detect Raspberry Pi model and instantiate driver
|
|
139
|
+
this.rpiModel = RPiDetect.model();
|
|
140
|
+
logger.info(`Detected hardware: ${this.rpiModel}`);
|
|
141
|
+
if (this.rpiModel.startsWith('Raspberry Pi 3')) {
|
|
142
|
+
this.rpiDriver = new RPi3Driver();
|
|
143
|
+
}
|
|
144
|
+
else if (this.rpiModel.startsWith('Raspberry Pi 4')) {
|
|
145
|
+
this.rpiDriver = new RPi4Driver();
|
|
146
|
+
}
|
|
147
|
+
else if (this.rpiModel.startsWith('Raspberry Pi 5')) {
|
|
148
|
+
this.rpiDriver = new RPi5Driver();
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
logger.warn('TJBot is running on unsupported Raspberry Pi hardware. Resorting to RPi3 hardware driver, but errors may occur.');
|
|
152
|
+
this.rpiDriver = new RPi3Driver();
|
|
153
|
+
}
|
|
154
|
+
logger.verbose(`TJBot library version ${TJBot.VERSION}`);
|
|
155
|
+
logger.debug(`TJBot configuration:\n${JSON.stringify(this.config, null, 2)}`);
|
|
156
|
+
// Initialize hardware
|
|
157
|
+
await this.initializeHardware();
|
|
158
|
+
// Eagerly initialize AI models (if configured)
|
|
159
|
+
await this.initializeAIModels();
|
|
160
|
+
this._initialized = true;
|
|
161
|
+
logger.info('TJBot initialization complete');
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Initialize hardware devices
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
async initializeHardware() {
|
|
169
|
+
const hwConfig = this.config.hardware;
|
|
170
|
+
if (!hwConfig || Object.keys(hwConfig).length === 0) {
|
|
171
|
+
logger.debug('No hardware configured');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const hardwareToInit = [];
|
|
175
|
+
// Map config keys to Hardware enum values
|
|
176
|
+
if (hwConfig.speaker) {
|
|
177
|
+
hardwareToInit.push(Hardware.SPEAKER);
|
|
178
|
+
}
|
|
179
|
+
if (hwConfig.microphone) {
|
|
180
|
+
hardwareToInit.push(Hardware.MICROPHONE);
|
|
181
|
+
}
|
|
182
|
+
if (hwConfig.camera) {
|
|
183
|
+
hardwareToInit.push(Hardware.CAMERA);
|
|
184
|
+
}
|
|
185
|
+
if (hwConfig.led) {
|
|
186
|
+
hardwareToInit.push(Hardware.LED);
|
|
187
|
+
}
|
|
188
|
+
if (hwConfig.servo) {
|
|
189
|
+
hardwareToInit.push(Hardware.SERVO);
|
|
190
|
+
}
|
|
191
|
+
if (hardwareToInit.length === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
logger.info('Initializing hardware...');
|
|
195
|
+
for (const device of hardwareToInit) {
|
|
196
|
+
switch (device) {
|
|
197
|
+
case Hardware.CAMERA: {
|
|
198
|
+
const config = this.config.see;
|
|
199
|
+
this.rpiDriver.setupCamera(config);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case Hardware.LED: {
|
|
203
|
+
const shineConfig = this.config.shine;
|
|
204
|
+
const hasNeopixel = shineConfig?.hasNeopixelLED ?? false;
|
|
205
|
+
const hasCommonAnode = shineConfig?.hasCommonAnodeLED ?? false;
|
|
206
|
+
if (!hasNeopixel && !hasCommonAnode) {
|
|
207
|
+
throw new TJBotError('LED hardware enabled but no LED type configured. Set shine.hasNeopixelLED or shine.hasCommonAnodeLED to true in your tjbot configuration file (~/.tjbot/tjbot.toml).');
|
|
208
|
+
}
|
|
209
|
+
if (hasNeopixel) {
|
|
210
|
+
logger.info('Setting up NeoPixel LED ' +
|
|
211
|
+
'[' +
|
|
212
|
+
(shineConfig?.neopixel?.gpioPin ? `pin: ${shineConfig?.neopixel?.gpioPin}` : '') +
|
|
213
|
+
' ' +
|
|
214
|
+
(shineConfig?.neopixel?.spiInterface
|
|
215
|
+
? `SPI: ${shineConfig.neopixel?.spiInterface}`
|
|
216
|
+
: '') +
|
|
217
|
+
']');
|
|
218
|
+
}
|
|
219
|
+
if (hasCommonAnode) {
|
|
220
|
+
logger.info(`Setting up Common Anode LED [r/g/b pins: ${shineConfig?.commonanode?.redPin}/${shineConfig?.commonanode?.greenPin}/${shineConfig?.commonanode?.bluePin}]`);
|
|
221
|
+
}
|
|
222
|
+
await this.rpiDriver.setupLED(shineConfig);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case Hardware.MICROPHONE: {
|
|
226
|
+
const config = this.config.listen;
|
|
227
|
+
logger.info(`Setting up microphone [device: ${config?.device || 'default'}]`);
|
|
228
|
+
this.rpiDriver.setupMicrophone(config);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case Hardware.SERVO: {
|
|
232
|
+
const config = this.config.wave;
|
|
233
|
+
logger.info(`Setting up servo [pin: ${config?.servoPin}]`);
|
|
234
|
+
this.rpiDriver.setupServo(config);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case Hardware.SPEAKER: {
|
|
238
|
+
const config = this.config.speak;
|
|
239
|
+
logger.info(`Setting up speaker [device: ${config?.device || 'default'}]`);
|
|
240
|
+
this.rpiDriver.setupSpeaker(config);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Eagerly initialize local AI models (STT, TTS, Vision) if configured
|
|
250
|
+
* @private
|
|
251
|
+
*/
|
|
252
|
+
async initializeAIModels() {
|
|
253
|
+
// Initialize STT engine if microphone is configured
|
|
254
|
+
if (this.rpiDriver.hasCapability(Capability.LISTEN)) {
|
|
255
|
+
logger.info('Initializing STT engine...');
|
|
256
|
+
await this.rpiDriver.initializeSTTEngine();
|
|
257
|
+
}
|
|
258
|
+
// Initialize TTS engine if speaker is configured
|
|
259
|
+
if (this.rpiDriver.hasCapability(Capability.SPEAK)) {
|
|
260
|
+
logger.info('Initializing TTS engine...');
|
|
261
|
+
await this.rpiDriver.initializeTTSEngine();
|
|
262
|
+
}
|
|
263
|
+
// Initialize Vision engine if camera is configured
|
|
264
|
+
if (this.rpiDriver.hasCapability(Capability.SEE)) {
|
|
265
|
+
logger.info('Initializing Vision engine...');
|
|
266
|
+
await this.rpiDriver.initializeVisionEngine();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Clean up all resources. Called automatically before re-initialization.
|
|
271
|
+
* @throws {TJBotError} if cleanup fails
|
|
272
|
+
* @private
|
|
273
|
+
*/
|
|
274
|
+
async cleanup() {
|
|
275
|
+
if (this._cleanupPromise) {
|
|
276
|
+
return this._cleanupPromise;
|
|
277
|
+
}
|
|
278
|
+
this._cleanupPromise = (async () => {
|
|
279
|
+
try {
|
|
280
|
+
if (this.rpiDriver) {
|
|
281
|
+
await this.rpiDriver.cleanup();
|
|
282
|
+
}
|
|
283
|
+
this._initialized = false;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
throw new TJBotError('Failed to clean up TJBot resources', {
|
|
287
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
this._cleanupPromise = null;
|
|
292
|
+
}
|
|
293
|
+
})();
|
|
294
|
+
return this._cleanupPromise;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Install process lifecycle hooks so TJBot hardware resources are cleaned up
|
|
298
|
+
* automatically when a recipe exits or is interrupted.
|
|
299
|
+
*/
|
|
300
|
+
installProcessCleanupHooks() {
|
|
301
|
+
if (this._processHooksInstalled) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this._processHooksInstalled = true;
|
|
305
|
+
process.once('beforeExit', () => {
|
|
306
|
+
void this.runLifecycleCleanup('beforeExit');
|
|
307
|
+
});
|
|
308
|
+
process.once('SIGINT', () => {
|
|
309
|
+
void this.runLifecycleCleanup('SIGINT', 130);
|
|
310
|
+
});
|
|
311
|
+
process.once('SIGTERM', () => {
|
|
312
|
+
void this.runLifecycleCleanup('SIGTERM', 143);
|
|
313
|
+
});
|
|
314
|
+
process.once('SIGHUP', () => {
|
|
315
|
+
void this.runLifecycleCleanup('SIGHUP', 129);
|
|
316
|
+
});
|
|
317
|
+
process.once('uncaughtException', (err) => {
|
|
318
|
+
logger.error(`uncaughtException: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
319
|
+
void this.runLifecycleCleanup('uncaughtException', 1);
|
|
320
|
+
});
|
|
321
|
+
process.once('unhandledRejection', (reason) => {
|
|
322
|
+
logger.error(`unhandledRejection: ${reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)}`);
|
|
323
|
+
void this.runLifecycleCleanup('unhandledRejection', 1);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Best-effort automatic cleanup path used by process lifecycle hooks.
|
|
328
|
+
* Uses a timeout in fatal/signal scenarios so process termination does not hang.
|
|
329
|
+
*/
|
|
330
|
+
async runLifecycleCleanup(reason, exitCode) {
|
|
331
|
+
const CLEANUP_TIMEOUT_MS = 3000;
|
|
332
|
+
if (exitCode === undefined) {
|
|
333
|
+
try {
|
|
334
|
+
await this.cleanup();
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
logger.warn(`automatic cleanup failed during ${reason}: ${String(err)}`);
|
|
338
|
+
}
|
|
339
|
+
process.exit(0);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
process.exitCode = exitCode;
|
|
343
|
+
try {
|
|
344
|
+
await Promise.race([
|
|
345
|
+
this.cleanup(),
|
|
346
|
+
new Promise((resolve) => setTimeout(resolve, CLEANUP_TIMEOUT_MS)),
|
|
347
|
+
]);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
logger.warn(`automatic cleanup failed during ${reason}: ${String(err)}`);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
process.exit(exitCode);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Change the level of TJBot's logging.
|
|
358
|
+
* @param {string} level Logging level (see Winston's [list of logging levels](https://github.com/winstonjs/winston?tab=readme-ov-file#using-logging-levels))
|
|
359
|
+
* @public
|
|
360
|
+
*/
|
|
361
|
+
setLogLevel(level) {
|
|
362
|
+
logger.level = level;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Assert that TJBot is able to perform a specified capability.
|
|
366
|
+
* @private
|
|
367
|
+
* @param {string} capability The capability assert (see TJBot.prototype.capabilities).
|
|
368
|
+
*/
|
|
369
|
+
assertCapability(capability) {
|
|
370
|
+
if (!this._initialized) {
|
|
371
|
+
throw new TJBotError('TJBot has not been initialized. Please call await tj.initialize() before using TJBot.');
|
|
372
|
+
}
|
|
373
|
+
logger.debug(`Asserting capability: ${capability}`);
|
|
374
|
+
logger.silly(`TJBot capabilities: ${Array.from(this.rpiDriver.getHardware()).join(', ')}`);
|
|
375
|
+
switch (capability) {
|
|
376
|
+
case Capability.LISTEN:
|
|
377
|
+
if (!this.rpiDriver.hasCapability(Capability.LISTEN)) {
|
|
378
|
+
throw new TJBotError('TJBot is not configured to listen. ' +
|
|
379
|
+
'Please check that you included the ' +
|
|
380
|
+
`${Hardware.MICROPHONE} hardware in TJBot's configuration.`);
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
case Capability.SEE:
|
|
384
|
+
if (!this.rpiDriver.hasCapability(Capability.SEE)) {
|
|
385
|
+
throw new TJBotError('TJBot is not configured to see. ' +
|
|
386
|
+
'Please check that you included the ' +
|
|
387
|
+
`${Hardware.CAMERA} hardware in TJBot's configuration.`);
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
case Capability.SHINE:
|
|
391
|
+
if (!this.rpiDriver.hasCapability(Capability.SHINE)) {
|
|
392
|
+
throw new TJBotError('TJBot is not configured with an LED. ' +
|
|
393
|
+
'Please check that you included the ' +
|
|
394
|
+
`${Hardware.LED} ` +
|
|
395
|
+
"hardware in TJBot's configuration.");
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
case Capability.SPEAK:
|
|
399
|
+
if (!this.rpiDriver.hasCapability(Capability.SPEAK)) {
|
|
400
|
+
throw new TJBotError('TJBot is not configured to speak. ' +
|
|
401
|
+
'Please check that you included the ' +
|
|
402
|
+
`${Hardware.SPEAKER} hardware in TJBot's configuration.`);
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
case Capability.WAVE:
|
|
406
|
+
if (!this.rpiDriver.hasCapability(Capability.WAVE)) {
|
|
407
|
+
throw new TJBotError('TJBot is not configured with an arm. ' +
|
|
408
|
+
'Please check that you included the ' +
|
|
409
|
+
`${Hardware.SERVO} hardware in TJBot's configuration.`);
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
default:
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Sleep for the specified number of seconds.
|
|
418
|
+
* @param sec Number of seconds to sleep
|
|
419
|
+
*/
|
|
420
|
+
async sleep(sec) {
|
|
421
|
+
await asyncSleep(sec);
|
|
422
|
+
}
|
|
423
|
+
/** ------------------------------------------------------------------------ */
|
|
424
|
+
/** LOCAL AI/ML MODELS */
|
|
425
|
+
/** ------------------------------------------------------------------------ */
|
|
426
|
+
/**
|
|
427
|
+
* List the AI/ML models on this device.
|
|
428
|
+
* @returns {string[]} Array of installed model keys
|
|
429
|
+
*/
|
|
430
|
+
getLocalModels(modelType, installedOnly = true) {
|
|
431
|
+
const registry = ModelRegistry.getInstance();
|
|
432
|
+
const models = registry.lookupModels(modelType, installedOnly);
|
|
433
|
+
return models.map((model) => model.key);
|
|
434
|
+
}
|
|
435
|
+
async listen(onPartialResult, onFinalResult) {
|
|
436
|
+
// make sure we can listen
|
|
437
|
+
this.assertCapability(Capability.LISTEN);
|
|
438
|
+
const listenConfig = this.config.listen ?? {};
|
|
439
|
+
const mode = inferSTTMode(listenConfig);
|
|
440
|
+
const modelName = listenConfig.backend?.local?.model ?? '<unknown>';
|
|
441
|
+
if (mode === 'streaming' && !onPartialResult) {
|
|
442
|
+
throw new TJBotError(`STT model "${modelName}" is streaming. Call listen(onPartialResult, onFinalResult) so TJBot can deliver partial/final transcripts.`);
|
|
443
|
+
}
|
|
444
|
+
if (mode === 'offline' && onPartialResult) {
|
|
445
|
+
throw new TJBotError(`STT model "${modelName}" is offline. Call await listen() without a callback.`);
|
|
446
|
+
}
|
|
447
|
+
if (mode === 'streaming') {
|
|
448
|
+
// Streaming: deliver partial/final via the provided callback. The promise resolves when the backend signals completion.
|
|
449
|
+
return await this.rpiDriver.listenForTranscript({
|
|
450
|
+
onPartialResult: (text) => onPartialResult?.(text),
|
|
451
|
+
onFinalResult: (text) => onFinalResult?.(text),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
// Offline / single-shot: return the transcript
|
|
455
|
+
const message = await this.rpiDriver.listenForTranscript();
|
|
456
|
+
logger.info(`Heard: "${message}"`);
|
|
457
|
+
return message;
|
|
458
|
+
}
|
|
459
|
+
/** ------------------------------------------------------------------------ */
|
|
460
|
+
/** SEE */
|
|
461
|
+
/** ------------------------------------------------------------------------ */
|
|
462
|
+
/**
|
|
463
|
+
* Capture an image and return it as a buffer.
|
|
464
|
+
* @return {Promise<Buffer>} The captured image as a buffer.
|
|
465
|
+
* @throws {TJBotError} if the camera hardware is not initialized
|
|
466
|
+
* @public
|
|
467
|
+
*/
|
|
468
|
+
async see() {
|
|
469
|
+
this.assertCapability(Capability.SEE);
|
|
470
|
+
try {
|
|
471
|
+
const buffer = await this.rpiDriver.capturePhotoBuffer();
|
|
472
|
+
return buffer;
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
const photoPath = await this.rpiDriver.capturePhoto();
|
|
476
|
+
try {
|
|
477
|
+
return await fsPromises.readFile(photoPath);
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
// Best-effort cleanup for temporary capture paths.
|
|
481
|
+
await fsPromises.unlink(photoPath).catch(() => { });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Capture an image and save it in the given path.
|
|
487
|
+
* @param {string=} filePath (optional) Path at which to save the photo file. If not
|
|
488
|
+
* specified, photo will be saved in a temp location.
|
|
489
|
+
* @return {string} Path at which the photo was saved.
|
|
490
|
+
* @throws {TJBotError} if the camera hardware is not initialized
|
|
491
|
+
* @public
|
|
492
|
+
*/
|
|
493
|
+
async look(filePath) {
|
|
494
|
+
this.assertCapability(Capability.SEE);
|
|
495
|
+
const path = await this.rpiDriver.capturePhoto(filePath);
|
|
496
|
+
return path;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Detect objects in an image using the configured vision engine.
|
|
500
|
+
* @param {Buffer|string} image Image buffer or file path
|
|
501
|
+
* @returns {Promise<ObjectDetectionResult[]>}
|
|
502
|
+
*/
|
|
503
|
+
async detectObjects(image) {
|
|
504
|
+
this.assertCapability(Capability.SEE);
|
|
505
|
+
return this.rpiDriver.detectObjects(image);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Classify an image using the configured vision engine.
|
|
509
|
+
* @param {Buffer|string} image Image buffer or file path
|
|
510
|
+
* @returns {Promise<ImageClassificationResult[]>}
|
|
511
|
+
*/
|
|
512
|
+
async classifyImage(image) {
|
|
513
|
+
this.assertCapability(Capability.SEE);
|
|
514
|
+
return this.rpiDriver.classifyImage(image);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Detect faces in an image using the configured vision engine.
|
|
518
|
+
* @param {Buffer|string} image Image buffer or file path
|
|
519
|
+
* @returns {Promise<{isFaceDetected: boolean, metadata: FaceDetectionMetadata[]}>}
|
|
520
|
+
*/
|
|
521
|
+
async detectFaces(image) {
|
|
522
|
+
this.assertCapability(Capability.SEE);
|
|
523
|
+
return this.rpiDriver.detectFaces(image);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Describe an image using the configured vision engine (Azure only).
|
|
527
|
+
* @param {Buffer|string} image Image buffer or file path
|
|
528
|
+
* @returns {Promise<ImageDescriptionResult>}
|
|
529
|
+
*/
|
|
530
|
+
async describeImage(image) {
|
|
531
|
+
this.assertCapability(Capability.SEE);
|
|
532
|
+
return this.rpiDriver.describeImage(image);
|
|
533
|
+
}
|
|
534
|
+
/** ------------------------------------------------------------------------ */
|
|
535
|
+
/** SHINE */
|
|
536
|
+
/** ------------------------------------------------------------------------ */
|
|
537
|
+
/**
|
|
538
|
+
* Change the color of the LED.
|
|
539
|
+
* @param {string} color The color to shine the LED. May be specified in a number of
|
|
540
|
+
* formats, including: hexadecimal, (e.g. "0xF12AC4", "11FF22", "#AABB24"), "on", "off",
|
|
541
|
+
* or may be a named color in the `colornames` package. Hexadecimal colors
|
|
542
|
+
* follow an #RRGGBB format.
|
|
543
|
+
* @returns {Promise<void>} A promise that resolves when the LED color has been set.
|
|
544
|
+
* @see {@link https://github.com/timoxley/colornames|Colornames} for a list of color names.
|
|
545
|
+
* @throws {TJBotError} if the LED hardware is not initialized or if color is invalid
|
|
546
|
+
* @public
|
|
547
|
+
*/
|
|
548
|
+
async shine(color) {
|
|
549
|
+
this.assertCapability(Capability.SHINE);
|
|
550
|
+
// normalize the color
|
|
551
|
+
let c = normalizeColor(color);
|
|
552
|
+
// remove leading '#' if present
|
|
553
|
+
if (c.startsWith('#')) {
|
|
554
|
+
c = c.substring(1);
|
|
555
|
+
}
|
|
556
|
+
// shine!
|
|
557
|
+
await this.rpiDriver.renderLED(c);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Pulse the LED a single time.
|
|
561
|
+
* @param {string} color The color to shine the LED. May be specified in a number of
|
|
562
|
+
* formats, including: hexadecimal, (e.g. "0xF12AC4", "11FF22", "#AABB24"), "on", "off",
|
|
563
|
+
* or may be a named color in the `colornames` package. Hexadecimal colors
|
|
564
|
+
* follow an #RRGGBB format.
|
|
565
|
+
* @param {float=} duration The duration the pulse should last. The duration should be in
|
|
566
|
+
* the range [0.5, 2.0] seconds.
|
|
567
|
+
* @returns {Promise<void>} A promise that resolves when the LED pulse animation completes.
|
|
568
|
+
* @see {@link https://github.com/timoxley/colornames|Colornames} for a list of color names.
|
|
569
|
+
* @throws {TJBotError} if the LED hardware is not initialized, color is invalid, or duration exceeds 2.0 seconds
|
|
570
|
+
* @public
|
|
571
|
+
*/
|
|
572
|
+
async pulse(color, duration = 1.0) {
|
|
573
|
+
this.assertCapability(Capability.SHINE);
|
|
574
|
+
if (duration < 0.5) {
|
|
575
|
+
logger.warn('TJBot cannot pulse for less than 0.5 seconds, using duration of 0.5 seconds');
|
|
576
|
+
duration = 0.5;
|
|
577
|
+
}
|
|
578
|
+
if (duration > 2.0) {
|
|
579
|
+
logger.warn('TJBot cannot pulse for more than 2 seconds, using duration of 2.0 seconds');
|
|
580
|
+
duration = 2.0;
|
|
581
|
+
}
|
|
582
|
+
// number of easing steps
|
|
583
|
+
const numSteps = 20;
|
|
584
|
+
// quadratic in-out easing
|
|
585
|
+
let ease = [];
|
|
586
|
+
for (let i = 0; i < numSteps; i += 1) {
|
|
587
|
+
ease.push(i);
|
|
588
|
+
}
|
|
589
|
+
ease = ease.map((x, i) => easeInOutQuad(i, 0, 1, ease.length));
|
|
590
|
+
// normalize to 'duration' sec
|
|
591
|
+
ease = ease.map((x) => x * duration);
|
|
592
|
+
// convert to deltas
|
|
593
|
+
const easeDelays = [];
|
|
594
|
+
for (let i = 0; i < ease.length - 1; i += 1) {
|
|
595
|
+
easeDelays[i] = ease[i + 1] - ease[i];
|
|
596
|
+
}
|
|
597
|
+
// color ramp
|
|
598
|
+
const rgb = normalizeColor(color).slice(1); // remove the #
|
|
599
|
+
const hex = new cm.HexRgb(rgb);
|
|
600
|
+
const colorRamp = [];
|
|
601
|
+
for (let i = 0; i < numSteps / 2; i += 1) {
|
|
602
|
+
const l = 0.0 + (i / (numSteps / 2)) * 0.5;
|
|
603
|
+
colorRamp[i] = hex.toHsl().lightness(l).toRgb().toHexString().replace('#', '0x');
|
|
604
|
+
}
|
|
605
|
+
logger.silly(`color ramp for pulse: ${colorRamp.join(', ')}`);
|
|
606
|
+
// perform the ease
|
|
607
|
+
logger.verbose(`pulsing my LED to RGB color ${rgb}`);
|
|
608
|
+
for (let i = 0; i < easeDelays.length; i += 1) {
|
|
609
|
+
const c = i < colorRamp.length ? colorRamp[i] : colorRamp[colorRamp.length - 1 - (i - colorRamp.length) - 1];
|
|
610
|
+
logger.silly(`pulse step ${i}: setting color to ${c}`);
|
|
611
|
+
await this.shine(c);
|
|
612
|
+
await asyncSleep(easeDelays[i]);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Get the list of all colors recognized by TJBot.
|
|
617
|
+
* @return {array} List of all named colors recognized by `shine()` and `pulse()`.
|
|
618
|
+
* @public
|
|
619
|
+
*/
|
|
620
|
+
shineColors() {
|
|
621
|
+
if (this._shineColors.length === 0) {
|
|
622
|
+
this._shineColors = getShineColors();
|
|
623
|
+
}
|
|
624
|
+
return this._shineColors;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Get a random color.
|
|
628
|
+
* @return {string} Random named color.
|
|
629
|
+
* @public
|
|
630
|
+
*/
|
|
631
|
+
randomColor() {
|
|
632
|
+
const colors = this.shineColors();
|
|
633
|
+
const randIdx = Math.floor(Math.random() * colors.length);
|
|
634
|
+
const randColor = colors[randIdx];
|
|
635
|
+
return randColor;
|
|
636
|
+
}
|
|
637
|
+
/** ------------------------------------------------------------------------ */
|
|
638
|
+
/** SPEAK */
|
|
639
|
+
/** ------------------------------------------------------------------------ */
|
|
640
|
+
/**
|
|
641
|
+
* Speak a message.
|
|
642
|
+
* @param {string} message The message to speak.
|
|
643
|
+
* @throws {TJBotError} if the speaker hardware is not initialized
|
|
644
|
+
* @public
|
|
645
|
+
*/
|
|
646
|
+
async speak(message) {
|
|
647
|
+
this.assertCapability(Capability.SPEAK);
|
|
648
|
+
logger.info(`Speaking: "${message}"`);
|
|
649
|
+
// Delegate to the SpeakerController which handles TTS synthesis and audio playback
|
|
650
|
+
await this.rpiDriver.speak(message);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Play a sound at the specified path.
|
|
654
|
+
* @param {string} soundFile The path to the sound file to be played.
|
|
655
|
+
* @public
|
|
656
|
+
*/
|
|
657
|
+
async play(soundFile) {
|
|
658
|
+
logger.info(`Playing sound: ${soundFile}`);
|
|
659
|
+
await this.rpiDriver.playAudio(soundFile);
|
|
660
|
+
}
|
|
661
|
+
/** ------------------------------------------------------------------------ */
|
|
662
|
+
/** WAVE */
|
|
663
|
+
/** ------------------------------------------------------------------------ */
|
|
664
|
+
/**
|
|
665
|
+
* Moves TJBot's arm all the way back. If this method doesn't move the arm all the way back, the servo motor stop point defined in TJBot.Servo.ARM_BACK may need to be overridden. Valid servo values are in the range [500, 2300].
|
|
666
|
+
* @throws {TJBotError} if the servo hardware is not initialized
|
|
667
|
+
* @returns {Promise<void>} Resolves when the arm is fully back.
|
|
668
|
+
* @example tj.armBack()
|
|
669
|
+
* @public
|
|
670
|
+
*/
|
|
671
|
+
armBack() {
|
|
672
|
+
this.assertCapability(Capability.WAVE);
|
|
673
|
+
logger.info("Moving TJBot's arm back");
|
|
674
|
+
return new Promise((resolve) => {
|
|
675
|
+
this.rpiDriver.renderServoPosition(ServoPosition.ARM_BACK);
|
|
676
|
+
resolve();
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Raises TJBot's arm. If this method doesn't move the arm all the way back, the servo motor stop point defined in TJBot.Servo.ARM_UP may need to be overridden. Valid servo values are in the range [500, 2300].
|
|
681
|
+
* @throws {TJBotError} if the servo hardware is not initialized
|
|
682
|
+
* @returns {Promise<void>} Resolves when the arm is fully raised.
|
|
683
|
+
* @example tj.raiseArm()
|
|
684
|
+
* @public
|
|
685
|
+
*/
|
|
686
|
+
async raiseArm() {
|
|
687
|
+
this.assertCapability(Capability.WAVE);
|
|
688
|
+
logger.info("Raising TJBot's arm");
|
|
689
|
+
return new Promise((resolve) => {
|
|
690
|
+
this.rpiDriver.renderServoPosition(ServoPosition.ARM_UP);
|
|
691
|
+
resolve();
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Lowers TJBot's arm. If this method doesn't move the arm all the way back, the servo motor stop point defined in TJBot.Servo.ARM_DOWN may need to be overridden. Valid servo values are in the range [500, 2300].
|
|
696
|
+
* @throws {TJBotError} if the servo hardware is not initialized
|
|
697
|
+
* @returns {Promise<void>} Resolves when the arm is fully lowered.
|
|
698
|
+
* @example tj.lowerArm()
|
|
699
|
+
* @public
|
|
700
|
+
*/
|
|
701
|
+
async lowerArm() {
|
|
702
|
+
this.assertCapability(Capability.WAVE);
|
|
703
|
+
logger.info("Lowering TJBot's arm");
|
|
704
|
+
return new Promise((resolve) => {
|
|
705
|
+
this.rpiDriver.renderServoPosition(ServoPosition.ARM_DOWN);
|
|
706
|
+
resolve();
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Waves TJBots's arm once.
|
|
711
|
+
* @throws {TJBotError} if the servo hardware is not initialized
|
|
712
|
+
* @returns {Promise<void>} Resolves when the wave is complete.
|
|
713
|
+
* @example tj.wave()
|
|
714
|
+
* @public
|
|
715
|
+
*/
|
|
716
|
+
async wave() {
|
|
717
|
+
this.assertCapability(Capability.WAVE);
|
|
718
|
+
logger.verbose("Waving TJBot's arm");
|
|
719
|
+
const delay = 0.2;
|
|
720
|
+
this.rpiDriver.renderServoPosition(ServoPosition.ARM_UP);
|
|
721
|
+
await asyncSleep(delay);
|
|
722
|
+
this.rpiDriver.renderServoPosition(ServoPosition.ARM_DOWN);
|
|
723
|
+
await asyncSleep(delay);
|
|
724
|
+
this.rpiDriver.renderServoPosition(ServoPosition.ARM_UP);
|
|
725
|
+
await asyncSleep(delay);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/** ------------------------------------------------------------------------ */
|
|
729
|
+
/** MODULE EXPORTS */
|
|
730
|
+
/** ------------------------------------------------------------------------ */
|
|
731
|
+
/**
|
|
732
|
+
* Export TJBot!
|
|
733
|
+
*/
|
|
734
|
+
export { TJBot };
|
|
735
|
+
export default TJBot;
|
|
736
|
+
//# sourceMappingURL=tjbot.js.map
|