Skip to content

Protocol number registry

KM43 / numeric authority

Every message type, field, outcome, capability, metric, and event gets one permanent number here before an implementation is allowed to use it.

Source
protocol.toml and generated tables
Allocation rule
Reserve the number before writing code
Lifecycle
Live, reserved, withdrawn, or retired
Vendor range
0xF000–0xFFFF for metric and event kinds

A number is allocated here before it appears in any implementation. Two people picking 0x0503 independently produce two firmwares that disagree about what a generator is doing, and the disagreement is invisible until one of them is at a site. The collision must surface in the pull request that allocates the number, not on the wire.

Retired numbers stay in the table, marked retired, with the date and the reason. Deleting the row is how a number gets reused by somebody who never knew.

Reserved ranges. 0xF000–0xFFFF is reserved for vendor and experimental use in the metric-kind and event-kind spaces, and will never be allocated here. A device may emit one, and a client shows that channel or that record as unrecognised and keeps the rest of the message.

Skip what you cannot name; reject what you cannot act on. Those two spaces are labels on data — not knowing a metric kind costs one row on a screen. Every other number in this file is a value something decides on, and there the rule is P-014 in PROTOCOL.md: an unrecognised discriminant is error 1, with no vendor range and no 0 = unknown to fall back to. P-019 is the carve-out by space and it names these two and no others; P-125 is the carve-out by position, and it is the one place a discriminant is not in a discriminant field — the three metric kinds below whose key 3 is an answer rather than a measurement.

SpaceAn unrecognised value is
Metric kinds, event kindsskipped — that one Value or Event is surfaced as unrecognised, the rest of the message stands
Qualityrejected — a reading whose trustworthiness cannot be named is not a reading
Generator state, generator selector, boot reasonskipped for that one channel — P-125, and it is the second carve-out: an answer nobody can name is not rendered, and the Snapshot around it stands
Command, SetConfig, Pair, Firmware and Time outcomesrejected — a client that cannot tell accepted from inhibited has learned nothing
Client kinds, time sources, config sections, error codesrejected

What separates the first row from the rest is the difference between the label on a quantity and the answer to a question. Generator state is the label, and a client that does not hold metric 0x0501 can say there is a channel here I cannot name and be honest. The generator is in state 6 is the answer, and a client that renders an answer it cannot read is the room card showing 0.0 °C all over again.

Status column, and the four are genuinely different things:

StatusThe number isMay it be emitted?
liveallocated and fully specifiedYes
reservedallocated; its body or argument schema is deferred, see DEFERRED.mdYes, where a normative rule requires it — but a receiver must not depend on the body’s contents, because the body is what is deferred
withdrawnallocated; the condition it named is answered somewhere else nowNo. Nothing emits it, and the number is not free
retiredwas used, and is gone foreverNo

reserved is not withdrawn, and conflating them was a real bug. An earlier revision defined reserved as “nothing may emit it”, which quietly forbade three things the specification requires: the records dropped event that explains a hole in seq, the time set event that records which source moved the clock, and the comms-processor lifecycle events LINK.md leans on. All three have allocated kinds whose bodies are deferred — the number is emittable, the schema is not yet written. A status that forbids a MUST is a status that is wrong.


The high bit means response to a request. Event is the only unsolicited message. 0x60–0x7E is the link-local range from LINK.md, whose responses are 0xE0–0xFE — the same high-bit rule, so nothing special has to be remembered. 0x7F is deliberately left out of the range, because 0x7F with the high bit set is 0xFF, which is Error: 31 requests against 31 responses is a rule that stays true, and a 32nd request whose response is somebody else’s opcode is a trap for whoever allocates last. A link-local type arriving on a client-facing transport is error 257, not error 2: it is a routing bug in the comms processor, not a client sending nonsense, and the two want different investigations.

The two auth columns are one column in disguise, and splitting them is the point. A request and its response are not authenticated the same way — a Snapshot request is wrq and its response is rsp, under different preimages — and a single column saying “session” covered both while agreeing with P-052 only by luck. These are the labels P-052 defines, and nothing else is a legal value.

RequestResponseNameAuth (request)Auth (response)SinceStatus
0x000x80Discovernonenone1.0live
0x010x81Helloproofrsp1.0live
0x020x82Snapshotwrqrsp1.0retired
0x030x83Subscribewrqrsp1.0live
—0x04Event—evt1.0live
0x050x85ReadLogwrqrsp1.0live
0x060x86GetConfigwrqrsp1.0reserved
0x070x87SetConfigsignedrsp1.0reserved
0x080x88Commandsignedrsp1.0reserved
0x090x89Firmwaresignedrsp1.0reserved
0x0A0x8ATimesignedrsp1.0live
0x0B0x8BPairpair_keypair_key1.0live
0x0C0x8CGoodbyewrqrsp1.0live
0x0D0x8DInventorywrqrsp1.0live
0x0E0x8EReadingswrqrsp1.0live
0x0F0x8FConcernswrqrsp1.0reserved
0x100x90Historywrqrsp1.0reserved
0x60–0x7E0xE0–0xFElink-local, see LINK.md——1.0live
—0xFFError—rsp_or_bare1.0live

GetConfig and SetConfig are reserved rather than live because their envelopes are settled and their body is not: no section has a field list yet, so two implementations can exchange a config read and disagree about every byte inside it. The numbers are allocated; the schemas are deferred — see DEFERRED.md.

Pair authenticates under pair_key, which P-088 derives from printed_secret with the label km43/v1/pair-key. The printed secret is never an HMAC key itself: it can never be rotated, and the comms processor watches every pairing exchange, so keying it directly would hand the one component this protocol calls hostile an oracle under the one secret the device depends on.

Goodbye exists because a session table with no way to empty it locks every client out after eight reconnections inside the expiry window. It is not politeness; it is the difference between a browser refresh working and not.

Section titled “Goodbye exists because a session table with no way to empty it locks every client out after eight reconnections inside the expiry window. It is not politeness; it is the difference between a browser refresh working and not.”
CodeMeaningMAC’d?Status
1Malformed framenolive
2Unknown message typenolive
3Protocol major mismatchnolive
4Hello required firstnolive
5Payload too largenolive
6Unknown sectionyeslive
7Busy — retryyeslive
8Session table fullnolive
9Session expirednolive
10Bad MACnolive
11Counter not freshyeslive
12Unknown clientnolive
13Pairing window closednowithdrawn
14Stale challenge — reconnect and retrynolive
15Snapshot exceeds channel capyeswithdrawn
16Not permitted on this transportnowithdrawn
17Clock not setyeswithdrawn
256–511link-local, see LINK.mdnolive

13, 15, 16 and 17 are withdrawn, not live, because nothing produces them.

13 because P-066 answers a Pair arriving with no window open with Pair 0x8B outcome 2 window_closed, MAC’d under the printed secret. That is P-141 doing its job: a refusal the comms processor can forge is a refusal that sends somebody back to the panel to press a button that was never needed.

15 because P-090 refuses an over-cap configuration at write time with SetConfigAck outcome 5 exceeds_cap. Nothing builds an over-cap Snapshot to refuse — the failure lands on the person editing config rather than on a client at 2 a.m.

16 because there is no per-transport permission policy yet — that is DEFERRED.md entry 4 — and the only signal a controller has about which transport a client arrived on is a field the comms processor writes, which is the one party the boundary exists to distrust.

17 because no rule in PROTOCOL.md or LINK.md raises it. A controller whose clock has never been set does not refuse anything: P-093 omits at, and P-092’s stale simply stops being expressible until the first Time write. There is nothing left for a refusal to say.

All four numbers are held so nobody else takes them. They are retired-adjacent rather than free — each one had a rule pointing at it once, and reusing a number somebody’s early implementation may already have emitted is the collision this file exists to stop. Each stays unemitted until a rule says what refuses what.

This table allocates a meaning. The rule that produces a code lives in PROTOCOL.md or LINK.md, and a condition those documents describe without naming a code here is a bug in them: two implementations will pick different numbers for the same refusal and neither is wrong.

The MAC’d column is the receiver’s rule, not the sender’s. It says which codes a receiver refuses to read out of a bare Error body — a bare error 6 or 11 is discarded, not acted on. What shape a sender emits is P-142’s question and is answered by whether that sender holds a session, never by looking a code up here. One column, one meaning, or the two tests disagree in exactly the case that matters: a session the controller has already dropped.

Link-local codes start at 256 so a client can tell the link failed from your request failed. Three of them reach a client on purpose: 257 (it sent a link-local type), 258 (it connected before the two firmwares had exchanged LinkUp) and 259 (its handle is gone, which is what a controller reboot looks like from the outside). All three mean reconnect, and none is MAC’d, so the rule below applies to them like any other unauthenticated error. The rest — 256 and 260–264 — never leave the UART, and one of those in a client-facing frame is wrong on sight.

An unauthenticated error is a hint, never a fact. The comms processor can forge every no row above. A client may retry or reconnect on one; it must never render one as a statement about the site, and it must never conclude from error 9 that its own writes did not land — that is what the log is for.


Every metric carries a unit and a scale, and the value on the wire is an integer. There is no FPU on the target, and a scaled integer cannot disagree with itself the way a float rounded by two languages can.

value = wire_integer × 10^scale, in the stated unit.

A metric kind and an event kind can be the same number, and fourteen of the twenty event kinds are. 0x0604 is log ring utilisation here and time set in the event table; 0x0301 is ambient temperature here and behaviour decision there. Nothing on the wire is ambiguous — a metric kind appears only in Value.kind, an event kind only in Event.kind and LogEntry.kind, and no field takes both — so this is a trap for a person rather than for a decoder: somebody greps 0x0604, finds one row, and writes it into the wrong table. That is the exact mistake this file exists to catch, sitting inside the file itself.

The two spaces are not renumbered onto disjoint prefixes. It would buy a decoder nothing, cost half of both spaces, and retire twenty numbers that four documents and the vector file already name in prose — and a renumber applied here and not in LINK.md is precisely the collision the allocation rule exists to stop. What is required instead is a habit with teeth: prose naming one of these numbers says which space it is in — metric 0x0604, event 0x0604 — and a bare number in a sentence about kinds is a bug in that sentence.

P-019’s vendor range 0xF000–0xFFFF spans both spaces on purpose. It is the same rule in each — surface that one carrier as unrecognised, keep the rest of the message — so it needs one range and not two.

KindNameUnitScaleStatus
0x0101DC voltageV−3live
0x0102DC current (+ into the component)A−3live
0x0103DC power (signed)W−1live
0x0104state of charge%−1live
0x0105battery temperature°C−1retired
0x0110PV array voltageV−3retired
0x0111PV array currentA−3retired
0x0112PV array powerW−1retired
0x0120load currentA−3retired
0x0121load powerW−1retired
0x0130start battery voltageV−3retired

Battery current is signed, and that is the whole point of it. A PZEM-017 reports current as unsigned and therefore cannot fill this slot at all — it cannot tell 40 A into the bank from 40 A out of it. A source that cannot distinguish direction does not implement this measurement, even if it exposes an ampere value.

KindNameUnitScaleStatus
0x0201AC voltageV−1live
0x0202AC currentA−3live
0x0203AC powerW−1live
0x0204AC frequencyHz−2live
0x0205AC energyWh0live
0x0206ac energy importedWh0reserved
0x0207ac energy exportedWh0reserved

AC frequency is the discriminator that proves an engine caught. Current into the bank is also what the sun does; 120 V at 60 Hz is not.

KindNameUnitScaleStatus
0x0301ambient temperature°C−1retired
0x0302temperature°C−1live
KindNameUnitScaleStatus
0x0401tank level%−1live
0x0402tank sender currentA−6live
KindNameUnitScaleStatus
0x0501generator state (enum, see below)—0live
0x0502generator run hoursh−2live
0x0503generator starts since commissioningcount0live
0x0504run contact commanded (bool)—0live
KindNameUnitScaleStatus
0x0601generator selector position (enum, see below)—0live
0x0602uptime since boots0live
0x0603last boot reason (enum)—0live
0x0604log ring utilisation%−1live

0x0501, 0x0601 and 0x0603 carry an answer in a Value’s key 3 where every other metric carries a measurement. P-125 is the rule that follows from that, and it is the one place in the protocol where a discriminant is not in a discriminant field: a value here that a client cannot name is surfaced as unrecognised for that one channel and never rendered as a state, while the Snapshot around it stands.

They were three sentences of prose under the tables above until a client had to decide what an unrecognised one means. A number a reader has to parse out of a sentence is a number the bindings cannot be generated from, which is the whole argument for this file.

ValueNameMeaning
1stoppedNo AC and our contact open. Autostart may fire
2startingOur contact closed, no AC yet. Gives up after the start window
3runningAC present with our contact closed. Ours, and ours to stop
4running, not oursAC present with our contact open — somebody started it with the fob. Not a fault, and never rendered as one
5stop not honouredOur command was withdrawn and the AC persists. Notify; never retry
6gave upCommanded to run and it did not, as many times as the starter is worth. The contact stays open and nothing here will try again until a person clears it — out of fuel, an oil-alert shutdown and a flat start battery are indistinguishable from the controller, and hammering a start line into any of them is wrong

The state model is two facts: is there AC, and is our contact closed. 4 is the row that matters: somebody started it with the fob, and closing our contact on top of that converts the fob owns it into both own it, after which the human’s stop button does not work.

ValueNameMeaning
1autoBehaviours may command the generator
2offNever command it, for any reason. A behaviour that needs it reports itself inhibited
3manualThe operator is driving. The controller monitors and logs only

Named for what it selects, because SelectorPosition on its own is a client rendering three positions of something without knowing what they govern. It is also a different thing from the lockout switch at the genset: that one is a service lockout — nothing cranks while somebody’s hands are in there — and this one is an operator override, at the controller.

ValueName
1power-on
2watchdog
3brown-out
4software reset
5panic

Five different situations, and a generator that was running through one of them is a sixth — the log record carries that, not this metric.


ValueNameMeaning
1measuredRead directly from an instrument
2countedIntegrated or accumulated, and trustworthy as a total
3estimatedDerived from something else, e.g. SoC from terminal voltage
4staleWas measured, is no longer current: older than its channel’s maximum age, or the same reading for so long that the instrument is presumed hung
5absentNo reading. The value key is omitted entirely

A behaviour that needs a counted figure refuses an estimated one. This is the founding rule of the project and the reason 5 exists rather than a zero.


How long a charger has spent in each stage. Quantities, not a point on one kind.

KindNameUnitScaleStatus
0x0701time in bulkmin0reserved
0x0702time in absorptionmin0reserved
0x0703time in floatmin0reserved

This is a different space from the metric kinds above, and fourteen of the twenty numbers below are also a live metric kind. 0x0201 is generator state changed here and AC voltage there. Nothing decodes ambiguously — the two never meet in one field — but a sentence that says 0x0201 without saying which space it means is a sentence somebody implements backwards. Say event 0x0201. The argument for leaving the numbers overlapping is at the head of the metric table.

KindNameClassStatus
0x0101value changedBreserved
0x0102signal validity changedAreserved
0x0201generator state changedAreserved
0x0202generator command withdrawnAreserved
0x0203generator stop not honouredAreserved
0x0301behaviour decision (shadow or applied)Areserved
0x0302behaviour inhibitedAreserved
0x0401output changedAreserved
0x0501concern raisedAreserved
0x0502concern changedAreserved
0x0601bootAreserved
0x0602config changedAreserved
0x0603client enrolledAreserved
0x0604time setAreserved
0x0701records droppedAreserved
0x0702record failed CRCAreserved
0x0801comms link lostAreserved
0x0802comms power cycledAreserved
0x0803comms unrecoverableAreserved
0x0804sessions shed for backpressureAreserved
0x0901topology changedAreserved
0x0902device presence changedAreserved

Every row is reserved, and the reason is the body map. The kind numbers and the classes are settled; not one kind says what is inside its body, so two implementations can agree on framing, on the MAC and on the sequence number and still disagree about every event they exchange. The numbers are allocated; the body schemas are deferred — see DEFERRED.md.

The Class column decides which records survive a controller under pressure, and the two words are defined here, normatively, rather than left to a design document. A column with no definition beside it is a column every implementation fills in for itself, and the two answers it picks between are keep this alarm and drop it. A controller that ranks alarm raised below value changed loses the alarm on the busiest day, which is the day it mattered.

Class A is never dropped — not from the log, and not from a queue. When the implementation’s bounded global write budget is exceeded on class A alone, the controller raises a diagnostic instead of shedding: a site producing that many state changes has something genuinely wrong with it, and the record of what went wrong is the last thing to throw away. When a session’s MAX_EVENT_QUEUE cannot take a class A event, P-098 in PROTOCOL.md closes that session and the client reconnects and catches up with ReadLog. That is worse than a delivered event and far better than a client sitting on a socket it believes is current while an alarm never reached it.

Class B is dropped first, under pressure, and every drop is counted. In the log it is what the write budget sheds; in a session’s outbound queue it goes oldest first, and the count reaches that session in its own records dropped (0x0701) record — a log-ring count cannot explain a hole one queue made. Counted is half the definition and not a detail: a hole in seq with no number attached is the mystery P-097 forbids a client to render as a complete stream.

Today class B is exactly one kind — value changed — and that is the whole of the split. It is the only kind whose rate follows a sampling loop rather than something happening at the site. Everything else in the table is a state change, a command outcome, an alarm, a boot, or a record about the log itself, and every one of those is a sentence somebody has to be able to find in April.

A new event kind is allocated with its class, or it is not allocated. A row added with the class left blank is a number on the wire before anybody has decided whether it may go missing.

Behaviour decision carries whether the decision was applied or shadowed. While every site runs in shadow, a client that cannot tell a shadowed decision from an applied one cannot audit the thing the shadow deployment exists to prove.

0x08xx is the link to the comms processor. LINK.md L-023 and L-110 through L-112 require the controller to log every one of these — the heartbeat ladder ends in a rail switched off — and had no numbers to log them under, which is how a number gets invented at a bench by whoever hit it first. Comms power cycled carries the cycle count, because three power cycles in an hour, rail left off is a sentence somebody has to be able to read at a client: a link that goes quiet leaving no record behind looks exactly like a site that is fine.

Sessions shed for backpressure is the newest of them, and it is allocated because LINK.md’s L-023 requires a class A record for the first session shed in an hour and had no kind to log it under. A comms processor that answers every heartbeat and drains the UART slowly closes all eight sessions without forging a byte or dropping a frame, and the shed record is the only trace it leaves — the link is up and carrying nothing is otherwise a state the untrusted peer can drive at will with nobody able to name it afterwards. It is class A for the same reason: it is the evidence, so it is the one record that must not be shed by the pressure it describes. Comms link lost (0x0801) stays the escalation at three in an hour; 0x0804 is the first occurrence.

A clock that jumps is not a fourth kind. LINK.md’s L-160 logs a step of more than five seconds in 0x0604 time set, carrying the old value alongside the new and the source that set it. One clock change is one record. Splitting it into a routine kind and a jumped kind gives a client two things to reconcile and gives whoever is asking did the schedule fire twice on Tuesday two places to look.


SectionNameStatus
0x0001identity and sitereserved
0x0002channelsreserved
0x0003buses and devicesreserved
0x0010generator behaviourreserved
0x0011frost behaviourreserved
0x0012schedule behaviourreserved
0x0013load-shed behaviourreserved
0x0020networkreserved
0x0021cloudreserved

Every row is reserved, and again it is the body. The section numbers are settled — a client can ask for 0x0011 and be told whether it exists — and no section has a field list, a key number, a unit or a scale for anything inside it. The numbers are allocated; the body schemas are deferred — see DEFERRED.md.

The network and cloud sections hold credentials — the site’s Wi-Fi passphrase among them, because the controller holds the master copy and LINK.md says why. One rule about those schemas is settled already and is not waiting on the field lists: a field marked secret is never returned by GetConfig, which the controller answers with a body and which every enrolled client can call, the cloud client included. DEFERRED.md entry 9 carries it. Anybody drafting a section body from this table alone would write the leak straight back in.

Every behaviour section carries a shadow flag. It is a flag rather than a subsystem, and it is readable by a client so that “nothing is actuating” is a thing a person can confirm rather than believe. Its key number arrives with the section schemas, and it must be the same number in all four behaviour sections: a shadow that means key 9 for frost and key 4 for the generator is a flag somebody reads wrong exactly once.


Reserved, not specified. No actuation until a controller is granted authority over an output — see DEFERRED.md. The space is allocated now so that nothing squats on it in the meantime.

KindNameStatus
0x0101start generatorreserved
0x0102stop generatorreserved
0x0103run exercise cycle nowreserved
0x0201set outputreserved
0x0202clear alarmretired
0x0203acknowledge concernreserved
0x0301clear overridereserved

Those six names are allocations, not specifications: none has an argument schema and none may be sent by a client yet. 0x8000–0xFFFF is where bench and experimental kinds go instead — permanently non-normative, never allocated in this file, and free to abandon. That is the whole of the squatting rule, and DEFERRED.md entry 8 says when a low number becomes sendable.

There is deliberately no factory-reset command and no unpair command. A compromised cloud that can unpair a controller which starts engines is worth more to an attacker than any single command. Both require the physical button, and that is the only way a full client table is emptied.


ValueNameMeaningStatus
1acceptedActed onlive
2rejectedRefused, detail says whylive
3duplicateAlready seen this cmd_id; not acted on twicelive
4inhibitedThe selector is at Off, or the generator is not ourslive
5unauthorisedThis client may not do thislive
6shadowedWould have been accepted; nothing was actuatedlive
7stale_topologyThe rev the command was composed under is not current, so dev and cmp may name something else now (P-166)reserved
8wrong_targetThe target does not list this kind in its cmds (P-166)reserved

4 and 6 are not failures. inhibited is the controller declining with a reason a person can read; shadowed is every decision a site makes while it runs in shadow.

5 is what the client capability mask below produces. A client whose mask does not carry send commands is refused here, inside a MAC’d response, rather than with an error the comms processor could have forged — P-141, applied.

ValueNameMeaningStatus
1acceptedlive
2stale_versionlive
3invalidlive
4unauthorisedlive
5exceeds_caplive
9stagedCorrect, and applied at the next MIN_REV_INTERVAL_MS boundary rather than now (P-154)reserved

4 is the capability mask refusing one of two different things: a client that may not write configuration at all, or a client that may not write this section — a cloud client editing 0x0020 network. One outcome for both, because from the client’s side there is one thing to do about either, and the section it named is already echoed back in key 1.

ValueName
1enrolled
2window_closed
3bad_proof
4table_full
5reclaimed

5 is what stops the client table being a consumable, and the rule that produces it is P-078 in PROTOCOL.md, not this file. Which Pair reuses an occupied row, that the label comparison is over the exact UTF-8 bytes that entered the pair-proof preimage, that matching runs before allocation so P-067’s table_full means eight distinct labels rather than eight pairings, that a reclaim is not a revocation, and why setting the counter back to 0 does not re-open replay — all of it is there, with the arithmetic that says why eight cumulative re-pairings is a season and not a lifetime. What this file allocates is the number.

Outcome 5 rather than outcome 1 is the part that belongs here, because it is the allocation decision: a reclaim silently reported as an enrolment is a row overwritten with nobody told. The person at the panel needs to read this replaced the row called kitchen phone, and the log record needs to say the same — see DEFERRED.md entry 10.

ValueName
1accepted
2rejected
3unauthorised
4needs_button

3 exists because the capability mask can refuse a Time write, and rejected would have told a client its clock was implausible when the real answer is that it is not allowed to set one. Moving the clock moves schedule, exercise and quiet_hours with it, so who may and is this a sane time are two questions and a client that cannot tell them apart retries forever against the wrong one.

4 splits the same hair one more time. A backward set refused for P-114’s floor is refused for a reason a person can act on — hold the button at the panel and send it again — and under 2 it was indistinguishable from a time nobody could believe. A client that cannot tell those apart puts the controller says that time is implausible on the screen while the correction somebody drove four hours to make sits one gesture away.

ValueNameMeaning
1clientA signed Time write from an enrolled client
2ntp-via-commsA TimeOffer the controller accepted from the comms processor

One space for what gets recorded. The Time operation carries a value from this table and P-111’s time set event records one, and there is no second space either of them may draw from. An accepted TimeOffer from LINK.md is recorded as 2 (L-162). That offer carries a source field of its own — a link-local enum with one value in it — and its number is never copied through: 1 there means NTP, 1 here means a client set the clock, which is the opposite. The point of recording a source at all is to tell a drifted RTC from a lying uplink, so a number copied straight across inverts the one thing the field exists to say.

Reserved — see DEFERRED.md.

ValueNameStatus
1acceptedreserved
2bad_offsetreserved
3bad_signaturereserved
4wrong_targetreserved
5too_largereserved
6no_transfer_in_progressreserved
7not_ownerreserved
8unauthorisedreserved

7 and 8 are different refusals and were one word covering both. 7 not_owner is about a transfer: a Firmware operation naming a transfer that another client began. One image lands at a time, and a second client feeding chunks into somebody else’s transfer is how a half-and-half image gets written and then fails its signature after the reboot. 8 unauthorised is about the client: its capability mask does not carry push firmware, and no transfer of its own would have been accepted either. A client told not_owner retries when the other transfer finishes, which is right for one and a loop for the other.

Nothing produces 7 yet, and that is a gap in the message bodies rather than in this table. No document says how a transfer records which client owns it, so there is nothing an implementation could compare a second client against. It is owed by DEFERRED.md entry 6 along with the rest of the Firmware field list, and it is named there so the number does not sit here looking implemented.


ValueName
1app
2browser
3cloud
4cli

A per-client bitfield, fixed at enrolment from client_kind and stored in FRAM beside that client’s counter. P-105 in PROTOCOL.md is the rule — where the mask comes from, why client_kind is a sound input where transport is not, that no message raises or lowers one, and what each refusal is answered with. This file allocates the bits and the row each client_kind is handed.

It is not a wire discriminant — P-014 does not reach it, and an unallocated bit is a capability nobody has defined yet, not a value to reject. It is allocated here because two firmwares that disagree about what bit 4 means disagree about whether the cloud can push firmware, which is the same collision as two people picking 0x0503 and worse in its consequences.

BitThe client mayappbrowsercloudcli
0write configuration at allyesyesyesyes
1write the network (0x0020) and cloud (0x0021) sectionsyesyesnoyes
2send Command 0x08yesyesyesyes
3set the clock with Time 0x0Ayesyesnoyes
4push firmware with Firmware 0x09yesyesnoyes
5–15unallocated————

Allocated in LINK.md, which is where the rule for each one lives; L-180 is the rule for the third column. They are listed here because this file is where a number is handed out, and a space allocated somewhere else is a space two people can allocate from.

Three of them reach a client on purpose and six never do. That distinction is the reason they are a separate space from the client error codes rather than a continuation of them: a client that cannot tell the link failed from your request failed retries against the wrong thing.

CodeMeaningReaches a client?
256Unknown link-local opcode, or one sent from the wrong sideno
257Link-local type on a client transportyes
258Client frame before LinkUp completedyes
259Unknown connection handleyes
260Connection table fullno
261Link protocol major mismatchno
262Too many outstanding link-local requestsno
263Link-local type with a non-zero sessionno
264No authorisation matches this imageno

Bit 1 is only consulted when bit 0 is set: a client that may not write configuration cannot write two particular sections of it either.

Which response answers which denial, because those are outcome numbers and outcome numbers live here. P-105 is what requires every one of them to ride inside the MAC’d response rather than inside an Error the comms processor could have forged:

RefusedAnswer
SetConfig, bit 0 clear — or bit 1 clear on 0x0020/0x0021SetConfigAck 0x87 outcome 4 unauthorised
Command, bit 2 clearAck 0x88 outcome 5 unauthorised
Time, bit 3 clearTimeAck 0x8A outcome 3 unauthorised
Firmware, bit 4 clearFirmware 0x89 outcome 8 unauthorised

A client kind added to the table above arrives with its row here in the same commit — P-105 refuses enrolment of a kind with no row, and this is the allocation half of the same rule. A kind with no mask row is a client whose permissions are whatever the implementer’s zero value happens to be, and a default that opens is the one direction this project never defaults.

A cloud client keeps bit 2, and that is deliberate. Telling a generator to stop from somewhere that is not the site is the whole point of having a cloud at all, and every command is MAC’d end to end regardless — a compromised relay cannot forge one, it can only refuse to carry it. What it does not need is the ability to re-flash the controller, move the clock under a schedule, or read back the credentials it forwards. Command authority is separately gated in any case: no kind below 0x8000 is sendable by anybody yet (DEFERRED.md entry 8).

The failure the cloud column closes is that Command 5 unauthorised, SetConfig 4 unauthorised and Firmware 8 were a vocabulary of refusals with nothing behind them — allocated in this file, produced by nothing, and reading as implemented to everybody who grepped for them. P-105 says what that left the most exposed client in the product able to do; this table is where it stops.

The mask granted at enrolment belongs in the client enrolled record (event 0x0603) when that body schema is written — DEFERRED.md entry 10. Somebody was enrolled and somebody was enrolled and may push firmware are different sentences, and only one of them is an audit record.

Returned in the Hello response. A bit set means the controller can do the thing; a client must not assume an unset bit is a failure, only an absence.

BitMeaningStatus
0event log readablelive
1configuration writablelive
2commands acceptedreserved
3firmware update acceptedreserved
4clock settable by clientlive
5counted state of charge availablelive
6AC metering on the generatorlive
7any behaviour is in shadow modelive
8–31unallocated—

Bit 7 is deliberately coarse. A client that wants to know which behaviour is shadowed reads the config sections; the bit exists so an app can put a banner up without four round trips.

Bits 1, 2, 3 and 4 are answered per calling session, not per device: the controller reports its own capability and the calling client’s mask. A cloud client sees firmware update accepted and clock settable by client clear, and does not draw a button that can only ever be refused. No new field carries this, because the Hello response is bound to exactly one client_id by construction — the field that can say it is already in the right place.

Bit 1 is coarser than the mask and stays that way. A cloud client may write configuration and may not write two sections of it, so it reads bit 1 set and is still refused with outcome 4 on the network section. A bit here is a claim about a message type, not about every section that message can name.


The topology design’s spaces follow. Everything here is reserved until the rule that produces it is written into a document every_live_number_is_reachable sweeps.

What a port physically is. rs485, can and ip are the addressed ones, which is what makes DeviceRow.addr required on them and meaningless on the rest.

ValueNameMeaningStatus
1rs485A two-wire multidrop serial bus; addr is required on it (P-189)reserved
2canCAN, where a device is addressed by node idreserved
3ve_directVictron’s point-to-point serial; one device per port, so no addressreserved
4local_ioThe controller’s own inputs and outputsreserved
5ipEthernet or Wi-Fi, addressed by hostreserved
6onewire1-Wire, addressed by ROM idreserved
7internalInside the controller; nothing physical to unplugreserved

Which of the five descriptor tables a page is paging through.

ValueNameMeaningStatus
1busesBusRowreserved
2devicesDeviceRowreserved
3componentsComponentRowreserved
4signalsSignalRowreserved
5parametersParamRowreserved

Whether a signal carries one value or n of them. The container only — what one value is is the value type below.

ValueNameMeaningStatus
1scalarOne valuereserved
2seriesn values in one signal, addressed by positionreserved

What a single value is, independent of how many there are. Splitting this from the shape is what lets a series of flags exist: sixteen per-cell balancing bits are one signal, not sixteen.

ValueNameMeaningStatus
1gaugeA quantity that moves in both directionsreserved
2counterA running total. It never reverses, which is why a coarse history bucket takes the last constituent and not their sum (P-195)reserved
3enumOne member of the space named by espreserved
4flagsA bitmask over the space named by esp; member m is bit m − 1 (P-186)reserved

Which window a value describes. The same quantity at live and at max are two signals, not one signal read twice.

ValueNameMeaningStatus
1liveWhat it is nowreserved
2lifetimeSince the device was madereserved
3since_resetSince somebody last cleared itreserved
4todaySince local midnightreserved
5yesterdayThe previous whole dayreserved
6limit_upperA ceiling the source states it is honouringreserved
7limit_lowerA floor the source states it is honouringreserved
8maxThe largest seen over the domain’s windowreserved
9minThe smallest seen over the domain’s windowreserved

Which way is positive, stated on the row rather than assumed from the quantity.

ValueNameMeaningStatus
1positive_is_inPositive flows into the componentreserved
2positive_is_outPositive flows out of itreserved
3magnitude_onlyUnsigned; the sign carries no meaningreserved

Where a quantity is measured. Never a charge stage — a stage duration is a quantity and lives in the metric table.

KindNameStatus
0x0001line to neutralreserved
0x0002line to linereserved
0x0003terminalreserved
0x0004cellreserved
0x0005internalreserved
0x0006ambientreserved
0x0007heatsinkreserved
0x0008casereserved
0x0009inletreserved
0x000Aoutletreserved
0x000Btransformerreserved
0xF000–0xFFFFvendor range, skip-unknown under P-019—

What a vendor kind is measured in. Standard kinds carry their unit in the metric table; a vendor kind carries it on the row, and needs a number to carry.

ValueNameMeaningStatus
1voltVreserved
2ampereAreserved
3wattWreserved
4watt hourWhreserved
5ampere hourAhreserved
6degree celsius°Creserved
7percent%reserved
8hertzHzreserved
9secondsreserved
10minuteminreserved
11hourhreserved
12counta plain tallyreserved
13ohmΩreserved
14pascalPareserved
15litreLreserved
16nonea number with no unit — an enum or a flags wordreserved

Whether a number is usable now. Separated from provenance because counted and stale is a real state and one word could not say it.

ValueNameMeaning
1okCurrent
2staleWas good; the value is the last known and age says how old
3initialisingThe source is up and has not produced a first reading
4unsupportedThis device does not implement it, including a vendor’s in-band absence pattern (P-176)
5sensor_faultThe source reports it bad, or the reply did not arrive intact
6out_of_rangeA number arrived and was refused: impossible here, or outside i32 at the registry’s scale (P-185)
7absentThe component or device is not present
8unnamed_stateThe source reported an operating state we have no value for (P-164)

Where a number came from. 0 exactly when there is no number at all.

ValueNameMeaning
0noneThere is no number
1measuredRead directly from an instrument
2countedIntegrated or accumulated by the source, and trustworthy as a total
3derivedThis controller computed it from other signals
4estimatedInferred from a proxy — state of charge from terminal voltage
5reportedThe device states it: a limit, or a setpoint it says it is honouring
6commandedWhat was asked for, never what was observed

Whether a device is answering. Separate from any of its signals’ validity, and it never moves rev.

ValueNameMeaningStatus
1onlineAnsweringreserved
2degradedAnswering, with errors or omissionsreserved
3offlineWas seen and has stopped answeringreserved
4never_seenConfigured and has never answeredreserved

How much a concern matters. The table reserves rows above warning so per-cell noise cannot hide a pack fault.

ValueNameMeaningStatus
1infoWorth recordingreserved
2warningWorth looking atreserved
3faultSomething is brokenreserved
4protectionA source is refusing to operate to protect itselfreserved

Where a concern is in its life. Acknowledging is not clearing, and only the condition going away clears it.

ValueNameMeaningStatus
1activeThe condition is presentreserved
2active_ackedSomebody has read it. The condition is still presentreserved
3latched_clearedThe condition is gone; the source’s latch is not, and will clear on the source’s own termsreserved
4clearing_blockedThe condition is gone and the source states it cannot reset the latch yet — the difference between wait and drive outreserved
5clearedOver. Terminal, and the only state a row carries as it leaves the table (P-180)reserved

A normalized thing that has gone wrong. Open, so a condition nobody has allocated still renders as a sentence somebody can act on.

KindNameStatus
0x0001over voltagereserved
0x0002under voltagereserved
0x0003over temperaturereserved
0x0004under temperaturereserved
0x0005charge over currentreserved
0x0006discharge over currentreserved
0x0007short circuitreserved
0x0008overloadreserved
0x0009cell imbalancereserved
0x000Alow state of chargereserved
0x000Bcharge inhibitedreserved
0x000Cdischarge inhibitedreserved
0x000Dunnamed statereserved
0x000Eregistration refusedreserved
0x000Fseries too longreserved
0x0010inventory fullreserved
0x0011bay flappingreserved
0xF000–0xFFFFvendor range, skip-unknown under P-019—

The three window sizes. They nest exactly — four quarter-hours to an hour, twenty-four hours to a day — so converting between them has no rounding rule.

ValueNameMeaningStatus
1dayCoarsestreserved
2hourFour quarter-hoursreserved
3quarter_hourFinest. The three nest exactly, which is why P-194’s conversion has no rounding rulereserved

Whether a bucket is the device’s own record or one this controller integrated. Per bucket, because a window can span the day the device stopped supplying one.

ValueNameMeaningStatus
1device_reportedThe device’s own record, on its own axisreserved
2controller_derivedThis controller integrated it, including any bucket it aggregated (P-195)reserved

Why a history response is shorter than asked for. Evaluated in order, so a response that hits a seam and then an edge names the seam.

ValueNameMeaningStatus
1device_replacedA DeviceRow.since boundary: a different instrumentreserved
2component_reassignedA ComponentRow.since boundary: the same instrument, a different meaningreserved
3gap_in_recordThe store holds nothing here and does not claim how wide the hole isreserved
4end_of_recordPast the newest closed bucketreserved
5older_than_storeBefore the oldest bucket heldreserved
6page_fullThe response reached its byte or point capreserved

Why rev moved. There is deliberately no capability changed value: a signal a unit does not implement is published at validity 4, so it becoming available is a validity change and not a topology change.

ValueNameMeaningStatus
1bootFirst inventory after a restartreserved
2config_writeAn operator wrote topologyreserved
3sub_device_adoptedA module answered and was taken into the tablesreserved
4sub_device_removedA module was taken out of themreserved
5device_replacedThe instrument behind a row changed (P-156)reserved

What a component is. Open: a role nobody has allocated renders under its number rather than blanking the row around it.

KindNameStatus
0x0001pv arrayreserved
0x0002mppt trackerreserved
0x0003battery bankreserved
0x0004battery packreserved
0x0005heaterreserved
0x0006contactorreserved
0x0007fetreserved
0x0008ac inputreserved
0x0009ac outputreserved
0x000Aphasereserved
0x000Bline pairreserved
0x000Cdc outputreserved
0x000Dload outputreserved
0x000Estart batteryreserved
0x000Fmeterreserved
0x0010circuitreserved
0x0011merged circuitreserved
0x0012transfer relayreserved
0x0013inverterreserved
0x0014chargerreserved
0x0015probereserved
0x0016tankreserved
0x0017cellreserved
0x0018relayreserved
0x0019generatorreserved
0xF000–0xFFFFvendor range, skip-unknown under P-019—

What a whole enclosure is. Open, on the same terms.

KindNameStatus
0x0001solar chargerreserved
0x0002inverterreserved
0x0003inverter chargerreserved
0x0004bmsreserved
0x0005energy meterreserved
0x0006shuntreserved
0x0007sensorreserved
0x0008power modulereserved
0x0009controllerreserved
0x000Abattery monitorreserved
0x000Btemperature sensorreserved
0xF000–0xFFFFvendor range, skip-unknown under P-019—

A product this controller has a driver for. Open, and a vendor range is expected to carry most of it.

KindNameStatus
0x0001origin 89 controllerreserved
0x0002pzem 003reserved
0x0003pzem 017reserved
0x0004epever tracer breserved
0x0005victron mppt rsreserved
0x0006victron multiplusreserved
0x0007eg4 lifepower4reserved
0x0008morningstar sunsaver duoreserved
0x0009emporia vue 3reserved
0x000Aecoflow power kitreserved
0xF000–0xFFFFvendor range, skip-unknown under P-019—

A protocol dialect the controller can speak. A dialect is distinct when its framing or register model differs, not when a vendor ships a second model.

KindNameStatus
0x0001no protocolreserved
0x0002pzem dcreserved
0x0003epever breserved
0x0004victron mppt rs hexreserved
0x0005victron gx modbus vebusreserved
0x0006eg4 lifepower4 serialreserved
0x0007morningstar sunsaver duoreserved
0x0008emporia vue3 esphome i2creserved
0x0009ecoflow power kitsreserved
0xF000–0xFFFFvendor range, skip-unknown under P-019—

The spaces a SignalRow can point esp at. This is the registry of registries, and it is what lets a client learn a value is a charge stage from the descriptor.

KindNameStatus
0x0001presencereserved
0x0002control ownerreserved
0x0003generator statereserved
0x0004generator selectorreserved
0x0005boot reasonreserved
0x0006charge stagereserved
0x0007balancingreserved
0xF000–0xFFFFvendor range, skip-unknown under P-019—

Who is deciding, carried as an ordinary signal so the BMS is driving the charge voltage and we are not is a reading rather than an inference.

ValueNameMeaningStatus
1local_panelThe buttons on the device itselfreserved
2this_controllerOrigin 89reserved
3bmsThe battery is decidingreserved
4gx_essA Victron GX or ESS assistantreserved
5remote_clientA person, through this protocolreserved
6device_automationThe device’s own schedule or logicreserved

One namespace per vendor, so two drivers cannot both pick 7 for a vendor kind or a raw condition code.

KindNameStatus
0x0001victronreserved
0x0002epeverreserved
0x0003pzemreserved
0x0004eg4reserved
0x0005morningstarreserved
0x0006emporiareserved
0x0007ecoflowreserved
0x0008magnumreserved
0x0009xantrexreserved
0xF000–0xFFFFvendor range, skip-unknown under P-019—

How an Inventory 0x8D answered.

ValueNameMeaning
1okThe request was answered, in whole or in part
2supersededrev is neither 0 nor the controller’s current one (P-153)
3unknown_kindwhat is not in 1..5
4out_of_rangefrom is past the last row the request resolves to

How a Readings 0x8E answered.

ValueNameMeaning
1okThe request was answered, in whole or in part
2supersededrev is neither 0 nor the controller’s current one (P-153)
3unknown_selectorA Sel names a dev, cmp or sig that does not exist at this rev
4out_of_rangefrom is past the last row the request resolves to

How a Concerns 0x8F answered.

ValueNameMeaningStatus
1okThe request was answered, in whole or in partreserved
2supersededrev is neither 0 nor the controller’s current one (P-153)reserved
3out_of_rangefrom is past the last row the request resolves toreserved

How a History 0x90 answered.

ValueNameMeaningStatus
1okThe request was answered, in whole or in partreserved
2supersededrev is neither 0 nor the controller’s current one (P-153)reserved
3unknown_signalsig does not exist at this revreserved
4series_not_historableThe signal exists and its shape is 2 seriesreserved
5no_historyThe signal exists, is a scalar, and carries no hist keyreserved
6out_of_rangefrom is past the last row the request resolves toreserved

None yet. When the first one lands it stays here forever, with the date and the reason, because the alternative is somebody reusing it in three years.