diff --git a/charger-display.py b/charger-display.py index 8cc31c7..7ae802a 100755 --- a/charger-display.py +++ b/charger-display.py @@ -8,6 +8,8 @@ import random import serial import sys +import numpy as np + from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal from PyQt5.QtWidgets import ( QApplication, @@ -25,14 +27,32 @@ from PyQt5.QtWidgets import ( BITRATE = 115200 # baud/s TIMEOUT = 1 # seconds -N_SLAVES = 7 +N_SLAVES = 9 LOG_FRAME_LENGTH = 8 # bytes +PARALLEL_CELLS = 9 CELLS_PER_SLAVE = 10 TEMP_SENSORS_PER_SLAVE = 32 VOLTAGE_CONV = 5.0 / 255 # volts/quantum TEMP_CONV = 0.0625 * 16 # °C/quantum +ERRORCODE_TIMEOUT_SLAVE = 1 +ERRORCODE_SLAVE_PANIC = 2 +ERRORCODE_TIMEOUT_SLAVE_FRAMES = 3 +ERRORCODE_TOO_FEW_TEMPS = 4 +ERRORCODE_TIMEOUT_SHUNT = 6 +ERRORCODE_MASTER_THRESH = 7 +SLAVE_ERROR_UV = 0 +SLAVE_ERROR_OV = 1 +SLAVE_ERROR_UT = 2 +SLAVE_ERROR_OT = 3 +SLAVE_ERROR_BQ = 4 +SLAVE_ERROR_TMP144 = 5 +MASTER_THRESH_UT = 0 +MASTER_THRESH_OT = 1 +MASTER_THRESH_UV = 2 +MASTER_THRESH_OV = 3 + class SlaveData: cell_voltages: list[float] @@ -47,6 +67,8 @@ class AccumulatorData: slaves: list[SlaveData] min_voltage: float max_voltage: float + min_soc: float + max_soc: float min_temp: float max_temp: float last_frame: float @@ -60,6 +82,10 @@ class AccumulatorData: self.slaves = [SlaveData() for _ in range(N_SLAVES)] self.min_voltage = ( self.max_voltage + ) = ( + self.min_soc + ) = ( + self.max_soc ) = self.min_temp = self.max_temp = self.last_frame = self.current = 0 self.panic = False self.panic_errorcode = self.panic_errorarg = 0 @@ -257,25 +283,37 @@ class Window(QWidget): self.l_max_voltage.setNum(data.max_voltage) self.l_max_voltage.setAlignment(Qt.AlignLeft) - self.l3 = QLabel("Min Temperature [°C]") + self.l3 = QLabel("Min SoC [%]") self.l3.setAlignment(Qt.AlignLeft) + self.l_min_soc = QLabel() + self.l_min_soc.setNum(data.min_soc) + self.l_min_soc.setAlignment(Qt.AlignLeft) + + self.l4 = QLabel("Max SoC [%]") + self.l4.setAlignment(Qt.AlignLeft) + self.l_max_soc = QLabel() + self.l_max_soc.setNum(data.max_soc) + self.l_max_soc.setAlignment(Qt.AlignLeft) + + self.l5 = QLabel("Min Temperature [°C]") + self.l5.setAlignment(Qt.AlignLeft) self.l_min_temp = QLabel() self.l_min_temp.setNum(data.min_temp) self.l_min_temp.setAlignment(Qt.AlignLeft) - self.l4 = QLabel("Max Temperature [°C]") - self.l4.setAlignment(Qt.AlignLeft) + self.l6 = QLabel("Max Temperature [°C]") + self.l6.setAlignment(Qt.AlignLeft) self.l_max_temp = QLabel() self.l_max_temp.setNum(data.max_temp) self.l_max_temp.setAlignment(Qt.AlignLeft) - self.l5 = QLabel("Current [A]") - self.l5.setAlignment(Qt.AlignLeft) + self.l7 = QLabel("Current [A]") + self.l7.setAlignment(Qt.AlignLeft) self.l_current = QLabel() self.l_current.setNum(data.current) self.l_current.setAlignment(Qt.AlignLeft) - self.l6 = QLabel("Error") + self.l8 = QLabel("Error") self.l_error = QLabel() self.l_error.setText(str(data.panic)) self.l_error.setAlignment(Qt.AlignLeft) @@ -286,7 +324,7 @@ class Window(QWidget): self.l_errorarg.setText(str(data.panic_errorarg)) self.l_errorcode.setAlignment(Qt.AlignLeft) - self.l7 = QLabel("Time Since Last Dataframe") + self.l9 = QLabel("Time Since Last Dataframe") self.l_time_since_last_frame = QLabel() self.l_time_since_last_frame.setText(str(data.time_since_last_frame)) self.l_time_since_last_frame.setAlignment(Qt.AlignLeft) @@ -295,21 +333,25 @@ class Window(QWidget): grid_accumulator = QGridLayout() grid_accumulator.addWidget(self.l1, 0, 0) grid_accumulator.addWidget(self.l2, 1, 0) - grid_accumulator.addWidget(self.l3, 0, 2) - grid_accumulator.addWidget(self.l4, 1, 2) - grid_accumulator.addWidget(self.l5, 2, 0) - grid_accumulator.addWidget(self.l6, 3, 0) - grid_accumulator.addWidget(self.l7, 4, 0) + grid_accumulator.addWidget(self.l3, 2, 0) + grid_accumulator.addWidget(self.l4, 3, 0) + grid_accumulator.addWidget(self.l5, 0, 2) + grid_accumulator.addWidget(self.l6, 1, 2) + grid_accumulator.addWidget(self.l7, 2, 2) + grid_accumulator.addWidget(self.l8, 5, 0) + grid_accumulator.addWidget(self.l9, 3, 2) grid_accumulator.addWidget(self.l_min_voltage, 0, 1) grid_accumulator.addWidget(self.l_max_voltage, 1, 1) + grid_accumulator.addWidget(self.l_min_soc, 2, 1) + grid_accumulator.addWidget(self.l_max_soc, 3, 1) grid_accumulator.addWidget(self.l_min_temp, 0, 3) grid_accumulator.addWidget(self.l_max_temp, 1, 3) - grid_accumulator.addWidget(self.l_current, 2, 1) - grid_accumulator.addWidget(self.l_error, 3, 1) - grid_accumulator.addWidget(self.l_errorcode, 3, 2) - grid_accumulator.addWidget(self.l_errorarg, 3, 3) - grid_accumulator.addWidget(self.l_time_since_last_frame, 4, 1) + grid_accumulator.addWidget(self.l_current, 2, 3) + grid_accumulator.addWidget(self.l_error, 5, 1) + grid_accumulator.addWidget(self.l_errorcode, 5, 2) + grid_accumulator.addWidget(self.l_errorarg, 5, 3) + grid_accumulator.addWidget(self.l_time_since_last_frame, 3, 3) groupBox_accumulator = QGroupBox("Accumulator General") groupBox_accumulator.setLayout(grid_accumulator) @@ -400,15 +442,17 @@ class Window(QWidget): def update(self): # Accumulator - self.l_min_voltage.setNum(data.min_voltage) - self.l_max_voltage.setNum(data.max_voltage) - self.l_min_temp.setNum(data.min_temp) - self.l_max_temp.setNum(data.max_temp) - self.l_current.setNum(data.current) + self.l_min_voltage.setText(f"{data.min_voltage:.02f}") + self.l_max_voltage.setText(f"{data.max_voltage:.02f}") + self.l_min_soc.setText(f"{data.min_soc:.01f}") + self.l_max_soc.setText(f"{data.max_soc:.01f}") + self.l_min_temp.setText(f"{data.min_temp:.00f}") + self.l_max_temp.setText(f"{data.max_temp:.00f}") + self.l_current.setText(f"{data.current:.03f}") self.l_error.setText(str(data.panic)) self.l_errorcode.setText(str(data.panic_errorcode)) self.l_errorarg.setText(str(data.panic_errorarg)) - self.l_time_since_last_frame.setText(str(data.time_since_last_frame)) + self.l_time_since_last_frame.setText(str(time.time() - data.last_frame)) # Stacks for i, sge in enumerate(self.stack_gui_elements): @@ -434,6 +478,7 @@ class Worker(QObject): def charger_communication(self): rx_data = ser.read(256) + # print(len(rx_data), rx_data) while len(rx_data) > 0: if (frame_start := self.check_log_start(rx_data)) != -1: self.decode_log_frame(rx_data[frame_start + 3 : frame_start + 11]) @@ -503,6 +548,8 @@ class Worker(QObject): ] data.min_voltage = min(voltages) data.max_voltage = max(voltages) + data.min_soc = self.calculate_soc(data.min_voltage) + data.max_soc = self.calculate_soc(data.max_voltage) data.min_temp = min(temps) data.max_temp = max(temps) data.time_since_last_frame = time.time() - data.last_frame @@ -516,11 +563,72 @@ class Worker(QObject): def decode_panic_frame(self, buf: bytes): data.panic = True - data.panic_errorcode = buf[0] - data.panic_errorarg = buf[1] + errorcode = int(buf[0]) + errorargs = [int(buf[1]), int(buf[2])] + if errorcode == ERRORCODE_TIMEOUT_SLAVE: + data.panic_errorcode = "Slave timeout" + data.panic_errorarg = f"Slave ID {errorargs[0]}" + elif errorcode == ERRORCODE_SLAVE_PANIC: + data.panic_errorcode = "Slave panic" + if errorargs[1] == SLAVE_ERROR_UV: + slave_error = "Undervoltage" + elif errorargs[1] == SLAVE_ERROR_OV: + slave_error = "Overvoltage" + elif errorargs[1] == SLAVE_ERROR_UT: + slave_error = "Undertemperature" + elif errorargs[1] == SLAVE_ERROR_OT: + slave_error = "Overtemperature" + elif errorargs[1] == SLAVE_ERROR_BQ: + slave_error = "BQ Communication error" + elif errorargs[1] == SLAVE_ERROR_TMP144: + slave_error = "TMP144 communication error" + else: + slave_error = f"Unknown error ({errorargs[1]})" + data.panic_errorarg = f"Slave ID {errorargs[0]}: {slave_error}" + elif errorcode == ERRORCODE_TIMEOUT_SLAVE_FRAMES: + data.panic_errorcode = "Slave frame timeout" + data.panic_errorarg = f"Slave ID {errorargs[0]}, frame {errorargs[1]}" + elif errorcode == ERRORCODE_TOO_FEW_TEMPS: + data.panic_errorcode = "Too few temperature sensors" + data.panic_errorarg = f"Slave ID {errorargs[0]}" + elif errorcode == ERRORCODE_TIMEOUT_SHUNT: + data.panic_errorcode = "Shunt timeout" + elif errorcode == ERRORCODE_MASTER_THRESH: + data.panic_errorcode = "Master detected threshold" + if errorargs[0] == MASTER_THRESH_UT: + data.panic_errorarg = "Undertemperature" + elif errorargs[0] == MASTER_THRESH_OT: + data.panic_errorarg = "Overtemperature" + elif errorargs[0] == MASTER_THRESH_UV: + data.panic_errorarg = "Undervoltage" + elif errorargs[0] == MASTER_THRESH_OV: + data.panic_errorarg = "Overvoltage" + else: + data.panic_errorarg = f"Unknown threshold ({errorargs[0]})" + else: + data.panic_errorcode = buf[0] + data.panic_errorarg = buf[1] + + INTERNAL_RESISTANCE_CURVE_X = [2.0, 4.12] + INTERNAL_RESISTANCE_CURVE_Y = [0.0528, 0.0294] + SOC_OCV_X = [2.1, 2.9, 3.2, 3.3, 3.4, 3.5, 3.68, 4.0, 4.15, 4.2] + SOC_OCV_Y = [0, 0.023, 0.06, 0.08, 0.119, 0.227, 0.541, 0.856, 0.985, 1.0] + + def calculate_soc(self, voltage: float): + r_i = np.interp( + [voltage], + self.INTERNAL_RESISTANCE_CURVE_X, + self.INTERNAL_RESISTANCE_CURVE_Y, + )[0] + # i = data.current / PARALLEL_CELLS + i = 3 / PARALLEL_CELLS + ocv = voltage - i * r_i + return np.interp([ocv], self.SOC_OCV_X, self.SOC_OCV_Y)[0] * 100 def parse_cell_temps(self, slave: int): - temps = list(filter(lambda t: t > 0, data.slaves[slave].cell_temps[:16])) + temps = list( + filter(lambda t: t > 0 and t < 60, data.slaves[slave].cell_temps[:16]) + ) if len(temps) == 0: temps = [-1] min_t = min(temps) @@ -542,10 +650,7 @@ if __name__ == "__main__": SERIAL_PORT = sys.argv[1] print(SERIAL_PORT) - try: - ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) - except serial.serialutil.SerialException: - pass + ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) app = QApplication(sys.argv) gui = Window()