Various small changes

This commit is contained in:
2026-03-06 01:28:58 -06:00
parent 29e80e74cd
commit 93fae9b5ea
11 changed files with 79 additions and 64 deletions

View File

@@ -18,7 +18,7 @@ dependencies = [
"pyyaml >= 6.0",
"pyvisa >= 1.13.0",
"pyvisa-py >= 0.7.0",
"dataclass-mage >= 0.25.1",
"pydantic_yaml >= 1.6.0",
]
[project.optional-dependencies]

View File

@@ -264,7 +264,7 @@ class Client:
pbar = _make_pbar(total=n, desc="Capturing", unit="frame")
for i in iterator:
raw = recv_bytes_or_msg(self._sock, frame_bytes)
raw = recv_bytes_or_msg(self._sock)
if not isinstance(raw, bytes):
raise ValueError(f"Expected frame data but got message: {raw}")
frames[i] = np.frombuffer(raw, dtype=dtype).reshape(ny, nx)

View File

@@ -171,7 +171,7 @@ def load_config(path: str | Path | None = None) -> Team1kConfig:
config = parse_yaml_raw_as(Team1kConfig, f.read())
logger.info("Config loaded: detector=%s:%d, prefix=%s",
config.detector_ip, config.register_port, config.pv_prefix)
config.detector.ip, config.detector.register_port, config.server.pv_prefix)
logger.debug("Full config:\n%s", to_yaml_str(config))
return config

View File

@@ -51,6 +51,7 @@ class DataPort:
# Try large buffer first, fall back to smaller
try:
self._socket = UDPSocket(detector_ip, port, recv_buffer_size=buffer_size)
assert self._socket._sock is not None, "Failed to create UDP socket"
actual = self._socket._sock.getsockopt(
__import__('socket').SOL_SOCKET, __import__('socket').SO_RCVBUF
)

View File

@@ -52,6 +52,7 @@ class UDPSocket:
@property
def fileno(self) -> int:
"""File descriptor for select()."""
assert self._sock is not None, "Socket is closed"
return self._sock.fileno()
def send(self, data: bytes | bytearray | memoryview) -> int:
@@ -64,6 +65,7 @@ class UDPSocket:
Raises:
OSError: On send failure.
"""
assert self._sock is not None, "Socket is closed"
nsent = self._sock.sendto(data, self._dest_addr)
if nsent != len(data):
raise OSError(f"Sent {nsent} of {len(data)} bytes")
@@ -80,6 +82,7 @@ class UDPSocket:
Returns:
Received bytes, or None on timeout.
"""
assert self._sock is not None, "Socket is closed"
ready, _, _ = select.select([self._sock], [], [], timeout_sec)
if not ready:
return None
@@ -97,6 +100,7 @@ class UDPSocket:
Returns:
Number of bytes received, 0 on timeout.
"""
assert self._sock is not None, "Socket is closed"
ready, _, _ = select.select([self._sock], [], [], timeout_sec)
if not ready:
return 0
@@ -104,6 +108,7 @@ class UDPSocket:
def clear_buffer(self) -> None:
"""Drain all pending data from the socket (non-blocking)."""
assert self._sock is not None, "Socket is closed"
logger.debug("Clearing socket buffer...")
self._sock.setblocking(False)
try:

View File

@@ -10,6 +10,11 @@ import threading
import dataclasses
import pyvisa
import pyvisa.resources
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from team1k.config import PowerSupplyChannelConfig
logger = logging.getLogger(__name__)
@@ -20,7 +25,7 @@ class PowerSupplyBase:
Args:
port: Device port (e.g., "/dev/DetectorPowerSupply" or "COM3").
settings: Channel settings: {ch_num: (voltage, current, ovp)}.
channels: Channel settings: {ch_num: PowerSupplyChannelConfig}.
voltage_step: Voltage step size for ramping (default 0.2V).
name: Human-readable name for logging.
"""
@@ -31,20 +36,20 @@ class PowerSupplyBase:
current: float
def __init__(self, port: str,
settings: dict[int, tuple[float, float, float]],
channels: dict[int, PowerSupplyChannelConfig],
voltage_step: float = 0.2,
name: str = "Power Supply"):
self._port = f"ASRL{port}::INSTR"
self._settings = settings
self._channels = channels
self._voltage_step = voltage_step
self.name = name
self._is_on = False
self._lock = threading.Lock()
def _open_resource(self) -> pyvisa.Resource:
def _open_resource(self) -> pyvisa.resources.MessageBasedResource:
"""Open the VISA resource."""
rm = pyvisa.ResourceManager("@py")
return rm.open_resource(self._port)
return rm.open_resource(self._port) # pyright: ignore[reportReturnType]
def initialize(self) -> None:
"""
@@ -57,11 +62,11 @@ class PowerSupplyBase:
inst = self._open_resource()
inst.write("OUTP:GEN 0 \n")
for ch, (voltage, current, ovp) in self._settings.items():
for ch, config in self._channels.items():
inst.write(f"INST:NSEL {ch} \n")
inst.write(f"OUTP:SEL 0 \n")
inst.write(f"APPL {voltage}, {current} \n")
inst.write(f"SOUR:VOLT:PROT {ovp} \n")
inst.write(f"APPL {config.voltage}, {config.current} \n")
inst.write(f"SOUR:VOLT:PROT {config.ovp} \n")
inst.write(f"SOUR:VOLT:STEP {self._voltage_step} \n")
volt = float(inst.query("SOUR:VOLT? \n"))
@@ -85,8 +90,8 @@ class PowerSupplyBase:
inst = self._open_resource()
for ch, (voltage, _, _) in self._settings.items():
output_on = voltage > 0
for ch, config in self._channels.items():
output_on = config.voltage > 0
inst.write(f"INST:NSEL {ch} \n")
inst.write(f"OUTP:SEL {1 if output_on else 0} \n")
@@ -106,7 +111,7 @@ class PowerSupplyBase:
inst = self._open_resource()
inst.write("OUTP:GEN 0 \n")
for ch in self._settings:
for ch in self._channels.keys():
inst.write(f"INST:NSEL {ch} \n")
inst.write(f"OUTP:SEL 0 \n")
@@ -130,8 +135,8 @@ class PowerSupplyBase:
try:
inst = self._open_resource()
for ch, (voltage, _, _) in self._settings.items():
if voltage == 0:
for ch, config in self._channels.items():
if config.voltage == 0:
continue
inst.write(f"INST:NSEL {ch} \n")
volt = float(inst.query("MEAS:VOLT? \n"))

View File

@@ -26,7 +26,7 @@ class DetectorPowerSupply(PowerSupplyBase):
if self._enabled:
super().__init__(
port=config.port,
settings=config.settings,
channels=config.channels,
voltage_step=config.voltage_step,
name="Detector Power Supply",
)

View File

@@ -26,7 +26,7 @@ class TECController(PowerSupplyBase):
if self._enabled:
super().__init__(
port=config.port,
settings=config.settings,
channels=config.channels,
voltage_step=config.voltage_step,
name="TEC Power Supply",
)
@@ -53,7 +53,7 @@ class TECController(PowerSupplyBase):
self._is_on = False
logger.info("TEC: power_off (no-op, not configured)")
def get_status(self) -> dict[int, dict[str, float]]:
def get_status(self) -> dict[int, PowerSupplyBase.ChannelStatus]:
if self._enabled:
return super().get_status()
return {}

View File

@@ -30,7 +30,7 @@ import argparse
import threading
from typing import Any
from .config import Team1kConfig, load_config
from .config import Team1kConfig, TriggerMode, TriggerPolarity, load_config
from .state import DetectorState
from .detector.registers import RegisterInterface
@@ -67,10 +67,10 @@ class Team1kServer:
# Cached parameter values (avoids register reads)
self._params = {
"exposure_mode": config.exposure_mode,
"trigger_mode": config.trigger_mode,
"trigger_polarity": config.trigger_polarity,
"integration_time": config.integration_time_ms,
"exposure_mode": config.defaults.exposure_mode,
"trigger_mode": config.defaults.trigger_mode,
"trigger_polarity": config.defaults.trigger_polarity,
"integration_time": config.defaults.integration_time_ms,
"frame_rate": 0.0,
"frame_count": 0,
}
@@ -82,25 +82,25 @@ class Team1kServer:
# Acquisition subprocess
self.acquisition = AcquisitionProcess(
config.detector_ip, config.data_port,
config.detector.ip, config.detector.data_port,
ring_name="team1k_frames",
num_ring_slots=32,
chip_config=self.chip_config,
)
# PVA interface
self.pva = PVAInterface(self, prefix=config.pv_prefix)
self.pva = PVAInterface(self, prefix=config.server.pv_prefix)
# PVA streamer (created after PVA setup)
self._pva_streamer: PVAStreamer | None = None
# TCP client server
self.tcp_server = TCPClientServer(self, port=config.client_port)
self.tcp_server = TCPClientServer(self, port=config.server.client_port)
# Peripherals
self.bellow_stage = BellowStage(config.bellow_stage)
self.detector_power = DetectorPowerSupply(config.detector_power)
self.tec = TECController(config.tec)
self.bellow_stage = BellowStage(config.peripherals.bellow_stage)
self.detector_power = DetectorPowerSupply(config.peripherals.detector_power)
self.tec = TECController(config.peripherals.tec)
@property
def state(self) -> DetectorState:
@@ -129,7 +129,7 @@ class Team1kServer:
pass
self.registers = RegisterInterface(
self.config.detector_ip, self.config.register_port,
self.config.detector.ip, self.config.detector.register_port,
)
self.commands = DetectorCommands(self.registers)
self.adc = ADCController(self.registers)
@@ -149,6 +149,10 @@ class Team1kServer:
if not self._connect_detector():
self.state = DetectorState.ERROR
return False
assert self.registers is not None
assert self.commands is not None
assert self.adc is not None
try:
# Firmware version
@@ -162,32 +166,30 @@ class Team1kServer:
configure_chip(self.registers, self.chip_config)
# Apply config defaults
self.commands.set_exposure_mode(self.config.exposure_mode)
self.commands.set_exposure_mode(self.config.defaults.exposure_mode)
self.commands.set_trigger_mode(
external=self.config.trigger_external,
polarity=not self.config.trigger_polarity_rising,
external=self.config.defaults.trigger_mode == TriggerMode.EXTERNAL,
polarity=self.config.defaults.trigger_polarity == TriggerPolarity.FALLING_EDGE,
)
self.commands.set_integration_time(self.config.integration_time_ms)
self.adc.set_clock_freq(self.config.adc_clock_frequency_mhz)
self.commands.set_adc_data_delay(self.config.adc_data_delay)
self.commands.set_integration_time(self.config.defaults.integration_time_ms)
self.adc.set_clock_freq(self.config.adc.clock_frequency_mhz)
self.commands.set_adc_data_delay(self.config.adc.data_delay)
self.commands.set_adc_data_averaging(0)
self.commands.enable_fpga_test_data(False)
# ADC order registers
self.registers.write_register(30, self.config.adc_order_7to0)
self.registers.write_register(31, self.config.adc_order_15to8)
self.registers.write_register(30, self.config.adc.order_7to0)
self.registers.write_register(31, self.config.adc.order_15to8)
# Digital signal registers
self.registers.write_register(27, self.config.digital_polarity)
self.registers.write_register(28, self.config.digital_order_7to0)
self.registers.write_register(29, self.config.digital_order_15to8)
self.registers.write_register(27, self.config.digital.polarity)
self.registers.write_register(28, self.config.digital.order_7to0)
self.registers.write_register(29, self.config.digital.order_15to8)
# Update cached params
self._params["exposure_mode"] = self.config.exposure_mode
self._params["trigger_mode"] = (
1 if self.config.trigger_external else 0
)
self._params["integration_time"] = self.config.integration_time_ms
self._params["exposure_mode"] = self.config.defaults.exposure_mode
self._params["trigger_mode"] = self.config.defaults.trigger_mode == TriggerMode.EXTERNAL
self._params["integration_time"] = self.config.defaults.integration_time_ms
self.state = DetectorState.IDLE
logger.info("Detector initialized")
@@ -222,11 +224,11 @@ class Team1kServer:
elif name == "trigger_mode" or name == "trigger_polarity":
if name == "trigger_mode":
mode = int(value)
polarity = self.config.trigger_polarity
mode = TriggerMode(int(value))
polarity = self.config.defaults.trigger_polarity
else:
mode = self.config.trigger_mode
polarity = int(value)
mode = self.config.defaults.trigger_mode
polarity = TriggerPolarity(int(value))
self.commands.set_trigger_mode(external=bool(mode), polarity=bool(polarity))
self._params["trigger_mode"] = mode
self._params["trigger_polarity"] = polarity
@@ -349,7 +351,7 @@ class Team1kServer:
def _auto_reconnect_loop(self) -> None:
"""Background thread: auto-reconnect when in ERROR state."""
interval = self.config.reconnect_interval
interval = self.config.detector.reconnect_interval
while not self._shutdown_event.is_set():
if self.state == DetectorState.ERROR:
logger.info("Auto-reconnect: attempting...")
@@ -433,7 +435,7 @@ class Team1kServer:
).start()
# Auto-reconnect thread (if enabled)
if self.config.auto_reconnect:
if self.config.detector.auto_reconnect:
threading.Thread(
target=self._auto_reconnect_loop,
daemon=True, name="team1k-reconnect",
@@ -506,11 +508,11 @@ def main():
# Apply CLI overrides
if args.detector_ip:
config.detector_ip = args.detector_ip
config.detector.ip = args.detector_ip
if args.pv_prefix:
config.pv_prefix = args.pv_prefix
config.server.pv_prefix = args.pv_prefix
if args.client_port:
config.client_port = args.client_port
config.server.client_port = args.client_port
server = Team1kServer(config)

View File

@@ -12,7 +12,6 @@ Message types:
import json
import struct
import socket
from typing import Union
# Length prefix formats
_JSON_HEADER = struct.Struct("!I") # 4-byte unsigned int (max ~4 GB)
@@ -26,7 +25,7 @@ def send_msg(sock: socket.socket, obj: dict) -> None:
def recv_msg(sock: socket.socket) -> dict:
"""Receive a length-prefixed JSON message. Returns parsed dict."""
data = recv_bytes_or_msg(sock)
if isinstance(data, bytes):
if not isinstance(data, dict):
raise ValueError("Expected JSON message but got binary data")
return data
@@ -35,7 +34,7 @@ def send_bytes(sock: socket.socket, data: bytes | memoryview) -> None:
sock.sendall(b"D" + _DATA_HEADER.pack(len(data)) + data)
def recv_bytes_or_msg(sock: socket.socket) -> Union[bytes | dict]:
def recv_bytes_or_msg(sock: socket.socket) -> bytes | dict:
"""Receive length-prefixed binary data (reads the 8-byte header first)."""
type_byte = _recv_exact(sock, 1)
if type_byte == b"J":

View File

@@ -9,12 +9,13 @@ Only one capture can run at a time (capture lock).
import socket
import logging
import threading
from typing import TYPE_CHECKING, Any
from dataclasses import asdict
from .tcp_protocol import send_msg, recv_msg
from .capture import BufferedCapture
from .state import DetectorState
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .server import Team1kServer
@@ -113,7 +114,7 @@ class TCPClientServer(threading.Thread):
"acquiring": self._server.state == DetectorState.ACQUIRING,
"power_on": self._server.detector_power.is_on,
"tec_on": self._server.tec.is_on,
"bellow_inserted": abs(self._server.bellow_stage.position - self._server.config.bellow_stage.inserted_position_um) < 100,
"bellow_inserted": abs(self._server.bellow_stage.position - self._server.config.peripherals.bellow_stage.inserted_position_um) < 100,
})
elif cmd == "capture":
@@ -183,12 +184,14 @@ class TCPClientServer(threading.Thread):
elif cmd == "register_read":
addr = int(msg["address"])
assert self._server.registers is not None, "Registers not configured"
value = self._server.registers.read_register(addr)
send_msg(sock, {"ok": True, "value": value})
elif cmd == "register_write":
addr = int(msg["address"])
value = int(msg["value"])
assert self._server.registers is not None, "Registers not configured"
self._server.registers.write_register(addr, value)
send_msg(sock, {"ok": True})
@@ -225,14 +228,14 @@ class TCPClientServer(threading.Thread):
# Convert int keys to strings for JSON
send_msg(sock, {
"ok": True,
"channels": {str(k): dict(v) for k, v in status.items()},
"channels": {str(k): asdict(v) for k, v in status.items()},
})
elif cmd == "tec_status":
status = self._server.tec.get_status()
send_msg(sock, {
"ok": True,
"channels": {str(k): dict(v) for k, v in status.items()},
"channels": {str(k): asdict(v) for k, v in status.items()},
})
elif cmd == "bellow_status":
@@ -242,7 +245,7 @@ class TCPClientServer(threading.Thread):
"ok": True,
"position_um": position,
"is_moving": is_moving,
"bellow_inserted": abs(position - self._server.config.bellow_stage.inserted_position_um) < 100,
"bellow_inserted": abs(position - self._server.config.peripherals.bellow_stage.inserted_position_um) < 100,
})
else: