This commit is contained in:
Vincent Hanewinkel 2025-08-14 23:01:45 +02:00
parent b2406ed037
commit dc5269e595
3 changed files with 116 additions and 85 deletions

View File

@ -1,57 +1,65 @@
import threading, queue, time, csv, os, statistics import threading, queue, time, csv, os, statistics
class DeviceWorker: class DeviceWorker:
def __init__(self, sm, dev, outdir, """
filter_window_size=10, interval=0.1): Arbeitet genau wie dein Single-Gerät-Logger:
self.sm = sm - 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.dev = dev
self.serial = getattr(dev, "serial", "UNKNOWN") self.serial = getattr(dev, "serial", "UNKNOWN")
self.outdir = outdir
self.filter_window_size = filter_window_size self.filter_window_size = filter_window_size
self.interval = interval self.interval = interval
self.outdir = outdir
self.cmdq = queue.Queue() self._running = False
self.stop_evt = threading.Event() self._stop_evt = threading.Event()
self.running = False self._cmdq = queue.Queue()
self._writer_q = queue.Queue(maxsize=50)
self.reader_t = threading.Thread(target=self.reader_loop, daemon=True) self._reader_t = threading.Thread(target=self._reader_loop, daemon=True)
self.writer_t = threading.Thread(target=self.writer_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
# ---- API ----
def start(self): def start(self):
if not self.reader_t.is_alive(): self.reader_t.start() if not self._reader_t.is_alive(): self._reader_t.start()
if not self.writer_t.is_alive(): self.writer_t.start() if not self._writer_t.is_alive(): self._writer_t.start()
self.cmdq.put(("start", None)) self._cmdq.put(("start", None))
def stop(self): 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): 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 self.dev.channels[key].mode = mode
def reader_loop(self): # ---- Threads ----
def _reader_loop(self):
last_log = 0.0 last_log = 0.0
while not self.stop_evt.is_set(): while not self._stop_evt.is_set():
# Start/Stop Kommandos # Kommandos
try: try:
cmd, _ = self.cmdq.get_nowait() cmd, _ = self._cmdq.get_nowait()
if cmd == "start" and not self.running: if cmd == "start": self._running = True
self.sm.start() elif cmd == "stop": self._running = False
self.running = True
elif cmd == "stop" and self.running:
self.running = False
self.sm.stop()
except queue.Empty: except queue.Empty:
pass pass
if not self.running: if not self._running:
time.sleep(0.05) time.sleep(0.05)
continue continue
try: try:
# exakt wie dein Single-Loop: # wie im Single-Logger: n Samples holen, mitteln
samples = self.dev.read(self.filter_window_size, -1) # ggf. read(n) # 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: if not samples:
time.sleep(self.interval) time.sleep(self.interval)
continue continue
@ -60,38 +68,34 @@ class DeviceWorker:
vB = statistics.mean(row[2] for row in samples) vB = statistics.mean(row[2] for row in samples)
now = time.time() now = time.time()
if now - last_log >= 1.0: # 1 Hz logging if now - last_log >= 1.0: # 1 Hz ins File
try: try:
self.writer_q.put((now, vA, vB), timeout=0.2) self._writer_q.put((now, vA, vB), timeout=0.2)
last_log = now last_log = now
except queue.Full: except queue.Full:
pass # selten, bei 1Hz praktisch nie pass
except Exception as e: except Exception as e:
print(f"[{self.serial}] Read-Fehler: {e}") print(f"[{self.serial}] Read-Fehler: {e}")
time.sleep(0.02) time.sleep(0.02)
# Loop-Rate wie bei dir: # Loop-Takt exakt wie beim Single-Logger
time.sleep(max(0.05, self.interval)) time.sleep(max(0.05, self.interval))
def writer_loop(self): def _writer_loop(self):
os.makedirs(self.outdir, exist_ok=True) 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") fn = os.path.join(self.outdir, f"{time.strftime('%Y%m%d_%H%M%S')}_{self.serial}.csv")
try: try:
with open(fn, "w", newline="") as f: with open(fn, "w", newline="") as f:
w = csv.writer(f) w = csv.writer(f)
w.writerow(["timestamp", "A", "B"]) w.writerow(["timestamp", "A", "B"])
while not (self.stop_evt.is_set() and self.writer_q.empty()): while not (self._stop_evt.is_set() and self._writer_q.empty()):
try: try:
ts, vA, vB = self.writer_q.get(timeout=0.5) ts, vA, vB = self._writer_q.get(timeout=0.5)
except queue.Empty: except queue.Empty:
continue continue
w.writerow([ts, vA, vB]) w.writerow([ts, vA, vB])
f.flush() # 1 Zeile/Sek → ok f.flush() # bei 1 Hz ok
except Exception as e: except Exception as e:
print(f"[{self.serial}] Writer-Fehler: {e}") print(f"[{self.serial}] Writer-Fehler: {e}")
finally: finally:
print(f"[{self.serial}] Datei geschlossen: {fn}") print(f"[{self.serial}] Datei geschlossen: {fn}")
def shutdown(self):
self.stop()
self.stop_evt.set()

View File

@ -3,41 +3,65 @@ from PyQt5.QtWidgets import QApplication, QWidget, QListWidget, QListWidgetItem,
from multi_logger import MultiLogger from multi_logger import MultiLogger
class ListItemWidget(QWidget): class ListItemWidget(QWidget):
def __init__(self, text, worker): def __init__(self, label, worker):
super().__init__() super().__init__()
self.worker = worker self.worker = worker
layout = QHBoxLayout(self) lay = QHBoxLayout(self)
layout.addWidget(QLabel(text)) lay.addWidget(QLabel(label))
layout.addStretch() lay.addStretch()
btn_start = QPushButton("Start") self.btn_start = QPushButton("Start")
btn_stop = QPushButton("Stop") self.btn_stop = QPushButton("Stop")
btn_start.clicked.connect(lambda: self.worker.start()) self.btn_stop.setEnabled(False)
btn_stop.clicked.connect(lambda: self.worker.stop()) self.btn_start.clicked.connect(self._on_start)
layout.addWidget(btn_start) self.btn_stop.clicked.connect(self._on_stop)
layout.addWidget(btn_stop) lay.addWidget(self.btn_start)
layout.setContentsMargins(5, 2, 5, 2) 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): class MainWindow(QWidget):
def __init__(self, logger): def __init__(self, mlog: MultiLogger):
super().__init__() super().__init__()
self.logger = logger self.mlog = mlog
self.setWindowTitle("Multi-Geräte-Logger") self.setWindowTitle("Multi-Geräte-Logger")
self.resize(400, 300) self.resize(420, 320)
main_layout = QVBoxLayout(self)
self.list_widget = QListWidget() main = QVBoxLayout(self)
main_layout.addWidget(self.list_widget) self.list = QListWidget()
for worker in self.logger.workers: main.addWidget(self.list)
for w in self.mlog.workers:
item = QListWidgetItem() item = QListWidgetItem()
widget = ListItemWidget(worker.serial, worker) widget = ListItemWidget(w.serial, w)
item.setSizeHint(widget.sizeHint()) item.setSizeHint(widget.sizeHint())
self.list_widget.addItem(item) self.list.addItem(item)
self.list_widget.setItemWidget(item, widget) self.list.setItemWidget(item, widget)
if __name__ == "__main__": if __name__ == "__main__":
# 1) Logger + Geräte
ml = MultiLogger(outdir="./logs", filter_window_size=10, interval=0.1) 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() ml.set_default_modes()
# 2) Session EINMAL im Hauptthread starten (wie beim Single-Logger!)
ml.start_session()
# 3) GUI
app = QApplication(sys.argv) app = QApplication(sys.argv)
window = MainWindow(ml) win = MainWindow(ml)
window.show() win.show()
sys.exit(app.exec_()) rc = app.exec_()
# 4) Sauber beenden
ml.stop_all()
ml.shutdown()
sys.exit(rc)

View File

@ -1,24 +1,19 @@
import pysmu, os import pysmu
from session_manager import SessionManager
from device_worker import DeviceWorker from device_worker import DeviceWorker
class MultiLogger: class MultiLogger:
def __init__(self, outdir="./logs", """
filter_window_size=10, interval=0.1): Verwaltet eine Session und mehrere DeviceWorker.
self.outdir = outdir Session wird EINMAL im Hauptthread gestartet wie beim Single-Logger.
self.filter_window_size = filter_window_size """
self.interval = interval def __init__(self, outdir="./logs", filter_window_size=10, interval=0.1):
self.sess = pysmu.Session() self.sess = pysmu.Session()
self.sess.add_all() self.sess.add_all()
self.sm = SessionManager(self.sess)
self.devices = list(self.sess.devices) self.devices = list(self.sess.devices)
self.workers = [DeviceWorker(self.sm, dev, outdir, self.workers = [DeviceWorker(dev, outdir, filter_window_size, interval)
filter_window_size, interval) for dev in self.devices] for dev in self.devices]
def set_default_modes(self): def set_default_modes(self):
# wie gehabt: A/B-Modi setzen
for w in self.workers: for w in self.workers:
try: try:
w.set_mode("A", pysmu.Mode.SIMV) w.set_mode("A", pysmu.Mode.SIMV)
@ -26,14 +21,22 @@ class MultiLogger:
except Exception as e: except Exception as e:
print(f"[{w.serial}] Mode-Set-Fehler: {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): def start_all(self):
for w in self.workers: for w in self.workers: w.start()
w.start()
def stop_all(self): def stop_all(self):
for w in self.workers: for w in self.workers: w.stop()
w.stop()
def shutdown(self): def shutdown(self):
for w in self.workers: for w in self.workers: w.shutdown()
w.shutdown() # Session am Ende schließen
self.end_session()