Source code for mpylab.env.eut.session
"""RF-neutral lifecycle management for EUT monitoring."""
from __future__ import annotations
from collections.abc import Iterable, Mapping
from .events import validate_eut_event
from .monitors import CompositeEUTMonitor, EUTMonitor, ManualEUTMonitor
[docs]
class EUTMonitoringSession:
"""Manage monitors and classify their events for one EUT exposure.
The session deliberately has no access to RF hardware and does not apply
retry or stop policies. Measurement kernels remain responsible for those
safety and workflow decisions.
Parameters
----------
monitor : EUTMonitor
Monitor, commonly a :class:`CompositeEUTMonitor`, whose lifecycle is
managed by the session.
Attributes
----------
monitor : EUTMonitor
Managed monitor instance.
active : bool
Whether an exposure is active.
phase : str or None
Current exposure phase, or ``None`` outside an exposure.
"""
[docs]
def __init__(self, monitor: EUTMonitor) -> None:
if not isinstance(monitor, EUTMonitor):
raise TypeError("monitor must implement EUTMonitor")
self.monitor = monitor
self.active = False
self.phase = None
self._events = []
self._diagnostics = []
[docs]
@classmethod
def from_monitors(
cls,
manual_monitor: ManualEUTMonitor,
automatic_monitors: EUTMonitor | Iterable[EUTMonitor] = (),
) -> "EUTMonitoringSession":
"""Build a session with mandatory manual operator intervention.
Parameters
----------
manual_monitor : ManualEUTMonitor
Always-available manual observation and intervention source.
automatic_monitors : EUTMonitor or iterable of EUTMonitor, optional
Additional automatic observation sources.
Returns
-------
EUTMonitoringSession
Session managing the combined monitor.
"""
return cls(CompositeEUTMonitor(manual_monitor, automatic_monitors))
@property
def events(self) -> tuple[dict, ...]:
"""Return status events collected during the current exposure."""
return tuple(dict(event) for event in self._events)
@property
def diagnostics(self) -> tuple[dict, ...]:
"""Return monitor diagnostics collected during the current exposure."""
return tuple(dict(event) for event in self._diagnostics)
[docs]
def start_exposure(self, context: Mapping) -> None:
"""Start a new exposure and clear records from the previous one.
Parameters
----------
context : mapping
Measurement-specific exposure context forwarded to every monitor.
Raises
------
RuntimeError
If another exposure is already active.
"""
if self.active:
raise RuntimeError("an EUT monitoring exposure is already active")
self._events = []
self._diagnostics = []
self.monitor.start_exposure(dict(context))
self.active = True
self.phase = "during_exposure"
[docs]
def start_phase(self, phase: str, context: Mapping) -> None:
"""Switch all monitors to another phase of the active exposure.
Parameters
----------
phase : str
Exposure phase accepted by the EUT event schema.
context : mapping
Measurement-specific phase context forwarded to every monitor.
Raises
------
RuntimeError
If no exposure is active.
"""
if not self.active:
raise RuntimeError("cannot change phase without an active exposure")
self.monitor.start_phase(phase, dict(context))
self.phase = phase
[docs]
def poll_event(self, diagnostic_context: Mapping | None = None):
"""Return the next status event and retain diagnostics separately.
Parameters
----------
diagnostic_context : mapping, optional
Metadata added with ``setdefault`` to monitor diagnostics, for
example the current frequency, position, or attempt number.
Returns
-------
dict or None
Next validated status event. Diagnostics and an empty monitor
queue both return ``None``; diagnostics are available through
:attr:`diagnostics` and :meth:`pop_diagnostics`.
"""
if not self.active:
return None
candidate = self.monitor.poll_event()
if candidate is None:
return None
event = validate_eut_event(candidate)
if event.get("event_type") == "monitor_diagnostic":
event = dict(event)
for key, value in dict(diagnostic_context or {}).items():
event.setdefault(key, value)
self._diagnostics.append(event)
return None
self._events.append(event)
return event
[docs]
def pop_diagnostics(self) -> list[dict]:
"""Remove and return all diagnostics collected since the last call.
Returns
-------
list of dict
Collected monitor diagnostics in arrival order.
"""
diagnostics = [dict(event) for event in self._diagnostics]
self._diagnostics = []
return diagnostics
[docs]
def stop_exposure(self) -> None:
"""Stop the active exposure; repeated calls are harmless."""
if not self.active:
self.phase = None
return
try:
self.monitor.stop_exposure()
finally:
self.active = False
self.phase = None
[docs]
def close(self) -> None:
"""Stop an active exposure and release all monitor resources."""
self.stop_exposure()
self.monitor.close()
__all__ = ["EUTMonitoringSession"]