infinicode 2.8.94 → 2.8.95
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.
|
@@ -83,6 +83,10 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
83
83
|
const children = [];
|
|
84
84
|
let stopping = false;
|
|
85
85
|
let recycling = false;
|
|
86
|
+
const requestedVideo = String(opts.videoDevice ?? '').trim();
|
|
87
|
+
const hardwareVideo = !requestedVideo || ['auto', 'default', 'first'].includes(requestedVideo.toLowerCase())
|
|
88
|
+
? '/dev/video0'
|
|
89
|
+
: requestedVideo;
|
|
86
90
|
const infinicode = await resolveInfinicodeBin();
|
|
87
91
|
if (!infinicode)
|
|
88
92
|
throw new Error('could not resolve infinicode CLI');
|
|
@@ -100,14 +104,15 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
100
104
|
args: [cli, 'vision-agent', '--foreground', '--port', '5000',
|
|
101
105
|
'--motion-webhook-url', 'http://127.0.0.1:5057/'],
|
|
102
106
|
env: {
|
|
103
|
-
ROBOPARK_CAMERA_DEVICE:
|
|
107
|
+
ROBOPARK_CAMERA_DEVICE: hardwareVideo,
|
|
104
108
|
},
|
|
105
109
|
});
|
|
106
110
|
}
|
|
107
111
|
const previewArgs = [cli, 'preview-agent', '--foreground',
|
|
108
112
|
'--scheduler-url', opts.schedulerUrl, '--robot-id', opts.name,
|
|
109
|
-
'--video-device',
|
|
113
|
+
'--video-device', hardwareVideo,
|
|
110
114
|
'--audio-device', opts.audioDevice ?? 'Usb Audio Device: USB Audio (hw:3,0)',
|
|
115
|
+
'--robovision-url', 'http://127.0.0.1:5000',
|
|
111
116
|
'--save-config'];
|
|
112
117
|
if (opts.enrollmentToken)
|
|
113
118
|
previewArgs.push('--enrollment-token', opts.enrollmentToken);
|
|
@@ -127,6 +132,9 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
127
132
|
? 'http://127.0.0.1:5000/api/media/inventory'
|
|
128
133
|
: '',
|
|
129
134
|
ROBOVISION_CAMERA: opts.vision !== false ? '1' : '0',
|
|
135
|
+
// Motion remains enabled, but it reads RoboVision's shared stream. Only
|
|
136
|
+
// RoboVision may open the physical V4L2 device.
|
|
137
|
+
LOCAL_CAMERA_MOTION: 'true',
|
|
130
138
|
// Fleet hardware profile: resolve volatile ALSA hw:X,Y coordinates
|
|
131
139
|
// from these USB product labels on every inventory refresh.
|
|
132
140
|
ROBOPARK_AUDIO_INPUT_MATCH: 'Usb Audio Device: USB Audio',
|
|
@@ -150,3 +150,13 @@ Required result: `playback_started ok`.
|
|
|
150
150
|
and microphone publication, TTS subscription, and first robot playback.
|
|
151
151
|
- An unattended readiness pass does not claim an STT/LLM turn occurred. That
|
|
152
152
|
semantic turn still requires injected speech or a person near the robot.
|
|
153
|
+
|
|
154
|
+
## Camera Ownership Contract (2.8.95+)
|
|
155
|
+
|
|
156
|
+
- RoboVision is the only process permitted to open the physical V4L2 camera.
|
|
157
|
+
- Dashboard, motion sampling, and conversation video consume RoboVision's
|
|
158
|
+
shared MJPEG stream; they never open `/dev/video0` independently.
|
|
159
|
+
- Local camera motion remains enabled by default. Production mode controls
|
|
160
|
+
whether motion creates a conversation, not whether the camera worker runs.
|
|
161
|
+
- `auto`, `default`, and `first` resolve to `/dev/robopark-camera` when present,
|
|
162
|
+
otherwise `/dev/video0`; those literal strings are never passed to OpenCV.
|
package/package.json
CHANGED
|
@@ -486,7 +486,7 @@ class PreviewAgent:
|
|
|
486
486
|
# to its hardware name before opening any playback stream.
|
|
487
487
|
self.audio_playback_device = self.audio_output_device
|
|
488
488
|
self.robovision_url = cfg.get("robovision_url", os.getenv("ROBOVISION_URL", "http://127.0.0.1:5000"))
|
|
489
|
-
self.use_robovision_camera = bool(cfg.get("use_robovision_camera",
|
|
489
|
+
self.use_robovision_camera = bool(cfg.get("use_robovision_camera", True))
|
|
490
490
|
self.width = int(cfg.get("video_width", os.getenv("VIDEO_WIDTH", DEFAULT_VIDEO_WIDTH)))
|
|
491
491
|
self.height = int(cfg.get("video_height", os.getenv("VIDEO_HEIGHT", DEFAULT_VIDEO_HEIGHT)))
|
|
492
492
|
self.fps = int(cfg.get("video_fps", os.getenv("VIDEO_FPS", DEFAULT_FPS)))
|
|
@@ -522,7 +522,8 @@ class PreviewAgent:
|
|
|
522
522
|
self._speaker_operation_lock = asyncio.Lock()
|
|
523
523
|
self._vision_hard_deadline: float = 0.0
|
|
524
524
|
self._vision_last_activity: float = 0.0
|
|
525
|
-
self.production_mode = False
|
|
525
|
+
self.production_mode = False
|
|
526
|
+
self._robovision_motion_state: Optional[bool] = None
|
|
526
527
|
self._remote_session_ended = False
|
|
527
528
|
self._last_device_config_poll = 0.0
|
|
528
529
|
self._motion_reference = None
|
|
@@ -632,9 +633,13 @@ class PreviewAgent:
|
|
|
632
633
|
if capture is not None:
|
|
633
634
|
await asyncio.to_thread(capture.stop)
|
|
634
635
|
|
|
635
|
-
async def _motion_loop(self) -> None:
|
|
636
|
-
"""
|
|
637
|
-
while not self._shutdown.is_set():
|
|
636
|
+
async def _motion_loop(self) -> None:
|
|
637
|
+
"""Detect motion from RoboVision's shared stream without owning V4L2."""
|
|
638
|
+
while not self._shutdown.is_set():
|
|
639
|
+
if not self.production_mode:
|
|
640
|
+
await self._stop_motion_capture()
|
|
641
|
+
await asyncio.sleep(0.5)
|
|
642
|
+
continue
|
|
638
643
|
if self._vision_session_id or self._current_state.active:
|
|
639
644
|
await self._stop_motion_capture()
|
|
640
645
|
await asyncio.sleep(0.25)
|
|
@@ -643,16 +648,17 @@ class PreviewAgent:
|
|
|
643
648
|
async with self._motion_capture_lock:
|
|
644
649
|
if self._motion_capture is None:
|
|
645
650
|
self._motion_capture = await asyncio.to_thread(
|
|
646
|
-
create_video_capture,
|
|
647
|
-
self.video_device,
|
|
648
|
-
self.width,
|
|
649
|
-
self.height,
|
|
650
|
-
self.fps,
|
|
651
|
-
|
|
651
|
+
create_video_capture,
|
|
652
|
+
self.video_device,
|
|
653
|
+
self.width,
|
|
654
|
+
self.height,
|
|
655
|
+
self.fps,
|
|
656
|
+
self.robovision_url,
|
|
657
|
+
)
|
|
652
658
|
self._motion_reference = None
|
|
653
659
|
if self._motion_capture is None:
|
|
654
660
|
continue
|
|
655
|
-
logger.info("motion
|
|
661
|
+
logger.info("motion sampler connected to RoboVision shared stream")
|
|
656
662
|
frame = await asyncio.to_thread(self._motion_capture.read)
|
|
657
663
|
if frame is not None:
|
|
658
664
|
self._detect_motion(frame)
|
|
@@ -921,6 +927,20 @@ class PreviewAgent:
|
|
|
921
927
|
self._next_mesh_bootstrap = 0.0
|
|
922
928
|
if production_mode is not None:
|
|
923
929
|
self.production_mode = production_mode
|
|
930
|
+
# Exactly one motion detector is active. With the default
|
|
931
|
+
# local sampler enabled, preview reads RoboVision's shared
|
|
932
|
+
# MJPEG stream and RoboVision only owns capture/encoding.
|
|
933
|
+
robovision_motion = bool(production_mode and not self.local_camera_motion)
|
|
934
|
+
if self._robovision_motion_state != robovision_motion:
|
|
935
|
+
try:
|
|
936
|
+
response = await self._session.post(
|
|
937
|
+
f"{self.robovision_url.rstrip('/')}/api/motion/toggle",
|
|
938
|
+
json={"enabled": robovision_motion}, timeout=1.5,
|
|
939
|
+
)
|
|
940
|
+
response.raise_for_status()
|
|
941
|
+
self._robovision_motion_state = robovision_motion
|
|
942
|
+
except Exception as exc:
|
|
943
|
+
logger.warning(f"failed to synchronize RoboVision motion mode: {exc}")
|
|
924
944
|
await self._report_pipeline("robot_online", message="Robot heartbeat accepted")
|
|
925
945
|
inventory = _get_device_inventory()
|
|
926
946
|
real_video = [d for d in inventory.get("video", []) if str(d.get("id", "")).lower() not in ("auto", "none")]
|
|
@@ -2128,7 +2148,7 @@ def main() -> None:
|
|
|
2128
2148
|
"audio_device": args.audio_device,
|
|
2129
2149
|
"robovision_url": args.robovision_url or os.getenv("ROBOVISION_URL", "http://127.0.0.1:5000"),
|
|
2130
2150
|
"use_robovision_camera": bool(args.robovision_url) or os.getenv(
|
|
2131
|
-
"ROBOVISION_CAMERA", ""
|
|
2151
|
+
"ROBOVISION_CAMERA", "true"
|
|
2132
2152
|
).lower() in ("1", "true", "yes", "on"),
|
|
2133
2153
|
"video_width": args.width,
|
|
2134
2154
|
"video_height": args.height,
|
|
@@ -17,8 +17,19 @@ CORS(app)
|
|
|
17
17
|
|
|
18
18
|
# Camera management. Production installs create /dev/robopark-camera from
|
|
19
19
|
# the primary USB capture interface, avoiding /dev/videoN renumbering.
|
|
20
|
+
def _normalize_camera_device(value):
|
|
21
|
+
configured = str(value or "").strip()
|
|
22
|
+
if configured.lower() in ("", "auto", "default", "first"):
|
|
23
|
+
if sys.platform.startswith("linux"):
|
|
24
|
+
return "/dev/robopark-camera" if os.path.exists("/dev/robopark-camera") else "/dev/video0"
|
|
25
|
+
return 0
|
|
26
|
+
if configured.isdigit():
|
|
27
|
+
return int(configured)
|
|
28
|
+
return configured
|
|
29
|
+
|
|
30
|
+
|
|
20
31
|
_configured_camera = os.getenv("ROBOPARK_CAMERA_DEVICE", "")
|
|
21
|
-
current_camera_index = _configured_camera
|
|
32
|
+
current_camera_index = _normalize_camera_device(_configured_camera)
|
|
22
33
|
camera = None
|
|
23
34
|
camera_lock = threading.Lock()
|
|
24
35
|
frame_condition = threading.Condition()
|
|
@@ -41,9 +52,17 @@ def _open_camera(index):
|
|
|
41
52
|
# webcam — isOpened() then stays False forever and callers retry in an
|
|
42
53
|
# infinite doomed loop. Force DirectShow, the backend that actually
|
|
43
54
|
# enumerates standard Windows webcams.
|
|
44
|
-
if sys.platform == 'win32':
|
|
45
|
-
return cv2.VideoCapture(index, cv2.CAP_DSHOW)
|
|
46
|
-
|
|
55
|
+
if sys.platform == 'win32':
|
|
56
|
+
return cv2.VideoCapture(index, cv2.CAP_DSHOW)
|
|
57
|
+
if sys.platform.startswith('linux'):
|
|
58
|
+
cap = cv2.VideoCapture(index, cv2.CAP_V4L2)
|
|
59
|
+
if cap.isOpened():
|
|
60
|
+
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG'))
|
|
61
|
+
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
|
|
62
|
+
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
|
63
|
+
cap.set(cv2.CAP_PROP_FPS, 15)
|
|
64
|
+
return cap
|
|
65
|
+
return cv2.VideoCapture(index)
|
|
47
66
|
|
|
48
67
|
|
|
49
68
|
def get_camera():
|
|
@@ -469,7 +488,7 @@ def switch_camera():
|
|
|
469
488
|
with camera_lock:
|
|
470
489
|
if camera:
|
|
471
490
|
camera.release()
|
|
472
|
-
current_camera_index = new_index
|
|
491
|
+
current_camera_index = _normalize_camera_device(new_index)
|
|
473
492
|
camera = None
|
|
474
493
|
|
|
475
494
|
return jsonify({"status": "ok", "camera_index": current_camera_index})
|
|
@@ -531,7 +550,7 @@ def media_config():
|
|
|
531
550
|
switch_camera_value = value
|
|
532
551
|
if camera:
|
|
533
552
|
camera.release()
|
|
534
|
-
current_camera_index =
|
|
553
|
+
current_camera_index = _normalize_camera_device(switch_camera_value)
|
|
535
554
|
camera = None
|
|
536
555
|
if "audio_device" in data:
|
|
537
556
|
audio_input_device = str(data["audio_device"])
|