upd
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import heapq
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -13,6 +14,8 @@ from ..models import Endpoint, Device, Channel
|
|||||||
from ..writer import Writer
|
from ..writer import Writer
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
# we don't use vxi11
|
||||||
|
logging.getLogger("pymeasure.adapters.vxi11").setLevel(logging.ERROR)
|
||||||
|
|
||||||
KEITHLEY_DRIVER_KEYS = {"keithley2000", "keithley_2000"}
|
KEITHLEY_DRIVER_KEYS = {"keithley2000", "keithley_2000"}
|
||||||
|
|
||||||
@@ -65,16 +68,11 @@ class PrologixEndpointLoop:
|
|||||||
":INIT:CONT OFF;:ABORT",
|
":INIT:CONT OFF;:ABORT",
|
||||||
# DC voltage config
|
# DC voltage config
|
||||||
':SENS:FUNC "VOLT:DC"',
|
':SENS:FUNC "VOLT:DC"',
|
||||||
":SENS:VOLT:DC:RANG 10", # fixed 10 V range
|
":SENS:VOLT:DC:RANG 10", # 10V range
|
||||||
":SENS:VOLT:DC:NPLC 10", # slow/low-noise integration
|
":SENS:VOLT:DC:NPLC 10", # slow/low-noise integration
|
||||||
":SENS:VOLT:DC:DIG 7", # max digits
|
":SENS:VOLT:DC:DIG 7", # max digits
|
||||||
# offset drift?
|
# offset drift?
|
||||||
":SYST:AZER:STAT ON",
|
":SYST:AZER:STAT ON",
|
||||||
# trig
|
|
||||||
":TRIG:SOUR IMM",
|
|
||||||
":TRIG:COUN 1",
|
|
||||||
":SAMP:COUN 1",
|
|
||||||
":TRIG:DEL 0",
|
|
||||||
# read only
|
# read only
|
||||||
":FORM:ELEM READ",
|
":FORM:ELEM READ",
|
||||||
# turn off VFD
|
# turn off VFD
|
||||||
@@ -186,39 +184,90 @@ class PrologixEndpointLoop:
|
|||||||
|
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
log.info(
|
log.info(
|
||||||
f"Starting Prologix loop: {self.endpoint.endpoint_key} ({len(self.bindings)} bindings)"
|
"Starting Prologix loop: %s (%d bindings)",
|
||||||
|
self.endpoint.endpoint_key,
|
||||||
|
len(self.bindings),
|
||||||
)
|
)
|
||||||
|
|
||||||
# group by device
|
loop = asyncio.get_running_loop()
|
||||||
by_device = defaultdict(list)
|
|
||||||
|
def poll_s_for(b: PrologixBinding) -> float:
|
||||||
|
v = b.channel.poll_interval_s
|
||||||
|
return float(self.default_poll_s if v is None else v)
|
||||||
|
|
||||||
|
# group bindings by device
|
||||||
|
by_device: defaultdict[int, list[PrologixBinding]] = defaultdict(list)
|
||||||
for b in self.bindings:
|
for b in self.bindings:
|
||||||
by_device[b.device.device_id].append(b)
|
by_device[b.device.device_id].append(b)
|
||||||
|
|
||||||
while True:
|
devices: list[tuple[int, str, dict[str, Any], list[PrologixBinding]]] = []
|
||||||
start_ts = asyncio.get_running_loop().time()
|
|
||||||
|
|
||||||
for _dev_id, dev_bindings in by_device.items():
|
for _dev_id, dev_bindings in by_device.items():
|
||||||
# validation
|
meta: dict[str, Any] = dev_bindings[0].device.metadata or {}
|
||||||
meta = dev_bindings[0].device.metadata
|
|
||||||
gpib_addr = meta.get("gpib_addr")
|
gpib_addr = meta.get("gpib_addr")
|
||||||
if gpib_addr is None:
|
if gpib_addr is None:
|
||||||
|
log.warning(
|
||||||
|
"Skipping device %s: missing metadata.gpib_addr",
|
||||||
|
dev_bindings[0].device.device_key,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
gpib_addr = int(gpib_addr)
|
|
||||||
driver_key = str(meta.get("driver", "")).strip() or None
|
|
||||||
|
|
||||||
if gpib_addr not in self._initialized_addrs:
|
|
||||||
await asyncio.to_thread(
|
|
||||||
self._init_device, gpib_addr, driver_key, meta
|
|
||||||
)
|
|
||||||
|
|
||||||
for b in dev_bindings:
|
|
||||||
try:
|
try:
|
||||||
# offload to thread
|
gpib_addr_i = int(gpib_addr)
|
||||||
|
except Exception:
|
||||||
|
log.warning(
|
||||||
|
"Skipping device %s: invalid metadata.gpib_addr=%r",
|
||||||
|
dev_bindings[0].device.device_key,
|
||||||
|
gpib_addr,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
driver_key: str = str(meta.get("driver", "")).strip()
|
||||||
|
devices.append((gpib_addr_i, driver_key, meta, dev_bindings))
|
||||||
|
|
||||||
|
if not devices:
|
||||||
|
log.warning("No usable Prologix bindings; sleeping forever")
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
# init all bindings as due "now"
|
||||||
|
now = loop.time()
|
||||||
|
heap: list[tuple[float, int, int]] = [] # (due_time, tie_breaker, device_index)
|
||||||
|
tie = 0
|
||||||
|
|
||||||
|
# build a flat list of (device_index, binding) and a heap of next due times
|
||||||
|
flat: list[tuple[int, PrologixBinding]] = []
|
||||||
|
for di, (_addr, _drv, _meta, dev_bindings) in enumerate(devices):
|
||||||
|
for b in dev_bindings:
|
||||||
|
flat.append((di, b))
|
||||||
|
heapq.heappush(heap, (now, tie, len(flat) - 1))
|
||||||
|
tie += 1
|
||||||
|
|
||||||
|
async def ensure_initialized(
|
||||||
|
gpib_addr: int, driver_key: str, meta: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
if gpib_addr in self._initialized_addrs:
|
||||||
|
return
|
||||||
|
await asyncio.to_thread(self._init_device, gpib_addr, driver_key, meta)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
due, _tie, flat_idx = heapq.heappop(heap)
|
||||||
|
|
||||||
|
# sleep until this binding is due
|
||||||
|
now = loop.time()
|
||||||
|
if due > now:
|
||||||
|
await asyncio.sleep(due - now)
|
||||||
|
|
||||||
|
di, b = flat[flat_idx]
|
||||||
|
gpib_addr, driver_key, meta, _dev_bindings = devices[di]
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ensure_initialized(gpib_addr, driver_key, meta)
|
||||||
|
|
||||||
raw = await asyncio.to_thread(
|
raw = await asyncio.to_thread(
|
||||||
self._exec_sync, gpib_addr, b.query, driver_key
|
self._exec_sync, gpib_addr, b.query, driver_key
|
||||||
)
|
)
|
||||||
|
|
||||||
val = float(raw)
|
val = float(raw)
|
||||||
val = val * b.channel.scale_value + b.channel.offset_value
|
val = val * b.channel.scale_value + b.channel.offset_value
|
||||||
|
|
||||||
@@ -229,15 +278,35 @@ class PrologixEndpointLoop:
|
|||||||
metric=b.channel.metric,
|
metric=b.channel.metric,
|
||||||
value=val,
|
value=val,
|
||||||
)
|
)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(
|
log.error(
|
||||||
f"Prologix error {b.device.device_key}/{b.channel.metric} query={b.query!r}: {e}"
|
"Prologix error %s/%s query=%r: %s",
|
||||||
|
b.device.device_key,
|
||||||
|
b.channel.metric,
|
||||||
|
b.query,
|
||||||
|
e,
|
||||||
)
|
)
|
||||||
await self.writer.write_error(
|
await self.writer.write_error(
|
||||||
device_id=b.device.device_id, error=f"prologix: {e}"
|
device_id=b.device.device_id,
|
||||||
|
error=f"prologix: {e}",
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
# steady cadence scheduling
|
||||||
|
interval = poll_s_for(b)
|
||||||
|
next_due = due + interval
|
||||||
|
now = loop.time()
|
||||||
|
if next_due <= now:
|
||||||
|
# fast fw without spamming immediate catch-up polls
|
||||||
|
# (ceil((now - next_due)/interval) + 1 steps)
|
||||||
|
steps = int((now - next_due) // interval) + 1
|
||||||
|
next_due += steps * interval
|
||||||
|
|
||||||
|
heapq.heappush(heap, (next_due, tie, flat_idx))
|
||||||
|
tie += 1
|
||||||
|
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
|
except asyncio.CancelledError:
|
||||||
elapsed = asyncio.get_running_loop().time() - start_ts
|
log.info("Prologix loop cancelled: %s", self.endpoint.endpoint_key)
|
||||||
await asyncio.sleep(max(0.5, self.default_poll_s - elapsed))
|
raise
|
||||||
|
|||||||
Reference in New Issue
Block a user