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
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Copyright 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 SPI from 'pi-spi';
18
+ /**
19
+ * LED controller for SPI-based NeoPixel LEDs (Raspberry Pi 5)
20
+ * This is based on pi5neo.py:
21
+ * https://github.com/vanshksingh/Pi5Neo/blob/main/pi5neo/pi5neo.py
22
+ */
23
+ export declare class LEDNeopixelSPI {
24
+ spi: SPI.SPI;
25
+ useGRBFormat: boolean;
26
+ private isPrimed;
27
+ private static readonly HIGH;
28
+ private static readonly LOW;
29
+ private static readonly FREQ;
30
+ private static readonly RESET_BYTES;
31
+ private static readonly FRAME_REPEATS;
32
+ private static readonly PRIME_OFF_FRAMES;
33
+ private static readonly INTER_FRAME_DELAY_MS;
34
+ private static readonly PRIME_DELAY_MS;
35
+ private static isSPIEnabledFromConfig;
36
+ private static assertSPIEnabled;
37
+ private static isSPIEnabledInFirmwareConfig;
38
+ private static isGpio10MuxedForSPI;
39
+ private static assertSPIPreconditions;
40
+ constructor(spiInterface: string, useGRB?: boolean);
41
+ private static sleep;
42
+ private primeLink;
43
+ private transferFrame;
44
+ private buildFramedBitstream;
45
+ static bitMask(byte: number, index: number): boolean;
46
+ static byteToBitstream(byte: number): number[];
47
+ static rgbToSpiBitstream(red: number, green: number, blue: number, useGRB: boolean): Buffer;
48
+ /**
49
+ * Render the LED to a specified color.
50
+ * @param color The color to render, specified as a string of hexadecimal digits
51
+ * with no leading '0x' or '#' in RRGGBB format.
52
+ * @returns A promise that resolves when the SPI transfer completes.
53
+ */
54
+ render(color: string): Promise<void>;
55
+ /**
56
+ * Clean up resources
57
+ */
58
+ cleanup(): void;
59
+ }
60
+ //# sourceMappingURL=led-neopixel-spi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"led-neopixel-spi.d.ts","sourceRoot":"","sources":["../../src/led/led-neopixel-spi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,GAAG,MAAM,QAAQ,CAAC;AAMzB;;;;GAIG;AACH,qBAAa,cAAc;IACvB,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAU;IAE1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAgB;IAC5C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAgB;IAC3C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAmB;IAC/C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAe;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAa;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAa;IACrD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAa;IACzD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAa;IAEnD,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAarC,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAuB/B,OAAO,CAAC,MAAM,CAAC,4BAA4B;IAS3C,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAoBlC,OAAO,CAAC,MAAM,CAAC,sBAAsB;gBAkBzB,YAAY,EAAE,MAAM,EAAE,MAAM,GAAE,OAAe;IAazD,OAAO,CAAC,MAAM,CAAC,KAAK;YAIN,SAAS;YAUT,aAAa;IAiB3B,OAAO,CAAC,oBAAoB;IAW5B,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAIpD,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAY9C,MAAM,CAAC,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM;IAU3F;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B1C;;OAEG;IACH,OAAO,IAAI,IAAI;CAGlB"}
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Copyright 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 { execSync } from 'child_process';
18
+ import { existsSync, readFileSync } from 'fs';
19
+ import SPI from 'pi-spi';
20
+ import { TJBotError } from '../utils/errors.js';
21
+ import { getLogger } from '../utils/logging.js';
22
+ const logger = getLogger(import.meta.url);
23
+ /**
24
+ * LED controller for SPI-based NeoPixel LEDs (Raspberry Pi 5)
25
+ * This is based on pi5neo.py:
26
+ * https://github.com/vanshksingh/Pi5Neo/blob/main/pi5neo/pi5neo.py
27
+ */
28
+ export class LEDNeopixelSPI {
29
+ spi;
30
+ useGRBFormat;
31
+ isPrimed;
32
+ static HIGH = 0xf8; // possibles: F0, F8, FC
33
+ static LOW = 0xc0; // possibles: C0
34
+ static FREQ = 6400000;
35
+ static RESET_BYTES = 100;
36
+ static FRAME_REPEATS = 2;
37
+ static PRIME_OFF_FRAMES = 6;
38
+ static INTER_FRAME_DELAY_MS = 1;
39
+ static PRIME_DELAY_MS = 2;
40
+ static isSPIEnabledFromConfig(configPath) {
41
+ if (!existsSync(configPath)) {
42
+ return null;
43
+ }
44
+ try {
45
+ const config = readFileSync(configPath, 'utf8');
46
+ return /^\s*dtparam\s*=\s*spi\s*=\s*on\s*$/m.test(config);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ static assertSPIEnabled(spiInterface) {
53
+ const firmwareConfig = '/boot/firmware/config.txt';
54
+ const legacyConfig = '/boot/config.txt';
55
+ const fwEnabled = LEDNeopixelSPI.isSPIEnabledFromConfig(firmwareConfig);
56
+ const legacyEnabled = LEDNeopixelSPI.isSPIEnabledFromConfig(legacyConfig);
57
+ // If either known config explicitly enables SPI, accept.
58
+ if (fwEnabled === true || legacyEnabled === true) {
59
+ return;
60
+ }
61
+ // If a matching spidev node exists, SPI is effectively available.
62
+ if (existsSync(spiInterface)) {
63
+ return;
64
+ }
65
+ throw new TJBotError(`SPI appears to be disabled or unavailable (missing ${spiInterface}). ` +
66
+ 'To enable SPI, edit /boot/firmware/config.txt and set dtparam=spi=on, then reboot your Raspberry Pi.');
67
+ }
68
+ static isSPIEnabledInFirmwareConfig() {
69
+ try {
70
+ const config = readFileSync('/boot/firmware/config.txt', 'utf8');
71
+ return /^\s*dtparam\s*=\s*spi\s*=\s*on\s*$/m.test(config);
72
+ }
73
+ catch {
74
+ return true;
75
+ }
76
+ }
77
+ static isGpio10MuxedForSPI() {
78
+ try {
79
+ const output = execSync('pinctrl get 10', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
80
+ const lower = output.toLowerCase();
81
+ if (lower.includes('= none')) {
82
+ return false;
83
+ }
84
+ if (lower.includes('spi')) {
85
+ return true;
86
+ }
87
+ // On Raspberry Pi 5, SPI0 MOSI on GPIO10 is often ALT3 (shown as a3).
88
+ if (lower.includes(' a3 ') || lower.includes(' a3|') || lower.includes(' a3\t') || lower.includes(': a3')) {
89
+ return true;
90
+ }
91
+ return true;
92
+ }
93
+ catch {
94
+ return true;
95
+ }
96
+ }
97
+ static assertSPIPreconditions(spiInterface) {
98
+ LEDNeopixelSPI.assertSPIEnabled(spiInterface);
99
+ if (!LEDNeopixelSPI.isSPIEnabledInFirmwareConfig()) {
100
+ throw new TJBotError('SPI appears disabled in /boot/firmware/config.txt (dtparam=spi=on not set). ' +
101
+ 'Enable SPI in /boot/firmware/config.txt, reboot, then retry.');
102
+ }
103
+ if (!LEDNeopixelSPI.isGpio10MuxedForSPI()) {
104
+ throw new TJBotError('GPIO10 is not currently muxed for SPI (pinctrl reports none). ' +
105
+ 'Enable SPI in /boot/firmware/config.txt (dtparam=spi=on), reboot, then retry.');
106
+ }
107
+ }
108
+ constructor(spiInterface, useGRB = false) {
109
+ const i = spiInterface || '/dev/spidev0.0';
110
+ LEDNeopixelSPI.assertSPIPreconditions(i);
111
+ this.spi = SPI.initialize(i);
112
+ this.spi.clockSpeed(LEDNeopixelSPI.FREQ);
113
+ this.spi.dataMode(0);
114
+ this.spi.bitOrder(SPI.order.MSB_FIRST);
115
+ this.useGRBFormat = useGRB;
116
+ this.isPrimed = false;
117
+ logger.verbose(`Initialized NeoPixel SPI LED on interface ${i} with GRB format: ${useGRB}`);
118
+ }
119
+ static sleep(ms) {
120
+ return new Promise((resolve) => setTimeout(resolve, ms));
121
+ }
122
+ async primeLink() {
123
+ logger.debug('Priming NeoPixel SPI link with OFF frames');
124
+ const offFrame = this.buildFramedBitstream('000000');
125
+ for (let i = 0; i < LEDNeopixelSPI.PRIME_OFF_FRAMES; i++) {
126
+ await this.transferFrame(offFrame);
127
+ await LEDNeopixelSPI.sleep(LEDNeopixelSPI.PRIME_DELAY_MS);
128
+ }
129
+ logger.debug('NeoPixel SPI link prime complete');
130
+ }
131
+ async transferFrame(bitstream) {
132
+ for (let i = 0; i < LEDNeopixelSPI.FRAME_REPEATS; i++) {
133
+ await new Promise((resolve, reject) => {
134
+ this.spi.transfer(bitstream, (err) => {
135
+ if (err) {
136
+ logger.error('SPI transfer error:', err);
137
+ reject(new TJBotError('SPI transfer failed', { cause: err }));
138
+ }
139
+ else {
140
+ logger.debug('LEDNeopixelSPI.render spi.transfer success');
141
+ resolve();
142
+ }
143
+ });
144
+ });
145
+ await LEDNeopixelSPI.sleep(LEDNeopixelSPI.INTER_FRAME_DELAY_MS);
146
+ }
147
+ }
148
+ buildFramedBitstream(color) {
149
+ const c = parseInt(color, 16);
150
+ const r = (c & 0xff0000) >> 16;
151
+ const g = (c & 0x00ff00) >> 8;
152
+ const b = c & 0x0000ff;
153
+ const colorFrame = LEDNeopixelSPI.rgbToSpiBitstream(r, g, b, this.useGRBFormat);
154
+ const resetBuf = Buffer.alloc(LEDNeopixelSPI.RESET_BYTES, 0x00);
155
+ return Buffer.concat([resetBuf, colorFrame, resetBuf]);
156
+ }
157
+ static bitMask(byte, index) {
158
+ return (byte & (1 << (7 - index))) !== 0;
159
+ }
160
+ static byteToBitstream(byte) {
161
+ // Initialize with low bits
162
+ const bitstream = Array(8).fill(LEDNeopixelSPI.LOW);
163
+ for (let i = 0; i < 8; i++) {
164
+ if (LEDNeopixelSPI.bitMask(byte, i)) {
165
+ // Set high bits for '1'
166
+ bitstream[i] = LEDNeopixelSPI.HIGH;
167
+ }
168
+ }
169
+ return bitstream;
170
+ }
171
+ static rgbToSpiBitstream(red, green, blue, useGRB) {
172
+ const red_bits = LEDNeopixelSPI.byteToBitstream(red);
173
+ const green_bits = LEDNeopixelSPI.byteToBitstream(green);
174
+ const blue_bits = LEDNeopixelSPI.byteToBitstream(blue);
175
+ const bitstream = useGRB
176
+ ? Buffer.from(green_bits.concat(red_bits).concat(blue_bits))
177
+ : Buffer.from(red_bits.concat(green_bits).concat(blue_bits));
178
+ return bitstream;
179
+ }
180
+ /**
181
+ * Render the LED to a specified color.
182
+ * @param color The color to render, specified as a string of hexadecimal digits
183
+ * with no leading '0x' or '#' in RRGGBB format.
184
+ * @returns A promise that resolves when the SPI transfer completes.
185
+ */
186
+ async render(color) {
187
+ logger.debug(`Rendering NeoPixel LED (SPI) with color: ${color}`);
188
+ try {
189
+ if (!this.isPrimed) {
190
+ await this.primeLink();
191
+ this.isPrimed = true;
192
+ }
193
+ const bitstream = this.buildFramedBitstream(color);
194
+ // Transfer data via SPI to update the LED
195
+ // Wait for the transfer to complete before returning
196
+ logger.debug('LEDNeopixelSPI.render about to call spi.transfer');
197
+ await this.transferFrame(bitstream);
198
+ logger.debug('LEDNeopixelSPI.render completed normally');
199
+ }
200
+ catch (e) {
201
+ logger.error('Exception in LEDNeopixelSPI.render:', e);
202
+ // Print stack trace if available
203
+ if (e instanceof Error && e.stack) {
204
+ logger.error(e.stack);
205
+ }
206
+ throw e;
207
+ }
208
+ }
209
+ /**
210
+ * Clean up resources
211
+ */
212
+ cleanup() {
213
+ logger.debug('LEDNeopixelSPI cleanup (no-op)');
214
+ }
215
+ }
216
+ //# sourceMappingURL=led-neopixel-spi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"led-neopixel-spi.js","sourceRoot":"","sources":["../../src/led/led-neopixel-spi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Copyright 2026-present TJBot Contributors. All Rights Reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ /**
20
+ * NeoPixel root helper process.
21
+ *
22
+ * This process is launched as root (via sudo) by LEDNeopixel to gain access to
23
+ * rpi-ws281x-native, which requires root privileges on RPi 3/4.
24
+ *
25
+ * Protocol: newline-delimited JSON on stdin/stdout.
26
+ * Requests:
27
+ * { id, cmd: "init", pin, numLeds }
28
+ * { id, cmd: "render", color } -- color is a 32-bit integer (0xRRGGBB)
29
+ * { id, cmd: "reset" }
30
+ * { id, cmd: "shutdown" }
31
+ * Responses:
32
+ * { id, ok: true }
33
+ * { id, ok: false, error: "<message>" }
34
+ *
35
+ * stderr is for human-readable diagnostics only and does not affect the protocol.
36
+ */
37
+
38
+ import { createRequire } from 'module';
39
+
40
+ // rpi-ws281x-native is a native CJS addon; load it via createRequire so this
41
+ // file can remain an ES module (.js with "type":"module" in package.json).
42
+ const require = createRequire(import.meta.url);
43
+ const ws281x = require('rpi-ws281x-native');
44
+
45
+ const LED_DMA = 10;
46
+ const LED_FREQ_HZ = 800000;
47
+ const LED_BRIGHTNESS = 255;
48
+ const LED_INVERT = false;
49
+ const LED_STRIP_TYPE = ws281x.stripType.WS2812;
50
+
51
+ let initialized = false;
52
+ let channel;
53
+
54
+ /**
55
+ * Reply with a structured JSON response on stdout.
56
+ * @param {number|string} id
57
+ * @param {boolean} ok
58
+ * @param {string|undefined} error
59
+ */
60
+ function reply(id, ok, error) {
61
+ const msg = ok ? { id, ok: true } : { id, ok: false, error: String(error ?? 'unknown error') };
62
+ process.stdout.write(JSON.stringify(msg) + '\n');
63
+ }
64
+
65
+ /**
66
+ * Handle a single parsed command object.
67
+ * @param {{ id: number|string, cmd: string, pin?: number, numLeds?: number, color?: number }} req
68
+ */
69
+ function handle(req) {
70
+ const { id, cmd } = req;
71
+
72
+ try {
73
+ switch (cmd) {
74
+ case 'init': {
75
+ const pin = Number(req.pin);
76
+ const numLeds = Number(req.numLeds ?? 1);
77
+ if (!Number.isInteger(pin) || pin < 0 || pin > 40) {
78
+ reply(id, false, `invalid pin: ${req.pin}`);
79
+ return;
80
+ }
81
+ channel = ws281x(numLeds, {
82
+ gpio: pin,
83
+ dma: LED_DMA,
84
+ freq: LED_FREQ_HZ,
85
+ invert: LED_INVERT,
86
+ brightness: LED_BRIGHTNESS,
87
+ stripType: LED_STRIP_TYPE,
88
+ });
89
+ initialized = true;
90
+ reply(id, true);
91
+ break;
92
+ }
93
+
94
+ case 'render': {
95
+ if (!initialized) {
96
+ reply(id, false, 'not initialized');
97
+ return;
98
+ }
99
+ const color = Number(req.color);
100
+ if (!Number.isInteger(color) || color < 0 || color > 0xffffff) {
101
+ reply(id, false, `invalid color: ${req.color}`);
102
+ return;
103
+ }
104
+ channel.array[0] = color;
105
+ ws281x.render();
106
+ reply(id, true);
107
+ break;
108
+ }
109
+
110
+ case 'reset': {
111
+ if (initialized) {
112
+ ws281x.reset();
113
+ }
114
+ reply(id, true);
115
+ break;
116
+ }
117
+
118
+ case 'shutdown': {
119
+ if (initialized) {
120
+ ws281x.reset();
121
+ ws281x.finalize();
122
+ initialized = false;
123
+ channel = undefined;
124
+ }
125
+ reply(id, true);
126
+ // Give stdout a chance to flush before exiting.
127
+ process.stdout.once('drain', () => process.exit(0));
128
+ // Force exit if drain takes too long (e.g. pipe already closed).
129
+ setTimeout(() => process.exit(0), 200).unref();
130
+ break;
131
+ }
132
+
133
+ default:
134
+ reply(id, false, `unknown command: ${cmd}`);
135
+ }
136
+ } catch (err) {
137
+ reply(id, false, String(err));
138
+ }
139
+ }
140
+
141
+ // --- Stdin line reader ---
142
+ let buffer = '';
143
+
144
+ process.stdin.setEncoding('utf8');
145
+
146
+ process.stdin.on('data', (chunk) => {
147
+ buffer += chunk;
148
+ let newline;
149
+ while ((newline = buffer.indexOf('\n')) !== -1) {
150
+ const line = buffer.slice(0, newline).trim();
151
+ buffer = buffer.slice(newline + 1);
152
+ if (!line) continue;
153
+ try {
154
+ const req = JSON.parse(line);
155
+ handle(req);
156
+ } catch (_) {
157
+ // Malformed JSON — no id to reply to, so silently ignore.
158
+ }
159
+ }
160
+ });
161
+
162
+ process.stdin.on('end', () => {
163
+ // Parent closed the pipe — clean up and exit.
164
+ if (initialized) {
165
+ try {
166
+ ws281x.reset();
167
+ ws281x.finalize();
168
+ } catch (_) {
169
+ /* best effort */
170
+ }
171
+ }
172
+ process.exit(0);
173
+ });
174
+
175
+ // Ensure the helper does not keep a stale process alive if the parent dies.
176
+ process.on('SIGTERM', () => {
177
+ if (initialized) {
178
+ try {
179
+ ws281x.reset();
180
+ ws281x.finalize();
181
+ } catch (_) {
182
+ /* best effort */
183
+ }
184
+ }
185
+ process.exit(0);
186
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Copyright 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
+ /**
18
+ * LED controller for NeoPixel (WS281x) LEDs on Raspberry Pi 3/4.
19
+ *
20
+ * rpi-ws281x-native requires root privileges. Rather than launching every
21
+ * TJBot recipe that uses the LED as root, this class spawns a small, long-lived
22
+ * helper process (in led-neopixel-ws281x.js) using sudo and communicates with it
23
+ * over a newline-delimited JSON IPC channel on stdin/stdout.
24
+ *
25
+ * Sudo authentication is performed once at construction time (either
26
+ * passwordless or via an interactive prompt). Subsequent render() calls are
27
+ * cheap IPC messages with no additional privilege escalation.
28
+ */
29
+ export declare class LEDNeopixel {
30
+ private helper;
31
+ private reader?;
32
+ private helperStderrTail;
33
+ private _ready;
34
+ private _pendingById;
35
+ private _nextId;
36
+ private _helperDead;
37
+ constructor(pin: number);
38
+ /**
39
+ * Wait for the NeoPixel helper to be fully initialized and ready.
40
+ * Call this before loading long-running tasks if the LED needs to be available early.
41
+ */
42
+ initialize(): Promise<void>;
43
+ /**
44
+ * Render the NeoPixel to a specific color.
45
+ * @param color Color as a 32-bit integer in RGB format (0xRRGGBB)
46
+ */
47
+ render(color: number): Promise<void>;
48
+ /**
49
+ * Send a reset command and terminate the helper process.
50
+ */
51
+ cleanup(): Promise<void>;
52
+ private _handleLine;
53
+ private _send;
54
+ private _setHelperHandleRefState;
55
+ private _killHelper;
56
+ }
57
+ //# sourceMappingURL=led-neopixel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"led-neopixel.d.ts","sourceRoot":"","sources":["../../src/led/led-neopixel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAwBH;;;;;;;;;;;GAWG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,MAAM,CAAC,CAAY;IAC3B,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,YAAY,CAA0C;IAC9D,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,WAAW,CAAsB;gBAE7B,GAAG,EAAE,MAAM;IA+FvB;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM1C;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAY9B,OAAO,CAAC,WAAW;IA4BnB,OAAO,CAAC,KAAK;IA0Bb,OAAO,CAAC,wBAAwB;IAqBhC,OAAO,CAAC,WAAW;CAYtB"}