Menlo OFC Setup
===============

This driver is split into one core device and several modules attached to it.

:py:class:`~herosdevices.hardware.menlo.OFC` opens the single QWebChannel websocket connection to the comb and
exposes its raw control/status tree via ``get_node``/``set_node``/``explore``.

Each functional-layer module (:py:class:`~herosdevices.hardware.menlo.DDS`,
:py:class:`~herosdevices.hardware.menlo.LaserLock`, :py:class:`~herosdevices.hardware.menlo.DualLaserLock`,
:py:class:`~herosdevices.hardware.menlo.FXE`, :py:class:`~herosdevices.hardware.menlo.RepetitionRate`,
:py:class:`~herosdevices.hardware.menlo.CEO`, :py:class:`~herosdevices.hardware.menlo.Oscillator`) is a
separate HERO. It takes the already-running ``OFC``
HERO as a constructor argument via BOSS's ``"ofc": "$device_menlo_ofc"`` reference, so all modules share the
one physical connection instead of each opening their own.

See ``examples/menlo/ofc.json`` for a complete BOSS config wiring one comb with all its modules.

Some CW channels lock two wavelengths through one shared lock loop, with two separate frequency-distribution
blocks instead of one. Deploying plain ``LaserLock`` against one of these fails, since it hardcodes a single
frequency-distribution node name that these channels don't have. Use
:py:class:`~herosdevices.hardware.menlo.DualLaserLock` instead, passing both wavelengths' node names
explicitly; see ``examples/menlo/ofc.json`` for an example.

:py:class:`~herosdevices.hardware.menlo.FXE` wraps the comb's 16-channel frequency counter. Unlike the other
modules, it has no default observables, since which channel carries which signal is deployment-specific.
Pass ``observables`` for the channels relevant to your comb, e.g. a beat on channel 3:

.. code-block:: json

   "arguments": {
     "ofc": "$device_ofc",
     "observables": {
       "cw_beat": {"path": "counterFrequencies.channel03", "unit": "Hz"}
     }
   }

Node paths (used in ``get_node``/``set_node``/``observables``) are firmware-dependent and not documented by
Menlo. Use :py:meth:`~herosdevices.hardware.menlo.OFC.format_tree` to find them interactively:

.. code-block:: pycon

   >>> print(ofc.format_tree("functionalLayer.rrSettings", depth=2))
   functionalLayer.rrSettings
   |-- dds
   |   |-- ddsFrequency = 28286800
   |   |-- outputOn = True
   |   `-- outputPower = 0.64
   |-- mainControls
   |   |-- fastOutput ...
   |   |-- lock = True
   |   `-- slowOutput ...
   `-- repetitionRate
       |-- rrCounterRepRate = 250105340.0
       `-- rrTargetBeatRF = 250105340

Nodes shown as ``...`` are unexpanded branches; raise ``depth`` or call ``format_tree`` again rooted at that
path to descend further. :py:meth:`~herosdevices.hardware.menlo.OFC.explore` returns the same tree as a plain
dict instead of a rendered string, if you want to process it programmatically.

For anything specific to your comb (e.g. a customer-specific fiber-noise-cancellation module),
poll or control it directly instead of adding a new class: every module accepts an ``observables`` argument,
merged on top of its ``DEFAULT_OBSERVABLES``, and ``OFC.get_node``/``OFC.set_node`` give full read/write
access to any node regardless.

.. code-block:: json

   "arguments": {
     "host": "IP_OR_HOSTNAME",
     "observables": {
       "fnc578_locked": {"path": "functionalLayer.fnc578Settings.mainControls.lock", "unit": ""},
       "fnc1157_locked": {"path": "functionalLayer.fnc1157Settings.mainControls.lock", "unit": ""}
     }
   }

.. important::

   The ``OFC`` device's ``_ensure_connected`` method must be reachable from the other modules over the
   network. HEROS excludes underscore-prefixed methods from a ``RemoteHERO`` proxy unless marked
   ``force_remote``, so the ``OFC`` row in your BOSS config needs:

   .. code-block:: json

      "extra_decorators": [["_ensure_connected", "heros.inspect.force_remote"]]

   See ``examples/menlo/ofc.json`` for this in context.

.. warning::

   The PyPI package named ``pywebchannel`` is an unrelated project; installing it will not work. Install it
   from source instead:

   .. code-block:: bash

      pip install "pywebchannel @ git+https://github.com/MenloSystems/pywebchannel"

   ``pywebchannel`` is not declared as a project dependency, so this install step must be run manually wherever the Menlo driver
   is used: locally, in CI, and in any Docker image.

   In a Docker Container deployment via BOSS, use the ``BOSS_PIP_PKGS`` environment variable (see the
   `BOSS documentation <https://boss-eb4966.gitlab.io/getting_started.html#additional-dependencies-in-docker>`_)
   instead of extending the image:

   .. code-block:: yaml
      :caption: docker-compose.yml

       services:
         device_ofc:
           image: registry.gitlab.com/atomiq-project/herosdevices:latest
           restart: always
           network_mode: host
           volumes:
             - ./ofc.json:/ofc.json:ro
           environment:
             - BOSS_PIP_PKGS=pywebchannel@git+https://github.com/MenloSystems/pywebchannel
           command: python -m boss.starter -u file:///ofc.json --log info
