Describe a device
Candeo learns a device from a JSON file: how to find it, the bytes it expects, where its lights sit, and the effects its own firmware runs. No code and no rebuild — save the file, choose it on the device's card, and the device obeys it.
Start from one that works
- In Devices, under Every device Candeo knows, Open the device closest to yours. A built-in definition opens read only.
-
Click Copy to yours. The copy lands in
Documents\candeo\devices, opens in its place, and becomes the one the device uses. - Edit it. The editor checks every field as you type and shows what each one means on hover; Ctrl+S puts it in shape and saves it.
- Saved, it takes effect at once: the device opens again on it, and the effect it was showing starts again.
For a device Candeo does not know, change match to its ids: the file then
adds a device rather than replacing one. Several files may describe the same device —
a variant each — and the card's Definition list says which one drives it. The
built-in one is the default, and a file never takes over by being in the folder.
A file that does not load is listed under Your definitions with the reason, the same one the editor underlines, and the device keeps the definition it had.
A whole definition
The lights around the Alienware m18 R1's keyboard, as Candeo ships them: three zones, a report to select lights, another to colour them, and two images checked byte for byte.
{
"$schema": "https://o2csi.github.io/candeo/device-definition.schema.json",
"name": "Alienware m18 R1 zones",
"notes": [
"Survey: docs/protocol/alienware-m18-r1.md, section 2.",
"Matched on the machine: SMBIOS names it “Alienware m18 R1”, as Candeo read it at startup on the surveyed machine on 2026-10-16. The AW-ELC, 187c:0551, also lights the m16 R1, the m16 R2 and the m18 R2, each wiring its ids its own way (alienfx-tools' maps, #328).",
"The AMD edition, “Alienware m18 R1 AMD”, is another machine: not matched until someone confirms its ids.",
"The ring's two halves make one region: one half lit alone spreads through the whole ring, and the halves show only when both are lit (docs/protocol/alienware-m18-r1.md §2)."
],
"match": { "vid": "187c", "pid": "0551", "usagePage": "ff00", "usage": "0001", "machine": "Alienware m18 R1" },
"report": { "wire": "output", "length": 33, "prefix": "00" },
"frame": {
"pace": { "atLeastMs": 100, "skipUnchanged": true },
"open": ["03 21 00 01 ff ff"],
"each": {
"group": "colour",
"chunk": 4,
"send": ["03 23 01 00 {count} {addresses}", "03 24 00 07 d0 00 fa {r} {g} {b}"]
},
"close": ["03 21 00 03 00 ff"]
},
"lights": {
"kind": "zones",
"items": [
{ "name": "ring, upper half", "address": 0, "shape": "archUp", "rect": [2, 7.9, 9, 1.1] },
{ "name": "ring, lower half", "address": 1, "shape": "archDown", "rect": [2, 9, 9, 1.1] },
{ "name": "logo on the lid", "address": 2, "shape": "disc", "rect": [5.5, 2.5, 2, 2] }
]
},
"outline": [
{ "rect": [0.5, 0, 12, 7], "r": 0.5 },
{ "rect": [0, 7.4, 13, 3.2], "r": 0.8 },
{ "rect": [3.2, 8.65, 0.8, 0.7], "r": 0.12 },
{ "rect": [4.4, 8.65, 1.4, 0.7], "r": 0.12 },
{ "rect": [6.2, 8.65, 0.6, 0.7], "r": 0.12 },
{ "rect": [7.2, 8.65, 1.4, 0.7], "r": 0.12 },
{ "rect": [9, 8.65, 0.8, 0.7], "r": 0.12 }
],
"regions": [
{ "id": "ring", "name": { "en": "Ring", "fr": "Anneau" }, "lights": [0, 1] },
{ "id": "logo", "name": "Logo", "lights": [2] }
],
"examples": [
{
"colours": ["ff0000", "00ff00", "0000ff"],
"reports": [
"03 21 00 01 ff ff",
"03 23 01 00 01 00", "03 24 00 07 d0 00 fa ff 00 00",
"03 23 01 00 01 01", "03 24 00 07 d0 00 fa 00 ff 00",
"03 23 01 00 01 02", "03 24 00 07 d0 00 fa 00 00 ff",
"03 21 00 03 00 ff"
]
},
{
"colours": ["ff0000", "ff0000", "000000"],
"reports": [
"03 21 00 01 ff ff",
"03 23 01 00 02 00 01", "03 24 00 07 d0 00 fa ff 00 00",
"03 23 01 00 01 02", "03 24 00 07 d0 00 fa",
"03 21 00 03 00 ff"
]
}
]
}The sections below take its parts in turn, and the fields the other devices add.
Finding the device: match
A device is found by its vendor and product ids, four hexadecimal digits each. A device with lights usually exposes several HID entries — keys, media keys, lighting — and only one of them takes the lighting reports, so the definition names which:
-
interface, the number of the interface carrying it:3on the DeathStalker V2 Pro; -
or
usagePageandusage, when several collections share one interface:ff00and0001for the m18's zones.
On Windows, the device's Hardware Ids in Device Manager read like
HID\VID_1532&PID_0292&MI_03: vendor, product, interface. The wrong
entry opens fine and silently drops every write, which is why it is named rather than
guessed.
A laptop's lights are different: the maker fits the same controller in many machines and
wires it differently in each, so the same ids light the keyboard on one model and the lid
on another. There, machine names the model the definition was made on, as
the system gives it — the machine line of the diagnostic Candeo copies:
"machine": "Alienware 17 R5". It is compared exactly, and on any other
machine the definition drives nothing. A keyboard or a mouse needs none.
The shape of a report: report
-
wire—feature,outputorwrite, how reports leave: a feature report, an output report on the control pipe, or a write on the interrupt pipe, as most makers' own software does. A device may act on one and ignore the same bytes sent another way. -
length— every report's length in bytes. Reports are padded with zeros to it, and one that could grow past it is refused when the file loads. -
prefix— bytes the HID API wants in front of every report without being part of it, usually the report id:00for a device without one. Offsets in the file count after it. -
checksum— an integrity byte, named rather than written:{ "function": "xor", "from": 2, "to": 87, "at": 88 }puts at byte 88 the XOR of bytes 2 to 87.xorandsum(modulo 256) exist; a protocol needing another asks for it, and it is added once for every device. -
command— where a report carries its command, as{ "class": 6, "id": 7 }on the Razer: with it, a command the device says it does not know is never sent.
An image of every light: frame
Up to thirty times a second, Candeo sends an image: the open reports, then
each for the lights, then the close reports — often what makes
the device show it. Reports are written as a capture shows them, bytes in hexadecimal,
and placeholders in braces that the image fills in.
By default, each.send goes out once per light. Two ways to send fewer:
-
"group": "colour", with achunksize, puts lights sharing a colour in one report, as a device that selects lights then colours them wants — the m18's zones above. -
"by": "row"sends one report per row of the grid, each light written byeach.lightwhere{lights}stands — the DeathStalker, with"light": "{r} {g} {b}".
| Placeholder | What it is |
|---|---|
{r} {g} {b} | A colour, one byte each. |
{address} | A light's address, as the device calls it. |
{count} {addresses} | How many lights the report carries, and their addresses. |
{lights} | Their entries, each written as each.light says. |
{row} {start} {end} | A row, and its first and last column. |
{length:N} | How many bytes the report holds after offset N, once filled. |
{level} | The brightness, in brightness. |
{kind} {r2} {g2} {b2} | A firmware effect's kind, and its second colour. |
Two more fields, for devices with habits:
-
pace— for a device that takes changes rather than a stream:atLeastMsbetween two images, andskipUnchangedto send nothing when nothing changed. -
takeOver— reports sent once when one of Candeo's effects starts, to stop an animation the firmware would otherwise keep drawing over every image.
Where the lights are: lights
kind says what they are: keys, which can be pressed, or
zones — a ring, a logo, a strip — which cannot, so effects reacting to typing
are not offered there. Each item then gives:
-
address— what the device calls the light. Not always its position: the m18's keyboard counts from one. -
rect—[x, y, width, height]in key units, a letter key being 1 wide, from the top left. The simulator draws it there, and effects measure distances with it, so an effect written for one device looks right on another. -
rowandcol— its cell, when the definition gives a grid withrowsandcols. Without one, the items make a single row. -
scancode— what the key sends, as Windows reports it:1cEnter,e01dright Ctrl. It is how a key press finds its light. -
shape—disc,archUporarchDownto draw something other than a key; andname, for whoever reads the file.
outline draws, faintly, the parts that light nothing — a lid, a base, ports —
so zones sit where they are on the device. A keyboard needs none.
Parts that run their own effect: regions
A laptop's controller often lights the keyboard, the lid and the base together, so the
whole machine runs one effect. regions names its parts, and a device that
declares them is offered Split into regions: each part then runs an effect of its
own. Each region gives:
-
id— how settings name it: lower case letters, digits and hyphens. Keep it when you copy a definition, and what was kept for the region follows. name— what the interface calls it, in one language or several.-
lights— the addresses of its lights, asitemsgives them.
"regions": [
{ "id": "keyboard", "name": { "en": "Keyboard", "fr": "Clavier" }, "lights": [4, 5, 6, 7, 12] },
{ "id": "lid", "name": { "en": "Lid", "fr": "Capot" }, "lights": [0, 1, 10, 11] },
{ "id": "base", "name": { "en": "Base", "fr": "Base" }, "lights": [8, 9, 2] },
{ "id": "power", "name": { "en": "Power button", "fr": "Bouton d'alimentation" }, "lights": [3] }
]
Every light belongs to exactly one region, and there are at least two: a light in none would stay dark once the device is split, a light in two would have two effects fighting over it, and the file is refused, naming the address. An effect running on a region sees the region alone, measured where it sits on the device. Split only what the hardware keeps apart: the m18 R1's ring is one region, since one half lit alone spreads through the whole ring.
Brightness and the firmware's own effects
brightness lists the reports setting the overall level, {level}
from 00 to ff. Without it, the device has no brightness slider.
firmware offers the effects the device runs by itself, in the gallery beside
Candeo's: they cost the computer nothing, and keep going when Candeo is closed.
send is the report selecting one, with {kind} and its colours;
off the kind that shows nothing, when there is one. Each effect then says:
-
id— how Candeo knows it,hardware:first. Settings, rules and the effect applied name it by this: keep it when you copy a definition, and your settings follow. kind, andcolours— how many it asks for, none to two.send— its own report, when it carries more than a kind.-
nameandsummary— what the gallery calls it and says of it, in one language or several:
{
"id": "hardware:m18-08",
"kind": "08",
"looks": "breathing",
"name": { "en": "Breathing", "fr": "Respiration" },
"summary": {
"en": "Colours follow one another, fading through black in between.",
"fr": "Les couleurs se succèdent, en passant par le noir entre chacune."
}
}
A plain string is the same in every language, and English is shown where the interface's
language is missing. looks picks the drawing the gallery shows as a legend of
the effect — steady, pulse, breathing,
morph, wave, scanner or spectrum.
It is a legend, not a preview: the firmware draws the effect and shows nothing back.
Proving it: examples
A reference frame is an image and the reports it must give: colours in, bytes out, as a capture of the device showed them. Candeo's tests replay those of every built-in definition byte for byte — which is what lets someone without the device review one, and what keeps it right as Candeo changes. In a file of yours they document; in a contribution they are required.
surveyed records the firmware version the survey was made against, and
notes what a reviewer should know. Candeo reads neither to drive the device;
when a device reports another firmware version, its card says so.
Finding the bytes
The bytes come from watching the maker's own software: a USB capture — Wireshark with USBPcap on Windows — while it changes a colour, then another, then the brightness. What changes between two captures is the colour; what stays is the frame around it. Two worked surveys, with the method and every fact dated, are in docs/protocol.
Then try it on the device, carefully:
- A status that says “accepted” proves little. On the Razer protocol it validates the command, never its arguments. Read a setting back to know it was taken.
- Send only what you saw the maker's software send. Commands reading the device's information are for reading; a command switching the device to its software-driven mode makes the firmware stop handling some keys.
- Keep the keyboard in view the first time a definition drives it.
Making it built in
A definition that works for you can ship with Candeo, for everyone with that device. A
pull request adds the file to
crates/candeo-device/devices
with its reference frames, and the survey behind it to docs/protocol: the
firmware version, how each fact was established, and when. The tests replay the frames,
and the schema checks the rest.
#34 is where what a contribution carries is being
settled.
Some definitions are waiting for someone with the device: the ones to verify were written from published facts, and one report that it lights is what they need.
Reference
-
The schema — every field and what it accepts.
Name it in
$schemaand any editor reading JSON schemas checks your file:"$schema": "https://o2csi.github.io/candeo/device-definition.schema.json". - The built-in definitions — the DeathStalker V2 Pro, the m18 R1's keyboard and zones, and the 17 R5's zones.
- docs/design/device-sdk.md — why definitions are built this way.