-
Notifications
You must be signed in to change notification settings - Fork 3
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 enforces this.
DeviceTypeRecords.json is consumed by scripts/ProcessDevTypeJsonToC.py at build time, which produces:
- A C struct
<TYPE>_struct_t(used internally byRaftBusDevice), - A
decode()lambda (or a transpiled custom-pseudocode function), - The
constdevice type record that the device-identification machinery walks.
Build and flash as normal:
raft build && raft flash && raft monitorWatch for:
-
Build failure with
byte count mismatch— 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, you should see
DevIdentlog it asHDC1080@0x40. -
REST API:
-
GET /api/devman/typeinfo?bus=I2CA&type=HDC1080returns the record you just added (sanity check that JSON parsed). -
GET /api/devman/devicedata?bus=I2CAreturns the most recent decoded sample, e.g.{"_t":1234567,"temperature":22.81,"humidity":48.3}.
-
-
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
"0x01=0x80", // initValues
"c=01=r6&i=100&s=10", // pollInfo (compact form)
6, // pollDataSizeBytes
R"({"name":"MySensor","desc":"...","clas":["TEMP"]})",
myDecodeFunction);
uint16_t typeIdx = 0;
deviceTypeRecords.addExtendedDeviceTypeRecord(devTypeRec, typeIdx);Up to 64 such extended records are supported. They are added to the high-priority scan list so newly-plugged devices are picked up promptly. 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 expected b=N got M
|
Your resp.b and the byte count of the poll-command reads disagree. Sum the rN reads and update b. |
| 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
- 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
Helpers
Reference