infinicode 2.8.89 → 2.8.90

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.
@@ -4004,7 +4004,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
4004
4004
  var _statusMem = {}; // name -> { lastActiveAt, lastEndedAt, prevReadiness }
4005
4005
  var drawerId=null, camRAF=null, meterRAF=null, camAbort=null, drawerTimer=null, pipelineTimer=null, deviceSelectorsReady=false;
4006
4006
  var previewOn=false, previewTimer=null; // on-demand operator camera preview
4007
- var visionOn=false; // RoboVisionAI_PI overlay feed (direct MJPEG, not LiveKit)
4007
+ var visionOn=false, visionRetryTimer=null; // RoboVisionAI_PI overlay feed (direct MJPEG, not LiveKit)
4008
4008
  var lkRoom=null, micAnalyser=null, micMonitorSource=null, micMonitorGain=null, audioCtx=null, lkAudioEls=[], micMonitorConnecting=false, micMonitorState='no active conversation session', micMonitorAudible=false, micMonitorVolume=0.8;
4009
4009
  var imgs={}; // preloaded robot concept avatars
4010
4010
  var TW=44, TH=22, PARK_W=15, PARK_D=8;
@@ -6217,8 +6217,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
6217
6217
  directImg.style.display='block';
6218
6218
  if(directCamNote)directCamNote.textContent='opening RoboVision camera relay...';
6219
6219
  if(directRes)directRes.textContent='CONNECTING';
6220
- directImg.onerror=function(){ visionOn=false; directImg.style.display='none'; directImg.src=''; if(directCamNote) directCamNote.textContent='RoboVision feed unavailable on robot'; if(directLiveNote) directLiveNote.textContent='camera unavailable'; if(directRes) directRes.textContent='OFFLINE'; };
6221
- directImg.onload=function(){ directImg.style.display='block'; if(directCamNote) directCamNote.textContent='live camera via RoboVision mesh relay'; if(directLiveNote) directLiveNote.textContent='live'; if(directRes) directRes.textContent='LIVE'; };
6220
+ directImg.onerror=function(){ visionOn=false; directImg.style.display='none'; if(directCamNote) directCamNote.textContent='RoboVision feed unavailable; reconnecting...'; if(directLiveNote) directLiveNote.textContent='camera reconnecting'; if(directRes) directRes.textContent='RETRYING'; if(visionRetryTimer)clearTimeout(visionRetryTimer);visionRetryTimer=setTimeout(function(){if(drawerId&&byId[drawerId]===n&&!visionOn)tryVisionOverlay(n,true);},3000); };
6221
+ directImg.onload=function(){ visionOn=true;if(visionRetryTimer){clearTimeout(visionRetryTimer);visionRetryTimer=null;}directImg.style.display='block'; if(directCamNote) directCamNote.textContent='live camera via RoboVision mesh relay'; if(directLiveNote) directLiveNote.textContent='live'; if(directRes) directRes.textContent='LIVE'; };
6222
6222
  directImg.src='/fed/media/robots/'+encodeURIComponent(directRef)+'/video_feed'+QS+(QS?'&':'?')+'_='+Date.now();
6223
6223
  return;
6224
6224
  var img=$('pk-vision-img'); if(!img) return;
@@ -6235,9 +6235,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
6235
6235
  img.onload=function(){ img.style.display='block'; };
6236
6236
  img.src='http://'+ip+':'+port+'/video_feed';
6237
6237
  }
6238
- function stopVision(){
6239
- visionOn=false;
6240
- var img=$('pk-vision-img'); if(img){ img.src=''; img.style.display='none'; }
6238
+ function stopVision(){
6239
+ visionOn=false;
6240
+ if(visionRetryTimer){clearTimeout(visionRetryTimer);visionRetryTimer=null;}
6241
+ var img=$('pk-vision-img'); if(img){ img.src=''; img.style.display='none'; }
6241
6242
  }
6242
6243
  function pollRoboparkQuiet(){
6243
6244
  fetch('/robopark/api/telemetry'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
@@ -6874,7 +6875,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
6874
6875
  function set(id,v){ var e=$(id); if(e) e.textContent=v; }
6875
6876
  function setFill(id,pct){ var e=$(id); if(!e||pct==null) return; e.style.width=Math.max(2,Math.min(100,pct))+'%'; e.style.background=pct>=75?'var(--bad)':pct>=45?'var(--warn)':'var(--gold-bright)'; }
6876
6877
 
6877
- var cameraOpsNode=null,cameraOpsTimer=null;
6878
+ var cameraOpsNode=null,cameraOpsTimer=null,cameraOpsRetryTimer=null;
6878
6879
  function cameraOpsFeedUrl(n){
6879
6880
  var ref=n&&(n.displayName||n.name||n.nodeId||n.id); if(!ref)return '';
6880
6881
  return '/fed/media/robots/'+encodeURIComponent(ref)+'/video_feed'+QS+(QS?'&':'?')+'_='+Date.now();
@@ -6892,13 +6893,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
6892
6893
  }
6893
6894
  function reloadCameraOperations(){
6894
6895
  if(!cameraOpsNode)return;var img=$('pk-camera-ops-img'),empty=$('pk-camera-ops-empty');if(!img)return;img.dataset.live='false';if(empty){empty.style.display='block';empty.textContent='opening RoboVision camera...';}
6895
- img.onload=function(){img.dataset.live='true';if(empty)empty.style.display='none';refreshCameraOperations();};img.onerror=function(){img.dataset.live='false';if(empty){empty.style.display='block';empty.textContent='camera relay unavailable / retrying';}refreshCameraOperations();};img.src=cameraOpsFeedUrl(cameraOpsNode);refreshCameraOperations();
6896
+ img.onload=function(){img.dataset.live='true';if(cameraOpsRetryTimer){clearTimeout(cameraOpsRetryTimer);cameraOpsRetryTimer=null;}if(empty)empty.style.display='none';refreshCameraOperations();};img.onerror=function(){var failedNode=cameraOpsNode;img.dataset.live='false';if(empty){empty.style.display='block';empty.textContent='camera relay unavailable / retrying';}refreshCameraOperations();if(cameraOpsRetryTimer)clearTimeout(cameraOpsRetryTimer);cameraOpsRetryTimer=setTimeout(function(){if(cameraOpsNode&&cameraOpsNode===failedNode)reloadCameraOperations();},3000);};img.src=cameraOpsFeedUrl(cameraOpsNode);refreshCameraOperations();
6896
6897
  }
6897
6898
  function openCameraOperations(n){
6898
6899
  cameraOpsNode=n;var host=$('pk-camera-ops');if(!host)return;host.classList.add('open');document.body.classList.add('camera-ops-open');var name=n.displayName||n.name||'robot',avatar=rpAvatarFor(name),av=$('pk-camera-ops-avatar');$('pk-camera-ops-name').textContent=name+' / live operations';$('pk-camera-ops-sub').textContent='RoboVision mesh relay / '+(n.connected?'connected':'offline');if(av){if(avatar){av.src=avatar+QS;av.style.display='block';}else{av.removeAttribute('src');av.style.display='none';}}reloadCameraOperations();if(cameraOpsTimer)clearInterval(cameraOpsTimer);cameraOpsTimer=setInterval(refreshCameraOperations,1000);
6899
6900
  }
6900
6901
  function closeCameraOperations(){
6901
- var host=$('pk-camera-ops'),img=$('pk-camera-ops-img');if(host)host.classList.remove('open');document.body.classList.remove('camera-ops-open');if(img){img.src='';img.dataset.live='false';}if(cameraOpsTimer){clearInterval(cameraOpsTimer);cameraOpsTimer=null;}cameraOpsNode=null;if(document.fullscreenElement&&document.exitFullscreen)document.exitFullscreen().catch(function(){});
6902
+ var host=$('pk-camera-ops'),img=$('pk-camera-ops-img');if(host)host.classList.remove('open');document.body.classList.remove('camera-ops-open');if(cameraOpsRetryTimer){clearTimeout(cameraOpsRetryTimer);cameraOpsRetryTimer=null;}if(img){img.src='';img.dataset.live='false';}if(cameraOpsTimer){clearInterval(cameraOpsTimer);cameraOpsTimer=null;}cameraOpsNode=null;if(document.fullscreenElement&&document.exitFullscreen)document.exitFullscreen().catch(function(){});
6902
6903
  }
6903
6904
  (function bindCameraOperations(){
6904
6905
  var close=$('pk-camera-ops-close'),refresh=$('pk-camera-ops-refresh'),nativeBtn=$('pk-camera-ops-native'),trigger=$('pk-camera-ops-trigger'),prod=$('pk-camera-ops-production'),end=$('pk-camera-ops-end');
@@ -85,7 +85,15 @@ export class MeshServer {
85
85
  }
86
86
  async streamHttp(req, res, target, headers = {}) {
87
87
  const ctrl = new AbortController();
88
- req.once('close', () => ctrl.abort());
88
+ let clientClosed = false;
89
+ let timeout;
90
+ const armTimeout = () => {
91
+ if (timeout)
92
+ clearTimeout(timeout);
93
+ timeout = setTimeout(() => ctrl.abort('camera upstream timed out'), 10_000);
94
+ };
95
+ req.once('close', () => { clientClosed = true; ctrl.abort(); });
96
+ armTimeout();
89
97
  try {
90
98
  const upstream = await fetch(target, { headers, signal: ctrl.signal });
91
99
  if (!upstream.ok || !upstream.body) {
@@ -105,6 +113,7 @@ export class MeshServer {
105
113
  const { done, value } = await reader.read();
106
114
  if (done || res.writableEnded)
107
115
  break;
116
+ armTimeout();
108
117
  if (!res.write(Buffer.from(value)))
109
118
  await once(res, 'drain');
110
119
  }
@@ -112,16 +121,20 @@ export class MeshServer {
112
121
  res.end();
113
122
  }
114
123
  catch (err) {
115
- if (ctrl.signal.aborted)
124
+ if (clientClosed)
116
125
  return;
117
126
  if (!res.headersSent) {
118
127
  res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
119
- res.end(`robot camera unavailable: ${err instanceof Error ? err.message : String(err)}`);
128
+ res.end(`robot camera unavailable: ${ctrl.signal.aborted ? 'upstream frame timeout' : (err instanceof Error ? err.message : String(err))}`);
120
129
  }
121
130
  else if (!res.writableEnded) {
122
131
  res.end();
123
132
  }
124
133
  }
134
+ finally {
135
+ if (timeout)
136
+ clearTimeout(timeout);
137
+ }
125
138
  }
126
139
  streamLocalRoboVision(req, res) {
127
140
  void this.streamHttp(req, res, 'http://127.0.0.1:5000/video_feed');
@@ -82,6 +82,12 @@ motion detection consume a shared latest-frame stream. Starting one
82
82
  `VideoCapture.read()` calls, which can block the second client and make the hub
83
83
  relay time out with zero bytes.
84
84
 
85
+ Long-running camera operation also requires recovery at every layer: the Pi
86
+ releases and reopens a V4L2 handle after an eight-second blocked read, the mesh
87
+ relay returns an error after ten seconds without an upstream frame, and both
88
+ drawer and fullscreen camera views reconnect every three seconds. A camera
89
+ request must never remain in an unbounded `connecting` state.
90
+
85
91
  ### Duplicate Speaker Tests
86
92
 
87
93
  Only one robot process may claim a scheduler `speaker_test`. The scheduler
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.89",
3
+ "version": "2.8.90",
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",
@@ -23,6 +23,8 @@ frame_condition = threading.Condition()
23
23
  latest_frame_bytes = None
24
24
  latest_frame_sequence = 0
25
25
  camera_worker_started = False
26
+ camera_read_started_at = 0.0
27
+ camera_last_frame_at = 0.0
26
28
  audio_input_device = "default"
27
29
  audio_output_device = "default"
28
30
  ROBOVISION_AUDIO_URL = os.getenv("ROBOVISION_AUDIO_URL", "http://127.0.0.1:8000")
@@ -233,6 +235,7 @@ def camera_worker():
233
235
  global latest_detections, motion_detection_active, motion_detected_state
234
236
  global last_motion_time, motion_frame_buffer
235
237
  global latest_frame_bytes, latest_frame_sequence
238
+ global camera_read_started_at, camera_last_frame_at, camera
236
239
 
237
240
  prev_gray = None
238
241
 
@@ -243,11 +246,24 @@ def camera_worker():
243
246
  time.sleep(1)
244
247
  continue
245
248
 
246
- with camera_lock:
249
+ camera_read_started_at = time.monotonic()
250
+ try:
251
+ # This is the only camera reader. Do not hold camera_lock here:
252
+ # the watchdog must be able to release a wedged V4L2 handle.
247
253
  success, frame = cam.read()
254
+ except Exception as exc:
255
+ print(f"Camera read error: {exc}")
256
+ success, frame = False, None
257
+ finally:
258
+ camera_read_started_at = 0.0
248
259
  if not success:
260
+ with camera_lock:
261
+ if camera is cam:
262
+ camera.release()
263
+ camera = None
249
264
  time.sleep(0.1)
250
265
  continue
266
+ camera_last_frame_at = time.monotonic()
251
267
 
252
268
  # Run object detection
253
269
  processed_frame, detections = detect_objects(frame, conf_threshold=0.5)
@@ -312,6 +328,25 @@ def _ensure_camera_worker():
312
328
  return
313
329
  camera_worker_started = True
314
330
  threading.Thread(target=camera_worker, name='robovision-camera', daemon=True).start()
331
+ threading.Thread(target=_camera_watchdog, name='robovision-camera-watchdog', daemon=True).start()
332
+
333
+
334
+ def _camera_watchdog():
335
+ global camera, camera_read_started_at
336
+ while True:
337
+ time.sleep(2.0)
338
+ started = camera_read_started_at
339
+ if not started or time.monotonic() - started < 8.0:
340
+ continue
341
+ print("Camera read stalled for 8s; releasing V4L2 handle")
342
+ with camera_lock:
343
+ if camera is not None:
344
+ try:
345
+ camera.release()
346
+ except Exception:
347
+ pass
348
+ camera = None
349
+ camera_read_started_at = 0.0
315
350
 
316
351
 
317
352
  def generate_frames():
@@ -339,10 +374,23 @@ def index():
339
374
  def video_feed():
340
375
  return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
341
376
 
342
- @app.route('/api/detections')
343
- def get_detections():
344
- with lock:
345
- return jsonify(latest_detections)
377
+ @app.route('/api/detections')
378
+ def get_detections():
379
+ with lock:
380
+ return jsonify(latest_detections)
381
+
382
+
383
+ @app.route('/api/camera/status')
384
+ def camera_status():
385
+ age = None if not camera_last_frame_at else round(time.monotonic() - camera_last_frame_at, 3)
386
+ return jsonify({
387
+ "device": str(current_camera_index),
388
+ "open": bool(camera is not None and camera.isOpened()),
389
+ "worker_started": camera_worker_started,
390
+ "frame_sequence": latest_frame_sequence,
391
+ "last_frame_age_seconds": age,
392
+ "read_stalled": bool(camera_read_started_at and time.monotonic() - camera_read_started_at >= 8.0),
393
+ })
346
394
 
347
395
  @app.route('/api/caption')
348
396
  def get_caption():