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 entry in
DeviceTypeRecords.jsonthat 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, append one record to RaftCore/devtypes/DeviceTypeRecords.json 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).
Open RaftCore/devtypes/DeviceTypeRecords.json and add the record inside the top-level "devTypes" object (alphabetical order is the convention):
"HDC1080": {
"addresses": "0x40",
"deviceType": "HDC1080",
"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 top-level key (
"HDC1080") must equal thedeviceTypefield. The build script does not check this: the generated struct is named from the key and the runtime type name comes fromdeviceType, so a mismatch produces confusing names. - If another record already claims your address, records are tried in file order and the first match wins — make sure a record with weaker detection does not come before yours.
DeviceTypeRecords.json is 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.
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);
uint16_t typeIdx = 0;
deviceTypeRecords.addExtendedDeviceTypeRecord(devTypeRec, typeIdx);Up to 20 such extended records are supported in total (MAX_EXTENDED_DEV_TYPE_RECORDS, shared with anything else that registers them). Their addresses are added to the high-priority scan list if they are registered before the bus scanner starts, and they are tried before compiled records at the same address. 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
- PlatformIO / Arduino
Scaffolding & Building
- Raft CLI
- 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
ROS 2
Helpers
Reference