robopark 2.8.35 → 3.0.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.
- package/README.md +88 -63
- package/bin/robopark.js +7 -17
- package/conversation/elevenlabs_agent.py +1985 -0
- package/conversation/requirements.txt +3 -0
- package/conversation/supervisor_store.py +189 -0
- package/dist/kernel/config-schema.js +37 -0
- package/dist/kernel/types.js +7 -0
- package/dist/robopark/access.js +99 -0
- package/dist/robopark/add-robot.js +188 -0
- package/dist/robopark/agent-ctl.js +305 -0
- package/dist/robopark/auto-start.js +289 -0
- package/dist/robopark/conversation.js +505 -0
- package/dist/robopark/deployment-commands.js +47 -0
- package/dist/robopark/discovery.js +180 -0
- package/dist/robopark/doctor.js +175 -0
- package/dist/robopark/enroll.js +68 -0
- package/dist/robopark/llm-set.js +87 -0
- package/dist/robopark/motor-control.js +195 -0
- package/dist/robopark/preview-agent-launcher.js +77 -0
- package/dist/robopark/probe.js +138 -0
- package/dist/robopark/profile.js +69 -0
- package/dist/robopark/python-env.js +162 -0
- package/dist/robopark/robot-runtime.js +489 -0
- package/dist/robopark/scan.js +97 -0
- package/dist/robopark/screen-control.js +55 -0
- package/dist/robopark/secrets.js +41 -0
- package/dist/robopark/serve.js +285 -0
- package/dist/robopark/server-add.js +114 -0
- package/dist/robopark/setup-livekit.js +300 -0
- package/dist/robopark/setup.js +286 -0
- package/dist/robopark/standalone.js +466 -0
- package/dist/robopark/stop-all.js +141 -0
- package/dist/robopark/verify.js +192 -0
- package/dist/robopark/vision-agent-launcher.js +98 -0
- package/dist/robopark/vision-control.js +81 -0
- package/dist/robopark-cli.js +799 -0
- package/package.json +21 -5
- package/pi-client/_install_steps.sh +29 -29
- package/pi-client/client.py +61 -2
- package/pi-client/install.sh +40 -40
- package/pi-client/join_convo.sh +54 -54
- package/pi-client/livekit_bridge.py +16 -7
- package/pi-client/motor_bridge.py +6 -3
- package/scheduler/fleet_config.json +75 -0
- package/scheduler/main.py +4505 -135
- package/scheduler/media_lock.py +57 -0
- package/scheduler/preview_agent.py +1465 -87
- package/scheduler/production_config.json +139 -0
- package/scheduler/robot_supervisor.py +1705 -0
- package/scheduler/scripts/install-robot-supervisor-linux.sh +33 -0
- package/scheduler/scripts/install-robot-supervisor-windows.ps1 +49 -0
- package/scheduler/scripts/robopark-supervisor.service +20 -0
- package/scheduler/scripts/start-scheduler-local.ps1 +50 -0
- package/scheduler/supervisor.example.json +26 -0
- package/scheduler/vision_motion_trigger.py +101 -0
- package/screen/screen_runtime.py +75 -0
- package/vision/app_pi_clean.py +253 -16
- package/vision/audio_server_pi.py +19 -0
- package/vision/install.sh +34 -34
- package/vision/motor_server.py +224 -61
- package/vision/requirements_camera.txt +6 -0
- package/vision/requirements_motor.txt +4 -0
- package/vision/requirements_pi_unified.txt +1 -0
- package/vision/requirements_vision_agent.txt +19 -0
- package/vision/run.sh +244 -244
- package/vision/services/services.sh +12 -12
- package/scheduler/__pycache__/main.cpython-312.pyc +0 -0
- package/scheduler/__pycache__/preview_agent.cpython-312.pyc +0 -0
package/vision/motor_server.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from fastapi import FastAPI, HTTPException
|
|
1
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
2
3
|
from fastapi.middleware.cors import CORSMiddleware
|
|
3
4
|
from pydantic import BaseModel
|
|
4
5
|
from typing import Optional, List
|
|
@@ -6,20 +7,46 @@ import time
|
|
|
6
7
|
import threading
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
10
|
+
import hmac
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
try:
|
|
13
|
+
import lgpio
|
|
14
|
+
except ImportError:
|
|
15
|
+
lgpio = None
|
|
11
16
|
|
|
12
|
-
|
|
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.
|
|
13
37
|
app.add_middleware(
|
|
14
38
|
CORSMiddleware,
|
|
15
|
-
allow_origins=["
|
|
16
|
-
allow_credentials=
|
|
17
|
-
allow_methods=["
|
|
18
|
-
allow_headers=["
|
|
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"],
|
|
19
43
|
)
|
|
20
44
|
|
|
21
45
|
# Storage file
|
|
22
|
-
MOTORS_FILE =
|
|
46
|
+
MOTORS_FILE = os.getenv(
|
|
47
|
+
"ROBOPARK_MOTORS_FILE",
|
|
48
|
+
os.path.expanduser("~/.robopark/motors.json"),
|
|
49
|
+
)
|
|
23
50
|
|
|
24
51
|
# In-memory storage
|
|
25
52
|
motors = {}
|
|
@@ -28,6 +55,10 @@ remaining_seconds = 0
|
|
|
28
55
|
last_action = "None"
|
|
29
56
|
motor_thread = None
|
|
30
57
|
stop_flag = False
|
|
58
|
+
motor_error = None
|
|
59
|
+
gpio_handle = None
|
|
60
|
+
claimed_pins = set()
|
|
61
|
+
motor_lock = threading.Lock()
|
|
31
62
|
|
|
32
63
|
# Load motors from file on startup
|
|
33
64
|
def load_motors():
|
|
@@ -37,6 +68,14 @@ def load_motors():
|
|
|
37
68
|
if os.path.exists(MOTORS_FILE):
|
|
38
69
|
with open(MOTORS_FILE, 'r') as f:
|
|
39
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
|
+
}
|
|
40
79
|
print(f"[LOAD] Loaded {len(motors)} motors from {MOTORS_FILE}")
|
|
41
80
|
for name, gpio in motors.items():
|
|
42
81
|
print(f" - {name}: GPIO {gpio}")
|
|
@@ -49,8 +88,13 @@ def load_motors():
|
|
|
49
88
|
def save_motors():
|
|
50
89
|
"""Save motors to motors.json file"""
|
|
51
90
|
try:
|
|
52
|
-
|
|
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:
|
|
53
94
|
json.dump(motors, f, indent=2)
|
|
95
|
+
f.flush()
|
|
96
|
+
os.fsync(f.fileno())
|
|
97
|
+
os.replace(temporary, MOTORS_FILE)
|
|
54
98
|
print(f"[SAVE] Motors saved to {MOTORS_FILE}")
|
|
55
99
|
except Exception as e:
|
|
56
100
|
print(f"[ERROR] Failed to save motors: {e}")
|
|
@@ -62,52 +106,98 @@ load_motors()
|
|
|
62
106
|
class MotorConfig(BaseModel):
|
|
63
107
|
name: str
|
|
64
108
|
gpio: int
|
|
109
|
+
active_high: bool = True
|
|
65
110
|
|
|
66
111
|
class UpdateMotorConfig(BaseModel):
|
|
67
112
|
name: str
|
|
68
113
|
gpio: int
|
|
114
|
+
active_high: bool = True
|
|
69
115
|
|
|
70
116
|
class TriggerMotor(BaseModel):
|
|
71
117
|
motor_name: str
|
|
72
|
-
seconds:
|
|
118
|
+
seconds: float = 1.0
|
|
73
119
|
|
|
74
120
|
class StopMotors(BaseModel):
|
|
75
121
|
motor_name: Optional[str] = None
|
|
76
122
|
|
|
77
123
|
class TestConfig(BaseModel):
|
|
78
|
-
seconds_on:
|
|
79
|
-
seconds_pause:
|
|
124
|
+
seconds_on: float = 0.3
|
|
125
|
+
seconds_pause: float = 0.2
|
|
80
126
|
pins: Optional[List[int]] = None
|
|
81
127
|
|
|
82
128
|
# Helper Functions
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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()
|
|
104
192
|
|
|
105
193
|
# API Endpoints
|
|
106
194
|
@app.get("/")
|
|
107
195
|
async def root():
|
|
108
196
|
return {
|
|
109
197
|
"message": "Motor Control API",
|
|
110
|
-
"version": "1.
|
|
198
|
+
"version": "1.2.0",
|
|
199
|
+
"robot": ROBOT_NAME,
|
|
200
|
+
"authentication": "token" if MOTOR_API_TOKEN else "localhost-only",
|
|
111
201
|
"endpoints": ["/status", "/test", "/add-motor", "/trigger-motor", "/list-motors", "/stop-motors"]
|
|
112
202
|
}
|
|
113
203
|
|
|
@@ -119,17 +209,25 @@ async def get_status():
|
|
|
119
209
|
"active_motor": active_motor,
|
|
120
210
|
"remaining_seconds": remaining_seconds,
|
|
121
211
|
"last_action": last_action,
|
|
122
|
-
"
|
|
212
|
+
"error": motor_error,
|
|
213
|
+
"total_motors": len(motors),
|
|
214
|
+
"robot": ROBOT_NAME,
|
|
215
|
+
"gpio_available": lgpio is not None,
|
|
123
216
|
}
|
|
124
217
|
|
|
125
218
|
@app.post("/add-motor")
|
|
126
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")
|
|
127
222
|
if config.name in motors:
|
|
128
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")
|
|
129
226
|
|
|
130
227
|
motors[config.name] = {
|
|
131
228
|
"name": config.name,
|
|
132
|
-
"gpio": config.gpio
|
|
229
|
+
"gpio": config.gpio,
|
|
230
|
+
"active_high": config.active_high,
|
|
133
231
|
}
|
|
134
232
|
save_motors()
|
|
135
233
|
|
|
@@ -150,8 +248,22 @@ async def list_motors():
|
|
|
150
248
|
"motors": motor_list
|
|
151
249
|
}
|
|
152
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
|
+
|
|
153
263
|
@app.put("/update-motor/{motor_name}")
|
|
154
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")
|
|
155
267
|
if motor_name not in motors:
|
|
156
268
|
raise HTTPException(status_code=404, detail=f"Motor '{motor_name}' not found")
|
|
157
269
|
|
|
@@ -160,13 +272,16 @@ async def update_motor(motor_name: str, config: UpdateMotorConfig):
|
|
|
160
272
|
|
|
161
273
|
if config.name != motor_name and config.name in motors:
|
|
162
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")
|
|
163
277
|
|
|
164
278
|
if config.name != motor_name:
|
|
165
279
|
del motors[motor_name]
|
|
166
280
|
|
|
167
281
|
motors[config.name] = {
|
|
168
282
|
"name": config.name,
|
|
169
|
-
"gpio": config.gpio
|
|
283
|
+
"gpio": config.gpio,
|
|
284
|
+
"active_high": config.active_high,
|
|
170
285
|
}
|
|
171
286
|
save_motors()
|
|
172
287
|
|
|
@@ -199,17 +314,29 @@ async def delete_motor(motor_name: str):
|
|
|
199
314
|
|
|
200
315
|
@app.post("/trigger-motor")
|
|
201
316
|
async def trigger_motor(config: TriggerMotor):
|
|
202
|
-
global motor_thread, stop_flag
|
|
317
|
+
global motor_thread, stop_flag, active_motor, remaining_seconds, last_action, motor_error
|
|
203
318
|
|
|
204
319
|
if config.motor_name not in motors:
|
|
205
320
|
raise HTTPException(status_code=404, detail=f"Motor '{config.motor_name}' not found")
|
|
206
321
|
|
|
207
|
-
if
|
|
208
|
-
raise HTTPException(status_code=
|
|
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")
|
|
209
324
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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()
|
|
213
340
|
|
|
214
341
|
return {
|
|
215
342
|
"status": "success",
|
|
@@ -224,6 +351,7 @@ async def stop_motors(config: Optional[StopMotors] = None):
|
|
|
224
351
|
|
|
225
352
|
try:
|
|
226
353
|
if not active_motor:
|
|
354
|
+
_all_off()
|
|
227
355
|
return {
|
|
228
356
|
"status": "success",
|
|
229
357
|
"message": "No motors are currently running"
|
|
@@ -235,6 +363,7 @@ async def stop_motors(config: Optional[StopMotors] = None):
|
|
|
235
363
|
# Wait for thread to finish
|
|
236
364
|
if motor_thread and motor_thread.is_alive():
|
|
237
365
|
motor_thread.join(timeout=2)
|
|
366
|
+
_all_off()
|
|
238
367
|
|
|
239
368
|
return {
|
|
240
369
|
"status": "success",
|
|
@@ -253,12 +382,29 @@ async def stop_motors(config: Optional[StopMotors] = None):
|
|
|
253
382
|
|
|
254
383
|
@app.post("/test")
|
|
255
384
|
async def run_test(config: TestConfig):
|
|
256
|
-
global last_action
|
|
385
|
+
global last_action, active_motor, motor_error
|
|
257
386
|
|
|
258
|
-
if
|
|
259
|
-
raise HTTPException(status_code=
|
|
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")
|
|
260
391
|
|
|
261
|
-
|
|
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
|
|
262
408
|
|
|
263
409
|
print(f"[TEST] Running test sequence:")
|
|
264
410
|
print(f" - Seconds ON: {config.seconds_on}")
|
|
@@ -267,19 +413,32 @@ async def run_test(config: TestConfig):
|
|
|
267
413
|
|
|
268
414
|
last_action = f"Test sequence started"
|
|
269
415
|
|
|
270
|
-
# Simulate test sequence
|
|
271
416
|
def test_sequence():
|
|
272
|
-
global last_action
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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)
|
|
283
442
|
test_thread.start()
|
|
284
443
|
|
|
285
444
|
return {
|
|
@@ -297,8 +456,12 @@ if __name__ == "__main__":
|
|
|
297
456
|
print("=" * 60)
|
|
298
457
|
print("Motor Control API Server")
|
|
299
458
|
print("=" * 60)
|
|
300
|
-
print("Starting server on http://
|
|
459
|
+
print("Starting server on http://127.0.0.1:8001")
|
|
301
460
|
print("API Documentation: http://localhost:8001/docs")
|
|
302
461
|
print("ReDoc: http://localhost:8001/redoc")
|
|
303
462
|
print("=" * 60)
|
|
304
|
-
uvicorn.run(
|
|
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
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Runtime requirements for the packaged RoboPark vision-agent.
|
|
2
|
+
#
|
|
3
|
+
# This intentionally excludes the legacy optional YOLO/GPIO stack from
|
|
4
|
+
# requirements_pi_unified.txt. The packaged agent launches app_pi_clean.py and
|
|
5
|
+
# audio_server_pi.py only. Raspberry Pi OpenCV/PyAudio/Picamera2 are supplied
|
|
6
|
+
# by apt and exposed to ~/.robopark/venv through --system-site-packages.
|
|
7
|
+
Flask==3.0.0
|
|
8
|
+
Flask-CORS==4.0.0
|
|
9
|
+
requests>=2.31
|
|
10
|
+
fastapi>=0.115
|
|
11
|
+
uvicorn[standard]>=0.30
|
|
12
|
+
python-multipart>=0.0.12
|
|
13
|
+
sounddevice>=0.4.6
|
|
14
|
+
soundfile>=0.12.1
|
|
15
|
+
groq>=0.11
|
|
16
|
+
lgpio==0.2.2.0; platform_system == 'Linux'
|
|
17
|
+
# Python 3.13 has no compatible NumPy 1.24 wheel. On older Pi images, use the
|
|
18
|
+
# distro NumPy provided through system-site-packages instead.
|
|
19
|
+
numpy>=2.1; python_version >= '3.13'
|