Adding a New Hardware Type
Adding a new abstract hardware type — a new interface class that no
existing driver matches, alongside AWG,
Clock, FtmwDigitizer, FlowController,
and the eight other types Blackchirp ships — is the rarer and broader
contributor task. It is a coordinated change across hardware/,
data/experiment/, and gui/ rather than a self-contained file
drop, so the recipe for it is correspondingly larger than the one for
Adding a New Hardware Driver.
This page walks the design and integration steps end to end: deciding
that a new type is genuinely warranted, picking the state-management
shape, sketching the interface class, wiring it into the build,
authoring the optional config object that travels with the experiment,
hooking the GUI surfaces (status box, control widget, experiment-setup
page) so the device is usable, plumbing the per-type fan-out through
HardwareManager, and recommending the Python trampoline
and test fixtures that round out the type. Before reading further,
make sure you have already read
Adding a New Hardware Driver — the C++ surface a new
driver carries (constructor signature, registration macros,
HardwareObject::initialize() /
HardwareObject::testConnection(), aux/validation data) is
the same surface every driver of the new type will carry, and
this page does not repeat it.
When this applies vs. adding a driver
A new hardware type is justified when no existing interface class
captures the role the new device will play in Blackchirp, even
loosely. The litmus test is the existing types’ interfaces: if any
one of them could be adapted with reasonable effort — by adding a
new hw* virtual, a new aux-data key, or a small extension to its
config — that is the right move. Adding a new type pays for itself
only when the role is genuinely new; otherwise you are creating a
new branch in the dispatch logic of every cross-cutting subsystem
(HardwareManager, the Hardware menu in
MainWindow, ExperimentSetupDialog, the
test-hardware library) for a device that an existing type would
have absorbed.
Concretely:
A new model of an existing role — a different vendor’s mass flow controller, a different GPIB-attached synthesizer, a different AWG — is a new driver against an existing type. Use Adding a New Hardware Driver.
A new role that does not yet exist — a beam blocker, a magnetic field coil, a sample-loading robot — is a new type. This page applies.
Drivers carry no cross-system blast radius beyond their own
.cpp/.h pair (and hardwarekeys.h if they declare new
keys). Types carry it everywhere. Plan accordingly.
Designing the interface
Before writing code, decide six things. Each one shapes the interface class and is awkward to revisit later because every driver of the type has to be reworked alongside it.
State-management pattern
The C++ patterns from Adding a New Hardware Driver (State-management patterns) are not driver choices — they are type choices. The interface class commits to one pattern; every driver follows it. The same A/B/C taxonomy describes the Python trampolines on Python Hardware, so a new type’s pattern also decides what its Python side will look like.
Pattern A (Bulk Configure) when the type owns or carries a complex config object — channel maps, trigger settings, per-output state — and the experiment hands the device a fully- formed configuration in one shot. The interface class typically inherits its config object via multiple inheritance (the way
IOBoardinherits fromIOBoardConfig, andFtmwDigitizerfromFtmwDigitizerConfig), final-overridesHardwareObject::hwPrepareForExperiment()to pull the desired config out of the experiment, dispatch a pure-virtualconfigure(config&)to the driver, and write the validated config back throughExperiment::addOptHwConfig().Pattern B (Granular methods) when interaction is per-channel or per-parameter — each call setting or reading one value. The interface class contains a config object as a member, exposes public
setX/readXslots, owns the polling sequence on aQTimer, and delegates the per-call hardware I/O tohw*pure virtuals.FlowController,PulseGenerator,PressureController, andTemperatureControllerall follow this pattern.Pattern C (Stateless / pass-through) when the type carries no internal config to manage and is configured exclusively at experiment time. The interface class is intentionally thin; each driver overrides
HardwareObject::prepareForExperiment()directly to read the per-experiment data out of theExperiment, program the hardware, and return.AWGandClockfollow this pattern.
The pattern interacts with how readily users can supply Python
drivers for the new type: Pattern A wants a single configure
JSON dispatch per experiment, Pattern B wants one IPC round trip
per hw* call, Pattern C wants a single
prepare_for_experiment dispatch with the experiment payload.
The trampoline contract on Python Hardware
maps each pattern onto an explicit recipe.
Threading and criticality defaults
Set d_threaded in the interface-class constructor body. Most
hardware types are threaded — vendor I/O is expensive enough that
running it on the manager thread starves every other device — and
the interface class is the right place to set the default so every
driver inherits it. The user can still override the per-
profile threading at profile creation time
(Hardware Runtime covers the runtime side).
The same is true for d_critical, which defaults to true;
override only when the device is non-essential by nature (the
existing Clock / FtmwDigitizer types use the
default; an instrument whose absence should never abort an
experiment is the rare exception).
Once a type sets d_threaded = true, the
threaded-hardware constructor restriction from
Hardware Runtime applies to every
driver: no QObject parent at construction, and no
child QObject constructed in the constructor. Construct
children inside HardwareObject::initialize(). Document
this once at the top of the new interface header so driver authors
do not have to rediscover it.
Supported communication protocols
Decide which subset of
CommunicationProtocol::Rs232,
CommunicationProtocol::Tcp,
CommunicationProtocol::Gpib,
CommunicationProtocol::Custom, and
CommunicationProtocol::Virtual the type can
support across drivers. Each driver further narrows the
set with its own REGISTER_HARDWARE_PROTOCOLS invocation. Most
new types should at least allow Virtual so the system-profile
fall-back pattern (every required type carries a virtual
profile, see
Hardware Configuration) extends to the
new type.
Optional config object: decide if you need one
If the type carries experiment-time configuration that should
travel with the experiment record, plan a dedicated
HeaderStorage subclass — the role
FlowConfig plays for FlowController,
IOBoardConfig for IOBoard,
LifDigitizerConfig for LifDigitizer. The
config object owns the storeValues / retrieveValues / prepareChildren
contract from Persistence; the interface
class registers the validated config with
Experiment::addOptHwConfig() from
HardwareObject::hwPrepareForExperiment() (Pattern A) or
HardwareObject::prepareForExperiment() (Pattern B/C).
Experiment owns the registered copy via
std::shared_ptr<HeaderStorage>, keyed by header key, and
Experiment::getOptHwConfig() hands it back to the GUI
and to driver code that needs to inspect it.
The interface class
Sketch for a new type BeamBlocker that uses Pattern B (per-
channel granular methods). Adapt the pattern to A or C by removing
the hw* virtuals and the public-slot wrapper, and overriding
either HardwareObject::hwPrepareForExperiment() (Pattern A)
or HardwareObject::prepareForExperiment() (Pattern C)
instead.
// beamblocker.h
#include <hardware/core/hardwareobject.h>
#include <data/settings/hardwarekeys.h>
namespace BC::Key::BeamBlocker {
inline constexpr QLatin1StringView numChannels{"numChannels"};
inline constexpr QLatin1StringView pollInterval{"pollInterval"};
}
class BeamBlocker : public HardwareObject
{
Q_OBJECT
public:
BeamBlocker(const QString& impl, const QString& label,
QObject *parent = nullptr);
~BeamBlocker() override;
QStringList validationKeys() const override;
public slots:
void setBlocked(int channel, bool blocked);
bool readBlocked(int channel);
signals:
void blockedUpdate(int channel, bool blocked, QPrivateSignal);
protected:
virtual void hwSetBlocked(int channel, bool blocked) = 0;
virtual int hwReadBlocked(int channel) = 0;
AuxDataStorage::AuxDataMap readAuxData() override;
private:
int d_numChannels{0};
};
// beamblocker.cpp
#include "beamblocker.h"
#include <hardware/core/hardwareregistration.h>
REGISTER_HARDWARE_BASE(BeamBlocker,
{BC::Key::BeamBlocker::numChannels, "Channels",
"Number of beam blocker channels.",
2, 1, 16, HwSettingPriority::Required},
{BC::Key::BeamBlocker::pollInterval, "Poll Interval (ms)",
"Interval between blocker readbacks in milliseconds.",
500, 1, QVariant{}, HwSettingPriority::Optional}
)
BeamBlocker::BeamBlocker(const QString& impl, const QString& label,
QObject *parent) :
HardwareObject(QString(BeamBlocker::staticMetaObject.className()),
impl, label, parent),
d_numChannels(get(BC::Key::BeamBlocker::numChannels, 2))
{
d_threaded = true;
// d_critical defaults to true; leave alone unless intentionally optional.
}
A few pieces are worth calling out:
The base-class constructor takes
hwTypefromstaticMetaObject.className(). Every driver calls the base- class constructor with its ownstaticMetaObject.className()forhwImpl, so the combination yields a uniqued_keyof"BeamBlocker.<label>"for the type and"<DriverName>"for the driver. Renaming the class renames the registry key.REGISTER_HARDWARE_BASEis the type-level counterpart ofREGISTER_HARDWARE_SETTINGS. The settings declared here are applied to every driver throughHardwareObject::applyRegisteredSettings()from the base-class constructor; a driver that needs different defaults re-registers the same keys withREGISTER_HARDWARE_SETTINGS, and the driver-level entry wins. The macro reference is on HardwareRegistry.The public
setBlocked/readBlockedslots are the surfaceHardwareManagerand the GUI consume; they call the protectedhw*pure virtuals, emitblockedUpdateso observers can react, and handle errors. Keep thehw*virtuals as narrow as possible — a single command/response interaction per call — so each driver can be written without re-reasoning about the polling cadence or the signal protocol.
Wiring into the build
A new hardware type touches the build system in three places. The Build System and Project Layout page covers the cmake layout in detail; the points specific to a new type are:
Place the interface source files. Pick whether the type is core (required to run an FTMW experiment, like
ClockorFtmwDigitizer) or optional (everything else). Put the interface.cpp/.hin a new subdirectory undersrc/hardware/core/<type>/orsrc/hardware/optional/<type>/. Drivers of the new type will live alongside the interface in the same directory.Add the interface .cpp to
HARDWARE_TYPES_SOURCES. The list incmake/BlackchirpHardware.cmakeenumerates every interface.cppexplicitly. Append the new type’s interface path; the driver source files are picked up by the glob pattern in step 4.Add the interface .h to
HARDWARE_TYPE_HEADERS. The same file enumerates every interface header explicitly so the generatedhw_base.haggregator picks them up. The aggregator is what givesHardwareRegistryaccess to every interface metaobject at static-init time.Add the new directory’s glob patterns to
HARDWARE_IMPLEMENTATIONS_SOURCESandHARDWARE_IMPLEMENTATION_HEADERS.BlackchirpHardware.cmakediscovers driver source files by directory and filename prefix —virtual*,mks*,awg*, and so on. A new directory means a new pair of glob patterns. Use the existing per-type blocks as templates; the entries forflowcontroller/are a minimal model.
After adding source files, re-run cmake so the globs are
re-evaluated; cmake --build alone will not pick up new files.
Optional config object
When the type needs experiment-time configuration that should
travel with the experiment record, the convention is one
HeaderStorage subclass per type, owning all of the type’s
configurable state. Convention:
File:
src/data/experiment/hardware/<core|optional>/<type>/<typename>config.{cpp,h}.Class:
<TypeName>Config, inheriting fromHeaderStorage. Constructor takes the parent hardware key soHeaderStorage::headerKey()resolves correctly.Override
HeaderStorage::storeValues()/HeaderStorage::retrieveValues()to round-trip the scalar fields. OverrideHeaderStorage::prepareChildren()if the config has its own nestedHeaderStoragechildren (rare for hardware configs).Add value keys in a
BC::Store::<TypeName>::sub-namespace next to the class, per Persistence (Key namespaces).
The interface class registers the validated config on the
experiment by calling Experiment::addOptHwConfig()
from inside its experiment-preparation hook (see Lifecycle hooks
below). Experiment owns the registered copy via
std::shared_ptr<HeaderStorage>, keyed by the config’s header
key. Reading the config back — from a GUI page, from a Python
trampoline, from another HardwareObject — goes
through Experiment::getOptHwConfig(), which returns a
std::weak_ptr typed to the requested config class.
If the config object should be configurable from the experiment-
setup wizard, plan a corresponding
ExperimentConfigPage subclass at the same time;
GUI integration below covers the page side.
Lifecycle hooks
The interface class is also where the type-level lifecycle overrides land. Most types do not need to override every hook — pick the minimal set that delivers the type’s contract.
HardwareObject::hwPrepareForExperiment()for Pattern A types. Final-override the wrapper, pull the desired config out ofExperiment, dispatch a pure-virtualconfigure(config&)to the driver, write the validated config back viaExperiment::addOptHwConfig(). TheIOBoardimplementation insrc/hardware/optional/ioboard/ioboard.cppis the canonical template.HardwareObject::prepareForExperiment()for Pattern B/C types. The base class’sHardwareObject::hwPrepareForExperiment()already reattempts a connection if disconnected and dispatches toHardwareObject::prepareForExperiment(); a Pattern B type final-overrides the inner virtual to push experiment-time setpoints to the device, register aux-data keys withAuxDataStorage::registerKey(), and callExperiment::addOptHwConfig(). TheFlowControllerimplementation insrc/hardware/optional/flowcontroller/flowcontroller.cppis the canonical Pattern B template; for Pattern C, each driver overridesHardwareObject::prepareForExperiment()directly and the interface class itself stays out of the way (seeAWG).HardwareObject::beginAcquisition()/HardwareObject::endAcquisition()when the type needs to start or stop hardware actions at experiment boundaries — an AWG starting waveform playback, a digitizer arming for triggers. Default is a no-op.HardwareObject::sleep()when the hardware supports a low-power state and you wantHardwareManagerto put the device into it between experiments. Default is a no-op.HardwareObject::hwReadSettings()when the user editing settings inHWDialogshould refresh in-driver cached state (a re-read of channel-count after a Required setting change, a re-validation of array sizes). The base class dispatches to it fromHardwareObject::bcReadSettings()after it has reloaded settings from disk and refreshedd_criticalandd_commType.When the type itself owns work that must run on every settings refresh (a poll-interval applied to a base-class
QTimer, a base-class config rebuilt against a new array size), follow the NVI pattern the existing types use:final-overrideHardwareObject::hwReadSettings()in the interface class, do the type-level work there, then call a new per-type virtual hook with a default no-op body that drivers and trampolines override. The hook name follows the per-type prefix already used forinitialize/testConnection(e.g.fcReadSettings,pcReadSettings,tcReadSettings,pgReadSettings,awgReadSettings,clockReadSettings,ftmwReadSettings,gpibReadSettings,ioReadSettings,lifLaserReadSettings,lifDigitizerReadSettings); pick the matching prefix for the new type. ThefinalonhwReadSettingsmakes it impossible for a derived driver — most importantly a Python trampoline — to silently skip the type-level refresh by forgetting to chain to the base.
The full bring-up sequence (bcInitInstrument →
buildCommunication → initialize → bcTestConnection)
is described on Hardware Runtime.
GUI integration
A new hardware type usually contributes three GUI surfaces. Each is optional, but each is what makes the new type usable from the running application; do not skip them unless the type is genuinely headless.
Status box
The HardwareStatusBox subclass that shows live
device state on the Hardware Status panel.
File:
src/gui/widget/<typename>statusbox.{cpp,h}.Inherits
HardwareStatusBox(which is aQFramecarrying the configure-requested signal so clicking the box opens the per-device dialog).Subscribes to the type-specific update signals on
HardwareManager(see HardwareManager fan-out below) and renders them into the layout.
Status boxes are not auto-discovered. The dispatch site is the
hwType if/else chain inside
MainWindow::buildHardwareUI(). Add a new else if
branch keyed on
QString(BeamBlocker::staticMetaObject.className()): construct
the status box, add it to ui->hwStatusLayout, wire its
configureRequested signal to the menu action, connect every
type-specific HardwareManager update signal to the
box’s update slots. Use the existing
PressureStatusBox and PulseStatusBox
branches as templates.
Control widget
The widget that occupies the Control tab of
HWDialog for live device interaction.
File:
src/gui/widget/<typename>controlwidget.{cpp,h}.Inherits
QWidget(andSettingsStorageif the widget itself needs persistent state — seeGasControlWidgetfor the multiple-inheritance pattern).Communicates with the live
HardwareObjectonly throughHardwareManagerslots, never directly. The threaded-hardware threading rules from Hardware Runtime mean every interaction must be queued through a manager slot so the call lands on the device’s thread. This is also why the widget takes a manager pointer (or no hardware-side reference at all, with the manager-side connections wired byMainWindow) instead of a hardware-object pointer.
Like the status box, the control widget is wired in
MainWindow::buildHardwareUI() — inside the same
else if branch you added for the status box. Construct the
control widget on the menu action’s triggered slot, connect
it to the manager’s update signals and to its own
type-specific slots, and pass it to createHWDialog so it
appears as the Control tab. The
GasControlWidget plus
HardwareManager::flowSetpointUpdate() plumbing in the
FlowController branch is a clean template.
Experiment-setup page
The page in ExperimentSetupDialog that lets the
user configure the type’s experiment-time settings before the
experiment starts. Only relevant when the type contributes an
optional config object.
File:
src/gui/expsetup/experiment<typename>configpage.{cpp,h}.Inherits
ExperimentConfigPage(which is itself aQWidgetplusSettingsStorage).Constructor signature
(const QString hwKey, const QString title, Experiment *exp, QWidget *parent = nullptr)to match theaddOptHwPages<T>template thatExperimentSetupDialoguses to instantiate one page per active profile of the type.Implements the slots
ExperimentConfigPage::initialize(),ExperimentConfigPage::validate(), andExperimentConfigPage::apply().applyis the hook that callsExperiment::addOptHwConfig()with the page’s edited config so the experiment record carries the user’s choices into the acquisition.
The dispatch site is the constructor of
ExperimentSetupDialog, which already calls
addOptHwPages<PageT>(hwTypeName, expTypeItem) once per
page-bearing hardware type. Add a new line for the new type:
addOptHwPages<ExperimentBeamBlockerConfigPage>(
QString(BeamBlocker::staticMetaObject.className()), expTypeItem);
The addOptHwPages template walks the active hardware map,
filters to the requested type, and instantiates one page per
matching profile, so the dialog automatically shows one
configuration page per profile of the new type without further
plumbing. ExperimentFlowConfigPage in
src/gui/expsetup/experimentflowconfigpage.{cpp,h} is a
minimal Pattern B template; ExperimentIOBoardConfigPage is
the Pattern A counterpart.
Auxiliary and validation data
The aux/validation pipeline is the same one Adding a New Hardware Driver describes for new drivers. From the type’s perspective:
HardwareObject::readAuxData()returns the per-experiment readings the type produces — temperatures, flows, blocker positions, anything worth plotting on the Aux/Rolling tabs and persisting inAuxDataStorage. For Pattern B types, implement this on the interface class so every driver gets the aux-data emission for free; the base class’s polling sequence already populates the cached statereadAuxDatareads from. SeeFlowController::readAuxDatafor the convention.HardwareObject::readValidationData()returns the subset of readings the validator should range-check. The keys must be a subset of the values returned byHardwareObject::validationKeys().
Both run from the wrapper
HardwareObject::bcReadAuxData(). The
HardwareManager prefixes the per-device map keys
with the source object’s hwKey before fanning the data out
to consumers, so multiple instances of the new type
distinguish themselves automatically. The full plumbing is on
Hardware Runtime (Auxiliary, validation,
and rolling data); the persistence side is on
AuxDataStorage.
HardwareManager fan-out
HardwareManager already handles the generic
auxData / validationData / rollingData signals with
hwKey prefixing — no per-type code needed there.
Type-specific signals — per-channel updates, configuration
broadcasts, mode changes — go through the convention already
established for the existing types: a HardwareManager signal
named <typename>Update(QString hwKey, …) (or a small set of
related signals) that the manager forwards from the source
HardwareObject’s signal of the same shape. The
forwarding installs in the manager’s
setupHardwareObjectWithTracking helper or in the per-type
branch of HardwareManager::syncWithRuntimeConfig(),
depending on whether the signal is generic or type-specific.
The pattern to follow is
HardwareManager::flowUpdate() (hwKey, channel,
value) and
HardwareManager::pressureControlMode() (hwKey,
mode) — every signal carries the source hwKey as its first
argument, so consumers (status boxes, control widgets, the Aux
tab) can disambiguate readings from multiple instances of the
new type without enumerating the active profiles in advance. The
existing per-type signal lists on HardwareManager
are the ground truth.
Virtual driver
Every hardware type in Blackchirp ships with a Virtual<TypeName>
driver, and a new type is no exception. The virtual driver
is not a test-only artifact — it is the driver Blackchirp
itself runs whenever the user has not configured a real device, and
it is what makes the new type visible in the running application
from the moment the type lands. Plan and write it alongside the
interface class, not after.
Three responsibilities the virtual driver carries inside the application:
System-profile fall-back.
HardwareProfileManagerguarantees that every required hardware type carries a profile labeledvirtualbacked by the type’s virtual driver, soHardwareManager::initialize()always has something to instantiate even when no real hardware is configured (see Hardware Configuration, System profiles). Without a virtual driver, a new required type would leave Blackchirp in a state where it cannot start; without a virtual driver for a new optional type, users have no way to exercise the new GUI surfaces, the experiment-setup page, or the aux-data plumbing without first acquiring the real hardware.Live exercise of the new GUI surfaces. The status box, control widget, and experiment-setup page added in GUI integration above need a live
HardwareObjectto talk to during development. Wire the virtual driver up first, then iterate on the GUI against synthesized readings before chasing vendor protocol details on the bench.End-to-end experiment runs. A user evaluating Blackchirp, writing a Python trampoline, or setting up a new instrument configuration can run a full experiment end-to-end against the virtual driver — chirp generation, FID acquisition, aux-data recording, validation — without owning the real hardware. The virtual driver is what keeps that workflow possible for the new type.
Conventions for the virtual driver:
File:
src/hardware/<core|optional>/<type>/virtual<typename>.{cpp,h}, alongside the interface class. Class nameVirtual<TypeName>(matching the existingVirtualAwg,VirtualFlowController,VirtualIOBoard,VirtualFtmwDigitizerpattern).Inherits from the new interface class. Implements every pure virtual the interface declares — the
hw*slots for Pattern B types, theconfigure(config&)virtual for Pattern A types, and eitherHardwareObject::prepareForExperiment()or whatever per-driver hook the interface delegates to for Pattern C.Synthesizes plausible readings rather than returning fixed values. Use
QRandomGeneratorfor noise (the existingVirtualFlowControlleris the canonical model: flows wander around their setpoints, pressure drifts within bounds), and let the synthesized values track any user-driven setpoints so control-widget round trips behave the way they would against real hardware.Registers with
CommunicationProtocol::Virtualin itsREGISTER_HARDWARE_PROTOCOLSinvocation. TheVirtualprotocol carries no realQIODevice, so the driver’sHardwareObject::testConnection()simply returnstrue— the synthesis itself is the hardware contract.
The virtual driver also doubles as the canonical fixture for unit tests; the test-side wiring is in Tests below.
Optional Python trampoline
A new hardware type should ship with a Python trampoline so users can write drivers in Python without recompiling. The trampoline is one C++ class plus one Python template script; the C++ class is small.
Subclass both the new interface and
PythonHardwareBase. Initialize the mixin in the initializer list withd_keyandd_model:PythonBeamBlocker::PythonBeamBlocker(const QString &label, QObject *parent) : BeamBlocker(QString(PythonBeamBlocker::staticMetaObject.className()), label, parent), PythonHardwareBase(d_key, d_model) { d_threaded = true; save(); }
Pick the matching state-management pattern. The trampoline uses the same A/B/C taxonomy as the C++ side; see Python Hardware (Three state-management patterns) for the IPC shape per pattern.
Wire the mixin in the type-specific initialize/test hooks. For a plain
HardwareObjectsubclass that isHardwareObject::initialize()andHardwareObject::testConnection(); for hardware bases that final-override those (such asClockandFlowController) it is the typed helper virtual the base class calls into (initializeClock/testClockConnection,fcInitialize/fcTestConnection, …). Decide which the new type will use at the same time you draft the interface class.Provide
python_<typename>_template.pynext to the trampoline source. The host script’s generic dispatch picks methods up by name; the template defines a class with the canonical name<TypeName>Driver(BeamBlockerDriver) that works out of the box on theVirtualprotocol.
Tests
A new hardware type warrants three test additions:
Round-trip serialization for the optional config object. Add a fixture in
tst_headerstoragetest(or a newtst_<typename>configtestif the round-trip logic is non-trivial) that exercisesHeaderStorage::storeValues()andHeaderStorage::retrieveValues()for the new config class. The existingHeaderStoragefixtures are the template.Wire the virtual driver into
blackchirp-test-hardware. TheVirtual<TypeName>driver authored in Virtual driver above is the canonical test fixture; add its.cppto the explicit list of test-hardware sources in the top-levelCMakeLists.txt(alongsidevirtualflowcontroller.cpp,virtualawg.cpp, and the rest) so the test executables link against it. Tests built againstblackchirp-test-hardwarerely on the virtual driver being present for every active hardware type.A registration-pipeline assertion. Extend
tst_hardwareregistrytestwith a check that the new type’s factory is registered, that its protocol set is non-empty, and that the inheritance chain fromHardwareObjectis what the type expects. A typo inREGISTER_HARDWARE_BASEor a missingQ_OBJECTwill surface here.tst_hardwarekeyssimilarly catches collisions in the newBC::Key::<TypeName>::namespace.
Beyond the unit tests, follow the smoke-testing checklist in
Adding a New Hardware Driver (Smoke testing): build
with BC_BUILD_TESTS=ON, run the existing hardware suite,
launch the application, confirm the new type appears in the
Add Profile dialog under its hardware-type entry, and create a
profile against the Virtual protocol to verify the static
registration is intact.
Documentation follow-up
A new hardware type is a documentation event in three places. Plan all three at the same time you draft the interface; doing them in one pass keeps the type’s vocabulary consistent across chapters.
API reference. Add a class page at
doc/source/classes/<typename>.rstfollowing API reference style. The page carries a 1–3 paragraph orientation intro and a finalAPI Referencesection with the.. doxygenclass::directive. Doxygen comments in the header are the source of truth for member- level prose.User guide. Add a per-device page under
doc/source/user_guide/hw/<typename>.rstonce at least one concrete driver exists, mirroring the existing pages for AWG, flow controllers, pulse generators, and the rest. The user-facing pages are how operators discover that the new type is available.Developer guide. If the new type introduces a pattern not covered here — a new threading model, a new mid-experiment hook, a fourth state-management shape — flag it for a refresh of this page and of Adding a New Hardware Driver so future contributors do not have to reverse-engineer the precedent from your code.