Heater and Stirrer can be real serial hardware (hendi, Pololu1376), but there was no connection concept at all - the constructors opened the port and crashed the whole server if the device was missing, with no way to see connection status or firmware version and no way to reconnect without a restart. Adds an observable Connectable mixin (components/connectable.py) shared by AHeater/AStirrer; real devices defer opening the serial port to an explicit connect(), auto-connect on server startup, and surface Connected/FirmwareVersion/Simulated plus manual Connect/Disconnect over the web GUI. Heating/stirring and Sud Start are all gated on connection state, and a disconnect mid-brew force-stops the run via the same path as a manual Stop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
106 lines
3.5 KiB
Python
106 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""CLI tool for talking to a Hendi induction plate over serial - see
|
|
components/actor/hendiCtrl.py for the protocol/HendiCtrl class this wraps."""
|
|
|
|
import argparse as ap
|
|
import time
|
|
|
|
from components.actor.hendiCtrl import HendiCtrl
|
|
|
|
DEFAULT_PORT = '/dev/ttyUSB0'
|
|
DEFAULT_BAUDRATE = 115200
|
|
|
|
|
|
def print_version(hendi):
|
|
print(f"{hendi.getSoftwareIdentifier()}, F/W-Version: {hendi.getSoftwareVersion()}")
|
|
|
|
|
|
def print_capabilities(hendi):
|
|
print("Capabilities:", hendi.getCapabilties())
|
|
|
|
|
|
def test_converter(hendi):
|
|
print("Test converters")
|
|
for pwr in range(500, 3600, 100):
|
|
digits = hendi.toDigits(pwr)
|
|
power = hendi.toWatts(digits)
|
|
print(f"pwr={pwr} digits={digits} power={power}")
|
|
|
|
|
|
def test_heater(hendi):
|
|
print("Test Hendi")
|
|
with hendi.remote_open():
|
|
hendi.setSwitch(1)
|
|
time.sleep(1.0)
|
|
for pwr in range(500, 3600, 100):
|
|
hendi.setPowerWatts(pwr)
|
|
pw = hendi.getPowerWatts()
|
|
print("power_w", pw)
|
|
time.sleep(1.0)
|
|
|
|
hendi.setSwitch(0)
|
|
|
|
|
|
def trigger_error_state(hendi):
|
|
"""Enables remote control and deliberately exits without disabling it
|
|
again - reproduces the Hendi's "not closed gracefully" lockout (its
|
|
built-in security feature that fails subsequent connect attempts)
|
|
on demand, instead of waiting for an actual crash to trigger it."""
|
|
print("Triggering error state: enabling remote control, exiting without releasing it")
|
|
hendi.remoteEnable(True)
|
|
|
|
|
|
def reset(hendi):
|
|
"""Toggles the reset line. Confirmed on hardware that this does NOT
|
|
clear the ungraceful-disconnect lockout (see trigger_error_state()) -
|
|
only the device's physical knob/power cycle does that."""
|
|
print("Resetting Hendi (toggling DTR/RTS)")
|
|
hendi.reset()
|
|
time.sleep(1.0)
|
|
|
|
|
|
def update_firmware(hendi, filename):
|
|
hendi.firmware_update(filename)
|
|
|
|
|
|
def main():
|
|
parser = ap.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--serial-port", default=DEFAULT_PORT)
|
|
parser.add_argument("--baudrate", type=int, default=DEFAULT_BAUDRATE)
|
|
parser.add_argument("--debug", action="store_true", help="print raw request/response bytes for every command")
|
|
parser.add_argument("--reset", action="store_true", help="toggle the reset line (does not clear the ungraceful-disconnect lockout - use the device's knob for that)")
|
|
parser.add_argument("--version", action="store_true", help="print software identifier/version")
|
|
parser.add_argument("--capabilities", action="store_true", help="print power/digit capability info")
|
|
parser.add_argument("--test-converter", action="store_true", help="round-trip the watts<->digits polynomials")
|
|
parser.add_argument("--test-heater", action="store_true", help="drive the heater through its power range")
|
|
parser.add_argument("--trigger-error-state", action="store_true",
|
|
help="enable remote control and exit without disabling it, to deliberately reproduce the ungraceful-disconnect lockout")
|
|
parser.add_argument("--update-firmware", metavar="FILE", help="flash the given firmware file")
|
|
args = parser.parse_args()
|
|
|
|
hendi = HendiCtrl(args.serial_port, args.baudrate, debug=args.debug)
|
|
if not hendi.connect():
|
|
print("Could not connect to Hendi - check --serial-port/--baudrate and that the device is powered on.")
|
|
return
|
|
|
|
if args.reset:
|
|
reset(hendi)
|
|
if args.version:
|
|
print_version(hendi)
|
|
if args.capabilities:
|
|
print_capabilities(hendi)
|
|
if args.test_converter:
|
|
test_converter(hendi)
|
|
if args.test_heater:
|
|
test_heater(hendi)
|
|
if args.trigger_error_state:
|
|
trigger_error_state(hendi)
|
|
if args.update_firmware:
|
|
update_firmware(hendi, args.update_firmware)
|
|
|
|
print("End of program.")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|