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