diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..a5438b0
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,83 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this project is
+
+A PyQt5 instrument control suite for CMS detector module testing at Pisa (CERN). Operators select a module, connect cables, and run automated electrical tests while the system monitors cooling safety in real time.
+
+## Running the applications
+
+```bash
+# Main module testing GUI
+python integration.py
+
+# Coldroom / MARTA cooling monitor GUI
+python cold.py
+
+# Run all tests
+python -m pytest tests/
+# or
+python -m unittest discover tests/
+
+# Run a single test file
+python -m unittest tests.test_module_db
+python -m unittest tests.test_integration
+```
+
+Tests use `unittest` with mocked GUI components — hardware is not required to run them.
+
+## Architecture
+
+Two independent PyQt5 applications share some library code:
+
+### `integration.py` (main testing app, ~2272 lines)
+- Subclasses `ui/integration_gui.py` (auto-generated from Qt Designer — **never edit directly**)
+- Creates `CAENControl`, `ModuleDB`, and a raw `paho.mqtt.client` at startup
+- One giant `on_mqtt_message()` handles ALL incoming MQTT topics
+- Tests run via `CommandWorker(QThread)` wrapping `subprocess.Popen` — **not** `subprocess.run`
+- GUI updates from MQTT thread must go through `pyqtSignal` — direct widget writes from non-main threads crash randomly
+
+### `cold.py` (coldroom monitor, ~2000 lines)
+- Subclasses `coldroom/marta_coldroom.ui`
+- Owns a `System` object (`coldroom/system.py`) which holds the single shared `_status` dict
+- `MartaColdRoomMQTTClient` writes to `_status`; `coldroom/safety.py` reads it
+- A `QTimer` fires `soft_interlock_check()` every **5 seconds** → calls `soft_interlock_loop()` → cuts LV if cooling is lost
+- `self.system._martacoldroom` can be `None` if the MQTT broker is unreachable at startup — always guard before calling methods on it
+
+### `coldroom/system.py`
+Initializes `_status` with keys: `marta`, `coldroom`, `thermal_camera`, `caen`, `cleanroom`, `coldroomair`. The key `serviceroom` is **never populated** — code that requires it will silently fail.
+
+### `coldroom/safety.py`
+All safety logic is here. Key functions:
+- `soft_interlock_loop(system_status, caen_ch_status, used_channels, caen, publish_alarm)` — main 5-second safety loop; returns `(bool, str)`
+- `Is_it_safe_to_on_lv(...)` — returns a **tuple** `(bool, str)`; callers must unpack, not compare directly
+- `check_marta_on_for_OT/IT(system_status)` — uses `marta.fsm_state` as primary signal; `serviceroom` valve data is optional
+- `switch_all_lv_off(caen, used_channels)` — the actual power cutoff action
+
+### Communication layers
+| Protocol | Target | Used by |
+|----------|--------|---------|
+| HTTP REST | `pccmslab1:5000` (MongoDB) | `db/module_db.py` |
+| TCP binary | `192.168.0.45:7000` (CAEN HV/LV) | `caen/caenGUI.py` |
+| VISA/TCP | `192.168.0.16` (Rigol DP116A) | `power_supply/` |
+| MQTT | `pccmslab1:1883` | everything else |
+| subprocess | Ph2_ACF test framework | `CommandWorker` |
+
+MQTT topics are defined in `settings_coldroom.yaml` and `settings_integration.yaml`.
+
+## Key invariants
+
+- **MARTA FSM state** is the primary CO2 cooling indicator. States `"DISCONNECTED"`, `"NONE"`, `""` mean cooling is off. This is what `check_marta_on_for_OT/IT` reads — not valve data.
+- `used_channels` dict format: `{"LV": [...channel_ids...], "HV": [...channel_ids...]}` — sourced from `modules_list_tab.get_used_channels()`, which currently only returns OT channels.
+- `caen.on(channel)` / `caen.off(channel)` — the CAEN API for LV/HV control.
+- Qt Designer `.ui` files are compiled to `*_gui.py` — the `ui/` and `coldroom/*.ui` files are the source of truth for layouts, not the generated Python.
+
+## Known open bugs (issues.md)
+
+Critical bugs not yet fixed:
+- **BUG-01** `integration.py:792` — thermal safety interlock uses `self.caenGUI` (doesn't exist); should be `self.caen`
+- **BUG-02** `integration.py:418` — `new_session()` crashes if API call fails; must check `success` before indexing `result`
+- **BUG-03** `coldroom/system.py:213` — `cleanup()` calls `loop_start()` instead of `loop_stop()` on the thermal camera client
+- **BUG-04** `coldroom/command_worker.py` — `terminate_process()` calls `.poll()` on `CompletedProcess`; switch to `Popen`
+- **BUG-05** `power_supply/power_supply_ctrl.py:204` — `QMainWindow` imported inside `__main__` guard, causing `NameError` when called as a library
\ No newline at end of file
diff --git a/Inner_tracker_GUI/caenGUIall_v2.py b/Inner_tracker_GUI/caenGUIall_v2.py
index 1092dce..25dcbff 100755
--- a/Inner_tracker_GUI/caenGUIall_v2.py
+++ b/Inner_tracker_GUI/caenGUIall_v2.py
@@ -29,33 +29,29 @@ def __init__(self, ip, port):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(0.5)
self.headerBytes = 4
-
+
self.connectSocket()
- pass
+ pass
def __del__(self):
- """Destructor, closes socket
- """
+ """Destructor, closes socket"""
try:
self.closeSocket()
except Exception:
pass
def connectSocket(self):
- """Connects socket
- """
+ """Connects socket"""
self.socket.connect((self.ip, self.port))
pass
def closeSocket(self):
- """Closes socket connection
- """
+ """Closes socket connection"""
self.socket.close()
pass
def sendMessage(self, message):
- """Encodes message and sends it on socket
- """
+ """Encodes message and sends it on socket"""
encodedMessage = self.encodeMessage(message)
self.socket.send(encodedMessage)
pass
@@ -82,7 +78,7 @@ class CAENQueryThread(QThread):
"""
dataReady = pyqtSignal(dict) # Emitted when parsed data is ready
- error = pyqtSignal(str) # Emitted when an error occurs
+ error = pyqtSignal(str) # Emitted when an error occurs
def __init__(self, ip="192.168.0.45", port=7000):
super().__init__()
@@ -91,7 +87,7 @@ def __init__(self, ip="192.168.0.45", port=7000):
self.message = None
self.receive = False
self.running = True
- self.queue = [] # list of messages to send
+ self.queue = [] # list of messages to send
self.receiveQueue = [] # list of booleans: whether to read a reply
def setup_query(self, message, receive=False):
@@ -103,11 +99,11 @@ def stop(self):
"""Stop the thread"""
self.running = False
self.wait()
-
+
def run(self):
"""Thread's main method : Process the queued messages."""
while self.queue:
- #create lock
+ # create lock
self.message = self.queue.pop(0)
self.receive = self.receiveQueue.pop(0)
try:
@@ -155,7 +151,7 @@ class caenGUI8LV(QWidget):
def __init__(self, ip="192.168.0.45", port=7000):
super().__init__()
-
+
# Create timer for periodic update
self.channels = []
self.led = {}
@@ -172,11 +168,11 @@ def __init__(self, ip="192.168.0.45", port=7000):
# Timer to periodically request status
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_status)
-
+
# build the UI
self.initUI()
-
- #start periodic status update
+
+ # start periodic status update
self.timer.start(2000) # ms
def change_host(self, ip, port):
@@ -199,14 +195,14 @@ def initUI(self):
self.main_layout = QVBoxLayout()
self.setLayout(self.main_layout)
-
+
# Safety checkbox to enable/disable current setting
self.enable_current_checkbox = QCheckBox("Enable current setting (safety)")
self.enable_current_checkbox.stateChanged.connect(self.toggle_current_setting)
self.main_layout.addWidget(self.enable_current_checkbox)
# Define 8 LV channels: LV15.1 ... LV15.8
- for i in range(1,9):
+ for i in range(1, 9):
self.channels.append(f"LV15.{i}")
for channel in self.channels:
@@ -217,7 +213,7 @@ def initUI(self):
ch_label = QLabel(channel + ":")
ch_label.setFont(QFont("Arial", 10))
hlayout.addWidget(ch_label)
-
+
# Current set text box
current_input = QLineEdit()
current_input.setFixedWidth(60)
@@ -228,27 +224,21 @@ def initUI(self):
# 'Set I' button
btn_setI = QPushButton("Set I", self)
btn_setI.setMinimumWidth(50)
- btn_setI.clicked.connect(
- lambda checked, ch=channel: self.set_current(ch)
- )
- btn_setI.setEnabled(False) # ← DISABLE by default
- self.setI_buttons[channel] = btn_setI # ← STORE button
+ btn_setI.clicked.connect(lambda checked, ch=channel: self.set_current(ch))
+ btn_setI.setEnabled(False) # ← DISABLE by default
+ self.setI_buttons[channel] = btn_setI # ← STORE button
hlayout.addWidget(btn_setI)
-
+
# ON button
btn_on = QPushButton("ON", self)
btn_on.setMinimumWidth(40)
- btn_on.clicked.connect(
- lambda checked, ch=channel: self.turn_on(ch)
- )
+ btn_on.clicked.connect(lambda checked, ch=channel: self.turn_on(ch))
hlayout.addWidget(btn_on)
# OFF button
btn_off = QPushButton("OFF", self)
btn_off.setMinimumWidth(40)
- btn_off.clicked.connect(
- lambda checked, ch=channel: self.turn_off(ch)
- )
+ btn_off.clicked.connect(lambda checked, ch=channel: self.turn_off(ch))
hlayout.addWidget(btn_off)
# Voltage/Current label
@@ -264,15 +254,15 @@ def initUI(self):
hlayout.addWidget(self.led[channel])
self.show()
-
+
def toggle_current_setting(self, state):
"""
Enable or disable all 'Set I' buttons based on the safety checkbox.
"""
- enabled = (state == Qt.Checked)
+ enabled = state == Qt.Checked
for btn in self.setI_buttons.values():
btn.setEnabled(enabled)
-
+
# ---------------- Periodic status update ----------------
@pyqtSlot()
@@ -282,7 +272,7 @@ def update_status(self):
ask the server for the status of all channels.
"""
print("Update")
-
+
message = "GetStatus,PowerSupplyId:caen"
self.queryThread.setup_query(message, True)
@@ -380,9 +370,7 @@ def set_current(self, channel):
return
# Adjust "SetCurrent" to your actual server protocol if needed.
- message = (
- f"SetCurrent,PowerSupplyId:caen,ChannelId:{channel},Current:{value}"
- )
+ message = f"SetCurrent,PowerSupplyId:caen,ChannelId:{channel},Current:{value}"
print(message)
self.queryThread.setup_query(message, receive=False)
@@ -395,7 +383,9 @@ def set_current(self, channel):
if __name__ == "__main__":
import argparse
- parser = argparse.ArgumentParser(description="INNER TRACKER CAEN GUI - 8 LV Channels")
+ parser = argparse.ArgumentParser(
+ description="INNER TRACKER CAEN GUI - 8 LV Channels"
+ )
parser.add_argument(
"--ip",
type=str,
diff --git a/Onboarding_integration_tools.md b/Onboarding_integration_tools.md
new file mode 100644
index 0000000..c943470
--- /dev/null
+++ b/Onboarding_integration_tools.md
@@ -0,0 +1,109 @@
+Onboarding: integration_tools_new
+What this project is
+A physics lab instrument control suite for CMS detector module testing at Pisa (CERN collaboration). Operators use it daily to:
+
+Select a detector module from inventory
+Connect fiber/power cables to it
+Run 3 automated electrical tests (ID check, noise with HV off, noise with HV on)
+Monitor cooling and thermal safety in real time
+Think of it as a hardware dashboard — it talks to 4 different systems over 4 different protocols simultaneously.
+
+The 4 communication layers (this is the architecture)
+
+Your GUI ─────── HTTP REST ──────────▶ pccmslab1:5000 (MongoDB, module inventory)
+ ─────── TCP binary ───────────▶ 192.168.0.45:7000 (CAEN HV/LV supply)
+ ─────── VISA/TCP ─────────────▶ 192.168.0.16 (Rigol DP116A bench supply)
+ ─────── MQTT pub/sub ─────────▶ pccmslab1:1883 (everything else)
+ ─────── subprocess ───────────▶ Ph2_ACF (the actual detector test framework)
+The MQTT broker is the nervous system: all sensor data (temperatures, cooling state, door lock, CO₂, thermal camera images) flows through it. Commands to the coldroom and MARTA also go over MQTT.
+
+5 entry points — know which one does what
+File Role When to touch it
+integration.py Main app — module testing, CAEN control, thermal camera, ring diagram Most development happens here
+cold.py Coldroom + MARTA monitoring GUI Cooling system changes
+coldroom/safety.py Safety interlock logic Has 4 HIGH bugs — treat carefully
+caen/caenGUI.py CAEN power supply widget HV/LV control changes
+power_supply/power_supply_ctrl.py Rigol bench supply panel Standalone power supply work
+The module test workflow (what operators actually do)
+
+1. Open integration.py
+2. Type ring name (e.g. "L1_47_A#1") → ring diagram draws
+3. Click a slot → Module ID fills automatically (from DB)
+4. Verify fiber (SfibA/B) and power (R01;M1.x) dropdowns are green (connected)
+5. Click [Check ID] → runs Ph2_ACF → confirms module is alive
+6. Click [HV OFF Test] → noise measurement, room lights on
+7. Click [HV ON Test] → dark noise + calibration
+8. Watch thermal plot for overheating (Tmax displayed live from thermal camera)
+9. Results auto-populate the noise table
+Between steps, the thermal camera and Ph2_ACF chip temperatures arrive continuously via MQTT and update the plot live.
+
+The codebase mental model
+integration.py (2272 lines) is the hub. It:
+
+Inherits from ui/integration_gui.py (auto-generated from Qt Designer .ui file — don't edit directly)
+Creates a ModuleDB widget, a CAENControl object, and a paho MQTT client
+Has one giant on_mqtt_message() callback handling ALL incoming MQTT topics
+Runs tests via CommandWorker(QThread) which wraps subprocess.run()
+coldroom/system.py is the hub for the coldroom side. It holds one shared _status dict that all subsystems read/write. MartaColdRoomMQTTClient fills it; safety.py reads it.
+
+db/module_db.py is the inventory tab — loads all modules from REST, filters them, shows details. All DB writes go through make_api_request() in integration.py.
+
+The bugs you need to fix first (priority order)
+These are in issues.md. Here's the action order for a new developer:
+
+1. Fix immediately (safety-critical, will crash in production)
+Bug File 1-line fix
+BUG-01 integration.py:792 self.caenGUI → self.caen (thermal shutdown is broken)
+BUG-03 coldroom/system.py:213 loop_start() → loop_stop()
+BUG-04 coldroom/command_worker.py:79 Switch to Popen so Cancel button works
+BUG-10 coldroom/safety.py:76,88,98 Remove tuple from return False, "..." → just return False
+BUG-12 coldroom/safety.py:139 Fix elapsed_time → compute from last_update
+2. Fix soon (safety logic broken but not immediately crashing)
+Bug File What's wrong
+BUG-11 coldroom/safety.py:82 Checks door_status key, reads CmdDoorUnlock_Reff — KeyError
+BUG-13 coldroom/safety.py:435 switch_all_lv_off() is commented out — LV never actually cuts
+BUG-09 db/module_db.py:352 Disconnect button crashes — missing argument
+3. Fix when you touch those areas
+The MEDIUM bugs (BUG-16 through BUG-26) won't crash immediately but cause subtle wrong behaviour — stale Cancel button, GUI freezes on slow network, wrong fuse ID lookup, camera name mismatch.
+
+Key PyQt5 patterns used in this project
+You'll encounter these everywhere:
+
+QThread for background work:
+
+
+class CommandWorker(QThread):
+ finished = pyqtSignal(int) # emits when done
+ def run(self):
+ result = subprocess.run(...)
+ self.finished.emit(result.returncode)
+Signals/slots for thread-safe GUI updates (the right pattern):
+
+
+# WRONG (BUG-14): updating widget from MQTT thread
+self.tMaxLabel.setText("45.2") # called from paho thread → random crashes
+
+# RIGHT: emit a signal, catch it on main thread
+self.temperatureUpdate.emit(45.2) # signal defined as pyqtSignal(float)
+QTimer for periodic polling:
+
+
+self.timer = QTimer()
+self.timer.timeout.connect(self.update)
+self.timer.start(2000) # fires every 2 seconds
+Files to read when you want to understand a specific area
+Topic Read this file
+CAEN protocol caen/caenGUI.py — look at tcp_util and CAENQueryThread
+Safety interlocks coldroom/safety.py — all functions are self-contained
+MQTT topics map settings_coldroom.yaml + section 11 of workflow.md
+Module DB fields db/utils.py + section 5 of workflow.md
+Test commands settings_integration.yaml — check_id_command, light_on_command, dark_test_command
+Ring diagram drawing integration.py — search for draw_ring()
+Thermal panorama coldroom/module_temperatures_gui.py — stitch_multiple_images()
+Suggested first week plan
+Day 1: Read workflow.md fully. Run grep -n "def " integration.py to map the main class methods.
+Day 2: Fix BUG-01 and BUG-03 (5 minutes each, enormous safety impact). Read safety.py fully.
+Day 3: Fix BUG-10, BUG-11, BUG-12, BUG-13 in safety.py — these are all in one file and related.
+Day 4: Fix BUG-04 (command_worker.py) and BUG-09 (module_db.py).
+Day 5+: Work through the MEDIUM bugs in issues.md as you encounter the related features.
+Ask me to explain or fix any specific bug, any file, or any concept — I have the full codebase context and can walk you through anything.
\ No newline at end of file
diff --git a/caen/caenGUI.py b/caen/caenGUI.py
index 744b7f6..35edf6f 100755
--- a/caen/caenGUI.py
+++ b/caen/caenGUI.py
@@ -1,18 +1,30 @@
#!/bin/env python3
-#create QT GUI with one button to send message to TCP server
+# create QT GUI with one button to send message to TCP server
import sys
-from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QTextEdit, QHBoxLayout, QFrame, QLabel
+from PyQt5.QtWidgets import (
+ QApplication,
+ QWidget,
+ QPushButton,
+ QVBoxLayout,
+ QTextEdit,
+ QHBoxLayout,
+ QFrame,
+ QLabel,
+)
from PyQt5.QtCore import pyqtSlot, QTimer, QThread, pyqtSignal, QObject
import socket
import json
+
BUFFER_SIZE = 100000
+
class CAENQueryThread(QThread):
"""Thread class for handling CAEN queries"""
+
dataReady = pyqtSignal(dict) # Signal to emit when data is received
error = pyqtSignal(str) # Signal to emit when error occurs
- def __init__(self, ip='192.168.0.45', port=7000):
+ def __init__(self, ip="192.168.0.45", port=7000):
super().__init__()
self.ip = ip
self.port = port
@@ -24,7 +36,7 @@ def setup_query(self, message, receive=False):
"""Setup the query to be executed"""
self.message = message
self.receive = receive
-
+
def stop(self):
"""Stop the thread"""
self.running = False
@@ -38,44 +50,46 @@ def run(self):
try:
tcpClass = tcp_util(ip=self.ip, port=self.port)
tcpClass.sendMessage(self.message)
-
+
if self.receive:
data = tcpClass.socket.recv(BUFFER_SIZE)
length = data[3] | (data[2] << 8) | (data[1] << 16) | (data[0] << 24)
- while len(data) < length :
- print("wait for more data",len(data), length)
- chunk=tcpClass.socket.recv(BUFFER_SIZE)
+ while len(data) < length:
+ print("wait for more data", len(data), length)
+ chunk = tcpClass.socket.recv(BUFFER_SIZE)
if not chunk:
break
- data+=chunk
- data=data[8:].decode("utf-8")
+ data += chunk
+ data = data[8:].decode("utf-8")
parsedData = {}
- for token in data.split(','):
- if token.startswith('caen'):
+ for token in data.split(","):
+ if token.startswith("caen"):
key, value = token.split(":")
value = float(value)
parsedData[key] = value
self.dataReady.emit(parsedData)
-
+
tcpClass.closeSocket()
-
+
except Exception as e:
self.error.emit(str(e))
-class tcp_util():
+
+class tcp_util:
"""Utility class for tcp
- comunication management. """
+ comunication management."""
+
def __init__(self, ip, port):
- self.ip = ip
- self.port = port
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.ip = ip
+ self.port = port
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.headerBytes = 4
self.connectSocket()
pass
- def __del__ (self):
+ def __del__(self):
"""Desctuctor, closes socket"""
try:
self.closeSocket()
@@ -84,7 +98,7 @@ def __del__ (self):
def connectSocket(self):
"""Connects socket"""
- self.socket.connect((self.ip,self.port))
+ self.socket.connect((self.ip, self.port))
pass
def closeSocket(self):
@@ -92,36 +106,37 @@ def closeSocket(self):
self.socket.close()
pass
- def sendMessage(self,message):
+ def sendMessage(self, message):
"""Encodes message and sends it on socket"""
encodedMessage = self.encodeMessage(message)
self.socket.send(encodedMessage)
pass
- def encodeMessage(self,message):
+ def encodeMessage(self, message):
"""Encodes message adding 4 bytes header"""
- messageLength = len(message) + self.headerBytes +4
- N=0
- encodedMessage = (messageLength).to_bytes(4, byteorder='big') + N.to_bytes(4, byteorder='big') + message.encode('utf-8')
+ messageLength = len(message) + self.headerBytes + 4
+ N = 0
+ encodedMessage = (
+ (messageLength).to_bytes(4, byteorder="big")
+ + N.to_bytes(4, byteorder="big")
+ + message.encode("utf-8")
+ )
return encodedMessage
+
from argparse import Namespace
+
class CAENControl(QObject):
"""CAEN control logic without UI elements"""
+
def __init__(self, ui):
super().__init__()
self.ui = ui
self.channels = {"LV": "LV7.1", "HV": "HV0.11"}
- self.led = {
- 'LV': self.ui.lvLed,
- 'HV': self.ui.hvLed
- }
- self.label = {
- 'LV': self.ui.lvLabel,
- 'HV': self.ui.hvLabel
- }
-
+ self.led = {"LV": self.ui.lvLed, "HV": self.ui.hvLed}
+ self.label = {"LV": self.ui.lvLabel, "HV": self.ui.hvLabel}
+
# Create query thread
self.queryThread = CAENQueryThread()
self.queryThread.dataReady.connect(self.handle_query_response)
@@ -130,27 +145,27 @@ def __init__(self, ui):
# Connect buttons
self.ui.lvOnButton.clicked.connect(lambda: self.on(self.channels["LV"]))
self.ui.lvOffButton.clicked.connect(lambda: self.safe_lv_off())
- self.ui.hvOnButton.clicked.connect(lambda: self.on(self.channels["HV"] ))
- self.ui.hvOffButton.clicked.connect(lambda: self.off(self.channels["HV"] ))
+ self.ui.hvOnButton.clicked.connect(lambda: self.on(self.channels["HV"]))
+ self.ui.hvOffButton.clicked.connect(lambda: self.off(self.channels["HV"]))
# Create timer for periodic update
self.timer = QTimer(self)
self.timer.timeout.connect(self.update)
self.timer.start(2000)
- self.lv_off_when_hv_off=False
+ self.lv_off_when_hv_off = False
def safe_lv_off(self):
- if self.lv_off_when_hv_off : # if the user insists we switch off
+ if self.lv_off_when_hv_off: # if the user insists we switch off
self.off(self.channels["LV"])
self.off(self.channels["HV"])
- self.lv_off_when_hv_off=True
+ self.lv_off_when_hv_off = True
- def setLV(self,channel_name):
- self.channels["LV"]=channel_name
+ def setLV(self, channel_name):
+ self.channels["LV"] = channel_name
- def setHV(self,channel_name):
- self.channels["HV"]=channel_name
+ def setHV(self, channel_name):
+ self.channels["HV"] = channel_name
def __del__(self):
"""Cleanup when widget is destroyed"""
@@ -159,38 +174,41 @@ def __del__(self):
@pyqtSlot()
def update(self):
"""Periodic update method"""
- self.queryThread.setup_query('GetStatus,PowerSupplyId:caen', True)
+ self.queryThread.setup_query("GetStatus,PowerSupplyId:caen", True)
self.queryThread.start()
def handle_query_response(self, data):
"""Handle the response from query thread"""
try:
- if data['caen_'+self.channels["LV"]+'_IsOn'] < 0.5:
- self.lv_off_when_hv_off = False
-
- for hl,channel in self.channels.items():
- if self.lv_off_when_hv_off :
- self.led[hl].setStyleSheet("background-color: yellow")
+ if data["caen_" + self.channels["LV"] + "_IsOn"] < 0.5:
+ self.lv_off_when_hv_off = False
+
+ for hl, channel in self.channels.items():
+ if self.lv_off_when_hv_off:
+ self.led[hl].setStyleSheet("background-color: yellow")
else:
- if data['caen_'+channel+'_IsOn'] > 0.5:
+ if data["caen_" + channel + "_IsOn"] > 0.5:
self.led[hl].setStyleSheet("background-color: green")
else:
self.led[hl].setStyleSheet("background-color: red")
- if hl=="HV":
+ if hl == "HV":
self.label[hl].setText(
- f'V: {data["caen_"+channel+"_Voltage"]:3.1f}V '
- f'C: {data["caen_"+channel+"_Current"]:2.1f}uA '
+ f'V: {data["caen_"+channel+"_Voltage"]:3.1f}V '
+ f'C: {data["caen_"+channel+"_Current"]:2.1f}uA '
)
# Check if we need to turn off LV based on HV voltage
- if self.lv_off_when_hv_off and data["caen_"+channel+"_Voltage"] <= 10.0:
+ if (
+ self.lv_off_when_hv_off
+ and data["caen_" + channel + "_Voltage"] <= 10.0
+ ):
print("Switching OFF LV ")
self.off(self.channels["LV"])
-
+
else:
self.label[hl].setText(
- f'V: {data["caen_"+channel+"_Voltage"]:2.1f}V '
- f'C: {data["caen_"+channel+"_Current"]:1.1f}A '
- f'P: {data["caen_"+channel+"_Voltage"]*data["caen_"+channel+"_Current"]:1.1f}W'
+ f'V: {data["caen_"+channel+"_Voltage"]:2.1f}V '
+ f'C: {data["caen_"+channel+"_Current"]:1.1f}A '
+ f'P: {data["caen_"+channel+"_Voltage"]*data["caen_"+channel+"_Current"]:1.1f}W'
)
except Exception as e:
print(f"Error handling response: {e}")
@@ -201,22 +219,22 @@ def handle_query_error(self, error_msg):
@pyqtSlot()
def on(self, channel):
- print(f'TurnOn,PowerSupplyId:caen,ChannelId:{channel}')
- self.queryThread.setup_query(f'TurnOn,PowerSupplyId:caen,ChannelId:{channel}')
+ print(f"TurnOn,PowerSupplyId:caen,ChannelId:{channel}")
+ self.queryThread.setup_query(f"TurnOn,PowerSupplyId:caen,ChannelId:{channel}")
self.queryThread.start()
@pyqtSlot()
def off(self, channel):
- print(f'TurnOff,PowerSupplyId:caen,ChannelId:{channel}')
- self.queryThread.setup_query(f'TurnOff,PowerSupplyId:caen,ChannelId:{channel}')
+ print(f"TurnOff,PowerSupplyId:caen,ChannelId:{channel}")
+ self.queryThread.setup_query(f"TurnOff,PowerSupplyId:caen,ChannelId:{channel}")
self.queryThread.start()
+
def main():
app = QApplication(sys.argv)
ex = caenGUI()
sys.exit(app.exec_())
-if __name__ == '__main__':
- main()
-
+if __name__ == "__main__":
+ main()
diff --git a/caen/caenGUIall.py b/caen/caenGUIall.py
index ec6839e..da28095 100755
--- a/caen/caenGUIall.py
+++ b/caen/caenGUIall.py
@@ -1,7 +1,16 @@
#!/bin/env python3
# create QT GUI with one button to send message to TCP server
import sys
-from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QTextEdit, QHBoxLayout, QFrame, QLabel
+from PyQt5.QtWidgets import (
+ QApplication,
+ QWidget,
+ QPushButton,
+ QVBoxLayout,
+ QTextEdit,
+ QHBoxLayout,
+ QFrame,
+ QLabel,
+)
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QFont
@@ -10,7 +19,16 @@
BUFFER_SIZE = 100000
import sys
-from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QTextEdit, QHBoxLayout, QFrame, QLabel
+from PyQt5.QtWidgets import (
+ QApplication,
+ QWidget,
+ QPushButton,
+ QVBoxLayout,
+ QTextEdit,
+ QHBoxLayout,
+ QFrame,
+ QLabel,
+)
from PyQt5.QtCore import pyqtSlot, QTimer, QThread, pyqtSignal, QObject
import socket
import json
@@ -31,14 +49,13 @@ def __init__(self, ip="192.168.0.45", port=7000):
self.message = None
self.receive = False
self.running = True
- self.queue =[]
+ self.queue = []
self.receiveQueue = []
def setup_query(self, message, receive=False):
"""Setup the query to be executed"""
self.queue.append(message)
self.receiveQueue.append(receive)
-
def stop(self):
"""Stop the thread"""
@@ -50,7 +67,7 @@ def run(self):
# print("Running query thread",self.message)
while self.queue:
- #create lock
+ # create lock
self.message = self.queue.pop(0)
self.receive = self.receiveQueue.pop(0)
try:
@@ -64,9 +81,14 @@ def run(self):
if not chunk:
break
data += chunk
- length = data[3] | (data[2] << 8) | (data[1] << 16) | (data[0] << 24)
- if len(data) >= length :
- break
+ length = (
+ data[3]
+ | (data[2] << 8)
+ | (data[1] << 16)
+ | (data[0] << 24)
+ )
+ if len(data) >= length:
+ break
except:
break
data = data[8:]
@@ -95,7 +117,7 @@ def __init__(self, ip, port):
self.ip = ip
self.port = port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-# self.socket.settimeout(0.5)
+ # self.socket.settimeout(0.5)
self.headerBytes = 4
self.connectSocket()
@@ -129,7 +151,9 @@ def encodeMessage(self, message):
messageLength = len(message) + self.headerBytes + 4
N = 0
encodedMessage = (
- (messageLength).to_bytes(4, byteorder="big") + N.to_bytes(4, byteorder="big") + message.encode("utf-8")
+ (messageLength).to_bytes(4, byteorder="big")
+ + N.to_bytes(4, byteorder="big")
+ + message.encode("utf-8")
)
return encodedMessage
@@ -191,11 +215,15 @@ def initUI(self):
hlayout.addWidget(l)
self.button = QPushButton("ON", self)
self.button.setMinimumWidth(30)
- self.button.clicked.connect(lambda checked, channel=channel: self.on(channel))
+ self.button.clicked.connect(
+ lambda checked, channel=channel: self.on(channel)
+ )
hlayout.addWidget(self.button)
self.button = QPushButton("OFF", self)
self.button.setMinimumWidth(30)
- self.button.clicked.connect(lambda checked, channel=channel: self.off(channel))
+ self.button.clicked.connect(
+ lambda checked, channel=channel: self.off(channel)
+ )
hlayout.addWidget(self.button)
# use QFrame as a led
self.label[channel] = QLabel("n/a")
@@ -220,7 +248,7 @@ def initUI(self):
def update(self):
"""Periodic update method"""
print("Update")
-
+
self.queryThread.setup_query("GetStatus,PowerSupplyId:caen", True)
# if thread not running, start it
if not self.queryThread.isRunning():
@@ -262,10 +290,10 @@ def handle_query_error(self, error_msg):
def on(self, channel):
print(f"TurnOn,PowerSupplyId:caen,ChannelId:{channel}")
self.queryThread.setup_query(f"TurnOn,PowerSupplyId:caen,ChannelId:{channel}")
- #if thread not running, start it
+ # if thread not running, start it
if not self.queryThread.isRunning():
print("Starting query thread")
- self.queryThread.start()
+ self.queryThread.start()
@pyqtSlot()
def off(self, channel):
@@ -273,36 +301,41 @@ def off(self, channel):
self.queryThread.setup_query(f"TurnOff,PowerSupplyId:caen,ChannelId:{channel}")
if not self.queryThread.isRunning():
print("Starting query thread")
- self.queryThread.start()
+ self.queryThread.start()
+
# self.queryThread.start()
- # def send(self,message,receive=False):
- # print(message)
- # tcpClass = tcp_util(ip='192.168.0.45',port=7000)
- # tcpClass.sendMessage(message)
- # try:
- # if receive :
- # data = tcpClass.socket.recv(BUFFER_SIZE)[8:].decode("utf-8")
- # print(data)
- # parsedData={}
- # for token in data.split(',') :
- # if token.startswith('caen'):
- # key,value=token.split(":")
- # value=float(value)
- # parsedData[key]=value
- # print(json.dumps(parsedData,indent=4),flush=True)
- # return parsedData
- # except:
- # print("Cannot parse")
+# def send(self,message,receive=False):
+# print(message)
+# tcpClass = tcp_util(ip='192.168.0.45',port=7000)
+# tcpClass.sendMessage(message)
+# try:
+# if receive :
+# data = tcpClass.socket.recv(BUFFER_SIZE)[8:].decode("utf-8")
+# print(data)
+# parsedData={}
+# for token in data.split(',') :
+# if token.startswith('caen'):
+# key,value=token.split(":")
+# value=float(value)
+# parsedData[key]=value
+# print(json.dumps(parsedData,indent=4),flush=True)
+# return parsedData
+# except:
+# print("Cannot parse")
import argparse
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="CAEN GUI")
- parser.add_argument("--ip", type=str, default="192.168.0.45", help="IP address of the CAEN server")
- parser.add_argument("--port", type=int, default=7000, help="Port of the CAEN server")
+ parser.add_argument(
+ "--ip", type=str, default="192.168.0.45", help="IP address of the CAEN server"
+ )
+ parser.add_argument(
+ "--port", type=int, default=7000, help="Port of the CAEN server"
+ )
args = parser.parse_args()
app = QApplication(sys.argv)
diff --git a/caen/caencli.py b/caen/caencli.py
index fcf99f4..f888370 100755
--- a/caen/caencli.py
+++ b/caen/caencli.py
@@ -9,6 +9,7 @@
class tcp_util:
"""Utility class for tcp communication management."""
+
def __init__(self, ip, port):
self.ip = ip
self.port = port
@@ -40,32 +41,55 @@ def encodeMessage(self, message):
"""Encodes message adding 4 bytes header"""
messageLength = len(message) + self.headerBytes + 4
N = 0
- encodedMessage = (messageLength).to_bytes(4, byteorder='big') + N.to_bytes(4, byteorder='big') + message.encode('utf-8')
+ encodedMessage = (
+ (messageLength).to_bytes(4, byteorder="big")
+ + N.to_bytes(4, byteorder="big")
+ + message.encode("utf-8")
+ )
return encodedMessage
+
class caen:
def on(self, channel, verbose=True):
- self.send('TurnOn,PowerSupplyId:caen,ChannelId:' + channel, verbose=verbose)
+ self.send("TurnOn,PowerSupplyId:caen,ChannelId:" + channel, verbose=verbose)
def off(self, channel, verbose=True):
- self.send('TurnOff,PowerSupplyId:caen,ChannelId:' + channel, verbose=verbose)
+ self.send("TurnOff,PowerSupplyId:caen,ChannelId:" + channel, verbose=verbose)
def send(self, message, receive=False, verbose=True):
- if verbose: print(str(datetime.datetime.now()), message)
- tcpClass = tcp_util(ip='192.168.0.45', port=7000)
+ if verbose:
+ print(str(datetime.datetime.now()), message)
+ tcpClass = tcp_util(ip="192.168.0.45", port=7000)
tcpClass.sendMessage(message)
if receive:
data = tcpClass.socket.recv(BUFFER_SIZE)[8:].decode("utf-8")
- if verbose: print(data)
+ if verbose:
+ print(data)
+
if __name__ == "__main__":
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
- parser.add_argument("-c", "--channel", dest="channel", required=True,
- help="channel number for LV Caen control. e.g. BLV02")
- parser.add_argument("--on", dest="action", action="store_const", const="on",
- help="turn on the specified channel")
- parser.add_argument("--off", dest="action", action="store_const", const="off",
- help="turn off the specified channel")
+ parser.add_argument(
+ "-c",
+ "--channel",
+ dest="channel",
+ required=True,
+ help="channel number for LV Caen control. e.g. BLV02",
+ )
+ parser.add_argument(
+ "--on",
+ dest="action",
+ action="store_const",
+ const="on",
+ help="turn on the specified channel",
+ )
+ parser.add_argument(
+ "--off",
+ dest="action",
+ action="store_const",
+ const="off",
+ help="turn off the specified channel",
+ )
args = parser.parse_args()
if not args.action:
@@ -77,5 +101,5 @@ def send(self, message, receive=False, verbose=True):
caen_controller.on(args.channel)
elif args.action == "off":
caen_controller.off(args.channel)
-
+
print("DONE")
diff --git a/cold.py b/cold.py
index 3005456..889c4e4 100755
--- a/cold.py
+++ b/cold.py
@@ -19,13 +19,16 @@
check_door_status,
check_light_safe_to_turn_on,
check_marta_safe,
+ soft_interlock_loop,
+ interpret_door_safety,
+ interpret_soft_interlock,
)
from caen.caenGUIall import caenGUIall
from Inner_tracker_GUI.caenGUIall_v2 import caenGUI8LV
from db.module_db import ModuleDB
+from power_supply.power_supply_ctrl import PowerSupplyController
from db.utils import *
-
# Configure logging
logger = logging.getLogger("integration")
@@ -49,6 +52,11 @@ def __init__(self):
self.update_timer.timeout.connect(self.update_ui)
self.update_timer.start(1000) # Update every second
+ # Setup soft interlock timer (5-second interval, independent of UI updates)
+ self.soft_interlock_timer = QTimer()
+ self.soft_interlock_timer.timeout.connect(self.soft_interlock_check)
+ self.soft_interlock_timer.start(5000)
+
# Connect to MQTT broker at startup
self.connect_mqtt()
@@ -62,6 +70,11 @@ def closeEvent(self, event):
self.update_timer.stop()
logger.debug("Stopped update timer")
+ # Stop the soft interlock timer
+ if hasattr(self, "soft_interlock_timer"):
+ self.soft_interlock_timer.stop()
+ logger.debug("Stopped soft interlock timer")
+
# Cleanup Thermal Camera tab if it exists
if hasattr(self, "thermal_camera_tab"):
self.thermal_camera_tab.cleanup()
@@ -125,7 +138,9 @@ def setup_ui(self):
# Load the MARTA Cold Room tab from UI file
# Create a temporary QMainWindow to load the UI
temp_window = QtWidgets.QMainWindow()
- marta_ui_file = os.path.join(os.path.dirname(__file__), "coldroom", "marta_coldroom.ui")
+ marta_ui_file = os.path.join(
+ os.path.dirname(__file__), "coldroom", "marta_coldroom.ui"
+ )
uic.loadUi(marta_ui_file, temp_window)
# Create a QWidget for our tab and get the central widget from temp_window
@@ -160,14 +175,18 @@ def setup_ui(self):
self.tab_widget.addTab(self.module_db.ui.tab_2, "Module Inventory")
self.tab_widget.addTab(self.module_db.ui.moduleDetailsTab, "Module Details")
self.module_db.ui.viewDetailsPB.clicked.connect(
- lambda: self.tab_widget.setCurrentIndex(self.tab_widget.indexOf(self.module_db.ui.moduleDetailsTab))
+ lambda: self.tab_widget.setCurrentIndex(
+ self.tab_widget.indexOf(self.module_db.ui.moduleDetailsTab)
+ )
)
self.module_db.ui.selectModulePB.setEnabled(False)
self.modules_list_tab.db_url = self.module_db.db_url
# Load settings tab from UI file
self.settings_tab = QtWidgets.QWidget()
- settings_ui_file = os.path.join(os.path.dirname(__file__), "coldroom", "settings_coldroom.ui")
+ settings_ui_file = os.path.join(
+ os.path.dirname(__file__), "coldroom", "settings_coldroom.ui"
+ )
uic.loadUi(settings_ui_file, self.settings_tab)
self.tab_widget.addTab(self.settings_tab, "Settings")
@@ -175,6 +194,10 @@ def setup_ui(self):
self.IT_caen_tab = caenGUI8LV()
self.tab_widget.addTab(self.IT_caen_tab, "IT CAEN")
+ # Add Power Supply tab
+ self.power_supply_tab = PowerSupplyController()
+ self.tab_widget.addTab(self.power_supply_tab, "Power Supply")
+
# Pre-fill settings with values from system
self.load_settings_to_ui()
self.get_ring_id()
@@ -190,10 +213,15 @@ def setup_ui(self):
def get_ring_id(self):
self.number_of_modules = 0 # Add default at the start
- self.ring_id=get_ring_from_cable("I1") #hardcode the single harting cable I1
+ # self.ring_id=get_ring_from_cable("I1") #hardcode the single harting cable I1
+ self.ring_id = get_ring_from_cable(
+ "I1", db_url=self.module_db.db_url
+ ) # hardcode the single harting cable I1
if self.ring_id == None:
- ring_history_file = os.path.join(os.path.dirname(__file__), "ring_history.txt")
-
+ ring_history_file = os.path.join(
+ os.path.dirname(__file__), "ring_history.txt"
+ )
+
if os.path.exists(ring_history_file):
with open(ring_history_file, "r") as f:
lines = f.readlines()
@@ -201,8 +229,8 @@ def get_ring_id(self):
self.ring_id = lines[-1].strip()
else:
self.ring_id = None
-
- #TODO: move this to the right place so it works also when ring ID LineEdit is updated
+
+ # TODO: move this to the right place so it works also when ring ID LineEdit is updated
if self.ring_id != None:
self.modules_list_tab.ring_id_LE.setText(self.ring_id)
logger.info(f"Loaded ring ID from history: {self.ring_id}")
@@ -214,7 +242,10 @@ def get_ring_id(self):
elif self.ring_id.startswith("L3_"):
self.number_of_modules = 36
self.modules_list_tab.populate_from_config(
- self.caen_tab, self.mounted_modules, self.number_of_modules, self.thermal_camera_tab
+ self.caen_tab,
+ self.mounted_modules,
+ self.number_of_modules,
+ self.thermal_camera_tab,
)
return self.ring_id
@@ -232,9 +263,13 @@ def setup_ring_id(self):
elif self.ring_id.startswith("L3_"):
self.number_of_modules = 36
else:
- self.message_box.setText("Invalid ring ID format. Must start with L1_, L2_, or L3_.")
+ self.message_box.setText(
+ "Invalid ring ID format. Must start with L1_, L2_, or L3_."
+ )
self.message_box.exec_()
- self.modules_list_tab.populate_from_config(self.caen_tab, self.mounted_modules, self.number_of_modules)
+ self.modules_list_tab.populate_from_config(
+ self.caen_tab, self.mounted_modules, self.number_of_modules
+ )
def save_ring_id(self):
ring_history_file = os.path.join(os.path.dirname(__file__), "ring_history.txt")
@@ -243,22 +278,30 @@ def save_ring_id(self):
logger.info(f"Ring ID {self.ring_id} saved successfully.")
def get_mounted_modules(self):
- self.mounted_modules = get_modules_on_ring(self.ring_id, db_url=self.module_db.db_url)
+ self.mounted_modules = get_modules_on_ring(
+ self.ring_id, db_url=self.module_db.db_url
+ )
for module_name in self.mounted_modules:
- self.mounted_modules[module_name].update(get_module_endpoints(module_name, db_url=self.module_db.db_url))
self.mounted_modules[module_name].update(
- {"speed": get_module_speed(module_name, db_url=self.module_db.db_url)}
+ get_module_endpoints(module_name, db_url=self.module_db.db_url)
)
self.mounted_modules[module_name].update(
- {"fuseId": get_module_fuse_id(module_name, db_url=self.module_db.db_url)}
+ {"speed": get_module_speed(module_name, db_url=self.module_db.db_url)}
)
self.mounted_modules[module_name].update(
{
- "temperature_offsets": get_module(module_name, db_url=self.module_db.db_url).get(
- "temperature_offsets", {}
+ "fuseId": get_module_fuse_id(
+ module_name, db_url=self.module_db.db_url
)
}
)
+ self.mounted_modules[module_name].update(
+ {
+ "temperature_offsets": get_module(
+ module_name, db_url=self.module_db.db_url
+ ).get("temperature_offsets", {})
+ }
+ )
logger.debug(f"Mounted modules for ring {self.ring_id}: {self.mounted_modules}")
return self.mounted_modules
@@ -266,11 +309,21 @@ def load_settings_to_ui(self):
# Fill settings UI with current values
self.settings_tab.brokerLineEdit.setText(self.system.settings["mqtt"]["broker"])
self.settings_tab.portSpinBox.setValue(self.system.settings["mqtt"]["port"])
- self.settings_tab.martaTopicLineEdit.setText(self.system.settings["MARTA"]["mqtt_topic"])
- self.settings_tab.coldroomTopicLineEdit.setText(self.system.settings["Coldroom"]["mqtt_topic"])
- self.settings_tab.co2SensorTopicLineEdit.setText(self.system.settings["Coldroom"]["co2_sensor_topic"])
- self.settings_tab.thermalCameraTopicLineEdit.setText(self.system.settings["ThermalCamera"]["mqtt_topic"])
- self.settings_tab.cleanroomTopicLineEdit.setText(self.system.settings["Cleanroom"]["mqtt_topic"])
+ self.settings_tab.martaTopicLineEdit.setText(
+ self.system.settings["MARTA"]["mqtt_topic"]
+ )
+ self.settings_tab.coldroomTopicLineEdit.setText(
+ self.system.settings["Coldroom"]["mqtt_topic"]
+ )
+ self.settings_tab.co2SensorTopicLineEdit.setText(
+ self.system.settings["Coldroom"]["co2_sensor_topic"]
+ )
+ self.settings_tab.thermalCameraTopicLineEdit.setText(
+ self.system.settings["ThermalCamera"]["mqtt_topic"]
+ )
+ self.settings_tab.cleanroomTopicLineEdit.setText(
+ self.system.settings["Cleanroom"]["mqtt_topic"]
+ )
def connect_signals(self):
# Module list
@@ -287,66 +340,88 @@ def configure_line_edit(field_name, placeholder):
le.setStyleSheet("QLineEdit { color: grey; }")
# Connect signals
- le.textChanged.connect(lambda: le.setStyleSheet("QLineEdit { color: black; }"))
- le.editingFinished.connect(lambda: le.setPlaceholderText(placeholder) if le.text() == "" else None)
+ le.textChanged.connect(
+ lambda: le.setStyleSheet("QLineEdit { color: black; }")
+ )
+ le.editingFinished.connect(
+ lambda: (
+ le.setPlaceholderText(placeholder) if le.text() == "" else None
+ )
+ )
# Light controls
- self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_light_on_PB").clicked.connect(
- self.coldroom_light_on
- )
- self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_light_off_PB").clicked.connect(
- self.coldroom_light_off
- )
+ self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_light_on_PB"
+ ).clicked.connect(self.coldroom_light_on)
+ self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_light_off_PB"
+ ).clicked.connect(self.coldroom_light_off)
# Dry air controls
- self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_dry_air_on_PB").clicked.connect(
- self.coldroom_dry_air_on
- )
- self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_dry_air_off_PB").clicked.connect(
- self.coldroom_dry_air_off
- )
+ self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_dry_air_on_PB"
+ ).clicked.connect(self.coldroom_dry_air_on)
+ self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_dry_air_off_PB"
+ ).clicked.connect(self.coldroom_dry_air_off)
# Dry air bypass controls
- self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_dry_air_bypass_on_PB").clicked.connect(
- self.coldroom_dry_air_bypass_on
- )
- self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_dry_air_bypass_off_PB").clicked.connect(
- self.coldroom_dry_air_bypass_off
- )
+ self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_dry_air_bypass_on_PB"
+ ).clicked.connect(self.coldroom_dry_air_bypass_on)
+ self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_dry_air_bypass_off_PB"
+ ).clicked.connect(self.coldroom_dry_air_bypass_off)
# Door controls
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_door_toggle_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_door_toggle_PB"
+ )
if button:
button.clicked.connect(self.toggle_coldroom_door)
# Temperature controls
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_temp_ctrl_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_temp_ctrl_PB"
+ )
if button:
button.clicked.connect(self.toggle_coldroom_temp_control)
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_run_start")
- if button:
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_run_start"
+ )
+ if button and self.system._martacoldroom:
button.clicked.connect(self.system._martacoldroom.run)
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_run_stop")
- if button:
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_run_stop"
+ )
+ if button and self.system._martacoldroom:
button.clicked.connect(self.system._martacoldroom.stop)
configure_line_edit("coldroom_temp_LE", "-30°C to 30°C")
configure_line_edit("coldroom_humidity_LE", "0% to 50%")
# Temperature setpoint label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "coldroom_temp_set_point_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "coldroom_temp_set_point_label"
+ )
if label:
logger.debug("Connected temperature set point label")
# Humidity setpoint label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "coldroom_humidity_set_point_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "coldroom_humidity_set_point_label"
+ )
if label:
logger.debug("Connected humidity set point label")
# Temperature control LED
- ctrl_temp_led = self.marta_coldroom_tab.findChild(QtWidgets.QFrame, "ctrl_temp_LED")
+ ctrl_temp_led = self.marta_coldroom_tab.findChild(
+ QtWidgets.QFrame, "ctrl_temp_LED"
+ )
if ctrl_temp_led:
logger.debug("Connected temperature control LED")
# Humidity control LED
- ctrl_humidity_led = self.marta_coldroom_tab.findChild(QtWidgets.QFrame, "ctrl_humidity_LED")
+ ctrl_humidity_led = self.marta_coldroom_tab.findChild(
+ QtWidgets.QFrame, "ctrl_humidity_LED"
+ )
if ctrl_humidity_led:
logger.debug("Connected humidity control LED")
# Light control LED
@@ -358,136 +433,206 @@ def configure_line_edit(field_name, placeholder):
if dry_air_led:
logger.debug("Connected dry air control LED")
# Safe to open LED
- safe_to_open_led = self.marta_coldroom_tab.findChild(QtWidgets.QFrame, "safe_to_open_LED")
+ safe_to_open_led = self.marta_coldroom_tab.findChild(
+ QtWidgets.QFrame, "safe_to_open_LED"
+ )
if safe_to_open_led:
logger.debug("Connected safe to open LED")
# Door state label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "coldroom_door_state_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "coldroom_door_state_label"
+ )
if label:
logger.debug("Connected door state label")
+ # Soft interlock LED
+ soft_interlock_led = self.marta_coldroom_tab.findChild(
+ QtWidgets.QFrame, "soft_interlock_LED"
+ )
+ if soft_interlock_led:
+ logger.debug("Connected soft interlock LED")
+ # Soft interlock message label
+ soft_interlock_msg = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "soft_interlock_msg"
+ )
+ if soft_interlock_msg:
+ logger.debug("Connected soft interlock message label")
# Temperature setpoint controls
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_temp_set_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_temp_set_PB"
+ )
if button:
button.clicked.connect(self.set_coldroom_temperature)
# Humidity controls
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_humidity_ctrl_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_humidity_ctrl_PB"
+ )
if button:
button.clicked.connect(self.toggle_coldroom_humidity_control)
# Humidity setpoint controls
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_humidity_set_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_humidity_set_PB"
+ )
if button:
button.clicked.connect(self.set_coldroom_humidity)
# Run controls
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_run_toggle_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_run_toggle_PB"
+ )
if button:
button.clicked.connect(self.toggle_coldroom_run)
# Reset alarms
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "coldroom_reset_alarms_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "coldroom_reset_alarms_PB"
+ )
if button:
button.clicked.connect(self.reset_coldroom_alarms)
# MARTA CO2 Plant controls
# Temperature controls
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_temp_set_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_temp_set_PB"
+ )
if button:
button.clicked.connect(self.set_marta_temperature)
- configure_line_edit("marta_temp_LE", "-30°C to 18°C") # Placeholder for temperature input
+ configure_line_edit(
+ "marta_temp_LE", "-30°C to 18°C"
+ ) # Placeholder for temperature input
# Speed controls
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_speed_set_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_speed_set_PB"
+ )
if button:
button.clicked.connect(self.set_marta_speed)
- configure_line_edit("marta_speed_LE", "5000 to 6000 RPM") # Placeholder for speed input
+ configure_line_edit(
+ "marta_speed_LE", "5000 to 6000 RPM"
+ ) # Placeholder for speed input
# Flow controls
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_flow_set_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_flow_set_PB"
+ )
if button:
button.clicked.connect(self.set_marta_flow)
- configure_line_edit("marta_flow_LE", "0 to 5 L/min") # Placeholder for flow input
+ configure_line_edit(
+ "marta_flow_LE", "0 to 5 L/min"
+ ) # Placeholder for flow input
# Supply temperature label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "marta_temp_supply_value_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "marta_temp_supply_value_label"
+ )
if label:
logger.debug("Connected supply temperature label")
# Return temperature label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "marta_temp_return_value_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "marta_temp_return_value_label"
+ )
if label:
logger.debug("Connected return temperature label")
# Supply pressure label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "marta_pressure_supply_value_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "marta_pressure_supply_value_label"
+ )
if label:
logger.debug("Connected supply pressure label")
# Return pressure label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "marta_pressure_return_value_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "marta_pressure_return_value_label"
+ )
if label:
logger.debug("Connected return pressure label")
# Speed label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "marta_speed_value_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "marta_speed_value_label"
+ )
if label:
logger.debug("Connected speed label")
# Temperature Set Point Label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "marta_temp_set_point_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "marta_temp_set_point_label"
+ )
if label:
logger.debug("Connected temperature set point label")
# Speed Set Point Label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "marta_speed_set_point_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "marta_speed_set_point_label"
+ )
if label:
logger.debug("Connected speed set point label")
# Flow Set Point Label
- label = self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "marta_flow_set_point_label")
+ label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "marta_flow_set_point_label"
+ )
if label:
logger.debug("Connected flow set point label")
# Other MARTA controls
# Start chiller button
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_chiller_start_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_chiller_start_PB"
+ )
if button:
button.clicked.connect(self.start_marta_chiller)
# Start CO2 button
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_co2_start_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_co2_start_PB"
+ )
if button:
button.clicked.connect(self.start_marta_co2)
# Stop CO2 button
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_co2_stop_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_co2_stop_PB"
+ )
if button:
button.clicked.connect(self.stop_marta_co2)
# Stop chiller button
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_chiller_stop_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_chiller_stop_PB"
+ )
if button:
button.clicked.connect(self.stop_marta_chiller)
# Clear alarms button
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_alarms_clear_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_alarms_clear_PB"
+ )
if button:
button.clicked.connect(self.clear_marta_alarms)
# Reconnect button
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_reconnect_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_reconnect_PB"
+ )
if button:
button.clicked.connect(self.reconnect_marta)
# Refresh button
- button = self.marta_coldroom_tab.findChild(QtWidgets.QPushButton, "marta_refresh_PB")
+ button = self.marta_coldroom_tab.findChild(
+ QtWidgets.QPushButton, "marta_refresh_PB"
+ )
if button:
button.clicked.connect(self.refresh_marta)
# Flow active checkbox
- checkbox = self.marta_coldroom_tab.findChild(QtWidgets.QCheckBox, "marta_flow_active_CB")
+ checkbox = self.marta_coldroom_tab.findChild(
+ QtWidgets.QCheckBox, "marta_flow_active_CB"
+ )
if checkbox:
checkbox.clicked.connect(self.toggle_marta_flow_active)
@@ -501,28 +646,40 @@ def configure_line_edit(field_name, placeholder):
logger.debug(f"Found checkbox: {checkbox.objectName()}")
# Update validators to match placeholder ranges
- temp_lineedit_coldroom = self.marta_coldroom_tab.findChild(QtWidgets.QLineEdit, "coldroom_temp_LE")
+ temp_lineedit_coldroom = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLineEdit, "coldroom_temp_LE"
+ )
if temp_lineedit_coldroom:
- temp_lineedit_coldroom.setValidator(QtGui.QDoubleValidator(-30, 30, 2)) # Matches placeholder
+ temp_lineedit_coldroom.setValidator(
+ QtGui.QDoubleValidator(-30, 30, 2)
+ ) # Matches placeholder
- humid_lineedit_coldroom = self.marta_coldroom_tab.findChild(QtWidgets.QLineEdit, "coldroom_humidity_LE")
+ humid_lineedit_coldroom = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLineEdit, "coldroom_humidity_LE"
+ )
if humid_lineedit_coldroom:
humid_lineedit_coldroom.setValidator(QtGui.QDoubleValidator(0, 50, 2))
# Add input validation for numeric fields
- temp_lineedit = self.marta_coldroom_tab.findChild(QtWidgets.QLineEdit, "marta_temp_LE")
+ temp_lineedit = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLineEdit, "marta_temp_LE"
+ )
if temp_lineedit:
validator = QtGui.QDoubleValidator(-30, 18, 2) # min, max, decimals
temp_lineedit.setValidator(validator)
# Add input validation for numeric fields
- speed_lineedit = self.marta_coldroom_tab.findChild(QtWidgets.QLineEdit, "marta_speed_LE")
+ speed_lineedit = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLineEdit, "marta_speed_LE"
+ )
if speed_lineedit:
validator = QtGui.QDoubleValidator(5000, 6000, 0) # min, max, decimals
speed_lineedit.setValidator(validator)
# Add input validation for numeric fields
- flow_lineedit = self.marta_coldroom_tab.findChild(QtWidgets.QLineEdit, "marta_flow_LE")
+ flow_lineedit = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLineEdit, "marta_flow_LE"
+ )
if flow_lineedit:
validator = QtGui.QDoubleValidator(0, 5, 2) # min, max, decimals
flow_lineedit.setValidator(validator)
@@ -551,15 +708,72 @@ def connect_mqtt(self):
self.statusBar().showMessage(error_msg)
logger.error(error_msg)
+ def soft_interlock_check(self):
+ logger.info("Called soft Interlock")
+ used_caen_channels = self.modules_list_tab.get_used_channels()
+ logger.info(f"Used CAEN channels for safety checks: {used_caen_channels}")
+ alarm_publish = (
+ (lambda msg: self.system._martacoldroom._client.publish("/alarm", msg))
+ if self.system._martacoldroom
+ else None
+ )
+ logger.info(f"Alarm publish function: {alarm_publish}")
+ is_safe, msg = soft_interlock_loop(
+ self.system.status,
+ self.caen_tab.last_response,
+ used_caen_channels,
+ self.caen_tab,
+ publish_alarm=alarm_publish,
+ )
+ logger.info(f"Soft interlock result: is_safe={is_safe}, msg={msg}")
+
+ soft_interlock_led = self.marta_coldroom_tab.findChild(
+ QtWidgets.QFrame, "soft_interlock_LED"
+ )
+ if soft_interlock_led:
+ status_color = "green" if is_safe else "red"
+ soft_interlock_led.setStyleSheet("background-color: white;")
+ QTimer.singleShot(
+ 500,
+ lambda led=soft_interlock_led, color=status_color: led.setStyleSheet(
+ f"background-color: {color};"
+ ),
+ )
+ logger.info(f"Updated soft interlock LED: {status_color} with pulse")
+
+ verdict = interpret_soft_interlock(is_safe)
+
+ # Also show the verdict as the LED caption so the light is self-explanatory
+ soft_interlock_caption = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "soft_interlock_label"
+ )
+ if soft_interlock_caption:
+ caption = "Soft Interlock\n" + ("OK" if is_safe else "TRIPPED")
+ soft_interlock_caption.setText(caption)
+ soft_interlock_caption.setStyleSheet(
+ "color: #1a7f37;" if is_safe else "color: #c0362c;"
+ )
+
+ soft_interlock_msg_label = self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "soft_interlock_msg"
+ )
+ if soft_interlock_msg_label:
+ soft_interlock_msg_label.setText(f"{verdict}\n{msg}")
+ logger.info(f"Updated soft interlock message: {verdict} | {msg}")
+
+ logger.info("Completed soft interlock loop")
+
def update_ui(self):
"""Update UI with current system status"""
try:
- self.system._martacoldroom._cleanroom_last_update_elapsed_time = (
- time.time() - self.system._martacoldroom._cleanroom_last_update_timer
- )
- self.system.status["cleanroom"][
- "elapsed_time"
- ] = self.system._martacoldroom._cleanroom_last_update_elapsed_time
+ if self.system._martacoldroom:
+ self.system._martacoldroom._cleanroom_last_update_elapsed_time = (
+ time.time()
+ - self.system._martacoldroom._cleanroom_last_update_timer
+ )
+ self.system.status["cleanroom"][
+ "elapsed_time"
+ ] = self.system._martacoldroom._cleanroom_last_update_elapsed_time
self.modules_list_tab.light_on = check_light_status(self.system.status)
# Get the central widget
central = self.marta_coldroom_tab
@@ -573,43 +787,79 @@ def update_ui(self):
logger.debug(f"Updating Cleanroom UI with status: {cleanroom}")
# Temperature
- label = central.findChild(QtWidgets.QLabel, "cleanroom_temp_value_label")
- if label and "temperature" in cleanroom and cleanroom["temperature"] is not None:
+ label = central.findChild(
+ QtWidgets.QLabel, "cleanroom_temp_value_label"
+ )
+ if (
+ label
+ and "temperature" in cleanroom
+ and cleanroom["temperature"] is not None
+ ):
temp_value = cleanroom["temperature"]
label.setText(f"{temp_value:.1f}")
logger.debug(f"Updated Cleanroom temperature: {temp_value}")
# Humidity
- label = central.findChild(QtWidgets.QLabel, "cleanroom_humidity_value_label")
- if label and "humidity" in cleanroom and cleanroom["humidity"] is not None:
+ label = central.findChild(
+ QtWidgets.QLabel, "cleanroom_humidity_value_label"
+ )
+ if (
+ label
+ and "humidity" in cleanroom
+ and cleanroom["humidity"] is not None
+ ):
humid_value = cleanroom["humidity"]
label.setText(f"{humid_value:.1f}")
logger.debug(f"Updated Cleanroom humidity: {humid_value}")
# Dewpoint
- label = central.findChild(QtWidgets.QLabel, "cleanroom_dewpoint_value_label")
- if label and "dewpoint" in cleanroom and cleanroom["dewpoint"] is not None:
+ label = central.findChild(
+ QtWidgets.QLabel, "cleanroom_dewpoint_value_label"
+ )
+ if (
+ label
+ and "dewpoint" in cleanroom
+ and cleanroom["dewpoint"] is not None
+ ):
dewpoint = cleanroom["dewpoint"]
label.setText(f"{dewpoint:.1f}")
logger.debug(f"Updated Cleanroom dewpoint: {dewpoint}")
# Pressure
- label = central.findChild(QtWidgets.QLabel, "cleanroom_pressure_value_label")
- if label and "pressure" in cleanroom and cleanroom["pressure"] is not None:
+ label = central.findChild(
+ QtWidgets.QLabel, "cleanroom_pressure_value_label"
+ )
+ if (
+ label
+ and "pressure" in cleanroom
+ and cleanroom["pressure"] is not None
+ ):
pressure = cleanroom["pressure"]
label.setText(f"{pressure:.1f}")
logger.debug(f"Updated Cleanroom pressure: {pressure}")
- label = central.findChild(QtWidgets.QLabel, "cleanroom_last_update_value_label")
- if label and "last_update" in cleanroom and cleanroom["last_update"] is not None:
+ label = central.findChild(
+ QtWidgets.QLabel, "cleanroom_last_update_value_label"
+ )
+ if (
+ label
+ and "last_update" in cleanroom
+ and cleanroom["last_update"] is not None
+ ):
last_update = cleanroom["last_update"]
label.setText(f"{last_update}")
logger.debug(f"Updated Cleanroom last update: {last_update}")
- label = central.findChild(QtWidgets.QLabel, "cleanroom_last_update_value_label_2")
+ label = central.findChild(
+ QtWidgets.QLabel, "cleanroom_last_update_value_label_2"
+ )
if label:
- delta_t_update = self.system._martacoldroom._cleanroom_last_update_elapsed_time
- delta_t_update = time.strftime("%H:%M:%S", time.gmtime(delta_t_update))
+ delta_t_update = (
+ self.system._martacoldroom._cleanroom_last_update_elapsed_time
+ )
+ delta_t_update = time.strftime(
+ "%H:%M:%S", time.gmtime(delta_t_update)
+ )
label.setText(delta_t_update)
logger.debug(f"Updated Cleanroom delta t update: {delta_t_update}")
@@ -621,15 +871,21 @@ def update_ui(self):
coldroom = self.system.status["coldroom"]
logger.debug(f"Updating Coldroom UI with status: {coldroom}")
- temp_control_active = coldroom.get("ch_temperature", {}).get("status", False)
- hum_control_active = coldroom.get("ch_humidity", {}).get("status", False)
+ temp_control_active = coldroom.get("ch_temperature", {}).get(
+ "status", False
+ )
+ hum_control_active = coldroom.get("ch_humidity", {}).get(
+ "status", False
+ )
# =========================================================================================== COLDROOM TEMPERATURE PROCESS ===========================================================================================
# Temperature Current Value
if "ch_temperature" in coldroom:
# Current temperature
- label = central.findChild(QtWidgets.QLabel, "coldroom_temp_value_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "coldroom_temp_value_label"
+ )
if label:
temp_value = coldroom["ch_temperature"].get("value", "?")
label.setText(f"{temp_value:.1f}")
@@ -639,7 +895,9 @@ def update_ui(self):
if "ch_temperature" in coldroom:
if "status" in coldroom["ch_temperature"]:
# Update Temperature control LED
- ctrl_temp_led = central.findChild(QtWidgets.QFrame, "ctrl_temp_LED")
+ ctrl_temp_led = central.findChild(
+ QtWidgets.QFrame, "ctrl_temp_LED"
+ )
if ctrl_temp_led:
ctrl_temp_led.setStyleSheet(
"background-color: green;"
@@ -671,7 +929,9 @@ def update_ui(self):
# Temperature setpoint label
if "ch_temperature" in coldroom:
if "setpoint" in coldroom["ch_temperature"]:
- label = central.findChild(QtWidgets.QLabel, "coldroom_temp_set_point_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "coldroom_temp_set_point_label"
+ )
if label:
temp_setpoint = coldroom["ch_temperature"]["setpoint"]
label.setText(f"{temp_setpoint:.1f}")
@@ -682,7 +942,9 @@ def update_ui(self):
# Humidity Current Value
if "ch_humidity" in coldroom:
# Current humidity
- label = central.findChild(QtWidgets.QLabel, "coldroom_humidity_value_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "coldroom_humidity_value_label"
+ )
if label:
humid_value = coldroom["ch_humidity"].get("value", "?")
label.setText(f"{humid_value:.1f}")
@@ -692,7 +954,9 @@ def update_ui(self):
if "ch_humidity" in coldroom:
if "status" in coldroom["ch_humidity"]:
# Update Humidity control LED
- ctrl_humidity_led = central.findChild(QtWidgets.QFrame, "ctrl_humidity_LED")
+ ctrl_humidity_led = central.findChild(
+ QtWidgets.QFrame, "ctrl_humidity_LED"
+ )
if ctrl_humidity_led:
ctrl_humidity_led.setStyleSheet(
"background-color: green;"
@@ -704,7 +968,9 @@ def update_ui(self):
)
# Humidity setpoint lineedit
- lineedit = central.findChild(QtWidgets.QLineEdit, "coldroom_humidity_LE")
+ lineedit = central.findChild(
+ QtWidgets.QLineEdit, "coldroom_humidity_LE"
+ )
if lineedit:
user_has_entered_value = bool(lineedit.text().strip())
# Case 1: User is actively typing - preserve their input
@@ -724,7 +990,9 @@ def update_ui(self):
# Humidity setpoint label
if "ch_humidity" in coldroom:
if "setpoint" in coldroom["ch_humidity"]:
- label = central.findChild(QtWidgets.QLabel, "coldroom_humidity_set_point_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "coldroom_humidity_set_point_label"
+ )
if label:
humid_setpoint = coldroom["ch_humidity"]["setpoint"]
label.setText(f"{humid_setpoint:.1f}")
@@ -732,7 +1000,9 @@ def update_ui(self):
# =========================================================================================== COLDROOM DEWPOINT PROCESS ===========================================================================================
# Dewpoint (from coldroom data)
- label = central.findChild(QtWidgets.QLabel, "coldroom_dewpoint_value_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "coldroom_dewpoint_value_label"
+ )
if label and "dew_point_c" in coldroom:
dewpoint = coldroom["dew_point_c"]
label.setText(f"{dewpoint:.1f}")
@@ -745,32 +1015,50 @@ def update_ui(self):
light_led = central.findChild(QtWidgets.QFrame, "light_LED")
if light_led:
light_led.setStyleSheet(
- "background-color: yellow;" if coldroom["light"] else "background-color: black;"
+ "background-color: yellow;"
+ if coldroom["light"]
+ else "background-color: black;"
) # LED is yellow when ON
- logger.debug(f"Updated light LED: {'yellow' if coldroom['light'] else 'black'}")
+ logger.debug(
+ f"Updated light LED: {'yellow' if coldroom['light'] else 'black'}"
+ )
if bool(coldroom["light"]) is False:
logger.debug("Light off, check if it is safe")
- used_caen_channels = self.modules_list_tab.get_used_channels()
+ used_caen_channels = (
+ self.modules_list_tab.get_used_channels()
+ )
is_safe = check_light_safe_to_turn_on(
- self.system.status, self.caen_tab.last_response, used_caen_channels
+ self.system.status,
+ self.caen_tab.last_response,
+ used_caen_channels,
+ )
+ button = central.findChild(
+ QtWidgets.QPushButton, "coldroom_light_on_PB"
)
- button = central.findChild(QtWidgets.QPushButton, "coldroom_light_on_PB")
if not is_safe:
logger.debug("not safe")
if button:
button.setEnabled(False)
- logger.debug("Disabled light button due to safety check")
+ logger.debug(
+ "Disabled light button due to safety check"
+ )
else:
if button:
button.setEnabled(True)
- logger.debug("Enabled light button after safety check")
+ logger.debug(
+ "Enabled light button after safety check"
+ )
# Dry air bypass
- dryair_bypass_led = central.findChild(QtWidgets.QFrame, "dryair_bypass_LED")
+ dryair_bypass_led = central.findChild(
+ QtWidgets.QFrame, "dryair_bypass_LED"
+ )
coldroom_air = self.system.status.get("coldroomair", {})
if dryair_bypass_led:
if "air_bypass_status" in coldroom_air:
dryair_bypass_led.setStyleSheet(
- "background-color: green;" if coldroom_air["air_bypass_status"] else "background-color: red;"
+ "background-color: green;"
+ if coldroom_air["air_bypass_status"]
+ else "background-color: red;"
)
logger.debug(
f"Updated dry air bypass LED: {'green' if coldroom_air['air_bypass_status'] else 'red'}"
@@ -783,37 +1071,74 @@ def update_ui(self):
# Door status and LED
if "CmdDoorUnlock_Reff" in coldroom:
door_status = "OPEN" if coldroom["CmdDoorUnlock_Reff"] else "CLOSED"
- label = central.findChild(QtWidgets.QLabel, "coldroom_door_state_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "coldroom_door_state_label"
+ )
if label:
label.setText(door_status)
logger.debug(f"Updated door status: {door_status}")
# =========================================================================================== COLDROOM RUN PROCESS ===========================================================================================
# Update safe to open LED based on door safety check
- safe_to_open_led = central.findChild(QtWidgets.QFrame, "safe_to_open_LED")
+ safe_to_open_led = central.findChild(
+ QtWidgets.QFrame, "safe_to_open_LED"
+ )
if safe_to_open_led:
used_caen_channels = self.modules_list_tab.get_used_channels()
is_safe, door_msg = check_door_safe_to_open(
- self.system.status, self.caen_tab.last_response, used_caen_channels
+ self.system.status,
+ self.caen_tab.last_response,
+ used_caen_channels,
)
# Check co2 values
if "co2_sensor" in self.system.status:
co2_data = self.system.status["co2_sensor"]
if "CO2" in co2_data:
- if co2_data["CO2"] > 800:
+ ppm = co2_data["CO2"]
+ if ppm > 800:
is_safe = False
- door_msg += "CO2 levels safe: False"
+ door_msg += f"CO2 levels safe: NO (current {ppm:.0f} ppm > 800 ppm threshold — risk of oxygen depletion)\n"
else:
- door_msg += "CO2 levels safe: True"
- safe_to_open_led.setStyleSheet("background-color: green;" if is_safe else "background-color: red;")
- logger.debug(f"Updated safe to open LED: {'green' if is_safe else 'red'} (is_safe={is_safe})")
- self.system._martacoldroom.publish_door_safety_status(is_safe)
- self.marta_coldroom_tab.findChild(QtWidgets.QLabel, "door_safety_msg").setText(door_msg)
+ door_msg += f"CO2 levels safe: YES (current {ppm:.0f} ppm < 800 ppm threshold)\n"
+ else:
+ door_msg += "CO2 levels safe: UNKNOWN (sensor present but no CO2 reading)\n"
+ else:
+ door_msg += "CO2 levels safe: UNKNOWN (CO2 sensor data not available)\n"
+ safe_to_open_led.setStyleSheet(
+ "background-color: green;"
+ if is_safe
+ else "background-color: red;"
+ )
+ logger.debug(
+ f"Updated safe to open LED: {'green' if is_safe else 'red'} (is_safe={is_safe})"
+ )
+ if self.system._martacoldroom:
+ self.system._martacoldroom.publish_door_safety_status(is_safe)
+
+ verdict = interpret_door_safety(is_safe)
+
+ # Show the verdict as the LED caption so the light is self-explanatory
+ door_caption = central.findChild(
+ QtWidgets.QLabel, "coldroom_runlabel_2"
+ )
+ if door_caption:
+ door_caption.setText(
+ "Safe to open\n" + ("YES" if is_safe else "NO")
+ )
+ door_caption.setStyleSheet(
+ "color: #1a7f37;" if is_safe else "color: #c0362c;"
+ )
+
+ self.marta_coldroom_tab.findChild(
+ QtWidgets.QLabel, "door_safety_msg"
+ ).setText(f"{verdict}\n\n{door_msg}")
# =========================================================================================== COLDROOM RUN PROCESS ===========================================================================================
# Run status
if "running" in coldroom:
- label = central.findChild(QtWidgets.QLabel, "coldroom_run_state_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "coldroom_run_state_label"
+ )
if label:
run_text = "Running" if coldroom["running"] else "Stopped"
label.setText(run_text)
@@ -826,9 +1151,13 @@ def update_ui(self):
dry_air_led = central.findChild(QtWidgets.QFrame, "dryair_LED")
if dry_air_led:
dry_air_led.setStyleSheet(
- "background-color: green;" if coldroom["dry_air_status"] else "background-color: red;"
+ "background-color: green;"
+ if coldroom["dry_air_status"]
+ else "background-color: red;"
+ )
+ logger.debug(
+ f"Updated dry air LED: {'green' if coldroom['dry_air_status'] else 'red'}"
)
- logger.debug(f"Updated dry air LED: {'green' if coldroom['dry_air_status'] else 'red'}")
# Update CO2 sensor data
if "co2_sensor" in self.system.status:
@@ -905,7 +1234,18 @@ def update_ui(self):
# =========================================================================================== MARTA ===========================================================================================
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- self.modules_list_tab.marta_safe, self.modules_list_tab.marta_log_msg = check_marta_safe(self.system.status)
+ self.modules_list_tab.marta_safe, self.modules_list_tab.marta_log_msg = (
+ check_marta_safe(self.system.status)
+ )
+
+ # logger.info("Called soft Interlock")
+ # used_caen_channels = self.modules_list_tab.get_used_channels()
+ # logger.info(f"Used CAEN channels for safety checks: {used_caen_channels}")
+ # alarm_publish = (lambda msg: self.system._martacoldroom._client.publish("/alarm", msg)) if self.system._martacoldroom else None
+ # logger.info(f"Alarm publish function: {alarm_publish}")
+ # soft_interlock_loop(self.system.status, self.caen_tab.last_response, used_caen_channels, self.caen_tab, publish_alarm=alarm_publish)
+ # logger.info("Completed soft interlock loop")
+
# Update MARTA CO2 Plant values
if "marta" in self.system.status:
marta = self.system.status["marta"]
@@ -914,38 +1254,50 @@ def update_ui(self):
# =========================================================================================== MARTA FSM STATE PROCESS ===========================================================================================
# Update FSM state
if "fsm_state" in marta:
- state_label = central.findChild(QtWidgets.QLabel, "marta_state_label")
+ state_label = central.findChild(
+ QtWidgets.QLabel, "marta_state_label"
+ )
if state_label:
state_label.setText(marta["fsm_state"])
logger.debug(f"Updated MARTA state: {marta['fsm_state']}")
# Update state-specific UI elements
if marta["fsm_state"] == "ALARM":
# Highlight alarm state
- alarm_frame = central.findChild(QtWidgets.QFrame, "marta_alarm_frame")
+ alarm_frame = central.findChild(
+ QtWidgets.QFrame, "marta_alarm_frame"
+ )
if alarm_frame:
alarm_frame.setStyleSheet("background-color: red;")
# Show alarm message
- alarm_msg = central.findChild(QtWidgets.QLabel, "marta_alarm_msg_label")
+ alarm_msg = central.findChild(
+ QtWidgets.QLabel, "marta_alarm_msg_label"
+ )
if alarm_msg and "alarm_message" in marta:
alarm_msg.setText(marta["alarm_message"])
else:
# Clear alarm highlighting
- alarm_frame = central.findChild(QtWidgets.QFrame, "marta_alarm_frame")
+ alarm_frame = central.findChild(
+ QtWidgets.QFrame, "marta_alarm_frame"
+ )
if alarm_frame:
alarm_frame.setStyleSheet("")
# =========================================================================================== MARTA TEMPERATURE PROCESS ===========================================================================================
# Temperature from TT05_CO2 (Supply)
if "TT05_CO2" in marta:
- label = central.findChild(QtWidgets.QLabel, "marta_temp_supply_value_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "marta_temp_supply_value_label"
+ )
if label:
temp_value = marta["TT05_CO2"]
label.setText(f"{temp_value:.1f}")
logger.debug(f"Updated MARTA supply temperature: {temp_value}")
# Temperature from TT06_CO2 (Return)
if "TT06_CO2" in marta:
- label = central.findChild(QtWidgets.QLabel, "marta_temp_return_value_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "marta_temp_return_value_label"
+ )
if label:
temp_value = marta["TT06_CO2"]
label.setText(f"{temp_value:.1f}")
@@ -963,7 +1315,9 @@ def update_ui(self):
marta_temp_lE_active_flag = True
temp_setpoint = lineedit.text()
lineedit.setText(str(temp_setpoint))
- logger.debug(f"Updated MARTA temperature setpoint: {temp_setpoint}")
+ logger.debug(
+ f"Updated MARTA temperature setpoint: {temp_setpoint}"
+ )
else:
marta_temp_lE_active_flag = False
lineedit.clear()
@@ -971,16 +1325,22 @@ def update_ui(self):
# Temperature setpoint label
if "temperature_setpoint" in marta:
- label = central.findChild(QtWidgets.QLabel, "marta_temp_set_point_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "marta_temp_set_point_label"
+ )
if label:
temp_setpoint = marta["temperature_setpoint"]
label.setText(f"{temp_setpoint:.1f}")
- logger.debug(f"Updated MARTA temperature setpoint: {temp_setpoint}")
+ logger.debug(
+ f"Updated MARTA temperature setpoint: {temp_setpoint}"
+ )
# =========================================================================================== MARTA PRESSURE PROCESS ===========================================================================================
# Pressure from PT05_CO2 (Supply)
if "PT05_CO2" in marta:
- label = central.findChild(QtWidgets.QLabel, "marta_pressure_supply_value_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "marta_pressure_supply_value_label"
+ )
if label:
pressure_value = marta["PT05_CO2"]
label.setText(f"{pressure_value:.3f}")
@@ -988,7 +1348,9 @@ def update_ui(self):
# Pressure from PT06_CO2 (Return)
if "PT06_CO2" in marta:
- label = central.findChild(QtWidgets.QLabel, "marta_pressure_return_value_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "marta_pressure_return_value_label"
+ )
if label:
pressure_value = marta["PT06_CO2"]
label.setText(f"{pressure_value:.3f}")
@@ -997,7 +1359,9 @@ def update_ui(self):
# =========================================================================================== MARTA SPEED PROCESS ===========================================================================================
# Speed
if "LP_speed" in marta:
- label = central.findChild(QtWidgets.QLabel, "marta_speed_value_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "marta_speed_value_label"
+ )
if label:
speed_value = marta["LP_speed"]
label.setText(f"{speed_value:.1f}")
@@ -1024,7 +1388,9 @@ def update_ui(self):
# Speed setpoint label
if "speed_setpoint" in marta:
- label = central.findChild(QtWidgets.QLabel, "marta_speed_set_point_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "marta_speed_set_point_label"
+ )
if label:
speed_setpoint = marta["speed_setpoint"]
label.setText(f"{speed_setpoint:.1f}")
@@ -1035,12 +1401,18 @@ def update_ui(self):
# Flow control LED
if "set_flow_active" in marta:
# Update Flow control LED
- ctrl_flow_led = central.findChild(QtWidgets.QFrame, "marta_flow_flag_LED")
+ ctrl_flow_led = central.findChild(
+ QtWidgets.QFrame, "marta_flow_flag_LED"
+ )
if ctrl_flow_led:
ctrl_flow_led.setStyleSheet(
- "background-color: green;" if marta["set_flow_active"] else "background-color: black;"
+ "background-color: green;"
+ if marta["set_flow_active"]
+ else "background-color: black;"
) # LED is green when ON
- logger.debug(f"Updated flow control LED: {'green' if marta['set_flow_active'] else 'black'}")
+ logger.debug(
+ f"Updated flow control LED: {'green' if marta['set_flow_active'] else 'black'}"
+ )
# Flow setpoint lineedit
if "set_flow_setpoint" in marta:
@@ -1061,7 +1433,9 @@ def update_ui(self):
# Flow setpoint (affects speed)
if "flow_setpoint" in marta:
- label = central.findChild(QtWidgets.QLabel, "marta_flow_set_point_label")
+ label = central.findChild(
+ QtWidgets.QLabel, "marta_flow_set_point_label"
+ )
if label:
flow_setpoint = marta["flow_setpoint"]
label.setText(f"{flow_setpoint:.1f}")
@@ -1083,7 +1457,9 @@ def set_coldroom_temperature(self):
central = self.marta_coldroom_tab
# Check if temperature control is enabled
coldroom = self.system.status.get("coldroom", {})
- if "ch_temperature" not in coldroom or not coldroom.get("ch_temperature", {}).get("status", False):
+ if "ch_temperature" not in coldroom or not coldroom.get(
+ "ch_temperature", {}
+ ).get("status", False):
msg = "Temperature control is not enabled. Please enable temperature control first."
self.statusBar().showMessage(msg)
logger.warning(msg)
@@ -1123,8 +1499,12 @@ def set_coldroom_humidity(self):
central = self.marta_coldroom_tab
# Check if humidity control is enabled
coldroom = self.system.status.get("coldroom", {})
- if "ch_humidity" not in coldroom or not coldroom.get("ch_humidity", {}).get("status", False):
- msg = "Humidity control is not enabled. Please enable humidity control first."
+ if "ch_humidity" not in coldroom or not coldroom.get("ch_humidity", {}).get(
+ "status", False
+ ):
+ msg = (
+ "Humidity control is not enabled. Please enable humidity control first."
+ )
self.statusBar().showMessage(msg)
logger.warning(msg)
return
@@ -1142,11 +1522,15 @@ def set_coldroom_humidity(self):
if 0 <= value <= 50: # Humidity range from UI validator
if self.system._martacoldroom:
self.system._martacoldroom.set_humidity(value)
- msg = f"Set coldroom humidity to {value}%"
- self.statusBar().showMessage(msg)
- logger.info(msg)
+ msg = f"Set coldroom humidity to {value}%"
+ self.statusBar().showMessage(msg)
+ logger.info(msg)
+ else:
+ msg = "MARTA Cold Room client not initialized"
+ self.statusBar().showMessage(msg)
+ logger.error(msg)
else:
- msg = "MARTA Cold Room client not initialized"
+ msg = "Humidity must be between 0% and 50%"
self.statusBar().showMessage(msg)
logger.error(msg)
except ValueError:
@@ -1257,7 +1641,9 @@ def toggle_coldroom_door(self):
# This would normally require safety checks
if self.system._martacoldroom:
# For demo purposes, we're just toggling the state
- self.system._martacoldroom.publish_cmd("door", self.system._martacoldroom._coldroom_client, str(new_state))
+ self.system._martacoldroom.publish_cmd(
+ "door", self.system._martacoldroom._coldroom_client, str(new_state)
+ )
msg = f"Set door to {'OPEN' if new_state else 'CLOSED'}"
self.statusBar().showMessage(msg)
logger.info(msg)
@@ -1312,8 +1698,12 @@ def update_marta_button_states(self, state):
central = self.marta_coldroom_tab
# Get all MARTA control buttons
- start_chiller_btn = central.findChild(QtWidgets.QPushButton, "marta_chiller_start_PB")
- stop_chiller_btn = central.findChild(QtWidgets.QPushButton, "marta_stop_chiller_PB")
+ start_chiller_btn = central.findChild(
+ QtWidgets.QPushButton, "marta_chiller_start_PB"
+ )
+ stop_chiller_btn = central.findChild(
+ QtWidgets.QPushButton, "marta_stop_chiller_PB"
+ )
start_co2_btn = central.findChild(QtWidgets.QPushButton, "marta_co2_start_PB")
stop_co2_btn = central.findChild(QtWidgets.QPushButton, "marta_co2_stop_PB")
@@ -1582,13 +1972,25 @@ def toggle_marta_flow_active(self):
def save_settings(self):
try:
# Update settings object
- self.system.settings["mqtt"]["broker"] = self.settings_tab.brokerLineEdit.text()
+ self.system.settings["mqtt"][
+ "broker"
+ ] = self.settings_tab.brokerLineEdit.text()
self.system.settings["mqtt"]["port"] = self.settings_tab.portSpinBox.value()
- self.system.settings["MARTA"]["mqtt_topic"] = self.settings_tab.martaTopicLineEdit.text()
- self.system.settings["Coldroom"]["mqtt_topic"] = self.settings_tab.coldroomTopicLineEdit.text()
- self.system.settings["Coldroom"]["co2_sensor_topic"] = self.settings_tab.co2SensorTopicLineEdit.text()
- self.system.settings["ThermalCamera"]["mqtt_topic"] = self.settings_tab.thermalCameraTopicLineEdit.text()
- self.system.settings["Cleanroom"]["mqtt_topic"] = self.settings_tab.cleanroomTopicLineEdit.text()
+ self.system.settings["MARTA"][
+ "mqtt_topic"
+ ] = self.settings_tab.martaTopicLineEdit.text()
+ self.system.settings["Coldroom"][
+ "mqtt_topic"
+ ] = self.settings_tab.coldroomTopicLineEdit.text()
+ self.system.settings["Coldroom"][
+ "co2_sensor_topic"
+ ] = self.settings_tab.co2SensorTopicLineEdit.text()
+ self.system.settings["ThermalCamera"][
+ "mqtt_topic"
+ ] = self.settings_tab.thermalCameraTopicLineEdit.text()
+ self.system.settings["Cleanroom"][
+ "mqtt_topic"
+ ] = self.settings_tab.cleanroomTopicLineEdit.text()
# Write to file
with open("settings.yaml", "w") as f:
@@ -1611,12 +2013,17 @@ def save_settings(self):
if __name__ == "__main__":
args = argparse.ArgumentParser(description="Cold Room Control Application")
args.add_argument(
- "--loglevel", "-log", default="WARNING", help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)"
+ "--loglevel",
+ "-log",
+ default="WARNING",
+ help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
parsed_args = args.parse_args()
logger = logging.getLogger("integration")
log_level = getattr(logging, parsed_args.loglevel.upper(), logging.WARNING)
- logging.basicConfig(level=log_level, format="%(asctime)s - %(levelname)s - %(message)s")
+ logging.basicConfig(
+ level=log_level, format="%(asctime)s - %(levelname)s - %(message)s"
+ )
app = QtWidgets.QApplication(sys.argv)
window = MainApp()
diff --git a/coldroom/camera_config.yaml b/coldroom/camera_config.yaml
new file mode 100644
index 0000000..2fd1a01
--- /dev/null
+++ b/coldroom/camera_config.yaml
@@ -0,0 +1,12 @@
+camera1:
+ position: 0
+ side: Front
+camera2:
+ position: 0
+ side: Back
+camera3:
+ position: 180
+ side: Front
+camera4:
+ position: 180
+ side: Back
diff --git a/coldroom/command_worker.py b/coldroom/command_worker.py
index 161525b..d3fb003 100644
--- a/coldroom/command_worker.py
+++ b/coldroom/command_worker.py
@@ -3,6 +3,7 @@
import os
from PyQt5.QtCore import QThread, pyqtSignal
import logging
+
logger = logging.getLogger(__name__)
@@ -44,29 +45,29 @@ def run(self):
logger.debug(f"Running command: {expanded_command}")
# Use Popen instead of run() to get process control
# self.process = subprocess.Popen(
- # expanded_command,
- # shell=True,
- # stdout=subprocess.PIPE,
+ # expanded_command,
+ # shell=True,
+ # stdout=subprocess.PIPE,
# stderr=subprocess.PIPE,
# text=True,
# preexec_fn=os.setsid # Create new process group for proper cleanup
# )
self.process = subprocess.run(
- expanded_command,
- shell=True,
- # stdout=subprocess.PIPE,
+ expanded_command,
+ shell=True,
+ # stdout=subprocess.PIPE,
# stderr=subprocess.PIPE,
capture_output=True,
text=True,
# preexec_fn=os.setsid # Create new process group for proper cleanup
)
-
+
if self._should_terminate:
self.finished.emit(False, "", "Process was terminated by user")
else:
success = self.process.returncode == 0
self.finished.emit(success, self.process.stdout, self.process.stderr)
-
+
except Exception as e:
self.finished.emit(False, "", str(e))
finally:
@@ -79,7 +80,7 @@ def terminate_process(self):
try:
# Try graceful termination first
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
-
+
# Wait a bit for graceful shutdown
try:
self.process.wait(timeout=3)
@@ -87,7 +88,7 @@ def terminate_process(self):
# Force kill if graceful termination didn't work
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
self.process.wait()
-
+
except (ProcessLookupError, OSError):
# Process already terminated
pass
@@ -95,4 +96,4 @@ def terminate_process(self):
def terminate(self):
"""Override Qt terminate to properly handle subprocess"""
self.terminate_process()
- return super().terminate()
\ No newline at end of file
+ return super().terminate()
diff --git a/coldroom/marta_coldroom.py b/coldroom/marta_coldroom.py
index 006372b..dab0cf0 100644
--- a/coldroom/marta_coldroom.py
+++ b/coldroom/marta_coldroom.py
@@ -56,7 +56,9 @@ def __init__(self, system_obj):
# Connect to broker
try:
- logger.debug(f"Connecting to MQTT broker at {self._system.BROKER}:{self._system.PORT}")
+ logger.debug(
+ f"Connecting to MQTT broker at {self._system.BROKER}:{self._system.PORT}"
+ )
self._client.connect(self._system.BROKER, self._system.PORT, keepalive=60)
logger.debug("MQTT client connected successfully")
except Exception as e:
@@ -121,7 +123,9 @@ def on_message(self, client, userdata, msg):
elif msg.topic.startswith(self.TOPIC_COLDROOM_AIR): # Add Coldroom Air topic
logger.info("Processing Coldroom Air message")
self.handle_air_bypass_message(msg.payload)
- logger.info(f"Updated Coldroom Air status: {self._coldroom_state.get('air_bypass_status', None)}")
+ logger.info(
+ f"Updated Coldroom Air status: {self._coldroom_state.get('air_bypass_status', None)}"
+ )
# Handle MARTA messages
elif msg.topic.startswith(self.TOPIC_BASE_MARTA): # Add MARTA topic
@@ -155,9 +159,13 @@ def on_message(self, client, userdata, msg):
# Safety checks
if self._system.has_valid_status():
- self._system.safety_flags["door_locked"] = not check_dew_point(self._system.status)
+ self._system.safety_flags["door_locked"] = not check_dew_point(
+ self._system.status
+ )
self._system.safety_flags["sleep"] = check_door_status(self._system.status)
- self._system.safety_flags["door_safe"] = check_door_safe_to_open(self._system.status)
+ self._system.safety_flags["door_safe"] = check_door_safe_to_open(
+ self._system.status, self._system.status.get("caen", {}), {"HV": []}
+ )
logger.debug(f"Safety flags updated: {self._system.safety_flags}")
def publish_cmd(self, command, target, payload):
@@ -169,7 +177,7 @@ def publish_cmd(self, command, target, payload):
target (str): Either 'marta' or 'coldroom' or 'cleanroom'
payload: The command payload
"""
- logger.debug("Publishing", command, target, payload)
+ logger.debug(f"Publishing {command} {target} {payload}")
if target == "marta": # Add MARTA topic
topic = f"{self.TOPIC_BASE_MARTA}cmd/{command}"
elif target == "cleanroom": # Add cleanroom topic
@@ -260,19 +268,33 @@ def handle_cleanroom_status_message(self, payload):
# Update cleanroom status based on topic
if isinstance(data, dict):
if "temperature" in data or "temp" in data:
- self._cleanroom_status["temperature"] = float(data.get("temperature", data.get("temp")))
- logger.info(f"Updated temperature: {self._cleanroom_status['temperature']}")
+ self._cleanroom_status["temperature"] = float(
+ data.get("temperature", data.get("temp"))
+ )
+ logger.info(
+ f"Updated temperature: {self._cleanroom_status['temperature']}"
+ )
if "RH" in data or "humidity" in data:
- self._cleanroom_status["humidity"] = float(data.get("RH", data.get("humidity")))
- logger.info(f"Updated humidity: {self._cleanroom_status['humidity']}")
+ self._cleanroom_status["humidity"] = float(
+ data.get("RH", data.get("humidity"))
+ )
+ logger.info(
+ f"Updated humidity: {self._cleanroom_status['humidity']}"
+ )
if "dewpoint" in data:
self._cleanroom_status["dewpoint"] = float(data["dewpoint"])
- logger.info(f"Updated dewpoint: {self._cleanroom_status['dewpoint']}")
+ logger.info(
+ f"Updated dewpoint: {self._cleanroom_status['dewpoint']}"
+ )
if "Pressure" in data:
self._cleanroom_status["pressure"] = float(data["Pressure"])
- logger.info(f"Updated pressure: {self._cleanroom_status['pressure']}")
+ logger.info(
+ f"Updated pressure: {self._cleanroom_status['pressure']}"
+ )
- self._cleanroom_status["last_update"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ self._cleanroom_status["last_update"] = (
+ datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ )
self._cleanroom_last_update_timer = time.time()
self._cleanroom_last_update_elapsed_time = 0
@@ -292,10 +314,14 @@ def handle_coldroom_state_message(self, payload):
# Update control states
if "ch_temperature" in self._coldroom_state:
if "status" in self._coldroom_state["ch_temperature"]:
- self._coldroom_state["temperature_control"] = self._coldroom_state["ch_temperature"]["status"]
+ self._coldroom_state["temperature_control"] = self._coldroom_state[
+ "ch_temperature"
+ ]["status"]
if "ch_humidity" in self._coldroom_state:
if "status" in self._coldroom_state["ch_humidity"]:
- self._coldroom_state["humidity_control"] = self._coldroom_state["ch_humidity"]["status"]
+ self._coldroom_state["humidity_control"] = self._coldroom_state[
+ "ch_humidity"
+ ]["status"]
self._system.update_status({"coldroom": self._coldroom_state})
except Exception as e:
logger.error(f"Error parsing Coldroom state message: {e}")
@@ -305,7 +331,9 @@ def handle_air_bypass_message(self, payload):
data = json.loads(payload)
logger.debug(f"Parsed Coldroom air bypass data: {data}")
self._dry_air_bypass_status = data["apower"] > 0.1
- self._system._status["coldroomair"]["air_bypass_status"] = self._dry_air_bypass_status
+ self._system._status["coldroomair"][
+ "air_bypass_status"
+ ] = self._dry_air_bypass_status
except Exception as e:
logger.error(f"Error parsing Coldroom air bypass message: {e}")
@@ -356,7 +384,9 @@ def dry_air_bypass_on(self):
if response.status_code == 200:
logger.info("Dry air bypass turned ON successfully")
else:
- logger.error(f"Failed to turn ON dry air bypass, status code: {response.status_code}")
+ logger.error(
+ f"Failed to turn ON dry air bypass, status code: {response.status_code}"
+ )
except Exception as e:
logger.error(f"Error turning ON dry air bypass: {e}")
@@ -367,7 +397,9 @@ def dry_air_bypass_off(self):
if response.status_code == 200:
logger.info("Dry air bypass turned OFF successfully")
else:
- logger.error(f"Failed to turn OFF dry air bypass, status code: {response.status_code}")
+ logger.error(
+ f"Failed to turn OFF dry air bypass, status code: {response.status_code}"
+ )
except Exception as e:
logger.error(f"Error turning OFF dry air bypass: {e}")
@@ -386,4 +418,6 @@ def coldroom_state(self):
@property
def door_locked(self):
- return self._system.safety_flags.get("door_locked", True) # Default to True (safe) if not available
+ return self._system.safety_flags.get(
+ "door_locked", True
+ ) # Default to True (safe) if not available
diff --git a/coldroom/marta_coldroom.ui b/coldroom/marta_coldroom.ui
index 35c8ea8..5398e40 100644
--- a/coldroom/marta_coldroom.ui
+++ b/coldroom/marta_coldroom.ui
@@ -14,7 +14,92 @@
MainWindow
+
+ QWidget {
+ background-color: #eef1f5;
+ color: #2b3648;
+ font-family: "Segoe UI", "Ubuntu", "Helvetica Neue", Arial, sans-serif;
+ font-size: 13px;
+}
+QGroupBox {
+ background-color: #ffffff;
+ border: 1px solid #e2e7ee;
+ border-radius: 12px;
+ margin-top: 16px;
+ padding: 12px 10px 10px 10px;
+}
+QGroupBox::title {
+ subcontrol-origin: margin;
+ subcontrol-position: top left;
+ left: 14px;
+ padding: 2px 8px;
+ color: #5b6b82;
+ font-weight: 700;
+ letter-spacing: 1px;
+}
+QLabel {
+ background: transparent;
+ color: #3a4759;
+}
+QPushButton {
+ background-color: #ffffff;
+ color: #2b3648;
+ border: 1px solid #cdd6e2;
+ border-radius: 8px;
+ padding: 7px 14px;
+ font-weight: 600;
+}
+QPushButton:hover {
+ background-color: #f2f6fc;
+ border-color: #3b82f6;
+ color: #1d4ed8;
+}
+QPushButton:pressed {
+ background-color: #e6edf7;
+}
+QPushButton:disabled {
+ color: #aab4c2;
+ background-color: #f6f8fb;
+ border-color: #e2e7ee;
+}
+QLineEdit {
+ background-color: #ffffff;
+ border: 1px solid #cdd6e2;
+ border-radius: 8px;
+ padding: 6px 10px;
+ selection-background-color: #3b82f6;
+}
+QLineEdit:focus {
+ border: 1px solid #3b82f6;
+}
+QCheckBox {
+ spacing: 6px;
+}
+#safe_to_open_LED, #soft_interlock_LED {
+ border-radius: 38px;
+ border: 4px solid rgba(43, 54, 72, 0.08);
+}
+#ctrl_temp_LED, #ctrl_humidity_LED, #dryair_LED, #dryair_bypass_LED, #light_LED, #marta_flow_flag_LED {
+ border-radius: 15px;
+ border: 2px solid rgba(43, 54, 72, 0.10);
+}
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
+ 14
+
-
@@ -25,18 +110,18 @@
-
-
-
+
-
- 60
- 60
+ 76
+ 76
- 300
- 300
+ 76
+ 76
@@ -60,7 +145,9 @@
- 20
+ 15
+ 75
+ true
@@ -84,7 +171,82 @@
- 16
+ 12
+
+
+
+ -
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+
+ 76
+ 76
+
+
+
+
+ 76
+ 76
+
+
+
+ background-color: red;
+
+
+ QFrame::NoFrame
+
+
+ QFrame::Raised
+
+
+
+ -
+
+
+
+ 160
+ 60
+
+
+
+
+ 15
+ 75
+ true
+
+
+
+ Soft Interlock
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+
+ 12
diff --git a/coldroom/module_temperatures_gui.py b/coldroom/module_temperatures_gui.py
index b05a15c..f4ddcec 100644
--- a/coldroom/module_temperatures_gui.py
+++ b/coldroom/module_temperatures_gui.py
@@ -24,8 +24,12 @@ def __init__(self, system_obj):
uic.loadUi(ui_file, self)
# Initialize scaling modes - default to CO2 scaling
- self.use_co2_scaling_stitched = True # Default to CO2 scaling for stitched views
- self.use_co2_scaling_temperature_plot = True # Default to CO2 scaling for temperature plot
+ self.use_co2_scaling_stitched = (
+ True # Default to CO2 scaling for stitched views
+ )
+ self.use_co2_scaling_temperature_plot = (
+ True # Default to CO2 scaling for temperature plot
+ )
# Initialize stitching-related attributes
# Initialize camera-specific FOV values (default to 20 degrees for all cameras)
@@ -51,7 +55,9 @@ def __init__(self, system_obj):
# Set default temperature range to CO2 and connect signal
if hasattr(self, "t_range_comboBox"):
self.t_range_comboBox.setCurrentText("CO2")
- self.t_range_comboBox.currentTextChanged.connect(self.on_temperature_range_changed)
+ self.t_range_comboBox.currentTextChanged.connect(
+ self.on_temperature_range_changed
+ )
# Setup update timer
self.update_timer = QTimer()
@@ -118,7 +124,9 @@ def get_camera_effective_position(self, camera_id):
logger.error(f"Invalid camera ID: {camera_id}")
return None
except Exception as e:
- logger.error(f"Error calculating effective position for camera {camera_id}: {e}")
+ logger.error(
+ f"Error calculating effective position for camera {camera_id}: {e}"
+ )
return None
def validate_camera_config(self, config):
@@ -154,7 +162,12 @@ def setup_stitched_views(self):
# Get the tab widget from the UI
self.stitched_tab_widget = self.findChild(QtWidgets.QTabWidget, "tabWidget")
- for graphics_view in [self.graphics_1, self.graphics_2, self.graphics_3, self.graphics_4]:
+ for graphics_view in [
+ self.graphics_1,
+ self.graphics_2,
+ self.graphics_3,
+ self.graphics_4,
+ ]:
scene = QtWidgets.QGraphicsScene()
graphics_view.setScene(scene)
@@ -187,7 +200,11 @@ def setup_stitched_views(self):
)
# Set up coordinate formatter for stitched views
- ax.format_coord = lambda x, y, ax_ref=ax, img_ref=img: self.format_coord_stitched(x, y, ax_ref, img_ref)
+ ax.format_coord = (
+ lambda x, y, ax_ref=ax, img_ref=img: self.format_coord_stitched(
+ x, y, ax_ref, img_ref
+ )
+ )
# Set up the axes for degrees
ax.set_xticks(np.linspace(0, 360, 9)) # Ticks every 45 degrees
@@ -248,7 +265,9 @@ def snapshot(self):
os.makedirs(save_dir)
# Save stitched images
- for i, (fig, canvas) in enumerate(zip(self.stitched_figs, self.stitched_canvases)):
+ for i, (fig, canvas) in enumerate(
+ zip(self.stitched_figs, self.stitched_canvases)
+ ):
filename = f"{base_filename}_view_camera{i+1}.png"
filename = os.path.join(save_dir, filename)
fig.savefig(filename, dpi=300)
@@ -328,21 +347,33 @@ def setup_temperature_plot(self):
color = colors[i]
# Create lines for max and min temperatures
- (max_line,) = self.temp_ax.plot([], [], "o-", color=color, label=f"{camera_name} Max", markersize=3)
+ (max_line,) = self.temp_ax.plot(
+ [], [], "o-", color=color, label=f"{camera_name} Max", markersize=3
+ )
(min_line,) = self.temp_ax.plot(
- [], [], "s--", color=color, label=f"{camera_name} Min", markersize=3, alpha=0.7
+ [],
+ [],
+ "s--",
+ color=color,
+ label=f"{camera_name} Min",
+ markersize=3,
+ alpha=0.7,
)
self.temp_lines[f"{camera_name}_max"] = max_line
self.temp_lines[f"{camera_name}_min"] = min_line
# Add legend with better positioning to account for module annotations
- self.temp_ax.legend(bbox_to_anchor=(1.0, 0.5), loc="center left", fontsize=8, frameon=False)
+ self.temp_ax.legend(
+ bbox_to_anchor=(1.0, 0.5), loc="center left", fontsize=8, frameon=False
+ )
# Adjust layout to make room for legend and module annotations
self.temp_fig.subplots_adjust(right=0.82, top=0.85)
- logger.info("Temperature plot tab with navigation toolbar added successfully")
+ logger.info(
+ "Temperature plot tab with navigation toolbar added successfully"
+ )
except Exception as e:
logger.error(f"Error setting up temperature plot: {e}")
@@ -369,21 +400,33 @@ def update_temperature_plot_co2_scale(self):
y_max = co2_temp + 20.0
# Get temperature data from thermal camera
- if hasattr(self.system._thermalcamera, "_stitching_max_temperature") and hasattr(
- self.system._thermalcamera, "_stitching_min_temperature"
- ):
+ if hasattr(
+ self.system._thermalcamera, "_stitching_max_temperature"
+ ) and hasattr(self.system._thermalcamera, "_stitching_min_temperature"):
for i in range(4):
- camera_name = f"camera{i}" # This matches the thermal_camera.py naming
+ camera_name = (
+ f"camera{i}" # This matches the thermal_camera.py naming
+ )
camera_display_name = f"camera{i+1}" # This matches the GUI naming
if (
- camera_name in self.system._thermalcamera._stitching_max_temperature
- and camera_name in self.system._thermalcamera._stitching_min_temperature
+ camera_name
+ in self.system._thermalcamera._stitching_max_temperature
+ and camera_name
+ in self.system._thermalcamera._stitching_min_temperature
):
- max_temp_data = self.system._thermalcamera._stitching_max_temperature[camera_name]
- min_temp_data = self.system._thermalcamera._stitching_min_temperature[camera_name]
+ max_temp_data = (
+ self.system._thermalcamera._stitching_max_temperature[
+ camera_name
+ ]
+ )
+ min_temp_data = (
+ self.system._thermalcamera._stitching_min_temperature[
+ camera_name
+ ]
+ )
if max_temp_data and min_temp_data:
# Extract positions and temperatures
@@ -392,12 +435,17 @@ def update_temperature_plot_co2_scale(self):
min_temps = []
# Get all positions that have both max and min data
- common_positions = set(max_temp_data.keys()) & set(min_temp_data.keys())
+ common_positions = set(max_temp_data.keys()) & set(
+ min_temp_data.keys()
+ )
for pos in sorted(common_positions, key=float):
# Apply camera position offset to get effective position
effective_position = (
- float(pos) + self.camera_positions[camera_display_name]["position"]
+ float(pos)
+ + self.camera_positions[camera_display_name][
+ "position"
+ ]
) % 360
positions.append(effective_position)
@@ -409,8 +457,12 @@ def update_temperature_plot_co2_scale(self):
filtered_max_temps = self.simple_spike_filter(max_temps)
# Update the plot lines
- self.temp_lines[f"{camera_display_name}_max"].set_data(positions, filtered_max_temps)
- self.temp_lines[f"{camera_display_name}_min"].set_data(positions, min_temps)
+ self.temp_lines[f"{camera_display_name}_max"].set_data(
+ positions, filtered_max_temps
+ )
+ self.temp_lines[f"{camera_display_name}_min"].set_data(
+ positions, min_temps
+ )
# Set fixed Y-axis range based on CO2 temperature
self.temp_ax.set_ylim(y_min, y_max)
@@ -437,21 +489,33 @@ def update_temperature_plot_auto_scale(self):
# Get temperature data and update plot lines
all_temps = []
- if hasattr(self.system._thermalcamera, "_stitching_max_temperature") and hasattr(
- self.system._thermalcamera, "_stitching_min_temperature"
- ):
+ if hasattr(
+ self.system._thermalcamera, "_stitching_max_temperature"
+ ) and hasattr(self.system._thermalcamera, "_stitching_min_temperature"):
for i in range(4):
- camera_name = f"camera{i}" # This matches the thermal_camera.py naming
+ camera_name = (
+ f"camera{i}" # This matches the thermal_camera.py naming
+ )
camera_display_name = f"camera{i+1}" # This matches the GUI naming
if (
- camera_name in self.system._thermalcamera._stitching_max_temperature
- and camera_name in self.system._thermalcamera._stitching_min_temperature
+ camera_name
+ in self.system._thermalcamera._stitching_max_temperature
+ and camera_name
+ in self.system._thermalcamera._stitching_min_temperature
):
- max_temp_data = self.system._thermalcamera._stitching_max_temperature[camera_name]
- min_temp_data = self.system._thermalcamera._stitching_min_temperature[camera_name]
+ max_temp_data = (
+ self.system._thermalcamera._stitching_max_temperature[
+ camera_name
+ ]
+ )
+ min_temp_data = (
+ self.system._thermalcamera._stitching_min_temperature[
+ camera_name
+ ]
+ )
if max_temp_data and min_temp_data:
# Extract positions and temperatures
@@ -460,12 +524,17 @@ def update_temperature_plot_auto_scale(self):
min_temps = []
# Get all positions that have both max and min data
- common_positions = set(max_temp_data.keys()) & set(min_temp_data.keys())
+ common_positions = set(max_temp_data.keys()) & set(
+ min_temp_data.keys()
+ )
for pos in sorted(common_positions, key=float):
# Apply camera position offset to get effective position
effective_position = (
- float(pos) + self.camera_positions[camera_display_name]["position"]
+ float(pos)
+ + self.camera_positions[camera_display_name][
+ "position"
+ ]
) % 360
positions.append(effective_position)
@@ -477,8 +546,12 @@ def update_temperature_plot_auto_scale(self):
filtered_max_temps = self.simple_spike_filter(max_temps)
# Update the plot lines
- self.temp_lines[f"{camera_display_name}_max"].set_data(positions, filtered_max_temps)
- self.temp_lines[f"{camera_display_name}_min"].set_data(positions, min_temps)
+ self.temp_lines[f"{camera_display_name}_max"].set_data(
+ positions, filtered_max_temps
+ )
+ self.temp_lines[f"{camera_display_name}_min"].set_data(
+ positions, min_temps
+ )
# Collect all temperature values for auto-scaling
all_temps.extend(filtered_max_temps)
@@ -540,7 +613,9 @@ def add_module_annotations_to_temperature_plot(self):
module_side = module_info["side"]
# Get module slot information if available
- if "mounted_on" in module_info and ";" in str(module_info["mounted_on"]):
+ if "mounted_on" in module_info and ";" in str(
+ module_info["mounted_on"]
+ ):
module_slot = str(module_info["mounted_on"]).split(";")[1]
else:
module_slot = "-"
@@ -548,7 +623,9 @@ def add_module_annotations_to_temperature_plot(self):
# Create annotation text with side information for clarity
annotation_text = f"{module_slot}:{module_name}"
- logger.debug(f"Adding temperature plot annotation '{annotation_text}' at x={module_position}")
+ logger.debug(
+ f"Adding temperature plot annotation '{annotation_text}' at x={module_position}"
+ )
# Add text annotation at the top of the plot with rotation
y_top = self.temp_ax.get_ylim()[1]
@@ -570,9 +647,17 @@ def add_module_annotations_to_temperature_plot(self):
# Add a dashed vertical line to mark the exact position
# Use different colors based on side for better distinction
- line_color = "black" if module_side == "13" else "gray" if module_side == "24" else "gray"
+ line_color = (
+ "black"
+ if module_side == "13"
+ else "gray" if module_side == "24" else "gray"
+ )
line_obj = self.temp_ax.axvline(
- x=module_position, color=line_color, linestyle="--", alpha=0.5, linewidth=1
+ x=module_position,
+ color=line_color,
+ linestyle="--",
+ alpha=0.5,
+ linewidth=1,
)
# Mark as module annotation for easy removal
@@ -583,7 +668,9 @@ def add_module_annotations_to_temperature_plot(self):
f"Added temperature plot annotation {annotation_count}: {annotation_text} at position {module_position}"
)
- logger.info(f"Added {annotation_count} module annotations to temperature plot")
+ logger.info(
+ f"Added {annotation_count} module annotations to temperature plot"
+ )
# Adjust the top margin to make room for the angled text
current_top = self.temp_fig.subplotpars.top
@@ -609,13 +696,18 @@ def simple_spike_filter(self, temps, max_change=5.0):
next_temp = temps[i + 1]
# Check if current value is very different from both neighbors
- if abs(curr_temp - prev_temp) > max_change and abs(curr_temp - next_temp) > max_change:
+ if (
+ abs(curr_temp - prev_temp) > max_change
+ and abs(curr_temp - next_temp) > max_change
+ ):
# Replace with average of neighbors
filtered[i] = (prev_temp + next_temp) / 2
return filtered
- def stitch_multiple_images(self, images, positions, temp_min, temp_max, camera_name, full_coverage=360):
+ def stitch_multiple_images(
+ self, images, positions, temp_min, temp_max, camera_name, full_coverage=360
+ ):
"""Stitch multiple images from different positions into a panorama"""
# Get dimensions of a single image
h, w = images[0].shape
@@ -673,16 +765,22 @@ def stitch_multiple_images(self, images, positions, temp_min, temp_max, camera_n
# Convert to uint8 for display - use actual temperature range
if np.isclose(temp_min, temp_max):
- temp_max = temp_min + 1.0 # Add a small difference to avoid division by zero
+ temp_max = (
+ temp_min + 1.0
+ ) # Add a small difference to avoid division by zero
# Create normalized array with proper handling of NaN values
panorama_norm = np.zeros_like(panorama)
valid_mask = ~np.isnan(panorama)
- panorama_norm[valid_mask] = 255 * (panorama[valid_mask] - temp_min) / (temp_max - temp_min)
+ panorama_norm[valid_mask] = (
+ 255 * (panorama[valid_mask] - temp_min) / (temp_max - temp_min)
+ )
# Areas without data will be black (0)
panorama_norm = np.nan_to_num(panorama_norm, nan=0.0) # Convert NaNs to 0
- panorama_norm = np.clip(panorama_norm, 0, 255) # Ensure values are in valid range
+ panorama_norm = np.clip(
+ panorama_norm, 0, 255
+ ) # Ensure values are in valid range
panorama_norm = panorama_norm.astype(np.uint8)
return panorama_norm, panorama
@@ -720,7 +818,9 @@ def update_displays_co2_scale(self):
# Get stitching data from thermal camera
if hasattr(self.system._thermalcamera, "_stitching_data"):
- for i, (camera_name, camera_data) in enumerate(self.system._thermalcamera._stitching_data.items()):
+ for i, (camera_name, camera_data) in enumerate(
+ self.system._thermalcamera._stitching_data.items()
+ ):
# add +1 to the camera name to match the UI naming
camera_index = int(camera_name[-1]) + 1
camera_name = f"camera{camera_index}"
@@ -735,12 +835,20 @@ def update_displays_co2_scale(self):
# Keep only the last image at this position
last_img = pos_images[-1]
images.append(last_img)
- positions.append(float(pos) + self.camera_positions[camera_name]["position"])
+ positions.append(
+ float(pos)
+ + self.camera_positions[camera_name]["position"]
+ )
if images: # If we have any images to stitch
# Create stitched panorama using CO2 temperature range
panorama_norm, panorama = self.stitch_multiple_images(
- images, positions, colorbar_min, colorbar_max, camera_name, full_coverage=360
+ images,
+ positions,
+ colorbar_min,
+ colorbar_max,
+ camera_name,
+ full_coverage=360,
)
# Ensure the panorama is properly sized for 360 degrees
@@ -753,7 +861,9 @@ def update_displays_co2_scale(self):
# Update the stitched view with CO2-based scaling
self.stitched_images[camera_index - 1].set_array(panorama)
- self.stitched_images[camera_index - 1].set_clim(colorbar_min, colorbar_max)
+ self.stitched_images[camera_index - 1].set_clim(
+ colorbar_min, colorbar_max
+ )
# Clear previous annotations and add module names
ax = self.stitched_axes[camera_index - 1]
@@ -764,11 +874,15 @@ def update_displays_co2_scale(self):
# Remove previous annotation lines (both module and camera position)
for line in ax.lines[:]:
- if hasattr(line, "_module_annotation") or hasattr(line, "_camera_position"):
+ if hasattr(line, "_module_annotation") or hasattr(
+ line, "_camera_position"
+ ):
line.remove()
# Add camera position line (red dashed line showing current camera position)
- current_camera_pos = self.get_camera_effective_position(camera_index)
+ current_camera_pos = self.get_camera_effective_position(
+ camera_index
+ )
if current_camera_pos is not None:
camera_line = ax.axvline(
x=current_camera_pos,
@@ -783,16 +897,27 @@ def update_displays_co2_scale(self):
# Add module annotations if we have mounted modules
if self.mounted_modules is not None:
- self.add_module_annotations_to_stitched_image(ax, camera_name)
+ self.add_module_annotations_to_stitched_image(
+ ax, camera_name
+ )
else:
- logger.debug(f"No mounted modules available for {camera_name}")
+ logger.debug(
+ f"No mounted modules available for {camera_name}"
+ )
# Update colorbar with CO2-based range
cbar = self.stitched_images[camera_index - 1].colorbar
if cbar is not None:
- cbar.set_ticks(np.linspace(colorbar_min, colorbar_max, 5))
+ cbar.set_ticks(
+ np.linspace(colorbar_min, colorbar_max, 5)
+ )
cbar.set_ticklabels(
- [f"{temp:.1f}°C" for temp in np.linspace(colorbar_min, colorbar_max, 5)]
+ [
+ f"{temp:.1f}°C"
+ for temp in np.linspace(
+ colorbar_min, colorbar_max, 5
+ )
+ ]
)
# Draw canvas
@@ -814,7 +939,9 @@ def update_displays_auto_scale(self):
try:
# Get stitching data from thermal camera
if hasattr(self.system._thermalcamera, "_stitching_data"):
- for i, (camera_name, camera_data) in enumerate(self.system._thermalcamera._stitching_data.items()):
+ for i, (camera_name, camera_data) in enumerate(
+ self.system._thermalcamera._stitching_data.items()
+ ):
camera_index = int(camera_name[-1]) + 1
camera_ui_name = f"camera{camera_index}"
if camera_data:
@@ -829,7 +956,10 @@ def update_displays_auto_scale(self):
if pos_images:
last_img = pos_images[-1]
images.append(last_img)
- positions.append(float(pos) + self.camera_positions[camera_ui_name]["position"])
+ positions.append(
+ float(pos)
+ + self.camera_positions[camera_ui_name]["position"]
+ )
# Update temperature range based on actual data
temp_min = min(temp_min, last_img.min())
@@ -838,7 +968,12 @@ def update_displays_auto_scale(self):
if images:
# Create stitched panorama using actual temperature range
panorama_norm, panorama = self.stitch_multiple_images(
- images, positions, temp_min, temp_max, camera_ui_name, full_coverage=360
+ images,
+ positions,
+ temp_min,
+ temp_max,
+ camera_ui_name,
+ full_coverage=360,
)
# Ensure the panorama is properly sized for 360 degrees
@@ -850,7 +985,9 @@ def update_displays_auto_scale(self):
# Update the stitched view with auto-scaling
self.stitched_images[camera_index - 1].set_array(panorama)
- self.stitched_images[camera_index - 1].set_clim(temp_min, temp_max)
+ self.stitched_images[camera_index - 1].set_clim(
+ temp_min, temp_max
+ )
# Clear previous annotations and add new ones
ax = self.stitched_axes[camera_index - 1]
@@ -859,11 +996,15 @@ def update_displays_auto_scale(self):
txt.remove()
for line in ax.lines[:]:
- if hasattr(line, "_module_annotation") or hasattr(line, "_camera_position"):
+ if hasattr(line, "_module_annotation") or hasattr(
+ line, "_camera_position"
+ ):
line.remove()
# Add camera position line
- current_camera_pos = self.get_camera_effective_position(camera_index)
+ current_camera_pos = self.get_camera_effective_position(
+ camera_index
+ )
if current_camera_pos is not None:
camera_line = ax.axvline(
x=current_camera_pos,
@@ -877,13 +1018,20 @@ def update_displays_auto_scale(self):
# Add module annotations
if self.mounted_modules is not None:
- self.add_module_annotations_to_stitched_image(ax, camera_ui_name)
+ self.add_module_annotations_to_stitched_image(
+ ax, camera_ui_name
+ )
# Update colorbar with auto-scale range
cbar = self.stitched_images[camera_index - 1].colorbar
if cbar is not None:
cbar.set_ticks(np.linspace(temp_min, temp_max, 5))
- cbar.set_ticklabels([f"{temp:.1f}°C" for temp in np.linspace(temp_min, temp_max, 5)])
+ cbar.set_ticklabels(
+ [
+ f"{temp:.1f}°C"
+ for temp in np.linspace(temp_min, temp_max, 5)
+ ]
+ )
# Draw canvas
self.stitched_canvases[camera_index - 1].draw()
@@ -940,7 +1088,9 @@ def update_stitched_images(self):
stitching_data = self.system._thermalcamera._stitching_data
- for i, camera_name in enumerate(["camera1", "camera2", "camera3", "camera4"]):
+ for i, camera_name in enumerate(
+ ["camera1", "camera2", "camera3", "camera4"]
+ ):
if (
camera_name in stitching_data
and len(stitching_data[camera_name]) > 0
@@ -954,15 +1104,25 @@ def update_stitched_images(self):
if len(images) > 0:
# Get temperature range for this camera
- max_temps = self.system._thermalcamera._stitching_max_temperature.get(camera_name, {})
- min_temps = self.system._thermalcamera._stitching_min_temperature.get(camera_name, {})
+ max_temps = (
+ self.system._thermalcamera._stitching_max_temperature.get(
+ camera_name, {}
+ )
+ )
+ min_temps = (
+ self.system._thermalcamera._stitching_min_temperature.get(
+ camera_name, {}
+ )
+ )
if max_temps and min_temps:
temp_max = max(max_temps.values())
temp_min = min(min_temps.values())
# Stitch images
- stitched_image, _ = self.stitch_multiple_images(images, positions, temp_min, temp_max, camera_name)
+ stitched_image, _ = self.stitch_multiple_images(
+ images, positions, temp_min, temp_max, camera_name
+ )
# Update display
if i < len(self.stitched_images):
@@ -991,7 +1151,9 @@ def add_module_annotations_to_stitched_image(self, ax, camera_name):
else:
target_side = camera_side
- logger.debug(f"Camera {camera_name} side: {camera_side}, target_side: {target_side}")
+ logger.debug(
+ f"Camera {camera_name} side: {camera_side}, target_side: {target_side}"
+ )
# Clear previous annotations
# Remove previous text annotations and lines
@@ -1010,7 +1172,9 @@ def add_module_annotations_to_stitched_image(self, ax, camera_name):
module_side = module_info["side"]
module_position = module_info["angular_position"]
- logger.debug(f"Module {module_name}: side={module_side}, position={module_position}")
+ logger.debug(
+ f"Module {module_name}: side={module_side}, position={module_position}"
+ )
# Only show modules on the same side as the camera
if module_side == target_side:
@@ -1019,7 +1183,9 @@ def add_module_annotations_to_stitched_image(self, ax, camera_name):
module_x_pos = module_position
# Get module slot information if available
- if "mounted_on" in module_info and ";" in str(module_info["mounted_on"]):
+ if "mounted_on" in module_info and ";" in str(
+ module_info["mounted_on"]
+ ):
module_slot = str(module_info["mounted_on"]).split(";")[1]
else:
module_slot = "-"
@@ -1027,7 +1193,9 @@ def add_module_annotations_to_stitched_image(self, ax, camera_name):
# Create annotation text
annotation_text = f"{module_slot}:{module_name}"
- logger.debug(f"Adding annotation '{annotation_text}' at x={module_x_pos}")
+ logger.debug(
+ f"Adding annotation '{annotation_text}' at x={module_x_pos}"
+ )
# Add text annotation on the top x-axis with 45-degree rotation
text_obj = ax.text(
@@ -1047,15 +1215,25 @@ def add_module_annotations_to_stitched_image(self, ax, camera_name):
text_obj._module_annotation = True
# Add a dashed vertical line to mark the exact position
- line_obj = ax.axvline(x=module_x_pos, color="black", linestyle="--", alpha=0.7, linewidth=1.5)
+ line_obj = ax.axvline(
+ x=module_x_pos,
+ color="black",
+ linestyle="--",
+ alpha=0.7,
+ linewidth=1.5,
+ )
# Mark as module annotation for easy removal
line_obj._module_annotation = True
annotation_count += 1
- logger.debug(f"Added annotation {annotation_count}: {annotation_text} at position {module_x_pos}")
+ logger.debug(
+ f"Added annotation {annotation_count}: {annotation_text} at position {module_x_pos}"
+ )
- logger.info(f"Added {annotation_count} module annotations for {camera_name} on side {camera_side}")
+ logger.info(
+ f"Added {annotation_count} module annotations for {camera_name} on side {camera_side}"
+ )
# Adjust the top margin to make room for the angled text
# Get current subplot parameters
@@ -1084,23 +1262,29 @@ def setup_fov_sliders(self):
# Get slider and label widgets
slider = self.findChild(QtWidgets.QSlider, slider_name)
label = self.findChild(QtWidgets.QLabel, config["label"])
-
+
if slider and label:
camera_name = config["camera"]
-
+
# Set initial slider value and label text
initial_fov = self.camera_fovs[camera_name]
slider.setValue(initial_fov)
label.setText(str(initial_fov))
-
+
# Connect slider to update function using lambda to capture camera name
slider.valueChanged.connect(
- lambda value, cam=camera_name, lbl=label: self.on_fov_changed(cam, value, lbl)
+ lambda value, cam=camera_name, lbl=label: self.on_fov_changed(
+ cam, value, lbl
+ )
+ )
+
+ logger.info(
+ f"Connected FOV slider for {camera_name}, initial value: {initial_fov}"
)
-
- logger.info(f"Connected FOV slider for {camera_name}, initial value: {initial_fov}")
else:
- logger.warning(f"Could not find slider '{slider_name}' or label '{config['label']}'")
+ logger.warning(
+ f"Could not find slider '{slider_name}' or label '{config['label']}'"
+ )
except Exception as e:
logger.error(f"Error setting up FOV sliders: {e}")
@@ -1110,17 +1294,17 @@ def on_fov_changed(self, camera_name, fov_value, label):
try:
# Update the camera's FOV value
self.camera_fovs[camera_name] = fov_value
-
+
# Update the label to show current value
label.setText(str(fov_value))
-
+
logger.info(f"FOV for {camera_name} changed to {fov_value} degrees")
-
+
# Optionally trigger immediate update of displays
# Note: This might cause frequent updates while dragging the slider
# You can uncomment this if you want real-time updates
# self.update_displays()
-
+
except Exception as e:
logger.error(f"Error handling FOV change for {camera_name}: {e}")
@@ -1160,7 +1344,9 @@ def setup_module_temperature_table(self):
table.setHorizontalHeaderLabels(horizontal_headers)
# Set vertical headers (temperature keys)
- vertical_headers = temp_keys.copy() # Copy temperature keys for vertical headers
+ vertical_headers = (
+ temp_keys.copy()
+ ) # Copy temperature keys for vertical headers
table.setVerticalHeaderLabels(vertical_headers)
# Populate the table with empty strings initially
for row in range(len(temp_keys)):
@@ -1209,7 +1395,9 @@ def update_temperature_table(self):
"""Update the module temperature table with current data"""
try:
if not hasattr(self, "mqtt_client"):
- logger.warning("MQTT client not initialized, cannot update temperature table")
+ logger.warning(
+ "MQTT client not initialized, cannot update temperature table"
+ )
return
# Get the latest status from the MQTT client
@@ -1223,22 +1411,38 @@ def update_temperature_table(self):
for mounted_module, mounted_module_info in self.mounted_modules.items():
mounted_module_fuse_id = mounted_module_info.get("fuseId", None)
if mounted_module_fuse_id is None:
- # logger.warning(f"Mounted module {mounted_module} does not have a fuseId, skipping")
+ # logger.warning(f"Mounted module {mounted_module} does not have a fuseId, skipping")
continue
if mounted_module_fuse_id == monitored_fuse_id:
- column_index = int(mounted_module_info.get("mounted_on", "-").split(";")[1])
- if column_index is not None and column_index < self.module_temp_table.columnCount():
+ column_index = int(
+ mounted_module_info.get("mounted_on", "-").split(";")[1]
+ )
+ if (
+ column_index is not None
+ and column_index < self.module_temp_table.columnCount()
+ ):
for temp_key, temperature in temp_data.items():
if temp_key in self.temp_keys:
- if temp_key in mounted_module_info["temperature_offsets"]:
- temperature += mounted_module_info["temperature_offsets"][temp_key]
- item = QtWidgets.QTableWidgetItem(f"{temperature:.1f}")
+ if (
+ temp_key
+ in mounted_module_info["temperature_offsets"]
+ ):
+ temperature += mounted_module_info[
+ "temperature_offsets"
+ ][temp_key]
+ item = QtWidgets.QTableWidgetItem(
+ f"{temperature:.1f}"
+ )
else:
# change the color of the cell to red if no offset is found
- item = QtWidgets.QTableWidgetItem(f"{temperature:.1f}")
+ item = QtWidgets.QTableWidgetItem(
+ f"{temperature:.1f}"
+ )
item.setBackground(QColor(255, 0, 0))
row_index = self.temp_keys.index(temp_key)
- self.module_temp_table.setItem(row_index, column_index - 1, item)
+ self.module_temp_table.setItem(
+ row_index, column_index - 1, item
+ )
# Note: Removed the publishing logic from here - it's now handled in handle_message
@@ -1247,10 +1451,12 @@ def update_temperature_table(self):
def publish_calibrated_data(self, fuse_id, temp_data):
"""Publish calibrated temperature data for a specific module"""
- print(fuse_id,temp_data)
+ print(fuse_id, temp_data)
try:
if not hasattr(self, "mqtt_client"):
- logger.warning("MQTT client not initialized, cannot publish calibrated data")
+ logger.warning(
+ "MQTT client not initialized, cannot publish calibrated data"
+ )
return
to_publish = {}
@@ -1263,11 +1469,20 @@ def publish_calibrated_data(self, fuse_id, temp_data):
for temp_key, temperature in temp_data.items():
if temp_key in self.temp_keys:
if temp_key in mounted_module_info["temperature_offsets"]:
- calibrated_temp = temperature + mounted_module_info["temperature_offsets"][temp_key]
- original_key = self.mqtt_client.key_map.get(temp_key, temp_key)
+ calibrated_temp = (
+ temperature
+ + mounted_module_info["temperature_offsets"][
+ temp_key
+ ]
+ )
+ original_key = self.mqtt_client.key_map.get(
+ temp_key, temp_key
+ )
to_publish[original_key] = calibrated_temp
else:
- logger.warning(f"No offset found for {temp_key} in module {mounted_module}")
+ logger.warning(
+ f"No offset found for {temp_key} in module {mounted_module}"
+ )
break # Found the module, no need to continue
@@ -1276,9 +1491,13 @@ def publish_calibrated_data(self, fuse_id, temp_data):
topic = f"{self.mqtt_client.BASE_TOPIC}/calib_data"
payload = json.dumps(to_publish)
self.mqtt_client.client.publish(topic, payload)
- logger.info(f"Published calibrated temperature data for fuse_id {fuse_id}: {to_publish}")
+ logger.info(
+ f"Published calibrated temperature data for fuse_id {fuse_id}: {to_publish}"
+ )
else:
- logger.debug(f"No calibrated temperature data to publish for fuse_id {fuse_id}")
+ logger.debug(
+ f"No calibrated temperature data to publish for fuse_id {fuse_id}"
+ )
except Exception as e:
logger.error(f"Error publishing calibrated data: {e}")
@@ -1365,8 +1584,12 @@ def __init__(self, system):
self.mqtt_settings = self.system._settings["mqtt"]
self.gui_reference = None
try:
- self.client.connect(self.mqtt_settings["broker"], self.mqtt_settings["port"], keepalive=60)
- logger.info(f"Connected to MQTT broker at {self.system.BROKER}:{self.system.PORT}")
+ self.client.connect(
+ self.mqtt_settings["broker"], self.mqtt_settings["port"], keepalive=60
+ )
+ logger.info(
+ f"Connected to MQTT broker at {self.system.BROKER}:{self.system.PORT}"
+ )
except Exception as e:
logger.error(f"Failed to connect to MQTT broker: {e}")
diff --git a/coldroom/modules_list_gui.py b/coldroom/modules_list_gui.py
index ccb95c6..589854f 100644
--- a/coldroom/modules_list_gui.py
+++ b/coldroom/modules_list_gui.py
@@ -51,9 +51,13 @@ def __init__(self):
# Connect existing buttons
self.select_all_button_2.clicked.connect(self.select_all_modules)
- self.turn_lv_off_all_button.clicked.connect(self.turn_off_lv_for_selected_modules)
+ self.turn_lv_off_all_button.clicked.connect(
+ self.turn_off_lv_for_selected_modules
+ )
self.turn_lv_on_all_button.clicked.connect(self.turn_on_lv_for_selected_modules)
- self.turn_hv_off_all_button.clicked.connect(self.turn_off_hv_for_selected_modules)
+ self.turn_hv_off_all_button.clicked.connect(
+ self.turn_off_hv_for_selected_modules
+ )
self.turn_hv_on_all_button.clicked.connect(self.turn_on_hv_for_selected_modules)
self.refresh_button.clicked.connect(self.update_module_list)
@@ -61,10 +65,10 @@ def __init__(self):
self.start_test_all_button.clicked.connect(self.run_test_for_selected_modules)
self.cancel_test_all_button.clicked.connect(self.stop_all_tests)
self.start_t_monitor_button.clicked.connect(
- lambda: self.start_temperature_monitoring() #self.module_temperature_tab)
+ lambda: self.start_temperature_monitoring() # self.module_temperature_tab)
)
self.stop_t_monitor_button.clicked.connect(
- lambda: self.stop_temperature_monitoring() #self.module_temperature_tab)
+ lambda: self.stop_temperature_monitoring() # self.module_temperature_tab)
)
# Tests Combobox
@@ -97,8 +101,10 @@ def _make_api_request(self, endpoint, method, data=None):
url = f"{self.db_url.rstrip('/')}/{endpoint.lstrip('/')}"
try:
# Simulate API response for demonstration purposes
- #result = {"sessionName": "SIMULATED_SESSION_12345"}
- response = requests.request(method=method.lower(), url=url, json=data if data else None)
+ # result = {"sessionName": "SIMULATED_SESSION_12345"}
+ response = requests.request(
+ method=method.lower(), url=url, json=data if data else None
+ )
if response.status_code != 200 and response.status_code != 201:
logger.error(f"API Error ({response.status_code}): {response.text}")
return False, None
@@ -127,7 +133,9 @@ def _new_session(self):
def turn_on_lv_for_selected_modules(self):
"""Turn on LV for all selected modules."""
if not self.marta_safe:
- self.message_box.setText(f"MARTA not safe, cannot turn on LV:\n{self.marta_log_msg}")
+ self.message_box.setText(
+ f"MARTA not safe, cannot turn on LV:\n{self.marta_log_msg}"
+ )
self.message_box.exec_()
return
@@ -191,7 +199,9 @@ def caen_hv_on_wrap(self, channel):
def caen_lv_on_wrap(self, channel):
"""Wrapper for LV on with safety check."""
if not self.marta_safe:
- self.message_box.setText(f"MARTA not safe, cannot turn on LV:\n{self.marta_log_msg}")
+ self.message_box.setText(
+ f"MARTA not safe, cannot turn on LV:\n{self.marta_log_msg}"
+ )
self.message_box.exec_()
return
else:
@@ -205,7 +215,9 @@ def caen_hv_off(self, channel):
"""Queue HV off command."""
self.caen.off(channel)
- def populate_from_config(self, caen, modules, number_of_modules, thermal_camera_system=None):
+ def populate_from_config(
+ self, caen, modules, number_of_modules, thermal_camera_system=None
+ ):
"""Populate the moduleList QTreeWidget with module data from a config list."""
if number_of_modules < 1:
return
@@ -215,7 +227,10 @@ def populate_from_config(self, caen, modules, number_of_modules, thermal_camera_
self.thermal_camera_system = thermal_camera_system
# Create all items first
- values = {str(i): [str(i), "", "", "", "", "", "", "", "", ""] for i in range(1, number_of_modules + 1)}
+ values = {
+ str(i): [str(i), "", "", "", "", "", "", "", "", ""]
+ for i in range(1, number_of_modules + 1)
+ }
# Fill the moduleList with the values first
for i in values:
@@ -241,9 +256,13 @@ def populate_from_config(self, caen, modules, number_of_modules, thermal_camera_
) * self.MODULE_ANGULAR_WIDTH + (self.MODULE_ANGULAR_WIDTH / 2) * (
1 if int(module_position) <= (number_of_modules / 2) else 0
)
- module_side = "13" if int(module_position) <= (number_of_modules / 2) else "24"
+ module_side = (
+ "13" if int(module_position) <= (number_of_modules / 2) else "24"
+ )
- self.mounted_modules[module_name]["angular_position"] = module_angular_position
+ self.mounted_modules[module_name][
+ "angular_position"
+ ] = module_angular_position
self.mounted_modules[module_name]["side"] = module_side
item_index = int(module_position) - 1
fc7 = module_info.get("FC7", "")
@@ -271,28 +290,42 @@ def populate_from_config(self, caen, modules, number_of_modules, thermal_camera_
# Use queued commands for buttons
btn_lv_on = QtWidgets.QPushButton("LV On")
- btn_lv_on.clicked.connect(lambda checked, ch=module_info.get("LV"): self.caen_lv_on_wrap(ch))
+ btn_lv_on.clicked.connect(
+ lambda checked, ch=module_info.get("LV"): self.caen_lv_on_wrap(ch)
+ )
btn_lv_off = QtWidgets.QPushButton("LV Off")
- btn_lv_off.clicked.connect(lambda checked, ch=module_info.get("LV"): self.caen_lv_off(ch))
+ btn_lv_off.clicked.connect(
+ lambda checked, ch=module_info.get("LV"): self.caen_lv_off(ch)
+ )
btn_hv_on = QtWidgets.QPushButton("HV On")
- btn_hv_on.clicked.connect(lambda checked, ch=module_info.get("HV"): self.caen_hv_on_wrap(ch))
+ btn_hv_on.clicked.connect(
+ lambda checked, ch=module_info.get("HV"): self.caen_hv_on_wrap(ch)
+ )
btn_hv_off = QtWidgets.QPushButton("HV Off")
- btn_hv_off.clicked.connect(lambda checked, ch=module_info.get("HV"): self.caen_hv_off(ch))
+ btn_hv_off.clicked.connect(
+ lambda checked, ch=module_info.get("HV"): self.caen_hv_off(ch)
+ )
# Connect test buttons to test functions
btn_start_test = QtWidgets.QPushButton("Test On")
- btn_start_test.clicked.connect(lambda checked, mod=module_name: self.start_single_module_test(mod))
+ btn_start_test.clicked.connect(
+ lambda checked, mod=module_name: self.start_single_module_test(mod)
+ )
btn_stop_test = QtWidgets.QPushButton("Test Off")
- btn_stop_test.clicked.connect(lambda checked, mod=module_name: self.stop_single_module_test(mod))
+ btn_stop_test.clicked.connect(
+ lambda checked, mod=module_name: self.stop_single_module_test(mod)
+ )
# Connect camera button to focus function
btn_focus_camera = QtWidgets.QPushButton("Camera")
btn_focus_camera.clicked.connect(
- lambda checked, mod=module_name: self.focus_camera_on_module(mod, self.thermal_camera_system)
+ lambda checked, mod=module_name: self.focus_camera_on_module(
+ mod, self.thermal_camera_system
+ )
)
for btn in [
@@ -324,7 +357,7 @@ def set_test_command(self):
def run_test_for_selected_modules(self):
"""Run the test command for all selected modules."""
- #check if operator field is non empty
+ # check if operator field is non empty
if not self.operator_le.text().strip():
self._show_message("Please enter operator name before starting a test.")
return
@@ -340,7 +373,9 @@ def run_test_for_selected_modules(self):
added_modules.append(module_name)
if added_modules:
- logger.debug(f"Added {len(added_modules)} modules to test queue: {added_modules}")
+ logger.debug(
+ f"Added {len(added_modules)} modules to test queue: {added_modules}"
+ )
self.process_test_queue()
def process_test_queue(self):
@@ -355,13 +390,17 @@ def process_test_queue(self):
# Get the next module from the queue
module_name = self.test_queue.pop(0)
- logger.debug(f"Starting test for module: {module_name} ({len(self.test_queue)} remaining in queue)")
+ logger.debug(
+ f"Starting test for module: {module_name} ({len(self.test_queue)} remaining in queue)"
+ )
self._start_test_for_module(module_name)
def _start_test_for_module(self, module_name):
"""Start the actual test execution for a module."""
if self.current_worker is not None:
- logger.debug(f"Cannot start test for {module_name}: another test is already running")
+ logger.debug(
+ f"Cannot start test for {module_name}: another test is already running"
+ )
return
# Update testing status
@@ -441,10 +480,10 @@ def select_all_modules(self):
def start_single_module_test(self, module_name):
"""Start test for a single module via button click."""
- #check if operator field is non empty
-# if not self.operator_field.text().strip():
-# self._show_message("Please enter operator name before starting a test.")
-
+ # check if operator field is non empty
+ # if not self.operator_field.text().strip():
+ # self._show_message("Please enter operator name before starting a test.")
+
if self._add_module_to_queue(module_name):
logger.debug(f"Added module {module_name} to test queue via button click")
self.process_test_queue()
@@ -494,7 +533,10 @@ def _remove_module_from_testing(self, module_name):
def _is_module_currently_testing(self, module_name):
"""Check if a module is currently being tested."""
- return self.current_worker and self.current_worker.placeholders.get("module_id") == module_name
+ return (
+ self.current_worker
+ and self.current_worker.placeholders.get("module_id") == module_name
+ )
def _update_module_testing_status(self, module_name, status):
"""Update the testing status for a module."""
@@ -533,7 +575,9 @@ def update_module_list(self):
item.setText(8, str(module_info.get("temperature", "")))
# Update testing status with visual indicators
- self._update_testing_status_display(item, module_info.get("testing", ""))
+ self._update_testing_status_display(
+ item, module_info.get("testing", "")
+ )
else:
# Clear testing status for empty slots
item.setText(9, "")
@@ -603,7 +647,11 @@ def get_test_status(self):
return {
"is_testing": self.current_worker is not None,
"queue_length": len(self.test_queue),
- "current_module": (self.current_worker.placeholders.get("module_id") if self.current_worker else None),
+ "current_module": (
+ self.current_worker.placeholders.get("module_id")
+ if self.current_worker
+ else None
+ ),
"queued_modules": self.test_queue.copy(),
}
@@ -618,7 +666,9 @@ def focus_camera_on_module(self, module_name, thermal_camera_system):
side = module_info.get("side", "Undefined")
if angular_position == -1 or side == "Undefined":
- logger.debug(f"Invalid position data for module {module_name}: position={angular_position}, side={side}")
+ logger.debug(
+ f"Invalid position data for module {module_name}: position={angular_position}, side={side}"
+ )
return
# Choose camera pair based on side
@@ -628,7 +678,9 @@ def focus_camera_on_module(self, module_name, thermal_camera_system):
return
# Choose specific camera based on angular position
- selected_camera = self._select_camera_for_position(camera_pair, angular_position)
+ selected_camera = self._select_camera_for_position(
+ camera_pair, angular_position
+ )
logger.debug(
f"Focusing camera {selected_camera} on module {module_name} at angular position {angular_position}° (side {side})"
@@ -640,7 +692,9 @@ def focus_camera_on_module(self, module_name, thermal_camera_system):
# f"Moving camera {selected_camera} to {angular_position}° for module {module_name} (side {side})"
# )
# self.camera_status_message_box.exec_()
- self._move_camera_to_angular_position(thermal_camera_system, selected_camera, angular_position)
+ self._move_camera_to_angular_position(
+ thermal_camera_system, selected_camera, angular_position
+ )
else:
logger.debug(f"No thermal camera system available")
@@ -659,10 +713,14 @@ def __fill_message_box(self, camera_pair, angular_position):
self.camera_status_message_box = QtWidgets.QMessageBox()
self.camera_status_message_box.setWindowTitle("Camera Selection")
self.camera_status_message_box.setIcon(QtWidgets.QMessageBox.Information)
- self.camera_status_message_box.setText(f"Select camera for angular position {angular_position}°:")
+ self.camera_status_message_box.setText(
+ f"Select camera for angular position {angular_position}°:"
+ )
for camera_name in camera_pair:
shown_camera_name = camera_name.replace("camera", "Camera ")
- self.camera_status_message_box.addButton(shown_camera_name, QtWidgets.QMessageBox.AcceptRole)
+ self.camera_status_message_box.addButton(
+ shown_camera_name, QtWidgets.QMessageBox.AcceptRole
+ )
self.camera_status_message_box.setStandardButtons(QtWidgets.QMessageBox.Cancel)
self.camera_status_message_box.setDefaultButton(QtWidgets.QMessageBox.Cancel)
self.camera_status_message_box.setEscapeButton(QtWidgets.QMessageBox.Cancel)
@@ -693,15 +751,21 @@ def _select_camera_for_position(self, camera_pair, angular_position):
selected_camera = selected_camera.replace("Camera ", "camera")
return camera_pair[selected_camera]
- def _move_camera_to_angular_position(self, thermal_camera, camera_id, angular_position):
+ def _move_camera_to_angular_position(
+ self, thermal_camera, camera_id, angular_position
+ ):
"""Move the specified camera to the target angular position."""
try:
# Use the thermal camera GUI's move_camera method
success = thermal_camera.move_camera(camera_id, angular_position)
if success:
- logger.debug(f"Camera {camera_id} moved successfully to {angular_position}°")
+ logger.debug(
+ f"Camera {camera_id} moved successfully to {angular_position}°"
+ )
else:
- logger.debug(f"Failed to move camera {camera_id} to {angular_position}°")
+ logger.debug(
+ f"Failed to move camera {camera_id} to {angular_position}°"
+ )
except Exception as e:
logger.debug(f"Error moving camera {camera_id}: {str(e)}")
@@ -722,7 +786,9 @@ def get_camera_status_for_module(self, module_name):
if not camera_pair:
return None
- selected_camera = self._select_camera_for_position(camera_pair, angular_position)
+ selected_camera = self._select_camera_for_position(
+ camera_pair, angular_position
+ )
return {
"module_name": module_name,
diff --git a/coldroom/safety.py b/coldroom/safety.py
index 3a3f2e9..d8af14a 100644
--- a/coldroom/safety.py
+++ b/coldroom/safety.py
@@ -1,4 +1,5 @@
import os
+import datetime
import yaml
import logging
@@ -25,7 +26,9 @@ def check_dew_point(system_status):
# Get the three required temperature values
marta_supply_temp = system_status.get("marta", {}).get("TT05_CO2")
marta_return_temp = system_status.get("marta", {}).get("TT06_CO2")
- coldroom_temp = system_status.get("coldroom", {}).get("ch_temperature", {}).get("value")
+ coldroom_temp = (
+ system_status.get("coldroom", {}).get("ch_temperature", {}).get("value")
+ )
# coldroom = system_status.get("coldroom", {})
# logger.debug(f"Coldroom: {coldroom.get('CmdDoorUnlock_Reff')}")
# door_status = system_status.get("coldroom", {}).get("CmdDoorUnlock_Reff")
@@ -52,28 +55,38 @@ def check_dew_point(system_status):
if "coldroom" not in system_status:
logger.debug("Coldroom data not available")
- return False # Conservative approach - if we can't check, assume it's unsafe
+ return (
+ False # Conservative approach - if we can't check, assume it's unsafe
+ )
# Check if environment data exists
- if "cleanroom" not in system_status or "dewpoint" not in system_status["cleanroom"]:
+ if (
+ "cleanroom" not in system_status
+ or "dewpoint" not in system_status["cleanroom"]
+ ):
return False # Conservative approach
- reference_dew_point = system_status["cleanroom"]["dewpoint"] # External dewpoint
+ reference_dew_point = system_status["cleanroom"][
+ "dewpoint"
+ ] # External dewpoint
logger.debug(f"Reference dew point: {reference_dew_point}")
delta = 1 # Allowable delta between dew point and temperature
return min_temperature > reference_dew_point + delta
except Exception as e:
logger.debug(f"Error in check_dew_point: {str(e)}")
- return False # Conservative approach
+ return False # Conservative approach - a bare False, not a truthy tuple
def check_door_status(system_status):
try:
- if "coldroom" not in system_status or "door_status" not in system_status["coldroom"]:
+ if (
+ "coldroom" not in system_status
+ or "CmdDoorUnlock_Reff" not in system_status["coldroom"]
+ ):
return False # Conservative approach
return system_status["coldroom"]["CmdDoorUnlock_Reff"] == 1 # Door is open
except Exception as e:
logger.debug(f"Error in check_door_status: {str(e)}")
- return False # Conservative approach
+ return False # Conservative approach - a bare False, not a truthy tuple
def check_light_status(system_status):
@@ -83,7 +96,7 @@ def check_light_status(system_status):
return system_status["coldroom"]["light"] == 1 # Light is on
except Exception as e:
logger.debug(f"Error in check_light_status: {str(e)}")
- return False # Conservative approach
+ return False # Conservative approach - a bare False, not a truthy tuple
def check_any_hv_on(caen_ch_status, used_channels):
@@ -98,6 +111,8 @@ def check_any_hv_on(caen_ch_status, used_channels):
return hv_on
except Exception as e:
logger.debug(f"Error in check_any_hv_on: {str(e)}")
+ # Conservative approach - if we can't check, assume HV is on (unsafe).
+ # Return a bare True, not a truthy tuple, so callers' `not hv_on` works.
return True
@@ -105,6 +120,28 @@ def check_cleanroom_expired(elapsed_time, threshold=600):
return elapsed_time > threshold
+def interpret_door_safety(is_safe):
+ """
+ Human-readable verdict for the 'Safe to open' LED, so the operator is told
+ what the light means instead of having to infer it from the colour.
+ """
+ if is_safe:
+ return "SAFE TO OPEN — you may open the door."
+ return "DO NOT OPEN — conditions are unsafe, keep the door closed."
+
+
+def interpret_soft_interlock(is_safe):
+ """
+ Human-readable verdict for the 'Soft Interlock' LED.
+
+ Note: green means "no protective action needed" (e.g. LV is off), not
+ necessarily "safe to energize LV" — the detailed message says which.
+ """
+ if is_safe:
+ return "OK — no protective action needed (LV is protected)."
+ return "INTERLOCK TRIPPED — unsafe condition, LV has been switched off."
+
+
def check_door_safe_to_open(system_status, caen_ch_status, used_channels):
"""
Check if it's safe to open the door based on multiple safety conditions.
@@ -114,21 +151,70 @@ def check_door_safe_to_open(system_status, caen_ch_status, used_channels):
try:
# Check if we have all necessary data
if "coldroom" not in system_status:
- return False # Conservative approach - if we can't check, assume it's unsafe
+ return False, "coldroom data not available — assuming unsafe"
+
+ # 1. Check if dew point conditions are safe.
+ # Cleanroom status carries a "last_update" timestamp string, not an
+ # "elapsed_time" value — derive the age from it here.
+ last_update = system_status.get("cleanroom", {}).get("last_update")
+ if last_update is None:
+ clean_room_expired = True # No cleanroom data → treat as expired
+ else:
+ try:
+ last_dt = datetime.datetime.strptime(
+ last_update, "%Y-%m-%d %H:%M:%S"
+ )
+ elapsed_time = (datetime.datetime.now() - last_dt).total_seconds()
+ clean_room_expired = check_cleanroom_expired(elapsed_time)
+ except (ValueError, TypeError) as e:
+ logger.debug(f"Could not parse cleanroom last_update: {str(e)}")
+ clean_room_expired = True # Unparseable → treat as expired
+ # Pull raw values used by check_dew_point so we can show the actual numbers.
+ marta_supply = system_status.get("marta", {}).get("TT05_CO2")
+ marta_return = system_status.get("marta", {}).get("TT06_CO2")
+ coldroom_t = system_status.get("coldroom", {}).get("ch_temperature", {}).get("value")
+ temps = [t for t in [marta_supply, marta_return, coldroom_t] if t is not None]
+ ext_dew = system_status.get("cleanroom", {}).get("dewpoint")
- # 1. Check if dew point conditions are safe
- clean_room_expired = check_cleanroom_expired(system_status["cleanroom"]["elapsed_time"])
if clean_room_expired:
dew_point_safe = False
- log_msg += f"!!! Warning: Cleanroom data expired, not able to check dew point !!!\n"
+ dew_reason = "cleanroom sensor data expired (no update in >10 min)"
+ log_msg += "!!! Warning: Cleanroom data expired, not able to check dew point !!!\n"
+ elif not temps:
+ dew_point_safe = False
+ dew_reason = "no internal temperature readings available"
+ elif ext_dew is None:
+ dew_point_safe = False
+ dew_reason = "no dew point reading from cleanroom sensor"
else:
dew_point_safe = check_dew_point(system_status)
- log_msg += f"Dew point safe: {dew_point_safe}\n"
+ min_t = min(temps)
+ if dew_point_safe:
+ dew_reason = (
+ f"min internal temp {min_t:.1f}°C > "
+ f"dew point {ext_dew:.1f}°C + 1°C safety margin"
+ )
+ else:
+ dew_reason = (
+ f"min internal temp {min_t:.1f}°C ≤ "
+ f"dew point {ext_dew:.1f}°C + 1°C safety margin"
+ )
+ log_msg += "!!! Warning: Dew point conditions are not safe for opening door !!!\n"
+ log_msg += f"Dew point safe: {'YES' if dew_point_safe else 'NO'} ({dew_reason})\n"
# 2. Check if high voltage is off
- hv_on = check_any_hv_on(caen_ch_status, used_channels)
+ hv_on = check_any_hv_on(caen_ch_status, used_channels)
hv_safe = not hv_on
- log_msg += f"High voltage safe: {hv_safe}\n"
+ if not hv_safe:
+ on_hv = [
+ ch for ch in used_channels.get("HV", [])
+ if ch and bool(caen_ch_status.get(f"caen_{ch}_IsOn", False))
+ ]
+ hv_reason = f"channel(s) still ON: {on_hv}" if on_hv else "HV status uncertain"
+ log_msg += "!!! Warning: High voltage is ON, not safe to open door !!!\n"
+ else:
+ hv_reason = "all HV channels are off"
+ log_msg += f"High voltage safe: {'YES' if hv_safe else 'NO'} ({hv_reason})\n"
# 3. Check if light is off (light should be off when opening door)
# light_off = not check_light_status(system_status)
@@ -148,7 +234,7 @@ def check_door_safe_to_open(system_status, caen_ch_status, used_channels):
except Exception as e:
logger.debug(f"Error in check_door_safe_to_open: {str(e)}")
- return False # Conservative approach - if we can't check, assume it's unsafe
+ return False, "Error checking door safety"
def check_light_safe_to_turn_on(system_status, caen_ch_status, used_channels):
@@ -160,7 +246,9 @@ def check_light_safe_to_turn_on(system_status, caen_ch_status, used_channels):
try:
# Check if we have all necessary data
if "coldroom" not in system_status:
- return False # Conservative approach - if we can't check, assume it's unsafe
+ return (
+ False # Conservative approach - if we can't check, assume it's unsafe
+ )
# Check if high voltage is off
hv_on = check_any_hv_on(caen_ch_status, used_channels)
@@ -191,11 +279,279 @@ def check_marta_safe(system_status):
# def check_marta_on_for_ot
# def check_marta_on_for_it
-#def switch_all_lv_off
+# def switch_all_lv_off
# def soft_interlock_loop: ### describe only highlevel
# if condition then : action
# if ! check_lv_safe_on and something_on : switch_all_lv_off()
-#when performing an active safety action here we send a msg to the alarm topic "/alarm"
\ No newline at end of file
+# when performing an active safety action here we send a msg to the alarm topic "/alarm"
+
+
+def Is_any_lv_on(caen_ch_status, used_channels):
+ """
+ Check if any LV channel is on.
+ Returns True if any LV channel is on, False if all are off.
+ """
+ try:
+ for channel in used_channels["LV"]:
+ if channel is None:
+ continue
+ ch_str = f"caen_{channel}_IsOn"
+ if bool(caen_ch_status.get(ch_str, False)):
+ logger.info(f"LV channel {channel} is ON")
+ return True
+ return False
+ except Exception as e:
+ logger.debug(f"Error in Is_any_lv_on: {str(e)}")
+ return True # Conservative - assume LV is on if we can't check
+
+
+def Is_it_safe_to_on_lv(system_status, caen_ch_status, used_channels):
+ """
+ Check if it's safe to turn on LV channels based on multiple safety conditions.
+ Returns True if it's safe to turn on LV channels, False otherwise.
+ """
+ log_msg = ""
+ try:
+ # Check if we have all necessary data
+ if "coldroom" not in system_status:
+ return False, "coldroom data not available — assuming unsafe"
+
+ # Check if MARTA is running
+ marta_safe, marta_msg = check_marta_safe(system_status)
+ log_msg += f"MARTA safe: {marta_safe} ({marta_msg})\n"
+
+ # It's safe to turn on LV channels if MARTA is safe
+ is_safe = marta_safe
+ return is_safe, log_msg
+
+ except Exception as e:
+ logger.debug(f"Error in Is_it_safe_to_on_lv: {str(e)}")
+ return False, "Error checking LV safety"
+
+
+# def check_lv_safe_on(caen_ch_status, used_channels):
+# """
+# Check if any LV channel is on.
+# Returns True if any LV channel is on, False if all are off.
+# """
+# try:
+# for channel in used_channels["LV"]:
+# if channel is None:
+# continue
+# ch_str = f"caen_{channel}_IsOn"
+# if bool(caen_ch_status.get(ch_str, False)):
+# return True
+# return False
+# except Exception as e:
+# logger.debug(f"Error in check_lv_safe_on: {str(e)}")
+# return True # Conservative - assume LV is on if we can't check
+
+
+def check_marta_on_for_OT(system_status):
+ """
+ Check if MARTA CO2 supply is active for OT (Outer Tracker).
+
+ Primary signal: MARTA FSM state is not disconnected/idle.
+ Secondary signal: outer_valve from serviceroom data (used when available).
+ Returns True only when CO2 is confirmed to be flowing to OT modules.
+ """
+ try:
+ if "marta" not in system_status:
+ logger.debug("MARTA data not available, treating OT CO2 as OFF")
+ return False
+
+ fsm_state = system_status["marta"].get("fsm_state", "")
+ logger.info(f"Checking MARTA OT status: fsm_state={fsm_state!r}")
+
+ if fsm_state in ("DISCONNECTED", "NONE", ""):
+ logger.info("MARTA is disconnected/idle, OT CO2 is OFF")
+ return False
+
+ # If serviceroom valve data is available, use it for a precise check.
+ # Without it, fall back to FSM state as the best available indicator.
+ if "serviceroom" in system_status:
+ OT_valve = system_status["serviceroom"].get("outer_valve", 0)
+ logger.info(f"MARTA OT valve status: outer_valve={OT_valve}")
+ if OT_valve != 1:
+ logger.info("OT valve is closed, OT CO2 is OFF")
+ return False
+ else:
+ logger.debug("Serviceroom data unavailable, relying on MARTA FSM state for OT check")
+
+ return True
+
+ except Exception as e:
+ logger.debug(f"Error in check_marta_on_for_OT: {str(e)}")
+ return False
+
+
+def check_marta_on_for_IT(system_status):
+ """
+ Check if MARTA CO2 supply is active for IT (Inner Tracker).
+
+ Primary signal: MARTA FSM state is not disconnected/idle.
+ Secondary signal: inner_valve from serviceroom data (used when available).
+ Returns True only when CO2 is confirmed to be flowing to IT modules.
+ """
+ try:
+ if "marta" not in system_status:
+ logger.debug("MARTA data not available, treating IT CO2 as OFF")
+ return False
+
+ fsm_state = system_status["marta"].get("fsm_state", "")
+ logger.info(f"Checking MARTA IT status: fsm_state={fsm_state!r}")
+
+ if fsm_state in ("DISCONNECTED", "NONE", ""):
+ logger.info("MARTA is disconnected/idle, IT CO2 is OFF")
+ return False
+
+ # If serviceroom valve data is available, use it for a precise check.
+ if "serviceroom" in system_status:
+ IT_valve = system_status["serviceroom"].get("inner_valve", 0)
+ logger.info(f"MARTA IT valve status: inner_valve={IT_valve}")
+ if IT_valve != 1:
+ logger.info("IT valve is closed, IT CO2 is OFF")
+ return False
+ else:
+ logger.debug("Serviceroom data unavailable, relying on MARTA FSM state for IT check")
+
+ return True
+
+ except Exception as e:
+ logger.debug(f"Error in check_marta_on_for_IT: {str(e)}")
+ return False
+
+
+def switch_all_hv_off(caen, used_channels):
+ """
+ Turn off all HV channels.
+ Must be called BEFORE switch_all_lv_off — cutting LV while HV is still
+ ramped risks a sudden uncontrolled discharge through the silicon sensors.
+ Returns True if all commands were sent successfully.
+ """
+ try:
+ for channel in used_channels["HV"]:
+ if channel is None:
+ continue
+ logger.warning(f"Safety interlock: turning off HV channel {channel}")
+ caen.off(channel)
+ return True
+ except Exception as e:
+ logger.error(f"Error in switch_all_hv_off: {str(e)}")
+ return False
+
+
+def switch_all_lv_off(caen, used_channels):
+ """
+ Turn off all LV channels.
+ Always call switch_all_hv_off first so HV has been cut before LV is removed.
+ Returns True if all commands were sent successfully.
+ """
+ try:
+ for channel in used_channels["LV"]:
+ logger.info(f"Attempting to turn off LV channel {channel}")
+ if channel is None:
+ continue
+ logger.warning(f"Safety interlock: turning off LV channel {channel}")
+ caen.off(channel)
+ return True
+ except Exception as e:
+ logger.error(f"Error in switch_all_lv_off: {str(e)}")
+ return False
+
+
+def soft_interlock_loop(
+ system_status, caen_ch_status, used_channels, caen, publish_alarm=None
+):
+ """
+ Soft interlock loop - monitors safety conditions and takes protective action.
+
+ Decision tree (evaluated every ~5 s):
+ 1. If MARTA is not in a safe/connected state AND any power (HV or LV) is on
+ → switch all HV off first, then all LV off
+ 2. Else if any power is on AND MARTA CO2 is not flowing to OT modules
+ → switch all HV off first, then all LV off
+ HV is always cut before LV to avoid an uncontrolled discharge
+ through the silicon sensors.
+ (IT modules share the same MARTA CO2 system; a full MARTA shutdown
+ is caught by condition 1. Per-valve IT protection requires
+ serviceroom data to be subscribed — see check_marta_on_for_IT.)
+
+ A message is published to /alarm whenever a protective action fires.
+
+ Args:
+ system_status (dict): Full system status including MARTA, coldroom, etc.
+ caen_ch_status (dict): CAEN channel status with caen_{channel}_IsOn keys.
+ used_channels (dict): Active channel list {"LV": [...], "HV": [...]}.
+ caen: CAEN control object with on()/off() methods.
+ publish_alarm (callable, optional): publish_alarm(message_string)
+
+ Returns:
+ tuple: (is_safe: bool, message: str)
+ """
+ try:
+ lv_on = Is_any_lv_on(caen_ch_status, used_channels)
+ # Is_it_safe_to_on_lv returns (bool, str) — unpack properly
+ lv_safe_to_on, lv_safe_msg = Is_it_safe_to_on_lv(
+ system_status, caen_ch_status, used_channels
+ )
+ marta_ot = check_marta_on_for_OT(system_status)
+ marta_it = check_marta_on_for_IT(system_status)
+
+ lv_status = "ON" if lv_on else "OFF"
+ marta_ot_status = "RUNNING" if marta_ot else "NOT RUNNING"
+ marta_it_status = "RUNNING" if marta_it else "NOT RUNNING"
+
+ log_msg = (
+ f"\nSoft interlock: LV={lv_status}, "
+ f"MARTA_OT={marta_ot_status}, MARTA_IT={marta_it_status}\n"
+ )
+ logger.info(log_msg)
+
+ hv_on = check_any_hv_on(caen_ch_status, used_channels)
+ hv_status = "ON" if hv_on else "OFF"
+ log_msg += f"HV={hv_status}\n"
+
+ # --- Condition 1: MARTA itself is not safe (e.g. disconnected) ---
+ if not lv_safe_to_on:
+ log_msg += "\n!!! Warning: MARTA not safe — LV safe to turn on: NO\n"
+ log_msg += lv_safe_msg
+ if lv_on or hv_on:
+ alarm_msg = (
+ "SAFETY INTERLOCK: MARTA is not in a safe state and power is ON. "
+ "Turning off all HV then LV channels to prevent module damage."
+ )
+ logger.warning(alarm_msg)
+ switch_all_hv_off(caen, used_channels)
+ switch_all_lv_off(caen, used_channels)
+ if publish_alarm:
+ publish_alarm(alarm_msg)
+ return False, alarm_msg
+ return True, log_msg
+
+ log_msg += "\nLV safe to turn on: YES\n"
+
+ # --- Condition 2: MARTA connected but CO2 not flowing to OT ---
+ if (lv_on or hv_on) and not marta_ot:
+ alarm_msg = (
+ "SAFETY INTERLOCK: Power is ON but MARTA OT CO2 is not flowing. "
+ "Turning off all HV then LV channels to prevent module damage."
+ )
+ logger.warning(alarm_msg)
+ switch_all_hv_off(caen, used_channels)
+ switch_all_lv_off(caen, used_channels)
+ if publish_alarm:
+ publish_alarm(alarm_msg)
+ return False, alarm_msg
+
+ log_msg += "\nAll safety conditions met.\n"
+ logger.info(log_msg)
+ return True, log_msg
+
+ except Exception as e:
+ err_msg = f"Error in soft_interlock_loop: {str(e)}"
+ logger.error(err_msg)
+ return False, err_msg
diff --git a/coldroom/system.py b/coldroom/system.py
index 9002f45..c4337d7 100644
--- a/coldroom/system.py
+++ b/coldroom/system.py
@@ -16,7 +16,12 @@ def __init__(self):
# Load settings
self._settings = {} # Initialize private settings variable
try:
- with open(os.path.join(os.path.dirname(__file__), os.pardir, "settings_coldroom.yaml"), "r") as f:
+ with open(
+ os.path.join(
+ os.path.dirname(__file__), os.pardir, "settings_coldroom.yaml"
+ ),
+ "r",
+ ) as f:
self._settings = yaml.safe_load(f)
logger.debug("Settings loaded successfully")
logger.debug(f"MQTT Broker: {self._settings['mqtt']['broker']}")
@@ -28,14 +33,25 @@ def __init__(self):
"Cleanroom": {"mqtt_topic": "/environment/HumAndTemp001/#"},
"MARTA": {"mqtt_topic": "/MARTA/#"},
"Coldroom": {"mqtt_topic": "/coldroom/#"},
- "ThermalCamera": {"mqtt_topic": "/thermalcamera/#"}
+ "ThermalCamera": {"mqtt_topic": "/thermalcamera/#"},
}
# Global variables
self.BROKER = self._settings["mqtt"]["broker"]
self.PORT = self._settings["mqtt"]["port"]
- self._status = {"marta": {}, "coldroom": {}, "thermal_camera": {}, "caen": {}, "cleanroom": {}, "coldroomair": {}}
- self.safety_flags = {"door_locked": True, "sleep": True, "hv_safe": False} # Default value to safest state
+ self._status = {
+ "marta": {},
+ "coldroom": {},
+ "thermal_camera": {},
+ "caen": {},
+ "cleanroom": {},
+ "coldroomair": {},
+ }
+ self.safety_flags = {
+ "door_locked": True,
+ "sleep": True,
+ "hv_safe": False,
+ } # Default value to safest state
# Thread control
self._mqtt_thread = None
@@ -58,9 +74,9 @@ def __init__(self):
except Exception as e:
logger.error(f"Error initializing Thermal Camera client: {e}")
self._thermalcamera = None
-
+
try:
- self._caen = None
+ self._caen = None
except Exception as e:
logger.error(f"Error initializing CAEN client: {e}")
self._caen = None
@@ -73,7 +89,7 @@ def __init__(self):
def settings(self):
"""Get the current settings"""
return self._settings
-
+
@settings.setter
def settings(self, value):
"""Update settings and save to file"""
@@ -81,7 +97,9 @@ def settings(self, value):
raise ValueError("Settings must be a dictionary")
self._settings = value
try:
- with open(os.path.join(os.path.dirname(__file__), "settings.yaml"), "w") as f:
+ with open(
+ os.path.join(os.path.dirname(__file__), "settings.yaml"), "w"
+ ) as f:
yaml.dump(self._settings, f, default_flow_style=False)
except Exception as e:
logger.error(f"Error saving settings: {e}")
@@ -115,7 +133,7 @@ def start_mqtt_thread(self):
self._mqtt_thread.daemon = True
self._mqtt_thread.start()
logger.info("MQTT thread started")
-
+
# Start CAEN status update thread
if self._caen and not self._caen_thread:
self._caen_thread_stop = False
@@ -133,7 +151,7 @@ def stop_mqtt_thread(self):
self._thread_stop = True
self._mqtt_thread.join(timeout=2)
logger.info("MQTT thread stopped")
-
+
# Stop CAEN status thread
if self._caen_thread and self._caen_thread.is_alive():
self._caen_thread_stop = True
@@ -154,13 +172,13 @@ def _mqtt_loop(self):
if self._thermalcamera:
logger.debug("Starting Thermal Camera client loop")
self._thermalcamera.loop_start()
-
+
logger.info("All MQTT client loops started")
-
+
# Keep thread running
while not self._thread_stop:
time.sleep(1)
-
+
except Exception as e:
logger.error(f"Error in MQTT loop: {e}")
finally:
@@ -184,21 +202,21 @@ def cleanup(self):
try:
# Stop MQTT thread first
self.stop_mqtt_thread()
-
+
# Stop individual client loops
- if hasattr(self, '_martacoldroom') and self._martacoldroom:
+ if hasattr(self, "_martacoldroom") and self._martacoldroom:
logger.debug("Stopping MARTA/Coldroom/Cleanroom client loops")
self._martacoldroom.stop_client_loops()
- if hasattr(self, '_thermalcamera') and self._thermalcamera:
+ if hasattr(self, "_thermalcamera") and self._thermalcamera:
logger.debug("Stopping Thermal Camera client loop")
self._thermalcamera.loop_start()
-
- if hasattr(self, '_caen') and self._caen:
+
+ if hasattr(self, "_caen") and self._caen:
logger.debug("Disconnecting CAEN TCP client")
self._caen.disconnect()
-
+
logger.info("All resources cleaned up")
-
+
except Exception as e:
logger.error(f"Error during cleanup: {e}")
diff --git a/coldroom/thermal_camera.py b/coldroom/thermal_camera.py
index da53dfb..3368939 100644
--- a/coldroom/thermal_camera.py
+++ b/coldroom/thermal_camera.py
@@ -36,7 +36,9 @@ def __init__(self, system_obj):
try:
self._client.connect(self._system.BROKER, self._system.PORT, keepalive=60)
- logger.info(f"Connected to MQTT broker at {self._system.BROKER}:{self._system.PORT}")
+ logger.info(
+ f"Connected to MQTT broker at {self._system.BROKER}:{self._system.PORT}"
+ )
except Exception as e:
logger.error(f"Failed to connect to MQTT broker: {e}")
raise
@@ -88,10 +90,15 @@ def handle_camera_message(self, topic, payload):
position = float(data["position"])
# Convert to float array
- flo_arr = [struct.unpack("f", image_data[i : i + 4])[0] for i in range(0, len(image_data), 4)]
+ flo_arr = [
+ struct.unpack("f", image_data[i : i + 4])[0]
+ for i in range(0, len(image_data), 4)
+ ]
# Process image
- processed_image = np.flip(np.rot90(np.array(flo_arr).reshape(24, 32)), axis=0)
+ processed_image = np.flip(
+ np.rot90(np.array(flo_arr).reshape(24, 32)), axis=0
+ )
# Store for stitching - accumulate images for each position
if camera_name not in self._stitching_data:
@@ -104,7 +111,9 @@ def handle_camera_message(self, topic, payload):
# Keep only the last 5 images for each position to prevent memory issues
if len(self._stitching_data[camera_name][position]) > 5:
- self._stitching_data[camera_name][position] = self._stitching_data[camera_name][position][-5:]
+ self._stitching_data[camera_name][position] = self._stitching_data[
+ camera_name
+ ][position][-5:]
# Update current image
self._images[camera_name] = processed_image
diff --git a/coldroom/thermal_camera_gui.py b/coldroom/thermal_camera_gui.py
index b4be869..ba83681 100644
--- a/coldroom/thermal_camera_gui.py
+++ b/coldroom/thermal_camera_gui.py
@@ -20,7 +20,9 @@ def __init__(self, system):
# Load the UI file into a QMainWindow
self.widget = QtWidgets.QMainWindow()
- uic.loadUi(os.path.join(os.path.dirname(__file__), "thermal_camera.ui"), self.widget)
+ uic.loadUi(
+ os.path.join(os.path.dirname(__file__), "thermal_camera.ui"), self.widget
+ )
self.ui = self.widget # Assign the loaded UI to self.ui
# Initialize camera positions from config file
@@ -40,9 +42,11 @@ def __init__(self, system):
self.update_camera_displays()
# Set default temperature range to CO2 and connect signal
- if hasattr(self.ui, 't_range_comboBox'):
+ if hasattr(self.ui, "t_range_comboBox"):
self.ui.t_range_comboBox.setCurrentText("CO2")
- self.ui.t_range_comboBox.currentTextChanged.connect(self.on_temperature_range_changed)
+ self.ui.t_range_comboBox.currentTextChanged.connect(
+ self.on_temperature_range_changed
+ )
# Setup update timer
self.update_timer = QTimer()
@@ -60,7 +64,9 @@ def __init__(self, system):
def get_camera_modules_map(self):
if self.mounted_modules is None:
- logger.debug("Mounted modules not set, returning default camera modules map")
+ logger.debug(
+ "Mounted modules not set, returning default camera modules map"
+ )
return {
"camera1": "Unknown",
"camera2": "Unknown",
@@ -81,7 +87,10 @@ def get_camera_modules_map(self):
for camera_name, camera_offset in self.camera_positions.items():
camera_position = (absolute_position + camera_offset["position"]) % 360
module_slot = str(module_info.get("mounted_on", "-").split(";")[1])
- if abs(module_position - camera_position) <= 20 and module_side == camera_offset["side"]:
+ if (
+ abs(module_position - camera_position) <= 20
+ and module_side == camera_offset["side"]
+ ):
camera_modules[camera_name] = f"{module_slot};{module_name}"
close_camera = True
break
@@ -116,7 +125,9 @@ def load_camera_config(self):
self.save_camera_config(default_config)
return default_config
else:
- logger.info(f"Config file not found at {self.config_file}, creating with defaults")
+ logger.info(
+ f"Config file not found at {self.config_file}, creating with defaults"
+ )
# Save default config to file
self.save_camera_config(default_config)
return default_config
@@ -180,22 +191,24 @@ def setup_camera_views(self):
# Create matplotlib figure for individual cameras with adjusted size
self.cameras_fig = Figure(figsize=(8, 5), dpi=100) # Increased width
self.cameras_canvas = FigureCanvas(self.cameras_fig)
-
+
# Create navigation toolbar for camera views
self.cameras_toolbar = NavigationToolbar(self.cameras_canvas, self.widget)
-
+
# Create a widget to hold both toolbar and canvas
camera_widget = QtWidgets.QWidget()
camera_layout = QtWidgets.QVBoxLayout(camera_widget)
camera_layout.addWidget(self.cameras_toolbar)
camera_layout.addWidget(self.cameras_canvas)
camera_layout.setContentsMargins(0, 0, 0, 0)
-
+
# Add the combined widget to the scene
scene1.addWidget(camera_widget)
# Create 2x2 grid for cameras with minimal spacing
- self.cameras_axes = self.cameras_fig.subplots(2, 2, gridspec_kw={"hspace": 0.15, "wspace": 0.1})
+ self.cameras_axes = self.cameras_fig.subplots(
+ 2, 2, gridspec_kw={"hspace": 0.15, "wspace": 0.1}
+ )
self.camera_images = []
# Initialize camera images with proper sizing and coordinate formatting
@@ -203,7 +216,12 @@ def setup_camera_views(self):
for i in range(2):
for j in range(2):
ax = self.cameras_axes[i, j]
- img = ax.imshow(np.zeros((24, 32)), cmap="plasma", aspect="equal", interpolation="nearest")
+ img = ax.imshow(
+ np.zeros((24, 32)),
+ cmap="plasma",
+ aspect="equal",
+ interpolation="nearest",
+ )
self.camera_images.append(img)
camera_index += 1 # Increment for each camera
ax.set_title(f"Camera {camera_index}")
@@ -211,14 +229,20 @@ def setup_camera_views(self):
ax.set_yticks([])
# Set up coordinate formatter to show temperature values
- ax.format_coord = lambda x, y, ax_ref=ax, img_ref=img: self.format_coord_temperature(x, y, ax_ref, img_ref)
+ ax.format_coord = lambda x, y, ax_ref=ax, img_ref=img: self.format_coord_temperature(
+ x, y, ax_ref, img_ref
+ )
# Add colorbar with proper sizing
- cbar = self.cameras_fig.colorbar(img, ax=ax, fraction=0.046, pad=0.04)
+ cbar = self.cameras_fig.colorbar(
+ img, ax=ax, fraction=0.046, pad=0.04
+ )
cbar.ax.tick_params(labelsize=8)
# Adjust subplot parameters to remove excess whitespace
- self.cameras_fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)
+ self.cameras_fig.subplots_adjust(
+ left=0.05, right=0.95, top=0.95, bottom=0.05
+ )
logger.info("Camera views with navigation toolbar initialized")
@@ -230,22 +254,22 @@ def format_coord_temperature(self, x, y, ax, img):
try:
# Get the image data
data = img.get_array()
-
+
# Convert mouse coordinates to array indices
numrows, numcols = data.shape
col = int(x + 0.5)
row = int(y + 0.5)
-
+
# Check if coordinates are within bounds
if 0 <= col < numcols and 0 <= row < numrows:
temp_value = data[row, col]
- return f'x={col}, y={row}, T={temp_value:.1f}°C'
+ return f"x={col}, y={row}, T={temp_value:.1f}°C"
else:
- return f'x={x:.1f}, y={y:.1f}'
-
+ return f"x={x:.1f}, y={y:.1f}"
+
except Exception as e:
logger.error(f"Error formatting coordinates: {e}")
- return f'x={x:.1f}, y={y:.1f}'
+ return f"x={x:.1f}, y={y:.1f}"
def snapshot(self):
"""Save current camera views"""
@@ -274,7 +298,7 @@ def enable_controls(self, enabled=True):
self.ui.calibrate_PB,
self.ui.set_abs_pos_PB,
self.ui.export_abs_pos_PB,
-# self.ui.get_frms_PB,
+ # self.ui.get_frms_PB,
self.ui.relse_mtr_PB,
self.ui.run_PB,
self.ui.stop_PB,
@@ -297,17 +321,25 @@ def connect_signals(self):
self.ui.calibrate_PB.clicked.connect(self.calibrate)
self.ui.set_abs_pos_PB.clicked.connect(self.set_absolute_position)
self.ui.export_abs_pos_PB.clicked.connect(self.export_absolute_position)
- # self.ui.get_frms_PB.clicked.connect(self.get_frames)
+ # self.ui.get_frms_PB.clicked.connect(self.get_frames)
self.ui.relse_mtr_PB.clicked.connect(self.release_motor)
self.ui.run_PB.clicked.connect(self.run)
self.ui.stop_PB.clicked.connect(self.stop)
self.ui.snapshot_PB.clicked.connect(self.snapshot)
# Connect camera coordinate setting buttons
- self.ui.camera_set_pos_button_1.clicked.connect(lambda: self.set_camera_position(1))
- self.ui.camera_set_pos_button_2.clicked.connect(lambda: self.set_camera_position(2))
- self.ui.camera_set_pos_button_3.clicked.connect(lambda: self.set_camera_position(3))
- self.ui.camera_set_pos_button_4.clicked.connect(lambda: self.set_camera_position(4))
+ self.ui.camera_set_pos_button_1.clicked.connect(
+ lambda: self.set_camera_position(1)
+ )
+ self.ui.camera_set_pos_button_2.clicked.connect(
+ lambda: self.set_camera_position(2)
+ )
+ self.ui.camera_set_pos_button_3.clicked.connect(
+ lambda: self.set_camera_position(3)
+ )
+ self.ui.camera_set_pos_button_4.clicked.connect(
+ lambda: self.set_camera_position(4)
+ )
logger.info("Thermal camera signals connected")
except Exception as e:
@@ -323,12 +355,16 @@ def set_camera_position(self, camera_id):
try:
new_position = float(pos_le.text())
except ValueError:
- logger.error(f"Invalid position value for camera {camera_id}: {pos_le.text()}")
+ logger.error(
+ f"Invalid position value for camera {camera_id}: {pos_le.text()}"
+ )
return
# Validate position range (0-360 degrees)
if not (0 <= new_position <= 360):
- logger.error(f"Position out of range for camera {camera_id}: {new_position}. Must be 0-360°")
+ logger.error(
+ f"Position out of range for camera {camera_id}: {new_position}. Must be 0-360°"
+ )
return
# Update camera position offset in our data structure
@@ -358,7 +394,9 @@ def update_camera_displays(self):
camera_modules_map = self.get_camera_modules_map()
logger.debug(f"Camera modules map: {camera_modules_map}")
- for i, (camera_name, camera_info) in enumerate(self.camera_positions.items(), 1):
+ for i, (camera_name, camera_info) in enumerate(
+ self.camera_positions.items(), 1
+ ):
# Calculate effective position based on current system position and camera offset
camera_offset = camera_info["position"]
effective_position = (current_system_pos + camera_offset) % 360
@@ -404,7 +442,9 @@ def move_camera(self, camera_id, target_angle):
absolute_position = absolute_position % 360
logger.info(f"Moving camera {camera_id} to target {target_angle}°")
- logger.info(f"Camera offset: {camera_offset}°, calculated absolute position: {absolute_position}°")
+ logger.info(
+ f"Camera offset: {camera_offset}°, calculated absolute position: {absolute_position}°"
+ )
# Use the go_to function to move to the calculated absolute position
if self.system._thermalcamera:
@@ -414,7 +454,9 @@ def move_camera(self, camera_id, target_angle):
# Wait a moment for the system to update (you might want to make this more robust)
self._update_all_camera_positions_after_move(absolute_position)
- logger.info(f"Camera {camera_id} moved to target position {target_angle}°")
+ logger.info(
+ f"Camera {camera_id} moved to target position {target_angle}°"
+ )
return True
else:
logger.error("Thermal camera system not available")
@@ -473,7 +515,9 @@ def get_camera_effective_position(self, camera_id):
logger.error(f"Invalid camera ID: {camera_id}")
return None
except Exception as e:
- logger.error(f"Error calculating effective position for camera {camera_id}: {e}")
+ logger.error(
+ f"Error calculating effective position for camera {camera_id}: {e}"
+ )
return None
def update_status(self):
@@ -495,7 +539,9 @@ def update_status(self):
"background-color: green;" if streaming else "background-color: red;"
)
running = status.get("running", False)
- self.ui.run_stat_flg.setStyleSheet("background-color: green;" if running else "background-color: red;")
+ self.ui.run_stat_flg.setStyleSheet(
+ "background-color: green;" if running else "background-color: red;"
+ )
switch_state = status.get("switch_state", False)
self.ui.switch_state_flag.setStyleSheet(
@@ -524,7 +570,9 @@ def update_camera_images_co2_scale(self):
marta_status = self.system.status.get("marta", {})
co2_temp = float(marta_status.get("TT06_CO2", 0.0))
except (ValueError, TypeError):
- logger.warning("Could not get CO2 temperature from MARTA status, using default range")
+ logger.warning(
+ "Could not get CO2 temperature from MARTA status, using default range"
+ )
co2_temp = 0.0
# Calculate colorbar range: CO2 temp to CO2 temp + 20
@@ -532,7 +580,9 @@ def update_camera_images_co2_scale(self):
colorbar_max = co2_temp + 15.0
# Update individual camera views
- for i, (camera_name, image_data) in enumerate(self.system._thermalcamera._images.items()):
+ for i, (camera_name, image_data) in enumerate(
+ self.system._thermalcamera._images.items()
+ ):
if isinstance(image_data, np.ndarray):
# Convert to float if needed
if image_data.dtype != np.float64:
@@ -552,7 +602,12 @@ def update_camera_images_co2_scale(self):
cbar = self.camera_images[i].colorbar
if cbar is not None:
cbar.set_ticks(np.linspace(colorbar_min, colorbar_max, 5))
- cbar.set_ticklabels([f"{temp:.1f}°C" for temp in np.linspace(colorbar_min, colorbar_max, 5)])
+ cbar.set_ticklabels(
+ [
+ f"{temp:.1f}°C"
+ for temp in np.linspace(colorbar_min, colorbar_max, 5)
+ ]
+ )
# Draw camera canvas
self.cameras_canvas.draw()
@@ -568,7 +623,9 @@ def update_camera_images_auto_scale(self):
"""Update camera images using automatic scaling based on image data"""
try:
# Update individual camera views with auto-scaling
- for i, (camera_name, image_data) in enumerate(self.system._thermalcamera._images.items()):
+ for i, (camera_name, image_data) in enumerate(
+ self.system._thermalcamera._images.items()
+ ):
if isinstance(image_data, np.ndarray):
# Convert to float if needed
if image_data.dtype != np.float64:
@@ -590,7 +647,9 @@ def update_camera_images_auto_scale(self):
cbar = self.camera_images[i].colorbar
if cbar is not None:
cbar.set_ticks(np.linspace(vmin, vmax, 5))
- cbar.set_ticklabels([f"{temp:.1f}°C" for temp in np.linspace(vmin, vmax, 5)])
+ cbar.set_ticklabels(
+ [f"{temp:.1f}°C" for temp in np.linspace(vmin, vmax, 5)]
+ )
# Draw camera canvas
self.cameras_canvas.draw()
@@ -608,10 +667,12 @@ def set_scaling_mode(self, use_co2_scale=True):
"""
try:
self.use_co2_scaling = use_co2_scale
- logger.info(f"Camera scaling mode set to: {'CO2 temperature range' if use_co2_scale else 'Auto-scale'}")
+ logger.info(
+ f"Camera scaling mode set to: {'CO2 temperature range' if use_co2_scale else 'Auto-scale'}"
+ )
# Update the ComboBox to reflect the change
- if hasattr(self.ui, 't_range_comboBox'):
+ if hasattr(self.ui, "t_range_comboBox"):
combo_text = "CO2" if use_co2_scale else "Auto"
self.ui.t_range_comboBox.setCurrentText(combo_text)
@@ -653,8 +714,14 @@ def rotate(self):
try:
angle = float(self.ui.ip_DAngle_LE.text())
if self.system._thermalcamera:
- direction = "bw" if self.ui.direction_combo.currentText() == "Clockwise" else "fw"
- self.system._thermalcamera.rotate({"angle": angle, "direction": direction})
+ direction = (
+ "bw"
+ if self.ui.direction_combo.currentText() == "Clockwise"
+ else "fw"
+ )
+ self.system._thermalcamera.rotate(
+ {"angle": angle, "direction": direction}
+ )
logger.info(f"Rotating camera by {angle} degrees")
except ValueError:
logger.error("Invalid angle value")
@@ -680,8 +747,14 @@ def calibrate(self):
# Use ip_angle_LE_2 which contains the 90-degree angle input
limit = float(self.ui.ip_angle_LE_2.text())
if self.system._thermalcamera:
- direction = "bw" if self.ui.direction_combo.currentText() == "Clockwise" else "fw"
- self.system._thermalcamera.calibrate({"prudence": limit, "direction": direction})
+ direction = (
+ "bw"
+ if self.ui.direction_combo.currentText() == "Clockwise"
+ else "fw"
+ )
+ self.system._thermalcamera.calibrate(
+ {"prudence": limit, "direction": direction}
+ )
logger.info(f"Calibrating camera with limit {limit} degrees")
self.update_camera_displays()
except ValueError:
diff --git a/db/module_db.py b/db/module_db.py
index 41ca4f3..201a686 100644
--- a/db/module_db.py
+++ b/db/module_db.py
@@ -1,6 +1,14 @@
from PyQt5.QtWidgets import (
- QWidget, QVBoxLayout, QLineEdit, QLabel, QTreeWidgetItem,
- QMessageBox, QPushButton, QInputDialog, QHBoxLayout, QTreeWidget
+ QWidget,
+ QVBoxLayout,
+ QLineEdit,
+ QLabel,
+ QTreeWidgetItem,
+ QMessageBox,
+ QPushButton,
+ QInputDialog,
+ QHBoxLayout,
+ QTreeWidget,
)
from PyQt5.QtCore import Qt, pyqtSignal
import requests
@@ -9,24 +17,25 @@
import re
from db.module_db_gui import Ui_ModuleDBWidget
+
class ModuleDB(QWidget):
"""Widget for managing module inventory and details"""
-
+
# Signals
module_selected = pyqtSignal(str) # Emitted when module is selected
-
+
def __init__(self, parent=None):
super().__init__(parent)
-
+
# Set up UI
self.ui = Ui_ModuleDBWidget()
self.ui.setupUi(self)
-
+
# Initialize variables
self.all_modules = []
self.mounted_modules = {}
self.current_module_id = None
-
+
# Add layer type mapping
self.layers_to_filters = {
"L1_47": {
@@ -58,19 +67,19 @@ def __init__(self, parent=None):
"speed": "5G",
},
}
-
+
# Setup UI components
self.setup_filters()
self.setup_tree_widget()
self.setup_search()
self.setup_module_details_tab()
-
+
# Connect signals
self.connect_signals()
-
+
# Load settings
self.load_settings()
-
+
# Initial module list load
self.update_module_list()
@@ -80,10 +89,10 @@ def setup_filters(self):
self.ui.layertypeCB.clear()
self.ui.layertypeCB.addItem("any")
self.ui.layertypeCB.addItems(sorted(self.layers_to_filters.keys()))
-
+
# Add "any" to status filter
- #self.ui.spacerCB_3.insertItem(0, "any")
-
+ # self.ui.spacerCB_3.insertItem(0, "any")
+
# Add grade options
self.ui.spacerCB_2.addItems(["A++", "A+", "A", "B", "C"])
@@ -91,15 +100,15 @@ def setup_tree_widget(self):
"""Setup tree widget properties"""
# Enable sorting
self.ui.treeWidget.setSortingEnabled(True)
-
+
# Set column widths
self.ui.treeWidget.setColumnWidth(0, 150) # Name
self.ui.treeWidget.setColumnWidth(1, 70) # Inventory Slot
- self.ui.treeWidget.setColumnWidth(2, 70) # Speed
- self.ui.treeWidget.setColumnWidth(3, 70) # Spacer
+ self.ui.treeWidget.setColumnWidth(2, 70) # Speed
+ self.ui.treeWidget.setColumnWidth(3, 70) # Spacer
self.ui.treeWidget.setColumnWidth(4, 100) # Status
self.ui.treeWidget.setColumnWidth(5, 150) # Description
-
+
# Enable selection mode
self.ui.treeWidget.setSelectionMode(QTreeWidget.SingleSelection)
@@ -111,7 +120,7 @@ def connect_signals(self):
self.ui.editDetailsButton.clicked.connect(self.edit_selected_detail)
self.ui.saveDetailsButton.clicked.connect(self.save_module_details)
self.ui.disconnectButton.clicked.connect(self.disconnect_module)
-
+
# Connect filter signals
self.ui.speedCB.currentTextChanged.connect(self.update_module_list)
self.ui.spacerCB.currentTextChanged.connect(self.update_module_list)
@@ -121,8 +130,12 @@ def connect_signals(self):
def get_settings_file(self):
"""Get the settings file path"""
- config_file = os.path.join(os.path.expanduser("~/.config/module_db"), 'settings.yaml')
- bundled_file = os.path.join(os.path.dirname(__file__), os.pardir, 'settings_integration.yaml')
+ config_file = os.path.join(
+ os.path.expanduser("~/.config/module_db"), "settings.yaml"
+ )
+ bundled_file = os.path.join(
+ os.path.dirname(__file__), os.pardir, "settings_integration.yaml"
+ )
if os.path.exists(config_file):
return config_file
elif os.path.exists(bundled_file):
@@ -133,147 +146,161 @@ def get_settings_file(self):
def load_settings(self):
"""Load settings from YAML file"""
try:
- with open(self.get_settings_file(), 'r') as f:
+ with open(self.get_settings_file(), "r") as f:
settings = yaml.safe_load(f)
if settings:
- self.db_url = settings.get('db_url', 'http://localhost:5000')
+ self.db_url = settings.get("db_url", "http://localhost:5000")
except FileNotFoundError:
- self.db_url = 'http://localhost:5000'
+ self.db_url = "http://localhost:5000"
self.save_settings()
def save_settings(self):
"""Save settings to YAML file"""
- settings = {
- 'db_url': self.db_url
- }
- with open(self.get_settings_file(), 'w') as f:
+ settings = {"db_url": self.db_url}
+ with open(self.get_settings_file(), "w") as f:
yaml.dump(settings, f)
- def get_api_url(self, endpoint=''):
+ def get_api_url(self, endpoint=""):
"""Get full API URL with endpoint"""
- base_url = self.db_url.rstrip('/')
+ base_url = self.db_url.rstrip("/")
return f"{base_url}/{endpoint.lstrip('/')}" if endpoint else base_url
def setup_module_details_tab(self):
"""Setup the module details tab"""
# Set up tree widget
- self.ui.detailsTree.setHeaderLabels(['Field', 'Value'])
+ self.ui.detailsTree.setHeaderLabels(["Field", "Value"])
self.ui.detailsTree.setColumnWidth(0, 200)
- def make_api_request(self, endpoint, method='GET', data=None):
+ def make_api_request(self, endpoint, method="GET", data=None):
"""Make API request with error handling"""
try:
url = self.get_api_url(endpoint)
- if method == 'GET':
+ if method == "GET":
response = requests.get(url)
- elif method == 'POST':
+ elif method == "POST":
response = requests.post(url, json=data)
- elif method == 'PUT':
+ elif method == "PUT":
response = requests.put(url, json=data)
else:
return False, f"Unsupported method: {method}"
-
+
if response.status_code == 200:
return True, response.json()
else:
return False, response.text
-
+
except Exception as e:
return False, str(e)
def update_module_list(self):
"""Update the module list from database"""
- success, modules = self.make_api_request('modules')
+ success, modules = self.make_api_request("modules")
if not success:
self.show_error_dialog(f"Failed to fetch modules: {modules}")
return
-
+
self.all_modules = modules
self.filter_modules()
def filter_modules(self, search_text=None):
"""Filter modules based on search text and all filters"""
if search_text is None:
- search_text = self.searchBox.text()
- search_text=search_text
-
+ search_text = self.searchBox.text()
+ search_text = search_text
+
self.ui.treeWidget.clear()
-
+
for module in self.all_modules:
print(module["moduleName"])
# Get module speed
module_speed = ""
- if "_5_" in module.get("moduleName", "") or "_05_" in module.get("moduleName", "") or "_5-" in module.get("moduleName", "") or "_05-" in module.get("moduleName", ""):
+ if (
+ "_5_" in module.get("moduleName", "")
+ or "_05_" in module.get("moduleName", "")
+ or "_5-" in module.get("moduleName", "")
+ or "_05-" in module.get("moduleName", "")
+ ):
module_speed = "5G"
- if "_10_" in module.get("moduleName", "") or "_10-" in module.get("moduleName", ""):
+ if "_10_" in module.get("moduleName", "") or "_10-" in module.get(
+ "moduleName", ""
+ ):
module_speed = "10G"
-
+
# Check hybrid details for speed
- if isinstance(module.get("children"),dict):
- if isinstance(module.get("children").get("PS Read-out Hybrid"),dict):
- if isinstance(module.get("children").get("PS Read-out Hybrid").get("details"),dict):
- if module.get("children").get("PS Read-out Hybrid").get("details").get("ALPGBT_BANDWIDTH") is not None:
- module_speed = module.get("children").get("PS Read-out Hybrid").get("details").get("ALPGBT_BANDWIDTH")
+ if isinstance(module.get("children"), dict):
+ if isinstance(module.get("children").get("PS Read-out Hybrid"), dict):
+ if isinstance(
+ module.get("children").get("PS Read-out Hybrid").get("details"),
+ dict,
+ ):
+ if (
+ module.get("children")
+ .get("PS Read-out Hybrid")
+ .get("details")
+ .get("ALPGBT_BANDWIDTH")
+ is not None
+ ):
+ module_speed = (
+ module.get("children")
+ .get("PS Read-out Hybrid")
+ .get("details")
+ .get("ALPGBT_BANDWIDTH")
+ )
if module_speed == "10Gbps":
module_speed = "10G"
if module_speed == "5Gbps":
module_speed = "5G"
-
+
module["speed"] = module_speed
-
+
# Get spacer
fields = module.get("moduleName", "").split("_")
spacer = fields[1] if len(fields) > 2 else ""
module["spacer"] = spacer
- if not "Pisa" in module.get("details",{}).get("LOCATION",""):
- continue
+ if not "Pisa" in module.get("details", {}).get("LOCATION", ""):
+ continue
print("In Pisa")
# Apply all filters
- if search_text and search_text not in module.get('moduleName', ''):
+ if search_text and search_text not in module.get("moduleName", ""):
continue
print("search ok")
-
+
speed = self.ui.speedCB.currentText()
- if speed != 'any' and module_speed != speed:
+ if speed != "any" and module_speed != speed:
continue
print("Speed ok")
spacer_filter = self.ui.spacerCB.currentText()
- spacer_dict = {
- "4.0mm": "40",
- "2.6mm": "26",
- "1.8mm": "18",
- "any": "any"
- }
+ spacer_dict = {"4.0mm": "40", "2.6mm": "26", "1.8mm": "18", "any": "any"}
spacer_filter = spacer_dict[spacer_filter]
- if spacer_filter != 'any' and spacer != spacer_filter:
+ if spacer_filter != "any" and spacer != spacer_filter:
continue
print("Spacer ok")
-
+
grade = self.ui.spacerCB_2.currentText()
- if grade != 'any' and module.get('grade') != grade:
+ if grade != "any" and module.get("grade") != grade:
continue
-
+
status = self.ui.spacerCB_3.currentText()
- if status != 'any' and module.get('status') != status:
+ if status != "any" and module.get("status") != status:
continue
-
+
# Add matching module to tree
item = QTreeWidgetItem(self.ui.treeWidget)
item.setText(0, module.get("moduleName", ""))
-# item.setText(1, module.get("inventorySlot", ""))
- item.setText(1, module.get("position",""))
+ # item.setText(1, module.get("inventorySlot", ""))
+ item.setText(1, module.get("position", ""))
item.setText(2, module.get("speed", ""))
item.setText(3, str(module.get("spacer", "")))
- item.setText(4, module.get("status",""))
+ item.setText(4, module.get("status", ""))
item.setText(6, module.get("details", {}).get("DESCRIPTION", ""))
-
+
# Handle connections column
connections = module.get("crateSide", {})
print(connections)
- connections = [y[0] for x,y in connections.items() if len(y)>0]
+ connections = [y[0] for x, y in connections.items() if len(y) > 0]
connections = list(set(connections)) # Make connections entries unique
item.setText(5, "/".join(connections))
-
+
# Add disconnect button if there are connections
if len(connections) > 0:
# Create a widget to hold both connection info and button
@@ -281,7 +308,7 @@ def filter_modules(self, search_text=None):
layout = QVBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
-
+
# Create button with connection info
disconnect_button = QPushButton()
# Make button smaller
@@ -289,16 +316,20 @@ def filter_modules(self, search_text=None):
font = disconnect_button.font()
font.setPointSize(8)
disconnect_button.setFont(font)
- print("conn",connections)
- button_text = "Disconnect "+ ("/".join(connections))
+ print("conn", connections)
+ button_text = "Disconnect " + ("/".join(connections))
disconnect_button.setText(button_text)
- disconnect_button.clicked.connect(lambda checked, m=module.get("moduleName", ""): self.disconnect_module(m))
-
+ disconnect_button.clicked.connect(
+ lambda checked, m=module.get(
+ "moduleName", ""
+ ): self.disconnect_module(m)
+ )
+
layout.addWidget(disconnect_button)
self.ui.treeWidget.setItemWidget(item, 5, container)
-
+
item.setText(8, module.get("mounted_on", ""))
-
+
# Resize columns to content
for i in range(self.ui.treeWidget.columnCount()):
self.ui.treeWidget.resizeColumnToContents(i)
@@ -311,19 +342,19 @@ def populate_details_tree(self, data, parent=None, path=""):
# Store original data structure for reference
self.original_data_types = {}
self.store_data_types(data, "")
-
+
# Add disconnect button if module has connections or is mounted
- if data.get('connections') or data.get('mounted_on'):
+ if data.get("connections") or data.get("mounted_on"):
disconnect_item = QTreeWidgetItem(parent)
disconnect_item.setText(0, "Actions")
disconnect_button = QPushButton("Disconnect")
disconnect_button.clicked.connect(self.disconnect_module)
self.ui.detailsTree.setItemWidget(disconnect_item, 1, disconnect_button)
-
+
if isinstance(data, dict):
for key, value in sorted(data.items()):
current_path = f"{path}.{key}" if path else key
- item = QTreeWidgetItem([str(key), ''])
+ item = QTreeWidgetItem([str(key), ""])
# Store the path in the item for later reference
item.setData(0, Qt.UserRole, current_path)
if parent == self.ui.detailsTree:
@@ -334,11 +365,13 @@ def populate_details_tree(self, data, parent=None, path=""):
elif isinstance(data, list):
for i, value in enumerate(data):
current_path = f"{path}[{i}]"
- if isinstance(value, dict) and 'childName' in value:
+ if isinstance(value, dict) and "childName" in value:
# Special handling for child components
- item = QTreeWidgetItem([value['childName'], value.get('childType', '')])
+ item = QTreeWidgetItem(
+ [value["childName"], value.get("childType", "")]
+ )
else:
- item = QTreeWidgetItem([f"[{i}]", ''])
+ item = QTreeWidgetItem([f"[{i}]", ""])
item.setData(0, Qt.UserRole, current_path)
parent.addChild(item)
self.populate_details_tree(value, item, current_path)
@@ -349,12 +382,12 @@ def populate_details_tree(self, data, parent=None, path=""):
def store_data_types(self, data, path):
"""Store the original data types for later reconstruction"""
if isinstance(data, dict):
- self.original_data_types[path] = 'dict'
+ self.original_data_types[path] = "dict"
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
self.store_data_types(value, current_path)
elif isinstance(data, list):
- self.original_data_types[path] = 'list'
+ self.original_data_types[path] = "list"
for i, value in enumerate(data):
current_path = f"{path}[{i}]"
self.store_data_types(value, current_path)
@@ -382,16 +415,16 @@ def get_item_value(self, item, path):
if item.childCount() == 0:
# Leaf node - convert to appropriate type
text_value = item.text(1)
- original_type = self.original_data_types.get(path, 'str')
-
- if original_type == 'bool':
- return text_value.lower() == 'true'
- elif original_type == 'int':
+ original_type = self.original_data_types.get(path, "str")
+
+ if original_type == "bool":
+ return text_value.lower() == "true"
+ elif original_type == "int":
try:
return int(text_value)
except ValueError:
return text_value
- elif original_type == 'float':
+ elif original_type == "float":
try:
return float(text_value)
except ValueError:
@@ -400,9 +433,9 @@ def get_item_value(self, item, path):
return text_value
else:
# Non-leaf node - check if it should be a list or dict
- original_type = self.original_data_types.get(path, 'dict')
-
- if original_type == 'list':
+ original_type = self.original_data_types.get(path, "dict")
+
+ if original_type == "list":
# Reconstruct as list
result = []
children_data = []
@@ -411,7 +444,7 @@ def get_item_value(self, item, path):
child_path = child.data(0, Qt.UserRole)
child_value = self.get_item_value(child, child_path)
children_data.append((child_path, child_value))
-
+
# Sort by list index and add to result
children_data.sort(key=lambda x: self.extract_list_index(x[0]))
for _, value in children_data:
@@ -431,17 +464,18 @@ def get_item_value(self, item, path):
def extract_list_index(self, path):
"""Extract list index from path like 'children[0]' -> 0"""
import re
- match = re.search(r'\[(\d+)\]$', path)
+
+ match = re.search(r"\[(\d+)\]$", path)
return int(match.group(1)) if match else 0
def tree_to_dict(self, item):
"""Convert tree widget items back to dictionary recursively"""
result = {}
-
+
for i in range(item.childCount()):
child = item.child(i)
key = child.text(0)
-
+
if child.childCount() > 0:
# This is a non-leaf node, recurse
value = self.tree_to_dict(child)
@@ -450,33 +484,37 @@ def tree_to_dict(self, item):
value = child.text(1)
# Try to convert to appropriate type
try:
- if value.lower() == 'true':
+ if value.lower() == "true":
value = True
- elif value.lower() == 'false':
+ elif value.lower() == "false":
value = False
elif value.isdigit():
value = int(value)
- elif value.replace('.','',1).isdigit():
+ elif value.replace(".", "", 1).isdigit():
value = float(value)
except:
pass
-
+
# Handle nested paths using dot notation
- key_parts = key.split('.')
+ key_parts = key.split(".")
current_dict = result
for part in key_parts[:-1]:
if part not in current_dict:
current_dict[part] = {}
current_dict = current_dict[part]
current_dict[key_parts[-1]] = value
-
+
return result
def merge_dicts(self, dict1, dict2):
"""Recursively merge two dictionaries"""
result = dict1.copy()
for key, value in dict2.items():
- if key in result and isinstance(result[key], dict) and isinstance(value, dict):
+ if (
+ key in result
+ and isinstance(result[key], dict)
+ and isinstance(value, dict)
+ ):
result[key] = self.merge_dicts(result[key], value)
else:
result[key] = value
@@ -488,7 +526,7 @@ def select_module(self):
if not selected_items:
self.show_error_dialog("No module selected")
return
-
+
module_id = selected_items[0].text(0)
self.module_selected.emit(module_id)
@@ -498,7 +536,7 @@ def view_module_details(self):
if not selected_items:
self.show_error_dialog("No module selected")
return
-
+
module_id = selected_items[0].text(0) # Get module name
self.current_module_id = module_id
self.ui.moduleNameLabel.setText(module_id)
@@ -510,10 +548,12 @@ def load_module_details(self):
if not self.current_module_id:
self.ui.moduleNameLabel.setText("")
return
-
+
try:
- success, module_data = self.make_api_request(f'modules/{self.current_module_id}')
- self.current_module_data=module_data
+ success, module_data = self.make_api_request(
+ f"modules/{self.current_module_id}"
+ )
+ self.current_module_data = module_data
if success:
self.populate_details_tree(module_data)
else:
@@ -533,7 +573,7 @@ def edit_selected_detail(self):
dialog.setWindowTitle("Edit Value")
dialog.setLabelText(f"Edit value for {item.text(0)}:")
dialog.setTextValue(item.text(1))
-
+
if dialog.exec_():
item.setText(1, dialog.textValue())
@@ -541,34 +581,38 @@ def save_module_details(self):
"""Save the modified module details back to the database"""
try:
# First get current module data to preserve all fields
- success, current_data = self.make_api_request(f'modules/{self.current_module_id}')
+ success, current_data = self.make_api_request(
+ f"modules/{self.current_module_id}"
+ )
if not success:
self.show_error_dialog(f"Error fetching module: {current_data}")
return
-
+
# Convert tree widget back to dictionary preserving types
- new_data = self.tree_to_dict_preserving_types(self.ui.detailsTree.invisibleRootItem())
-
+ new_data = self.tree_to_dict_preserving_types(
+ self.ui.detailsTree.invisibleRootItem()
+ )
+
# Recursively merge the data
merged_data = self.merge_dicts(current_data, new_data)
-
+
# Remove _id from merged data
if "_id" in merged_data:
del merged_data["_id"]
-
+
# Make API request to update module
success, result = self.make_api_request(
- endpoint=f'modules/{self.current_module_id}',
- method='PUT',
- data=merged_data
+ endpoint=f"modules/{self.current_module_id}",
+ method="PUT",
+ data=merged_data,
)
-
+
if success:
self.show_info_dialog("Module details updated successfully")
self.update_module_list() # Refresh the module list to show updated data
else:
self.show_error_dialog(f"Error updating module: {result}")
-
+
except Exception as e:
self.show_error_dialog(f"Error saving module details: {str(e)}")
@@ -582,7 +626,7 @@ def update_filters_from_layer(self, layer_type):
self.ui.speedCB.setCurrentText("any")
self.ui.spacerCB.setCurrentText("any")
return
-
+
if layer_type in self.layers_to_filters:
filters = self.layers_to_filters[layer_type]
self.ui.speedCB.setCurrentText(filters["speed"])
@@ -597,7 +641,7 @@ def setup_search(self):
searchLayout.addWidget(searchLabel)
searchLayout.addWidget(self.searchBox)
searchLayout.addStretch()
-
+
# Insert search layout before the tree widget
layout = self.ui.tab_2.layout()
if layout:
@@ -608,28 +652,26 @@ def setup_search(self):
layout = QVBoxLayout(self.ui.tab_2)
layout.addLayout(searchLayout)
- def disconnect_module(self,m):
+ def disconnect_module(self, m):
"""Disconnect the current module"""
-
+
try:
data = {
- "cable": m,
- }
+ "cable": m,
+ }
success, result = self.make_api_request(
- endpoint="disconnect_all_crateSide",
- method="POST",
- data=data
- )
-
+ endpoint="disconnect_all_crateSide", method="POST", data=data
+ )
+
if success:
self.show_info_dialog("Module disconnected successfully")
self.update_module_list() # Refresh the module list
self.load_module_details() # Refresh the details view
else:
self.show_error_dialog(f"Error disconnecting module: {result}")
-
+
except Exception as e:
self.show_error_dialog(f"Error disconnecting module: {str(e)}")
- # ... Copy other methods from main.py ...
+ # ... Copy other methods from main.py ...
diff --git a/db/module_db_gui.py b/db/module_db_gui.py
index 39a4026..3cf9368 100644
--- a/db/module_db_gui.py
+++ b/db/module_db_gui.py
@@ -79,7 +79,9 @@ def setupUi(self, ModuleDBWidget):
self.selectModulePB = QtWidgets.QPushButton(self.tab_2)
self.selectModulePB.setObjectName("selectModulePB")
self.horizontalLayout_10.addWidget(self.selectModulePB)
- spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ spacerItem = QtWidgets.QSpacerItem(
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
+ )
self.horizontalLayout_10.addItem(spacerItem)
self.viewDetailsPB = QtWidgets.QPushButton(self.tab_2)
self.viewDetailsPB.setObjectName("viewDetailsPB")
@@ -141,23 +143,39 @@ def retranslateUi(self, ModuleDBWidget):
self.spacerCB_2.setItemText(0, _translate("ModuleDBWidget", "any"))
self.label_11.setText(_translate("ModuleDBWidget", "Status:"))
self.spacerCB_3.setItemText(0, _translate("ModuleDBWidget", "any"))
- self.spacerCB_3.setItemText(1, _translate("ModuleDBWidget", "Ready For Mounting"))
+ self.spacerCB_3.setItemText(
+ 1, _translate("ModuleDBWidget", "Ready For Mounting")
+ )
self.spacerCB_3.setItemText(2, _translate("ModuleDBWidget", "Mounted"))
self.spacerCB_3.setItemText(3, _translate("ModuleDBWidget", "To Be Tested"))
self.treeWidget.headerItem().setText(0, _translate("ModuleDBWidget", "Name"))
- self.treeWidget.headerItem().setText(1, _translate("ModuleDBWidget", "Inventory Slot"))
+ self.treeWidget.headerItem().setText(
+ 1, _translate("ModuleDBWidget", "Inventory Slot")
+ )
self.treeWidget.headerItem().setText(2, _translate("ModuleDBWidget", "Speed"))
self.treeWidget.headerItem().setText(3, _translate("ModuleDBWidget", "Spacer"))
self.treeWidget.headerItem().setText(4, _translate("ModuleDBWidget", "Status"))
- self.treeWidget.headerItem().setText(5, _translate("ModuleDBWidget", "Connections"))
- self.treeWidget.headerItem().setText(6, _translate("ModuleDBWidget", "Description"))
- self.treeWidget.headerItem().setText(7, _translate("ModuleDBWidget", "Mounted_on"))
+ self.treeWidget.headerItem().setText(
+ 5, _translate("ModuleDBWidget", "Connections")
+ )
+ self.treeWidget.headerItem().setText(
+ 6, _translate("ModuleDBWidget", "Description")
+ )
+ self.treeWidget.headerItem().setText(
+ 7, _translate("ModuleDBWidget", "Mounted_on")
+ )
self.selectModulePB.setText(_translate("ModuleDBWidget", "<-- Select Module"))
self.viewDetailsPB.setText(_translate("ModuleDBWidget", "View Details -->"))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("ModuleDBWidget", "Module Inventory"))
+ self.tabWidget.setTabText(
+ self.tabWidget.indexOf(self.tab_2),
+ _translate("ModuleDBWidget", "Module Inventory"),
+ )
self.detailsTree.headerItem().setText(0, _translate("ModuleDBWidget", "Field"))
self.detailsTree.headerItem().setText(1, _translate("ModuleDBWidget", "Value"))
self.editDetailsButton.setText(_translate("ModuleDBWidget", "Edit Selected"))
self.saveDetailsButton.setText(_translate("ModuleDBWidget", "Save Changes"))
self.disconnectButton.setText(_translate("ModuleDBWidget", "Disconnect"))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.moduleDetailsTab), _translate("ModuleDBWidget", "Module Details"))
+ self.tabWidget.setTabText(
+ self.tabWidget.indexOf(self.moduleDetailsTab),
+ _translate("ModuleDBWidget", "Module Details"),
+ )
diff --git a/db/utils.py b/db/utils.py
index d1cd1f7..f5d7d5c 100644
--- a/db/utils.py
+++ b/db/utils.py
@@ -4,15 +4,17 @@
def get_module_name_from_fc7(fc7, optical_group, db_url="http://cmslabserver:5000"):
"""Get the module name from the FC7 and optical group."""
url = f"{db_url}/snapshot"
- response = requests.post(url, json={"cable": fc7, "port": optical_group, "side": "detSide"})
+ response = requests.post(
+ url, json={"cable": fc7, "port": optical_group, "side": "detSide"}
+ )
if response.status_code == 200:
snapshot = response.json()
for line in snapshot:
if snapshot[line]["connections"]:
last_conn = snapshot[line]["connections"][-1]
- #{'cable': 'PS_40_IBA-10003', 'line': 1, 'det_port': [], 'crate_port': ['fiber']}
- #{'cable': 'PS_40_IBA-10003', 'line': 2, 'det_port': [], 'crate_port': ['fiber']}
- if 'fiber' in last_conn["crate_port"]:
+ # {'cable': 'PS_40_IBA-10003', 'line': 1, 'det_port': [], 'crate_port': ['fiber']}
+ # {'cable': 'PS_40_IBA-10003', 'line': 2, 'det_port': [], 'crate_port': ['fiber']}
+ if "fiber" in last_conn["crate_port"]:
return last_conn["cable"]
return None
@@ -20,32 +22,36 @@ def get_module_name_from_fc7(fc7, optical_group, db_url="http://cmslabserver:500
def get_ring_from_cable(cable_id, db_url="http://cmslabserver:5000"):
"""Navigate from cable to modules, then check the mounted_on attribute removing the ;position trailing part"""
url = f"{db_url}/snapshot"
- modules=[]
+ modules = []
response = requests.post(url, json={"cable": cable_id, "side": "detSide"})
-
+
if response.status_code == 200:
snapshot = response.json()
- # print(snapshot)
+ # print(snapshot)
for line in snapshot:
if snapshot[line]["connections"]:
last_conn = snapshot[line]["connections"][-1]
- #{'cable': 'PS_40_IBA-10003', 'line': 1, 'det_port': [], 'crate_port': ['fiber']}
- #{'cable': 'PS_40_IBA-10003', 'line': 2, 'det_port': [], 'crate_port': ['fiber']}
- if 'power' in last_conn["crate_port"] or 'fiber' in last_conn["crate_port"]:
+ # {'cable': 'PS_40_IBA-10003', 'line': 1, 'det_port': [], 'crate_port': ['fiber']}
+ # {'cable': 'PS_40_IBA-10003', 'line': 2, 'det_port': [], 'crate_port': ['fiber']}
+ if (
+ "power" in last_conn["crate_port"]
+ or "fiber" in last_conn["crate_port"]
+ ):
modules.append(last_conn["cable"])
-
- rings=[]
+
+ rings = []
for m in modules:
m_info = get_module(m, db_url)
if m_info:
mounted_on = m_info.get("mounted_on", "")
- if mounted_on == "" :
+ if mounted_on == "":
continue
if ";" in mounted_on:
mounted_on = mounted_on.split(";")[0]
rings.append(mounted_on)
return rings[0] if len(rings) else None
+
def get_modules_on_ring(ring_id, db_url="http://cmslabserver:5000"):
"""Get the modules on a specific ring from the database snapshot."""
url = f"{db_url}/generic_module_query"
@@ -66,27 +72,28 @@ def get_modules_on_ring(ring_id, db_url="http://cmslabserver:5000"):
return None
-
-def get_module_from_lpGBT_hwId(serial_number, db_url ="http://cmslabserver:5000"):
+def get_module_from_lpGBT_hwId(serial_number, db_url="http://cmslabserver:5000"):
"""Get the module from the lpGBT serial number using generic_query"""
url = f"{db_url}/generic_module_query"
try:
- response = requests.post(url, json= {"children.lpGBT.CHILD_SERIAL_NUMBER": str(serial_number)})
+ response = requests.post(
+ url, json={"children.lpGBT.CHILD_SERIAL_NUMBER": str(serial_number)}
+ )
if response.status_code == 200:
- snapshot = response.json()
+ snapshot = response.json()
for module in snapshot:
- if str(serial_number) in module.get("children", {}).get("lpGBT", {}).get("CHILD_SERIAL_NUMBER", ""):
+ if str(serial_number) in module.get("children", {}).get(
+ "lpGBT", {}
+ ).get("CHILD_SERIAL_NUMBER", ""):
return module["moduleName"]
-
+
else:
print(f"Error: Received status code {response.status_code}")
return None
except requests.RequestException as e:
print(f"Error making request to {url}: {str(e)}")
return None
-
-
def get_module(module_id, db_url="http://cmslabserver:5000"):
@@ -104,6 +111,7 @@ def get_module(module_id, db_url="http://cmslabserver:5000"):
print(f"Error making request to {url}: {str(e)}")
return None
+
def get_module_lpgbtVersion(module, db_url="http://cmslabserver:5000"):
# the version is under childer, PS Read-out Hybrid, details, ALPGBT_VERSION
if isinstance(module, str):
@@ -114,11 +122,25 @@ def get_module_lpgbtVersion(module, db_url="http://cmslabserver:5000"):
lpgbtVersion = "N/A"
if isinstance(module.get("children"), dict):
if isinstance(module.get("children").get("PS Read-out Hybrid"), dict):
- if isinstance(module.get("children").get("PS Read-out Hybrid").get("details"), dict):
- if module.get("children").get("PS Read-out Hybrid").get("details").get("ALPGBT_VERSION") is not None:
- lpgbtVersion = module.get("children").get("PS Read-out Hybrid").get("details").get("ALPGBT_VERSION")
+ if isinstance(
+ module.get("children").get("PS Read-out Hybrid").get("details"), dict
+ ):
+ if (
+ module.get("children")
+ .get("PS Read-out Hybrid")
+ .get("details")
+ .get("ALPGBT_VERSION")
+ is not None
+ ):
+ lpgbtVersion = (
+ module.get("children")
+ .get("PS Read-out Hybrid")
+ .get("details")
+ .get("ALPGBT_VERSION")
+ )
return lpgbtVersion
+
def get_module_speed(module, db_url="http://cmslabserver:5000"):
# check if the name or the object is given
if isinstance(module, str):
@@ -139,10 +161,21 @@ def get_module_speed(module, db_url="http://cmslabserver:5000"):
# Check hybrid details for speed
if isinstance(module.get("children"), dict):
if isinstance(module.get("children").get("PS Read-out Hybrid"), dict):
- if isinstance(module.get("children").get("PS Read-out Hybrid").get("details"), dict):
- if module.get("children").get("PS Read-out Hybrid").get("details").get("ALPGBT_BANDWIDTH") is not None:
+ if isinstance(
+ module.get("children").get("PS Read-out Hybrid").get("details"), dict
+ ):
+ if (
+ module.get("children")
+ .get("PS Read-out Hybrid")
+ .get("details")
+ .get("ALPGBT_BANDWIDTH")
+ is not None
+ ):
module_speed = (
- module.get("children").get("PS Read-out Hybrid").get("details").get("ALPGBT_BANDWIDTH")
+ module.get("children")
+ .get("PS Read-out Hybrid")
+ .get("details")
+ .get("ALPGBT_BANDWIDTH")
)
if module_speed == "10Gbps":
module_speed = "10G"
@@ -169,7 +202,9 @@ def get_module_endpoints(module_id, db_url="http://cmslabserver:5000"):
elif "XSLOT" in last_conn["cable"]:
ret["LV"] = f"LV{last_conn['cable'][5:]}.{last_conn['line']}"
elif "ASLOT" in last_conn["cable"]:
- ret["HV"] = f"HV{last_conn['cable'][5:]}.{last_conn['det_port'][0]}"
+ ret["HV"] = (
+ f"HV{last_conn['cable'][5:]}.{last_conn['det_port'][0]}"
+ )
return ret
else:
print(f"Error: Received status code {response.status_code}")
@@ -207,9 +242,12 @@ def get_module_fuse_id(module, db_url="http://cmslabserver:5000"):
if modules:
# iterate on modules and print the module name and speed and mounted on, sorted by mounted_on
for module_name, module in sorted(
- modules.items(), key=lambda x: int(x[1].get("mounted_on", ";0").split(";")[1])
+ modules.items(),
+ key=lambda x: int(x[1].get("mounted_on", ";0").split(";")[1]),
):
speed = get_module_speed(module)
- print(f"Speed: {speed} \t {module.get('mounted_on', 'N/A')} \t {module_name}")
+ print(
+ f"Speed: {speed} \t {module.get('mounted_on', 'N/A')} \t {module_name}"
+ )
else:
print(f"Failed to retrieve modules for ring {ring_id}.")
diff --git a/dbbrowser.py b/dbbrowser.py
index 079b174..b036a37 100755
--- a/dbbrowser.py
+++ b/dbbrowser.py
@@ -3,23 +3,26 @@
from PyQt5.QtWidgets import QApplication, QMainWindow
from db.module_db import ModuleDB
+
class DBMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Module Database")
-
+
# Create and set the module DB widget as central widget
self.module_db = ModuleDB()
self.setCentralWidget(self.module_db)
-
+
# Set initial size
self.resize(1000, 800)
+
def main():
app = QApplication(sys.argv)
window = DBMainWindow()
window.show()
sys.exit(app.exec_())
-if __name__ == '__main__':
- main()
+
+if __name__ == "__main__":
+ main()
diff --git a/dbrunsearch.py b/dbrunsearch.py
index 536cf04..cde6db2 100755
--- a/dbrunsearch.py
+++ b/dbrunsearch.py
@@ -4,6 +4,7 @@
import json
from datetime import datetime
+
def get_run_details(run_number, base_url="http://cmslabserver:5000"):
"""Get details for a specific run number."""
try:
@@ -12,13 +13,14 @@ def get_run_details(run_number, base_url="http://cmslabserver:5000"):
if response.status_code != 200:
print(f"Error fetching run details: {response.status_code}")
return None
-
+
return response.json()
-
+
except requests.RequestException as e:
print(f"Error connecting to database: {e}")
return None
+
def get_session_details(session_name, base_url="http://cmslabserver:5000"):
"""Get detailed information for a specific session."""
try:
@@ -26,13 +28,14 @@ def get_session_details(session_name, base_url="http://cmslabserver:5000"):
if response.status_code != 200:
print(f"Error fetching session details: {response.status_code}")
return None
-
+
return response.json()
-
+
except requests.RequestException as e:
print(f"Error fetching session details: {e}")
return None
+
def format_run_details(run_data):
"""Format run details for display."""
output = []
@@ -46,20 +49,21 @@ def format_run_details(run_data):
output.append(f"Type: {run_data.get('runType', 'N/A')}")
output.append(f"Module Tests: {', '.join(run_data.get('moduleTestName', []))}")
output.append(f"Run File: {run_data.get('runFile', 'N/A')}")
-
+
# Format run configuration if present
- if 'runConfiguration' in run_data:
+ if "runConfiguration" in run_data:
output.append("\nRun Configuration:")
- config = run_data['runConfiguration']
+ config = run_data["runConfiguration"]
output.append(f" Events: {config.get('Nevents', 'N/A')}")
- if 'boards' in config:
- for board_id, board_data in config['boards'].items():
+ if "boards" in config:
+ for board_id, board_data in config["boards"].items():
output.append(f" Board {board_id}:")
output.append(f" IP: {board_data.get('ip', 'N/A')}")
-
+
output.append("=" * 80)
return "\n".join(output)
+
def format_session_details(session):
"""Format session details for display."""
output = []
@@ -70,72 +74,97 @@ def format_session_details(session):
output.append(f"Timestamp: {session.get('timestamp', 'N/A')}")
output.append(f"Description: {session.get('description', 'N/A')}")
output.append(f"Modules List: {', '.join(session.get('modulesList', []))}")
-
+
# Add any additional details that might be present
for key, value in session.items():
- if key not in ['sessionName', 'operator', 'timestamp', 'description', 'modulesList']:
+ if key not in [
+ "sessionName",
+ "operator",
+ "timestamp",
+ "description",
+ "modulesList",
+ ]:
output.append(f"{key}: {value}")
-
+
output.append("=" * 80)
return "\n".join(output)
-def update_session_comment(session_name, new_comment, base_url="http://cmslabserver:5000"):
+
+def update_session_comment(
+ session_name, new_comment, base_url="http://cmslabserver:5000"
+):
"""Update the description/comment for a specific session."""
try:
# Prepare the update data
update_data = {"description": new_comment}
-
+
# Send PUT request to update the session
response = requests.put(f"{base_url}/sessions/{session_name}", json=update_data)
-
+
if response.status_code != 200:
print(f"Error updating session comment: {response.status_code}")
return False
-
+
return True
-
+
except requests.RequestException as e:
print(f"Error updating session comment: {e}")
return False
+
def main():
- parser = argparse.ArgumentParser(description='Search for run and session details by run number')
- parser.add_argument('run_number', type=str, help='Run number to search for')
- parser.add_argument('--url', default='http://cmslabserver:5000',
- help='Base URL for the database API')
- parser.add_argument('--edit-comment', nargs='?', const=True, metavar='COMMENT',
- help='Edit the session comment. If COMMENT is provided, updates directly. If no COMMENT, enters interactive mode')
-
+ parser = argparse.ArgumentParser(
+ description="Search for run and session details by run number"
+ )
+ parser.add_argument("run_number", type=str, help="Run number to search for")
+ parser.add_argument(
+ "--url",
+ default="http://cmslabserver:5000",
+ help="Base URL for the database API",
+ )
+ parser.add_argument(
+ "--edit-comment",
+ nargs="?",
+ const=True,
+ metavar="COMMENT",
+ help="Edit the session comment. If COMMENT is provided, updates directly. If no COMMENT, enters interactive mode",
+ )
+
args = parser.parse_args()
-
+
# Get run details
run_data = get_run_details(args.run_number, args.url)
-
+
if not run_data:
print(f"No run found with number {args.run_number}")
return
-
+
# Print run details
print(format_run_details(run_data))
-
+
# Get and display session details if available
- session_name = run_data.get('runSession')
+ session_name = run_data.get("runSession")
if session_name:
session_details = get_session_details(session_name, args.url)
if session_details:
print(format_session_details(session_details))
-
+
# Handle comment editing if requested
if args.edit_comment is not None:
- print("\nCurrent comment:", session_details.get('description', 'No comment'))
-
+ print(
+ "\nCurrent comment:",
+ session_details.get("description", "No comment"),
+ )
+
# If comment was provided via command line, use it directly
if isinstance(args.edit_comment, str):
new_comment = args.edit_comment
else:
# Otherwise enter interactive mode
- new_comment = input("Enter new comment (press Enter to keep current): ").strip()
-
+ new_comment = input(
+ "Enter new comment (press Enter to keep current): "
+ ).strip()
+
if new_comment:
if update_session_comment(session_name, new_comment, args.url):
print("Comment updated successfully!")
@@ -151,6 +180,6 @@ def main():
else:
print("\nNo session information available for this run")
+
if __name__ == "__main__":
main()
-
diff --git a/drawEvent.py b/drawEvent.py
index b3185d6..b198a9e 100644
--- a/drawEvent.py
+++ b/drawEvent.py
@@ -6,6 +6,7 @@
layer_spacers = {0: 26, 1: 40, 2: 40, 3: 40, 4: 26}
+
def get_xy_strip(hybrid_id, address, fe_id):
# Logic from drawEvent.C:
# X: -int(SCluster_HybridId/2)*500+spacer
@@ -20,6 +21,7 @@ def get_xy_strip(hybrid_id, address, fe_id):
y = 960 - y_val
return x, y
+
def get_xy_pixel(hybrid_id, address, fe_id):
# Logic from drawEvent.C:
# X: -int(PCluster_HybridId/2)*500
@@ -32,6 +34,7 @@ def get_xy_pixel(hybrid_id, address, fe_id):
y = 960 - y_val
return x, y
+
def get_xy_stub(hybrid_id, column, fe_id):
# Logic from drawEvent.C:
# X: -int(Stub_HybridId/2)*500+spacer/2
@@ -46,6 +49,7 @@ def get_xy_stub(hybrid_id, column, fe_id):
y = 960 - y_val
return x, y
+
def get_z_pixel(hybrid_id, zpos):
# Logic from drawEvent.C:
# (PCluster_HybridId%2==0)?PCluster_Zpos:32-PCluster_Zpos
@@ -56,6 +60,7 @@ def get_z_pixel(hybrid_id, zpos):
z = 32 - zpos
return z * 10.0
+
def get_z_stub(hybrid_id, row):
# Logic from drawEvent.C:
# (Stub_HybridId%2==0)?Stub_Row:32-Stub_Row
@@ -66,37 +71,44 @@ def get_z_stub(hybrid_id, row):
z = 32 - row
return z * 10.0
+
def draw_event(tree, entry, canvas):
tree.GetEntry(entry)
-
+
canvas.Clear()
canvas.Divide(2, 1)
-
+
# --- Pad 1: 3D View (Left) ---
pad_3d = canvas.cd(1)
- #resize it to leave top 10% free
+ # resize it to leave top 10% free
pad_3d.SetPad(0.0, 0.0, 0.5, 0.9)
# Create TView3D
view = ROOT.TView.CreateView(1)
-
+
# Center of the 5 modules
# Modules at 0, -500, -1000, -1500, -2000
center_real_y = -1000.0
- center_real_x = 480.0 # Middle of 0-960
- center_real_z = 160.0 # Middle of 0-320
-
+ center_real_x = 480.0 # Middle of 0-960
+ center_real_z = 160.0 # Middle of 0-320
+
# Mapping: (Real X, Real Z, Real Y) -> (3D X, 3D Y, 3D Z)
center_x = center_real_x
center_y = center_real_z
center_z = center_real_y
-
+
# Define a view range centered on this point
- range_dx = 500 # +/- 500 (Real X width is 960)
+ range_dx = 500 # +/- 500 (Real X width is 960)
range_dy = 300 # +/- 250 (Real Z height is 320)
- range_dz = 1100 # +/- 1500 (Real Y height is ~2200)
-
- view.SetRange(center_x - range_dx, center_y - range_dy, center_z - range_dz,
- center_x + range_dx, center_y + range_dy, center_z + range_dz)
+ range_dz = 1100 # +/- 1500 (Real Y height is ~2200)
+
+ view.SetRange(
+ center_x - range_dx,
+ center_y - range_dy,
+ center_z - range_dz,
+ center_x + range_dx,
+ center_y + range_dy,
+ center_z + range_dz,
+ )
view.ShowAxis()
# Store primitives to prevent garbage collection
@@ -108,7 +120,7 @@ def draw_box_real(x1, z1, y1, x2, z2, y2, color):
# 3D X = Real X
# 3D Y = Real Z
# 3D Z = Real Y
-
+
# Corners
p0 = (x1, z1, y1)
p1 = (x2, z1, y1)
@@ -118,7 +130,7 @@ def draw_box_real(x1, z1, y1, x2, z2, y2, color):
p5 = (x2, z1, y2)
p6 = (x2, z2, y2)
p7 = (x1, z2, y2)
-
+
# Draw edges
# Bottom face
l = ROOT.TPolyLine3D(5)
@@ -130,7 +142,7 @@ def draw_box_real(x1, z1, y1, x2, z2, y2, color):
l.SetLineColor(color)
l.Draw()
canvas.primitives.append(l)
-
+
# Top face
l2 = ROOT.TPolyLine3D(5)
l2.SetPoint(0, p4[0], p4[1], p4[2])
@@ -141,7 +153,7 @@ def draw_box_real(x1, z1, y1, x2, z2, y2, color):
l2.SetLineColor(color)
l2.Draw()
canvas.primitives.append(l2)
-
+
# Verticals
for pa, pb in [(p0, p4), (p1, p5), (p2, p6), (p3, p7)]:
l3 = ROOT.TPolyLine3D(2)
@@ -156,14 +168,16 @@ def draw_box_real(x1, z1, y1, x2, z2, y2, color):
# Base Real Y for this module
real_y = float(-i * 500)
spacer = layer_spacers.get(i, 40)
-
+
# Draw module frame (Real X: 0-960, Real Z: 0-320, Thickness: 3?)
# User said "draw also the modules without hits".
# Let's draw them as thin frames or boxes.
draw_box_real(0.0, 0.0, real_y, 960.0, 320.0, real_y + 3.0, ROOT.kGray)
-
+
# Strips part (offset spacer in Real Y)
- draw_box_real(0.0, 0.0, real_y + spacer, 960.0, 320.0, real_y + spacer + 3.0, ROOT.kGray)
+ draw_box_real(
+ 0.0, 0.0, real_y + spacer, 960.0, 320.0, real_y + spacer + 3.0, ROOT.kGray
+ )
# Helper to safely get list/array
def get_collection(obj, name):
@@ -179,16 +193,16 @@ def get_collection(obj, name):
s_addr = get_collection(tree, "SCluster_Address")
s_fe = get_collection(tree, "SCluster_FrontEndId")
s_width = get_collection(tree, "SCluster_Width")
-
+
n_strips = len(s_hids)
for i in range(n_strips):
hid = int(s_hids[i])
addr = int(s_addr[i])
fe = int(s_fe[i])
width = float(s_width[i]) if s_width else 1.0
-
+
real_y, real_x = get_xy_strip(hid, addr, fe)
-
+
# Determine Real Z range based on parity
if hid % 2 == 0:
real_z_start = 0.0
@@ -210,17 +224,25 @@ def get_collection(obj, name):
# `real_x - width` to `real_x`.
real_x_start = real_x - width
real_x_end = real_x
-
+
# Draw Box
# Thickness 3
- draw_box_real(real_x_start, real_z_start, real_y, real_x_end, real_z_end, real_y + 3.0, ROOT.kRed)
-
+ draw_box_real(
+ real_x_start,
+ real_z_start,
+ real_y,
+ real_x_end,
+ real_z_end,
+ real_y + 3.0,
+ ROOT.kRed,
+ )
+
# Draw Pixels as Boxes
p_addr = get_collection(tree, "PCluster_Address")
p_fe = get_collection(tree, "PCluster_FrontEndId")
p_zpos = get_collection(tree, "PCluster_Zpos")
p_width = get_collection(tree, "PCluster_Width")
-
+
n_pixels = len(p_hids)
for i in range(n_pixels):
hid = int(p_hids[i])
@@ -228,18 +250,18 @@ def get_collection(obj, name):
fe = int(p_fe[i])
z_raw = float(p_zpos[i])
width = float(p_width[i]) if p_width else 1.0
-
+
real_y, real_x = get_xy_pixel(hid, addr, fe)
real_z = get_z_pixel(hid, z_raw)
-
+
# Pixel Z Length = 10 units (1 pitch)
# Parity handling for X and Z directions
-
+
if hid % 2 == 0:
# Even
real_x_start = real_x
real_x_end = real_x + width
-
+
# Z: Zpos -> Zpos*10.
# Covers Zpos to Zpos+1 -> Z*10 to (Z+1)*10.
real_z_start = real_z
@@ -248,7 +270,7 @@ def get_collection(obj, name):
# Odd
real_x_start = real_x - width
real_x_end = real_x
-
+
# Z: 32 - Zpos -> (32-Zpos)*10.
# If Zpos increases, Real Z decreases.
# Covers Zpos to Zpos+1.
@@ -256,30 +278,38 @@ def get_collection(obj, name):
# (32-Zpos)*10 - 10 to (32-Zpos)*10.
real_z_start = real_z - 10.0
real_z_end = real_z
-
- draw_box_real(real_x_start, real_z_start, real_y, real_x_end, real_z_end, real_y + 3.0, ROOT.kBlue)
-
+
+ draw_box_real(
+ real_x_start,
+ real_z_start,
+ real_y,
+ real_x_end,
+ real_z_end,
+ real_y + 3.0,
+ ROOT.kBlue,
+ )
+
# Draw Stubs as Points (Removed in favor of lines)
# st_col = get_collection(tree, "Stub_Column")
# st_fe = get_collection(tree, "Stub_FrontEndId")
# st_row = get_collection(tree, "Stub_Row")
- #
+ #
# n_stubs = len(st_hids)
# if n_stubs > 0:
# pm_stub = ROOT.TPolyMarker3D(n_stubs)
# pm_stub.SetMarkerStyle(22)
# pm_stub.SetMarkerColor(ROOT.kGreen + 2)
# pm_stub.SetMarkerSize(1.2)
- #
+ #
# for i in range(n_stubs):
# hid = int(st_hids[i])
# col = float(st_col[i])
# fe = int(st_fe[i])
# row = float(st_row[i])
- #
+ #
# real_y, real_x = get_xy_stub(hid, col, fe)
# real_z = get_z_stub(hid, row)
- #
+ #
# # Mapping: (Real X, Real Z, Real Y)
# pm_stub.SetPoint(i, real_x, real_z, real_y)
# pm_stub.Draw()
@@ -289,21 +319,21 @@ def get_collection(obj, name):
st_fe = get_collection(tree, "Stub_FrontEndId")
st_row = get_collection(tree, "Stub_Row")
st_bend = get_collection(tree, "Stub_Bend")
-
+
# Lookup table for bending
# Indices 5-13 map to offsets
bend_lut = {
- 5: -4.0, # DM8
- 6: -3.5, # M76
- 7: -2.5, # M54
- 8: -1.5, # M32
- 9: -0.5, # M10
- 10: 0.5, # P12
- 11: 1.5, # P34
- 12: 2.5, # P56
- 13: 3.5 # P78
+ 5: -4.0, # DM8
+ 6: -3.5, # M76
+ 7: -2.5, # M54
+ 8: -1.5, # M32
+ 9: -0.5, # M10
+ 10: 0.5, # P12
+ 11: 1.5, # P34
+ 12: 2.5, # P56
+ 13: 3.5, # P78
}
-
+
n_stubs = len(st_hids)
print(f"Drawing {n_stubs} stubs")
for i in range(n_stubs):
@@ -312,32 +342,32 @@ def get_collection(obj, name):
fe = int(st_fe[i])
row = float(st_row[i])
bend_code = int(st_bend[i]) if st_bend else 0
- #take only first 3 bits
+ # take only first 3 bits
bend_code = bend_code & 0x7
offset = bend_lut.get(bend_code, 0.0)
print(f"Bend code: {bend_code}, offset: {offset}")
- #flip sign based on hybrid parity
+ # flip sign based on hybrid parity
if hid % 2 == 1:
offset = -offset
real_y, real_x = get_xy_stub(hid, col, fe)
real_z = get_z_stub(hid, row)
-
+
# Stub connects Pixel Layer (Real Y) to Strip Layer (Real Y + 40? or similar distance)
# We assume the stub is anchored at the Pixel position (real_x, real_z, real_y)
# And connects to (real_x + offset, real_z, real_y + 40)
-
+
# Point 1 (Pixel Layer)
p1_real_x = real_x
p1_real_z = real_z
- p1_real_y = real_y
-
+ p1_real_y = real_y
+
# Point 2 (Strip Layer)
- spacer = layer_spacers.get(int(hid/2), 40)
+ spacer = layer_spacers.get(int(hid / 2), 40)
p2_real_x = real_x + offset
p2_real_z = real_z
p2_real_y = real_y + spacer
-
+
# Draw Line
# Mapping: (Real X, Real Z, Real Y) -> (3D X, 3D Y, 3D Z)
line = ROOT.TPolyLine3D(2)
@@ -349,7 +379,7 @@ def get_collection(obj, name):
canvas.primitives.append(line)
# --- Custom Track Drawing ---
-
+
# Group stubs by layer
stubs_by_layer = {}
for i in range(n_stubs):
@@ -358,52 +388,54 @@ def get_collection(obj, name):
if layer not in stubs_by_layer:
stubs_by_layer[layer] = []
stubs_by_layer[layer].append(i)
-
+
sorted_layers = sorted(stubs_by_layer.keys())
if len(sorted_layers) >= 2:
min_layer = sorted_layers[0]
max_layer = sorted_layers[-1]
-
+
# Use first stub in min/max layer
idx1 = stubs_by_layer[min_layer][0]
idx2 = stubs_by_layer[max_layer][0]
-
+
hid1 = int(st_hids[idx1])
col1 = float(st_col[idx1])
fe1 = int(st_fe[idx1])
row1 = float(st_row[idx1])
-
+
hid2 = int(st_hids[idx2])
col2 = float(st_col[idx2])
fe2 = int(st_fe[idx2])
row2 = float(st_row[idx2])
-
- y1, x1 = get_xy_stub(hid1, col1, fe1) # Note: get_xy_stub returns (real_y, real_x)
+
+ y1, x1 = get_xy_stub(
+ hid1, col1, fe1
+ ) # Note: get_xy_stub returns (real_y, real_x)
z1 = get_z_stub(hid1, row1)
-
+
y2, x2 = get_xy_stub(hid2, col2, fe2)
z2 = get_z_stub(hid2, row2)
-
+
# Draw Track Line
tline = ROOT.TPolyLine3D(2)
- tline.SetPoint(0, x1, z1, y1) # 3D: x=real_x, y=real_z, z=real_y
+ tline.SetPoint(0, x1, z1, y1) # 3D: x=real_x, y=real_z, z=real_y
tline.SetPoint(1, x2, z2, y2)
tline.SetLineColor(ROOT.kGray)
tline.SetLineWidth(2)
tline.Draw()
canvas.primitives.append(tline)
-
+
# Draw dots in intermediate layers
if abs(y2 - y1) > 1e-5:
for layer in range(min_layer + 1, max_layer):
spacer = layer_spacers.get(layer, 40)
-
+
# Sensor 1: y = -layer * 500
y_s1 = -layer * 500.0
t_s1 = (y_s1 - y1) / (y2 - y1)
x_s1 = x1 + t_s1 * (x2 - x1)
z_s1 = z1 + t_s1 * (z2 - z1)
-
+
pm1 = ROOT.TPolyMarker3D(1)
pm1.SetPoint(0, x_s1, z_s1, y_s1)
pm1.SetMarkerStyle(20)
@@ -411,13 +443,13 @@ def get_collection(obj, name):
pm1.SetMarkerSize(1.0)
pm1.Draw()
canvas.primitives.append(pm1)
-
+
# Sensor 2: y = -layer * 500 + spacer
y_s2 = -layer * 500.0 + spacer
t_s2 = (y_s2 - y1) / (y2 - y1)
x_s2 = x1 + t_s2 * (x2 - x1)
z_s2 = z1 + t_s2 * (z2 - z1)
-
+
pm2 = ROOT.TPolyMarker3D(1)
pm2.SetPoint(0, x_s2, z_s2, y_s2)
pm2.SetMarkerStyle(20)
@@ -429,27 +461,35 @@ def get_collection(obj, name):
# --- Pad 2: 2D Views (Right) ---
pad_right = canvas.cd(2)
pad_right.Divide(1, 2)
-
+
# --- Pad 2_1: XY View (Top Right) ---
pad_xy = pad_right.cd(1)
pad_xy.SetGridx()
pad_xy.SetGridy()
-
+
# Background histogram for XY
h_xy = ROOT.TH2F("h_xy", "XY View", 1000, -100, 1000, 1000, -2200, 100)
h_xy.SetStats(0)
h_xy.Draw()
-
+
# SCluster (Strips) - Red
tree.SetMarkerStyle(20)
tree.SetMarkerColor(ROOT.kRed)
- tree.Draw("-int(SCluster_HybridId/2)*500+( (int(SCluster_HybridId/2)==0 || int(SCluster_HybridId/2)==4) ? 26 : 40 ):(SCluster_HybridId%2==0)?(SCluster_Address+SCluster_FrontEndId*120):(960-(SCluster_Address+SCluster_FrontEndId*120))", f"Entry$=={entry}", "same")
-
+ tree.Draw(
+ "-int(SCluster_HybridId/2)*500+( (int(SCluster_HybridId/2)==0 || int(SCluster_HybridId/2)==4) ? 26 : 40 ):(SCluster_HybridId%2==0)?(SCluster_Address+SCluster_FrontEndId*120):(960-(SCluster_Address+SCluster_FrontEndId*120))",
+ f"Entry$=={entry}",
+ "same",
+ )
+
# PCluster (Pixels) - Blue
tree.SetMarkerStyle(21)
tree.SetMarkerColor(ROOT.kBlue)
- tree.Draw("-int(PCluster_HybridId/2)*500:(PCluster_HybridId%2==0)?(PCluster_Address+PCluster_FrontEndId*120):(960-(PCluster_Address+PCluster_FrontEndId*120))", f"Entry$=={entry}", "same")
-
+ tree.Draw(
+ "-int(PCluster_HybridId/2)*500:(PCluster_HybridId%2==0)?(PCluster_Address+PCluster_FrontEndId*120):(960-(PCluster_Address+PCluster_FrontEndId*120))",
+ f"Entry$=={entry}",
+ "same",
+ )
+
# Stub - Green (lines showing bending)
# Draw lines from pixel position to strip position with offset
for i in range(n_stubs):
@@ -460,113 +500,130 @@ def get_collection(obj, name):
bend_code = bend_code & 0x7
offset = bend_lut.get(bend_code, 0.0)
print(f"Bend code: {bend_code}, offset: {offset}")
- #flip sign based on hybrid parity
+ # flip sign based on hybrid parity
if hid % 2 == 1:
offset = -offset
# Pixel position (X coordinate)
- spacer = layer_spacers.get(int(hid/2), 40)
- real_y_pixel, real_x_pixel = get_xy_pixel(hid, int(col/2), fe)
+ spacer = layer_spacers.get(int(hid / 2), 40)
+ real_y_pixel, real_x_pixel = get_xy_pixel(hid, int(col / 2), fe)
real_y_pixel -= spacer
real_x_pixel -= offset
# Strip position (X coordinate with offset)
- real_y_strip = real_y_pixel + spacer*3
- real_x_strip = real_x_pixel + offset*3
-
+ real_y_strip = real_y_pixel + spacer * 3
+ real_x_strip = real_x_pixel + offset * 3
+
# Draw line in XY view
line_xy = ROOT.TLine(real_x_pixel, real_y_pixel, real_x_strip, real_y_strip)
line_xy.SetLineColor(ROOT.kGreen + 2)
line_xy.SetLineWidth(2)
line_xy.Draw()
canvas.primitives.append(line_xy)
-
+
# Legend
leg = ROOT.TLegend(0.1, 0.5, 0.25, 0.65)
- gS = ROOT.TGraph(); gS.SetMarkerStyle(20); gS.SetMarkerColor(ROOT.kRed); gS.SetTitle("SCluster")
- gP = ROOT.TGraph(); gP.SetMarkerStyle(21); gP.SetMarkerColor(ROOT.kBlue); gP.SetTitle("PCluster")
- gStub = ROOT.TGraph(); gStub.SetMarkerStyle(22); gStub.SetMarkerColor(ROOT.kGreen + 2); gStub.SetTitle("Stub")
+ gS = ROOT.TGraph()
+ gS.SetMarkerStyle(20)
+ gS.SetMarkerColor(ROOT.kRed)
+ gS.SetTitle("SCluster")
+ gP = ROOT.TGraph()
+ gP.SetMarkerStyle(21)
+ gP.SetMarkerColor(ROOT.kBlue)
+ gP.SetTitle("PCluster")
+ gStub = ROOT.TGraph()
+ gStub.SetMarkerStyle(22)
+ gStub.SetMarkerColor(ROOT.kGreen + 2)
+ gStub.SetTitle("Stub")
leg.AddEntry(gS, "SCluster", "p")
leg.AddEntry(gP, "PCluster", "p")
leg.AddEntry(gStub, "Stub", "p")
leg.Draw()
-
+
# --- Pad 2_2: Z View (Bottom Right) ---
pad_z = pad_right.cd(2)
pad_z.SetGridx()
pad_z.SetGridy()
-
+
h_z = ROOT.TH2F("h_z", "Z View", 1000, -5, 50, 1000, -2200, 100)
h_z.SetStats(0)
h_z.Draw()
-
+
# PCluster (Pixels) - ZPos
tree.SetMarkerStyle(21)
tree.SetMarkerColor(ROOT.kBlue)
- tree.Draw("-int(PCluster_HybridId/2)*500:(PCluster_HybridId%2==0)?PCluster_Zpos:32-PCluster_Zpos", f"Entry$=={entry}", "same")
-
+ tree.Draw(
+ "-int(PCluster_HybridId/2)*500:(PCluster_HybridId%2==0)?PCluster_Zpos:32-PCluster_Zpos",
+ f"Entry$=={entry}",
+ "same",
+ )
+
# Stub - Row (triangles)
tree.SetMarkerStyle(22)
tree.SetMarkerColor(ROOT.kGreen + 2)
- tree.Draw("-int(Stub_HybridId/2)*500+( (int(Stub_HybridId/2)==0 || int(Stub_HybridId/2)==4) ? 13 : 20 ):(Stub_HybridId%2==0)?Stub_Row:32-Stub_Row", f"Entry$=={entry}", "same")
-
+ tree.Draw(
+ "-int(Stub_HybridId/2)*500+( (int(Stub_HybridId/2)==0 || int(Stub_HybridId/2)==4) ? 13 : 20 ):(Stub_HybridId%2==0)?Stub_Row:32-Stub_Row",
+ f"Entry$=={entry}",
+ "same",
+ )
canvas.Update()
+
def main():
# Initialize TApplication to handle GUI events properly
app = ROOT.TApplication("app", 0, 0)
-
+
parser = argparse.ArgumentParser(description="Draw events from ROOT file")
parser.add_argument("--input", "-i", required=True, help="Input ROOT file")
parser.add_argument("--start", type=int, default=0, help="Start entry")
args = parser.parse_args()
-
+
f = ROOT.TFile.Open(args.input)
if not f or f.IsZombie():
print(f"Error opening file {args.input}")
sys.exit(1)
-
+
tree = f.Get("Events")
if not tree:
print("Error: Events tree not found")
sys.exit(1)
-
+
canvas = ROOT.TCanvas("c1", "Event Display", 1200, 800)
-
+
n_entries = tree.GetEntries()
print(f"Found {n_entries} entries.")
-
+
import select
import time
-
+
# State variable to track button clicks
button_clicked = [None] # Use list so it can be modified in nested function
-
+
# Define callback functions that will be called when buttons are clicked
def on_prev_click():
- button_clicked[0] = 'prev'
-
+ button_clicked[0] = "prev"
+
def on_next_click():
- button_clicked[0] = 'next'
-
+ button_clicked[0] = "next"
+
current_entry = args.start
while current_entry < n_entries:
print(f"Drawing entry {current_entry}...")
draw_event(tree, current_entry, canvas)
-
+
# Add buttons after drawing event (they need to be recreated after canvas.Clear())
# Draw on the main canvas, not in a sub-pad
canvas.cd(0) # Select the main canvas
-
+
prev_button = ROOT.TButton("Previous", "", 0.02, 0.92, 0.10, 0.97)
prev_button.SetFillColor(ROOT.kGray + 1)
prev_button.SetTextSize(0.5)
prev_button.Draw()
-
+
next_button = ROOT.TButton("Next", "", 0.12, 0.92, 0.20, 0.97)
next_button.SetFillColor(ROOT.kGray + 1)
next_button.SetTextSize(0.5)
next_button.Draw()
-
+
# Event counter display
event_label = ROOT.TPaveText(0.22, 0.92, 0.38, 0.97, "NDC")
event_label.SetFillColor(ROOT.kWhite)
@@ -576,19 +633,21 @@ def on_next_click():
event_label.SetBorderSize(1)
event_label.AddText(f"Event: {current_entry} / {n_entries - 1}")
event_label.Draw()
-
+
canvas.Update()
-
- print(f"Entry {current_entry}. Click buttons or press Enter for next, 'p' for previous, 'q' to quit.")
-
+
+ print(
+ f"Entry {current_entry}. Click buttons or press Enter for next, 'p' for previous, 'q' to quit."
+ )
+
# Reset button click state
button_clicked[0] = None
-
+
# Non-blocking wait loop
while True:
# Process ROOT events (GUI, Zooming, etc.)
ROOT.gSystem.ProcessEvents()
-
+
# Check if a button was clicked by checking if the selected object is one of our buttons
# Only respond to actual button release events (kButton1Up = 1)
event = ROOT.gPad.GetEvent()
@@ -605,17 +664,17 @@ def on_next_click():
current_entry = min(n_entries - 1, current_entry + 1)
ROOT.gPad.SetSelected(ROOT.nullptr) # Clear selection
break # Redraw
-
+
# Check for input on stdin
# Wait up to 20ms for input
r, w, e = select.select([sys.stdin], [], [], 0.02)
if r:
user_input = sys.stdin.readline().strip()
- if user_input.lower() == 'q':
+ if user_input.lower() == "q":
sys.exit(0)
- elif user_input.lower() == 'p':
+ elif user_input.lower() == "p":
current_entry = max(0, current_entry - 1)
- break # Redraw
+ break # Redraw
elif user_input.isdigit():
# Go to specific event
goto_entry = int(user_input)
@@ -623,13 +682,16 @@ def on_next_click():
current_entry = goto_entry
print(f"Going to event {current_entry}")
else:
- print(f"Invalid entry number. Must be between 0 and {n_entries - 1}")
- break # Redraw
+ print(
+ f"Invalid entry number. Must be between 0 and {n_entries - 1}"
+ )
+ break # Redraw
else:
current_entry += 1
- break # Redraw
-
+ break # Redraw
+
# No explicit sleep needed as select waits up to timeout
+
if __name__ == "__main__":
main()
diff --git a/edit_ompo_report.py b/edit_ompo_report.py
index c86736c..6f55847 100755
--- a/edit_ompo_report.py
+++ b/edit_ompo_report.py
@@ -22,45 +22,45 @@
def find_and_replace_text_in_shape(shape, old_text, new_text):
"""
Find and replace text in a shape, preserving formatting.
-
+
Args:
shape: The shape object to search
old_text: Text to find
new_text: Text to replace with
-
+
Returns:
bool: True if text was replaced, False otherwise
"""
if not shape.has_text_frame:
return False
-
+
replaced = False
text_frame = shape.text_frame
-
+
# Check all paragraphs and runs
for paragraph in text_frame.paragraphs:
for run in paragraph.runs:
if old_text in run.text:
run.text = run.text.replace(old_text, new_text)
replaced = True
-
+
# If no runs, try the whole text
if not replaced and old_text in text_frame.text:
text_frame.text = text_frame.text.replace(old_text, new_text)
replaced = True
-
+
return replaced
def find_and_replace_in_slide(slide, old_text, new_text):
"""
Find and replace text in all shapes in a slide.
-
+
Args:
slide: The slide object
old_text: Text to find
new_text: Text to replace with
-
+
Returns:
int: Number of replacements made
"""
@@ -74,135 +74,122 @@ def find_and_replace_in_slide(slide, old_text, new_text):
def edit_ompo_report(input_file, output_file, name=None, date=None, status_text=None):
"""
Edit the OMPO Report PowerPoint presentation.
-
+
Args:
input_file: Path to input PowerPoint file
output_file: Path to output PowerPoint file
name: Name of person filling/submitting the report
date: Date in YYYY-MM-DD format
status_text: Text to replace in the Status of Burnin Center section
-
+
Returns:
dict: Summary of replacements made
"""
# Load presentation
prs = Presentation(input_file)
-
- summary = {
- 'name': 0,
- 'date': 0,
- 'status': 0
- }
-
+
+ summary = {"name": 0, "date": 0, "status": 0}
+
# Process each slide
for slide_idx, slide in enumerate(prs.slides):
print(f"Processing slide {slide_idx + 1}...")
-
+
# Replace Name field
if name:
count = find_and_replace_in_slide(slide, "Name", name)
- summary['name'] += count
+ summary["name"] += count
if count > 0:
print(f" - Replaced 'Name' with '{name}' ({count} occurrences)")
-
+
# Replace Date field
if date:
count = find_and_replace_in_slide(slide, "YYYY-MM-DD", date)
- summary['date'] += count
+ summary["date"] += count
if count > 0:
print(f" - Replaced 'YYYY-MM-DD' with '{date}' ({count} occurrences)")
-
+
# Replace Status text (the "Text" placeholder in yellow box)
if status_text:
count = find_and_replace_in_slide(slide, "Text", status_text)
- summary['status'] += count
+ summary["status"] += count
if count > 0:
- print(f" - Replaced 'Text' with status information ({count} occurrences)")
-
+ print(
+ f" - Replaced 'Text' with status information ({count} occurrences)"
+ )
+
# Save modified presentation
prs.save(output_file)
print(f"\nSaved modified presentation to: {output_file}")
-
+
return summary
def main():
"""Main function for command-line usage."""
parser = argparse.ArgumentParser(
- description='Edit OMPO Report PowerPoint presentation fields',
+ description="Edit OMPO Report PowerPoint presentation fields",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s input.pptx output.pptx --name "John Doe" --date "2025-11-18"
%(prog)s template.pptx filled.pptx --name "Jane Smith" --date "2025-11-20" --status "All systems operational. No downtime expected."
%(prog)s input.pptx output.pptx --date $(date +%%Y-%%m-%%d) # Use current date
- """
- )
-
- parser.add_argument(
- 'input_file',
- type=str,
- help='Input PowerPoint file path'
+ """,
)
-
- parser.add_argument(
- 'output_file',
- type=str,
- help='Output PowerPoint file path'
- )
-
+
+ parser.add_argument("input_file", type=str, help="Input PowerPoint file path")
+
+ parser.add_argument("output_file", type=str, help="Output PowerPoint file path")
+
parser.add_argument(
- '--name',
- type=str,
- help='Name of person filling/submitting the report'
+ "--name", type=str, help="Name of person filling/submitting the report"
)
-
+
parser.add_argument(
- '--date',
- type=str,
- help='Date in YYYY-MM-DD format (default: today)'
+ "--date", type=str, help="Date in YYYY-MM-DD format (default: today)"
)
-
+
parser.add_argument(
- '--status',
- type=str,
- help='Status text for the Burnin Center section'
+ "--status", type=str, help="Status text for the Burnin Center section"
)
-
+
parser.add_argument(
- '--use-today',
- action='store_true',
- help='Use today\'s date automatically'
+ "--use-today", action="store_true", help="Use today's date automatically"
)
-
+
args = parser.parse_args()
-
+
# Validate input file exists
if not Path(args.input_file).exists():
print(f"Error: Input file '{args.input_file}' not found.", file=sys.stderr)
return 1
-
+
# Use today's date if requested
date = args.date
if args.use_today or (date is None and args.name is None and args.status is None):
# If no arguments provided, at least use today's date
- date = datetime.now().strftime('%Y-%m-%d')
+ date = datetime.now().strftime("%Y-%m-%d")
print(f"Using today's date: {date}")
-
+
# Validate date format if provided
if date:
try:
- datetime.strptime(date, '%Y-%m-%d')
+ datetime.strptime(date, "%Y-%m-%d")
except ValueError:
- print(f"Error: Date must be in YYYY-MM-DD format, got '{date}'", file=sys.stderr)
+ print(
+ f"Error: Date must be in YYYY-MM-DD format, got '{date}'",
+ file=sys.stderr,
+ )
return 1
-
+
# Check if at least one field is being updated
if not any([args.name, date, args.status]):
- print("Warning: No fields specified to update. Use --name, --date, or --status options.")
+ print(
+ "Warning: No fields specified to update. Use --name, --date, or --status options."
+ )
parser.print_help()
return 1
-
+
try:
# Edit the presentation
summary = edit_ompo_report(
@@ -210,22 +197,23 @@ def main():
args.output_file,
name=args.name,
date=date,
- status_text=args.status
+ status_text=args.status,
)
-
+
print("\nSummary:")
print(f" - Name field: {summary['name']} replacements")
print(f" - Date field: {summary['date']} replacements")
print(f" - Status field: {summary['status']} replacements")
-
+
return 0
-
+
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
import traceback
+
traceback.print_exc()
return 1
-if __name__ == '__main__':
+if __name__ == "__main__":
sys.exit(main())
diff --git a/glue_analyzer.py b/glue_analyzer.py
index 08d7f28..62a23eb 100644
--- a/glue_analyzer.py
+++ b/glue_analyzer.py
@@ -6,10 +6,11 @@
# --- Configuration ---
# You can adjust these values if needed
RESIZE_WIDTH = 1200 # Resize images wider than this for better display
-MANUAL_BRUSH_SIZE = 10 # Brush size for manual correction
+MANUAL_BRUSH_SIZE = 10 # Brush size for manual correction
INSTRUCTION_FONT_SCALE = 0.6
INSTRUCTION_FONT_THICKNESS = 1
+
class GlueContactAnalyzer:
"""
An interactive tool to analyze glue contact area in images.
@@ -43,37 +44,41 @@ def __init__(self, image_path):
self.roi_mask = None
self.manual_good_mask = np.zeros(self.image.shape[:2], dtype=np.uint8)
self.manual_bad_mask = np.zeros(self.image.shape[:2], dtype=np.uint8)
-
+
# For perimeter drawing mode
self.perimeter_points = []
self.drawing_perimeter = False
-
+
# We will work on the blue channel, as the glue is blueish/gray
# and has good contrast with the black carbon fiber.
- self.gray_image = self.image[:, :, 0] # Using Blue channel
-
- self.threshold_value = 120 # Initial threshold value
- self.use_advanced_segmentation = False # Toggle between simple threshold and gradient-corrected threshold
- self.invert_logic = False # Whether to invert the segmentation logic
-
+ self.gray_image = self.image[:, :, 0] # Using Blue channel
+
+ self.threshold_value = 120 # Initial threshold value
+ self.use_advanced_segmentation = (
+ False # Toggle between simple threshold and gradient-corrected threshold
+ )
+ self.invert_logic = False # Whether to invert the segmentation logic
+
# Gradient correction parameters
self.gradient_kernel_size = 50 # Size of kernel for estimating local background
- self.gradient_correction_strength = 1.0 # How much to apply the correction (0.0 to 1.0)
-
+ self.gradient_correction_strength = (
+ 1.0 # How much to apply the correction (0.0 to 1.0)
+ )
+
# Display options
self.show_overlay = True # Whether to show green/red overlay
self.show_corrected_image = False # Whether to show gradient-corrected image
self.show_help_text = True # Whether to show instruction text overlay
-
- self.mode = 'ROI_SELECT' # Modes: 'ROI_SELECT', 'THRESHOLD', 'MANUAL_GOOD', 'MANUAL_BAD', 'PERIMETER_GOOD', 'PERIMETER_BAD'
-
+
+ self.mode = "ROI_SELECT" # Modes: 'ROI_SELECT', 'THRESHOLD', 'MANUAL_GOOD', 'MANUAL_BAD', 'PERIMETER_GOOD', 'PERIMETER_BAD'
+
self.window_name = "Glue Contact Analyzer"
cv2.namedWindow(self.window_name)
cv2.setMouseCallback(self.window_name, self.mouse_callback)
def mouse_callback(self, event, x, y, flags, param):
"""Handles all mouse interactions."""
- if self.mode == 'ROI_SELECT':
+ if self.mode == "ROI_SELECT":
if event == cv2.EVENT_LBUTTONDOWN:
self.roi_points.append((x, y))
print(f"Added ROI point: ({x}, {y})")
@@ -82,7 +87,7 @@ def mouse_callback(self, event, x, y, flags, param):
self.roi_points.pop()
print("Removed last ROI point.")
- elif self.mode in ['PERIMETER_GOOD', 'PERIMETER_BAD']:
+ elif self.mode in ["PERIMETER_GOOD", "PERIMETER_BAD"]:
if event == cv2.EVENT_LBUTTONDOWN:
if not self.drawing_perimeter:
# Start new perimeter
@@ -96,28 +101,34 @@ def mouse_callback(self, event, x, y, flags, param):
elif event == cv2.EVENT_MBUTTONDOWN: # Middle click to complete
if self.drawing_perimeter and len(self.perimeter_points) > 2:
# Complete the perimeter and fill it
- target_mask = self.manual_good_mask if self.mode == 'PERIMETER_GOOD' else self.manual_bad_mask
+ target_mask = (
+ self.manual_good_mask
+ if self.mode == "PERIMETER_GOOD"
+ else self.manual_bad_mask
+ )
cv2.fillPoly(target_mask, [np.array(self.perimeter_points)], 255)
- print(f"Completed perimeter with {len(self.perimeter_points)} points")
+ print(
+ f"Completed perimeter with {len(self.perimeter_points)} points"
+ )
self.perimeter_points = []
self.drawing_perimeter = False
elif self.drawing_perimeter:
print("Need at least 3 points to complete perimeter")
- elif flags == cv2.EVENT_FLAG_LBUTTON: # Mouse is being dragged
- if self.mode == 'MANUAL_GOOD':
+ elif flags == cv2.EVENT_FLAG_LBUTTON: # Mouse is being dragged
+ if self.mode == "MANUAL_GOOD":
cv2.circle(self.manual_good_mask, (x, y), MANUAL_BRUSH_SIZE, 255, -1)
- elif self.mode == 'MANUAL_BAD':
+ elif self.mode == "MANUAL_BAD":
cv2.circle(self.manual_bad_mask, (x, y), MANUAL_BRUSH_SIZE, 255, -1)
-
+
def set_threshold(self, val):
"""Callback for the threshold trackbar."""
self.threshold_value = val
-
+
def set_gradient_kernel_size(self, val):
"""Callback for gradient kernel size trackbar."""
self.gradient_kernel_size = max(10, val) # Minimum size of 10
-
+
def set_gradient_correction_strength(self, val):
"""Callback for gradient correction strength trackbar."""
self.gradient_correction_strength = val / 100.0 # Convert to 0.0-1.0 range
@@ -125,90 +136,132 @@ def set_gradient_correction_strength(self, val):
def create_trackbars(self):
"""Create all trackbars for parameter adjustment."""
# Basic threshold
- cv2.createTrackbar('Threshold', self.window_name, self.threshold_value, 255, self.set_threshold)
-
+ cv2.createTrackbar(
+ "Threshold", self.window_name, self.threshold_value, 255, self.set_threshold
+ )
+
# Gradient correction parameters
- cv2.createTrackbar('Gradient Kernel', self.window_name, self.gradient_kernel_size, 200, self.set_gradient_kernel_size)
- cv2.createTrackbar('Correction Strength', self.window_name, int(self.gradient_correction_strength * 100), 100, self.set_gradient_correction_strength)
+ cv2.createTrackbar(
+ "Gradient Kernel",
+ self.window_name,
+ self.gradient_kernel_size,
+ 200,
+ self.set_gradient_kernel_size,
+ )
+ cv2.createTrackbar(
+ "Correction Strength",
+ self.window_name,
+ int(self.gradient_correction_strength * 100),
+ 100,
+ self.set_gradient_correction_strength,
+ )
def process_and_display(self):
"""The main display loop, updates the image based on the current state."""
display_image = self.image.copy()
-
+
# --- Draw ROI points as they are being selected ---
if self.roi_points:
for i in range(len(self.roi_points)):
- cv2.circle(display_image, self.roi_points[i], 4, (0, 255, 255), -1) # Yellow dots
+ cv2.circle(
+ display_image, self.roi_points[i], 4, (0, 255, 255), -1
+ ) # Yellow dots
if i > 0:
- cv2.line(display_image, self.roi_points[i-1], self.roi_points[i], (0, 255, 255), 2)
- if self.mode != 'ROI_SELECT' and len(self.roi_points) > 2:
+ cv2.line(
+ display_image,
+ self.roi_points[i - 1],
+ self.roi_points[i],
+ (0, 255, 255),
+ 2,
+ )
+ if self.mode != "ROI_SELECT" and len(self.roi_points) > 2:
# Draw the closed polygon after confirmation
- cv2.polylines(display_image, [np.array(self.roi_points)], isClosed=True, color=(0, 255, 255), thickness=2)
-
+ cv2.polylines(
+ display_image,
+ [np.array(self.roi_points)],
+ isClosed=True,
+ color=(0, 255, 255),
+ thickness=2,
+ )
+
# --- Draw perimeter points during perimeter drawing ---
if self.drawing_perimeter and self.perimeter_points:
- color = (0, 255, 0) if self.mode == 'PERIMETER_GOOD' else (0, 0, 255)
+ color = (0, 255, 0) if self.mode == "PERIMETER_GOOD" else (0, 0, 255)
for i in range(len(self.perimeter_points)):
cv2.circle(display_image, self.perimeter_points[i], 3, color, -1)
if i > 0:
- cv2.line(display_image, self.perimeter_points[i-1], self.perimeter_points[i], color, 2)
+ cv2.line(
+ display_image,
+ self.perimeter_points[i - 1],
+ self.perimeter_points[i],
+ color,
+ 2,
+ )
# --- Process and overlay segmentation if ROI is defined ---
if self.roi_mask is not None:
# 1. Choose segmentation method
if self.use_advanced_segmentation:
- auto_good_mask, corrected_image = self.gradient_corrected_threshold(self.roi_mask)
+ auto_good_mask, corrected_image = self.gradient_corrected_threshold(
+ self.roi_mask
+ )
else:
# Simple thresholding
- thresh_type = cv2.THRESH_BINARY if self.invert_logic else cv2.THRESH_BINARY_INV
- _, auto_good_mask = cv2.threshold(self.gray_image, self.threshold_value, 255, thresh_type)
+ thresh_type = (
+ cv2.THRESH_BINARY if self.invert_logic else cv2.THRESH_BINARY_INV
+ )
+ _, auto_good_mask = cv2.threshold(
+ self.gray_image, self.threshold_value, 255, thresh_type
+ )
corrected_image = None
-
+
# Show gradient-corrected image if requested
if self.show_corrected_image and corrected_image is not None:
# Convert single channel to 3-channel for display
corrected_display = cv2.cvtColor(corrected_image, cv2.COLOR_GRAY2BGR)
display_image = corrected_display
-
+
# 2. Apply manual corrections
# Start with the automatic mask
final_good_mask = auto_good_mask
# Force manually painted good areas to be white
final_good_mask = cv2.bitwise_or(final_good_mask, self.manual_good_mask)
# Force manually painted bad areas to be black
- final_good_mask = cv2.bitwise_and(final_good_mask, cv2.bitwise_not(self.manual_bad_mask))
+ final_good_mask = cv2.bitwise_and(
+ final_good_mask, cv2.bitwise_not(self.manual_bad_mask)
+ )
# 3. Constrain everything to the ROI
final_good_mask = cv2.bitwise_and(final_good_mask, self.roi_mask)
-
+
# Apply overlays only if show_overlay is True
if self.show_overlay:
# Create overlays for visualization
good_overlay = np.zeros_like(display_image)
- good_overlay[:] = (0, 255, 0) # Green for good contact
-
+ good_overlay[:] = (0, 255, 0) # Green for good contact
+
bad_overlay = np.zeros_like(display_image)
- bad_overlay[:] = (0, 0, 255) # Red for bad contact
+ bad_overlay[:] = (0, 0, 255) # Red for bad contact
# Find the bad contact area within the ROI
- roi_area_only = cv2.bitwise_not(final_good_mask) # Invert to get bad areas
+ roi_area_only = cv2.bitwise_not(
+ final_good_mask
+ ) # Invert to get bad areas
bad_mask_in_roi = cv2.bitwise_and(roi_area_only, self.roi_mask)
-
+
# Apply overlays
# Create masked overlays and blend them
good_indices = final_good_mask > 0
bad_indices = bad_mask_in_roi > 0
-
+
# Apply green overlay for good contact areas
display_image[good_indices] = cv2.addWeighted(
- display_image[good_indices], 0.6,
- good_overlay[good_indices], 0.4, 0
+ display_image[good_indices], 0.6, good_overlay[good_indices], 0.4, 0
)
-
+
# Apply red overlay for bad contact areas
display_image[bad_indices] = cv2.addWeighted(
- display_image[bad_indices], 0.6,
- bad_overlay[bad_indices], 0.4, 0
+ display_image[bad_indices], 0.6, bad_overlay[bad_indices], 0.4, 0
)
# --- Add instructions to the screen ---
@@ -219,30 +272,60 @@ def process_and_display(self):
def draw_instructions(self, image):
"""Draws text instructions on the image."""
y_pos = 20
+
def put_text(text, color=(255, 255, 255)):
nonlocal y_pos
- cv2.putText(image, text, (10, y_pos), cv2.FONT_HERSHEY_SIMPLEX,
- INSTRUCTION_FONT_SCALE, (0,0,0), INSTRUCTION_FONT_THICKNESS + 1, cv2.LINE_AA) # Shadow
- cv2.putText(image, text, (10, y_pos), cv2.FONT_HERSHEY_SIMPLEX,
- INSTRUCTION_FONT_SCALE, color, INSTRUCTION_FONT_THICKNESS, cv2.LINE_AA)
+ cv2.putText(
+ image,
+ text,
+ (10, y_pos),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ INSTRUCTION_FONT_SCALE,
+ (0, 0, 0),
+ INSTRUCTION_FONT_THICKNESS + 1,
+ cv2.LINE_AA,
+ ) # Shadow
+ cv2.putText(
+ image,
+ text,
+ (10, y_pos),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ INSTRUCTION_FONT_SCALE,
+ color,
+ INSTRUCTION_FONT_THICKNESS,
+ cv2.LINE_AA,
+ )
y_pos += 20
-
- if self.mode == 'ROI_SELECT':
+
+ if self.mode == "ROI_SELECT":
put_text(f"MODE: Select ROI", (0, 255, 255))
put_text("Left-click: Add point. Right-click: Remove last point.")
put_text("Press 'c' to confirm ROI when done.")
else:
- mode_color = (255, 255, 0) if self.mode == 'THRESHOLD' else (0, 255, 0) if self.mode in ['MANUAL_GOOD', 'PERIMETER_GOOD'] else (0, 0, 255)
+ mode_color = (
+ (255, 255, 0)
+ if self.mode == "THRESHOLD"
+ else (
+ (0, 255, 0)
+ if self.mode in ["MANUAL_GOOD", "PERIMETER_GOOD"]
+ else (0, 0, 255)
+ )
+ )
put_text(f"MODE: {self.mode}", mode_color)
-
+
# Show current segmentation method
- seg_method = "Gradient-Corrected" if self.use_advanced_segmentation else "Simple"
+ seg_method = (
+ "Gradient-Corrected" if self.use_advanced_segmentation else "Simple"
+ )
put_text(f"Segmentation: {seg_method}", (255, 255, 0))
put_text(f"Threshold: {self.threshold_value}", (255, 255, 0))
if self.use_advanced_segmentation:
put_text(f"Gradient Kernel: {self.gradient_kernel_size}", (255, 255, 0))
- put_text(f"Correction: {self.gradient_correction_strength:.2f}", (255, 255, 0))
-
+ put_text(
+ f"Correction: {self.gradient_correction_strength:.2f}",
+ (255, 255, 0),
+ )
+
put_text("--- Controls ---")
put_text("'t': Adjust Threshold with slider")
put_text("'a': Toggle Gradient-corrected/Simple threshold")
@@ -252,114 +335,133 @@ def put_text(text, color=(255, 255, 255)):
put_text("'h': Toggle help text visibility")
put_text("'g': Paint GOOD contact (hold left mouse)")
put_text("'b': Paint BAD contact (hold left mouse)")
- put_text("'p': Draw GOOD perimeter (left-click points, middle-click to fill)")
- put_text("'n': Draw BAD perimeter (left-click points, middle-click to fill)")
+ put_text(
+ "'p': Draw GOOD perimeter (left-click points, middle-click to fill)"
+ )
+ put_text(
+ "'n': Draw BAD perimeter (left-click points, middle-click to fill)"
+ )
if self.drawing_perimeter:
put_text("ESC to cancel perimeter", (255, 0, 255))
put_text("'s': Calculate and show results")
put_text("'r': Reset everything")
put_text("'q': Quit")
-
def run(self):
"""Main application loop."""
print("--- Glue Contact Analyzer ---")
print("Step 1: Define the ROI by clicking points on the image.")
print(" Press 'c' to confirm the ROI and proceed.")
-
+
while True:
self.process_and_display()
key = cv2.waitKey(20) & 0xFF
- if key == ord('q'):
+ if key == ord("q"):
break
-
- elif key == ord('c') and self.mode == 'ROI_SELECT':
+
+ elif key == ord("c") and self.mode == "ROI_SELECT":
if len(self.roi_points) > 2:
- print("\nStep 2: ROI Confirmed. Adjust threshold or perform manual corrections.")
- self.mode = 'THRESHOLD'
+ print(
+ "\nStep 2: ROI Confirmed. Adjust threshold or perform manual corrections."
+ )
+ self.mode = "THRESHOLD"
self.roi_mask = np.zeros(self.image.shape[:2], dtype=np.uint8)
cv2.fillPoly(self.roi_mask, [np.array(self.roi_points)], 255)
self.create_trackbars()
else:
print("Please select at least 3 points to form a polygon.")
- elif key == ord('t') and self.mode != 'ROI_SELECT':
- self.mode = 'THRESHOLD'
+ elif key == ord("t") and self.mode != "ROI_SELECT":
+ self.mode = "THRESHOLD"
print("Mode changed to THRESHOLD adjustment.")
- elif key == ord('g') and self.mode != 'ROI_SELECT':
- self.mode = 'MANUAL_GOOD'
+ elif key == ord("g") and self.mode != "ROI_SELECT":
+ self.mode = "MANUAL_GOOD"
print("Mode changed to MANUAL GOOD. Hold and drag left mouse to paint.")
- elif key == ord('b') and self.mode != 'ROI_SELECT':
- self.mode = 'MANUAL_BAD'
+ elif key == ord("b") and self.mode != "ROI_SELECT":
+ self.mode = "MANUAL_BAD"
print("Mode changed to MANUAL BAD. Hold and drag left mouse to paint.")
-
- elif key == ord('p') and self.mode != 'ROI_SELECT':
- self.mode = 'PERIMETER_GOOD'
+
+ elif key == ord("p") and self.mode != "ROI_SELECT":
+ self.mode = "PERIMETER_GOOD"
self.perimeter_points = []
self.drawing_perimeter = False
- print("Mode changed to PERIMETER GOOD. Left-click to add points, middle-click to fill perimeter.")
-
- elif key == ord('n') and self.mode != 'ROI_SELECT':
- self.mode = 'PERIMETER_BAD'
+ print(
+ "Mode changed to PERIMETER GOOD. Left-click to add points, middle-click to fill perimeter."
+ )
+
+ elif key == ord("n") and self.mode != "ROI_SELECT":
+ self.mode = "PERIMETER_BAD"
self.perimeter_points = []
self.drawing_perimeter = False
- print("Mode changed to PERIMETER BAD. Left-click to add points, middle-click to fill perimeter.")
-
+ print(
+ "Mode changed to PERIMETER BAD. Left-click to add points, middle-click to fill perimeter."
+ )
+
elif key == 27: # ESC key
if self.drawing_perimeter:
print("Cancelled perimeter drawing")
self.perimeter_points = []
self.drawing_perimeter = False
-
- elif key == ord('a') and self.mode != 'ROI_SELECT':
+
+ elif key == ord("a") and self.mode != "ROI_SELECT":
self.use_advanced_segmentation = not self.use_advanced_segmentation
- method = "Gradient-corrected threshold" if self.use_advanced_segmentation else "Simple threshold"
+ method = (
+ "Gradient-corrected threshold"
+ if self.use_advanced_segmentation
+ else "Simple threshold"
+ )
print(f"Switched to {method} segmentation.")
-
- elif key == ord('i') and self.mode != 'ROI_SELECT':
+
+ elif key == ord("i") and self.mode != "ROI_SELECT":
self.invert_good_bad_masks()
-
- elif key == ord('o') and self.mode != 'ROI_SELECT':
+
+ elif key == ord("o") and self.mode != "ROI_SELECT":
self.show_overlay = not self.show_overlay
status = "ON" if self.show_overlay else "OFF"
print(f"Overlay visibility: {status}")
-
- elif key == ord('v') and self.mode != 'ROI_SELECT':
+
+ elif key == ord("v") and self.mode != "ROI_SELECT":
self.show_corrected_image = not self.show_corrected_image
status = "ON" if self.show_corrected_image else "OFF"
print(f"Gradient-corrected image view: {status}")
-
- elif key == ord('h'):
+
+ elif key == ord("h"):
self.show_help_text = not self.show_help_text
status = "ON" if self.show_help_text else "OFF"
print(f"Help text visibility: {status}")
-
- elif key == ord('s') and self.roi_mask is not None:
+
+ elif key == ord("s") and self.roi_mask is not None:
self.calculate_and_show_results()
- elif key == ord('r'):
+ elif key == ord("r"):
print("Resetting application state.")
- self.__init__(self.image_path) # Re-initialize the object
+ self.__init__(self.image_path) # Re-initialize the object
cv2.destroyWindow(self.window_name)
- self.run() # Restart the run loop
- return # Exit the current loop
+ self.run() # Restart the run loop
+ return # Exit the current loop
cv2.destroyAllWindows()
-
+
def calculate_and_show_results(self):
"""Final calculation and printing of results."""
# Recalculate the final mask to be sure it's up to date
if self.use_advanced_segmentation:
auto_good_mask, _ = self.gradient_corrected_threshold(self.roi_mask)
else:
- thresh_type = cv2.THRESH_BINARY if self.invert_logic else cv2.THRESH_BINARY_INV
- _, auto_good_mask = cv2.threshold(self.gray_image, self.threshold_value, 255, thresh_type)
-
+ thresh_type = (
+ cv2.THRESH_BINARY if self.invert_logic else cv2.THRESH_BINARY_INV
+ )
+ _, auto_good_mask = cv2.threshold(
+ self.gray_image, self.threshold_value, 255, thresh_type
+ )
+
final_good_mask = cv2.bitwise_or(auto_good_mask, self.manual_good_mask)
- final_good_mask = cv2.bitwise_and(final_good_mask, cv2.bitwise_not(self.manual_bad_mask))
+ final_good_mask = cv2.bitwise_and(
+ final_good_mask, cv2.bitwise_not(self.manual_bad_mask)
+ )
final_good_mask = cv2.bitwise_and(final_good_mask, self.roi_mask)
total_roi_pixels = cv2.countNonZero(self.roi_mask)
@@ -372,7 +474,7 @@ def calculate_and_show_results(self):
good_percentage = (good_contact_pixels / total_roi_pixels) * 100
bad_percentage = (bad_contact_pixels / total_roi_pixels) * 100
-
+
print("\n--- Analysis Results ---")
print(f"Total Pixels in ROI: {total_roi_pixels}")
print(f"Good Contact Pixels: {good_contact_pixels}")
@@ -381,13 +483,45 @@ def calculate_and_show_results(self):
print(f"Good Contact: {good_percentage:.2f}%")
print(f"Bad Contact: {bad_percentage:.2f}%")
print("--------------------------\n")
-
+
# Display results on a new image
result_img = np.zeros((200, 500, 3), dtype=np.uint8)
- cv2.putText(result_img, "Analysis Results", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 0), 2)
- cv2.putText(result_img, f"Good Contact: {good_percentage:.2f}%", (10, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
- cv2.putText(result_img, f"Bad Contact: {bad_percentage:.2f}%", (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
- cv2.putText(result_img, "Press any key to close.", (10, 180), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
+ cv2.putText(
+ result_img,
+ "Analysis Results",
+ (10, 30),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 1,
+ (255, 255, 0),
+ 2,
+ )
+ cv2.putText(
+ result_img,
+ f"Good Contact: {good_percentage:.2f}%",
+ (10, 80),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 0.8,
+ (0, 255, 0),
+ 2,
+ )
+ cv2.putText(
+ result_img,
+ f"Bad Contact: {bad_percentage:.2f}%",
+ (10, 120),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 0.8,
+ (0, 0, 255),
+ 2,
+ )
+ cv2.putText(
+ result_img,
+ "Press any key to close.",
+ (10, 180),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 0.6,
+ (255, 255, 255),
+ 1,
+ )
cv2.imshow("Results", result_img)
cv2.waitKey(0)
cv2.destroyWindow("Results")
@@ -399,49 +533,56 @@ def gradient_corrected_threshold(self, roi_mask):
"""
# Apply gradient correction to the ENTIRE image first, not just ROI
# This avoids boundary artifacts and gets better lighting estimation
-
+
# Estimate local background using morphological opening with a large kernel
kernel_size = self.gradient_kernel_size
- kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
-
+ kernel = cv2.getStructuringElement(
+ cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)
+ )
+
# Estimate background (local lighting) on the full image
background = cv2.morphologyEx(self.gray_image, cv2.MORPH_OPEN, kernel)
-
+
# Alternative: use Gaussian blur for smoother background estimation
- blur_size = kernel_size//2*2+1 # Ensure odd number
+ blur_size = kernel_size // 2 * 2 + 1 # Ensure odd number
background_smooth = cv2.GaussianBlur(self.gray_image, (blur_size, blur_size), 0)
-
+
# Blend the two background estimates
background_final = cv2.addWeighted(background, 0.5, background_smooth, 0.5, 0)
-
+
# Apply gradient correction to the full image
if self.gradient_correction_strength > 0:
# Subtract background to remove lighting gradients
corrected = cv2.subtract(self.gray_image, background_final)
-
+
# Add back a uniform level to maintain good contrast
# Use mean of entire image for better global balance
mean_background = cv2.mean(background_final)[0]
corrected = cv2.add(corrected, int(mean_background))
-
+
# Blend original and corrected based on correction strength
corrected_final = cv2.addWeighted(
- self.gray_image, 1.0 - self.gradient_correction_strength,
- corrected, self.gradient_correction_strength, 0
+ self.gray_image,
+ 1.0 - self.gradient_correction_strength,
+ corrected,
+ self.gradient_correction_strength,
+ 0,
)
else:
corrected_final = self.gray_image
-
+
# Now apply ROI mask to the corrected image
roi_corrected = cv2.bitwise_and(corrected_final, roi_mask)
-
+
# Apply threshold to the corrected image
thresh_type = cv2.THRESH_BINARY if self.invert_logic else cv2.THRESH_BINARY_INV
- _, thresholded = cv2.threshold(corrected_final, self.threshold_value, 255, thresh_type)
-
+ _, thresholded = cv2.threshold(
+ corrected_final, self.threshold_value, 255, thresh_type
+ )
+
# Apply ROI constraint to the final mask
final_mask = cv2.bitwise_and(thresholded, roi_mask)
-
+
return final_mask, corrected_final
def invert_good_bad_masks(self):
@@ -450,18 +591,20 @@ def invert_good_bad_masks(self):
temp_mask = self.manual_good_mask.copy()
self.manual_good_mask = self.manual_bad_mask.copy()
self.manual_bad_mask = temp_mask
-
+
# Invert the automatic segmentation logic
self.invert_logic = not self.invert_logic
-
+
logic_state = "INVERTED" if self.invert_logic else "NORMAL"
print(f"Inverted good and bad regions. Segmentation logic: {logic_state}")
if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="Interactively analyze glue contact area in an image.")
+ parser = argparse.ArgumentParser(
+ description="Interactively analyze glue contact area in an image."
+ )
parser.add_argument("image_path", help="Path to the input image file.")
args = parser.parse_args()
analyzer = GlueContactAnalyzer(args.image_path)
- analyzer.run()
\ No newline at end of file
+ analyzer.run()
diff --git a/integration.py b/integration.py
index 2551070..bc3b630 100755
--- a/integration.py
+++ b/integration.py
@@ -2,9 +2,22 @@
import threading
import ui.integration_gui as integration_gui
from PyQt5.QtWidgets import (
- QApplication, QMainWindow, QVBoxLayout, QWidget,
- QLineEdit, QLabel, QFormLayout, QTreeWidgetItem,
- QTreeWidget, QMessageBox, QPushButton, QInputDialog, QHBoxLayout, QSpacerItem, QSizePolicy, QComboBox
+ QApplication,
+ QMainWindow,
+ QVBoxLayout,
+ QWidget,
+ QLineEdit,
+ QLabel,
+ QFormLayout,
+ QTreeWidgetItem,
+ QTreeWidget,
+ QMessageBox,
+ QPushButton,
+ QInputDialog,
+ QHBoxLayout,
+ QSpacerItem,
+ QSizePolicy,
+ QComboBox,
)
from PyQt5.QtGui import QPen
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject, QThread, QSize, QUrl
@@ -20,9 +33,11 @@
import numpy as np
from caen.caenGUI import CAENControl
import json
+
# Set matplotlib backend before importing pyplot
import matplotlib
-matplotlib.use('Qt5Agg')
+
+matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
@@ -37,31 +52,34 @@
from datetime import datetime
from db.module_db import ModuleDB
from db.utils import get_module_fuse_id
+
+
# Add this class near the top of the file
class LogEmitter(QObject):
"""Helper class to emit log messages from any thread"""
+
log_message = pyqtSignal(str)
+
# Add new CommandWorker class
class CommandWorker(QThread):
finished = pyqtSignal(bool, str, str) # success, stdout, stderr
-
+
def __init__(self, command):
super().__init__()
self.command = command
-
+
def run(self):
try:
expanded_command = self.command
- #open spinning wheel dialog
- result = subprocess.run(expanded_command, shell=True,
- capture_output=True, text=True)
+ # open spinning wheel dialog
+ result = subprocess.run(
+ expanded_command, shell=True, capture_output=True, text=True
+ )
self.finished.emit(result.returncode == 0, result.stdout, result.stderr)
- #create a waiting dialog
+ # create a waiting dialog
# Create a waiting dialog
-
-
-
+
except Exception as e:
self.finished.emit(False, "", str(e))
@@ -70,42 +88,44 @@ class MainApp(integration_gui.Ui_MainWindow):
def __init__(self, window):
# First call setupUi to create all UI elements from the .ui file
self.setupUi(window)
-
+
# Add Ph2ACF topic to API settings layout programmatically
# Need to do this before load_settings
self.ph2acfTopicLabel = QLabel("Ph2ACF Topic:")
self.ph2acfTopicLE = QLineEdit()
self.ph2acfTopicLE.setObjectName("ph2acfTopicLE")
self.formLayout_2.addRow(self.ph2acfTopicLabel, self.ph2acfTopicLE)
-
+
# Load settings before setting up connections
self.load_settings()
-
+
# Connect settings change
self.ph2acfTopicLE.textChanged.connect(self.save_settings)
-
+
# Create ModuleDB instance
self.module_db = ModuleDB()
-
+
# Replace inventory and details tabs with ModuleDB tabs
- self.tabWidget.insertTab(1,self.module_db.ui.tab_2, "Module Inventory")
- self.tabWidget.insertTab(2,self.module_db.ui.moduleDetailsTab, "Module Details")
-
+ self.tabWidget.insertTab(1, self.module_db.ui.tab_2, "Module Inventory")
+ self.tabWidget.insertTab(
+ 2, self.module_db.ui.moduleDetailsTab, "Module Details"
+ )
+
# Connect module selection signal
self.module_db.module_selected.connect(self.on_module_selected)
self.module_db.ui.viewDetailsPB.clicked.connect(self.view_module_details)
# Set stretch factors for the main grid layout to make left side expand more
-# grid_layout = self.tab.layout()
-# grid_layout.setColumnStretch(0, 2) # Left column gets 2 parts
-# grid_layout.setColumnStretch(1, 1) # Right column gets 1 part
-
+ # grid_layout = self.tab.layout()
+ # grid_layout.setColumnStretch(0, 2) # Left column gets 2 parts
+ # grid_layout.setColumnStretch(1, 1) # Right column gets 1 part
+
# Set up square aspect ratio for graphics view
self.graphicsView.setMinimumSize(300, 300)
self.graphicsView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.graphicsView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-
+
# Create a custom resize event for the graphics view
# def resizeEvent(event):
# # Get the smaller of width or height
@@ -116,27 +136,26 @@ def __init__(self, window):
# self.graphicsView.resize(square_size)
# # Call parent resize event
# QGraphicsView.resizeEvent(self.graphicsView, event)
-
+
# # Attach the custom resize event
# self.graphicsView.resizeEvent = resizeEvent
-
+
# Initialize log emitter
self.log_emitter = LogEmitter()
self.log_emitter.log_message.connect(self.append_log)
-
+
# Fix text format setting
self.placeholdersHelpLabel.setTextFormat(Qt.PlainText)
-
+
# Initialize MQTT client to None
self.client = None
-
+
# Initialize air state
self.air_state = False
-
-
+
# Store the full module list for filtering - initialize before using
self.all_modules = []
-
+
# # Add search box - move this before connecting signals
# searchLayout = QHBoxLayout()
# searchLabel = QLabel("Search:")
@@ -145,17 +164,17 @@ def __init__(self, window):
# searchLayout.addWidget(searchLabel)
# searchLayout.addWidget(self.searchBox)
# searchLayout.addStretch()
-
+
# # Insert search layout before the tree widget
# layout = self.tab_2.layout()
# layout.insertLayout(1, searchLayout)
-
+
# Enable sorting
# self.treeWidget.setSortingEnabled(True)
-
+
# Enable sorting
# self.treeWidget.setSortingEnabled(True)
-
+
# Initialize lists for Ph2ACF temperatures
self.ph2acf_time = []
self.ssa_mean_values = []
@@ -163,26 +182,26 @@ def __init__(self, window):
self.mpa_mean_values = []
self.mpa_max_values = []
self.current_fuse_id = None
-
+
# Setup module details tab
- # self.setup_module_details_tab()
-
+ # self.setup_module_details_tab()
+
# Now connect signals after UI elements exist
# self.searchBox.textChanged.connect(self.filter_modules)
self.ringLE.returnPressed.connect(self.split_ring_and_position)
self.positionLE.returnPressed.connect(self.draw_ring)
self.mountPB.clicked.connect(self.mount_module)
self.unmountPB.clicked.connect(self.unmount_module)
-
+
# Initialize mounted modules dict
self.mounted_modules = {}
- self.analysisURL=""
- fibers=["SfibA","SfibB"]
- fibers+=[f"E3;{x}" for x in range(1,6)]
- powers=["BINT1"]
- powers+=[f"R01;M1.{x}" for x in range(1,13)]
- powers+=[f"R01;M2.{x}" for x in range(1,13)]
- powers+=[f"R01;M3.{x}" for x in range(1,13)]
+ self.analysisURL = ""
+ fibers = ["SfibA", "SfibB"]
+ fibers += [f"E3;{x}" for x in range(1, 6)]
+ powers = ["BINT1"]
+ powers += [f"R01;M1.{x}" for x in range(1, 13)]
+ powers += [f"R01;M2.{x}" for x in range(1, 13)]
+ powers += [f"R01;M3.{x}" for x in range(1, 13)]
self.layers_to_filters = {
"L1_47": {
"spacer": "2.6mm",
@@ -196,7 +215,7 @@ def __init__(self, window):
"spacer": "4.0mm",
"speed": "10G",
},
- #40,55,68
+ # 40,55,68
"L2_40": {
"spacer": "2.6mm",
"speed": "10G",
@@ -216,25 +235,24 @@ def __init__(self, window):
}
self.fiberCB.addItems(fibers)
self.powerCB.addItems(powers)
- self.number_of_modules=18
+ self.number_of_modules = 18
-# config_dir = os.path.expanduser("~/.config/integration_ui")
- #load last session from ~/.config/integration_ui/lastsession.txt
+ # config_dir = os.path.expanduser("~/.config/integration_ui")
+ # load last session from ~/.config/integration_ui/lastsession.txt
-
# Setup thermal camera plotting
self.setup_thermal_plot()
-
+
# Setup CAEN control
self.caen = CAENControl(self)
-
+
# Connect button signals
self.checkIDPB.clicked.connect(self.run_check_id)
self.hvOFFTestPB.clicked.connect(self.run_light_on_test)
self.hvONTestPB.clicked.connect(self.run_dark_test)
self.connectPowerPB.clicked.connect(self.connect_power)
self.connectFiberPB.clicked.connect(self.connect_fiber)
-
+
# Connect settings-related signals
self.checkIDCommandLE.textChanged.connect(self.save_settings)
self.lightOnCommandLE.textChanged.connect(self.save_settings)
@@ -244,7 +262,7 @@ def __init__(self, window):
self.mqttTopicLE.textChanged.connect(self.save_settings)
self.airCommandLE.textChanged.connect(self.save_settings)
self.resultsUrlLE.textChanged.connect(self.save_settings)
-
+
# Connect Apply button
self.applySettingsPB.clicked.connect(self.apply_settings)
@@ -256,13 +274,13 @@ def __init__(self, window):
# self.spacerCB.currentTextChanged.connect(self.update_module_list)
# self.spacerCB_2.currentTextChanged.connect(self.update_module_list)
# self.spacerCB_3.currentTextChanged.connect(self.update_module_list)
-
+
# # Initial module list load
self.update_module_list()
# # Connect select module button
# self.selectModulePB.clicked.connect(self.select_module)
-
+
# # Enable selection mode for tree widget
# self.treeWidget.setSelectionMode(QTreeWidget.SingleSelection)
@@ -270,18 +288,20 @@ def __init__(self, window):
self.moduleLE.textChanged.connect(self.module_changed)
self.powerCB.currentTextChanged.connect(self.update_connection_status)
self.fiberCB.currentTextChanged.connect(self.update_connection_status)
- self.fiber_endpoint=""
-
+ self.fiber_endpoint = ""
+
# Initial connection status check
- QTimer.singleShot(100, self.update_connection_status) # Small delay to ensure UI is ready
+ QTimer.singleShot(
+ 100, self.update_connection_status
+ ) # Small delay to ensure UI is ready
# Connect module details buttons
# self.editDetailsButton.clicked.connect(self.edit_selected_detail)
# self.saveDetailsButton.clicked.connect(self.save_module_details)
-
+
# Setup inventory buttons
# self.setup_inventory_buttons()
-
+
# Initialize current module tracking
self.current_module_id = None
@@ -289,38 +309,40 @@ def __init__(self, window):
self.checkIDLED.setStyleSheet("background-color: rgb(255, 255, 0);") # Yellow
self.hvOFFTestLED.setStyleSheet("background-color: rgb(255, 255, 0);")
self.hvONTestLED.setStyleSheet("background-color: rgb(255, 255, 0);")
-
+
# Disable tests 2 and 3 initially
-# self.hvOFFTestPB.setEnabled(False)
-# self.hvONTestPB.setEnabled(False)
-
+ # self.hvOFFTestPB.setEnabled(False)
+ # self.hvONTestPB.setEnabled(False)
+
# Track command workers
self.current_worker = None
# Connect module ID changes to reset test states
self.moduleLE.textChanged.connect(self.reset_test_states)
-
+
# Connect module selection to reset test states
self.module_db.ui.selectModulePB.clicked.connect(self.reset_test_states)
-
+
self.logsPB.clicked.connect(self.open_ph2acf_log)
# Connect module ID changes to load details
-# self.moduleLE.textChanged.connect(self.load_module_details)
+ # self.moduleLE.textChanged.connect(self.load_module_details)
# Populate layer type combo box
# self.ui.layertypeCB.clear()
# self.ui.layertypeCB.addItem("any")
# self.ui.layertypeCB.addItems(sorted(self.layers_to_filters.keys()))
-
+
# Connect signals
- #self.ui.layertypeCB.currentTextChanged.connect(self.update_filters_from_layer)
+ # self.ui.layertypeCB.currentTextChanged.connect(self.update_filters_from_layer)
self.ringLE.textChanged.connect(self.update_layer_from_ring)
# Connect module ID changes to check mounting status
self.moduleLE.textChanged.connect(self.check_module_mounting_status)
- session_file = os.path.join(os.path.expanduser("~/.config/integration_ui"), "lastsession.txt")
+ session_file = os.path.join(
+ os.path.expanduser("~/.config/integration_ui"), "lastsession.txt"
+ )
if os.path.exists(session_file):
with open(session_file, "r") as f:
session = f.read().strip()
@@ -330,16 +352,15 @@ def __init__(self, window):
self.ringLE.setText("L1")
self.split_ring_and_position()
- self.draw_ring()
+ self.draw_ring()
# Connect air control buttons
self.airONPB.clicked.connect(lambda: self.control_air(True))
self.airOFFPB.clicked.connect(lambda: self.control_air(False))
-
+
# Connect test results button
self.showPB.clicked.connect(self.show_test_results)
-
-
+
# Store max temperature
self.max_temperature = 0.0
@@ -351,24 +372,25 @@ def __init__(self, window):
self.current_session_operator = None
self.current_session_comments = None
self.current_module_id = None
- self.pbstatus={}
+ self.pbstatus = {}
self.airLed.setStyleSheet("background-color: yellow;")
- #define test session for DB
-# session = {
-# "operator": self.BI_Operator_line.text(),
-# "timestamp": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
-# "description": self.SeshDescription_db.text(),
-# "temperatures": {
-# "low": self.BI_LowTemp_dsb.value(),
-# "high": self.BI_HighTemp_dsb.value(),
-# },
-# "nCycles": self.BI_NCycles_sb.value(),
-# # "status": "Open" #to be implemented
-# "modulesList": [],
-# }
+ # define test session for DB
+
+ # session = {
+ # "operator": self.BI_Operator_line.text(),
+ # "timestamp": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
+ # "description": self.SeshDescription_db.text(),
+ # "temperatures": {
+ # "low": self.BI_LowTemp_dsb.value(),
+ # "high": self.BI_HighTemp_dsb.value(),
+ # },
+ # "nCycles": self.BI_NCycles_sb.value(),
+ # # "status": "Open" #to be implemented
+ # "modulesList": [],
+ # }
def disable_test_pbs_enable_cancel(self):
- self.pbstatus ={
+ self.pbstatus = {
self.checkIDPB: self.checkIDPB.isEnabled(),
self.hvOFFTestPB: self.hvOFFTestPB.isEnabled(),
self.hvONTestPB: self.hvONTestPB.isEnabled(),
@@ -377,7 +399,6 @@ def disable_test_pbs_enable_cancel(self):
self.hvOFFTestPB.setEnabled(False)
self.hvONTestPB.setEnabled(False)
self.cancelPB.setEnabled(True)
-
def reset_test_pbs(self):
print("Resetting test PBs")
@@ -386,35 +407,37 @@ def reset_test_pbs(self):
pb.setEnabled(status)
self.cancelPB.setEnabled(False)
-
def new_session(self):
- session={
+ session = {
"operator": self.operatorLE.text(),
- "description": "INTEGRATION: "+self.commentsLE.text(),
- "timestamp": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
- "modulesList": [ self.moduleLE.text() ],
+ "description": "INTEGRATION: " + self.commentsLE.text(),
+ "timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
+ "modulesList": [self.moduleLE.text()],
}
- #create new session in DB
- success, result = self.make_api_request("sessions", "POST", session)
+ # create new session in DB
+ success, result = self.make_api_request("sessions", "POST", session)
self.current_session = result["sessionName"]
self.current_module_id = self.moduleLE.text()
self.current_session_operator = self.operatorLE.text()
self.current_session_comments = self.commentsLE.text()
print(self.current_session)
return self.current_session
-
+
def get_session(self):
if self.current_session is None:
return self.new_session()
- if self.current_session_operator != self.operatorLE.text() or self.current_session_comments != self.commentsLE.text():
+ if (
+ self.current_session_operator != self.operatorLE.text()
+ or self.current_session_comments != self.commentsLE.text()
+ ):
return self.new_session()
if self.moduleLE.text() != self.current_module_id:
return self.new_session()
return self.current_session
-
+
def setup_thermal_plot(self):
self.fig, (self.ax1, self.ax2) = plt.subplots(2, 1, figsize=(8, 6))
-
+
# First plot: Image
self.im = self.ax1.imshow(np.random.rand(24, 32) * 30 + 10, cmap="plasma")
divider1 = make_axes_locatable(self.ax1)
@@ -427,18 +450,26 @@ def setup_thermal_plot(self):
self.max_values = []
self.avg_values = []
- self.line_min, = self.ax2.plot(self.time, self.min_values, label='Min')
- self.line_max, = self.ax2.plot(self.time, self.max_values, label='Max')
- self.line_avg, = self.ax2.plot(self.time, self.avg_values, label='Avg')
-
+ (self.line_min,) = self.ax2.plot(self.time, self.min_values, label="Min")
+ (self.line_max,) = self.ax2.plot(self.time, self.max_values, label="Max")
+ (self.line_avg,) = self.ax2.plot(self.time, self.avg_values, label="Avg")
+
# SSA/MPA trend lines
- self.line_ssa_mean, = self.ax2.plot([], [], label='SSA Mean', linestyle='-', marker='s', markersize=3)
- self.line_ssa_max, = self.ax2.plot([], [], label='SSA Max', linestyle='-', marker='^', markersize=3)
- self.line_mpa_mean, = self.ax2.plot([], [], label='MPA Mean', linestyle='-', marker='o', markersize=3)
- self.line_mpa_max, = self.ax2.plot([], [], label='MPA Max', linestyle='-', marker='v', markersize=3)
-
- self.ax2.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
- self.ax2.legend(fontsize='xx-small', loc='upper left', ncol=2)
+ (self.line_ssa_mean,) = self.ax2.plot(
+ [], [], label="SSA Mean", linestyle="-", marker="s", markersize=3
+ )
+ (self.line_ssa_max,) = self.ax2.plot(
+ [], [], label="SSA Max", linestyle="-", marker="^", markersize=3
+ )
+ (self.line_mpa_mean,) = self.ax2.plot(
+ [], [], label="MPA Mean", linestyle="-", marker="o", markersize=3
+ )
+ (self.line_mpa_max,) = self.ax2.plot(
+ [], [], label="MPA Max", linestyle="-", marker="v", markersize=3
+ )
+
+ self.ax2.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
+ self.ax2.legend(fontsize="xx-small", loc="upper left", ncol=2)
# Create canvas and add to layout
self.canvas = FigureCanvas(self.fig)
@@ -447,8 +478,12 @@ def setup_thermal_plot(self):
def get_settings_file(self):
"""Get the settings file path"""
- config_file = os.path.join(os.path.expanduser("~/.config/integration_ui"), 'settings.yaml')
- bundled_file = os.path.join(os.path.dirname(__file__), 'settings_integration.yaml')
+ config_file = os.path.join(
+ os.path.expanduser("~/.config/integration_ui"), "settings.yaml"
+ )
+ bundled_file = os.path.join(
+ os.path.dirname(__file__), "settings_integration.yaml"
+ )
if os.path.exists(config_file):
return config_file
elif os.path.exists(bundled_file):
@@ -459,57 +494,65 @@ def get_settings_file(self):
def load_settings(self):
"""Load settings from YAML file"""
try:
- with open(self.get_settings_file(), 'r') as f:
+ with open(self.get_settings_file(), "r") as f:
settings = yaml.safe_load(f)
-
+
if settings:
print(settings)
print(self.get_settings_file())
# Load database URL
- self.dbEndpointLE.setText(settings.get('db_url', 'http://localhost:5000'))
-
+ self.dbEndpointLE.setText(
+ settings.get("db_url", "http://localhost:5000")
+ )
+
# Load MQTT settings
- self.mqttServerLE.setText(settings.get('mqtt_server', 'test.mosquitto.org'))
- self.mqttTopicLE.setText(settings.get('mqtt_topic', '/ar/thermal/image'))
-
+ self.mqttServerLE.setText(
+ settings.get("mqtt_server", "test.mosquitto.org")
+ )
+ self.mqttTopicLE.setText(
+ settings.get("mqtt_topic", "/ar/thermal/image")
+ )
+
# Load command settings
- self.checkIDCommandLE.setText(settings.get('check_id_command', ''))
- self.lightOnCommandLE.setText(settings.get('light_on_command', ''))
- self.darkTestCommandLE.setText(settings.get('dark_test_command', ''))
- self.airCommandLE.setText(settings.get('air_command', 'air.sh {airOn}'))
- self.ph2acfTopicLE.setText(settings.get('ph2acf_topic', '/ph2acf/data'))
- self.resultsUrlLE.setText(settings.get('results_url', 'file:Results/html/latest/index.html'))
-
+ self.checkIDCommandLE.setText(settings.get("check_id_command", ""))
+ self.lightOnCommandLE.setText(settings.get("light_on_command", ""))
+ self.darkTestCommandLE.setText(settings.get("dark_test_command", ""))
+ self.airCommandLE.setText(settings.get("air_command", "air.sh {airOn}"))
+ self.ph2acfTopicLE.setText(settings.get("ph2acf_topic", "/ph2acf/data"))
+ self.resultsUrlLE.setText(
+ settings.get("results_url", "file:Results/html/latest/index.html")
+ )
+
except FileNotFoundError:
# Use defaults if no settings file exists
self.save_settings()
- def get_api_url(self, endpoint=''):
+ def get_api_url(self, endpoint=""):
"""Get full API URL with endpoint"""
- base_url = self.dbEndpointLE.text().rstrip('/')
+ base_url = self.dbEndpointLE.text().rstrip("/")
return f"{base_url}/{endpoint.lstrip('/')}" if endpoint else base_url
def save_settings(self):
"""Save settings to YAML file"""
settings = {
- 'db_url': self.dbEndpointLE.text(),
- 'mqtt_server': self.mqttServerLE.text(),
- 'mqtt_topic': self.mqttTopicLE.text(),
+ "db_url": self.dbEndpointLE.text(),
+ "mqtt_server": self.mqttServerLE.text(),
+ "mqtt_topic": self.mqttTopicLE.text(),
# Add command settings
- 'check_id_command': self.checkIDCommandLE.text(),
- 'light_on_command': self.lightOnCommandLE.text(),
- 'dark_test_command': self.darkTestCommandLE.text(),
- 'air_command': self.airCommandLE.text(),
- 'ph2acf_topic': self.ph2acfTopicLE.text(),
- 'results_url': self.resultsUrlLE.text(),
+ "check_id_command": self.checkIDCommandLE.text(),
+ "light_on_command": self.lightOnCommandLE.text(),
+ "dark_test_command": self.darkTestCommandLE.text(),
+ "air_command": self.airCommandLE.text(),
+ "ph2acf_topic": self.ph2acfTopicLE.text(),
+ "results_url": self.resultsUrlLE.text(),
}
-
+
try:
# Ensure config directory exists
config_dir = os.path.dirname(self.get_settings_file())
os.makedirs(config_dir, exist_ok=True)
-
- with open(self.get_settings_file(), 'w') as f:
+
+ with open(self.get_settings_file(), "w") as f:
yaml.dump(settings, f, default_flow_style=False)
except Exception as e:
self.log_output(f"Error saving settings: {e}")
@@ -524,20 +567,20 @@ def setup_mqtt(self):
self.client.loop_stop()
except:
pass
-
+
self.client = mqtt.Client()
self.client.on_connect = self.on_mqtt_connect
self.client.on_message = self.on_mqtt_message
self.client.on_disconnect = self.on_mqtt_disconnect
-
+
# Use settings from UI
mqtt_server = self.mqttServerLE.text()
-
+
# Connect with timeout
self.log_output(f"Connecting to MQTT server: {mqtt_server}")
self.client.connect_async(mqtt_server, 1883, 60)
self.client.loop_start()
-
+
except Exception as e:
self.log_output(f"MQTT Setup Error: {str(e)}")
# Don't let MQTT errors crash the UI
@@ -552,12 +595,14 @@ def on_mqtt_connect(self, client, userdata, flags, rc):
mqtt_topic = self.mqttTopicLE.text()
ph2acf_topic = self.ph2acfTopicLE.text()
self.log_output(f"Subscribing to topics: {mqtt_topic}, {ph2acf_topic}")
- client.subscribe([
- ("/air/status", 0),
- (mqtt_topic, 0),
- (ph2acf_topic, 0),
- ("shellies/ventola/status/switch:0", 0)
- ])
+ client.subscribe(
+ [
+ ("/air/status", 0),
+ (mqtt_topic, 0),
+ (ph2acf_topic, 0),
+ ("shellies/ventola/status/switch:0", 0),
+ ]
+ )
else:
self.log_output(f"MQTT Connection failed with code {rc}")
except Exception as e:
@@ -568,184 +613,203 @@ def on_mqtt_disconnect(self, client, userdata, rc):
try:
self.log_output("Disconnected from MQTT server")
if rc != 0:
- self.log_output("Unexpected disconnection. Will attempt to reconnect...")
+ self.log_output(
+ "Unexpected disconnection. Will attempt to reconnect..."
+ )
except Exception as e:
self.log_output(f"MQTT Disconnect Error: {str(e)}")
def on_mqtt_message(self, client, userdata, msg):
"""Handle MQTT messages with error protection"""
-# print(msg.topic)
- if msg.topic == "shellies/ventola/status/switch:0" :
- j=json.loads(msg.payload)
+ # print(msg.topic)
+ if msg.topic == "shellies/ventola/status/switch:0":
+ j = json.loads(msg.payload)
print(j)
- if j.get("apower",0) > 0.1 :
+ if j.get("apower", 0) > 0.1:
self.airLed.setStyleSheet("background-color: green;")
else:
self.airLed.setStyleSheet("background-color: red;")
-
+
elif msg.topic == "/air/status":
- # print(msg.payload)
- if int(msg.payload)==1:
+ # print(msg.payload)
+ if int(msg.payload) == 1:
self.airLed.setStyleSheet("background-color: green;")
else:
self.airLed.setStyleSheet("background-color: red;")
elif msg.topic == self.ph2acfTopicLE.text():
- try:
- data = json.loads(msg.payload)
- # Only process if fuseId matches (and is not None)
- fuse_id_msg = data.get("LpGBT_OG0_fuseId")
- if self.current_fuse_id is not None and fuse_id_msg is not None and int(fuse_id_msg) == int(self.current_fuse_id):
- # We have matching data, extract temperatures
- ssa_temps = []
- mpa_temps = []
- missing_any_offset = False
-
- offsets = {}
- if self.current_module_data:
- if "temperature_offset" in self.current_module_data:
- offsets = self.current_module_data["temperature_offset"]
- else:
- # Look for alternative keys starting with 'temperature_offset'
- variants = sorted([k for k in self.current_module_data.keys() if k.startswith("temperature_offset")])
- if variants:
- offsets = self.current_module_data[variants[0]]
-
- if not isinstance(offsets, dict):
+ try:
+ data = json.loads(msg.payload)
+ # Only process if fuseId matches (and is not None)
+ fuse_id_msg = data.get("LpGBT_OG0_fuseId")
+ if (
+ self.current_fuse_id is not None
+ and fuse_id_msg is not None
+ and int(fuse_id_msg) == int(self.current_fuse_id)
+ ):
+ # We have matching data, extract temperatures
+ ssa_temps = []
+ mpa_temps = []
+ missing_any_offset = False
+
offsets = {}
- print(offsets)
- for key, value in data.items():
- if key.endswith("_temp"):
- # Extract hybrid and chip from key like SSA_H0_C0_temp
- print(key,value)
- parts = key.split('_')
- if len(parts) >= 3:
- ctype = parts[0] # SSA or MPA (though MPA often encoded as SSA in some Ph2_ACF versions)
- hybrid = parts[1] # H0 or H1
- chip = parts[2] # C0, C1, ...
-
- # Clean up indices
- h_idx = hybrid[1:] if hybrid.startswith('H') else hybrid
- c_idx = chip[1:] if chip.startswith('C') else chip
-
- # Construct offset key like SSA_H0_0
- # Note: The mapping provided in prompt says SSA_H0_0, MPA_H0_8 etc.
- # We need to determine if it's SSA or MPA based on the chip index or key name.
- # User said "SSA/MPA mean and max".
- # Let's use the provided mapping logic: keys like SSA_H0_0
- offset_key = f"{ctype}_{hybrid}_{c_idx}"
-
- temp = float(value)
- if offset_key in offsets:
- temp += float(offsets[offset_key])
- else:
- missing_any_offset = True
-
- if ctype == "SSA":
- ssa_temps.append(temp)
- elif ctype == "MPA":
- mpa_temps.append(temp)
- else:
- # Fallback/Mixed
- ssa_temps.append(temp)
-
+ if self.current_module_data:
+ if "temperature_offset" in self.current_module_data:
+ offsets = self.current_module_data["temperature_offset"]
+ else:
+ # Look for alternative keys starting with 'temperature_offset'
+ variants = sorted(
+ [
+ k
+ for k in self.current_module_data.keys()
+ if k.startswith("temperature_offset")
+ ]
+ )
+ if variants:
+ offsets = self.current_module_data[variants[0]]
+
+ if not isinstance(offsets, dict):
+ offsets = {}
+ print(offsets)
+ for key, value in data.items():
+ if key.endswith("_temp"):
+ # Extract hybrid and chip from key like SSA_H0_C0_temp
+ print(key, value)
+ parts = key.split("_")
+ if len(parts) >= 3:
+ ctype = parts[
+ 0
+ ] # SSA or MPA (though MPA often encoded as SSA in some Ph2_ACF versions)
+ hybrid = parts[1] # H0 or H1
+ chip = parts[2] # C0, C1, ...
+
+ # Clean up indices
+ h_idx = hybrid[1:] if hybrid.startswith("H") else hybrid
+ c_idx = chip[1:] if chip.startswith("C") else chip
+
+ # Construct offset key like SSA_H0_0
+ # Note: The mapping provided in prompt says SSA_H0_0, MPA_H0_8 etc.
+ # We need to determine if it's SSA or MPA based on the chip index or key name.
+ # User said "SSA/MPA mean and max".
+ # Let's use the provided mapping logic: keys like SSA_H0_0
+ offset_key = f"{ctype}_{hybrid}_{c_idx}"
+
+ temp = float(value)
+ if offset_key in offsets:
+ temp += float(offsets[offset_key])
+ else:
+ missing_any_offset = True
+
+ if ctype == "SSA":
+ ssa_temps.append(temp)
+ elif ctype == "MPA":
+ mpa_temps.append(temp)
+ else:
+ # Fallback/Mixed
+ ssa_temps.append(temp)
+
+ now = datetime.now()
+ # Update trend
+ self.ph2acf_time.append(now)
+
+ # Keep maximum 600 values for Ph2ACF data
+ if len(self.ph2acf_time) > 600:
+ self.ph2acf_time.pop(0)
+ self.ssa_mean_values.pop(0)
+ self.ssa_max_values.pop(0)
+ self.mpa_mean_values.pop(0)
+ self.mpa_max_values.pop(0)
+
+ # SSA Stats
+ if ssa_temps:
+ self.ssa_mean_values.append(np.mean(ssa_temps))
+ self.ssa_max_values.append(np.max(ssa_temps))
+ else:
+ self.ssa_mean_values.append(np.nan)
+ self.ssa_max_values.append(np.nan)
+
+ # MPA Stats
+ if mpa_temps:
+ self.mpa_mean_values.append(np.mean(mpa_temps))
+ self.mpa_max_values.append(np.max(mpa_temps))
+ else:
+ self.mpa_mean_values.append(np.nan)
+ self.mpa_max_values.append(np.nan)
+
+ print(
+ f"DEBUG: SSA Mean: {self.ssa_mean_values[-1] if ssa_temps else 'N/A'}"
+ )
+
+ # Update plot lines
+ # Set line style based on offset presence
+ ls = "--" if missing_any_offset else "-"
+
+ self.line_ssa_mean.set_data(self.ph2acf_time, self.ssa_mean_values)
+ self.line_ssa_mean.set_linestyle(ls)
+ self.line_ssa_max.set_data(self.ph2acf_time, self.ssa_max_values)
+ self.line_ssa_max.set_linestyle(ls)
+
+ self.line_mpa_mean.set_data(self.ph2acf_time, self.mpa_mean_values)
+ self.line_mpa_mean.set_linestyle(ls)
+ self.line_mpa_max.set_data(self.ph2acf_time, self.mpa_max_values)
+ self.line_mpa_max.set_linestyle(ls)
+
+ self.ax2.relim()
+ self.ax2.autoscale_view()
+ self.canvas.draw()
+
+ except Exception as e:
+ self.log_output(f"Ph2ACF Message Error: {str(e)}")
+
+ elif msg.topic == self.mqttTopicLE.text():
+ try:
+ flo_arr = [
+ struct.unpack("f", msg.payload[i : i + 4])[0]
+ for i in range(0, len(msg.payload), 4)
+ ]
+
+ # Update image plot
+ self.im.set_data(np.array(flo_arr).reshape(24, 32))
+ self.im.set_clim(18, 35)
+
+ # Update trend plot
now = datetime.now()
- # Update trend
- self.ph2acf_time.append(now)
-
- # Keep maximum 600 values for Ph2ACF data
- if len(self.ph2acf_time) > 600:
- self.ph2acf_time.pop(0)
- self.ssa_mean_values.pop(0)
- self.ssa_max_values.pop(0)
- self.mpa_mean_values.pop(0)
- self.mpa_max_values.pop(0)
-
- # SSA Stats
- if ssa_temps:
- self.ssa_mean_values.append(np.mean(ssa_temps))
- self.ssa_max_values.append(np.max(ssa_temps))
- else:
- self.ssa_mean_values.append(np.nan)
- self.ssa_max_values.append(np.nan)
-
- # MPA Stats
- if mpa_temps:
- self.mpa_mean_values.append(np.mean(mpa_temps))
- self.mpa_max_values.append(np.max(mpa_temps))
- else:
- self.mpa_mean_values.append(np.nan)
- self.mpa_max_values.append(np.nan)
-
- print(f"DEBUG: SSA Mean: {self.ssa_mean_values[-1] if ssa_temps else 'N/A'}")
-
- # Update plot lines
- # Set line style based on offset presence
- ls = '--' if missing_any_offset else '-'
-
- self.line_ssa_mean.set_data(self.ph2acf_time, self.ssa_mean_values)
- self.line_ssa_mean.set_linestyle(ls)
- self.line_ssa_max.set_data(self.ph2acf_time, self.ssa_max_values)
- self.line_ssa_max.set_linestyle(ls)
-
- self.line_mpa_mean.set_data(self.ph2acf_time, self.mpa_mean_values)
- self.line_mpa_mean.set_linestyle(ls)
- self.line_mpa_max.set_data(self.ph2acf_time, self.mpa_max_values)
- self.line_mpa_max.set_linestyle(ls)
-
+ self.time.append(now)
+
+ self.min_values.append(min(flo_arr))
+ self.max_values.append(max(flo_arr))
+ self.avg_values.append(np.mean(flo_arr))
+ data = {
+ "min": min(flo_arr),
+ "max": max(flo_arr),
+ "avg": np.mean(flo_arr),
+ }
+ ret = client.publish("/integration/thermalcamera", json.dumps(data))
+
+ # Update max temperature display
+ self.max_temperature = max(flo_arr)
+ self.tMaxLabel.setText(f"Tmax: {self.max_temperature:.1f}")
+ if self.max_temperature > 45:
+ self.caenGUI.safe_lv_off()
+ if self.max_temperature > 50:
+ self.caenGUI.off(self.caenGUI.channels["HV"])
+ self.caenGUI.off(self.caenGUI.channels["LV"])
+ # Keep maximum 600 values
+ if len(self.time) > 600:
+ self.time.pop(0)
+ self.min_values.pop(0)
+ self.max_values.pop(0)
+ self.avg_values.pop(0)
+
+ self.line_min.set_data(self.time, self.min_values)
+ self.line_max.set_data(self.time, self.max_values)
+ self.line_avg.set_data(self.time, self.avg_values)
self.ax2.relim()
self.ax2.autoscale_view()
+
self.canvas.draw()
-
- except Exception as e:
- self.log_output(f"Ph2ACF Message Error: {str(e)}")
- elif msg.topic == self.mqttTopicLE.text():
- try:
- flo_arr = [
- struct.unpack("f", msg.payload[i : i + 4])[0]
- for i in range(0, len(msg.payload), 4)
- ]
-
- # Update image plot
- self.im.set_data(np.array(flo_arr).reshape(24, 32))
- self.im.set_clim(18, 35)
-
- # Update trend plot
- now = datetime.now()
- self.time.append(now)
-
- self.min_values.append(min(flo_arr))
- self.max_values.append(max(flo_arr))
- self.avg_values.append(np.mean(flo_arr))
- data={ "min": min(flo_arr), "max": max(flo_arr), "avg":np.mean(flo_arr) }
- ret=client.publish("/integration/thermalcamera",json.dumps(data))
-
-
- # Update max temperature display
- self.max_temperature = max(flo_arr)
- self.tMaxLabel.setText(f"Tmax: {self.max_temperature:.1f}")
- if self.max_temperature > 45 :
- self.caenGUI.safe_lv_off()
- if self.max_temperature > 50 :
- self.caenGUI.off(self.caenGUI.channels["HV"])
- self.caenGUI.off(self.caenGUI.channels["LV"])
- # Keep maximum 600 values
- if len(self.time) > 600:
- self.time.pop(0)
- self.min_values.pop(0)
- self.max_values.pop(0)
- self.avg_values.pop(0)
-
- self.line_min.set_data(self.time, self.min_values)
- self.line_max.set_data(self.time, self.max_values)
- self.line_avg.set_data(self.time, self.avg_values)
- self.ax2.relim()
- self.ax2.autoscale_view()
-
- self.canvas.draw()
-
- except Exception as e:
- self.log_output(f"MQTT Message Error: {str(e)}")
+ except Exception as e:
+ self.log_output(f"MQTT Message Error: {str(e)}")
else:
# Handle other topics if any (already handled for air and fan)
pass
@@ -757,116 +821,119 @@ def split_ring_and_position(self):
session_file = os.path.join(config_dir, "lastsession.txt")
with open(session_file, "w") as f:
f.write(self.ringLE.text())
- ring=ring_position
- position="0"
- if ';' in ring_position:
- ring, position = ring_position.split(';')
+ ring = ring_position
+ position = "0"
+ if ";" in ring_position:
+ ring, position = ring_position.split(";")
self.ringLE.setText(ring)
self.positionLE.setText(position)
if ring[:2] == "L1":
- self.number_of_modules=18
+ self.number_of_modules = 18
if ring[:2] == "L2":
- self.number_of_modules=26
+ self.number_of_modules = 26
if ring[:2] == "L3":
- self.number_of_modules=36
+ self.number_of_modules = 36
self.draw_ring()
-
+
def draw_ring(self):
- #draw a ring with number_of_modules modules, each is draw as a rectangle
+ # draw a ring with number_of_modules modules, each is draw as a rectangle
# even and odd modules are drawn at different radii
# draw in graphics view
self.update_module_list()
scene = QGraphicsScene()
self.graphicsView.setScene(scene)
-
+
# Store module coordinates for click detection
self.module_coordinates = []
-
+
pen = QPen()
pen.setWidth(2)
pen.setColor(Qt.black)
- #dashed style
+ # dashed style
pen.setStyle(Qt.DashLine)
- radius=min(self.graphicsView.width(),self.graphicsView.height())/2.5
- scene.addEllipse(0, 0, radius*2, radius*2, pen)
- deltaphi=360/self.number_of_modules*2
- #solid
+ radius = min(self.graphicsView.width(), self.graphicsView.height()) / 2.5
+ scene.addEllipse(0, 0, radius * 2, radius * 2, pen)
+ deltaphi = 360 / self.number_of_modules * 2
+ # solid
pen.setStyle(Qt.SolidLine)
- #draw two circles in the middle-top
- scene.addEllipse(radius-7, -20, 6, 6, pen)
- scene.addEllipse(radius+7, -20, 6, 6, pen)
- scene.addEllipse(radius-4, -17, 1, 1, pen)
- scene.addEllipse(radius+10, -17, 1, 1, pen)
+ # draw two circles in the middle-top
+ scene.addEllipse(radius - 7, -20, 6, 6, pen)
+ scene.addEllipse(radius + 7, -20, 6, 6, pen)
+ scene.addEllipse(radius - 4, -17, 1, 1, pen)
+ scene.addEllipse(radius + 10, -17, 1, 1, pen)
ring_id = self.ringLE.text()
-
- for i in range(1,self.number_of_modules+1):
- phi=-(i)*deltaphi+deltaphi/2
- phi-=90
- if i> self.number_of_modules/2:
- phi+=deltaphi/2
-
+
+ for i in range(1, self.number_of_modules + 1):
+ phi = -(i) * deltaphi + deltaphi / 2
+ phi -= 90
+ if i > self.number_of_modules / 2:
+ phi += deltaphi / 2
+
# Check if this position has a mounted module
position_key = f"{ring_id};{i}"
is_mounted = position_key in self.mounted_modules
-
- if self.positionLE.text()==str(i):
+
+ if self.positionLE.text() == str(i):
pen.setColor(Qt.red)
elif is_mounted:
pen.setColor(Qt.blue) # Use blue for mounted positions
else:
pen.setColor(Qt.black)
-
+
# Calculate module coordinates
- if i<= self.number_of_modules/2:
- x1=(radius*0.95)*cos((phi+deltaphi/4)/180*pi)+radius
- y1=(radius*0.95)*sin((phi+deltaphi/4)/180*pi)+radius
- x2=(radius*0.95)*cos((phi-deltaphi/4)/180*pi)+radius
- y2=(radius*0.95)*sin((phi-deltaphi/4)/180*pi)+radius
- x3=(radius*1.1)*cos((phi+deltaphi/4)/180*pi)+radius
- y3=(radius*1.1)*sin((phi+deltaphi/4)/180*pi)+radius
- x4=(radius*1.1)*cos((phi-deltaphi/4)/180*pi)+radius
- y4=(radius*1.1)*sin((phi-deltaphi/4)/180*pi)+radius
+ if i <= self.number_of_modules / 2:
+ x1 = (radius * 0.95) * cos((phi + deltaphi / 4) / 180 * pi) + radius
+ y1 = (radius * 0.95) * sin((phi + deltaphi / 4) / 180 * pi) + radius
+ x2 = (radius * 0.95) * cos((phi - deltaphi / 4) / 180 * pi) + radius
+ y2 = (radius * 0.95) * sin((phi - deltaphi / 4) / 180 * pi) + radius
+ x3 = (radius * 1.1) * cos((phi + deltaphi / 4) / 180 * pi) + radius
+ y3 = (radius * 1.1) * sin((phi + deltaphi / 4) / 180 * pi) + radius
+ x4 = (radius * 1.1) * cos((phi - deltaphi / 4) / 180 * pi) + radius
+ y4 = (radius * 1.1) * sin((phi - deltaphi / 4) / 180 * pi) + radius
else:
- x1=(radius*0.9)*cos((phi+deltaphi/4)/180*pi)+radius
- y1=(radius*0.9)*sin((phi+deltaphi/4)/180*pi)+radius
- x2=(radius*0.9)*cos((phi-deltaphi/4)/180*pi)+radius
- y2=(radius*0.9)*sin((phi-deltaphi/4)/180*pi)+radius
- x3=(radius*1.05)*cos((phi+deltaphi/4)/180*pi)+radius
- y3=(radius*1.05)*sin((phi+deltaphi/4)/180*pi)+radius
- x4=(radius*1.05)*cos((phi-deltaphi/4)/180*pi)+radius
- y4=(radius*1.05)*sin((phi-deltaphi/4)/180*pi)+radius
-
+ x1 = (radius * 0.9) * cos((phi + deltaphi / 4) / 180 * pi) + radius
+ y1 = (radius * 0.9) * sin((phi + deltaphi / 4) / 180 * pi) + radius
+ x2 = (radius * 0.9) * cos((phi - deltaphi / 4) / 180 * pi) + radius
+ y2 = (radius * 0.9) * sin((phi - deltaphi / 4) / 180 * pi) + radius
+ x3 = (radius * 1.05) * cos((phi + deltaphi / 4) / 180 * pi) + radius
+ y3 = (radius * 1.05) * sin((phi + deltaphi / 4) / 180 * pi) + radius
+ x4 = (radius * 1.05) * cos((phi - deltaphi / 4) / 180 * pi) + radius
+ y4 = (radius * 1.05) * sin((phi - deltaphi / 4) / 180 * pi) + radius
+
# Store module coordinates and position number
- self.module_coordinates.append({
- 'position': i,
- 'coords': [(x1,y1), (x2,y2), (x3,y3), (x4,y4)]
- })
-
+ self.module_coordinates.append(
+ {"position": i, "coords": [(x1, y1), (x2, y2), (x3, y3), (x4, y4)]}
+ )
+
scene.addLine(x1, y1, x2, y2, pen)
scene.addLine(x2, y2, x4, y4, pen)
scene.addLine(x4, y4, x3, y3, pen)
scene.addLine(x3, y3, x1, y1, pen)
-
+
# Add module ID text for mounted modules
if is_mounted:
text = self.mounted_modules[position_key]
else:
- text= str(i)
+ text = str(i)
# Calculate center point of the module
center_x = (x1 + x3) / 2
center_y = (y1 + y3) / 2
-
+
# Calculate angle for radial text (in degrees)
angle = atan2(center_y - radius, center_x - radius) * 180 / pi
# Add 90 degrees to make text perpendicular to radius
- angle += 180-deltaphi/4
-
+ angle += 180 - deltaphi / 4
+
text_item = scene.addText(text)
# Center the text around its position
text_bounds = text_item.boundingRect()
- text_item.setPos((radius*0.5)*cos(phi/180*pi)+radius - text_bounds.width()/2,
- (radius*0.5)*sin(phi/180*pi)+radius - text_bounds.height()/2)
+ text_item.setPos(
+ (radius * 0.5) * cos(phi / 180 * pi) + radius - text_bounds.width() / 2,
+ (radius * 0.5) * sin(phi / 180 * pi)
+ + radius
+ - text_bounds.height() / 2,
+ )
# Set the rotation around the center of the text
text_item.setTransformOriginPoint(text_bounds.center())
text_item.setRotation(angle)
@@ -881,78 +948,80 @@ def handle_ring_click(self, event):
# Convert click coordinates to scene coordinates
scene_pos = self.graphicsView.mapToScene(event.pos())
x, y = scene_pos.x(), scene_pos.y()
-
+
# Check if click is inside any module
for module in self.module_coordinates:
- coords = module['coords']
+ coords = module["coords"]
# Create polygon from module coordinates
polygon = [(x_, y_) for x_, y_ in coords]
-
+
# Calculate bounding box for quick check
min_x = min(x for x, _ in polygon)
max_x = max(x for x, _ in polygon)
min_y = min(y for _, y in polygon)
max_y = max(y for _, y in polygon)
-
+
# First do a bounding box check
- if (min_x <= x <= max_x and min_y <= y <= max_y):
+ if min_x <= x <= max_x and min_y <= y <= max_y:
# If in bounding box, do detailed polygon check
if self.point_in_polygon(x, y, polygon):
- new_position = str(module['position'])
-
+ new_position = str(module["position"])
+
# Only proceed if position actually changed
if new_position != self.positionLE.text():
self.log_output(f"Found match in module {new_position}")
-
+
# Update position LE
self.positionLE.setText(new_position)
-
+
# Check if a module is mounted at this position
ring_id = self.ringLE.text()
position_key = f"{ring_id};{new_position}"
-
+
if position_key in self.mounted_modules:
# Update module ID with the mounted module's ID
mounted_module_id = self.mounted_modules[position_key]
self.moduleLE.setText(mounted_module_id)
-
+
# Reset test states since we have a new module
self.reset_test_states()
-
+
# Redraw ring to update highlighting
self.draw_ring()
break
def point_in_polygon(self, x, y, polygon):
"""Check if point (x,y) is inside polygon using cross product method"""
+
def sign(p1, p2, p3):
return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1])
-
+
def is_point_in_triangle(pt, v1, v2, v3):
d1 = sign(pt, v1, v2)
d2 = sign(pt, v2, v3)
d3 = sign(pt, v3, v1)
-
+
has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)
-
+
return not (has_neg and has_pos)
-
+
# For quadrilateral, split into two triangles and check both
point = (x, y)
- return (is_point_in_triangle(point, polygon[0], polygon[1], polygon[2]) or
- is_point_in_triangle(point, polygon[2], polygon[3], polygon[0]))
+ return is_point_in_triangle(
+ point, polygon[0], polygon[1], polygon[2]
+ ) or is_point_in_triangle(point, polygon[2], polygon[3], polygon[0])
def get_placeholder_values(self):
"""Get current values for all placeholders"""
return {
- 'ring_id': self.ringLE.text(),
- 'position': self.positionLE.text(),
- 'module_id': self.moduleLE.text(),
- 'fiber': self.fiberCB.currentText(),
- 'power': self.powerCB.currentText(),
- 'fiber_endpoint': self.fiber_endpoint,
- 'session': self.get_session(),
+ "ring_id": self.ringLE.text(),
+ "position": self.positionLE.text(),
+ "module_id": self.moduleLE.text(),
+ "fiber": self.fiberCB.currentText(),
+ "power": self.powerCB.currentText(),
+ "fiber_endpoint": self.fiber_endpoint,
+ "session": self.get_session(),
}
def expand_placeholders(self, text):
@@ -973,11 +1042,11 @@ def run_command(self, command):
self.current_worker.finished.disconnect()
self.current_worker.terminate()
self.current_worker.wait()
-
+
expanded_command = self.expand_placeholders(command)
self.log_output(f"Running command: {expanded_command}")
self.log_output(f"Using placeholders: {self.get_placeholder_values()}")
-
+
self.current_worker = CommandWorker(expanded_command)
self.current_worker.finished.connect(self.handle_command_finished)
self.current_worker.start()
@@ -988,13 +1057,12 @@ def handle_command_finished(self, success, stdout, stderr):
self.log_output(f"Output:\n{stdout}")
if stderr:
self.log_output(f"Error:\n{stderr}")
-
+
# Clean up worker
if self.current_worker:
self.current_worker.finished.disconnect()
self.current_worker = None
-
-
+
def show_error_dialog(self, message):
"""Show error dialog with the given message"""
msg = QMessageBox()
@@ -1007,19 +1075,17 @@ def show_error_dialog(self, message):
def make_api_request(self, endpoint, method, data=None):
"""Make API request and return result"""
url = self.get_api_url(endpoint)
-
+
try:
self.log_output(f"Making {method} request to: {url}")
-
+
if data:
self.log_output(f"With data: {data}")
-
+
response = requests.request(
- method=method.lower(),
- url=url,
- json=data if data else None
+ method=method.lower(), url=url, json=data if data else None
)
-
+
if response.status_code != 200 and response.status_code != 201:
error_msg = f"API Error ({response.status_code}): {response.text}"
self.log_output(error_msg)
@@ -1053,44 +1119,55 @@ def append_log(self, text):
def run_check_id(self):
"""Run Check ID test"""
self.log_output("=== Running Check ID ===")
- #check that operator, comment and module are not empty
- if self.operatorLE.text()=="" or self.commentsLE.text()=="" or self.moduleLE.text()=="":
+ # check that operator, comment and module are not empty
+ if (
+ self.operatorLE.text() == ""
+ or self.commentsLE.text() == ""
+ or self.moduleLE.text() == ""
+ ):
self.log_output("Operator, Comments and Module ID must be filled")
- #open dialog
+ # open dialog
self.show_error_dialog("Operator, Comments and Module ID must be filled")
return
+
def handle_check_id(success, stdout, stderr):
if success:
- self.checkIDLED.setStyleSheet("background-color: rgb(85, 170, 0);") # Green
+ self.checkIDLED.setStyleSheet(
+ "background-color: rgb(85, 170, 0);"
+ ) # Green
self.checkIDPB.setEnabled(True)
self.hvOFFTestPB.setEnabled(True) # Enable test 2
self.hvONTestPB.setEnabled(True) # Enable test 3
-
+
# Try to parse module ID from output
try:
# Assuming output format contains "Module ID: XXXX"
print(stdout)
- # module_id = stdout.split("Module ID:")[1].strip().split()[0]
- # self.checkIDlabel.setText(f"ID: {module_id}")
- module_id = re.search(r"Board.*Module (.*?)\s\(", stdout).group(1).strip()
+ # module_id = stdout.split("Module ID:")[1].strip().split()[0]
+ # self.checkIDlabel.setText(f"ID: {module_id}")
+ module_id = (
+ re.search(r"Board.*Module (.*?)\s\(", stdout).group(1).strip()
+ )
print(re.search(r"Module (.*?)\s\(", stdout))
self.checkIDlabel.setText(f"ID: {module_id}")
except:
self.log_output("Could not parse module ID from output")
else:
self.checkIDLED.setStyleSheet("background-color: red;")
- self.hvOFFTestPB.setEnabled(False)
+ self.hvOFFTestPB.setEnabled(False)
self.hvONTestPB.setEnabled(False)
self.reset_test_pbs()
- self.handle_command_finished(success,stdout,stderr)
+ self.handle_command_finished(success, stdout, stderr)
if self.current_worker:
self.current_worker.finished.disconnect()
- self.current_worker = CommandWorker(self.expand_placeholders(self.checkIDCommandLE.text()))
+ self.current_worker = CommandWorker(
+ self.expand_placeholders(self.checkIDCommandLE.text())
+ )
self.current_worker.finished.connect(handle_check_id)
- #self.current_worker.finished.connect(self.reset_test_pbs)
- #self.current_worker.finished.connect(self.handle_command_finished)
+ # self.current_worker.finished.connect(self.reset_test_pbs)
+ # self.current_worker.finished.connect(self.handle_command_finished)
self.disable_test_pbs_enable_cancel()
self.cancelPB.clicked.connect(self.current_worker.terminate)
self.cancelPB.clicked.connect(self.reset_test_pbs)
@@ -1102,17 +1179,19 @@ def handle_check_id(success, stdout, stderr):
# self.waiting_dialog.setStandardButtons(QMessageBox.Cancel)
# self.waiting_dialog.buttonClicked.connect(lambda button: self.current_worker.terminate())
# self.waiting_dialog.show()
- #self.current_worker.finished.connect(self.waiting_dialog.accept)
+ # self.current_worker.finished.connect(self.waiting_dialog.accept)
self.current_worker.start()
def run_light_on_test(self):
"""Run Light On Test"""
self.log_output("=== Running Light On Test ===")
-
+
def handle_light_on(success, stdout, stderr):
if success:
- self.hvOFFTestLED.setStyleSheet("background-color: rgb(85, 170, 0);") # Green
+ self.hvOFFTestLED.setStyleSheet(
+ "background-color: rgb(85, 170, 0);"
+ ) # Green
self.hvOFFTestCB.setChecked(True)
self.checkIDPB.setEnabled(True)
self.hvOFFTestPB.setEnabled(True) # Enable test 2
@@ -1122,21 +1201,21 @@ def handle_light_on(success, stdout, stderr):
# try:
# # Update results label with the output
# noise_text = "
Noise:
"
-
+
# # Look for MPA and SSA noise values in the output
# mpa_noise = "0.0"
# ssa_noise = "0.0"
-
+
# for line in stdout.split('\n'):
# if "MPA noise:" in line:
# mpa_noise = line.split(":")[-1].strip()
# elif "SSA noise:" in line:
# ssa_noise = line.split(":")[-1].strip()
-
+
# noise_text += f"MPA: {mpa_noise}
"
# noise_text += f"SSA: {ssa_noise}
"
# noise_text += ""
-
+
# self.resultsLabel.setText(noise_text)
# except Exception as e:
# self.log_output(f"Error parsing test results: {e}")
@@ -1145,21 +1224,20 @@ def handle_light_on(success, stdout, stderr):
self.hvOFFTestCB.setChecked(False)
self.log_output(f"Light on test error: {stderr}")
self.reset_test_pbs()
- self.handle_command_finished(success,stdout,stderr)
-
-
-
+ self.handle_command_finished(success, stdout, stderr)
if self.current_worker:
self.current_worker.finished.disconnect()
- self.current_worker = CommandWorker(self.expand_placeholders(self.lightOnCommandLE.text()))
-# self.log_worker = CommandWorker("konsole -e tail -f /home/thermal/BurnIn_moduleTest/logs/Ph2_ACF.log")
+ self.current_worker = CommandWorker(
+ self.expand_placeholders(self.lightOnCommandLE.text())
+ )
+ # self.log_worker = CommandWorker("konsole -e tail -f /home/thermal/BurnIn_moduleTest/logs/Ph2_ACF.log")
self.current_worker.finished.connect(handle_light_on)
- # self.current_worker.finished.connect(self.reset_test_pbs)
-# self.current_worker.finished.connect(self.handle_command_finished)
+ # self.current_worker.finished.connect(self.reset_test_pbs)
+ # self.current_worker.finished.connect(self.handle_command_finished)
self.disable_test_pbs_enable_cancel()
self.cancelPB.clicked.connect(self.current_worker.terminate)
- self.cancelPB.clicked.connect(self.reset_test_pbs)
+ self.cancelPB.clicked.connect(self.reset_test_pbs)
# self.waiting_dialog = QMessageBox()
# self.waiting_dialog.setWindowTitle("Running Ph2_ACF")
# self.waiting_dialog.setText("test with lights on")
@@ -1169,74 +1247,90 @@ def handle_light_on(success, stdout, stderr):
# self.waiting_dialog.show()
# self.current_worker.finished.connect(self.waiting_dialog.accept)
-
self.current_worker.start()
-# self.log_worker.start()
+
+ # self.log_worker.start()
def run_dark_test(self):
"""Run Dark Test"""
self.log_output("=== Running Dark Test ===")
-
+
def handle_dark_test(success, stdout, stderr):
if success:
try:
- self.hvONTestLED.setStyleSheet("background-color: rgb(85, 170, 0);") # Green
+ self.hvONTestLED.setStyleSheet(
+ "background-color: rgb(85, 170, 0);"
+ ) # Green
self.hvONTestCB.setChecked(True)
self.checkIDPB.setEnabled(True)
self.hvOFFTestPB.setEnabled(True) # Enable test 2
self.hvONTestPB.setEnabled(True) # Enable test 3
- #{"message": "Entry created", "moduleTestAnalysisName": "Module_PS_26_IPG-10005_Run_run590_Result_Test7"}
- moduleTestAnalysisName=re.search(r".*moduleTestAnalysisName\": \"(.*?)\"}", stdout).group(1)
- #query DB from http://....:5000/module_test_analysis endpoint
- #get the result from the DB
- analysisData=self.make_api_request("module_test_analysis/"+moduleTestAnalysisName, "GET")
+ # {"message": "Entry created", "moduleTestAnalysisName": "Module_PS_26_IPG-10005_Run_run590_Result_Test7"}
+ moduleTestAnalysisName = re.search(
+ r".*moduleTestAnalysisName\": \"(.*?)\"}", stdout
+ ).group(1)
+ # query DB from http://....:5000/module_test_analysis endpoint
+ # get the result from the DB
+ analysisData = self.make_api_request(
+ "module_test_analysis/" + moduleTestAnalysisName, "GET"
+ )
if analysisData[0]:
print(analysisData[1])
- self.analysisURL=analysisData[1]["analysisFile"]
- res={}
+ self.analysisURL = analysisData[1]["analysisFile"]
+ res = {}
for k in analysisData[1]["analysisSummary"].keys():
print(k)
if "Average" in k:
- chip=k.split("_")[-1]
- hybrid=int(k.split("_")[-2][1:]) % 2
- res[(hybrid,chip)]=analysisData[1]["analysisSummary"][k]
-
+ chip = k.split("_")[-1]
+ hybrid = int(k.split("_")[-2][1:]) % 2
+ res[(hybrid, chip)] = analysisData[1][
+ "analysisSummary"
+ ][k]
+
noise_text = "Noise:
"
noise_text += ""
- noise_text += " | Hybrid 0 | Hybrid 1 |
"
- noise_text += "| SSA | %2.2f | %2.2f |
"%(res[(0,"SSA")],res[(1,"SSA")])
- noise_text += "| MPA | %2.2f | %2.2f |
"%(res[(0,"MPA")],res[(1,"MPA")])
+ noise_text += (
+ " | Hybrid 0 | Hybrid 1 |
"
+ )
+ noise_text += (
+ "| SSA | %2.2f | %2.2f |
"
+ % (res[(0, "SSA")], res[(1, "SSA")])
+ )
+ noise_text += (
+ "| MPA | %2.2f | %2.2f |
"
+ % (res[(0, "MPA")], res[(1, "MPA")])
+ )
noise_text += ""
print(noise_text)
self.resultsLabel.setText(noise_text)
- #ensure label is redrawn
+ # ensure label is redrawn
self.resultsLabel.repaint()
- self.resultsLabel.setToolTip(str(analysisData[1]["analysisSummary"]))
+ self.resultsLabel.setToolTip(
+ str(analysisData[1]["analysisSummary"])
+ )
except Exception as e:
self.log_output(f"Error parsing test results: {e}")
-# self.log_worker.terminate()
-
+ # self.log_worker.terminate()
+
else:
self.hvONTestLED.setStyleSheet("background-color: red;")
self.hvONTestCB.setChecked(False)
self.log_output(f"Dark test error: {stderr}")
self.reset_test_pbs()
- self.handle_command_finished(success,stdout,stderr)
+ self.handle_command_finished(success, stdout, stderr)
if self.current_worker:
self.current_worker.finished.disconnect()
- self.current_worker = CommandWorker(self.expand_placeholders(self.darkTestCommandLE.text()))
+ self.current_worker = CommandWorker(
+ self.expand_placeholders(self.darkTestCommandLE.text())
+ )
self.current_worker.finished.connect(handle_dark_test)
- # self.current_worker.finished.connect(self.reset_test_pbs)
- #self.current_worker.finished.connect(self.handle_command_finished)
+ # self.current_worker.finished.connect(self.reset_test_pbs)
+ # self.current_worker.finished.connect(self.handle_command_finished)
self.disable_test_pbs_enable_cancel()
self.cancelPB.clicked.connect(self.current_worker.terminate)
self.cancelPB.clicked.connect(self.reset_test_pbs)
-
-
-
-
# self.waiting_dialog = QMessageBox()
# self.waiting_dialog.setWindowTitle("Running Ph2_ACF")
# self.waiting_dialog.setText("test with modules in dark")
@@ -1245,24 +1339,25 @@ def handle_dark_test(success, stdout, stderr):
# self.waiting_dialog.buttonClicked.connect(lambda button: self.current_worker.terminate())
# self.waiting_dialog.show()
# self.current_worker.finished.connect(self.waiting_dialog.accept)
- #self.current_worker.finished.connect(self.log_worker.quit)
+ # self.current_worker.finished.connect(self.log_worker.quit)
self.current_worker.start()
+
def open_ph2acf_log(self):
- self.log_worker = CommandWorker("konsole -e tail -f /home/thermal/BurnIn_moduleTest/logs/Ph2_ACF.log")
+ self.log_worker = CommandWorker(
+ "konsole -e tail -f /home/thermal/BurnIn_moduleTest/logs/Ph2_ACF.log"
+ )
self.log_worker.start()
def disconnect_connection(self, cable_id, port):
"""Disconnect a cable's detSide connections"""
try:
- data = {
- "cable": cable_id,
- }
- self.make_api_request(
- endpoint="disconnect_all_detSide",
- method="POST",
- data=data
- )
+ data = {
+ "cable": cable_id,
+ }
+ self.make_api_request(
+ endpoint="disconnect_all_detSide", method="POST", data=data
+ )
except Exception as e:
self.log_output(f"Error disconnecting: {str(e)}")
@@ -1270,20 +1365,26 @@ def connect_power(self):
self.log_output("=== Connecting Power ===")
# First disconnect any existing connections
self.disconnect_connection(self.powerCB.currentText(), "power")
-
+
# Then make the new connection
data = {
"cable1": self.moduleLE.text(),
- "cable2": self.powerCB.currentText().split(";")[0] if ";" in self.powerCB.currentText() else self.powerCB.currentText(),
+ "cable2": (
+ self.powerCB.currentText().split(";")[0]
+ if ";" in self.powerCB.currentText()
+ else self.powerCB.currentText()
+ ),
"port1": "power",
- "port2": (""+self.powerCB.currentText().split(";")[1]) if ";" in self.powerCB.currentText() else 'power'
+ "port2": (
+ ("" + self.powerCB.currentText().split(";")[1])
+ if ";" in self.powerCB.currentText()
+ else "power"
+ ),
}
success, result = self.make_api_request(
- endpoint='connect', # Simplified endpoint
- method='POST',
- data=data
+ endpoint="connect", method="POST", data=data # Simplified endpoint
)
-
+
# Update connection status and LEDs
self.update_connection_status()
self.update_connection_leds()
@@ -1292,21 +1393,19 @@ def connect_fiber(self):
self.log_output("=== Connecting Fiber ===")
# First disconnect any existing connections
self.disconnect_connection(self.fiberCB.currentText(), "A")
-
+
# Then make the new connection
- fib=self.fiberCB.currentText()
+ fib = self.fiberCB.currentText()
data = {
"cable1": self.moduleLE.text(),
"cable2": fib.split(";")[0] if ";" in fib else fib,
"port1": "fiber",
- "port2": fib.split(";")[1] if ";" in fib else "A"
+ "port2": fib.split(";")[1] if ";" in fib else "A",
}
success, result = self.make_api_request(
- endpoint='connect', # Simplified endpoint
- method='POST',
- data=data
+ endpoint="connect", method="POST", data=data # Simplified endpoint
)
-
+
# Update connection status and LEDs
self.update_connection_status()
self.update_connection_leds()
@@ -1314,10 +1413,10 @@ def connect_fiber(self):
def apply_settings(self):
"""Apply current settings"""
self.log_output("=== Applying Settings ===")
-
+
# Save settings to file
self.save_settings()
-
+
# Reconnect MQTT
if self.setup_mqtt():
self.log_output("MQTT settings applied successfully")
@@ -1329,73 +1428,69 @@ def update_module_list(self):
self.module_db.update_module_list()
# Store full module list
self.all_modules = self.module_db.all_modules
-
- # Update mounted modules dict
+
+ # Update mounted modules dict
self.mounted_modules = {
- m.get("mounted_on", ""): m.get("moduleName", "")
- for m in self.all_modules
- if m.get("mounted_on")
+ m.get("mounted_on", ""): m.get("moduleName", "")
+ for m in self.all_modules
+ if m.get("mounted_on")
}
-
-
def disconnect_module(self, module_id):
"""Disconnect all connections for a module"""
try:
self.log_output(f"Disconnecting module: {module_id}")
data = {
- "cable": module_id,
- }
-
+ "cable": module_id,
+ }
+
success, result = self.make_api_request(
- endpoint="disconnect_all_crateSide",
- method="POST",
- data=data
- )
-
+ endpoint="disconnect_all_crateSide", method="POST", data=data
+ )
+
# Update the display
self.update_module_list()
self.update_connection_status()
-
+
except Exception as e:
self.log_output(f"Error disconnecting module: {str(e)}")
self.show_error_dialog(f"Failed to disconnect module: {str(e)}")
-
def module_changed(self):
self.load_module_details()
- module_id=self.moduleLE.text()
- query={"cable": module_id, "side": "crateSide"}
- response = requests.post(
- self.get_api_url('snapshot'),
- json=query
- )
+ module_id = self.moduleLE.text()
+ query = {"cable": module_id, "side": "crateSide"}
+ response = requests.post(self.get_api_url("snapshot"), json=query)
if response.status_code == 200:
snapshot = response.json()
- print("LOADED:",snapshot.get("1",None))
- print("LOADED:",snapshot.get("3",None))
- fiber=snapshot.get("1",None)
- power=snapshot.get("3",None)
- if fiber and len(fiber["connections"])>0:
- fib=fiber["connections"][0]["cable"]
- fports=fiber["connections"][0]["det_port"]
- fports=[x for x in fports if x != "A"]
- self.fiberCB.setCurrentText(fib+(f";{fports[0]}" if len(fports) >0 else '' ))
- print(fib+(f";{fports[0]}" if len(fports) >0 else '' ))
- if power and len(power["connections"])>0:
- pcable=power["connections"][0]["cable"]
- pports=power["connections"][0]["det_port"]
- pports=[x for x in pports if x != "power" and x!= "A"]
- self.powerCB.setCurrentText(pcable+(f";{pports[0]}" if len(pports) >0 else '' ))
- print(pcable+(f";{pports[0]}" if len(pports) >0 else '' ))
-
-#LOADED: {'crate_port': 'fiber', 'connections': [{'cable': 'SfibA', 'line': 1, 'det_port': ['A'], 'crate_port': ['67']}, {'cable': 'FC7OT8', 'line': 3, 'det_port': ['OG1'], 'crate_port': []}]}
-#LOADED: {'crate_port': 'power', 'connections': [{'cable': 'BINT1', 'line': 3, 'det_port': ['power'], 'crate_port': ['HV', 'HVLV']}, {'cable': 'P001', 'line': 12, 'det_port': ['A', 'B12'], 'crate_port': ['HV12']}, {'cable': 'H48', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT0', 'line': 12, 'det_port': ['12'], 'crate_port': []}]}
-# print("LOADED:",self.current_module_data["crateSide"])
- # fiber=self.current_module_data["crateSide"].get("2","")
- # power=self.current_module_data["crateSide"].get("3","")
- # print(str(fiber[0])+";"+str(fiber[1]//2),power[0]+";"+str(power[1]))
+ print("LOADED:", snapshot.get("1", None))
+ print("LOADED:", snapshot.get("3", None))
+ fiber = snapshot.get("1", None)
+ power = snapshot.get("3", None)
+ if fiber and len(fiber["connections"]) > 0:
+ fib = fiber["connections"][0]["cable"]
+ fports = fiber["connections"][0]["det_port"]
+ fports = [x for x in fports if x != "A"]
+ self.fiberCB.setCurrentText(
+ fib + (f";{fports[0]}" if len(fports) > 0 else "")
+ )
+ print(fib + (f";{fports[0]}" if len(fports) > 0 else ""))
+ if power and len(power["connections"]) > 0:
+ pcable = power["connections"][0]["cable"]
+ pports = power["connections"][0]["det_port"]
+ pports = [x for x in pports if x != "power" and x != "A"]
+ self.powerCB.setCurrentText(
+ pcable + (f";{pports[0]}" if len(pports) > 0 else "")
+ )
+ print(pcable + (f";{pports[0]}" if len(pports) > 0 else ""))
+
+ # LOADED: {'crate_port': 'fiber', 'connections': [{'cable': 'SfibA', 'line': 1, 'det_port': ['A'], 'crate_port': ['67']}, {'cable': 'FC7OT8', 'line': 3, 'det_port': ['OG1'], 'crate_port': []}]}
+ # LOADED: {'crate_port': 'power', 'connections': [{'cable': 'BINT1', 'line': 3, 'det_port': ['power'], 'crate_port': ['HV', 'HVLV']}, {'cable': 'P001', 'line': 12, 'det_port': ['A', 'B12'], 'crate_port': ['HV12']}, {'cable': 'H48', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT0', 'line': 12, 'det_port': ['12'], 'crate_port': []}]}
+ # print("LOADED:",self.current_module_data["crateSide"])
+ # fiber=self.current_module_data["crateSide"].get("2","")
+ # power=self.current_module_data["crateSide"].get("3","")
+ # print(str(fiber[0])+";"+str(fiber[1]//2),power[0]+";"+str(power[1]))
self.update_connection_status()
def update_connection_status(self):
@@ -1403,19 +1498,21 @@ def update_connection_status(self):
try:
power_id = self.powerCB.currentText()
fiber_id = self.fiberCB.currentText()
- power_id_slot=""
+ power_id_slot = ""
if ";" in power_id:
- power_id_slot= power_id.split(";")[1]
- power_id= power_id.split(";")[0]
- fiber_id_slot=""
+ power_id_slot = power_id.split(";")[1]
+ power_id = power_id.split(";")[0]
+ fiber_id_slot = ""
if ";" in fiber_id:
- fiber_id_slot= fiber_id.split(";")[1]
- fiber_id= fiber_id.split(";")[0]
+ fiber_id_slot = fiber_id.split(";")[1]
+ fiber_id = fiber_id.split(";")[0]
# Get power connections
power_status = set()
module_id = self.moduleLE.text()
if module_id:
- det_endpoint, crate_endpoints = self.get_power_endpoints_from_module(module_id)
+ det_endpoint, crate_endpoints = self.get_power_endpoints_from_module(
+ module_id
+ )
if crate_endpoints:
for crate_endpoint in crate_endpoints:
power_status.add(f"{module_id} <--> {crate_endpoint}")
@@ -1423,14 +1520,20 @@ def update_connection_status(self):
# Get fiber connections
fiber_status = set()
if fiber_id:
- det_endpoint, crate_endpoint = self.get_fiber_endpoints(fiber_id,fiber_id_slot)
+ det_endpoint, crate_endpoint = self.get_fiber_endpoints(
+ fiber_id, fiber_id_slot
+ )
if det_endpoint and crate_endpoint:
fiber_status.add(f"{det_endpoint} <--> {crate_endpoint}")
self.fiber_endpoint = crate_endpoint
# Update labels
- self.powerConnectionLabel.setText("\n".join(power_status) if power_status else "Not connected")
- self.fiberConnectionLabel.setText("\n".join(fiber_status) if fiber_status else "Not connected")
+ self.powerConnectionLabel.setText(
+ "\n".join(power_status) if power_status else "Not connected"
+ )
+ self.fiberConnectionLabel.setText(
+ "\n".join(fiber_status) if fiber_status else "Not connected"
+ )
# Update LED colors
self.update_connection_leds()
@@ -1451,36 +1554,44 @@ def update_connection_leds(self):
try:
power_id = self.powerCB.currentText()
fiber_id = self.fiberCB.currentText()
- power_id_slot=""
+ power_id_slot = ""
if ";" in power_id:
- power_id_slot= power_id.split(";")[1]
- power_id= power_id.split(";")[0]
- fiber_id_slot=""
+ power_id_slot = power_id.split(";")[1]
+ power_id = power_id.split(";")[0]
+ fiber_id_slot = ""
if ";" in fiber_id:
- fiber_id_slot= fiber_id.split(";")[1]
- fiber_id= fiber_id.split(";")[0]
+ fiber_id_slot = fiber_id.split(";")[1]
+ fiber_id = fiber_id.split(";")[0]
# Get power connection status
if power_id:
- det_endpoint, _ = self.get_power_endpoints(power_id,power_id_slot)
+ det_endpoint, _ = self.get_power_endpoints(power_id, power_id_slot)
if det_endpoint:
if self.check_connection_match(det_endpoint):
- self.connectPowerLED.setStyleSheet("background-color: rgb(85, 170, 0);") # Green
+ self.connectPowerLED.setStyleSheet(
+ "background-color: rgb(85, 170, 0);"
+ ) # Green
else:
- self.connectPowerLED.setStyleSheet("background-color: rgb(255, 255, 0);") # Yellow
+ self.connectPowerLED.setStyleSheet(
+ "background-color: rgb(255, 255, 0);"
+ ) # Yellow
else:
self.connectPowerLED.setStyleSheet("background-color: red;")
-
+
# Get fiber connection status
if fiber_id:
- det_endpoint, _ = self.get_fiber_endpoints(fiber_id,fiber_id_slot)
+ det_endpoint, _ = self.get_fiber_endpoints(fiber_id, fiber_id_slot)
if det_endpoint:
if self.check_connection_match(det_endpoint):
- self.connectFiberLED.setStyleSheet("background-color: rgb(85, 170, 0);") # Green
+ self.connectFiberLED.setStyleSheet(
+ "background-color: rgb(85, 170, 0);"
+ ) # Green
else:
- self.connectFiberLED.setStyleSheet("background-color: rgb(255, 255, 0);") # Yellow
+ self.connectFiberLED.setStyleSheet(
+ "background-color: rgb(255, 255, 0);"
+ ) # Yellow
else:
self.connectFiberLED.setStyleSheet("background-color: red;")
-
+
except Exception as e:
self.log_output(f"Error updating LEDs: {str(e)}")
@@ -1488,20 +1599,23 @@ def get_fiber_endpoints(self, fiber_id, fiber_id_slot):
"""Get both detSide and crateSide endpoints for a fiber connection"""
try:
# Get detSide path
- data={"cable": fiber_id, "side": "detSide"}
+ data = {"cable": fiber_id, "side": "detSide"}
det_response = requests.post(
- f"{self.dbEndpointLE.text()}/snapshot",
- json=data
+ f"{self.dbEndpointLE.text()}/snapshot", json=data
)
if det_response.status_code != 200:
- return None, None
+ return None, None
det_snapshot = det_response.json()
det_endpoint = None
- if fiber_id_slot != "" :
- #filter to get only the lines with det_port=fiber_id_slot
- det_snapshot = {k: v for k, v in det_snapshot.items() if v.get("det_port") == fiber_id_slot}
-
+ if fiber_id_slot != "":
+ # filter to get only the lines with det_port=fiber_id_slot
+ det_snapshot = {
+ k: v
+ for k, v in det_snapshot.items()
+ if v.get("det_port") == fiber_id_slot
+ }
+
for line in det_snapshot:
if det_snapshot[line]["connections"]:
# Get the last connection in the detSide path
@@ -1509,28 +1623,33 @@ def get_fiber_endpoints(self, fiber_id, fiber_id_slot):
break
# Get crateSide path from the module
- #f det_endpoint:
+ # f det_endpoint:
if True:
- data={"cable": fiber_id, "side": "crateSide"}
-
+ data = {"cable": fiber_id, "side": "crateSide"}
crate_response = requests.post(
- f"{self.dbEndpointLE.text()}/snapshot",
- json=data
+ f"{self.dbEndpointLE.text()}/snapshot", json=data
)
if crate_response.status_code == 200:
crate_snapshot = crate_response.json()
- print("Crate",crate_snapshot)
+ print("Crate", crate_snapshot)
# Crate {'1': {'crate_port': 'A', 'det_port': '1', 'connections': [{'cable': 'D3', 'line': 1, 'det_port': ['A'], 'crate_port': ['P12']}, {'cable': 'FC7OT5', 'line': 1, 'det_port': ['OG0'], 'crate_port': []}]}, '2': {'crate_port': 'A', 'det_port': '1', 'connections': [{'cable': 'D3', 'line': 2, 'det_port': ['A'], 'crate_port': ['P12']}, {'cable': 'FC7OT5', 'line': 2, 'det_port': ['OG0'], 'crate_port': []}]}, '3': {'crate_port': 'A', 'det_port': '2', 'connections': [{'cable': 'D3', 'line': 3, 'det_port': ['A'], 'crate_port': ['P34']}, {'cable': 'FC7OT5', 'line': 3, 'det_port': ['OG1'], 'crate_port': []}]}, '4': {'crate_port': 'A', 'det_port': '2', 'connections': [{'cable': 'D3', 'line': 4, 'det_port': ['A'], 'crate_port': ['P34']}, {'cable': 'FC7OT5', 'line': 4, 'det_port': ['OG1'], 'crate_port': []}]}, '5': {'crate_port': 'A', 'det_port': '3', 'connections': [{'cable': 'D3', 'line': 5, 'det_port': ['A'], 'crate_port': ['P56']}, {'cable': 'FC7OT5', 'line': 5, 'det_port': ['OG2'], 'crate_port': []}]}, '6': {'crate_port': 'A', 'det_port': '3', 'connections': [{'cable': 'D3', 'line': 6, 'det_port': ['A'], 'crate_port': ['P56']}, {'cable': 'FC7OT5', 'line': 6, 'det_port': ['OG2'], 'crate_port': []}]}, '7': {'crate_port': 'A', 'det_port': '4', 'connections': [{'cable': 'D3', 'line': 7, 'det_port': ['A'], 'crate_port': ['P78']}, {'cable': 'FC7OT5', 'line': 7, 'det_port': ['OG3'], 'crate_port': []}]}, '8': {'crate_port': 'A', 'det_port': '4', 'connections': [{'cable': 'D3', 'line': 8, 'det_port': ['A'], 'crate_port': ['P78']}, {'cable': 'FC7OT5', 'line': 8, 'det_port': ['OG3'], 'crate_port': []}]}, '9': {'crate_port': 'A', 'det_port': '5', 'connections': [{'cable': 'D3', 'line': 9, 'det_port': ['A'], 'crate_port': ['P910']}, {'cable': 'FC7OT5', 'line': 9, 'det_port': ['OG4'], 'crate_port': []}]}, '10': {'crate_port': 'A', 'det_port': '5', 'connections': [{'cable': 'D3', 'line': 10, 'det_port': ['A'], 'crate_port': ['P910']}, {'cable': 'FC7OT5', 'line': 10, 'det_port': ['OG4'], 'crate_port': []}]}, '11': {'crate_port': 'A', 'det_port': '6', 'connections': [{'cable': 'D3', 'line': 11, 'det_port': ['A'], 'crate_port': ['P1112']}, {'cable': 'FC7OT5', 'line': 11, 'det_port': ['OG5'], 'crate_port': []}]}, '12': {'crate_port': 'A', 'det_port': '6', 'connections': [{'cable': 'D3', 'line': 12, 'det_port': ['A'], 'crate_port': ['P1112']}, {'cable': 'FC7OT5', 'line': 12, 'det_port': ['OG5'], 'crate_port': []}]}}
- if fiber_id_slot != "" :
- #filter to get only the lines with det_port=fiber_id_slot
- crate_snapshot = {k: v for k, v in crate_snapshot.items() if v["det_port"]==fiber_id_slot}
-
+ if fiber_id_slot != "":
+ # filter to get only the lines with det_port=fiber_id_slot
+ crate_snapshot = {
+ k: v
+ for k, v in crate_snapshot.items()
+ if v["det_port"] == fiber_id_slot
+ }
+
for line in crate_snapshot.keys():
- if line in crate_snapshot and crate_snapshot[line]["connections"]:
+ if (
+ line in crate_snapshot
+ and crate_snapshot[line]["connections"]
+ ):
last_conn = crate_snapshot[line]["connections"][-1]
- ports = last_conn['crate_port'] + last_conn['det_port']
+ ports = last_conn["crate_port"] + last_conn["det_port"]
port = ports[0] if ports else "?"
return det_endpoint, f"{last_conn['cable']}_{port}"
@@ -1538,6 +1657,7 @@ def get_fiber_endpoints(self, fiber_id, fiber_id_slot):
except Exception as e:
self.log_output(f"Error getting fiber endpoints: {str(e)}")
return None, None
+
def get_power_endpoints_from_module(self, module_id):
"""Get the power endpoint from the module's crateSide connections"""
ret = (module_id, [])
@@ -1546,12 +1666,12 @@ def get_power_endpoints_from_module(self, module_id):
return ret
try:
response = requests.post(
- self.get_api_url('snapshot'),
- json={"cable": module_id, "side": "crateSide"}
+ self.get_api_url("snapshot"),
+ json={"cable": module_id, "side": "crateSide"},
)
if response.status_code == 200:
snapshot = response.json()
- print("json:",snapshot)
+ print("json:", snapshot)
for line in snapshot:
if snapshot[line]["connections"]:
last_conn = snapshot[line]["connections"][-1]
@@ -1559,89 +1679,112 @@ def get_power_endpoints_from_module(self, module_id):
# Get the last connection in the crateSide path
if "FC" in last_conn["cable"]:
continue
- #set LV/HV channels
- if "XSLOT" in last_conn['cable'] :
- self.caen.setLV("LV"+last_conn['cable'][5:]+f".{last_conn['line']}")
- if "ASLOT" in last_conn['cable'] :
- self.caen.setHV("HV"+last_conn['cable'][5:]+f".{last_conn['line']}")
-
+ # set LV/HV channels
+ if "XSLOT" in last_conn["cable"]:
+ self.caen.setLV(
+ "LV" + last_conn["cable"][5:] + f".{last_conn['line']}"
+ )
+ if "ASLOT" in last_conn["cable"]:
+ self.caen.setHV(
+ "HV" + last_conn["cable"][5:] + f".{last_conn['line']}"
+ )
+
ret[1].append(f"{last_conn['cable']}.{last_conn['line']}")
- print("ret",ret)
+ print("ret", ret)
return ret
except Exception as e:
self.log_output(f"Error getting power endpoint: {str(e)}")
return ret
- def get_power_endpoints(self, power_id,power_id_slot=""):
+ def get_power_endpoints(self, power_id, power_id_slot=""):
"""Get both detSide and crateSide endpoints for power connections"""
try:
# Get detSide path
- query={"cable": power_id, "side": "detSide"}
+ query = {"cable": power_id, "side": "detSide"}
- det_response = requests.post(
- self.get_api_url('snapshot'),
- json=query
- )
+ det_response = requests.post(self.get_api_url("snapshot"), json=query)
if det_response.status_code != 200:
return None, []
det_snapshot = det_response.json()
det_endpoint = None
- if power_id_slot != "" :
- #filter to get only the lines with det_port=power_id_slot
- if "B" == power_id_slot[0] :
- power_id_slot=power_id_slot[1:]
- det_snapshot = {k: v for k, v in det_snapshot.items() if v.get("crate_port") == "HV%s"%power_id_slot}
-
+ if power_id_slot != "":
+ # filter to get only the lines with det_port=power_id_slot
+ if "B" == power_id_slot[0]:
+ power_id_slot = power_id_slot[1:]
+ det_snapshot = {
+ k: v
+ for k, v in det_snapshot.items()
+ if v.get("crate_port") == "HV%s" % power_id_slot
+ }
+
for line in det_snapshot:
if det_snapshot[line]["connections"]:
# Get the last connection in the detSide path
det_endpoint = det_snapshot[line]["connections"][-1]["cable"]
break
- print("Det endpoint", det_endpoint)
+ print("Det endpoint", det_endpoint)
# Get crateSide path from the module
-# if det_endpoint:
+ # if det_endpoint:
if True:
crate_response = requests.post(
- self.get_api_url('snapshot'),
- json={"cable": power_id, "side": "crateSide"}
+ self.get_api_url("snapshot"),
+ json={"cable": power_id, "side": "crateSide"},
)
if crate_response.status_code == 200:
crate_snapshot = crate_response.json()
crate_endpoints = []
- #print("PCrate_pre",crate_snapshot)
-#PCrate_pre {'1': {'crate_port': 'HV1', 'det_port': 'A', 'connections': [{'cable': 'H36', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 1, 'det_port': ['1'], 'crate_port': []}]},
-# '2': {'crate_port': 'HV2', 'det_port': 'A', 'connections': [{'cable': 'H37', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 2, 'det_port': ['2'], 'crate_port': []}]},
-# '3': {'crate_port': 'HV3', 'det_port': 'A', 'connections': [{'cable': 'H38', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 3, 'det_port': ['3'], 'crate_port': []}]},
-# '4': {'crate_port': 'HV4', 'det_port': 'A', 'connections': [{'cable': 'H39', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 4, 'det_port': ['4'], 'crate_port': []}]},
-# '5': {'crate_port': 'HV5', 'det_port': 'A', 'connections': [{'cable': 'H40', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 5, 'det_port': ['5'], 'crate_port': []}]},
-# '6': {'crate_port': 'HV6', 'det_port': 'A', 'connections': [{'cable': 'H41', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 6, 'det_port': ['6'], 'crate_port': []}]},
-# '7': {'crate_port': 'HV7', 'det_port': 'A', 'connections': [{'cable': 'H42', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 7, 'det_port': ['7'], 'crate_port': []}]},
-# '8': {'crate_port': 'HV8', 'det_port': 'A', 'connections': [{'cable': 'H43', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 8, 'det_port': ['8'], 'crate_port': []}]},
-# '9': {'crate_port': 'HV9', 'det_port': 'A', 'connections': [{'cable': 'H44', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 9, 'det_port': ['9'], 'crate_port': []}]},
-# '10': {'crate_port': 'HV10', 'det_port': 'A', 'connections': [{'cable': 'H45', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 10, 'det_port': ['10'], 'crate_port': []}]},
-# '11': {'crate_port': 'HV11', 'det_port': 'A', 'connections': [{'cable': 'H46', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 11, 'det_port': ['11'], 'crate_port': []}]}, '12': {'crate_port': 'HV12', 'det_port': 'A', 'connections': [{'cable': 'H47', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 12, 'det_port': ['12'], 'crate_port': []}]},
-# '13': {'crate_port': 'LV1', 'det_port': 'A', 'connections': [{'cable': 'L5', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT7', 'line': 5, 'det_port': ['down'], 'crate_port': []}]}, '14': {'crate_port': 'LV1', 'det_port': 'A', 'connections': [{'cable': 'L5', 'line': 2, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT7', 'line': 6, 'det_port': ['down'], 'crate_port': []}]}, '15': {'crate_port': 'LV1', 'det_port': 'A', 'connections': [{'cable': 'L5', 'line': 3, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT7', 'line': 7, 'det_port': ['down'], 'crate_port': []}]},
-# '16': {'crate_port': 'LV1', 'det_port': 'A', 'connections': [{'cable': 'L5', 'line': 4, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT7', 'line': 8, 'det_port': ['down'], 'crate_port': []}]}, '17': {'crate_port': 'LV2', 'det_port': 'A', 'connections': [{'cable': 'L13', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 1, 'det_port': ['up'], 'crate_port': []}]}, '18': {'crate_port': 'LV2', 'det_port': 'A', 'connections': [{'cable': 'L13', 'line': 2, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 2, 'det_port': ['up'], 'crate_port': []}]}, '19': {'crate_port': 'LV2', 'det_port': 'A', 'connections': [{'cable': 'L13', 'line': 3, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 3, 'det_port': ['up'], 'crate_port': []}]}, '20': {'crate_port': 'LV2', 'det_port': 'A', 'connections': [{'cable': 'L13', 'line': 4, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 4, 'det_port': ['up'], 'crate_port': []}]}, '21': {'crate_port': 'LV3', 'det_port': 'A', 'connections': [{'cable': 'L9', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 5, 'det_port': ['down'], 'crate_port': []}]}, '22': {'crate_port': 'LV3', 'det_port': 'A', 'connections': [{'cable': 'L9', 'line': 2, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 6, 'det_port': ['down'], 'crate_port': []}]}, '23': {'crate_port': 'LV3', 'det_port': 'A', 'connections': [{'cable': 'L9', 'line': 3, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 7, 'det_port': ['down'], 'crate_port': []}]},
-# '24': {'crate_port': 'LV3', 'det_port': 'A', 'connections': [{'cable': 'L9', 'line': 4, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 8, 'det_port': ['down'], 'crate_port': []}]}}
-
- if power_id_slot != "" :
- crate_snapshot = {k: v for k, v in crate_snapshot.items() if v["crate_port"]=="HV%s"%power_id_slot or int(k) == int(power_id_slot)+12}
- # print("PCrate_post",crate_snapshot)
+ # print("PCrate_pre",crate_snapshot)
+ # PCrate_pre {'1': {'crate_port': 'HV1', 'det_port': 'A', 'connections': [{'cable': 'H36', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 1, 'det_port': ['1'], 'crate_port': []}]},
+ # '2': {'crate_port': 'HV2', 'det_port': 'A', 'connections': [{'cable': 'H37', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 2, 'det_port': ['2'], 'crate_port': []}]},
+ # '3': {'crate_port': 'HV3', 'det_port': 'A', 'connections': [{'cable': 'H38', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 3, 'det_port': ['3'], 'crate_port': []}]},
+ # '4': {'crate_port': 'HV4', 'det_port': 'A', 'connections': [{'cable': 'H39', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 4, 'det_port': ['4'], 'crate_port': []}]},
+ # '5': {'crate_port': 'HV5', 'det_port': 'A', 'connections': [{'cable': 'H40', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 5, 'det_port': ['5'], 'crate_port': []}]},
+ # '6': {'crate_port': 'HV6', 'det_port': 'A', 'connections': [{'cable': 'H41', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 6, 'det_port': ['6'], 'crate_port': []}]},
+ # '7': {'crate_port': 'HV7', 'det_port': 'A', 'connections': [{'cable': 'H42', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 7, 'det_port': ['7'], 'crate_port': []}]},
+ # '8': {'crate_port': 'HV8', 'det_port': 'A', 'connections': [{'cable': 'H43', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 8, 'det_port': ['8'], 'crate_port': []}]},
+ # '9': {'crate_port': 'HV9', 'det_port': 'A', 'connections': [{'cable': 'H44', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 9, 'det_port': ['9'], 'crate_port': []}]},
+ # '10': {'crate_port': 'HV10', 'det_port': 'A', 'connections': [{'cable': 'H45', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 10, 'det_port': ['10'], 'crate_port': []}]},
+ # '11': {'crate_port': 'HV11', 'det_port': 'A', 'connections': [{'cable': 'H46', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 11, 'det_port': ['11'], 'crate_port': []}]}, '12': {'crate_port': 'HV12', 'det_port': 'A', 'connections': [{'cable': 'H47', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'ASLOT2', 'line': 12, 'det_port': ['12'], 'crate_port': []}]},
+ # '13': {'crate_port': 'LV1', 'det_port': 'A', 'connections': [{'cable': 'L5', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT7', 'line': 5, 'det_port': ['down'], 'crate_port': []}]}, '14': {'crate_port': 'LV1', 'det_port': 'A', 'connections': [{'cable': 'L5', 'line': 2, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT7', 'line': 6, 'det_port': ['down'], 'crate_port': []}]}, '15': {'crate_port': 'LV1', 'det_port': 'A', 'connections': [{'cable': 'L5', 'line': 3, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT7', 'line': 7, 'det_port': ['down'], 'crate_port': []}]},
+ # '16': {'crate_port': 'LV1', 'det_port': 'A', 'connections': [{'cable': 'L5', 'line': 4, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT7', 'line': 8, 'det_port': ['down'], 'crate_port': []}]}, '17': {'crate_port': 'LV2', 'det_port': 'A', 'connections': [{'cable': 'L13', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 1, 'det_port': ['up'], 'crate_port': []}]}, '18': {'crate_port': 'LV2', 'det_port': 'A', 'connections': [{'cable': 'L13', 'line': 2, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 2, 'det_port': ['up'], 'crate_port': []}]}, '19': {'crate_port': 'LV2', 'det_port': 'A', 'connections': [{'cable': 'L13', 'line': 3, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 3, 'det_port': ['up'], 'crate_port': []}]}, '20': {'crate_port': 'LV2', 'det_port': 'A', 'connections': [{'cable': 'L13', 'line': 4, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 4, 'det_port': ['up'], 'crate_port': []}]}, '21': {'crate_port': 'LV3', 'det_port': 'A', 'connections': [{'cable': 'L9', 'line': 1, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 5, 'det_port': ['down'], 'crate_port': []}]}, '22': {'crate_port': 'LV3', 'det_port': 'A', 'connections': [{'cable': 'L9', 'line': 2, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 6, 'det_port': ['down'], 'crate_port': []}]}, '23': {'crate_port': 'LV3', 'det_port': 'A', 'connections': [{'cable': 'L9', 'line': 3, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 7, 'det_port': ['down'], 'crate_port': []}]},
+ # '24': {'crate_port': 'LV3', 'det_port': 'A', 'connections': [{'cable': 'L9', 'line': 4, 'det_port': ['A'], 'crate_port': ['A']}, {'cable': 'XSLOT8', 'line': 8, 'det_port': ['down'], 'crate_port': []}]}}
+
+ if power_id_slot != "":
+ crate_snapshot = {
+ k: v
+ for k, v in crate_snapshot.items()
+ if v["crate_port"] == "HV%s" % power_id_slot
+ or int(k) == int(power_id_slot) + 12
+ }
+ # print("PCrate_post",crate_snapshot)
# Look at power lines (3,4)
for line in crate_snapshot.keys():
print(crate_snapshot[line]["connections"])
- if line in crate_snapshot and crate_snapshot[line]["connections"]:
+ if (
+ line in crate_snapshot
+ and crate_snapshot[line]["connections"]
+ ):
last_conn = crate_snapshot[line]["connections"][-1]
- ports = last_conn['crate_port'] + last_conn['det_port']
+ ports = last_conn["crate_port"] + last_conn["det_port"]
port = ports[0] if ports else "?"
- #crate_endpoints.append(f"{last_conn['cable']}_{port}_{last_conn['line']}")
- crate_endpoints.append(f"{last_conn['cable']}.{last_conn['line']}")
- if "XSLOT" in last_conn['cable'] :
- self.caen.setLV("LV"+last_conn['cable'][5:]+f".{last_conn['line']}")
- if "ASLOT" in last_conn['cable'] :
- self.caen.setHV("HV"+last_conn['cable'][5:]+f".{last_conn['line']}")
+ # crate_endpoints.append(f"{last_conn['cable']}_{port}_{last_conn['line']}")
+ crate_endpoints.append(
+ f"{last_conn['cable']}.{last_conn['line']}"
+ )
+ if "XSLOT" in last_conn["cable"]:
+ self.caen.setLV(
+ "LV"
+ + last_conn["cable"][5:]
+ + f".{last_conn['line']}"
+ )
+ if "ASLOT" in last_conn["cable"]:
+ self.caen.setHV(
+ "HV"
+ + last_conn["cable"][5:]
+ + f".{last_conn['line']}"
+ )
return det_endpoint, crate_endpoints
@@ -1655,7 +1798,7 @@ def get_power_endpoints(self, power_id,power_id_slot=""):
# # Set up tree widget
# self.detailsTree.setHeaderLabels(['Field', 'Value'])
# self.detailsTree.setColumnWidth(0, 200)
-
+
# # Connect buttons
# self.editDetailsButton.clicked.connect(self.edit_selected_detail)
# self.saveDetailsButton.clicked.connect(self.save_module_details)
@@ -1665,7 +1808,7 @@ def get_power_endpoints(self, power_id,power_id_slot=""):
# if parent is None:
# self.detailsTree.clear()
# parent = self.detailsTree
-
+
# if isinstance(data, dict):
# for key, value in sorted(data.items()):
# item = QTreeWidgetItem([str(key), ''])
@@ -1694,18 +1837,18 @@ def get_power_endpoints(self, power_id,power_id_slot=""):
# dialog.setWindowTitle("Edit Value")
# dialog.setLabelText(f"Edit value for {item.text(0)}:")
# dialog.setTextValue(item.text(1))
-
+
# if dialog.exec_():
# item.setText(1, dialog.textValue())
def tree_to_dict(self, item):
"""Convert tree widget items back to dictionary recursively"""
result = {}
-
+
for i in range(item.childCount()):
child = item.child(i)
key = child.text(0)
-
+
if child.childCount() > 0:
# This is a non-leaf node, recurse
value = self.tree_to_dict(child)
@@ -1714,33 +1857,37 @@ def tree_to_dict(self, item):
value = child.text(1)
# Try to convert to appropriate type
try:
- if value.lower() == 'true':
+ if value.lower() == "true":
value = True
- elif value.lower() == 'false':
+ elif value.lower() == "false":
value = False
elif value.isdigit():
value = int(value)
- elif value.replace('.','',1).isdigit():
+ elif value.replace(".", "", 1).isdigit():
value = float(value)
except:
pass
-
+
# Handle nested paths using dot notation
- key_parts = key.split('.')
+ key_parts = key.split(".")
current_dict = result
for part in key_parts[:-1]:
if part not in current_dict:
current_dict[part] = {}
current_dict = current_dict[part]
current_dict[key_parts[-1]] = value
-
+
return result
def merge_dicts(self, dict1, dict2):
"""Recursively merge two dictionaries"""
result = dict1.copy()
for key, value in dict2.items():
- if key in result and isinstance(result[key], dict) and isinstance(value, dict):
+ if (
+ key in result
+ and isinstance(result[key], dict)
+ and isinstance(value, dict)
+ ):
result[key] = self.merge_dicts(result[key], value)
else:
result[key] = value
@@ -1754,20 +1901,20 @@ def merge_dicts(self, dict1, dict2):
# if response.status_code != 200:
# self.log_output(f"Error fetching module: {response.text}")
# return
-
+
# current_data = response.json()
-
+
# # Convert tree widget back to dictionary
# new_data = self.tree_to_dict(self.detailsTree.invisibleRootItem())
# print(new_data)
# print(current_data)
# # Recursively merge the data
# merged_data = self.merge_dicts(current_data, new_data)
-
+
# # Remove _id from merged data
# if "_id" in merged_data:
# del merged_data["_id"]
-
+
# # Make API request to update module
# print(merged_data)
# #return #debug check what it would do
@@ -1775,13 +1922,13 @@ def merge_dicts(self, dict1, dict2):
# self.get_api_url(f'modules/{self.current_module_id}'),
# json=merged_data
# )
-
+
# if response.status_code == 200:
# self.log_output("Module details updated successfully")
# self.update_module_list() # Refresh the module list to show updated data
# else:
# self.log_output(f"Error updating module: {response.text}")
-
+
# except Exception as e:
# self.log_output(f"Error saving module details: {str(e)}")
@@ -1791,7 +1938,7 @@ def view_module_details(self):
# if not selected_items:
# self.log_output("No module selected")
# return
-
+
# module_id = selected_items[0].text(0) # Get module name
# self.moduleLE.setText(module_id) # This will trigger load_module_details
# print("here")
@@ -1800,12 +1947,12 @@ def view_module_details(self):
def setup_inventory_buttons(self):
"""Setup buttons for inventory tab"""
buttonLayout = QHBoxLayout()
-
+
spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
buttonLayout.addItem(spacer)
-
+
# Add button layout to tab
-
+
# Connect the "View Details" button signal
self.viewDetailsPB.clicked.connect(self.view_module_details)
@@ -1815,20 +1962,22 @@ def reset_test_states(self):
self.checkIDLED.setStyleSheet("background-color: rgb(255, 255, 0);") # Yellow
self.hvOFFTestLED.setStyleSheet("background-color: rgb(255, 255, 0);")
self.hvONTestLED.setStyleSheet("background-color: rgb(255, 255, 0);")
-
+
# Reset checkboxes
self.hvOFFTestCB.setChecked(False)
self.hvONTestCB.setChecked(False)
-
+
# Enable tests 2 and 3
self.hvOFFTestPB.setEnabled(True)
self.hvONTestPB.setEnabled(True)
-
+
# Clear ID label
self.checkIDlabel.setText("ID:")
-
+
# Reset results label
- self.resultsLabel.setText("
Noise:
MPA: 0.0
SSA: 0.0
")
+ self.resultsLabel.setText(
+ " Noise:
MPA: 0.0
SSA: 0.0
"
+ )
def update_filters_from_layer(self, layer_type):
"""Update speed and spacer filters based on selected layer type"""
@@ -1836,7 +1985,7 @@ def update_filters_from_layer(self, layer_type):
self.speedCB.setCurrentText("any")
self.spacerCB.setCurrentText("any")
return
-
+
if layer_type in self.layers_to_filters:
filters = self.layers_to_filters[layer_type]
self.speedCB.setCurrentText(filters["speed"])
@@ -1846,7 +1995,7 @@ def update_layer_from_ring(self, ring_id):
"""Update layer type selection based on ring ID"""
if not ring_id:
return
-
+
# Find matching layer type
matching_layer = "any"
for layer in self.layers_to_filters:
@@ -1862,7 +2011,7 @@ def update_layer_from_ring(self, ring_id):
# Otherwise keep looking for a better match
elif matching_layer == "any":
matching_layer = layer
-
+
# Update layer type combo box
self.module_db.ui.layertypeCB.setCurrentText(matching_layer)
@@ -1871,7 +2020,7 @@ def mount_module(self):
module_id = self.moduleLE.text()
ring_id = self.ringLE.text()
position = self.positionLE.text()
-
+
if not all([module_id, ring_id, position]):
self.show_error_dialog("Please specify module ID, ring ID and position")
return
@@ -1884,19 +2033,17 @@ def mount_module(self):
return
# Check if position is valid for the layer
- max_positions = {
- "L1": 18,
- "L2": 26,
- "L3": 36
- }
- layer = next((key for key in max_positions.keys() if ring_id.startswith(key)), None)
+ max_positions = {"L1": 18, "L2": 26, "L3": 36}
+ layer = next(
+ (key for key in max_positions.keys() if ring_id.startswith(key)), None
+ )
if not layer:
self.show_error_dialog(
f"Invalid ring ID format: {ring_id}\n\n"
f"Ring ID must start with one of: L1, L2, L3"
)
return
-
+
if position_num < 1 or position_num > max_positions[layer]:
self.show_error_dialog(
f"Invalid position for {layer}\n\n"
@@ -1906,12 +2053,11 @@ def mount_module(self):
return
# Check if module is already mounted somewhere
- success, modules = self.make_api_request(
- endpoint='modules',
- method='GET'
- )
+ success, modules = self.make_api_request(endpoint="modules", method="GET")
if not success:
- self.show_error_dialog("Failed to check module mounting status.\nPlease check your database connection.")
+ self.show_error_dialog(
+ "Failed to check module mounting status.\nPlease check your database connection."
+ )
return
for module in modules:
@@ -1923,7 +2069,7 @@ def mount_module(self):
f"Please unmount it first if you want to move it."
)
return
-
+
# Check if there's already a module in the target position
if module.get("mounted_on") == f"{ring_id};{position}":
self.show_error_dialog(
@@ -1935,28 +2081,26 @@ def mount_module(self):
# If we get here, all validations passed
# First get current module data
- response = requests.get(self.get_api_url(f'modules/{module_id}'))
+ response = requests.get(self.get_api_url(f"modules/{module_id}"))
if response.status_code != 200:
self.show_error_dialog(f"Error fetching module data: {response.text}")
return
-
+
module_data = response.json()
mounted_on = f"{ring_id};{position}"
-
+
# Update only the relevant fields while preserving others
module_data["mounted_on"] = mounted_on
module_data["status"] = "MOUNTED"
module_data["position"] = "OnDet@ " + mounted_on
- # Remove _id from mounted_on
+ # Remove _id from mounted_on
if "_id" in module_data:
del module_data["_id"]
-
+
success, result = self.make_api_request(
- endpoint=f'modules/{module_id}',
- method='PUT',
- data=module_data
+ endpoint=f"modules/{module_id}", method="PUT", data=module_data
)
-
+
if success:
self.log_output(f"Module {module_id} mounted at {mounted_on}")
self.unmountPB.setEnabled(True)
@@ -1969,31 +2113,29 @@ def unmount_module(self):
"""Unmount the currently selected module"""
module_id = self.moduleLE.text()
print("UNMOUNTING", module_id)
-
+
if not module_id:
self.log_output("No module selected")
return
-
+
# First get current module data
- response = requests.get(self.get_api_url(f'modules/{module_id}'))
+ response = requests.get(self.get_api_url(f"modules/{module_id}"))
if response.status_code != 200:
self.log_output(f"Error fetching module: {response.text}")
return
-
+
module_data = response.json()
-
+
# Update only the relevant fields while preserving others
module_data["mounted_on"] = ""
module_data["status"] = "un-mounted"
- if "_id" in module_data :
+ if "_id" in module_data:
del module_data["_id"]
-
+
success, result = self.make_api_request(
- endpoint=f'modules/{module_id}',
- method='PUT',
- data=module_data
+ endpoint=f"modules/{module_id}", method="PUT", data=module_data
)
-
+
if success:
self.log_output(f"Module {module_id} unmounted")
self.unmountPB.setEnabled(False)
@@ -2005,13 +2147,15 @@ def unmount_module(self):
def check_module_mounting_status(self):
"""Check if the current module ID is mounted and update unmount button state"""
module_id = self.moduleLE.text()
-
+
if not module_id:
self.unmountPB.setEnabled(False)
return
-
+
# Check if this module ID exists in mounted_modules values
- is_mounted = any(module_id == mounted_id for mounted_id in self.mounted_modules.values())
+ is_mounted = any(
+ module_id == mounted_id for mounted_id in self.mounted_modules.values()
+ )
self.unmountPB.setEnabled(is_mounted)
def load_module_details(self):
@@ -2020,25 +2164,27 @@ def load_module_details(self):
if not module_id:
self.module_db.ui.moduleNameLabel.setText("")
return
-
+
try:
- response = requests.get(self.get_api_url(f'modules/{module_id}'))
+ response = requests.get(self.get_api_url(f"modules/{module_id}"))
if response.status_code == 200:
module_data = response.json()
- self.current_module_data=module_data
+ self.current_module_data = module_data
self.current_module_id = module_id
-
+
# Fetch Fuse ID
self.current_fuse_id = get_module_fuse_id(module_data)
- self.log_output(f"Module {module_id} loaded. Fuse ID: {self.current_fuse_id}")
-
+ self.log_output(
+ f"Module {module_id} loaded. Fuse ID: {self.current_fuse_id}"
+ )
+
# Reset Ph2ACF plots for new module
self.ph2acf_time = []
self.ssa_mean_values = []
self.ssa_max_values = []
self.mpa_mean_values = []
self.mpa_max_values = []
-
+
self.module_db.ui.moduleNameLabel.setText(module_id)
self.module_db.populate_details_tree(module_data)
else:
@@ -2049,11 +2195,12 @@ def load_module_details(self):
def control_air(self, turn_on):
"""Control air system"""
self.air_state = turn_on
-# command = self.airCommandLE.text().format(airOn="1" if turn_on else "0")
+ # command = self.airCommandLE.text().format(airOn="1" if turn_on else "0")
command = self.airCommandLE.text().format(airOn="on" if turn_on else "off")
-
+
def handle_air_result(success, stdout, stderr):
pass
+
if self.current_worker:
self.current_worker.finished.disconnect()
self.current_worker = CommandWorker(command)
@@ -2064,19 +2211,19 @@ def show_test_results(self):
"""Show test results in plots tab"""
# Switch to plots tab
self.tabWidget.setCurrentWidget(self.tab_5)
-
+
# Load URL from settings and normalize path
url = self.resultsUrlLE.text()
- if url.startswith('file:'):
+ if url.startswith("file:"):
# Remove file: prefix if present and make absolute path
- url = url.replace('file:', '', 1)
+ url = url.replace("file:", "", 1)
url = os.path.abspath(url)
- url = f'file://{url}'
- elif not url.startswith('http://') and not url.startswith('https://'):
+ url = f"file://{url}"
+ elif not url.startswith("http://") and not url.startswith("https://"):
# If no protocol specified, assume local file
url = os.path.abspath(url)
- url = f'file://{url}'
-
+ url = f"file://{url}"
+
# Try to load the content into text browser
try:
self.resultsBrowser.setSource(QUrl(url))
@@ -2093,15 +2240,15 @@ def show_test_results(self):
def open_results_in_browser(self):
"""Open test results in system default browser"""
-# self.analysisUrl
-# url = self.resultsUrlLE.text()
-# if url.startswith('file:'):
-# url = url.replace('file:', '', 1)
-# url = os.path.abspath(url)
-# url = f'file://{url}'
-# elif not url.startswith('http://') and not url.startswith('https://'):
-# url = os.path.abspath(url)
-# url = f'file://{url}'
+ # self.analysisUrl
+ # url = self.resultsUrlLE.text()
+ # if url.startswith('file:'):
+ # url = url.replace('file:', '', 1)
+ # url = os.path.abspath(url)
+ # url = f'file://{url}'
+ # elif not url.startswith('http://') and not url.startswith('https://'):
+ # url = os.path.abspath(url)
+ # url = f'file://{url}'
webbrowser.open(self.analysisURL)
def on_module_selected(self, module_id):
@@ -2109,6 +2256,7 @@ def on_module_selected(self, module_id):
self.moduleLE.setText(module_id)
self.tabWidget.setCurrentIndex(0) # Switch to first tab
+
def main():
app = QApplication(sys.argv)
window = QMainWindow()
@@ -2116,7 +2264,8 @@ def main():
window.show()
sys.exit(app.exec_())
-if __name__ == '__main__':
+
+if __name__ == "__main__":
import sys
main()
diff --git a/issues.md b/issues.md
new file mode 100644
index 0000000..660da30
--- /dev/null
+++ b/issues.md
@@ -0,0 +1,404 @@
+# Bug & Issue Report
+> Auto-generated by code review — branch `fix-safely-and-coldroom-power-supply`
+
+---
+
+## CRITICAL (will crash or silently do nothing at runtime)
+
+### BUG-01 · `self.caenGUI` does not exist — safety interlock is broken
+**File:** [integration.py:792-795](integration.py#L792-L795)
+
+```python
+if self.max_temperature > 45:
+ self.caenGUI.safe_lv_off() # ← AttributeError
+if self.max_temperature > 50:
+ self.caenGUI.off(...) # ← AttributeError
+```
+The CAEN object is stored as `self.caen` (set at line 247), not `self.caenGUI`. When the thermal camera temperature exceeds 45 °C the safety interlock fires and immediately crashes. **The safety shutdown never executes.**
+
+**Fix:** replace `self.caenGUI` with `self.caen` on both lines.
+
+---
+
+### BUG-02 · `new_session()` crashes if the API call fails
+**File:** [integration.py:418-420](integration.py#L418-L420)
+
+```python
+success, result = self.make_api_request("sessions", "POST", session)
+self.current_session = result["sessionName"] # crashes when result is None or a str
+```
+`make_api_request` returns `(False, None)` or `(False, str(e))` on failure, but the next line assumes `result` is always a dict.
+
+**Fix:** check `success` before accessing `result["sessionName"]`.
+
+---
+
+### BUG-03 · `cleanup()` starts the thermal camera loop instead of stopping it
+**File:** [coldroom/system.py:213](coldroom/system.py#L213)
+
+```python
+self._thermalcamera.loop_start() # BUG: should be loop_stop()
+```
+During application shutdown, calling `loop_start()` re-starts the background MQTT thread instead of stopping it, leaking threads and connections.
+
+**Fix:** change to `self._thermalcamera.loop_stop()`.
+
+---
+
+### BUG-04 · `terminate_process()` calls `.poll()` on a `CompletedProcess` object
+**File:** [coldroom/command_worker.py:79](coldroom/command_worker.py#L79)
+
+```python
+self.process = subprocess.run(...) # returns CompletedProcess
+...
+if self.process and self.process.poll() is None: # AttributeError: poll() doesn't exist
+```
+`subprocess.run()` returns a `CompletedProcess`; only `Popen` has `.poll()`. The Cancel button will always raise `AttributeError`. The commented-out `Popen` code was the original intent.
+
+**Fix:** switch to `subprocess.Popen(...)` so the process can be killed while running, or remove the `terminate_process` body since `subprocess.run` is already blocking.
+
+---
+
+### BUG-05 · `power_supply/power_supply_ctrl.py` `main()` uses `QMainWindow` before it is imported
+**File:** [power_supply/power_supply_ctrl.py:204](power_supply/power_supply_ctrl.py#L204)
+
+```python
+def main():
+ ...
+ window = QMainWindow() # NameError — QMainWindow imported only inside __main__ guard
+```
+`from PyQt5.QtWidgets import QMainWindow` is at line 214, inside `if __name__ == "__main__":`. Calling `main()` from another module raises `NameError`.
+
+**Fix:** move the `QMainWindow` import to the top of the file.
+
+---
+
+### BUG-06 · `power_supply/rigolDP116A.py` connects to hardware at import time
+**File:** [power_supply/rigolDP116A.py:8-10](power_supply/rigolDP116A.py#L8-L10)
+
+```python
+rm = visa.ResourceManager()
+power_supply = rm.open_resource(f"TCPIP0::{ip_address}::INSTR")
+```
+These lines run the moment the file is imported. If the power supply is unreachable, importing this module crashes the calling application.
+
+**Fix:** wrap connection logic in a `connect()` function or class and call it explicitly.
+
+---
+
+### BUG-07 · `update_stitched_images` accesses `camera_data[pos]["image"]` but the value is a list
+**File:** [coldroom/module_temperatures_gui.py:1103](coldroom/module_temperatures_gui.py#L1103)
+
+```python
+images = [camera_data[pos]["image"] for pos in positions] # TypeError: list has no key "image"
+```
+`thermal_camera.py` stores raw numpy arrays in a list per position (`.append(processed_image)`), not dicts. This method is dead but would crash if called.
+
+---
+
+### BUG-08 · `caenGUI.py` `main()` references undefined class `caenGUI`
+**File:** [caen/caenGUI.py:235](caen/caenGUI.py#L235)
+
+```python
+def main():
+ ex = caenGUI() # NameError — class was renamed to CAENControl
+```
+The standalone test entry-point is broken; calling it raises `NameError`.
+
+---
+
+### BUG-09 · `disconnect_module` in ModuleDB wired without argument
+**File:** [db/module_db.py:352](db/module_db.py#L352)
+
+```python
+disconnect_button.clicked.connect(self.disconnect_module)
+# disconnect_module(self, m) requires argument m
+```
+The `clicked` signal passes a `bool` (checked state), not a module ID. Clicking the Disconnect button in the Details tree will raise `TypeError`.
+
+**Fix:** use `lambda: self.disconnect_module(self.current_module_id)`.
+
+---
+
+## HIGH (safety-logic errors, silent wrong results)
+
+### BUG-10 · Safety functions return a tuple on error — tuples are truthy
+**File:** [coldroom/safety.py:76, 88, 98](coldroom/safety.py#L76)
+
+```python
+def check_dew_point(...):
+ ...
+ except Exception as e:
+ return False, "Error checking dew point safety" # returns (False, str) — truthy!
+```
+`check_door_status` and `check_light_status` have the same pattern. Code that does `if not check_dew_point(status):` evaluates a non-empty tuple as `True`, i.e., "safe", which is the **opposite** of the intended conservative behaviour.
+
+**Fix:** return only `False` (or raise) on exceptions; never a tuple.
+
+---
+
+### BUG-11 · `check_door_status` checks wrong key then reads a different key
+**File:** [coldroom/safety.py:82-86](coldroom/safety.py#L82-L86)
+
+```python
+if "coldroom" not in system_status or "door_status" not in system_status["coldroom"]:
+ return False
+return system_status["coldroom"]["CmdDoorUnlock_Reff"] == 1 # KeyError if not present
+```
+Guards check `"door_status"` but then reads `"CmdDoorUnlock_Reff"`. If `"door_status"` exists but `"CmdDoorUnlock_Reff"` does not, raises `KeyError`.
+
+---
+
+### BUG-12 · `check_door_safe_to_open` accesses `cleanroom["elapsed_time"]` key that does not exist
+**File:** [coldroom/safety.py:139](coldroom/safety.py#L139)
+
+```python
+clean_room_expired = check_cleanroom_expired(system_status["cleanroom"]["elapsed_time"])
+```
+`MartaColdRoomMQTTClient` stores `last_update` (a datetime string) in cleanroom status, not `elapsed_time`. This always raises `KeyError`.
+
+---
+
+### BUG-13 · Soft interlock **never actually shuts off LV** — actions are commented out
+**File:** [coldroom/safety.py:435, 445](coldroom/safety.py#L435-L445)
+
+```python
+# switch_all_lv_off(caen, used_channels) ← commented out in both branches
+```
+The safety interlock loop only publishes alarm messages. The actual protective action (turning off LV) is disabled.
+
+---
+
+### BUG-14 · MQTT callbacks update GUI widgets from a background thread
+**File:** [integration.py:629, 637, 756-758, 790, 808-809](integration.py#L629)
+
+`on_mqtt_message` is called from paho-mqtt's network thread and directly calls `self.airLed.setStyleSheet(...)`, `self.canvas.draw()`, `self.tMaxLabel.setText(...)`, etc. PyQt5 is **not thread-safe**: GUI updates from non-main threads cause random crashes and visual corruption.
+
+**Fix:** emit signals from `on_mqtt_message` and process them in slots on the main thread (the `LogEmitter` pattern already used in this file is the right model).
+
+---
+
+### BUG-15 · `MartaColdRoomMQTTClient` starts its loop twice
+**File:** [coldroom/marta_coldroom.py:69](coldroom/marta_coldroom.py#L69) and [coldroom/system.py:170](coldroom/system.py#L170)
+
+`__init__` calls `self.start_client_loops()` at line 69, then `System._mqtt_loop()` calls `self._martacoldroom.start_client_loops()` again at line 170. Calling `loop_start()` twice on a paho client creates a second network thread.
+
+---
+
+## MEDIUM (functional bugs, bad fallbacks)
+
+### BUG-16 · `cancelPB` accumulates multiple signal connections
+**File:** [integration.py:1172-1173, 1239-1240, 1331-1332](integration.py#L1172)
+
+```python
+self.cancelPB.clicked.connect(self.current_worker.terminate)
+self.cancelPB.clicked.connect(self.reset_test_pbs)
+```
+Each call to `run_check_id()`, `run_light_on_test()`, or `run_dark_test()` adds new connections without disconnecting old ones. Clicking Cancel after 3 tests fires 6 slots, trying to terminate already-finished workers.
+
+**Fix:** call `self.cancelPB.clicked.disconnect()` before re-connecting, or use a single persistent slot.
+
+---
+
+### BUG-17 · Blocking HTTP requests on the main (GUI) thread
+**Files:** [integration.py:1464, 1603, 1668, 1705, 2056, 2084, 2122, 2169](integration.py#L1464); [db/module_db.py:197](db/module_db.py#L197)
+
+Multiple methods (`module_changed`, `update_connection_status`, `mount_module`, `load_module_details`, `update_module_list`) call `requests.get/post` directly on the main thread. Any network delay freezes the entire GUI.
+
+**Fix:** run all API calls in a `QThread` (use the existing `CommandWorker` pattern) and update the GUI via signals.
+
+---
+
+### BUG-18 · `struct` and `numpy` imported twice
+**File:** [integration.py:31-33 and 46-47](integration.py#L31)
+
+```python
+import struct
+import numpy as np
+...
+import struct # duplicate
+import numpy as np # duplicate
+```
+Not a crash, but dead code.
+
+---
+
+### BUG-19 · `logger.debug("Stopping", payload)` passes extra positional arg
+**File:** [coldroom/marta_coldroom.py:367](coldroom/marta_coldroom.py#L367)
+
+```python
+logger.debug("Stopping", payload)
+```
+Python's `logging` API doesn't accept a second positional non-format argument here; it silently ignores `payload` in older Python but may warn. Should be `logger.debug(f"Stopping {payload}")`.
+
+---
+
+### BUG-20 · `from tkinter import NO, YES, YES` in safety.py — wrong library, unused import
+**File:** [coldroom/safety.py:2](coldroom/safety.py#L2)
+
+tkinter is not used anywhere in `safety.py`. This imports the tk event loop in a PyQt5 app and `YES` is imported twice. Dead import.
+
+---
+
+### BUG-21 · `CAENQueryThread.start()` called every 2 s without checking if previous query finished
+**File:** [caen/caenGUI.py:177-178](caen/caenGUI.py#L177)
+
+```python
+def update(self):
+ self.queryThread.setup_query("GetStatus,...", True)
+ self.queryThread.start() # ← called even if thread is already running
+```
+`QThread.start()` on a running thread does nothing but logs a Qt warning. If a CAEN query takes longer than 2 s the thread queue grows and responses pile up, causing stale or duplicate updates.
+
+---
+
+### BUG-22 · `filter_modules` writes to columns 6 and 8 — likely out-of-range
+**File:** [db/module_db.py:295, 331](db/module_db.py#L295)
+
+```python
+item.setText(6, module.get("details", {}).get("DESCRIPTION", ""))
+item.setText(8, module.get("mounted_on", ""))
+```
+`QTreeWidget.setText` silently does nothing if the column index exceeds the column count. Unless the widget has ≥9 columns these values are discarded and never shown.
+
+---
+
+### BUG-23 · `get_module_fuse_id` always overwrites `hwId` with `children.lpGBT` value
+**File:** [db/utils.py:226-228](db/utils.py#L226)
+
+```python
+if "hwId" in module:
+ fuseId = int(module["hwId"])
+if "children" in module and "lpGBT" in module["children"]: # second if, not elif
+ fuseId = int(module["children"]["lpGBT"]["CHILD_SERIAL_NUMBER"])
+```
+Both are plain `if`, so the second silently overwrites the first. If `CHILD_SERIAL_NUMBER` is missing or non-numeric, raises `KeyError`/`ValueError`.
+
+---
+
+### BUG-24 · `if True:` dead conditional masking commented-out logic
+**File:** [integration.py:1627, 1730](integration.py#L1627)
+
+```python
+# f det_endpoint:
+if True:
+```
+This is a commented-out guard replaced by `if True:`, bypassing intended conditional behaviour and making control flow hard to follow.
+
+---
+
+### BUG-25 · Camera naming mismatch: `camera0-3` vs `camera1-4`
+**File:** [coldroom/thermal_camera.py:23](coldroom/thermal_camera.py#L23) vs [coldroom/module_temperatures_gui.py:407-410](coldroom/module_temperatures_gui.py#L407)
+
+`thermal_camera.py` initialises images as `camera0`–`camera3`; `module_temperatures_gui.py` expects `camera1`–`camera4`. Conversion (`int(camera_name[-1]) + 1`) is scattered through the code and fragile (breaks if camera names ever use two-digit suffixes).
+
+---
+
+### BUG-26 · `update_temperature_table` crashes if `mounted_on` has no `;`
+**File:** [coldroom/module_temperatures_gui.py:1418](coldroom/module_temperatures_gui.py#L1418)
+
+```python
+column_index = int(mounted_module_info.get("mounted_on", "-").split(";")[1])
+```
+If `mounted_on` is empty or `"-"`, `split(";")` returns a one-element list and `[1]` raises `IndexError`.
+
+---
+
+## LOW (code quality, minor functional issues)
+
+### BUG-27 · `import sys` missing at module level in `integration.py`
+**File:** [integration.py:2264](integration.py#L2264)
+
+`main()` calls `sys.exit(app.exec_())`. `sys` is only imported inside the `if __name__ == "__main__":` guard (line 2269). Works only when run directly, fails when `main()` is imported and called.
+
+**Fix:** move `import sys` to the top-level imports.
+
+---
+
+### BUG-28 · `open_ph2acf_log` hardcodes `konsole` terminal and an absolute path
+**File:** [integration.py:1347-1349](integration.py#L1347)
+
+```python
+CommandWorker("konsole -e tail -f /home/thermal/BurnIn_moduleTest/logs/Ph2_ACF.log")
+```
+Assumes the KDE `konsole` terminal is installed and the log path is fixed. Silently does nothing on GNOME systems or if the path doesn't exist.
+
+---
+
+### BUG-29 · IP address hardcoded in `power_supply_ctrl.py`
+**File:** [power_supply/power_supply_ctrl.py:66](power_supply/power_supply_ctrl.py#L66)
+
+```python
+ip_address = "192.168.0.16"
+```
+Not read from settings; requires source-code edit to change the instrument address.
+
+---
+
+### BUG-30 · `handle_ring_click` is re-assigned as a monkey-patch every redraw
+**File:** [integration.py:943](integration.py#L943)
+
+```python
+self.graphicsView.mousePressEvent = self.handle_ring_click
+```
+Called inside `draw_ring()` which runs on every UI change. Reassigning instance methods is fragile; use `QGraphicsView.mousePressEvent` subclass override or a proper event filter instead.
+
+---
+
+### BUG-31 · No automatic MQTT reconnection in main integration window
+**File:** [integration.py:611-619](integration.py#L611)
+
+`on_mqtt_disconnect` logs the event but the paho client's built-in reconnect (`reconnect_delay_set` + `loop_start`) is never configured. If the broker drops, data stops silently.
+
+---
+
+### BUG-32 · `tcp_util.__del__` swallows all socket exceptions silently
+**File:** [caen/caenGUI.py:92-95](caen/caenGUI.py#L92)
+
+```python
+def __del__(self):
+ try:
+ self.closeSocket()
+ except: # bare except hides errors
+ pass
+```
+Bare `except` masks all exceptions including `NameError`, `AttributeError`, and socket errors that might be useful for debugging.
+
+---
+
+## Summary Table
+
+| ID | Severity | File | Description |
+|----|----------|------|-------------|
+| BUG-01 | CRITICAL | integration.py:792 | `self.caenGUI` → should be `self.caen`; safety shutdown broken |
+| BUG-02 | CRITICAL | integration.py:418 | `new_session()` crashes on API failure |
+| BUG-03 | CRITICAL | coldroom/system.py:213 | `loop_start()` instead of `loop_stop()` in cleanup |
+| BUG-04 | CRITICAL | coldroom/command_worker.py:79 | `poll()` on `CompletedProcess` → Cancel button crashes |
+| BUG-05 | CRITICAL | power_supply/power_supply_ctrl.py:204 | `QMainWindow` imported after use |
+| BUG-06 | CRITICAL | power_supply/rigolDP116A.py:8 | Hardware connection at import time |
+| BUG-07 | CRITICAL | coldroom/module_temperatures_gui.py:1103 | Wrong data structure access (`list` vs `dict`) |
+| BUG-08 | CRITICAL | caen/caenGUI.py:235 | `main()` references undefined class `caenGUI` |
+| BUG-09 | CRITICAL | db/module_db.py:352 | `disconnect_module` wired without required argument |
+| BUG-10 | HIGH | coldroom/safety.py:76 | Safety functions return truthy tuple on error |
+| BUG-11 | HIGH | coldroom/safety.py:82 | Wrong key checked vs key read in door status |
+| BUG-12 | HIGH | coldroom/safety.py:139 | `elapsed_time` key doesn't exist → KeyError |
+| BUG-13 | HIGH | coldroom/safety.py:435 | Actual LV-off action is commented out |
+| BUG-14 | HIGH | integration.py:629 | GUI updates from MQTT background thread |
+| BUG-15 | HIGH | coldroom/marta_coldroom.py:69 | MQTT loop started twice |
+| BUG-16 | MEDIUM | integration.py:1172 | Cancel button accumulates signal connections |
+| BUG-17 | MEDIUM | integration.py:1464 | Blocking HTTP on GUI thread freezes UI |
+| BUG-18 | MEDIUM | integration.py:31,46 | `struct`/`numpy` imported twice |
+| BUG-19 | MEDIUM | coldroom/marta_coldroom.py:367 | `logger.debug` extra positional arg |
+| BUG-20 | MEDIUM | coldroom/safety.py:2 | `tkinter` imported in PyQt5 app, unused |
+| BUG-21 | MEDIUM | caen/caenGUI.py:177 | CAEN query thread started while possibly running |
+| BUG-22 | MEDIUM | db/module_db.py:295 | Column indices 6 and 8 likely out of range |
+| BUG-23 | MEDIUM | db/utils.py:226 | `hwId` always overwritten by second `if` branch |
+| BUG-24 | MEDIUM | integration.py:1627 | `if True:` obscures commented-out conditional |
+| BUG-25 | MEDIUM | thermal_camera.py vs module_temperatures_gui.py | Camera name offset mismatch (0-index vs 1-index) |
+| BUG-26 | MEDIUM | coldroom/module_temperatures_gui.py:1418 | `mounted_on` split without `;` guard → IndexError |
+| BUG-27 | LOW | integration.py:2264 | `sys` not imported at module level |
+| BUG-28 | LOW | integration.py:1347 | Hardcoded `konsole` terminal and log path |
+| BUG-29 | LOW | power_supply/power_supply_ctrl.py:66 | Hardcoded power supply IP |
+| BUG-30 | LOW | integration.py:943 | Monkey-patching `mousePressEvent` every redraw |
+| BUG-31 | LOW | integration.py:611 | No MQTT auto-reconnect configured |
+| BUG-32 | LOW | caen/caenGUI.py:92 | Bare `except` in destructor hides errors |
diff --git a/modulesearch.py b/modulesearch.py
index 06bd4d0..8e72bc2 100755
--- a/modulesearch.py
+++ b/modulesearch.py
@@ -3,6 +3,7 @@
import argparse
from db.utils import *
+
def print_module_info(module_name):
print(f"********** module: {module_name} ***********")
# get module connections
@@ -16,7 +17,7 @@ def print_module_info(module_name):
print(f"{conn_type}: {conn_value}")
else:
print(f"{conn_type}: Not connected")
-
+
module = get_module(module_name)
if module:
print(f"Mounted_on: {module.get('mounted_on', 'N/A')}")
@@ -26,11 +27,19 @@ def print_module_info(module_name):
print(f"Module details for '{module_name}' not found.")
return True
+
def main():
parser = argparse.ArgumentParser(description="Search for module information.")
- parser.add_argument("search_terms", nargs="*", help="Module names or FC7 identifiers to search for.")
- parser.add_argument("-r", "--reverse", action="store_true", help="Reverse search from FC7 identifier (format: FC7_NAME_PORT).")
-
+ parser.add_argument(
+ "search_terms", nargs="*", help="Module names or FC7 identifiers to search for."
+ )
+ parser.add_argument(
+ "-r",
+ "--reverse",
+ action="store_true",
+ help="Reverse search from FC7 identifier (format: FC7_NAME_PORT).",
+ )
+
args = parser.parse_args()
if not args.search_terms:
@@ -45,7 +54,7 @@ def main():
else:
print(f"Invalid FC7 identifier format: {term}. Expected FC7NAME_PORT.")
continue
-
+
module_name = get_module_name_from_fc7(fc7, port)
if module_name:
print_module_info(module_name)
@@ -56,5 +65,6 @@ def main():
# Optionally handle module not found
pass
+
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/plot_tab/influx_3.py b/plot_tab/influx_3.py
index edae922..6f9af04 100644
--- a/plot_tab/influx_3.py
+++ b/plot_tab/influx_3.py
@@ -5,16 +5,18 @@
verbose = 0 # Set to 1 for basic output, 2 for more detailed, 3 for debug-level output
+
def getInfluxQueryAPI():
token_location = os.path.join(os.path.dirname(__file__), "influx.sct")
token = open(os.path.expanduser(token_location)).read().strip()
client = InfluxDBClient(url="http://pccmslab1:8086/", token=token)
return client.query_api()
+
def get_sensor_hierarchy():
"""Get the complete sensor hierarchy organized by topic"""
query_api = getInfluxQueryAPI()
-
+
try:
# Query to get all unique topics
topics_query = """
@@ -25,7 +27,7 @@ def get_sensor_hierarchy():
|> distinct(column: "topic")
|> sort()
"""
-
+
# Query to get fields for a specific topic
fields_query_template = """
from(bucket: "sensor_data")
@@ -36,58 +38,59 @@ def get_sensor_hierarchy():
|> distinct(column: "_field")
|> sort()
"""
-
+
# Get all topics
topics = []
topics_result = query_api.query(topics_query, org="pisaoutertracker")
for table in topics_result:
for record in table.records:
topics.append(record.get_value())
-
+
if not topics:
print("No topics found in mqtt_consumer measurement")
return {}
-
+
# For each topic, get its fields
topic_sensors = {}
for topic in sorted(topics):
fields_result = query_api.query(
- fields_query_template.format(topic=topic),
- org="pisaoutertracker"
+ fields_query_template.format(topic=topic), org="pisaoutertracker"
)
fields = []
for table in fields_result:
for record in table.records:
fields.append(record.get_value())
-
+
if fields:
topic_sensors[topic] = sorted(fields)
-
+
return topic_sensors
-
+
except Exception as e:
print(f"Error fetching sensor list: {e}")
return {}
+
def print_sensor_hierarchy(topic_sensors):
"""Print the sensor hierarchy in a tree-like structure"""
if not topic_sensors:
print("No sensor data available")
return
-
+
print("\nSensor Hierarchy:")
print("sensor_data (bucket)")
print("└── mqtt_consumer (measurement)")
-
+
for topic, sensors in topic_sensors.items():
print(f" └── {topic}")
for sensor in sensors:
print(f" └── {sensor}")
-
+
print(f"\nTotal topics: {len(topic_sensors)}")
total_sensors = sum(len(sensors) for sensors in topic_sensors.values())
print(f"Total sensors: {total_sensors}")
+
def getSensorValueAt(timestamp, sensorName, org="pisaoutertracker"):
"""Get a single sensor value at a specific timestamp"""
window = timedelta(seconds=30)
@@ -106,6 +109,7 @@ def getSensorValueAt(timestamp, sensorName, org="pisaoutertracker"):
values = [record.get_value() for table in tables for record in table.records]
return sum(values) / len(values) if values else None
+
def getSensorValueSeries(start_time, end_time, sensorName, org="pisaoutertracker"):
"""Get time series data for a sensor"""
query = f"""
@@ -116,7 +120,7 @@ def getSensorValueSeries(start_time, end_time, sensorName, org="pisaoutertracker
|> aggregateWindow(every: 1m, fn: mean, createEmpty: false)
|> yield(name: "mean")
"""
-
+
tables = getInfluxQueryAPI().query(query, org=org)
timestamps = []
values = []
@@ -126,104 +130,115 @@ def getSensorValueSeries(start_time, end_time, sensorName, org="pisaoutertracker
values.append(record.get_value())
return timestamps, values
+
def plot_sensor_data(start_time, end_time, sensorNames):
"""Plot multiple sensors in subplots"""
if not sensorNames:
print("No sensors specified for plotting")
return
-
+
num_sensors = len(sensorNames)
- fig, axes = plt.subplots(num_sensors, 1, figsize=(12, 6*num_sensors), sharex=True)
-
+ fig, axes = plt.subplots(num_sensors, 1, figsize=(12, 6 * num_sensors), sharex=True)
+
if num_sensors == 1:
axes = [axes]
-
+
for i, sensor in enumerate(sensorNames):
timestamps, values = getSensorValueSeries(start_time, end_time, sensor)
-
+
if not timestamps:
print(f"No data found for {sensor}")
continue
-
- axes[i].plot(timestamps, values, marker='o', linestyle='-', label=sensor)
- axes[i].set_title(f'Sensor: {sensor}')
- axes[i].set_ylabel('Value')
+
+ axes[i].plot(timestamps, values, marker="o", linestyle="-", label=sensor)
+ axes[i].set_title(f"Sensor: {sensor}")
+ axes[i].set_ylabel("Value")
axes[i].grid(True)
axes[i].legend()
-
- axes[-1].set_xlabel('Time')
+
+ axes[-1].set_xlabel("Time")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
+
def interactive_menu():
"""Interactive menu for exploring and plotting sensors"""
topic_sensors = get_sensor_hierarchy()
if not topic_sensors:
return
-
+
while True:
print("\n===== Sensor Data Explorer =====")
print("1. View sensor hierarchy")
print("2. Plot sensor data")
print("3. Exit")
-
+
choice = input("Select an option (1-3): ").strip()
-
+
if choice == "1":
print_sensor_hierarchy(topic_sensors)
-
+
elif choice == "2":
# Select topic
print("\nAvailable Topics:")
topics = sorted(topic_sensors.keys())
for i, topic in enumerate(topics, 1):
print(f"{i}. {topic}")
-
- topic_choice = input("\nSelect a topic (number) or 'b' to go back: ").strip()
- if topic_choice.lower() == 'b':
+
+ topic_choice = input(
+ "\nSelect a topic (number) or 'b' to go back: "
+ ).strip()
+ if topic_choice.lower() == "b":
continue
-
+
try:
- selected_topic = topics[int(topic_choice)-1]
+ selected_topic = topics[int(topic_choice) - 1]
except (ValueError, IndexError):
print("Invalid selection")
continue
-
+
# Select sensors
sensors = topic_sensors[selected_topic]
print(f"\nAvailable Sensors in {selected_topic}:")
for i, sensor in enumerate(sensors, 1):
print(f"{i}. {sensor}")
-
- sensor_choices = input("\nSelect sensors (comma-separated numbers) or 'b' to go back: ").strip()
- if sensor_choices.lower() == 'b':
+
+ sensor_choices = input(
+ "\nSelect sensors (comma-separated numbers) or 'b' to go back: "
+ ).strip()
+ if sensor_choices.lower() == "b":
continue
-
+
try:
- selected_indices = [int(x.strip())-1 for x in sensor_choices.split(",")]
- selected_sensors = [sensors[i] for i in selected_indices if 0 <= i < len(sensors)]
-
+ selected_indices = [
+ int(x.strip()) - 1 for x in sensor_choices.split(",")
+ ]
+ selected_sensors = [
+ sensors[i] for i in selected_indices if 0 <= i < len(sensors)
+ ]
+
if not selected_sensors:
print("No valid sensors selected")
continue
-
+
# Get time range
print("\nEnter time range (UTC):")
start_time = input("Start time (YYYY-MM-DDTHH:MM:SSZ): ").strip()
end_time = input("End time (YYYY-MM-DDTHH:MM:SSZ): ").strip()
-
+
# Plot the data
plot_sensor_data(start_time, end_time, selected_sensors)
-
+
except ValueError:
print("Invalid sensor selection")
-
+
elif choice == "3":
break
-
+
else:
print("Invalid choice")
+
if __name__ == "__main__":
- interactive_menu()
\ No newline at end of file
+ interactive_menu()
diff --git a/plot_tab/sensor_monitor.py b/plot_tab/sensor_monitor.py
index ca19e4f..7c08bcc 100644
--- a/plot_tab/sensor_monitor.py
+++ b/plot_tab/sensor_monitor.py
@@ -1,8 +1,16 @@
import sys
import os
from datetime import datetime, timedelta
-from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QWidget,
- QTreeWidgetItem, QMessageBox, QComboBox, QListWidget)
+from PyQt5.QtWidgets import (
+ QApplication,
+ QMainWindow,
+ QVBoxLayout,
+ QWidget,
+ QTreeWidgetItem,
+ QMessageBox,
+ QComboBox,
+ QListWidget,
+)
from PyQt5.QtCore import QTimer, pyqtSignal
from PyQt5.uic import loadUi
from influxdb_client import InfluxDBClient
@@ -13,156 +21,175 @@
verbose = 0
+
class PlotWidget(QWidget):
def __init__(self, plot_title="Plot"):
super().__init__()
self.plot_title = plot_title
self.setup_ui()
-
+
def setup_ui(self):
layout = QVBoxLayout()
self.figure = Figure(figsize=(8, 4), dpi=100)
self.canvas = FigureCanvas(self.figure)
layout.addWidget(self.canvas)
self.setLayout(layout)
-
+
# Initialize with empty plot
self.ax = self.figure.add_subplot(111)
self.ax.set_title(self.plot_title, fontsize=10, pad=10)
- self.ax.grid(True, linestyle=':', alpha=0.3)
+ self.ax.grid(True, linestyle=":", alpha=0.3)
self.figure.tight_layout()
-
- def update_plot(self, timestamps, values, sensor_names, topic, start_time, end_time):
+
+ def update_plot(
+ self, timestamps, values, sensor_names, topic, start_time, end_time
+ ):
"""Update the plot with new data"""
self.ax.clear()
-
+
# Set style
- plt.style.use('seaborn-v0_8')
- self.figure.set_facecolor('#f5f5f5')
- self.ax.set_facecolor('#ffffff')
-
+ plt.style.use("seaborn-v0_8")
+ self.figure.set_facecolor("#f5f5f5")
+ self.ax.set_facecolor("#ffffff")
+
if not timestamps or not values:
self.ax.set_title(f"{self.plot_title}\nNo Data", fontsize=10)
- self.ax.text(0.5, 0.5, 'No data available',
- horizontalalignment='center',
- verticalalignment='center',
- transform=self.ax.transAxes,
- fontsize=9,
- alpha=0.7)
+ self.ax.text(
+ 0.5,
+ 0.5,
+ "No data available",
+ horizontalalignment="center",
+ verticalalignment="center",
+ transform=self.ax.transAxes,
+ fontsize=9,
+ alpha=0.7,
+ )
else:
- colors = ['#4C72B0', '#55A868', '#C44E52', '#8172B2', '#CCB974']
+ colors = ["#4C72B0", "#55A868", "#C44E52", "#8172B2", "#CCB974"]
has_artists = False
-
- for i, (sensor_name, sensor_timestamps, sensor_values) in enumerate(zip(sensor_names, timestamps, values)):
+
+ for i, (sensor_name, sensor_timestamps, sensor_values) in enumerate(
+ zip(sensor_names, timestamps, values)
+ ):
if sensor_timestamps and sensor_values:
color = colors[i % len(colors)]
- self.ax.plot(sensor_timestamps, sensor_values,
- marker='o', markersize=4,
- linewidth=1.5, alpha=0.8,
- label=sensor_name, color=color,
- markerfacecolor='white',
- markeredgewidth=1)
+ self.ax.plot(
+ sensor_timestamps,
+ sensor_values,
+ marker="o",
+ markersize=4,
+ linewidth=1.5,
+ alpha=0.8,
+ label=sensor_name,
+ color=color,
+ markerfacecolor="white",
+ markeredgewidth=1,
+ )
has_artists = True
-
+
# Title and labels
- self.ax.set_title(f"{self.plot_title}\n{topic}",
- fontsize=10, pad=15, fontweight='bold')
- self.ax.set_xlabel('Time', fontsize=9)
- self.ax.set_ylabel('Value', fontsize=9)
-
+ self.ax.set_title(
+ f"{self.plot_title}\n{topic}", fontsize=10, pad=15, fontweight="bold"
+ )
+ self.ax.set_xlabel("Time", fontsize=9)
+ self.ax.set_ylabel("Value", fontsize=9)
+
# Legend
if has_artists:
legend = self.ax.legend(
- loc='upper right',
+ loc="upper right",
fontsize=8,
frameon=True,
fancybox=True,
framealpha=0.8,
- edgecolor='#cccccc'
+ edgecolor="#cccccc",
)
legend.get_frame().set_linewidth(0.5)
-
+
# Grid and borders
- self.ax.grid(True, linestyle='--', alpha=0.5)
- self.ax.spines['top'].set_visible(False)
- self.ax.spines['right'].set_visible(False)
-
+ self.ax.grid(True, linestyle="--", alpha=0.5)
+ self.ax.spines["top"].set_visible(False)
+ self.ax.spines["right"].set_visible(False)
+
# Date formatting
hours = (end_time.toSecsSinceEpoch() - start_time.toSecsSinceEpoch()) / 3600
-
+
if hours <= 6:
- self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
+ self.ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
self.ax.xaxis.set_major_locator(mdates.MinuteLocator(interval=30))
elif hours <= 24:
- self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
+ self.ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
self.ax.xaxis.set_major_locator(mdates.HourLocator(interval=2))
else:
- self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))
+ self.ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d"))
self.ax.xaxis.set_major_locator(mdates.DayLocator())
-
- self.figure.autofmt_xdate(rotation=30, ha='right')
-
+
+ self.figure.autofmt_xdate(rotation=30, ha="right")
+
self.figure.tight_layout()
self.canvas.draw()
+
class SensorMonitor(QMainWindow):
def __init__(self):
super().__init__()
-
+
# Load UI from file
ui_file = os.path.join(os.path.dirname(__file__), "sensor_monitor.ui")
loadUi(ui_file, self)
-
+
self.topic_sensors = {}
self.plot_configs = [
{"topic": "", "sensors": []},
{"topic": "", "sensors": []},
{"topic": "", "sensors": []},
- {"topic": "", "sensors": []}
+ {"topic": "", "sensors": []},
]
-
+
self.setup_ui()
self.setup_plots()
self.setup_timers()
self.load_sensor_hierarchy()
self.setup_default_plots()
-
+
def setup_ui(self):
"""Setup UI connections and initial state"""
# Connect buttons
self.refreshButton.clicked.connect(self.refresh_data)
self.autoRefreshButton.clicked.connect(self.toggle_auto_refresh)
self.updatePlotButton.clicked.connect(self.update_selected_plot)
-
+
# Time range buttons
self.lastHourButton.clicked.connect(lambda: self.set_time_range(1))
self.last6HoursButton.clicked.connect(lambda: self.set_time_range(6))
self.last24HoursButton.clicked.connect(lambda: self.set_time_range(24))
-
+
# Topic selection
self.topicCombo.currentTextChanged.connect(self.on_topic_changed)
-
+
# Set initial time range (last hour)
self.set_time_range(1)
-
+
# Status
self.statusLabel.setText("Status: Ready")
-
+
def setup_plots(self):
"""Setup the four plot widgets"""
self.plot_widgets = []
-
+
# Create plot widgets
for i in range(4):
plot_widget = PlotWidget(f"Plot {i+1}")
self.plot_widgets.append(plot_widget)
-
+
# Replace placeholder widgets with actual plot widgets
# placeholder = getattr(self, f'plotWidget{i+1}')
- placeholder = getattr(self, f'plotWidget{i+3}') # Note: plotWidget3-6 correspond to plots 1-4
+ placeholder = getattr(
+ self, f"plotWidget{i+3}"
+ ) # Note: plotWidget3-6 correspond to plots 1-4
parent = placeholder.parent()
layout = parent.layout()
-
+
# Find the placeholder and replace it
for j in range(layout.count()):
item = layout.itemAt(j)
@@ -173,17 +200,17 @@ def setup_plots(self):
# layout.addWidget(plot_widget, j // 2, j % 2)
layout.addWidget(plot_widget)
break
-
+
def setup_timers(self):
"""Setup refresh timer"""
self.refresh_timer = QTimer()
self.refresh_timer.timeout.connect(self.refresh_plots)
-
+
def setup_default_plots(self):
"""Setup default plots with some common sensors"""
# This will be called after sensor hierarchy is loaded
pass
-
+
def load_sensor_hierarchy(self):
"""Load sensor hierarchy from InfluxDB"""
try:
@@ -192,28 +219,28 @@ def load_sensor_hierarchy(self):
self.populate_sensor_tree()
self.populate_topic_combo()
self.statusLabel.setText(f"Status: Loaded {len(self.topic_sensors)} topics")
-
+
# Load default plots after hierarchy is loaded
self.load_default_plots()
-
+
except Exception as e:
self.statusLabel.setText(f"Status: Error loading sensors - {str(e)}")
QMessageBox.critical(self, "Error", f"Failed to load sensors: {str(e)}")
-
+
def load_default_plots(self):
"""Load default plots for the first two plot windows"""
if not self.topic_sensors:
return
-
+
topics = list(self.topic_sensors.keys())
-
+
# Default plot 1 - first topic, first few sensors
if len(topics) > 0:
topic = topics[0]
sensors = self.topic_sensors[topic][:2] # First 2 sensors
self.plot_configs[0] = {"topic": topic, "sensors": sensors}
self.update_plot(0, topic, sensors)
-
+
# Default plot 2 - second topic if available, or first topic with different sensors
if len(topics) > 1:
topic = topics[1]
@@ -225,12 +252,12 @@ def load_default_plots(self):
sensors = self.topic_sensors[topic][2:4] # Next 2 sensors
self.plot_configs[1] = {"topic": topic, "sensors": sensors}
self.update_plot(1, topic, sensors)
-
+
def get_sensor_hierarchy(self):
"""Get sensor hierarchy from InfluxDB"""
try:
query_api = self.getInfluxQueryAPI()
-
+
# Query to get all unique topics
topics_query = """
from(bucket: "sensor_data")
@@ -240,17 +267,17 @@ def get_sensor_hierarchy(self):
|> distinct(column: "topic")
|> sort()
"""
-
+
# Get all topics
topics = []
topics_result = query_api.query(topics_query, org="pisaoutertracker")
for table in topics_result:
for record in table.records:
topics.append(record.get_value())
-
+
if not topics:
return {}
-
+
# For each topic, get its fields
topic_sensors = {}
for topic in sorted(topics):
@@ -263,149 +290,151 @@ def get_sensor_hierarchy(self):
|> distinct(column: "_field")
|> sort()
"""
-
+
fields_result = query_api.query(fields_query, org="pisaoutertracker")
fields = []
for table in fields_result:
for record in table.records:
fields.append(record.get_value())
-
+
if fields:
topic_sensors[topic] = sorted(fields)
-
+
return topic_sensors
-
+
except Exception as e:
print(f"Error fetching sensor hierarchy: {e}")
return {}
-
+
def getInfluxQueryAPI(self):
"""Get InfluxDB query API"""
token_location = os.path.join(os.path.dirname(__file__), "influx.sct")
token = open(os.path.expanduser(token_location)).read().strip()
client = InfluxDBClient(url="http://pccmslab1:8086/", token=token)
return client.query_api()
-
+
def populate_sensor_tree(self):
"""Populate the sensor tree widget"""
self.sensorTree.clear()
-
+
if not self.topic_sensors:
return
-
+
root_item = QTreeWidgetItem(self.sensorTree)
root_item.setText(0, "sensor_data")
-
+
mqtt_item = QTreeWidgetItem(root_item)
mqtt_item.setText(0, "mqtt_consumer")
-
+
for topic, sensors in self.topic_sensors.items():
topic_item = QTreeWidgetItem(mqtt_item)
topic_item.setText(0, f"{topic} ({len(sensors)} sensors)")
-
+
for sensor in sensors:
sensor_item = QTreeWidgetItem(topic_item)
sensor_item.setText(0, sensor)
-
+
self.sensorTree.expandAll()
-
+
def populate_topic_combo(self):
"""Populate the topic combo box"""
self.topicCombo.clear()
self.topicCombo.addItems(sorted(self.topic_sensors.keys()))
-
+
def on_topic_changed(self, topic):
"""Handle topic selection change"""
self.sensorList.clear()
if topic and topic in self.topic_sensors:
self.sensorList.addItems(self.topic_sensors[topic])
-
+
def set_time_range(self, hours):
"""Set time range for the plots"""
end_time = datetime.now()
start_time = end_time - timedelta(hours=hours)
-
+
self.startTime.setDateTime(start_time)
self.endTime.setDateTime(end_time)
-
+
def toggle_auto_refresh(self):
"""Toggle auto refresh on/off"""
if self.autoRefreshButton.isChecked():
interval = self.intervalSpinBox.value() * 1000 # Convert to milliseconds
self.refresh_timer.start(interval)
self.autoRefreshButton.setText("Auto: ON")
- self.statusLabel.setText(f"Status: Auto-refresh ON ({self.intervalSpinBox.value()}s)")
+ self.statusLabel.setText(
+ f"Status: Auto-refresh ON ({self.intervalSpinBox.value()}s)"
+ )
else:
self.refresh_timer.stop()
self.autoRefreshButton.setText("Auto: OFF")
self.statusLabel.setText("Status: Auto-refresh OFF")
-
+
def refresh_data(self):
"""Refresh sensor data and update plots"""
self.statusLabel.setText("Status: Refreshing data...")
self.load_sensor_hierarchy()
self.refresh_plots()
-
+
def refresh_plots(self):
"""Refresh all plots with current data"""
for i, config in enumerate(self.plot_configs):
if config["topic"] and config["sensors"]:
self.update_plot(i, config["topic"], config["sensors"])
-
+
def update_selected_plot(self):
"""Update the selected plot with chosen sensors"""
plot_index = self.plotCombo.currentIndex()
topic = self.topicCombo.currentText()
-
+
# Get selected sensors
selected_sensors = []
for i in range(self.sensorList.count()):
item = self.sensorList.item(i)
if item.isSelected():
selected_sensors.append(item.text())
-
+
if not selected_sensors:
QMessageBox.warning(self, "Warning", "Please select at least one sensor")
return
-
+
# Update plot configuration
self.plot_configs[plot_index] = {"topic": topic, "sensors": selected_sensors}
self.update_plot(plot_index, topic, selected_sensors)
-
+
def update_plot(self, plot_index, topic, sensor_names):
"""Update a specific plot with sensor data"""
if plot_index >= len(self.plot_widgets):
return
-
+
try:
start_time = self.startTime.dateTime()
end_time = self.endTime.dateTime()
-
+
timestamps = []
values = []
-
+
for sensor_name in sensor_names:
sensor_timestamps, sensor_values = self.get_sensor_data(
start_time.toString("yyyy-MM-ddThh:mm:ss") + "Z",
end_time.toString("yyyy-MM-ddThh:mm:ss") + "Z",
- sensor_name, topic
+ sensor_name,
+ topic,
)
timestamps.append(sensor_timestamps)
values.append(sensor_values)
-
+
self.plot_widgets[plot_index].update_plot(
- timestamps, values, sensor_names, topic,
- start_time, end_time
+ timestamps, values, sensor_names, topic, start_time, end_time
)
-
+
except Exception as e:
print(f"Error updating plot {plot_index}: {e}")
-
+
def get_sensor_data(self, start_time, end_time, sensor_name, topic):
"""Get sensor data from InfluxDB"""
try:
query_api = self.getInfluxQueryAPI()
-
+
query = f"""
from(bucket: "sensor_data")
|> range(start: {start_time}, stop: {end_time})
@@ -415,27 +444,29 @@ def get_sensor_data(self, start_time, end_time, sensor_name, topic):
|> aggregateWindow(every: 1m, fn: mean, createEmpty: false)
|> yield(name: "mean")
"""
-
+
tables = query_api.query(query, org="pisaoutertracker")
timestamps = []
values = []
-
+
for table in tables:
for record in table.records:
timestamps.append(record.get_time())
values.append(record.get_value())
-
+
return timestamps, values
-
+
except Exception as e:
print(f"Error fetching sensor data: {e}")
return [], []
+
def main():
app = QApplication(sys.argv)
window = SensorMonitor()
window.show()
sys.exit(app.exec_())
+
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/power_supply/power_supply.ui b/power_supply/power_supply.ui
index 19fab89..82cb2b2 100644
--- a/power_supply/power_supply.ui
+++ b/power_supply/power_supply.ui
@@ -1,7 +1,7 @@
- MainWindow
-
+ PowerSupply
+
0
@@ -11,184 +11,171 @@
- MainWindow
+ Power Supply
-
-
- -
-
-
- Power Supply Control
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- -
-
-
- Set Voltage Value
-
-
- Qt::AlignCenter
-
-
-
- -
-
-
- ?
-
-
- Qt::AlignCenter
-
-
-
- -
-
-
- Set Current
-
-
-
- -
-
-
- ON
-
-
-
- -
-
-
- ?
-
-
- Qt::AlignCenter
-
-
-
- -
-
-
- ?
-
-
- Qt::AlignCenter
-
-
-
- -
-
-
- Set Voltage
-
-
-
- -
-
-
- -
-
-
- Set Curent Value
-
-
- Qt::AlignCenter
-
-
-
- -
-
-
- ?
-
-
- Qt::AlignCenter
-
-
-
- -
-
-
-
- 30
- 30
-
-
-
-
- 30
- 30
-
-
-
- background-color: red;
-
-
- QFrame::NoFrame
-
-
- QFrame::Raised
-
-
-
- -
-
-
- Voltage ( v )
-
-
- Qt::AlignCenter
-
-
-
- -
-
-
- OFF
-
-
-
- -
-
-
- Current Status ( i )
-
-
- Qt::AlignCenter
-
-
-
-
-
-
-
-
-
-
+
+ -
+
+
+ Power Supply Control
+
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ -
+
+
+ Set Voltage Value
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ ?
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Set Current
+
+
+
+ -
+
+
+ ON
+
+
+
+ -
+
+
+ ?
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ ?
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Set Voltage
+
+
+
+ -
+
+
+ -
+
+
+ Set Curent Value
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ ?
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 30
+ 30
+
+
+
+
+ 30
+ 30
+
+
+
+ background-color: red;
+
+
+ QFrame::NoFrame
+
+
+ QFrame::Raised
+
+
+
+ -
+
+
+ Voltage ( v )
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ OFF
+
+
+
+ -
+
+
+ Current Status ( i )
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
diff --git a/power_supply/power_supply_ctrl.py b/power_supply/power_supply_ctrl.py
index 39dbab8..00fba34 100644
--- a/power_supply/power_supply_ctrl.py
+++ b/power_supply/power_supply_ctrl.py
@@ -1,47 +1,56 @@
+import os
import sys
import logging
import pyvisa as visa
-from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
+from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox
from PyQt5.uic import loadUi
from PyQt5.QtCore import QTimer
# Configure logging
logging.basicConfig(
level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s',
+ format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.StreamHandler(sys.stdout),
# logging.FileHandler('power_supply.log')
- ]
+ ],
)
logger = logging.getLogger(__name__)
-class PowerSupplyController(QMainWindow):
- def __init__(self):
- super(PowerSupplyController, self).__init__()
+
+class PowerSupplyController(QWidget):
+ def __init__(self, parent=None):
+ super(PowerSupplyController, self).__init__(parent)
# Load the UI file
- loadUi('power_supply.ui', self)
-
+ ui_path = os.path.join(os.path.dirname(__file__), "power_supply.ui")
+ loadUi(ui_path, self)
+
# Initialize status dictionary
self.status = {
- 'voltage': 0.000,
- 'current': 0.000,
- 'power': 'OFF',
- 'set_voltage': 0.0,
- 'set_current': 0.0
+ "voltage": 0.000,
+ "current": 0.000,
+ "power": "OFF",
+ "set_voltage": 0.0,
+ "set_current": 0.0,
}
-
+ self.connected = False
+
+ # Set up timer early so closeEvent doesn't crash on connection failure
+ self.timer = QTimer(self)
+
# Connect to the power supply
self.connect_to_power_supply()
-
+ if not self.connected:
+ self.timer.stop()
+ return
+
# Connect UI signals to slots
self.setup_ui_connections()
-
+
# Set up a timer to periodically update measurements
- self.timer = QTimer(self)
self.timer.timeout.connect(self.update_measurements)
self.timer.start(1000) # Update every 1000ms (1 second)
-
+
# Update UI with initial status
self.update_ui()
self.log_status("Initial Power Supply Status")
@@ -50,18 +59,20 @@ def __init__(self):
def log_status(self, message):
"""Log the current status with a descriptive message"""
logger.info(f"{message}: {self.status}")
-
+
def connect_to_power_supply(self):
"""Connect to the power supply device"""
try:
- ip_address = '192.168.0.16'
+ ip_address = "192.168.0.16"
self.rm = visa.ResourceManager()
- self.power_supply = self.rm.open_resource(f'TCPIP0::{ip_address}::INSTR')
+ self.power_supply = self.rm.open_resource(f"TCPIP0::{ip_address}::INSTR")
+ self.connected = True
print("Successfully connected to power supply")
except Exception as e:
logger.error(f"Failed connection: {e}")
- QMessageBox.critical(self, "Connection Error", f"Failed to connect to power supply: {e}")
- self.close()
+ QMessageBox.critical(
+ self, "Connection Error", f"Failed to connect to power supply: {e}"
+ )
def setup_ui_connections(self):
"""Connect UI signals to slots"""
@@ -72,71 +83,80 @@ def setup_ui_connections(self):
def update_ui(self):
"""Update the UI elements with current status"""
- self.powsup_current_value_label.setText(str(self.status['current']))
- self.powsup_voltage_value_label.setText(str(self.status['voltage']))
- self.powsup_voltage_set_value_label.setText(str(self.status['set_voltage']))
- self.powsup_current_set_value_label.setText(str(self.status['set_current']))
+ self.powsup_current_value_label.setText(str(self.status["current"]))
+ self.powsup_voltage_value_label.setText(str(self.status["voltage"]))
+ self.powsup_voltage_set_value_label.setText(str(self.status["set_voltage"]))
+ self.powsup_current_set_value_label.setText(str(self.status["set_current"]))
# Update power LED color
- if self.status['power'] == 'ON':
+ if self.status["power"] == "ON":
self.power_LED.setStyleSheet("background-color: green;")
else:
self.power_LED.setStyleSheet("background-color: red;")
-
+
def update_measurements(self):
"""Periodically update the voltage and current measurements"""
- # if self.status['power'] == 'ON':
+ if not self.connected:
+ return
try:
# Read actual voltage and current
- voltage = self.power_supply.query('MEAS:VOLT?\x00').strip()
- current = self.power_supply.query('MEAS:CURR?\x00').strip()
-
+ voltage = self.power_supply.query("MEAS:VOLT?\x00").strip()
+ current = self.power_supply.query("MEAS:CURR?\x00").strip()
+
# Update status
- self.status['voltage'] = voltage
- self.status['current'] = current
-
+ self.status["voltage"] = voltage
+ self.status["current"] = current
+
# Update UI
self.update_ui()
self.log_status("Updated measurements")
except Exception as e:
logger.error(f"Error reading measurements: {e}")
- QMessageBox.critical(self, "Measurement Error", f"Failed to read measurements: {e}")
- print(f"Error reading measurements: {e}")
-
+
def set_voltage(self):
"""Set the voltage value from the line edit"""
+ if not self.connected:
+ return
try:
voltage = float(self.powsup_voltage_LE.text())
- self.power_supply.write(f'APPLY {voltage}\x00')
- self.status['set_voltage'] = voltage
+ self.power_supply.write(f"APPLY {voltage}\x00")
+ self.status["set_voltage"] = voltage
logger.info(f"Voltage set to {voltage}V")
print(f"Voltage set to {voltage}V")
except ValueError:
logger.error("Invalid voltage input")
- QMessageBox.warning(self, "Input Error", "Please enter a valid number for voltage")
+ QMessageBox.warning(
+ self, "Input Error", "Please enter a valid number for voltage"
+ )
except Exception as e:
logger.error(f"Failed to set voltage: {e}")
QMessageBox.critical(self, "Error", f"Failed to set voltage: {e}")
-
+
def set_current(self):
"""Set the current value from the line edit"""
+ if not self.connected:
+ return
try:
current = float(self.powsup_current_LE.text())
- self.power_supply.write(f'SOUR:CURR {current}\x00')
- self.status['set_current'] = current
+ self.power_supply.write(f"SOUR:CURR {current}\x00")
+ self.status["set_current"] = current
logger.info(f"Current set to {current}A")
print(f"Current set to {current}A")
except ValueError:
logger.error("Invalid current input")
- QMessageBox.warning(self, "Input Error", "Please enter a valid number for current")
+ QMessageBox.warning(
+ self, "Input Error", "Please enter a valid number for current"
+ )
except Exception as e:
logger.error(f"Failed to set current: {e}")
QMessageBox.critical(self, "Error", f"Failed to set current: {e}")
-
+
def power_on(self):
"""Turn the power supply ON"""
+ if not self.connected:
+ return
try:
- self.power_supply.write('OUTP:STAT ON\x00')
- self.status['power'] = 'ON'
+ self.power_supply.write("OUTP:STAT ON\x00")
+ self.status["power"] = "ON"
self.update_ui()
self.log_status("Power Supply turned ON")
print("Power Supply is ON")
@@ -144,12 +164,14 @@ def power_on(self):
print(f"Error turning power ON: {e}")
logger.error(f"Failed to turn power ON: {e}")
QMessageBox.critical(self, "Error", f"Failed to turn power ON: {e}")
-
+
def power_off(self):
"""Turn the power supply OFF"""
+ if not self.connected:
+ return
try:
- self.power_supply.write('OUTP:STAT OFF\x00')
- self.status['power'] = 'OFF'
+ self.power_supply.write("OUTP:STAT OFF\x00")
+ self.status["power"] = "OFF"
self.update_ui()
# Force a measurement update to show actual off values
self.update_measurements()
@@ -159,15 +181,16 @@ def power_off(self):
logger.error(f"Failed to turn power OFF: {e}")
QMessageBox.critical(self, "Error", f"Failed to turn power OFF: {e}")
print(f"Error turning power OFF: {e}")
-
+
def closeEvent(self, event):
"""Ensure proper cleanup when closing the application"""
try:
- self.timer.stop()
- if hasattr(self, 'power_supply'):
+ if hasattr(self, "timer"):
+ self.timer.stop()
+ if hasattr(self, "power_supply"):
self.power_off() # Turn off power supply when closing
self.power_supply.close()
- if hasattr(self, 'rm'):
+ if hasattr(self, "rm"):
self.rm.close()
logger.info("Power Supply Controller closed successfully")
except Exception as e:
@@ -175,11 +198,19 @@ def closeEvent(self, event):
logger.error(f"Error during cleanup: {e}")
event.accept()
+
def main():
app = QApplication(sys.argv)
- window = PowerSupplyController()
+ window = QMainWindow()
+ controller = PowerSupplyController()
+ window.setCentralWidget(controller)
+ window.setWindowTitle("Power Supply")
+ window.resize(controller.width(), controller.height())
window.show()
sys.exit(app.exec_())
-if __name__ == '__main__':
+
+if __name__ == "__main__":
+ from PyQt5.QtWidgets import QMainWindow
+
main()
diff --git a/power_supply/rigolDP116A.py b/power_supply/rigolDP116A.py
index b25646a..1ac00b2 100644
--- a/power_supply/rigolDP116A.py
+++ b/power_supply/rigolDP116A.py
@@ -3,84 +3,95 @@
import argparse
# Define the IP address of the Rigol DP1116A power supply
-ip_address = '192.168.0.16'
+ip_address = "192.168.0.16"
# Connect to the device via Ethernet using VISA
rm = visa.ResourceManager()
-power_supply = rm.open_resource(f'TCPIP0::{ip_address}::INSTR')
+power_supply = rm.open_resource(f"TCPIP0::{ip_address}::INSTR")
+
# Function to turn the power supply ON
def power_on():
try:
- power_supply.write('OUTP:STAT ON\x00') # Turn on output
+ power_supply.write("OUTP:STAT ON\x00") # Turn on output
print("Power Supply is ON.")
except Exception as e:
print(f"Error turning on power supply: {e}")
+
# Function to turn the power supply OFF
def power_off():
try:
- power_supply.write('OUTP:STAT OFF\x00') # Turn off output
+ power_supply.write("OUTP:STAT OFF\x00") # Turn off output
print("Power Supply is OFF.")
except Exception as e:
print(f"Error turning off power supply: {e}")
+
# Function to set the voltage
def set_voltage(voltage):
try:
- power_supply.write(f'APPLY {voltage}\x00') # Set voltage
+ power_supply.write(f"APPLY {voltage}\x00") # Set voltage
print(f"Voltage set to {voltage}V.")
except Exception as e:
print(f"Error setting voltage: {e}")
+
# Function to set the current
def set_current(current):
try:
- power_supply.write(f'SOUR:CURR {current}\x00') # Set current
+ power_supply.write(f"SOUR:CURR {current}\x00") # Set current
print(f"Current set to {current}A.")
except Exception as e:
print(f"Error setting current: {e}")
+
# Function to read voltage
def read_voltage():
try:
- voltage = power_supply.query('MEAS:VOLT?\x00') # Query voltage
+ voltage = power_supply.query("MEAS:VOLT?\x00") # Query voltage
print(f"Voltage: {voltage} V")
except Exception as e:
print(f"Error reading voltage: {e}")
+
# Function to read current
def read_current():
try:
- current = power_supply.query('MEAS:CURR?\x00') # Query current
+ current = power_supply.query("MEAS:CURR?\x00") # Query current
print(f"Current: {current} A")
except Exception as e:
print(f"Error reading current: {e}")
+
# Main function to handle command-line arguments
def main():
- parser = argparse.ArgumentParser(description='Control Rigol DP1116A Power Supply via VISA.')
-
+ parser = argparse.ArgumentParser(
+ description="Control Rigol DP1116A Power Supply via VISA."
+ )
+
# Command-line arguments
- parser.add_argument('-v', '--voltage', type=float, help='Set the voltage (in volts)')
- parser.add_argument('-i', '--current', type=float, help='Set the current (in amps)')
- parser.add_argument('--on', action='store_true', help='Turn the power supply ON')
- parser.add_argument('--off', action='store_true', help='Turn the power supply OFF')
+ parser.add_argument(
+ "-v", "--voltage", type=float, help="Set the voltage (in volts)"
+ )
+ parser.add_argument("-i", "--current", type=float, help="Set the current (in amps)")
+ parser.add_argument("--on", action="store_true", help="Turn the power supply ON")
+ parser.add_argument("--off", action="store_true", help="Turn the power supply OFF")
args = parser.parse_args()
# Turn on power supply if --on is specified
if args.on:
power_on()
-
+
# Turn off power supply if --off is specified
if args.off:
power_off()
-
+
# Set voltage if specified with -v argument
if args.voltage is not None:
set_voltage(args.voltage)
-
+
# Set current if specified with -i argument
if args.current is not None:
set_current(args.current)
@@ -91,10 +102,11 @@ def main():
read_current()
# Optional: Add delay before turning off power supply after all operations
+
+
# if not args.on and not args.off:
# time.sleep(2)
# power_off()
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
-
diff --git a/scripts/mount_modules.py b/scripts/mount_modules.py
index 4a70418..4626edc 100644
--- a/scripts/mount_modules.py
+++ b/scripts/mount_modules.py
@@ -9,6 +9,7 @@
PISA_LOCATION = "Pisa"
+
def check_components(db, module_label, ring_label):
# Check module location
module_loc = db.component_location(module_label)
@@ -23,38 +24,45 @@ def check_components(db, module_label, ring_label):
# Verify same location
if module_loc != ring_loc:
- raise ValueError(f"Module and Ring are in different locations: {module_loc} vs {ring_loc}")
+ raise ValueError(
+ f"Module and Ring are in different locations: {module_loc} vs {ring_loc}"
+ )
- # Need to add a check for Tracker Detector ROOT
+ # Need to add a check for Tracker Detector ROOT
# Get location name
location_id = db.get_location_id(PISA_LOCATION)
if module_loc != location_id:
raise ValueError(f"Components not in PISA location")
-
- print(f"Components verified: {module_label} and {ring_label} are in {PISA_LOCATION}")
+
+ print(
+ f"Components verified: {module_label} and {ring_label} are in {PISA_LOCATION}"
+ )
return True
-def build_connect_xml(ring_barcode, module_label, position_index, integration_status="Attached"):
+
+def build_connect_xml(
+ ring_barcode, module_label, position_index, integration_status="Attached"
+):
root = etree.Element("ROOT")
parts = etree.SubElement(root, "PARTS")
-
+
# Ring part
ring = etree.SubElement(parts, "PART")
ring.set("mode", "auto")
etree.SubElement(ring, "KIND_OF_PART").text = "TBPS Ring"
etree.SubElement(ring, "NAME_LABEL").text = ring_barcode
-
+
# Children section
children = etree.SubElement(ring, "CHILDREN")
-
+
# Module part
module = etree.SubElement(children, "PART")
module.set("mode", "auto")
etree.SubElement(module, "KIND_OF_PART").text = "PS Module"
etree.SubElement(module, "NAME_LABEL").text = module_label
-
+
# Add Status attribute
attrs = etree.SubElement(module, "PREDEFINED_ATTRIBUTES")
attr = etree.SubElement(attrs, "ATTRIBUTE")
@@ -64,28 +72,29 @@ def build_connect_xml(ring_barcode, module_label, position_index, integration_st
attr2 = etree.SubElement(attrs, "ATTRIBUTE")
etree.SubElement(attr2, "NAME").text = "Module Integration Status"
etree.SubElement(attr2, "VALUE").text = integration_status
-
+
return root
+
def build_disconnect_xml(module_label):
root = etree.Element("ROOT")
parts = etree.SubElement(root, "PARTS")
-
+
# Tracker ROOT part
tracker = etree.SubElement(parts, "PART")
tracker.set("mode", "auto")
etree.SubElement(tracker, "KIND_OF_PART").text = "Tracker Detector ROOT"
etree.SubElement(tracker, "NAME_LABEL").text = "ROOT"
-
+
# Children section
children = etree.SubElement(tracker, "CHILDREN")
-
+
# Module part
module = etree.SubElement(children, "PART")
module.set("mode", "auto")
etree.SubElement(module, "KIND_OF_PART").text = "PS Module"
etree.SubElement(module, "NAME_LABEL").text = module_label
-
+
# Remove Status attribute
attrs = etree.SubElement(module, "PREDEFINED_ATTRIBUTES")
attr = etree.SubElement(attrs, "ATTRIBUTE")
@@ -96,21 +105,35 @@ def build_disconnect_xml(module_label):
# print(etree.tostring(root, pretty_print=True, xml_declaration=True, encoding='ASCII', standalone='yes'))
return root
+
def main():
- parser = argparse.ArgumentParser(description='Connect/Disconnect modules from rings')
- parser.add_argument('module_label', help='Module name label')
- parser.add_argument('ring_barcode', help='Ring barcode')
- parser.add_argument('--disconnect', action='store_true', help='Disconnect instead of connect')
- parser.add_argument('--position-index', type=int, default=0, help='Position index for module')
- parser.add_argument('--integration-status', type=str, default='Attached', help='Module Integration Status')
+ parser = argparse.ArgumentParser(
+ description="Connect/Disconnect modules from rings"
+ )
+ parser.add_argument("module_label", help="Module name label")
+ parser.add_argument("ring_barcode", help="Ring barcode")
+ parser.add_argument(
+ "--disconnect", action="store_true", help="Disconnect instead of connect"
+ )
+ parser.add_argument(
+ "--position-index", type=int, default=0, help="Position index for module"
+ )
+ parser.add_argument(
+ "--integration-status",
+ type=str,
+ default="Attached",
+ help="Module Integration Status",
+ )
args = parser.parse_args()
# Initialize DB access
- path = os.path.dirname(os.environ.get('DBLOADER'))
+ path = os.path.dirname(os.environ.get("DBLOADER"))
# cmsr is production database, int2r is test database
- db = DBaccess(database="trker_cmsr", verbose=True, login_type='login')
- uploader = DBupload(database='cmsr', verbose=True, path_to_dbloader_api=path, login_type='login')
+ db = DBaccess(database="trker_cmsr", verbose=True, login_type="login")
+ uploader = DBupload(
+ database="cmsr", verbose=True, path_to_dbloader_api=path, login_type="login"
+ )
try:
# Verify components
@@ -122,17 +145,33 @@ def main():
xml_root = build_disconnect_xml(args.module_label)
else:
action_prefix = "connect"
- xml_root = build_connect_xml(args.ring_barcode, args.module_label, args.position_index, args.integration_status)
+ xml_root = build_connect_xml(
+ args.ring_barcode,
+ args.module_label,
+ args.position_index,
+ args.integration_status,
+ )
# Create output directory and unique filename
- output_dir = 'xml_submissions'
+ output_dir = "xml_submissions"
os.makedirs(output_dir, exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
- filename = os.path.join(output_dir, f'{action_prefix}_{args.ring_barcode}_{args.module_label}_{timestamp}.xml')
+ filename = os.path.join(
+ output_dir,
+ f"{action_prefix}_{args.ring_barcode}_{args.module_label}_{timestamp}.xml",
+ )
# Write XML file
- with open(filename, 'wb') as f:
- f.write(etree.tostring(xml_root, pretty_print=True, xml_declaration=True, encoding='ASCII', standalone='yes'))
+ with open(filename, "wb") as f:
+ f.write(
+ etree.tostring(
+ xml_root,
+ pretty_print=True,
+ xml_declaration=True,
+ encoding="ASCII",
+ standalone="yes",
+ )
+ )
# Upload to database
uploader.upload_data(filename)
@@ -143,6 +182,6 @@ def main():
return 0
+
if __name__ == "__main__":
exit(main())
-
diff --git a/scripts/rigolDP116A.py b/scripts/rigolDP116A.py
index b25646a..1ac00b2 100644
--- a/scripts/rigolDP116A.py
+++ b/scripts/rigolDP116A.py
@@ -3,84 +3,95 @@
import argparse
# Define the IP address of the Rigol DP1116A power supply
-ip_address = '192.168.0.16'
+ip_address = "192.168.0.16"
# Connect to the device via Ethernet using VISA
rm = visa.ResourceManager()
-power_supply = rm.open_resource(f'TCPIP0::{ip_address}::INSTR')
+power_supply = rm.open_resource(f"TCPIP0::{ip_address}::INSTR")
+
# Function to turn the power supply ON
def power_on():
try:
- power_supply.write('OUTP:STAT ON\x00') # Turn on output
+ power_supply.write("OUTP:STAT ON\x00") # Turn on output
print("Power Supply is ON.")
except Exception as e:
print(f"Error turning on power supply: {e}")
+
# Function to turn the power supply OFF
def power_off():
try:
- power_supply.write('OUTP:STAT OFF\x00') # Turn off output
+ power_supply.write("OUTP:STAT OFF\x00") # Turn off output
print("Power Supply is OFF.")
except Exception as e:
print(f"Error turning off power supply: {e}")
+
# Function to set the voltage
def set_voltage(voltage):
try:
- power_supply.write(f'APPLY {voltage}\x00') # Set voltage
+ power_supply.write(f"APPLY {voltage}\x00") # Set voltage
print(f"Voltage set to {voltage}V.")
except Exception as e:
print(f"Error setting voltage: {e}")
+
# Function to set the current
def set_current(current):
try:
- power_supply.write(f'SOUR:CURR {current}\x00') # Set current
+ power_supply.write(f"SOUR:CURR {current}\x00") # Set current
print(f"Current set to {current}A.")
except Exception as e:
print(f"Error setting current: {e}")
+
# Function to read voltage
def read_voltage():
try:
- voltage = power_supply.query('MEAS:VOLT?\x00') # Query voltage
+ voltage = power_supply.query("MEAS:VOLT?\x00") # Query voltage
print(f"Voltage: {voltage} V")
except Exception as e:
print(f"Error reading voltage: {e}")
+
# Function to read current
def read_current():
try:
- current = power_supply.query('MEAS:CURR?\x00') # Query current
+ current = power_supply.query("MEAS:CURR?\x00") # Query current
print(f"Current: {current} A")
except Exception as e:
print(f"Error reading current: {e}")
+
# Main function to handle command-line arguments
def main():
- parser = argparse.ArgumentParser(description='Control Rigol DP1116A Power Supply via VISA.')
-
+ parser = argparse.ArgumentParser(
+ description="Control Rigol DP1116A Power Supply via VISA."
+ )
+
# Command-line arguments
- parser.add_argument('-v', '--voltage', type=float, help='Set the voltage (in volts)')
- parser.add_argument('-i', '--current', type=float, help='Set the current (in amps)')
- parser.add_argument('--on', action='store_true', help='Turn the power supply ON')
- parser.add_argument('--off', action='store_true', help='Turn the power supply OFF')
+ parser.add_argument(
+ "-v", "--voltage", type=float, help="Set the voltage (in volts)"
+ )
+ parser.add_argument("-i", "--current", type=float, help="Set the current (in amps)")
+ parser.add_argument("--on", action="store_true", help="Turn the power supply ON")
+ parser.add_argument("--off", action="store_true", help="Turn the power supply OFF")
args = parser.parse_args()
# Turn on power supply if --on is specified
if args.on:
power_on()
-
+
# Turn off power supply if --off is specified
if args.off:
power_off()
-
+
# Set voltage if specified with -v argument
if args.voltage is not None:
set_voltage(args.voltage)
-
+
# Set current if specified with -i argument
if args.current is not None:
set_current(args.current)
@@ -91,10 +102,11 @@ def main():
read_current()
# Optional: Add delay before turning off power supply after all operations
+
+
# if not args.on and not args.off:
# time.sleep(2)
# power_off()
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
-
diff --git a/session-ses_19b8.md b/session-ses_19b8.md
new file mode 100644
index 0000000..430632d
--- /dev/null
+++ b/session-ses_19b8.md
@@ -0,0 +1,313 @@
+# Codebase overview and connections
+
+**Session ID:** ses_19b84853cffeZtVdgrIeqyIyIP
+**Created:** 5/26/2026, 3:31:14 PM
+**Updated:** 5/26/2026, 3:45:22 PM
+
+---
+
+## User
+
+ubderstand the whole codebase, and give me a detail overview of the project, how things are connected and working.
+
+---
+
+## Assistant
+
+**Tool: read**
+
+---
+
+## Assistant
+
+**Tool: task**
+
+---
+
+## Assistant
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+---
+
+## Assistant
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+---
+
+## Assistant
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+---
+
+## Assistant
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+**Tool: read**
+
+---
+
+## Assistant
+
+Here is a comprehensive overview of the project.
+
+---
+
+## Project: CMS Pisa Integration Tools
+
+This is a **CMS (Compact Muon Solenoid) detector integration and testing system** for the Phase-2 Upgrade of the Inner Tracker (IT) at **CERN's Pisa site**. It provides a suite of tools to control, monitor, and test detector modules (silicon sensors) during their assembly and burn-in process.
+
+---
+
+### Architecture Overview
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ TWO MAIN GUI APPLICATIONS │
+│ │
+│ integration.py (Burn-In Test Station) cold.py (Cold Room Ctrl) │
+│ ┌──────────────────────────────────┐ ┌────────────────────────┐ │
+│ │ Module Testing & Verification │ │ Environmental Control │ │
+│ │ HV/LV via CAEN power supplies │ │ MARTA CO2 Plant │ │
+│ │ Ph2_ACF readout system │ │ Temperature/Humidity │ │
+│ │ Thermal Camera MQTT monitoring │ │ Door/Light/Dry Air │ │
+│ │ Module mounting ring diagram │ │ Safety interlocks │ │
+│ │ Test session management │ │ Modules List/Layout │ │
+│ └──────────────────────────────────┘ └────────────────────────┘ │
+└─────────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│ SHARED LAYERS │
+│ │
+│ db/ - Database REST API client (ModuleDB, utils) │
+│ caen/ - CAEN HV/LV power supply control (TCP/IP) │
+│ coldroom/ - System state, MQTT, safety, thermal camera │
+│ ui/ - PyQt5 .ui compiled GUIs │
+│ power_supply/ - Rigol bench power supply control (USB) │
+│ plot_tab/ - InfluxDB sensor monitoring │
+│ scripts/ - Mount modules, DB upload │
+│ tests/ - Unit/integration tests │
+└─────────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│ EXTERNAL HARDWARE / NETWORK │
+│ │
+│ MQTT Broker (192.168.0.45) ← Thermal Camera, Sensors, Actuators │
+│ REST API (cmslabserver:5000) ← Module database backend │
+│ CAEN Power Supply (TCP:7000) ← HV/LV bias for modules │
+│ Ph2_ACF Test System ← Readout board testing framework │
+│ Rigol DP116A (USB/Serial) ← Bench power supply │
+│ Shellies Smart Switches ← Air/dry air control │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+### Major Components
+
+#### 1. `integration.py` — Main Burn-In Test Station GUI
+- **Purpose**: The primary interface for operators testing detector modules on the integration bench
+- **Key features**:
+ - **Ring Diagram**: Visual representation of the detector ring with clickable module positions (`integration.py:774-877`)
+ - **Test Execution Pipeline**: Three sequential tests: Check ID → Light ON Test (eyeOpening) → Dark Test (calibration+pedestal+noise) (`integration.py:1053-1201`)
+ - **Command Expansion**: Placeholder system (`{module_id}`, `{fiber_endpoint}`, `{session}`) for flexible command construction (`integration.py:958-968`)
+ - **MQTT Monitoring**: Subscribes to thermal camera data (`/ar/thermal/image`), Ph2_ACF temperatures (`/ph2acf/data`), and air status (`integration.py:517-751`)
+ - **CAEN Power Control**: Integrated CAEN HV/LV GUI tab for biasing modules
+ - **Session Management**: Creates test sessions on the database with operator info, comments, timestamp (`integration.py:390-413`)
+ - **Thermal Safety**: Auto-shutdown HV/LV if temperature exceeds 45°C/50°C (`integration.py:727-731`)
+ - **Module DB Integration**: Full module inventory browse, search, filter, and detail view
+
+#### 2. `cold.py` — Cold Room & MARTA Control GUI
+- **Purpose**: Full environmental control and monitoring for the cold room where modules are tested at low temperatures
+- **Key features**:
+ - **MARTA CO₂ Plant Control**: Temperature setpoint, chiller speed, CO₂ flow control, start/stop (`cold.py:294-492`)
+ - **Cold Room Control**: Temperature/Humidity regulation, lights, dry air, door lock, run state (`cold.py:554-903`)
+ - **Safety Interlocks**: Dew point checking, door safe-to-open logic, light safe-to-turn-on, HV interlock, CO₂ level monitoring (`coldroom/safety.py`)
+ - **Modules List Tab**: Interactive table showing each module's position, power status (HV/LV), fiber connection, temperature, and grade (`coldroom/modules_list_gui.py`)
+ - **Thermal Camera Tab**: Real-time 24×32 thermal image display, trend plots (`coldroom/thermal_camera_gui.py`)
+ - **Module Temperatures Tab**: Ph2_ACF temperature monitoring with SSA/MPA mean/max trends (`coldroom/module_temperatures_gui.py`)
+ - **CAEN Control**: Two CAEN tabs — one for the Outer Tracker (OT) and one for the Inner Tracker (IT)
+
+#### 3. `db/` — Database Layer
+- **`module_db.py`**: Central DB widget with module inventory tree, search/filter by layer/speed/spacer/grade, module detail view (FB, EB, connections, temperature offsets, grade, comments) (`db/module_db.py`)
+- **`utils.py`**: REST API helper functions for all DB operations:
+ - `get_module()` — Fetch single module by ID
+ - `get_modules_on_ring()` — Query all modules mounted on a given ring
+ - `get_module_endpoints()` — Get fiber/power connections for a module
+ - `get_module_name_from_fc7()` — Reverse lookup from FC7 readout board + port
+ - `get_ring_from_cable()` — Determine which ring a Harting cable connects to
+ - `get_module_speed()`, `get_module_fuse_id()`, `get_module_lpgbtVersion()` — Various metadata lookups
+- **`module_db_gui.py`**: Compiled Qt UI (`module_db.ui`) with the tree widget
+
+#### 4. `caen/` — CAEN Power Supply Control
+- **`caenGUIall.py`**: OT CAEN GUI and TCP/IP protocol client — controls HV (up to ~200V) and LV channels; queries status (current, voltage, temperature); threaded non-blocking I/O
+- **`caenGUI.py`**: Simplified CAEN control used by `integration.py`
+- **`caencli.py`**: CLI tool to send raw commands to CAEN power supplies
+
+#### 5. `coldroom/` — Cold Room Subsystem
+- **`system.py`**: Central `System` class holding shared state (status dict, MQTT clients, settings)
+- **`marta_coldroom.py`**: `MartaColdRoomMQTTClient` — handles MQTT pub/sub for MARTA, cold room, cleanroom, CO₂ sensor, dry air bypass (`http://192.168.0.204/relay/0`)
+- **`thermal_camera.py`**: `ThermalCameraMQTTClient` — receives thermal image data from MQTT
+- **`safety.py`**: Safety functions — `check_dew_point()`, `check_door_safe_to_open()`, `check_light_safe_to_turn_on()`, `check_marta_safe()`, `check_any_hv_on()`
+- **`modules_list_gui.py`**: Module configuration table per position
+- **`module_temperatures_gui.py`**: Real-time temperature plots per module
+
+#### 6. Standalone Tools
+- **`glue_analyzer.py`** — Interactive OpenCV tool for analyzing glue contact area in module assembly images. Supports ROI selection, thresholding, gradient correction, manual painting (`glue_analyzer.py`)
+- **`drawEvent.py`** — ROOT-based 3D event display for visualizing tracker hits (strips, pixels, stubs with bending) from ROOT data files (`drawEvent.py`)
+- **`dbbrowser.py`** — Standalone GUI for browsing the module database
+- **`dbrunsearch.py`** — CLI tool to search runs, view session details, edit session comments against the DB API
+- **`modulesearch.py`** — CLI tool to query module info (connections, speed, lpGBT version) by module ID or FC7 port
+- **`edit_ompo_report.py`** — PowerPoint template editor for OMPO shift reports (replaces Name/Date/Status fields)
+- **`monitor_module.sh`** & **`balistic_calibration.sh`** — Shell scripts for monitoring and calibration
+
+#### 7. `power_supply/` — Bench Power Supply
+- **`rigolDP116A.py`** & **`power_supply_ctrl.py`** — Control for Rigol DP116A programmable bench power supply via USB/serial
+
+#### 8. `plot_tab/` — Sensor Monitoring
+- **`influx_3.py`** & **`sensor_monitor.py`** — InfluxDB-based sensor monitoring with historical data plotting
+
+#### 9. `scripts/` — Utilities
+- **`mount_modules.py`** — Script for mounting modules on rings
+- **`py4dbupload/`** — Database upload scripts
+
+---
+
+### Communication & Data Flow
+
+| Protocol / Interface | Usage |
+|---|---|
+| **MQTT** (paho-mqtt) | Thermal camera images, environmental sensors, actuator commands, status broadcasts |
+| **REST API** (requests) | Module database CRUD operations to `cmslabserver:5000` |
+| **TCP/IP** (socket) | CAEN power supply control on port 7000 |
+| **USB/Serial** | Rigol DP116A bench supply |
+| **Shell Commands** (subprocess) | Ph2_ACF test execution, shell scripts |
+| **HTTP** (requests) | Shellies smart switch control (`192.168.0.204`) |
+
+---
+
+### Configuration
+
+- **`settings_integration.yaml`**: Database URL, MQTT broker/topics, test commands (check_id, light_on, dark_test), air command, results URL
+- **`settings_coldroom.yaml`**: MQTT topics for Cleanroom, Coldroom, MARTA, ThermalCamera, CO₂ sensor, Shellies air switch
+- User settings saved to `~/.config/integration_ui/settings.yaml`
+- Session persistence via `~/.config/integration_ui/lastsession.txt`
+
+---
+
+### Module Types & Geometry
+
+The project supports three ring layers with different configurations:
+- **L1** (18 modules): Three variants (47mm, 60mm, 72mm) with 2.6mm or 4.0mm spacers
+- **L2** (26 modules): Three variants (40mm, 55mm, 68mm)
+- **L3** (36 modules): Single variant, 5G speed, 2.6mm spacer
+
+Each module has fiber connections (SfibA, SfibB, E3;1-5) and power connections (BINT1, R01;M1.1-12, R01;M2.1-12, R01;M3.1-12).
+
+---
+
+### Entry Points
+
+| Command | Purpose |
+|---|---|
+| `python integration.py` | Launch Burn-In Test Station GUI |
+| `python cold.py` | Launch Cold Room / MARTA Control GUI |
+| `python dbbrowser.py` | Standalone Module DB browser |
+| `python modulesearch.py ` | CLI module info lookup |
+| `python dbrunsearch.py ` | CLI run/session search |
+| `python glue_analyzer.py ` | Glue contact analysis tool |
+| `python drawEvent.py -i .root` | ROOT event display |
+| `python edit_ompo_report.py ` | OMPO report editor |
+
+---
+
+### Test Dependencies
+
+- Python 3 with PyQt5, matplotlib, numpy, requests, paho-mqtt, pyyaml, opencv-python, python-pptx, ROOT
+- Hardware: CAEN power supplies, thermal camera (FLIR/Lepton), Rigol DP116A, Shellies smart relays, FC7/Ph2_ACF readout boards
+
+---
+
+## User
+
+How to save this chat in a markdown ?
+
+---
+
+## Assistant
+
+**Tool: websearch**
+
+---
+
+## Assistant
+
+There are a couple ways:
+
+**Within the TUI**: Type `/export` during a conversation to export the current session.
+
+**From the CLI** (if the TUI doesn't work):
+```bash
+opencode session list # find the session ID
+opencode export --format md -o conversation.md
+```
+
+Alternatively, install a dedicated tool for this:
+```bash
+pip install opencode-log
+opencode-log --format both # generates both HTML and Markdown
+```
+
+Or install the autosave plugin:
+```bash
+npm install -g opencode-autosave-conversation
+# then add to your opencode.json
+```
+
+---
+
diff --git a/settings_coldroom.yaml b/settings_coldroom.yaml
index 988172f..0c9e868 100644
--- a/settings_coldroom.yaml
+++ b/settings_coldroom.yaml
@@ -10,5 +10,6 @@ ThermalCamera:
mqtt_topic: /thermalcamera/#
stitch_camera: camera0
mqtt:
- broker: 192.168.0.45
+ # broker: 192.168.0.45
+ broker: pccmslab1
port: 1883
diff --git a/settings_integration.yaml b/settings_integration.yaml
index 81a90d2..a5c7137 100644
--- a/settings_integration.yaml
+++ b/settings_integration.yaml
@@ -5,10 +5,12 @@ check_id_command: cd /home/thermal/BurnIn_moduleTest && python3 moduleTest.py --
dark_test_command: cd /home/thermal/BurnIn_moduleTest && python3 moduleTest.py --module
{module_id} --slot `echo {fiber_endpoint} | perl -pe 's/.*OG//'` --board `echo {fiber_endpoint}
| perl -pe 's/_.*//'` -c calibrationandpedenoise --session {session} | tee /tmp/analysis.log
-db_url: http://192.168.0.45:5000
+# db_url: http://192.168.0.45:5000
+db_url: http://pccmslab1:5000
light_on_command: cd /home/thermal/BurnIn_moduleTest && python3 moduleTest.py --module
{module_id} --slot `echo {fiber_endpoint} | perl -pe 's/.*OG//'` --board `echo {fiber_endpoint}
| perl -pe 's/_.*//'` -c eyeOpening --session {session}
-mqtt_server: 192.168.0.45
+mqtt_server: pccmslab1 #192.168.0.45
+# mqtt_server: 192.168.0.45
mqtt_topic: /ar/thermal/image
results_url: file:///tmp/latest_ana/index.html
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..bc2f955
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,12 @@
+"""
+pytest configuration for the integration_tools test suite.
+Shows the test docstring alongside the test name in verbose output.
+"""
+
+
+def pytest_collection_modifyitems(items):
+ for item in items:
+ lines = (item.obj.__doc__ or "").strip().splitlines()
+ doc = lines[0].strip() if lines else ""
+ if doc:
+ item._nodeid = f"{item.nodeid} [{doc}]"
diff --git a/tests/simple_test.py b/tests/simple_test.py
index 176186e..b43b9cc 100644
--- a/tests/simple_test.py
+++ b/tests/simple_test.py
@@ -9,52 +9,54 @@
# Add the parent directory to sys.path to import the modules
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
def test_data_type_preservation():
"""Test the core data type preservation functionality without GUI"""
-
+
# Create a simple mock class with just the data type preservation methods
class MockModuleDB:
def __init__(self):
self.original_data_types = {}
-
+
def store_data_types(self, data, path):
"""Store the original data types for later reconstruction"""
if isinstance(data, dict):
- self.original_data_types[path] = 'dict'
+ self.original_data_types[path] = "dict"
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
self.store_data_types(value, current_path)
elif isinstance(data, list):
- self.original_data_types[path] = 'list'
+ self.original_data_types[path] = "list"
for i, value in enumerate(data):
current_path = f"{path}[{i}]"
self.store_data_types(value, current_path)
else:
self.original_data_types[path] = type(data).__name__
-
+
def extract_list_index(self, path):
"""Extract list index from path like 'children[0]' -> 0"""
import re
- match = re.search(r'\[(\d+)\]$', path)
+
+ match = re.search(r"\[(\d+)\]$", path)
return int(match.group(1)) if match else 0
-
+
def simulate_type_conversion(self, value, original_type):
"""Simulate the type conversion that would happen"""
- if original_type == 'bool':
- return str(value).lower() == 'true'
- elif original_type == 'int':
+ if original_type == "bool":
+ return str(value).lower() == "true"
+ elif original_type == "int":
try:
return int(value)
except ValueError:
return value
- elif original_type == 'float':
+ elif original_type == "float":
try:
return float(value)
except ValueError:
return value
else:
return str(value)
-
+
# Test data
test_data = {
"moduleName": "TEST_MODULE_001",
@@ -64,33 +66,30 @@ def simulate_type_conversion(self, value, original_type):
"readings": [1.1, 2.2, 3.3],
"metadata": {
"created": "2024-01-01",
- "flags": {
- "enabled": False,
- "priority": 1
- }
+ "flags": {"enabled": False, "priority": 1},
},
"children": [
{"name": "child1", "type": "sensor"},
- {"name": "child2", "type": "actuator"}
- ]
+ {"name": "child2", "type": "actuator"},
+ ],
}
-
+
# Create mock instance
mock_db = MockModuleDB()
-
+
# Store data types
mock_db.store_data_types(test_data, "")
-
+
# Verify types were stored correctly
expected_types = {
"": "dict",
"moduleName": "str",
- "isActive": "bool",
+ "isActive": "bool",
"temperature": "float",
"count": "int",
"readings": "list",
"readings[0]": "float",
- "readings[1]": "float",
+ "readings[1]": "float",
"readings[2]": "float",
"metadata": "dict",
"metadata.created": "str",
@@ -103,20 +102,22 @@ def simulate_type_conversion(self, value, original_type):
"children[0].type": "str",
"children[1]": "dict",
"children[1].name": "str",
- "children[1].type": "str"
+ "children[1].type": "str",
}
-
+
print("Testing data type storage...")
all_passed = True
-
+
for path, expected_type in expected_types.items():
stored_type = mock_db.original_data_types.get(path)
if stored_type != expected_type:
- print(f"❌ FAIL: Path '{path}' expected '{expected_type}' but got '{stored_type}'")
+ print(
+ f"❌ FAIL: Path '{path}' expected '{expected_type}' but got '{stored_type}'"
+ )
all_passed = False
else:
print(f"✅ PASS: Path '{path}' -> '{expected_type}'")
-
+
# Test type conversion
print("\nTesting type conversion...")
test_conversions = [
@@ -125,26 +126,30 @@ def simulate_type_conversion(self, value, original_type):
("42", "int", 42),
("3.14", "float", 3.14),
("hello", "str", "hello"),
- ("invalid", "int", "invalid") # Should fall back to string
+ ("invalid", "int", "invalid"), # Should fall back to string
]
-
+
for input_val, input_type, expected_output in test_conversions:
result = mock_db.simulate_type_conversion(input_val, input_type)
if result == expected_output and type(result) == type(expected_output):
- print(f"✅ PASS: '{input_val}' ({input_type}) -> {result} ({type(result).__name__})")
+ print(
+ f"✅ PASS: '{input_val}' ({input_type}) -> {result} ({type(result).__name__})"
+ )
else:
- print(f"❌ FAIL: '{input_val}' ({input_type}) -> {result} ({type(result).__name__}), expected {expected_output} ({type(expected_output).__name__})")
+ print(
+ f"❌ FAIL: '{input_val}' ({input_type}) -> {result} ({type(result).__name__}), expected {expected_output} ({type(expected_output).__name__})"
+ )
all_passed = False
-
+
# Test list index extraction
print("\nTesting list index extraction...")
index_tests = [
("children[0]", 0),
("readings[5]", 5),
("data.items[12]", 12),
- ("simple_path", 0)
+ ("simple_path", 0),
]
-
+
for path, expected_index in index_tests:
result = mock_db.extract_list_index(path)
if result == expected_index:
@@ -152,7 +157,7 @@ def simulate_type_conversion(self, value, original_type):
else:
print(f"❌ FAIL: '{path}' -> {result}, expected {expected_index}")
all_passed = False
-
+
print(f"\n{'='*50}")
if all_passed:
print("🎉 ALL TESTS PASSED! Data type preservation is working correctly.")
@@ -161,6 +166,7 @@ def simulate_type_conversion(self, value, original_type):
print("❌ SOME TESTS FAILED. Check the implementation.")
return False
+
if __name__ == "__main__":
success = test_data_type_preservation()
sys.exit(0 if success else 1)
diff --git a/tests/test_data_generator.py b/tests/test_data_generator.py
index 45e1c8d..90dc30d 100644
--- a/tests/test_data_generator.py
+++ b/tests/test_data_generator.py
@@ -8,14 +8,14 @@
class ModuleTestDataGenerator:
"""Generate realistic test data for module database testing"""
-
+
LAYER_TYPES = ["L1_47", "L1_60", "L1_72", "L2_40", "L2_55", "L2_68", "L3"]
SPACER_SIZES = ["18", "26", "40"]
SPEEDS = ["5G", "10G"]
GRADES = ["A++", "A+", "A", "B", "C"]
STATUSES = ["active", "inactive", "testing", "failed", "retired"]
CENTERS = ["Pisa", "CERN", "Other"]
-
+
@staticmethod
def generate_module_name(layer_type=None, spacer=None, speed=None):
"""Generate a realistic module name"""
@@ -25,89 +25,91 @@ def generate_module_name(layer_type=None, spacer=None, speed=None):
spacer = random.choice(ModuleTestDataGenerator.SPACER_SIZES)
if not speed:
speed = random.choice(ModuleTestDataGenerator.SPEEDS)
-
+
speed_suffix = "5" if speed == "5G" else "10"
serial = random.randint(1000, 9999)
-
+
return f"{layer_type}_{spacer}_{speed_suffix}_{serial:04d}"
-
+
@staticmethod
def generate_sensor_readings(count=10):
"""Generate realistic sensor readings"""
base_temp = 25.0
return [round(base_temp + random.uniform(-2.0, 2.0), 2) for _ in range(count)]
-
+
@staticmethod
def generate_child_component():
"""Generate a child component structure"""
component_types = ["PS Read-out Hybrid", "Sensor", "Connector", "Cable"]
component_type = random.choice(component_types)
-
+
base_component = {
"childName": f"{component_type}_{random.randint(1, 999):03d}",
"childType": component_type,
"serialNumber": f"SN{random.randint(100000, 999999)}",
- "status": random.choice(ModuleTestDataGenerator.STATUSES)
+ "status": random.choice(ModuleTestDataGenerator.STATUSES),
}
-
+
# Add specific details based on component type
if component_type == "PS Read-out Hybrid":
base_component["details"] = {
"ALPGBT_BANDWIDTH": random.choice(["5Gbps", "10Gbps"]),
"FIRMWARE_VERSION": f"v{random.randint(1, 5)}.{random.randint(0, 9)}",
"TEMPERATURE_LIMIT": random.randint(60, 80),
- "POWER_CONSUMPTION": round(random.uniform(2.0, 8.0), 2)
+ "POWER_CONSUMPTION": round(random.uniform(2.0, 8.0), 2),
}
elif component_type == "Sensor":
base_component["details"] = {
"SENSOR_TYPE": random.choice(["temperature", "humidity", "pressure"]),
- "CALIBRATION_DATE": (datetime.now() - timedelta(days=random.randint(0, 365))).isoformat(),
- "READINGS": ModuleTestDataGenerator.generate_sensor_readings()
+ "CALIBRATION_DATE": (
+ datetime.now() - timedelta(days=random.randint(0, 365))
+ ).isoformat(),
+ "READINGS": ModuleTestDataGenerator.generate_sensor_readings(),
}
-
+
return base_component
-
+
@staticmethod
def generate_crate_side_connections():
"""Generate crate side connections"""
connections = {}
-
+
# Randomly generate some connections
for i in range(random.randint(0, 4)):
crate_name = f"Crate_{chr(65 + i)}" # Crate_A, Crate_B, etc.
slot_count = random.randint(1, 3)
slots = [f"Slot_{j+1}" for j in range(slot_count)]
connections[crate_name] = slots
-
+
return connections
-
+
@staticmethod
def generate_full_module_data(module_name=None):
"""Generate a complete module data structure"""
if not module_name:
module_name = ModuleTestDataGenerator.generate_module_name()
-
+
# Extract info from module name
name_parts = module_name.split("_")
layer_type = "_".join(name_parts[:2]) if len(name_parts) >= 2 else "L1_47"
spacer = name_parts[1] if len(name_parts) > 1 else "26"
speed_suffix = name_parts[2] if len(name_parts) > 2 else "10"
speed = "10G" if speed_suffix == "10" else "5G"
-
+
# Generate children
num_children = random.randint(1, 5)
children = {}
children_list = []
-
+
for i in range(num_children):
child = ModuleTestDataGenerator.generate_child_component()
child_name = child["childName"]
children[child_name] = child
children_list.append(child)
-
+
# Generate connections
crate_connections = ModuleTestDataGenerator.generate_crate_side_connections()
-
+
module_data = {
"moduleName": module_name,
"inventorySlot": f"INV_{random.randint(1000, 9999)}",
@@ -117,47 +119,61 @@ def generate_full_module_data(module_name=None):
"spacer": spacer,
"Current Center": random.choice(ModuleTestDataGenerator.CENTERS),
"crateSide": crate_connections,
- "mounted_on": f"TestBench_{random.randint(1, 10)}" if random.random() > 0.5 else "",
+ "mounted_on": (
+ f"TestBench_{random.randint(1, 10)}" if random.random() > 0.5 else ""
+ ),
"children": children, # Dictionary format
"childrenList": children_list, # List format - for testing type preservation
"details": {
"DESCRIPTION": f"Test module {module_name}",
- "MANUFACTURE_DATE": (datetime.now() - timedelta(days=random.randint(30, 730))).isoformat(),
- "LAST_TESTED": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(),
+ "MANUFACTURE_DATE": (
+ datetime.now() - timedelta(days=random.randint(30, 730))
+ ).isoformat(),
+ "LAST_TESTED": (
+ datetime.now() - timedelta(days=random.randint(0, 30))
+ ).isoformat(),
"TEST_CYCLES": random.randint(0, 1000),
"TEMPERATURE_RANGE": {
"min": round(random.uniform(-40, -20), 1),
"max": round(random.uniform(60, 85), 1),
- "operating": round(random.uniform(20, 30), 1)
+ "operating": round(random.uniform(20, 30), 1),
},
"ELECTRICAL_SPECS": {
"voltage": round(random.uniform(3.0, 5.0), 2),
"current": round(random.uniform(0.5, 2.0), 3),
- "resistance": round(random.uniform(10, 100), 1)
+ "resistance": round(random.uniform(10, 100), 1),
},
"QA_CHECKS": [
{"test": "visual_inspection", "passed": True, "date": "2024-01-15"},
- {"test": "electrical_test", "passed": random.choice([True, False]), "date": "2024-01-16"},
- {"test": "thermal_cycling", "passed": True, "date": "2024-01-17"}
- ]
+ {
+ "test": "electrical_test",
+ "passed": random.choice([True, False]),
+ "date": "2024-01-16",
+ },
+ {"test": "thermal_cycling", "passed": True, "date": "2024-01-17"},
+ ],
},
"metadata": {
"created_by": f"user_{random.randint(1, 100)}",
- "created_at": (datetime.now() - timedelta(days=random.randint(1, 365))).isoformat(),
+ "created_at": (
+ datetime.now() - timedelta(days=random.randint(1, 365))
+ ).isoformat(),
"updated_at": datetime.now().isoformat(),
"version": random.randint(1, 10),
- "tags": random.sample(["prototype", "production", "test", "experimental", "certified"],
- random.randint(1, 3)),
+ "tags": random.sample(
+ ["prototype", "production", "test", "experimental", "certified"],
+ random.randint(1, 3),
+ ),
"flags": {
"is_prototype": random.choice([True, False]),
"requires_special_handling": random.choice([True, False]),
- "has_known_issues": random.choice([True, False])
- }
- }
+ "has_known_issues": random.choice([True, False]),
+ },
+ },
}
-
+
return module_data
-
+
@staticmethod
def generate_multiple_modules(count=5):
"""Generate multiple module data structures"""
@@ -170,14 +186,14 @@ def generate_multiple_modules(count=5):
# Example usage and test data
if __name__ == "__main__":
generator = ModuleTestDataGenerator()
-
+
# Generate a single module
single_module = generator.generate_full_module_data()
print("Single module example:")
print(f"Name: {single_module['moduleName']}")
print(f"Children count: {len(single_module['children'])}")
print(f"Has connections: {bool(single_module['crateSide'])}")
-
+
# Generate multiple modules
multiple_modules = generator.generate_multiple_modules(3)
print(f"\nGenerated {len(multiple_modules)} modules")
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 8fbba2b..992446c 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -14,7 +14,7 @@
class TestModuleDBWithRealData(unittest.TestCase):
"""Integration tests using realistic generated data"""
-
+
@classmethod
def setUpClass(cls):
"""Set up QApplication for GUI tests"""
@@ -22,47 +22,58 @@ def setUpClass(cls):
cls.app = QApplication([])
else:
cls.app = QApplication.instance()
-
+
def setUp(self):
"""Set up test fixtures"""
- with patch('db.module_db.Ui_ModuleDBWidget'):
+ with patch("db.module_db.Ui_ModuleDBWidget"):
self.module_db = ModuleDB()
-
+
# Mock UI components
self.module_db.ui = Mock()
self.module_db.ui.detailsTree = Mock()
self.module_db.ui.moduleNameLabel = Mock()
self.module_db.ui.tabWidget = Mock()
self.module_db.ui.moduleDetailsTab = Mock()
-
+
# Generate test data
self.generator = ModuleTestDataGenerator()
self.test_modules = self.generator.generate_multiple_modules(5)
-
+
def test_complex_data_type_preservation(self):
"""Test data type preservation with complex, realistic module data"""
for module_data in self.test_modules:
- with self.subTest(module=module_data['moduleName']):
+ with self.subTest(module=module_data["moduleName"]):
# Store original data types
self.module_db.original_data_types = {}
self.module_db.store_data_types(module_data, "")
-
+
# Verify key data types are preserved
self.assertEqual(self.module_db.original_data_types[""], "dict")
- self.assertEqual(self.module_db.original_data_types["moduleName"], "str")
-
+ self.assertEqual(
+ self.module_db.original_data_types["moduleName"], "str"
+ )
+
# Check children structure (both dict and list formats)
if "children" in module_data:
- self.assertEqual(self.module_db.original_data_types["children"], "dict")
+ self.assertEqual(
+ self.module_db.original_data_types["children"], "dict"
+ )
if "childrenList" in module_data:
- self.assertEqual(self.module_db.original_data_types["childrenList"], "list")
-
+ self.assertEqual(
+ self.module_db.original_data_types["childrenList"], "list"
+ )
+
# Check nested structures
if "details" in module_data:
- self.assertEqual(self.module_db.original_data_types["details"], "dict")
+ self.assertEqual(
+ self.module_db.original_data_types["details"], "dict"
+ )
if "QA_CHECKS" in module_data["details"]:
- self.assertEqual(self.module_db.original_data_types["details.QA_CHECKS"], "list")
-
+ self.assertEqual(
+ self.module_db.original_data_types["details.QA_CHECKS"],
+ "list",
+ )
+
# Check metadata flags
if "metadata" in module_data and "flags" in module_data["metadata"]:
flags = module_data["metadata"]["flags"]
@@ -70,10 +81,9 @@ def test_complex_data_type_preservation(self):
flag_path = f"metadata.flags.{flag_name}"
if isinstance(flag_value, bool):
self.assertEqual(
- self.module_db.original_data_types[flag_path],
- "bool"
+ self.module_db.original_data_types[flag_path], "bool"
)
-
+
def test_list_order_preservation(self):
"""Test that list order is preserved during roundtrip"""
# Create a module with ordered lists
@@ -81,19 +91,19 @@ def test_list_order_preservation(self):
"ordered_list": ["first", "second", "third", "fourth"],
"qa_checks": [
{"step": 1, "name": "visual"},
- {"step": 2, "name": "electrical"},
- {"step": 3, "name": "thermal"}
- ]
+ {"step": 2, "name": "electrical"},
+ {"step": 3, "name": "thermal"},
+ ],
}
-
+
# Store data types
self.module_db.original_data_types = {}
self.module_db.store_data_types(test_data, "")
-
+
# Simulate reconstruction for ordered_list
mock_list_item = Mock()
mock_list_item.childCount.return_value = 4
-
+
# Create mock children in specific order
children = []
for i, value in enumerate(test_data["ordered_list"]):
@@ -102,14 +112,14 @@ def test_list_order_preservation(self):
child.childCount.return_value = 0
child.text.return_value = value
children.append(child)
-
+
mock_list_item.child.side_effect = children
-
+
result = self.module_db.get_item_value(mock_list_item, "ordered_list")
-
+
self.assertEqual(result, test_data["ordered_list"])
self.assertIsInstance(result, list)
-
+
def test_nested_dict_list_combinations(self):
"""Test complex nested combinations of dicts and lists"""
complex_data = {
@@ -119,44 +129,41 @@ def test_nested_dict_list_combinations(self):
"readings": [25.1, 25.2, 25.0],
"metadata": {
"calibrated": True,
- "calibration_points": [0.0, 25.0, 50.0, 75.0, 100.0]
- }
+ "calibration_points": [0.0, 25.0, 50.0, 75.0, 100.0],
+ },
},
{
- "name": "pressure_sensor_1",
+ "name": "pressure_sensor_1",
"readings": [101325, 101330, 101320],
- "metadata": {
- "calibrated": False,
- "calibration_points": []
- }
- }
+ "metadata": {"calibrated": False, "calibration_points": []},
+ },
]
}
-
+
# Store data types
self.module_db.original_data_types = {}
self.module_db.store_data_types(complex_data, "")
-
+
# Verify complex nested paths
expected_types = {
"": "dict",
- "sensors": "list",
+ "sensors": "list",
"sensors[0]": "dict",
"sensors[0].readings": "list",
"sensors[0].readings[0]": "float",
"sensors[0].metadata": "dict",
"sensors[0].metadata.calibrated": "bool",
"sensors[0].metadata.calibration_points": "list",
- "sensors[1].metadata.calibrated": "bool"
+ "sensors[1].metadata.calibrated": "bool",
}
-
+
for path, expected_type in expected_types.items():
self.assertEqual(
- self.module_db.original_data_types[path],
+ self.module_db.original_data_types[path],
expected_type,
- f"Wrong type for path {path}"
+ f"Wrong type for path {path}",
)
-
+
def test_edge_case_data_types(self):
"""Test edge cases with various data types"""
edge_case_data = {
@@ -170,13 +177,13 @@ def test_edge_case_data_types(self):
"negative_int": -42,
"scientific_notation": 1.23e-4,
"unicode_string": "测试数据",
- "special_chars": "!@#$%^&*()"
+ "special_chars": "!@#$%^&*()",
}
-
+
# Store data types
self.module_db.original_data_types = {}
self.module_db.store_data_types(edge_case_data, "")
-
+
# Test type preservation for edge cases
test_cases = [
("null_value", None, "NoneType"),
@@ -186,126 +193,144 @@ def test_edge_case_data_types(self):
("empty_list", [], "list"),
("empty_dict", {}, "dict"),
("negative_int", -42, "int"),
- ("scientific_notation", 1.23e-4, "float")
+ ("scientific_notation", 1.23e-4, "float"),
]
-
+
for field, value, expected_type in test_cases:
self.assertEqual(
self.module_db.original_data_types[field],
expected_type,
- f"Wrong type stored for {field}"
+ f"Wrong type stored for {field}",
)
-
+
# Test value reconstruction
mock_item = Mock()
mock_item.childCount.return_value = 0
mock_item.text.return_value = str(value)
-
+
if value is not None: # Skip None values for text conversion
result = self.module_db.get_item_value(mock_item, field)
if expected_type in ["int", "float", "bool"]:
self.assertEqual(result, value)
self.assertIsInstance(result, type(value))
-
- @patch('db.module_db.ModuleDB.make_api_request')
+
+ @patch("db.module_db.ModuleDB.make_api_request")
def test_full_roundtrip_with_realistic_data(self, mock_api):
"""Test complete save/load cycle with realistic module data"""
# Use a realistic module
original_module = self.test_modules[0]
-
+
# Set up the module DB state
- self.module_db.current_module_id = original_module['moduleName']
-
+ self.module_db.current_module_id = original_module["moduleName"]
+
# Store the original data types that would be created
self.module_db.original_data_types = {}
self.module_db.store_data_types(original_module, "")
-
+
# Simulate tree reconstruction
- with patch.object(self.module_db, 'tree_to_dict_preserving_types') as mock_tree_to_dict:
+ with patch.object(
+ self.module_db, "tree_to_dict_preserving_types"
+ ) as mock_tree_to_dict:
# Return a modified version of the data
modified_data = original_module.copy()
- modified_data['status'] = 'updated_status'
+ modified_data["status"] = "updated_status"
mock_tree_to_dict.return_value = modified_data
-
+
# Mock API calls for save (only the ones needed for save_module_details)
mock_api.side_effect = [
(True, original_module), # GET for current data
- (True, {"success": True}) # PUT for update
+ (True, {"success": True}), # PUT for update
]
-
- with patch.object(self.module_db, 'show_info_dialog'), \
- patch.object(self.module_db, 'update_module_list'):
-
+
+ with patch.object(self.module_db, "show_info_dialog"), patch.object(
+ self.module_db, "update_module_list"
+ ):
+
# Call save
self.module_db.save_module_details()
-
+
# Verify the type-preserving method was called
mock_tree_to_dict.assert_called_once()
-
+
# Verify the correct number of API calls were made
self.assertEqual(mock_api.call_count, 2)
class TestDataIntegrity(unittest.TestCase):
"""Test data integrity and consistency"""
-
+
def setUp(self):
"""Set up test fixtures"""
self.generator = ModuleTestDataGenerator()
-
+
def test_generated_data_consistency(self):
"""Test that generated data is internally consistent"""
modules = self.generator.generate_multiple_modules(10)
-
+
for module in modules:
- with self.subTest(module=module['moduleName']):
+ with self.subTest(module=module["moduleName"]):
# Check module name format
- name_parts = module['moduleName'].split('_')
- self.assertGreaterEqual(len(name_parts), 3, "Module name should have at least 3 parts")
-
+ name_parts = module["moduleName"].split("_")
+ self.assertGreaterEqual(
+ len(name_parts), 3, "Module name should have at least 3 parts"
+ )
+
# Check speed consistency
- if 'speed' in module:
- speed = module['speed']
- self.assertIn(speed, ['5G', '10G'], f"Invalid speed: {speed}")
-
+ if "speed" in module:
+ speed = module["speed"]
+ self.assertIn(speed, ["5G", "10G"], f"Invalid speed: {speed}")
+
# Check children structure
- if 'children' in module:
- self.assertIsInstance(module['children'], dict, "Children should be a dict")
-
- if 'childrenList' in module:
- self.assertIsInstance(module['childrenList'], list, "ChildrenList should be a list")
-
+ if "children" in module:
+ self.assertIsInstance(
+ module["children"], dict, "Children should be a dict"
+ )
+
+ if "childrenList" in module:
+ self.assertIsInstance(
+ module["childrenList"], list, "ChildrenList should be a list"
+ )
+
# Check nested data structures
- if 'details' in module and 'QA_CHECKS' in module['details']:
- qa_checks = module['details']['QA_CHECKS']
+ if "details" in module and "QA_CHECKS" in module["details"]:
+ qa_checks = module["details"]["QA_CHECKS"]
self.assertIsInstance(qa_checks, list, "QA_CHECKS should be a list")
for check in qa_checks:
- self.assertIsInstance(check, dict, "Each QA check should be a dict")
- self.assertIn('test', check, "Each QA check should have a 'test' field")
- self.assertIn('passed', check, "Each QA check should have a 'passed' field")
-
+ self.assertIsInstance(
+ check, dict, "Each QA check should be a dict"
+ )
+ self.assertIn(
+ "test", check, "Each QA check should have a 'test' field"
+ )
+ self.assertIn(
+ "passed",
+ check,
+ "Each QA check should have a 'passed' field",
+ )
+
def test_data_serialization(self):
"""Test that generated data can be JSON serialized/deserialized"""
modules = self.generator.generate_multiple_modules(3)
-
+
for module in modules:
- with self.subTest(module=module['moduleName']):
+ with self.subTest(module=module["moduleName"]):
try:
# Test JSON serialization
json_str = json.dumps(module, default=str)
-
- # Test JSON deserialization
+
+ # Test JSON deserialization
restored_module = json.loads(json_str)
-
+
# Basic structure should be preserved
self.assertEqual(
- restored_module['moduleName'],
- module['moduleName']
+ restored_module["moduleName"], module["moduleName"]
)
-
+
except (TypeError, ValueError) as e:
- self.fail(f"Failed to serialize/deserialize module {module['moduleName']}: {e}")
+ self.fail(
+ f"Failed to serialize/deserialize module {module['moduleName']}: {e}"
+ )
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main(verbosity=2)
diff --git a/tests/test_module_db.py b/tests/test_module_db.py
index 9efacaf..008e3db 100644
--- a/tests/test_module_db.py
+++ b/tests/test_module_db.py
@@ -14,7 +14,7 @@
class TestModuleDBDataTypePreservation(unittest.TestCase):
"""Test suite for ModuleDB data type preservation functionality"""
-
+
@classmethod
def setUpClass(cls):
"""Set up QApplication for GUI tests"""
@@ -22,19 +22,19 @@ def setUpClass(cls):
cls.app = QApplication([])
else:
cls.app = QApplication.instance()
-
+
def setUp(self):
"""Set up test fixtures"""
# Mock the UI setup
- with patch('db.module_db.Ui_ModuleDBWidget'):
+ with patch("db.module_db.Ui_ModuleDBWidget"):
self.module_db = ModuleDB()
-
+
# Mock the UI components
self.module_db.ui = Mock()
self.module_db.ui.detailsTree = Mock()
self.module_db.ui.detailsTree.invisibleRootItem = Mock()
self.module_db.ui.detailsTree.clear = Mock()
-
+
# Sample test data with mixed types
self.test_data = {
"moduleName": "Test_Module_001",
@@ -47,110 +47,107 @@ def setUp(self):
{
"childName": "Sensor_1",
"childType": "temperature",
- "readings": [25.5, 26.0, 24.8]
+ "readings": [25.5, 26.0, 24.8],
},
{
- "childName": "Sensor_2",
+ "childName": "Sensor_2",
"childType": "pressure",
- "readings": [101325, 101330, 101320]
- }
+ "readings": [101325, 101330, 101320],
+ },
],
"metadata": {
"created": "2024-01-01",
"tags": ["test", "prototype"],
- "config": {
- "timeout": 30,
- "retries": 3,
- "enabled": False
- }
- }
+ "config": {"timeout": 30, "retries": 3, "enabled": False},
+ },
}
-
+
def test_store_data_types(self):
"""Test that data types are correctly stored"""
self.module_db.original_data_types = {}
self.module_db.store_data_types(self.test_data, "")
-
+
# Check root level types
self.assertEqual(self.module_db.original_data_types[""], "dict")
self.assertEqual(self.module_db.original_data_types["moduleName"], "str")
self.assertEqual(self.module_db.original_data_types["version"], "float")
self.assertEqual(self.module_db.original_data_types["isEnabled"], "bool")
self.assertEqual(self.module_db.original_data_types["testCount"], "int")
-
+
# Check nested types
self.assertEqual(self.module_db.original_data_types["children"], "list")
self.assertEqual(self.module_db.original_data_types["children[0]"], "dict")
- self.assertEqual(self.module_db.original_data_types["children[0].readings"], "list")
- self.assertEqual(self.module_db.original_data_types["children[0].readings[0]"], "float")
-
+ self.assertEqual(
+ self.module_db.original_data_types["children[0].readings"], "list"
+ )
+ self.assertEqual(
+ self.module_db.original_data_types["children[0].readings[0]"], "float"
+ )
+
# Check deeply nested types
self.assertEqual(self.module_db.original_data_types["metadata"], "dict")
self.assertEqual(self.module_db.original_data_types["metadata.config"], "dict")
- self.assertEqual(self.module_db.original_data_types["metadata.config.timeout"], "int")
- self.assertEqual(self.module_db.original_data_types["metadata.config.enabled"], "bool")
-
+ self.assertEqual(
+ self.module_db.original_data_types["metadata.config.timeout"], "int"
+ )
+ self.assertEqual(
+ self.module_db.original_data_types["metadata.config.enabled"], "bool"
+ )
+
def test_extract_list_index(self):
"""Test extraction of list indices from paths"""
self.assertEqual(self.module_db.extract_list_index("children[0]"), 0)
self.assertEqual(self.module_db.extract_list_index("children[5]"), 5)
self.assertEqual(self.module_db.extract_list_index("data.items[12]"), 12)
self.assertEqual(self.module_db.extract_list_index("simple_path"), 0)
-
+
def test_get_item_value_leaf_nodes(self):
"""Test value extraction for leaf nodes with type preservation"""
# Set up original data types
self.module_db.original_data_types = {
"string_path": "str",
- "int_path": "int",
+ "int_path": "int",
"float_path": "float",
"bool_true_path": "bool",
- "bool_false_path": "bool"
+ "bool_false_path": "bool",
}
-
+
# Create mock tree items for leaf nodes
string_item = Mock()
string_item.childCount.return_value = 0
string_item.text.return_value = "test_string"
-
+
int_item = Mock()
int_item.childCount.return_value = 0
int_item.text.return_value = "42"
-
+
float_item = Mock()
float_item.childCount.return_value = 0
float_item.text.return_value = "3.14"
-
+
bool_true_item = Mock()
bool_true_item.childCount.return_value = 0
bool_true_item.text.return_value = "true"
-
+
bool_false_item = Mock()
bool_false_item.childCount.return_value = 0
bool_false_item.text.return_value = "false"
-
+
# Test type conversions
self.assertEqual(
- self.module_db.get_item_value(string_item, "string_path"),
- "test_string"
- )
- self.assertEqual(
- self.module_db.get_item_value(int_item, "int_path"),
- 42
+ self.module_db.get_item_value(string_item, "string_path"), "test_string"
)
+ self.assertEqual(self.module_db.get_item_value(int_item, "int_path"), 42)
self.assertAlmostEqual(
- self.module_db.get_item_value(float_item, "float_path"),
- 3.14
+ self.module_db.get_item_value(float_item, "float_path"), 3.14
)
self.assertEqual(
- self.module_db.get_item_value(bool_true_item, "bool_true_path"),
- True
+ self.module_db.get_item_value(bool_true_item, "bool_true_path"), True
)
self.assertEqual(
- self.module_db.get_item_value(bool_false_item, "bool_false_path"),
- False
+ self.module_db.get_item_value(bool_false_item, "bool_false_path"), False
)
-
+
def test_get_item_value_list_reconstruction(self):
"""Test list reconstruction from tree items"""
# Set up original data types for a list
@@ -158,36 +155,36 @@ def test_get_item_value_list_reconstruction(self):
"list_path": "list",
"list_path[0]": "str",
"list_path[1]": "int",
- "list_path[2]": "float"
+ "list_path[2]": "float",
}
-
+
# Create mock tree item for the list
list_item = Mock()
list_item.childCount.return_value = 3
-
+
# Create mock child items
child0 = Mock()
child0.data.return_value = "list_path[0]"
child0.childCount.return_value = 0
child0.text.return_value = "first"
-
+
child1 = Mock()
child1.data.return_value = "list_path[1]"
child1.childCount.return_value = 0
child1.text.return_value = "42"
-
+
child2 = Mock()
child2.data.return_value = "list_path[2]"
child2.childCount.return_value = 0
child2.text.return_value = "3.14"
-
+
list_item.child.side_effect = [child0, child1, child2]
-
+
result = self.module_db.get_item_value(list_item, "list_path")
-
+
self.assertEqual(result, ["first", 42, 3.14])
self.assertIsInstance(result, list)
-
+
def test_get_item_value_dict_reconstruction(self):
"""Test dictionary reconstruction from tree items"""
# Set up original data types for a dict
@@ -195,46 +192,49 @@ def test_get_item_value_dict_reconstruction(self):
"dict_path": "dict",
"dict_path.name": "str",
"dict_path.count": "int",
- "dict_path.enabled": "bool"
+ "dict_path.enabled": "bool",
}
-
+
# Create mock tree item for the dict
dict_item = Mock()
dict_item.childCount.return_value = 3
-
+
# Create mock child items
child0 = Mock()
child0.data.return_value = "dict_path.name"
- child0.text.side_effect = ["name", "test"] # text(0) returns key, text(1) returns value
+ child0.text.side_effect = [
+ "name",
+ "test",
+ ] # text(0) returns key, text(1) returns value
child0.childCount.return_value = 0
-
+
child1 = Mock()
child1.data.return_value = "dict_path.count"
child1.text.side_effect = ["count", "5"]
child1.childCount.return_value = 0
-
+
child2 = Mock()
child2.data.return_value = "dict_path.enabled"
child2.text.side_effect = ["enabled", "true"]
child2.childCount.return_value = 0
-
+
dict_item.child.side_effect = [child0, child1, child2]
-
+
result = self.module_db.get_item_value(dict_item, "dict_path")
-
+
expected = {"name": "test", "count": 5, "enabled": True}
self.assertEqual(result, expected)
self.assertIsInstance(result, dict)
-
+
def test_full_data_roundtrip(self):
"""Test complete data roundtrip: data -> tree -> data"""
# Store original data types
self.module_db.original_data_types = {}
self.module_db.store_data_types(self.test_data, "")
-
+
# Simulate the tree population and reconstruction process
# This is a simplified version since we can't easily mock the full tree widget
-
+
# Test specific paths and values
test_cases = [
("moduleName", "Test_Module_001", str),
@@ -242,42 +242,42 @@ def test_full_data_roundtrip(self):
("isEnabled", True, bool),
("testCount", 42, int),
]
-
+
for path, expected_value, expected_type in test_cases:
# Simulate a leaf item
mock_item = Mock()
mock_item.childCount.return_value = 0
mock_item.text.return_value = str(expected_value)
-
+
result = self.module_db.get_item_value(mock_item, path)
-
+
self.assertEqual(result, expected_value)
self.assertIsInstance(result, expected_type)
-
+
def test_edge_cases(self):
"""Test edge cases and error handling"""
self.module_db.original_data_types = {}
-
+
# Test with None values
test_data_with_none = {"field": None}
self.module_db.store_data_types(test_data_with_none, "")
self.assertEqual(self.module_db.original_data_types["field"], "NoneType")
-
+
# Test with empty structures
empty_data = {"empty_list": [], "empty_dict": {}}
self.module_db.store_data_types(empty_data, "")
self.assertEqual(self.module_db.original_data_types["empty_list"], "list")
self.assertEqual(self.module_db.original_data_types["empty_dict"], "dict")
-
+
# Test invalid numeric conversion
mock_item = Mock()
mock_item.childCount.return_value = 0
mock_item.text.return_value = "not_a_number"
-
+
self.module_db.original_data_types["invalid_int"] = "int"
result = self.module_db.get_item_value(mock_item, "invalid_int")
self.assertEqual(result, "not_a_number") # Should fall back to string
-
+
def test_deeply_nested_structures(self):
"""Test handling of deeply nested data structures"""
nested_data = {
@@ -285,33 +285,31 @@ def test_deeply_nested_structures(self):
"level2": {
"level3": [
{"name": "item1", "values": [1, 2, 3]},
- {"name": "item2", "values": [4, 5, 6]}
+ {"name": "item2", "values": [4, 5, 6]},
]
}
}
}
-
+
self.module_db.original_data_types = {}
self.module_db.store_data_types(nested_data, "")
-
+
# Verify deep nesting is correctly tracked
self.assertEqual(
- self.module_db.original_data_types["level1.level2.level3"],
- "list"
+ self.module_db.original_data_types["level1.level2.level3"], "list"
)
self.assertEqual(
- self.module_db.original_data_types["level1.level2.level3[0].values"],
- "list"
+ self.module_db.original_data_types["level1.level2.level3[0].values"], "list"
)
self.assertEqual(
- self.module_db.original_data_types["level1.level2.level3[0].values[0]"],
- "int"
+ self.module_db.original_data_types["level1.level2.level3[0].values[0]"],
+ "int",
)
class TestModuleDBIntegration(unittest.TestCase):
"""Integration tests for ModuleDB with mocked API calls"""
-
+
@classmethod
def setUpClass(cls):
"""Set up QApplication for GUI tests"""
@@ -319,20 +317,20 @@ def setUpClass(cls):
cls.app = QApplication([])
else:
cls.app = QApplication.instance()
-
+
def setUp(self):
"""Set up test fixtures with mocked components"""
- with patch('db.module_db.Ui_ModuleDBWidget'):
+ with patch("db.module_db.Ui_ModuleDBWidget"):
self.module_db = ModuleDB()
-
+
# Mock UI components
self.module_db.ui = Mock()
self.module_db.ui.detailsTree = Mock()
self.module_db.ui.moduleNameLabel = Mock()
self.module_db.ui.tabWidget = Mock()
self.module_db.ui.moduleDetailsTab = Mock()
-
- @patch('db.module_db.ModuleDB.make_api_request')
+
+ @patch("db.module_db.ModuleDB.make_api_request")
def test_load_module_details_preserves_types(self, mock_api):
"""Test that loading module details preserves data types"""
# Mock API response
@@ -341,79 +339,83 @@ def test_load_module_details_preserves_types(self, mock_api):
"isActive": True,
"temperature": 25.5,
"readings": [1, 2, 3],
- "metadata": {"created": "2024-01-01"}
+ "metadata": {"created": "2024-01-01"},
}
-
+
mock_api.return_value = (True, test_module_data)
-
+
# Set current module ID
self.module_db.current_module_id = "TEST_MODULE_001"
-
+
# Call load_module_details without mocking populate_details_tree
# so that store_data_types() gets called
- with patch.object(self.module_db.ui, 'detailsTree') as mock_tree:
+ with patch.object(self.module_db.ui, "detailsTree") as mock_tree:
# Mock the tree widget methods
mock_tree.clear = Mock()
mock_tree.addTopLevelItem = Mock()
-
+
self.module_db.load_module_details()
-
+
# Verify data types were stored
- self.assertIsNotNone(getattr(self.module_db, 'original_data_types', None))
+ self.assertIsNotNone(getattr(self.module_db, "original_data_types", None))
expected_types = {
"": "dict",
- "moduleName": "str",
+ "moduleName": "str",
"isActive": "bool",
"temperature": "float",
"readings": "list",
"readings[0]": "int",
- "readings[1]": "int",
+ "readings[1]": "int",
"readings[2]": "int",
"metadata": "dict",
- "metadata.created": "str"
+ "metadata.created": "str",
}
self.assertEqual(self.module_db.original_data_types, expected_types)
-
- @patch('db.module_db.ModuleDB.make_api_request')
- @patch('db.module_db.ModuleDB.show_info_dialog')
- @patch('db.module_db.ModuleDB.update_module_list')
- def test_save_module_details_preserves_types(self, mock_update_list, mock_show_info, mock_api):
+
+ @patch("db.module_db.ModuleDB.make_api_request")
+ @patch("db.module_db.ModuleDB.show_info_dialog")
+ @patch("db.module_db.ModuleDB.update_module_list")
+ def test_save_module_details_preserves_types(
+ self, mock_update_list, mock_show_info, mock_api
+ ):
"""Test that saving module details preserves data types"""
# Mock current module data
current_data = {
"moduleName": "TEST_MODULE_001",
"isActive": True,
"temperature": 25.5,
- "readings": [1, 2, 3]
+ "readings": [1, 2, 3],
}
-
+
# Mock API responses
mock_api.side_effect = [
(True, current_data), # GET request
- (True, {"success": True}) # PUT request
+ (True, {"success": True}), # PUT request
]
-
+
# Set up module state
self.module_db.current_module_id = "TEST_MODULE_001"
self.module_db.original_data_types = {}
-
+
# Mock the tree_to_dict_preserving_types method
- with patch.object(self.module_db, 'tree_to_dict_preserving_types') as mock_tree_to_dict:
+ with patch.object(
+ self.module_db, "tree_to_dict_preserving_types"
+ ) as mock_tree_to_dict:
mock_tree_to_dict.return_value = {"moduleName": "TEST_MODULE_001_UPDATED"}
-
+
# Call save_module_details
self.module_db.save_module_details()
-
+
# Verify the preserving method was called
mock_tree_to_dict.assert_called_once()
-
+
# Verify API was called correctly
self.assertEqual(mock_api.call_count, 2)
-
+
# Verify success message was shown
mock_show_info.assert_called_once()
-if __name__ == '__main__':
+if __name__ == "__main__":
# Run the tests
unittest.main(verbosity=2)
diff --git a/tests/test_safety.py b/tests/test_safety.py
new file mode 100644
index 0000000..ae5f8ae
--- /dev/null
+++ b/tests/test_safety.py
@@ -0,0 +1,567 @@
+"""
+Unit tests for coldroom/safety.py.
+
+All tests use MagicMock CAEN and plain dicts for system_status / caen_ch_status
+so no real hardware or MQTT broker is required.
+
+Minimal "fully safe" system_status:
+ {
+ "marta": {"fsm_state": "RUNNING"},
+ "coldroom": {}, # required guard key in Is_it_safe_to_on_lv
+ }
+Extend per-test to trigger the specific condition under test.
+"""
+
+
+"""
+# All tests, grouped by class with progress
+python -m pytest tests/test_safety.py -v
+
+# Just a quick pass/fail summary (no names)
+python -m pytest tests/test_safety.py
+"""
+
+import datetime
+import logging
+import unittest
+from unittest.mock import MagicMock, call
+
+# Silence safety.py's logger during tests — the interlock messages are expected
+# output of intentional test conditions, not real alarms.
+logging.getLogger("coldroom.safety").setLevel(logging.CRITICAL)
+
+from coldroom.safety import (
+ check_door_safe_to_open,
+ check_marta_on_for_IT,
+ check_marta_on_for_OT,
+ soft_interlock_loop,
+ switch_all_hv_off,
+ switch_all_lv_off,
+)
+
+
+# ---------------------------------------------------------------------------
+# Shared helpers
+# ---------------------------------------------------------------------------
+
+def _safe_status():
+ """Minimal system_status for a fully connected, safe system."""
+ return {
+ "marta": {"fsm_state": "RUNNING"},
+ "coldroom": {},
+ }
+
+
+def _make_caen():
+ """MagicMock CAEN that records every .off() call without touching hardware."""
+ caen = MagicMock()
+ caen.off = MagicMock()
+ return caen
+
+
+def _all_off(lv=("LV0.1",), hv=("HV0.1",)):
+ """caen_ch_status dict with every listed channel reported as OFF."""
+ status = {}
+ for ch in lv:
+ status[f"caen_{ch}_IsOn"] = False
+ for ch in hv:
+ status[f"caen_{ch}_IsOn"] = False
+ return status
+
+
+def _recent_ts():
+ """A last_update timestamp that is less than 10 minutes old."""
+ return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+
+def _stale_ts():
+ """A last_update timestamp that is more than 10 minutes old."""
+ return (datetime.datetime.now() - datetime.timedelta(minutes=15)).strftime(
+ "%Y-%m-%d %H:%M:%S"
+ )
+
+
+USED = {"LV": ["LV0.1"], "HV": ["HV0.1"]}
+USED_MULTI = {"LV": ["LV0.1", "LV0.2"], "HV": ["HV0.1", "HV0.2"]}
+
+
+# ---------------------------------------------------------------------------
+# soft_interlock_loop
+# ---------------------------------------------------------------------------
+
+class TestSoftInterlock(unittest.TestCase):
+ """5-second safety loop: monitors MARTA state and cuts power if cooling is lost."""
+
+ # ---- Condition 1: MARTA is not safe (FSM state bad / no coldroom data) --
+
+ def test_marta_disconnected_lv_on_triggers_shutdown(self):
+ """Classic danger: cooling lost, LV still energised."""
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": "DISCONNECTED"}}
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": False}
+ published = []
+
+ is_safe, msg = soft_interlock_loop(
+ status, ch_status, USED, caen, published.append
+ )
+
+ self.assertFalse(is_safe)
+ caen.off.assert_called()
+ self.assertEqual(len(published), 1)
+ self.assertIn("SAFETY INTERLOCK", published[0])
+
+ def test_marta_disconnected_hv_only_on_triggers_shutdown(self):
+ """HV alone being on is sufficient to trigger the interlock."""
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": "DISCONNECTED"}}
+ ch_status = {"caen_LV0.1_IsOn": False, "caen_HV0.1_IsOn": True}
+
+ is_safe, _ = soft_interlock_loop(status, ch_status, USED, caen)
+
+ self.assertFalse(is_safe)
+ caen.off.assert_called()
+
+ def test_marta_disconnected_all_power_off_no_action(self):
+ """MARTA gone but everything already off — nothing to protect, no action."""
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": "DISCONNECTED"}}
+ ch_status = _all_off()
+
+ is_safe, _ = soft_interlock_loop(status, ch_status, USED, caen)
+
+ caen.off.assert_not_called()
+ self.assertTrue(is_safe)
+
+ def test_marta_fsm_none_treated_as_disconnected(self):
+ """FSM state 'NONE' is an offline indicator."""
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": "NONE"}}
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": False}
+
+ is_safe, _ = soft_interlock_loop(status, ch_status, USED, caen)
+
+ self.assertFalse(is_safe)
+ caen.off.assert_called()
+
+ def test_marta_fsm_empty_string_treated_as_disconnected(self):
+ """Empty FSM state means MARTA has not reported yet — assume offline."""
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": ""}}
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": False}
+
+ is_safe, _ = soft_interlock_loop(status, ch_status, USED, caen)
+
+ self.assertFalse(is_safe)
+ caen.off.assert_called()
+
+ def test_missing_coldroom_key_with_power_on_triggers_shutdown(self):
+ """Is_it_safe_to_on_lv requires 'coldroom' to be present; without it → unsafe."""
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": "RUNNING"}} # no 'coldroom'
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": False}
+
+ is_safe, _ = soft_interlock_loop(status, ch_status, USED, caen)
+
+ self.assertFalse(is_safe)
+ caen.off.assert_called()
+
+ # ---- Condition 2: MARTA connected but OT CO2 not flowing ----------------
+
+ def test_marta_running_ot_valve_closed_triggers_shutdown(self):
+ """MARTA connected but serviceroom reports OT valve closed → cut power."""
+ caen = _make_caen()
+ status = {
+ "marta": {"fsm_state": "RUNNING"},
+ "coldroom": {},
+ "serviceroom": {"outer_valve": 0},
+ }
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": False}
+
+ is_safe, msg = soft_interlock_loop(status, ch_status, USED, caen)
+
+ self.assertFalse(is_safe)
+ caen.off.assert_called()
+ self.assertIn("OT CO2", msg)
+
+ def test_marta_running_ot_valve_open_is_safe(self):
+ """Valve confirmed open → no protective action."""
+ caen = _make_caen()
+ status = {
+ "marta": {"fsm_state": "RUNNING"},
+ "coldroom": {},
+ "serviceroom": {"outer_valve": 1},
+ }
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": False}
+
+ is_safe, _ = soft_interlock_loop(status, ch_status, USED, caen)
+
+ self.assertTrue(is_safe)
+ caen.off.assert_not_called()
+
+ def test_marta_running_no_serviceroom_falls_back_to_fsm(self):
+ """Without serviceroom data, RUNNING FSM state is trusted for OT."""
+ caen = _make_caen()
+ status = _safe_status() # no 'serviceroom' key
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": False}
+
+ is_safe, _ = soft_interlock_loop(status, ch_status, USED, caen)
+
+ self.assertTrue(is_safe)
+ caen.off.assert_not_called()
+
+ def test_ot_valve_closed_but_power_off_no_action(self):
+ """OT CO2 not flowing, but all channels already off — nothing to cut."""
+ caen = _make_caen()
+ status = {
+ "marta": {"fsm_state": "RUNNING"},
+ "coldroom": {},
+ "serviceroom": {"outer_valve": 0},
+ }
+ ch_status = _all_off()
+
+ is_safe, _ = soft_interlock_loop(status, ch_status, USED, caen)
+
+ self.assertTrue(is_safe)
+ caen.off.assert_not_called()
+
+ # ---- Fully safe baseline ------------------------------------------------
+
+ def test_fully_safe_baseline_no_action(self):
+ """MARTA running, OT confirmed via FSM, all power off → safe."""
+ caen = _make_caen()
+ is_safe, _ = soft_interlock_loop(_safe_status(), _all_off(), USED, caen)
+ self.assertTrue(is_safe)
+ caen.off.assert_not_called()
+
+ # ---- HV/LV shutdown ordering — critical safety invariant ----------------
+
+ def test_hv_cut_before_lv(self):
+ """
+ HV must be switched off BEFORE LV.
+ Cutting LV first while HV is still ramped risks an uncontrolled
+ discharge through the silicon sensors.
+ """
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": "DISCONNECTED"}}
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": True}
+
+ soft_interlock_loop(status, ch_status, USED, caen)
+
+ calls = caen.off.call_args_list
+ self.assertEqual(len(calls), 2)
+ self.assertEqual(calls[0], call("HV0.1"))
+ self.assertEqual(calls[1], call("LV0.1"))
+
+ def test_all_channels_shut_down_and_hv_first(self):
+ """With multiple channels, every channel is cut and all HV calls precede all LV calls."""
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": "DISCONNECTED"}}
+ ch_status = {
+ "caen_LV0.1_IsOn": True, "caen_LV0.2_IsOn": True,
+ "caen_HV0.1_IsOn": True, "caen_HV0.2_IsOn": True,
+ }
+
+ soft_interlock_loop(status, ch_status, USED_MULTI, caen)
+
+ turned_off = [c.args[0] for c in caen.off.call_args_list]
+ for ch in ("HV0.1", "HV0.2", "LV0.1", "LV0.2"):
+ self.assertIn(ch, turned_off)
+
+ hv_last = max(i for i, ch in enumerate(turned_off) if ch.startswith("HV"))
+ lv_first = min(i for i, ch in enumerate(turned_off) if ch.startswith("LV"))
+ self.assertLess(hv_last, lv_first, "All HV calls must precede all LV calls")
+
+ # ---- Alarm publishing ---------------------------------------------------
+
+ def test_alarm_published_exactly_once_per_trip(self):
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": "DISCONNECTED"}}
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": False}
+ published = []
+
+ soft_interlock_loop(status, ch_status, USED, caen, published.append)
+
+ self.assertEqual(len(published), 1)
+
+ def test_publish_alarm_none_does_not_crash(self):
+ """publish_alarm=None is the test/dry-run mode — must not raise."""
+ caen = _make_caen()
+ status = {"marta": {"fsm_state": "DISCONNECTED"}}
+ ch_status = {"caen_LV0.1_IsOn": True, "caen_HV0.1_IsOn": False}
+
+ is_safe, _ = soft_interlock_loop(status, ch_status, USED, caen, publish_alarm=None)
+
+ self.assertFalse(is_safe)
+
+ def test_no_alarm_when_everything_safe(self):
+ caen = _make_caen()
+ published = []
+
+ soft_interlock_loop(_safe_status(), _all_off(), USED, caen, published.append)
+
+ self.assertEqual(len(published), 0)
+
+
+# ---------------------------------------------------------------------------
+# check_door_safe_to_open
+# ---------------------------------------------------------------------------
+
+class TestDoorSafety(unittest.TestCase):
+ """Door can be opened only when dew point is safe AND HV is off."""
+
+ def _door_safe_status(self, min_temp=-5.0, dew_point=-10.0):
+ """
+ Fully safe status for door-open check:
+ min internal temp (-5 °C) comfortably above dew point (-10 °C) + 1 °C margin.
+ """
+ return {
+ "coldroom": {"ch_temperature": {"value": min_temp}},
+ "marta": {"TT05_CO2": min_temp, "TT06_CO2": min_temp},
+ "cleanroom": {"dewpoint": dew_point, "last_update": _recent_ts()},
+ }
+
+ # ---- Missing data guards ------------------------------------------------
+
+ def test_no_coldroom_key_returns_false(self):
+ is_safe, msg = check_door_safe_to_open({}, {}, USED)
+ self.assertFalse(is_safe)
+ self.assertIn("coldroom", msg.lower())
+
+ def test_no_cleanroom_last_update_treats_as_expired(self):
+ status = {"coldroom": {}, "cleanroom": {"dewpoint": -10.0}} # no last_update
+ is_safe, msg = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertFalse(is_safe)
+ self.assertIn("expired", msg)
+
+ def test_stale_cleanroom_data_blocks_door(self):
+ status = {
+ "coldroom": {},
+ "cleanroom": {"dewpoint": -10.0, "last_update": _stale_ts()},
+ }
+ is_safe, msg = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertFalse(is_safe)
+ self.assertIn("expired", msg)
+
+ def test_unparseable_timestamp_treated_as_expired(self):
+ status = {
+ "coldroom": {},
+ "cleanroom": {"dewpoint": -10.0, "last_update": "not-a-date"},
+ }
+ is_safe, msg = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertFalse(is_safe)
+ self.assertIn("expired", msg)
+
+ def test_no_internal_temps_blocks_door(self):
+ """No MARTA or coldroom temperatures → cannot verify dew point."""
+ status = {
+ "coldroom": {}, # no ch_temperature
+ "cleanroom": {"dewpoint": -10.0, "last_update": _recent_ts()},
+ }
+ is_safe, msg = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertFalse(is_safe)
+ self.assertIn("temperature", msg.lower())
+
+ def test_no_dewpoint_reading_blocks_door(self):
+ status = {
+ "coldroom": {"ch_temperature": {"value": -5.0}},
+ "marta": {"TT05_CO2": -5.0},
+ "cleanroom": {"last_update": _recent_ts()}, # no "dewpoint" key
+ }
+ is_safe, msg = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertFalse(is_safe)
+ self.assertIn("dew point", msg.lower())
+
+ # ---- Dew point logic ----------------------------------------------------
+
+ def test_safe_when_temp_well_above_dew_point(self):
+ """-5 °C internal > -10 °C dew + 1 °C margin (-9 °C) → safe."""
+ status = self._door_safe_status(min_temp=-5.0, dew_point=-10.0)
+ is_safe, msg = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertTrue(is_safe)
+ self.assertIn("YES", msg)
+
+ def test_unsafe_when_temp_below_dew_point_plus_margin(self):
+ """-10 °C internal ≤ -9 °C margin → condensation risk."""
+ status = self._door_safe_status(min_temp=-10.0, dew_point=-10.0)
+ is_safe, msg = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertFalse(is_safe)
+ self.assertIn("NO", msg)
+
+ def test_exact_margin_boundary_is_unsafe(self):
+ """Condition is strictly >; equal to margin means not safe."""
+ # min_temp == dew_point + 1 → -9 == -10 + 1 → NOT strictly greater
+ status = self._door_safe_status(min_temp=-9.0, dew_point=-10.0)
+ is_safe, _ = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertFalse(is_safe)
+
+ def test_dew_point_reason_includes_actual_temperatures(self):
+ """Message must show the numeric values so operators know the actual margin."""
+ status = self._door_safe_status(min_temp=-5.0, dew_point=-10.0)
+ _, msg = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertIn("-5.0", msg)
+ self.assertIn("-10.0", msg)
+
+ # ---- HV checks ----------------------------------------------------------
+
+ def test_hv_on_blocks_door_and_lists_channel(self):
+ status = self._door_safe_status()
+ ch_status = {"caen_HV0.1_IsOn": True}
+ is_safe, msg = check_door_safe_to_open(status, ch_status, USED)
+ self.assertFalse(is_safe)
+ self.assertIn("NO", msg)
+ self.assertIn("HV0.1", msg)
+
+ def test_hv_off_passes_hv_check(self):
+ status = self._door_safe_status()
+ is_safe, msg = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertTrue(is_safe)
+ self.assertIn("all HV channels are off", msg)
+
+ def test_multiple_hv_channels_one_on_blocks_and_names_it(self):
+ status = self._door_safe_status()
+ ch_status = {"caen_HV0.1_IsOn": False, "caen_HV0.2_IsOn": True}
+ used = {"LV": ["LV0.1"], "HV": ["HV0.1", "HV0.2"]}
+ is_safe, msg = check_door_safe_to_open(status, ch_status, used)
+ self.assertFalse(is_safe)
+ self.assertIn("HV0.2", msg)
+ self.assertNotIn("HV0.1", msg) # off channels should not appear
+
+ # ---- Combined verdict ---------------------------------------------------
+
+ def test_both_conditions_met_allows_door(self):
+ status = self._door_safe_status()
+ is_safe, _ = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertTrue(is_safe)
+
+ def test_dew_point_ok_but_hv_on_blocks_door(self):
+ status = self._door_safe_status()
+ is_safe, _ = check_door_safe_to_open(status, {"caen_HV0.1_IsOn": True}, USED)
+ self.assertFalse(is_safe)
+
+ def test_hv_off_but_dew_point_unsafe_blocks_door(self):
+ # min_temp at margin → dew point not safe
+ status = self._door_safe_status(min_temp=-9.0, dew_point=-10.0)
+ is_safe, _ = check_door_safe_to_open(status, _all_off(), USED)
+ self.assertFalse(is_safe)
+
+
+# ---------------------------------------------------------------------------
+# check_marta_on_for_OT / IT
+# ---------------------------------------------------------------------------
+
+class TestMartaCO2Flow(unittest.TestCase):
+ """OT and IT valve checks: FSM state is primary; valve data refines when available."""
+
+ # ---- OT -----------------------------------------------------------------
+
+ def test_ot_no_marta_data_returns_false(self):
+ self.assertFalse(check_marta_on_for_OT({}))
+
+ def test_ot_fsm_disconnected_returns_false(self):
+ self.assertFalse(check_marta_on_for_OT({"marta": {"fsm_state": "DISCONNECTED"}}))
+
+ def test_ot_fsm_none_returns_false(self):
+ self.assertFalse(check_marta_on_for_OT({"marta": {"fsm_state": "NONE"}}))
+
+ def test_ot_fsm_empty_returns_false(self):
+ self.assertFalse(check_marta_on_for_OT({"marta": {"fsm_state": ""}}))
+
+ def test_ot_running_no_serviceroom_trusts_fsm(self):
+ """Without valve data, RUNNING FSM state is sufficient."""
+ self.assertTrue(check_marta_on_for_OT({"marta": {"fsm_state": "RUNNING"}}))
+
+ def test_ot_running_outer_valve_open_returns_true(self):
+ status = {"marta": {"fsm_state": "RUNNING"}, "serviceroom": {"outer_valve": 1}}
+ self.assertTrue(check_marta_on_for_OT(status))
+
+ def test_ot_running_outer_valve_closed_returns_false(self):
+ """Valve data overrides FSM state — closed valve means no CO2 to OT."""
+ status = {"marta": {"fsm_state": "RUNNING"}, "serviceroom": {"outer_valve": 0}}
+ self.assertFalse(check_marta_on_for_OT(status))
+
+ # ---- IT -----------------------------------------------------------------
+
+ def test_it_no_marta_data_returns_false(self):
+ self.assertFalse(check_marta_on_for_IT({}))
+
+ def test_it_fsm_disconnected_returns_false(self):
+ self.assertFalse(check_marta_on_for_IT({"marta": {"fsm_state": "DISCONNECTED"}}))
+
+ def test_it_running_no_serviceroom_trusts_fsm(self):
+ self.assertTrue(check_marta_on_for_IT({"marta": {"fsm_state": "RUNNING"}}))
+
+ def test_it_running_inner_valve_open_returns_true(self):
+ status = {"marta": {"fsm_state": "RUNNING"}, "serviceroom": {"inner_valve": 1}}
+ self.assertTrue(check_marta_on_for_IT(status))
+
+ def test_it_running_inner_valve_closed_returns_false(self):
+ status = {"marta": {"fsm_state": "RUNNING"}, "serviceroom": {"inner_valve": 0}}
+ self.assertFalse(check_marta_on_for_IT(status))
+
+ def test_ot_and_it_valves_are_independent(self):
+ """OT valve closed should not affect IT result and vice versa."""
+ status = {
+ "marta": {"fsm_state": "RUNNING"},
+ "serviceroom": {"outer_valve": 0, "inner_valve": 1},
+ }
+ self.assertFalse(check_marta_on_for_OT(status))
+ self.assertTrue(check_marta_on_for_IT(status))
+
+
+# ---------------------------------------------------------------------------
+# switch_all_hv_off / switch_all_lv_off
+# ---------------------------------------------------------------------------
+
+class TestSwitchHelpers(unittest.TestCase):
+ """Direct tests for the channel-switching primitives."""
+
+ def test_switch_hv_off_calls_caen_off_for_each_channel(self):
+ caen = _make_caen()
+ used = {"LV": [], "HV": ["HV0.1", "HV0.2"]}
+ result = switch_all_hv_off(caen, used)
+ self.assertTrue(result)
+ caen.off.assert_any_call("HV0.1")
+ caen.off.assert_any_call("HV0.2")
+
+ def test_switch_lv_off_calls_caen_off_for_each_channel(self):
+ caen = _make_caen()
+ used = {"LV": ["LV0.1", "LV0.2"], "HV": []}
+ result = switch_all_lv_off(caen, used)
+ self.assertTrue(result)
+ caen.off.assert_any_call("LV0.1")
+ caen.off.assert_any_call("LV0.2")
+
+ def test_switch_hv_off_skips_none_entries(self):
+ caen = _make_caen()
+ used = {"LV": [], "HV": [None, "HV0.1"]}
+ switch_all_hv_off(caen, used)
+ caen.off.assert_called_once_with("HV0.1")
+
+ def test_switch_lv_off_skips_none_entries(self):
+ caen = _make_caen()
+ used = {"LV": [None, "LV0.1"], "HV": []}
+ switch_all_lv_off(caen, used)
+ caen.off.assert_called_once_with("LV0.1")
+
+ def test_switch_hv_returns_false_on_caen_error(self):
+ caen = _make_caen()
+ caen.off.side_effect = RuntimeError("TCP connection lost")
+ result = switch_all_hv_off(caen, {"LV": [], "HV": ["HV0.1"]})
+ self.assertFalse(result)
+
+ def test_switch_lv_returns_false_on_caen_error(self):
+ caen = _make_caen()
+ caen.off.side_effect = RuntimeError("TCP connection lost")
+ result = switch_all_lv_off(caen, {"LV": ["LV0.1"], "HV": []})
+ self.assertFalse(result)
+
+ def test_empty_channel_lists_return_true_without_calling_off(self):
+ """No channels configured — nothing to do, still success."""
+ caen = _make_caen()
+ self.assertTrue(switch_all_hv_off(caen, {"LV": [], "HV": []}))
+ self.assertTrue(switch_all_lv_off(caen, {"LV": [], "HV": []}))
+ caen.off.assert_not_called()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/ui/integration_gui.py b/ui/integration_gui.py
index f400260..245ba1d 100644
--- a/ui/integration_gui.py
+++ b/ui/integration_gui.py
@@ -56,7 +56,9 @@ def setupUi(self, MainWindow):
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
self.ringLE = QtWidgets.QLineEdit(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ringLE.sizePolicy().hasHeightForWidth())
@@ -68,7 +70,9 @@ def setupUi(self, MainWindow):
self.label_2.setObjectName("label_2")
self.horizontalLayout.addWidget(self.label_2)
self.positionLE = QtWidgets.QLineEdit(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.positionLE.sizePolicy().hasHeightForWidth())
@@ -81,7 +85,9 @@ def setupUi(self, MainWindow):
self.graphicsView.setMinimumSize(QtCore.QSize(400, 400))
self.graphicsView.setObjectName("graphicsView")
self.verticalLayout.addWidget(self.graphicsView)
- spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
+ spacerItem = QtWidgets.QSpacerItem(
+ 20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum
+ )
self.verticalLayout.addItem(spacerItem)
self.label_7 = QtWidgets.QLabel(self.tab)
self.label_7.setObjectName("label_7")
@@ -121,10 +127,14 @@ def setupUi(self, MainWindow):
self.connectPowerPB.setObjectName("connectPowerPB")
self.gridLayout.addWidget(self.connectPowerPB, 2, 0, 1, 1)
self.connectPowerLED = QtWidgets.QFrame(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.connectPowerLED.sizePolicy().hasHeightForWidth())
+ sizePolicy.setHeightForWidth(
+ self.connectPowerLED.sizePolicy().hasHeightForWidth()
+ )
self.connectPowerLED.setSizePolicy(sizePolicy)
self.connectPowerLED.setMinimumSize(QtCore.QSize(30, 34))
self.connectPowerLED.setStyleSheet("background-color: rgb(85, 170, 0);")
@@ -136,17 +146,23 @@ def setupUi(self, MainWindow):
self.fiberCB.setObjectName("fiberCB")
self.gridLayout.addWidget(self.fiberCB, 0, 1, 1, 1)
self.connectFiberLED = QtWidgets.QFrame(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.connectFiberLED.sizePolicy().hasHeightForWidth())
+ sizePolicy.setHeightForWidth(
+ self.connectFiberLED.sizePolicy().hasHeightForWidth()
+ )
self.connectFiberLED.setSizePolicy(sizePolicy)
self.connectFiberLED.setMinimumSize(QtCore.QSize(30, 34))
self.connectFiberLED.setStyleSheet("background-color: rgb(85, 170, 0);")
self.connectFiberLED.setFrameShape(QtWidgets.QFrame.NoFrame)
self.connectFiberLED.setObjectName("connectFiberLED")
self.gridLayout.addWidget(self.connectFiberLED, 0, 3, 1, 1)
- spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ spacerItem1 = QtWidgets.QSpacerItem(
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
+ )
self.gridLayout.addItem(spacerItem1, 0, 2, 1, 1)
self.verticalLayout.addLayout(self.gridLayout)
self.horizontalLayout_8.addLayout(self.verticalLayout)
@@ -161,10 +177,14 @@ def setupUi(self, MainWindow):
self.checkIDlabel = QtWidgets.QLabel(self.tab)
self.checkIDlabel.setObjectName("checkIDlabel")
self.horizontalLayout_3.addWidget(self.checkIDlabel)
- spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ spacerItem2 = QtWidgets.QSpacerItem(
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
+ )
self.horizontalLayout_3.addItem(spacerItem2)
self.checkIDLED = QtWidgets.QFrame(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.checkIDLED.sizePolicy().hasHeightForWidth())
@@ -177,12 +197,18 @@ def setupUi(self, MainWindow):
self.verticalLayout_5.addLayout(self.horizontalLayout_3)
self.gridLayout_2 = QtWidgets.QGridLayout()
self.gridLayout_2.setObjectName("gridLayout_2")
- spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ spacerItem3 = QtWidgets.QSpacerItem(
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
+ )
self.gridLayout_2.addItem(spacerItem3, 2, 2, 1, 1)
- spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ spacerItem4 = QtWidgets.QSpacerItem(
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
+ )
self.gridLayout_2.addItem(spacerItem4, 0, 2, 1, 1)
self.hvOFFTestLED = QtWidgets.QFrame(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hvOFFTestLED.sizePolicy().hasHeightForWidth())
@@ -202,7 +228,9 @@ def setupUi(self, MainWindow):
self.hvONTestCB.setObjectName("hvONTestCB")
self.gridLayout_2.addWidget(self.hvONTestCB, 2, 1, 1, 1)
self.hvONTestLED = QtWidgets.QFrame(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hvONTestLED.sizePolicy().hasHeightForWidth())
@@ -253,7 +281,9 @@ def setupUi(self, MainWindow):
self.lvOffButton.setObjectName("lvOffButton")
self.horizontalLayout_4.addWidget(self.lvOffButton)
self.lvLabel = QtWidgets.QLabel(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.lvLabel.sizePolicy().hasHeightForWidth())
@@ -278,7 +308,9 @@ def setupUi(self, MainWindow):
self.hvOffButton.setObjectName("hvOffButton")
self.horizontalLayout_5.addWidget(self.hvOffButton)
self.hvLabel = QtWidgets.QLabel(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hvLabel.sizePolicy().hasHeightForWidth())
@@ -302,7 +334,9 @@ def setupUi(self, MainWindow):
self.airOFFPB.setObjectName("airOFFPB")
self.horizontalLayout_6.addWidget(self.airOFFPB)
self.tMaxLabel = QtWidgets.QLabel(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.tMaxLabel.sizePolicy().hasHeightForWidth())
@@ -310,7 +344,9 @@ def setupUi(self, MainWindow):
self.tMaxLabel.setObjectName("tMaxLabel")
self.horizontalLayout_6.addWidget(self.tMaxLabel)
self.airLed = QtWidgets.QFrame(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.airLed.sizePolicy().hasHeightForWidth())
@@ -321,7 +357,9 @@ def setupUi(self, MainWindow):
self.horizontalLayout_6.addWidget(self.airLed)
self.verticalLayout_5.addLayout(self.horizontalLayout_6)
self.plotWidget = QtWidgets.QWidget(self.tab)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
+ sizePolicy = QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
+ )
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.plotWidget.sizePolicy().hasHeightForWidth())
@@ -351,19 +389,27 @@ def setupUi(self, MainWindow):
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.checkIDLabel)
self.checkIDCommandLE = QtWidgets.QLineEdit(self.commandsGroupBox)
self.checkIDCommandLE.setObjectName("checkIDCommandLE")
- self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.checkIDCommandLE)
+ self.formLayout.setWidget(
+ 0, QtWidgets.QFormLayout.FieldRole, self.checkIDCommandLE
+ )
self.lightOnLabel = QtWidgets.QLabel(self.commandsGroupBox)
self.lightOnLabel.setObjectName("lightOnLabel")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lightOnLabel)
self.lightOnCommandLE = QtWidgets.QLineEdit(self.commandsGroupBox)
self.lightOnCommandLE.setObjectName("lightOnCommandLE")
- self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.lightOnCommandLE)
+ self.formLayout.setWidget(
+ 1, QtWidgets.QFormLayout.FieldRole, self.lightOnCommandLE
+ )
self.darkTestLabel = QtWidgets.QLabel(self.commandsGroupBox)
self.darkTestLabel.setObjectName("darkTestLabel")
- self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.darkTestLabel)
+ self.formLayout.setWidget(
+ 2, QtWidgets.QFormLayout.LabelRole, self.darkTestLabel
+ )
self.darkTestCommandLE = QtWidgets.QLineEdit(self.commandsGroupBox)
self.darkTestCommandLE.setObjectName("darkTestCommandLE")
- self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.darkTestCommandLE)
+ self.formLayout.setWidget(
+ 2, QtWidgets.QFormLayout.FieldRole, self.darkTestCommandLE
+ )
self.label_13 = QtWidgets.QLabel(self.commandsGroupBox)
self.label_13.setObjectName("label_13")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_13)
@@ -383,32 +429,48 @@ def setupUi(self, MainWindow):
self.formLayout_2.setObjectName("formLayout_2")
self.dbEndpointLabel = QtWidgets.QLabel(self.apiGroupBox)
self.dbEndpointLabel.setObjectName("dbEndpointLabel")
- self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.dbEndpointLabel)
+ self.formLayout_2.setWidget(
+ 0, QtWidgets.QFormLayout.LabelRole, self.dbEndpointLabel
+ )
self.dbEndpointLE = QtWidgets.QLineEdit(self.apiGroupBox)
self.dbEndpointLE.setObjectName("dbEndpointLE")
- self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.dbEndpointLE)
+ self.formLayout_2.setWidget(
+ 0, QtWidgets.QFormLayout.FieldRole, self.dbEndpointLE
+ )
self.mqttServerLabel = QtWidgets.QLabel(self.apiGroupBox)
self.mqttServerLabel.setObjectName("mqttServerLabel")
- self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.mqttServerLabel)
+ self.formLayout_2.setWidget(
+ 1, QtWidgets.QFormLayout.LabelRole, self.mqttServerLabel
+ )
self.mqttServerLE = QtWidgets.QLineEdit(self.apiGroupBox)
self.mqttServerLE.setObjectName("mqttServerLE")
- self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.mqttServerLE)
+ self.formLayout_2.setWidget(
+ 1, QtWidgets.QFormLayout.FieldRole, self.mqttServerLE
+ )
self.mqttTopicLabel = QtWidgets.QLabel(self.apiGroupBox)
self.mqttTopicLabel.setObjectName("mqttTopicLabel")
- self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.mqttTopicLabel)
+ self.formLayout_2.setWidget(
+ 2, QtWidgets.QFormLayout.LabelRole, self.mqttTopicLabel
+ )
self.mqttTopicLE = QtWidgets.QLineEdit(self.apiGroupBox)
self.mqttTopicLE.setObjectName("mqttTopicLE")
- self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.mqttTopicLE)
+ self.formLayout_2.setWidget(
+ 2, QtWidgets.QFormLayout.FieldRole, self.mqttTopicLE
+ )
self.verticalLayout_6.addWidget(self.apiGroupBox)
self.horizontalLayout_9 = QtWidgets.QHBoxLayout()
self.horizontalLayout_9.setObjectName("horizontalLayout_9")
self.applySettingsPB = QtWidgets.QPushButton(self.tab_3)
self.applySettingsPB.setObjectName("applySettingsPB")
self.horizontalLayout_9.addWidget(self.applySettingsPB)
- spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ spacerItem5 = QtWidgets.QSpacerItem(
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
+ )
self.horizontalLayout_9.addItem(spacerItem5)
self.verticalLayout_6.addLayout(self.horizontalLayout_9)
- spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+ spacerItem6 = QtWidgets.QSpacerItem(
+ 20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding
+ )
self.verticalLayout_6.addItem(spacerItem6)
self.tabWidget.addTab(self.tab_3, "")
self.tab_4 = QtWidgets.QWidget()
@@ -448,10 +510,20 @@ def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.label_12.setText(_translate("MainWindow", "Operator"))
self.label_15.setText(_translate("MainWindow", "Comments"))
- self.label_5.setText(_translate("MainWindow", "Ring:
"))
+ self.label_5.setText(
+ _translate(
+ "MainWindow",
+ 'Ring:
',
+ )
+ )
self.label.setText(_translate("MainWindow", "Ring ID"))
self.label_2.setText(_translate("MainWindow", "Position"))
- self.label_7.setText(_translate("MainWindow", "Module:
"))
+ self.label_7.setText(
+ _translate(
+ "MainWindow",
+ 'Module:
',
+ )
+ )
self.label_3.setText(_translate("MainWindow", "Module ID"))
self.mountPB.setText(_translate("MainWindow", "Mount"))
self.unmountPB.setText(_translate("MainWindow", "Un-mount"))
@@ -463,7 +535,11 @@ def retranslateUi(self, MainWindow):
self.checkIDlabel.setText(_translate("MainWindow", "TextLabel"))
self.hvONTestPB.setText(_translate("MainWindow", "Dark Tests (HV on)"))
self.hvOFFTestPB.setText(_translate("MainWindow", "Light On Tests (HV off)"))
- self.resultsLabel.setText(_translate("MainWindow", "Noise:
"))
+ self.resultsLabel.setText(
+ _translate(
+ "MainWindow", "Noise:
"
+ )
+ )
self.cancelPB.setText(_translate("MainWindow", "Cancel Test"))
self.logsPB.setText(_translate("MainWindow", "Ph2ACF logs"))
self.showPB.setText(_translate("MainWindow", "Show Results"))
@@ -480,27 +556,49 @@ def retranslateUi(self, MainWindow):
self.airOFFPB.setText(_translate("MainWindow", "Air OFF"))
self.tMaxLabel.setText(_translate("MainWindow", "Tmax: 0.0"))
self.airLed.setStyleSheet(_translate("MainWindow", "background-color: red;"))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("MainWindow", "Mount and Test"))
- self.placeholdersHelpLabel.setText(_translate("MainWindow", "Available placeholders:\n"
-"{ring_id} - Ring ID\n"
-"{position} - Position number\n"
-"{module_id} - Module ID\n"
-"{fiber} - Selected fiber\n"
-"{power} - Selected power source\n"
-"{fiber_endpoint} - Fiber endpoint (i.e. FC7)\n"
-"\n"
-"Placeholders can be used in commands."))
+ self.tabWidget.setTabText(
+ self.tabWidget.indexOf(self.tab), _translate("MainWindow", "Mount and Test")
+ )
+ self.placeholdersHelpLabel.setText(
+ _translate(
+ "MainWindow",
+ "Available placeholders:\n"
+ "{ring_id} - Ring ID\n"
+ "{position} - Position number\n"
+ "{module_id} - Module ID\n"
+ "{fiber} - Selected fiber\n"
+ "{power} - Selected power source\n"
+ "{fiber_endpoint} - Fiber endpoint (i.e. FC7)\n"
+ "\n"
+ "Placeholders can be used in commands.",
+ )
+ )
self.commandsGroupBox.setTitle(_translate("MainWindow", "Test Commands"))
self.checkIDLabel.setText(_translate("MainWindow", "Check ID Command:"))
- self.checkIDCommandLE.setText(_translate("MainWindow", "check_id.sh --module {module_id} --position {position}"))
+ self.checkIDCommandLE.setText(
+ _translate(
+ "MainWindow", "check_id.sh --module {module_id} --position {position}"
+ )
+ )
self.lightOnLabel.setText(_translate("MainWindow", "Light-On Test Command:"))
- self.lightOnCommandLE.setText(_translate("MainWindow", "light_on_test.sh --ring {ring_id} --pos {position} --fiber {fiber}"))
+ self.lightOnCommandLE.setText(
+ _translate(
+ "MainWindow",
+ "light_on_test.sh --ring {ring_id} --pos {position} --fiber {fiber}",
+ )
+ )
self.darkTestLabel.setText(_translate("MainWindow", "Dark Test Command:"))
- self.darkTestCommandLE.setText(_translate("MainWindow", "dark_test.sh --module {module_id} --power {power}"))
+ self.darkTestCommandLE.setText(
+ _translate(
+ "MainWindow", "dark_test.sh --module {module_id} --power {power}"
+ )
+ )
self.label_13.setText(_translate("MainWindow", "Air control command:"))
self.airCommandLE.setText(_translate("MainWindow", "air.sh {airOn}"))
self.label_14.setText(_translate("MainWindow", "Results URL:"))
- self.resultsUrlLE.setText(_translate("MainWindow", "Results/html/latest/index.html"))
+ self.resultsUrlLE.setText(
+ _translate("MainWindow", "Results/html/latest/index.html")
+ )
self.apiGroupBox.setTitle(_translate("MainWindow", "API Settings"))
self.dbEndpointLabel.setText(_translate("MainWindow", "Database URL:"))
self.dbEndpointLE.setText(_translate("MainWindow", "http://localhost:5000"))
@@ -509,7 +607,14 @@ def retranslateUi(self, MainWindow):
self.mqttTopicLabel.setText(_translate("MainWindow", "MQTT Topic:"))
self.mqttTopicLE.setText(_translate("MainWindow", "/ar/thermal/image"))
self.applySettingsPB.setText(_translate("MainWindow", "Apply settings"))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), _translate("MainWindow", "Settings"))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), _translate("MainWindow", "Commands Output"))
+ self.tabWidget.setTabText(
+ self.tabWidget.indexOf(self.tab_3), _translate("MainWindow", "Settings")
+ )
+ self.tabWidget.setTabText(
+ self.tabWidget.indexOf(self.tab_4),
+ _translate("MainWindow", "Commands Output"),
+ )
self.openInBrowserPB.setText(_translate("MainWindow", "Open in System Browser"))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_5), _translate("MainWindow", "Plots"))
+ self.tabWidget.setTabText(
+ self.tabWidget.indexOf(self.tab_5), _translate("MainWindow", "Plots")
+ )
diff --git a/workflow.md b/workflow.md
new file mode 100644
index 0000000..75efd8b
--- /dev/null
+++ b/workflow.md
@@ -0,0 +1,442 @@
+# Integration Tools — Simplified Workflow & Architecture
+> Covers the `integration_tools_new` repository as of branch `fix-safely-and-coldroom-power-supply`
+
+---
+
+## 1. What this software does
+
+This is a **PyQt5 desktop GUI suite** used at CERN/Pisa for testing CMS Outer Tracker and Inner Tracker modules. It lets operators:
+
+- Track detector modules in a database (inventory, mounting status, connections)
+- Run automated electrical tests on individual modules (check ID, noise tests)
+- Control and monitor the CAEN high-voltage / low-voltage power supply
+- Monitor and control the cold room and MARTA CO₂ cooling system
+- Watch a rotating thermal camera and spot overheating in real time
+- Enforce safety interlocks (dew point, HV status, door status)
+
+---
+
+## 2. High-level architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ integration.py (MainApp) │
+│ Main GUI window — ties everything together │
+│ │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
+│ │ Module │ │ CAEN │ │ MQTT │ │ Shell │ │
+│ │ DB Tab │ │ Control │ │ listener │ │ Commands │ │
+│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │
+│ │ │ │ │ │
+└───────┼──────────────┼──────────────┼───────────────┼───────┘
+ │ │ │ │
+ HTTP REST TCP socket MQTT broker subprocess
+ (module DB) (CAEN server) (pccmslab1) (Ph2_ACF tests)
+
+┌─────────────────────────────────┐
+│ cold.py / marta_coldroom.py │
+│ Coldroom / MARTA GUI │
+│ │
+│ ┌──────────┐ ┌─────────────┐ │
+│ │ System │ │ Safety │ │
+│ │ (MQTT) │ │ Interlocks │ │
+│ └──────────┘ └─────────────┘ │
+└─────────────────────────────────┘
+
+┌──────────────────────────────────┐
+│ power_supply/power_supply_ctrl │
+│ Rigol DP116A GUI (standalone) │
+│ Connects via VISA/TCP │
+└──────────────────────────────────┘
+```
+
+---
+
+## 3. Entry points
+
+| Script | Purpose |
+|--------|---------|
+| `python integration.py` | Main integration GUI (most used) |
+| `python cold.py` | Coldroom / MARTA monitoring GUI |
+| `python power_supply/power_supply_ctrl.py` | Standalone Rigol power supply panel |
+| `python caen/caenGUIall.py` | Standalone CAEN multi-channel panel |
+| `python Inner_tracker_GUI/caenGUIall_v2.py` | Inner Tracker CAEN panel |
+
+---
+
+## 4. Main Integration GUI — step-by-step workflow
+
+### 4.1 Application startup (`integration.py`)
+
+```
+app starts
+ │
+ ├─ load UI from ui/integration.ui
+ ├─ load settings from ~/.config/integration_ui/settings.yaml
+ │ (falls back to settings_integration.yaml in repo)
+ ├─ create ModuleDB widget → queries REST API at db_url/modules
+ ├─ create CAENControl object → starts 2-second timer to poll CAEN
+ ├─ connect to MQTT broker → subscribe to:
+ │ /ar/thermal/image (thermal camera frames)
+ │ /ph2acf/data (chip temperatures from Ph2_ACF)
+ │ /air/status (dry-air system)
+ │ shellies/ventola/... (fan power monitor via Shelly plug)
+ └─ restore last session ring from ~/.config/integration_ui/lastsession.txt
+```
+
+### 4.2 Selecting a module for testing
+
+```
+Operator types ring name in Ring field (e.g. "L1_47_A#1")
+ │
+ ├─ split_ring_and_position() extracts ring ID and slot number
+ ├─ draw_ring() redraws the circular ring diagram
+ │ blue rectangles = slots with mounted modules
+ │ red rectangle = currently selected slot
+ │
+ └─ Operator clicks a rectangle OR types in Position field
+ │
+ ├─ If a module is mounted there, the Module ID field auto-fills
+ └─ Operator can also manually type a Module ID
+ │
+ └─ module_changed() fires:
+ ├─ load_module_details() → GET /modules/{id}
+ │ stores module data, extracts lpGBT fuse ID
+ │ (used to match incoming Ph2_ACF temperature data)
+ └─ queries /snapshot to restore fiber & power dropdowns
+```
+
+### 4.3 Connecting fiber and power
+
+```
+Fiber dropdown (SfibA, SfibB, E3;1 … E3;5)
+Power dropdown (BINT1, R01;M1.1 … R01;M3.12)
+
+ ├─ Connect Power button → POST /disconnect_all_detSide, then POST /connect
+ ├─ Connect Fiber button → same pattern
+ │
+ └─ Status LEDs update automatically:
+ green = connected and matches selected module
+ yellow = connected but to a different module
+ red = not connected
+```
+
+Connections are stored in the REST database and reflected back in the Module Inventory tab.
+
+### 4.4 Running tests
+
+Three test buttons trigger shell commands defined in Settings:
+
+```
+[Check ID] → runs check_id_command
+ │ expands {module_id}, {fiber_endpoint}, {session}, etc.
+ │
+ └─ CommandWorker (QThread) runs the command
+ ├─ success: LED turns green, enables next tests, parses module ID from stdout
+ └─ fail: LED turns red
+
+[HV OFF Test] → runs light_on_command
+ │ (noise test with room lights ON, HV OFF)
+ └─ same pattern; parses noise results from DB on success
+
+[HV ON Test] → runs dark_test_command
+ │ (dark noise + calibration, HV ON)
+ └─ on success: queries /module_test_analysis/{name}
+ extracts per-chip Average SSA/MPA noise
+ displays result in the Noise table label
+```
+
+The `{fiber_endpoint}` placeholder is resolved at test time from the live connection snapshot — it encodes which FC7 board and optical group to use.
+
+All commands are cancellable via the Cancel button (kills the worker thread).
+
+### 4.5 Thermal camera feedback
+
+```
+MQTT /ar/thermal/image → on_mqtt_message()
+ │ raw bytes: 24×32 float array (768 floats × 4 bytes)
+ │
+ ├─ reshapes to 24×32, updates thermal image plot
+ ├─ tracks min/max/avg trend over last 600 samples
+ ├─ publishes summary to /integration/thermalcamera
+ │
+ └─ Safety thresholds:
+ Tmax > 45 °C → CAEN safe_lv_off() (ramp HV down first, then kill LV)
+ Tmax > 50 °C → immediate HV + LV off
+```
+
+### 4.6 Ph2_ACF chip temperature overlay
+
+```
+MQTT /ph2acf/data → on_mqtt_message()
+ │ JSON with lpGBT fuse ID and per-chip temperatures
+ │
+ ├─ filters to only process data matching current module's fuse ID
+ ├─ looks up temperature offsets from module DB record
+ ├─ computes SSA mean/max and MPA mean/max
+ └─ overlays on the same thermal trend plot (dashed lines if offsets missing)
+```
+
+### 4.7 Mounting / unmounting modules
+
+```
+Mount button:
+ 1. Validate: module ID, ring ID, position all filled
+ 2. Check no module already at target position
+ 3. Check module not already mounted elsewhere
+ 4. PUT /modules/{id} with mounted_on = "L1;5", status = "MOUNTED"
+ 5. Redraw ring (slot turns blue)
+
+Unmount button:
+ 1. PUT /modules/{id} with mounted_on = "", status = "un-mounted"
+ 2. Redraw ring (slot turns black)
+```
+
+---
+
+## 5. Database layer (`db/module_db.py` + REST API)
+
+The module database is a **MongoDB backend** exposed through a REST API at `http://pccmslab1:5000`.
+
+Key endpoints used:
+
+| Endpoint | Method | Purpose |
+|----------|--------|---------|
+| `/modules` | GET | List all modules |
+| `/modules/{id}` | GET/PUT | Fetch or update one module |
+| `/sessions` | POST | Create a test session |
+| `/connect` | POST | Record a cable connection |
+| `/disconnect_all_detSide` | POST | Remove all crate-side connections |
+| `/disconnect_all_crateSide` | POST | Remove all detector-side connections |
+| `/snapshot` | POST | Walk connection graph from a cable |
+| `/module_test_analysis/{name}` | GET | Fetch analysis results |
+| `/generic_module_query` | POST | MongoDB-style query |
+
+**Module record structure** (simplified):
+
+```json
+{
+ "moduleName": "PS_26_IPG-10005",
+ "status": "MOUNTED",
+ "mounted_on": "L1_47_A#1;5",
+ "position": "OnDet@ L1_47_A#1;5",
+ "grade": "A+",
+ "hwId": 12345,
+ "children": {
+ "lpGBT": { "CHILD_SERIAL_NUMBER": "12345" },
+ "PS Read-out Hybrid": { "details": { "ALPGBT_BANDWIDTH": "10Gbps" } }
+ },
+ "details": { "LOCATION": "Pisa", "DESCRIPTION": "..." },
+ "temperature_offset": { "SSA_H0_0": -2.1, "MPA_H0_8": 1.3, ... }
+}
+```
+
+The `ModuleDB` widget (tab "Module Inventory") filters by location (Pisa), speed (5G/10G), spacer type, grade, and status, and lets operators view/edit any field and disconnect cable connections.
+
+---
+
+## 6. CAEN control (`caen/caenGUI.py`)
+
+```
+CAENControl
+ │
+ ├─ CAENQueryThread (QThread)
+ │ connects TCP socket to 192.168.0.45:7000 per query
+ │ sends custom text protocol: "GetStatus,PowerSupplyId:caen"
+ │ parses response: "caen_HV0.11_IsOn:1,caen_HV0.11_Voltage:100.0,..."
+ │
+ ├─ 2-second QTimer → fires update() → sends GetStatus query
+ │ → updates LV/HV LEDs and voltage/current labels
+ │
+ ├─ LV On/Off buttons → send TurnOn/TurnOff commands
+ ├─ HV On/Off buttons → same
+ │
+ └─ safe_lv_off():
+ first call: ramp HV down (TurnOff HV), set flag lv_off_when_hv_off=True
+ second call (or when HV voltage ≤ 10 V): also turn off LV
+ (ensures LV is never cut before HV ramps safely)
+```
+
+The channels are updated dynamically when a module is selected:
+- `setLV("LV7.5")` — called when the module's LV cable path is resolved
+- `setHV("HV2.3")` — called when the module's HV cable path is resolved
+
+---
+
+## 7. Coldroom system (`coldroom/`)
+
+The coldroom GUI (`cold.py`) and its supporting classes form an independent subsystem:
+
+```
+System (coldroom/system.py)
+ │
+ ├─ loads settings_coldroom.yaml
+ ├─ holds shared _status dict:
+ │ { "marta": {}, "coldroom": {}, "thermal_camera": {}, "cleanroom": {},
+ │ "caen": {}, "coldroomair": {} }
+ │
+ ├─ MartaColdRoomMQTTClient (subscribes to /MARTA/#, /coldroom/#, /environment/#, etc.)
+ │ ├─ parses temperatures, humidity, dew point, door status
+ │ ├─ handles CO₂ sensor data
+ │ └─ sends commands: start_chiller, stop_co2, set_temperature_setpoint, etc.
+ │
+ └─ ThermalCameraMQTTClient (subscribes to /thermalcamera/#)
+ ├─ receives 24×32 float images + position from rotating camera
+ ├─ accumulates last 5 images per angular position
+ └─ used by ModuleTemperaturesTAB to stitch 360° panoramas
+```
+
+### Coldroom safety flow
+
+Every MQTT message triggers safety checks in `marta_coldroom.py → on_message()`:
+
+```
+on_message received
+ │
+ ├─ update relevant subsystem status
+ │
+ └─ if has_valid_status(): ← all subsystems have reported at least once
+ │
+ ├─ check_dew_point():
+ │ min(MARTA_supply_T, MARTA_return_T, coldroom_T) > dewpoint + 1°C ?
+ │ → sets safety_flags["door_locked"]
+ │
+ ├─ check_door_status():
+ │ is CmdDoorUnlock_Reff == 1?
+ │ → sets safety_flags["sleep"]
+ │
+ └─ check_door_safe_to_open():
+ dew point safe AND HV off?
+ → sets safety_flags["door_safe"]
+```
+
+The `soft_interlock_loop` (called externally) checks:
+- If LV is ON but MARTA OT is not running → publish alarm to `/alarm`
+- (The actual `switch_all_lv_off()` call is currently commented out — see BUG-13)
+
+---
+
+## 8. Thermal camera panorama stitching (`coldroom/module_temperatures_gui.py`)
+
+```
+Camera rotates around the detector ring (0°–360°)
+ │
+ ├─ At each angular position, ThermalCameraMQTTClient receives a 24×32 image
+ ├─ Stores last 5 images per position per camera in _stitching_data
+ │
+ └─ ModuleTemperaturesTAB.update_displays() (every 1 second)
+ │
+ ├─ For each camera (0–3):
+ │ ├─ Collect all (position, image) pairs
+ │ ├─ stitch_multiple_images():
+ │ │ - places each image on a (24 × total_width) canvas
+ │ │ at the pixel column corresponding to its angle
+ │ │ - averages overlapping regions
+ │ │ - normalises to 8-bit using either CO₂ temp range or auto range
+ │ └─ displays as a 360°-wide false-colour strip
+ │
+ ├─ Overlays mounted module names at their angular positions
+ └─ Temperature plot tab: per-camera max/min vs angle (with spike filter)
+```
+
+Colour scale can be fixed (CO₂ return temperature as baseline, +15/+20 °C range) or auto-scaled.
+
+---
+
+## 9. Power supply — Rigol DP116A (`power_supply/`)
+
+A **simple standalone panel** (not embedded in the main GUI):
+
+```
+PowerSupplyController
+ │
+ ├─ connects via PyVISA over TCP: TCPIP0::192.168.0.16::INSTR
+ ├─ 1-second QTimer → queries MEAS:VOLT? and MEAS:CURR?
+ ├─ Set Voltage button → APPLY {voltage}
+ ├─ Set Current button → SOUR:CURR {current}
+ ├─ Power ON/OFF → OUTP:STAT ON/OFF
+ └─ closeEvent: turns off supply, closes VISA resource
+```
+
+`scripts/rigolDP116A.py` is a CLI version of the same instrument control.
+
+---
+
+## 10. Settings files
+
+| File | Loaded by | Key settings |
+|------|-----------|--------------|
+| `settings_integration.yaml` | `integration.py`, `db/module_db.py` | `db_url`, `mqtt_server`, test commands, topic names |
+| `settings_coldroom.yaml` | `coldroom/system.py` | MQTT broker, per-subsystem topic names |
+| `~/.config/integration_ui/settings.yaml` | `integration.py` (overrides bundled) | user-local overrides |
+| `~/.config/integration_ui/lastsession.txt` | `integration.py` | remembers last selected ring |
+| `coldroom/camera_config.yaml` | `module_temperatures_gui.py` | camera angular offsets and Front/Back side assignment |
+
+---
+
+## 11. Communication summary
+
+```
+┌────────────────────────────────────────────────────────────────┐
+│ MQTT broker (pccmslab1:1883) │
+│ │
+│ /ar/thermal/image ← thermal camera raw frames │
+│ /thermalcamera/# ← camera state + per-camera imgs │
+│ /ph2acf/data ← chip temperatures from Ph2_ACF │
+│ /MARTA/# ← MARTA CO₂ chiller status/cmd │
+│ /coldroom/# ← coldroom state/cmd │
+│ /environment/HumAndTemp001/#← cleanroom T/RH/dewpoint │
+│ /ble/CO2-1 ← CO₂ sensor │
+│ /air/status ← dry-air on/off │
+│ /air/control ← dry-air command │
+│ /alarm ← safety alarm messages │
+│ shellies/ventola/... ← fan power via Shelly plug │
+│ shellies/coldroomair/... ← coldroom air bypass via Shelly │
+└────────────────────────────────────────────────────────────────┘
+
+┌─────────────────────────────────────┐
+│ REST API (pccmslab1:5000) │
+│ Module database (MongoDB backend) │
+└─────────────────────────────────────┘
+
+┌─────────────────────────────────────┐
+│ TCP 192.168.0.45:7000 │
+│ CAEN power supply daemon │
+└─────────────────────────────────────┘
+
+┌─────────────────────────────────────┐
+│ TCP 192.168.0.16 (VISA/INSTR) │
+│ Rigol DP116A power supply │
+└─────────────────────────────────────┘
+
+┌─────────────────────────────────────┐
+│ HTTP 192.168.0.204 (Shelly relay) │
+│ Dry-air bypass valve │
+└─────────────────────────────────────┘
+```
+
+---
+
+## 12. Typical operator session flow
+
+```
+1. Start integration.py
+2. Last ring auto-loaded from lastsession.txt
+3. Click slot on ring diagram → Module ID auto-fills (if mounted)
+4. Verify fiber (SfibX) and power (R01;Mx.y) dropdowns show correct connections
+ (green LEDs = confirmed connected)
+5. Fill in Operator and Comments fields
+6. Click [Check ID]
+ → runs moduleTest.py --board FC7OT8 --slot 2 -c readOnlyID
+ → LED turns green, module ID confirmed
+7. Click [HV OFF Test]
+ → runs eyeOpening test
+ → LED turns green
+8. Click [HV ON Test]
+ → runs calibrationandpedenoise
+ → noise table updates with SSA/MPA values per hybrid
+9. Monitor thermal plot during test (Tmax displayed live)
+10. Click [Show Results] or [Open in Browser] to view full HTML analysis
+11. If moving to next module: type new ring position or click ring diagram
+ → test LEDs reset automatically
+```