infinicode 2.8.106 → 2.8.108
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.
|
@@ -825,6 +825,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
825
825
|
<div class="section"><h3>Live insights</h3><div class="pk-camera-insight" id="pk-camera-ops-insights"></div></div>
|
|
826
826
|
<div class="section"><h3>Conversation pipeline</h3><div class="pk-camera-pipeline" id="pk-camera-ops-pipeline"></div></div>
|
|
827
827
|
<div class="section"><h3>Hardware and media</h3><div class="pk-camera-insight" id="pk-camera-ops-hardware"></div></div>
|
|
828
|
+
<div class="section"><h3>Motor operations</h3><div class="rp-field"><label>Saved sequence</label><select id="pk-camera-ops-motor-sequence"></select></div><div class="pk-ctl"><button class="rp-btn" id="pk-camera-ops-motor-discover">Discover relays</button><button class="rp-btn primary" id="pk-camera-ops-motor-run">Run sequence</button><button class="rp-btn danger" id="pk-camera-ops-motor-test">Test registered GPIO</button></div><div class="pk-motor-status" id="pk-camera-ops-motor-status">Loading robot motor profile...</div></div>
|
|
828
829
|
<div class="section"><h3>Operator note</h3><div class="pk-note">Video is relayed directly from the robot's RoboVision MJPEG service through the authenticated mesh. Voice remains on LiveKit, avoiding camera ownership conflicts.</div></div>
|
|
829
830
|
</aside>
|
|
830
831
|
</div>
|
|
@@ -1278,6 +1279,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1278
1279
|
else if(Number.isFinite(enrolled)) score+=enrolled;
|
|
1279
1280
|
return score;
|
|
1280
1281
|
}
|
|
1282
|
+
function schedulerHeartbeatFresh(device){
|
|
1283
|
+
var heartbeat=Date.parse(device&&device.last_heartbeat||'');
|
|
1284
|
+
return Number.isFinite(heartbeat)&&(Date.now()-heartbeat)<45000;
|
|
1285
|
+
}
|
|
1281
1286
|
var voiceStacks = []; // scheduler voice stacks (shared with setup forms + templates)
|
|
1282
1287
|
var FALLBACK_SLOTS = [
|
|
1283
1288
|
{label:'VOLT', id:'volt', keys:['volt'], nx:0.18, ny:0.26, img:'/static/robopark/volt.png'},
|
|
@@ -6829,7 +6834,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6829
6834
|
}
|
|
6830
6835
|
$('pk-name').textContent=n.label||n.name;
|
|
6831
6836
|
$('pk-sub').textContent = n.connected ? (n.name+' · '+(n.platform||'?')+' · v'+n.version+(n.site?(' · '+n.site):'')) : 'pad reserved · not joined';
|
|
6832
|
-
var
|
|
6837
|
+
var initialDevice=schedulerDevice(n),initialHeartbeat=schedulerHeartbeatFresh(initialDevice);
|
|
6838
|
+
var st=$('pk-state'); st.textContent=initialHeartbeat?'live':(n.connected?'mesh only':'offline'); st.className='pk-pill '+(initialHeartbeat?'live':'idle');
|
|
6833
6839
|
buildDrawer(n);
|
|
6834
6840
|
pollRoboparkQuiet(); // fill session telemetry right away instead of waiting for the tick
|
|
6835
6841
|
if(drawerTimer) clearInterval(drawerTimer);
|
|
@@ -7635,10 +7641,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7635
7641
|
}
|
|
7636
7642
|
function motorEditorAddMotor(){captureMotorEditor();var number=motorProfile.registry.length+1;motorProfile.registry.push({id:'motor-'+number,name:'Motor '+number,gpio:Math.min(27,Math.max(2,number+4)),active_high:true,max_duration_ms:3000});renderMotorEditor();}
|
|
7637
7643
|
function motorEditorAddStep(){captureMotorEditor();if(!motorProfile.registry.length){motorStatus('Register a motor before adding a timed node','bad');return;}motorEditorSequence().steps.push({motor_id:motorProfile.registry[0].id,delay_ms:0,duration_ms:300});renderMotorEditor();}
|
|
7638
|
-
function motorStatus(message,kind){
|
|
7644
|
+
function motorStatus(message,kind){['pk-motor-status','pk-camera-ops-motor-status'].forEach(function(id){var host=$(id);if(host){host.textContent=message;host.className='pk-motor-status '+(kind||'');}});}
|
|
7645
|
+
function motorErrorMessage(error){var message=error&&error.message?error.message:String(error||'unknown error');return /(?:\s|^)404(?:\s|$)|not found/i.test(message)?'Motor API is not loaded on the running livekit-1 scheduler. Update and restart the scheduler, then retry.':message;}
|
|
7639
7646
|
function loadMotorProfile(n){
|
|
7640
7647
|
var robotId=schedId(n);motorStatus('Loading saved motor profile...');
|
|
7641
|
-
getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||data.motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();if(motorProfile.registry.length){motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');return;}motorStatus('No central registry; discovering robot-local relays...');return discoverMotorRelays(n,true);}).catch(function(error){motorStatus('Motor profile unavailable: '+error
|
|
7648
|
+
getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||data.motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();if(motorProfile.registry.length){motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');return;}motorStatus('No central registry; discovering robot-local relays...');return discoverMotorRelays(n,true);}).catch(function(error){motorStatus('Motor profile unavailable: '+motorErrorMessage(error),'bad');});
|
|
7642
7649
|
}
|
|
7643
7650
|
function saveMotorProfile(n){
|
|
7644
7651
|
captureMotorEditor();var robotId=schedId(n),url=motorProfile.motor_server_url;
|
|
@@ -7665,7 +7672,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7665
7672
|
}
|
|
7666
7673
|
function testAllMotorRelays(n){
|
|
7667
7674
|
captureMotorEditor();var ready=motorProfile.registry.length?Promise.resolve():discoverMotorRelays(n,true);
|
|
7668
|
-
ready.then(function(){if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return null;var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');return saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/test-all?duration_ms=300&pause_ms=200',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','gpio-test-all');}).then(function(result){motorStatus(result.ok?'Full registered GPIO test completed: '+(result.completed_steps||[]).length+' relay(s).':'GPIO test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');});}).catch(function(error){motorStatus('GPIO test failed: '+error
|
|
7675
|
+
ready.then(function(){if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return null;var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');return saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/test-all?duration_ms=300&pause_ms=200',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','gpio-test-all');}).then(function(result){motorStatus(result.ok?'Full registered GPIO test completed: '+(result.completed_steps||[]).length+' relay(s).':'GPIO test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');});}).catch(function(error){motorStatus('GPIO test failed: '+motorErrorMessage(error),'bad');});
|
|
7669
7676
|
}
|
|
7670
7677
|
|
|
7671
7678
|
var pipelineHistoryPage=1;
|
|
@@ -7834,7 +7841,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7834
7841
|
set('pk-tv-lat', row.avg_latency_ms!=null?Math.round(row.avg_latency_ms)+'ms':'—');
|
|
7835
7842
|
var dEl=$('pk-tv-drop'); if(dEl){ var d=row.drop_rate!=null?Math.round(row.drop_rate*100):null; dEl.textContent=d!=null?d+'%':'—'; dEl.className='v '+(d>=20?'bad':d>=5?'warn':'ok'); }
|
|
7836
7843
|
var active=row.active_session||row.in_session||row.session_active||row.current_session_id||row.status==='connecting'||row.status==='running';
|
|
7837
|
-
var
|
|
7844
|
+
var heartbeatLive=schedulerHeartbeatFresh(schedulerDevice(n));
|
|
7845
|
+
var st=$('pk-state'); if(st){ st.textContent=active?'in session':(heartbeatLive?'live':(n.connected?'mesh only':'offline')); st.className='pk-pill '+(active||heartbeatLive?'live':'idle'); }
|
|
7838
7846
|
}
|
|
7839
7847
|
}
|
|
7840
7848
|
function set(id,v){ var e=$(id); if(e) e.textContent=v; }
|
|
@@ -7856,20 +7864,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7856
7864
|
var stages=[[currentStageLabel,true,'ACTIVE'],['Robot online',!!n.connected,'ready'],['Camera streaming',cameraLive,'ready'],['Motion detector armed',!!device.production_mode,'armed'],['Scheduler session',active,'active']],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s,index){return '<div class="'+(index===0?'fleet-stage-focus '+esc(currentStageStatus):'')+'"><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?s[2]:'waiting')+'</b></div>';}).join('');
|
|
7857
7865
|
var inv=device.device_inventory||{},mh=inv.media_health||{},hardware=$('pk-camera-ops-hardware');if(hardware)hardware.innerHTML='<span>CPU</span><b>'+(hw.cpu&&hw.cpu.percent!=null?Math.round(hw.cpu.percent)+'%':'--')+'</b><span>Memory</span><b>'+(hw.memory&&hw.memory.percent!=null?Math.round(hw.memory.percent)+'%':'--')+'</b><span>Temperature</span><b>'+(hw.temperatureC!=null?Math.round(hw.temperatureC)+'C':'--')+'</b><span>Camera inputs</span><b>'+((inv.video||[]).length||0)+'</b><span>Microphones</span><b>'+((inv.audio_input||[]).length||0)+'</b><span>Speaker outputs</span><b>'+((inv.audio_output||[]).length||0)+'</b><span>Camera permission</span><b class="'+(mh.camera_access===false?'bad':'ok')+'">'+(mh.camera_access===false?'DENIED':mh.camera_access===true?'granted':'--')+'</b><span>Audio permission</span><b class="'+(mh.audio_access===false?'bad':'ok')+'">'+(mh.audio_access===false?'DENIED':mh.audio_access===true?'granted':'--')+'</b><span>Camera worker</span><b class="'+(mh.camera_stalled?'bad':'ok')+'">'+(mh.camera_stalled?'STALLED':mh.camera_worker?'streaming':'--')+'</b><span>Frame age</span><b>'+(mh.camera_frame_age_seconds!=null?Number(mh.camera_frame_age_seconds).toFixed(1)+'s':'--')+'</b><span>Selected camera</span><b>'+esc(device.video_device||'auto')+'</b><span>Selected microphone</span><b>'+esc(device.audio_device||'default')+'</b>';
|
|
7858
7866
|
var prod=$('pk-camera-ops-production');if(prod){prod.textContent=device.production_mode?'Disable production':'Enable production';prod.classList.toggle('primary',!!device.production_mode);}
|
|
7867
|
+
var motorSelect=$('pk-camera-ops-motor-sequence');if(motorSelect){var selected=motorSelect.value||motorSequenceId;motorSelect.innerHTML=(motorProfile.sequences||[]).map(function(sequence){return '<option value="'+esc(sequence.id)+'">'+esc(sequence.name||sequence.id)+'</option>';}).join('')||'<option value="">No saved sequences</option>';if(selected)motorSelect.value=selected;}
|
|
7859
7868
|
}
|
|
7860
7869
|
function reloadCameraOperations(){
|
|
7861
7870
|
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...';}
|
|
7862
7871
|
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();
|
|
7863
7872
|
}
|
|
7864
7873
|
function openCameraOperations(n){
|
|
7865
|
-
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);
|
|
7874
|
+
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';}}loadMotorProfile(n);reloadCameraOperations();if(cameraOpsTimer)clearInterval(cameraOpsTimer);cameraOpsTimer=setInterval(refreshCameraOperations,1000);
|
|
7866
7875
|
}
|
|
7867
7876
|
function closeCameraOperations(){
|
|
7868
7877
|
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(){});
|
|
7869
7878
|
}
|
|
7870
7879
|
(function bindCameraOperations(){
|
|
7871
|
-
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');
|
|
7880
|
+
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'),motorDiscover=$('pk-camera-ops-motor-discover'),motorRun=$('pk-camera-ops-motor-run'),motorTest=$('pk-camera-ops-motor-test'),motorSelect=$('pk-camera-ops-motor-sequence');
|
|
7872
7881
|
if(close)close.addEventListener('click',closeCameraOperations);if(refresh)refresh.addEventListener('click',reloadCameraOperations);if(nativeBtn)nativeBtn.addEventListener('click',function(){var host=$('pk-camera-ops');if(host&&host.requestFullscreen)host.requestFullscreen().catch(function(e){showOut('fullscreen: '+e.message);});});if(trigger)trigger.addEventListener('click',function(){if(cameraOpsNode)triggerSession(schedId(cameraOpsNode));});if(prod)prod.addEventListener('click',function(){if(cameraOpsNode){var d=schedulerDevice(cameraOpsNode)||{};setProductionMode(cameraOpsNode,!d.production_mode);setTimeout(refreshCameraOperations,400);}});if(end)end.addEventListener('click',function(){if(cameraOpsNode)endSession(schedId(cameraOpsNode));});document.addEventListener('keydown',function(e){if(e.key==='Escape'&&cameraOpsNode)closeCameraOperations();});
|
|
7882
|
+
if(motorDiscover)motorDiscover.addEventListener('click',function(){if(cameraOpsNode)discoverMotorRelays(cameraOpsNode,false).catch(function(error){motorStatus('Discovery failed: '+error.message,'bad');});});if(motorRun)motorRun.addEventListener('click',function(){if(cameraOpsNode)runMotorSequence(cameraOpsNode);});if(motorTest)motorTest.addEventListener('click',function(){if(cameraOpsNode)testAllMotorRelays(cameraOpsNode);});if(motorSelect)motorSelect.addEventListener('change',function(){motorSequenceId=motorSelect.value;renderMotorEditor();refreshCameraOperations();});
|
|
7873
7883
|
})();
|
|
7874
7884
|
|
|
7875
7885
|
// ── cam: target a LiveKit track; fall back to a styled preview ──
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
11
|
import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
-
import { homedir } from 'node:os';
|
|
12
|
+
import { homedir, networkInterfaces } from 'node:os';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import chalk from 'chalk';
|
|
@@ -18,6 +18,122 @@ const VISION_URL = 'http://127.0.0.1:5000/api/media/inventory';
|
|
|
18
18
|
const VISION_STATUS_URL = 'http://127.0.0.1:5000/api/camera/status';
|
|
19
19
|
const RESTART_DELAY_MS = 5_000;
|
|
20
20
|
const MEDIA_WATCHDOG_INTERVAL_MS = 10_000;
|
|
21
|
+
const SCHEDULER_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
22
|
+
function currentLanIp() {
|
|
23
|
+
for (const addresses of Object.values(networkInterfaces())) {
|
|
24
|
+
for (const address of addresses ?? []) {
|
|
25
|
+
if (address.family === 'IPv4' && !address.internal)
|
|
26
|
+
return address.address;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
function readSchedulerIdentity() {
|
|
32
|
+
const configDir = join(homedir(), '.robopark');
|
|
33
|
+
try {
|
|
34
|
+
const config = JSON.parse(readFileSync(join(configDir, 'preview_agent.json'), 'utf8'));
|
|
35
|
+
const deviceId = String(config.device_id ?? '').trim();
|
|
36
|
+
const fileToken = existsSync(join(configDir, 'device_token'))
|
|
37
|
+
? readFileSync(join(configDir, 'device_token'), 'utf8').trim()
|
|
38
|
+
: '';
|
|
39
|
+
const deviceToken = fileToken || String(config.device_token ?? '').trim();
|
|
40
|
+
if (deviceId && deviceToken)
|
|
41
|
+
return { deviceId, deviceToken };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// First boot has no identity yet.
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function persistSchedulerIdentity(identity, schedulerUrl) {
|
|
49
|
+
const configDir = join(homedir(), '.robopark');
|
|
50
|
+
const configPath = join(configDir, 'preview_agent.json');
|
|
51
|
+
mkdirSync(configDir, { recursive: true });
|
|
52
|
+
let config = {};
|
|
53
|
+
try {
|
|
54
|
+
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
catch { /* first boot */ }
|
|
57
|
+
config.device_id = identity.deviceId;
|
|
58
|
+
config.device_token = identity.deviceToken;
|
|
59
|
+
config.scheduler_url = schedulerUrl;
|
|
60
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
61
|
+
writeFileSync(join(configDir, 'device_token'), identity.deviceToken, { encoding: 'utf8', mode: 0o600 });
|
|
62
|
+
}
|
|
63
|
+
function startSchedulerHeartbeat(opts, livekitUrl) {
|
|
64
|
+
let identity = readSchedulerIdentity();
|
|
65
|
+
let stopped = false;
|
|
66
|
+
let inFlight = false;
|
|
67
|
+
const bootstrap = async () => {
|
|
68
|
+
const response = await fetch(`${opts.schedulerUrl.replace(/\/+$/, '')}/api/devices/bootstrap`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'content-type': 'application/json',
|
|
72
|
+
'x-robopark-mesh-token': opts.token,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({ name: opts.name, lan_ip: currentLanIp(), livekit_url: livekitUrl }),
|
|
75
|
+
signal: AbortSignal.timeout(10_000),
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
throw new Error(`bootstrap HTTP ${response.status}`);
|
|
79
|
+
const data = await response.json();
|
|
80
|
+
const next = {
|
|
81
|
+
deviceId: String(data.device_id ?? '').trim(),
|
|
82
|
+
deviceToken: String(data.device_token ?? '').trim(),
|
|
83
|
+
};
|
|
84
|
+
if (!next.deviceId || !next.deviceToken)
|
|
85
|
+
throw new Error('bootstrap returned no device credentials');
|
|
86
|
+
persistSchedulerIdentity(next, opts.schedulerUrl);
|
|
87
|
+
return next;
|
|
88
|
+
};
|
|
89
|
+
const beat = async () => {
|
|
90
|
+
if (stopped || inFlight)
|
|
91
|
+
return;
|
|
92
|
+
inFlight = true;
|
|
93
|
+
try {
|
|
94
|
+
identity = readSchedulerIdentity() ?? identity;
|
|
95
|
+
if (!identity)
|
|
96
|
+
identity = await bootstrap();
|
|
97
|
+
let response = await fetch(`${opts.schedulerUrl.replace(/\/+$/, '')}/api/devices/${encodeURIComponent(identity.deviceId)}/heartbeat`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
authorization: `Bearer ${identity.deviceToken}`,
|
|
101
|
+
'content-type': 'application/json',
|
|
102
|
+
'x-robopark-mesh-token': opts.token,
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({ status: 'online', ip: currentLanIp(), livekit_url: livekitUrl }),
|
|
105
|
+
signal: AbortSignal.timeout(10_000),
|
|
106
|
+
});
|
|
107
|
+
if (response.status === 401 || response.status === 404) {
|
|
108
|
+
identity = await bootstrap();
|
|
109
|
+
response = await fetch(`${opts.schedulerUrl.replace(/\/+$/, '')}/api/devices/${encodeURIComponent(identity.deviceId)}/heartbeat`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: {
|
|
112
|
+
authorization: `Bearer ${identity.deviceToken}`,
|
|
113
|
+
'content-type': 'application/json',
|
|
114
|
+
'x-robopark-mesh-token': opts.token,
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({ status: 'online', ip: currentLanIp(), livekit_url: livekitUrl }),
|
|
117
|
+
signal: AbortSignal.timeout(10_000),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (!response.ok)
|
|
121
|
+
throw new Error(`heartbeat HTTP ${response.status}`);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error(chalk.yellow(` scheduler heartbeat watchdog: ${error instanceof Error ? error.message : String(error)}`));
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
inFlight = false;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
void beat();
|
|
131
|
+
const timer = setInterval(() => { void beat(); }, SCHEDULER_HEARTBEAT_INTERVAL_MS);
|
|
132
|
+
return () => {
|
|
133
|
+
stopped = true;
|
|
134
|
+
clearInterval(timer);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
21
137
|
function resetStaleEnrollment() {
|
|
22
138
|
const configDir = join(homedir(), '.robopark');
|
|
23
139
|
const tokenPath = join(configDir, 'device_token');
|
|
@@ -288,6 +404,7 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
288
404
|
}
|
|
289
405
|
// Identity and inventory reporting start immediately. RoboVision may still
|
|
290
406
|
// be installing or recovering without making the robot disappear.
|
|
407
|
+
const stopSchedulerHeartbeat = startSchedulerHeartbeat(opts, livekitUrl);
|
|
291
408
|
start(children[0]);
|
|
292
409
|
start(preview);
|
|
293
410
|
if (opts.vision !== false) {
|
|
@@ -303,6 +420,7 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
303
420
|
const mediaWatchdog = setInterval(() => { void monitorVision(); }, MEDIA_WATCHDOG_INTERVAL_MS);
|
|
304
421
|
const shutdown = () => {
|
|
305
422
|
stopping = true;
|
|
423
|
+
stopSchedulerHeartbeat();
|
|
306
424
|
clearInterval(mediaWatchdog);
|
|
307
425
|
for (const entry of [...children, preview])
|
|
308
426
|
signalChild(entry, 'SIGTERM');
|
package/package.json
CHANGED
|
@@ -634,10 +634,20 @@ class PreviewAgent:
|
|
|
634
634
|
# Consume it before generic mesh recovery so heartbeats cannot bind to
|
|
635
635
|
# a stale same-name identity.
|
|
636
636
|
if not self.device_token and enrollment_token:
|
|
637
|
-
|
|
638
|
-
self.
|
|
639
|
-
|
|
640
|
-
|
|
637
|
+
try:
|
|
638
|
+
self.device_id, self.device_token = await _enroll(
|
|
639
|
+
self.scheduler_url, enrollment_token, self.robot_id
|
|
640
|
+
)
|
|
641
|
+
self._mesh_bootstrap_ready = True
|
|
642
|
+
except Exception as exc:
|
|
643
|
+
if not _mesh_proxy_headers():
|
|
644
|
+
raise
|
|
645
|
+
# Enrollment tokens are intentionally one-time. A service
|
|
646
|
+
# reinstall can retain the original systemd argument after
|
|
647
|
+
# its credential file was removed; recover through the hub's
|
|
648
|
+
# authenticated mesh path rather than crash-loop forever.
|
|
649
|
+
logger.warning("device enrollment failed; recovering through mesh bootstrap: %s", exc)
|
|
650
|
+
await self._ensure_mesh_identity()
|
|
641
651
|
elif _mesh_proxy_headers():
|
|
642
652
|
await self._ensure_mesh_identity()
|
|
643
653
|
|