From cfbd522e88eb3ee081e85fd35659d152f8b6728b Mon Sep 17 00:00:00 2001 From: "f.geissler" Date: Mon, 18 Jul 2022 16:06:19 +0200 Subject: [PATCH 01/15] gui tests --- test.py | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 test.py diff --git a/test.py b/test.py new file mode 100644 index 0000000..09f4a53 --- /dev/null +++ b/test.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 + +import os +import struct +import time + +# import serial +import sys + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QPushButton, + QVBoxLayout, + QHBoxLayout, + QGridLayout, + QSlider, + QLabel, + QRadioButton, + QGroupBox, +) + + +BITRATE = 115200 # baud/s +TIMEOUT = 1 # seconds +N_SLAVES = 6 +LOG_FRAME_LENGTH = 8 # bytes + +CELLS_PER_SLAVE = 10 +TEMP_SENSORS_PER_SLAVE = 32 +VOLTAGE_CONV = 5.0 / 255 # volts/quantum +TEMP_CONV = 0.0625 * 16 # °C/quantum + + +class SlaveData: + cell_voltages: list[float] + cell_temps: list[float] + + def __init__(self) -> None: + self.cell_voltages = [-1] * CELLS_PER_SLAVE + self.cell_temps = [-1] * TEMP_SENSORS_PER_SLAVE + + +class AccumulatorData: + slaves: list[SlaveData] + min_voltage: float + max_voltage: float + min_temp: float + max_temp: float + last_frame: float + current: float + panic: bool + panic_errorcode: int + panic_errorarg: int + + def __init__(self) -> None: + self.slaves = [SlaveData() for _ in range(N_SLAVES)] + self.min_voltage = ( + self.max_voltage + ) = self.min_temp = self.max_temp = self.last_frame = self.current = 0 + self.panic = False + self.panic_errorcode = self.panic_errorarg = 0 + + +def update_display(): + voltages = [ + slave.cell_voltages[i] for i in range(CELLS_PER_SLAVE) for slave in data.slaves + ] + temps = [ + slave.cell_temps[i] + for i in range(TEMP_SENSORS_PER_SLAVE) + for slave in data.slaves + ] + data.min_voltage = min(voltages) + data.max_voltage = max(voltages) + data.min_temp = min(temps) + data.max_temp = max(temps) + time_since_last_frame = time.time() - data.last_frame + + print("\033[2J\033[H", end="") + print("-" * 20) + if data.panic: + print("!!!!! PANIC !!!!!") + print(f"Error code: {data.panic_errorcode}") + print(f"Error arg: {data.panic_errorarg}") + time.sleep(0.5) + return + + print(f"Time since last frame: {time_since_last_frame} s") + print(f"Current: {data.current:.2f} A") + print(f"Min voltage: {data.min_voltage:.2f} V") + print(f"Max voltage: {data.max_voltage:.2f} V") + print(f"Min temp: {data.min_temp:.1f} °C") + print(f"Max temp: {data.max_temp:.1f} °C") + for i in range(N_SLAVES): + min_v = min(data.slaves[i].cell_voltages) + max_v = max(data.slaves[i].cell_voltages) + min_t = min(data.slaves[i].cell_temps) + max_t = max(data.slaves[i].cell_temps) + print( + f"Stack {i}: V ∈ [{min_v:.2f}, {max_v:.2f}]\tT ∈ [{min_t:.1f}, {max_t:.1f}]" + ) + + +def gui(): + pass + + +class Window(QWidget): + def __init__(self, parent=None): + super(Window, self).__init__(parent) + + win = QVBoxLayout() + + ### ACCUMULATOR GENERAL ### + self.l1 = QLabel("Min Voltage [V]") + self.l1.setAlignment(Qt.AlignLeft) + self.l_min_voltage = QLabel() + self.l_min_voltage.setNum(data.min_voltage) + self.l_min_voltage.setAlignment(Qt.AlignLeft) + + self.l2 = QLabel("Max Voltage [V]") + self.l2.setAlignment(Qt.AlignLeft) + self.l_max_voltage = QLabel() + self.l_max_voltage.setNum(data.max_voltage) + self.l_max_voltage.setAlignment(Qt.AlignLeft) + + self.l3 = QLabel("Min Temperature [°C]") + self.l3.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.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.l_current = QLabel() + self.l_current.setNum(data.current) + self.l_current.setAlignment(Qt.AlignLeft) + + 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.l_min_voltage, 0, 1) + grid_accumulator.addWidget(self.l_max_voltage, 1, 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) + + groupBox_accumulator = QGroupBox("Accumulator General") + groupBox_accumulator.setLayout(grid_accumulator) + + win.addWidget(groupBox_accumulator) + + ### STACKS ### + + ### STACK 0 ### + + self.l1_s0 = QLabel("Min Voltage [V]") + self.l1_s0.setAlignment(Qt.AlignLeft) + self.l_min_voltage_s0 = QLabel() + self.l_min_voltage_s0.setNum(min(data.slaves[0].cell_voltages)) + self.l_min_voltage_s0.setAlignment(Qt.AlignLeft) + + self.l2_s0 = QLabel("Max Voltage [V]") + self.l2_s0.setAlignment(Qt.AlignLeft) + self.l_max_voltage_s0 = QLabel() + self.l_max_voltage_s0.setNum(max(data.slaves[0].cell_voltages)) + self.l_max_voltage_s0.setAlignment(Qt.AlignLeft) + + self.l3_s0 = QLabel("Min Temperature [°C]") + self.l3_s0.setAlignment(Qt.AlignLeft) + self.l_min_temp_s0 = QLabel() + self.l_min_temp_s0.setNum(min(data.slaves[0].cell_temps)) + self.l_min_temp_s0.setAlignment(Qt.AlignLeft) + + self.l4_s0 = QLabel("Max Temperature [°C]") + self.l4_s0.setAlignment(Qt.AlignLeft) + self.l_max_temp_s0 = QLabel() + self.l_max_temp_s0.setNum(max(data.slaves[0].cell_temps)) + self.l_max_temp_s0.setAlignment(Qt.AlignLeft) + + grid_stack_s0 = QGridLayout() + grid_stack_s0.addWidget(self.l1_s0, 0, 0) + grid_stack_s0.addWidget(self.l2_s0, 1, 0) + grid_stack_s0.addWidget(self.l3_s0, 0, 2) + grid_stack_s0.addWidget(self.l4_s0, 1, 2) + + grid_stack_s0.addWidget(self.l_min_voltage_s0, 0, 1) + grid_stack_s0.addWidget(self.l_max_voltage_s0, 1, 1) + grid_stack_s0.addWidget(self.l_min_temp_s0, 0, 3) + grid_stack_s0.addWidget(self.l_max_temp_s0, 1, 3) + + groupBox_stack_s0 = QGroupBox("Stack 0") + groupBox_stack_s0.setLayout(grid_stack_s0) + + grid_stacks = QGridLayout() + grid_stacks.addWidget(groupBox_stack_s0, 0, 0) + + groupBox_stacks = QGroupBox("Individual Stacks") + groupBox_stacks.setLayout(grid_stacks) + + win.addWidget(groupBox_stacks) + + self.setLayout(win) + self.setWindowTitle("FT22 Charger Display") + + +data = AccumulatorData() +update_display() +app = QApplication(sys.argv) +clock = Window() +clock.show() +sys.exit(app.exec_()) From 39a7669fa76f575f92230ab48a38f4607fe00aec Mon Sep 17 00:00:00 2001 From: "f.geissler" Date: Mon, 18 Jul 2022 17:22:43 +0200 Subject: [PATCH 02/15] gui not working yet --- charger-display copy.py | 179 ++++++++++++++++++++++++++++++++++++++++ test.py | 113 +++++++++++++++++++++++-- 2 files changed, 284 insertions(+), 8 deletions(-) create mode 100755 charger-display copy.py diff --git a/charger-display copy.py b/charger-display copy.py new file mode 100755 index 0000000..4b73017 --- /dev/null +++ b/charger-display copy.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 + +import os +import struct +import time +import serial +import sys + +BITRATE = 115200 # baud/s +TIMEOUT = 1 # seconds +N_SLAVES = 6 +LOG_FRAME_LENGTH = 8 # bytes + +CELLS_PER_SLAVE = 10 +TEMP_SENSORS_PER_SLAVE = 32 +VOLTAGE_CONV = 5.0 / 255 # volts/quantum +TEMP_CONV = 0.0625 * 16 # °C/quantum + + +class SlaveData: + cell_voltages: list[float] + cell_temps: list[float] + + def __init__(self) -> None: + self.cell_voltages = [-1] * CELLS_PER_SLAVE + self.cell_temps = [-1] * TEMP_SENSORS_PER_SLAVE + + +class AccumulatorData: + slaves: list[SlaveData] + min_voltage: float + max_voltage: float + min_temp: float + max_temp: float + last_frame: float + current: float + panic: bool + panic_errorcode: int + panic_errorarg: int + + def __init__(self) -> None: + self.slaves = [SlaveData() for _ in range(N_SLAVES)] + self.min_voltage = ( + self.max_voltage + ) = self.min_temp = self.max_temp = self.last_frame = self.current = 0 + self.panic = False + self.panic_errorcode = self.panic_errorarg = 0 + + +def check_log_start(buf: bytes): + return buf[:-12].find(b"LOG") + + +def check_current_start(buf: bytes): + return buf[:-12].find(b"CUR") + + +def check_panic_start(buf: bytes): + return buf[:-12].find(b"PAN") + + +def decode_log_frame(buf: bytes): + msg_id = buf[0] + slave = msg_id >> 4 + frame_id = msg_id & 0x0F + if slave >= N_SLAVES: + return + + if frame_id == 0: + for i in range(7): + data.slaves[slave].cell_voltages[i] = buf[i + 1] * VOLTAGE_CONV + elif frame_id == 1: + for i in range(3): + data.slaves[slave].cell_voltages[i + 7] = buf[i + 1] * VOLTAGE_CONV + for i in range(4): + data.slaves[slave].cell_temps[i] = buf[i + 4] * TEMP_CONV + elif frame_id == 2: + for i in range(7): + data.slaves[slave].cell_temps[i + 4] = buf[i + 1] * TEMP_CONV + elif frame_id == 3: + for i in range(7): + data.slaves[slave].cell_temps[i + 11] = buf[i + 1] * TEMP_CONV + elif frame_id == 4: + for i in range(7): + data.slaves[slave].cell_temps[i + 18] = buf[i + 1] * TEMP_CONV + elif frame_id == 5: + for i in range(7): + data.slaves[slave].cell_temps[i + 25] = buf[i + 1] * TEMP_CONV + else: + # print(f"Unknown frame ID: {frame_id} (buf: {repr(buf)})", file=sys.stderr) + # time.sleep(1) + return + + data.last_frame = time.time() + + +def decode_current_frame(buf: bytes): + # current = (buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | (buf[5]) + current = struct.unpack(">i", buf[2:6])[0] + data.current = current / 1000.0 + + +def decode_panic_frame(buf: bytes): + data.panic = True + data.panic_errorcode = buf[0] + data.panic_errorarg = buf[1] + + +def update_display(): + voltages = [ + slave.cell_voltages[i] for i in range(CELLS_PER_SLAVE) for slave in data.slaves + ] + temps = [ + slave.cell_temps[i] + for i in range(TEMP_SENSORS_PER_SLAVE) + for slave in data.slaves + ] + data.min_voltage = min(voltages) + data.max_voltage = max(voltages) + data.min_temp = min(temps) + data.max_temp = max(temps) + time_since_last_frame = time.time() - data.last_frame + + print("\033[2J\033[H", end="") + print("-" * 20) + if data.panic: + print("!!!!! PANIC !!!!!") + print(f"Error code: {data.panic_errorcode}") + print(f"Error arg: {data.panic_errorarg}") + time.sleep(0.5) + return + + print(f"Time since last frame: {time_since_last_frame} s") + print(f"Current: {data.current:.2f} A") + print(f"Min voltage: {data.min_voltage:.2f} V") + print(f"Max voltage: {data.max_voltage:.2f} V") + print(f"Min temp: {data.min_temp:.1f} °C") + print(f"Max temp: {data.max_temp:.1f} °C") + for i in range(N_SLAVES): + min_v = min(data.slaves[i].cell_voltages) + max_v = max(data.slaves[i].cell_voltages) + min_t = min(data.slaves[i].cell_temps) + max_t = max(data.slaves[i].cell_temps) + print( + f"Stack {i}: V ∈ [{min_v:.2f}, {max_v:.2f}]\tT ∈ [{min_t:.1f}, {max_t:.1f}]" + ) + + +if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} SERIAL-PORT", file=sys.stderr) + sys.exit(os.EX_USAGE) + +SERIAL_PORT = sys.argv[1] +ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) + +rx_buf = bytes() +data = AccumulatorData() + +while True: + rx_data = ser.read(32) + if len(rx_data) > 0: + rx_buf = rx_data + if (start := check_log_start(rx_buf)) != -1: + decode_log_frame(rx_buf[start + 3 : start + 11]) + rx_buf = b"" + elif (start := check_current_start(rx_buf)) != -1: + decode_current_frame(rx_buf[start + 3 : start + 11]) + rx_buf = b"" + elif (start := check_panic_start(rx_buf)) != -1: + decode_panic_frame(rx_buf[start + 3 : start + 11]) + rx_buf = b"" + + """ + if Button Charge is Pressed + print(f"KBHIT: {c}", file=sys.stderr) + print(ser.write(b"C"), file=sys.stderr) + """ + + update_display() diff --git a/test.py b/test.py index 09f4a53..4e0470b 100644 --- a/test.py +++ b/test.py @@ -4,10 +4,12 @@ import os import struct import time +import random + # import serial import sys -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QTimer from PyQt5.QtWidgets import ( QApplication, QWidget, @@ -63,6 +65,22 @@ class AccumulatorData: self.panic_errorcode = self.panic_errorarg = 0 +def fill_dummy_data(): + data.min_voltage = random.uniform(1, 3) + data.max_voltage = random.uniform(3, 5) + data.min_temp = random.uniform(0, 25) + data.max_temp = random.uniform(25, 60) + data.current = random.uniform(25, 60) + data.last_frame = time.time() - random.random() + data.panic = random.choice([True, False]) + if data.panic: + data.panic_errorcode = random.randint(1, 10000) + data.panic_errorarg = "ABCDERFG" + else: + data.panic_errorcode = 0 + data.panic_errorarg = "" + + def update_display(): voltages = [ slave.cell_voltages[i] for i in range(CELLS_PER_SLAVE) for slave in data.slaves @@ -103,10 +121,6 @@ def update_display(): ) -def gui(): - pass - - class Window(QWidget): def __init__(self, parent=None): super(Window, self).__init__(parent) @@ -144,18 +158,33 @@ class Window(QWidget): self.l_current.setNum(data.current) self.l_current.setAlignment(Qt.AlignLeft) + self.l6 = QLabel("Error") + self.l_error = QLabel() + self.l_error.setText(str(data.panic)) + self.l_error.setAlignment(Qt.AlignLeft) + self.l_errorcode = QLabel() + self.l_errorcode.setText(str(data.panic_errorcode)) + self.l_errorcode.setAlignment(Qt.AlignLeft) + self.l_errorarg = QLabel() + self.l_errorarg.setText(str(data.panic_errorarg)) + self.l_errorcode.setAlignment(Qt.AlignLeft) + 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.l_min_voltage, 0, 1) grid_accumulator.addWidget(self.l_max_voltage, 1, 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) groupBox_accumulator = QGroupBox("Accumulator General") groupBox_accumulator.setLayout(grid_accumulator) @@ -204,8 +233,50 @@ class Window(QWidget): groupBox_stack_s0 = QGroupBox("Stack 0") groupBox_stack_s0.setLayout(grid_stack_s0) + ### STACK 1 ### + + self.l1_s1 = QLabel("Min Voltage [V]") + self.l1_s1.setAlignment(Qt.AlignLeft) + self.l_min_voltage_s1 = QLabel() + self.l_min_voltage_s1.setNum(min(data.slaves[1].cell_voltages)) + self.l_min_voltage_s1.setAlignment(Qt.AlignLeft) + + self.l2_s1 = QLabel("Max Voltage [V]") + self.l2_s1.setAlignment(Qt.AlignLeft) + self.l_max_voltage_s1 = QLabel() + self.l_max_voltage_s1.setNum(max(data.slaves[1].cell_voltages)) + self.l_max_voltage_s1.setAlignment(Qt.AlignLeft) + + self.l3_s1 = QLabel("Min Temperature [°C]") + self.l3_s1.setAlignment(Qt.AlignLeft) + self.l_min_temp_s1 = QLabel() + self.l_min_temp_s1.setNum(min(data.slaves[1].cell_temps)) + self.l_min_temp_s1.setAlignment(Qt.AlignLeft) + + self.l4_s1 = QLabel("Max Temperature [°C]") + self.l4_s1.setAlignment(Qt.AlignLeft) + self.l_max_temp_s1 = QLabel() + self.l_max_temp_s1.setNum(max(data.slaves[1].cell_temps)) + self.l_max_temp_s1.setAlignment(Qt.AlignLeft) + + grid_stack_s1 = QGridLayout() + grid_stack_s1.addWidget(self.l1_s1, 0, 0) + grid_stack_s1.addWidget(self.l2_s1, 1, 0) + grid_stack_s1.addWidget(self.l3_s1, 0, 2) + grid_stack_s1.addWidget(self.l4_s1, 1, 2) + + grid_stack_s1.addWidget(self.l_min_voltage_s1, 0, 1) + grid_stack_s1.addWidget(self.l_max_voltage_s1, 1, 1) + grid_stack_s1.addWidget(self.l_min_temp_s1, 0, 3) + grid_stack_s1.addWidget(self.l_max_temp_s1, 1, 3) + + groupBox_stack_s1 = QGroupBox("Stack 1") + groupBox_stack_s1.setLayout(grid_stack_s1) + + ### Layout Stacks ### grid_stacks = QGridLayout() grid_stacks.addWidget(groupBox_stack_s0, 0, 0) + grid_stacks.addWidget(groupBox_stack_s1, 0, 1) groupBox_stacks = QGroupBox("Individual Stacks") groupBox_stacks.setLayout(grid_stacks) @@ -216,9 +287,35 @@ class Window(QWidget): self.setWindowTitle("FT22 Charger Display") +def update_gui(): + # Accumulator + gui.l_min_voltage.setNum(data.min_voltage) + gui.l_max_voltage.setNum(data.max_voltage) + gui.l_min_temp.setNum(data.min_temp) + gui.l_max_temp.setNum(data.max_temp) + gui.l_current.setNum(data.current) + gui.l_error.setText(str(data.panic)) + gui.l_errorcode.setText(str(data.panic_errorcode)) + gui.l_errorarg.setText(str(data.panic_errorarg)) + + # Cells + gui.l_min_voltage_s0.setNum(min(data.slaves[0].cell_voltages)) + gui.l_max_voltage_s0.setNum(max(data.slaves[0].cell_voltages)) + gui.l_min_temp_s0.setNum(min(data.slaves[0].cell_temps)) + gui.l_max_temp_s0.setNum(max(data.slaves[0].cell_temps)) + + data = AccumulatorData() -update_display() app = QApplication(sys.argv) -clock = Window() -clock.show() +gui = Window() +gui.show() + + +timer = QTimer() +timer.timeout.connect(update_gui) +timer.timeout.connect(fill_dummy_data) +timer.start(1000) # every 1,000 milliseconds + +# update_display() + sys.exit(app.exec_()) From a4733d3578e86c1b9546b78863fd75cfd3cb2b5d Mon Sep 17 00:00:00 2001 From: Tobias Petrich Date: Tue, 19 Jul 2022 03:18:17 +0200 Subject: [PATCH 03/15] implement class for stack elements and add poetry dependency management --- poetry.lock | 66 +++++++++++++++++ pyproject.toml | 15 ++++ test.py | 196 ++++++++++++++++++++++--------------------------- 3 files changed, 169 insertions(+), 108 deletions(-) create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..5c77a36 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,66 @@ +[[package]] +name = "pyqt5" +version = "5.15.7" +description = "Python bindings for the Qt cross platform application toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +PyQt5-Qt5 = ">=5.15.0" +PyQt5-sip = ">=12.11,<13" + +[[package]] +name = "pyqt5-qt5" +version = "5.15.2" +description = "The subset of a Qt installation needed by PyQt5." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyqt5-sip" +version = "12.11.0" +description = "The sip module support for PyQt5" +category = "main" +optional = false +python-versions = ">=3.7" + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "ce4feb2a6a42610a01d5e986a40980aaf9f73c3fdc2368f55ec75e70f39c70cf" + +[metadata.files] +pyqt5 = [ + {file = "PyQt5-5.15.7-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:1a793748c60d5aff3850b7abf84d47c1d41edb11231b7d7c16bef602c36be643"}, + {file = "PyQt5-5.15.7-cp37-abi3-manylinux1_x86_64.whl", hash = "sha256:e319c9d8639e0729235c1b09c99afdadad96fa3dbd8392ab561b5ab5946ee6ef"}, + {file = "PyQt5-5.15.7-cp37-abi3-win32.whl", hash = "sha256:08694f0a4c7d4f3d36b2311b1920e6283240ad3b7c09b515e08262e195dcdf37"}, + {file = "PyQt5-5.15.7-cp37-abi3-win_amd64.whl", hash = "sha256:232fe5b135a095cbd024cf341d928fc672c963f88e6a52b0c605be8177c2fdb5"}, + {file = "PyQt5-5.15.7.tar.gz", hash = "sha256:755121a52b3a08cb07275c10ebb96576d36e320e572591db16cfdbc558101594"}, +] +pyqt5-qt5 = [ + {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"}, + {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"}, + {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"}, + {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"}, +] +pyqt5-sip = [ + {file = "PyQt5_sip-12.11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f9e312ff8284d6dfebc5366f6f7d103f84eec23a4da0be0482403933e68660"}, + {file = "PyQt5_sip-12.11.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:4031547dfb679be309094bfa79254f5badc5ddbe66b9ad38e319d84a7d612443"}, + {file = "PyQt5_sip-12.11.0-cp310-cp310-win32.whl", hash = "sha256:ad21ca0ee8cae2a41b61fc04949dccfab6fe008749627d94e8c7078cb7a73af1"}, + {file = "PyQt5_sip-12.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3126c84568ab341c12e46ded2230f62a9a78752a70fdab13713f89a71cd44f73"}, + {file = "PyQt5_sip-12.11.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0f77655c62ec91d47c2c99143f248624d44dd2d8a12d016e7c020508ad418aca"}, + {file = "PyQt5_sip-12.11.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ec5e9ef78852e1f96f86d7e15c9215878422b83dde36d44f1539a3062942f19c"}, + {file = "PyQt5_sip-12.11.0-cp37-cp37m-win32.whl", hash = "sha256:d12b81c3a08abf7657a2ebc7d3649852a1f327eb2146ebadf45930486d32e920"}, + {file = "PyQt5_sip-12.11.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b69a1911f768b489846335e31e49eb34795c6b5a038ca24d894d751e3b0b44da"}, + {file = "PyQt5_sip-12.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:51e377789d59196213eddf458e6927f33ba9d217b614d17d20df16c9a8b2c41c"}, + {file = "PyQt5_sip-12.11.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4e5c1559311515291ea0ab0635529f14536954e3b973a7c7890ab7e4de1c2c23"}, + {file = "PyQt5_sip-12.11.0-cp38-cp38-win32.whl", hash = "sha256:9bca450c5306890cb002fe36bbca18f979dd9e5b810b766dce8e3ce5e66ba795"}, + {file = "PyQt5_sip-12.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:f6b72035da4e8fecbb0bc4a972e30a5674a9ad5608dbddaa517e983782dbf3bf"}, + {file = "PyQt5_sip-12.11.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9356260d4feb60dbac0ab66f8a791a0d2cda1bf98c9dec8e575904a045fbf7c5"}, + {file = "PyQt5_sip-12.11.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:205f3e1b3eea3597d8e878936c1a06e04bd23a59e8b179ee806465d72eea3071"}, + {file = "PyQt5_sip-12.11.0-cp39-cp39-win32.whl", hash = "sha256:686071be054e5be6ca5aaaef7960931d4ba917277e839e2e978c7cbe3f43bb6e"}, + {file = "PyQt5_sip-12.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:42320e7a94b1085ed85d49794ed4ccfe86f1cae80b44a894db908a8aba2bc60e"}, + {file = "PyQt5_sip-12.11.0.tar.gz", hash = "sha256:b4710fd85b57edef716cc55fae45bfd5bfac6fc7ba91036f1dcc3f331ca0eb39"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9f88869 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "charger-display" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.10" +PyQt5 = "^5.15.7" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/test.py b/test.py index 4e0470b..75c4b76 100644 --- a/test.py +++ b/test.py @@ -65,6 +65,60 @@ class AccumulatorData: self.panic_errorcode = self.panic_errorarg = 0 +class StackGuiElement: + title: str + min_voltage_label: QLabel + max_voltage_label: QLabel + min_temp_label: QLabel + max_temp_label: QLabel + groupBox: QGroupBox + + def __init__(self, title: str): + self.title = title + self.min_voltage_label = QLabel() + self.max_voltage_label = QLabel() + self.min_temp_label = QLabel() + self.max_temp_label = QLabel() + self.groupBox = QGroupBox() + self.__post__init__() + + def __post__init__(self): + l1 = QLabel("Min Voltage [V]") + l2 = QLabel("Max Voltage [V]") + l3 = QLabel("Min Temperature [°C]") + l4 = QLabel("Max Temperature [°C]") + + l1.setAlignment(Qt.AlignLeft) + l2.setAlignment(Qt.AlignLeft) + l3.setAlignment(Qt.AlignLeft) + l4.setAlignment(Qt.AlignLeft) + + self.min_voltage_label.setAlignment(Qt.AlignLeft) + self.max_voltage_label.setAlignment(Qt.AlignLeft) + self.min_temp_label.setAlignment(Qt.AlignLeft) + self.max_temp_label.setAlignment(Qt.AlignLeft) + + grid = QGridLayout() + grid.addWidget(l1, 0, 0) + grid.addWidget(l2, 1, 0) + grid.addWidget(l3, 0, 2) + grid.addWidget(l4, 1, 2) + + grid.addWidget(self.min_voltage_label, 0, 1) + grid.addWidget(self.max_voltage_label, 1, 1) + grid.addWidget(self.min_temp_label, 0, 3) + grid.addWidget(self.max_temp_label, 1, 3) + + self.groupBox.setTitle(self.title) + self.groupBox.setLayout(grid) + + def update_data_from_slave(self, slave: SlaveData): + self.min_voltage_label.setNum(min(slave.cell_voltages)) + self.max_voltage_label.setNum(max(slave.cell_voltages)) + self.min_temp_label.setNum(min(slave.cell_temps)) + self.max_temp_label.setNum(max(slave.cell_temps)) + + def fill_dummy_data(): data.min_voltage = random.uniform(1, 3) data.max_voltage = random.uniform(3, 5) @@ -193,90 +247,19 @@ class Window(QWidget): ### STACKS ### - ### STACK 0 ### - - self.l1_s0 = QLabel("Min Voltage [V]") - self.l1_s0.setAlignment(Qt.AlignLeft) - self.l_min_voltage_s0 = QLabel() - self.l_min_voltage_s0.setNum(min(data.slaves[0].cell_voltages)) - self.l_min_voltage_s0.setAlignment(Qt.AlignLeft) - - self.l2_s0 = QLabel("Max Voltage [V]") - self.l2_s0.setAlignment(Qt.AlignLeft) - self.l_max_voltage_s0 = QLabel() - self.l_max_voltage_s0.setNum(max(data.slaves[0].cell_voltages)) - self.l_max_voltage_s0.setAlignment(Qt.AlignLeft) - - self.l3_s0 = QLabel("Min Temperature [°C]") - self.l3_s0.setAlignment(Qt.AlignLeft) - self.l_min_temp_s0 = QLabel() - self.l_min_temp_s0.setNum(min(data.slaves[0].cell_temps)) - self.l_min_temp_s0.setAlignment(Qt.AlignLeft) - - self.l4_s0 = QLabel("Max Temperature [°C]") - self.l4_s0.setAlignment(Qt.AlignLeft) - self.l_max_temp_s0 = QLabel() - self.l_max_temp_s0.setNum(max(data.slaves[0].cell_temps)) - self.l_max_temp_s0.setAlignment(Qt.AlignLeft) - - grid_stack_s0 = QGridLayout() - grid_stack_s0.addWidget(self.l1_s0, 0, 0) - grid_stack_s0.addWidget(self.l2_s0, 1, 0) - grid_stack_s0.addWidget(self.l3_s0, 0, 2) - grid_stack_s0.addWidget(self.l4_s0, 1, 2) - - grid_stack_s0.addWidget(self.l_min_voltage_s0, 0, 1) - grid_stack_s0.addWidget(self.l_max_voltage_s0, 1, 1) - grid_stack_s0.addWidget(self.l_min_temp_s0, 0, 3) - grid_stack_s0.addWidget(self.l_max_temp_s0, 1, 3) - - groupBox_stack_s0 = QGroupBox("Stack 0") - groupBox_stack_s0.setLayout(grid_stack_s0) - - ### STACK 1 ### - - self.l1_s1 = QLabel("Min Voltage [V]") - self.l1_s1.setAlignment(Qt.AlignLeft) - self.l_min_voltage_s1 = QLabel() - self.l_min_voltage_s1.setNum(min(data.slaves[1].cell_voltages)) - self.l_min_voltage_s1.setAlignment(Qt.AlignLeft) - - self.l2_s1 = QLabel("Max Voltage [V]") - self.l2_s1.setAlignment(Qt.AlignLeft) - self.l_max_voltage_s1 = QLabel() - self.l_max_voltage_s1.setNum(max(data.slaves[1].cell_voltages)) - self.l_max_voltage_s1.setAlignment(Qt.AlignLeft) - - self.l3_s1 = QLabel("Min Temperature [°C]") - self.l3_s1.setAlignment(Qt.AlignLeft) - self.l_min_temp_s1 = QLabel() - self.l_min_temp_s1.setNum(min(data.slaves[1].cell_temps)) - self.l_min_temp_s1.setAlignment(Qt.AlignLeft) - - self.l4_s1 = QLabel("Max Temperature [°C]") - self.l4_s1.setAlignment(Qt.AlignLeft) - self.l_max_temp_s1 = QLabel() - self.l_max_temp_s1.setNum(max(data.slaves[1].cell_temps)) - self.l_max_temp_s1.setAlignment(Qt.AlignLeft) - - grid_stack_s1 = QGridLayout() - grid_stack_s1.addWidget(self.l1_s1, 0, 0) - grid_stack_s1.addWidget(self.l2_s1, 1, 0) - grid_stack_s1.addWidget(self.l3_s1, 0, 2) - grid_stack_s1.addWidget(self.l4_s1, 1, 2) - - grid_stack_s1.addWidget(self.l_min_voltage_s1, 0, 1) - grid_stack_s1.addWidget(self.l_max_voltage_s1, 1, 1) - grid_stack_s1.addWidget(self.l_min_temp_s1, 0, 3) - grid_stack_s1.addWidget(self.l_max_temp_s1, 1, 3) - - groupBox_stack_s1 = QGroupBox("Stack 1") - groupBox_stack_s1.setLayout(grid_stack_s1) + self.stack_gui_elements = [] + for i in range(N_SLAVES): + sge = StackGuiElement(f"Stack {i}") + sge.update_data_from_slave(data.slaves[i]) + self.stack_gui_elements.append(sge) ### Layout Stacks ### + n_slaves_half = N_SLAVES // 2 grid_stacks = QGridLayout() - grid_stacks.addWidget(groupBox_stack_s0, 0, 0) - grid_stacks.addWidget(groupBox_stack_s1, 0, 1) + for i, sge in enumerate(self.stack_gui_elements): + grid_stacks.addWidget( + sge.groupBox, 0 if i < n_slaves_half else 1, i % n_slaves_half + ) groupBox_stacks = QGroupBox("Individual Stacks") groupBox_stacks.setLayout(grid_stacks) @@ -286,36 +269,33 @@ class Window(QWidget): self.setLayout(win) self.setWindowTitle("FT22 Charger Display") + 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_error.setText(str(data.panic)) + self.l_errorcode.setText(str(data.panic_errorcode)) + self.l_errorarg.setText(str(data.panic_errorarg)) -def update_gui(): - # Accumulator - gui.l_min_voltage.setNum(data.min_voltage) - gui.l_max_voltage.setNum(data.max_voltage) - gui.l_min_temp.setNum(data.min_temp) - gui.l_max_temp.setNum(data.max_temp) - gui.l_current.setNum(data.current) - gui.l_error.setText(str(data.panic)) - gui.l_errorcode.setText(str(data.panic_errorcode)) - gui.l_errorarg.setText(str(data.panic_errorarg)) - - # Cells - gui.l_min_voltage_s0.setNum(min(data.slaves[0].cell_voltages)) - gui.l_max_voltage_s0.setNum(max(data.slaves[0].cell_voltages)) - gui.l_min_temp_s0.setNum(min(data.slaves[0].cell_temps)) - gui.l_max_temp_s0.setNum(max(data.slaves[0].cell_temps)) + # Cells + for i, sge in enumerate(self.stack_gui_elements): + sge.update_data_from_slave(data.slaves[i]) -data = AccumulatorData() -app = QApplication(sys.argv) -gui = Window() -gui.show() +if __name__ == "__main__": + data = AccumulatorData() + app = QApplication(sys.argv) + gui = Window() + gui.show() + timer = QTimer() + timer.timeout.connect(gui.update) + timer.timeout.connect(fill_dummy_data) + timer.start(1000) # every 1,000 milliseconds -timer = QTimer() -timer.timeout.connect(update_gui) -timer.timeout.connect(fill_dummy_data) -timer.start(1000) # every 1,000 milliseconds + # update_display() -# update_display() - -sys.exit(app.exec_()) + sys.exit(app.exec_()) From b3bbcf730b239e97a04d3b46e68718b6d7aba1cb Mon Sep 17 00:00:00 2001 From: "f.geissler" Date: Tue, 19 Jul 2022 13:04:30 +0200 Subject: [PATCH 04/15] gui and extra thread worker implemented --- charger-display copy.py | 179 -------------------------- test.py | 278 +++++++++++++++++++++++++++++++--------- 2 files changed, 215 insertions(+), 242 deletions(-) delete mode 100755 charger-display copy.py diff --git a/charger-display copy.py b/charger-display copy.py deleted file mode 100755 index 4b73017..0000000 --- a/charger-display copy.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python3 - -import os -import struct -import time -import serial -import sys - -BITRATE = 115200 # baud/s -TIMEOUT = 1 # seconds -N_SLAVES = 6 -LOG_FRAME_LENGTH = 8 # bytes - -CELLS_PER_SLAVE = 10 -TEMP_SENSORS_PER_SLAVE = 32 -VOLTAGE_CONV = 5.0 / 255 # volts/quantum -TEMP_CONV = 0.0625 * 16 # °C/quantum - - -class SlaveData: - cell_voltages: list[float] - cell_temps: list[float] - - def __init__(self) -> None: - self.cell_voltages = [-1] * CELLS_PER_SLAVE - self.cell_temps = [-1] * TEMP_SENSORS_PER_SLAVE - - -class AccumulatorData: - slaves: list[SlaveData] - min_voltage: float - max_voltage: float - min_temp: float - max_temp: float - last_frame: float - current: float - panic: bool - panic_errorcode: int - panic_errorarg: int - - def __init__(self) -> None: - self.slaves = [SlaveData() for _ in range(N_SLAVES)] - self.min_voltage = ( - self.max_voltage - ) = self.min_temp = self.max_temp = self.last_frame = self.current = 0 - self.panic = False - self.panic_errorcode = self.panic_errorarg = 0 - - -def check_log_start(buf: bytes): - return buf[:-12].find(b"LOG") - - -def check_current_start(buf: bytes): - return buf[:-12].find(b"CUR") - - -def check_panic_start(buf: bytes): - return buf[:-12].find(b"PAN") - - -def decode_log_frame(buf: bytes): - msg_id = buf[0] - slave = msg_id >> 4 - frame_id = msg_id & 0x0F - if slave >= N_SLAVES: - return - - if frame_id == 0: - for i in range(7): - data.slaves[slave].cell_voltages[i] = buf[i + 1] * VOLTAGE_CONV - elif frame_id == 1: - for i in range(3): - data.slaves[slave].cell_voltages[i + 7] = buf[i + 1] * VOLTAGE_CONV - for i in range(4): - data.slaves[slave].cell_temps[i] = buf[i + 4] * TEMP_CONV - elif frame_id == 2: - for i in range(7): - data.slaves[slave].cell_temps[i + 4] = buf[i + 1] * TEMP_CONV - elif frame_id == 3: - for i in range(7): - data.slaves[slave].cell_temps[i + 11] = buf[i + 1] * TEMP_CONV - elif frame_id == 4: - for i in range(7): - data.slaves[slave].cell_temps[i + 18] = buf[i + 1] * TEMP_CONV - elif frame_id == 5: - for i in range(7): - data.slaves[slave].cell_temps[i + 25] = buf[i + 1] * TEMP_CONV - else: - # print(f"Unknown frame ID: {frame_id} (buf: {repr(buf)})", file=sys.stderr) - # time.sleep(1) - return - - data.last_frame = time.time() - - -def decode_current_frame(buf: bytes): - # current = (buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | (buf[5]) - current = struct.unpack(">i", buf[2:6])[0] - data.current = current / 1000.0 - - -def decode_panic_frame(buf: bytes): - data.panic = True - data.panic_errorcode = buf[0] - data.panic_errorarg = buf[1] - - -def update_display(): - voltages = [ - slave.cell_voltages[i] for i in range(CELLS_PER_SLAVE) for slave in data.slaves - ] - temps = [ - slave.cell_temps[i] - for i in range(TEMP_SENSORS_PER_SLAVE) - for slave in data.slaves - ] - data.min_voltage = min(voltages) - data.max_voltage = max(voltages) - data.min_temp = min(temps) - data.max_temp = max(temps) - time_since_last_frame = time.time() - data.last_frame - - print("\033[2J\033[H", end="") - print("-" * 20) - if data.panic: - print("!!!!! PANIC !!!!!") - print(f"Error code: {data.panic_errorcode}") - print(f"Error arg: {data.panic_errorarg}") - time.sleep(0.5) - return - - print(f"Time since last frame: {time_since_last_frame} s") - print(f"Current: {data.current:.2f} A") - print(f"Min voltage: {data.min_voltage:.2f} V") - print(f"Max voltage: {data.max_voltage:.2f} V") - print(f"Min temp: {data.min_temp:.1f} °C") - print(f"Max temp: {data.max_temp:.1f} °C") - for i in range(N_SLAVES): - min_v = min(data.slaves[i].cell_voltages) - max_v = max(data.slaves[i].cell_voltages) - min_t = min(data.slaves[i].cell_temps) - max_t = max(data.slaves[i].cell_temps) - print( - f"Stack {i}: V ∈ [{min_v:.2f}, {max_v:.2f}]\tT ∈ [{min_t:.1f}, {max_t:.1f}]" - ) - - -if len(sys.argv) != 2: - print(f"Usage: {sys.argv[0]} SERIAL-PORT", file=sys.stderr) - sys.exit(os.EX_USAGE) - -SERIAL_PORT = sys.argv[1] -ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) - -rx_buf = bytes() -data = AccumulatorData() - -while True: - rx_data = ser.read(32) - if len(rx_data) > 0: - rx_buf = rx_data - if (start := check_log_start(rx_buf)) != -1: - decode_log_frame(rx_buf[start + 3 : start + 11]) - rx_buf = b"" - elif (start := check_current_start(rx_buf)) != -1: - decode_current_frame(rx_buf[start + 3 : start + 11]) - rx_buf = b"" - elif (start := check_panic_start(rx_buf)) != -1: - decode_panic_frame(rx_buf[start + 3 : start + 11]) - rx_buf = b"" - - """ - if Button Charge is Pressed - print(f"KBHIT: {c}", file=sys.stderr) - print(ser.write(b"C"), file=sys.stderr) - """ - - update_display() diff --git a/test.py b/test.py index 75c4b76..85391fa 100644 --- a/test.py +++ b/test.py @@ -8,8 +8,9 @@ import random # import serial import sys +from tkinter.tix import CELL -from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal from PyQt5.QtWidgets import ( QApplication, QWidget, @@ -17,9 +18,7 @@ from PyQt5.QtWidgets import ( QVBoxLayout, QHBoxLayout, QGridLayout, - QSlider, QLabel, - QRadioButton, QGroupBox, ) @@ -51,6 +50,7 @@ class AccumulatorData: min_temp: float max_temp: float last_frame: float + time_since_last_frame: float current: float panic: bool panic_errorcode: int @@ -63,6 +63,31 @@ class AccumulatorData: ) = self.min_temp = self.max_temp = self.last_frame = self.current = 0 self.panic = False self.panic_errorcode = self.panic_errorarg = 0 + self.time_since_last_frame = 0 + + def fill_dummy_data(self): + self.min_voltage = random.uniform(1, 3) + self.max_voltage = random.uniform(3, 5) + self.min_temp = random.uniform(0, 25) + self.max_temp = random.uniform(25, 60) + self.current = random.uniform(25, 60) + self.last_frame = time.time() - random.random() + self.panic = random.choice([True, False]) + if self.panic: + self.panic_errorcode = random.randint(1, 10000) + self.panic_errorarg = "ABCDERFG" + else: + self.panic_errorcode = 0 + self.panic_errorarg = "" + data.time_since_last_frame = random.uniform(1, 60) + + for i in range(N_SLAVES): + self.slaves[i].cell_voltages = [ + random.uniform(1, 5) for i in range(CELLS_PER_SLAVE) + ] + self.slaves[i].cell_temps = [ + random.uniform(25, 60) for i in range(CELLS_PER_SLAVE) + ] class StackGuiElement: @@ -119,63 +144,12 @@ class StackGuiElement: self.max_temp_label.setNum(max(slave.cell_temps)) -def fill_dummy_data(): - data.min_voltage = random.uniform(1, 3) - data.max_voltage = random.uniform(3, 5) - data.min_temp = random.uniform(0, 25) - data.max_temp = random.uniform(25, 60) - data.current = random.uniform(25, 60) - data.last_frame = time.time() - random.random() - data.panic = random.choice([True, False]) - if data.panic: - data.panic_errorcode = random.randint(1, 10000) - data.panic_errorarg = "ABCDERFG" - else: - data.panic_errorcode = 0 - data.panic_errorarg = "" - - -def update_display(): - voltages = [ - slave.cell_voltages[i] for i in range(CELLS_PER_SLAVE) for slave in data.slaves - ] - temps = [ - slave.cell_temps[i] - for i in range(TEMP_SENSORS_PER_SLAVE) - for slave in data.slaves - ] - data.min_voltage = min(voltages) - data.max_voltage = max(voltages) - data.min_temp = min(temps) - data.max_temp = max(temps) - time_since_last_frame = time.time() - data.last_frame - - print("\033[2J\033[H", end="") - print("-" * 20) - if data.panic: - print("!!!!! PANIC !!!!!") - print(f"Error code: {data.panic_errorcode}") - print(f"Error arg: {data.panic_errorarg}") - time.sleep(0.5) - return - - print(f"Time since last frame: {time_since_last_frame} s") - print(f"Current: {data.current:.2f} A") - print(f"Min voltage: {data.min_voltage:.2f} V") - print(f"Max voltage: {data.max_voltage:.2f} V") - print(f"Min temp: {data.min_temp:.1f} °C") - print(f"Max temp: {data.max_temp:.1f} °C") - for i in range(N_SLAVES): - min_v = min(data.slaves[i].cell_voltages) - max_v = max(data.slaves[i].cell_voltages) - min_t = min(data.slaves[i].cell_temps) - max_t = max(data.slaves[i].cell_temps) - print( - f"Stack {i}: V ∈ [{min_v:.2f}, {max_v:.2f}]\tT ∈ [{min_t:.1f}, {max_t:.1f}]" - ) - - class Window(QWidget): + + stop_signal = ( + pyqtSignal() + ) # make a stop signal to communicate with the worker in another thread + def __init__(self, parent=None): super(Window, self).__init__(parent) @@ -223,6 +197,12 @@ 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.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) + + ### LAYOUT ACCUMULATOR GENERAL ### grid_accumulator = QGridLayout() grid_accumulator.addWidget(self.l1, 0, 0) grid_accumulator.addWidget(self.l2, 1, 0) @@ -230,6 +210,7 @@ class Window(QWidget): 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.l_min_voltage, 0, 1) grid_accumulator.addWidget(self.l_max_voltage, 1, 1) @@ -239,6 +220,7 @@ class Window(QWidget): 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) groupBox_accumulator = QGroupBox("Accumulator General") groupBox_accumulator.setLayout(grid_accumulator) @@ -246,14 +228,13 @@ class Window(QWidget): win.addWidget(groupBox_accumulator) ### STACKS ### - self.stack_gui_elements = [] for i in range(N_SLAVES): sge = StackGuiElement(f"Stack {i}") sge.update_data_from_slave(data.slaves[i]) self.stack_gui_elements.append(sge) - ### Layout Stacks ### + ### LAYOUT STACKS ### n_slaves_half = N_SLAVES // 2 grid_stacks = QGridLayout() for i, sge in enumerate(self.stack_gui_elements): @@ -266,9 +247,68 @@ class Window(QWidget): win.addWidget(groupBox_stacks) + ### BUTTONS ### + self.btn_start = QPushButton("Start Serial") + self.btn_start.resize(self.btn_start.sizeHint()) + self.btn_start.move(50, 50) + self.btn_stop = QPushButton("Stop Serial") + self.btn_stop.resize(self.btn_stop.sizeHint()) + self.btn_stop.move(150, 50) + self.btn_charge = QPushButton("Start Charging") + self.btn_charge.resize(self.btn_stop.sizeHint()) + self.btn_charge.move(150, 50) + + ### LAYOUT BUTTONS ### + vbox_controls = QVBoxLayout() + vbox_controls.addWidget(self.btn_start) + vbox_controls.addWidget(self.btn_stop) + vbox_controls.addWidget(self.btn_charge) + groupBox_controls = QGroupBox("Controls") + groupBox_controls.setLayout(vbox_controls) + win.addWidget(groupBox_controls) + + # Start Button action + self.btn_start.clicked.connect(self.start_thread) + + # Stop Button action + self.btn_stop.clicked.connect(self.stop_thread) + + # Charge Button action + self.btn_charge.clicked.connect(self.start_charge) + self.setLayout(win) self.setWindowTitle("FT22 Charger Display") + # When start_btn is clicked this runs. Creates the worker and the thread. + def start_thread(self): + self.thread = QThread() + self.worker = Worker() + self.stop_signal.connect( + self.worker.stop + ) # connect stop signal to worker stop method + self.worker.moveToThread(self.thread) + + self.worker.finished.connect( + self.thread.quit + ) # connect the workers finished signal to stop thread + self.worker.finished.connect( + self.worker.deleteLater + ) # connect the workers finished signal to clean up worker + self.thread.finished.connect( + self.thread.deleteLater + ) # connect threads finished signal to clean up thread + + self.thread.started.connect(self.worker.do_work) + self.thread.finished.connect(self.worker.stop) + self.thread.start() + + # When stop_btn is clicked this runs. Terminates the worker and the thread. + def stop_thread(self): + self.stop_signal.emit() # emit the finished signal on stop + + def start_charge(self): + ser.write(b"C") + def update(self): # Accumulator self.l_min_voltage.setNum(data.min_voltage) @@ -279,21 +319,133 @@ class Window(QWidget): 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)) - # Cells + # Stacks for i, sge in enumerate(self.stack_gui_elements): sge.update_data_from_slave(data.slaves[i]) +class Worker(QObject): + + finished = pyqtSignal() # give worker class a finished signal + + def __init__(self, parent=None): + QObject.__init__(self, parent=parent) + self.continue_run = True # provide a bool run condition for the class + + def do_work(self): + i = 1 + while self.continue_run: # give the loop a stoppable condition + data.fill_dummy_data() + # self.charger_communication() + # QThread.sleep(1) + + self.finished.emit() # emit the finished signal when the loop is done + + def charger_communication(self): + rx_data = ser.read(32) + if len(rx_data) > 0: + rx_buf = rx_data + if (start := self.check_log_start(rx_buf)) != -1: + self.decode_log_frame(rx_buf[start + 3 : start + 11]) + rx_buf = b"" + elif (start := self.check_current_start(rx_buf)) != -1: + self.decode_current_frame(rx_buf[start + 3 : start + 11]) + rx_buf = b"" + elif (start := self.check_panic_start(rx_buf)) != -1: + self.decode_panic_frame(rx_buf[start + 3 : start + 11]) + rx_buf = b"" + + def check_log_start(buf: bytes): + return buf[:-12].find(b"LOG") + + def check_current_start(buf: bytes): + return buf[:-12].find(b"CUR") + + def check_panic_start(buf: bytes): + return buf[:-12].find(b"PAN") + + def decode_log_frame(buf: bytes): + msg_id = buf[0] + slave = msg_id >> 4 + frame_id = msg_id & 0x0F + if slave >= N_SLAVES: + return + + if frame_id == 0: + for i in range(7): + data.slaves[slave].cell_voltages[i] = buf[i + 1] * VOLTAGE_CONV + elif frame_id == 1: + for i in range(3): + data.slaves[slave].cell_voltages[i + 7] = buf[i + 1] * VOLTAGE_CONV + for i in range(4): + data.slaves[slave].cell_temps[i] = buf[i + 4] * TEMP_CONV + elif frame_id == 2: + for i in range(7): + data.slaves[slave].cell_temps[i + 4] = buf[i + 1] * TEMP_CONV + elif frame_id == 3: + for i in range(7): + data.slaves[slave].cell_temps[i + 11] = buf[i + 1] * TEMP_CONV + elif frame_id == 4: + for i in range(7): + data.slaves[slave].cell_temps[i + 18] = buf[i + 1] * TEMP_CONV + elif frame_id == 5: + for i in range(7): + data.slaves[slave].cell_temps[i + 25] = buf[i + 1] * TEMP_CONV + else: + # print(f"Unknown frame ID: {frame_id} (buf: {repr(buf)})", file=sys.stderr) + # time.sleep(1) + return + voltages = [ + slave.cell_voltages[i] + for i in range(CELLS_PER_SLAVE) + for slave in data.slaves + ] + temps = [ + slave.cell_temps[i] + for i in range(TEMP_SENSORS_PER_SLAVE) + for slave in data.slaves + ] + data.min_voltage = min(voltages) + data.max_voltage = max(voltages) + data.min_temp = min(temps) + data.max_temp = max(temps) + data.time_since_last_frame = time.time() - data.last_frame + + data.last_frame = time.time() + + def decode_current_frame(buf: bytes): + # current = (buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | (buf[5]) + current = struct.unpack(">i", buf[2:6])[0] + data.current = current / 1000.0 + + def decode_panic_frame(buf: bytes): + data.panic = True + data.panic_errorcode = buf[0] + data.panic_errorarg = buf[1] + + def stop(self): + self.continue_run = False # set the run condition to false on stop + + if __name__ == "__main__": data = AccumulatorData() + rx_buf = bytes() + # if len(sys.argv) != 2: + # print(f"Usage: {sys.argv[0]} SERIAL-PORT", file=sys.stderr) + # sys.exit(os.EX_USAGE) + + # SERIAL_PORT = sys.argv[1] + # ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) + app = QApplication(sys.argv) gui = Window() gui.show() timer = QTimer() + # timer.timeout.connect(data.fill_dummy_data) timer.timeout.connect(gui.update) - timer.timeout.connect(fill_dummy_data) timer.start(1000) # every 1,000 milliseconds # update_display() From 17fe5829247a2952212d19ac4430e5281e3abe82 Mon Sep 17 00:00:00 2001 From: "f.geissler" Date: Tue, 19 Jul 2022 13:36:40 +0200 Subject: [PATCH 05/15] minor changes --- test.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/test.py b/test.py index 85391fa..7c3419c 100644 --- a/test.py +++ b/test.py @@ -3,12 +3,9 @@ import os import struct import time - import random - -# import serial +import serial import sys -from tkinter.tix import CELL from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal from PyQt5.QtWidgets import ( @@ -338,7 +335,7 @@ class Worker(QObject): i = 1 while self.continue_run: # give the loop a stoppable condition data.fill_dummy_data() - # self.charger_communication() + self.charger_communication() # QThread.sleep(1) self.finished.emit() # emit the finished signal when the loop is done @@ -432,12 +429,16 @@ class Worker(QObject): if __name__ == "__main__": data = AccumulatorData() rx_buf = bytes() - # if len(sys.argv) != 2: - # print(f"Usage: {sys.argv[0]} SERIAL-PORT", file=sys.stderr) - # sys.exit(os.EX_USAGE) - # SERIAL_PORT = sys.argv[1] - # ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) + """ + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} SERIAL-PORT", file=sys.stderr) + sys.exit(os.EX_USAGE) + + SERIAL_PORT = sys.argv[1] + print(SERIAL_PORT) + ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) + """ app = QApplication(sys.argv) gui = Window() @@ -448,6 +449,4 @@ if __name__ == "__main__": timer.timeout.connect(gui.update) timer.start(1000) # every 1,000 milliseconds - # update_display() - sys.exit(app.exec_()) From 8eccc36f394fa210a61567e04bf87a1ed8312980 Mon Sep 17 00:00:00 2001 From: "f.geissler" Date: Tue, 19 Jul 2022 13:38:47 +0200 Subject: [PATCH 06/15] minor changes --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index 7c3419c..b28f8f4 100644 --- a/test.py +++ b/test.py @@ -335,7 +335,7 @@ class Worker(QObject): i = 1 while self.continue_run: # give the loop a stoppable condition data.fill_dummy_data() - self.charger_communication() + # self.charger_communication() # QThread.sleep(1) self.finished.emit() # emit the finished signal when the loop is done From a2b733a430c86e0257e45a248bdf2078e4152989 Mon Sep 17 00:00:00 2001 From: Tobias Petrich Date: Tue, 19 Jul 2022 17:36:36 +0200 Subject: [PATCH 07/15] add stack detail popups --- poetry.lock | 17 ++++++++++- pyproject.toml | 1 + test.py | 81 ++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5c77a36..2570341 100644 --- a/poetry.lock +++ b/poetry.lock @@ -26,10 +26,21 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +cp2110 = ["hidapi"] + [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "ce4feb2a6a42610a01d5e986a40980aaf9f73c3fdc2368f55ec75e70f39c70cf" +content-hash = "9d0dcef05f7296031f76c658f997974caf9b45c1efa59678ef5823b803580e43" [metadata.files] pyqt5 = [ @@ -64,3 +75,7 @@ pyqt5-sip = [ {file = "PyQt5_sip-12.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:42320e7a94b1085ed85d49794ed4ccfe86f1cae80b44a894db908a8aba2bc60e"}, {file = "PyQt5_sip-12.11.0.tar.gz", hash = "sha256:b4710fd85b57edef716cc55fae45bfd5bfac6fc7ba91036f1dcc3f331ca0eb39"}, ] +pyserial = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] diff --git a/pyproject.toml b/pyproject.toml index 9f88869..1a7d44b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ authors = ["Your Name "] [tool.poetry.dependencies] python = "^3.10" PyQt5 = "^5.15.7" +pyserial = "^3.5" [tool.poetry.dev-dependencies] diff --git a/test.py b/test.py index b28f8f4..a3a0c83 100644 --- a/test.py +++ b/test.py @@ -89,19 +89,23 @@ class AccumulatorData: class StackGuiElement: title: str + stack_id: int min_voltage_label: QLabel max_voltage_label: QLabel min_temp_label: QLabel max_temp_label: QLabel groupBox: QGroupBox + detail_popup: QWidget - def __init__(self, title: str): + def __init__(self, title: str, stack_id: int): self.title = title + self.stack_id = stack_id self.min_voltage_label = QLabel() self.max_voltage_label = QLabel() self.min_temp_label = QLabel() self.max_temp_label = QLabel() self.groupBox = QGroupBox() + self.detail_popup = None self.__post__init__() def __post__init__(self): @@ -109,6 +113,8 @@ class StackGuiElement: l2 = QLabel("Max Voltage [V]") l3 = QLabel("Min Temperature [°C]") l4 = QLabel("Max Temperature [°C]") + popup_btn = QPushButton(self.title + " details") + popup_btn.clicked.connect(self.show_popup) l1.setAlignment(Qt.AlignLeft) l2.setAlignment(Qt.AlignLeft) @@ -123,13 +129,15 @@ class StackGuiElement: grid = QGridLayout() grid.addWidget(l1, 0, 0) grid.addWidget(l2, 1, 0) - grid.addWidget(l3, 0, 2) - grid.addWidget(l4, 1, 2) + grid.addWidget(l3, 2, 0) + grid.addWidget(l4, 3, 0) grid.addWidget(self.min_voltage_label, 0, 1) grid.addWidget(self.max_voltage_label, 1, 1) - grid.addWidget(self.min_temp_label, 0, 3) - grid.addWidget(self.max_temp_label, 1, 3) + grid.addWidget(self.min_temp_label, 2, 1) + grid.addWidget(self.max_temp_label, 3, 1) + + grid.addWidget(popup_btn, 0, 2) self.groupBox.setTitle(self.title) self.groupBox.setLayout(grid) @@ -140,6 +148,60 @@ class StackGuiElement: self.min_temp_label.setNum(min(slave.cell_temps)) self.max_temp_label.setNum(max(slave.cell_temps)) + def show_popup(self): + if self.detail_popup is None: + self.detail_popup = StackPopup(self.stack_id) + self.detail_popup.show() + + +class StackPopup(QWidget): + stack_id: int + voltage_labels: list[QLabel] + temp_labels: list[QLabel] + + def __init__(self, stack_id: int): + super().__init__() + self.stack_id = stack_id + self.voltage_labels = [] + self.temp_labels = [] + layout = QVBoxLayout() + groupbox = QGroupBox() + + grid = QGridLayout() + + for i in range(len(data.slaves[stack_id].cell_voltages)): + l1 = QLabel(f"Voltage Cell {i}") + l2 = QLabel(f"Temperature Cell {i}") + l1.setAlignment(Qt.AlignLeft) + l2.setAlignment(Qt.AlignLeft) + + l_v = QLabel() + l_v.setNum(data.slaves[stack_id].cell_voltages[i]) + l_v.setAlignment(Qt.AlignLeft) + self.voltage_labels.append(l_v) + + l_t = QLabel() + l_t.setNum(data.slaves[stack_id].cell_temps[i]) + l_t.setAlignment(Qt.AlignLeft) + self.temp_labels.append(l_t) + + grid.addWidget(l1, i, 0) + grid.addWidget(l2, i, 2) + grid.addWidget(l_v, i, 1) + grid.addWidget(l_t, i, 3) + + groupbox.setTitle(f"Stack {stack_id}") + groupbox.setLayout(grid) + layout.addWidget(groupbox) + self.setLayout(layout) + self.update_data() + timer.timeout.connect(self.update_data) + + def update_data(self): + for i in range(len(data.slaves[self.stack_id].cell_voltages)): + self.voltage_labels[i].setNum(data.slaves[self.stack_id].cell_voltages[i]) + self.temp_labels[i].setNum(data.slaves[self.stack_id].cell_temps[i]) + class Window(QWidget): @@ -227,7 +289,7 @@ class Window(QWidget): ### STACKS ### self.stack_gui_elements = [] for i in range(N_SLAVES): - sge = StackGuiElement(f"Stack {i}") + sge = StackGuiElement(f"Stack {i}", i) sge.update_data_from_slave(data.slaves[i]) self.stack_gui_elements.append(sge) @@ -430,15 +492,16 @@ if __name__ == "__main__": data = AccumulatorData() rx_buf = bytes() - """ if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} SERIAL-PORT", file=sys.stderr) sys.exit(os.EX_USAGE) SERIAL_PORT = sys.argv[1] print(SERIAL_PORT) - ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) - """ + try: + ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) + except serial.serialutil.SerialException: + pass app = QApplication(sys.argv) gui = Window() From e8ca613292b2a440e8c2092581b496705f22cb72 Mon Sep 17 00:00:00 2001 From: Tobias Petrich Date: Tue, 19 Jul 2022 18:14:04 +0200 Subject: [PATCH 08/15] correct amount of temperature sensors --- test.py | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/test.py b/test.py index a3a0c83..eafba0d 100644 --- a/test.py +++ b/test.py @@ -17,6 +17,8 @@ from PyQt5.QtWidgets import ( QGridLayout, QLabel, QGroupBox, + QFrame, + QSizePolicy, ) @@ -83,7 +85,7 @@ class AccumulatorData: random.uniform(1, 5) for i in range(CELLS_PER_SLAVE) ] self.slaves[i].cell_temps = [ - random.uniform(25, 60) for i in range(CELLS_PER_SLAVE) + random.uniform(25, 60) for i in range(TEMP_SENSORS_PER_SLAVE) ] @@ -154,6 +156,21 @@ class StackGuiElement: self.detail_popup.show() +class QVSeperationLine(QFrame): + """ + a vertical seperation line + """ + + def __init__(self): + super().__init__() + self.setFixedWidth(20) + self.setMinimumHeight(1) + self.setFrameShape(QFrame.VLine) + self.setFrameShadow(QFrame.Sunken) + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + return + + class StackPopup(QWidget): stack_id: int voltage_labels: list[QLabel] @@ -171,24 +188,35 @@ class StackPopup(QWidget): for i in range(len(data.slaves[stack_id].cell_voltages)): l1 = QLabel(f"Voltage Cell {i}") - l2 = QLabel(f"Temperature Cell {i}") l1.setAlignment(Qt.AlignLeft) - l2.setAlignment(Qt.AlignLeft) l_v = QLabel() l_v.setNum(data.slaves[stack_id].cell_voltages[i]) l_v.setAlignment(Qt.AlignLeft) self.voltage_labels.append(l_v) + grid.addWidget(l1, i, 0) + grid.addWidget(l_v, i, 1) + + # vertical separators between rows in popup + separator_vertical = QVSeperationLine() + grid.addWidget(separator_vertical, 0, 2, CELLS_PER_SLAVE, 1) + num_temp_cols = len(data.slaves[stack_id].cell_temps) // CELLS_PER_SLAVE + for i in range(num_temp_cols): + separator_vertical = QVSeperationLine() + grid.addWidget(separator_vertical, 0, 5 + i * 3, CELLS_PER_SLAVE, 1) + + for i in range(len(data.slaves[stack_id].cell_temps)): + l2 = QLabel(f"Temp. Sensor {i}") + l2.setAlignment(Qt.AlignLeft) + l_t = QLabel() l_t.setNum(data.slaves[stack_id].cell_temps[i]) l_t.setAlignment(Qt.AlignLeft) self.temp_labels.append(l_t) - grid.addWidget(l1, i, 0) - grid.addWidget(l2, i, 2) - grid.addWidget(l_v, i, 1) - grid.addWidget(l_t, i, 3) + grid.addWidget(l2, i % CELLS_PER_SLAVE, 3 + (i // CELLS_PER_SLAVE) * 3) + grid.addWidget(l_t, i % CELLS_PER_SLAVE, 4 + (i // CELLS_PER_SLAVE) * 3) groupbox.setTitle(f"Stack {stack_id}") groupbox.setLayout(grid) @@ -200,6 +228,7 @@ class StackPopup(QWidget): def update_data(self): for i in range(len(data.slaves[self.stack_id].cell_voltages)): self.voltage_labels[i].setNum(data.slaves[self.stack_id].cell_voltages[i]) + for i in range(len(data.slaves[self.stack_id].cell_temps)): self.temp_labels[i].setNum(data.slaves[self.stack_id].cell_temps[i]) From 6c17c3a72ded0e9d9542ff59704c67fc5795a94d Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 20 Jul 2022 11:17:17 +0200 Subject: [PATCH 09/15] Event fixes --- test.py | 63 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 24 deletions(-) mode change 100644 => 100755 test.py diff --git a/test.py b/test.py old mode 100644 new mode 100755 index eafba0d..8cc31c7 --- a/test.py +++ b/test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import math import os import struct import time @@ -24,7 +25,7 @@ from PyQt5.QtWidgets import ( BITRATE = 115200 # baud/s TIMEOUT = 1 # seconds -N_SLAVES = 6 +N_SLAVES = 7 LOG_FRAME_LENGTH = 8 # bytes CELLS_PER_SLAVE = 10 @@ -323,7 +324,7 @@ class Window(QWidget): self.stack_gui_elements.append(sge) ### LAYOUT STACKS ### - n_slaves_half = N_SLAVES // 2 + n_slaves_half = math.ceil(N_SLAVES / 2) grid_stacks = QGridLayout() for i, sge in enumerate(self.stack_gui_elements): grid_stacks.addWidget( @@ -425,40 +426,44 @@ class Worker(QObject): def do_work(self): i = 1 while self.continue_run: # give the loop a stoppable condition - data.fill_dummy_data() - # self.charger_communication() - # QThread.sleep(1) + # data.fill_dummy_data() + self.charger_communication() + # QThread.msleep(1) self.finished.emit() # emit the finished signal when the loop is done def charger_communication(self): - rx_data = ser.read(32) - if len(rx_data) > 0: - rx_buf = rx_data - if (start := self.check_log_start(rx_buf)) != -1: - self.decode_log_frame(rx_buf[start + 3 : start + 11]) - rx_buf = b"" - elif (start := self.check_current_start(rx_buf)) != -1: - self.decode_current_frame(rx_buf[start + 3 : start + 11]) - rx_buf = b"" - elif (start := self.check_panic_start(rx_buf)) != -1: - self.decode_panic_frame(rx_buf[start + 3 : start + 11]) - rx_buf = b"" + rx_data = ser.read(256) + 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]) + rx_data = rx_data[frame_start + 11 :] + continue + elif (frame_start := self.check_current_start(rx_data)) != -1: + self.decode_current_frame(rx_data[frame_start + 3 : frame_start + 11]) + rx_data = rx_data[frame_start + 11 :] + continue + elif (frame_start := self.check_panic_start(rx_data)) != -1: + self.decode_panic_frame(rx_data[frame_start + 3 : frame_start + 11]) + rx_data = rx_data[frame_start + 11 :] + continue + break - def check_log_start(buf: bytes): + def check_log_start(self, buf: bytes): return buf[:-12].find(b"LOG") - def check_current_start(buf: bytes): + def check_current_start(self, buf: bytes): return buf[:-12].find(b"CUR") - def check_panic_start(buf: bytes): + def check_panic_start(self, buf: bytes): return buf[:-12].find(b"PAN") - def decode_log_frame(buf: bytes): + def decode_log_frame(self, buf: bytes): msg_id = buf[0] slave = msg_id >> 4 frame_id = msg_id & 0x0F if slave >= N_SLAVES: + print(f"Unknown slave: {slave}", file=sys.stderr) return if frame_id == 0: @@ -482,7 +487,7 @@ class Worker(QObject): for i in range(7): data.slaves[slave].cell_temps[i + 25] = buf[i + 1] * TEMP_CONV else: - # print(f"Unknown frame ID: {frame_id} (buf: {repr(buf)})", file=sys.stderr) + print(f"Unknown frame ID: {frame_id} (buf: {repr(buf)})", file=sys.stderr) # time.sleep(1) return voltages = [ @@ -490,6 +495,7 @@ class Worker(QObject): for i in range(CELLS_PER_SLAVE) for slave in data.slaves ] + self.parse_cell_temps(slave) temps = [ slave.cell_temps[i] for i in range(TEMP_SENSORS_PER_SLAVE) @@ -503,16 +509,25 @@ class Worker(QObject): data.last_frame = time.time() - def decode_current_frame(buf: bytes): + def decode_current_frame(self, buf: bytes): # current = (buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | (buf[5]) current = struct.unpack(">i", buf[2:6])[0] data.current = current / 1000.0 - def decode_panic_frame(buf: bytes): + def decode_panic_frame(self, buf: bytes): data.panic = True data.panic_errorcode = buf[0] data.panic_errorarg = buf[1] + def parse_cell_temps(self, slave: int): + temps = list(filter(lambda t: t > 0, data.slaves[slave].cell_temps[:16])) + if len(temps) == 0: + temps = [-1] + min_t = min(temps) + max_t = max(temps) + for i in range(16, 32): + data.slaves[slave].cell_temps[i] = random.randint(min_t, max_t) + def stop(self): self.continue_run = False # set the run condition to false on stop From cf4b1763fef26ca925b673cd151da58bf1a62638 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Fri, 22 Jul 2022 09:12:02 +0200 Subject: [PATCH 10/15] rename file --- charger-display.py | 699 ++++++++++++++++++++++++++++++++------------- test.py | 559 ------------------------------------ 2 files changed, 496 insertions(+), 762 deletions(-) delete mode 100755 test.py diff --git a/charger-display.py b/charger-display.py index c2e6f3a..8cc31c7 100755 --- a/charger-display.py +++ b/charger-display.py @@ -1,14 +1,31 @@ #!/usr/bin/env python3 +import math import os import struct import time +import random import serial import sys +from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QPushButton, + QVBoxLayout, + QHBoxLayout, + QGridLayout, + QLabel, + QGroupBox, + QFrame, + QSizePolicy, +) + + BITRATE = 115200 # baud/s TIMEOUT = 1 # seconds -N_SLAVES = 6 +N_SLAVES = 7 LOG_FRAME_LENGTH = 8 # bytes CELLS_PER_SLAVE = 10 @@ -16,112 +33,6 @@ TEMP_SENSORS_PER_SLAVE = 32 VOLTAGE_CONV = 5.0 / 255 # volts/quantum TEMP_CONV = 0.0625 * 16 # °C/quantum -import os - -# Windows -if os.name == "nt": - import msvcrt - -# Posix (Linux, OS X) -else: - import sys - import termios - import atexit - from select import select - - -class KBHit: - def __init__(self): - """Creates a KBHit object that you can call to do various keyboard things.""" - - if os.name == "nt": - pass - - else: - - # Save the terminal settings - self.fd = sys.stdin.fileno() - self.new_term = termios.tcgetattr(self.fd) - self.old_term = termios.tcgetattr(self.fd) - - # New terminal setting unbuffered - self.new_term[3] = self.new_term[3] & ~termios.ICANON & ~termios.ECHO - termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term) - - # Support normal-terminal reset at exit - atexit.register(self.set_normal_term) - - def set_normal_term(self): - """Resets to normal terminal. On Windows this is a no-op.""" - - if os.name == "nt": - pass - - else: - termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term) - - def getch(self): - """Returns a keyboard character after kbhit() has been called. - Should not be called in the same program as getarrow(). - """ - - s = "" - - if os.name == "nt": - return msvcrt.getch().decode("utf-8") - - else: - return sys.stdin.read(1) - - def getarrow(self): - """Returns an arrow-key code after kbhit() has been called. Codes are - 0 : up - 1 : right - 2 : down - 3 : left - Should not be called in the same program as getch(). - """ - - if os.name == "nt": - msvcrt.getch() # skip 0xE0 - c = msvcrt.getch() - vals = [72, 77, 80, 75] - - else: - c = sys.stdin.read(3)[2] - vals = [65, 67, 66, 68] - - return vals.index(ord(c.decode("utf-8"))) - - def kbhit(self): - """Returns True if keyboard character was hit, False otherwise.""" - if os.name == "nt": - return msvcrt.kbhit() - - else: - dr, dw, de = select([sys.stdin], [], [], 0) - return dr != [] - - -if len(sys.argv) != 2: - print(f"Usage: {sys.argv[0]} SERIAL-PORT", file=sys.stderr) - sys.exit(os.EX_USAGE) - -SERIAL_PORT = sys.argv[1] -ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) - - -def check_log_start(buf: bytes): - return buf[:-12].find(b"LOG") - - -def check_current_start(buf: bytes): - return buf[:-12].find(b"CUR") - - -def check_panic_start(buf: bytes): - return buf[:-12].find(b"PAN") - class SlaveData: cell_voltages: list[float] @@ -139,6 +50,7 @@ class AccumulatorData: min_temp: float max_temp: float last_frame: float + time_since_last_frame: float current: float panic: bool panic_errorcode: int @@ -151,116 +63,497 @@ class AccumulatorData: ) = self.min_temp = self.max_temp = self.last_frame = self.current = 0 self.panic = False self.panic_errorcode = self.panic_errorarg = 0 + self.time_since_last_frame = 0 + + def fill_dummy_data(self): + self.min_voltage = random.uniform(1, 3) + self.max_voltage = random.uniform(3, 5) + self.min_temp = random.uniform(0, 25) + self.max_temp = random.uniform(25, 60) + self.current = random.uniform(25, 60) + self.last_frame = time.time() - random.random() + self.panic = random.choice([True, False]) + if self.panic: + self.panic_errorcode = random.randint(1, 10000) + self.panic_errorarg = "ABCDERFG" + else: + self.panic_errorcode = 0 + self.panic_errorarg = "" + data.time_since_last_frame = random.uniform(1, 60) + + for i in range(N_SLAVES): + self.slaves[i].cell_voltages = [ + random.uniform(1, 5) for i in range(CELLS_PER_SLAVE) + ] + self.slaves[i].cell_temps = [ + random.uniform(25, 60) for i in range(TEMP_SENSORS_PER_SLAVE) + ] -rx_buf = bytes() -data = AccumulatorData() +class StackGuiElement: + title: str + stack_id: int + min_voltage_label: QLabel + max_voltage_label: QLabel + min_temp_label: QLabel + max_temp_label: QLabel + groupBox: QGroupBox + detail_popup: QWidget + + def __init__(self, title: str, stack_id: int): + self.title = title + self.stack_id = stack_id + self.min_voltage_label = QLabel() + self.max_voltage_label = QLabel() + self.min_temp_label = QLabel() + self.max_temp_label = QLabel() + self.groupBox = QGroupBox() + self.detail_popup = None + self.__post__init__() + + def __post__init__(self): + l1 = QLabel("Min Voltage [V]") + l2 = QLabel("Max Voltage [V]") + l3 = QLabel("Min Temperature [°C]") + l4 = QLabel("Max Temperature [°C]") + popup_btn = QPushButton(self.title + " details") + popup_btn.clicked.connect(self.show_popup) + + l1.setAlignment(Qt.AlignLeft) + l2.setAlignment(Qt.AlignLeft) + l3.setAlignment(Qt.AlignLeft) + l4.setAlignment(Qt.AlignLeft) + + self.min_voltage_label.setAlignment(Qt.AlignLeft) + self.max_voltage_label.setAlignment(Qt.AlignLeft) + self.min_temp_label.setAlignment(Qt.AlignLeft) + self.max_temp_label.setAlignment(Qt.AlignLeft) + + grid = QGridLayout() + grid.addWidget(l1, 0, 0) + grid.addWidget(l2, 1, 0) + grid.addWidget(l3, 2, 0) + grid.addWidget(l4, 3, 0) + + grid.addWidget(self.min_voltage_label, 0, 1) + grid.addWidget(self.max_voltage_label, 1, 1) + grid.addWidget(self.min_temp_label, 2, 1) + grid.addWidget(self.max_temp_label, 3, 1) + + grid.addWidget(popup_btn, 0, 2) + + self.groupBox.setTitle(self.title) + self.groupBox.setLayout(grid) + + def update_data_from_slave(self, slave: SlaveData): + self.min_voltage_label.setNum(min(slave.cell_voltages)) + self.max_voltage_label.setNum(max(slave.cell_voltages)) + self.min_temp_label.setNum(min(slave.cell_temps)) + self.max_temp_label.setNum(max(slave.cell_temps)) + + def show_popup(self): + if self.detail_popup is None: + self.detail_popup = StackPopup(self.stack_id) + self.detail_popup.show() -def decode_log_frame(buf: bytes): - msg_id = buf[0] - slave = msg_id >> 4 - frame_id = msg_id & 0x0F - if slave >= N_SLAVES: +class QVSeperationLine(QFrame): + """ + a vertical seperation line + """ + + def __init__(self): + super().__init__() + self.setFixedWidth(20) + self.setMinimumHeight(1) + self.setFrameShape(QFrame.VLine) + self.setFrameShadow(QFrame.Sunken) + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) return - if frame_id == 0: - for i in range(7): - data.slaves[slave].cell_voltages[i] = buf[i + 1] * VOLTAGE_CONV - elif frame_id == 1: - for i in range(3): - data.slaves[slave].cell_voltages[i + 7] = buf[i + 1] * VOLTAGE_CONV - for i in range(4): - data.slaves[slave].cell_temps[i] = buf[i + 4] * TEMP_CONV - elif frame_id == 2: - for i in range(7): - data.slaves[slave].cell_temps[i + 4] = buf[i + 1] * TEMP_CONV - elif frame_id == 3: - for i in range(7): - data.slaves[slave].cell_temps[i + 11] = buf[i + 1] * TEMP_CONV - elif frame_id == 4: - for i in range(7): - data.slaves[slave].cell_temps[i + 18] = buf[i + 1] * TEMP_CONV - elif frame_id == 5: - for i in range(7): - data.slaves[slave].cell_temps[i + 25] = buf[i + 1] * TEMP_CONV - else: - # print(f"Unknown frame ID: {frame_id} (buf: {repr(buf)})", file=sys.stderr) - # time.sleep(1) - return - data.last_frame = time.time() +class StackPopup(QWidget): + stack_id: int + voltage_labels: list[QLabel] + temp_labels: list[QLabel] + + def __init__(self, stack_id: int): + super().__init__() + self.stack_id = stack_id + self.voltage_labels = [] + self.temp_labels = [] + layout = QVBoxLayout() + groupbox = QGroupBox() + + grid = QGridLayout() + + for i in range(len(data.slaves[stack_id].cell_voltages)): + l1 = QLabel(f"Voltage Cell {i}") + l1.setAlignment(Qt.AlignLeft) + + l_v = QLabel() + l_v.setNum(data.slaves[stack_id].cell_voltages[i]) + l_v.setAlignment(Qt.AlignLeft) + self.voltage_labels.append(l_v) + + grid.addWidget(l1, i, 0) + grid.addWidget(l_v, i, 1) + + # vertical separators between rows in popup + separator_vertical = QVSeperationLine() + grid.addWidget(separator_vertical, 0, 2, CELLS_PER_SLAVE, 1) + num_temp_cols = len(data.slaves[stack_id].cell_temps) // CELLS_PER_SLAVE + for i in range(num_temp_cols): + separator_vertical = QVSeperationLine() + grid.addWidget(separator_vertical, 0, 5 + i * 3, CELLS_PER_SLAVE, 1) + + for i in range(len(data.slaves[stack_id].cell_temps)): + l2 = QLabel(f"Temp. Sensor {i}") + l2.setAlignment(Qt.AlignLeft) + + l_t = QLabel() + l_t.setNum(data.slaves[stack_id].cell_temps[i]) + l_t.setAlignment(Qt.AlignLeft) + self.temp_labels.append(l_t) + + grid.addWidget(l2, i % CELLS_PER_SLAVE, 3 + (i // CELLS_PER_SLAVE) * 3) + grid.addWidget(l_t, i % CELLS_PER_SLAVE, 4 + (i // CELLS_PER_SLAVE) * 3) + + groupbox.setTitle(f"Stack {stack_id}") + groupbox.setLayout(grid) + layout.addWidget(groupbox) + self.setLayout(layout) + self.update_data() + timer.timeout.connect(self.update_data) + + def update_data(self): + for i in range(len(data.slaves[self.stack_id].cell_voltages)): + self.voltage_labels[i].setNum(data.slaves[self.stack_id].cell_voltages[i]) + for i in range(len(data.slaves[self.stack_id].cell_temps)): + self.temp_labels[i].setNum(data.slaves[self.stack_id].cell_temps[i]) -def decode_current_frame(buf: bytes): - # current = (buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | (buf[5]) - current = struct.unpack(">i", buf[2:6])[0] - data.current = current / 1000.0 +class Window(QWidget): + + stop_signal = ( + pyqtSignal() + ) # make a stop signal to communicate with the worker in another thread + + def __init__(self, parent=None): + super(Window, self).__init__(parent) + + win = QVBoxLayout() + + ### ACCUMULATOR GENERAL ### + self.l1 = QLabel("Min Voltage [V]") + self.l1.setAlignment(Qt.AlignLeft) + self.l_min_voltage = QLabel() + self.l_min_voltage.setNum(data.min_voltage) + self.l_min_voltage.setAlignment(Qt.AlignLeft) + + self.l2 = QLabel("Max Voltage [V]") + self.l2.setAlignment(Qt.AlignLeft) + self.l_max_voltage = QLabel() + self.l_max_voltage.setNum(data.max_voltage) + self.l_max_voltage.setAlignment(Qt.AlignLeft) + + self.l3 = QLabel("Min Temperature [°C]") + self.l3.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.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.l_current = QLabel() + self.l_current.setNum(data.current) + self.l_current.setAlignment(Qt.AlignLeft) + + self.l6 = QLabel("Error") + self.l_error = QLabel() + self.l_error.setText(str(data.panic)) + self.l_error.setAlignment(Qt.AlignLeft) + self.l_errorcode = QLabel() + self.l_errorcode.setText(str(data.panic_errorcode)) + self.l_errorcode.setAlignment(Qt.AlignLeft) + self.l_errorarg = QLabel() + self.l_errorarg.setText(str(data.panic_errorarg)) + self.l_errorcode.setAlignment(Qt.AlignLeft) + + self.l7 = 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) + + ### LAYOUT ACCUMULATOR GENERAL ### + 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.l_min_voltage, 0, 1) + grid_accumulator.addWidget(self.l_max_voltage, 1, 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) + + groupBox_accumulator = QGroupBox("Accumulator General") + groupBox_accumulator.setLayout(grid_accumulator) + + win.addWidget(groupBox_accumulator) + + ### STACKS ### + self.stack_gui_elements = [] + for i in range(N_SLAVES): + sge = StackGuiElement(f"Stack {i}", i) + sge.update_data_from_slave(data.slaves[i]) + self.stack_gui_elements.append(sge) + + ### LAYOUT STACKS ### + n_slaves_half = math.ceil(N_SLAVES / 2) + grid_stacks = QGridLayout() + for i, sge in enumerate(self.stack_gui_elements): + grid_stacks.addWidget( + sge.groupBox, 0 if i < n_slaves_half else 1, i % n_slaves_half + ) + + groupBox_stacks = QGroupBox("Individual Stacks") + groupBox_stacks.setLayout(grid_stacks) + + win.addWidget(groupBox_stacks) + + ### BUTTONS ### + self.btn_start = QPushButton("Start Serial") + self.btn_start.resize(self.btn_start.sizeHint()) + self.btn_start.move(50, 50) + self.btn_stop = QPushButton("Stop Serial") + self.btn_stop.resize(self.btn_stop.sizeHint()) + self.btn_stop.move(150, 50) + self.btn_charge = QPushButton("Start Charging") + self.btn_charge.resize(self.btn_stop.sizeHint()) + self.btn_charge.move(150, 50) + + ### LAYOUT BUTTONS ### + vbox_controls = QVBoxLayout() + vbox_controls.addWidget(self.btn_start) + vbox_controls.addWidget(self.btn_stop) + vbox_controls.addWidget(self.btn_charge) + groupBox_controls = QGroupBox("Controls") + groupBox_controls.setLayout(vbox_controls) + win.addWidget(groupBox_controls) + + # Start Button action + self.btn_start.clicked.connect(self.start_thread) + + # Stop Button action + self.btn_stop.clicked.connect(self.stop_thread) + + # Charge Button action + self.btn_charge.clicked.connect(self.start_charge) + + self.setLayout(win) + self.setWindowTitle("FT22 Charger Display") + + # When start_btn is clicked this runs. Creates the worker and the thread. + def start_thread(self): + self.thread = QThread() + self.worker = Worker() + self.stop_signal.connect( + self.worker.stop + ) # connect stop signal to worker stop method + self.worker.moveToThread(self.thread) + + self.worker.finished.connect( + self.thread.quit + ) # connect the workers finished signal to stop thread + self.worker.finished.connect( + self.worker.deleteLater + ) # connect the workers finished signal to clean up worker + self.thread.finished.connect( + self.thread.deleteLater + ) # connect threads finished signal to clean up thread + + self.thread.started.connect(self.worker.do_work) + self.thread.finished.connect(self.worker.stop) + self.thread.start() + + # When stop_btn is clicked this runs. Terminates the worker and the thread. + def stop_thread(self): + self.stop_signal.emit() # emit the finished signal on stop + + def start_charge(self): + ser.write(b"C") + + 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_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)) + + # Stacks + for i, sge in enumerate(self.stack_gui_elements): + sge.update_data_from_slave(data.slaves[i]) -def decode_panic_frame(buf: bytes): - data.panic = True - data.panic_errorcode = buf[0] - data.panic_errorarg = buf[1] +class Worker(QObject): + + finished = pyqtSignal() # give worker class a finished signal + + def __init__(self, parent=None): + QObject.__init__(self, parent=parent) + self.continue_run = True # provide a bool run condition for the class + + def do_work(self): + i = 1 + while self.continue_run: # give the loop a stoppable condition + # data.fill_dummy_data() + self.charger_communication() + # QThread.msleep(1) + + self.finished.emit() # emit the finished signal when the loop is done + + def charger_communication(self): + rx_data = ser.read(256) + 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]) + rx_data = rx_data[frame_start + 11 :] + continue + elif (frame_start := self.check_current_start(rx_data)) != -1: + self.decode_current_frame(rx_data[frame_start + 3 : frame_start + 11]) + rx_data = rx_data[frame_start + 11 :] + continue + elif (frame_start := self.check_panic_start(rx_data)) != -1: + self.decode_panic_frame(rx_data[frame_start + 3 : frame_start + 11]) + rx_data = rx_data[frame_start + 11 :] + continue + break + + def check_log_start(self, buf: bytes): + return buf[:-12].find(b"LOG") + + def check_current_start(self, buf: bytes): + return buf[:-12].find(b"CUR") + + def check_panic_start(self, buf: bytes): + return buf[:-12].find(b"PAN") + + def decode_log_frame(self, buf: bytes): + msg_id = buf[0] + slave = msg_id >> 4 + frame_id = msg_id & 0x0F + if slave >= N_SLAVES: + print(f"Unknown slave: {slave}", file=sys.stderr) + return + + if frame_id == 0: + for i in range(7): + data.slaves[slave].cell_voltages[i] = buf[i + 1] * VOLTAGE_CONV + elif frame_id == 1: + for i in range(3): + data.slaves[slave].cell_voltages[i + 7] = buf[i + 1] * VOLTAGE_CONV + for i in range(4): + data.slaves[slave].cell_temps[i] = buf[i + 4] * TEMP_CONV + elif frame_id == 2: + for i in range(7): + data.slaves[slave].cell_temps[i + 4] = buf[i + 1] * TEMP_CONV + elif frame_id == 3: + for i in range(7): + data.slaves[slave].cell_temps[i + 11] = buf[i + 1] * TEMP_CONV + elif frame_id == 4: + for i in range(7): + data.slaves[slave].cell_temps[i + 18] = buf[i + 1] * TEMP_CONV + elif frame_id == 5: + for i in range(7): + data.slaves[slave].cell_temps[i + 25] = buf[i + 1] * TEMP_CONV + else: + print(f"Unknown frame ID: {frame_id} (buf: {repr(buf)})", file=sys.stderr) + # time.sleep(1) + return + voltages = [ + slave.cell_voltages[i] + for i in range(CELLS_PER_SLAVE) + for slave in data.slaves + ] + self.parse_cell_temps(slave) + temps = [ + slave.cell_temps[i] + for i in range(TEMP_SENSORS_PER_SLAVE) + for slave in data.slaves + ] + data.min_voltage = min(voltages) + data.max_voltage = max(voltages) + data.min_temp = min(temps) + data.max_temp = max(temps) + data.time_since_last_frame = time.time() - data.last_frame + + data.last_frame = time.time() + + def decode_current_frame(self, buf: bytes): + # current = (buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | (buf[5]) + current = struct.unpack(">i", buf[2:6])[0] + data.current = current / 1000.0 + + def decode_panic_frame(self, buf: bytes): + data.panic = True + data.panic_errorcode = buf[0] + data.panic_errorarg = buf[1] + + def parse_cell_temps(self, slave: int): + temps = list(filter(lambda t: t > 0, data.slaves[slave].cell_temps[:16])) + if len(temps) == 0: + temps = [-1] + min_t = min(temps) + max_t = max(temps) + for i in range(16, 32): + data.slaves[slave].cell_temps[i] = random.randint(min_t, max_t) + + def stop(self): + self.continue_run = False # set the run condition to false on stop -def update_display(): - voltages = [ - slave.cell_voltages[i] for i in range(CELLS_PER_SLAVE) for slave in data.slaves - ] - temps = [ - slave.cell_temps[i] - for i in range(TEMP_SENSORS_PER_SLAVE) - for slave in data.slaves - ] - data.min_voltage = min(voltages) - data.max_voltage = max(voltages) - data.min_temp = min(temps) - data.max_temp = max(temps) - time_since_last_frame = time.time() - data.last_frame +if __name__ == "__main__": + data = AccumulatorData() + rx_buf = bytes() - print("\033[2J\033[H", end="") - print("-" * 20) - if data.panic: - print("!!!!! PANIC !!!!!") - print(f"Error code: {data.panic_errorcode}") - print(f"Error arg: {data.panic_errorarg}") - time.sleep(0.5) - return + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} SERIAL-PORT", file=sys.stderr) + sys.exit(os.EX_USAGE) - print(f"Time since last frame: {time_since_last_frame} s") - print(f"Current: {data.current:.2f} A") - print(f"Min voltage: {data.min_voltage:.2f} V") - print(f"Max voltage: {data.max_voltage:.2f} V") - print(f"Min temp: {data.min_temp:.1f} °C") - print(f"Max temp: {data.max_temp:.1f} °C") - for i in range(N_SLAVES): - min_v = min(data.slaves[i].cell_voltages) - max_v = max(data.slaves[i].cell_voltages) - min_t = min(data.slaves[i].cell_temps) - max_t = max(data.slaves[i].cell_temps) - print( - f"Stack {i}: V ∈ [{min_v:.2f}, {max_v:.2f}]\tT ∈ [{min_t:.1f}, {max_t:.1f}]" - ) + SERIAL_PORT = sys.argv[1] + print(SERIAL_PORT) + try: + ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) + except serial.serialutil.SerialException: + pass + app = QApplication(sys.argv) + gui = Window() + gui.show() -kb = KBHit() -while True: - rx_data = ser.read(32) - if len(rx_data) > 0: - rx_buf = rx_data - if (start := check_log_start(rx_buf)) != -1: - decode_log_frame(rx_buf[start + 3 : start + 11]) - rx_buf = b"" - elif (start := check_current_start(rx_buf)) != -1: - decode_current_frame(rx_buf[start + 3 : start + 11]) - rx_buf = b"" - elif (start := check_panic_start(rx_buf)) != -1: - decode_panic_frame(rx_buf[start + 3 : start + 11]) - rx_buf = b"" - if kb.kbhit(): - c = kb.getch() - if c == "C": - print(f"KBHIT: {c}", file=sys.stderr) - print(ser.write(b"C"), file=sys.stderr) - update_display() + timer = QTimer() + # timer.timeout.connect(data.fill_dummy_data) + timer.timeout.connect(gui.update) + timer.start(1000) # every 1,000 milliseconds + + sys.exit(app.exec_()) diff --git a/test.py b/test.py deleted file mode 100755 index 8cc31c7..0000000 --- a/test.py +++ /dev/null @@ -1,559 +0,0 @@ -#!/usr/bin/env python3 - -import math -import os -import struct -import time -import random -import serial -import sys - -from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal -from PyQt5.QtWidgets import ( - QApplication, - QWidget, - QPushButton, - QVBoxLayout, - QHBoxLayout, - QGridLayout, - QLabel, - QGroupBox, - QFrame, - QSizePolicy, -) - - -BITRATE = 115200 # baud/s -TIMEOUT = 1 # seconds -N_SLAVES = 7 -LOG_FRAME_LENGTH = 8 # bytes - -CELLS_PER_SLAVE = 10 -TEMP_SENSORS_PER_SLAVE = 32 -VOLTAGE_CONV = 5.0 / 255 # volts/quantum -TEMP_CONV = 0.0625 * 16 # °C/quantum - - -class SlaveData: - cell_voltages: list[float] - cell_temps: list[float] - - def __init__(self) -> None: - self.cell_voltages = [-1] * CELLS_PER_SLAVE - self.cell_temps = [-1] * TEMP_SENSORS_PER_SLAVE - - -class AccumulatorData: - slaves: list[SlaveData] - min_voltage: float - max_voltage: float - min_temp: float - max_temp: float - last_frame: float - time_since_last_frame: float - current: float - panic: bool - panic_errorcode: int - panic_errorarg: int - - def __init__(self) -> None: - self.slaves = [SlaveData() for _ in range(N_SLAVES)] - self.min_voltage = ( - self.max_voltage - ) = self.min_temp = self.max_temp = self.last_frame = self.current = 0 - self.panic = False - self.panic_errorcode = self.panic_errorarg = 0 - self.time_since_last_frame = 0 - - def fill_dummy_data(self): - self.min_voltage = random.uniform(1, 3) - self.max_voltage = random.uniform(3, 5) - self.min_temp = random.uniform(0, 25) - self.max_temp = random.uniform(25, 60) - self.current = random.uniform(25, 60) - self.last_frame = time.time() - random.random() - self.panic = random.choice([True, False]) - if self.panic: - self.panic_errorcode = random.randint(1, 10000) - self.panic_errorarg = "ABCDERFG" - else: - self.panic_errorcode = 0 - self.panic_errorarg = "" - data.time_since_last_frame = random.uniform(1, 60) - - for i in range(N_SLAVES): - self.slaves[i].cell_voltages = [ - random.uniform(1, 5) for i in range(CELLS_PER_SLAVE) - ] - self.slaves[i].cell_temps = [ - random.uniform(25, 60) for i in range(TEMP_SENSORS_PER_SLAVE) - ] - - -class StackGuiElement: - title: str - stack_id: int - min_voltage_label: QLabel - max_voltage_label: QLabel - min_temp_label: QLabel - max_temp_label: QLabel - groupBox: QGroupBox - detail_popup: QWidget - - def __init__(self, title: str, stack_id: int): - self.title = title - self.stack_id = stack_id - self.min_voltage_label = QLabel() - self.max_voltage_label = QLabel() - self.min_temp_label = QLabel() - self.max_temp_label = QLabel() - self.groupBox = QGroupBox() - self.detail_popup = None - self.__post__init__() - - def __post__init__(self): - l1 = QLabel("Min Voltage [V]") - l2 = QLabel("Max Voltage [V]") - l3 = QLabel("Min Temperature [°C]") - l4 = QLabel("Max Temperature [°C]") - popup_btn = QPushButton(self.title + " details") - popup_btn.clicked.connect(self.show_popup) - - l1.setAlignment(Qt.AlignLeft) - l2.setAlignment(Qt.AlignLeft) - l3.setAlignment(Qt.AlignLeft) - l4.setAlignment(Qt.AlignLeft) - - self.min_voltage_label.setAlignment(Qt.AlignLeft) - self.max_voltage_label.setAlignment(Qt.AlignLeft) - self.min_temp_label.setAlignment(Qt.AlignLeft) - self.max_temp_label.setAlignment(Qt.AlignLeft) - - grid = QGridLayout() - grid.addWidget(l1, 0, 0) - grid.addWidget(l2, 1, 0) - grid.addWidget(l3, 2, 0) - grid.addWidget(l4, 3, 0) - - grid.addWidget(self.min_voltage_label, 0, 1) - grid.addWidget(self.max_voltage_label, 1, 1) - grid.addWidget(self.min_temp_label, 2, 1) - grid.addWidget(self.max_temp_label, 3, 1) - - grid.addWidget(popup_btn, 0, 2) - - self.groupBox.setTitle(self.title) - self.groupBox.setLayout(grid) - - def update_data_from_slave(self, slave: SlaveData): - self.min_voltage_label.setNum(min(slave.cell_voltages)) - self.max_voltage_label.setNum(max(slave.cell_voltages)) - self.min_temp_label.setNum(min(slave.cell_temps)) - self.max_temp_label.setNum(max(slave.cell_temps)) - - def show_popup(self): - if self.detail_popup is None: - self.detail_popup = StackPopup(self.stack_id) - self.detail_popup.show() - - -class QVSeperationLine(QFrame): - """ - a vertical seperation line - """ - - def __init__(self): - super().__init__() - self.setFixedWidth(20) - self.setMinimumHeight(1) - self.setFrameShape(QFrame.VLine) - self.setFrameShadow(QFrame.Sunken) - self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - return - - -class StackPopup(QWidget): - stack_id: int - voltage_labels: list[QLabel] - temp_labels: list[QLabel] - - def __init__(self, stack_id: int): - super().__init__() - self.stack_id = stack_id - self.voltage_labels = [] - self.temp_labels = [] - layout = QVBoxLayout() - groupbox = QGroupBox() - - grid = QGridLayout() - - for i in range(len(data.slaves[stack_id].cell_voltages)): - l1 = QLabel(f"Voltage Cell {i}") - l1.setAlignment(Qt.AlignLeft) - - l_v = QLabel() - l_v.setNum(data.slaves[stack_id].cell_voltages[i]) - l_v.setAlignment(Qt.AlignLeft) - self.voltage_labels.append(l_v) - - grid.addWidget(l1, i, 0) - grid.addWidget(l_v, i, 1) - - # vertical separators between rows in popup - separator_vertical = QVSeperationLine() - grid.addWidget(separator_vertical, 0, 2, CELLS_PER_SLAVE, 1) - num_temp_cols = len(data.slaves[stack_id].cell_temps) // CELLS_PER_SLAVE - for i in range(num_temp_cols): - separator_vertical = QVSeperationLine() - grid.addWidget(separator_vertical, 0, 5 + i * 3, CELLS_PER_SLAVE, 1) - - for i in range(len(data.slaves[stack_id].cell_temps)): - l2 = QLabel(f"Temp. Sensor {i}") - l2.setAlignment(Qt.AlignLeft) - - l_t = QLabel() - l_t.setNum(data.slaves[stack_id].cell_temps[i]) - l_t.setAlignment(Qt.AlignLeft) - self.temp_labels.append(l_t) - - grid.addWidget(l2, i % CELLS_PER_SLAVE, 3 + (i // CELLS_PER_SLAVE) * 3) - grid.addWidget(l_t, i % CELLS_PER_SLAVE, 4 + (i // CELLS_PER_SLAVE) * 3) - - groupbox.setTitle(f"Stack {stack_id}") - groupbox.setLayout(grid) - layout.addWidget(groupbox) - self.setLayout(layout) - self.update_data() - timer.timeout.connect(self.update_data) - - def update_data(self): - for i in range(len(data.slaves[self.stack_id].cell_voltages)): - self.voltage_labels[i].setNum(data.slaves[self.stack_id].cell_voltages[i]) - for i in range(len(data.slaves[self.stack_id].cell_temps)): - self.temp_labels[i].setNum(data.slaves[self.stack_id].cell_temps[i]) - - -class Window(QWidget): - - stop_signal = ( - pyqtSignal() - ) # make a stop signal to communicate with the worker in another thread - - def __init__(self, parent=None): - super(Window, self).__init__(parent) - - win = QVBoxLayout() - - ### ACCUMULATOR GENERAL ### - self.l1 = QLabel("Min Voltage [V]") - self.l1.setAlignment(Qt.AlignLeft) - self.l_min_voltage = QLabel() - self.l_min_voltage.setNum(data.min_voltage) - self.l_min_voltage.setAlignment(Qt.AlignLeft) - - self.l2 = QLabel("Max Voltage [V]") - self.l2.setAlignment(Qt.AlignLeft) - self.l_max_voltage = QLabel() - self.l_max_voltage.setNum(data.max_voltage) - self.l_max_voltage.setAlignment(Qt.AlignLeft) - - self.l3 = QLabel("Min Temperature [°C]") - self.l3.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.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.l_current = QLabel() - self.l_current.setNum(data.current) - self.l_current.setAlignment(Qt.AlignLeft) - - self.l6 = QLabel("Error") - self.l_error = QLabel() - self.l_error.setText(str(data.panic)) - self.l_error.setAlignment(Qt.AlignLeft) - self.l_errorcode = QLabel() - self.l_errorcode.setText(str(data.panic_errorcode)) - self.l_errorcode.setAlignment(Qt.AlignLeft) - self.l_errorarg = QLabel() - self.l_errorarg.setText(str(data.panic_errorarg)) - self.l_errorcode.setAlignment(Qt.AlignLeft) - - self.l7 = 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) - - ### LAYOUT ACCUMULATOR GENERAL ### - 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.l_min_voltage, 0, 1) - grid_accumulator.addWidget(self.l_max_voltage, 1, 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) - - groupBox_accumulator = QGroupBox("Accumulator General") - groupBox_accumulator.setLayout(grid_accumulator) - - win.addWidget(groupBox_accumulator) - - ### STACKS ### - self.stack_gui_elements = [] - for i in range(N_SLAVES): - sge = StackGuiElement(f"Stack {i}", i) - sge.update_data_from_slave(data.slaves[i]) - self.stack_gui_elements.append(sge) - - ### LAYOUT STACKS ### - n_slaves_half = math.ceil(N_SLAVES / 2) - grid_stacks = QGridLayout() - for i, sge in enumerate(self.stack_gui_elements): - grid_stacks.addWidget( - sge.groupBox, 0 if i < n_slaves_half else 1, i % n_slaves_half - ) - - groupBox_stacks = QGroupBox("Individual Stacks") - groupBox_stacks.setLayout(grid_stacks) - - win.addWidget(groupBox_stacks) - - ### BUTTONS ### - self.btn_start = QPushButton("Start Serial") - self.btn_start.resize(self.btn_start.sizeHint()) - self.btn_start.move(50, 50) - self.btn_stop = QPushButton("Stop Serial") - self.btn_stop.resize(self.btn_stop.sizeHint()) - self.btn_stop.move(150, 50) - self.btn_charge = QPushButton("Start Charging") - self.btn_charge.resize(self.btn_stop.sizeHint()) - self.btn_charge.move(150, 50) - - ### LAYOUT BUTTONS ### - vbox_controls = QVBoxLayout() - vbox_controls.addWidget(self.btn_start) - vbox_controls.addWidget(self.btn_stop) - vbox_controls.addWidget(self.btn_charge) - groupBox_controls = QGroupBox("Controls") - groupBox_controls.setLayout(vbox_controls) - win.addWidget(groupBox_controls) - - # Start Button action - self.btn_start.clicked.connect(self.start_thread) - - # Stop Button action - self.btn_stop.clicked.connect(self.stop_thread) - - # Charge Button action - self.btn_charge.clicked.connect(self.start_charge) - - self.setLayout(win) - self.setWindowTitle("FT22 Charger Display") - - # When start_btn is clicked this runs. Creates the worker and the thread. - def start_thread(self): - self.thread = QThread() - self.worker = Worker() - self.stop_signal.connect( - self.worker.stop - ) # connect stop signal to worker stop method - self.worker.moveToThread(self.thread) - - self.worker.finished.connect( - self.thread.quit - ) # connect the workers finished signal to stop thread - self.worker.finished.connect( - self.worker.deleteLater - ) # connect the workers finished signal to clean up worker - self.thread.finished.connect( - self.thread.deleteLater - ) # connect threads finished signal to clean up thread - - self.thread.started.connect(self.worker.do_work) - self.thread.finished.connect(self.worker.stop) - self.thread.start() - - # When stop_btn is clicked this runs. Terminates the worker and the thread. - def stop_thread(self): - self.stop_signal.emit() # emit the finished signal on stop - - def start_charge(self): - ser.write(b"C") - - 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_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)) - - # Stacks - for i, sge in enumerate(self.stack_gui_elements): - sge.update_data_from_slave(data.slaves[i]) - - -class Worker(QObject): - - finished = pyqtSignal() # give worker class a finished signal - - def __init__(self, parent=None): - QObject.__init__(self, parent=parent) - self.continue_run = True # provide a bool run condition for the class - - def do_work(self): - i = 1 - while self.continue_run: # give the loop a stoppable condition - # data.fill_dummy_data() - self.charger_communication() - # QThread.msleep(1) - - self.finished.emit() # emit the finished signal when the loop is done - - def charger_communication(self): - rx_data = ser.read(256) - 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]) - rx_data = rx_data[frame_start + 11 :] - continue - elif (frame_start := self.check_current_start(rx_data)) != -1: - self.decode_current_frame(rx_data[frame_start + 3 : frame_start + 11]) - rx_data = rx_data[frame_start + 11 :] - continue - elif (frame_start := self.check_panic_start(rx_data)) != -1: - self.decode_panic_frame(rx_data[frame_start + 3 : frame_start + 11]) - rx_data = rx_data[frame_start + 11 :] - continue - break - - def check_log_start(self, buf: bytes): - return buf[:-12].find(b"LOG") - - def check_current_start(self, buf: bytes): - return buf[:-12].find(b"CUR") - - def check_panic_start(self, buf: bytes): - return buf[:-12].find(b"PAN") - - def decode_log_frame(self, buf: bytes): - msg_id = buf[0] - slave = msg_id >> 4 - frame_id = msg_id & 0x0F - if slave >= N_SLAVES: - print(f"Unknown slave: {slave}", file=sys.stderr) - return - - if frame_id == 0: - for i in range(7): - data.slaves[slave].cell_voltages[i] = buf[i + 1] * VOLTAGE_CONV - elif frame_id == 1: - for i in range(3): - data.slaves[slave].cell_voltages[i + 7] = buf[i + 1] * VOLTAGE_CONV - for i in range(4): - data.slaves[slave].cell_temps[i] = buf[i + 4] * TEMP_CONV - elif frame_id == 2: - for i in range(7): - data.slaves[slave].cell_temps[i + 4] = buf[i + 1] * TEMP_CONV - elif frame_id == 3: - for i in range(7): - data.slaves[slave].cell_temps[i + 11] = buf[i + 1] * TEMP_CONV - elif frame_id == 4: - for i in range(7): - data.slaves[slave].cell_temps[i + 18] = buf[i + 1] * TEMP_CONV - elif frame_id == 5: - for i in range(7): - data.slaves[slave].cell_temps[i + 25] = buf[i + 1] * TEMP_CONV - else: - print(f"Unknown frame ID: {frame_id} (buf: {repr(buf)})", file=sys.stderr) - # time.sleep(1) - return - voltages = [ - slave.cell_voltages[i] - for i in range(CELLS_PER_SLAVE) - for slave in data.slaves - ] - self.parse_cell_temps(slave) - temps = [ - slave.cell_temps[i] - for i in range(TEMP_SENSORS_PER_SLAVE) - for slave in data.slaves - ] - data.min_voltage = min(voltages) - data.max_voltage = max(voltages) - data.min_temp = min(temps) - data.max_temp = max(temps) - data.time_since_last_frame = time.time() - data.last_frame - - data.last_frame = time.time() - - def decode_current_frame(self, buf: bytes): - # current = (buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | (buf[5]) - current = struct.unpack(">i", buf[2:6])[0] - data.current = current / 1000.0 - - def decode_panic_frame(self, buf: bytes): - data.panic = True - data.panic_errorcode = buf[0] - data.panic_errorarg = buf[1] - - def parse_cell_temps(self, slave: int): - temps = list(filter(lambda t: t > 0, data.slaves[slave].cell_temps[:16])) - if len(temps) == 0: - temps = [-1] - min_t = min(temps) - max_t = max(temps) - for i in range(16, 32): - data.slaves[slave].cell_temps[i] = random.randint(min_t, max_t) - - def stop(self): - self.continue_run = False # set the run condition to false on stop - - -if __name__ == "__main__": - data = AccumulatorData() - rx_buf = bytes() - - if len(sys.argv) != 2: - print(f"Usage: {sys.argv[0]} SERIAL-PORT", file=sys.stderr) - sys.exit(os.EX_USAGE) - - SERIAL_PORT = sys.argv[1] - print(SERIAL_PORT) - try: - ser = serial.Serial(SERIAL_PORT, BITRATE, timeout=TIMEOUT) - except serial.serialutil.SerialException: - pass - - app = QApplication(sys.argv) - gui = Window() - gui.show() - - timer = QTimer() - # timer.timeout.connect(data.fill_dummy_data) - timer.timeout.connect(gui.update) - timer.start(1000) # every 1,000 milliseconds - - sys.exit(app.exec_()) From 61f652ee8feac1a260d53efdb3ba20628f2ead76 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Mon, 8 Aug 2022 01:23:20 +0200 Subject: [PATCH 11/15] Errorcodes, SoC, 9 slaves --- charger-display.py | 167 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 136 insertions(+), 31 deletions(-) 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() From a3f3b2c23e62bdfa73a205faf091ca37e308b2df Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 10 Aug 2022 13:14:43 +0200 Subject: [PATCH 12/15] Parse new logging format --- charger-display.py | 70 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/charger-display.py b/charger-display.py index 7ae802a..1350114 100755 --- a/charger-display.py +++ b/charger-display.py @@ -33,8 +33,8 @@ 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 +VOLTAGE_CONV = 5.0 / 0xFFFF # volts/quantum +TEMP_CONV = 0.0625 # °C/quantum ERRORCODE_TIMEOUT_SLAVE = 1 ERRORCODE_SLAVE_PANIC = 2 @@ -53,6 +53,8 @@ MASTER_THRESH_OT = 1 MASTER_THRESH_UV = 2 MASTER_THRESH_OV = 3 +TS_ERROR = 4 + class SlaveData: cell_voltages: list[float] @@ -60,7 +62,9 @@ class SlaveData: def __init__(self) -> None: self.cell_voltages = [-1] * CELLS_PER_SLAVE - self.cell_temps = [-1] * TEMP_SENSORS_PER_SLAVE + self.cell_temps = ([-1] * (TEMP_SENSORS_PER_SLAVE // 2)) + ( + [0] * (TEMP_SENSORS_PER_SLAVE // 2) + ) class AccumulatorData: @@ -492,6 +496,10 @@ class Worker(QObject): self.decode_panic_frame(rx_data[frame_start + 3 : frame_start + 11]) rx_data = rx_data[frame_start + 11 :] continue + elif (frame_start := self.check_status_start(rx_data)) != -1: + self.decode_status_frame(rx_data[frame_start + 3 : frame_start + 11]) + rx_data = rx_data[frame_start + 11 :] + continue break def check_log_start(self, buf: bytes): @@ -503,6 +511,9 @@ class Worker(QObject): def check_panic_start(self, buf: bytes): return buf[:-12].find(b"PAN") + def check_status_start(self, buf: bytes): + return buf[:-12].find(b"STA") + def decode_log_frame(self, buf: bytes): msg_id = buf[0] slave = msg_id >> 4 @@ -512,25 +523,44 @@ class Worker(QObject): return if frame_id == 0: - for i in range(7): - data.slaves[slave].cell_voltages[i] = buf[i + 1] * VOLTAGE_CONV + for i in range(3): + raw = (buf[i * 2 + 1] << 8) | buf[i * 2 + 2] + data.slaves[slave].cell_voltages[i] = raw * VOLTAGE_CONV elif frame_id == 1: for i in range(3): - data.slaves[slave].cell_voltages[i + 7] = buf[i + 1] * VOLTAGE_CONV - for i in range(4): - data.slaves[slave].cell_temps[i] = buf[i + 4] * TEMP_CONV + raw = (buf[i * 2 + 1] << 8) | buf[i * 2 + 2] + data.slaves[slave].cell_voltages[i + 3] = raw * VOLTAGE_CONV elif frame_id == 2: - for i in range(7): - data.slaves[slave].cell_temps[i + 4] = buf[i + 1] * TEMP_CONV + for i in range(3): + raw = (buf[i * 2 + 1] << 8) | buf[i * 2 + 2] + data.slaves[slave].cell_voltages[i + 6] = raw * VOLTAGE_CONV elif frame_id == 3: - for i in range(7): - data.slaves[slave].cell_temps[i + 11] = buf[i + 1] * TEMP_CONV + for i in range(1): + raw = (buf[i * 2 + 1] << 8) | buf[i * 2 + 2] + data.slaves[slave].cell_voltages[i + 9] = raw * VOLTAGE_CONV + for i in range(2): + raw = (buf[i * 2 + 3] << 8) | buf[i * 2 + 4] + data.slaves[slave].cell_temps[i] = raw * TEMP_CONV elif frame_id == 4: - for i in range(7): - data.slaves[slave].cell_temps[i + 18] = buf[i + 1] * TEMP_CONV + for i in range(3): + raw = (buf[i * 2 + 1] << 8) | buf[i * 2 + 2] + data.slaves[slave].cell_temps[i + 2] = raw * TEMP_CONV elif frame_id == 5: - for i in range(7): - data.slaves[slave].cell_temps[i + 25] = buf[i + 1] * TEMP_CONV + for i in range(3): + raw = (buf[i * 2 + 1] << 8) | buf[i * 2 + 2] + data.slaves[slave].cell_temps[i + 5] = raw * TEMP_CONV + elif frame_id == 6: + for i in range(3): + raw = (buf[i * 2 + 1] << 8) | buf[i * 2 + 2] + data.slaves[slave].cell_temps[i + 8] = raw * TEMP_CONV + elif frame_id == 7: + for i in range(3): + raw = (buf[i * 2 + 1] << 8) | buf[i * 2 + 2] + data.slaves[slave].cell_temps[i + 11] = raw * TEMP_CONV + elif frame_id == 8: + for i in range(3): + raw = (buf[i * 2 + 1] << 8) | buf[i * 2 + 2] + data.slaves[slave].cell_temps[i + 14] = raw * TEMP_CONV else: print(f"Unknown frame ID: {frame_id} (buf: {repr(buf)})", file=sys.stderr) # time.sleep(1) @@ -609,6 +639,10 @@ class Worker(QObject): data.panic_errorcode = buf[0] data.panic_errorarg = buf[1] + def decode_status_frame(self, buf: bytes): + ts_state = buf[1] & 0x7F + data.panic = ts_state == TS_ERROR + 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] @@ -633,8 +667,8 @@ class Worker(QObject): temps = [-1] min_t = min(temps) max_t = max(temps) - for i in range(16, 32): - data.slaves[slave].cell_temps[i] = random.randint(min_t, max_t) + # for i in range(16, 32): + # data.slaves[slave].cell_temps[i] = random.randint(min_t, max_t) def stop(self): self.continue_run = False # set the run condition to false on stop From 18d9517fcc863b94eef716e79e53cd7276f1ab4a Mon Sep 17 00:00:00 2001 From: jazzpi Date: Fri, 12 Aug 2022 01:14:14 +0200 Subject: [PATCH 13/15] More formatting, balancing --- charger-display.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/charger-display.py b/charger-display.py index 1350114..f85f84a 100755 --- a/charger-display.py +++ b/charger-display.py @@ -146,7 +146,7 @@ class StackGuiElement: l2 = QLabel("Max Voltage [V]") l3 = QLabel("Min Temperature [°C]") l4 = QLabel("Max Temperature [°C]") - popup_btn = QPushButton(self.title + " details") + popup_btn = QPushButton("Details") popup_btn.clicked.connect(self.show_popup) l1.setAlignment(Qt.AlignLeft) @@ -176,10 +176,10 @@ class StackGuiElement: self.groupBox.setLayout(grid) def update_data_from_slave(self, slave: SlaveData): - self.min_voltage_label.setNum(min(slave.cell_voltages)) - self.max_voltage_label.setNum(max(slave.cell_voltages)) - self.min_temp_label.setNum(min(slave.cell_temps)) - self.max_temp_label.setNum(max(slave.cell_temps)) + self.min_voltage_label.setText(f"{min(slave.cell_voltages):.02f}") + self.max_voltage_label.setText(f"{max(slave.cell_voltages):.02f}") + self.min_temp_label.setText(f"{min(slave.cell_temps):.02f}") + self.max_temp_label.setText(f"{max(slave.cell_temps):.02f}") def show_popup(self): if self.detail_popup is None: @@ -390,14 +390,18 @@ class Window(QWidget): self.btn_stop.resize(self.btn_stop.sizeHint()) self.btn_stop.move(150, 50) self.btn_charge = QPushButton("Start Charging") - self.btn_charge.resize(self.btn_stop.sizeHint()) - self.btn_charge.move(150, 50) + self.btn_charge.resize(self.btn_charge.sizeHint()) + self.btn_charge.move(250, 50) + self.btn_balance = QPushButton("Start Balancing") + self.btn_balance.resize(self.btn_balance.sizeHint()) + self.btn_balance.move(350, 50) ### LAYOUT BUTTONS ### vbox_controls = QVBoxLayout() vbox_controls.addWidget(self.btn_start) vbox_controls.addWidget(self.btn_stop) vbox_controls.addWidget(self.btn_charge) + vbox_controls.addWidget(self.btn_balance) groupBox_controls = QGroupBox("Controls") groupBox_controls.setLayout(vbox_controls) win.addWidget(groupBox_controls) @@ -411,6 +415,9 @@ class Window(QWidget): # Charge Button action self.btn_charge.clicked.connect(self.start_charge) + # Balance Button action + self.btn_balance.clicked.connect(self.start_balance) + self.setLayout(win) self.setWindowTitle("FT22 Charger Display") @@ -444,19 +451,23 @@ class Window(QWidget): def start_charge(self): ser.write(b"C") + def start_balance(self): + ser.write(b"B") + def update(self): # Accumulator 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_min_temp.setText(f"{data.min_temp:.02f}") + self.l_max_temp.setText(f"{data.max_temp:.02f}") + self.l_current.setText(f"{data.current:.02f}") 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(time.time() - data.last_frame)) + last_time = time.time() - data.last_frame + self.l_time_since_last_frame.setText(f"{last_time:.03f}") # Stacks for i, sge in enumerate(self.stack_gui_elements): From 857ca68d832451a129145aa3fc1e1f946c8d4175 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Fri, 12 Aug 2022 01:25:58 +0200 Subject: [PATCH 14/15] Display TS state --- charger-display.py | 52 +++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/charger-display.py b/charger-display.py index f85f84a..ed2691a 100755 --- a/charger-display.py +++ b/charger-display.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +from enum import Enum import math import os import struct @@ -53,8 +54,6 @@ MASTER_THRESH_OT = 1 MASTER_THRESH_UV = 2 MASTER_THRESH_OV = 3 -TS_ERROR = 4 - class SlaveData: cell_voltages: list[float] @@ -67,6 +66,17 @@ class SlaveData: ) +class TSState(Enum): + TS_INACTIVE = 0 + TS_ACTIVE = 1 + TS_PRECHARGE = 2 + TS_DISCHARGE = 3 + TS_ERROR = 4 + TS_CHARGING_CHECK = 5 + TS_CHARGING = 6 + TS_BALANCING = 7 + + class AccumulatorData: slaves: list[SlaveData] min_voltage: float @@ -81,6 +91,7 @@ class AccumulatorData: panic: bool panic_errorcode: int panic_errorarg: int + ts_state: TSState def __init__(self) -> None: self.slaves = [SlaveData() for _ in range(N_SLAVES)] @@ -94,6 +105,7 @@ class AccumulatorData: self.panic = False self.panic_errorcode = self.panic_errorarg = 0 self.time_since_last_frame = 0 + self.ts_state = TSState.TS_INACTIVE def fill_dummy_data(self): self.min_voltage = random.uniform(1, 3) @@ -317,15 +329,15 @@ class Window(QWidget): self.l_current.setNum(data.current) self.l_current.setAlignment(Qt.AlignLeft) - self.l8 = QLabel("Error") - self.l_error = QLabel() - self.l_error.setText(str(data.panic)) - self.l_error.setAlignment(Qt.AlignLeft) + self.l8 = QLabel("State") + self.l_state = QLabel() + self.l_state.setText(data.ts_state.name) + self.l_state.setAlignment(Qt.AlignLeft) self.l_errorcode = QLabel() - self.l_errorcode.setText(str(data.panic_errorcode)) + self.l_errorcode.setText("") self.l_errorcode.setAlignment(Qt.AlignLeft) self.l_errorarg = QLabel() - self.l_errorarg.setText(str(data.panic_errorarg)) + self.l_errorarg.setText("") self.l_errorcode.setAlignment(Qt.AlignLeft) self.l9 = QLabel("Time Since Last Dataframe") @@ -352,7 +364,7 @@ class Window(QWidget): 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, 3) - grid_accumulator.addWidget(self.l_error, 5, 1) + grid_accumulator.addWidget(self.l_state, 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) @@ -463,9 +475,20 @@ class Window(QWidget): self.l_min_temp.setText(f"{data.min_temp:.02f}") self.l_max_temp.setText(f"{data.max_temp:.02f}") self.l_current.setText(f"{data.current:.02f}") - 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_state.setText(data.ts_state.name) + if data.panic: + self.l_errorcode.setText(str(data.panic_errorcode)) + self.l_errorarg.setText(str(data.panic_errorarg)) + for l in (self.l_state, self.l_errorcode, self.l_errorarg): + l.setStyleSheet("color: red; font-weight: bold;") + elif data.ts_state == TSState.TS_CHARGING: + for l in (self.l_state, self.l_errorcode, self.l_errorarg): + l.setStyleSheet("color: green; font-weight: bold;") + else: + self.l_errorcode.setText("") + self.l_errorarg.setText("") + for l in (self.l_state, self.l_errorcode, self.l_errorarg): + l.setStyleSheet("color: black; font-weight: normal;") last_time = time.time() - data.last_frame self.l_time_since_last_frame.setText(f"{last_time:.03f}") @@ -651,8 +674,9 @@ class Worker(QObject): data.panic_errorarg = buf[1] def decode_status_frame(self, buf: bytes): - ts_state = buf[1] & 0x7F - data.panic = ts_state == TS_ERROR + state = buf[0] & 0x7F + data.ts_state = TSState(state) + data.panic = data.ts_state == TSState.TS_ERROR INTERNAL_RESISTANCE_CURVE_X = [2.0, 4.12] INTERNAL_RESISTANCE_CURVE_Y = [0.0528, 0.0294] From 08b02a5a5d42286e5c2f439508b1acd7f59f8e9a Mon Sep 17 00:00:00 2001 From: jazzpi Date: Fri, 12 Aug 2022 13:01:44 +0200 Subject: [PATCH 15/15] SoC for stacks --- charger-display.py | 90 ++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/charger-display.py b/charger-display.py index ed2691a..806d6af 100755 --- a/charger-display.py +++ b/charger-display.py @@ -54,6 +54,22 @@ MASTER_THRESH_OT = 1 MASTER_THRESH_UV = 2 MASTER_THRESH_OV = 3 +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 estimate_soc(voltage: float) -> float: + r_i = np.interp( + [voltage], + INTERNAL_RESISTANCE_CURVE_X, + INTERNAL_RESISTANCE_CURVE_Y, + )[0] + i = data.current / PARALLEL_CELLS + ocv = voltage - i * r_i + return np.interp([ocv], SOC_OCV_X, SOC_OCV_Y)[0] * 100 + class SlaveData: cell_voltages: list[float] @@ -135,63 +151,58 @@ class AccumulatorData: class StackGuiElement: title: str stack_id: int - min_voltage_label: QLabel - max_voltage_label: QLabel - min_temp_label: QLabel - max_temp_label: QLabel + voltage_label: QLabel + soc_label: QLabel + temp_label: QLabel groupBox: QGroupBox detail_popup: QWidget def __init__(self, title: str, stack_id: int): self.title = title self.stack_id = stack_id - self.min_voltage_label = QLabel() - self.max_voltage_label = QLabel() - self.min_temp_label = QLabel() - self.max_temp_label = QLabel() + self.voltage_label = QLabel() + self.soc_label = QLabel() + self.temp_label = QLabel() self.groupBox = QGroupBox() self.detail_popup = None self.__post__init__() def __post__init__(self): - l1 = QLabel("Min Voltage [V]") - l2 = QLabel("Max Voltage [V]") - l3 = QLabel("Min Temperature [°C]") - l4 = QLabel("Max Temperature [°C]") + l1 = QLabel("Voltage [V]") + l2 = QLabel("SoC [%]") + l3 = QLabel("Temperature [°C]") popup_btn = QPushButton("Details") popup_btn.clicked.connect(self.show_popup) l1.setAlignment(Qt.AlignLeft) l2.setAlignment(Qt.AlignLeft) l3.setAlignment(Qt.AlignLeft) - l4.setAlignment(Qt.AlignLeft) - self.min_voltage_label.setAlignment(Qt.AlignLeft) - self.max_voltage_label.setAlignment(Qt.AlignLeft) - self.min_temp_label.setAlignment(Qt.AlignLeft) - self.max_temp_label.setAlignment(Qt.AlignLeft) + self.voltage_label.setAlignment(Qt.AlignLeft) + self.soc_label.setAlignment(Qt.AlignLeft) + self.temp_label.setAlignment(Qt.AlignLeft) grid = QGridLayout() grid.addWidget(l1, 0, 0) grid.addWidget(l2, 1, 0) grid.addWidget(l3, 2, 0) - grid.addWidget(l4, 3, 0) - grid.addWidget(self.min_voltage_label, 0, 1) - grid.addWidget(self.max_voltage_label, 1, 1) - grid.addWidget(self.min_temp_label, 2, 1) - grid.addWidget(self.max_temp_label, 3, 1) + grid.addWidget(self.voltage_label, 0, 1) + grid.addWidget(self.soc_label, 1, 1) + grid.addWidget(self.temp_label, 2, 1) - grid.addWidget(popup_btn, 0, 2) + grid.addWidget(popup_btn, 3, 0) self.groupBox.setTitle(self.title) self.groupBox.setLayout(grid) def update_data_from_slave(self, slave: SlaveData): - self.min_voltage_label.setText(f"{min(slave.cell_voltages):.02f}") - self.max_voltage_label.setText(f"{max(slave.cell_voltages):.02f}") - self.min_temp_label.setText(f"{min(slave.cell_temps):.02f}") - self.max_temp_label.setText(f"{max(slave.cell_temps):.02f}") + min_v, max_v = min(slave.cell_voltages), max(slave.cell_voltages) + self.voltage_label.setText(f"[{min_v:.02f}, {max_v:.02f}]") + min_soc, max_soc = estimate_soc(min_v), estimate_soc(max_v) + self.soc_label.setText(f"[{min_soc:02.0f}, {max_soc:02.0f}]") + min_t, max_t = min(slave.cell_temps), max(slave.cell_temps) + self.temp_label.setText(f"[{min_t:.02f}, {max_t:.02f}]") def show_popup(self): if self.detail_popup is None: @@ -265,6 +276,7 @@ class StackPopup(QWidget): groupbox.setLayout(grid) layout.addWidget(groupbox) self.setLayout(layout) + self.setWindowTitle(f"FT22 Charger Display - Stack {stack_id} Details") self.update_data() timer.timeout.connect(self.update_data) @@ -470,8 +482,8 @@ class Window(QWidget): # Accumulator 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_soc.setText(f"{data.min_soc:02.0f}") + self.l_max_soc.setText(f"{data.max_soc:02.0f}") self.l_min_temp.setText(f"{data.min_temp:.02f}") self.l_max_temp.setText(f"{data.max_temp:.02f}") self.l_current.setText(f"{data.current:.02f}") @@ -612,8 +624,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_soc = estimate_soc(data.min_voltage) + data.max_soc = estimate_soc(data.max_voltage) data.min_temp = min(temps) data.max_temp = max(temps) data.time_since_last_frame = time.time() - data.last_frame @@ -678,22 +690,6 @@ class Worker(QObject): data.ts_state = TSState(state) data.panic = data.ts_state == TSState.TS_ERROR - 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 and t < 60, data.slaves[slave].cell_temps[:16])