From ad58fc8bc2c7335dd0b0ce4002fc17f8b4a5e2a3 Mon Sep 17 00:00:00 2001 From: Timothy Allen Date: Sat, 2 May 2026 14:47:53 +0200 Subject: [PATCH] Add debugger --- bmspy/__init__.py | 30 +++++++++++------ bmspy/client.py | 27 ++++++++-------- bmspy/influxdb.py | 17 +++++----- bmspy/jbd_ups.py | 79 +++++++++++++++++++++++---------------------- bmspy/prometheus.py | 8 +++-- bmspy/server.py | 30 ++++++++--------- bmspy/utilities.py | 19 +++++++++++ 7 files changed, 122 insertions(+), 88 deletions(-) create mode 100755 bmspy/utilities.py diff --git a/bmspy/__init__.py b/bmspy/__init__.py index 8f1154d..4f8749f 100644 --- a/bmspy/__init__.py +++ b/bmspy/__init__.py @@ -4,7 +4,8 @@ import atexit import argparse import pprint -import time + +from bmspy.utilities import debugger def parse_args(): @@ -73,28 +74,37 @@ def main(): elif args.report_textfile: from bmspy import prometheus - prometheus.prometheus_export(daemonize=False, filename=args.report_textfile, debug=debug) + prometheus.prometheus_export( + daemonize=False, filename=args.report_textfile, debug=debug + ) - else: + elif args.report_print: from bmspy import client - client.handle_registration(args.socket, 'bmspy', debug) - atexit.register(client.handle_registration, args.socket, 'bmspy', debug) + + pp = pprint.PrettyPrinter(indent=4) + + client.handle_registration(args.socket, "bmspy", debug) + atexit.register(client.handle_registration, args.socket, "bmspy", debug) # {ups_name: JBDUPS} data = client.read_data(args.socket, 'bmspy', ups=args.ups, debug=debug) if args.report_json: import json - print(json.dumps({name: dict(ups.items()) for name, ups in data.items()}, default=str)) - elif args.report_print: - pp = pprint.PrettyPrinter(indent=4) + pp.pprint( + json.dumps( + {name: dict(ups.items()) for name, ups in data.items()}, + default=str, + ) + ) + else: for ups_name, ups_data in data.items(): print("=== {} ===".format(ups_name)) - pp.pprint(ups_data) + pp.pprint(dict(ups_data.items())) except KeyboardInterrupt as e: - print(e) + debugger(e) if __name__ == '__main__': diff --git a/bmspy/client.py b/bmspy/client.py index 0366679..eeebe7d 100644 --- a/bmspy/client.py +++ b/bmspy/client.py @@ -5,6 +5,7 @@ import sys import struct import json import socket +from bmspy.utilities import debugger is_registered = False @@ -33,9 +34,9 @@ def handle_registration(socket_path, client_name, debug=0): except Exception as e: if is_registered: - print("{}: failed to register with daemon: {}".format(client_name, e)) + debugger("{}: failed to register with daemon: {}".format(client_name, e)) else: - print("{}: failed to deregister with daemon: {}".format(client_name, e)) + debugger("{}: failed to deregister with daemon: {}".format(client_name, e)) return data @@ -48,34 +49,34 @@ def socket_comms(socket_path, request_data, debug=0): # Connect the socket to the port where the server is listening if debug > 2: - print("socket client: connecting to {}".format(socket_path)) + debugger("socket client: connecting to {}".format(socket_path)) try: sock.connect(socket_path) except socket.error as msg: if msg.errno == 2: - print("Failed to connect to bmspy daemon") + debugger("Failed to connect to bmspy daemon") else: - print("socket client: {}".format(msg)) + debugger("socket client: {}".format(msg)) # Send request if debug > 2: - print("socket client: sending {!r}".format(request_data)) + debugger("socket client: sending {!r}".format(request_data)) request = bytes() try: request = json.dumps(request_data).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(request)) if debug > 3: - print( + debugger( "socket client: outgoing request length: {}, encoded as {}".format( len(request), length ) ) request = length + request if debug > 4: - print("socket client: outgoing request: {}".format(request)) + debugger("socket client: outgoing request: {}".format(request)) except Exception: - print("socket client ERROR: unable to encode request") + debugger("socket client ERROR: unable to encode request") sys.exit(1) sock.sendall(request) @@ -84,7 +85,7 @@ def socket_comms(socket_path, request_data, debug=0): try: length = struct.unpack("!I", response)[0] if debug > 4: - print( + debugger( "socket client: incoming length: {}, encoded as {}".format( length, response ) @@ -92,13 +93,13 @@ def socket_comms(socket_path, request_data, debug=0): # read length bytes response = sock.recv(length) if debug > 3: - print("socket client: incoming response: {}".format(response)) + debugger("socket client: incoming response: {}".format(response)) response_data = json.loads(response) except Exception: - print("socket client ERROR: unable to decode response") + debugger("socket client ERROR: unable to decode response") sys.exit(1) if debug > 2: - print("socket client: received {!r}".format(response_data)) + debugger("socket client: received {!r}".format(response_data)) sock.close() diff --git a/bmspy/influxdb.py b/bmspy/influxdb.py index 90fbb3b..d93547d 100644 --- a/bmspy/influxdb.py +++ b/bmspy/influxdb.py @@ -1,6 +1,7 @@ import atexit, datetime, os, sys, time from influxdb_client_3 import InfluxDBClient3, Point from bmspy import client +from bmspy.utilities import debugger DAEMON_UPDATE_PERIOD = 30 @@ -34,14 +35,14 @@ def influxdb_export(bucket, url=None, org=None, token=None, socket_path=None, up def influxdb_write_snapshot(influxclient, bucket, ups_data, debug=0): if debug > 1: - print("influxdb: creating snapshot") + debugger("influxdb: creating snapshot") points = influxdb_create_snapshot(ups_data, debug) if debug > 1: - print("influxdb: writing snapshot") + debugger("influxdb: writing snapshot") try: influxclient.write(record=points, database=bucket) except Exception as e: - print(e) + debugger(e) def influxdb_create_snapshot(ups_data, debug=0): @@ -57,7 +58,7 @@ def influxdb_create_snapshot(ups_data, debug=0): if contains.get('raw_value') is not None: value = contains.get('raw_value') if debug > 2: - print("value: {} [{}] : {}".format(kind, ups_name, value)) + debugger("value: {} [{}] : {}".format(kind, ups_name, value)) points.append( Point(kind) .tag("ups", ups_name) @@ -71,7 +72,7 @@ def influxdb_create_snapshot(ups_data, debug=0): label = contains.get('label') for idx, label_value in contains.get('raw_values').items(): if debug > 2: - print("labels: {} [{}][{}] : {}".format(kind, ups_name, idx, label_value)) + debugger("labels: {} [{}][{}] : {}".format(kind, ups_name, idx, label_value)) points.append( Point(kind) .tag("ups", ups_name) @@ -85,7 +86,7 @@ def influxdb_create_snapshot(ups_data, debug=0): if contains.get('info') is not None: value = contains.get('info') if debug > 2: - print("info: {} [{}] : {}".format(kind, ups_name, value)) + debugger("info: {} [{}] : {}".format(kind, ups_name, value)) points.append( Point(kind) .tag("ups", ups_name) @@ -131,10 +132,10 @@ def main(): if not os.getenv('INFLUXDB_V2_TOKEN') and not args.influx_token: raise argparse.ArgumentTypeError('Missing value for --token') except Exception as e: - print("bmspy-influxdb: {}".format(e)) + debugger("bmspy-influxdb: {}".format(e)) sys.exit(1) - print("Running BMS influxdb daemon on socket {}".format(args.socket)) + debugger("Running BMS influxdb daemon on socket {}".format(args.socket)) client.handle_registration(args.socket, 'influxdb', debug) atexit.register(client.handle_registration, args.socket, 'influxdb', debug) diff --git a/bmspy/jbd_ups.py b/bmspy/jbd_ups.py index a6e2d6e..03e02a5 100644 --- a/bmspy/jbd_ups.py +++ b/bmspy/jbd_ups.py @@ -8,6 +8,7 @@ import serial.rs485 import time from dataclasses import dataclass, fields as dataclass_fields +from bmspy.utilities import debugger from bmspy.classes import BMSScalarField, BMSMultiField, BMSInfoField @@ -51,7 +52,7 @@ class JBDUPS: def serial_cleanup(ser, debug=0): if debug > 2: - print("serial: cleaning up...") + debugger("serial: cleaning up...") if ser.is_open: ser.reset_input_buffer() ser.reset_output_buffer() @@ -114,14 +115,14 @@ def bytes_to_date(high, low): def requestMessage(ser, reqmsg, debug=0): if debug > 2: - print("serial: starting up monitor") + debugger("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))) + debugger("serial: error open port: {0}".format(str(e))) return False if ser.is_open: @@ -132,16 +133,16 @@ def requestMessage(ser, reqmsg, debug=0): ser.reset_input_buffer() ser.reset_output_buffer() if debug > 0: - print( + debugger( "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)) + debugger("serial: bytes written: {0}".format(w)) if w != len(reqmsg): - print( + debugger( "serial ERROR: {0} bytes written, {1} expected.".format( w, len(reqmsg) ) @@ -153,22 +154,22 @@ def requestMessage(ser, reqmsg, debug=0): serial_cleanup(ser, debug) return "" if debug > 2: - print("serial: waiting for data...") + debugger("serial: waiting for data...") time.sleep(0.5) wait_time += 1 if debug > 1: - print("serial: waiting reading: {0}".format(ser.in_waiting)) + debugger("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())) + debugger("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))) + debugger("serial: error communicating: {0}".format(str(e))) else: - print("serial: cannot open port") + debugger("serial: cannot open port") def parse_03_response(response, debug=0): @@ -183,11 +184,11 @@ def parse_03_response(response, debug=0): # 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])) + debugger("parse_03_response ERROR: first byte not found: {0}".format(response[0])) return False if bytes([response[2]]) == b"\x80": - print( + debugger( "parse_03_response ERROR: error byte returned from BMS: {0}".format( response[2] ) @@ -196,7 +197,7 @@ def parse_03_response(response, debug=0): data_len = response[3] if debug > 2: - print("parse_03_response: data length (trimming 4 bytes): {0}".format(data_len)) + debugger("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 @@ -204,11 +205,11 @@ def parse_03_response(response, debug=0): first = data_len + 4 second = data_len + 5 if second > len(response): - print("parse_03_response ERROR: primary response checksum not found") + debugger("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") + debugger("parse_03_response ERROR: failed to validate received checksum") return False if data_len == 0: @@ -221,14 +222,14 @@ def parse_03_response(response, debug=0): help="Total Voltage", raw_value=vtot, value="{:.2f}".format(vtot), units="V" ) if debug > 1: - print(" Total voltage: {:.2f}V".format(vtot)) + debugger(" 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)) + debugger(" 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 @@ -245,29 +246,29 @@ def parse_03_response(response, debug=0): units="Ah", ) if debug > 1: - print(" Remaining capacity: {:.2f}Ah".format(res_cap)) - print(" Nominal capacity: {:.2f}Ah".format(nom_cap)) + debugger(" Remaining capacity: {:.2f}Ah".format(res_cap)) + debugger(" 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)) + debugger(" 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)) + debugger(" 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)) + debugger(" 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 @@ -306,7 +307,7 @@ def parse_03_response(response, debug=0): raw_balancing[cell + 1] = balancing str_balancing[cell + 1] = "{0}".format(int(balancing)) if debug > 1: - print(" Balancing cell {0}: {1}".format(cell, balancing)) + debugger(" Balancing cell {0}: {1}".format(cell, balancing)) result.bms_cells_balancing = BMSMultiField( help="Cells balancing", label="cell", @@ -355,7 +356,7 @@ def parse_03_response(response, debug=0): result.bms_protection_slmos_bool = _prot("Software lock MOS", slm) if debug > 2: - print(" Protection state: {0}".format(protection_state)) + debugger(" Protection state: {0}".format(protection_state)) for attr in ( "sop", "sup", @@ -372,14 +373,14 @@ def parse_03_response(response, debug=0): "slm", ): val = locals()[attr] - print(" {}: {}".format(attr, bool(val))) + debugger(" {}: {}".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)) + debugger(" Capacity remaining: {0}%".format(rsoc * 100)) # bit0 = charging; bit1 = discharging; 0 = MOS closing; 1 = MOS opening control_status = response[24] @@ -394,8 +395,8 @@ def parse_03_response(response, debug=0): 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( + debugger(" MOSFET charging: {0}".format("yes" if (control_status & 1) else "no")) + debugger( " MOSFET discharging: {0}".format( "yes" if ((control_status >> 1) & 1) else "no" ) @@ -417,9 +418,9 @@ def parse_03_response(response, debug=0): units="°C", ) if debug > 1: - print(" Number of temperature sensors: {0}".format(ntc_num)) + debugger(" Number of temperature sensors: {0}".format(ntc_num)) for i, temp in enumerate(temperatures): - print(" Temperature sensor {:d}: {:.2f}°C".format(i + 1, temp)) + debugger(" Temperature sensor {:d}: {:.2f}°C".format(i + 1, temp)) return result @@ -436,11 +437,11 @@ def parse_04_response(response, debug=0): # 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])) + debugger("parse_04_response ERROR: first byte not found: {0}".format(response[0])) return False if bytes([response[2]]) == b"\x80": - print( + debugger( "parse_04_response ERROR: error byte returned from BMS: {0}".format( response[2] ) @@ -449,7 +450,7 @@ def parse_04_response(response, debug=0): data_len = response[3] if debug > 2: - print(" Data length (trimming 4 bytes): {0}".format(data_len)) + debugger(" 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 @@ -457,11 +458,11 @@ def parse_04_response(response, debug=0): first = data_len + 4 second = data_len + 5 if second > len(response): - print("parse_04_response ERROR: cell voltage checksum not found") + debugger("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") + debugger("parse_04_response ERROR: failed to validate received checksum") return False if data_len == 0: @@ -476,7 +477,7 @@ def parse_04_response(response, debug=0): raw_values[cell + 1] = cellv str_values[cell + 1] = "{:.3f}".format(cellv) if debug > 1: - print(" Cell {:.0f}: {:.3f}V".format(cell + 1, cellv)) + debugger(" Cell {:.0f}: {:.3f}V".format(cell + 1, cellv)) return BMSMultiField( help="Cell Voltages", @@ -498,7 +499,7 @@ def collect_data(ser, debug=0): if len(response_03) == 0: if debug > 0: - print("collect_data: Error retrieving BMS info. Trying again...") + debugger("collect_data: Error retrieving BMS info. Trying again...") return False response_03 = bytearray(response_03) @@ -507,7 +508,7 @@ def collect_data(ser, debug=0): if len(response_04) == 0: if debug > 0: - print("collect_data: Error retrieving BMS info. Trying again...") + debugger("collect_data: Error retrieving BMS info. Trying again...") return False response_04 = bytearray(response_04) diff --git a/bmspy/prometheus.py b/bmspy/prometheus.py index 598c034..5032a05 100644 --- a/bmspy/prometheus.py +++ b/bmspy/prometheus.py @@ -1,4 +1,6 @@ import prometheus_client +from bmspy.utilities import debugger + def prometheus_export(daemonize=True, filename=None): global debug @@ -32,7 +34,7 @@ def prometheus_export(daemonize=True, filename=None): prometheus_client.generate_latest(registry) else: if filename is None: - print("Invalid filename supplied"); + debugger("Invalid filename supplied"); return False prometheus_client.write_to_textfile(filename, registry=registry) return True @@ -50,7 +52,7 @@ def prometheus_create_metric(registry, data): # Has multiple values, each a different label elif contains.get('values') is not None: if contains.get('label') is None: - print("ERROR: no label for {0} specified".format(name)) + debugger("ERROR: no label for {0} specified".format(name)) label = contains.get('label') metric[name] = prometheus_client.Gauge(name, helpmsg, [label], registry=registry) elif contains.get('info') is not None: @@ -78,7 +80,7 @@ def prometheus_populate_metric(metric, data): # TODO fork bms daemon if need be? def main(): - print("TODO. At present, run from bmspy directly.") + debugger("TODO. At present, run from bmspy directly.") # influxdb_export(bucket=args.influx_bucket, \ # url=args.influx_url, \ diff --git a/bmspy/server.py b/bmspy/server.py index 823f3d8..86a8ee7 100755 --- a/bmspy/server.py +++ b/bmspy/server.py @@ -12,6 +12,7 @@ import json import struct from dataclasses import asdict as dataclass_asdict +from bmspy.utilities import debugger from bmspy.jbd_ups import collect_data, initialise_serial # Expected kernel log output when the USB-serial adapter is plugged in: @@ -52,7 +53,7 @@ def read_request(connection, debug=0): except Exception as e: raise Exception("unable to determine request length: {}".format(e)) if debug > 4: - print("socket: incoming length: {}, encoded as {}".format(length, request)) + debugger("socket: incoming length: {}, encoded as {}".format(length, request)) # read length bytes try: @@ -60,22 +61,21 @@ def read_request(connection, debug=0): except Exception as e: raise OSError("unable to read socket: {}".format(e)) if debug > 3: - print("socket: incoming request: {}".format(request)) + debugger("socket: incoming request: {}".format(request)) try: request_data = json.loads(request) except Exception as e: raise Exception("unable to read incoming request: {}".format(e)) if debug > 2: - print("socket: received {!r}".format(request_data)) + debugger("socket: received {!r}".format(request_data)) return request_data def send_response(connection, response_data, client, debug=0): if debug > 2: - print("socket: sending {!r}".format(response_data)) + debugger("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()} @@ -164,7 +164,7 @@ def main(): for device_str in device_list: name, path = parse_device(device_str) if name in ups_devices: - print("server: duplicate UPS name '{}', skipping {}".format(name, path)) + debugger("server: duplicate UPS name '{}', skipping {}".format(name, path)) continue ups_devices[name] = { "ser": initialise_serial(path, debug), @@ -175,7 +175,7 @@ def main(): print("server: registered UPS '{}' on {}".format(name, path)) if debug > 0: - print("Running BMS query daemon on socket {}".format(args.socket)) + debugger("Running BMS query daemon on socket {}".format(args.socket)) socket_dir = os.path.dirname(args.socket) socket_dir_created = False @@ -207,7 +207,7 @@ def main(): new_umask = 0o003 old_umask = os.umask(new_umask) if debug > 1: - print( + debugger( "socket: old umask: %s, new umask: %s" % (oct(old_umask), oct(new_umask)) ) @@ -215,17 +215,17 @@ def main(): try: os.setgid(running_gid) except OSError as e: - print("could not set effective group id: {}".format(e)) + debugger("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)) + debugger("could not set effective user id: {}".format(e)) final_uid = os.getuid() final_gid = os.getgid() if debug > 0: - print( + debugger( "socket: running as {}:{}".format( pwd.getpwuid(final_uid)[0], grp.getgrgid(final_gid)[0] ) @@ -237,7 +237,7 @@ def main(): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) if debug > 2: - print("starting up on {}".format(args.socket)) + debugger("starting up on {}".format(args.socket)) sock.bind(args.socket) atexit.register(socket_cleanup, args.socket, debug) @@ -249,7 +249,7 @@ def main(): try: if debug > 2: - print("socket: waiting for a connection") + debugger("socket: waiting for a connection") connection, client_address = sock.accept() request_data = dict() @@ -294,7 +294,7 @@ def main(): result = {} for name, device in targets.items(): if debug > 0: - print( + debugger( "reading data for '{}', timestamp={}, time={}".format( name, device["timestamp"], time.time() ) @@ -311,7 +311,7 @@ def main(): send_response(connection, result, client, debug) case _: - print( + debugger( "socket: invalid request from {}".format(request_data["client"]) ) break diff --git a/bmspy/utilities.py b/bmspy/utilities.py new file mode 100755 index 0000000..fde696b --- /dev/null +++ b/bmspy/utilities.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# +# Daemon: listens on a Unix socket and serves JBD BMS data to clients +# +import datetime +import pprint + + +def debugger(data, pretty: bool = False): + if pretty: + pp = pprint.PrettyPrinter(indent=4) + pp.pprint( + { + "time": datetime.datetime.now(), + "data": data, + } + ) + msg = f"{str(datetime.datetime.now())} {data}" + print(msg)