.. _getting_started_heros: Getting Started with HEROS ========================== This guide is the best way to get you started with the Atomiq One software stack. A basic understanding of a few tools and concepts is assumed, so we recommend familiarising yourself with the following topics: `uv `_ A modern python virtual environment manager. You need uv installed on your system. CLI Basic usage of a command line interface like bash. .. tip:: For this guide we start with setting up the non-realtime part. If you want to only run ARTIQ with Atomiq or want to start with the realtime part, jump to :ref:`getting_started_atomiq`. .. tip:: Also check out our `"From Zero to HERO in 45 Minutes" webinar `_! Setting up your first HERO -------------------------- Create a new folder and navigate to it. In this tutorial we call the folder `my_first_hero`. Now run: .. code-block:: bash uv venv uv pip install heros-boss herosdevices Now we need to create a definitions file for :repo-doc-link:`BOSS ` to know what python class it should make into a HERO: .. code-block:: json :caption: my_first_hero.json { "_id": "my_first_hero", "classname": "herosdevices.hardware.dummy.camera.CameraDummy", "arguments": { "config_dict":{"default":{"height":200, "width":200, "frame_count":-1, "auto_trigger":false}} } } The ``_id`` key is the name of the HERO under which it is identified in the network. `classname` is the full path of the Python class to be used. In this example, we use the dummy camera object from :repo-doc-link:`herosdevices`, which takes a configuration dictionary as argument in its ``init`` function to configure among others the height and width of the generated image. .. tip:: Learn more about the configuration syntax beyond this tutorial :ref:`here `. To start the HERO, run: .. code-block:: uv run boss -u file://${PWD}/my_first_hero.json This process now runs indefinitely and provides an interface to the underlying class to the network. Interacting with your HERO -------------------------- .. important:: To be able to automatically discover HEROS in the network, you need to open the ports 7447/tcp and 7446/udp (broadcast). Also make sure that multicast is enabled. .. note:: You can do this on **any** machine on the same network as the machine running the BOSS command above. To interact with your new hero (besides through code as we will see in the next section), you can use the `HERO monitor `_. Therefore open a new terminal, navigate to your ``my_first_hero`` directory and run .. code-block:: bash uv pip install git+https://gitlab.com/atomiq-project/hero-monitor uv run hero-monitor A window will open where the ``my_first_hero`` is visible in the list to the left. Double clicking it will display its remote interface: .. image:: ../../static/getting_started_heros_monitor.png :width: 70% :alt: HERO interface in the HERO monitor. There you can run functions of the remote dummy camera by pressing the shown buttons or manipulate its attributes directly. .. tip:: The Jupyter console at the bottom allows to run more complicated functions. Use ``obj.`` to access the remote object (e.g. ``obj.arm(metadata={"name":"my_picture"})``). You can press ``tab`` for autocompletion. Pressing the ``arm (*)`` [#f1]_ button now arms the camera, i.e. configures it and let it wait for an external trigger signal. The ``start`` functions then sends a software trigger to the camera and takes (or in our dummy case generates) an image. This image is then send as an event into the network and can be captured at any point. You can see this in the BOSS log as: .. code-block bash 2026-03-18 18:01:31,130 [INFO] herosdevices [acq_device.py:108 start]: Firing software trigger 2026-03-18 18:01:31,802 [INFO] herosdevices [acq_device.py:283 acquisition_data]: Emitting frame 0 with shape (200, 200) and dtype uint16 .. [#f1] The ``(*)`` indicates that the function can be called with optional arguments. Working with events ------------------- Now we will write a small program that captures the image data we can generate by calling the ``start`` method. .. code-block:: python :caption: image_printer.py from heros import RemoteHERO if __name__ == "__main__": my_first_hero_remote = RemoteHERO("my_first_hero") my_first_hero_remote.acquisition_data.connect(print) This script first opens a connection to the remote HERO ``my_first_hero`` and then connects the standard Python ``print`` function as a listener to the event ``acquisition_data`` which is emitted by ``my_first_hero`` when a picture is taken. Run the script with ``uv run image_printer.py`` and press ``start`` in the HERO monitor of ``my_first_hero``. You can now observe that every time you press the ``start`` button, i.e. "take an image", the ``image_printer`` program prints output like: .. code-block:: python [array([[2782, 0, 0, ..., 0, 0, 0], [ 0, 3070, 1103, ..., 0, 496, 3043], [3281, 0, 0, ..., 0, 0, 2478], ..., [ 0, 0, 0, ..., 3408, 0, 0], [2837, 4637, 0, ..., 0, 0, 115], [ 0, 0, 0, ..., 673, 0, 161]], shape=(200, 200), dtype=uint16), {'frame': 1}] The printed payload is an array with two elements, the image data itself and a dictionary with metadata. As we did not configure anything else [#f2]_, only the running number of the frame is in the metadata. You can close the program now by pressing ``strg + c`` .. [#f2] Metadata can for example be used with the `Disk Archiver `_ to tell it where to save a certain image. .. tip:: You can also run the ``image_printer`` program on multiple machines simultaneously. Then you will observe that both print the emitted image. This is an important tool to implement e.g. simultaneous data storage and evaluation pipelines. Now let us expand the ``image_printer`` program to use our own function as callback: .. code-block:: python :caption: image_printer.py from heros import RemoteHERO def print_frame(payload: tuple) -> None: frame_num = payload[1]["frame"] print(f"Got an image with number {frame_num} and shape {payload[0].shape}") if __name__ == "__main__": my_first_hero_remote = RemoteHERO("my_first_hero") my_first_hero_remote.acquisition_data.connect(print_frame) Now when generating images again, you will observe our ``print_frame`` function being called every time: .. code-block:: bash Got an image with number 7 and shape (200, 200) Got an image with number 8 and shape (200, 200) Got an image with number 9 and shape (200, 200) Got an image with number 10 and shape (200, 200) .. tip:: Learn more about events and callbacks :ref:`here `. Up to this point we used the HERO Monitor GUI to trigger and arm the camera. Naturally we can also do this via Python code. Let's expand our image printer script by these functions. .. code-block:: python :caption: image_printer.py from heros import RemoteHERO def print_frame(payload: tuple) -> None: frame_num = payload[1]["frame"] print(f"Got an image with number {frame_num} and shape {payload[0].shape}") if __name__ == "__main__": my_first_hero_remote = RemoteHERO("my_first_hero") my_first_hero_remote.acquisition_data.connect(print_frame) # Check remote attribute if the camera is armed. if my_first_hero_remote.acquisition_running: # If it is already running, stop the previous acquisition print("Previous acquisition still running, stopping...") my_first_hero_remote.reset() # Arm the camera my_first_hero_remote.arm() # Check remote attribute again. print("acquisition_running:", my_first_hero_remote.acquisition_running) # Send software triggers to the camera for i in range(4): print("Sending trigger {i}...") my_first_hero_remote.start() Notice how we simply **call the methods** and **read/write the attributes** of the remote object as though it were local. Running the script again with ``uv run image_printer.py`` will generate the following (or similar) output: .. code-block:: Previous acquisition still running, stopping... acquisition_running: True Sending trigger {i}... Sending trigger {i}... Got an image with number 0 and shape (200, 200) Sending trigger {i}... Got an image with number 1 and shape (200, 200) Sending trigger {i}... Got an image with number 2 and shape (200, 200) Got an image with number 3 and shape (200, 200) Here we can see the asynchronous nature of the event mechanism: The first image arrives only after the second trigger is already sent. .. _getting_started_heros-sensor: Setting up a Sensor and Monitoring its state -------------------------------------------- Now we have seen how we can use HEROS to control a device and acquire data from it. Another important aspect of running a complex machine is **continuously monitoring** its state. In this section we will set up a small sensor, which automatically publishes its data to the network. From there, another process can gather that data and save it into a database. Lets first set up the code for our dummy sensor: .. code-block:: python :caption: my_sensor.py import numpy as np class DummySensor: scale: float = 1.0 def __init__(self): self.value = 0 def read_value(self): self.value = np.cos(self.value) return self.value Here we set up a simple class which implements a ``read_value`` method, which returns a dummy "sensor" value. With the JSON description for our dummy sensor .. code-block:: json :caption: my_first_monitoring.json { "_id": "my_dummy_sensor", "classname": "my_sensor.DummySensor", "arguments": {}, "datasource": { "interval": 30, "observables":{ "my_sensor_value": { "target": "read_value", "unit": "°C" } } } } we can use BOSS to do all the heavy lifting again. By defining the ``datasource`` key, BOSS automatically creates and ``observable_data`` event based on the defined ``observables`` and triggers/emits it every ``interval`` second (here every 30 second). The entries in the ``observables`` dict tell BOSS that it should call the method ``read_value`` to get the observable ``my_sensor_value`` and that the returned value has the unit "°C". .. tip:: Observables also support advanced features like conversions and bounds for the emitted values. Learn more about that :external:ref:`here `. .. note:: Many of the device representations in :repo-doc-link:`herosdevices` also implement a default set of observables which are emitted if the ``observables`` key is not specified. If you want to directly use a real device in this tutorial, check out for example the :ref:`device_DLPro` Driver. To capture the data from all your data sources, :repo-doc-link:`herostools` provides the :external:py:class:`herostools.actor.statemachine.HERODatasourceStateMachine`. It captures all ``observable_data`` events and aggregates it together with a timestamp on a Prometheus-compatible endpoint. Install it with: .. code-block:: bash uv pip install herostools We can add it to our JSON description as follows: .. code-block:: json :caption: my_first_monitoring.json { "rows": [ { "_id": "my_dummy_sensor", "classname": "my_sensor.DummySensor", "arguments": {}, "datasource": { "interval": 30, "observables":{ "my_sensor_value": { "target": "read_value", "unit": "°C" } } } }, { "_id": "statemachine", "classname": "herostools.actor.statemachine.HERODatasourceStateMachine", "arguments": { "loop": "@_boss_loop", "http_port": 9099, "bind_address": "0.0.0.0" } } ] } This now starts the statemachine on all interfaces of your machine (``"bind_address": "0.0.0.0"``) on port ``9099``. Now navigate to `http://localhost:9099/metrics `_. After the sensor sent its first metrics, you will see something like: .. code-block:: bash my_dummy_sensor_my_sensor_value_inbound{prefix="my_dummy_sensor",key="my_sensor_value"} -1 1779285321560 my_dummy_sensor_my_sensor_value_value{prefix="my_dummy_sensor",key="my_sensor_value",unit="°C"} 0.7934803587425656 1779285321560 my_dummy_sensor_my_sensor_value_raw_value{prefix="my_dummy_sensor",key="my_sensor_value",unit="°C"} 0.7934803587425656 1779285321560 If you want to continue with the setup of your realtime hardware, continue with the :ref:`Atomiq tutorial `... If you want to learn more about how to deploy HEROS in production, continue with :ref:`deploy_boss`...