diff --git a/device_worker.py b/device_worker.py index 8ba674b..ff7180e 100644 --- a/device_worker.py +++ b/device_worker.py @@ -1,57 +1,65 @@ import threading, queue, time, csv, os, statistics class DeviceWorker: - def __init__(self, sm, dev, outdir, - filter_window_size=10, interval=0.1): - self.sm = sm + """ + Arbeitet genau wie dein Single-Gerät-Logger: + - filter_window_size Rohsamples pro Loop + - ~10 Hz Loop (interval=0.1) + - 1 Hz Logging (eine CSV-Zeile pro Sekunde) + Wichtig: kontrolliert NICHT die Session! + """ + def __init__(self, dev, outdir="./logs", filter_window_size=10, interval=0.1): self.dev = dev self.serial = getattr(dev, "serial", "UNKNOWN") + self.outdir = outdir self.filter_window_size = filter_window_size self.interval = interval - self.outdir = outdir - self.cmdq = queue.Queue() - self.stop_evt = threading.Event() - self.running = False + self._running = False + self._stop_evt = threading.Event() + self._cmdq = queue.Queue() + 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) - self.writer_q = queue.Queue(maxsize=50) # 1 Wert/Sek → reicht dicke + self._reader_t = threading.Thread(target=self._reader_loop, daemon=True) + self._writer_t = threading.Thread(target=self._writer_loop, daemon=True) + # ---- API ---- 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() - self.cmdq.put(("start", None)) + if not self._reader_t.is_alive(): self._reader_t.start() + if not self._writer_t.is_alive(): self._writer_t.start() + self._cmdq.put(("start", None)) def stop(self): - self.cmdq.put(("stop", None)) + self._cmdq.put(("stop", None)) + + def shutdown(self): + self.stop() + self._stop_evt.set() def set_mode(self, ch, mode): - key = {0:"A",1:"B"}.get(ch, str(ch).upper()) + key = {0: "A", 1: "B"}.get(ch, str(ch).upper()) self.dev.channels[key].mode = mode - def reader_loop(self): + # ---- Threads ---- + def _reader_loop(self): last_log = 0.0 - while not self.stop_evt.is_set(): - # Start/Stop Kommandos + while not self._stop_evt.is_set(): + # Kommandos try: - cmd, _ = self.cmdq.get_nowait() - if cmd == "start" and not self.running: - self.sm.start() - self.running = True - elif cmd == "stop" and self.running: - self.running = False - self.sm.stop() + cmd, _ = self._cmdq.get_nowait() + if cmd == "start": self._running = True + elif cmd == "stop": self._running = False except queue.Empty: pass - if not self.running: + if not self._running: time.sleep(0.05) continue try: - # exakt wie dein Single-Loop: - samples = self.dev.read(self.filter_window_size, -1) # ggf. read(n) + # wie im Single-Logger: n Samples holen, mitteln + # WICHTIG: kleiner Timeout, damit der Thread nie "fest" hängt + samples = self.dev.read(self.filter_window_size, 50) # timeout ~50 ms if not samples: time.sleep(self.interval) continue @@ -60,38 +68,34 @@ class DeviceWorker: vB = statistics.mean(row[2] for row in samples) now = time.time() - if now - last_log >= 1.0: # 1 Hz logging + if now - last_log >= 1.0: # 1 Hz ins File try: - self.writer_q.put((now, vA, vB), timeout=0.2) + self._writer_q.put((now, vA, vB), timeout=0.2) last_log = now except queue.Full: - pass # selten, bei 1Hz praktisch nie + pass except Exception as e: print(f"[{self.serial}] Read-Fehler: {e}") time.sleep(0.02) - # Loop-Rate wie bei dir: + # Loop-Takt exakt wie beim Single-Logger time.sleep(max(0.05, self.interval)) - def writer_loop(self): + def _writer_loop(self): os.makedirs(self.outdir, exist_ok=True) fn = os.path.join(self.outdir, f"{time.strftime('%Y%m%d_%H%M%S')}_{self.serial}.csv") try: with open(fn, "w", newline="") as f: w = csv.writer(f) - w.writerow(["timestamp","A","B"]) - while not (self.stop_evt.is_set() and self.writer_q.empty()): + w.writerow(["timestamp", "A", "B"]) + while not (self._stop_evt.is_set() and self._writer_q.empty()): try: - ts, vA, vB = self.writer_q.get(timeout=0.5) + ts, vA, vB = self._writer_q.get(timeout=0.5) except queue.Empty: continue w.writerow([ts, vA, vB]) - f.flush() # 1 Zeile/Sek → ok + f.flush() # bei 1 Hz ok except Exception as e: print(f"[{self.serial}] Writer-Fehler: {e}") finally: - print(f"[{self.serial}] Datei geschlossen: {fn}") - - def shutdown(self): - self.stop() - self.stop_evt.set() \ No newline at end of file + print(f"[{self.serial}] Datei geschlossen: {fn}") \ No newline at end of file diff --git a/gui_main.py b/gui_main.py index 746f33b..e91ef11 100644 --- a/gui_main.py +++ b/gui_main.py @@ -3,41 +3,65 @@ from PyQt5.QtWidgets import QApplication, QWidget, QListWidget, QListWidgetItem, from multi_logger import MultiLogger class ListItemWidget(QWidget): - def __init__(self, text, worker): + def __init__(self, label, worker): super().__init__() self.worker = worker - layout = QHBoxLayout(self) - layout.addWidget(QLabel(text)) - layout.addStretch() - btn_start = QPushButton("Start") - btn_stop = QPushButton("Stop") - btn_start.clicked.connect(lambda: self.worker.start()) - btn_stop.clicked.connect(lambda: self.worker.stop()) - layout.addWidget(btn_start) - layout.addWidget(btn_stop) - layout.setContentsMargins(5, 2, 5, 2) + lay = QHBoxLayout(self) + lay.addWidget(QLabel(label)) + lay.addStretch() + self.btn_start = QPushButton("Start") + self.btn_stop = QPushButton("Stop") + self.btn_stop.setEnabled(False) + self.btn_start.clicked.connect(self._on_start) + self.btn_stop.clicked.connect(self._on_stop) + lay.addWidget(self.btn_start) + lay.addWidget(self.btn_stop) + lay.setContentsMargins(5,2,5,2) + + def _on_start(self): + self.worker.start() + self.btn_start.setEnabled(False) + self.btn_stop.setEnabled(True) + + def _on_stop(self): + self.worker.stop() + self.btn_start.setEnabled(True) + self.btn_stop.setEnabled(False) class MainWindow(QWidget): - def __init__(self, logger): + def __init__(self, mlog: MultiLogger): super().__init__() - self.logger = logger + self.mlog = mlog self.setWindowTitle("Multi-Geräte-Logger") - self.resize(400, 300) - main_layout = QVBoxLayout(self) - self.list_widget = QListWidget() - main_layout.addWidget(self.list_widget) - for worker in self.logger.workers: + self.resize(420, 320) + + main = QVBoxLayout(self) + self.list = QListWidget() + main.addWidget(self.list) + + for w in self.mlog.workers: item = QListWidgetItem() - widget = ListItemWidget(worker.serial, worker) + widget = ListItemWidget(w.serial, w) item.setSizeHint(widget.sizeHint()) - self.list_widget.addItem(item) - self.list_widget.setItemWidget(item, widget) + self.list.addItem(item) + self.list.setItemWidget(item, widget) if __name__ == "__main__": + # 1) Logger + Geräte ml = MultiLogger(outdir="./logs", filter_window_size=10, interval=0.1) + print("Gefundene Geräte:", [w.serial for w in ml.workers]) ml.set_default_modes() + # 2) Session EINMAL im Hauptthread starten (wie beim Single-Logger!) + ml.start_session() + + # 3) GUI app = QApplication(sys.argv) - window = MainWindow(ml) - window.show() - sys.exit(app.exec_()) \ No newline at end of file + win = MainWindow(ml) + win.show() + rc = app.exec_() + + # 4) Sauber beenden + ml.stop_all() + ml.shutdown() + sys.exit(rc) \ No newline at end of file diff --git a/multi_logger.py b/multi_logger.py index 613999f..882a1d1 100644 --- a/multi_logger.py +++ b/multi_logger.py @@ -1,24 +1,19 @@ -import pysmu, os -from session_manager import SessionManager +import pysmu from device_worker import DeviceWorker class MultiLogger: - def __init__(self, outdir="./logs", - filter_window_size=10, interval=0.1): - self.outdir = outdir - self.filter_window_size = filter_window_size - self.interval = interval - + """ + Verwaltet eine Session und mehrere DeviceWorker. + Session wird EINMAL im Hauptthread gestartet – wie beim Single-Logger. + """ + def __init__(self, outdir="./logs", filter_window_size=10, interval=0.1): self.sess = pysmu.Session() self.sess.add_all() - self.sm = SessionManager(self.sess) - self.devices = list(self.sess.devices) - self.workers = [DeviceWorker(self.sm, dev, outdir, - filter_window_size, interval) for dev in self.devices] + self.workers = [DeviceWorker(dev, outdir, filter_window_size, interval) + for dev in self.devices] def set_default_modes(self): - # wie gehabt: A/B-Modi setzen for w in self.workers: try: w.set_mode("A", pysmu.Mode.SIMV) @@ -26,14 +21,22 @@ class MultiLogger: except Exception as e: print(f"[{w.serial}] Mode-Set-Fehler: {e}") + # Session zentral steuern: + def start_session(self): + # WICHTIG: im Hauptthread aufrufen (vor den GUI-Events) + self.sess.start(0) + + def end_session(self): + self.sess.end() + + # Helfer: def start_all(self): - for w in self.workers: - w.start() + for w in self.workers: w.start() def stop_all(self): - for w in self.workers: - w.stop() + for w in self.workers: w.stop() def shutdown(self): - for w in self.workers: - w.shutdown() \ No newline at end of file + for w in self.workers: w.shutdown() + # Session am Ende schließen + self.end_session() \ No newline at end of file