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,326 @@
|
|
|
1
|
+
from flask import Flask, Response, jsonify, request
|
|
2
|
+
from flask_cors import CORS
|
|
3
|
+
import argparse
|
|
4
|
+
import cv2
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import numpy as np
|
|
8
|
+
import threading
|
|
9
|
+
import requests
|
|
10
|
+
import base64
|
|
11
|
+
|
|
12
|
+
app = Flask(__name__)
|
|
13
|
+
CORS(app)
|
|
14
|
+
|
|
15
|
+
# Camera management
|
|
16
|
+
current_camera_index = 0
|
|
17
|
+
camera = None
|
|
18
|
+
|
|
19
|
+
def get_camera():
|
|
20
|
+
global camera, current_camera_index
|
|
21
|
+
if camera is None or not camera.isOpened():
|
|
22
|
+
camera = cv2.VideoCapture(current_camera_index)
|
|
23
|
+
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
|
|
24
|
+
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
|
25
|
+
print(f"Camera {current_camera_index} initialized")
|
|
26
|
+
return camera
|
|
27
|
+
|
|
28
|
+
# Global variables
|
|
29
|
+
latest_detections = []
|
|
30
|
+
lock = threading.Lock()
|
|
31
|
+
caption_mode_enabled = False
|
|
32
|
+
motion_detection_active = False
|
|
33
|
+
motion_detected_state = False
|
|
34
|
+
last_motion_time = 0
|
|
35
|
+
motion_frame_buffer = None
|
|
36
|
+
webhook_url = None
|
|
37
|
+
last_webhook_send_time = 0
|
|
38
|
+
webhook_send_interval = 0.5
|
|
39
|
+
|
|
40
|
+
# Simple object detection using OpenCV DNN (MobileNet SSD)
|
|
41
|
+
try:
|
|
42
|
+
# Load pre-trained MobileNet SSD model
|
|
43
|
+
net = cv2.dnn.readNetFromCaffe(
|
|
44
|
+
'deploy.prototxt',
|
|
45
|
+
'mobilenet_iter_73000.caffemodel'
|
|
46
|
+
)
|
|
47
|
+
DETECTION_AVAILABLE = True
|
|
48
|
+
print("Object detection model loaded")
|
|
49
|
+
except:
|
|
50
|
+
DETECTION_AVAILABLE = False
|
|
51
|
+
print("Object detection model not found - running without detection")
|
|
52
|
+
|
|
53
|
+
# COCO class labels
|
|
54
|
+
CLASSES = ["background", "aeroplane", "bicycle", "bird", "boat",
|
|
55
|
+
"bottle", "bus", "car", "cat", "chair", "cow", "diningtable",
|
|
56
|
+
"dog", "horse", "motorbike", "person", "pottedplant", "sheep",
|
|
57
|
+
"sofa", "train", "tvmonitor"]
|
|
58
|
+
|
|
59
|
+
def detect_objects(frame, conf_threshold=0.5):
|
|
60
|
+
"""Detect objects using OpenCV DNN"""
|
|
61
|
+
if not DETECTION_AVAILABLE:
|
|
62
|
+
return frame, []
|
|
63
|
+
|
|
64
|
+
(h, w) = frame.shape[:2]
|
|
65
|
+
blob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 0.007843, (300, 300), 127.5)
|
|
66
|
+
net.setInput(blob)
|
|
67
|
+
detections_dnn = net.forward()
|
|
68
|
+
|
|
69
|
+
detected_objects = []
|
|
70
|
+
|
|
71
|
+
for i in range(detections_dnn.shape[2]):
|
|
72
|
+
confidence = detections_dnn[0, 0, i, 2]
|
|
73
|
+
|
|
74
|
+
if confidence > conf_threshold:
|
|
75
|
+
idx = int(detections_dnn[0, 0, i, 1])
|
|
76
|
+
if idx >= len(CLASSES):
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
box = detections_dnn[0, 0, i, 3:7] * np.array([w, h, w, h])
|
|
80
|
+
(startX, startY, endX, endY) = box.astype("int")
|
|
81
|
+
|
|
82
|
+
label = CLASSES[idx]
|
|
83
|
+
|
|
84
|
+
# Draw bounding box
|
|
85
|
+
cv2.rectangle(frame, (startX, startY), (endX, endY), (0, 255, 0), 2)
|
|
86
|
+
|
|
87
|
+
# Draw label with confidence
|
|
88
|
+
text = f"{label}: {confidence*100:.1f}%"
|
|
89
|
+
y = startY - 15 if startY - 15 > 15 else startY + 15
|
|
90
|
+
cv2.putText(frame, text, (startX, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
|
|
91
|
+
|
|
92
|
+
detected_objects.append({
|
|
93
|
+
"label": label,
|
|
94
|
+
"confidence": float(confidence),
|
|
95
|
+
"bbox": [int(startX), int(startY), int(endX), int(endY)],
|
|
96
|
+
"is_focus": False
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return frame, detected_objects
|
|
100
|
+
|
|
101
|
+
def send_webhook(frame_data):
|
|
102
|
+
"""Send frame to webhook URL"""
|
|
103
|
+
global webhook_url, last_webhook_send_time
|
|
104
|
+
|
|
105
|
+
if not webhook_url:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
current_time = time.time()
|
|
109
|
+
if current_time - last_webhook_send_time < webhook_send_interval:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
_, buffer = cv2.imencode('.jpg', frame_data)
|
|
114
|
+
img_base64 = base64.b64encode(buffer).decode('utf-8')
|
|
115
|
+
|
|
116
|
+
payload = {
|
|
117
|
+
'timestamp': time.strftime('%Y-%m-%dT%H:%M:%S'),
|
|
118
|
+
'image': img_base64,
|
|
119
|
+
'format': 'jpeg',
|
|
120
|
+
'encoding': 'base64'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def send_async():
|
|
124
|
+
try:
|
|
125
|
+
response = requests.post(webhook_url, json=payload, headers={'Content-Type': 'application/json'}, timeout=5)
|
|
126
|
+
if response.status_code == 200:
|
|
127
|
+
print(f"Webhook sent successfully")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"Webhook error: {e}")
|
|
130
|
+
|
|
131
|
+
thread = threading.Thread(target=send_async, daemon=True)
|
|
132
|
+
thread.start()
|
|
133
|
+
last_webhook_send_time = current_time
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"Error preparing webhook: {e}")
|
|
136
|
+
|
|
137
|
+
def generate_frames():
|
|
138
|
+
global latest_detections, motion_detection_active, motion_detected_state
|
|
139
|
+
global last_motion_time, motion_frame_buffer
|
|
140
|
+
|
|
141
|
+
prev_gray = None
|
|
142
|
+
|
|
143
|
+
while True:
|
|
144
|
+
cam = get_camera()
|
|
145
|
+
if cam is None or not cam.isOpened():
|
|
146
|
+
time.sleep(1)
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
success, frame = cam.read()
|
|
150
|
+
if not success:
|
|
151
|
+
time.sleep(0.1)
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# Run object detection
|
|
155
|
+
processed_frame, detections = detect_objects(frame, conf_threshold=0.5)
|
|
156
|
+
|
|
157
|
+
with lock:
|
|
158
|
+
latest_detections = detections
|
|
159
|
+
|
|
160
|
+
# Motion detection logic
|
|
161
|
+
if motion_detection_active:
|
|
162
|
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
163
|
+
gray = cv2.GaussianBlur(gray, (21, 21), 0)
|
|
164
|
+
|
|
165
|
+
if prev_gray is not None:
|
|
166
|
+
frame_delta = cv2.absdiff(prev_gray, gray)
|
|
167
|
+
thresh = cv2.threshold(frame_delta, 25, 255, cv2.THRESH_BINARY)[1]
|
|
168
|
+
thresh = cv2.dilate(thresh, None, iterations=2)
|
|
169
|
+
contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
170
|
+
|
|
171
|
+
motion_detected = False
|
|
172
|
+
for contour in contours:
|
|
173
|
+
if cv2.contourArea(contour) >= 500:
|
|
174
|
+
motion_detected = True
|
|
175
|
+
(x, y, w, h) = cv2.boundingRect(contour)
|
|
176
|
+
cv2.rectangle(processed_frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
if motion_detected:
|
|
180
|
+
motion_detected_state = True
|
|
181
|
+
last_motion_time = time.time()
|
|
182
|
+
motion_frame_buffer = processed_frame.copy()
|
|
183
|
+
print(f"Motion detected!")
|
|
184
|
+
send_webhook(processed_frame)
|
|
185
|
+
else:
|
|
186
|
+
motion_detected_state = False
|
|
187
|
+
|
|
188
|
+
prev_gray = gray
|
|
189
|
+
|
|
190
|
+
ret, buffer = cv2.imencode('.jpg', processed_frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
|
|
191
|
+
frame_bytes = buffer.tobytes()
|
|
192
|
+
|
|
193
|
+
yield (b'--frame\r\n'
|
|
194
|
+
b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
|
|
195
|
+
|
|
196
|
+
@app.route('/')
|
|
197
|
+
def index():
|
|
198
|
+
return jsonify({"status": "ok", "message": "RoboVision Pi Server", "version": "1.0"})
|
|
199
|
+
|
|
200
|
+
@app.route('/video_feed')
|
|
201
|
+
def video_feed():
|
|
202
|
+
return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
|
203
|
+
|
|
204
|
+
@app.route('/api/detections')
|
|
205
|
+
def get_detections():
|
|
206
|
+
with lock:
|
|
207
|
+
return jsonify(latest_detections)
|
|
208
|
+
|
|
209
|
+
@app.route('/api/caption')
|
|
210
|
+
def get_caption():
|
|
211
|
+
return jsonify({"caption": "Awaiting caption..."})
|
|
212
|
+
|
|
213
|
+
@app.route('/api/caption_mode', methods=['GET', 'POST'])
|
|
214
|
+
def caption_mode():
|
|
215
|
+
global caption_mode_enabled
|
|
216
|
+
if request.method == 'GET':
|
|
217
|
+
return jsonify({"enabled": caption_mode_enabled})
|
|
218
|
+
data = request.json or {}
|
|
219
|
+
caption_mode_enabled = bool(data.get('enabled', False))
|
|
220
|
+
return jsonify({"enabled": caption_mode_enabled})
|
|
221
|
+
|
|
222
|
+
@app.route('/api/cameras', methods=['GET'])
|
|
223
|
+
def list_cameras():
|
|
224
|
+
available = []
|
|
225
|
+
for i in range(4):
|
|
226
|
+
cap = cv2.VideoCapture(i)
|
|
227
|
+
if cap.isOpened():
|
|
228
|
+
available.append({"index": i, "name": f"Camera {i}"})
|
|
229
|
+
cap.release()
|
|
230
|
+
return jsonify({"cameras": available, "current": current_camera_index})
|
|
231
|
+
|
|
232
|
+
@app.route('/api/camera/switch', methods=['POST'])
|
|
233
|
+
def switch_camera():
|
|
234
|
+
global camera, current_camera_index
|
|
235
|
+
data = request.json or {}
|
|
236
|
+
new_index = int(data.get('index', 0))
|
|
237
|
+
|
|
238
|
+
if camera:
|
|
239
|
+
camera.release()
|
|
240
|
+
|
|
241
|
+
current_camera_index = new_index
|
|
242
|
+
camera = None
|
|
243
|
+
|
|
244
|
+
return jsonify({"status": "ok", "camera_index": current_camera_index})
|
|
245
|
+
|
|
246
|
+
@app.route('/api/motion/status', methods=['GET'])
|
|
247
|
+
def motion_status():
|
|
248
|
+
global motion_detection_active, motion_detected_state, last_motion_time
|
|
249
|
+
return jsonify({
|
|
250
|
+
"active": motion_detection_active,
|
|
251
|
+
"motion_detected": motion_detected_state,
|
|
252
|
+
"last_motion": last_motion_time,
|
|
253
|
+
"time_since_motion": time.time() - last_motion_time if last_motion_time > 0 else None
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
@app.route('/api/motion/toggle', methods=['POST'])
|
|
257
|
+
def motion_toggle():
|
|
258
|
+
global motion_detection_active
|
|
259
|
+
data = request.json or {}
|
|
260
|
+
motion_detection_active = bool(data.get('active', False))
|
|
261
|
+
return jsonify({"status": "success", "active": motion_detection_active})
|
|
262
|
+
|
|
263
|
+
@app.route('/api/motion/snapshot', methods=['GET'])
|
|
264
|
+
def motion_snapshot():
|
|
265
|
+
global motion_frame_buffer
|
|
266
|
+
if motion_frame_buffer is not None:
|
|
267
|
+
_, buffer = cv2.imencode('.jpg', motion_frame_buffer, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
|
268
|
+
img_base64 = base64.b64encode(buffer).decode('utf-8')
|
|
269
|
+
return jsonify({
|
|
270
|
+
"image": img_base64,
|
|
271
|
+
"timestamp": time.time()
|
|
272
|
+
})
|
|
273
|
+
return jsonify({"error": "No frame available"}), 404
|
|
274
|
+
|
|
275
|
+
@app.route('/api/motion/webhook', methods=['GET', 'POST'])
|
|
276
|
+
def motion_webhook():
|
|
277
|
+
global webhook_url
|
|
278
|
+
|
|
279
|
+
if request.method == 'GET':
|
|
280
|
+
return jsonify({
|
|
281
|
+
"webhook_url": webhook_url or "",
|
|
282
|
+
"configured": webhook_url is not None and len(webhook_url) > 0
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
data = request.json or {}
|
|
286
|
+
new_url = data.get('url', '').strip()
|
|
287
|
+
|
|
288
|
+
if new_url:
|
|
289
|
+
webhook_url = new_url
|
|
290
|
+
return jsonify({
|
|
291
|
+
"status": "success",
|
|
292
|
+
"message": "Webhook URL configured",
|
|
293
|
+
"webhook_url": webhook_url
|
|
294
|
+
})
|
|
295
|
+
else:
|
|
296
|
+
webhook_url = None
|
|
297
|
+
return jsonify({
|
|
298
|
+
"status": "success",
|
|
299
|
+
"message": "Webhook URL cleared",
|
|
300
|
+
"webhook_url": None
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
if __name__ == '__main__':
|
|
304
|
+
parser = argparse.ArgumentParser(description="RoboVision — camera/motion detection server")
|
|
305
|
+
parser.add_argument("--port", type=int, default=int(os.getenv("VISION_PORT", "5000")))
|
|
306
|
+
parser.add_argument("--motion-webhook-url", default=os.getenv("MOTION_WEBHOOK_URL", ""),
|
|
307
|
+
help="where to POST a snapshot when motion is detected, e.g. http://localhost:5057/")
|
|
308
|
+
parser.add_argument("--motion-active", action="store_true",
|
|
309
|
+
default=os.getenv("MOTION_ACTIVE", "").lower() in ("1", "true", "yes"),
|
|
310
|
+
help="arm motion detection immediately on startup (no manual /api/motion/toggle call needed)")
|
|
311
|
+
args = parser.parse_args()
|
|
312
|
+
|
|
313
|
+
if args.motion_webhook_url:
|
|
314
|
+
webhook_url = args.motion_webhook_url
|
|
315
|
+
if args.motion_active:
|
|
316
|
+
motion_detection_active = True
|
|
317
|
+
|
|
318
|
+
print("=" * 60)
|
|
319
|
+
print("RoboVision - Raspberry Pi Vision Server (Minimal)")
|
|
320
|
+
print("=" * 60)
|
|
321
|
+
print(f"Starting Flask server on http://0.0.0.0:{args.port}")
|
|
322
|
+
if webhook_url:
|
|
323
|
+
print(f"Motion webhook: {webhook_url}")
|
|
324
|
+
print(f"Motion detection: {'ARMED' if motion_detection_active else 'off (POST /api/motion/toggle to arm)'}")
|
|
325
|
+
print("=" * 60)
|
|
326
|
+
app.run(host='0.0.0.0', port=args.port, debug=False, threaded=True)
|