refactor: drop firmware-version display, move Enable toggle to Controller

Firmware version is already printed to the log at connect() time
(HendiCtrl/Pololu1376) - remove the live GUI/websocket firmware
reporting entirely (Connectable, HeaterHendi/StirrerPololu1376,
Heater/StirrerTask, web GUI) as redundant.

Also move the Heater panel's Closed-loop checkbox into the Controller
panel and rename it "Enable" - the Controller panel now shows a
live Enabled (green) / Disabled (gray) status badge next to its
heading, matching the Connected/Disconnected badges on the Heater/
Stirrer panels.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
This commit is contained in:
2026-07-03 20:23:55 +02:00
co-authored by Claude Sonnet 5
parent 7377fe3b6b
commit 7b162cf1f5
8 changed files with 30 additions and 43 deletions
-2
View File
@@ -23,13 +23,11 @@ class HeaterHendi(AHeater):
def connect(self):
ok = self.hendi.connect()
self.connected = ok
self.firmware_version = self.hendi.sw_ver if ok else None
return ok
def disconnect(self):
self.hendi.disconnect()
self.connected = False
self.firmware_version = None
def activate(self, enable):
if not self.connected:
-2
View File
@@ -23,13 +23,11 @@ class StirrerPololu1376(AStirrer):
def connect(self):
ok = self.drv.connect()
self.connected = ok
self.firmware_version = self.drv.firmware_version if ok else None
return ok
def disconnect(self):
self.drv.disconnect()
self.connected = False
self.firmware_version = None
@contextmanager
def remote_open(self):
+3 -4
View File
@@ -4,16 +4,15 @@ from utils.value import AttributeChange
class Connectable(AttributeChange):
"""Observable hardware-connection state shared by AHeater/AStirrer.
Simulated devices are always connected and report no firmware version;
real (serial) devices start disconnected and only flip once connect()
actually reaches the hardware.
Simulated devices are always connected; real (serial) devices start
disconnected and only flip once connect() actually reaches the
hardware.
"""
def __init__(self, simulated):
AttributeChange.__init__(self)
self.simulated = simulated
self.connected = simulated
self.firmware_version = None
def connect(self):
return self.connected
-5
View File
@@ -21,7 +21,6 @@ class HeaterTask(ATask):
self._on_connected_changed = None
device.set_on_changed('power_eff', ChangedInteger(self.on_changed_power).set)
device.set_on_changed('connected', self.on_connected_changed)
device.set_on_changed('firmware_version', self.on_firmware_version_changed)
self.power_set_changed = ChangedInteger(self.on_changed_power_set).set
def set_on_closed_loop_changed(self, callback):
@@ -38,9 +37,6 @@ class HeaterTask(ATask):
if self._on_connected_changed:
self._on_connected_changed(value)
def on_firmware_version_changed(self, value):
fire_and_forget(self.send({'FirmwareVersion': value}))
def shutdown(self):
"""Called when the brew ends (DONE/IDLE). Switches to open-loop at
power 0 and disables TC, mirroring what a Stop press should do."""
@@ -125,7 +121,6 @@ class HeaterTask(ATask):
# fire and a freshly-subscribed client would never learn it's
# connected.
await self.send({'Connected': self.device.connected})
await self.send({'FirmwareVersion': self.device.firmware_version})
with self.device.open():
while True:
-5
View File
@@ -16,7 +16,6 @@ class StirrerTask(ATask):
stirrer_device.set_on_changed("dutyCycle", self.on_dutycycle_changed)
stirrer_device.set_on_changed("cycleTime", self.on_cycletime_changed)
stirrer_device.set_on_changed("connected", self.on_connected_changed)
stirrer_device.set_on_changed("firmware_version", self.on_firmware_version_changed)
def on_speed_changed(self, value):
asyncio.create_task(self.send({'Speed': value}))
@@ -38,9 +37,6 @@ class StirrerTask(ATask):
if self._on_connected_changed:
self._on_connected_changed(value)
def on_firmware_version_changed(self, value):
fire_and_forget(self.send({'FirmwareVersion': value}))
async def recv(self, data):
for pair in data.items():
if pair[0] == 'Connect':
@@ -69,7 +65,6 @@ class StirrerTask(ATask):
# for why a simulated device's connect() alone doesn't fire the
# observer.
await self.send({'Connected': self.device.connected})
await self.send({'FirmwareVersion': self.device.firmware_version})
with self.device.open():
while True:
+23 -18
View File
@@ -45,10 +45,8 @@ let closedLoop = true;
// are always connected and never expose Connect/Disconnect (see
// updateDeviceStatus()).
let heaterConnected = false;
let heaterFirmware = null;
let heaterSimulated = null;
let stirrerConnected = false;
let stirrerFirmware = null;
let stirrerSimulated = null;
// Pot hardware config from server's Pot section - used for display before
@@ -536,15 +534,14 @@ function updateSudActions() {
document.getElementById('btn-sud-stop').disabled = !(running || paused);
}
// Shared by onHeaterChanged/onStirrerChanged - updates the status badge,
// firmware label, and Connect/Disconnect buttons for one device. Simulated
// devices are always connected and have nothing to connect/disconnect, so
// their buttons are hidden entirely rather than just disabled.
function updateDeviceStatus(prefix, isConnected, firmware, simulated) {
// Shared by onHeaterChanged/onStirrerChanged - updates the status badge
// and Connect/Disconnect buttons for one device. Simulated devices are
// always connected and have nothing to connect/disconnect, so their
// buttons are hidden entirely rather than just disabled.
function updateDeviceStatus(prefix, isConnected, simulated) {
const statusEl = document.getElementById(`${prefix}-status`);
statusEl.textContent = isConnected ? 'Connected' : 'Disconnected';
statusEl.className = `panel-status ${isConnected ? 'status-connected' : 'status-disconnected'}`;
document.getElementById(`${prefix}-firmware`).textContent = firmware ? `F/W ${firmware}` : '';
const connectBtn = document.getElementById(`btn-${prefix}-connect`);
const disconnectBtn = document.getElementById(`btn-${prefix}-disconnect`);
connectBtn.classList.toggle('hidden', !!simulated);
@@ -553,6 +550,15 @@ function updateDeviceStatus(prefix, isConnected, firmware, simulated) {
disconnectBtn.disabled = !isConnected;
}
// Reflects closedLoop (the Controller panel's Enable toggle) in the
// Controller heading's status badge - Enabled/green when the TC is
// driving the heater, Disabled/gray in open-loop.
function updateClosedLoopStatus() {
const statusEl = document.getElementById('closed-loop-status');
statusEl.textContent = closedLoop ? 'Enabled' : 'Disabled';
statusEl.className = `panel-status ${closedLoop ? 'status-connected' : 'status-disconnected'}`;
}
// Mirrors client/brewpi_gui.py's show_user_message(): the Sud is already
// blocked in WAIT_USER server-side - dismissing this dialog (OK or Escape,
// both fire the dialog's 'close' event) is what unblocks it.
@@ -632,6 +638,7 @@ function onHeaterChanged(msg) {
} else if (key === 'ClosedLoop') {
closedLoop = msg.ClosedLoop;
document.getElementById('closed-loop').checked = closedLoop;
updateClosedLoopStatus();
updateControlsEnabled();
} else if (key === 'Capabilities') {
const power = msg.Capabilities.Power;
@@ -641,15 +648,12 @@ function onHeaterChanged(msg) {
slider.max = power.Max;
} else if (key === 'Connected') {
heaterConnected = msg.Connected;
updateDeviceStatus('heater', heaterConnected, heaterFirmware, heaterSimulated);
updateDeviceStatus('heater', heaterConnected, heaterSimulated);
updateControlsEnabled();
updateSudActions();
} else if (key === 'FirmwareVersion') {
heaterFirmware = msg.FirmwareVersion;
updateDeviceStatus('heater', heaterConnected, heaterFirmware, heaterSimulated);
} else if (key === 'Simulated') {
heaterSimulated = msg.Simulated;
updateDeviceStatus('heater', heaterConnected, heaterFirmware, heaterSimulated);
updateDeviceStatus('heater', heaterConnected, heaterSimulated);
}
}
}
@@ -669,15 +673,12 @@ function onStirrerChanged(msg) {
slider.max = power.Max;
} else if (key === 'Connected') {
stirrerConnected = msg.Connected;
updateDeviceStatus('stirrer', stirrerConnected, stirrerFirmware, stirrerSimulated);
updateDeviceStatus('stirrer', stirrerConnected, stirrerSimulated);
updateControlsEnabled();
updateSudActions();
} else if (key === 'FirmwareVersion') {
stirrerFirmware = msg.FirmwareVersion;
updateDeviceStatus('stirrer', stirrerConnected, stirrerFirmware, stirrerSimulated);
} else if (key === 'Simulated') {
stirrerSimulated = msg.Simulated;
updateDeviceStatus('stirrer', stirrerConnected, stirrerFirmware, stirrerSimulated);
updateDeviceStatus('stirrer', stirrerConnected, stirrerSimulated);
}
}
}
@@ -751,6 +752,7 @@ function onSudChanged(msg) {
if (isRunningNow && !wasRunning) {
closedLoop = true;
document.getElementById('closed-loop').checked = true;
updateClosedLoopStatus();
sendMsg('Heater', {ClosedLoop: true});
// Accept the next Soll push from the server even if it
// arrives before sudRunning has gone true at this client
@@ -886,6 +888,7 @@ function connect() {
initialTempSollSync = true;
initialPotTempSync = true;
closedLoop = true;
updateClosedLoopStatus();
potConfig = {};
setConnected(true);
for (const channel of CHANNELS) {
@@ -937,6 +940,8 @@ document.getElementById('btn-connect').addEventListener('click', () => {
});
document.getElementById('closed-loop').addEventListener('change', (e) => {
closedLoop = e.target.checked;
updateClosedLoopStatus();
sendMsg('Heater', {ClosedLoop: e.target.checked});
});
document.getElementById('btn-heater-connect').addEventListener('click', () => {
+4 -6
View File
@@ -38,7 +38,10 @@
<button id="btn-manual-toggle" title="Collapse manual panel">&#9664;</button>
<div id="manual-panels">
<div class="panel">
<h3>Controller</h3>
<h3>Controller <span id="closed-loop-status" class="panel-status status-connected">Enabled</span></h3>
<label class="checkbox-label">
<input type="checkbox" id="closed-loop" checked> Enable
</label>
<label>Temperature [&deg;C]
<div class="slider-row">
<input type="range" id="temp-soll" min="0" max="100" step="1">
@@ -52,13 +55,9 @@
<div class="panel">
<h3>Heater <span id="heater-status" class="panel-status status-disconnected">Disconnected</span></h3>
<div class="device-status-row">
<span id="heater-firmware" class="device-firmware"></span>
<button id="btn-heater-connect">Connect</button>
<button id="btn-heater-disconnect" disabled>Disconnect</button>
</div>
<label class="checkbox-label">
<input type="checkbox" id="closed-loop" checked> Closed-loop
</label>
<label>Power [W]
<div class="slider-row">
<input type="range" id="heater-power" min="0" max="100" step="1">
@@ -69,7 +68,6 @@
<div class="panel">
<h3>Stirrer <span id="stirrer-status" class="panel-status status-disconnected">Disconnected</span></h3>
<div class="device-status-row">
<span id="stirrer-firmware" class="device-firmware"></span>
<button id="btn-stirrer-connect">Connect</button>
<button id="btn-stirrer-disconnect" disabled>Disconnect</button>
</div>
-1
View File
@@ -350,7 +350,6 @@ header#connection-bar {
font-size: 0.9em;
}
.device-status-row button { padding: 0.15em 0.6em; font-size: 0.9em; }
.device-firmware { color: var(--text-muted); }
/* --- Hidden --- */
.hidden { display: none !important; }