Skip to content

AddingAnI2CDeviceType

Rob Dobson edited this page Oct 23, 2026 · 7 revisions

Adding an I2C Device Type

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:

  1. A new record file, devtypes/records/<Key>.json, that the build system compiles into C++ constants.
  2. Auto-detection on any I²C bus (or multiplexer slot) that your SysMod scans.
  3. 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.


TL;DR

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.


Step 1 — Gather the datasheet facts

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 − 40
RH(%) = raw_RH × 100 / 65536

Most sensors fit the same template. Dig the equivalent answers out of your datasheet before going further.


Step 2 — Pick a polling pattern

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).


Step 3 — Write the detection sequence

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.

"detectionValues": "0xFF=0b0001000001010000"

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.


Step 4 — Write the init sequence

We need to:

  1. Configure the device for "temperature + humidity, 14-bit each" by writing 0x1000 to register 0x02.
  2. Issue the very first trigger so that the first poll has data to read.
"initValues": "0x021000=&0x00="

Notes:

  • 0x021000 is a single write — register 0x02, then data bytes 0x10 and 0x00.
  • 0x00= writes only the pointer register 0x00 (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.


Step 5 — Write the poll command and decoder schema

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 at 0x00 from the previous trigger / init).
  • 0x00= — write pointer 0x00 to 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": "&deg;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 ⇒ divisor d = 65536 / 165 ≈ 397.188, post-divisor add a = −40.
  • Humidity: RH = raw × 100 / 65536 ⇒ divisor d = 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).


Step 6 — Add the record

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": "&deg;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. deviceType may 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 no detectionValues would 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".


Step 7 — Build and flash

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 monitor

Watch for:

  • Build failure with Poll data size mismatch for <TYPE> JSON <b> Calculated <n> — your resp.b doesn't match what the poll command reads. Recount.
  • Build failure citing JSON syntax — usually a stray comma or missing quote.

Step 8 — Verify on a real device

With the firmware running and the sensor wired to one of the I²C buses configured in your SysType:

  1. Logs — at boot, after scanning, the identification log (RaftI2C DEBUG_DEVICE_IDENT_MGR, on by default) shows identifyDevice new device HDC1080 at address 40.
  2. REST API:
    • GET /api/devman/typeinfo?type=HDC1080 returns the device info you just added (sanity check that JSON parsed).
    • GET /api/devman/listdevs lists the device (e.g. {"deviceid":"1_40","dtIdx":...}), and GET /api/devman/typeinfo?deviceid=1_40 confirms it was identified as HDC1080.
    • Decoded samples are published on the devjson / devbin topics — see Device Data Publishing.
  3. WebUI / BLE client — the device appears in the device list with class tags TEMP and RH, 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.

  1. 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, send devman/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.

Step 9 — (Optional) Adding actions

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.


Alternative — Registering a device type at runtime

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.


Troubleshooting

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.

See also

Clone this wiki locally