From 4b13450b6595110ba78f0891911774cfae13f406 Mon Sep 17 00:00:00 2001 From: Timothy Allen Date: Sat, 2 May 2026 09:00:52 +0200 Subject: [PATCH] Refactor data dict into a proper JBDUPS class. --- bmspy/influxdb.py | 4 - bmspy/jbd_ups.py | 560 +++++++++++++++++++++++++++++++++++++ bmspy/server.py | 692 +++++++++------------------------------------- 3 files changed, 691 insertions(+), 565 deletions(-) create mode 100644 bmspy/jbd_ups.py diff --git a/bmspy/influxdb.py b/bmspy/influxdb.py index 9a5bfba..84b46b4 100644 --- a/bmspy/influxdb.py +++ b/bmspy/influxdb.py @@ -64,10 +64,6 @@ def influxdb_create_snapshot(data, debug=0): now = datetime.datetime.now(datetime.timezone.utc).isoformat() for kind, contains in data.items(): - # discard bmspy metadata - if kind == 'client' or kind == 'timestamp': - break - helpmsg = '' if contains.get('help'): helpmsg = contains.get('help') diff --git a/bmspy/jbd_ups.py b/bmspy/jbd_ups.py new file mode 100644 index 0000000..4b6addd --- /dev/null +++ b/bmspy/jbd_ups.py @@ -0,0 +1,560 @@ +#!/usr/bin/env python3 +# +# Communicate with a JBD/SZLLT BMS and return basic information +# +import atexit +import serial +import serial.rs485 +import time +from dataclasses import dataclass, fields as dataclass_fields + + +@dataclass +class BMSScalarField: + """Single numeric or boolean measurement""" + + help: str + raw_value: float | bool + value: str + units: str | None = None + + def get(self, key, default=None): + return getattr(self, key, default) + + +@dataclass +class BMSMultiField: + """Indexed collection (cells, temperature sensors)""" + + help: str + label: str + raw_values: dict + values: dict + units: str | None = None + + def get(self, key, default=None): + return getattr(self, key, default) + + +@dataclass +class BMSInfoField: + """Text / date info (non-numeric)""" + + help: str + info: str + + def get(self, key, default=None): + return getattr(self, key, default) + + +@dataclass +class JBDUPS: + bms_voltage_total_volts: BMSScalarField | None = None + bms_current_amps: BMSScalarField | None = None + bms_capacity_remaining_ah: BMSScalarField | None = None + bms_capacity_nominal_ah: BMSScalarField | None = None + bms_charge_cycles: BMSScalarField | None = None + bms_manufacture_date: BMSInfoField | None = None + bms_protection_sop_bool: BMSScalarField | None = None + bms_protection_sup_bool: BMSScalarField | None = None + bms_protection_wgop_bool: BMSScalarField | None = None + bms_protection_wgup_bool: BMSScalarField | None = None + bms_protection_cotp_bool: BMSScalarField | None = None + bms_protection_cutp_bool: BMSScalarField | None = None + bms_protection_dotp_bool: BMSScalarField | None = None + bms_protection_dutp_bool: BMSScalarField | None = None + bms_protection_cocp_bool: BMSScalarField | None = None + bms_protection_docp_bool: BMSScalarField | None = None + bms_protection_scp_bool: BMSScalarField | None = None + bms_protection_fdic_bool: BMSScalarField | None = None + bms_protection_slmos_bool: BMSScalarField | None = None + bms_capacity_charge_ratio: BMSScalarField | None = None + bms_charge_is_charging: BMSScalarField | None = None + bms_charge_is_discharging: BMSScalarField | None = None + bms_cell_number: BMSScalarField | None = None + bms_cells_balancing: BMSMultiField | None = None + bms_temperature_sensor_num: BMSScalarField | None = None + bms_temperature_celcius: BMSMultiField | None = None + bms_voltage_cells_volts: BMSMultiField | None = None + + def items(self): + """Yield (name, field) for all non-None BMS field attributes.""" + for f in dataclass_fields(self): + v = getattr(self, f.name) + if v is not None: + yield f.name, v + + +def serial_cleanup(ser, debug=0): + if debug > 2: + print("serial: cleaning up...") + if ser.is_open: + ser.reset_input_buffer() + ser.reset_output_buffer() + ser.close() + + +def initialise_serial(device, debug=0): + ser = serial.Serial(device, baudrate=9600) + ser.parity = serial.PARITY_NONE + ser.bytesize = serial.EIGHTBITS + ser.stopbits = serial.STOPBITS_ONE + ser.timeout = 1 + ser.writeTimeout = 1 + + atexit.register(serial_cleanup, ser, debug) + + return ser + + +def calculate_checksum(msg): + checksum = "" + return checksum + + +def verify_checksum(data, checksum): + # (data + length + command code) checksum, then complement, then add 1, high bit first, low bit last + # data should have start/rw stripped + s = 0 + for i in data: + s += i + s = (s ^ 0xFFFF) + 1 + chk = bytes_to_digits(checksum[0], checksum[1]) + return s == chk + + +def convert_to_signed(x): + # For values below 1024, these seem to be actual results + # For values above 1024, these seem to be encoded to account for high and negative floats + max_uint = 1024 + if x >= max_uint: + return (x - 2**9) % 2**10 - 2**9 + else: + return x + + +def bytes_to_digits(high, low): + result = high + result <<= 8 + result = result | low + return result + + +def bytes_to_date(high, low): + result = bytes_to_digits(high, low) + day = result & 0x1F + mon = (result >> 5) & 0x0F + year = 2000 + (result >> 9) + return "{:04d}-{:02d}-{:02d}".format(year, mon, day) + + +def requestMessage(ser, reqmsg, debug=0): + if debug > 2: + print("serial: starting up monitor") + if ser.is_open: + ser.close() + + try: + ser.open() + except Exception as e: + print("serial: error open port: {0}".format(str(e))) + return False + + if ser.is_open: + try: + # Resetting once alone doesn't seem to do the trick when we discarded data + # on a previous run + for i in range(2): + ser.reset_input_buffer() + ser.reset_output_buffer() + if debug > 0: + print( + "serial: write data: {0}".format( + "".join("0x{:02x} ".format(x) for x in reqmsg) + ) + ) + w = ser.write(reqmsg) + if debug > 2: + print("serial: bytes written: {0}".format(w)) + if w != len(reqmsg): + print( + "serial ERROR: {0} bytes written, {1} expected.".format( + w, len(reqmsg) + ) + ) + return False + wait_time = 0 + while ser.in_waiting == 0: + if wait_time > 2: + serial_cleanup(ser, debug) + return "" + if debug > 2: + print("serial: waiting for data...") + time.sleep(0.5) + wait_time += 1 + if debug > 1: + print("serial: waiting reading: {0}".format(ser.in_waiting)) + response = ser.read_until(b"\x77") + if len(response) == 0: + return "" + if debug > 0: + print("serial: read data: {0}".format(response.hex())) + serial_cleanup(ser, debug) + return response + except Exception as e: + print("serial: error communicating: {0}".format(str(e))) + else: + print("serial: cannot open port") + + +def parse_03_response(response, debug=0): + # Response is 34 bytes: + # 00 begin: \xDD + # 01 r/w: \xA5 + # 02 status: \x00 = correct; \x80 = incorrect + # 03 length (usually 27) + # 04 data (size of length) + # ... + # length+4 checksum + # length+5 checksum + # length+6 end \x77 + if bytes([response[0]]) != b"\xdd": + print("parse_03_response ERROR: first byte not found: {0}".format(response[0])) + return False + + if bytes([response[2]]) == b"\x80": + print( + "parse_03_response ERROR: error byte returned from BMS: {0}".format( + response[2] + ) + ) + return False + + data_len = response[3] + if debug > 2: + print("parse_03_response: data length (trimming 4 bytes): {0}".format(data_len)) + + # The checksum is two bytes, offset by data_len + 4 + # Five bytes at the front of data: begin; rw; status, command; length + # The checksum should check command, length, and data: [3] to [3+data_len+1] + first = data_len + 4 + second = data_len + 5 + if second > len(response): + print("parse_03_response ERROR: primary response checksum not found") + return False + checksum = bytes([response[first], response[second]]) + if not verify_checksum(response[3:first], checksum): + print("parse_03_response ERROR: failed to validate received checksum") + return False + + if data_len == 0: + return False + + result = JBDUPS() + + vtot = bytes_to_digits(response[4], response[5]) * 0.01 + result.bms_voltage_total_volts = BMSScalarField( + help="Total Voltage", raw_value=vtot, value="{:.2f}".format(vtot), units="V" + ) + if debug > 1: + print(" Total voltage: {:.2f}V".format(vtot)) + + current = convert_to_signed(bytes_to_digits(response[6], response[7])) * 0.01 + result.bms_current_amps = BMSScalarField( + help="Current", raw_value=current, value="{:.2f}".format(current), units="A" + ) + if debug > 1: + print(" Current: {:.2f}A".format(current)) + + res_cap = bytes_to_digits(response[8], response[9]) * 0.01 + nom_cap = bytes_to_digits(response[10], response[11]) * 0.01 + result.bms_capacity_remaining_ah = BMSScalarField( + help="Remaining Capacity", + raw_value=res_cap, + value="{:.2f}".format(res_cap), + units="Ah", + ) + result.bms_capacity_nominal_ah = BMSScalarField( + help="Nominal Capacity", + raw_value=nom_cap, + value="{:.2f}".format(nom_cap), + units="Ah", + ) + if debug > 1: + print(" Remaining capacity: {:.2f}Ah".format(res_cap)) + print(" Nominal capacity: {:.2f}Ah".format(nom_cap)) + + cycle_times = bytes_to_digits(response[12], response[13]) + result.bms_charge_cycles = BMSScalarField( + help="Charge Cycles", raw_value=cycle_times, value="{0}".format(cycle_times) + ) + if debug > 1: + print(" Cycle times: {0}".format(cycle_times)) + + man_date = bytes_to_date(response[14], response[15]) + result.bms_manufacture_date = BMSInfoField( + help="Date of Manufacture", info="{0}".format(man_date) + ) + if debug > 1: + print(" Manufacturing date: {0}".format(man_date)) + + cells = response[25] + result.bms_cell_number = BMSScalarField( + help="Cells", raw_value=cells, value="{0}".format(cells) + ) + if debug > 1: + print(" Cells: {0}S".format(cells)) + + balance_state_high = bytes_to_digits(response[16], response[17]) # 1S to 16S + balance_state_low = bytes_to_digits(response[18], response[19]) # 17S to 32S + # 1 bit per 4S (2 bytes = 16S); in 4S, we should expect: + # 0x0 (no cells balancing) 0 + # 0x1 (cell 1 balancing) 1 + # 0x2 (cell 2 balancing) 2 + # 0x3 (cells 1 + 2 balancing) 3 + # 0x4 (cell 3 balancing) 4 + # 0x5 (cells 1 + 3 balancing) 5 + # 0x6 (cells 2 + 3 balancing) 6 + # 0x7 (cells 1 + 2 + 3 balancing) 7 + # 0x8 (cell 4 balancing) 8 + # 0x9 (cells 1 + 4 balancing) 9 + # 0xA (cells 2 + 4 balancing) 10 + # 0xB (cells 1 + 2 + 4 balancing) 11 + # 0xC (cells 3 + 4 balancing) 12 + # 0xD (cells 1 + 3 + 4 balancing) 13 + # 0xE (cells 2 + 3 + 4 balancing) 14 + # 0xF (cells 1 + 2 + 3 + 4 balancing) 15 + raw_balancing = {} + str_balancing = {} + for cell in range(cells): + # Cells 1-16 are recorded in balance_state_low, 17-32 in balance_state_high; + # hilo_cell records the offset relative to the state group + if cell >= 16: + state = balance_state_high + hilo_cell = cell - 16 + else: + state = balance_state_low + hilo_cell = cell + # Cells are recorded as groups of 4 bits (0x0-0xF) per 4 cells + g = int(hilo_cell / 4) + b = 2 ** (hilo_cell - (g * 4)) + balancing = bool((state >> g) & b) + raw_balancing[cell + 1] = balancing + str_balancing[cell + 1] = "{0}".format(int(balancing)) + if debug > 1: + print(" Balancing cell {0}: {1}".format(cell, balancing)) + result.bms_cells_balancing = BMSMultiField( + help="Cells balancing", + label="cell", + raw_values=raw_balancing, + values=str_balancing, + ) + + protection_state = bytes_to_digits(response[20], response[21]) + sop = protection_state & 1 + sup = protection_state & 2 + gop = protection_state & 4 + gup = protection_state & 8 + cotp = protection_state & 16 + cutp = protection_state & 32 + dotp = protection_state & 64 + dutp = protection_state & 128 + cocp = protection_state & 256 + docp = protection_state & 512 + scp = protection_state & 1024 + fdic = protection_state & 2048 + slm = protection_state & 4096 + + def _prot(help, flag): + return BMSScalarField( + help=help, raw_value=bool(flag), value="{0}".format(int(bool(flag))) + ) + + result.bms_protection_sop_bool = _prot("Single overvoltage protection", sop) + result.bms_protection_sup_bool = _prot("Single undervoltage protection", sup) + result.bms_protection_wgop_bool = _prot("Whole group overvoltage protection", gop) + result.bms_protection_wgup_bool = _prot("Whole group undervoltage protection", gup) + result.bms_protection_cotp_bool = _prot( + "Charging over-temperature protection", cotp + ) + result.bms_protection_cutp_bool = _prot( + "Charging under-temperature protection", cutp + ) + result.bms_protection_dotp_bool = _prot( + "Discharging over-temperature protection", dotp + ) + result.bms_protection_dutp_bool = _prot("Discharging under-protection", dutp) + result.bms_protection_cocp_bool = _prot("Charging over-current protection", cocp) + result.bms_protection_docp_bool = _prot("Discharging over-current protection", docp) + result.bms_protection_scp_bool = _prot("Short-circuit protection", scp) + result.bms_protection_fdic_bool = _prot("Front detection IC error", fdic) + result.bms_protection_slmos_bool = _prot("Software lock MOS", slm) + + if debug > 2: + print(" Protection state: {0}".format(protection_state)) + for attr in ( + "sop", + "sup", + "gop", + "gup", + "cotp", + "cutp", + "dotp", + "dutp", + "cocp", + "docp", + "scp", + "fdic", + "slm", + ): + val = locals()[attr] + print(" {}: {}".format(attr, bool(val))) + + rsoc = response[23] * 0.01 + result.bms_capacity_charge_ratio = BMSScalarField( + help="Percent Charge", raw_value=rsoc, value="{0}".format(rsoc), units="‰" + ) + if debug > 1: + print(" Capacity remaining: {0}%".format(rsoc * 100)) + + # bit0 = charging; bit1 = discharging; 0 = MOS closing; 1 = MOS opening + control_status = response[24] + result.bms_charge_is_charging = BMSScalarField( + help="MOSFET charging", + raw_value=bool(control_status & 1), + value="{0}".format(int(bool(control_status & 1))), + ) + result.bms_charge_is_discharging = BMSScalarField( + help="MOSFET discharging", + raw_value=bool((control_status >> 1) & 1), + value="{0}".format(int(bool((control_status >> 1) & 1))), + ) + if debug > 1: + print(" MOSFET charging: {0}".format("yes" if (control_status & 1) else "no")) + print( + " MOSFET discharging: {0}".format( + "yes" if ((control_status >> 1) & 1) else "no" + ) + ) + + ntc_num = response[26] + temperatures = [ + (bytes_to_digits(response[27 + (2 * i)], response[28 + (2 * i)]) - 2731) * 0.1 + for i in range(ntc_num) + ] + result.bms_temperature_sensor_num = BMSScalarField( + help="Temperature Sensors", raw_value=ntc_num, value="{0}".format(ntc_num) + ) + result.bms_temperature_celcius = BMSMultiField( + help="Temperature", + label="sensor", + raw_values={i + 1: temp for i, temp in enumerate(temperatures)}, + values={i + 1: "{:.2f}".format(temp) for i, temp in enumerate(temperatures)}, + units="°C", + ) + if debug > 1: + print(" Number of temperature sensors: {0}".format(ntc_num)) + for i, temp in enumerate(temperatures): + print(" Temperature sensor {:d}: {:.2f}°C".format(i + 1, temp)) + + return result + + +def parse_04_response(response, debug=0): + # Response is 7 + cells * 2 bytes: + # 00 begin: \xDD + # 01 r/w: \xA5 + # 02 status: \x00 = correct; \x80 = incorrect + # 03 length (usually 8) + # 04 data (size of length) + # ... + # length+4 checksum + # length+5 checksum + # length+6 end \x77 + if bytes([response[0]]) != b"\xdd": + print("parse_04_response ERROR: first byte not found: {0}".format(response[0])) + return False + + if bytes([response[2]]) == b"\x80": + print( + "parse_04_response ERROR: error byte returned from BMS: {0}".format( + response[2] + ) + ) + return False + + data_len = response[3] + if debug > 2: + print(" Data length (trimming 4 bytes): {0}".format(data_len)) + + # The checksum is two bytes, offset by data_len + 4 + # Five bytes at the front of data: begin; rw; status, command; length + # The checksum should check command, length, and data: [3] to [3+data_len+1] + first = data_len + 4 + second = data_len + 5 + if second > len(response): + print("parse_04_response ERROR: cell voltage checksum not found") + return False + checksum = bytes([response[first], response[second]]) + if not verify_checksum(response[3:first], checksum): + print("parse_04_response ERROR: failed to validate received checksum") + return False + + if data_len == 0: + return False + + raw_values = {} + str_values = {} + for cell in range(int(data_len / 2)): + lo = (cell * 2) + 4 + hi = (cell * 2) + 5 + cellv = bytes_to_digits(response[lo], response[hi]) * 0.001 + raw_values[cell + 1] = cellv + str_values[cell + 1] = "{:.3f}".format(cellv) + if debug > 1: + print(" Cell {:.0f}: {:.3f}V".format(cell + 1, cellv)) + + return BMSMultiField( + help="Cell Voltages", + label="cell", + raw_values=raw_values, + values=str_values, + units="V", + ) + + +def collect_data(ser, debug=0): + # Request is 7 bytes: + # \xDD for start + # \xA5 for read, \x5A for write + # \x03 for regular info; \x04 for individual voltages + # \x77 ends + reqmsg = bytearray([0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77]) + response_03 = requestMessage(ser, reqmsg, debug) + + if len(response_03) == 0: + if debug > 0: + print("collect_data: Error retrieving BMS info. Trying again...") + return False + response_03 = bytearray(response_03) + + reqmsg = bytearray([0xDD, 0xA5, 0x04, 0x00, 0xFF, 0xFC, 0x77]) + response_04 = requestMessage(ser, reqmsg, debug) + + if len(response_04) == 0: + if debug > 0: + print("collect_data: Error retrieving BMS info. Trying again...") + return False + response_04 = bytearray(response_04) + + result = parse_03_response(response_03, debug) + if result is False: + return False + + cell_field = parse_04_response(response_04, debug) + if cell_field is False: + return False + + result.bms_voltage_cells_volts = cell_field + + return result diff --git a/bmspy/server.py b/bmspy/server.py index 0177a02..2382c12 100755 --- a/bmspy/server.py +++ b/bmspy/server.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -# Communicate with a JBD/SZLLT BMS and return basic information -# in order to shutdown equipment when voltage levels drop or remaining -# capacity gets low +# Daemon: listens on a Unix socket and serves JBD BMS data to clients # -import os, sys, stat, time +import os +import sys +import stat +import time +import atexit +import signal import json -import atexit, signal -import serial, serial.rs485 -import struct, json -import pprint +import struct +from dataclasses import asdict as dataclass_asdict -connected_clients = list() -current_data = dict() +from bmspy.jbd_ups import collect_data, initialise_serial +# Expected kernel log output when the USB-serial adapter is plugged in: # usb 1-1.4: new full-speed USB device number 4 using xhci_hcd # usb 1-1.4: New USB device found, idVendor=0403, idProduct=6001, bcdDevice= 6.00 # usb 1-1.4: New USB device strings: Mfr=1, Product=2, SerialNumber=3 @@ -27,514 +28,28 @@ current_data = dict() # ftdi_sio 1-1.4:1.0: FTDI USB Serial Device converter detected # usb 1-1.4: Detected FT232RL # usb 1-1.4: FTDI USB Serial Device converter now attached to ttyUSB0 -# usb 1-1.4: usbfs: interface 0 claimed by ftdi_sio while 'python3' sets config #1 + +connected_clients = list() +current_data = None -# Catch systemd signals def signalHandler(): - raise SystemExit('terminating') + raise SystemExit("terminating") + -''' Clean up socket ''' def socket_cleanup(socket_path, debug=0): os.unlink(socket_path) -''' Clean up serial port ''' -def serial_cleanup(ser, debug=0): - if debug > 2: - print("serial: cleaning up...") - if ser.is_open: - ser.reset_input_buffer() # flush input buffer, discarding all its contents - ser.reset_output_buffer() # flush output buffer, aborting current output - ser.close() - -def initialise_serial(device, debug=0): - # TODO: ensure ttyUSB0 points to idVendor=0403, idProduct=6001 - # with serial.tools.list_ports.ListPortInfo - # python3 -m serial.tools.list_ports USB - ser = serial.Serial(device, baudrate=9600) - ser.parity = serial.PARITY_NONE # set parity check: no parity - ser.bytesize = serial.EIGHTBITS # number of bits per bytes - ser.stopbits = serial.STOPBITS_ONE # number of stop bits - ser.timeout = 1 # timeout block read - ser.writeTimeout = 1 # timeout for write - - atexit.register(serial_cleanup, ser, debug) - - return ser - -def calculate_checksum(msg): - checksum = '' - return checksum - -def verify_checksum(data, checksum): - # (data + length + command code) checksum, then complement, then add 1, high bit first, low bit last - # data should have start/rw stripped - s = 0 - for i in data: - s += i - s = (s ^ 0xFFFF) + 1 - chk = bytes_to_digits(checksum[0], checksum[1]) - return s == chk - -def convert_to_signed(x): - # For values below 1024, these seem to be actual results - # For values above 1024, these seem to be encoded to account for high and negative floats - max_uint = 1024 - if x >= max_uint: - return (x - 2**9) % 2**10 - 2**9 - else: - return x - -def bytes_to_digits(high, low): - result = high - result <<= 8 - result = result | low - return result - -def bytes_to_date(high, low): - result= bytes_to_digits(high, low) - day = result & 0x1f - mon = (result >> 5) & 0x0f - year = 2000 + (result >> 9) - return "{:04d}-{:02d}-{:02d}".format(year, mon, day) - -# takes a serial object and a message, returns a response -def requestMessage(ser, reqmsg, debug=0): - if debug > 2: - print('serial: starting up monitor') - if ser.is_open: - ser.close() - - try: - ser.open() - except Exception as e: - print("serial: error open port: {0}".format(str(e))) - return False - - if ser.is_open: - try: - # Resetting once alone doesn't seem to do the trick when we discarded data - # on a previous run - for i in range(2): - ser.reset_input_buffer() # flush input buffer, discarding all its contents - ser.reset_output_buffer() # flush output buffer, aborting current output - if debug > 0: - print("serial: write data: {0}".format("".join('0x{:02x} '.format(x) for x in reqmsg))) - w = ser.write(reqmsg) - if debug > 2: - print("serial: bytes written: {0}".format(w)) - #time.sleep(1) - if w != len(reqmsg): - print("serial ERROR: {0} bytes written, {1} expected.".format(w, len(reqmsg))) - return False - wait_time = 0 - while ser.in_waiting == 0: - # Return an empty string if we end up waiting too long - if wait_time > 2: - serial_cleanup(ser, debug) - return '' - if debug > 2: - print("serial: waiting for data...") - time.sleep(0.5) - wait_time += 1 - if debug > 1: - print("serial: waiting reading: {0}".format(ser.in_waiting)) - response = ser.read_until(b'\x77') - # Return an empty string if the read timed out or returned nothing - if len(response) == 0: - return '' - if debug > 0: - print("serial: read data: {0}".format(response.hex())) - serial_cleanup(ser, debug) - return response - except Exception as e: - print("serial: error communicating: {0}".format(str(e))) - else: - print("serial: cannot open port") - -def parse_03_response(response, debug=0): - data = dict() - # Response is 34 bytes: - # 00 begin: \xDD - # 01 r/w: \xA5 - # 02 status: \x00 = correct; \x80 = incorrect - # 03 length (usually 27) - # 04 data (size of length) - # ... - # length+4 checksum - # length+5 checksum - # length+6 end \x77 - if bytes([response[0]]) != b'\xdd': - print("parse_03_response ERROR: first byte not found: {0}".format(response[0])) - return False - - if bytes([response[2]]) == b'\x80': - print("parse_03_response ERROR: error byte returned from BMS: {0}".format(response[2])) - return False - - data_len = response[3] - if debug > 2: - print("parse_03_response: data length (trimming 4 bytes): {0}".format(data_len)) - - # The checksum is two bytes, offset by data_len + 4 - # Five bytes at the front of data - # begin; rw; status, command; length - # The checksum should check command, length, and data: [3] to [3+data_len+1] - first = data_len + 4 - second = data_len + 5 - if second > len(response): - print("parse_03_response ERROR: primary response checksum not found") - return False; - checksum = bytes([response[first], response[second]]) - if not verify_checksum(response[3:first], checksum): - print("parse_03_response ERROR: failed to validate received checksum") - return False - - if data_len > 0: - vtot = bytes_to_digits(response[4], response[5]) * 0.01 - data['bms_voltage_total_volts'] = dict() - data['bms_voltage_total_volts']['help'] = "Total Voltage" - data['bms_voltage_total_volts']['raw_value'] = vtot - data['bms_voltage_total_volts']['value'] = "{:.2f}".format(vtot) - data['bms_voltage_total_volts']['units'] = "V" - if debug > 1: - print(" Total voltage: {:.2f}V".format(vtot)) - - current = bytes_to_digits(response[6], response[7]) - current = convert_to_signed(current) * 0.01 - data['bms_current_amps'] = dict() - data['bms_current_amps']['help'] = "Current" - data['bms_current_amps']['raw_value'] = current - data['bms_current_amps']['value'] = "{:.2f}".format(current) - data['bms_current_amps']['units'] = "A" - if debug > 1: - print(" Current: {:.2f}A".format(current)) - - res_cap = bytes_to_digits(response[8], response[9]) * 0.01 - nom_cap = bytes_to_digits(response[10], response[11]) * 0.01 - data['bms_capacity_remaining_ah'] = dict() - data['bms_capacity_remaining_ah']['help'] = "Remaining Capacity" - data['bms_capacity_remaining_ah']['raw_value'] = res_cap - data['bms_capacity_remaining_ah']['value'] = "{:.2f}".format(res_cap) - data['bms_capacity_remaining_ah']['units'] = "Ah" - data['bms_capacity_nominal_ah'] = dict() - data['bms_capacity_nominal_ah']['help'] = "Nominal Capacity" - data['bms_capacity_nominal_ah']['raw_value'] = nom_cap - data['bms_capacity_nominal_ah']['value'] = "{:.2f}".format(nom_cap) - data['bms_capacity_nominal_ah']['units'] = "Ah" - if debug > 1: - print(" Remaining capacity: {:.2f}Ah".format(res_cap)) - print(" Nominal capacity: {:.2f}Ah".format(nom_cap)) - - cycle_times = bytes_to_digits(response[12], response[13]) - data['bms_charge_cycles'] = dict() - data['bms_charge_cycles']['help'] = "Charge Cycles" - data['bms_charge_cycles']['raw_value'] = cycle_times - data['bms_charge_cycles']['value'] = "{0}".format(cycle_times) - if debug > 1: - print(" Cycle times: {0}".format(cycle_times)) - - man_date = bytes_to_date(response[14], response[15]) - data['bms_manufacture_date'] = dict() - data['bms_manufacture_date']['help'] = "Date of Manufacture" - data['bms_manufacture_date']['info'] = "{0}".format(man_date) - if debug > 1: - print(" Manufacturing date: {0}".format(man_date)) - - cells = response[25] # 4S - data['bms_cell_number'] = dict() - data['bms_cell_number']['help'] = "Cells" - data['bms_cell_number']['raw_value'] = cells - data['bms_cell_number']['value'] = "{0}".format(cells) - if debug > 1: - print(" Cells: {0}S".format(cells)) - - balance_state_high = bytes_to_digits(response[16], response[17]) # 1S to 16S - balance_state_low = bytes_to_digits(response[18], response[19]) # 17S to 32S - # 1 bit per 4S (2 bytes = 16S); in 4S, we should expect: - # 0x0 (no cells balancing) 0 - # 0x1 (cell 1 balancing) 1 - # 0x2 (cell 2 balancing) 2 - # 0x3 (cells 1 + 2 balancing) 3 - # 0x4 (cell 3 balancing) 4 - # 0x5 (cells 1 + 3 balancing) 5 - # 0x6 (cells 2 + 3 balancing) 6 - # 0x7 (cells 1 + 2 + 3 balancing) 7 - # 0x8 (cell 4 balancing) 8 - # 0x9 (cells 1 + 4 balancing) 9 - # 0xA (cells 2 + 4 balancing) 10 - # 0xB (cells 1 + 2 + 4 balancing) 11 - # 0xC (cells 3 + 4 balancing) 12 - # 0xD (cells 1 + 3 + 4 balancing) 13 - # 0xE (cells 2 + 3 + 4 balancing) 14 - # 0xF (cells 1 + 2 + 3 + 4 balancing) 15 - #data["Balancing"] = dict() - data['bms_cells_balancing'] = dict() - data['bms_cells_balancing']['help'] = "Cells balancing" - data['bms_cells_balancing']['label'] = 'cell' - data['bms_cells_balancing']['raw_values'] = dict() - data['bms_cells_balancing']['values'] = dict() - for cell in range(cells): - # Cells from 1 to 16 are recorded in balance_state_low, - # and from 17 to 32 in balance_state_high; hilo_cell records the offset - # relative to the state group - if cell >= 16: - state = balance_state_high - hilo_cell = cell - 16 - else: - state = balance_state_low - hilo_cell = cell - # Cells are recorded as groups of 4 bits (0x0-0xF) per 4 cells - g = int(hilo_cell / 4) - b = 2**(hilo_cell - (g * 4 )) - data['bms_cells_balancing']['raw_values'][cell+1] = bool((state >> g) & b) - data['bms_cells_balancing']['values'][cell+1] = "{0}".format(int(bool((state >> g) & b))) - if debug > 1: - print(" Balancing cell {0}: {1}".format(cell, bool((state >> g & b)))) - - protection_state = bytes_to_digits(response[20], response[21]) - sop = protection_state & 1 - sup = protection_state & 2 - gop = protection_state & 4 - gup = protection_state & 8 - cotp = protection_state & 16 - cutp = protection_state & 32 - dotp = protection_state & 64 - dutp = protection_state & 128 - cocp = protection_state & 256 - docp = protection_state & 512 - scp = protection_state & 1024 - fdic = protection_state & 2048 - slm = protection_state & 4096 - data['bms_protection_sop_bool'] = dict() - data['bms_protection_sop_bool']['help'] = "Single overvoltage protection" - data['bms_protection_sop_bool']['raw_value'] = bool(sop) - data['bms_protection_sop_bool']['value'] = "{0}".format(int(bool(sop))) - data['bms_protection_sup_bool'] = dict() - data['bms_protection_sup_bool']['help'] = "Single undervoltage protection" - data['bms_protection_sup_bool']['raw_value'] = bool(sup) - data['bms_protection_sup_bool']['value'] = "{0}".format(int(bool(sup))) - data['bms_protection_wgop_bool'] = dict() - data['bms_protection_wgop_bool']['help'] = "Whole group overvoltage protection" - data['bms_protection_wgop_bool']['raw_value'] = bool(gop) - data['bms_protection_wgop_bool']['value'] = "{0}".format(int(bool(gop))) - data['bms_protection_wgup_bool'] = dict() - data['bms_protection_wgup_bool']['help'] = "Whole group undervoltage protection" - data['bms_protection_wgup_bool']['raw_value'] = bool(gup) - data['bms_protection_wgup_bool']['value'] = "{0}".format(int(bool(gup))) - data['bms_protection_cotp_bool'] = dict() - data['bms_protection_cotp_bool']['help'] = "Charging over-temperature protection" - data['bms_protection_cotp_bool']['raw_value'] = bool(cotp) - data['bms_protection_cotp_bool']['value'] = "{0}".format(int(bool(cotp))) - data['bms_protection_cutp_bool'] = dict() - data['bms_protection_cutp_bool']['help'] = "Charging under-temperature protection" - data['bms_protection_cutp_bool']['raw_value'] = bool(cutp) - data['bms_protection_cutp_bool']['value'] = "{0}".format(int(bool(cutp))) - data['bms_protection_dotp_bool'] = dict() - data['bms_protection_dotp_bool']['help'] = "Discharging over-temperature protection" - data['bms_protection_dotp_bool']['raw_value'] = bool(dotp) - data['bms_protection_dotp_bool']['value'] = "{0}".format(int(bool(dotp))) - data['bms_protection_dutp_bool'] = dict() - data['bms_protection_dutp_bool']['help'] = "Discharging under-protection" - data['bms_protection_dutp_bool']['raw_value'] = bool(dutp) - data['bms_protection_dutp_bool']['value'] = "{0}".format(int(bool(dutp))) - data['bms_protection_cocp_bool'] = dict() - data['bms_protection_cocp_bool']['help'] = "Charging over-current protection" - data['bms_protection_cocp_bool']['raw_value'] = bool(cocp) - data['bms_protection_cocp_bool']['value'] = "{0}".format(int(bool(cocp))) - data['bms_protection_docp_bool'] = dict() - data['bms_protection_docp_bool']['help'] = "Discharging over-current protection" - data['bms_protection_docp_bool']['raw_value'] = bool(docp) - data['bms_protection_docp_bool']['value'] = "{0}".format(int(bool(docp))) - data['bms_protection_scp_bool'] = dict() - data['bms_protection_scp_bool']['help'] = "Short-circuit protection" - data['bms_protection_scp_bool']['raw_value'] = bool(scp) - data['bms_protection_scp_bool']['value'] = "{0}".format(int(bool(scp))) - data['bms_protection_fdic_bool'] = dict() - data['bms_protection_fdic_bool']['help'] = "Front detection IC error" - data['bms_protection_fdic_bool']['raw_value'] = bool(fdic) - data['bms_protection_fdic_bool']['value'] = "{0}".format(int(bool(fdic))) - data['bms_protection_slmos_bool'] = dict() - data['bms_protection_slmos_bool']['help'] = "Software lock MOS" - data['bms_protection_slmos_bool']['raw_value'] = bool(slm) - data['bms_protection_slmos_bool']['value'] = "{0}".format(int(bool(slm))) - if debug > 2: - print(" Protection state: {0}".format(protection_state)) - print(" Single overvoltage protection: {0}".format(bool(sop))) - print(" Single undervoltage protection: {0}".format(bool(sup))) - print(" Whole group overvoltage protection: {0}".format(bool(gop))) - print(" Whole group undervoltage protection: {0}".format(bool(gup))) - print(" Charging over-temperature protection: {0}".format(bool(cotp))) - print(" Charging under-temperature protection: {0}".format(bool(cutp))) - print(" Discharging over-temperature protection: {0}".format(bool(dotp))) - print(" Discharging under-protection: {0}".format(bool(dutp))) - print(" Charging over-current protection: {0}".format(bool(cocp))) - print(" Discharging over-current protection: {0}".format(bool(docp))) - print(" Short-circuit protection: {0}".format(bool(scp))) - print(" Front detection IC error: {0}".format(bool(fdic))) - print(" Software lock MOS: {0}".format(bool(slm))) - - software_version = bytes([response[22]]) - - # percent of capacity remaining, converted to a per mille ratio between 0 and 1 - rsoc = response[23] * 0.01 - data['bms_capacity_charge_ratio'] = dict() - data['bms_capacity_charge_ratio']['help'] = "Percent Charge" - data['bms_capacity_charge_ratio']['raw_value'] = rsoc - data['bms_capacity_charge_ratio']['value'] = "{0}".format(rsoc) - data['bms_capacity_charge_ratio']['units'] = "\u2030" - if debug > 1: - print(" Capacity remaining: {0}%".format(rsoc * 100)) - - # bit0 = charging; bit1 = discharging; 0 = MOS closing; 1 = MOS opening - control_status = response[24] - data['bms_charge_is_charging'] = dict() - data['bms_charge_is_charging']['help'] = "MOSFET charging" - data['bms_charge_is_charging']['raw_value'] = bool(control_status & 1) - data['bms_charge_is_charging']['value'] = "{0}".format(int(bool(control_status & 1))) - data['bms_charge_is_discharging'] = dict() - data['bms_charge_is_discharging']['help'] = "MOSFET discharging" - data['bms_charge_is_discharging']['raw_value'] = bool(control_status & 1) - data['bms_charge_is_discharging']['value'] = "{0}".format(int(bool(control_status & 1))) - if debug > 1: - if (control_status & 1): - print(" MOSFET charging: yes") - else: - print(" MOSFET charging: no") - if ((control_status >> 1) & 1): - print(" MOSFET discharging: yes") - else: - print(" MOSFET discharging: no") - - ntc_num = response[26] # number of temperature sensors - ntc_content = bytearray() # 2 * ntc_num in size - temperatures = list() - for i in range(ntc_num): - temperatures.append((bytes_to_digits(response[27+(2*i)], response[28+(2*i)]) - 2731) * 0.1) - data['bms_temperature_sensor_num'] = dict() - data['bms_temperature_sensor_num']['help'] = "Temperature Sensors" - data['bms_temperature_sensor_num']['raw_value'] = ntc_num - data['bms_temperature_sensor_num']['value'] = "{0}".format(ntc_num) - data['bms_temperature_celcius'] = dict() - data['bms_temperature_celcius']['help'] = "Temperature" - data['bms_temperature_celcius']['units'] = "\u00B0C" - data['bms_temperature_celcius']['label'] = 'sensor' - data['bms_temperature_celcius']['raw_values'] = dict() - data['bms_temperature_celcius']['values'] = dict() - for i, temp in enumerate(temperatures): - data['bms_temperature_celcius']['raw_values'][i+1] = temp - data['bms_temperature_celcius']['values'][i+1] = "{:.2f}".format(temp) - if debug > 1: - print(" Number of temperature sensors: {0}".format(ntc_num)) - for i, temp in enumerate(temperatures): - print(u" Temperature sensor {:d}: {:.2f}\u00B0C".format(i+1, temp)) - - return data - -def parse_04_response(response, debug=0): - data = dict() - # Response is 7 + cells * 2 bytes: - # 00 begin: \xDD - # 01 r/w: \xA5 - # 02 status: \x00 = correct; \x80 = incorrect - # 03 length (usually 8) - # 04 data (size of length) - # ... - # length+4 checksum - # length+5 checksum - # length+6 end \x77 - if bytes([response[0]]) != b'\xdd': - print("parse_04_response ERROR: first byte not found: {0}".format(response[0])) - return False - - if bytes([response[2]]) == b'\x80': - print("parse_04_response ERROR: error byte returned from BMS: {0}".format(response[2])) - return False - - data_len = response[3] - if debug > 2: - print(" Data length (trimming 4 bytes): {0}".format(data_len)) - - # The checksum is two bytes, offset by data_len + 4 - # Five bytes at the front of data - # begin; rw; status, command; length - # The checksum should check command, length, and data: [3] to [3+data_len+1] - first = data_len + 4 - second = data_len + 5 - if second > len(response): - print("parse_04_response ERROR: cell voltage checksum not found") - return False - checksum = bytes([response[first], response[second]]) - if not verify_checksum(response[3:first], checksum): - print("parse_04_response ERROR: failed to validate received checksum") - return False - - if data_len > 0: - data['bms_voltage_cells_volts'] = dict() - data['bms_voltage_cells_volts']['help'] = "Cell Voltages" - data['bms_voltage_cells_volts']['units'] = "V" - data['bms_voltage_cells_volts']['label'] = "cell" - data['bms_voltage_cells_volts']['raw_values'] = dict() - data['bms_voltage_cells_volts']['values'] = dict() - for cell in range(int(data_len / 2)): - first = (cell * 2) + 4 - second = (cell * 2) + 5 - cellv = bytes_to_digits(response[first], response[second]) * 0.001 - data['bms_voltage_cells_volts']['raw_values'][cell+1] = cellv - data['bms_voltage_cells_volts']['values'][cell+1] = "{:.3f}".format(cellv) - if debug > 1: - print(" Cell {:.0f}: {:.3f}V".format(cell+1, cellv)) - return data - -def collect_data(ser, debug=0): - # Request is 7 bytes: - # \xDD for start - # \xA5 for read, \x5A for write - # \x03 for regular info; \x04 for individual voltages - # \x77 ends - data = dict() - reqmsg = bytearray([ 0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77 ]) - response_03 = requestMessage(ser, reqmsg, debug) - - if len(response_03) == 0: - if debug > 0: - print("collect_data: Error retrieving BMS info. Trying again...") - return False - response_03 = bytearray(response_03) - - reqmsg = bytearray([ 0xDD, 0xA5, 0x04, 0x00, 0xFF, 0xFC, 0x77 ]) - response_04 = requestMessage(ser, reqmsg, debug) - - if len(response_04) == 0: - if debug > 0: - print("collect_data: Error retrieving BMS info. Trying again...") - return False - response_04 = bytearray(response_04) - - parsed_03 = parse_03_response(response_03, debug) - if parsed_03 is not False: - data.update(parsed_03) - else: - return False - - parsed_04 = parse_04_response(response_04, debug) - if parsed_04 is not False: - data.update(parsed_04) - else: - return False - - return data - def read_request(connection, debug=0): # get length of expected json string request = bytes() try: - request = connection.recv(struct.calcsize('!I')) + request = connection.recv(struct.calcsize("!I")) except Exception as e: raise OSError("unable to read request length from socket: {}".format(e)) try: - length = struct.unpack('!I', request)[0] + length = struct.unpack("!I", request)[0] except Exception as e: raise Exception("unable to determine request length: {}".format(e)) if debug > 4: @@ -552,25 +67,31 @@ def read_request(connection, debug=0): except Exception as e: raise Exception("unable to read incoming request: {}".format(e)) if debug > 2: - print('socket: received {!r}'.format(request_data)) + print("socket: received {!r}".format(request_data)) return request_data def send_response(connection, response_data, debug=0): try: - client = response_data['client'] - except: - client = "unknown client" + client = response_data.client + except AttributeError: + client = response_data.get("client", "unknown client") if debug > 2: - print('socket: sending {!r}'.format(response_data)) + print("socket: sending {!r}".format(response_data)) try: response = json.dumps(response_data).encode() + response = json.dumps( + response_data, + default=lambda o: {k: dataclass_asdict(v) for k, v in o.items()} + if hasattr(o, "items") and not isinstance(o, dict) + else str(o), + ).encode() # add length to the start of the json string, so we know how much to read on the other end - length = struct.pack('!I', len(response)) + length = struct.pack("!I", len(response)) if debug > 4: - print('socket: sending {} data of length: {}'.format(client, length)) + print("socket: sending {} data of length: {}".format(client, length)) response = length + response if debug > 3: print("socket: outgoing response: {}".format(response)) @@ -581,38 +102,67 @@ def send_response(connection, response_data, debug=0): def main(): import argparse - import socket, socketserver - import pwd, grp - + import socket + import pwd + import grp + signal.signal(signal.SIGTERM, signalHandler) global current_data timestamp = 0 parser = argparse.ArgumentParser( - description='Query JBD BMS and report status', - add_help=True, + description="Query JBD BMS and report status", + add_help=True, + ) + parser.add_argument( + "--device", + "-d", + dest="device", + action="store", + default="/dev/ttyUSB0", + help="USB device to read", + ) + parser.add_argument( + "--socket", + "-s", + dest="socket", + action="store", + default="/run/bmspy/bms", + help="Socket to communicate with daemon", + ) + parser.add_argument( + "--user", + "-u", + dest="uid_name", + action="store", + default="nobody", + help="Run daemon as user", + ) + parser.add_argument( + "--group", + "-g", + dest="gid_name", + action="store", + default="dialout", + help="Run daemon as group", + ) + parser.add_argument( + "--verbose", + "-v", + action="count", + default=0, + help="Print more verbose information (can be specified multiple times)", ) - parser.add_argument('--device', '-d', dest='device', action='store', - default='/dev/ttyUSB0', help='USB device to read') - parser.add_argument('--socket', '-s', dest='socket', action='store', - default='/run/bmspy/bms', help='Socket to communicate with daemon') - parser.add_argument('--user', '-u', dest='uid_name', action='store', - default='nobody', help='Run daemon as user') - parser.add_argument('--group', '-g', dest='gid_name', action='store', - default='dialout', help='Run daemon as group') - parser.add_argument('--verbose', '-v', action='count', - default=0, help='Print more verbose information (can be specified multiple times)') args = parser.parse_args() - debug=args.verbose + debug = args.verbose if debug > 0: print("Running BMS query daemon on socket {}".format(args.socket)) ser = initialise_serial(args.device) - # Create any necessary directories for the socket socket_dir = os.path.dirname(args.socket) socket_dir_created = False if not os.path.isdir(socket_dir): @@ -625,48 +175,58 @@ def main(): running_uid = pwd.getpwnam(args.uid_name)[2] running_gid = grp.getgrnam(args.gid_name)[2] - # If we've created a new directory for the socket, ensure that - # the highest-level directory has the correct permissions if socket_dir_created: os.chown(socket_dir, running_uid, running_gid) - os.chmod(socket_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) + os.chmod( + socket_dir, + stat.S_IRUSR + | stat.S_IWUSR + | stat.S_IXUSR + | stat.S_IRGRP + | stat.S_IWGRP + | stat.S_IXGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH, + ) new_umask = 0o003 old_umask = os.umask(new_umask) if debug > 1: - print('socket: old umask: %s, new umask: %s' % \ - (oct(old_umask), oct(new_umask))) + print( + "socket: old umask: %s, new umask: %s" + % (oct(old_umask), oct(new_umask)) + ) - # Try setting the new uid/gid try: os.setgid(running_gid) except OSError as e: - print('could not set effective group id: {}'.format(e)) + print("could not set effective group id: {}".format(e)) try: os.setuid(running_uid) except OSError as e: - print('could not set effective user id: {}'.format(e)) + print("could not set effective user id: {}".format(e)) final_uid = os.getuid() final_gid = os.getgid() if debug > 0: - print('socket: running as {}:{}'.format(pwd.getpwuid(final_uid)[0], grp.getgrgid(final_gid)[0])) + print( + "socket: running as {}:{}".format( + pwd.getpwuid(final_uid)[0], grp.getgrgid(final_gid)[0] + ) + ) - # Make sure the socket does not exist if os.path.exists(args.socket): raise OSError("socket {} already exists; exiting...".format(args.socket)) - # Create socket sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - # Bind the socket to the port if debug > 2: - print('starting up on {}'.format(args.socket)) + print("starting up on {}".format(args.socket)) sock.bind(args.socket) atexit.register(socket_cleanup, args.socket, debug) - - # Listen for incoming connections + sock.listen(1) while True: @@ -674,9 +234,8 @@ def main(): client_address = None try: - # Wait for a connection if debug > 2: - print('socket: waiting for a connection') + print("socket: waiting for a connection") connection, client_address = sock.accept() request_data = dict() @@ -686,41 +245,52 @@ def main(): print("socket ERROR: {}".format(e)) continue - client = request_data['client'] or 'unknown' + client = request_data["client"] or "unknown" - match request_data['command']: - case 'REGISTER': + match request_data["command"]: + case "REGISTER": connected_clients.append(client) + send_response( + connection, {"status": "REGISTERED", "client": client}, debug + ) - send_response(connection, {'status': 'REGISTERED', 'client': client}, debug) - - case 'DEREGISTER': + case "DEREGISTER": try: connected_clients.remove(client) - except: + except Exception: pass + send_response( + connection, {"status": "DEREGISTERED", "client": client}, debug + ) - send_response(connection, {'status': 'DEREGISTERED', 'client': client}, debug) + send_response( + connection, {"status": "DEREGISTERED", "client": client}, debug + ) - case 'GET': + case "GET": timestamp = 0 if bool(current_data) is True: - timestamp = current_data.get('timestamp', 0) - print("reading data, current timestamp is {}, time is {}".format(timestamp, time.time())) + timestamp = current_data.get("timestamp", 0) + print( + "reading data, current timestamp is {}, time is {}".format( + timestamp, time.time() + ) + ) # only get new data five seconds after the last read if timestamp <= time.time() - 5: current_data = None - # Retry every second until we get a result while bool(current_data) is False: current_data = collect_data(ser, debug) time.sleep(1) - current_data['timestamp'] = time.time() - current_data['client'] = client - + current_data["timestamp"] = time.time() + current_data["client"] = client + send_response(connection, current_data, debug) case _: - print('socket: invalid request from {}'.format(request_data['client'])) + print( + "socket: invalid request from {}".format(request_data["client"]) + ) break except KeyboardInterrupt: @@ -729,9 +299,9 @@ def main(): sys.exit(1) finally: - # Clean up the connection if connection: connection.close() + if __name__ == "__main__": main()