infinicode 2.8.13 → 2.8.16
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 +32 -1
- package/dist/robopark/auto-start.d.ts +1 -1
- package/dist/robopark/preview-agent-launcher.d.ts +3 -0
- package/dist/robopark/preview-agent-launcher.js +8 -0
- package/dist/robopark/python-env.d.ts +10 -4
- package/dist/robopark/python-env.js +28 -16
- package/dist/robopark/setup.d.ts +1 -0
- package/dist/robopark/setup.js +22 -0
- package/dist/robopark/vision-agent-launcher.d.ts +7 -0
- package/dist/robopark/vision-agent-launcher.js +55 -0
- package/dist/robopark-cli.js +16 -0
- package/package.json +3 -1
- package/packages/robopark/pi-client/README.md +17 -0
- package/packages/robopark/pi-client/_install_steps.sh +29 -0
- package/packages/robopark/pi-client/client.py +305 -0
- package/packages/robopark/pi-client/install.sh +41 -0
- package/packages/robopark/pi-client/join_convo.sh +55 -0
- package/packages/robopark/pi-client/livekit_bridge.py +289 -0
- package/packages/robopark/pi-client/motor_bridge.py +82 -0
- package/packages/robopark/pi-client/pi_ui.py +375 -0
- package/packages/robopark/pi-client/requirements.txt +10 -0
- package/packages/robopark/pi-client/robopark-pi-client.service +26 -0
- package/packages/robopark/pi-client/robopark-pi-ui.service +24 -0
- package/packages/robopark/pi-client/smoke_bridge.py +15 -0
- package/packages/robopark/pi-client/stub_motor_server.py +46 -0
- package/packages/robopark/scheduler/main.py +45 -9
- package/packages/robopark/scheduler/preview_agent.py +132 -3
- package/packages/robopark/vision/.env.example +7 -0
- package/packages/robopark/vision/README.md +66 -0
- package/packages/robopark/vision/app_pi_clean.py +326 -0
- package/packages/robopark/vision/audio_server_pi.py +751 -0
- package/packages/robopark/vision/install.sh +34 -0
- package/packages/robopark/vision/motor_server.py +304 -0
- package/packages/robopark/vision/requirements-demo.txt +12 -0
- package/packages/robopark/vision/requirements_pi_unified.txt +101 -0
- package/packages/robopark/vision/run.sh +244 -0
- package/packages/robopark/vision/services/services.sh +12 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
VENV_DIR="$ROOT_DIR/venv"
|
|
6
|
+
REQ_FILE="$ROOT_DIR/requirements_pi_unified.txt"
|
|
7
|
+
|
|
8
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
9
|
+
echo "python3 not found"
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
if [ ! -f "$REQ_FILE" ]; then
|
|
14
|
+
echo "Missing $REQ_FILE"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
echo "System packages (run once):"
|
|
19
|
+
echo "sudo apt-get update"
|
|
20
|
+
echo "sudo apt-get install -y python3-opencv python3-numpy python3-pil libatlas-base-dev libopenblas-dev libhdf5-dev libhdf5-serial-dev libhdf5-103 libqt5gui5 libqt5widgets5 libqt5test5 portaudio19-dev libsndfile1 alsa-utils bluetooth bluez bluez-tools pulseaudio pulseaudio-module-bluetooth python3-pyaudio"
|
|
21
|
+
echo ""
|
|
22
|
+
|
|
23
|
+
echo "Creating venv: $VENV_DIR"
|
|
24
|
+
if [ ! -d "$VENV_DIR" ]; then
|
|
25
|
+
python3 -m venv "$VENV_DIR" --system-site-packages
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
source "$VENV_DIR/bin/activate"
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
python -m pip install -r "$REQ_FILE"
|
|
31
|
+
|
|
32
|
+
echo ""
|
|
33
|
+
echo "Done"
|
|
34
|
+
echo "Next: ./run.sh start-term"
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
from fastapi import FastAPI, HTTPException
|
|
2
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
app = FastAPI(title="Motor Control API", version="1.0.0")
|
|
11
|
+
|
|
12
|
+
# CORS Configuration - Allow all origins for cross-origin requests
|
|
13
|
+
app.add_middleware(
|
|
14
|
+
CORSMiddleware,
|
|
15
|
+
allow_origins=["*"], # Allows all origins
|
|
16
|
+
allow_credentials=True,
|
|
17
|
+
allow_methods=["*"], # Allows all methods (GET, POST, PUT, DELETE, OPTIONS)
|
|
18
|
+
allow_headers=["*"], # Allows all headers
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Storage file
|
|
22
|
+
MOTORS_FILE = "motors.json"
|
|
23
|
+
|
|
24
|
+
# In-memory storage
|
|
25
|
+
motors = {}
|
|
26
|
+
active_motor = None
|
|
27
|
+
remaining_seconds = 0
|
|
28
|
+
last_action = "None"
|
|
29
|
+
motor_thread = None
|
|
30
|
+
stop_flag = False
|
|
31
|
+
|
|
32
|
+
# Load motors from file on startup
|
|
33
|
+
def load_motors():
|
|
34
|
+
"""Load motors from motors.json file"""
|
|
35
|
+
global motors
|
|
36
|
+
try:
|
|
37
|
+
if os.path.exists(MOTORS_FILE):
|
|
38
|
+
with open(MOTORS_FILE, 'r') as f:
|
|
39
|
+
motors = json.load(f)
|
|
40
|
+
print(f"[LOAD] Loaded {len(motors)} motors from {MOTORS_FILE}")
|
|
41
|
+
for name, gpio in motors.items():
|
|
42
|
+
print(f" - {name}: GPIO {gpio}")
|
|
43
|
+
else:
|
|
44
|
+
print(f"[LOAD] No {MOTORS_FILE} found, starting with empty motors")
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"[ERROR] Failed to load motors: {e}")
|
|
47
|
+
motors = {}
|
|
48
|
+
|
|
49
|
+
def save_motors():
|
|
50
|
+
"""Save motors to motors.json file"""
|
|
51
|
+
try:
|
|
52
|
+
with open(MOTORS_FILE, 'w') as f:
|
|
53
|
+
json.dump(motors, f, indent=2)
|
|
54
|
+
print(f"[SAVE] Motors saved to {MOTORS_FILE}")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print(f"[ERROR] Failed to save motors: {e}")
|
|
57
|
+
|
|
58
|
+
# Load motors on startup
|
|
59
|
+
load_motors()
|
|
60
|
+
|
|
61
|
+
# Request Models
|
|
62
|
+
class MotorConfig(BaseModel):
|
|
63
|
+
name: str
|
|
64
|
+
gpio: int
|
|
65
|
+
|
|
66
|
+
class UpdateMotorConfig(BaseModel):
|
|
67
|
+
name: str
|
|
68
|
+
gpio: int
|
|
69
|
+
|
|
70
|
+
class TriggerMotor(BaseModel):
|
|
71
|
+
motor_name: str
|
|
72
|
+
seconds: int = 5
|
|
73
|
+
|
|
74
|
+
class StopMotors(BaseModel):
|
|
75
|
+
motor_name: Optional[str] = None
|
|
76
|
+
|
|
77
|
+
class TestConfig(BaseModel):
|
|
78
|
+
seconds_on: int = 2
|
|
79
|
+
seconds_pause: int = 1
|
|
80
|
+
pins: Optional[List[int]] = None
|
|
81
|
+
|
|
82
|
+
# Helper Functions
|
|
83
|
+
def run_motor_thread(motor_name: str, duration: int):
|
|
84
|
+
global active_motor, remaining_seconds, last_action, stop_flag
|
|
85
|
+
|
|
86
|
+
active_motor = motor_name
|
|
87
|
+
remaining_seconds = duration
|
|
88
|
+
last_action = f"Triggered {motor_name}"
|
|
89
|
+
|
|
90
|
+
print(f"[MOTOR] Starting {motor_name} for {duration} seconds")
|
|
91
|
+
|
|
92
|
+
# Simulate motor running
|
|
93
|
+
for i in range(duration):
|
|
94
|
+
if stop_flag:
|
|
95
|
+
print(f"[MOTOR] {motor_name} stopped by user")
|
|
96
|
+
break
|
|
97
|
+
remaining_seconds = duration - i
|
|
98
|
+
time.sleep(1)
|
|
99
|
+
|
|
100
|
+
active_motor = None
|
|
101
|
+
remaining_seconds = 0
|
|
102
|
+
stop_flag = False
|
|
103
|
+
print(f"[MOTOR] {motor_name} completed")
|
|
104
|
+
|
|
105
|
+
# API Endpoints
|
|
106
|
+
@app.get("/")
|
|
107
|
+
async def root():
|
|
108
|
+
return {
|
|
109
|
+
"message": "Motor Control API",
|
|
110
|
+
"version": "1.0.0",
|
|
111
|
+
"endpoints": ["/status", "/test", "/add-motor", "/trigger-motor", "/list-motors", "/stop-motors"]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@app.get("/status")
|
|
115
|
+
async def get_status():
|
|
116
|
+
status = "running" if active_motor else "idle"
|
|
117
|
+
return {
|
|
118
|
+
"status": status,
|
|
119
|
+
"active_motor": active_motor,
|
|
120
|
+
"remaining_seconds": remaining_seconds,
|
|
121
|
+
"last_action": last_action,
|
|
122
|
+
"total_motors": len(motors)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@app.post("/add-motor")
|
|
126
|
+
async def add_motor(config: MotorConfig):
|
|
127
|
+
if config.name in motors:
|
|
128
|
+
raise HTTPException(status_code=400, detail=f"Motor '{config.name}' already exists")
|
|
129
|
+
|
|
130
|
+
motors[config.name] = {
|
|
131
|
+
"name": config.name,
|
|
132
|
+
"gpio": config.gpio
|
|
133
|
+
}
|
|
134
|
+
save_motors()
|
|
135
|
+
|
|
136
|
+
print(f"[MOTOR] Added motor: {config.name} on GPIO {config.gpio}")
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"status": "success",
|
|
140
|
+
"message": f"Motor '{config.name}' added successfully",
|
|
141
|
+
"motor": motors[config.name]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@app.get("/list-motors")
|
|
145
|
+
async def list_motors():
|
|
146
|
+
motor_list = list(motors.values())
|
|
147
|
+
return {
|
|
148
|
+
"status": "success",
|
|
149
|
+
"count": len(motor_list),
|
|
150
|
+
"motors": motor_list
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@app.put("/update-motor/{motor_name}")
|
|
154
|
+
async def update_motor(motor_name: str, config: UpdateMotorConfig):
|
|
155
|
+
if motor_name not in motors:
|
|
156
|
+
raise HTTPException(status_code=404, detail=f"Motor '{motor_name}' not found")
|
|
157
|
+
|
|
158
|
+
if active_motor == motor_name:
|
|
159
|
+
raise HTTPException(status_code=409, detail=f"Cannot update motor '{motor_name}' while it is running")
|
|
160
|
+
|
|
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")
|
|
163
|
+
|
|
164
|
+
if config.name != motor_name:
|
|
165
|
+
del motors[motor_name]
|
|
166
|
+
|
|
167
|
+
motors[config.name] = {
|
|
168
|
+
"name": config.name,
|
|
169
|
+
"gpio": config.gpio
|
|
170
|
+
}
|
|
171
|
+
save_motors()
|
|
172
|
+
|
|
173
|
+
print(f"[MOTOR] Updated motor: {motor_name} -> {config.name} on GPIO {config.gpio}")
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
"status": "success",
|
|
177
|
+
"message": f"Motor updated successfully",
|
|
178
|
+
"motor": motors[config.name]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@app.delete("/delete-motor/{motor_name}")
|
|
182
|
+
async def delete_motor(motor_name: str):
|
|
183
|
+
if motor_name not in motors:
|
|
184
|
+
raise HTTPException(status_code=404, detail=f"Motor '{motor_name}' not found")
|
|
185
|
+
|
|
186
|
+
if active_motor == motor_name:
|
|
187
|
+
raise HTTPException(status_code=409, detail=f"Cannot delete motor '{motor_name}' while it is running")
|
|
188
|
+
|
|
189
|
+
deleted_motor = motors.pop(motor_name)
|
|
190
|
+
save_motors()
|
|
191
|
+
|
|
192
|
+
print(f"[MOTOR] Deleted motor: {motor_name}")
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"status": "success",
|
|
196
|
+
"message": f"Motor '{motor_name}' deleted successfully",
|
|
197
|
+
"motor": deleted_motor
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@app.post("/trigger-motor")
|
|
201
|
+
async def trigger_motor(config: TriggerMotor):
|
|
202
|
+
global motor_thread, stop_flag
|
|
203
|
+
|
|
204
|
+
if config.motor_name not in motors:
|
|
205
|
+
raise HTTPException(status_code=404, detail=f"Motor '{config.motor_name}' not found")
|
|
206
|
+
|
|
207
|
+
if active_motor:
|
|
208
|
+
raise HTTPException(status_code=409, detail=f"Motor '{active_motor}' is already running")
|
|
209
|
+
|
|
210
|
+
stop_flag = False
|
|
211
|
+
motor_thread = threading.Thread(target=run_motor_thread, args=(config.motor_name, config.seconds))
|
|
212
|
+
motor_thread.start()
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
"status": "success",
|
|
216
|
+
"message": f"Motor '{config.motor_name}' triggered for {config.seconds} seconds",
|
|
217
|
+
"motor_name": config.motor_name,
|
|
218
|
+
"duration": config.seconds
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@app.post("/stop-motors")
|
|
222
|
+
async def stop_motors(config: Optional[StopMotors] = None):
|
|
223
|
+
global stop_flag, last_action
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
if not active_motor:
|
|
227
|
+
return {
|
|
228
|
+
"status": "success",
|
|
229
|
+
"message": "No motors are currently running"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
stop_flag = True
|
|
233
|
+
last_action = f"Stopped {active_motor}"
|
|
234
|
+
|
|
235
|
+
# Wait for thread to finish
|
|
236
|
+
if motor_thread and motor_thread.is_alive():
|
|
237
|
+
motor_thread.join(timeout=2)
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"status": "success",
|
|
241
|
+
"message": "All motors stopped",
|
|
242
|
+
"stopped_motor": active_motor
|
|
243
|
+
}
|
|
244
|
+
except Exception as e:
|
|
245
|
+
# Handle GPIO busy or other errors gracefully
|
|
246
|
+
print(f"[ERROR] Stop motors failed: {e}")
|
|
247
|
+
stop_flag = True
|
|
248
|
+
return {
|
|
249
|
+
"status": "error",
|
|
250
|
+
"message": f"Error stopping motors: {str(e)}",
|
|
251
|
+
"detail": str(e)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@app.post("/test")
|
|
255
|
+
async def run_test(config: TestConfig):
|
|
256
|
+
global last_action
|
|
257
|
+
|
|
258
|
+
if active_motor:
|
|
259
|
+
raise HTTPException(status_code=409, detail="A motor is already running")
|
|
260
|
+
|
|
261
|
+
pins = config.pins if config.pins else list(range(1, 28))
|
|
262
|
+
|
|
263
|
+
print(f"[TEST] Running test sequence:")
|
|
264
|
+
print(f" - Seconds ON: {config.seconds_on}")
|
|
265
|
+
print(f" - Seconds Pause: {config.seconds_pause}")
|
|
266
|
+
print(f" - Pins: {pins}")
|
|
267
|
+
|
|
268
|
+
last_action = f"Test sequence started"
|
|
269
|
+
|
|
270
|
+
# Simulate test sequence
|
|
271
|
+
def test_sequence():
|
|
272
|
+
global last_action
|
|
273
|
+
for pin in pins:
|
|
274
|
+
last_action = f"test:gpio:{pin}"
|
|
275
|
+
print(f">>> TESTING GPIO {pin}")
|
|
276
|
+
time.sleep(config.seconds_on)
|
|
277
|
+
print(f"[TEST] Pin {pin} OFF, pausing {config.seconds_pause}s")
|
|
278
|
+
time.sleep(config.seconds_pause)
|
|
279
|
+
last_action = "Test sequence completed"
|
|
280
|
+
print("[TEST] Test sequence completed")
|
|
281
|
+
|
|
282
|
+
test_thread = threading.Thread(target=test_sequence)
|
|
283
|
+
test_thread.start()
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"status": "success",
|
|
287
|
+
"message": "Test sequence started",
|
|
288
|
+
"config": {
|
|
289
|
+
"seconds_on": config.seconds_on,
|
|
290
|
+
"seconds_pause": config.seconds_pause,
|
|
291
|
+
"pins": pins
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
import uvicorn
|
|
297
|
+
print("=" * 60)
|
|
298
|
+
print("Motor Control API Server")
|
|
299
|
+
print("=" * 60)
|
|
300
|
+
print("Starting server on http://0.0.0.0:8001")
|
|
301
|
+
print("API Documentation: http://localhost:8001/docs")
|
|
302
|
+
print("ReDoc: http://localhost:8001/redoc")
|
|
303
|
+
print("=" * 60)
|
|
304
|
+
uvicorn.run(app, host="0.0.0.0", port=8001)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Non-Pi (Windows/macOS/Linux dev laptop) requirements for app_pi_clean.py.
|
|
2
|
+
#
|
|
3
|
+
# requirements_pi_unified.txt deliberately excludes opencv-python (Pi installs
|
|
4
|
+
# it via `apt install python3-opencv` for the ARM-optimized system build) —
|
|
5
|
+
# that's wrong off a real Pi, where plain `pip install opencv-python` is the
|
|
6
|
+
# normal path. Use this file for a demo/dev machine; use
|
|
7
|
+
# requirements_pi_unified.txt (+ apt) on an actual Raspberry Pi.
|
|
8
|
+
flask==3.0.0
|
|
9
|
+
flask-cors==4.0.0
|
|
10
|
+
opencv-python>=4.8
|
|
11
|
+
numpy>=1.24,<2
|
|
12
|
+
requests>=2.31
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Unified Raspberry Pi Requirements for All RoboVision Servers
|
|
2
|
+
# Vision Server + Motor Server + Audio Server
|
|
3
|
+
# Install with: pip install -r requirements_pi_unified.txt
|
|
4
|
+
|
|
5
|
+
# ============================================================================
|
|
6
|
+
# SYSTEM DEPENDENCIES (Install these first with apt)
|
|
7
|
+
# ============================================================================
|
|
8
|
+
# sudo apt-get update
|
|
9
|
+
# sudo apt-get install -y python3-opencv python3-numpy python3-pil
|
|
10
|
+
# sudo apt-get install -y libatlas-base-dev libopenblas-dev
|
|
11
|
+
# sudo apt-get install -y libhdf5-dev libhdf5-serial-dev libhdf5-103
|
|
12
|
+
# sudo apt-get install -y libqt5gui5 libqt5widgets5 libqt5test5
|
|
13
|
+
# sudo apt-get install -y portaudio19-dev libsndfile1 alsa-utils
|
|
14
|
+
# sudo apt-get install -y bluetooth bluez bluez-tools pulseaudio pulseaudio-module-bluetooth
|
|
15
|
+
# sudo apt-get install -y python3-pyaudio
|
|
16
|
+
|
|
17
|
+
# ============================================================================
|
|
18
|
+
# WEB FRAMEWORKS
|
|
19
|
+
# ============================================================================
|
|
20
|
+
# Flask - For Vision Server
|
|
21
|
+
Flask==3.0.0
|
|
22
|
+
Flask-CORS==4.0.0
|
|
23
|
+
|
|
24
|
+
# FastAPI - For Motor Server and Audio Server
|
|
25
|
+
fastapi==0.104.1
|
|
26
|
+
uvicorn[standard]==0.24.0
|
|
27
|
+
python-multipart==0.0.6
|
|
28
|
+
|
|
29
|
+
# ============================================================================
|
|
30
|
+
# COMPUTER VISION & OBJECT DETECTION
|
|
31
|
+
# ============================================================================
|
|
32
|
+
# Note: Use system python3-opencv instead of pip opencv-python
|
|
33
|
+
# opencv-python - DO NOT INSTALL via pip, use system package
|
|
34
|
+
|
|
35
|
+
# NumPy - Compatible version for ARM
|
|
36
|
+
numpy==1.24.3
|
|
37
|
+
|
|
38
|
+
# Image processing
|
|
39
|
+
Pillow==10.1.0
|
|
40
|
+
|
|
41
|
+
# YOLO Object Detection (optional, for advanced vision features)
|
|
42
|
+
ultralytics==8.0.196
|
|
43
|
+
|
|
44
|
+
# ============================================================================
|
|
45
|
+
# AUDIO PROCESSING (For Audio Server)
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# Note: python3-pyaudio should be installed via apt (see system dependencies above)
|
|
48
|
+
sounddevice==0.4.6
|
|
49
|
+
soundfile==0.12.1
|
|
50
|
+
|
|
51
|
+
# ============================================================================
|
|
52
|
+
# AI & CAPTION SERVICES
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# Google Generative AI for image captioning (optional)
|
|
55
|
+
google-generativeai==0.3.1
|
|
56
|
+
|
|
57
|
+
# ============================================================================
|
|
58
|
+
# MOTOR CONTROL (For Motor Server)
|
|
59
|
+
# ============================================================================
|
|
60
|
+
# GPIO control for Raspberry Pi
|
|
61
|
+
lgpio==0.2.2.0
|
|
62
|
+
|
|
63
|
+
# Pydantic for data validation
|
|
64
|
+
pydantic==2.5.0
|
|
65
|
+
|
|
66
|
+
# ============================================================================
|
|
67
|
+
# NETWORKING & HTTP
|
|
68
|
+
# ============================================================================
|
|
69
|
+
# HTTP requests
|
|
70
|
+
requests==2.31.0
|
|
71
|
+
|
|
72
|
+
# CORS support
|
|
73
|
+
python-multipart==0.0.6
|
|
74
|
+
|
|
75
|
+
# ============================================================================
|
|
76
|
+
# UTILITIES
|
|
77
|
+
# ============================================================================
|
|
78
|
+
# Python dotenv for environment variables
|
|
79
|
+
python-dotenv==1.0.0
|
|
80
|
+
|
|
81
|
+
# ============================================================================
|
|
82
|
+
# OPTIONAL DEPENDENCIES
|
|
83
|
+
# ============================================================================
|
|
84
|
+
# Ngrok for tunneling (optional, for remote access)
|
|
85
|
+
# pyngrok==7.0.1
|
|
86
|
+
|
|
87
|
+
# Pi Camera Module support (optional, only if using Pi Camera Module)
|
|
88
|
+
# picamera2
|
|
89
|
+
|
|
90
|
+
# ============================================================================
|
|
91
|
+
# NOTES
|
|
92
|
+
# ============================================================================
|
|
93
|
+
# 1. Always install system dependencies FIRST before pip packages
|
|
94
|
+
# 2. Use system python3-opencv instead of pip opencv-python (it's pre-built for ARM)
|
|
95
|
+
# 3. Create venv with --system-site-packages to access system opencv:
|
|
96
|
+
# python3 -m venv venv --system-site-packages
|
|
97
|
+
# 4. For Bluetooth audio, make sure to pair devices first
|
|
98
|
+
# 5. For GPIO motor control, user must be in 'gpio' group:
|
|
99
|
+
# sudo usermod -a -G gpio $USER
|
|
100
|
+
# 6. For Bluetooth, user must be in 'bluetooth' group:
|
|
101
|
+
# sudo usermod -a -G bluetooth $USER
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
STATE_DIR="$ROOT_DIR/.run"
|
|
6
|
+
LOG_DIR="$STATE_DIR/logs"
|
|
7
|
+
VENV_DIR="$ROOT_DIR/venv"
|
|
8
|
+
|
|
9
|
+
mkdir -p "$LOG_DIR"
|
|
10
|
+
|
|
11
|
+
if [ -f "$ROOT_DIR/.env" ]; then
|
|
12
|
+
set -a
|
|
13
|
+
source "$ROOT_DIR/.env"
|
|
14
|
+
set +a
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
source "$ROOT_DIR/services/services.sh"
|
|
18
|
+
|
|
19
|
+
require_venv() {
|
|
20
|
+
if [ ! -f "$VENV_DIR/bin/activate" ]; then
|
|
21
|
+
echo "Missing venv. Run: ./install.sh"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
is_running() {
|
|
27
|
+
local name="$1"
|
|
28
|
+
local pid_file="$STATE_DIR/$name.pid"
|
|
29
|
+
if [ ! -f "$pid_file" ]; then
|
|
30
|
+
return 1
|
|
31
|
+
fi
|
|
32
|
+
local pid
|
|
33
|
+
pid="$(cat "$pid_file" 2>/dev/null || true)"
|
|
34
|
+
if [ -z "$pid" ]; then
|
|
35
|
+
return 1
|
|
36
|
+
fi
|
|
37
|
+
kill -0 "$pid" >/dev/null 2>&1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
start_one() {
|
|
41
|
+
local name="$1"
|
|
42
|
+
local cmd_var="SERVICE_${name}_CMD"
|
|
43
|
+
local cmd
|
|
44
|
+
cmd="${!cmd_var}"
|
|
45
|
+
if [ -z "$cmd" ]; then
|
|
46
|
+
echo "Unknown service: $name"
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if is_running "$name"; then
|
|
51
|
+
echo "$name already running"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
require_venv
|
|
56
|
+
source "$VENV_DIR/bin/activate"
|
|
57
|
+
|
|
58
|
+
local log_file="$LOG_DIR/$name.log"
|
|
59
|
+
local pid_file="$STATE_DIR/$name.pid"
|
|
60
|
+
|
|
61
|
+
echo "Starting $name"
|
|
62
|
+
nohup bash -lc "cd \"$ROOT_DIR\" && source \"$VENV_DIR/bin/activate\" && $cmd" >>"$log_file" 2>&1 &
|
|
63
|
+
echo $! >"$pid_file"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
detect_terminal() {
|
|
67
|
+
if command -v lxterminal >/dev/null 2>&1; then
|
|
68
|
+
echo "lxterminal"
|
|
69
|
+
return 0
|
|
70
|
+
fi
|
|
71
|
+
if command -v gnome-terminal >/dev/null 2>&1; then
|
|
72
|
+
echo "gnome-terminal"
|
|
73
|
+
return 0
|
|
74
|
+
fi
|
|
75
|
+
if command -v xterm >/dev/null 2>&1; then
|
|
76
|
+
echo "xterm"
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
return 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
start_one_term() {
|
|
83
|
+
local name="$1"
|
|
84
|
+
local cmd_var="SERVICE_${name}_CMD"
|
|
85
|
+
local cmd
|
|
86
|
+
cmd="${!cmd_var}"
|
|
87
|
+
if [ -z "$cmd" ]; then
|
|
88
|
+
echo "Unknown service: $name"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
if is_running "$name"; then
|
|
93
|
+
echo "$name already running"
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
require_venv
|
|
98
|
+
|
|
99
|
+
local term
|
|
100
|
+
term="$(detect_terminal || true)"
|
|
101
|
+
if [ -z "$term" ]; then
|
|
102
|
+
echo "No supported terminal found (need lxterminal, gnome-terminal, or xterm)"
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
local log_file="$LOG_DIR/$name.log"
|
|
107
|
+
local pid_file="$STATE_DIR/$name.pid"
|
|
108
|
+
local title="RoboVision:$name"
|
|
109
|
+
local run_cmd
|
|
110
|
+
run_cmd="cd \"$ROOT_DIR\" && source \"$VENV_DIR/bin/activate\" && exec $cmd"
|
|
111
|
+
|
|
112
|
+
echo "Starting $name in terminal ($term)"
|
|
113
|
+
|
|
114
|
+
if [ "$term" = "lxterminal" ]; then
|
|
115
|
+
lxterminal --title="$title" --command="bash -lc '$run_cmd'" >>"$log_file" 2>&1 &
|
|
116
|
+
echo $! >"$pid_file"
|
|
117
|
+
return 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if [ "$term" = "gnome-terminal" ]; then
|
|
121
|
+
gnome-terminal --title="$title" -- bash -lc "$run_cmd" >>"$log_file" 2>&1 &
|
|
122
|
+
echo $! >"$pid_file"
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [ "$term" = "xterm" ]; then
|
|
127
|
+
xterm -T "$title" -e bash -lc "$run_cmd" >>"$log_file" 2>&1 &
|
|
128
|
+
echo $! >"$pid_file"
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
stop_one() {
|
|
134
|
+
local name="$1"
|
|
135
|
+
local pid_file="$STATE_DIR/$name.pid"
|
|
136
|
+
|
|
137
|
+
if ! is_running "$name"; then
|
|
138
|
+
rm -f "$pid_file"
|
|
139
|
+
echo "$name not running"
|
|
140
|
+
return 0
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
local pid
|
|
144
|
+
pid="$(cat "$pid_file")"
|
|
145
|
+
echo "Stopping $name (pid $pid)"
|
|
146
|
+
kill "$pid" >/dev/null 2>&1 || true
|
|
147
|
+
|
|
148
|
+
local i
|
|
149
|
+
for i in $(seq 1 30); do
|
|
150
|
+
if kill -0 "$pid" >/dev/null 2>&1; then
|
|
151
|
+
sleep 0.2
|
|
152
|
+
else
|
|
153
|
+
break
|
|
154
|
+
fi
|
|
155
|
+
done
|
|
156
|
+
|
|
157
|
+
if kill -0 "$pid" >/dev/null 2>&1; then
|
|
158
|
+
kill -9 "$pid" >/dev/null 2>&1 || true
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
rm -f "$pid_file"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
status_one() {
|
|
165
|
+
local name="$1"
|
|
166
|
+
local port_var="SERVICE_${name}_PORT"
|
|
167
|
+
local port
|
|
168
|
+
port="${!port_var}"
|
|
169
|
+
|
|
170
|
+
if is_running "$name"; then
|
|
171
|
+
echo "$name: running (port ${port:-n/a})"
|
|
172
|
+
else
|
|
173
|
+
echo "$name: stopped"
|
|
174
|
+
fi
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
tail_one() {
|
|
178
|
+
local name="$1"
|
|
179
|
+
local log_file="$LOG_DIR/$name.log"
|
|
180
|
+
if [ ! -f "$log_file" ]; then
|
|
181
|
+
echo "No log file: $log_file"
|
|
182
|
+
exit 1
|
|
183
|
+
fi
|
|
184
|
+
tail -n 200 -f "$log_file"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
usage() {
|
|
188
|
+
echo "Usage: ./run.sh <start|start-term|stop|restart|status|logs> [service]"
|
|
189
|
+
echo "Services: ${SERVICES[*]}"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
ACTION="${1:-}"
|
|
193
|
+
NAME="${2:-all}"
|
|
194
|
+
|
|
195
|
+
case "$ACTION" in
|
|
196
|
+
start)
|
|
197
|
+
if [ "$NAME" = "all" ]; then
|
|
198
|
+
for s in "${SERVICES[@]}"; do start_one "$s"; done
|
|
199
|
+
else
|
|
200
|
+
start_one "$NAME"
|
|
201
|
+
fi
|
|
202
|
+
;;
|
|
203
|
+
start-term)
|
|
204
|
+
if [ "$NAME" = "all" ]; then
|
|
205
|
+
for s in "${SERVICES[@]}"; do start_one_term "$s"; done
|
|
206
|
+
else
|
|
207
|
+
start_one_term "$NAME"
|
|
208
|
+
fi
|
|
209
|
+
;;
|
|
210
|
+
stop)
|
|
211
|
+
if [ "$NAME" = "all" ]; then
|
|
212
|
+
for s in "${SERVICES[@]}"; do stop_one "$s"; done
|
|
213
|
+
else
|
|
214
|
+
stop_one "$NAME"
|
|
215
|
+
fi
|
|
216
|
+
;;
|
|
217
|
+
restart)
|
|
218
|
+
if [ "$NAME" = "all" ]; then
|
|
219
|
+
for s in "${SERVICES[@]}"; do stop_one "$s"; done
|
|
220
|
+
for s in "${SERVICES[@]}"; do start_one "$s"; done
|
|
221
|
+
else
|
|
222
|
+
stop_one "$NAME"
|
|
223
|
+
start_one "$NAME"
|
|
224
|
+
fi
|
|
225
|
+
;;
|
|
226
|
+
status)
|
|
227
|
+
if [ "$NAME" = "all" ]; then
|
|
228
|
+
for s in "${SERVICES[@]}"; do status_one "$s"; done
|
|
229
|
+
else
|
|
230
|
+
status_one "$NAME"
|
|
231
|
+
fi
|
|
232
|
+
;;
|
|
233
|
+
logs)
|
|
234
|
+
if [ "$NAME" = "all" ]; then
|
|
235
|
+
echo "Specify a service for logs"
|
|
236
|
+
exit 1
|
|
237
|
+
fi
|
|
238
|
+
tail_one "$NAME"
|
|
239
|
+
;;
|
|
240
|
+
*)
|
|
241
|
+
usage
|
|
242
|
+
exit 1
|
|
243
|
+
;;
|
|
244
|
+
esac
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
SERVICES=("vision" "motor" "audio")
|
|
4
|
+
|
|
5
|
+
SERVICE_vision_PORT=5000
|
|
6
|
+
SERVICE_vision_CMD="python app_pi_clean.py"
|
|
7
|
+
|
|
8
|
+
SERVICE_motor_PORT=8001
|
|
9
|
+
SERVICE_motor_CMD="python motor_server.py"
|
|
10
|
+
|
|
11
|
+
SERVICE_audio_PORT=8000
|
|
12
|
+
SERVICE_audio_CMD="python audio_server_pi.py"
|