herosdevices.hardware.menlo.ofc
===============================

.. py:module:: herosdevices.hardware.menlo.ofc

.. autoapi-nested-parse::

   HERO driver for Menlo Systems frequency combs exposed via QWebChannel.









Module Contents
---------------

.. py:data:: WebChannelObject

.. py:function:: poll_observables(observables: dict[str, dict[str, str]], ensure_connected: collections.abc.Callable[[], None], get_value: collections.abc.Callable[[str], Any], context: str) -> dict[str, tuple[Any, str]]

   Read a dict of `{name: {"path", "unit"}}` observables, tolerating an unreachable device.

   Shared by :py:class:`OFC` and :py:class:`~herosdevices.hardware.menlo.functional_layer.FunctionalLayerModule`,
   whose `_observable_data` implementations both poll the device on demand rather than react to pushed
   updates: the device is contacted once per poll (not once per observable), and a single unreachable device
   or a single failing path does not stop the rest of the observables from being read.

   :param observables: Maps an observable's name to a dict with keys "path" (passed to `get_value`) and "unit".
   :param ensure_connected: Called once before reading any observable; may raise if the device is unreachable.
   :param get_value: Reads the current value for one observable's "path".
   :param context: Identifies the device/module in the log message if it is unreachable.

   :returns: A dict mapping each observable's name to its `(value, unit)` pair. Empty if `ensure_connected` raised.


.. py:class:: OFC(host: str, port: int = QWEBCHANNEL_DEFAULT_PORT, user: str = 'guest', password: str = '', timeout: float = 5.0, reconnect_cooldown: float = 30.0, observables: dict[str, dict[str, str]] | None = None)

   Driver for a Menlo Systems optical frequency comb (OFC) exposed via the QWebChannel websocket interface.

   The OFC exposes its full control/status tree (functional layer, modules, settings, ...) via Qt's
   WebChannel protocol. Because that tree is deep and firmware-dependent, individual nodes are addressed by
   dotted path strings (see :py:meth:`get_node`) instead of being declared as fixed class attributes. Use
   :py:meth:`explore` interactively to discover which paths are available on a given device, then list the
   ones you want polled in `observables`.

   Values are pushed by the device and kept in a local cache as soon as the connection is established, so
   :py:meth:`get_node` and :py:meth:`_observable_data` never trigger network traffic themselves.

   :param host: Hostname or IP address of the OFC's QWebChannel websocket endpoint.
   :param port: Port of the websocket endpoint.
   :param user: Username used for authentication.
   :param password: Password used for authentication.
   :param timeout: Seconds to wait for a single connection attempt, see :py:class:`QWebChannelConnection`.
   :param reconnect_cooldown: Minimum seconds between two connection attempts, see
                              :py:class:`QWebChannelConnection`. Keeps a prolonged outage (e.g. the OFC being powered off for
                              half an hour) from causing a reconnect attempt on every single poll tick; the OFC is picked back
                              up automatically the next time it is read after coming back online, no restart needed.
   :param observables: Additional observables to poll, merged on top of :py:attr:`DEFAULT_OBSERVABLES` (an
                       entry here with the same name overrides the default) rather than replacing it. Each entry maps
                       the name under which a value is emitted with the `observable_data` event to a dict with keys
                       "path" (dotted node path, see :py:meth:`get_node`) and "unit". The repetition-rate, CEO, and
                       oscillator modules have their own default observables instead, see
                       :py:class:`~herosdevices.hardware.menlo.RepetitionRate`,
                       :py:class:`~herosdevices.hardware.menlo.CEO`, and
                       :py:class:`~herosdevices.hardware.menlo.Oscillator`.


   .. py:attribute:: DEFAULT_OBSERVABLES
      :type:  dict[str, dict[str, str]]


   .. py:attribute:: host


   .. py:attribute:: observables


   .. py:attribute:: connection


   .. py:method:: get_node(path: str) -> Any

      Read the current (cached) value of a node in the OFC's control tree.

      :param path: Dotted path to the node, e.g. `functionalLayer.rrSettings.repetitionRate.rrCounterRepRate`
                   or, for dict-valued nodes such as `modules`, `modules["SYNCRO3U"].functionalLayer...`.

      :returns: The current value of the node.



   .. py:method:: set_node(path: str, value: Any) -> None

      Set the value of a node in the OFC's control tree.

      :param path: Dotted path to the node, see :py:meth:`get_node`.
      :param value: Value to set.



   .. py:method:: call_method(path: str, *args: Any, timeout: float | None = None) -> Any

      Call a method on a node in the OFC's control tree and wait for its result.

      QWebChannel method calls are inherently asynchronous (the remote object's generated method wrapper
      never returns a value, it only accepts a callback for the result). This wraps that callback in a
      blocking wait so `call_method` behaves like a normal synchronous function call.

      :param path: Dotted path to the method, see :py:meth:`get_node`. E.g.
                   `functionalLayer.rrSettings.mainControls.unlockHere`.
      :param \*args: Positional arguments to pass to the method.
      :param timeout: Seconds to wait for the method's response. Defaults to the connection's own `timeout`.

      :returns: The method's return value.

      :raises TimeoutError: No response was received within `timeout` seconds.



   .. py:method:: explore(path: str = '', depth: int = 1) -> Any

      Explore the OFC's control/status node tree starting at `path`.

      Use this interactively to find the dotted paths to put into `observables`, e.g.
      `ofc.explore("functionalLayer.rrSettings", depth=2)`.

      :param path: Dotted path to start exploring from (same syntax as :py:meth:`get_node`). Defaults to the
                   root object.
      :param depth: How many levels of children to expand. Children beyond this depth are listed by name only,
                    without expanding further.

      :returns: A nested dictionary mapping child names to either their value (leaf), a list of child names
                (unexpanded branch), or another such dictionary (expanded branch).



   .. py:method:: format_tree(path: str = '', depth: int = 2) -> str

      Render the OFC's control/status node tree starting at `path` as an indented ASCII tree.

      A more readable alternative to :py:meth:`explore` for interactive use, e.g. in a REPL or script:
      `print(ofc.format_tree("functionalLayer.rrSettings", depth=2))`.

      :param path: Dotted path to start exploring from, see :py:meth:`explore`.
      :param depth: How many levels of children to expand, see :py:meth:`explore`.

      :returns: The rendered tree as a multi-line string, rooted at `path` (or "root" if not given).



