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: opts.videoDevice ?? '/dev/robopark-camera',
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', opts.videoDevice ?? '/dev/video0',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.94",
3
+ "version": "2.8.95",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",
@@ -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", False))
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
- """Own the camera while idle and detect motion without a second process."""
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 camera opened by preview agent")
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 or ("/dev/robopark-camera" if os.path.exists("/dev/robopark-camera") else 0)
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
- return cv2.VideoCapture(index)
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 = int(switch_camera_value) if switch_camera_value.isdigit() else switch_camera_value
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"])