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
182 lines
5.4 KiB
Python
182 lines
5.4 KiB
Python
import time
|
|
import serial
|
|
import enum
|
|
|
|
|
|
class Varid(enum.Enum):
|
|
STATUS = 0,
|
|
STATUS_ERRORS_OCCURED = 1,
|
|
STATUS_SERIAL_ERRORS_OCCURED = 2,
|
|
STATUS_LIMIT_STATUS = 3,
|
|
STATUS_RESET_FLAGS = 127,
|
|
RC_RC1_UNLIMITED_RAW_VALUE = 4,
|
|
RC_RC1_RAW_VALUE = 5,
|
|
RC_RC1_SCALED_VALUE = 6,
|
|
RC_RC2_UNLIMITED_RAW_VALUE = 8,
|
|
RC_RC2_RAW_VALUE = 9,
|
|
RC_RC2_SCALED_VALUE = 10,
|
|
ANALOG_AN1_UNLIMITED_RAW_VALUE = 12,
|
|
ANALOG_AN1_RAW_VALUE = 13,
|
|
ANALOG_AN1_SCALED_VALUE = 14,
|
|
ANALOG_AN2_UNLIMITED_RAW_VALUE = 16,
|
|
ANALOG_AN2_RAW_VALUE = 17,
|
|
ANALOG_AN2_SCALED_VALUE = 18,
|
|
DIAGNOSTIC_TARGET_SPEED = 20,
|
|
DIAGNOSTIC_SPEED = 21,
|
|
DIAGNOSTIC_BRAKE_AMOUNT = 22,
|
|
DIAGNOSTIC_INPUT_VOLTAGE = 23,
|
|
DIAGNOSTIC_TEMPERATURE_A = 24,
|
|
DIAGNOSTIC_TEMPERATURE_B = 25,
|
|
DIAGNOSTIC_RC_PERIOD = 26,
|
|
DIAGNOSTIC_BAUDRATE_REGISTER = 27,
|
|
DIAGNOSTIC_UP_TIME_LOW = 28,
|
|
DIAGNOSTIC_UP_TIME_HIGH = 29,
|
|
MOTOR_MAX_SPEED_FORWARD = 30,
|
|
MOTOR_MAX_ACCEL_FORWARD = 31,
|
|
MOTOR_MAX_DECEL_FORWARD = 32,
|
|
MOTOR_BREAK_DURATION_FORWARD = 33,
|
|
MOTOR_STARTING_SPEED_FORWARD = 34,
|
|
MOTOR_MAX_SPEED_REVERSE = 36,
|
|
MOTOR_MAX_ACCEL_REVERSE = 37,
|
|
MOTOR_MAX_DECEL_REVERSE = 38,
|
|
MOTOR_BREAK_DURATION_REVERSE = 39,
|
|
MOTOR_STARTING_SPEED_REVERSE = 40,
|
|
MOTOR_CURRENT_LIMIT = 41,
|
|
MOTOR_CURRENT_RAW = 42,
|
|
MOTOR_CURRENT_MA = 43,
|
|
MOTOR_CURRENT_LIMIT_CONSECUTIVE_COUNT = 44,
|
|
MOTOR_CURRENT_LIMIT_OCCURRENCE_COUNT = 45
|
|
|
|
|
|
class Pololu1376:
|
|
def __init__(self, serial_port):
|
|
# Deferred-open pyserial idiom: serial_port is constructed with no
|
|
# port set (see components/actor/stirrerpololu1376.py), so it isn't
|
|
# actually opened until connect() is called - a missing/unplugged
|
|
# device then doesn't crash the whole server at construction time.
|
|
self.ser = serial_port
|
|
self.firmware_version = None
|
|
|
|
def is_connected(self):
|
|
return self.ser.is_open
|
|
|
|
def connect(self):
|
|
if self.ser.is_open:
|
|
return True
|
|
try:
|
|
self.ser.open()
|
|
except serial.SerialException:
|
|
try:
|
|
self.ser.close()
|
|
self.ser.open()
|
|
except serial.SerialException as e:
|
|
print(f"Pololu1376: connect failed: {e}")
|
|
return False
|
|
# set_output_flow_control() requires the port to already be open -
|
|
# can only happen here, after open() above, not at construction time.
|
|
self.ser.set_output_flow_control(False)
|
|
try:
|
|
self.stop()
|
|
self.firmware_version = self.get_firmware_version()
|
|
print("Pololu1376 F/W-Version:", self.firmware_version)
|
|
return True
|
|
except Exception as e:
|
|
print(f"Pololu1376: connect failed: {e}")
|
|
self.ser.close()
|
|
self.firmware_version = None
|
|
return False
|
|
|
|
def disconnect(self):
|
|
if self.ser.is_open:
|
|
try:
|
|
self.stop()
|
|
except Exception:
|
|
pass
|
|
self.ser.close()
|
|
self.firmware_version = None
|
|
|
|
def send(self, data):
|
|
self.ser.write((data + '\r\n').encode())
|
|
|
|
def recv(self):
|
|
data = self.ser.readline().decode("utf-8").replace("\n", '').replace("\r", '')
|
|
return data
|
|
|
|
def cmd(self, cmd):
|
|
self.send(cmd)
|
|
rsp = self.recv()
|
|
|
|
if "." in rsp[0]:
|
|
pass
|
|
elif "!" in rsp[0]:
|
|
pass
|
|
elif "?" in rsp[0]:
|
|
raise Exception("The last command was not understood (a Serial Format Error has occurred).")
|
|
elif rsp is None:
|
|
raise Exception("Timeout.")
|
|
|
|
return rsp[1:]
|
|
|
|
def go(self):
|
|
return self.cmd("GO")
|
|
|
|
def stop(self):
|
|
return self.cmd("X")
|
|
|
|
def motor_forward(self, speed_percent):
|
|
return self.cmd("F" + str(int(speed_percent)) + "%")
|
|
|
|
def motor_reverse(self, speed_percent):
|
|
return self.cmd("R" + str(int(speed_percent)) + "%")
|
|
|
|
def motor_brake(self, brake_amount_percent):
|
|
return self.cmd("B" + str(int(brake_amount_percent)) + "%")
|
|
|
|
def motor_set_limit(self, value):
|
|
self.cmd("L" + str(value))
|
|
|
|
def get_firmware_version(self):
|
|
return self.cmd("V")
|
|
|
|
def get_variable(self, var):
|
|
return int(self.cmd("D" + str(var.value[0])))
|
|
|
|
def print_vars(self):
|
|
print ("-------------------------------------------")
|
|
print ("STATUS : {:04X}".format(self.get_variable(Varid.STATUS)))
|
|
print ("STATUS_ERROR_OCCURED : {:04X}".format(self.get_variable(Varid.STATUS_SERIAL_ERRORS_OCCURED)))
|
|
print ("STATUS_LIMIT_STATUS : {:04X}".format(self.get_variable(Varid.STATUS_LIMIT_STATUS)))
|
|
print ("RC_RC1_SCALED_VALUE : {:04X}".format(self.get_variable(Varid.RC_RC1_SCALED_VALUE)))
|
|
print ("RC_RC2_SCALED_VALUE : {:04X}".format(self.get_variable(Varid.RC_RC2_SCALED_VALUE)))
|
|
print ("ANALOG_AN1_SCALED_VALUE : {:04X}".format(self.get_variable(Varid.ANALOG_AN1_SCALED_VALUE)))
|
|
print ("ANALOG_AN2_SCALED_VALUE : {:04X}".format(self.get_variable(Varid.ANALOG_AN2_SCALED_VALUE)))
|
|
print ("DIAGNOSTIC_TARGET_SPEED : {:04X}".format(self.get_variable(Varid.DIAGNOSTIC_TARGET_SPEED)))
|
|
print ("DIAGNOSTIC_SPEED : {:04X}".format(self.get_variable(Varid.DIAGNOSTIC_SPEED)))
|
|
print ("DIAGNOSTIC_TEMPERATURE_A : {:04X}".format(self.get_variable(Varid.DIAGNOSTIC_TEMPERATURE_A)))
|
|
print ("DIAGNOSTIC_TEMPERATURE_B : {:04X}".format(self.get_variable(Varid.DIAGNOSTIC_TEMPERATURE_B)))
|
|
# print ("MOTOR_CURRENT_RAW : {:04X}".format(self.get_variable(Varid.MOTOR_CURRENT_RAW)))
|
|
# print ("MOTOR_CURRENT_MA : {:04X}".format(self.get_variable(Varid.MOTOR_CURRENT_MA)))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
ser = serial.Serial()
|
|
ser.port = "/dev/ttyACM0"
|
|
ser.baudrate = 115200
|
|
|
|
drv = Pololu1376(serial_port=ser)
|
|
drv.connect()
|
|
drv.go()
|
|
time.sleep(1)
|
|
|
|
for speed in range(0, 70, 5):
|
|
print("Speed = {}".format(speed))
|
|
drv.motor_forward(speed)
|
|
for n in range (0, 10, 1):
|
|
drv.print_vars()
|
|
time.sleep(0.5)
|
|
drv.motor_brake(10)
|
|
time.sleep(1)
|
|
|
|
print("End of program")
|
|
|