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.
Files changed (224) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +382 -0
  3. package/dist/camera/camera.d.ts +62 -0
  4. package/dist/camera/camera.d.ts.map +1 -0
  5. package/dist/camera/camera.js +155 -0
  6. package/dist/camera/camera.js.map +1 -0
  7. package/dist/camera/index.d.ts +18 -0
  8. package/dist/camera/index.d.ts.map +1 -0
  9. package/dist/camera/index.js +18 -0
  10. package/dist/camera/index.js.map +1 -0
  11. package/dist/config/config-types.d.ts +75 -0
  12. package/dist/config/config-types.d.ts.map +1 -0
  13. package/dist/config/config-types.generated.d.ts +495 -0
  14. package/dist/config/config-types.generated.d.ts.map +1 -0
  15. package/dist/config/config-types.generated.js +2 -0
  16. package/dist/config/config-types.generated.js.map +1 -0
  17. package/dist/config/config-types.js +175 -0
  18. package/dist/config/config-types.js.map +1 -0
  19. package/dist/config/index.d.ts +20 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +19 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/config/tjbot-config.d.ts +98 -0
  24. package/dist/config/tjbot-config.d.ts.map +1 -0
  25. package/dist/config/tjbot-config.js +309 -0
  26. package/dist/config/tjbot-config.js.map +1 -0
  27. package/dist/config/vendor/colors.yaml +61 -0
  28. package/dist/config/vendor/model-registry.yaml +275 -0
  29. package/dist/config/vendor/tjbot-config.schema.yaml +792 -0
  30. package/dist/config/vendor/tjbot.default.toml +452 -0
  31. package/dist/led/index.d.ts +20 -0
  32. package/dist/led/index.d.ts.map +1 -0
  33. package/dist/led/index.js +20 -0
  34. package/dist/led/index.js.map +1 -0
  35. package/dist/led/led-common-anode.d.ts +38 -0
  36. package/dist/led/led-common-anode.d.ts.map +1 -0
  37. package/dist/led/led-common-anode.js +79 -0
  38. package/dist/led/led-common-anode.js.map +1 -0
  39. package/dist/led/led-neopixel-spi.d.ts +60 -0
  40. package/dist/led/led-neopixel-spi.d.ts.map +1 -0
  41. package/dist/led/led-neopixel-spi.js +216 -0
  42. package/dist/led/led-neopixel-spi.js.map +1 -0
  43. package/dist/led/led-neopixel-ws281x.js +186 -0
  44. package/dist/led/led-neopixel.d.ts +57 -0
  45. package/dist/led/led-neopixel.d.ts.map +1 -0
  46. package/dist/led/led-neopixel.js +235 -0
  47. package/dist/led/led-neopixel.js.map +1 -0
  48. package/dist/microphone/index.d.ts +18 -0
  49. package/dist/microphone/index.d.ts.map +1 -0
  50. package/dist/microphone/index.js +18 -0
  51. package/dist/microphone/index.js.map +1 -0
  52. package/dist/microphone/microphone.d.ts +65 -0
  53. package/dist/microphone/microphone.d.ts.map +1 -0
  54. package/dist/microphone/microphone.js +179 -0
  55. package/dist/microphone/microphone.js.map +1 -0
  56. package/dist/rpi-drivers/index.d.ts +22 -0
  57. package/dist/rpi-drivers/index.d.ts.map +1 -0
  58. package/dist/rpi-drivers/index.js +22 -0
  59. package/dist/rpi-drivers/index.js.map +1 -0
  60. package/dist/rpi-drivers/rpi-detect.d.ts +24 -0
  61. package/dist/rpi-drivers/rpi-detect.d.ts.map +1 -0
  62. package/dist/rpi-drivers/rpi-detect.js +49 -0
  63. package/dist/rpi-drivers/rpi-detect.js.map +1 -0
  64. package/dist/rpi-drivers/rpi-driver.d.ts +116 -0
  65. package/dist/rpi-drivers/rpi-driver.d.ts.map +1 -0
  66. package/dist/rpi-drivers/rpi-driver.js +261 -0
  67. package/dist/rpi-drivers/rpi-driver.js.map +1 -0
  68. package/dist/rpi-drivers/rpi3-driver.d.ts +47 -0
  69. package/dist/rpi-drivers/rpi3-driver.d.ts.map +1 -0
  70. package/dist/rpi-drivers/rpi3-driver.js +145 -0
  71. package/dist/rpi-drivers/rpi3-driver.js.map +1 -0
  72. package/dist/rpi-drivers/rpi4-driver.d.ts +35 -0
  73. package/dist/rpi-drivers/rpi4-driver.d.ts.map +1 -0
  74. package/dist/rpi-drivers/rpi4-driver.js +101 -0
  75. package/dist/rpi-drivers/rpi4-driver.js.map +1 -0
  76. package/dist/rpi-drivers/rpi5-driver.d.ts +33 -0
  77. package/dist/rpi-drivers/rpi5-driver.d.ts.map +1 -0
  78. package/dist/rpi-drivers/rpi5-driver.js +78 -0
  79. package/dist/rpi-drivers/rpi5-driver.js.map +1 -0
  80. package/dist/servo/index.d.ts +19 -0
  81. package/dist/servo/index.d.ts.map +1 -0
  82. package/dist/servo/index.js +19 -0
  83. package/dist/servo/index.js.map +1 -0
  84. package/dist/servo/servo-constants.d.ts +33 -0
  85. package/dist/servo/servo-constants.d.ts.map +1 -0
  86. package/dist/servo/servo-constants.js +34 -0
  87. package/dist/servo/servo-constants.js.map +1 -0
  88. package/dist/servo/servo-lgpio.d.ts +82 -0
  89. package/dist/servo/servo-lgpio.d.ts.map +1 -0
  90. package/dist/servo/servo-lgpio.js +178 -0
  91. package/dist/servo/servo-lgpio.js.map +1 -0
  92. package/dist/speaker/audio-player.d.ts +30 -0
  93. package/dist/speaker/audio-player.d.ts.map +1 -0
  94. package/dist/speaker/audio-player.js +68 -0
  95. package/dist/speaker/audio-player.js.map +1 -0
  96. package/dist/speaker/index.d.ts +18 -0
  97. package/dist/speaker/index.d.ts.map +1 -0
  98. package/dist/speaker/index.js +18 -0
  99. package/dist/speaker/index.js.map +1 -0
  100. package/dist/speaker/speaker.d.ts +53 -0
  101. package/dist/speaker/speaker.d.ts.map +1 -0
  102. package/dist/speaker/speaker.js +125 -0
  103. package/dist/speaker/speaker.js.map +1 -0
  104. package/dist/stt/backends/azure-stt.d.ts +32 -0
  105. package/dist/stt/backends/azure-stt.d.ts.map +1 -0
  106. package/dist/stt/backends/azure-stt.js +227 -0
  107. package/dist/stt/backends/azure-stt.js.map +1 -0
  108. package/dist/stt/backends/google-cloud-stt.d.ts +31 -0
  109. package/dist/stt/backends/google-cloud-stt.d.ts.map +1 -0
  110. package/dist/stt/backends/google-cloud-stt.js +371 -0
  111. package/dist/stt/backends/google-cloud-stt.js.map +1 -0
  112. package/dist/stt/backends/ibm-watson-stt.d.ts +32 -0
  113. package/dist/stt/backends/ibm-watson-stt.d.ts.map +1 -0
  114. package/dist/stt/backends/ibm-watson-stt.js +190 -0
  115. package/dist/stt/backends/ibm-watson-stt.js.map +1 -0
  116. package/dist/stt/backends/sherpa-onnx-stt.d.ts +117 -0
  117. package/dist/stt/backends/sherpa-onnx-stt.d.ts.map +1 -0
  118. package/dist/stt/backends/sherpa-onnx-stt.js +694 -0
  119. package/dist/stt/backends/sherpa-onnx-stt.js.map +1 -0
  120. package/dist/stt/index.d.ts +20 -0
  121. package/dist/stt/index.d.ts.map +1 -0
  122. package/dist/stt/index.js +21 -0
  123. package/dist/stt/index.js.map +1 -0
  124. package/dist/stt/stt-engine.d.ts +68 -0
  125. package/dist/stt/stt-engine.d.ts.map +1 -0
  126. package/dist/stt/stt-engine.js +99 -0
  127. package/dist/stt/stt-engine.js.map +1 -0
  128. package/dist/stt/stt-utils.d.ts +36 -0
  129. package/dist/stt/stt-utils.d.ts.map +1 -0
  130. package/dist/stt/stt-utils.js +112 -0
  131. package/dist/stt/stt-utils.js.map +1 -0
  132. package/dist/stt/stt.d.ts +52 -0
  133. package/dist/stt/stt.d.ts.map +1 -0
  134. package/dist/stt/stt.js +100 -0
  135. package/dist/stt/stt.js.map +1 -0
  136. package/dist/tjbot.d.ts +317 -0
  137. package/dist/tjbot.d.ts.map +1 -0
  138. package/dist/tjbot.js +736 -0
  139. package/dist/tjbot.js.map +1 -0
  140. package/dist/tts/backends/azure-tts.d.ts +30 -0
  141. package/dist/tts/backends/azure-tts.d.ts.map +1 -0
  142. package/dist/tts/backends/azure-tts.js +92 -0
  143. package/dist/tts/backends/azure-tts.js.map +1 -0
  144. package/dist/tts/backends/google-cloud-tts.d.ts +38 -0
  145. package/dist/tts/backends/google-cloud-tts.d.ts.map +1 -0
  146. package/dist/tts/backends/google-cloud-tts.js +116 -0
  147. package/dist/tts/backends/google-cloud-tts.js.map +1 -0
  148. package/dist/tts/backends/ibm-watson-tts.d.ts +42 -0
  149. package/dist/tts/backends/ibm-watson-tts.d.ts.map +1 -0
  150. package/dist/tts/backends/ibm-watson-tts.js +99 -0
  151. package/dist/tts/backends/ibm-watson-tts.js.map +1 -0
  152. package/dist/tts/backends/sherpa-onnx-tts.d.ts +80 -0
  153. package/dist/tts/backends/sherpa-onnx-tts.d.ts.map +1 -0
  154. package/dist/tts/backends/sherpa-onnx-tts.js +237 -0
  155. package/dist/tts/backends/sherpa-onnx-tts.js.map +1 -0
  156. package/dist/tts/index.d.ts +19 -0
  157. package/dist/tts/index.d.ts.map +1 -0
  158. package/dist/tts/index.js +20 -0
  159. package/dist/tts/index.js.map +1 -0
  160. package/dist/tts/tts-engine.d.ts +67 -0
  161. package/dist/tts/tts-engine.d.ts.map +1 -0
  162. package/dist/tts/tts-engine.js +109 -0
  163. package/dist/tts/tts-engine.js.map +1 -0
  164. package/dist/tts/tts.d.ts +47 -0
  165. package/dist/tts/tts.d.ts.map +1 -0
  166. package/dist/tts/tts.js +101 -0
  167. package/dist/tts/tts.js.map +1 -0
  168. package/dist/utils/colors.d.ts +39 -0
  169. package/dist/utils/colors.d.ts.map +1 -0
  170. package/dist/utils/colors.js +155 -0
  171. package/dist/utils/colors.js.map +1 -0
  172. package/dist/utils/constants.d.ts +41 -0
  173. package/dist/utils/constants.d.ts.map +1 -0
  174. package/dist/utils/constants.js +43 -0
  175. package/dist/utils/constants.js.map +1 -0
  176. package/dist/utils/credentials.d.ts +43 -0
  177. package/dist/utils/credentials.d.ts.map +1 -0
  178. package/dist/utils/credentials.js +121 -0
  179. package/dist/utils/credentials.js.map +1 -0
  180. package/dist/utils/errors.d.ts +26 -0
  181. package/dist/utils/errors.d.ts.map +1 -0
  182. package/dist/utils/errors.js +32 -0
  183. package/dist/utils/errors.js.map +1 -0
  184. package/dist/utils/index.d.ts +25 -0
  185. package/dist/utils/index.d.ts.map +1 -0
  186. package/dist/utils/index.js +23 -0
  187. package/dist/utils/index.js.map +1 -0
  188. package/dist/utils/logging.d.ts +44 -0
  189. package/dist/utils/logging.d.ts.map +1 -0
  190. package/dist/utils/logging.js +113 -0
  191. package/dist/utils/logging.js.map +1 -0
  192. package/dist/utils/model-registry.d.ts +142 -0
  193. package/dist/utils/model-registry.d.ts.map +1 -0
  194. package/dist/utils/model-registry.js +391 -0
  195. package/dist/utils/model-registry.js.map +1 -0
  196. package/dist/utils/utils.d.ts +33 -0
  197. package/dist/utils/utils.d.ts.map +1 -0
  198. package/dist/utils/utils.js +50 -0
  199. package/dist/utils/utils.js.map +1 -0
  200. package/dist/vision/backends/azure-vision.d.ts +33 -0
  201. package/dist/vision/backends/azure-vision.d.ts.map +1 -0
  202. package/dist/vision/backends/azure-vision.js +151 -0
  203. package/dist/vision/backends/azure-vision.js.map +1 -0
  204. package/dist/vision/backends/google-cloud-vision.d.ts +32 -0
  205. package/dist/vision/backends/google-cloud-vision.d.ts.map +1 -0
  206. package/dist/vision/backends/google-cloud-vision.js +193 -0
  207. package/dist/vision/backends/google-cloud-vision.js.map +1 -0
  208. package/dist/vision/backends/onnx.d.ts +116 -0
  209. package/dist/vision/backends/onnx.d.ts.map +1 -0
  210. package/dist/vision/backends/onnx.js +781 -0
  211. package/dist/vision/backends/onnx.js.map +1 -0
  212. package/dist/vision/index.d.ts +19 -0
  213. package/dist/vision/index.d.ts.map +1 -0
  214. package/dist/vision/index.js +20 -0
  215. package/dist/vision/index.js.map +1 -0
  216. package/dist/vision/vision-engine.d.ts +131 -0
  217. package/dist/vision/vision-engine.d.ts.map +1 -0
  218. package/dist/vision/vision-engine.js +97 -0
  219. package/dist/vision/vision-engine.js.map +1 -0
  220. package/dist/vision/vision.d.ts +48 -0
  221. package/dist/vision/vision.d.ts.map +1 -0
  222. package/dist/vision/vision.js +83 -0
  223. package/dist/vision/vision.js.map +1 -0
  224. 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