diff --git a/load_controller/gui.py b/load_controller/gui.py index 2bd8398..01d8a42 100644 --- a/load_controller/gui.py +++ b/load_controller/gui.py @@ -1,9 +1,9 @@ import enum -import os.path -import datetime import csv +import time +from typing import cast -from PyQt6.QtCore import QDir +from PyQt6.QtCore import QTimer from PyQt6.QtWidgets import ( QWidget, QGridLayout, @@ -12,36 +12,33 @@ from PyQt6.QtWidgets import ( QLabel, QPushButton, QFileDialog, - QMessageBox, ) from PyQt6.QtCharts import QChart, QChartView, QLineSeries - -class ProfileState(enum.Enum): - STOPPED = 0 - RUNNING = 1 - PAUSED = 2 +from .profile_handler import ProfileHandler, ProfileState class GUI(QWidget): - def __init__(self): + _profile_handler: ProfileHandler + _update_timer: QTimer + + def __init__(self, profile_handler: ProfileHandler): super().__init__(None) profile_label = QLabel("Profile:") self.profile = QLabel("None") self.profile.setStyleSheet("font-style: italic") - profile_chooser = QPushButton("Choose") - profile_chooser.clicked.connect(self._choose_profile) + self.profile_chooser = QPushButton("Choose") + self.profile_chooser.clicked.connect(self._choose_profile) profile_choice = QHBoxLayout() profile_choice.addWidget(profile_label) profile_choice.addWidget(self.profile) - profile_choice.addWidget(profile_chooser) + profile_choice.addWidget(self.profile_chooser) chart_view = QChartView() self.chart = QChart() chart_view.setChart(self.chart) - self.profile_state = ProfileState.STOPPED self.start_pause = QPushButton("START") self.start_pause.setDisabled(True) self.start_pause.clicked.connect(self._start_pause) @@ -84,6 +81,14 @@ class GUI(QWidget): self.setLayout(layout) self.setWindowTitle("Load Controller") + self._profile_handler = profile_handler + self._profile_handler.current_changed.connect(self._update_current) + self._profile_handler.finished.connect(self._stop) + + self._update_timer = QTimer() + self._update_timer.timeout.connect(self._update) + self._update_timer.start(100) + def _choose_profile(self): dlg = QFileDialog(parent=self) dlg.setFileMode(QFileDialog.FileMode.ExistingFile) @@ -97,28 +102,54 @@ class GUI(QWidget): self.profile.setStyleSheet("font-style: normal") series = QLineSeries() - with open(profile_path) as fh: - t = 0 - for l in csv.reader(fh): - i = float(l[0]) - series.append(t, i) - t += float(l[1]) - series.append(t, i) + for t, i in self._profile_handler.load_profile(profile_path): + series.append(t, i) + # with open(profile_path) as fh: + # t = 0 + # for l in csv.reader(fh): + # i = float(l[0]) + # series.append(t, i) + # t += float(l[1]) + # series.append(t, i) + # series.append() self.chart.removeAllSeries() self.chart.addSeries(series) self.chart.createDefaultAxes() + self.start_pause.setDisabled(False) + + def _update_current(self, current: float): + self.current.setText(f"{current:02.1f}") def _start_pause(self): - if self.profile_state == ProfileState.RUNNING: - self.profile_state = ProfileState.PAUSED - self.start_pause.setText("RESUME") - else: - self.profile_state = ProfileState.RUNNING + if self._profile_handler.state == ProfileState.STOPPED: + self._profile_handler.start() self.start_pause.setText("PAUSE") self.stop.setDisabled(False) - # TODO: Start logging if we were stopped + self.profile_chooser.setDisabled(True) + # TODO: Start logging + elif self._profile_handler.state == ProfileState.RUNNING: + self._profile_handler.pause() + self.start_pause.setText("RESUME") + else: + self._profile_handler.resume() + self.start_pause.setText("PAUSE") def _stop(self): - self.profile_state = ProfileState.STOPPED self.start_pause.setText("START") self.stop.setDisabled(True) + self.profile_chooser.setDisabled(False) + + def _update(self): + if self._profile_handler.state == ProfileState.RUNNING: + dt = time.time() - self._profile_handler.profile_start + if len(self.chart.series()) == 1: + series = QLineSeries() + series.append(dt, 0) + series.append(dt, 100) + self.chart.addSeries(series) + self.chart.createDefaultAxes() + else: + series = cast(QLineSeries, self.chart.series()[-1]) + series.removePoints(0, 2) + series.append(dt, 0) + series.append(dt, 100) diff --git a/load_controller/load.py b/load_controller/load.py index ce1fc39..b831988 100644 --- a/load_controller/load.py +++ b/load_controller/load.py @@ -1,19 +1,33 @@ import serial import time -from PyQt6.QtCore import QObject, pyqtSignal +from PyQt6.QtCore import QObject + +from .profile_handler import ProfileHandler class Load(QObject): dev: serial.Serial - finished: pyqtSignal - def __init__(self, uart_path: str): - super(parent=None) - self.dev = serial.Serial(uart_path) - self.finished = pyqtSignal() + _current: float + + def __init__(self, uart_path: str, profile_handler: ProfileHandler): + super().__init__(None) + self.dev = serial.Serial(uart_path, 115200) + self._current = 0 + + profile_handler.current_changed.connect(self._update_current) + + def _update_current(self, current): + self._current = current def do_work(self): while True: - # TODO time.sleep(0.1) + + curr_quants = round(self._current / 0.1) + assert curr_quants <= 0xFFFF + + msb = curr_quants >> 8 + lsb = curr_quants & 0xFF + self.dev.write(bytes((msb, lsb))) diff --git a/load_controller/profile_handler.py b/load_controller/profile_handler.py new file mode 100644 index 0000000..86c4330 --- /dev/null +++ b/load_controller/profile_handler.py @@ -0,0 +1,95 @@ +import enum +import csv +import time + +from PyQt6.QtCore import QObject, pyqtSignal, QTimer + + +class ProfileState(enum.Enum): + STOPPED = 0 + RUNNING = 1 + PAUSED = 2 + + +class ProfileHandler(QObject): + current_changed = pyqtSignal(float) + finished = pyqtSignal() + + state: ProfileState + + _timer: QTimer + + profile_start: float + _profile: list[tuple[float, float]] + _pause_time: float + _last_current: float + + def __init__(self): + super().__init__(parent=None) + self.state = ProfileState.STOPPED + self._timer = QTimer() + self._timer.timeout.connect(self._update) + self._timer.start(100) + + self._profile = [] + self.profile_start = 0 + self._pause_time = 0 + self._last_current = 0 + + def load_profile(self, path: str) -> list[tuple[float, float]]: + with open(path) as fh: + result = [] + t = 0 + for l in csv.reader(fh): + i = float(l[0]) + result.append((t, i)) + t += float(l[1]) + result.append((t, i)) + self._profile = result + return result + + def start(self): + assert self.state == ProfileState.STOPPED + + self.profile_start = time.time() + self.state = ProfileState.RUNNING + current = self._profile[0][1] + self._last_current = current + self.current_changed.emit(current) + + def pause(self): + assert self.state == ProfileState.RUNNING + + self.state = ProfileState.PAUSED + self.current_changed.emit(0) + self._last_current = 0 + self._pause_time = time.time() + + def resume(self): + assert self.state == ProfileState.PAUSED + + dt = time.time() - self._pause_time + self.profile_start += dt + self.state = ProfileState.RUNNING + + def stop(self): + assert self.state != ProfileState.STOPPED + + self.state = ProfileState.STOPPED + self.current_changed.emit(0) + self._last_current = 0 + + def _update(self): + if self.state == ProfileState.RUNNING: + dt = time.time() - self.profile_start + try: + current = next(t[1] for t in self._profile if t[0] >= dt) + if current != self._last_current: + self.current_changed.emit(current) + self._last_current = current + except StopIteration: + self.state = ProfileState.STOPPED + self.finished.emit() + else: + self._last_current = 0 + self.current_changed.emit(0) diff --git a/main.py b/main.py index 7896056..1fed2c3 100755 --- a/main.py +++ b/main.py @@ -3,16 +3,26 @@ import os import sys +from PyQt6.QtCore import QThread from PyQt6.QtWidgets import QApplication from load_controller.gui import GUI +from load_controller.load import Load +from load_controller.profile_handler import ProfileHandler def main(argv: list[str]) -> int: app = QApplication(sys.argv) - gui = GUI() + profile_handler = ProfileHandler() + gui = GUI(profile_handler) gui.show() + load = Load(argv[1], profile_handler) + load_thread = QThread() + load.moveToThread(load_thread) + load_thread.started.connect(load.do_work) + load_thread.start() + return app.exec()