openvibers/openvibe.bot Commits
Held drive keeps going through Wi-Fi jitter; a locked control takes the controls (#391)
Changes
@@@ -666,3 +666,14 @@ body.bot-mode-compact .bot-compact :is(.bot-more, .bot-estop) { background: colobody.bot-mode-compact .bot-compact .bot-estop { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 70%, transparent); }body.bot-mode-compact .bot-compact .bot-drive { background: var(--fill-accent); border-color: var(--fill-accent); color: var(--on-fill-accent); }body.bot-mode-compact .bot-compact .bot-notice { color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent); }++++/* A control still locked (no Drive yet): it takes the press (its inputs pass it to their box, panel.js asks for the++ controls) and says so with a small lock at its corner. */++#panel[data-role="watcher"] [data-command] input:disabled,++#panel[data-role="watcher"] [data-command] button:disabled { pointer-events: none; }++#panel[data-role="watcher"]:has([data-drive]:not([hidden])) :is(.bot-stick, .bot-slider, .widget-drive .drive-pad) { cursor: pointer; position: relative; }++#panel[data-role="watcher"]:has([data-drive]:not([hidden])) :is(.bot-stick, .widget-drive .drive-pad)::after {++ content: ""; position: absolute; right: 6%; top: 6%; width: 18px; height: 18px; border-radius: 50%; pointer-events: none;++ background: color-mix(in srgb, var(--fill-accent) 85%, transparent) center / 11px 11px no-repeat++ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.6' stroke-linecap='round'%3E%3Crect x='5' y='11' width='14' height='9' rx='2'/%3E%3Cpath d='M8 11V8a4 4 0 0 1 8 0v3'/%3E%3C/svg%3E");++}@@@ -927,6 +927,18 @@send('point', { x: (e.clientX - left) / width, y: (e.clientY - top) / height });});++ // A press on a control that is still locked, by someone who may take the controls here, asks for them as Drive does++ // (the locked control dims with a lock; public/panel.css). Their next press drives.++ main.addEventListener('pointerdown', (e) => {++ if (!watcher || driving) return;++ const t = e.target && e.target.closest ? e.target.closest('[data-command], .bot-stick, .bot-slider') : null;++ if (!t || (e.target.closest && e.target.closest('[data-drive], [data-bot-more], [data-estop]'))) return;++ const drive = $('[data-drive]:not([hidden])');++ if (!drive) return;++ e.preventDefault();++ drive.click();++ }, true);++// ── State ─────────────────────────────────────────────────────────────────────────────────────────function enableControls() {for (const section of $$('[data-command]')) {@@@ -17,6 +17,6 @@{ "type": "battery", "capability": "battery", "label": "Battery", "order": 3 },{ "type": "latency", "label": "Latency", "order": 4 }],-- "limits": { "max_speed": 0.5, "max_turn": 0.6, "max_command_ms": 300, "heartbeat_ms": 1000 },++ "limits": { "max_speed": 0.5, "max_turn": 0.6, "max_command_ms": 500, "heartbeat_ms": 1000 },"ceiling": { "max_speed": 1, "max_turn": 1 }}@@@ -950,8 +950,10 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limiparentOrigins: mode === 'embed' ? ((config.embed && config.embed.origins) || []) : [],liveUrl: role === 'owner' && mode === 'page' ? liveOrigin : null,allowed_commands: role === 'watcher' ? [] : domain.control.allowedFor(robot, role, profile),-- // A held control is re-sent well inside the device's deadline, so it never stops between two frames.-- holdResendMs: Math.max(50, Math.min(HOLD_RESEND_MS, Math.floor(maxCommandMs / 2))),++ // A held control is re-sent well inside the device's deadline, so it never stops between two frames: at a++ // sixth of it (50 ms at the least). Half left about 90 ms of slack on LOOI, and ordinary Wi-Fi jitter expired++ // commands mid-hold (Maticus, 2026-11-11: four in 9 minutes, gaps of 171–257 ms).++ holdResendMs: Math.max(50, Math.min(HOLD_RESEND_MS, Math.floor(maxCommandMs / 6))),}));}@@@ -28,7 +28,7 @@ const { loadProfiles } = require('../server/profiles');await check('without commissioning, values above the profile default clamp to the default exactly as today', async () => {const { robot } = await t.robot(alex, { profile_id: 'looi', limits: { max_speed: 0.9, max_turn: 0.9 } });-- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 300, cooldownMs: 0 });++ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 500, cooldownMs: 0 });});await check('commissioning needs the owner, a person and an explicit confirm', async () => {@@@ -66,7 +66,7 @@ const { loadProfiles } = require('../server/profiles');assert.strictEqual(r.json.robot.commissioned.by, alex.subject);assert.ok(r.json.robot.commissioned.at);assert.strictEqual((await patch(robot.id, { max_speed: 0.8, max_turn: 0.9 })).status, 200);-- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.8, maxTurn: 0.9, maxCommandMs: 300, cooldownMs: 0 });++ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.8, maxTurn: 0.9, maxCommandMs: 500, cooldownMs: 0 });const audit = await t.domain.audit.list(robot.id, { limit: 20 });const row = audit.find((a) => a.kind === 'robot.commission');assert.ok(row && row.operator_subject === alex.subject && row.result === 'ack');@@@ -76,14 +76,14 @@ const { loadProfiles } = require('../server/profiles');const { robot } = await t.robot(alex, { profile_id: 'looi' });await commission(robot.id);await patch(robot.id, { max_speed: 5, max_turn: 5 });-- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 1, maxTurn: 1, maxCommandMs: 300, cooldownMs: 0 });++ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 1, maxTurn: 1, maxCommandMs: 500, cooldownMs: 0 });});await check('max_command_ms is never raised, even when commissioned', async () => {const { robot } = await t.robot(alex, { profile_id: 'looi' });await commission(robot.id);await patch(robot.id, { max_command_ms: 5000 });-- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 300, cooldownMs: 0 });++ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 500, cooldownMs: 0 });});await check('uncommissioning drops back to the defaults at once, and the audit says so', async () => {@@@ -94,7 +94,7 @@ const { loadProfiles } = require('../server/profiles');const r = await uncommission(robot.id);assert.strictEqual(r.status, 200, r.text);assert.strictEqual(r.json.robot.commissioned, null);-- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 300, cooldownMs: 0 });++ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 500, cooldownMs: 0 });const audit = await t.domain.audit.list(robot.id, { limit: 20 });assert.ok(audit.some((a) => a.kind === 'robot.uncommission' && a.result === 'ack'));});@@@ -105,16 +105,16 @@ const { loadProfiles } = require('../server/profiles');const dev = await t.ws('/device', { headers: { Authorization: `Bearer ${p.json.credential}` } });await dev.waitFor((m) => m.type === 'hello');const first = await dev.waitFor((m) => m.type === 'config');-- assert.deepStrictEqual(first.limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 }, 'clamped before commissioning');++ assert.deepStrictEqual(first.limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 }, 'clamped before commissioning');await commission(robot.id);await dev.waitFor((m) => m.type === 'config' && m.seq > first.seq);const high = await patch(robot.id, { max_speed: 0.9, max_turn: 0.9 });assert.strictEqual(high.status, 200);const second = await dev.waitFor((m) => m.type === 'config' && m.seq > first.seq && m.limits.max_turn === 0.9);-- assert.deepStrictEqual(second.limits, { max_speed: 0.9, max_turn: 0.9, max_command_ms: 300, heartbeat_ms: 1000 });++ assert.deepStrictEqual(second.limits, { max_speed: 0.9, max_turn: 0.9, max_command_ms: 500, heartbeat_ms: 1000 });await uncommission(robot.id);const third = await dev.waitFor((m) => m.type === 'config' && m.seq > second.seq && m.limits.max_speed === 0.5);-- assert.deepStrictEqual(third.limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 }, 'at once, on the device');++ assert.deepStrictEqual(third.limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 }, 'at once, on the device');dev.close();});@@@ -134,7 +134,7 @@ const { loadProfiles } = require('../server/profiles');assert.strictEqual((await post(`/robots/${robot.id}/limits`, bob, { max_speed: '1', max_turn: '1' })).status, 403, 'others cannot set them');assert.strictEqual((await post(`/robots/${robot.id}/limits`, alex, { max_speed: '2', max_turn: '1' })).status, 422, 'above 1 is refused');assert.strictEqual((await post(`/robots/${robot.id}/limits`, alex, { max_speed: '0.9', max_turn: '0.8' })).status, 303);-- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.9, maxTurn: 0.8, maxCommandMs: 300, cooldownMs: (await eff(robot.id)).cooldownMs }, 'saved and in force');++ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.9, maxTurn: 0.8, maxCommandMs: 500, cooldownMs: (await eff(robot.id)).cooldownMs }, 'saved and in force');assert.strictEqual((await post(`/robots/${robot.id}/uncommission`, alex, {})).status, 303);const back = await eff(robot.id);assert.deepStrictEqual([back.maxSpeed, back.maxTurn], [0.5, 0.6], 'uncommissioned: the defaults at once, whatever is stored');@@@ -108,7 +108,8 @@ function samples(profile, kind, names) {const head = renderWidget(p.widgets.find((w) => w.type === 'head'), { profile: p, allowed_commands: ['actuator'] });assert.match(head, /data-actuator="head" data-hold-repeat="90"/, 'the slider carries its repeat');assert.ok(!p.camera, 'no camera of its own');-- assert.ok(p.limits.max_speed <= 0.5 && p.limits.max_command_ms <= 300, 'a desk robot starts slow, with short commands');++ // 500 ms since 2026-11-11 (Maticus: held drive expired on ordinary Wi-Fi jitter at 300); still short.++ assert.ok(p.limits.max_speed <= 0.5 && p.limits.max_command_ms <= 500, 'a desk robot starts slow, with short commands');});await check('camera.onvif is PTZ only; sim.rover is a whole rover (drive, camera, range and line sensors, battery, headlights, horn)', () => {@@@ -32,8 +32,8 @@ const { boot, check, done } = require('./helpers/app');const r = await t.call('GET', `/api/v1/robots/${robot.id}`, { user: alex });assert.strictEqual(r.status, 200, r.text);assert.strictEqual(r.json.robot.owner_subject, alex.subject);-- assert.deepStrictEqual(r.json.robot.effective_limits, { max_speed: 0.4, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 });-- assert.deepStrictEqual(r.json.robot.limits_source.profile, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 });++ assert.deepStrictEqual(r.json.robot.effective_limits, { max_speed: 0.4, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 });++ assert.deepStrictEqual(r.json.robot.limits_source.profile, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 });assert.deepStrictEqual(r.json.robot.limits_source.robot, { max_speed: 0.4 }, 'only the owner\'s overrides');});@@@ -41,7 +41,7 @@ const { boot, check, done } = require('./helpers/app');const { robot } = await t.robot(alex, { profile_id: 'looi' });const r = await t.call('GET', `/api/v1/robots/${robot.id}`, { user: alex });assert.deepStrictEqual(r.json.robot.limits, {});-- assert.deepStrictEqual(r.json.robot.effective_limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 });++ assert.deepStrictEqual(r.json.robot.effective_limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 });assert.deepStrictEqual(r.json.robot.limits_source.robot, {});});
@@ -666,3 +666,14 @@ body.bot-mode-compact .bot-compact :is(.bot-more, .bot-estop) { background: colo
@@ -666,3 +666,14 @@ body.bot-mode-compact .bot-compact :is(.bot-more, .bot-estop) { background: colo
body.bot-mode-compact .bot-compact .bot-estop { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 70%, transparent); }
body.bot-mode-compact .bot-compact .bot-estop { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 70%, transparent); }
body.bot-mode-compact .bot-compact .bot-drive { background: var(--fill-accent); border-color: var(--fill-accent); color: var(--on-fill-accent); }
body.bot-mode-compact .bot-compact .bot-drive { background: var(--fill-accent); border-color: var(--fill-accent); color: var(--on-fill-accent); }
body.bot-mode-compact .bot-compact .bot-notice { color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent); }
body.bot-mode-compact .bot-compact .bot-notice { color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent); }
+
+/* A control still locked (no Drive yet): it takes the press (its inputs pass it to their box, panel.js asks for the
+ controls) and says so with a small lock at its corner. */
+#panel[data-role="watcher"] [data-command] input:disabled,
+#panel[data-role="watcher"] [data-command] button:disabled { pointer-events: none; }
+#panel[data-role="watcher"]:has([data-drive]:not([hidden])) :is(.bot-stick, .bot-slider, .widget-drive .drive-pad) { cursor: pointer; position: relative; }
+#panel[data-role="watcher"]:has([data-drive]:not([hidden])) :is(.bot-stick, .widget-drive .drive-pad)::after {
+ content: ""; position: absolute; right: 6%; top: 6%; width: 18px; height: 18px; border-radius: 50%; pointer-events: none;
+ background: color-mix(in srgb, var(--fill-accent) 85%, transparent) center / 11px 11px no-repeat
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.6' stroke-linecap='round'%3E%3Crect x='5' y='11' width='14' height='9' rx='2'/%3E%3Cpath d='M8 11V8a4 4 0 0 1 8 0v3'/%3E%3C/svg%3E");
+}
@@ -927,6 +927,18 @@
@@ -927,6 +927,18 @@
send('point', { x: (e.clientX - left) / width, y: (e.clientY - top) / height });
send('point', { x: (e.clientX - left) / width, y: (e.clientY - top) / height });
});
});
+ // A press on a control that is still locked, by someone who may take the controls here, asks for them as Drive does
+ // (the locked control dims with a lock; public/panel.css). Their next press drives.
+ main.addEventListener('pointerdown', (e) => {
+ if (!watcher || driving) return;
+ const t = e.target && e.target.closest ? e.target.closest('[data-command], .bot-stick, .bot-slider') : null;
+ if (!t || (e.target.closest && e.target.closest('[data-drive], [data-bot-more], [data-estop]'))) return;
+ const drive = $('[data-drive]:not([hidden])');
+ if (!drive) return;
+ e.preventDefault();
+ drive.click();
+ }, true);
+
// ── State ─────────────────────────────────────────────────────────────────────────────────────────
// ── State ─────────────────────────────────────────────────────────────────────────────────────────
function enableControls() {
function enableControls() {
for (const section of $$('[data-command]')) {
for (const section of $$('[data-command]')) {
@@ -17,6 +17,6 @@
@@ -17,6 +17,6 @@
{ "type": "battery", "capability": "battery", "label": "Battery", "order": 3 },
{ "type": "battery", "capability": "battery", "label": "Battery", "order": 3 },
{ "type": "latency", "label": "Latency", "order": 4 }
{ "type": "latency", "label": "Latency", "order": 4 }
],
],
- "limits": { "max_speed": 0.5, "max_turn": 0.6, "max_command_ms": 300, "heartbeat_ms": 1000 },
+ "limits": { "max_speed": 0.5, "max_turn": 0.6, "max_command_ms": 500, "heartbeat_ms": 1000 },
"ceiling": { "max_speed": 1, "max_turn": 1 }
"ceiling": { "max_speed": 1, "max_turn": 1 }
}
}
@@ -950,8 +950,10 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
@@ -950,8 +950,10 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
parentOrigins: mode === 'embed' ? ((config.embed && config.embed.origins) || []) : [],
parentOrigins: mode === 'embed' ? ((config.embed && config.embed.origins) || []) : [],
liveUrl: role === 'owner' && mode === 'page' ? liveOrigin : null,
liveUrl: role === 'owner' && mode === 'page' ? liveOrigin : null,
allowed_commands: role === 'watcher' ? [] : domain.control.allowedFor(robot, role, profile),
allowed_commands: role === 'watcher' ? [] : domain.control.allowedFor(robot, role, profile),
- // A held control is re-sent well inside the device's deadline, so it never stops between two frames.
- holdResendMs: Math.max(50, Math.min(HOLD_RESEND_MS, Math.floor(maxCommandMs / 2))),
+ // A held control is re-sent well inside the device's deadline, so it never stops between two frames: at a
+ // sixth of it (50 ms at the least). Half left about 90 ms of slack on LOOI, and ordinary Wi-Fi jitter expired
+ // commands mid-hold (Maticus, 2026-11-11: four in 9 minutes, gaps of 171–257 ms).
+ holdResendMs: Math.max(50, Math.min(HOLD_RESEND_MS, Math.floor(maxCommandMs / 6))),
}));
}));
}
}
@@ -28,7 +28,7 @@ const { loadProfiles } = require('../server/profiles');
@@ -28,7 +28,7 @@ const { loadProfiles } = require('../server/profiles');
await check('without commissioning, values above the profile default clamp to the default exactly as today', async () => {
await check('without commissioning, values above the profile default clamp to the default exactly as today', async () => {
const { robot } = await t.robot(alex, { profile_id: 'looi', limits: { max_speed: 0.9, max_turn: 0.9 } });
const { robot } = await t.robot(alex, { profile_id: 'looi', limits: { max_speed: 0.9, max_turn: 0.9 } });
- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 300, cooldownMs: 0 });
+ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 500, cooldownMs: 0 });
});
});
await check('commissioning needs the owner, a person and an explicit confirm', async () => {
await check('commissioning needs the owner, a person and an explicit confirm', async () => {
@@ -66,7 +66,7 @@ const { loadProfiles } = require('../server/profiles');
@@ -66,7 +66,7 @@ const { loadProfiles } = require('../server/profiles');
assert.strictEqual(r.json.robot.commissioned.by, alex.subject);
assert.strictEqual(r.json.robot.commissioned.by, alex.subject);
assert.ok(r.json.robot.commissioned.at);
assert.ok(r.json.robot.commissioned.at);
assert.strictEqual((await patch(robot.id, { max_speed: 0.8, max_turn: 0.9 })).status, 200);
assert.strictEqual((await patch(robot.id, { max_speed: 0.8, max_turn: 0.9 })).status, 200);
- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.8, maxTurn: 0.9, maxCommandMs: 300, cooldownMs: 0 });
+ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.8, maxTurn: 0.9, maxCommandMs: 500, cooldownMs: 0 });
const audit = await t.domain.audit.list(robot.id, { limit: 20 });
const audit = await t.domain.audit.list(robot.id, { limit: 20 });
const row = audit.find((a) => a.kind === 'robot.commission');
const row = audit.find((a) => a.kind === 'robot.commission');
assert.ok(row && row.operator_subject === alex.subject && row.result === 'ack');
assert.ok(row && row.operator_subject === alex.subject && row.result === 'ack');
@@ -76,14 +76,14 @@ const { loadProfiles } = require('../server/profiles');
@@ -76,14 +76,14 @@ const { loadProfiles } = require('../server/profiles');
const { robot } = await t.robot(alex, { profile_id: 'looi' });
const { robot } = await t.robot(alex, { profile_id: 'looi' });
await commission(robot.id);
await commission(robot.id);
await patch(robot.id, { max_speed: 5, max_turn: 5 });
await patch(robot.id, { max_speed: 5, max_turn: 5 });
- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 1, maxTurn: 1, maxCommandMs: 300, cooldownMs: 0 });
+ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 1, maxTurn: 1, maxCommandMs: 500, cooldownMs: 0 });
});
});
await check('max_command_ms is never raised, even when commissioned', async () => {
await check('max_command_ms is never raised, even when commissioned', async () => {
const { robot } = await t.robot(alex, { profile_id: 'looi' });
const { robot } = await t.robot(alex, { profile_id: 'looi' });
await commission(robot.id);
await commission(robot.id);
await patch(robot.id, { max_command_ms: 5000 });
await patch(robot.id, { max_command_ms: 5000 });
- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 300, cooldownMs: 0 });
+ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 500, cooldownMs: 0 });
});
});
await check('uncommissioning drops back to the defaults at once, and the audit says so', async () => {
await check('uncommissioning drops back to the defaults at once, and the audit says so', async () => {
@@ -94,7 +94,7 @@ const { loadProfiles } = require('../server/profiles');
@@ -94,7 +94,7 @@ const { loadProfiles } = require('../server/profiles');
const r = await uncommission(robot.id);
const r = await uncommission(robot.id);
assert.strictEqual(r.status, 200, r.text);
assert.strictEqual(r.status, 200, r.text);
assert.strictEqual(r.json.robot.commissioned, null);
assert.strictEqual(r.json.robot.commissioned, null);
- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 300, cooldownMs: 0 });
+ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.5, maxTurn: 0.6, maxCommandMs: 500, cooldownMs: 0 });
const audit = await t.domain.audit.list(robot.id, { limit: 20 });
const audit = await t.domain.audit.list(robot.id, { limit: 20 });
assert.ok(audit.some((a) => a.kind === 'robot.uncommission' && a.result === 'ack'));
assert.ok(audit.some((a) => a.kind === 'robot.uncommission' && a.result === 'ack'));
});
});
@@ -105,16 +105,16 @@ const { loadProfiles } = require('../server/profiles');
@@ -105,16 +105,16 @@ const { loadProfiles } = require('../server/profiles');
const dev = await t.ws('/device', { headers: { Authorization: `Bearer ${p.json.credential}` } });
const dev = await t.ws('/device', { headers: { Authorization: `Bearer ${p.json.credential}` } });
await dev.waitFor((m) => m.type === 'hello');
await dev.waitFor((m) => m.type === 'hello');
const first = await dev.waitFor((m) => m.type === 'config');
const first = await dev.waitFor((m) => m.type === 'config');
- assert.deepStrictEqual(first.limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 }, 'clamped before commissioning');
+ assert.deepStrictEqual(first.limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 }, 'clamped before commissioning');
await commission(robot.id);
await commission(robot.id);
await dev.waitFor((m) => m.type === 'config' && m.seq > first.seq);
await dev.waitFor((m) => m.type === 'config' && m.seq > first.seq);
const high = await patch(robot.id, { max_speed: 0.9, max_turn: 0.9 });
const high = await patch(robot.id, { max_speed: 0.9, max_turn: 0.9 });
assert.strictEqual(high.status, 200);
assert.strictEqual(high.status, 200);
const second = await dev.waitFor((m) => m.type === 'config' && m.seq > first.seq && m.limits.max_turn === 0.9);
const second = await dev.waitFor((m) => m.type === 'config' && m.seq > first.seq && m.limits.max_turn === 0.9);
- assert.deepStrictEqual(second.limits, { max_speed: 0.9, max_turn: 0.9, max_command_ms: 300, heartbeat_ms: 1000 });
+ assert.deepStrictEqual(second.limits, { max_speed: 0.9, max_turn: 0.9, max_command_ms: 500, heartbeat_ms: 1000 });
await uncommission(robot.id);
await uncommission(robot.id);
const third = await dev.waitFor((m) => m.type === 'config' && m.seq > second.seq && m.limits.max_speed === 0.5);
const third = await dev.waitFor((m) => m.type === 'config' && m.seq > second.seq && m.limits.max_speed === 0.5);
- assert.deepStrictEqual(third.limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 }, 'at once, on the device');
+ assert.deepStrictEqual(third.limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 }, 'at once, on the device');
dev.close();
dev.close();
});
});
@@ -134,7 +134,7 @@ const { loadProfiles } = require('../server/profiles');
@@ -134,7 +134,7 @@ const { loadProfiles } = require('../server/profiles');
assert.strictEqual((await post(`/robots/${robot.id}/limits`, bob, { max_speed: '1', max_turn: '1' })).status, 403, 'others cannot set them');
assert.strictEqual((await post(`/robots/${robot.id}/limits`, bob, { max_speed: '1', max_turn: '1' })).status, 403, 'others cannot set them');
assert.strictEqual((await post(`/robots/${robot.id}/limits`, alex, { max_speed: '2', max_turn: '1' })).status, 422, 'above 1 is refused');
assert.strictEqual((await post(`/robots/${robot.id}/limits`, alex, { max_speed: '2', max_turn: '1' })).status, 422, 'above 1 is refused');
assert.strictEqual((await post(`/robots/${robot.id}/limits`, alex, { max_speed: '0.9', max_turn: '0.8' })).status, 303);
assert.strictEqual((await post(`/robots/${robot.id}/limits`, alex, { max_speed: '0.9', max_turn: '0.8' })).status, 303);
- assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.9, maxTurn: 0.8, maxCommandMs: 300, cooldownMs: (await eff(robot.id)).cooldownMs }, 'saved and in force');
+ assert.deepStrictEqual(await eff(robot.id), { maxSpeed: 0.9, maxTurn: 0.8, maxCommandMs: 500, cooldownMs: (await eff(robot.id)).cooldownMs }, 'saved and in force');
assert.strictEqual((await post(`/robots/${robot.id}/uncommission`, alex, {})).status, 303);
assert.strictEqual((await post(`/robots/${robot.id}/uncommission`, alex, {})).status, 303);
const back = await eff(robot.id);
const back = await eff(robot.id);
assert.deepStrictEqual([back.maxSpeed, back.maxTurn], [0.5, 0.6], 'uncommissioned: the defaults at once, whatever is stored');
assert.deepStrictEqual([back.maxSpeed, back.maxTurn], [0.5, 0.6], 'uncommissioned: the defaults at once, whatever is stored');
@@ -108,7 +108,8 @@ function samples(profile, kind, names) {
@@ -108,7 +108,8 @@ function samples(profile, kind, names) {
const head = renderWidget(p.widgets.find((w) => w.type === 'head'), { profile: p, allowed_commands: ['actuator'] });
const head = renderWidget(p.widgets.find((w) => w.type === 'head'), { profile: p, allowed_commands: ['actuator'] });
assert.match(head, /data-actuator="head" data-hold-repeat="90"/, 'the slider carries its repeat');
assert.match(head, /data-actuator="head" data-hold-repeat="90"/, 'the slider carries its repeat');
assert.ok(!p.camera, 'no camera of its own');
assert.ok(!p.camera, 'no camera of its own');
- assert.ok(p.limits.max_speed <= 0.5 && p.limits.max_command_ms <= 300, 'a desk robot starts slow, with short commands');
+ // 500 ms since 2026-11-11 (Maticus: held drive expired on ordinary Wi-Fi jitter at 300); still short.
+ assert.ok(p.limits.max_speed <= 0.5 && p.limits.max_command_ms <= 500, 'a desk robot starts slow, with short commands');
});
});
await check('camera.onvif is PTZ only; sim.rover is a whole rover (drive, camera, range and line sensors, battery, headlights, horn)', () => {
await check('camera.onvif is PTZ only; sim.rover is a whole rover (drive, camera, range and line sensors, battery, headlights, horn)', () => {
@@ -32,8 +32,8 @@ const { boot, check, done } = require('./helpers/app');
@@ -32,8 +32,8 @@ const { boot, check, done } = require('./helpers/app');
const r = await t.call('GET', `/api/v1/robots/${robot.id}`, { user: alex });
const r = await t.call('GET', `/api/v1/robots/${robot.id}`, { user: alex });
assert.strictEqual(r.status, 200, r.text);
assert.strictEqual(r.status, 200, r.text);
assert.strictEqual(r.json.robot.owner_subject, alex.subject);
assert.strictEqual(r.json.robot.owner_subject, alex.subject);
- assert.deepStrictEqual(r.json.robot.effective_limits, { max_speed: 0.4, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 });
- assert.deepStrictEqual(r.json.robot.limits_source.profile, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 });
+ assert.deepStrictEqual(r.json.robot.effective_limits, { max_speed: 0.4, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 });
+ assert.deepStrictEqual(r.json.robot.limits_source.profile, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 });
assert.deepStrictEqual(r.json.robot.limits_source.robot, { max_speed: 0.4 }, 'only the owner\'s overrides');
assert.deepStrictEqual(r.json.robot.limits_source.robot, { max_speed: 0.4 }, 'only the owner\'s overrides');
});
});
@@ -41,7 +41,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -41,7 +41,7 @@ const { boot, check, done } = require('./helpers/app');
const { robot } = await t.robot(alex, { profile_id: 'looi' });
const { robot } = await t.robot(alex, { profile_id: 'looi' });
const r = await t.call('GET', `/api/v1/robots/${robot.id}`, { user: alex });
const r = await t.call('GET', `/api/v1/robots/${robot.id}`, { user: alex });
assert.deepStrictEqual(r.json.robot.limits, {});
assert.deepStrictEqual(r.json.robot.limits, {});
- assert.deepStrictEqual(r.json.robot.effective_limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 300, heartbeat_ms: 1000 });
+ assert.deepStrictEqual(r.json.robot.effective_limits, { max_speed: 0.5, max_turn: 0.6, max_command_ms: 500, heartbeat_ms: 1000 });
assert.deepStrictEqual(r.json.robot.limits_source.robot, {});
assert.deepStrictEqual(r.json.robot.limits_source.robot, {});
});
});