From a317fcb5d1223cba100be29ae94a833317b1334b Mon Sep 17 00:00:00 2001 From: Vincent Hanewinkel Date: Thu, 14 Aug 2025 22:21:22 +0200 Subject: [PATCH] test --- Controll.py | 52 +++---------------- DeviceController.py | 121 +++++++++++++++++++++----------------------- GUI.py | 62 ++++++----------------- 3 files changed, 80 insertions(+), 155 deletions(-) diff --git a/Controll.py b/Controll.py index 844a260..cca004e 100644 --- a/Controll.py +++ b/Controll.py @@ -1,57 +1,19 @@ import sys -import pysmu from PyQt5.QtWidgets import QApplication -from PyQt5.QtCore import pyqtSignal, QObject -from GUI import MainWindow -from DeviceController import DeviceController -from functools import partial - -class ControllerSignals(QObject): - started = pyqtSignal(str) # serial - stopped = pyqtSignal(str) - error = pyqtSignal(str, str) # serial, msg - -signals = ControllerSignals() +from gui import MainWindow +from device_controller import DeviceController def main(): - # 1) Geräte einsammeln (pysmu nur hier benutzen) - sess = pysmu.Session() - sess.add_all() - - controllers = {dev.serial: DeviceController(sess, dev) for dev in sess.devices} - serials = list(controllers.keys()) - print("Gefundene Geräte:", serials) - - # Optional: Modi setzen - for serial in serials: - c = controllers[serial] - c.set_mode("A", pysmu.Mode.SIMV) # CH A z. B. - c.set_mode("B", pysmu.Mode.HI_Z) # CH B z. B. - - # 2) GUI erstellen app = QApplication(sys.argv) win = MainWindow() - # Kleine Helfer, damit die ListItem-Buttons nur Controller.start/stop aufrufen - def on_start(ctrl, widget): - ctrl.start() - widget.set_running(True) + ctrl = DeviceController(serial="TEST123") + widget = win.add_list_item("TEST123", 0) - def on_stop(ctrl, widget): - ctrl.stop() - widget.set_running(False) + # 'checked' wird von Qt geliefert; wir fangen es in der Lambda ab + widget.btn_start.clicked.connect(lambda checked=False: (ctrl.start(), widget.set_running(True))) + widget.btn_stop.clicked.connect (lambda checked=False: (ctrl.stop(), widget.set_running(False))) - # 3) Liste befüllen (auf der INSTANZ, nicht auf der Klasse!) - for i, serial in enumerate(serials): - ctrl = controllers[serial] - w = win.add_list_item(serial, i) - - # WICHTIG: die Lambda fängt 'checked' ab (erster Parameter), wir ignorieren ihn. - w.btn_start.clicked.connect(lambda checked=False, c=ctrl, ww=w: on_start(c, ww)) - w.btn_stop.clicked.connect (lambda checked=False, c=ctrl, ww=w: on_stop(c, ww)) - - - # 4) Start win.show() sys.exit(app.exec_()) diff --git a/DeviceController.py b/DeviceController.py index 99262b2..c202965 100644 --- a/DeviceController.py +++ b/DeviceController.py @@ -1,23 +1,23 @@ -import threading, queue, time, csv, os +# device_controller.py +import threading, queue, time, math, os, csv -CHUNK = 2000 -SAMPLE_RATE = 100_000.0 # 100 kS/s +SAMPLE_RATE = 10_000.0 # 10 kS/s (Dummy) DT = 1.0 / SAMPLE_RATE -OUTDIR = "logs" -FLUSH_EVERY = 50 -os.makedirs(OUTDIR, exist_ok=True) +CHUNK = 500 # 50 ms pro Chunk +OUTDIR = "./logs" class DeviceController: - def __init__(self, sess, dev): - self.sess = sess # <— Session referenzieren - self.dev = dev + def __init__(self, serial="DUMMY"): + self.serial = serial self.cmdq = queue.Queue() + self.writer_q = queue.Queue(maxsize=100) self.stop_evt = threading.Event() self.running = False - self.writer_q = queue.Queue(maxsize=50) + self.reader_t = threading.Thread(target=self.reader_loop, daemon=True) self.writer_t = threading.Thread(target=self.writer_loop, daemon=True) + # === öffentliche API (nicht blockierend) === def start(self): if not self.reader_t.is_alive(): self.reader_t.start() if not self.writer_t.is_alive(): self.writer_t.start() @@ -26,81 +26,76 @@ class DeviceController: def stop(self): self.cmdq.put(("stop", None)) - def set_mode(self, ch, mode): - # BUGFIX: nicht rekursiv sich selbst aufrufen! - self.dev.channels[ch].mode = mode + def shutdown(self): + self.stop() + self.stop_evt.set() + # nicht joinen aus dem GUI-Thread; nur beim Programmende joinen! + # === Dummy-Reader: erzeugt Daten, statt pysmu zu lesen === def reader_loop(self): + phase = 0.0 + step = 2*math.pi * 50.0 * DT # 50 Hz Sinus while not self.stop_evt.is_set(): + # Kommandos abarbeiten try: cmd, arg = self.cmdq.get_nowait() - if cmd == "start" and not self.running: - # Continuous Session-Stream starten (für alle Devices) - self.sess.start(0) # <— statt dev.run(0) + if cmd == "start": self.running = True - elif cmd == "stop" and self.running: - self.sess.end() # <— Session sauber beenden - # optional: Puffer leeren - # self.sess.flush(-1, True) # falls verfügbar in deiner Version + print("[DUMMY] start") + elif cmd == "stop": self.running = False - elif cmd == "mode": - ch, mode = arg - self.dev.channels[ch].mode = mode + print("[DUMMY] stop") except queue.Empty: pass - if self.running: - try: - # Lies jeweils CHUNK Samples (blockierend bis voll) - data = self.dev.read(CHUNK, -1) # -> [[VA, IA, VB, IB], ...] - vsA = [row[0] for row in data] - vsB = [row[2] for row in data] - self.writer_q.put((time.time(), vsA, vsB)) - except Exception: - time.sleep(0.001) - else: - time.sleep(0.01) + if not self.running: + time.sleep(0.02) + continue + # CHUNK Samples erzeugen + va, vb = [], [] + for _ in range(CHUNK): + a = math.sin(phase) # Kanal A + b = 0.5*math.sin(phase*0.5) # Kanal B + va.append(a) + vb.append(b) + phase += step + + try: + self.writer_q.put((time.time(), va, vb), timeout=0.5) + except queue.Full: + print("[WARN] writer_q voll – Datenverlust") + # kurze Pause, um CPU zu schonen + time.sleep(CHUNK * DT) + + # === Writer: robust, Wide-Format === def writer_loop(self): os.makedirs(OUTDIR, exist_ok=True) - ts0 = None - fn = os.path.join(OUTDIR, f"{time.strftime('%Y%m%d_%H%M%S')}_{self.dev.serial}.csv") - chunks_written = 0 + fn = os.path.join(OUTDIR, f"{time.strftime('%Y%m%d_%H%M%S')}_{self.serial}.csv") sample_idx = 0 - + written_rows = 0 try: with open(fn, "w", newline="") as f: w = csv.writer(f) - # Long-Format: eine Zeile pro Kanalwert - w.writerow(["t_rel_s", "ch", "value"]) - + w.writerow(["t_rel_s", "A", "B"]) # Wide-Format while not (self.stop_evt.is_set() and self.writer_q.empty()): try: - ts, va, vb = self.writer_q.get(timeout=0.2) - except Exception: + ts, va, vb = self.writer_q.get(timeout=0.5) + except queue.Empty: continue - if ts0 is None: - ts0 = ts - - # Sicherstellen, dass A und B gleich lang sind - if len(va) != len(vb): - n = min(len(va), len(vb)) - va, vb = va[:n], vb[:n] - - # Zeitindex aus Sample-Index ableiten (konstante Rate) - for i, (a, b) in enumerate(zip(va, vb)): + n = min(len(va), len(vb)) + for i in range(n): t_rel = (sample_idx + i) * DT - w.writerow([t_rel, "A", a]) - w.writerow([t_rel, "B", b]) - - sample_idx += len(va) - chunks_written += 1 - - if chunks_written % FLUSH_EVERY == 0: - f.flush() # optional: os.fsync(f.fileno()) + w.writerow([t_rel, va[i], vb[i]]) + sample_idx += n + written_rows += n + # damit man live sieht, dass wirklich geschrieben wird + if written_rows and written_rows % (10*CHUNK) == 0: + f.flush() + print(f"[{self.serial}] geschrieben: {written_rows} Zeilen") except Exception as e: - print(f"[{self.dev.serial}] Writer-Fehler: {e}") + print(f"[{self.serial}] Writer-Fehler: {e}") finally: - print(f"[{self.dev.serial}] Datei geschlossen: {fn}") \ No newline at end of file + print(f"[{self.serial}] Datei geschlossen: {fn}") \ No newline at end of file diff --git a/GUI.py b/GUI.py index ec2e3c7..a5fb940 100644 --- a/GUI.py +++ b/GUI.py @@ -1,30 +1,19 @@ -import sys -from functools import partial -from PyQt5.QtWidgets import ( - QApplication, QWidget, QListWidget, QListWidgetItem, - QPushButton, QLabel, QHBoxLayout, QVBoxLayout -) +# gui.py +from PyQt5.QtWidgets import QWidget, QListWidget, QListWidgetItem, QPushButton, QLabel, QHBoxLayout, QVBoxLayout class ListItemWidget(QWidget): def __init__(self, text, index): super().__init__() - self.text = text self.index = index - - layout = QHBoxLayout(self) - - label = QLabel(text) - layout.addWidget(label) - - layout.addStretch() # schiebt die Buttons nach rechts - + lay = QHBoxLayout(self) + lay.addWidget(QLabel(text)) + lay.addStretch() self.btn_start = QPushButton("Start") self.btn_stop = QPushButton("Stop") self.btn_stop.setEnabled(False) - - layout.addWidget(self.btn_start) - layout.addWidget(self.btn_stop) - layout.setContentsMargins(5, 2, 5, 2) + lay.addWidget(self.btn_start) + lay.addWidget(self.btn_stop) + lay.setContentsMargins(5,2,5,2) def set_running(self, running: bool): self.btn_start.setEnabled(not running) @@ -33,36 +22,15 @@ class ListItemWidget(QWidget): class MainWindow(QWidget): def __init__(self): super().__init__() - self.setWindowTitle("Geräteliste") - self.resize(420, 320) - - main_layout = QVBoxLayout(self) - + self.setWindowTitle("Logger") + lay = QVBoxLayout(self) self.list_widget = QListWidget() - main_layout.addWidget(self.list_widget) - - # Beispiel-Daten - # for i in range(5): - # self.add_list_item(f"Eintrag {i+1}", i+1, self.handle_start, self.handle_stop) + lay.addWidget(self.list_widget) def add_list_item(self, text, index): item = QListWidgetItem() - widget = ListItemWidget(text, index) - item.setSizeHint(widget.sizeHint()) + w = ListItemWidget(text, index) + item.setSizeHint(w.sizeHint()) self.list_widget.addItem(item) - self.list_widget.setItemWidget(item, widget) - return widget - - # Die Aktionen, die beim Klick ausgeführt werden sollen - def handle_start(self, index): - print(f"Start gedrückt bei Eintrag {index}") - - def handle_stop(self, index): - print(f"Stop gedrückt bei Eintrag {index}") - - -def startGUI(): - app = QApplication(sys.argv) - w = MainWindow() - w.show() - sys.exit(app.exec_()) \ No newline at end of file + self.list_widget.setItemWidget(item, w) + return w