amaran-light-cli 1.11.0 → 1.12.0

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 (363) hide show
  1. package/README.md +108 -14
  2. package/dist/__tests__/autoCct.test.js +27 -0
  3. package/dist/__tests__/autoCct.test.js.map +1 -1
  4. package/dist/__tests__/bleHttpControl.test.d.ts +2 -0
  5. package/dist/__tests__/bleHttpControl.test.d.ts.map +1 -0
  6. package/dist/__tests__/bleHttpControl.test.js +230 -0
  7. package/dist/__tests__/bleHttpControl.test.js.map +1 -0
  8. package/dist/__tests__/bleStack.test.d.ts +2 -0
  9. package/dist/__tests__/bleStack.test.d.ts.map +1 -0
  10. package/dist/__tests__/bleStack.test.js +1239 -0
  11. package/dist/__tests__/bleStack.test.js.map +1 -0
  12. package/dist/__tests__/cctUtil.test.js +9 -4
  13. package/dist/__tests__/cctUtil.test.js.map +1 -1
  14. package/dist/__tests__/circadianDashboardSettings.test.d.ts +2 -0
  15. package/dist/__tests__/circadianDashboardSettings.test.d.ts.map +1 -0
  16. package/dist/__tests__/circadianDashboardSettings.test.js +82 -0
  17. package/dist/__tests__/circadianDashboardSettings.test.js.map +1 -0
  18. package/dist/__tests__/circadianIsolation.test.d.ts +2 -0
  19. package/dist/__tests__/circadianIsolation.test.d.ts.map +1 -0
  20. package/dist/__tests__/circadianIsolation.test.js +30 -0
  21. package/dist/__tests__/circadianIsolation.test.js.map +1 -0
  22. package/dist/__tests__/cli.config.test.js +49 -0
  23. package/dist/__tests__/cli.config.test.js.map +1 -1
  24. package/dist/__tests__/cli.smoke.test.js +34 -0
  25. package/dist/__tests__/cli.smoke.test.js.map +1 -1
  26. package/dist/__tests__/currentCct.test.d.ts +2 -0
  27. package/dist/__tests__/currentCct.test.d.ts.map +1 -0
  28. package/dist/__tests__/currentCct.test.js +29 -0
  29. package/dist/__tests__/currentCct.test.js.map +1 -0
  30. package/dist/__tests__/dashboard.test.d.ts +2 -0
  31. package/dist/__tests__/dashboard.test.d.ts.map +1 -0
  32. package/dist/__tests__/dashboard.test.js +126 -0
  33. package/dist/__tests__/dashboard.test.js.map +1 -0
  34. package/dist/__tests__/daylightSimulationParseUtils.test.d.ts +2 -0
  35. package/dist/__tests__/daylightSimulationParseUtils.test.d.ts.map +1 -0
  36. package/dist/__tests__/daylightSimulationParseUtils.test.js +16 -0
  37. package/dist/__tests__/daylightSimulationParseUtils.test.js.map +1 -0
  38. package/dist/__tests__/desktopImport.test.d.ts +2 -0
  39. package/dist/__tests__/desktopImport.test.d.ts.map +1 -0
  40. package/dist/__tests__/desktopImport.test.js +234 -0
  41. package/dist/__tests__/desktopImport.test.js.map +1 -0
  42. package/dist/__tests__/lightControl.basic.test.d.ts +2 -0
  43. package/dist/__tests__/lightControl.basic.test.d.ts.map +1 -0
  44. package/dist/__tests__/lightControl.basic.test.js +119 -0
  45. package/dist/__tests__/lightControl.basic.test.js.map +1 -0
  46. package/dist/__tests__/lightControl.colorEffects.test.d.ts +2 -0
  47. package/dist/__tests__/lightControl.colorEffects.test.d.ts.map +1 -0
  48. package/dist/__tests__/lightControl.colorEffects.test.js +170 -0
  49. package/dist/__tests__/lightControl.colorEffects.test.js.map +1 -0
  50. package/dist/__tests__/lightControl.management.test.d.ts +2 -0
  51. package/dist/__tests__/lightControl.management.test.d.ts.map +1 -0
  52. package/dist/__tests__/lightControl.management.test.js +90 -0
  53. package/dist/__tests__/lightControl.management.test.js.map +1 -0
  54. package/dist/__tests__/lightControlTestUtils.d.ts +10 -0
  55. package/dist/__tests__/lightControlTestUtils.d.ts.map +1 -0
  56. package/dist/__tests__/lightControlTestUtils.js +57 -0
  57. package/dist/__tests__/lightControlTestUtils.js.map +1 -0
  58. package/dist/__tests__/meshConfiguration.test.d.ts +2 -0
  59. package/dist/__tests__/meshConfiguration.test.d.ts.map +1 -0
  60. package/dist/__tests__/meshConfiguration.test.js +78 -0
  61. package/dist/__tests__/meshConfiguration.test.js.map +1 -0
  62. package/dist/__tests__/nativeGroups.test.d.ts +2 -0
  63. package/dist/__tests__/nativeGroups.test.d.ts.map +1 -0
  64. package/dist/__tests__/nativeGroups.test.js +104 -0
  65. package/dist/__tests__/nativeGroups.test.js.map +1 -0
  66. package/dist/__tests__/programApi.test.d.ts +2 -0
  67. package/dist/__tests__/programApi.test.d.ts.map +1 -0
  68. package/dist/__tests__/programApi.test.js +73 -0
  69. package/dist/__tests__/programApi.test.js.map +1 -0
  70. package/dist/__tests__/programs.test.d.ts +2 -0
  71. package/dist/__tests__/programs.test.d.ts.map +1 -0
  72. package/dist/__tests__/programs.test.js +176 -0
  73. package/dist/__tests__/programs.test.js.map +1 -0
  74. package/dist/__tests__/scheduleMaker.test.d.ts +2 -0
  75. package/dist/__tests__/scheduleMaker.test.d.ts.map +1 -0
  76. package/dist/__tests__/scheduleMaker.test.js +25 -0
  77. package/dist/__tests__/scheduleMaker.test.js.map +1 -0
  78. package/dist/ble/async.d.ts +2 -0
  79. package/dist/ble/async.d.ts.map +1 -0
  80. package/dist/ble/async.js +15 -0
  81. package/dist/ble/async.js.map +1 -0
  82. package/dist/ble/colors.d.ts +5 -0
  83. package/dist/ble/colors.d.ts.map +1 -0
  84. package/dist/ble/colors.js +44 -0
  85. package/dist/ble/colors.js.map +1 -0
  86. package/dist/ble/configuration.d.ts +22 -0
  87. package/dist/ble/configuration.d.ts.map +1 -0
  88. package/dist/ble/configuration.js +122 -0
  89. package/dist/ble/configuration.js.map +1 -0
  90. package/dist/ble/controller.d.ts +118 -0
  91. package/dist/ble/controller.d.ts.map +1 -0
  92. package/dist/ble/controller.js +931 -0
  93. package/dist/ble/controller.js.map +1 -0
  94. package/dist/ble/crypto.d.ts +34 -0
  95. package/dist/ble/crypto.d.ts.map +1 -0
  96. package/dist/ble/crypto.js +178 -0
  97. package/dist/ble/crypto.js.map +1 -0
  98. package/dist/ble/daemon.d.ts +2 -0
  99. package/dist/ble/daemon.d.ts.map +1 -0
  100. package/dist/ble/daemon.js +76 -0
  101. package/dist/ble/daemon.js.map +1 -0
  102. package/dist/ble/dashboard.d.ts +331 -0
  103. package/dist/ble/dashboard.d.ts.map +1 -0
  104. package/dist/ble/dashboard.js +280 -0
  105. package/dist/ble/dashboard.js.map +1 -0
  106. package/dist/ble/desktop.d.ts +4 -0
  107. package/dist/ble/desktop.d.ts.map +1 -0
  108. package/dist/ble/desktop.js +33 -0
  109. package/dist/ble/desktop.js.map +1 -0
  110. package/dist/ble/desktopLibrary.d.ts +12 -0
  111. package/dist/ble/desktopLibrary.d.ts.map +1 -0
  112. package/dist/ble/desktopLibrary.js +346 -0
  113. package/dist/ble/desktopLibrary.js.map +1 -0
  114. package/dist/ble/effects.d.ts +34 -0
  115. package/dist/ble/effects.d.ts.map +1 -0
  116. package/dist/ble/effects.js +138 -0
  117. package/dist/ble/effects.js.map +1 -0
  118. package/dist/ble/fan.d.ts +244 -0
  119. package/dist/ble/fan.d.ts.map +1 -0
  120. package/dist/ble/fan.js +46 -0
  121. package/dist/ble/fan.js.map +1 -0
  122. package/dist/ble/library.d.ts +341 -0
  123. package/dist/ble/library.d.ts.map +1 -0
  124. package/dist/ble/library.js +201 -0
  125. package/dist/ble/library.js.map +1 -0
  126. package/dist/ble/media.d.ts +133 -0
  127. package/dist/ble/media.d.ts.map +1 -0
  128. package/dist/ble/media.js +186 -0
  129. package/dist/ble/media.js.map +1 -0
  130. package/dist/ble/nativeGroups.d.ts +22 -0
  131. package/dist/ble/nativeGroups.d.ts.map +1 -0
  132. package/dist/ble/nativeGroups.js +114 -0
  133. package/dist/ble/nativeGroups.js.map +1 -0
  134. package/dist/ble/packets.d.ts +3 -0
  135. package/dist/ble/packets.d.ts.map +1 -0
  136. package/dist/ble/packets.js +15 -0
  137. package/dist/ble/packets.js.map +1 -0
  138. package/dist/ble/programs.d.ts +318 -0
  139. package/dist/ble/programs.d.ts.map +1 -0
  140. package/dist/ble/programs.js +329 -0
  141. package/dist/ble/programs.js.map +1 -0
  142. package/dist/ble/provisioning.d.ts +13 -0
  143. package/dist/ble/provisioning.d.ts.map +1 -0
  144. package/dist/ble/provisioning.js +39 -0
  145. package/dist/ble/provisioning.js.map +1 -0
  146. package/dist/ble/server.d.ts +18 -0
  147. package/dist/ble/server.d.ts.map +1 -0
  148. package/dist/ble/server.js +548 -0
  149. package/dist/ble/server.js.map +1 -0
  150. package/dist/ble/service.d.ts +2 -0
  151. package/dist/ble/service.d.ts.map +1 -0
  152. package/dist/ble/service.js +77 -0
  153. package/dist/ble/service.js.map +1 -0
  154. package/dist/ble/settings.d.ts +18 -0
  155. package/dist/ble/settings.d.ts.map +1 -0
  156. package/dist/ble/settings.js +25 -0
  157. package/dist/ble/settings.js.map +1 -0
  158. package/dist/ble/setup.d.ts +2 -0
  159. package/dist/ble/setup.d.ts.map +1 -0
  160. package/dist/ble/setup.js +31 -0
  161. package/dist/ble/setup.js.map +1 -0
  162. package/dist/ble/storage.d.ts +94 -0
  163. package/dist/ble/storage.d.ts.map +1 -0
  164. package/dist/ble/storage.js +132 -0
  165. package/dist/ble/storage.js.map +1 -0
  166. package/dist/ble/telink.d.ts +63 -0
  167. package/dist/ble/telink.d.ts.map +1 -0
  168. package/dist/ble/telink.js +108 -0
  169. package/dist/ble/telink.js.map +1 -0
  170. package/dist/ble/transitions.d.ts +3 -0
  171. package/dist/ble/transitions.d.ts.map +1 -0
  172. package/dist/ble/transitions.js +28 -0
  173. package/dist/ble/transitions.js.map +1 -0
  174. package/dist/ble/transport.d.ts +48 -0
  175. package/dist/ble/transport.d.ts.map +1 -0
  176. package/dist/ble/transport.js +370 -0
  177. package/dist/ble/transport.js.map +1 -0
  178. package/dist/circadian.d.ts +3 -0
  179. package/dist/circadian.d.ts.map +1 -1
  180. package/dist/circadian.js +2 -0
  181. package/dist/circadian.js.map +1 -1
  182. package/dist/cli.d.ts +3 -2
  183. package/dist/cli.d.ts.map +1 -1
  184. package/dist/cli.js +125 -6
  185. package/dist/cli.js.map +1 -1
  186. package/dist/commands/__tests__/newCommands.test.js +3 -4
  187. package/dist/commands/__tests__/newCommands.test.js.map +1 -1
  188. package/dist/commands/cmdUtils.d.ts +5 -2
  189. package/dist/commands/cmdUtils.d.ts.map +1 -1
  190. package/dist/commands/cmdUtils.js +29 -10
  191. package/dist/commands/cmdUtils.js.map +1 -1
  192. package/dist/commands/daylightSimulation/autoCct.d.ts.map +1 -1
  193. package/dist/commands/daylightSimulation/autoCct.js +67 -197
  194. package/dist/commands/daylightSimulation/autoCct.js.map +1 -1
  195. package/dist/commands/daylightSimulation/graphSchedule.d.ts +1 -1
  196. package/dist/commands/daylightSimulation/graphSchedule.d.ts.map +1 -1
  197. package/dist/commands/daylightSimulation/printSchedule.d.ts +1 -1
  198. package/dist/commands/daylightSimulation/printSchedule.d.ts.map +1 -1
  199. package/dist/commands/daylightSimulation/printSchedule.js +2 -2
  200. package/dist/commands/daylightSimulation/printSchedule.js.map +1 -1
  201. package/dist/commands/daylightSimulation/service.d.ts +1 -1
  202. package/dist/commands/daylightSimulation/service.d.ts.map +1 -1
  203. package/dist/commands/daylightSimulation/service.js +5 -3
  204. package/dist/commands/daylightSimulation/service.js.map +1 -1
  205. package/dist/commands/daylightSimulation/simulateSchedule.d.ts +1 -1
  206. package/dist/commands/daylightSimulation/simulateSchedule.d.ts.map +1 -1
  207. package/dist/commands/daylightSimulation/simulateSchedule.js +5 -4
  208. package/dist/commands/daylightSimulation/simulateSchedule.js.map +1 -1
  209. package/dist/commands/deviceControl/ble.d.ts +4 -0
  210. package/dist/commands/deviceControl/ble.d.ts.map +1 -0
  211. package/dist/commands/deviceControl/ble.js +214 -0
  212. package/dist/commands/deviceControl/ble.js.map +1 -0
  213. package/dist/commands/deviceControl/cct.d.ts.map +1 -1
  214. package/dist/commands/deviceControl/cct.js +41 -11
  215. package/dist/commands/deviceControl/cct.js.map +1 -1
  216. package/dist/commands/deviceControl/color.d.ts.map +1 -1
  217. package/dist/commands/deviceControl/color.js +13 -3
  218. package/dist/commands/deviceControl/color.js.map +1 -1
  219. package/dist/commands/deviceControl/config.d.ts.map +1 -1
  220. package/dist/commands/deviceControl/config.js +26 -3
  221. package/dist/commands/deviceControl/config.js.map +1 -1
  222. package/dist/commands/deviceControl/{discover.d.ts → desktop.d.ts} +3 -2
  223. package/dist/commands/deviceControl/desktop.d.ts.map +1 -0
  224. package/dist/commands/deviceControl/desktop.js +66 -0
  225. package/dist/commands/deviceControl/desktop.js.map +1 -0
  226. package/dist/commands/deviceControl/effect.d.ts.map +1 -1
  227. package/dist/commands/deviceControl/effect.js +90 -159
  228. package/dist/commands/deviceControl/effect.js.map +1 -1
  229. package/dist/commands/deviceControl/fan.d.ts.map +1 -1
  230. package/dist/commands/deviceControl/fan.js +77 -95
  231. package/dist/commands/deviceControl/fan.js.map +1 -1
  232. package/dist/commands/deviceControl/group.d.ts.map +1 -1
  233. package/dist/commands/deviceControl/group.js +43 -6
  234. package/dist/commands/deviceControl/group.js.map +1 -1
  235. package/dist/commands/deviceControl/hsi.d.ts.map +1 -1
  236. package/dist/commands/deviceControl/hsi.js +3 -1
  237. package/dist/commands/deviceControl/hsi.js.map +1 -1
  238. package/dist/commands/deviceControl/info.d.ts.map +1 -1
  239. package/dist/commands/deviceControl/info.js +0 -50
  240. package/dist/commands/deviceControl/info.js.map +1 -1
  241. package/dist/commands/deviceControl/intensity.d.ts.map +1 -1
  242. package/dist/commands/deviceControl/intensity.js +32 -8
  243. package/dist/commands/deviceControl/intensity.js.map +1 -1
  244. package/dist/commands/deviceControl/list.d.ts.map +1 -1
  245. package/dist/commands/deviceControl/list.js +3 -2
  246. package/dist/commands/deviceControl/list.js.map +1 -1
  247. package/dist/commands/deviceControl/power.d.ts.map +1 -1
  248. package/dist/commands/deviceControl/power.js +9 -3
  249. package/dist/commands/deviceControl/power.js.map +1 -1
  250. package/dist/commands/deviceControl/preset.d.ts.map +1 -1
  251. package/dist/commands/deviceControl/preset.js +56 -19
  252. package/dist/commands/deviceControl/preset.js.map +1 -1
  253. package/dist/commands/deviceControl/program.d.ts +4 -0
  254. package/dist/commands/deviceControl/program.d.ts.map +1 -0
  255. package/dist/commands/deviceControl/program.js +199 -0
  256. package/dist/commands/deviceControl/program.js.map +1 -0
  257. package/dist/commands/deviceControl/quickshot.d.ts.map +1 -1
  258. package/dist/commands/deviceControl/quickshot.js +75 -13
  259. package/dist/commands/deviceControl/quickshot.js.map +1 -1
  260. package/dist/commands/deviceControl/scene.d.ts.map +1 -1
  261. package/dist/commands/deviceControl/scene.js +72 -9
  262. package/dist/commands/deviceControl/scene.js.map +1 -1
  263. package/dist/commands/deviceControl/status.d.ts.map +1 -1
  264. package/dist/commands/deviceControl/status.js +26 -3
  265. package/dist/commands/deviceControl/status.js.map +1 -1
  266. package/dist/commands/parseUtils.d.ts +1 -1
  267. package/dist/commands/parseUtils.d.ts.map +1 -1
  268. package/dist/commands/parseUtils.js +1 -11
  269. package/dist/commands/parseUtils.js.map +1 -1
  270. package/dist/commands-api.d.ts +1 -1
  271. package/dist/commands-api.d.ts.map +1 -1
  272. package/dist/commands-api.js +1 -1
  273. package/dist/commands-api.js.map +1 -1
  274. package/dist/commands.d.ts.map +1 -1
  275. package/dist/commands.js +4 -2
  276. package/dist/commands.js.map +1 -1
  277. package/dist/config.d.ts +82 -0
  278. package/dist/config.d.ts.map +1 -1
  279. package/dist/config.js +13 -2
  280. package/dist/config.js.map +1 -1
  281. package/dist/daylightSimulation/cctUtil.d.ts.map +1 -1
  282. package/dist/daylightSimulation/cctUtil.js +2 -2
  283. package/dist/daylightSimulation/cctUtil.js.map +1 -1
  284. package/dist/daylightSimulation/constants.d.ts +10 -0
  285. package/dist/daylightSimulation/constants.d.ts.map +1 -1
  286. package/dist/daylightSimulation/constants.js +4 -0
  287. package/dist/daylightSimulation/constants.js.map +1 -1
  288. package/dist/daylightSimulation/currentCct.d.ts +42 -0
  289. package/dist/daylightSimulation/currentCct.d.ts.map +1 -0
  290. package/dist/daylightSimulation/currentCct.js +217 -0
  291. package/dist/daylightSimulation/currentCct.js.map +1 -0
  292. package/dist/daylightSimulation/dashboardSettings.d.ts +47 -0
  293. package/dist/daylightSimulation/dashboardSettings.d.ts.map +1 -0
  294. package/dist/daylightSimulation/dashboardSettings.js +95 -0
  295. package/dist/daylightSimulation/dashboardSettings.js.map +1 -0
  296. package/dist/daylightSimulation/dashboardStatus.d.ts +72 -0
  297. package/dist/daylightSimulation/dashboardStatus.d.ts.map +1 -0
  298. package/dist/daylightSimulation/dashboardStatus.js +191 -0
  299. package/dist/daylightSimulation/dashboardStatus.js.map +1 -0
  300. package/dist/daylightSimulation/parseUtils.d.ts +3 -0
  301. package/dist/daylightSimulation/parseUtils.d.ts.map +1 -0
  302. package/dist/daylightSimulation/parseUtils.js +25 -0
  303. package/dist/daylightSimulation/parseUtils.js.map +1 -0
  304. package/dist/daylightSimulation/scheduleMaker.d.ts +2 -2
  305. package/dist/daylightSimulation/scheduleMaker.d.ts.map +1 -1
  306. package/dist/daylightSimulation/scheduleMaker.js +6 -6
  307. package/dist/daylightSimulation/scheduleMaker.js.map +1 -1
  308. package/dist/daylightSimulation/types.d.ts +13 -2
  309. package/dist/daylightSimulation/types.d.ts.map +1 -1
  310. package/dist/daylightSimulation/types.js +0 -2
  311. package/dist/daylightSimulation/types.js.map +1 -1
  312. package/dist/device-control.d.ts +1 -0
  313. package/dist/device-control.d.ts.map +1 -1
  314. package/dist/device-control.js +1 -0
  315. package/dist/device-control.js.map +1 -1
  316. package/dist/deviceControl/bleHttpControl.d.ts +129 -0
  317. package/dist/deviceControl/bleHttpControl.d.ts.map +1 -0
  318. package/dist/deviceControl/bleHttpControl.js +753 -0
  319. package/dist/deviceControl/bleHttpControl.js.map +1 -0
  320. package/dist/deviceControl/lightControl.d.ts +8 -68
  321. package/dist/deviceControl/lightControl.d.ts.map +1 -1
  322. package/dist/deviceControl/lightControl.js +6 -246
  323. package/dist/deviceControl/lightControl.js.map +1 -1
  324. package/dist/deviceControl/lightControlCommands.d.ts +68 -0
  325. package/dist/deviceControl/lightControlCommands.d.ts.map +1 -0
  326. package/dist/deviceControl/lightControlCommands.js +244 -0
  327. package/dist/deviceControl/lightControlCommands.js.map +1 -0
  328. package/dist/deviceControl/types.d.ts +75 -4
  329. package/dist/deviceControl/types.d.ts.map +1 -1
  330. package/dist/deviceControl/types.js +1 -0
  331. package/dist/deviceControl/types.js.map +1 -1
  332. package/dist/index.d.ts +2 -1
  333. package/dist/index.d.ts.map +1 -1
  334. package/dist/index.js +1 -0
  335. package/dist/index.js.map +1 -1
  336. package/dist/test/MockLightServer.d.ts +1 -0
  337. package/dist/test/MockLightServer.d.ts.map +1 -1
  338. package/dist/test/MockLightServer.js +5 -0
  339. package/dist/test/MockLightServer.js.map +1 -1
  340. package/docs/CHANGELOG.md +37 -0
  341. package/docs/DIRECT_BLE.md +359 -0
  342. package/package.json +8 -4
  343. package/scripts/ble-cli-check.ts +129 -0
  344. package/scripts/ble-config-probe.ts +109 -0
  345. package/scripts/ble-effects-check.ts +82 -0
  346. package/scripts/ble-extended-probe.ts +44 -0
  347. package/scripts/ble-fan-check.ts +129 -0
  348. package/scripts/ble-feature-probe.ts +37 -0
  349. package/scripts/ble-followup-check.ts +165 -0
  350. package/scripts/ble-hardware-check.ts +99 -0
  351. package/scripts/ble-live-extended-check.ts +205 -0
  352. package/scripts/ble-non-ota-check.ts +193 -0
  353. package/scripts/ble-probe.ts +45 -0
  354. package/scripts/calibrate-webcam.ts +152 -0
  355. package/scripts/desktop-reference.ts +85 -0
  356. package/scripts/webcam.ts +129 -0
  357. package/dist/__tests__/lightControl.test.d.ts +0 -2
  358. package/dist/__tests__/lightControl.test.d.ts.map +0 -1
  359. package/dist/__tests__/lightControl.test.js +0 -424
  360. package/dist/__tests__/lightControl.test.js.map +0 -1
  361. package/dist/commands/deviceControl/discover.d.ts.map +0 -1
  362. package/dist/commands/deviceControl/discover.js +0 -29
  363. package/dist/commands/deviceControl/discover.js.map +0 -1
@@ -0,0 +1,205 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { z } from 'zod';
5
+ import { SavedStateSchema } from '../src/ble/library.js';
6
+ import { FAN_MODES, type FixtureState, parseFanMode } from '../src/ble/telink.js';
7
+ import { captureWebcam, imagePixels, RegionSchema, regionsFile } from './webcam.js';
8
+
9
+ const base = process.env.AMARAN_BLE_URL ?? 'http://127.0.0.1:2708';
10
+ const run = Date.now();
11
+ const results: unknown[] = [];
12
+ const errors: string[] = [];
13
+ const initial: Record<string, FixtureState> = {};
14
+ const fans: Record<string, number> = {};
15
+ const calibration = z.object({ regions: z.array(RegionSchema) }).parse(JSON.parse(readFileSync(regionsFile, 'utf8')));
16
+ const keys = ['desk', 'front', 'back'];
17
+ let changed = false;
18
+
19
+ async function request(route: string, body?: object, method = body ? 'POST' : 'GET'): Promise<unknown> {
20
+ const response = await fetch(`${base}${route}`, {
21
+ method,
22
+ ...(body ? { body: JSON.stringify(body), headers: { 'content-type': 'application/json' } } : {}),
23
+ signal: AbortSignal.timeout(60_000),
24
+ });
25
+ const data = z
26
+ .object({ ok: z.boolean(), result: z.unknown().optional(), error: z.string().optional() })
27
+ .parse(await response.json());
28
+ if (!response.ok || !data.ok) throw new Error(`${route}: ${data.error ?? response.status}`);
29
+ results.push({ route, body, result: data.result });
30
+ return data.result;
31
+ }
32
+ async function light(key: string, action: string, body?: object) {
33
+ return SavedStateSchema.parse(await request(`/lights/${key}/${action}`, body));
34
+ }
35
+ function image(label: string) {
36
+ const filename = captureWebcam(`extended-${run}-${label}`);
37
+ return z
38
+ .object({
39
+ regions: z.array(
40
+ z.object({ name: z.string(), rgb: z.array(z.number()), haloRgb: z.array(z.number()).optional() })
41
+ ),
42
+ })
43
+ .parse(JSON.parse(readFileSync(filename.replace(/\.png$/, '.json'), 'utf8'))).regions;
44
+ }
45
+ function mean(rgb: number[]) {
46
+ return rgb.reduce((sum, value) => sum + value, 0) / rgb.length;
47
+ }
48
+ function pulsingVideo(key: string) {
49
+ const region = calibration.regions.find((entry) => entry.name === key);
50
+ if (!region) throw new Error(`No calibrated region for ${key}`);
51
+ const prefix = `extended-${run}-pulsing`;
52
+ const capture = spawnSync(
53
+ 'ffmpeg',
54
+ [
55
+ '-hide_banner',
56
+ '-loglevel',
57
+ 'error',
58
+ '-f',
59
+ 'avfoundation',
60
+ '-pixel_format',
61
+ 'nv12',
62
+ '-framerate',
63
+ '30',
64
+ '-video_size',
65
+ '1280x720',
66
+ '-i',
67
+ `${process.env.AMARAN_CAMERA_INDEX ?? '0'}:none`,
68
+ '-t',
69
+ '12',
70
+ '-vf',
71
+ 'fps=2',
72
+ '-y',
73
+ `artifacts/webcam/${prefix}-%03d.png`,
74
+ ],
75
+ { encoding: 'utf8', timeout: 30_000 }
76
+ );
77
+ if (capture.status !== 0) throw new Error(capture.stderr || 'Pulsing video capture failed');
78
+ const levels = readdirSync('artifacts/webcam')
79
+ .filter((name) => name.startsWith(`${prefix}-`) && name.endsWith('.png'))
80
+ .sort()
81
+ .map((name) => {
82
+ const pixels = imagePixels(`artifacts/webcam/${name}`);
83
+ let sum = 0;
84
+ for (let y = region.y; y < region.y + region.height; y++)
85
+ for (let x = region.x; x < region.x + region.width; x++) {
86
+ const offset = (y * 1280 + x) * 3;
87
+ sum += mean([...pixels.subarray(offset, offset + 3)]);
88
+ }
89
+ return sum / (region.width * region.height);
90
+ });
91
+ assert.ok(levels.length >= 20, 'Need enough camera samples to observe pulsing');
92
+ const settled = levels.slice(6);
93
+ assert.ok(Math.max(...settled) - Math.min(...settled) > 40, `Camera did not confirm pulse modulation: ${levels}`);
94
+ const changes = settled.slice(1).map((value, index) => value - settled[index]);
95
+ assert.ok(
96
+ changes.some((value) => value > 20) && changes.some((value) => value < -20),
97
+ 'Need both rising and falling output after camera exposure settles'
98
+ );
99
+ results.push({ opticalPulsing: { key, levels, span: Math.max(...settled) - Math.min(...settled) } });
100
+ }
101
+
102
+ try {
103
+ for (const key of keys) {
104
+ initial[key] = await light(key, 'state');
105
+ const fan = z.object({ mode: z.number() }).parse(await request(`/lights/${key}/fan`));
106
+ parseFanMode(fan.mode);
107
+ assert.notEqual(fan.mode, FAN_MODES.manual, 'Cannot infer an original manual RPM setpoint for restoration');
108
+ fans[key] = fan.mode;
109
+ }
110
+ writeFileSync(`artifacts/extended-${run}-initial.json`, JSON.stringify({ states: initial, fans }, null, 2), {
111
+ mode: 0o600,
112
+ });
113
+ assert.ok(
114
+ Object.values(initial).every((state) => state.mode === 'cct'),
115
+ 'Begin hardware checks with all three fixtures in steady CCT mode'
116
+ );
117
+ changed = true;
118
+ await request('/batch', { targets: 'all', action: 'off', args: {} });
119
+ const dark = image('all-off');
120
+ await request('/batch', { targets: 'all', action: 'cct', args: { kelvin: 3200, brightness: 1 } });
121
+ const bright = image('all-1pct');
122
+ for (const key of keys) {
123
+ const before = dark.find((region) => region.name === key);
124
+ const after = bright.find((region) => region.name === key);
125
+ assert.ok(before && after);
126
+ assert.ok(mean(after.rgb) - mean(before.rgb) > 40, `Camera did not confirm ${key} illumination`);
127
+ const fan = z
128
+ .object({ mode: z.number(), speed: z.number(), highTemperature: z.boolean() })
129
+ .parse(await request(`/lights/${key}/fan`, { mode: 'medium' }));
130
+ assert.equal(fan.mode, 5);
131
+ assert.equal(fan.highTemperature, false);
132
+ await request(`/lights/${key}/fan`, { mode: fans[key] });
133
+ }
134
+ for (const gm of [-30, 30, 0]) {
135
+ assert.equal((await light('back', 'gm', { value: gm })).gm, gm);
136
+ image(`gm-${gm}`);
137
+ }
138
+ await request('/batch', { targets: 'all', action: 'brightness', args: { value: 2 }, broadcast: true });
139
+ for (const key of keys) assert.equal((await light(key, 'state')).intensity, 20);
140
+ image('broadcast-2pct');
141
+ await request('/fade', { targets: 'all', brightness: 1, seconds: 2 });
142
+ for (const key of keys) assert.equal((await light(key, 'state')).intensity, 10);
143
+ image('faded-1pct');
144
+ const scene = z.object({ id: z.string() }).parse(await request('/library/scenes', { name: `Hardware check ${run}` }));
145
+ try {
146
+ await light('back', 'color', { color: '#00ff00', brightness: 1 });
147
+ const colored = image('hsi-green').find((region) => region.name === 'back');
148
+ assert.ok(
149
+ colored?.haloRgb && colored.haloRgb[1] - Math.max(colored.haloRgb[0], colored.haloRgb[2]) > 20,
150
+ 'Camera did not confirm green HSI'
151
+ );
152
+ await request(`/library/scenes/${scene.id}/recall`, {});
153
+ for (const key of keys) assert.equal((await light(key, 'state')).cct, 3200);
154
+ } finally {
155
+ await request(`/library/scenes/${scene.id}`, undefined, 'DELETE');
156
+ }
157
+ await light('front', 'off', {});
158
+ await light('back', 'off', {});
159
+ const pulse = await light('desk', 'effect', { name: 'pulsing', brightness: 1, frequency: 1, kelvin: 3200 });
160
+ assert.equal(pulse.effect, 'pulsing');
161
+ pulsingVideo('desk');
162
+ await light('desk', 'effect-stop', {});
163
+ await light('desk', 'off', {});
164
+ for (const settings of [
165
+ { name: 'pulsing', brightness: 1, frequency: 1, hue: 120, saturation: 100 },
166
+ { name: 'party-lights', brightness: 1, frequency: 1, saturation: 80 },
167
+ ]) {
168
+ const state = await light('back', 'effect', settings);
169
+ assert.equal(state.effect, settings.name);
170
+ image(settings.name);
171
+ await light('back', 'effect-intensity', { value: 2 });
172
+ await light('back', 'effect-stop', {});
173
+ }
174
+ } catch (error) {
175
+ errors.push(String(error));
176
+ } finally {
177
+ for (const [key, state] of changed ? Object.entries(initial) : []) {
178
+ try {
179
+ assert.equal(state.mode, 'cct', 'This check restores an initial steady CCT setup');
180
+ await light(key, 'cct', {
181
+ kelvin: state.cct,
182
+ brightness: state.intensity / 10,
183
+ ...(key === 'back' ? { gm: state.gm } : {}),
184
+ });
185
+ await light(key, state.sleep ? 'off' : 'on', {});
186
+ const restored = await light(key, 'state');
187
+ for (const field of ['cct', 'intensity', 'sleep'] as const) assert.equal(restored[field], state[field]);
188
+ if (key === 'back') assert.equal(restored.gm, state.gm);
189
+ } catch (error) {
190
+ errors.push(`Restore ${key}: ${error}`);
191
+ }
192
+ if (fans[key] !== undefined) {
193
+ try {
194
+ await request(`/lights/${key}/fan`, { mode: fans[key] });
195
+ } catch (error) {
196
+ errors.push(`Restore ${key} fan: ${error}`);
197
+ }
198
+ }
199
+ }
200
+ writeFileSync(`artifacts/extended-${run}-results.json`, JSON.stringify({ results, errors }, null, 2), {
201
+ mode: 0o600,
202
+ });
203
+ }
204
+ if (errors.length) throw new Error(errors.join('\n'));
205
+ console.log(`Extended live checks passed; results saved to artifacts/extended-${run}-results.json`);
@@ -0,0 +1,193 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { realpathSync, writeFileSync } from 'node:fs';
4
+ import { z } from 'zod';
5
+ import { SavedStateSchema } from '../src/ble/library.js';
6
+
7
+ const base = process.env.AMARAN_BLE_URL ?? 'http://127.0.0.1:2708';
8
+ const database = process.argv[2];
9
+ if (!database) throw new Error('Supply the matching Desktop database path');
10
+ const run = Date.now();
11
+ const records: unknown[] = [],
12
+ failures: string[] = [];
13
+ const cleanup: string[] = [],
14
+ jobs: string[] = [];
15
+ let baseline: { id: string; states: Record<string, z.infer<typeof SavedStateSchema>> } | undefined;
16
+ let overrides: Record<string, number> = {};
17
+ async function request(route: string, body?: object, method = body ? 'POST' : 'GET') {
18
+ const response = await fetch(`${base}${route}`, {
19
+ method,
20
+ headers: { connection: 'close', ...(body ? { 'content-type': 'application/json' } : {}) },
21
+ ...(body ? { body: JSON.stringify(body) } : {}),
22
+ signal: AbortSignal.timeout(60_000),
23
+ });
24
+ const data = z
25
+ .object({ ok: z.boolean(), result: z.unknown().optional(), error: z.string().optional() })
26
+ .parse(await response.json());
27
+ if (!response.ok || !data.ok) throw new Error(`${route}: ${data.error ?? response.status}`);
28
+ records.push({ route, body, result: data.result });
29
+ return data.result;
30
+ }
31
+ const Status = z.object({ id: z.string(), state: z.string(), frames: z.number(), error: z.string().optional() });
32
+ async function start(body: object) {
33
+ const status = Status.parse(await request('/programs', body));
34
+ jobs.push(status.id);
35
+ return status;
36
+ }
37
+ async function wait(id: string, condition: (state: z.infer<typeof Status>) => boolean) {
38
+ const end = Date.now() + 30_000;
39
+ while (Date.now() < end) {
40
+ const status = Status.parse(await request(`/programs/${id}`));
41
+ if (status.state === 'failed') throw new Error(status.error);
42
+ if (condition(status)) return status;
43
+ await new Promise((resolve) => setTimeout(resolve, 150));
44
+ }
45
+ throw new Error('Timed out waiting for program');
46
+ }
47
+ async function state(key: string) {
48
+ return SavedStateSchema.parse(await request(`/lights/${key}/state`));
49
+ }
50
+ try {
51
+ overrides = Object.fromEntries(
52
+ Object.entries(z.record(z.number()).parse(await request('/overrides', { targets: 'all' }))).map(([key, ms]) => [
53
+ key,
54
+ ms ? Date.now() + ms : 0,
55
+ ])
56
+ );
57
+ for (const key of ['desk', 'front', 'back']) {
58
+ const light = await state(key);
59
+ assert.ok(!light.sleep && light.mode === 'cct' && light.intensity <= 50, 'Begin awake in CCT at 5% or less');
60
+ }
61
+ baseline = z
62
+ .object({ id: z.string(), states: z.record(SavedStateSchema) })
63
+ .parse(await request('/library/scenes', { name: `Non-OTA baseline ${run}` }));
64
+ const group = z.object({ id: z.string() }).parse(await request('/groups', { name: `Native verification ${run}` }));
65
+ cleanup.push(`/groups/${encodeURIComponent(group.id)}`);
66
+ for (const member of ['desk', 'back']) await request(`/groups/${encodeURIComponent(group.id)}/members`, { member });
67
+ await request(`/groups/${encodeURIComponent(group.id)}/native`, { action: 'enable' });
68
+ const changed = z
69
+ .object({ delivery: z.literal('native-group') })
70
+ .parse(await request(`/lights/${encodeURIComponent(group.id)}/brightness`, { value: 1 }));
71
+ assert.equal(changed.delivery, 'native-group');
72
+ assert.equal((await state('desk')).intensity, 10);
73
+ assert.equal((await state('back')).intensity, 10);
74
+ assert.equal((await state('front')).intensity, baseline.states.front.intensity);
75
+ await request(`/groups/${encodeURIComponent(group.id)}/members`, { member: 'front' });
76
+ await request(`/groups/${encodeURIComponent(group.id)}/members`, { member: 'front', remove: true });
77
+ await request(`/groups/${encodeURIComponent(group.id)}/native`, { action: 'disable' });
78
+ await request(`/library/scenes/${baseline.id}/recall`, {});
79
+ const preview = z
80
+ .object({
81
+ valid: z.boolean(),
82
+ plan: z.object({
83
+ groups: z.array(z.object({ id: z.string() })),
84
+ entries: z.array(z.object({ collection: z.string(), entry: z.object({ id: z.string() }) })),
85
+ }),
86
+ })
87
+ .parse(await request('/desktop/import', { database: realpathSync(database), prefix: `Import check ${run}` }));
88
+ assert.equal(preview.valid, true);
89
+ for (const item of preview.plan.groups) cleanup.push(`/groups/${encodeURIComponent(item.id)}`);
90
+ for (const item of preview.plan.entries)
91
+ cleanup.push(`/library/${item.collection}/${encodeURIComponent(item.entry.id)}`);
92
+ await request('/desktop/import', { database: realpathSync(database), prefix: `Import check ${run}`, apply: true });
93
+ const again = z
94
+ .object({ changes: z.object({ created: z.number(), unchanged: z.number() }) })
95
+ .parse(
96
+ await request('/desktop/import', { database: realpathSync(database), prefix: `Import check ${run}`, apply: true })
97
+ );
98
+ assert.equal(again.changes.created, 0);
99
+ assert.ok(again.changes.unchanged > 0);
100
+ const timeline = await start({
101
+ kind: 'timeline',
102
+ targets: 'all',
103
+ duration: 2,
104
+ steps: [
105
+ { at: 0, action: 'brightness', args: { value: 1 } },
106
+ { at: 1, action: 'brightness', args: { value: 2 } },
107
+ ],
108
+ });
109
+ await wait(timeline.id, (status) => status.state === 'completed');
110
+ const takeover = await start({
111
+ kind: 'timeline',
112
+ targets: ['back'],
113
+ duration: 5,
114
+ steps: [
115
+ { at: 0, action: 'brightness', args: { value: 1 } },
116
+ { at: 4, action: 'brightness', args: { value: 2 } },
117
+ ],
118
+ });
119
+ await wait(takeover.id, (status) => status.frames > 0);
120
+ await request('/lights/back/brightness', { value: 3 });
121
+ assert.equal(Status.parse(await request(`/programs/${takeover.id}`)).state, 'cancelled');
122
+ assert.equal((await state('back')).intensity, 30);
123
+ await request(`/library/scenes/${baseline.id}/recall`, {});
124
+ const picker = await start({
125
+ kind: 'picker',
126
+ targets: ['back'],
127
+ duration: 4,
128
+ maxBrightness: 1,
129
+ source: { kind: 'image-file', file: realpathSync('artifacts/program-test-green.png') },
130
+ });
131
+ await wait(picker.id, (status) => status.frames > 0);
132
+ assert.equal((await state('back')).mode, 'hsi');
133
+ assert.equal((await state('back')).hue, 120);
134
+ await wait(picker.id, (status) => status.state === 'completed');
135
+ const audio = await start({
136
+ kind: 'audio',
137
+ targets: 'all',
138
+ duration: 3,
139
+ maxBrightness: 2,
140
+ source: { kind: 'audio-file', file: realpathSync('artifacts/program-test-tone.wav') },
141
+ });
142
+ const audioDone = await wait(audio.id, (status) => status.state === 'completed');
143
+ assert.ok(audioDone.frames > 0);
144
+ const file = `artifacts/timeline-check-${run}.json`;
145
+ writeFileSync(file, JSON.stringify({ duration: 1, steps: [{ at: 0, action: 'brightness', args: { value: 1 } }] }), {
146
+ mode: 0o600,
147
+ });
148
+ execFileSync(process.execPath, ['dist/cli.js', 'ble', 'program', file, '--targets', 'back'], {
149
+ stdio: 'pipe',
150
+ timeout: 20_000,
151
+ });
152
+ } catch (error) {
153
+ failures.push(String(error));
154
+ } finally {
155
+ for (const id of jobs) {
156
+ try {
157
+ await request(`/programs/${id}`, undefined, 'DELETE');
158
+ } catch (error) {
159
+ failures.push(`Stop program ${id}: ${error}`);
160
+ }
161
+ }
162
+ if (baseline) {
163
+ try {
164
+ await request(`/library/scenes/${baseline.id}/recall`, {});
165
+ for (const [key, expected] of Object.entries(baseline.states)) {
166
+ const actual = await state(key);
167
+ for (const field of ['mode', 'intensity', 'sleep', 'cct'] as const)
168
+ assert.equal(actual[field], expected[field]);
169
+ }
170
+ } catch (error) {
171
+ failures.push(`Restore: ${error}`);
172
+ }
173
+ cleanup.push(`/library/scenes/${baseline.id}`);
174
+ }
175
+ for (const route of cleanup.reverse()) {
176
+ try {
177
+ await request(route, undefined, 'DELETE');
178
+ } catch (error) {
179
+ failures.push(`Cleanup ${route}: ${error}`);
180
+ }
181
+ }
182
+ if (!failures.length)
183
+ for (const [key, until] of Object.entries(overrides)) {
184
+ try {
185
+ await request('/overrides', { targets: [key], minutes: Math.max(0, (until - Date.now()) / 60_000) });
186
+ } catch (error) {
187
+ failures.push(`Restore override ${key}: ${error}`);
188
+ }
189
+ }
190
+ writeFileSync(`artifacts/non-ota-check-${run}.json`, JSON.stringify({ records, failures }, null, 2), { mode: 0o600 });
191
+ }
192
+ if (failures.length) throw new Error(failures.join('\n'));
193
+ console.log(`Non-OTA checks passed; original state restored: artifacts/non-ota-check-${run}.json`);
@@ -0,0 +1,45 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { MeshCrypto } from '../src/ble/crypto.js';
3
+ import { importMesh } from '../src/ble/setup.js';
4
+ import { loadMeshConfig, meshDirectory, SequenceStore } from '../src/ble/storage.js';
5
+ import { cctPacket, powerPacket } from '../src/ble/telink.js';
6
+ import { MeshTransport } from '../src/ble/transport.js';
7
+ import { captureWebcam } from './webcam.js';
8
+
9
+ if (process.argv[2] === 'import') {
10
+ importMesh(process.argv[3]);
11
+ } else {
12
+ const config = loadMeshConfig();
13
+ const crypto = new MeshCrypto(config.netKey, config.appKey);
14
+ const sequence = new SequenceStore(meshDirectory(), crypto.networkId.toString('hex'), config.source);
15
+ const transport = new MeshTransport(config, sequence, true);
16
+ try {
17
+ await transport.connect();
18
+ if (process.argv[2] === 'test-desk') {
19
+ const light = config.lights.find((entry) => entry.key === 'desk');
20
+ if (!light) throw new Error('Desk fixture not configured');
21
+ try {
22
+ await transport.send(light.address, cctPacket(3200, 10));
23
+ await delay(300);
24
+ await transport.send(light.address, powerPacket(true));
25
+ await delay(300);
26
+ console.log('ON state', await transport.readState(light.address));
27
+ console.log(captureWebcam(`ble-desk-on-${Date.now()}`));
28
+ } finally {
29
+ await transport.send(light.address, powerPacket(false));
30
+ await delay(300);
31
+ console.log('OFF state', await transport.readState(light.address));
32
+ }
33
+ console.log(captureWebcam(`ble-desk-off-${Date.now()}`));
34
+ } else {
35
+ for (const light of config.lights) {
36
+ console.log(light.name, await transport.readState(light.address));
37
+ console.log('Fan', await transport.readFan(light.address));
38
+ }
39
+ }
40
+ } finally {
41
+ await transport.disconnect();
42
+ sequence.close();
43
+ }
44
+ process.exit(0);
45
+ }
@@ -0,0 +1,152 @@
1
+ import { readdirSync, writeFileSync } from 'node:fs';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { MeshCrypto } from '../src/ble/crypto.js';
4
+ import { loadMeshConfig, meshDirectory, SequenceStore } from '../src/ble/storage.js';
5
+ import { cctPacket, type FixtureState, powerPacket } from '../src/ble/telink.js';
6
+ import { MeshTransport } from '../src/ble/transport.js';
7
+ import { type CameraRegion, captureWebcam, imagePixels, regionsFile } from './webcam.js';
8
+
9
+ function locateLight(dark: Buffer, lit: Buffer, name: string): CameraRegion {
10
+ const width = 320;
11
+ const height = 180;
12
+ const mask = new Uint8Array(width * height);
13
+ for (let y = 1; y < height - 1; y++)
14
+ for (let x = 1; x < width - 1; x++) {
15
+ let bright = true;
16
+ for (let dy = -1; dy <= 1; dy++)
17
+ for (let dx = -1; dx <= 1; dx++) {
18
+ const offset = ((y + dy) * 4 * 1280 + (x + dx) * 4) * 3;
19
+ const light = (lit[offset] + lit[offset + 1] + lit[offset + 2]) / 3;
20
+ const before = (dark[offset] + dark[offset + 1] + dark[offset + 2]) / 3;
21
+ if (light < 248 || light - before < 65) bright = false;
22
+ }
23
+ if (bright) mask[y * width + x] = 1;
24
+ }
25
+ const candidates: { count: number; x: number; y: number; width: number; height: number }[] = [];
26
+ for (let start = 0; start < mask.length; start++) {
27
+ if (!mask[start]) continue;
28
+ const queue = [start];
29
+ mask[start] = 0;
30
+ let minX = width,
31
+ maxX = 0,
32
+ minY = height,
33
+ maxY = 0;
34
+ for (let i = 0; i < queue.length; i++) {
35
+ const index = queue[i],
36
+ x = index % width,
37
+ y = Math.floor(index / width);
38
+ minX = Math.min(minX, x);
39
+ maxX = Math.max(maxX, x);
40
+ minY = Math.min(minY, y);
41
+ maxY = Math.max(maxY, y);
42
+ for (const next of [
43
+ index - width,
44
+ index + width,
45
+ ...(x > 0 ? [index - 1] : []),
46
+ ...(x < width - 1 ? [index + 1] : []),
47
+ ]) {
48
+ if (next >= 0 && next < mask.length && mask[next]) {
49
+ mask[next] = 0;
50
+ queue.push(next);
51
+ }
52
+ }
53
+ }
54
+ const w = maxX - minX + 1,
55
+ h = maxY - minY + 1;
56
+ if (queue.length >= 8 && w / h > 0.35 && w / h < 2.8 && w < 70 && h < 70) {
57
+ candidates.push({ count: queue.length, x: (minX + maxX) * 2, y: (minY + maxY) * 2, width: w * 4, height: h * 4 });
58
+ }
59
+ }
60
+ candidates.sort((a, b) => b.count - a.count);
61
+ const best = candidates[0];
62
+ if (!best || (candidates[1] && candidates[1].count > best.count * 0.7))
63
+ throw new Error(`Cannot identify a unique emitting fixture for ${name}: ${JSON.stringify(candidates)}`);
64
+ const size = Math.max(8, Math.min(24, Math.floor(Math.min(best.width, best.height) / 2)));
65
+ return { name, x: Math.round(best.x - size / 2), y: Math.round(best.y - size / 2), width: size, height: size };
66
+ }
67
+
68
+ const config = loadMeshConfig();
69
+ if (process.argv.includes('--reuse')) {
70
+ const capture = readdirSync('artifacts/webcam')
71
+ .filter((name) => /^calibration-\d+-off\.png$/.test(name))
72
+ .sort()
73
+ .at(-1);
74
+ if (!capture) throw new Error('No saved calibration captures');
75
+ const prefix = `artifacts/webcam/${capture.replace(/-off\.png$/, '')}`;
76
+ const dark = imagePixels(`${prefix}-off.png`);
77
+ const regions = config.lights.map((light) => locateLight(dark, imagePixels(`${prefix}-${light.key}.png`), light.key));
78
+ writeFileSync(
79
+ regionsFile,
80
+ JSON.stringify(
81
+ { cameraIndex: process.env.AMARAN_CAMERA_INDEX ?? '0', capturedAt: new Date().toISOString(), regions },
82
+ null,
83
+ 2
84
+ ),
85
+ { mode: 0o600 }
86
+ );
87
+ console.log(prefix, regions);
88
+ process.exit(0);
89
+ }
90
+ const crypto = new MeshCrypto(config.netKey, config.appKey);
91
+ const sequence = new SequenceStore(meshDirectory(), crypto.networkId.toString('hex'), config.source);
92
+ const transport = new MeshTransport(config, sequence);
93
+ const original = new Map<number, FixtureState>();
94
+ const regions: CameraRegion[] = [];
95
+ const errors: unknown[] = [];
96
+ try {
97
+ await transport.connect();
98
+ for (const light of config.lights) {
99
+ const state = await transport.readState(light.address);
100
+ if (state.mode !== 'cct')
101
+ throw new Error('Camera calibration currently requires CCT mode so all settings can be restored');
102
+ original.set(light.address, state);
103
+ }
104
+ for (const light of config.lights) await transport.send(light.address, powerPacket(false));
105
+ await delay(300);
106
+ const stamp = Date.now();
107
+ const dark = imagePixels(captureWebcam(`calibration-${stamp}-off`));
108
+ for (const light of config.lights) {
109
+ await transport.send(light.address, cctPacket(3200, 10, 0));
110
+ await delay(300);
111
+ const frame = captureWebcam(`calibration-${stamp}-${light.key}`);
112
+ regions.push(locateLight(dark, imagePixels(frame), light.key));
113
+ await transport.send(light.address, powerPacket(false));
114
+ await delay(300);
115
+ }
116
+ writeFileSync(
117
+ regionsFile,
118
+ JSON.stringify(
119
+ { cameraIndex: process.env.AMARAN_CAMERA_INDEX ?? '0', capturedAt: new Date().toISOString(), regions },
120
+ null,
121
+ 2
122
+ ),
123
+ { mode: 0o600 }
124
+ );
125
+ console.log('Identified fixture regions:', regions);
126
+ } catch (error) {
127
+ errors.push(error);
128
+ } finally {
129
+ for (const light of config.lights) {
130
+ const state = original.get(light.address);
131
+ if (!state) continue;
132
+ try {
133
+ await transport.send(light.address, cctPacket(state.cct ?? 3200, state.intensity, state.gm ?? 0));
134
+ await delay(200);
135
+ await transport.send(light.address, powerPacket(!state.sleep));
136
+ const actual = await transport.readState(light.address);
137
+ if (
138
+ actual.cct !== state.cct ||
139
+ actual.intensity !== state.intensity ||
140
+ actual.sleep !== state.sleep ||
141
+ (light.model === '150c' && actual.gm !== state.gm)
142
+ )
143
+ errors.push(new Error(`Failed to restore ${light.key}`));
144
+ } catch (error) {
145
+ errors.push(error);
146
+ }
147
+ }
148
+ await transport.disconnect();
149
+ sequence.close();
150
+ }
151
+ if (errors.length) throw new AggregateError(errors, 'Camera calibration failed');
152
+ process.exit(0);
@@ -0,0 +1,85 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import WebSocket from 'ws';
4
+ import { captureWebcam } from './webcam.js';
5
+
6
+ const ws = new WebSocket(process.env.AMARAN_REFERENCE_WS ?? 'ws://127.0.0.1:12345');
7
+ const pending = new Map<string, { resolve: (value: unknown) => void; reject: (error: Error) => void }>();
8
+ ws.on('message', (raw) => {
9
+ const message = JSON.parse(raw.toString());
10
+ const key = `${message.request?.type}:${message.request?.node_id ?? ''}`;
11
+ const request = pending.get(key);
12
+ if (!request) return;
13
+ if (message.code !== 0) request.reject(new Error(message.message));
14
+ else request.resolve(message.data?.data ?? message.data);
15
+ });
16
+
17
+ async function command(type: string, nodeId?: string, args?: Record<string, unknown>): Promise<unknown> {
18
+ const key = `${type}:${nodeId ?? ''}`;
19
+ if (pending.has(key)) throw new Error(`Concurrent duplicate command: ${key}`);
20
+ let timer: ReturnType<typeof setTimeout> | undefined;
21
+ try {
22
+ return await new Promise((resolve, reject) => {
23
+ pending.set(key, { resolve, reject });
24
+ timer = setTimeout(() => reject(new Error(`Desktop response timeout: ${key}`)), 8000);
25
+ ws.send(JSON.stringify({ version: 1, client_id: 'hardware-reference', type, node_id: nodeId, args }));
26
+ });
27
+ } finally {
28
+ clearTimeout(timer);
29
+ pending.delete(key);
30
+ }
31
+ }
32
+
33
+ const fixtures = [
34
+ { key: 'desk', nodeId: '400Q5-11B2F8' },
35
+ { key: 'front', nodeId: '400Q5-119B16' },
36
+ { key: 'back', nodeId: '400J5-F2C008' },
37
+ ];
38
+
39
+ try {
40
+ await new Promise<void>((resolve, reject) => {
41
+ const timeout = setTimeout(() => reject(new Error('Desktop connection timeout')), 8000);
42
+ ws.once('open', () => {
43
+ clearTimeout(timeout);
44
+ resolve();
45
+ });
46
+ ws.once('error', (error) => {
47
+ clearTimeout(timeout);
48
+ reject(error);
49
+ });
50
+ });
51
+ mkdirSync('artifacts', { recursive: true });
52
+ const snapshot = [];
53
+ for (const fixture of fixtures) {
54
+ snapshot.push({
55
+ ...fixture,
56
+ sleep: await command('get_sleep', fixture.nodeId),
57
+ intensity: await command('get_intensity', fixture.nodeId),
58
+ cct: await command('get_cct', fixture.nodeId),
59
+ });
60
+ }
61
+ writeFileSync(`artifacts/desktop-state-${Date.now()}.json`, JSON.stringify(snapshot, null, 2), { mode: 0o600 });
62
+ console.log(JSON.stringify(snapshot, null, 2));
63
+ if (process.argv[2] === 'reference') {
64
+ const stamp = Date.now();
65
+ for (const fixture of fixtures) {
66
+ await command('set_intensity', fixture.nodeId, { intensity: 10 });
67
+ await command('set_sleep', fixture.nodeId, { sleep: true });
68
+ }
69
+ await delay(1000);
70
+ console.log(captureWebcam(`desktop-${stamp}-all-off`));
71
+ for (const fixture of fixtures) {
72
+ await command('set_cct', fixture.nodeId, { cct: 3200, intensity: 10 });
73
+ await command('set_sleep', fixture.nodeId, { sleep: false });
74
+ await delay(1000);
75
+ console.log(captureWebcam(`desktop-${stamp}-${fixture.key}-3200-1pct`));
76
+ await command('set_cct', fixture.nodeId, { cct: 6500, intensity: 10 });
77
+ await delay(1000);
78
+ console.log(captureWebcam(`desktop-${stamp}-${fixture.key}-6500-1pct`));
79
+ await command('set_sleep', fixture.nodeId, { sleep: true });
80
+ }
81
+ console.log('Reference complete. All fixtures left asleep at 1%; original state saved in artifacts.');
82
+ }
83
+ } finally {
84
+ ws.close();
85
+ }