infinicode 2.8.44 → 2.8.46
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.
|
@@ -2032,7 +2032,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
2032
2032
|
var grid = $('rp-grid'), totals = $('rp-totals');
|
|
2033
2033
|
// stash per-robot rows so the Park drawer can show live session stats
|
|
2034
2034
|
roboparkByName = {};
|
|
2035
|
-
robotsFrom(d).forEach(function(x){ roboparkByName[robotName(x)] = x; if(x.name) roboparkByName[x.name] = x; });
|
|
2035
|
+
robotsFrom(d).forEach(function(x){ roboparkByName[String(robotName(x)).toLowerCase()] = x; if(x.name) roboparkByName[String(x.name).toLowerCase()] = x; });
|
|
2036
2036
|
// C4: also feed lastRobots for the Incidents sub-tab
|
|
2037
2037
|
lastRobots = robotsFrom(d) || [];
|
|
2038
2038
|
// push the same map into the Park so the canvas can color robots
|
|
@@ -3580,7 +3580,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
3580
3580
|
connected: match?!!match.connected:false,
|
|
3581
3581
|
load: (match&&match.load!=null)?match.load:0,
|
|
3582
3582
|
platform: match?(match.platform||''):'', version: match?(match.version||'?'):'—',
|
|
3583
|
-
site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
|
|
3583
|
+
site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
|
|
3584
|
+
schedulerRobotId: match?(match.schedulerRobotId||match.schedulerDeviceId||''):'',
|
|
3585
|
+
schedulerDeviceId: match?(match.schedulerDeviceId||match.schedulerRobotId||''):'',
|
|
3584
3586
|
telemKeys: [slot.label.toLowerCase()].concat(slot.keys||[]).concat(match?[(match.displayName||'').toLowerCase()]:[]),
|
|
3585
3587
|
wx:wx, wy:wy, _hit:prevHit(id)
|
|
3586
3588
|
};
|
|
@@ -5312,7 +5314,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5312
5314
|
// enrolled device's id is a generated dev_xxxx, distinct from its name).
|
|
5313
5315
|
// Falls back to n.name if telemetry hasn't matched a row yet.
|
|
5314
5316
|
function schedRow(n){
|
|
5315
|
-
var row=roboparkByName[n.name], exact=row, keys=(n.telemKeys||[]).concat([String(n.name||'').toLowerCase()]);
|
|
5317
|
+
var row=roboparkByName[String(n.name||'').toLowerCase()] || roboparkByName[n.name], exact=row, keys=(n.telemKeys||[]).concat([String(n.name||'').toLowerCase()]);
|
|
5316
5318
|
var seen={}, candidates=[];
|
|
5317
5319
|
for(var rk in roboparkByName){
|
|
5318
5320
|
var candidate=roboparkByName[rk], cid=candidate&&(candidate.robot_id||candidate.id);
|
|
@@ -5336,7 +5338,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5336
5338
|
function schedId(n){
|
|
5337
5339
|
var row=schedRow(n); if(row&&(row.id||row.robot_id)) return row.id||row.robot_id;
|
|
5338
5340
|
if(n.schedulerRobotId) return n.schedulerRobotId;
|
|
5339
|
-
var byName=schedulerDevicesByName[n.name]; if(byName&&byName.id) return byName.id;
|
|
5341
|
+
var byName=schedulerDevicesByName[String(n.name||'').toLowerCase()]; if(byName&&byName.id) return byName.id;
|
|
5340
5342
|
return n.name;
|
|
5341
5343
|
}
|
|
5342
5344
|
function closeDrawer(){
|
|
@@ -5466,13 +5468,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5466
5468
|
function pollRoboparkQuiet(){
|
|
5467
5469
|
fetch('/robopark/api/telemetry'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
|
|
5468
5470
|
var list=(Array.isArray(d)?d:(d&&d.robots)||[]); roboparkByName={};
|
|
5469
|
-
list.forEach(function(x){ roboparkByName[x.robot_id||x.id||x.name||'robot']=x; if(x.name) roboparkByName[x.name]=x; });
|
|
5471
|
+
list.forEach(function(x){ roboparkByName[String(x.robot_id||x.id||x.name||'robot').toLowerCase()]=x; if(x.name) roboparkByName[String(x.name).toLowerCase()]=x; });
|
|
5470
5472
|
refreshDrawer();
|
|
5471
5473
|
}).catch(function(){});
|
|
5472
5474
|
}
|
|
5473
5475
|
function schedulerDevice(n){
|
|
5474
5476
|
var row=schedRow(n), id=row&&(row.robot_id||row.id);
|
|
5475
|
-
return (id&&schedulerDevicesById[id]) || schedulerDevicesById[n.schedulerRobotId] || schedulerDevicesByName[n.name] || schedulerDevicesById[n.name] || null;
|
|
5477
|
+
return (id&&schedulerDevicesById[id]) || schedulerDevicesById[n.schedulerDeviceId] || schedulerDevicesById[n.schedulerRobotId] || schedulerDevicesByName[String(n.name||'').toLowerCase()] || schedulerDevicesById[n.name] || null;
|
|
5476
5478
|
}
|
|
5477
5479
|
function deviceList(inv, key, fallback){
|
|
5478
5480
|
var list=inv&&Array.isArray(inv[key])?inv[key]:[];
|
|
@@ -5619,7 +5621,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
5619
5621
|
// available services from the supervisor_status report (if any),
|
|
5620
5622
|
// otherwise shows a placeholder.
|
|
5621
5623
|
function buildShellSection(robotId){
|
|
5622
|
-
var device = robotId ?
|
|
5624
|
+
var device = robotId ? schedulerDevicesByName[String(robotName({name:robotId, id:robotId})||'').toLowerCase()] || null : null;
|
|
5623
5625
|
// Try to read the device row by id directly
|
|
5624
5626
|
try{
|
|
5625
5627
|
for(var k in schedulerDevicesById){
|
package/package.json
CHANGED
|
@@ -4166,6 +4166,8 @@ async def dashboard():
|
|
|
4166
4166
|
const showDevices = ref(true);
|
|
4167
4167
|
const showCommands = ref(false);
|
|
4168
4168
|
const commandNetwork = ref('lan');
|
|
4169
|
+
const commandHubLan = ref('http://192.168.1.12:47913');
|
|
4170
|
+
const commandHubTailscale = ref('http://100.84.147.24:47913');
|
|
4169
4171
|
const commandRobotId = ref('');
|
|
4170
4172
|
const newDevice = ref({ name: '', tailscale_ip: '', lan_ip: '', motor_server_url: '', character_id: '', livekit_url: '', video_device: 'auto', audio_device: 'default', notes: '' });
|
|
4171
4173
|
const rotatedToken = ref(null);
|
|
@@ -4228,13 +4230,24 @@ async def dashboard():
|
|
|
4228
4230
|
const buildDeviceInventory = () => {
|
|
4229
4231
|
const map = {};
|
|
4230
4232
|
for (const robot of robots.value) {
|
|
4231
|
-
const
|
|
4233
|
+
const robotName = String(robot.name || '').toLowerCase();
|
|
4234
|
+
const device = devices.value.find(d => String(d.name || '').toLowerCase() === robotName || d.id === robot.id);
|
|
4232
4235
|
const savedVideo = device?.video_device || 'auto';
|
|
4233
4236
|
const savedAudio = device?.audio_device || 'default';
|
|
4234
4237
|
const hardware = device?.device_inventory || {};
|
|
4235
|
-
const videoOptions = (hardware.video || []).
|
|
4236
|
-
const audioOptions = (hardware.audio_input || []).
|
|
4237
|
-
const audioOutputOptions = (hardware.audio_output || []).
|
|
4238
|
+
const videoOptions = (hardware.video || []).filter(Boolean);
|
|
4239
|
+
const audioOptions = (hardware.audio_input || []).filter(Boolean);
|
|
4240
|
+
const audioOutputOptions = (hardware.audio_output || []).filter(Boolean);
|
|
4241
|
+
const optionId = x => typeof x === 'string' ? x : x.id;
|
|
4242
|
+
const optionName = x => typeof x === 'string' ? x : (x.name || x.id);
|
|
4243
|
+
const uniqueOptions = (items, defaults) => {
|
|
4244
|
+
const seen = new Set();
|
|
4245
|
+
return defaults.concat(items).filter(x => {
|
|
4246
|
+
const id = optionId(x);
|
|
4247
|
+
if (!id || seen.has(id)) return false;
|
|
4248
|
+
seen.add(id); return true;
|
|
4249
|
+
});
|
|
4250
|
+
};
|
|
4238
4251
|
map[robot.id] = {
|
|
4239
4252
|
deviceId: device?.id || null,
|
|
4240
4253
|
robotName: robot.name,
|
|
@@ -4242,9 +4255,10 @@ async def dashboard():
|
|
|
4242
4255
|
selectedAudio: savedAudio,
|
|
4243
4256
|
selectedAudioOutput: device?.audio_output_device || 'default',
|
|
4244
4257
|
greetingText: (device?.greeting_phrases || []).join('\\n'),
|
|
4245
|
-
videoOptions:
|
|
4246
|
-
audioOptions:
|
|
4247
|
-
audioOutputOptions:
|
|
4258
|
+
videoOptions: uniqueOptions(videoOptions, ['auto', 'none', savedVideo]),
|
|
4259
|
+
audioOptions: uniqueOptions(audioOptions, ['default', 'none', savedAudio]),
|
|
4260
|
+
audioOutputOptions: uniqueOptions(audioOutputOptions, ['default', 'none', device?.audio_output_device || 'default']),
|
|
4261
|
+
optionId, optionName,
|
|
4248
4262
|
};
|
|
4249
4263
|
}
|
|
4250
4264
|
deviceInventory.value = map;
|
|
@@ -4364,7 +4378,7 @@ async def dashboard():
|
|
|
4364
4378
|
const inv = deviceInventory.value[robotId];
|
|
4365
4379
|
if (!inv || !inv.deviceId) return;
|
|
4366
4380
|
try {
|
|
4367
|
-
await fetch(`/api/devices/${inv.deviceId}`, {
|
|
4381
|
+
const response = await fetch(`/api/devices/${inv.deviceId}`, {
|
|
4368
4382
|
method: 'PATCH',
|
|
4369
4383
|
headers: {'Content-Type': 'application/json'},
|
|
4370
4384
|
body: JSON.stringify({
|
|
@@ -4414,6 +4428,31 @@ async def dashboard():
|
|
|
4414
4428
|
} catch (e) { console.warn('preview stop failed', e); }
|
|
4415
4429
|
}
|
|
4416
4430
|
};
|
|
4431
|
+
|
|
4432
|
+
const testSpeaker = async (robot) => {
|
|
4433
|
+
const inv = deviceInventory.value[robot.id];
|
|
4434
|
+
const deviceId = inv?.deviceId || robot.id;
|
|
4435
|
+
try {
|
|
4436
|
+
const response = await fetch(`/api/robots/${deviceId}/shell/speaker-test`, {
|
|
4437
|
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
4438
|
+
body: JSON.stringify({
|
|
4439
|
+
output: inv?.selectedAudioOutput || 'default',
|
|
4440
|
+
input: inv?.selectedAudio || 'default',
|
|
4441
|
+
duration: 0.6,
|
|
4442
|
+
}),
|
|
4443
|
+
});
|
|
4444
|
+
const queued = await response.json();
|
|
4445
|
+
if (!response.ok) throw new Error(queued.detail || `HTTP ${response.status}`);
|
|
4446
|
+
const result = await fetch(`/api/robots/${deviceId}/shell/wait/${queued.request_id}?timeout=12`).then(r => r.json());
|
|
4447
|
+
if (!result.completed) throw new Error('Robot did not answer. Check its heartbeat and supervisor.');
|
|
4448
|
+
const detail = result.result || {};
|
|
4449
|
+
alert(detail.ok
|
|
4450
|
+
? `Audio test passed\\nOutput: ${detail.output_device || 'default'}\\nInput: ${detail.input_device || 'default'}\\nPeak: ${detail.peak_db ?? 'n/a'} dB`
|
|
4451
|
+
: `Audio test failed: ${detail.error || 'no microphone signal'}`);
|
|
4452
|
+
} catch (e) {
|
|
4453
|
+
alert('Audio test failed: ' + e.message);
|
|
4454
|
+
}
|
|
4455
|
+
};
|
|
4417
4456
|
|
|
4418
4457
|
const simulateTrigger = async (robot) => {
|
|
4419
4458
|
if (!settings.value.production_mode || simulation.value.busy) return;
|
|
@@ -4527,11 +4566,9 @@ async def dashboard():
|
|
|
4527
4566
|
const dismissRotatedToken = () => { rotatedToken.value = null; };
|
|
4528
4567
|
const enrollmentCommand = (token) => {
|
|
4529
4568
|
if (!token) return '';
|
|
4530
|
-
const schedulerPort = window.location.port || '8080';
|
|
4531
4569
|
const networkFlag = enrollmentNetwork.value === 'tailscale' ? '--tailscale' : '--lan';
|
|
4532
|
-
const
|
|
4533
|
-
|
|
4534
|
-
return `robopark setup --robot --name "${token.name || token.device_id}" --hub-url ${hubUrl} --scheduler-port ${schedulerPort} ${networkFlag} --enrollment-token ${token.token} --start --auto-start`;
|
|
4570
|
+
const hubUrl = enrollmentNetwork.value === 'tailscale' ? commandHubTailscale.value : commandHubLan.value;
|
|
4571
|
+
return `robopark setup --robot --name "${token.name || token.device_id}" --hub-url ${hubUrl} --scheduler-port 8080 ${networkFlag} --enrollment-token ${token.token} --start --auto-start`;
|
|
4535
4572
|
};
|
|
4536
4573
|
|
|
4537
4574
|
const joinConversation = async (device) => {
|
|
@@ -4732,7 +4769,10 @@ async def dashboard():
|
|
|
4732
4769
|
const stackName = (id) => voiceStacks.value.find(s => s.id === id)?.name || '—';
|
|
4733
4770
|
const characterName = (id) => characterPresets.value.find(c => c.id === id)?.name || '—';
|
|
4734
4771
|
|
|
4735
|
-
const schedulerBase = () =>
|
|
4772
|
+
const schedulerBase = () => {
|
|
4773
|
+
const hub = commandNetwork.value === 'tailscale' ? commandHubTailscale.value : commandHubLan.value;
|
|
4774
|
+
return hub.replace(/:\\d+\\/?$/, ':8080').replace(/\\/$/, '');
|
|
4775
|
+
};
|
|
4736
4776
|
const selectedCommandRobot = computed(() => devices.value.find(d => d.id === commandRobotId.value) || devices.value[0] || null);
|
|
4737
4777
|
const commandRows = computed(() => {
|
|
4738
4778
|
const base = schedulerBase();
|
|
@@ -4741,10 +4781,11 @@ async def dashboard():
|
|
|
4741
4781
|
const robotName = robot?.name || '<ROBOT_NAME>';
|
|
4742
4782
|
const networkFlag = commandNetwork.value === 'tailscale' ? '--tailscale' : '--lan';
|
|
4743
4783
|
const tokenPlaceholder = '<ENROLLMENT_TOKEN_FROM_ADD_DEVICE>';
|
|
4784
|
+
const hub = commandNetwork.value === 'tailscale' ? commandHubTailscale.value : commandHubLan.value;
|
|
4744
4785
|
return [
|
|
4745
4786
|
{ group: 'Scheduler', name: 'Health check', description: 'Confirm the control center API is reachable.', command: `curl -s ${base}/api/settings` },
|
|
4746
4787
|
{ group: 'Scheduler', name: 'Start scheduler', description: 'Start the RoboPark scheduler and dashboard on port 8080.', command: 'robopark serve --port 8080' },
|
|
4747
|
-
{ group: 'Robot', name: 'Enroll robot', description: `Generated for ${robotName} over ${commandNetwork.value}. Paste the one-time token from the Add Device dialog.`, command: `robopark setup --robot --name "${robotName}" --hub-url ${
|
|
4788
|
+
{ group: 'Robot', name: 'Enroll robot', description: `Generated for ${robotName} over ${commandNetwork.value}. Paste the one-time token from the Add Device dialog.`, command: `robopark setup --robot --name "${robotName}" --hub-url ${hub} --scheduler-port 8080 ${networkFlag} --enrollment-token ${tokenPlaceholder} --start --auto-start` },
|
|
4748
4789
|
{ group: 'Robot', name: 'Simulate motion', description: 'Queue a full production trigger for the selected robot.', command: `curl -s -X POST ${base}/api/robots/${robotId}/simulate-trigger` },
|
|
4749
4790
|
{ group: 'Robot', name: 'Stop conversation', description: 'Stop the active conversation and clear queued trigger state.', command: `curl -s -X POST ${base}/api/robots/${robotId}/simulate-stop` },
|
|
4750
4791
|
{ group: 'Robot', name: 'Recover stale state', description: 'Release a robot stuck in connecting/running after a failed teardown.', command: `curl -s -X POST ${base}/api/robots/${robotId}/recover` },
|
|
@@ -4761,15 +4802,15 @@ async def dashboard():
|
|
|
4761
4802
|
|
|
4762
4803
|
return {
|
|
4763
4804
|
robots, servers, sessions, stats, connected, serverMetrics,
|
|
4764
|
-
devices, settings, livekit, showAddDevice, showDevices, showCommands, commandNetwork, commandRobotId, selectedCommandRobot, commandRows, copyCommand, newDevice, rotatedToken, enrollmentNetwork, enrollmentCommand, simulation,
|
|
4765
|
-
showLiveKitConfig, livekitForm, preview, deviceInventory,
|
|
4805
|
+
devices, settings, livekit, showAddDevice, showDevices, showCommands, commandNetwork, commandHubLan, commandHubTailscale, commandRobotId, selectedCommandRobot, commandRows, copyCommand, newDevice, rotatedToken, enrollmentNetwork, enrollmentCommand, simulation,
|
|
4806
|
+
showLiveKitConfig, livekitForm, preview, deviceInventory,
|
|
4766
4807
|
voiceStacks, characterPresets, showVoiceStackForm, showCharacterForm,
|
|
4767
4808
|
editingVoiceStack, editingCharacter, voiceStackForm, characterForm,
|
|
4768
4809
|
loadModel, unloadModel, statusColor, formatDuration, formatTime,
|
|
4769
4810
|
toggleProductionMode, rotateEnrollmentToken, provisionAgentToken,
|
|
4770
4811
|
submitNewDevice, deleteDevice, rotateDeviceToken, dismissRotatedToken,
|
|
4771
4812
|
joinConversation, saveLiveKitConfig, openLiveKitConfig,
|
|
4772
|
-
startPreview, stopPreview, simulateTrigger, stopConversation, recoverRobot, toggleDeviceProduction, setDeviceProduction, saveDeviceGreetings, updateDeviceMedia,
|
|
4813
|
+
startPreview, stopPreview, simulateTrigger, stopConversation, recoverRobot, toggleDeviceProduction, setDeviceProduction, saveDeviceGreetings, updateDeviceMedia, testSpeaker,
|
|
4773
4814
|
openVoiceStackForm, saveVoiceStack, deleteVoiceStack,
|
|
4774
4815
|
openCharacterForm, saveCharacter, deleteCharacter,
|
|
4775
4816
|
assignRobotCharacter, stackName, characterName,
|
|
@@ -4830,6 +4871,14 @@ async def dashboard():
|
|
|
4830
4871
|
<button @click="commandNetwork = 'tailscale'" :class="commandNetwork === 'tailscale' ? 'bg-cyan-600' : 'bg-gray-700'" class="px-3 py-2 rounded text-sm">Tailscale</button>
|
|
4831
4872
|
</div>
|
|
4832
4873
|
</div>
|
|
4874
|
+
<div class="grid md:grid-cols-2 gap-3 mb-4">
|
|
4875
|
+
<label class="text-xs text-gray-400">LAN hub URL
|
|
4876
|
+
<input v-model="commandHubLan" class="block w-full mt-1 bg-gray-700 rounded px-3 py-2 text-sm text-white" placeholder="http://192.168.x.x:47913">
|
|
4877
|
+
</label>
|
|
4878
|
+
<label class="text-xs text-gray-400">Tailscale hub URL
|
|
4879
|
+
<input v-model="commandHubTailscale" class="block w-full mt-1 bg-gray-700 rounded px-3 py-2 text-sm text-white" placeholder="http://100.x.x.x:47913">
|
|
4880
|
+
</label>
|
|
4881
|
+
</div>
|
|
4833
4882
|
<div class="grid md:grid-cols-2 gap-3">
|
|
4834
4883
|
<div v-for="cmd in commandRows" :key="cmd.group + cmd.name" class="bg-gray-900 rounded p-3">
|
|
4835
4884
|
<div class="flex items-start justify-between gap-2">
|
|
@@ -5106,7 +5155,7 @@ async def dashboard():
|
|
|
5106
5155
|
<select v-model="deviceInventory[robot.id].selectedVideo"
|
|
5107
5156
|
@change="updateDeviceMedia(robot.id)"
|
|
5108
5157
|
class="w-full bg-gray-800 rounded px-2 py-1 text-xs">
|
|
5109
|
-
<option v-for="opt in deviceInventory[robot.id].videoOptions" :key="opt" :value="opt">{{ opt }}</option>
|
|
5158
|
+
<option v-for="opt in deviceInventory[robot.id].videoOptions" :key="deviceInventory[robot.id].optionId(opt)" :value="deviceInventory[robot.id].optionId(opt)">{{ deviceInventory[robot.id].optionName(opt) }}</option>
|
|
5110
5159
|
</select>
|
|
5111
5160
|
</div>
|
|
5112
5161
|
<div>
|
|
@@ -5114,7 +5163,7 @@ async def dashboard():
|
|
|
5114
5163
|
<select v-model="deviceInventory[robot.id].selectedAudio"
|
|
5115
5164
|
@change="updateDeviceMedia(robot.id)"
|
|
5116
5165
|
class="w-full bg-gray-800 rounded px-2 py-1 text-xs">
|
|
5117
|
-
<option v-for="opt in deviceInventory[robot.id].audioOptions" :key="opt" :value="opt">{{ opt }}</option>
|
|
5166
|
+
<option v-for="opt in deviceInventory[robot.id].audioOptions" :key="deviceInventory[robot.id].optionId(opt)" :value="deviceInventory[robot.id].optionId(opt)">{{ deviceInventory[robot.id].optionName(opt) }}</option>
|
|
5118
5167
|
</select>
|
|
5119
5168
|
</div>
|
|
5120
5169
|
<div>
|
|
@@ -5122,7 +5171,7 @@ async def dashboard():
|
|
|
5122
5171
|
<select v-model="deviceInventory[robot.id].selectedAudioOutput"
|
|
5123
5172
|
@change="updateDeviceMedia(robot.id)"
|
|
5124
5173
|
class="w-full bg-gray-800 rounded px-2 py-1 text-xs">
|
|
5125
|
-
<option v-for="opt in deviceInventory[robot.id].audioOutputOptions" :key="opt" :value="opt">{{ opt }}</option>
|
|
5174
|
+
<option v-for="opt in deviceInventory[robot.id].audioOutputOptions" :key="deviceInventory[robot.id].optionId(opt)" :value="deviceInventory[robot.id].optionId(opt)">{{ deviceInventory[robot.id].optionName(opt) }}</option>
|
|
5126
5175
|
</select>
|
|
5127
5176
|
</div>
|
|
5128
5177
|
</div>
|
|
@@ -5131,12 +5180,16 @@ async def dashboard():
|
|
|
5131
5180
|
<textarea v-model="deviceInventory[robot.id].greetingText" rows="2" class="w-full bg-gray-800 rounded px-2 py-1 text-xs" placeholder="Hello! Want to go for a ride?"></textarea>
|
|
5132
5181
|
<button @click="saveDeviceGreetings(robot.id)" class="mt-1 px-2 py-1 rounded bg-gray-600 hover:bg-gray-500 text-xs">Save Greetings</button>
|
|
5133
5182
|
</div>
|
|
5134
|
-
<button @click="startPreview(robot)"
|
|
5183
|
+
<button @click="startPreview(robot)"
|
|
5135
5184
|
:disabled="!settings.production_mode || !livekit.url || !livekit.has_secret || preview.active"
|
|
5136
5185
|
:class="(settings.production_mode && livekit.url && livekit.has_secret && !preview.active) ? 'bg-cyan-600 hover:bg-cyan-700' : 'bg-gray-700 cursor-not-allowed'"
|
|
5137
5186
|
class="w-full px-2 py-1 rounded text-xs">
|
|
5138
|
-
{{ preview.active && preview.robot?.id === robot.id ? 'Preview Active' : 'Start Preview' }}
|
|
5139
|
-
</button>
|
|
5187
|
+
{{ preview.active && preview.robot?.id === robot.id ? 'Preview Active' : 'Start Preview' }}
|
|
5188
|
+
</button>
|
|
5189
|
+
<button @click="testSpeaker(robot)"
|
|
5190
|
+
class="w-full px-2 py-1 rounded text-xs bg-emerald-700 hover:bg-emerald-600">
|
|
5191
|
+
Test Speaker + Mic
|
|
5192
|
+
</button>
|
|
5140
5193
|
<div class="grid grid-cols-2 gap-2">
|
|
5141
5194
|
<button @click="simulateTrigger(robot)"
|
|
5142
5195
|
:disabled="!settings.production_mode || simulation.busy || robot.status === 'connecting' || robot.status === 'running'"
|
|
@@ -66,7 +66,9 @@ import httpx
|
|
|
66
66
|
|
|
67
67
|
logger = logging.getLogger("robopark.preview_agent")
|
|
68
68
|
|
|
69
|
-
_DEVICE_INVENTORY_CACHE: Optional[dict] = None
|
|
69
|
+
_DEVICE_INVENTORY_CACHE: Optional[dict] = None
|
|
70
|
+
_DEVICE_INVENTORY_CACHE_AT = 0.0
|
|
71
|
+
DEVICE_INVENTORY_CACHE_SECONDS = 30.0
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
def _normalize_livekit_url(url: Optional[str]) -> Optional[str]:
|
|
@@ -131,11 +133,13 @@ def _play_audio_effect(selected_output: str | None, effect: str) -> None:
|
|
|
131
133
|
pa.terminate()
|
|
132
134
|
|
|
133
135
|
|
|
134
|
-
def _get_device_inventory() -> dict:
|
|
135
|
-
"""Return discoverable camera and audio devices for dashboard selection."""
|
|
136
|
-
global _DEVICE_INVENTORY_CACHE
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
def _get_device_inventory() -> dict:
|
|
137
|
+
"""Return discoverable camera and audio devices for dashboard selection."""
|
|
138
|
+
global _DEVICE_INVENTORY_CACHE, _DEVICE_INVENTORY_CACHE_AT
|
|
139
|
+
now = time.monotonic()
|
|
140
|
+
if (_DEVICE_INVENTORY_CACHE is not None
|
|
141
|
+
and now - _DEVICE_INVENTORY_CACHE_AT < DEVICE_INVENTORY_CACHE_SECONDS):
|
|
142
|
+
return _DEVICE_INVENTORY_CACHE
|
|
139
143
|
|
|
140
144
|
inventory = {"video": [], "audio_input": [], "audio_output": [], "platform": sys.platform}
|
|
141
145
|
inventory["video"].append({"id": "auto", "name": "Auto detect"})
|
|
@@ -182,7 +186,8 @@ def _get_device_inventory() -> dict:
|
|
|
182
186
|
except Exception as e:
|
|
183
187
|
logger.debug(f"audio inventory unavailable: {e}")
|
|
184
188
|
|
|
185
|
-
_DEVICE_INVENTORY_CACHE = inventory
|
|
189
|
+
_DEVICE_INVENTORY_CACHE = inventory
|
|
190
|
+
_DEVICE_INVENTORY_CACHE_AT = now
|
|
186
191
|
logger.info(
|
|
187
192
|
f"device inventory: {len(inventory['video']) - 2} cameras, "
|
|
188
193
|
f"{len(inventory['audio_input']) - 1} inputs, "
|