Repository navigation
AddingAnI2CDeviceType
This tutorial walks through adding support for a new I2C sensor end-to-end. We use the TI HDC1080 temperature/humidity sensor as the worked example because:
- It has a real "device ID" register (good detection),
- Its conversion formulas are simple linear ones (clean attribute decoding),
- It uses the trigger-then-read pattern shared with many I²C sensors (AHT20, SHT3x, BME280…).
The same recipe applies to almost any modern I²C device. By the end you will have:
- A new record file,
devtypes/records/<Key>.json, that the build system compiles into C++ constants. - Auto-detection on any I²C bus (or multiplexer slot) that your SysMod scans.
- Decoded values published to BLE / WebSocket / REST clients with units, ranges and formatting.
Background reading: Device Type Record Format, I2C Device Identification and Polling, I2C Bus, Device Manager.
To add a new device type, add one record file, RaftCore/devtypes/records/<Key>.json (or a file in your project's own records folder - see Selecting Device Types), with five things populated:
| Section | What goes in it |
|---|---|
addresses |
I²C address(es) the device might appear at |
detectionValues |
A read that uniquely identifies the chip |
initValues |
The register writes that put it into the mode you want |
pollInfo |
The read (and optional trigger) that fetches data, plus poll period |
devInfoJson.resp.a |
The binary-decoder schema for the polled bytes |
Build, flash, and the device shows up automatically when plugged in.
Before writing JSON, extract these specifics from the datasheet:
| Question | HDC1080 answer |
|---|---|
| I²C address(es) |
0x40 (fixed) |
| Identification register(s) |
0xFE → Manufacturer ID 0x5449, 0xFF → Device ID 0x1050
|
| Configuration register |
0x02, 16-bit, write 0x1000 for "temperature and humidity, 14-bit each, single trigger" |
| How a measurement is triggered | Write the pointer register 0x00
|
| Conversion time | ~6.5 ms (temperature) + ~6.5 ms (humidity) at 14-bit |
| Where data appears | After conversion, a single read of 4 bytes from pointer 0x00 returns T_msb T_lsb H_msb H_lsb
|
| Conversion formulas |
T(°C) = raw_T × 165 / 65536 − 40RH(%) = raw_RH × 100 / 65536
|
Most sensors fit the same template. Dig the equivalent answers out of your datasheet before going further.
A trigger-and-wait device has two sensible polling shapes. Use the one that matches the device's worst-case conversion time vs. how often you want a sample.
Pattern A — read-then-retrigger (recommended for slow sensors):
- Initialisation issues the first trigger.
- Each poll reads the previous result, then issues the next trigger.
- The poll interval is much longer than the conversion time, so the data is always ready.
This is exactly what the existing AHT20 record does ("c": "=r6&0xac3300=").
Pattern B — write-pause-read in one cycle:
- Each poll issues the trigger, pauses, then reads.
- Higher latency per poll; needed only when the device cannot be left armed between polls.
For the HDC1080 we'll use Pattern A with a 1 s interval (the device idles between polls and there is no risk of stale data accumulating).
Detection happens once after the address responds. We want a read that only the HDC1080 will satisfy.
Best candidate: the Device-ID register 0xFF returns the 16-bit value 0x1050.
A 16-bit binary mask is used (one bit per significant bit, X for don't-care). Equivalently you can chain with & to also check the manufacturer ID, which makes detection even safer:
"detectionValues": "0xFE=0b0101010001001001&0xFF=0b0001000001010000"If your device has no ID register, fall back to checking the reset value of a config register with don't-care bits over the parts that depend on user state — see the MCP9808 and TMP102-style entries in Device Type Record Format for the syntax.
We need to:
- Configure the device for "temperature + humidity, 14-bit each" by writing
0x1000to register0x02. - Issue the very first trigger so that the first poll has data to read.
"initValues": "0x021000=&0x00="Notes:
-
0x021000is a single write — register0x02, then data bytes0x10and0x00. -
0x00=writes only the pointer register0x00(no data byte) — this is the "trigger" on the HDC1080. - The
=after each operation marks the end of the write (no read follows). Operations are chained with&.
If your device needs settling time, add =p20 (20 ms pause). See Init Values.
Poll command: read 4 bytes (the previous measurement), then re-arm the device.
"pollInfo": {
"c": "=r4&0x00=",
"i": 1000,
"s": 1
}-
=r4— read 4 bytes with no register write first (the device's pointer is already at0x00from the previous trigger / init). -
0x00=— write pointer0x00to start the next conversion. -
i: 1000— poll every 1000 ms. -
s: 1— publish each sample on its own (no batching).
Decoder schema: four bytes → two big-endian unsigned 16-bit values.
"devInfoJson": {
"name": "HDC1080",
"desc": "Temp&Humid",
"manu": "Texas Instruments",
"type": "HDC1080",
"clas": ["TEMP", "RH"],
"resp": {
"b": 4,
"a": [
{
"n": "temperature",
"t": ">H",
"u": "°C",
"r": [-40, 125],
"d": 397.1878787,
"a": -40,
"f": "3.2f",
"o": "float"
},
{
"n": "humidity",
"at": 2,
"t": ">H",
"u": "%",
"r": [0, 100],
"d": 655.36,
"f": "3.1f",
"o": "float"
}
]
}
}How the divisors and offsets fall out of the conversion formulas:
-
Temperature:
T = raw × 165 / 65536 − 40⇒ divisord = 65536 / 165 ≈ 397.188, post-divisor adda = −40. -
Humidity:
RH = raw × 100 / 65536⇒ divisord = 65536 / 100 = 655.36.
The decoder pipeline (xor → mask → shift → divide → add) is documented in Decoding Pipeline.
The b: 4 byte-count is validated at build time against the bytes implied by the poll command — if it disagrees the build fails. This is your primary safety net against silly mistakes.
r (range) and f (format) are not used for decoding but are surfaced to clients (graphs, gauges, BLE attribute info).
Create RaftCore/devtypes/records/HDC1080.json (the file name is the device type key) holding the record. To keep the record in your own project instead, put the file in a folder there and select it in DeviceTypeSources.json:
{
"addresses": "0x40",
"detectionValues": "0xFE=0b0101010001001001&0xFF=0b0001000001010000",
"initValues": "0x021000=&0x00=",
"pollInfo": { "c": "=r4&0x00=", "i": 1000, "s": 1 },
"scanPriority": "high",
"devInfoJson": {
"name": "HDC1080",
"desc": "Temp&Humid",
"manu": "Texas Instruments",
"type": "HDC1080",
"clas": ["TEMP", "RH"],
"resp": {
"b": 4,
"a": [
{ "n": "temperature", "t": ">H", "u": "°C",
"r": [-40, 125], "d": 397.1878787, "a": -40,
"f": "3.2f", "o": "float" },
{ "n": "humidity", "at": 2, "t": ">H", "u": "%",
"r": [0, 100], "d": 655.36,
"f": "3.1f", "o": "float" }
]
}
}
}Tips:
-
scanPriority: "high"puts the address into the fast-scan list — appropriate for hot-pluggable user devices. Omit it for rarely-changing system devices to keep the high-priority list short. See I2C Device Scanning. - The file name (
HDC1080) is the device type key.deviceTypemay be omitted; if present the build fails unless it equals the key. - If another record already claims your address, records are tried in descending
identPriority, then descending detection strength (the number of bits the detection compares), then in device type key order, and the first match wins — so give your record detection that checks enough bits to tell your device from the others at that address (the generator warns when a record with nodetectionValueswould still be tried first).
Optional fields for identification:
| Field | Meaning |
|---|---|
identPriority |
Integer from -128 to 127 (default 0). Records at the same address are tried in descending identPriority, then descending detection strength, then in key order. Use it where the order between two records matters |
probeOnly (or the older excludeFromAddrMap) |
The record isn't identified by its address: an identification hook that probes the device adopts it (e.g. RSAO devices). Its addresses aren't reserved or prioritised for scanning |
hwTypeId |
The ID the device reports when an identification hook probes it, as "<scheme>:<value>" in that scheme's canonical form, e.g. "rsao:0x0084" for the RSAO WHOAMI response (see Hardware type ID schemes). A hook finds the record with deviceTypeRecords.getRecordByHwTypeId()
|
The generator also warns when a key appears in more than one catalogue file (the later file's record replaces the earlier one). In a catalogue file ({"devTypes": {...}}, e.g. a SysType's DevTypes.json) records are written as "HDC1080": { ... } inside "devTypes".
The selected records are consumed by scripts/ProcessDevTypeJsonToC.py at build time, which produces:
- A C struct
poll_<TYPE>holding one decoded sample, - A
decode()lambda (or a transpiled custom-pseudocode function), - The device type record that the device-identification machinery walks.
Build and flash as normal:
raft build && raft flash && raft monitorWatch for:
-
Build failure with
Poll data size mismatch for <TYPE> JSON <b> Calculated <n>— yourresp.bdoesn't match what the poll command reads. Recount. - Build failure citing JSON syntax — usually a stray comma or missing quote.
With the firmware running and the sensor wired to one of the I²C buses configured in your SysType:
-
Logs — at boot, after scanning, the identification log (RaftI2C
DEBUG_DEVICE_IDENT_MGR, on by default) showsidentifyDevice new device HDC1080 at address 40. -
REST API:
-
GET /api/devman/typeinfo?type=HDC1080returns the device info you just added (sanity check that JSON parsed). -
GET /api/devman/listdevslists the device (e.g.{"deviceid":"1_40","dtIdx":...}), andGET /api/devman/typeinfo?deviceid=1_40confirms it was identified asHDC1080. - Decoded samples are published on the
devjson/devbintopics — see Device Data Publishing.
-
-
WebUI / BLE client — the device appears in the device list with class tags
TEMPandRH, and (if the WebUI knows those classes) draws a temperature gauge and a humidity bar.
See Device Manager REST API for the full endpoint set.
-
Identification probes — if the device's addresses are ones an identification hook covers (on Axiom, the RSAO check at 0x0F-0x6F), record what that hook's probe does to it in
hwProbeEffects(Hardware probe effects). For the RSAO probe: dump the registers and take a reading, senddevman/cmdraw?deviceid=<id>&hexWr=99&numToRd=9, then dump and read again and compare. Use the datasheet (what register 0x99, or 0x19 if the chip ignores bit 7, and the next 8 are) or the firmware source for a microcontroller-based board where a bench test isn't possible.
If your device exposes writable parameters (e.g. heater on/off, sample rate, calibration triggers), define an actions array in devInfoJson so clients can discover and send them. Skeleton:
"actions": [
{
"n": "heater",
"t": "B",
"w": "0210",
"wz": "00",
"r": [0, 1],
"d": 0,
"desc": "Internal heater (0=off, 1=on)"
}
]The full action grammar — including discrete maps, LED-pixel grids and configuration actions that change polling parameters — is documented in Actions.
If your device support cannot live in DeviceTypeRecords.json (e.g. it is shipped by a third-party module, or it needs a custom C++ decoder you would rather not transpile from pseudocode), you can register it dynamically from your application or SysMod:
DeviceTypeRecordDynamic devTypeRec(
"MY_SENSOR", // type name
"0x48,0x49", // addresses
"0x00=0x1234", // detectionValues
"0x0180", // initValues: write 0x80 to register 0x01
R"({"c":"0x01=r6","i":100,"s":10})", // pollInfo: JSON object, i in milliseconds
6, // pollDataSizeBytes
R"({"name":"MySensor","desc":"...","clas":["TEMP"]})",
myDecodeFunction);
DeviceTypeIndexType typeIdx = DEVICE_TYPE_INDEX_INVALID;
DeviceTypeRecords::AddResult result = deviceTypeRecords.addRecord(devTypeRec, typeIdx);addRecord() reports Added, AlreadyPresent (the same record was added before - typeIdx is its index), Conflict (a different record with the same key), Full or Invalid; the older addExtendedDeviceTypeRecord() still works. The number and size of these extended records are limited by the DeviceManager typeTable settings (32 records without PSRAM, 256 with, by default). A record added at runtime is treated like a compiled one: it is an identification candidate at its addresses (tried before compiled records at the same address with the same identPriority and detection strength), and its addresses are scanned at its scanPriority (high unless set on the DeviceTypeRecordDynamic), including when it is added after the bus scanner has started. identPriority, flags (e.g. DeviceTypeRecord::FLAG_PROBE_ONLY) and setHwTypeId() set the other fields. See Adding Extended Device Types.
For devices that need genuinely bespoke behaviour (custom REST endpoints, non-I²C interfacing, software-only "virtual" devices), implement a full RaftDevice subclass and register it with the device factory — see Device Factory and Classes for the pattern.
| Symptom | Likely cause |
|---|---|
| Device not detected at all | Wrong address, wrong bus, pull-ups missing, or detectionValues too strict. Try a permissive detection ("") temporarily and confirm the log shows the address responding. |
| Detected but immediately dropped | Detection succeeds intermittently — the bus is noisy, or your detection register's "fixed" bits are not actually fixed. Tighten with & of two registers, or relax with more X don't-care bits. |
Polling returns 0xFFFFs |
The device hasn't been triggered — check that initValues issues the first trigger, and that your poll c re-triggers each time. |
| Values look like reasonable numbers but are off by a factor | Wrong divisor d, or wrong endianness in t (>H vs <H). |
| Values are signed when they should be unsigned (or vice versa) | Use >h/<h for signed, >H/<H for unsigned. For "12-bit signed in a 16-bit container" use m + sb + ss — see the MCP9808 example. |
Build fails with Poll data size mismatch ... JSON N Calculated M
|
Your resp.b and the byte count of the poll-command reads disagree. Sum the rN reads and update b. |
Never detected, and the log shows invalid detection value
|
A detection check value is missing its 0x/0b prefix (e.g. 0xFF=1050 instead of 0xFF=0x1050). |
| Device shows up but is decoded as a different type | Two device types claim the same address with overlapping detection. Tighten one or both detectionValues. |
- Device Type Record Format — the complete reference for every field used above.
- I2C Device Identification and Polling — the runtime mechanics behind detection, init and polling.
-
I2C Device Scanning — how addresses are scanned and how
scanPriorityinteracts with multiplexer slots. - Device Manager — life-cycle and configuration of the device subsystem.
- Device Manager REST API — runtime endpoints for inspecting devices and changing settings.
- Device Data Publishing — how decoded samples reach BLE / WebSocket / MQTT subscribers.
-
Device Factory and Classes — when you need a full
RaftDevicesubclass instead of a JSON record.
Getting Started
- Quick Start
- Architecture at a Glance
- Writing Your First SysMod
- Adding a Comms Channel
- Adding an I2C Device Type
Scaffolding & Building
- Raft CLI
- Web Apps (Device Types, Flasher)
- SysTypes
- Top-Level SysType
- Build Process
- WebUI Build Pipeline
- File System
- Partitions & Flash
- Local Dev Libraries
- Library Developer Guide
Architecture
Built-in SysMods
- NetworkManager
- WiFi Troubleshooting
- BLEManager
- WebServer
- MQTTManager
- SerialConsole
- CommandSerial
- CommandSocket
- CommandFile
- FileManager
- LogManager
- ESPOTAUpdate
- StatePublisher
- Remote Logging
- Data Source Registration
Comms & Protocols
- Stack Overview
- Comms Channels
- ProtocolExchange
- RICREST Protocol
- Real-Time Streams
- Adding REST Endpoints
- Built-in REST Endpoints
- File Download (OKTO)
- OTA Update Flow
Devices & Buses
- DeviceManager
- Device Manager REST API
- Device Factory & Classes
- Device Type Records
- Adding an I2C Device Type
- Device Data Publishing
- Data Logger
- I2C Bus
- I2C Device Scanning
- I2C ID & Polling
- MotorControl Overview
- MotorControl Config
- MotorControl Commands
- Camera
ROS 2
Helpers
Reference