Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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
72 changes: 31 additions & 41 deletions Inner_tracker_GUI/caenGUIall_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__()
Expand All @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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 = {}
Expand All @@ -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):
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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,
Expand Down
Loading