From 56e64d9fc2d498ab5035088da0b26a4965efb56b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 20:11:44 +0000 Subject: [PATCH 1/3] Initial plan From 58225c679a837eafef5f23424d6eb8456b114bed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 20:16:55 +0000 Subject: [PATCH 2/3] fix web UI gamepad visual feedback --- .../DESIGN-CONTROLLER/base_station/web/app.js | 35 +++++++++++++++++-- .../base_station/web/index.html | 5 +++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/app.js b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/app.js index 5af3c931..e909f370 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/app.js +++ b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/app.js @@ -218,6 +218,11 @@ })(); // ----- Virtual joystick pads (touch + mouse) ----- + const virtualPads = {}; + function syncPadVisuals(fromGamepad) { + if (virtualPads.l) virtualPads.l.setFromAxes(state.lhx, state.lhy, fromGamepad); + if (virtualPads.r) virtualPads.r.setFromAxes(state.rhx, state.rhy, fromGamepad); + } function createPad(id, axisX, axisY) { const pad = document.getElementById(id); const canvas = pad.querySelector('canvas'); @@ -240,6 +245,14 @@ ctx.fillStyle = active ? '#4caf50' : '#888'; ctx.beginPath(); ctx.arc(cx + hx, cy + hy, 28, 0, Math.PI * 2); ctx.fill(); } + function setFromAxes(ax, ay, isActive) { + const nx = Math.max(-1, Math.min(1, (ax || 0) / 127)); + const ny = Math.max(-1, Math.min(1, (ay || 0) / 127)); + hx = nx * radius; + hy = -ny * radius; + active = !!isActive && (hx !== 0 || hy !== 0); + draw(); + } function setFromEvent(ev) { const r = canvas.getBoundingClientRect(); @@ -270,15 +283,24 @@ draw(); }); resize(); + return { setFromAxes }; } - createPad('pad-left', 'lhx', 'lhy'); - createPad('pad-right', 'rhx', 'rhy'); + virtualPads.l = createPad('pad-left', 'lhx', 'lhy'); + virtualPads.r = createPad('pad-right', 'rhx', 'rhy'); // ----- Button strip ----- let screenButtons = 0; let screenFlags = 0; + const buttonElsByBit = {}; + function syncButtonVisuals(mask) { + Object.keys(buttonElsByBit).forEach((bit) => { + buttonElsByBit[bit].classList.toggle('active', !!(mask & (1 << bit))); + }); + } document.querySelectorAll('button[data-btn]').forEach(btn => { - const bit = 1 << parseInt(btn.dataset.btn, 10); + const idx = parseInt(btn.dataset.btn, 10); + const bit = 1 << idx; + buttonElsByBit[idx] = btn; const set = (down) => { if (controlsLocked) return; if (down) screenButtons |= bit; else screenButtons &= ~bit; @@ -289,6 +311,7 @@ if (!gamepadActive) { state.buttons = screenButtons; state.flags = screenFlags; + syncButtonVisuals(state.buttons); } }; btn.addEventListener('pointerdown', () => set(true)); @@ -463,6 +486,8 @@ state.lhx = state.lhy = state.rhx = state.rhy = 0; state.buttons = screenButtons; state.flags = screenFlags; + syncPadVisuals(false); + syncButtonVisuals(state.buttons); } return; } @@ -477,6 +502,7 @@ state.lhy = clip(-Math.round(dz(gp.axes[1]) * 127)); state.rhx = clip( Math.round(dz(gp.axes[2] || 0) * 127)); state.rhy = clip(-Math.round(dz(gp.axes[3] || 0) * 127)); + syncPadVisuals(true); let buttons = 0, flags = 0; if (gp.buttons[0]?.pressed) buttons |= (1 << 0); // A → curl if (gp.buttons[1]?.pressed) buttons |= (1 << 1); // B → dump @@ -498,12 +524,15 @@ state.lhx = state.lhy = state.rhx = state.rhy = 0; state.buttons = 0; state.flags = 0; + syncPadVisuals(false); + syncButtonVisuals(0); return; } // Refresh hold-to-track edge memory for the buttons we report by level. [0, 1, 2, 3, 5].forEach(i => { prevButtons[i] = !!gp.buttons[i]?.pressed; }); state.buttons = (buttons | screenButtons) & 0xFFFF; state.flags = (flags | screenFlags) & 0xFF; + syncButtonVisuals(state.buttons); } setInterval(pollGamepad, 20); // 50 Hz window.addEventListener('gamepadconnected', pollGamepad); diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/index.html b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/index.html index 46c320cf..4f6a00a9 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/index.html +++ b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/index.html @@ -240,6 +240,11 @@ font-size: 13px; cursor: pointer; } .settings-drawer button:hover { background: rgba(44,50,57,0.85); } + .top-bar button[data-btn].active, + .settings-drawer button[data-btn].active { + background: rgba(76,175,80,0.7); + color: #000; + } .settings-drawer #camera-switch button.active, #camera-switch button.active { background: rgba(42,170,136,0.75); color: #fff; } .settings-drawer #radio-bench-status { From 84026fb38a90b883db6eb926c683d819cd2e62ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 20:21:15 +0000 Subject: [PATCH 3/3] fix gamepad virtual control visual sync --- .../DESIGN-CONTROLLER/base_station/web/app.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/app.js b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/app.js index e909f370..b3e2ff3d 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/app.js +++ b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/web/app.js @@ -246,11 +246,18 @@ ctx.beginPath(); ctx.arc(cx + hx, cy + hy, 28, 0, Math.PI * 2); ctx.fill(); } function setFromAxes(ax, ay, isActive) { + if (!isActive) { + hx = 0; + hy = 0; + active = false; + draw(); + return; + } const nx = Math.max(-1, Math.min(1, (ax || 0) / 127)); const ny = Math.max(-1, Math.min(1, (ay || 0) / 127)); hx = nx * radius; hy = -ny * radius; - active = !!isActive && (hx !== 0 || hy !== 0); + active = (hx !== 0 || hy !== 0); draw(); } @@ -291,16 +298,16 @@ // ----- Button strip ----- let screenButtons = 0; let screenFlags = 0; - const buttonElsByBit = {}; + const buttonVisuals = []; function syncButtonVisuals(mask) { - Object.keys(buttonElsByBit).forEach((bit) => { - buttonElsByBit[bit].classList.toggle('active', !!(mask & (1 << bit))); + buttonVisuals.forEach(({ el, maskBit }) => { + el.classList.toggle('active', !!(mask & maskBit)); }); } document.querySelectorAll('button[data-btn]').forEach(btn => { const idx = parseInt(btn.dataset.btn, 10); const bit = 1 << idx; - buttonElsByBit[idx] = btn; + buttonVisuals.push({ el: btn, maskBit: bit }); const set = (down) => { if (controlsLocked) return; if (down) screenButtons |= bit; else screenButtons &= ~bit;