robopark 3.2.0 → 3.3.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 (408) hide show
  1. package/README.md +66 -105
  2. package/bin/robopark.js +9 -2
  3. package/conversation/elevenlabs_agent.py +1617 -1617
  4. package/conversation/supervisor_store.py +189 -189
  5. package/dist/{kernel/config-schema.js → config-schema.js} +1 -0
  6. package/dist/config-schema.js.map +1 -0
  7. package/dist/robopark/add-robot.js +1 -0
  8. package/dist/robopark/add-robot.js.map +1 -0
  9. package/dist/robopark/agent-ctl.js +1 -0
  10. package/dist/robopark/agent-ctl.js.map +1 -0
  11. package/dist/robopark/auto-start.js +51 -73
  12. package/dist/robopark/auto-start.js.map +1 -0
  13. package/dist/robopark/conversation.js +1 -0
  14. package/dist/robopark/conversation.js.map +1 -0
  15. package/dist/robopark/discovery.js +1 -0
  16. package/dist/robopark/discovery.js.map +1 -0
  17. package/dist/robopark/doctor.js +1 -0
  18. package/dist/robopark/doctor.js.map +1 -0
  19. package/dist/robopark/enroll.js +1 -0
  20. package/dist/robopark/enroll.js.map +1 -0
  21. package/dist/robopark/llm-set.js +1 -0
  22. package/dist/robopark/llm-set.js.map +1 -0
  23. package/dist/robopark/motor-control.js +1 -0
  24. package/dist/robopark/motor-control.js.map +1 -0
  25. package/dist/robopark/preview-agent-launcher.js +1 -0
  26. package/dist/robopark/preview-agent-launcher.js.map +1 -0
  27. package/dist/robopark/probe.js +16 -15
  28. package/dist/robopark/probe.js.map +1 -0
  29. package/dist/robopark/profile.js +1 -0
  30. package/dist/robopark/profile.js.map +1 -0
  31. package/dist/robopark/python-env.js +1 -7
  32. package/dist/robopark/python-env.js.map +1 -0
  33. package/dist/robopark/robot-runtime.js +1 -0
  34. package/dist/robopark/robot-runtime.js.map +1 -0
  35. package/dist/robopark/scan.js +1 -0
  36. package/dist/robopark/scan.js.map +1 -0
  37. package/dist/robopark/screen-control.js +1 -0
  38. package/dist/robopark/screen-control.js.map +1 -0
  39. package/dist/robopark/secrets.js +1 -0
  40. package/dist/robopark/secrets.js.map +1 -0
  41. package/dist/robopark/serve.js +6 -8
  42. package/dist/robopark/serve.js.map +1 -0
  43. package/dist/robopark/server-add.js +1 -0
  44. package/dist/robopark/server-add.js.map +1 -0
  45. package/dist/robopark/setup-livekit.js +53 -52
  46. package/dist/robopark/setup-livekit.js.map +1 -0
  47. package/dist/robopark/setup.js +1 -0
  48. package/dist/robopark/setup.js.map +1 -0
  49. package/dist/robopark/stop-all.js +1 -0
  50. package/dist/robopark/stop-all.js.map +1 -0
  51. package/dist/robopark/verify.js +1 -0
  52. package/dist/robopark/verify.js.map +1 -0
  53. package/dist/robopark/vision-agent-launcher.js +1 -0
  54. package/dist/robopark/vision-agent-launcher.js.map +1 -0
  55. package/dist/robopark/vision-control.js +1 -0
  56. package/dist/robopark/vision-control.js.map +1 -0
  57. package/dist/robopark-cli.js +7 -125
  58. package/dist/robopark-cli.js.map +1 -0
  59. package/package.json +16 -56
  60. package/pi-client/_install_steps.sh +29 -29
  61. package/pi-client/client.py +76 -76
  62. package/pi-client/install.sh +40 -40
  63. package/pi-client/join_convo.sh +54 -54
  64. package/pi-client/livekit_bridge.py +24 -24
  65. package/pi-client/motor_bridge.py +10 -10
  66. package/scheduler/__pycache__/main.cpython-312.pyc +0 -0
  67. package/scheduler/main.py +196 -17
  68. package/scheduler/media_lock.py +57 -57
  69. package/scheduler/preview_agent.py +1122 -1122
  70. package/scheduler/production_config.json +73 -113
  71. package/scheduler/scripts/install-robot-supervisor-linux.sh +33 -33
  72. package/{ui/standalone/public → scheduler/static}/voice-join.html +101 -13
  73. package/screen/screen_runtime.py +75 -75
  74. package/vision/app_pi_clean.py +297 -297
  75. package/vision/audio_server_pi.py +24 -24
  76. package/vision/install.sh +34 -34
  77. package/vision/motor_server.py +275 -275
  78. package/vision/requirements_camera.txt +6 -6
  79. package/vision/requirements_motor.txt +4 -4
  80. package/vision/requirements_pi_unified.txt +2 -2
  81. package/vision/requirements_vision_agent.txt +19 -19
  82. package/vision/run.sh +244 -244
  83. package/vision/services/services.sh +12 -12
  84. package/dist/kernel/types.js +0 -7
  85. package/dist/robopark/access.js +0 -99
  86. package/dist/robopark/control-center-html.js +0 -9191
  87. package/dist/robopark/control-server.js +0 -148
  88. package/dist/robopark/deployment-commands.js +0 -47
  89. package/dist/robopark/frontend-service.js +0 -80
  90. package/dist/robopark/standalone.js +0 -584
  91. package/dist/robopark/voice-join-html.js +0 -57
  92. package/scripts/windows-shim-fix.cjs +0 -58
  93. package/static/robopark/barracuda.png +0 -0
  94. package/static/robopark/bmw.png +0 -0
  95. package/static/robopark/jaguar.png +0 -0
  96. package/static/robopark/magnus.jpg +0 -0
  97. package/static/robopark/panda.png +0 -0
  98. package/static/robopark/tesla.png +0 -0
  99. package/static/robopark/titan.jpg +0 -0
  100. package/static/robopark/volt.png +0 -0
  101. package/static/vendor/elevenlabs-client.LICENSE +0 -21
  102. package/static/vendor/elevenlabs-client.iife.js +0 -24200
  103. package/ui/ROBOVOICE_BUILD.json +0 -9
  104. package/ui/standalone/.next/BUILD_ID +0 -1
  105. package/ui/standalone/.next/app-build-manifest.json +0 -391
  106. package/ui/standalone/.next/app-path-routes-manifest.json +0 -55
  107. package/ui/standalone/.next/build-manifest.json +0 -33
  108. package/ui/standalone/.next/package.json +0 -1
  109. package/ui/standalone/.next/prerender-manifest.json +0 -41
  110. package/ui/standalone/.next/react-loadable-manifest.json +0 -14
  111. package/ui/standalone/.next/required-server-files.json +0 -362
  112. package/ui/standalone/.next/routes-manifest.json +0 -246
  113. package/ui/standalone/.next/server/app/(app)/opengraph-image-xg4ifa/route.js +0 -1
  114. package/ui/standalone/.next/server/app/(app)/opengraph-image-xg4ifa/route.js.nft.json +0 -1
  115. package/ui/standalone/.next/server/app/(app)/opengraph-image-xg4ifa/route_client-reference-manifest.js +0 -1
  116. package/ui/standalone/.next/server/app/(app)/page.js +0 -2
  117. package/ui/standalone/.next/server/app/(app)/page.js.nft.json +0 -1
  118. package/ui/standalone/.next/server/app/(app)/page_client-reference-manifest.js +0 -1
  119. package/ui/standalone/.next/server/app/(app)/voice/page.js +0 -12
  120. package/ui/standalone/.next/server/app/(app)/voice/page.js.nft.json +0 -1
  121. package/ui/standalone/.next/server/app/(app)/voice/page_client-reference-manifest.js +0 -1
  122. package/ui/standalone/.next/server/app/_not-found/page.js +0 -12
  123. package/ui/standalone/.next/server/app/_not-found/page.js.nft.json +0 -1
  124. package/ui/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  125. package/ui/standalone/.next/server/app/api/connection-details/route.js +0 -1
  126. package/ui/standalone/.next/server/app/api/connection-details/route.js.nft.json +0 -1
  127. package/ui/standalone/.next/server/app/api/connection-details/route_client-reference-manifest.js +0 -1
  128. package/ui/standalone/.next/server/app/api/device-character/route.js +0 -1
  129. package/ui/standalone/.next/server/app/api/device-character/route.js.nft.json +0 -1
  130. package/ui/standalone/.next/server/app/api/device-character/route_client-reference-manifest.js +0 -1
  131. package/ui/standalone/.next/server/app/api/download-piper-model/route.js +0 -1
  132. package/ui/standalone/.next/server/app/api/download-piper-model/route.js.nft.json +0 -1
  133. package/ui/standalone/.next/server/app/api/download-piper-model/route_client-reference-manifest.js +0 -1
  134. package/ui/standalone/.next/server/app/api/model-status/route.js +0 -1
  135. package/ui/standalone/.next/server/app/api/model-status/route.js.nft.json +0 -1
  136. package/ui/standalone/.next/server/app/api/model-status/route_client-reference-manifest.js +0 -1
  137. package/ui/standalone/.next/server/app/api/models/route.js +0 -1
  138. package/ui/standalone/.next/server/app/api/models/route.js.nft.json +0 -1
  139. package/ui/standalone/.next/server/app/api/models/route_client-reference-manifest.js +0 -1
  140. package/ui/standalone/.next/server/app/api/prewarm/route.js +0 -1
  141. package/ui/standalone/.next/server/app/api/prewarm/route.js.nft.json +0 -1
  142. package/ui/standalone/.next/server/app/api/prewarm/route_client-reference-manifest.js +0 -1
  143. package/ui/standalone/.next/server/app/api/prompt/route.js +0 -1
  144. package/ui/standalone/.next/server/app/api/prompt/route.js.nft.json +0 -1
  145. package/ui/standalone/.next/server/app/api/prompt/route_client-reference-manifest.js +0 -1
  146. package/ui/standalone/.next/server/app/api/reload-tools/route.js +0 -1
  147. package/ui/standalone/.next/server/app/api/reload-tools/route.js.nft.json +0 -1
  148. package/ui/standalone/.next/server/app/api/reload-tools/route_client-reference-manifest.js +0 -1
  149. package/ui/standalone/.next/server/app/api/robopark/camera-token/route.js +0 -1
  150. package/ui/standalone/.next/server/app/api/robopark/camera-token/route.js.nft.json +0 -1
  151. package/ui/standalone/.next/server/app/api/robopark/camera-token/route_client-reference-manifest.js +0 -1
  152. package/ui/standalone/.next/server/app/api/robopark/character-ai/route.js +0 -1
  153. package/ui/standalone/.next/server/app/api/robopark/character-ai/route.js.nft.json +0 -1
  154. package/ui/standalone/.next/server/app/api/robopark/character-ai/route_client-reference-manifest.js +0 -1
  155. package/ui/standalone/.next/server/app/api/robopark/device-access/route.js +0 -1
  156. package/ui/standalone/.next/server/app/api/robopark/device-access/route.js.nft.json +0 -1
  157. package/ui/standalone/.next/server/app/api/robopark/device-access/route_client-reference-manifest.js +0 -1
  158. package/ui/standalone/.next/server/app/api/robopark/devices/[deviceId]/control/route.js +0 -1
  159. package/ui/standalone/.next/server/app/api/robopark/devices/[deviceId]/control/route.js.nft.json +0 -1
  160. package/ui/standalone/.next/server/app/api/robopark/devices/[deviceId]/control/route_client-reference-manifest.js +0 -1
  161. package/ui/standalone/.next/server/app/api/robopark/live-rooms/route.js +0 -1
  162. package/ui/standalone/.next/server/app/api/robopark/live-rooms/route.js.nft.json +0 -1
  163. package/ui/standalone/.next/server/app/api/robopark/live-rooms/route_client-reference-manifest.js +0 -1
  164. package/ui/standalone/.next/server/app/api/robopark/monitor-token/route.js +0 -1
  165. package/ui/standalone/.next/server/app/api/robopark/monitor-token/route.js.nft.json +0 -1
  166. package/ui/standalone/.next/server/app/api/robopark/monitor-token/route_client-reference-manifest.js +0 -1
  167. package/ui/standalone/.next/server/app/api/robopark/robots/[robotId]/operations/route.js +0 -1
  168. package/ui/standalone/.next/server/app/api/robopark/robots/[robotId]/operations/route.js.nft.json +0 -1
  169. package/ui/standalone/.next/server/app/api/robopark/robots/[robotId]/operations/route_client-reference-manifest.js +0 -1
  170. package/ui/standalone/.next/server/app/api/robopark/robots/[robotId]/voice-engine/route.js +0 -1
  171. package/ui/standalone/.next/server/app/api/robopark/robots/[robotId]/voice-engine/route.js.nft.json +0 -1
  172. package/ui/standalone/.next/server/app/api/robopark/robots/[robotId]/voice-engine/route_client-reference-manifest.js +0 -1
  173. package/ui/standalone/.next/server/app/api/robopark/robots/route.js +0 -1
  174. package/ui/standalone/.next/server/app/api/robopark/robots/route.js.nft.json +0 -1
  175. package/ui/standalone/.next/server/app/api/robopark/robots/route_client-reference-manifest.js +0 -1
  176. package/ui/standalone/.next/server/app/api/robopark/servers/[serverId]/metrics/route.js +0 -1
  177. package/ui/standalone/.next/server/app/api/robopark/servers/[serverId]/metrics/route.js.nft.json +0 -1
  178. package/ui/standalone/.next/server/app/api/robopark/servers/[serverId]/metrics/route_client-reference-manifest.js +0 -1
  179. package/ui/standalone/.next/server/app/api/robopark/servers/[serverId]/route.js +0 -1
  180. package/ui/standalone/.next/server/app/api/robopark/servers/[serverId]/route.js.nft.json +0 -1
  181. package/ui/standalone/.next/server/app/api/robopark/servers/[serverId]/route_client-reference-manifest.js +0 -1
  182. package/ui/standalone/.next/server/app/api/robopark/servers/route.js +0 -1
  183. package/ui/standalone/.next/server/app/api/robopark/servers/route.js.nft.json +0 -1
  184. package/ui/standalone/.next/server/app/api/robopark/servers/route_client-reference-manifest.js +0 -1
  185. package/ui/standalone/.next/server/app/api/robopark/sessions/[sessionId]/end/route.js +0 -1
  186. package/ui/standalone/.next/server/app/api/robopark/sessions/[sessionId]/end/route.js.nft.json +0 -1
  187. package/ui/standalone/.next/server/app/api/robopark/sessions/[sessionId]/end/route_client-reference-manifest.js +0 -1
  188. package/ui/standalone/.next/server/app/api/robopark/sessions/route.js +0 -1
  189. package/ui/standalone/.next/server/app/api/robopark/sessions/route.js.nft.json +0 -1
  190. package/ui/standalone/.next/server/app/api/robopark/sessions/route_client-reference-manifest.js +0 -1
  191. package/ui/standalone/.next/server/app/api/robopark/transcripts/[sessionId]/route.js +0 -1
  192. package/ui/standalone/.next/server/app/api/robopark/transcripts/[sessionId]/route.js.nft.json +0 -1
  193. package/ui/standalone/.next/server/app/api/robopark/transcripts/[sessionId]/route_client-reference-manifest.js +0 -1
  194. package/ui/standalone/.next/server/app/api/robopark/transcripts/route.js +0 -1
  195. package/ui/standalone/.next/server/app/api/robopark/transcripts/route.js.nft.json +0 -1
  196. package/ui/standalone/.next/server/app/api/robopark/transcripts/route_client-reference-manifest.js +0 -1
  197. package/ui/standalone/.next/server/app/api/robovoice/robots/[robotId]/media-health/route.js +0 -1
  198. package/ui/standalone/.next/server/app/api/robovoice/robots/[robotId]/media-health/route.js.nft.json +0 -1
  199. package/ui/standalone/.next/server/app/api/robovoice/robots/[robotId]/media-health/route_client-reference-manifest.js +0 -1
  200. package/ui/standalone/.next/server/app/api/settings/route.js +0 -1
  201. package/ui/standalone/.next/server/app/api/settings/route.js.nft.json +0 -1
  202. package/ui/standalone/.next/server/app/api/settings/route_client-reference-manifest.js +0 -1
  203. package/ui/standalone/.next/server/app/api/setup/complete/route.js +0 -1
  204. package/ui/standalone/.next/server/app/api/setup/complete/route.js.nft.json +0 -1
  205. package/ui/standalone/.next/server/app/api/setup/complete/route_client-reference-manifest.js +0 -1
  206. package/ui/standalone/.next/server/app/api/setup/status/route.js +0 -1
  207. package/ui/standalone/.next/server/app/api/setup/status/route.js.nft.json +0 -1
  208. package/ui/standalone/.next/server/app/api/setup/status/route_client-reference-manifest.js +0 -1
  209. package/ui/standalone/.next/server/app/api/setup/test-groq/route.js +0 -1
  210. package/ui/standalone/.next/server/app/api/setup/test-groq/route.js.nft.json +0 -1
  211. package/ui/standalone/.next/server/app/api/setup/test-groq/route_client-reference-manifest.js +0 -1
  212. package/ui/standalone/.next/server/app/api/setup/test-hass/route.js +0 -1
  213. package/ui/standalone/.next/server/app/api/setup/test-hass/route.js.nft.json +0 -1
  214. package/ui/standalone/.next/server/app/api/setup/test-hass/route_client-reference-manifest.js +0 -1
  215. package/ui/standalone/.next/server/app/api/setup/test-n8n/route.js +0 -1
  216. package/ui/standalone/.next/server/app/api/setup/test-n8n/route.js.nft.json +0 -1
  217. package/ui/standalone/.next/server/app/api/setup/test-n8n/route_client-reference-manifest.js +0 -1
  218. package/ui/standalone/.next/server/app/api/setup/test-ollama/route.js +0 -1
  219. package/ui/standalone/.next/server/app/api/setup/test-ollama/route.js.nft.json +0 -1
  220. package/ui/standalone/.next/server/app/api/setup/test-ollama/route_client-reference-manifest.js +0 -1
  221. package/ui/standalone/.next/server/app/api/test-tts/route.js +0 -1
  222. package/ui/standalone/.next/server/app/api/test-tts/route.js.nft.json +0 -1
  223. package/ui/standalone/.next/server/app/api/test-tts/route_client-reference-manifest.js +0 -1
  224. package/ui/standalone/.next/server/app/api/turn-credentials/route.js +0 -1
  225. package/ui/standalone/.next/server/app/api/turn-credentials/route.js.nft.json +0 -1
  226. package/ui/standalone/.next/server/app/api/turn-credentials/route_client-reference-manifest.js +0 -1
  227. package/ui/standalone/.next/server/app/api/voices/route.js +0 -1
  228. package/ui/standalone/.next/server/app/api/voices/route.js.nft.json +0 -1
  229. package/ui/standalone/.next/server/app/api/voices/route_client-reference-manifest.js +0 -1
  230. package/ui/standalone/.next/server/app/api/wake/route.js +0 -1
  231. package/ui/standalone/.next/server/app/api/wake/route.js.nft.json +0 -1
  232. package/ui/standalone/.next/server/app/api/wake/route_client-reference-manifest.js +0 -1
  233. package/ui/standalone/.next/server/app/api/wake-word/models/route.js +0 -1
  234. package/ui/standalone/.next/server/app/api/wake-word/models/route.js.nft.json +0 -1
  235. package/ui/standalone/.next/server/app/api/wake-word/models/route_client-reference-manifest.js +0 -1
  236. package/ui/standalone/.next/server/app/api/wake-word/status/route.js +0 -1
  237. package/ui/standalone/.next/server/app/api/wake-word/status/route.js.nft.json +0 -1
  238. package/ui/standalone/.next/server/app/api/wake-word/status/route_client-reference-manifest.js +0 -1
  239. package/ui/standalone/.next/server/app/api/wake-word/upload/route.js +0 -1
  240. package/ui/standalone/.next/server/app/api/wake-word/upload/route.js.nft.json +0 -1
  241. package/ui/standalone/.next/server/app/api/wake-word/upload/route_client-reference-manifest.js +0 -1
  242. package/ui/standalone/.next/server/app/favicon.ico/route.js +0 -1
  243. package/ui/standalone/.next/server/app/favicon.ico/route.js.nft.json +0 -1
  244. package/ui/standalone/.next/server/app/favicon.ico.body +0 -0
  245. package/ui/standalone/.next/server/app/favicon.ico.meta +0 -1
  246. package/ui/standalone/.next/server/app/fed/[...path]/route.js +0 -4
  247. package/ui/standalone/.next/server/app/fed/[...path]/route.js.nft.json +0 -1
  248. package/ui/standalone/.next/server/app/fed/[...path]/route_client-reference-manifest.js +0 -1
  249. package/ui/standalone/.next/server/app/fed/media/robots/[robotId]/video_feed/route.js +0 -1
  250. package/ui/standalone/.next/server/app/fed/media/robots/[robotId]/video_feed/route.js.nft.json +0 -1
  251. package/ui/standalone/.next/server/app/fed/media/robots/[robotId]/video_feed/route_client-reference-manifest.js +0 -1
  252. package/ui/standalone/.next/server/app/robopark/[...path]/route.js +0 -1
  253. package/ui/standalone/.next/server/app/robopark/[...path]/route.js.nft.json +0 -1
  254. package/ui/standalone/.next/server/app/robopark/[...path]/route_client-reference-manifest.js +0 -1
  255. package/ui/standalone/.next/server/app/robopark/api/[...path]/route.js +0 -1
  256. package/ui/standalone/.next/server/app/robopark/api/[...path]/route.js.nft.json +0 -1
  257. package/ui/standalone/.next/server/app/robopark/api/[...path]/route_client-reference-manifest.js +0 -1
  258. package/ui/standalone/.next/server/app/robopark/api/robots/[robotId]/stream/route.js +0 -1
  259. package/ui/standalone/.next/server/app/robopark/api/robots/[robotId]/stream/route.js.nft.json +0 -1
  260. package/ui/standalone/.next/server/app/robopark/api/robots/[robotId]/stream/route_client-reference-manifest.js +0 -1
  261. package/ui/standalone/.next/server/app/robopark/page.js +0 -2
  262. package/ui/standalone/.next/server/app/robopark/page.js.nft.json +0 -1
  263. package/ui/standalone/.next/server/app/robopark/page_client-reference-manifest.js +0 -1
  264. package/ui/standalone/.next/server/app/tailnet/route.js +0 -1
  265. package/ui/standalone/.next/server/app/tailnet/route.js.nft.json +0 -1
  266. package/ui/standalone/.next/server/app/tailnet/route_client-reference-manifest.js +0 -1
  267. package/ui/standalone/.next/server/app/vendor/livekit-client.umd.js/route.js +0 -1
  268. package/ui/standalone/.next/server/app/vendor/livekit-client.umd.js/route.js.nft.json +0 -1
  269. package/ui/standalone/.next/server/app/vendor/livekit-client.umd.js/route_client-reference-manifest.js +0 -1
  270. package/ui/standalone/.next/server/app/voice-tester/page.js +0 -2
  271. package/ui/standalone/.next/server/app/voice-tester/page.js.nft.json +0 -1
  272. package/ui/standalone/.next/server/app/voice-tester/page_client-reference-manifest.js +0 -1
  273. package/ui/standalone/.next/server/app-paths-manifest.json +0 -55
  274. package/ui/standalone/.next/server/chunks/1428.js +0 -1
  275. package/ui/standalone/.next/server/chunks/1692.js +0 -1
  276. package/ui/standalone/.next/server/chunks/37.js +0 -1
  277. package/ui/standalone/.next/server/chunks/4876.js +0 -1
  278. package/ui/standalone/.next/server/chunks/5232.js +0 -9
  279. package/ui/standalone/.next/server/chunks/5455.js +0 -11
  280. package/ui/standalone/.next/server/chunks/5611.js +0 -6
  281. package/ui/standalone/.next/server/chunks/5873.js +0 -22
  282. package/ui/standalone/.next/server/chunks/6018.js +0 -1
  283. package/ui/standalone/.next/server/chunks/8755.js +0 -1
  284. package/ui/standalone/.next/server/chunks/9077.js +0 -11
  285. package/ui/standalone/.next/server/functions-config-manifest.json +0 -9
  286. package/ui/standalone/.next/server/middleware-build-manifest.js +0 -1
  287. package/ui/standalone/.next/server/middleware-manifest.json +0 -6
  288. package/ui/standalone/.next/server/middleware-react-loadable-manifest.js +0 -1
  289. package/ui/standalone/.next/server/next-font-manifest.js +0 -1
  290. package/ui/standalone/.next/server/next-font-manifest.json +0 -1
  291. package/ui/standalone/.next/server/pages/500.html +0 -1
  292. package/ui/standalone/.next/server/pages/_app.js +0 -1
  293. package/ui/standalone/.next/server/pages/_app.js.nft.json +0 -1
  294. package/ui/standalone/.next/server/pages/_document.js +0 -1
  295. package/ui/standalone/.next/server/pages/_document.js.nft.json +0 -1
  296. package/ui/standalone/.next/server/pages/_error.js +0 -19
  297. package/ui/standalone/.next/server/pages/_error.js.nft.json +0 -1
  298. package/ui/standalone/.next/server/pages-manifest.json +0 -5
  299. package/ui/standalone/.next/server/server-reference-manifest.js +0 -1
  300. package/ui/standalone/.next/server/server-reference-manifest.json +0 -1
  301. package/ui/standalone/.next/server/webpack-runtime.js +0 -1
  302. package/ui/standalone/.next/static/T3eGaQUuZNb6rhEStcouc/_buildManifest.js +0 -1
  303. package/ui/standalone/.next/static/T3eGaQUuZNb6rhEStcouc/_ssgManifest.js +0 -1
  304. package/ui/standalone/.next/static/chunks/1255-404bb8de49c90395.js +0 -1
  305. package/ui/standalone/.next/static/chunks/144d3bae-bd5d150c0cab972e.js +0 -1
  306. package/ui/standalone/.next/static/chunks/1646.9123ee47220ed70b.js +0 -1
  307. package/ui/standalone/.next/static/chunks/2333-43c39acb531f7d4e.js +0 -3
  308. package/ui/standalone/.next/static/chunks/4bd1b696-f785427dddbba9fb.js +0 -1
  309. package/ui/standalone/.next/static/chunks/5139.c5e46d26064a85db.js +0 -1
  310. package/ui/standalone/.next/static/chunks/76e8f37e-2ae0e3fe7b44d874.js +0 -1
  311. package/ui/standalone/.next/static/chunks/9880-53262785284464cc.js +0 -1
  312. package/ui/standalone/.next/static/chunks/adfb71a7-625c1b5ebd039b6a.js +0 -1
  313. package/ui/standalone/.next/static/chunks/app/(app)/layout-5f312b128a0caf75.js +0 -1
  314. package/ui/standalone/.next/static/chunks/app/(app)/opengraph-image-xg4ifa/route-5f312b128a0caf75.js +0 -1
  315. package/ui/standalone/.next/static/chunks/app/(app)/page-5f312b128a0caf75.js +0 -1
  316. package/ui/standalone/.next/static/chunks/app/(app)/voice/page-72d72d9a70d9c1d4.js +0 -1
  317. package/ui/standalone/.next/static/chunks/app/_not-found/page-cfd7daa82d71540c.js +0 -1
  318. package/ui/standalone/.next/static/chunks/app/api/connection-details/route-5f312b128a0caf75.js +0 -1
  319. package/ui/standalone/.next/static/chunks/app/api/device-character/route-5f312b128a0caf75.js +0 -1
  320. package/ui/standalone/.next/static/chunks/app/api/download-piper-model/route-5f312b128a0caf75.js +0 -1
  321. package/ui/standalone/.next/static/chunks/app/api/model-status/route-5f312b128a0caf75.js +0 -1
  322. package/ui/standalone/.next/static/chunks/app/api/models/route-5f312b128a0caf75.js +0 -1
  323. package/ui/standalone/.next/static/chunks/app/api/prewarm/route-5f312b128a0caf75.js +0 -1
  324. package/ui/standalone/.next/static/chunks/app/api/prompt/route-5f312b128a0caf75.js +0 -1
  325. package/ui/standalone/.next/static/chunks/app/api/reload-tools/route-5f312b128a0caf75.js +0 -1
  326. package/ui/standalone/.next/static/chunks/app/api/robopark/camera-token/route-5f312b128a0caf75.js +0 -1
  327. package/ui/standalone/.next/static/chunks/app/api/robopark/character-ai/route-5f312b128a0caf75.js +0 -1
  328. package/ui/standalone/.next/static/chunks/app/api/robopark/device-access/route-5f312b128a0caf75.js +0 -1
  329. package/ui/standalone/.next/static/chunks/app/api/robopark/devices/[deviceId]/control/route-5f312b128a0caf75.js +0 -1
  330. package/ui/standalone/.next/static/chunks/app/api/robopark/live-rooms/route-5f312b128a0caf75.js +0 -1
  331. package/ui/standalone/.next/static/chunks/app/api/robopark/monitor-token/route-5f312b128a0caf75.js +0 -1
  332. package/ui/standalone/.next/static/chunks/app/api/robopark/robots/[robotId]/operations/route-5f312b128a0caf75.js +0 -1
  333. package/ui/standalone/.next/static/chunks/app/api/robopark/robots/[robotId]/voice-engine/route-5f312b128a0caf75.js +0 -1
  334. package/ui/standalone/.next/static/chunks/app/api/robopark/robots/route-5f312b128a0caf75.js +0 -1
  335. package/ui/standalone/.next/static/chunks/app/api/robopark/servers/[serverId]/metrics/route-5f312b128a0caf75.js +0 -1
  336. package/ui/standalone/.next/static/chunks/app/api/robopark/servers/[serverId]/route-5f312b128a0caf75.js +0 -1
  337. package/ui/standalone/.next/static/chunks/app/api/robopark/servers/route-5f312b128a0caf75.js +0 -1
  338. package/ui/standalone/.next/static/chunks/app/api/robopark/sessions/[sessionId]/end/route-5f312b128a0caf75.js +0 -1
  339. package/ui/standalone/.next/static/chunks/app/api/robopark/sessions/route-5f312b128a0caf75.js +0 -1
  340. package/ui/standalone/.next/static/chunks/app/api/robopark/transcripts/[sessionId]/route-5f312b128a0caf75.js +0 -1
  341. package/ui/standalone/.next/static/chunks/app/api/robopark/transcripts/route-5f312b128a0caf75.js +0 -1
  342. package/ui/standalone/.next/static/chunks/app/api/robovoice/robots/[robotId]/media-health/route-5f312b128a0caf75.js +0 -1
  343. package/ui/standalone/.next/static/chunks/app/api/settings/route-5f312b128a0caf75.js +0 -1
  344. package/ui/standalone/.next/static/chunks/app/api/setup/complete/route-5f312b128a0caf75.js +0 -1
  345. package/ui/standalone/.next/static/chunks/app/api/setup/status/route-5f312b128a0caf75.js +0 -1
  346. package/ui/standalone/.next/static/chunks/app/api/setup/test-groq/route-5f312b128a0caf75.js +0 -1
  347. package/ui/standalone/.next/static/chunks/app/api/setup/test-hass/route-5f312b128a0caf75.js +0 -1
  348. package/ui/standalone/.next/static/chunks/app/api/setup/test-n8n/route-5f312b128a0caf75.js +0 -1
  349. package/ui/standalone/.next/static/chunks/app/api/setup/test-ollama/route-5f312b128a0caf75.js +0 -1
  350. package/ui/standalone/.next/static/chunks/app/api/test-tts/route-5f312b128a0caf75.js +0 -1
  351. package/ui/standalone/.next/static/chunks/app/api/turn-credentials/route-5f312b128a0caf75.js +0 -1
  352. package/ui/standalone/.next/static/chunks/app/api/voices/route-5f312b128a0caf75.js +0 -1
  353. package/ui/standalone/.next/static/chunks/app/api/wake/route-5f312b128a0caf75.js +0 -1
  354. package/ui/standalone/.next/static/chunks/app/api/wake-word/models/route-5f312b128a0caf75.js +0 -1
  355. package/ui/standalone/.next/static/chunks/app/api/wake-word/status/route-5f312b128a0caf75.js +0 -1
  356. package/ui/standalone/.next/static/chunks/app/api/wake-word/upload/route-5f312b128a0caf75.js +0 -1
  357. package/ui/standalone/.next/static/chunks/app/fed/[...path]/route-5f312b128a0caf75.js +0 -1
  358. package/ui/standalone/.next/static/chunks/app/fed/media/robots/[robotId]/video_feed/route-5f312b128a0caf75.js +0 -1
  359. package/ui/standalone/.next/static/chunks/app/layout-fcce963da1d8220f.js +0 -1
  360. package/ui/standalone/.next/static/chunks/app/robopark/[...path]/route-5f312b128a0caf75.js +0 -1
  361. package/ui/standalone/.next/static/chunks/app/robopark/api/[...path]/route-5f312b128a0caf75.js +0 -1
  362. package/ui/standalone/.next/static/chunks/app/robopark/api/robots/[robotId]/stream/route-5f312b128a0caf75.js +0 -1
  363. package/ui/standalone/.next/static/chunks/app/robopark/page-5f312b128a0caf75.js +0 -1
  364. package/ui/standalone/.next/static/chunks/app/tailnet/route-5f312b128a0caf75.js +0 -1
  365. package/ui/standalone/.next/static/chunks/app/vendor/livekit-client.umd.js/route-5f312b128a0caf75.js +0 -1
  366. package/ui/standalone/.next/static/chunks/app/voice-tester/page-ae699e048e0eb834.js +0 -1
  367. package/ui/standalone/.next/static/chunks/framework-e60c938074ff7136.js +0 -1
  368. package/ui/standalone/.next/static/chunks/main-9e24e8708ff075fc.js +0 -1
  369. package/ui/standalone/.next/static/chunks/main-app-a1f59168303c7f84.js +0 -1
  370. package/ui/standalone/.next/static/chunks/pages/_app-131c90850aef965b.js +0 -1
  371. package/ui/standalone/.next/static/chunks/pages/_error-e4ba546eb376bdf4.js +0 -1
  372. package/ui/standalone/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  373. package/ui/standalone/.next/static/chunks/webpack-5252e5254da079b5.js +0 -1
  374. package/ui/standalone/.next/static/css/3f478a573f9be82e.css +0 -1
  375. package/ui/standalone/.next/static/css/51560c4af1d5a21f.css +0 -1
  376. package/ui/standalone/.next/static/media/27fe93087559d480-s.p.otf +0 -0
  377. package/ui/standalone/.next/static/media/4d3869dd8a56345a-s.p.otf +0 -0
  378. package/ui/standalone/.next/static/media/63f3bb9ce29e22d5-s.p.otf +0 -0
  379. package/ui/standalone/.next/static/media/79e1c5d0f2fe9f68-s.p.otf +0 -0
  380. package/ui/standalone/.next/static/media/8bf7b2ceda89477b-s.p.woff2 +0 -0
  381. package/ui/standalone/.next/static/media/98baa4a7809fbf72-s.woff2 +0 -0
  382. package/ui/standalone/.next/static/media/f7650dfaa438c37c-s.woff2 +0 -0
  383. package/ui/standalone/package.json +0 -57
  384. package/ui/standalone/public/caal-logo.svg +0 -3
  385. package/ui/standalone/public/commit-mono-400-regular.woff +0 -0
  386. package/ui/standalone/public/cwl-logo-round.png +0 -0
  387. package/ui/standalone/public/everett-light.woff +0 -0
  388. package/ui/standalone/public/hey_cal.ppn +0 -0
  389. package/ui/standalone/public/install-robot-kiosk.sh +0 -242
  390. package/ui/standalone/public/lk-logo-dark.svg +0 -18
  391. package/ui/standalone/public/lk-logo.svg +0 -18
  392. package/ui/standalone/public/lk-wordmark.svg +0 -12
  393. package/ui/standalone/public/opengraph-image-bg.png +0 -0
  394. package/ui/standalone/public/porcupine_params.pv +0 -0
  395. package/ui/standalone/public/robopark-control.html +0 -9200
  396. package/ui/standalone/public/static/robopark/barracuda.png +0 -0
  397. package/ui/standalone/public/static/robopark/bmw.png +0 -0
  398. package/ui/standalone/public/static/robopark/jaguar.png +0 -0
  399. package/ui/standalone/public/static/robopark/magnus.jpg +0 -0
  400. package/ui/standalone/public/static/robopark/panda.png +0 -0
  401. package/ui/standalone/public/static/robopark/tesla.png +0 -0
  402. package/ui/standalone/public/static/robopark/titan.jpg +0 -0
  403. package/ui/standalone/public/static/robopark/volt.png +0 -0
  404. package/ui/standalone/public/tailnet/index.html +0 -8995
  405. package/ui/standalone/public/transcript-analytics.html +0 -72
  406. package/ui/standalone/public/vendor/livekit-client.umd.js +0 -2
  407. package/ui/standalone/server.js +0 -38
  408. /package/{ui/standalone/public → scheduler/static}/vendor/elevenlabs-client.iife.js +0 -0
@@ -1,52 +1,52 @@
1
- from fastapi import FastAPI, HTTPException, Request
2
- from fastapi.responses import JSONResponse
1
+ from fastapi import FastAPI, HTTPException, Request
2
+ from fastapi.responses import JSONResponse
3
3
  from fastapi.middleware.cors import CORSMiddleware
4
- from pydantic import BaseModel
4
+ from pydantic import BaseModel
5
5
  from typing import Optional, List
6
6
  import time
7
7
  import threading
8
- import json
9
- import os
10
- import hmac
11
-
12
- try:
13
- import lgpio
14
- except ImportError:
15
- lgpio = None
16
-
17
- app = FastAPI(title="RoboPark Motor Control API", version="1.2.0")
18
-
19
- ROBOT_NAME = os.getenv("ROBOPARK_ROBOT_NAME", "robot").strip() or "robot"
20
-
21
- MOTOR_API_TOKEN = (
22
- os.getenv("ROBOPARK_MOTOR_TOKEN", "").strip()
23
- or os.getenv("ROBOPARK_MESH_TOKEN", "").strip()
24
- )
25
-
26
-
27
- @app.middleware("http")
28
- async def require_motor_token(request: Request, call_next):
29
- """Keep actuation private even if an operator accidentally changes the bind host."""
30
- if MOTOR_API_TOKEN and request.url.path not in {"/", "/status"}:
31
- supplied = request.headers.get("x-robopark-motor-token", "").strip()
32
- if not supplied or not hmac.compare_digest(supplied, MOTOR_API_TOKEN):
33
- return JSONResponse(status_code=401, content={"detail": "Invalid or missing motor token"})
34
- return await call_next(request)
35
-
36
- # The dashboard never calls this service directly; robot-local services do.
37
- app.add_middleware(
38
- CORSMiddleware,
39
- allow_origins=["http://127.0.0.1", "http://localhost"],
40
- allow_credentials=False,
41
- allow_methods=["GET", "POST", "PUT", "DELETE"],
42
- allow_headers=["content-type", "x-robopark-motor-token"],
43
- )
8
+ import json
9
+ import os
10
+ import hmac
11
+
12
+ try:
13
+ import lgpio
14
+ except ImportError:
15
+ lgpio = None
16
+
17
+ app = FastAPI(title="RoboPark Motor Control API", version="1.2.0")
18
+
19
+ ROBOT_NAME = os.getenv("ROBOPARK_ROBOT_NAME", "robot").strip() or "robot"
20
+
21
+ MOTOR_API_TOKEN = (
22
+ os.getenv("ROBOPARK_MOTOR_TOKEN", "").strip()
23
+ or os.getenv("ROBOPARK_MESH_TOKEN", "").strip()
24
+ )
25
+
26
+
27
+ @app.middleware("http")
28
+ async def require_motor_token(request: Request, call_next):
29
+ """Keep actuation private even if an operator accidentally changes the bind host."""
30
+ if MOTOR_API_TOKEN and request.url.path not in {"/", "/status"}:
31
+ supplied = request.headers.get("x-robopark-motor-token", "").strip()
32
+ if not supplied or not hmac.compare_digest(supplied, MOTOR_API_TOKEN):
33
+ return JSONResponse(status_code=401, content={"detail": "Invalid or missing motor token"})
34
+ return await call_next(request)
35
+
36
+ # The dashboard never calls this service directly; robot-local services do.
37
+ app.add_middleware(
38
+ CORSMiddleware,
39
+ allow_origins=["http://127.0.0.1", "http://localhost"],
40
+ allow_credentials=False,
41
+ allow_methods=["GET", "POST", "PUT", "DELETE"],
42
+ allow_headers=["content-type", "x-robopark-motor-token"],
43
+ )
44
44
 
45
45
  # Storage file
46
- MOTORS_FILE = os.getenv(
47
- "ROBOPARK_MOTORS_FILE",
48
- os.path.expanduser("~/.robopark/motors.json"),
49
- )
46
+ MOTORS_FILE = os.getenv(
47
+ "ROBOPARK_MOTORS_FILE",
48
+ os.path.expanduser("~/.robopark/motors.json"),
49
+ )
50
50
 
51
51
  # In-memory storage
52
52
  motors = {}
@@ -54,11 +54,11 @@ active_motor = None
54
54
  remaining_seconds = 0
55
55
  last_action = "None"
56
56
  motor_thread = None
57
- stop_flag = False
58
- motor_error = None
59
- gpio_handle = None
60
- claimed_pins = set()
61
- motor_lock = threading.Lock()
57
+ stop_flag = False
58
+ motor_error = None
59
+ gpio_handle = None
60
+ claimed_pins = set()
61
+ motor_lock = threading.Lock()
62
62
 
63
63
  # Load motors from file on startup
64
64
  def load_motors():
@@ -66,16 +66,16 @@ def load_motors():
66
66
  global motors
67
67
  try:
68
68
  if os.path.exists(MOTORS_FILE):
69
- with open(MOTORS_FILE, 'r') as f:
70
- motors = json.load(f)
71
- motors = {
72
- str(name): ({"name": str(name), "gpio": int(value), "active_high": True}
73
- if not isinstance(value, dict) else {
74
- "name": str(value.get("name") or name), "gpio": int(value["gpio"]),
75
- "active_high": bool(value.get("active_high", True)),
76
- })
77
- for name, value in motors.items()
78
- }
69
+ with open(MOTORS_FILE, 'r') as f:
70
+ motors = json.load(f)
71
+ motors = {
72
+ str(name): ({"name": str(name), "gpio": int(value), "active_high": True}
73
+ if not isinstance(value, dict) else {
74
+ "name": str(value.get("name") or name), "gpio": int(value["gpio"]),
75
+ "active_high": bool(value.get("active_high", True)),
76
+ })
77
+ for name, value in motors.items()
78
+ }
79
79
  print(f"[LOAD] Loaded {len(motors)} motors from {MOTORS_FILE}")
80
80
  for name, gpio in motors.items():
81
81
  print(f" - {name}: GPIO {gpio}")
@@ -85,16 +85,16 @@ def load_motors():
85
85
  print(f"[ERROR] Failed to load motors: {e}")
86
86
  motors = {}
87
87
 
88
- def save_motors():
88
+ def save_motors():
89
89
  """Save motors to motors.json file"""
90
90
  try:
91
- os.makedirs(os.path.dirname(os.path.abspath(MOTORS_FILE)), exist_ok=True)
92
- temporary = f"{MOTORS_FILE}.tmp"
93
- with open(temporary, 'w') as f:
94
- json.dump(motors, f, indent=2)
95
- f.flush()
96
- os.fsync(f.fileno())
97
- os.replace(temporary, MOTORS_FILE)
91
+ os.makedirs(os.path.dirname(os.path.abspath(MOTORS_FILE)), exist_ok=True)
92
+ temporary = f"{MOTORS_FILE}.tmp"
93
+ with open(temporary, 'w') as f:
94
+ json.dump(motors, f, indent=2)
95
+ f.flush()
96
+ os.fsync(f.fileno())
97
+ os.replace(temporary, MOTORS_FILE)
98
98
  print(f"[SAVE] Motors saved to {MOTORS_FILE}")
99
99
  except Exception as e:
100
100
  print(f"[ERROR] Failed to save motors: {e}")
@@ -103,101 +103,101 @@ def save_motors():
103
103
  load_motors()
104
104
 
105
105
  # Request Models
106
- class MotorConfig(BaseModel):
107
- name: str
108
- gpio: int
109
- active_high: bool = True
106
+ class MotorConfig(BaseModel):
107
+ name: str
108
+ gpio: int
109
+ active_high: bool = True
110
110
 
111
- class UpdateMotorConfig(BaseModel):
112
- name: str
113
- gpio: int
114
- active_high: bool = True
111
+ class UpdateMotorConfig(BaseModel):
112
+ name: str
113
+ gpio: int
114
+ active_high: bool = True
115
115
 
116
- class TriggerMotor(BaseModel):
117
- motor_name: str
118
- seconds: float = 1.0
116
+ class TriggerMotor(BaseModel):
117
+ motor_name: str
118
+ seconds: float = 1.0
119
119
 
120
120
  class StopMotors(BaseModel):
121
121
  motor_name: Optional[str] = None
122
122
 
123
- class TestConfig(BaseModel):
124
- seconds_on: float = 0.3
125
- seconds_pause: float = 0.2
123
+ class TestConfig(BaseModel):
124
+ seconds_on: float = 0.3
125
+ seconds_pause: float = 0.2
126
126
  pins: Optional[List[int]] = None
127
127
 
128
128
  # Helper Functions
129
- def _motor_value(motor: dict, active: bool) -> int:
130
- return int(active == bool(motor.get("active_high", True)))
131
-
132
- def _claim_motor(motor: dict) -> None:
133
- global gpio_handle
134
- pin = int(motor["gpio"])
135
- if pin < 2 or pin > 27:
136
- raise ValueError(f"BCM GPIO {pin} is outside 2..27")
137
- if lgpio is None:
138
- raise RuntimeError("lgpio is not installed; refusing simulated production actuation")
139
- if gpio_handle is None:
140
- gpio_handle = lgpio.gpiochip_open(0)
141
- if pin not in claimed_pins:
142
- lgpio.gpio_claim_output(gpio_handle, pin, _motor_value(motor, False))
143
- claimed_pins.add(pin)
144
-
145
- def _write_motor(motor: dict, active: bool) -> None:
146
- _claim_motor(motor)
147
- lgpio.gpio_write(gpio_handle, int(motor["gpio"]), _motor_value(motor, active))
148
-
149
- def _all_off() -> None:
150
- for motor in motors.values():
151
- try:
152
- if isinstance(motor, dict):
153
- _write_motor(motor, False)
154
- except Exception as exc:
155
- print(f"[GPIO] failed to de-energize {motor}: {exc}")
156
-
157
- def run_motor_thread(motor_name: str, duration: float):
158
- global active_motor, remaining_seconds, last_action, stop_flag, motor_error
159
-
160
- motor = motors[motor_name]
161
- started = time.monotonic()
162
- print(f"[MOTOR] Starting {motor_name} for {duration:.3f} seconds")
163
- try:
164
- _write_motor(motor, True)
165
- while not stop_flag and time.monotonic() - started < duration:
166
- remaining_seconds = max(0, duration - (time.monotonic() - started))
167
- time.sleep(min(0.05, remaining_seconds or 0.01))
168
- except Exception as exc:
169
- motor_error = f"{type(exc).__name__}: {exc}"
170
- last_action = f"Failed {motor_name}: {motor_error}"
171
- print(f"[ERROR] Motor {motor_name} failed: {motor_error}")
172
- finally:
173
- try:
174
- _write_motor(motor, False)
175
- finally:
176
- with motor_lock:
177
- active_motor = None
178
- remaining_seconds = 0
179
- stop_flag = False
180
- print(f"[MOTOR] {motor_name} completed and GPIO is OFF")
181
-
182
- @app.on_event("shutdown")
183
- def shutdown_gpio() -> None:
184
- global gpio_handle
185
- _all_off()
186
- if lgpio is not None and gpio_handle is not None:
187
- try:
188
- lgpio.gpiochip_close(gpio_handle)
189
- finally:
190
- gpio_handle = None
191
- claimed_pins.clear()
129
+ def _motor_value(motor: dict, active: bool) -> int:
130
+ return int(active == bool(motor.get("active_high", True)))
131
+
132
+ def _claim_motor(motor: dict) -> None:
133
+ global gpio_handle
134
+ pin = int(motor["gpio"])
135
+ if pin < 2 or pin > 27:
136
+ raise ValueError(f"BCM GPIO {pin} is outside 2..27")
137
+ if lgpio is None:
138
+ raise RuntimeError("lgpio is not installed; refusing simulated production actuation")
139
+ if gpio_handle is None:
140
+ gpio_handle = lgpio.gpiochip_open(0)
141
+ if pin not in claimed_pins:
142
+ lgpio.gpio_claim_output(gpio_handle, pin, _motor_value(motor, False))
143
+ claimed_pins.add(pin)
144
+
145
+ def _write_motor(motor: dict, active: bool) -> None:
146
+ _claim_motor(motor)
147
+ lgpio.gpio_write(gpio_handle, int(motor["gpio"]), _motor_value(motor, active))
148
+
149
+ def _all_off() -> None:
150
+ for motor in motors.values():
151
+ try:
152
+ if isinstance(motor, dict):
153
+ _write_motor(motor, False)
154
+ except Exception as exc:
155
+ print(f"[GPIO] failed to de-energize {motor}: {exc}")
156
+
157
+ def run_motor_thread(motor_name: str, duration: float):
158
+ global active_motor, remaining_seconds, last_action, stop_flag, motor_error
159
+
160
+ motor = motors[motor_name]
161
+ started = time.monotonic()
162
+ print(f"[MOTOR] Starting {motor_name} for {duration:.3f} seconds")
163
+ try:
164
+ _write_motor(motor, True)
165
+ while not stop_flag and time.monotonic() - started < duration:
166
+ remaining_seconds = max(0, duration - (time.monotonic() - started))
167
+ time.sleep(min(0.05, remaining_seconds or 0.01))
168
+ except Exception as exc:
169
+ motor_error = f"{type(exc).__name__}: {exc}"
170
+ last_action = f"Failed {motor_name}: {motor_error}"
171
+ print(f"[ERROR] Motor {motor_name} failed: {motor_error}")
172
+ finally:
173
+ try:
174
+ _write_motor(motor, False)
175
+ finally:
176
+ with motor_lock:
177
+ active_motor = None
178
+ remaining_seconds = 0
179
+ stop_flag = False
180
+ print(f"[MOTOR] {motor_name} completed and GPIO is OFF")
181
+
182
+ @app.on_event("shutdown")
183
+ def shutdown_gpio() -> None:
184
+ global gpio_handle
185
+ _all_off()
186
+ if lgpio is not None and gpio_handle is not None:
187
+ try:
188
+ lgpio.gpiochip_close(gpio_handle)
189
+ finally:
190
+ gpio_handle = None
191
+ claimed_pins.clear()
192
192
 
193
193
  # API Endpoints
194
194
  @app.get("/")
195
195
  async def root():
196
196
  return {
197
- "message": "Motor Control API",
198
- "version": "1.2.0",
199
- "robot": ROBOT_NAME,
200
- "authentication": "token" if MOTOR_API_TOKEN else "localhost-only",
197
+ "message": "Motor Control API",
198
+ "version": "1.2.0",
199
+ "robot": ROBOT_NAME,
200
+ "authentication": "token" if MOTOR_API_TOKEN else "localhost-only",
201
201
  "endpoints": ["/status", "/test", "/add-motor", "/trigger-motor", "/list-motors", "/stop-motors"]
202
202
  }
203
203
 
@@ -208,26 +208,26 @@ async def get_status():
208
208
  "status": status,
209
209
  "active_motor": active_motor,
210
210
  "remaining_seconds": remaining_seconds,
211
- "last_action": last_action,
212
- "error": motor_error,
213
- "total_motors": len(motors),
214
- "robot": ROBOT_NAME,
215
- "gpio_available": lgpio is not None,
211
+ "last_action": last_action,
212
+ "error": motor_error,
213
+ "total_motors": len(motors),
214
+ "robot": ROBOT_NAME,
215
+ "gpio_available": lgpio is not None,
216
216
  }
217
217
 
218
218
  @app.post("/add-motor")
219
- async def add_motor(config: MotorConfig):
220
- if config.gpio < 2 or config.gpio > 27:
221
- raise HTTPException(status_code=422, detail="BCM GPIO must be in range 2..27")
222
- if config.name in motors:
223
- raise HTTPException(status_code=400, detail=f"Motor '{config.name}' already exists")
224
- if any(int(item["gpio"]) == config.gpio for item in motors.values()):
225
- raise HTTPException(status_code=409, detail=f"BCM GPIO {config.gpio} is already registered")
226
-
227
- motors[config.name] = {
228
- "name": config.name,
229
- "gpio": config.gpio,
230
- "active_high": config.active_high,
219
+ async def add_motor(config: MotorConfig):
220
+ if config.gpio < 2 or config.gpio > 27:
221
+ raise HTTPException(status_code=422, detail="BCM GPIO must be in range 2..27")
222
+ if config.name in motors:
223
+ raise HTTPException(status_code=400, detail=f"Motor '{config.name}' already exists")
224
+ if any(int(item["gpio"]) == config.gpio for item in motors.values()):
225
+ raise HTTPException(status_code=409, detail=f"BCM GPIO {config.gpio} is already registered")
226
+
227
+ motors[config.name] = {
228
+ "name": config.name,
229
+ "gpio": config.gpio,
230
+ "active_high": config.active_high,
231
231
  }
232
232
  save_motors()
233
233
 
@@ -239,49 +239,49 @@ async def add_motor(config: MotorConfig):
239
239
  "motor": motors[config.name]
240
240
  }
241
241
 
242
- @app.get("/list-motors")
243
- async def list_motors():
242
+ @app.get("/list-motors")
243
+ async def list_motors():
244
244
  motor_list = list(motors.values())
245
245
  return {
246
246
  "status": "success",
247
247
  "count": len(motor_list),
248
- "motors": motor_list
249
- }
250
-
251
- @app.get("/discover")
252
- async def discover_motors():
253
- """Return the relay registry and GPIO capability for fleet discovery."""
254
- motor_list = list(motors.values())
255
- return {
256
- "status": "success",
257
- "robot": ROBOT_NAME,
258
- "gpio_available": lgpio is not None,
259
- "count": len(motor_list),
260
- "motors": motor_list,
261
- }
248
+ "motors": motor_list
249
+ }
250
+
251
+ @app.get("/discover")
252
+ async def discover_motors():
253
+ """Return the relay registry and GPIO capability for fleet discovery."""
254
+ motor_list = list(motors.values())
255
+ return {
256
+ "status": "success",
257
+ "robot": ROBOT_NAME,
258
+ "gpio_available": lgpio is not None,
259
+ "count": len(motor_list),
260
+ "motors": motor_list,
261
+ }
262
262
 
263
263
  @app.put("/update-motor/{motor_name}")
264
- async def update_motor(motor_name: str, config: UpdateMotorConfig):
265
- if config.gpio < 2 or config.gpio > 27:
266
- raise HTTPException(status_code=422, detail="BCM GPIO must be in range 2..27")
264
+ async def update_motor(motor_name: str, config: UpdateMotorConfig):
265
+ if config.gpio < 2 or config.gpio > 27:
266
+ raise HTTPException(status_code=422, detail="BCM GPIO must be in range 2..27")
267
267
  if motor_name not in motors:
268
268
  raise HTTPException(status_code=404, detail=f"Motor '{motor_name}' not found")
269
269
 
270
270
  if active_motor == motor_name:
271
271
  raise HTTPException(status_code=409, detail=f"Cannot update motor '{motor_name}' while it is running")
272
272
 
273
- if config.name != motor_name and config.name in motors:
274
- raise HTTPException(status_code=400, detail=f"Motor name '{config.name}' already exists")
275
- if any(name != motor_name and int(item["gpio"]) == config.gpio for name, item in motors.items()):
276
- raise HTTPException(status_code=409, detail=f"BCM GPIO {config.gpio} is already registered")
273
+ if config.name != motor_name and config.name in motors:
274
+ raise HTTPException(status_code=400, detail=f"Motor name '{config.name}' already exists")
275
+ if any(name != motor_name and int(item["gpio"]) == config.gpio for name, item in motors.items()):
276
+ raise HTTPException(status_code=409, detail=f"BCM GPIO {config.gpio} is already registered")
277
277
 
278
278
  if config.name != motor_name:
279
279
  del motors[motor_name]
280
280
 
281
- motors[config.name] = {
282
- "name": config.name,
283
- "gpio": config.gpio,
284
- "active_high": config.active_high,
281
+ motors[config.name] = {
282
+ "name": config.name,
283
+ "gpio": config.gpio,
284
+ "active_high": config.active_high,
285
285
  }
286
286
  save_motors()
287
287
 
@@ -313,30 +313,30 @@ async def delete_motor(motor_name: str):
313
313
  }
314
314
 
315
315
  @app.post("/trigger-motor")
316
- async def trigger_motor(config: TriggerMotor):
317
- global motor_thread, stop_flag, active_motor, remaining_seconds, last_action, motor_error
316
+ async def trigger_motor(config: TriggerMotor):
317
+ global motor_thread, stop_flag, active_motor, remaining_seconds, last_action, motor_error
318
318
 
319
319
  if config.motor_name not in motors:
320
320
  raise HTTPException(status_code=404, detail=f"Motor '{config.motor_name}' not found")
321
321
 
322
- if config.seconds < 0.05 or config.seconds > 10.0:
323
- raise HTTPException(status_code=422, detail="Pulse duration must be 0.05..10 seconds")
324
-
325
- try:
326
- _claim_motor(motors[config.motor_name])
327
- except Exception as exc:
328
- raise HTTPException(status_code=503, detail=f"GPIO is unavailable: {exc}") from exc
329
-
330
- with motor_lock:
331
- if active_motor:
332
- raise HTTPException(status_code=409, detail=f"Motor '{active_motor}' is already running")
333
- stop_flag = False
334
- motor_error = None
335
- active_motor = config.motor_name
336
- remaining_seconds = config.seconds
337
- last_action = f"Triggered {config.motor_name}"
338
- motor_thread = threading.Thread(target=run_motor_thread, args=(config.motor_name, config.seconds), daemon=True)
339
- motor_thread.start()
322
+ if config.seconds < 0.05 or config.seconds > 10.0:
323
+ raise HTTPException(status_code=422, detail="Pulse duration must be 0.05..10 seconds")
324
+
325
+ try:
326
+ _claim_motor(motors[config.motor_name])
327
+ except Exception as exc:
328
+ raise HTTPException(status_code=503, detail=f"GPIO is unavailable: {exc}") from exc
329
+
330
+ with motor_lock:
331
+ if active_motor:
332
+ raise HTTPException(status_code=409, detail=f"Motor '{active_motor}' is already running")
333
+ stop_flag = False
334
+ motor_error = None
335
+ active_motor = config.motor_name
336
+ remaining_seconds = config.seconds
337
+ last_action = f"Triggered {config.motor_name}"
338
+ motor_thread = threading.Thread(target=run_motor_thread, args=(config.motor_name, config.seconds), daemon=True)
339
+ motor_thread.start()
340
340
 
341
341
  return {
342
342
  "status": "success",
@@ -350,20 +350,20 @@ async def stop_motors(config: Optional[StopMotors] = None):
350
350
  global stop_flag, last_action
351
351
 
352
352
  try:
353
- if not active_motor:
354
- _all_off()
355
- return {
353
+ if not active_motor:
354
+ _all_off()
355
+ return {
356
356
  "status": "success",
357
357
  "message": "No motors are currently running"
358
358
  }
359
359
 
360
- stop_flag = True
361
- last_action = f"Stopped {active_motor}"
360
+ stop_flag = True
361
+ last_action = f"Stopped {active_motor}"
362
362
 
363
363
  # Wait for thread to finish
364
- if motor_thread and motor_thread.is_alive():
365
- motor_thread.join(timeout=2)
366
- _all_off()
364
+ if motor_thread and motor_thread.is_alive():
365
+ motor_thread.join(timeout=2)
366
+ _all_off()
367
367
 
368
368
  return {
369
369
  "status": "success",
@@ -381,30 +381,30 @@ async def stop_motors(config: Optional[StopMotors] = None):
381
381
  }
382
382
 
383
383
  @app.post("/test")
384
- async def run_test(config: TestConfig):
385
- global last_action, active_motor, motor_error
386
-
387
- if config.seconds_on < 0.05 or config.seconds_on > 10.0:
388
- raise HTTPException(status_code=422, detail="Test pulse duration must be 0.05..10 seconds")
389
- if config.seconds_pause < 0 or config.seconds_pause > 10.0:
390
- raise HTTPException(status_code=422, detail="Test pause must be 0..10 seconds")
391
-
392
- registered_pins = {int(m["gpio"]) for m in motors.values() if isinstance(m, dict)}
393
- pins = config.pins if config.pins else sorted(registered_pins)
394
- if not pins or any(pin not in registered_pins for pin in pins):
395
- raise HTTPException(status_code=422, detail="Relay tests are limited to registered GPIO pins")
396
- try:
397
- for motor in motors.values():
398
- if int(motor["gpio"]) in pins:
399
- _claim_motor(motor)
400
- except Exception as exc:
401
- raise HTTPException(status_code=503, detail=f"GPIO is unavailable: {exc}") from exc
402
-
403
- with motor_lock:
404
- if active_motor:
405
- raise HTTPException(status_code=409, detail="A motor is already running")
406
- active_motor = "relay-test"
407
- motor_error = None
384
+ async def run_test(config: TestConfig):
385
+ global last_action, active_motor, motor_error
386
+
387
+ if config.seconds_on < 0.05 or config.seconds_on > 10.0:
388
+ raise HTTPException(status_code=422, detail="Test pulse duration must be 0.05..10 seconds")
389
+ if config.seconds_pause < 0 or config.seconds_pause > 10.0:
390
+ raise HTTPException(status_code=422, detail="Test pause must be 0..10 seconds")
391
+
392
+ registered_pins = {int(m["gpio"]) for m in motors.values() if isinstance(m, dict)}
393
+ pins = config.pins if config.pins else sorted(registered_pins)
394
+ if not pins or any(pin not in registered_pins for pin in pins):
395
+ raise HTTPException(status_code=422, detail="Relay tests are limited to registered GPIO pins")
396
+ try:
397
+ for motor in motors.values():
398
+ if int(motor["gpio"]) in pins:
399
+ _claim_motor(motor)
400
+ except Exception as exc:
401
+ raise HTTPException(status_code=503, detail=f"GPIO is unavailable: {exc}") from exc
402
+
403
+ with motor_lock:
404
+ if active_motor:
405
+ raise HTTPException(status_code=409, detail="A motor is already running")
406
+ active_motor = "relay-test"
407
+ motor_error = None
408
408
 
409
409
  print(f"[TEST] Running test sequence:")
410
410
  print(f" - Seconds ON: {config.seconds_on}")
@@ -413,32 +413,32 @@ async def run_test(config: TestConfig):
413
413
 
414
414
  last_action = f"Test sequence started"
415
415
 
416
- def test_sequence():
417
- global last_action, active_motor, motor_error
418
- try:
419
- for pin in pins:
420
- last_action = f"test:gpio:{pin}"
421
- motor = next(m for m in motors.values() if int(m["gpio"]) == pin)
422
- print(f">>> TESTING GPIO {pin}")
423
- try:
424
- _write_motor(motor, True)
425
- time.sleep(config.seconds_on)
426
- finally:
427
- _write_motor(motor, False)
428
- print(f"[TEST] Pin {pin} OFF, pausing {config.seconds_pause}s")
429
- time.sleep(config.seconds_pause)
430
- last_action = "Test sequence completed"
431
- print("[TEST] Test sequence completed")
432
- except Exception as exc:
433
- motor_error = f"{type(exc).__name__}: {exc}"
434
- last_action = f"Relay test failed: {motor_error}"
435
- print(f"[ERROR] Relay test failed: {motor_error}")
436
- finally:
437
- _all_off()
438
- with motor_lock:
439
- active_motor = None
440
-
441
- test_thread = threading.Thread(target=test_sequence, daemon=True)
416
+ def test_sequence():
417
+ global last_action, active_motor, motor_error
418
+ try:
419
+ for pin in pins:
420
+ last_action = f"test:gpio:{pin}"
421
+ motor = next(m for m in motors.values() if int(m["gpio"]) == pin)
422
+ print(f">>> TESTING GPIO {pin}")
423
+ try:
424
+ _write_motor(motor, True)
425
+ time.sleep(config.seconds_on)
426
+ finally:
427
+ _write_motor(motor, False)
428
+ print(f"[TEST] Pin {pin} OFF, pausing {config.seconds_pause}s")
429
+ time.sleep(config.seconds_pause)
430
+ last_action = "Test sequence completed"
431
+ print("[TEST] Test sequence completed")
432
+ except Exception as exc:
433
+ motor_error = f"{type(exc).__name__}: {exc}"
434
+ last_action = f"Relay test failed: {motor_error}"
435
+ print(f"[ERROR] Relay test failed: {motor_error}")
436
+ finally:
437
+ _all_off()
438
+ with motor_lock:
439
+ active_motor = None
440
+
441
+ test_thread = threading.Thread(target=test_sequence, daemon=True)
442
442
  test_thread.start()
443
443
 
444
444
  return {
@@ -456,12 +456,12 @@ if __name__ == "__main__":
456
456
  print("=" * 60)
457
457
  print("Motor Control API Server")
458
458
  print("=" * 60)
459
- print("Starting server on http://127.0.0.1:8001")
459
+ print("Starting server on http://127.0.0.1:8001")
460
460
  print("API Documentation: http://localhost:8001/docs")
461
461
  print("ReDoc: http://localhost:8001/redoc")
462
462
  print("=" * 60)
463
- uvicorn.run(
464
- app,
465
- host=os.getenv("ROBOPARK_MOTOR_HOST", "127.0.0.1"),
466
- port=int(os.getenv("ROBOPARK_MOTOR_PORT", "8001")),
467
- )
463
+ uvicorn.run(
464
+ app,
465
+ host=os.getenv("ROBOPARK_MOTOR_HOST", "127.0.0.1"),
466
+ port=int(os.getenv("ROBOPARK_MOTOR_PORT", "8001")),
467
+ )