Device support
How a device family's capabilities are declared across the codebase, and the checklist for adding support for a new device.
Every registered device carries a sensor family (its deviceType), chosen once at registration. That family is the single dispatch axis for everything device-kind specific: which local transports the web offers, whether the onboarding configuration can be pushed over a direct connection, which driver speaks to the hardware, and how workbook protocol cells execute against it. Downstream code reads capability registries; it never branches on family names inline. Adding a family therefore means adding registry entries and (when the hardware speaks its own protocol) a driver, not editing consumers.
This page is the internal counterpart to Device Integration, which documents the contract from the device implementer's side.
How a device receives its configuration
The onboarding configuration file is the canonical interface, and it assumes nothing about the device beyond storage: the operator downloads the JSON and loads it however the hardware allows (flashing during provisioning, SD card, a vendor tool). Every family supports this path, and for most it is the only one.
On top of that baseline there is one optional capability, declared per family in the registry:
| Capability | Registry flag | Behavior |
|---|---|---|
| Download (always available) | none | The Onboarding tab offers the file; the operator delivers it out of band. |
Direct push (SET_CONFIG) | supportsStoredConfig: true | The Onboarding tab additionally offers connect + push over serial or BLE; the file arrives as one SET_CONFIG command frame. |
When push is off, the panel explains why with a per-family note: the inline-procedure families (MultispeQ, Ambit, MiniPAR) receive their procedure per measurement and have nothing to store, while Ambyte edge devices load their configuration at provisioning time and expose no onboarding commands over a direct connection. Only the generic family opts into the push today.
A device that wants push support must implement the SET_CONFIG frame documented in Device Integration, persist the delivered config, and treat each delivery as full desired state.
Where family knowledge lives
| Layer | Artifact | Declares |
|---|---|---|
| Database | sensor_family enum in packages/database/src/schema.ts | Persisted family values on protocols and devices. |
| API contract | zSensorFamily in packages/api/src/domains/protocol/protocol.schema.ts | The wire enum shared by protocols, devices, and registration. |
| API contract | DEVICE_PROFILES in packages/api/src/domains/device/device-profile.ts | Canonical product spelling and display roles. The exhaustive Record makes a missing profile a compile error. |
| Device toolkit | DEVICE_TRANSPORT_SUPPORT in packages/iot/src/core/types.ts | Transports, BLE/Bluetooth Classic support, and the stored-config (push) flag per DeviceType. |
| Device toolkit | SENSOR_FAMILIES and packages/iot/src/driver/* | Which families are identifiable over a local probe, and the adapter speaking each device-native serial protocol. |
| Web host | sensorFamilyToDeviceType in apps/web/hooks/iot/device-type-mapping.ts | Routing from the API family to the toolkit DeviceType. |
| Web host | createDriver in apps/web/hooks/iot/useIotCommunication/useIotCommunication.ts | Which driver a local connection instantiates per family. |
| Web host | executeProtocolWithDriver in apps/web/hooks/iot/useIotProtocolExecution | How protocol cells run per family: direct JSON, the SET_CONFIG/RUN/GET_DATA sequence, or refused with a message. |
| Web host | DOWNLOAD_ONLY_NOTE_KEYS in apps/web/components/iot-devices/device-config-delivery.tsx | The reason copy shown when push is unavailable, resolved through @repo/i18n. |
@repo/iot stays dependency-free, so SENSOR_FAMILIES is duplicated by hand from zSensorFamily rather than imported; the two lists differ deliberately when a family has no local identification handshake (Ambyte today).
Two things intentionally do not change per family:
- The backend onboarding compose is family-agnostic. The family only appears in the returned
deviceTypefield and as thesensorTypesegment baked into eachtopicPrefix. - The ingest infrastructure needs no change:
sensorTypeis a parameter of the AsyncAPI channel, so topic rules and IoT policies already match any family value.
Current families
| Family | Product | Local transports | Config push | Protocol cells over a direct connection | Driver |
|---|---|---|---|---|---|
multispeq | MultispeQ | serial (BT Classic on native) | no | yes, protocol JSON sent directly | MultispeqDriver |
ambit | Ambit | serial | no | refused, command cells only | AmbitDriver |
minipar | MiniPAR | serial | no | yes, protocol JSON sent directly | MiniParDriver |
ambyte | Ambyte | serial | no | generic compatibility path for protocol testing | GenericDeviceDriver |
generic | none | serial, BLE | yes | SET_CONFIG / RUN / GET_DATA sequence | GenericDeviceDriver |
mobile | none | none | no | n/a, the phone runs the app itself | none |
The mobile family is special: phones register themselves silently when someone signs into the mobile app (one Thing per install, Cognito-authenticated, no certificate lifecycle), so it never appears in the manual registration dialog, has no credentials or onboarding surface, and cannot be selected as a protocol family.
Adding a new family
Work through the layers top to bottom; the exhaustive types surface most omissions as compile errors.
- Database: add the value to the
sensor_familyenum and generate a migration. - API contract: add the value to
zSensorFamily, then follow the compile error intoDEVICE_PROFILESwith the product spelling and display roles. - Device toolkit (
@repo/iot): add the value to theDeviceTypeunion and give it aDEVICE_TRANSPORT_SUPPORTentry stating its transports and whether it accepts a stored config. If the hardware is identifiable over a local probe and speaks its own protocol, add a driver underpackages/iot/src/driver/, list the family inSENSOR_FAMILIES, and extendcreateConnectorForFamily; otherwise leave it out ofSENSOR_FAMILIESand route it throughGenericDeviceDriverat the host. - Web host: add the mapping case in
sensorFamilyToDeviceType, thecreateDrivercase, and an execution branch inexecuteProtocolWithDriverwhen the generic flow does not fit. If push is off and the inline-procedure wording is wrong for the hardware, add a note key toDOWNLOAD_ONLY_NOTE_KEYSwith copy in all three locales. - Fixtures and tests: extend the seed if the family should exist in dev data, and update the specs that pin the registries:
onboarding.spec.tsin@repo/iot, and the web tests for the mapping, browser support, communication, protocol execution, and config delivery. - Docs: extend Device Integration when the configuration contract gains family-specific behavior.
Capability changes for an existing family follow the same path minus the enum work: flip the registry flag, adjust the host branches the compiler points at, and update the pinned specs.