.. index::
single: hardware configuration
single: HardwareRegistry; configuration layer
single: HardwareProfileManager; configuration layer
single: RuntimeHardwareConfig; configuration layer
single: LoadoutManager; configuration layer
single: REGISTER_HARDWARE_META
single: REGISTER_HARDWARE_SETTINGS
single: REGISTER_HARDWARE_BASE
single: REGISTER_HARDWARE_ARRAY
single: REGISTER_LIBRARY
single: REGISTER_CUSTOM_COMM
single: HwSettingPriority
single: HwSettingsWidget; create vs edit mode
single: AddProfileDialog
single: HWDialog
single: RuntimeHardwareConfigDialog
single: hardware profiles; persistence
single: loadouts; configuration
single: FTMW presets; configuration
single: __LastUsed__
single: drift detection
Hardware Configuration
======================
Blackchirp's hardware story has two halves. The *configuration* half — what
this page covers — describes the four singletons that decide which hardware
drivers exist, which user-visible profiles have been created from
them, which profiles are *active* in the running session, and which
profiles are grouped together as a named loadout. The *runtime* half —
:doc:`/developer_guide/hardware_runtime` — picks up where this page leaves
off: instantiating :cpp:class:`HardwareObject` instances, moving them onto
the right threads, opening their communication channels, and routing their
signals through :cpp:class:`HardwareManager`.
The configuration layer is built on four loosely coupled singletons. None
of them owns a live instrument; together they describe everything
:cpp:class:`HardwareManager` needs to know to bring instruments online.
The split reflects different read-access policies. The registry is
populated at static-initialization time and is everywhere-readable but
write-restricted to its own registration helpers. The profile manager
is the sole authority on per-profile metadata and settings. The runtime
configuration accepts reads from any thread but restricts writes to a
short list of friend classes. The loadout manager funnels all CRUD
through a single API.
The user-facing surface of the same four layers is the **Hardware
Configuration** dialog. The dialog's four panels are one-to-one with the
configuration singletons; orienting yourself to "panel X drives singleton
Y" is the fastest way into this part of the codebase. The user-guide
side of the same workflow is on :doc:`/user_guide/hardware_config`.
Compile-time to runtime: a layer at a time
------------------------------------------
A useful mental model is four layers of an onion, stacked from
compile-time outward:
1. **Catalog (compile time).** Every concrete driver registers itself
with :cpp:class:`HardwareRegistry` at static-initialization time using
the ``REGISTER_HARDWARE_*`` macros. This runs before ``main()``, so the
catalog of factories, supported communication protocols, setting
descriptors, and library dependencies is complete before any other
code runs.
2. **Profile metadata (per process).** When the application starts,
:cpp:class:`HardwareProfileManager` loads any persisted profile
records from ``QSettings`` into its in-memory cache. A *profile* is
a ``(hardwareType, label, driver)`` triple — fixed at
creation time — together with its persisted settings and, for
Python drivers, a script path / class name / environment path. The
``hardwareType`` and ``label`` together form the profile's identity
(``"FtmwDigitizer.frontPanel"``, ``"FlowController.backup"``); the
driver is immutable, so changing drivers means
creating a new profile rather than re-pointing an existing one.
3. **Active selection (per loadout).** :cpp:class:`RuntimeHardwareConfig`
records *which* profiles are active in the current runtime
configuration, keyed by profile identity. This is the layer
:cpp:class:`HardwareManager` reads when it instantiates objects.
4. **Named hardware maps (across sessions).** :cpp:class:`LoadoutManager`
stores named *loadouts* — each a complete hardware map plus its
collection of FTMW presets — so the user can switch between fully
prepared instrument configurations without manually toggling each
profile.
.. mermaid::
flowchart LR
subgraph Compile[Static initialization]
Macros[REGISTER_HARDWARE_* in driver .cpp files]
end
subgraph Persist[QSettings on disk]
QS[(QSettings: HardwareProfiles/... runtimeHardware/... Loadouts/...)]
end
subgraph Singletons[Process-wide singletons]
Reg[HardwareRegistry factories · settingDefs protocols · libraries]
PM[HardwareProfileManager profile metadata · per-profile settings]
RC[RuntimeHardwareConfig active type.label → impl map]
LM[LoadoutManager named hardware maps + FTMW presets]
end
Macros --> Reg
PM <--> QS
RC -. derived from active profiles .-> PM
LM <--> QS
LM -- applyHardwareMap --> RC
Reg -. settingDefs .-> PM
PM -. profile settings .-> HM[HardwareManager]
RC -. active selections .-> HM
The four singletons are not collapsed into a single class because each
serves a different boundary. The registry is the only writer of
factories and setting descriptors and is read everywhere. The profile
manager is the only authority on what a profile identity means. The
runtime configuration mediates between the GUI's preview state and the
live hardware map. The loadout manager brackets all of the above into
named records the user can save, recall, and share.
HardwareRegistry — the catalog
------------------------------
:cpp:class:`HardwareRegistry` is a pure catalog. It does not check
availability, resolve dependencies, or fall back to alternatives; it
simply records what was registered and constructs instances on demand
through stored factory lambdas. See :doc:`/classes/hardwareregistry` for
the per-method API.
Each driver file registers itself by placing one or more macros at file
scope in its ``.cpp`` file. The macros are defined in
``hardware/core/hardwareregistration.h`` and run during
static-initialization. Eight macros cover the registration surface:
``REGISTER_HARDWARE_META(CLASS, DESC)``
Primary registration. Declares the factory, the inheritance chain,
and the description. Must appear before any other macro for the
same class.
``REGISTER_HARDWARE_PROTOCOLS(CLASS, ...)``
Lists the :cpp:enum:`CommunicationProtocol::CommType` values the
driver supports (``Rs232``, ``Tcp``, ``Gpib``, ``Custom``,
``Virtual``).
``REGISTER_HARDWARE_SETTINGS(CLASS, ...)``
Declares :cpp:struct:`HwSettingDef` descriptors for the
driver's scalar settings: key, label, description,
type-aware default, optional bounds, and
:cpp:enum:`HwSettingPriority`.
``REGISTER_HARDWARE_BASE(CLASS, ...)``
Same shape as ``REGISTER_HARDWARE_SETTINGS``, but for a
non-instantiable base class (``Clock``, ``FtmwDigitizer``,
``HardwareObject`` itself). Settings are merged into every
driver whose inheritance chain contains the base class.
``REGISTER_HARDWARE_ARRAY`` / ``REGISTER_HARDWARE_ARRAY_ENTRY``
Declare an array setting and append entries to it. Each entry is a
:cpp:type:`SettingsStorage::SettingsMap` of sub-key/value pairs
(sample-rate tables, range tables, …).
``REGISTER_HARDWARE_BASE_ARRAY`` / ``REGISTER_HARDWARE_BASE_ARRAY_ENTRY``
Array equivalents for base classes. Calling
``REGISTER_HARDWARE_BASE_ARRAY`` with no entries reserves the array
key so it is always rendered in the settings dialog, even for
drivers (Python-backed drivers, virtual drivers) that supply
no entries of their own.
``REGISTER_LIBRARY(CLASS, LIBRARY_NAME)``
Records a :cpp:class:`VendorLibrary` dependency, so the registry can
answer "which drivers must be torn down before this library is
reloaded?".
``REGISTER_CUSTOM_COMM`` / ``REGISTER_CUSTOM_COMM_BASE``
Declare :cpp:struct:`CustomCommDef` field descriptors for drivers
whose communication type is ``CommunicationProtocol::Custom``. The
GUI reads these descriptors before construction so it can render the
right input widgets without instantiating the driver.
Hardware-type and driver keys are derived from Qt's
``staticMetaObject`` rather than passed by hand. ``REGISTER_HARDWARE_META``
walks the metaobject ``superClass()`` chain to find the direct child of
:cpp:class:`HardwareObject` (the *type* key — ``Clock``, ``FtmwDigitizer``,
``Awg``, …), and uses ``CLASS::staticMetaObject.className()`` for the
*driver* key (the class name itself — ``Valon5009``,
``M4i2220x8``). Renaming a class therefore renames its registry key for
free; there is no parallel string table to update.
Base / driver override pattern
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A setting registered with ``REGISTER_HARDWARE_BASE`` is shared by every
driver that inherits from the base. A driver that needs
a different default (or different bounds, or a different priority) for
the same key re-registers the key with ``REGISTER_HARDWARE_SETTINGS``.
:cpp:func:`HardwareRegistry::getSettingDefs` returns the
driver's entry first and skips the base-class entry for that
key, so no duplicate row appears in the UI and
:cpp:func:`HardwareObject::applyRegisteredSettings` writes the right
default. The same precedence applies to arrays.
This is the lever for tuning a driver's defaults without copy-pasting
the whole base-class set: register only what differs.
Priority levels and UI placement
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:cpp:enum:`HwSettingPriority` tags every setting with one of three
levels. The level determines where the setting appears in
:cpp:class:`HwSettingsWidget` — the shared widget used by both the
profile creation flow and the post-creation edit dialog.
.. list-table::
:header-rows: 1
:widths: 20 80
* - Priority
- UI placement
* - ``Required``
- Top form on the *Settings* tab. Editable in Create mode;
read-only in Edit mode. Examples: digitizer channel count,
pulse-generator channel count.
* - ``Important``
- Always-visible two-column table on the *Settings* tab.
Editable in both modes. Examples: sample-rate tables, voltage
ranges.
* - ``Optional``
- Collapsible *Advanced* tab. Editable in both modes. Examples:
rolling-data interval, "critical hardware" flag.
Required settings carry a hard contract: they must be correct *before*
the hardware object is constructed, because the constructor reads them
to size internal arrays, allocate buffers, and so on. That is why
:cpp:class:`HwSettingsWidget` renders them read-only in Edit mode — the
profile must be deleted and recreated to change a Required value. See
:doc:`/classes/hwsettingswidget` for the per-mode behavior and
:doc:`/classes/hardwareregistry` for the full
:cpp:struct:`HwSettingDef` field reference.
HardwareProfileManager — profile metadata
-----------------------------------------
:cpp:class:`HardwareProfileManager` owns the persistent collection of
profiles. A profile is the tuple
``(hardwareType, label, driver, settings, [pythonScriptPath,
pythonClassName, pythonEnvPath])``. The
``(hardwareType, label, driver)`` triple is set when the
profile is created and is immutable thereafter — only the settings
and the per-profile metadata fields (description, threading override,
Python paths) can be edited later. To use a different driver,
create a new profile.
The pair ``: