infinicode 2.8.73 → 2.8.76
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/bin/robopark.js +2 -2
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +227 -55
- package/dist/robopark/robot-runtime.js +3 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +103 -20
- package/packages/robopark/scheduler/preview_agent.py +80 -24
- package/packages/robopark/scheduler/robot_supervisor.py +41 -16
|
@@ -691,6 +691,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
691
691
|
@media(max-width:900px){.rp-config-columns{grid-template-columns:1fr}.rp-config-hero{align-items:flex-start;flex-direction:column}.rp-config-cards{max-height:430px}}
|
|
692
692
|
@media(max-width:620px){.rp-config-fields{grid-template-columns:1fr}.rp-config-fields .wide{grid-column:auto}.rp-config-pipeline{grid-template-columns:1fr}.rp-config-pipeline i{display:none}.rp-config-hero-actions{width:100%}.rp-config-hero-actions .rp-btn{flex:1}}
|
|
693
693
|
|
|
694
|
+
/* command sheet */
|
|
695
|
+
.rp-command-shell{display:grid;gap:1rem}.rp-command-env{display:grid;grid-template-columns:repeat(3,minmax(170px,1fr));gap:.65rem;padding:.85rem;border:1px solid var(--glass-border);border-radius:16px;background:var(--panel-2)}
|
|
696
|
+
.rp-command-toolbar{display:flex;gap:.55rem;align-items:center;flex-wrap:wrap}.rp-command-toolbar input{flex:1;min-width:220px}.rp-command-toolbar select{width:auto;min-width:170px}
|
|
697
|
+
.rp-command-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:.75rem}.rp-command-card{display:flex;flex-direction:column;gap:.55rem;min-width:0;padding:.85rem;border:1px solid var(--glass-border);border-radius:16px;background:linear-gradient(145deg,rgba(31,26,18,.66),rgba(9,8,6,.76));box-shadow:inset 0 1px 0 var(--glass-hi),0 12px 32px rgba(0,0,0,.22)}
|
|
698
|
+
.rp-command-head{display:flex;align-items:flex-start;justify-content:space-between;gap:.6rem}.rp-command-head b{font-family:var(--mono);font-size:.74rem;color:var(--gold-pale)}.rp-command-tag{font-family:var(--mono);font-size:.52rem;text-transform:uppercase;letter-spacing:.06em;color:var(--gold-bright);border:1px solid var(--border-strong);border-radius:100px;padding:.15rem .42rem;white-space:nowrap}.rp-command-card p{font-size:.64rem;line-height:1.5;color:var(--muted);min-height:2.9em}
|
|
699
|
+
.rp-command-code{position:relative;min-height:86px;padding:.68rem .72rem;border:1px solid rgba(255,224,151,.1);border-radius:11px;background:#050504;overflow:auto}.rp-command-code code{display:block;font-family:var(--mono);font-size:.59rem;line-height:1.55;color:#f2dfab;white-space:pre-wrap;overflow-wrap:anywhere}.rp-command-actions{display:flex;gap:.4rem;align-items:center;margin-top:auto}.rp-command-actions .rp-btn:first-child{flex:1}.rp-command-empty{grid-column:1/-1;padding:2rem;text-align:center;color:var(--muted);border:1px dashed var(--glass-border);border-radius:16px}
|
|
700
|
+
.rp-command-custom{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;padding:.9rem;border:1px solid var(--glass-border);border-radius:16px;background:var(--panel-2)}.rp-command-custom h4,.rp-command-custom .wide{grid-column:1/-1}.rp-command-custom h4{font-family:var(--mono);font-size:.72rem;color:var(--gold-bright)}.rp-command-custom textarea{min-height:92px;resize:vertical}.rp-command-note{font-size:.61rem;color:var(--muted)}
|
|
701
|
+
@media(max-width:800px){.rp-command-env{grid-template-columns:1fr 1fr}.rp-command-grid{grid-template-columns:1fr}}@media(max-width:540px){.rp-command-env,.rp-command-custom{grid-template-columns:1fr}.rp-command-custom h4,.rp-command-custom .wide{grid-column:auto}}
|
|
702
|
+
|
|
694
703
|
.rp-fleet-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:0.7rem;padding:0;}
|
|
695
704
|
.rp-fleet-card{position:relative;}
|
|
696
705
|
.rp-fleet-card .spark{margin-top:0.5rem;height:36px;}
|
|
@@ -831,6 +840,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
831
840
|
<button class="rp-subtab" data-sub="audit" id="rpsbtn-audit" role="tab" aria-selected="false">Audit Log</button>
|
|
832
841
|
<button class="rp-subtab" data-sub="fleet" id="rpsbtn-fleet" role="tab" aria-selected="false">Fleet</button>
|
|
833
842
|
<button class="rp-subtab" data-sub="config" id="rpsbtn-config" role="tab" aria-selected="false">Characters & Voice</button>
|
|
843
|
+
<button class="rp-subtab" data-sub="commands" id="rpsbtn-commands" role="tab" aria-selected="false">Commands</button>
|
|
834
844
|
<button class="rp-subtab" data-sub="setup" id="rpsbtn-setup" role="tab" aria-selected="false">Setup</button>
|
|
835
845
|
</div>
|
|
836
846
|
|
|
@@ -1072,7 +1082,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1072
1082
|
</div>
|
|
1073
1083
|
|
|
1074
1084
|
<!-- ═══ Setup ═══ -->
|
|
1075
|
-
<div class="rp-sub" id="rp-sub-setup">
|
|
1085
|
+
<div class="rp-sub" id="rp-sub-setup">
|
|
1076
1086
|
<!-- Add LiveKit server -->
|
|
1077
1087
|
<div class="rp-form" id="rp-addserver">
|
|
1078
1088
|
<h3>Add LiveKit server</h3>
|
|
@@ -1132,6 +1142,38 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1132
1142
|
|
|
1133
1143
|
</div>
|
|
1134
1144
|
|
|
1145
|
+
<!-- Command sheet -->
|
|
1146
|
+
<div class="rp-sub" id="rp-sub-commands">
|
|
1147
|
+
<div class="rp-command-shell">
|
|
1148
|
+
<div class="rp-config-hero">
|
|
1149
|
+
<div><h3>RoboPark Command Sheet</h3><p>Production one-liners for the hub, LAN and Tailscale robots, health checks, recovery, Docker, LiveKit, and package operations. Edit the environment once; every generated command below updates immediately.</p></div>
|
|
1150
|
+
<div class="rp-config-hero-actions"><button class="rp-btn" id="rp-command-reset">Reset environment</button><button class="rp-btn primary" id="rp-command-copy-all">Copy visible commands</button></div>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div class="rp-command-env">
|
|
1153
|
+
<div class="rp-field"><label>Hub LAN IP</label><input id="rp-command-lan" value="192.168.1.12" spellcheck="false"></div>
|
|
1154
|
+
<div class="rp-field"><label>Hub Tailscale IP / DNS</label><input id="rp-command-tail" value="100.84.147.24" spellcheck="false"></div>
|
|
1155
|
+
<div class="rp-field"><label>Site</label><input id="rp-command-site" value="Tel Aviv"></div>
|
|
1156
|
+
<div class="rp-field"><label>Robot name</label><input id="rp-command-robot" value="bmw" spellcheck="false"></div>
|
|
1157
|
+
<div class="rp-field"><label>Enrollment token</label><input id="rp-command-enrollment" placeholder="paste robot enrollment token" spellcheck="false"></div>
|
|
1158
|
+
<div class="rp-field"><label>Mesh token</label><input id="rp-command-token" spellcheck="false"></div>
|
|
1159
|
+
</div>
|
|
1160
|
+
<div class="rp-command-toolbar">
|
|
1161
|
+
<input type="search" id="rp-command-search" placeholder="Search setup, logs, camera, Docker, LiveKit, npm...">
|
|
1162
|
+
<select id="rp-command-category"><option value="">All categories</option><option>Provision</option><option>Health</option><option>Recovery</option><option>Media</option><option>Packages</option><option>Custom</option></select>
|
|
1163
|
+
<span class="rp-command-note" id="rp-command-count"></span>
|
|
1164
|
+
</div>
|
|
1165
|
+
<div class="rp-command-grid" id="rp-command-grid"></div>
|
|
1166
|
+
<form class="rp-command-custom" id="rp-command-custom-form">
|
|
1167
|
+
<h4>Add a persistent command</h4>
|
|
1168
|
+
<div class="rp-field"><label>Title</label><input id="rp-command-custom-title" required placeholder="Restart camera service"></div>
|
|
1169
|
+
<div class="rp-field"><label>Category</label><input id="rp-command-custom-category" value="Custom" placeholder="Custom"></div>
|
|
1170
|
+
<div class="rp-field wide"><label>Simple explanation</label><input id="rp-command-custom-description" placeholder="When to use this and what success looks like"></div>
|
|
1171
|
+
<div class="rp-field wide"><label>Command</label><textarea id="rp-command-custom-command" required spellcheck="false" placeholder="Paste a safe command or one-liner"></textarea></div>
|
|
1172
|
+
<div class="wide"><button class="rp-btn primary" type="submit">Save command to this browser</button> <span class="rp-command-note">Custom commands persist in localStorage on this operator browser.</span></div>
|
|
1173
|
+
</form>
|
|
1174
|
+
</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
|
|
1135
1177
|
<!-- Character + voice management -->
|
|
1136
1178
|
<div class="rp-sub" id="rp-sub-config">
|
|
1137
1179
|
<div class="rp-config-workspace" id="rp-templates">
|
|
@@ -1782,7 +1824,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1782
1824
|
$('pane-fleet').classList.toggle('active', name==='fleet');
|
|
1783
1825
|
$('pane-park').classList.toggle('active', name==='park');
|
|
1784
1826
|
$('pane-robopark').classList.toggle('active', name==='robopark');
|
|
1785
|
-
|
|
1827
|
+
var currentHash=location.hash.replace('#','');
|
|
1828
|
+
var keepRpSubroute=name==='robopark' && currentHash.indexOf('robopark/')===0;
|
|
1829
|
+
if (!keepRpSubroute && currentHash !== name) { try{ history.replaceState(null,'','#'+name); }catch(e){} }
|
|
1786
1830
|
if (name==='robopark'){
|
|
1787
1831
|
pollRobopark();
|
|
1788
1832
|
initRpSubTabs();
|
|
@@ -1793,11 +1837,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1793
1837
|
var fleetSessionsBtn=$('fleet-open-sessions'); if(fleetSessionsBtn) fleetSessionsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('live');});
|
|
1794
1838
|
var fleetIncidentsBtn=$('fleet-open-incidents'); if(fleetIncidentsBtn) fleetIncidentsBtn.addEventListener('click',function(){selectTab('robopark');setRpSubTab('incidents');});
|
|
1795
1839
|
var fleetRefreshBtn=$('fleet-refresh'); if(fleetRefreshBtn) fleetRefreshBtn.addEventListener('click',function(){poll();pollRobopark();});
|
|
1796
|
-
var initHash = location.hash.replace('#','');
|
|
1797
|
-
// Defer tab init until Park module is defined (same outer IIFE, later in source).
|
|
1798
|
-
function initTab(){
|
|
1799
|
-
if (initHash==='robopark'||initHash
|
|
1800
|
-
|
|
1840
|
+
var initHash = location.hash.replace('#','');
|
|
1841
|
+
// Defer tab init until Park module is defined (same outer IIFE, later in source).
|
|
1842
|
+
function initTab(){
|
|
1843
|
+
if (initHash==='robopark'||initHash.indexOf('robopark/')===0) selectTab('robopark');
|
|
1844
|
+
else if(initHash==='park') selectTab('park');
|
|
1845
|
+
}
|
|
1801
1846
|
// initTab() is called at the very bottom of this IIFE, after Park is assigned.
|
|
1802
1847
|
|
|
1803
1848
|
function robotsFrom(d){
|
|
@@ -2549,7 +2594,74 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2549
2594
|
}
|
|
2550
2595
|
|
|
2551
2596
|
// ── Sub-tab routing ──
|
|
2552
|
-
|
|
2597
|
+
// Command sheet: generate production commands from one saved environment.
|
|
2598
|
+
var RP_COMMAND_ENV_KEY='rp-command-env-v1';
|
|
2599
|
+
var RP_CUSTOM_COMMANDS_KEY='rp-custom-commands-v1';
|
|
2600
|
+
var rpVisibleCommands=[];
|
|
2601
|
+
function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:TOKEN};}
|
|
2602
|
+
function rpLoadStoredJson(key,fallback){try{var value=JSON.parse(localStorage.getItem(key)||'null');return value&&typeof value==='object'?value:fallback;}catch(e){return fallback;}}
|
|
2603
|
+
function rpCommandEnv(){
|
|
2604
|
+
var d=rpCommandDefaults(),s=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
|
|
2605
|
+
function value(name){var input=$('rp-command-'+name),raw=input?input.value:(s[name]!=null?s[name]:d[name]);return String(raw==null?'':raw).trim();}
|
|
2606
|
+
return {lan:value('lan'),tail:value('tail'),site:value('site'),robot:value('robot'),enrollment:value('enrollment'),token:value('token')};
|
|
2607
|
+
}
|
|
2608
|
+
function rpSaveCommandEnv(){try{localStorage.setItem(RP_COMMAND_ENV_KEY,JSON.stringify(rpCommandEnv()));}catch(e){}}
|
|
2609
|
+
function rpShellArg(value){var text=String(value||'');return /^[A-Za-z0-9._:\/-]+$/.test(text)?text:'"'+text.replace(/"/g,'\\"')+'"';}
|
|
2610
|
+
function rpCommandCatalog(){
|
|
2611
|
+
var e=rpCommandEnv(),lan='http://'+e.lan,tail='http://'+e.tail,enroll=e.enrollment?' --enrollment-token '+rpShellArg(e.enrollment):'';
|
|
2612
|
+
return [
|
|
2613
|
+
{id:'hub-up',category:'Provision',title:'Start production hub and full dashboard',description:'Run elevated on livekit-1. Starts the mesh hub, full Park control center, scheduler proxy, LAN discovery, supervision, and updates.',command:'infinicode serve --hub --name livekit-1 --port 47913 --token '+rpShellArg(e.token)+' --lan --dashboard --scheduler-url http://127.0.0.1:8080 --auto-update --supervised'},
|
|
2614
|
+
{id:'robot-lan',category:'Provision',title:'Provision LAN robot',description:'One command for a Linux robot on the hub LAN. Installs the supervised mesh, RoboVision, and preview runtime.',command:'sudo robopark setup --robot --name '+rpShellArg(e.robot)+' --site '+rpShellArg(e.site)+' --hub-url '+lan+':47913 --token '+rpShellArg(e.token)+' --scheduler-port 8080'+enroll+' --port 47913 --lan --start --auto-start --yes'},
|
|
2615
|
+
{id:'robot-tail',category:'Provision',title:'Provision Tailscale robot',description:'One command for a Windows or Linux robot reaching the hub through Tailscale.',command:'robopark setup --robot --name '+rpShellArg(e.robot)+' --site '+rpShellArg(e.site)+' --hub-url '+tail+':47913 --token '+rpShellArg(e.token)+' --scheduler-port 8080'+enroll+' --port 47913 --tailscale --start --auto-start --yes'},
|
|
2616
|
+
{id:'operator-tail',category:'Provision',title:'Connect operator MCP over Tailscale',description:'Connects an operator workstation and coding agents to the production mesh.',command:'infinicode mcp --lan --tailscale --seed '+tail+':47913 --name dev-laptop --token '+rpShellArg(e.token)+' --mesh-port 47913'},
|
|
2617
|
+
{id:'hub-ui',category:'Health',title:'Open full production control center',description:'Opens the full Park dashboard through the hub Tailscale address.',command:'Start-Process "'+tail+':47913/?token='+encodeURIComponent(e.token)+'#park"'},
|
|
2618
|
+
{id:'hub-build',category:'Health',title:'Verify current hub UI',description:'Shows the installed version and dashboard build marker after npm update or restart.',command:'infinicode --version; Invoke-RestMethod "http://127.0.0.1:47913/fed/ui-build?token='+encodeURIComponent(e.token)+'"'},
|
|
2619
|
+
{id:'hub-ports',category:'Health',title:'Inspect hub ports on Windows',description:'Lists processes owning the control center, scheduler, and LiveKit ports without changing them.',command:'Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -in 47913,8080,7880,7881,7882,7883 } | Sort-Object LocalPort | Format-Table LocalAddress,LocalPort,OwningProcess -AutoSize'},
|
|
2620
|
+
{id:'scheduler-health',category:'Health',title:'Check scheduler through hub',description:'Confirms the hub proxy can reach the scheduler and return robot inventory.',command:'Invoke-RestMethod "'+tail+':47913/robopark/api/robots?token='+encodeURIComponent(e.token)+'" | ConvertTo-Json -Depth 6'},
|
|
2621
|
+
{id:'mesh-status',category:'Health',title:'Check mesh nodes',description:'Lists connected hub, robot, and operator nodes.',command:'Invoke-RestMethod "'+tail+':47913/fed/status?token='+encodeURIComponent(e.token)+'" | ConvertTo-Json -Depth 6'},
|
|
2622
|
+
{id:'pi-runtime',category:'Health',title:'Follow robot runtime logs',description:'Live systemd logs for the selected robot unified runtime.',command:'sudo journalctl -u robopark-robot-runtime-'+e.robot+'.service -f'},
|
|
2623
|
+
{id:'pi-ports',category:'Health',title:'Inspect robot service ports',description:'Checks mesh, camera, motion webhook, and audio listeners on a Linux robot.',command:"sudo ss -ltnp | grep -E ':(47913|5000|5057|8000)'"},
|
|
2624
|
+
{id:'camera-inventory',category:'Media',title:'Read RoboVision media inventory',description:'Returns the real cameras, microphones, and speakers detected by RoboVision.',command:'curl -s http://127.0.0.1:5000/api/media/inventory'},
|
|
2625
|
+
{id:'audio-inventory',category:'Media',title:'Read RoboVision audio devices',description:'Returns the microphone and speaker inventory used by the robot audio server.',command:'curl -s http://127.0.0.1:8000/devices'},
|
|
2626
|
+
{id:'camera-devices',category:'Media',title:'List valid Linux capture devices',description:'Shows V4L2 devices and capabilities so metadata-only nodes are not selected as cameras.',command:'v4l2-ctl --list-devices; for d in /dev/video*; do echo "--- $d"; v4l2-ctl -d "$d" --all 2>/dev/null | grep -E "Card type|Video Capture|Device Caps"; done'},
|
|
2627
|
+
{id:'camera-preview',category:'Media',title:'Open direct RoboVision camera feed',description:'Opens the robot MJPEG feed locally for camera-only diagnosis.',command:'xdg-open http://127.0.0.1:5000/video_feed'},
|
|
2628
|
+
{id:'robot-restart',category:'Recovery',title:'Clean restart selected robot',description:'Stops stale supervised processes, then recreates the complete LAN runtime.',command:'sudo robopark stop && sudo robopark setup --robot --name '+rpShellArg(e.robot)+' --site '+rpShellArg(e.site)+' --hub-url '+lan+':47913 --token '+rpShellArg(e.token)+' --scheduler-port 8080'+enroll+' --port 47913 --lan --start --auto-start --yes'},
|
|
2629
|
+
{id:'robot-status',category:'Recovery',title:'Inspect robot service failure',description:'Shows service state and the latest 150 log lines after a failed start.',command:'sudo systemctl status robopark-robot-runtime-'+e.robot+'.service --no-pager; sudo journalctl -u robopark-robot-runtime-'+e.robot+'.service -n 150 --no-pager'},
|
|
2630
|
+
{id:'docker-health',category:'Health',title:'Inspect Docker and LiveKit containers',description:'Shows container state, health, ports, and names on the hub.',command:'docker ps -a --format "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\\t{{.Image}}"'},
|
|
2631
|
+
{id:'docker-restart',category:'Recovery',title:'Restart Docker Desktop service',description:'Run from elevated PowerShell only when Docker Desktop is stuck starting.',command:'Restart-Service com.docker.service; Start-Process -FilePath "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"'},
|
|
2632
|
+
{id:'tailscale-status',category:'Health',title:'Check Tailscale routes and Serve',description:'Confirms the node is connected and shows any active Serve proxy.',command:'tailscale status; tailscale serve status'},
|
|
2633
|
+
{id:'npm-version',category:'Packages',title:'Check installed and published versions',description:'Compares this device with the npm latest tag.',command:'infinicode --version; npm view infinicode version'},
|
|
2634
|
+
{id:'npm-update',category:'Packages',title:'Update InfiniCode and RoboPark',description:'Installs the current published production package globally.',command:'npm install -g infinicode@latest --force'},
|
|
2635
|
+
{id:'npm-cache',category:'Packages',title:'Repair npm metadata cache',description:'Use when a new version returns ETARGET. Verify metadata before clearing anything.',command:'npm cache verify; npm view infinicode dist-tags --json'}
|
|
2636
|
+
];
|
|
2637
|
+
}
|
|
2638
|
+
function rpCustomCommands(){var list=rpLoadStoredJson(RP_CUSTOM_COMMANDS_KEY,[]);return Array.isArray(list)?list:[];}
|
|
2639
|
+
function rpSaveCustomCommands(list){try{localStorage.setItem(RP_CUSTOM_COMMANDS_KEY,JSON.stringify(list));}catch(e){showOut('Could not save custom command: '+e.message);}}
|
|
2640
|
+
function rpLegacyCopy(value){var area=document.createElement('textarea');area.value=value;area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();try{document.execCommand('copy');showOut('command copied');}catch(e){showOut('Copy failed: select the command manually');}area.remove();}
|
|
2641
|
+
function rpCopyCommand(text){var value=String(text||'');if(navigator.clipboard&&window.isSecureContext){navigator.clipboard.writeText(value).then(function(){showOut('command copied');}).catch(function(){rpLegacyCopy(value);});}else rpLegacyCopy(value);}
|
|
2642
|
+
function renderCommandSheet(){
|
|
2643
|
+
var grid=$('rp-command-grid');if(!grid)return;
|
|
2644
|
+
var search=String(($('rp-command-search')||{}).value||'').toLowerCase().trim(),category=String(($('rp-command-category')||{}).value||'').toLowerCase();
|
|
2645
|
+
var custom=rpCustomCommands().map(function(x){return {id:String(x.id),category:String(x.category||'Custom'),title:String(x.title||'Custom command'),description:String(x.description||''),command:String(x.command||''),custom:true};});
|
|
2646
|
+
rpVisibleCommands=rpCommandCatalog().concat(custom).filter(function(x){if(category&&x.category.toLowerCase()!==category)return false;return !search||(x.title+' '+x.category+' '+x.description+' '+x.command).toLowerCase().indexOf(search)>=0;});
|
|
2647
|
+
var count=$('rp-command-count');if(count)count.textContent=rpVisibleCommands.length+' command'+(rpVisibleCommands.length===1?'':'s');
|
|
2648
|
+
if(!rpVisibleCommands.length){grid.innerHTML='<div class="rp-command-empty">No commands match this filter.</div>';return;}
|
|
2649
|
+
grid.innerHTML=rpVisibleCommands.map(function(x){return '<article class="rp-command-card"><div class="rp-command-head"><b>'+esc(x.title)+'</b><span class="rp-command-tag">'+esc(x.category)+'</span></div><p>'+esc(x.description)+'</p><pre class="rp-command-code"><code>'+esc(x.command)+'</code></pre><div class="rp-command-actions"><button class="rp-btn primary" data-command-copy="'+esc(x.id)+'">Copy command</button>'+(x.custom?'<button class="rp-btn danger" data-command-delete="'+esc(x.id)+'">Delete</button>':'')+'</div></article>';}).join('');
|
|
2650
|
+
}
|
|
2651
|
+
function initCommandSheet(){
|
|
2652
|
+
var fields=['lan','tail','site','robot','enrollment','token'],defaults=rpCommandDefaults(),saved=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
|
|
2653
|
+
fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=saved[name]!=null?saved[name]:defaults[name];input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
|
|
2654
|
+
var search=$('rp-command-search'),category=$('rp-command-category'),grid=$('rp-command-grid'),reset=$('rp-command-reset'),copyAll=$('rp-command-copy-all'),form=$('rp-command-custom-form');
|
|
2655
|
+
if(search)search.addEventListener('input',renderCommandSheet);if(category)category.addEventListener('change',renderCommandSheet);
|
|
2656
|
+
if(reset)reset.addEventListener('click',function(){try{localStorage.removeItem(RP_COMMAND_ENV_KEY);}catch(e){}fields.forEach(function(name){var input=$('rp-command-'+name);if(input)input.value=defaults[name];});renderCommandSheet();showOut('command environment reset');});
|
|
2657
|
+
if(copyAll)copyAll.addEventListener('click',function(){rpCopyCommand(rpVisibleCommands.map(function(x){return '# '+x.title+'\\n'+x.command;}).join('\\n\\n'));});
|
|
2658
|
+
if(grid)grid.addEventListener('click',function(ev){var copy=ev.target.closest('[data-command-copy]'),del=ev.target.closest('[data-command-delete]');if(copy){var id=copy.getAttribute('data-command-copy'),item=rpVisibleCommands.find(function(x){return x.id===id;});if(item)rpCopyCommand(item.command);}if(del){var removeId=del.getAttribute('data-command-delete');rpSaveCustomCommands(rpCustomCommands().filter(function(x){return String(x.id)!==removeId;}));renderCommandSheet();showOut('custom command deleted');}});
|
|
2659
|
+
if(form)form.addEventListener('submit',function(ev){ev.preventDefault();var title=$('rp-command-custom-title').value.trim(),command=$('rp-command-custom-command').value.trim();if(!title||!command)return;var list=rpCustomCommands();list.push({id:'custom-'+Date.now(),title:title,category:$('rp-command-custom-category').value.trim()||'Custom',description:$('rp-command-custom-description').value.trim(),command:command});rpSaveCustomCommands(list);$('rp-command-custom-title').value='';$('rp-command-custom-description').value='';$('rp-command-custom-command').value='';renderCommandSheet();showOut('custom command saved in this browser');});
|
|
2660
|
+
renderCommandSheet();
|
|
2661
|
+
}
|
|
2662
|
+
initCommandSheet();
|
|
2663
|
+
|
|
2664
|
+
var rpSubTab = 'overview';
|
|
2553
2665
|
var rpIntervals = {}; // sub-tab name -> setInterval handle
|
|
2554
2666
|
var rpHistoryAll = []; // full recent-session list (we paginate client-side)
|
|
2555
2667
|
function setRpSubTab(name){
|
|
@@ -2568,7 +2680,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2568
2680
|
s.classList.toggle('active', s.id==='rp-sub-'+name);
|
|
2569
2681
|
}
|
|
2570
2682
|
// stop every sub-tab interval, then start only the active one
|
|
2571
|
-
var names = ['overview','incidents','live','voice','history','audit','fleet','config','setup'];
|
|
2683
|
+
var names = ['overview','incidents','live','voice','history','audit','fleet','config','commands','setup'];
|
|
2572
2684
|
for(var k=0;k<names.length;k++){
|
|
2573
2685
|
var n=names[k];
|
|
2574
2686
|
if(rpIntervals[n]){ clearInterval(rpIntervals[n]); rpIntervals[n]=null; }
|
|
@@ -2579,7 +2691,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2579
2691
|
if(name==='voice'){ refreshVoiceCallOptions(); }
|
|
2580
2692
|
if(name==='history'){ rpHistoryPage=0; pollHistory(); }
|
|
2581
2693
|
if(name==='audit'){ pollAuditLog(); rpIntervals.audit = setInterval(pollAuditLog, 6000); }
|
|
2582
|
-
if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
|
|
2694
|
+
if(name==='fleet'){ pollFleet(); rpIntervals.fleet = setInterval(pollFleet, 5000); }
|
|
2695
|
+
if(name==='commands'){ renderCommandSheet(); }
|
|
2583
2696
|
if(name==='setup'){ pollSettings(); loadCharacterPresets(); loadVoiceStacks(); pollRuntimeHealth(); }
|
|
2584
2697
|
// remember last sub-tab in URL hash
|
|
2585
2698
|
try{ history.replaceState(null,'','#robopark/'+name); }catch(e){}
|
|
@@ -2595,7 +2708,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2595
2708
|
var h=String(location.hash||'').replace('#','');
|
|
2596
2709
|
var m = h.match(/^robopark\\/(.+)$/);
|
|
2597
2710
|
var initial = (m && m[1]) ? m[1] : 'overview';
|
|
2598
|
-
if(['overview','incidents','live','voice','history','audit','fleet','config','setup'].indexOf(initial)<0) initial='overview';
|
|
2711
|
+
if(['overview','incidents','live','voice','history','audit','fleet','config','commands','setup'].indexOf(initial)<0) initial='overview';
|
|
2599
2712
|
setRpSubTab(initial);
|
|
2600
2713
|
}
|
|
2601
2714
|
|
|
@@ -3869,7 +3982,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
3869
3982
|
var drawerId=null, camRAF=null, meterRAF=null, camAbort=null, drawerTimer=null, pipelineTimer=null, deviceSelectorsReady=false;
|
|
3870
3983
|
var previewOn=false, previewTimer=null; // on-demand operator camera preview
|
|
3871
3984
|
var visionOn=false; // RoboVisionAI_PI overlay feed (direct MJPEG, not LiveKit)
|
|
3872
|
-
var lkRoom=null, micAnalyser=null, audioCtx=null, lkAudioEls=[];
|
|
3985
|
+
var lkRoom=null, micAnalyser=null, audioCtx=null, lkAudioEls=[], micMonitorConnecting=false, micMonitorState='no active conversation session', micMonitorAudible=false, micMonitorVolume=0.8;
|
|
3873
3986
|
var imgs={}; // preloaded robot concept avatars
|
|
3874
3987
|
var TW=44, TH=22, PARK_W=15, PARK_D=8;
|
|
3875
3988
|
// Plaza footprint and parking lot (upper-right corner of the plaza)
|
|
@@ -5958,8 +6071,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5958
6071
|
if(camAbort){ try{camAbort.abort();}catch(e){} camAbort=null; }
|
|
5959
6072
|
if(drawerTimer){ clearInterval(drawerTimer); drawerTimer=null; }
|
|
5960
6073
|
if(pipelineTimer){ clearInterval(pipelineTimer); pipelineTimer=null; }
|
|
5961
|
-
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
5962
|
-
|
|
6074
|
+
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
6075
|
+
lkAudioEls.forEach(function(el){try{el.pause();el.remove();}catch(e){}});lkAudioEls=[];
|
|
6076
|
+
micMonitorAudible=false;micMonitorConnecting=false;micMonitorState='no active conversation session';
|
|
6077
|
+
micAnalyser=null;
|
|
5963
6078
|
}
|
|
5964
6079
|
|
|
5965
6080
|
// ── setup flow for unjoined park pads ──
|
|
@@ -6139,9 +6254,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6139
6254
|
}
|
|
6140
6255
|
fill('pk-video-device', video, device.video_device||'auto');
|
|
6141
6256
|
fill('pk-audio-input', input, device.audio_device||'default');
|
|
6142
|
-
fill('pk-audio-output', output, device.audio_output_device||'default');
|
|
6143
|
-
var
|
|
6144
|
-
if(
|
|
6257
|
+
fill('pk-audio-output', output, device.audio_output_device||'default');
|
|
6258
|
+
var selectedOutput=$('pk-audio-output'),selectedLabel=$('pk-spk-selected');
|
|
6259
|
+
if(selectedLabel&&selectedOutput){var selectedOption=selectedOutput.options&&selectedOutput.selectedIndex>=0?selectedOutput.options[selectedOutput.selectedIndex]:null;selectedLabel.textContent=selectedOption?String(selectedOption.textContent||selectedOption.value):String(selectedOutput.value||'default');}
|
|
6260
|
+
var greetings=$('pk-greeting-phrases');
|
|
6261
|
+
if(greetings && document.activeElement!==greetings) greetings.value=(device.greeting_phrases||[]).join(String.fromCharCode(10));
|
|
6262
|
+
var testPhrase=$('pk-spk-phrase');if(testPhrase&&document.activeElement!==testPhrase&&device.greeting_phrases&&device.greeting_phrases.length)testPhrase.value=device.greeting_phrases[0];
|
|
6145
6263
|
var note=$('pk-device-note');
|
|
6146
6264
|
if(note){
|
|
6147
6265
|
if(!device.id && n && n.connected){
|
|
@@ -6529,13 +6647,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6529
6647
|
+ '<div class="pk-ctl" style="margin-top:0.55rem"><button class="rp-btn primary" id="pk-pipeline-start">Start full test</button><button class="rp-btn danger" id="pk-pipeline-stop">Stop conversation</button></div>'
|
|
6530
6648
|
+ '<div class="pk-note">Starts the camera, sends the same motion trigger used in production, and follows every reported stage remotely.</div>'
|
|
6531
6649
|
+ '<div class="pk-pipeline" id="pk-pipeline"></div>')
|
|
6532
|
-
+ sect('🎙 microphone','<div class="pk-meter"><span class="lab">input</span><div class="pk-bars" id="pk-micbars"></div><span class="pk-val" id="pk-micval">—</span></div><div class="pk-note" id="pk-micstate">awaiting robot audio plugin</div>')
|
|
6650
|
+
+ sect('🎙 microphone','<div class="pk-meter"><span class="lab">input</span><div class="pk-bars" id="pk-micbars"></div><span class="pk-val" id="pk-micval">—</span></div><div class="pk-note" id="pk-micstate">awaiting robot audio plugin</div><div class="pk-ctl" style="margin-top:.5rem"><button class="rp-btn" id="pk-mic-listen">Listen in browser</button><label class="pk-note" style="display:flex;align-items:center;gap:.35rem;margin:0">volume <input id="pk-mic-volume" type="range" min="0" max="100" value="80" style="width:90px"></label></div><div class="pk-note" id="pk-mic-listen-note">Muted by default. Use headphones to prevent acoustic feedback.</div>')
|
|
6533
6651
|
+ sect('🔊 speaker','<canvas class="pk-wave" id="pk-spkwave" width="360" height="32"></canvas><div class="pk-meter"><span class="lab">output</span><div class="pk-bars" id="pk-spkbars"></div><span class="pk-val" id="pk-spkval">—</span></div><div class="pk-note" id="pk-spkstate">TTS plays on the robot speaker — use “Test speaker” below to round-trip a tone and confirm the audio path</div>'
|
|
6534
|
-
+ '<div class="rp-form" id="pk-spk-test" style="grid-template-columns:1fr 1fr;gap:0.4rem;margin-top:0.55rem;padding:0.55rem;border-color:var(--border-strong);">'
|
|
6535
|
-
+ '<div class="rp-field"><label>
|
|
6652
|
+
+ '<div class="rp-form" id="pk-spk-test" style="grid-template-columns:1fr 1fr;gap:0.4rem;margin-top:0.55rem;padding:0.55rem;border-color:var(--border-strong);">'
|
|
6653
|
+
+ '<div class="rp-field"><label>Test type</label><select id="pk-spk-mode"><option value="tts" selected>Cached character voice</option><option value="tone">Tone + mic round-trip</option></select></div>'
|
|
6654
|
+
+ '<div class="rp-field"><label>Selected output</label><div class="pk-note" id="pk-spk-selected" style="margin:.45rem 0 0">loading device...</div></div>'
|
|
6655
|
+
+ '<div class="rp-field" style="grid-column:1/-1"><label>Voice test phrase</label><input id="pk-spk-phrase" value="Hello, this is a RoboPark speaker test." maxlength="240"></div>'
|
|
6656
|
+
+ '<div class="rp-field"><label>Frequency</label><select id="pk-spk-freq"><option value="500">500 Hz (low)</option><option value="1000" selected>1000 Hz (1 kHz reference)</option><option value="2000">2000 Hz (presence)</option><option value="3000">3000 Hz (clarity)</option></select></div>'
|
|
6536
6657
|
+ '<div class="rp-field"><label>Duration</label><select id="pk-spk-dur"><option value="0.4">0.4 s</option><option value="0.6" selected>0.6 s</option><option value="1.0">1.0 s</option></select></div>'
|
|
6537
6658
|
+ '<div style="grid-column:1/-1;display:flex;gap:0.4rem;align-items:center;">'
|
|
6538
|
-
+ '<button class="rp-btn primary" id="pk-spk-runtest" data-robot="'+esc(schedId(n))+'"
|
|
6659
|
+
+ '<button class="rp-btn primary" id="pk-spk-runtest" data-robot="'+esc(schedId(n))+'">Play cached voice</button>'
|
|
6660
|
+
+ '<button class="rp-btn" id="pk-spk-tone">Quick tone</button>'
|
|
6539
6661
|
+ '<span class="pk-note" id="pk-spk-test-stateline" style="margin:0">awaiting test</span>'
|
|
6540
6662
|
+ '</div>'
|
|
6541
6663
|
+ '<div style="grid-column:1/-1;" id="pk-spk-test-result"></div>'
|
|
@@ -6547,7 +6669,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6547
6669
|
+ sect('🩺 robot services','<div id="pk-svc-list" class="pk-svc-list"><div class="pk-note">waiting for supervisor report…</div></div><div class="pk-note" id="pk-svc-age" style="margin-top:0.35rem;"></div><div class="rp-field" style="margin-top:0.55rem;"><label>Quick run (on the robot, from packages/robopark/scheduler)</label><input id="pk-svc-clicmd" readonly value="python robot_supervisor.py"></div><button class="rp-btn" id="pk-svc-clicmd-copy" style="margin-top:0.35rem;">📋 Copy command</button>')
|
|
6548
6670
|
+ buildShellSection(schedId(n))
|
|
6549
6671
|
+ sect('🎭 character / voice','<div class="pk-note" id="pk-eff-note" style="margin-bottom:0.45rem;">loading effective config…</div><div class="rp-field"><label>Character preset</label><select class="rp-char" id="pk-eff-char" data-robot="'+esc(schedId(n))+'">'+effPresetOptions+'</select></div><div class="rp-field"><label>Voice stack</label><select class="rp-voice" id="pk-eff-voice" data-robot="'+esc(schedId(n))+'">'+effVoiceOptions+'</select></div><div class="rp-config-actions" style="margin-top:.45rem"><button class="rp-btn primary" id="pk-eff-save" data-robot="'+esc(schedId(n))+'">Apply to robot</button><button class="rp-btn" id="pk-edit-character">Edit character</button><button class="rp-btn" id="pk-edit-stack">Edit stack</button><button class="rp-btn" id="pk-new-stack">New stack</button></div>');
|
|
6550
|
-
b.innerHTML += sect('Camera + audio devices','<div class="rp-field"><label>Search devices</label><input id="pk-device-search" placeholder="camera, USB, microphone, HDMI..." autocomplete="off"></div><div class="rp-field"><label>Camera input</label><select id="pk-video-device"></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input"></select></div><div class="rp-field"><label>Speaker / output inventory</label><select id="pk-audio-output"></select></div><div class="rp-field"><label>Preset greeting phrases <span class="pk-note">one per line, cached per voice</span></label><textarea id="pk-greeting-phrases" rows="3" placeholder="Hi, I am Jaguar! / Welcome to RoboPark."></textarea></div><button class="rp-btn" id="pk-device-save" style="margin-top:0.35rem">Save device selection + greetings</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
|
|
6672
|
+
b.innerHTML += sect('Camera + audio devices','<div class="rp-field"><label>Search devices</label><input id="pk-device-search" placeholder="camera, USB, microphone, HDMI..." autocomplete="off"></div><div class="rp-field"><label>Camera input</label><select id="pk-video-device"></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input"></select></div><div class="rp-field"><label>Speaker / output inventory</label><select id="pk-audio-output"></select></div><button class="rp-btn primary" id="pk-device-test-speaker" style="margin-top:0.35rem">Test selected speaker now</button><div class="rp-field"><label>Preset greeting phrases <span class="pk-note">one per line, cached per voice</span></label><textarea id="pk-greeting-phrases" rows="3" placeholder="Hi, I am Jaguar! / Welcome to RoboPark."></textarea></div><button class="rp-btn" id="pk-device-save" style="margin-top:0.35rem">Save device selection + greetings</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
|
|
6551
6673
|
// Put live hardware selection beside the camera and audio tests,
|
|
6552
6674
|
// ahead of persona and service administration in the Park drawer.
|
|
6553
6675
|
var deviceInput=$('pk-device-search'), deviceSection=deviceInput&&deviceInput.closest('.pk-sect');
|
|
@@ -6556,11 +6678,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6556
6678
|
refreshDeviceSelectors(n);
|
|
6557
6679
|
b.innerHTML += '<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn danger" id="pk-recover" data-robot="'+esc(n.name)+'">Recover robot</button><span class="pk-note" style="margin:0">clears stale sessions and queued triggers for this robot</span></div>';
|
|
6558
6680
|
var deviceSearch=$('pk-device-search'); if(deviceSearch) deviceSearch.addEventListener('input', function(){ refreshDeviceSelectors(n); });
|
|
6559
|
-
var deviceSave=$('pk-device-save'); if(deviceSave) deviceSave.addEventListener('click', function(){ saveDeviceSelectors(n); });
|
|
6681
|
+
var deviceSave=$('pk-device-save'); if(deviceSave) deviceSave.addEventListener('click', function(){ saveDeviceSelectors(n); });
|
|
6682
|
+
var outputSelect=$('pk-audio-output'); if(outputSelect) outputSelect.addEventListener('change', function(){ refreshDeviceSelectors(n); });
|
|
6683
|
+
var micListen=$('pk-mic-listen');if(micListen)micListen.addEventListener('click',function(){setMicMonitorAudible(!micMonitorAudible);});
|
|
6684
|
+
var micVolume=$('pk-mic-volume');if(micVolume)micVolume.addEventListener('input',function(){micMonitorVolume=Math.max(0,Math.min(1,Number(micVolume.value||80)/100));lkAudioEls.forEach(function(el){el.volume=micMonitorVolume;});});
|
|
6560
6685
|
startCam(n); startMeters(n); loadEffectiveConfig(schedId(n)); refreshSupervisorPanel(n);
|
|
6561
6686
|
_bindShellSection(n);
|
|
6562
6687
|
_spkTestRenderHistory(schedId(n));
|
|
6563
|
-
var runSpkTest = $('pk-spk-runtest'); if(runSpkTest) runSpkTest.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runSpeakerTest(n); });
|
|
6688
|
+
var runSpkTest = $('pk-spk-runtest'); if(runSpkTest) runSpkTest.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runSpeakerTest(n); });
|
|
6689
|
+
var runSpkTone = $('pk-spk-tone'); if(runSpkTone) runSpkTone.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runSpeakerTest(n,'tone'); });
|
|
6690
|
+
var quickSpkTest = $('pk-device-test-speaker'); if(quickSpkTest) quickSpkTest.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runSpeakerTest(n,'tts'); });
|
|
6691
|
+
var spkMode = $('pk-spk-mode'); if(spkMode) spkMode.addEventListener('change', function(){ if(runSpkTest) runSpkTest.textContent=spkMode.value==='tts'?'Play cached voice':'Run tone test'; });
|
|
6564
6692
|
var gl=$('pk-golive');
|
|
6565
6693
|
if(gl){
|
|
6566
6694
|
gl.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } goLive(n); });
|
|
@@ -6723,6 +6851,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6723
6851
|
function startCam(n){
|
|
6724
6852
|
idleCam('connecting');
|
|
6725
6853
|
tryVisionOverlay(n, true);
|
|
6854
|
+
startMicMonitor(n);
|
|
6726
6855
|
return;
|
|
6727
6856
|
var note=$('pk-camnote');
|
|
6728
6857
|
camAbort = ('AbortController' in window)?new AbortController():null;
|
|
@@ -6755,17 +6884,51 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6755
6884
|
var res=$('pk-camres'); if(res) res.textContent = reason==='idle'?'idle':'—';
|
|
6756
6885
|
var note=$('pk-camnote'); if(note) note.textContent = reason==='idle'?'the feed goes live automatically when the robot opens a session':msg;
|
|
6757
6886
|
}
|
|
6758
|
-
function
|
|
6887
|
+
function selectedRobotMic(){var el=$('pk-audio-input');if(!el)return 'default';var option=el.options&&el.selectedIndex>=0?el.options[el.selectedIndex]:null;return option?String(option.textContent||option.value):String(el.value||'default');}
|
|
6888
|
+
function selectedRobotOutput(){var el=$('pk-audio-output');if(!el)return 'default';var option=el.options&&el.selectedIndex>=0?el.options[el.selectedIndex]:null;return option?String(option.textContent||option.value):String(el.value||'default');}
|
|
6889
|
+
function setMicMonitorAudible(enabled){
|
|
6890
|
+
micMonitorAudible=!!enabled;
|
|
6891
|
+
var btn=$('pk-mic-listen'),note=$('pk-mic-listen-note');
|
|
6892
|
+
if(btn){btn.textContent=micMonitorAudible?'Mute browser monitor':'Listen in browser';btn.classList.toggle('primary',micMonitorAudible);}
|
|
6893
|
+
if(note)note.textContent=micMonitorAudible?(lkAudioEls.length?'Robot mic is playing in this browser. Use headphones.':'Listening armed — waiting for the robot mic track.'):'Browser monitoring muted.';
|
|
6894
|
+
lkAudioEls.forEach(function(el){
|
|
6895
|
+
el.muted=!micMonitorAudible;el.volume=micMonitorVolume;
|
|
6896
|
+
if(micMonitorAudible)el.play().catch(function(e){if(note)note.textContent='Browser blocked playback: '+e.message+' — press Listen again.';});
|
|
6897
|
+
});
|
|
6898
|
+
}
|
|
6899
|
+
function startMicMonitor(n){
|
|
6900
|
+
if(lkRoom||micMonitorConnecting)return;
|
|
6901
|
+
micMonitorConnecting=true;
|
|
6902
|
+
micMonitorState='checking active conversation session';
|
|
6903
|
+
var state=$('pk-micstate');if(state)state.textContent='selected '+selectedRobotMic()+' · checking active session...';
|
|
6904
|
+
fetch('/robopark/api/robots/'+encodeURIComponent(schedId(n))+'/stream'+QS,{cache:'no-store'})
|
|
6905
|
+
.then(function(r){if(!r.ok)throw new Error('scheduler '+r.status);return r.json();})
|
|
6906
|
+
.then(function(info){
|
|
6907
|
+
micMonitorConnecting=false;
|
|
6908
|
+
if(info&&info.active&&info.url&&info.token&&window.LivekitClient){micMonitorState='connecting to '+info.url;attachLiveKit(info,n,true);return;}
|
|
6909
|
+
micMonitorState=info&&info.reason?String(info.reason).replace(/_/g,' '):'waiting for conversation session';
|
|
6910
|
+
if(state)state.textContent='selected '+selectedRobotMic()+' · waiting for conversation session';
|
|
6911
|
+
})
|
|
6912
|
+
.catch(function(e){micMonitorConnecting=false;micMonitorState='monitor unavailable: '+e.message;if(state)state.textContent='selected '+selectedRobotMic()+' · '+micMonitorState;});
|
|
6913
|
+
}
|
|
6914
|
+
function attachLiveKit(info, n, audioOnly){
|
|
6759
6915
|
var note=$('pk-camnote'), res=$('pk-camres');
|
|
6760
6916
|
try{
|
|
6761
6917
|
var LK=window.LivekitClient; lkRoom=new LK.Room();
|
|
6762
6918
|
lkRoom.on(LK.RoomEvent.TrackSubscribed, function(track){
|
|
6763
|
-
if(track.kind==='video'){
|
|
6764
|
-
|
|
6919
|
+
if(track.kind==='video'){
|
|
6920
|
+
if(audioOnly)return;
|
|
6921
|
+
var el=track.attach(); el.setAttribute('playsinline',''); el.muted=true; el.autoplay=true;
|
|
6765
6922
|
var cv=$('pk-camcv'); if(cv&&cv.parentNode) cv.parentNode.replaceChild(el, cv);
|
|
6766
6923
|
if(res) res.textContent='LIVE'; if(note) note.textContent='live camera via LiveKit · '+esc(info.room);
|
|
6767
|
-
} else if(track.kind==='audio'){
|
|
6768
|
-
|
|
6924
|
+
} else if(track.kind==='audio'){
|
|
6925
|
+
// Listen only to the robot's published microphone. Agent TTS may
|
|
6926
|
+
// also be present in the room, but hearing that directly would
|
|
6927
|
+
// bypass the physical speaker -> microphone round-trip test.
|
|
6928
|
+
if(track.name&&track.name!=='microphone')return;
|
|
6929
|
+
hookMicAnalyser(track);
|
|
6930
|
+
micMonitorState='live track published';
|
|
6931
|
+
var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · live track published';
|
|
6769
6932
|
// Muted by default: this is the camera-preview panel, running on
|
|
6770
6933
|
// the SAME physical device as the robot in local testing. Auto-
|
|
6771
6934
|
// playing the subscribed track (robot mic, or later the agent's
|
|
@@ -6774,16 +6937,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6774
6937
|
// that drowns out the actual conversation. The VU meter above
|
|
6775
6938
|
// (hookMicAnalyser) taps the raw track directly and works fine
|
|
6776
6939
|
// muted.
|
|
6777
|
-
var audio=track.attach(); audio.autoplay=true; audio.controls=false; audio.muted
|
|
6778
|
-
|
|
6940
|
+
var audio=track.attach(); audio.autoplay=true; audio.controls=false; audio.muted=!micMonitorAudible; audio.volume=micMonitorVolume;audio.style.display='none'; document.body.appendChild(audio); lkAudioEls.push(audio);
|
|
6941
|
+
var listenNote=$('pk-mic-listen-note');if(listenNote&&micMonitorAudible)listenNote.textContent='Robot mic is playing in this browser. Use headphones.';
|
|
6942
|
+
audio.play().catch(function(){});
|
|
6779
6943
|
}
|
|
6780
6944
|
});
|
|
6781
|
-
lkRoom.on(LK.RoomEvent.Disconnected, function(){
|
|
6945
|
+
lkRoom.on(LK.RoomEvent.Disconnected, function(){lkRoom=null;micAnalyser=null;micMonitorConnecting=false;micMonitorState='session ended';var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · session ended';if(!audioOnly&¬e)note.textContent='session ended';if(!audioOnly&&res)res.textContent='—';});
|
|
6782
6946
|
var livekitUrl=info.url;
|
|
6783
6947
|
if((location.hostname==='localhost'||location.hostname==='127.0.0.1') && /^ws:\\/\\/localhost(?::|\\/)/i.test(livekitUrl)) livekitUrl=livekitUrl.replace(/^ws:\\/\\/localhost/i,'ws://127.0.0.1');
|
|
6784
|
-
lkRoom.connect(livekitUrl, info.token).then(function(){ if(
|
|
6785
|
-
.catch(function(e){
|
|
6786
|
-
}catch(e){ idleCam('no-sdk'); }
|
|
6948
|
+
lkRoom.connect(livekitUrl, info.token).then(function(){micMonitorState='connected, waiting for robot mic publication';var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · '+micMonitorState;if(!audioOnly&¬e)note.textContent='connected · waiting for camera track…';})
|
|
6949
|
+
.catch(function(e){lkRoom=null;micMonitorConnecting=false;micMonitorState='LiveKit connection failed: '+(e&&e.message?e.message:'unreachable');var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · '+micMonitorState;if(!audioOnly){idleCam('unreachable');if(note)note.textContent='LiveKit connection failed';}if(window.console)console.error('RoboPark LiveKit connection failed',e);});
|
|
6950
|
+
}catch(e){ micMonitorConnecting=false;micMonitorState='LiveKit SDK unavailable';idleCam('no-sdk'); }
|
|
6787
6951
|
}
|
|
6788
6952
|
// Real mic level: tap the robot's subscribed 'mic' audio track with a Web
|
|
6789
6953
|
// Audio analyser and read RMS in startMeters (no invented levels).
|
|
@@ -6824,7 +6988,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6824
6988
|
if(micAnalyser){ micAnalyser.getByteTimeDomainData(buf); var sum=0; for(var q=0;q<buf.length;q++){ var v=(buf[q]-128)/128; sum+=v*v; } lvl=Math.min(1, Math.sqrt(sum/buf.length)*3.4); }
|
|
6825
6989
|
for(var i=0;i<mi.length;i++){ var on=i<lvl*mi.length; mi[i].style.height=(3+(on?lvl*13:0))+'px'; mi[i].style.background=on?(i<mi.length*0.7?'var(--ok)':i<mi.length*0.9?'var(--warn)':'var(--bad)'):'var(--glass-border)'; }
|
|
6826
6990
|
set('pk-micval', micAnalyser?Math.round(lvl*100)+'%':'—');
|
|
6827
|
-
set('pk-micstate', micAnalyser?(lvl>0.12?'live
|
|
6991
|
+
set('pk-micstate', micAnalyser?('selected '+selectedRobotMic()+' · '+(lvl>0.12?'live, hearing audio':'live, quiet')):('selected '+selectedRobotMic()+' · '+micMonitorState));
|
|
6828
6992
|
// SPEAKER — the Pi plays TTS out its own speaker; we don't get a
|
|
6829
6993
|
// published track, so the meter stays at "—" unless a speaker
|
|
6830
6994
|
// round-trip test is currently running. While a test plays, the
|
|
@@ -6900,7 +7064,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6900
7064
|
var mark = t.pass===true ? '✓' : (t.pass===false ? '✗' : '·');
|
|
6901
7065
|
var when = new Date(t.t).toTimeString().slice(0,8);
|
|
6902
7066
|
var peak = t.peak_db != null ? (t.peak_db+' dB') : '—';
|
|
6903
|
-
var freq = t.frequency != null ? (Math.round(t.frequency)+' Hz') : '—';
|
|
7067
|
+
var freq = t.mode==='tts' ? 'cached voice' : (t.frequency != null ? (Math.round(t.frequency)+' Hz') : '—');
|
|
6904
7068
|
return '<div class="pk-spk-hist-row"><span class="pk-spk-mark" style="color:'+cls+'">'+mark+'</span>'
|
|
6905
7069
|
+ '<span class="pk-spk-when">'+when+'</span>'
|
|
6906
7070
|
+ '<span class="pk-spk-freq">'+freq+'</span>'
|
|
@@ -6923,14 +7087,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6923
7087
|
+ '</div>';
|
|
6924
7088
|
return;
|
|
6925
7089
|
}
|
|
6926
|
-
var
|
|
6927
|
-
|
|
7090
|
+
var isTts=result.mode==='tts';
|
|
7091
|
+
var banner = result.pass
|
|
7092
|
+
? '<div class="pk-spk-banner ok">✓ '+(isTts?'cached voice playback OK':'speaker round-trip OK')+'</div>'
|
|
6928
7093
|
: '<div class="pk-spk-banner bad">✗ no signal detected</div>';
|
|
6929
7094
|
var meta = '<div class="pk-spk-meta">'
|
|
6930
7095
|
+ '<div><span class="l">peak</span><span class="v">'+esc(result.recorded_peak_db+' dB')+'</span></div>'
|
|
6931
7096
|
+ '<div><span class="l">RMS</span><span class="v">'+esc(result.recorded_rms_db+' dB')+'</span></div>'
|
|
6932
7097
|
+ '<div><span class="l">threshold</span><span class="v">'+esc(result.threshold_db+' dB')+'</span></div>'
|
|
6933
|
-
+ '<div><span class="l">frequency</span><span class="v">'+esc(Math.round(result.frequency)+' Hz')+'</span></div>'
|
|
7098
|
+
+ (isTts?'<div><span class="l">voice</span><span class="v">'+esc((result.tts_provider||'tts')+' / '+(result.tts_voice||'default'))+'</span></div><div><span class="l">cache</span><span class="v">'+(result.cache_hit?'hit':'generated')+'</span></div>':'<div><span class="l">frequency</span><span class="v">'+esc(Math.round(result.frequency)+' Hz')+'</span></div>')
|
|
6934
7099
|
+ '<div><span class="l">duration</span><span class="v">'+esc(result.duration+' s')+'</span></div>'
|
|
6935
7100
|
+ '<div><span class="l">latency</span><span class="v">'+esc(result.duration_ms+' ms')+'</span></div>'
|
|
6936
7101
|
+ '<div><span class="l">output</span><span class="v">'+esc(result.output_device)+'</span></div>'
|
|
@@ -6940,29 +7105,34 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6940
7105
|
host.innerHTML = '<div class="pk-spk-result '+(result.pass?'ok':'bad')+'">'
|
|
6941
7106
|
+ banner + meta + diag + '</div>';
|
|
6942
7107
|
}
|
|
6943
|
-
function runSpeakerTest(n){
|
|
6944
|
-
var robotId = schedId(n);
|
|
6945
|
-
var
|
|
7108
|
+
function runSpeakerTest(n, forcedMode){
|
|
7109
|
+
var robotId = schedId(n), mode=forcedMode||(($('pk-spk-mode')||{}).value||'tts');
|
|
7110
|
+
var buttons=[$('pk-spk-runtest'),$('pk-spk-tone'),$('pk-device-test-speaker')];buttons.forEach(function(b){if(b)b.disabled=true;});
|
|
7111
|
+
var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = true; btn.textContent = mode==='tts'?'loading voice…':'testing…'; }
|
|
6946
7112
|
var status = $('pk-spk-test-stateline');
|
|
6947
|
-
if(status) status.textContent = 'queued — waiting for robot…';
|
|
6948
|
-
var freq = parseFloat(($('pk-spk-freq')||{}).value || '1000');
|
|
6949
|
-
var dur = parseFloat(($('pk-spk-dur')||{}).value || '0.6');
|
|
7113
|
+
if(status) status.textContent = mode==='tts'?'loading cached character voice…':'queued — waiting for robot…';
|
|
7114
|
+
var freq = parseFloat(($('pk-spk-freq')||{}).value || '1000');
|
|
7115
|
+
var dur = parseFloat(($('pk-spk-dur')||{}).value || '0.6');
|
|
7116
|
+
var phrase=String((($('pk-spk-phrase')||{}).value)||'Hello, I am ready to talk.').trim();
|
|
7117
|
+
var output=(($('pk-audio-output')||{}).value)||'default';
|
|
7118
|
+
var input=(($('pk-audio-input')||{}).value)||'default';
|
|
6950
7119
|
// Animate the meter with a synthetic "during-test" peak so the
|
|
6951
7120
|
// operator sees something happen immediately while waiting for
|
|
6952
7121
|
// the real recording.
|
|
6953
7122
|
_spkTestSetLive(robotId, {live:true, rms: 0.45, pass: undefined, waveform: null});
|
|
6954
7123
|
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',
|
|
6955
|
-
{frequency:
|
|
7124
|
+
{mode:mode, text:phrase, frequency:freq, duration:dur, amplitude:0.6, threshold_db:-30, output:output, input:input})
|
|
6956
7125
|
.then(function(q){
|
|
6957
7126
|
// long-poll up to 8s, then fall back to 12s of result polling
|
|
6958
|
-
if(status) status.textContent = 'playing tone + recording…';
|
|
7127
|
+
if(status) status.textContent = mode==='tts'?'playing cached voice on '+selectedRobotOutput()+'…':'playing tone + recording…';
|
|
6959
7128
|
return _shellWaitForResult(robotId, q.request_id, q.device_id || robotId, '', 'speaker-test');
|
|
6960
7129
|
})
|
|
6961
7130
|
.catch(function(e){
|
|
6962
7131
|
if(status) status.textContent = '✗ test failed: '+e.message;
|
|
6963
|
-
_spkTestSetLive(robotId, {live:false});
|
|
6964
|
-
_spkTestRenderResult(robotId, {ok:false, error:e.message});
|
|
6965
|
-
if(
|
|
7132
|
+
_spkTestSetLive(robotId, {live:false});
|
|
7133
|
+
_spkTestRenderResult(robotId, {ok:false, error:e.message});
|
|
7134
|
+
buttons.forEach(function(b){if(b)b.disabled=false;});
|
|
7135
|
+
if(btn){ btn.disabled = false; btn.textContent = mode==='tts'?'Play cached voice':'Run tone test'; }
|
|
6966
7136
|
});
|
|
6967
7137
|
}
|
|
6968
7138
|
// Render a synthetic "during-test" waveform so the meter animates
|
|
@@ -6975,7 +7145,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6975
7145
|
t: Date.now(),
|
|
6976
7146
|
pass: result && result.ok ? (result.pass===true) : null,
|
|
6977
7147
|
peak_db: result && result.recorded_peak_db != null ? result.recorded_peak_db : null,
|
|
6978
|
-
frequency: result && result.frequency != null ? result.frequency : null,
|
|
7148
|
+
frequency: result && result.frequency != null ? result.frequency : null,
|
|
7149
|
+
mode: result && result.mode || 'tone',
|
|
6979
7150
|
diagnostic: result && result.diagnostic || (result && !result.ok ? result.error : null),
|
|
6980
7151
|
});
|
|
6981
7152
|
while(list.length > 5) list.pop();
|
|
@@ -6990,15 +7161,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6990
7161
|
_origRenderShellResult(robotId, result, label);
|
|
6991
7162
|
if(label === 'speaker-test'){
|
|
6992
7163
|
_spkTestSetLive(robotId, {live:false});
|
|
6993
|
-
_spkTestMark(robotId, result);
|
|
6994
|
-
_spkTestRenderResult(robotId, result);
|
|
7164
|
+
_spkTestMark(robotId, result);
|
|
7165
|
+
_spkTestRenderResult(robotId, result);
|
|
7166
|
+
[$('pk-spk-runtest'),$('pk-spk-tone'),$('pk-device-test-speaker')].forEach(function(b){if(b)b.disabled=false;});
|
|
6995
7167
|
var status = $('pk-spk-test-stateline');
|
|
6996
7168
|
if(status){
|
|
6997
7169
|
if(!result || !result.ok){ status.textContent = '✗ test failed'; }
|
|
6998
7170
|
else if(result.pass){ status.textContent = '✓ pass · '+result.recorded_peak_db+' dB peak'; }
|
|
6999
7171
|
else { status.textContent = '✗ no signal — '+result.recorded_peak_db+' dB peak'; }
|
|
7000
7172
|
}
|
|
7001
|
-
var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = false; btn.textContent = '
|
|
7173
|
+
var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = false; btn.textContent = (($('pk-spk-mode')||{}).value==='tone'?'Run tone test':'Play cached voice'); }
|
|
7002
7174
|
}
|
|
7003
7175
|
};
|
|
7004
7176
|
|
|
@@ -76,6 +76,8 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
76
76
|
const network = opts.network ?? 'lan';
|
|
77
77
|
const foreground = opts.foreground === true;
|
|
78
78
|
const cli = roboparkCliPath();
|
|
79
|
+
const hub = new URL(opts.hubUrl);
|
|
80
|
+
const livekitUrl = `${hub.protocol === 'https:' ? 'wss:' : 'ws:'}//${hub.hostname}:7880`;
|
|
79
81
|
const children = [];
|
|
80
82
|
let stopping = false;
|
|
81
83
|
let recycling = false;
|
|
@@ -111,6 +113,7 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
111
113
|
// Keep this in the child environment, never in the persisted robot config.
|
|
112
114
|
env: {
|
|
113
115
|
ROBOPARK_MESH_TOKEN: opts.token,
|
|
116
|
+
ROBOPARK_LIVEKIT_URL: livekitUrl,
|
|
114
117
|
// Keep internal service wiring out of the Commander argument boundary.
|
|
115
118
|
// This also lets preview start while RoboVision is still warming up.
|
|
116
119
|
ROBOVISION_URL: opts.vision !== false ? 'http://127.0.0.1:5000' : '',
|
package/package.json
CHANGED