infinicode 2.8.97 → 2.8.98
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/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +471 -54
- package/dist/robopark/python-env.d.ts +1 -0
- package/dist/robopark/python-env.js +3 -0
- package/dist/robopark/robot-runtime.js +1 -1
- package/dist/robopark/vision-agent-launcher.js +19 -3
- package/docs/ROBOPARK_PRODUCTION_MEMORY.md +19 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +278 -22
- package/packages/robopark/scheduler/robot_supervisor.py +62 -5
- package/packages/robopark/vision/motor_server.py +204 -88
- package/packages/robopark/vision/requirements_vision_agent.txt +1 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
from fastapi import FastAPI, HTTPException
|
|
2
2
|
from fastapi.middleware.cors import CORSMiddleware
|
|
3
|
-
from pydantic import BaseModel
|
|
3
|
+
from pydantic import BaseModel
|
|
4
4
|
from typing import Optional, List
|
|
5
5
|
import time
|
|
6
6
|
import threading
|
|
7
7
|
import json
|
|
8
|
-
import os
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import lgpio
|
|
12
|
+
except ImportError:
|
|
13
|
+
lgpio = None
|
|
9
14
|
|
|
10
15
|
app = FastAPI(title="Motor Control API", version="1.0.0")
|
|
11
16
|
|
|
@@ -27,7 +32,11 @@ active_motor = None
|
|
|
27
32
|
remaining_seconds = 0
|
|
28
33
|
last_action = "None"
|
|
29
34
|
motor_thread = None
|
|
30
|
-
stop_flag = False
|
|
35
|
+
stop_flag = False
|
|
36
|
+
motor_error = None
|
|
37
|
+
gpio_handle = None
|
|
38
|
+
claimed_pins = set()
|
|
39
|
+
motor_lock = threading.Lock()
|
|
31
40
|
|
|
32
41
|
# Load motors from file on startup
|
|
33
42
|
def load_motors():
|
|
@@ -35,8 +44,16 @@ def load_motors():
|
|
|
35
44
|
global motors
|
|
36
45
|
try:
|
|
37
46
|
if os.path.exists(MOTORS_FILE):
|
|
38
|
-
with open(MOTORS_FILE, 'r') as f:
|
|
39
|
-
motors = json.load(f)
|
|
47
|
+
with open(MOTORS_FILE, 'r') as f:
|
|
48
|
+
motors = json.load(f)
|
|
49
|
+
motors = {
|
|
50
|
+
str(name): ({"name": str(name), "gpio": int(value), "active_high": True}
|
|
51
|
+
if not isinstance(value, dict) else {
|
|
52
|
+
"name": str(value.get("name") or name), "gpio": int(value["gpio"]),
|
|
53
|
+
"active_high": bool(value.get("active_high", True)),
|
|
54
|
+
})
|
|
55
|
+
for name, value in motors.items()
|
|
56
|
+
}
|
|
40
57
|
print(f"[LOAD] Loaded {len(motors)} motors from {MOTORS_FILE}")
|
|
41
58
|
for name, gpio in motors.items():
|
|
42
59
|
print(f" - {name}: GPIO {gpio}")
|
|
@@ -59,48 +76,92 @@ def save_motors():
|
|
|
59
76
|
load_motors()
|
|
60
77
|
|
|
61
78
|
# Request Models
|
|
62
|
-
class MotorConfig(BaseModel):
|
|
63
|
-
name: str
|
|
64
|
-
gpio: int
|
|
79
|
+
class MotorConfig(BaseModel):
|
|
80
|
+
name: str
|
|
81
|
+
gpio: int
|
|
82
|
+
active_high: bool = True
|
|
65
83
|
|
|
66
|
-
class UpdateMotorConfig(BaseModel):
|
|
67
|
-
name: str
|
|
68
|
-
gpio: int
|
|
84
|
+
class UpdateMotorConfig(BaseModel):
|
|
85
|
+
name: str
|
|
86
|
+
gpio: int
|
|
87
|
+
active_high: bool = True
|
|
69
88
|
|
|
70
|
-
class TriggerMotor(BaseModel):
|
|
71
|
-
motor_name: str
|
|
72
|
-
seconds:
|
|
89
|
+
class TriggerMotor(BaseModel):
|
|
90
|
+
motor_name: str
|
|
91
|
+
seconds: float = 1.0
|
|
73
92
|
|
|
74
93
|
class StopMotors(BaseModel):
|
|
75
94
|
motor_name: Optional[str] = None
|
|
76
95
|
|
|
77
|
-
class TestConfig(BaseModel):
|
|
78
|
-
seconds_on:
|
|
79
|
-
seconds_pause:
|
|
96
|
+
class TestConfig(BaseModel):
|
|
97
|
+
seconds_on: float = 0.3
|
|
98
|
+
seconds_pause: float = 0.2
|
|
80
99
|
pins: Optional[List[int]] = None
|
|
81
100
|
|
|
82
101
|
# Helper Functions
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
def _motor_value(motor: dict, active: bool) -> int:
|
|
103
|
+
return int(active == bool(motor.get("active_high", True)))
|
|
104
|
+
|
|
105
|
+
def _claim_motor(motor: dict) -> None:
|
|
106
|
+
global gpio_handle
|
|
107
|
+
pin = int(motor["gpio"])
|
|
108
|
+
if pin < 2 or pin > 27:
|
|
109
|
+
raise ValueError(f"BCM GPIO {pin} is outside 2..27")
|
|
110
|
+
if lgpio is None:
|
|
111
|
+
raise RuntimeError("lgpio is not installed; refusing simulated production actuation")
|
|
112
|
+
if gpio_handle is None:
|
|
113
|
+
gpio_handle = lgpio.gpiochip_open(0)
|
|
114
|
+
if pin not in claimed_pins:
|
|
115
|
+
lgpio.gpio_claim_output(gpio_handle, pin, _motor_value(motor, False))
|
|
116
|
+
claimed_pins.add(pin)
|
|
117
|
+
|
|
118
|
+
def _write_motor(motor: dict, active: bool) -> None:
|
|
119
|
+
_claim_motor(motor)
|
|
120
|
+
lgpio.gpio_write(gpio_handle, int(motor["gpio"]), _motor_value(motor, active))
|
|
121
|
+
|
|
122
|
+
def _all_off() -> None:
|
|
123
|
+
for motor in motors.values():
|
|
124
|
+
try:
|
|
125
|
+
if isinstance(motor, dict):
|
|
126
|
+
_write_motor(motor, False)
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
print(f"[GPIO] failed to de-energize {motor}: {exc}")
|
|
129
|
+
|
|
130
|
+
def run_motor_thread(motor_name: str, duration: float):
|
|
131
|
+
global active_motor, remaining_seconds, last_action, stop_flag, motor_error
|
|
132
|
+
|
|
133
|
+
motor = motors[motor_name]
|
|
134
|
+
started = time.monotonic()
|
|
135
|
+
print(f"[MOTOR] Starting {motor_name} for {duration:.3f} seconds")
|
|
136
|
+
try:
|
|
137
|
+
_write_motor(motor, True)
|
|
138
|
+
while not stop_flag and time.monotonic() - started < duration:
|
|
139
|
+
remaining_seconds = max(0, duration - (time.monotonic() - started))
|
|
140
|
+
time.sleep(min(0.05, remaining_seconds or 0.01))
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
motor_error = f"{type(exc).__name__}: {exc}"
|
|
143
|
+
last_action = f"Failed {motor_name}: {motor_error}"
|
|
144
|
+
print(f"[ERROR] Motor {motor_name} failed: {motor_error}")
|
|
145
|
+
finally:
|
|
146
|
+
try:
|
|
147
|
+
_write_motor(motor, False)
|
|
148
|
+
finally:
|
|
149
|
+
with motor_lock:
|
|
150
|
+
active_motor = None
|
|
151
|
+
remaining_seconds = 0
|
|
152
|
+
stop_flag = False
|
|
153
|
+
print(f"[MOTOR] {motor_name} completed and GPIO is OFF")
|
|
154
|
+
|
|
155
|
+
@app.on_event("shutdown")
|
|
156
|
+
def shutdown_gpio() -> None:
|
|
157
|
+
global gpio_handle
|
|
158
|
+
_all_off()
|
|
159
|
+
if lgpio is not None and gpio_handle is not None:
|
|
160
|
+
try:
|
|
161
|
+
lgpio.gpiochip_close(gpio_handle)
|
|
162
|
+
finally:
|
|
163
|
+
gpio_handle = None
|
|
164
|
+
claimed_pins.clear()
|
|
104
165
|
|
|
105
166
|
# API Endpoints
|
|
106
167
|
@app.get("/")
|
|
@@ -118,18 +179,24 @@ async def get_status():
|
|
|
118
179
|
"status": status,
|
|
119
180
|
"active_motor": active_motor,
|
|
120
181
|
"remaining_seconds": remaining_seconds,
|
|
121
|
-
"last_action": last_action,
|
|
182
|
+
"last_action": last_action,
|
|
183
|
+
"error": motor_error,
|
|
122
184
|
"total_motors": len(motors)
|
|
123
185
|
}
|
|
124
186
|
|
|
125
187
|
@app.post("/add-motor")
|
|
126
|
-
async def add_motor(config: MotorConfig):
|
|
127
|
-
if config.
|
|
128
|
-
raise HTTPException(status_code=
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
188
|
+
async def add_motor(config: MotorConfig):
|
|
189
|
+
if config.gpio < 2 or config.gpio > 27:
|
|
190
|
+
raise HTTPException(status_code=422, detail="BCM GPIO must be in range 2..27")
|
|
191
|
+
if config.name in motors:
|
|
192
|
+
raise HTTPException(status_code=400, detail=f"Motor '{config.name}' already exists")
|
|
193
|
+
if any(int(item["gpio"]) == config.gpio for item in motors.values()):
|
|
194
|
+
raise HTTPException(status_code=409, detail=f"BCM GPIO {config.gpio} is already registered")
|
|
195
|
+
|
|
196
|
+
motors[config.name] = {
|
|
197
|
+
"name": config.name,
|
|
198
|
+
"gpio": config.gpio,
|
|
199
|
+
"active_high": config.active_high,
|
|
133
200
|
}
|
|
134
201
|
save_motors()
|
|
135
202
|
|
|
@@ -151,22 +218,27 @@ async def list_motors():
|
|
|
151
218
|
}
|
|
152
219
|
|
|
153
220
|
@app.put("/update-motor/{motor_name}")
|
|
154
|
-
async def update_motor(motor_name: str, config: UpdateMotorConfig):
|
|
221
|
+
async def update_motor(motor_name: str, config: UpdateMotorConfig):
|
|
222
|
+
if config.gpio < 2 or config.gpio > 27:
|
|
223
|
+
raise HTTPException(status_code=422, detail="BCM GPIO must be in range 2..27")
|
|
155
224
|
if motor_name not in motors:
|
|
156
225
|
raise HTTPException(status_code=404, detail=f"Motor '{motor_name}' not found")
|
|
157
226
|
|
|
158
227
|
if active_motor == motor_name:
|
|
159
228
|
raise HTTPException(status_code=409, detail=f"Cannot update motor '{motor_name}' while it is running")
|
|
160
229
|
|
|
161
|
-
if config.name != motor_name and config.name in motors:
|
|
162
|
-
raise HTTPException(status_code=400, detail=f"Motor name '{config.name}' already exists")
|
|
230
|
+
if config.name != motor_name and config.name in motors:
|
|
231
|
+
raise HTTPException(status_code=400, detail=f"Motor name '{config.name}' already exists")
|
|
232
|
+
if any(name != motor_name and int(item["gpio"]) == config.gpio for name, item in motors.items()):
|
|
233
|
+
raise HTTPException(status_code=409, detail=f"BCM GPIO {config.gpio} is already registered")
|
|
163
234
|
|
|
164
235
|
if config.name != motor_name:
|
|
165
236
|
del motors[motor_name]
|
|
166
237
|
|
|
167
|
-
motors[config.name] = {
|
|
168
|
-
"name": config.name,
|
|
169
|
-
"gpio": config.gpio
|
|
238
|
+
motors[config.name] = {
|
|
239
|
+
"name": config.name,
|
|
240
|
+
"gpio": config.gpio,
|
|
241
|
+
"active_high": config.active_high,
|
|
170
242
|
}
|
|
171
243
|
save_motors()
|
|
172
244
|
|
|
@@ -198,18 +270,30 @@ async def delete_motor(motor_name: str):
|
|
|
198
270
|
}
|
|
199
271
|
|
|
200
272
|
@app.post("/trigger-motor")
|
|
201
|
-
async def trigger_motor(config: TriggerMotor):
|
|
202
|
-
global motor_thread, stop_flag
|
|
273
|
+
async def trigger_motor(config: TriggerMotor):
|
|
274
|
+
global motor_thread, stop_flag, active_motor, remaining_seconds, last_action, motor_error
|
|
203
275
|
|
|
204
276
|
if config.motor_name not in motors:
|
|
205
277
|
raise HTTPException(status_code=404, detail=f"Motor '{config.motor_name}' not found")
|
|
206
278
|
|
|
207
|
-
if
|
|
208
|
-
raise HTTPException(status_code=
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
279
|
+
if config.seconds < 0.05 or config.seconds > 10.0:
|
|
280
|
+
raise HTTPException(status_code=422, detail="Pulse duration must be 0.05..10 seconds")
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
_claim_motor(motors[config.motor_name])
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
raise HTTPException(status_code=503, detail=f"GPIO is unavailable: {exc}") from exc
|
|
286
|
+
|
|
287
|
+
with motor_lock:
|
|
288
|
+
if active_motor:
|
|
289
|
+
raise HTTPException(status_code=409, detail=f"Motor '{active_motor}' is already running")
|
|
290
|
+
stop_flag = False
|
|
291
|
+
motor_error = None
|
|
292
|
+
active_motor = config.motor_name
|
|
293
|
+
remaining_seconds = config.seconds
|
|
294
|
+
last_action = f"Triggered {config.motor_name}"
|
|
295
|
+
motor_thread = threading.Thread(target=run_motor_thread, args=(config.motor_name, config.seconds), daemon=True)
|
|
296
|
+
motor_thread.start()
|
|
213
297
|
|
|
214
298
|
return {
|
|
215
299
|
"status": "success",
|
|
@@ -223,18 +307,20 @@ async def stop_motors(config: Optional[StopMotors] = None):
|
|
|
223
307
|
global stop_flag, last_action
|
|
224
308
|
|
|
225
309
|
try:
|
|
226
|
-
if not active_motor:
|
|
227
|
-
|
|
310
|
+
if not active_motor:
|
|
311
|
+
_all_off()
|
|
312
|
+
return {
|
|
228
313
|
"status": "success",
|
|
229
314
|
"message": "No motors are currently running"
|
|
230
315
|
}
|
|
231
316
|
|
|
232
|
-
stop_flag = True
|
|
233
|
-
last_action = f"Stopped {active_motor}"
|
|
317
|
+
stop_flag = True
|
|
318
|
+
last_action = f"Stopped {active_motor}"
|
|
234
319
|
|
|
235
320
|
# Wait for thread to finish
|
|
236
|
-
if motor_thread and motor_thread.is_alive():
|
|
237
|
-
motor_thread.join(timeout=2)
|
|
321
|
+
if motor_thread and motor_thread.is_alive():
|
|
322
|
+
motor_thread.join(timeout=2)
|
|
323
|
+
_all_off()
|
|
238
324
|
|
|
239
325
|
return {
|
|
240
326
|
"status": "success",
|
|
@@ -252,13 +338,30 @@ async def stop_motors(config: Optional[StopMotors] = None):
|
|
|
252
338
|
}
|
|
253
339
|
|
|
254
340
|
@app.post("/test")
|
|
255
|
-
async def run_test(config: TestConfig):
|
|
256
|
-
global last_action
|
|
257
|
-
|
|
258
|
-
if
|
|
259
|
-
raise HTTPException(status_code=
|
|
260
|
-
|
|
261
|
-
|
|
341
|
+
async def run_test(config: TestConfig):
|
|
342
|
+
global last_action, active_motor, motor_error
|
|
343
|
+
|
|
344
|
+
if config.seconds_on < 0.05 or config.seconds_on > 10.0:
|
|
345
|
+
raise HTTPException(status_code=422, detail="Test pulse duration must be 0.05..10 seconds")
|
|
346
|
+
if config.seconds_pause < 0 or config.seconds_pause > 10.0:
|
|
347
|
+
raise HTTPException(status_code=422, detail="Test pause must be 0..10 seconds")
|
|
348
|
+
|
|
349
|
+
registered_pins = {int(m["gpio"]) for m in motors.values() if isinstance(m, dict)}
|
|
350
|
+
pins = config.pins if config.pins else sorted(registered_pins)
|
|
351
|
+
if not pins or any(pin not in registered_pins for pin in pins):
|
|
352
|
+
raise HTTPException(status_code=422, detail="Relay tests are limited to registered GPIO pins")
|
|
353
|
+
try:
|
|
354
|
+
for motor in motors.values():
|
|
355
|
+
if int(motor["gpio"]) in pins:
|
|
356
|
+
_claim_motor(motor)
|
|
357
|
+
except Exception as exc:
|
|
358
|
+
raise HTTPException(status_code=503, detail=f"GPIO is unavailable: {exc}") from exc
|
|
359
|
+
|
|
360
|
+
with motor_lock:
|
|
361
|
+
if active_motor:
|
|
362
|
+
raise HTTPException(status_code=409, detail="A motor is already running")
|
|
363
|
+
active_motor = "relay-test"
|
|
364
|
+
motor_error = None
|
|
262
365
|
|
|
263
366
|
print(f"[TEST] Running test sequence:")
|
|
264
367
|
print(f" - Seconds ON: {config.seconds_on}")
|
|
@@ -267,19 +370,32 @@ async def run_test(config: TestConfig):
|
|
|
267
370
|
|
|
268
371
|
last_action = f"Test sequence started"
|
|
269
372
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
373
|
+
def test_sequence():
|
|
374
|
+
global last_action, active_motor, motor_error
|
|
375
|
+
try:
|
|
376
|
+
for pin in pins:
|
|
377
|
+
last_action = f"test:gpio:{pin}"
|
|
378
|
+
motor = next(m for m in motors.values() if int(m["gpio"]) == pin)
|
|
379
|
+
print(f">>> TESTING GPIO {pin}")
|
|
380
|
+
try:
|
|
381
|
+
_write_motor(motor, True)
|
|
382
|
+
time.sleep(config.seconds_on)
|
|
383
|
+
finally:
|
|
384
|
+
_write_motor(motor, False)
|
|
385
|
+
print(f"[TEST] Pin {pin} OFF, pausing {config.seconds_pause}s")
|
|
386
|
+
time.sleep(config.seconds_pause)
|
|
387
|
+
last_action = "Test sequence completed"
|
|
388
|
+
print("[TEST] Test sequence completed")
|
|
389
|
+
except Exception as exc:
|
|
390
|
+
motor_error = f"{type(exc).__name__}: {exc}"
|
|
391
|
+
last_action = f"Relay test failed: {motor_error}"
|
|
392
|
+
print(f"[ERROR] Relay test failed: {motor_error}")
|
|
393
|
+
finally:
|
|
394
|
+
_all_off()
|
|
395
|
+
with motor_lock:
|
|
396
|
+
active_motor = None
|
|
397
|
+
|
|
398
|
+
test_thread = threading.Thread(target=test_sequence, daemon=True)
|
|
283
399
|
test_thread.start()
|
|
284
400
|
|
|
285
401
|
return {
|
|
@@ -297,8 +413,8 @@ if __name__ == "__main__":
|
|
|
297
413
|
print("=" * 60)
|
|
298
414
|
print("Motor Control API Server")
|
|
299
415
|
print("=" * 60)
|
|
300
|
-
print("Starting server on http://
|
|
416
|
+
print("Starting server on http://127.0.0.1:8001")
|
|
301
417
|
print("API Documentation: http://localhost:8001/docs")
|
|
302
418
|
print("ReDoc: http://localhost:8001/redoc")
|
|
303
419
|
print("=" * 60)
|
|
304
|
-
uvicorn.run(app, host="
|
|
420
|
+
uvicorn.run(app, host=os.getenv("ROBOPARK_MOTOR_HOST", "127.0.0.1"), port=8001)
|
|
@@ -13,6 +13,7 @@ python-multipart>=0.0.12
|
|
|
13
13
|
sounddevice>=0.4.6
|
|
14
14
|
soundfile>=0.12.1
|
|
15
15
|
groq>=0.11
|
|
16
|
+
lgpio==0.2.2.0; platform_system == 'Linux'
|
|
16
17
|
# Python 3.13 has no compatible NumPy 1.24 wheel. On older Pi images, use the
|
|
17
18
|
# distro NumPy provided through system-site-packages instead.
|
|
18
19
|
numpy>=2.1; python_version >= '3.13'
|