.. 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 ``: