Devices: fixes from review round 10

- Removing or moving the active headset's address waits for running installs,
  like switching.
- A volume change still waiting to be sent goes to the headset whose slider
  it was, and a switch cancels it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-28 23:03:27 +10:00
1 parent 43e19d17f6
commit c69ea6266f
3 files changed
+12 -6

No files matched your search

+3 -1
View File
@@ -349,7 +349,9 @@ class Connecting(unittest.TestCase):
d = self.device("localhost")
other = self.reg.add_device("frame-other")
for body in ({"action": "use", "id": other["id"]}, {"action": "remove", "id": d["id"]},
{"action": "update", "id": d["id"], "port": 2222}):
{"action": "update", "id": d["id"], "port": 2222},
{"action": "address-remove", "id": d["id"], "host": "localhost"},
{"action": "address-update", "id": d["id"], "host": "localhost", "newHost": "127.0.0.1"}):
with self.assertRaises(fd.DeviceError, msg=body):
fl.devices_action(self.link, body, open_setup=None, busy=lambda: 1)
# Renaming, or changing another headset, is fine.
+4 -2
View File
@@ -922,8 +922,10 @@ def devices_action(link, body, open_setup, busy=lambda: 0):
did = body.get("id")
active = link.active_device()
is_active = did == active["id"]
moves = action == "use" or (is_active and (action == "remove" or (
action == "update" and (body.get("user") is not None or body.get("port") is not None))))
moves = action == "use" or (is_active and (
action in ("remove", "address-remove")
or (action == "update" and (body.get("user") is not None or body.get("port") is not None))
or (action == "address-update" and body.get("newHost") not in (None, body.get("host")))))
if moves and busy():
raise frame_devices.DeviceError(
f"Wait for what's running on {active['name']} to finish (see the activity bar), then try again")
+5 -3
View File
@@ -925,11 +925,11 @@ const savesToDevice = () => !!(window.frameApp && window.frameApp.saveImages);
let devGen = 0;
// Answers that don't depend on the headset (this computer's data, the catalogue, jobs).
const HEADSET_FREE = /^\/api\/(devices|connection|host|job|titles\/job|webinstall\/job|apk-versions|android\/(catalog|reports)|steam\/search)\b/;
async function api(path, body) {
async function api(path, body, device = window.connDevice) {
const gen = devGen;
// Changes name the headset the page was showing, so the server refuses one meant
// for a headset it has since switched away from.
const dev = window.connDevice ? { "X-Frame-Device": window.connDevice } : {};
const dev = device ? { "X-Frame-Device": device } : {};
const opts = body === undefined ? { headers: {"X-Frame-UI": UI_KEY} } : {
method: "POST", headers: {"Content-Type": "application/json", "X-Frame-UI": UI_KEY, ...dev}, body: JSON.stringify(body) };
let r;
@@ -1485,8 +1485,9 @@ function setVolume(pct, muted) {
$("vol").oninput = () => {
setVolume(+$("vol").value);
clearTimeout(volTimer);
const dev = window.connDevice; // the headset whose slider this was, even 250 ms later
volTimer = setTimeout(async () => {
try { await api("/api/volume", { level: $("vol").value / 100 }); } catch (e) { toast(e.message, true); }
try { await api("/api/volume", { level: $("vol").value / 100 }, dev); } catch (e) { toast(e.message, true); }
volTimer = null;
}, 250);
};
@@ -2551,6 +2552,7 @@ function onConnection(s) {
devGen++; // answers still on their way are about the other headset
refreshing = null; // and the next status check must be a new one
if (live) toggleLive(false); // the other headset's video or captures
clearTimeout(volTimer); volTimer = null; // a volume change still waiting to be sent
viewGen++; // a capture still on its way is the other headset's too
lastImg = null; lastShot = null;
$("canvas").hidden = true; $("viewerEmpty").hidden = false; $("zoombar").hidden = true;