20992883ff
---ci--- project: atelier phase: 1 milestone: v0.4 status: complete phase_role: execution phase_tag: v0.3.1 requirements: covered: [ATELIER-92, ATELIER-93, ATELIER-94, ATELIER-95, ATELIER-96] partial: [] ---/ci---
258 lines
12 KiB
Markdown
258 lines
12 KiB
Markdown
# IoT — Derived Rules
|
||
|
||
> Derives from `domains/edge/first-principles.md`. Applies P3
|
||
> (Resources are Constrained and Declared) primarily, with P5
|
||
> (command idempotency), P7 (partial degradation when devices drop),
|
||
> P9 (device identity and provisioning), and P10 (telemetry from
|
||
> devices). Cross-links `domains/security/secrets` for device
|
||
> credentials and `domains/messaging/queues` for the MQTT QoS
|
||
> parallels to delivery semantics.
|
||
|
||
## What IoT at the Edge Is (P3 Resources are Constrained and Declared)
|
||
|
||
- IoT at the edge is the engineering discipline of operating
|
||
constrained devices — sensors, actuators, gateways, microcontrollers
|
||
— as first-class participants in a distributed system. The
|
||
distinguishing constraint is per-device resource bounds (P3): a
|
||
battery-powered sensor has kilobytes of RAM, a constrained
|
||
protocol, and a multi-year sleep budget. These constraints are
|
||
declared per device class, never assumed infinite.
|
||
- The boundary is per D-061: edge owns the constrained-device
|
||
reality; performance owns the generic unbounded-growth-is-a-bug
|
||
principle (`performance/P4 Resource Bounds`); concurrency owns
|
||
in-process primitives. IoT is an edge concern because its defining
|
||
traits are constrained resources (P3), geographic distribution as
|
||
a fleet (P8), partition-prone operation (P2), and device-scoped
|
||
identity (P9) — concerns that only arise at the network edge.
|
||
- See `domains/edge/offline-first.md` for the partition-survival
|
||
discipline that constrained devices depend on, and
|
||
`domains/edge/sync.md` for the reconciliation of device state
|
||
across partitions.
|
||
|
||
## Device Resource Classes (P3, C8 Economy)
|
||
|
||
- A device resource class declares the bounds for a class of
|
||
devices: CPU (MHz, cores), memory (KB/MB), power (battery mAh,
|
||
duty-cycle budget), bandwidth (bytes/sec, latency budget), and
|
||
storage (KB/MB). Every device in the fleet is assigned to a class;
|
||
every operation is budgeted against its class.
|
||
- An undeclared budget is a defect (P3 violation): a sensor that
|
||
sends telemetry every second without a duty-cycle budget exhausts
|
||
its battery in days, not years. The budget is the correctness
|
||
bound (C1) and the economy bound (C8).
|
||
- A device class implies a protocol choice: a class-0 device
|
||
(constrained sensor, KB RAM) speaks CoAP; a class-1 device
|
||
(gateway, MB RAM) speaks MQTT; a class-2 device (edge compute
|
||
node, GB RAM) speaks HTTP. The protocol follows the constraint,
|
||
not the reverse.
|
||
|
||
| Class | RAM | Power | Protocol | Typical role |
|
||
|-------|-----|-------|----------|--------------|
|
||
| 0 (constrained sensor) | < 10 KB | Battery, multi-year | CoAP, LoRaWAN | Telemetry only, no inbound commands |
|
||
| 1 (actuator, gateway) | 10 KB – 1 MB | Battery or wired, weeks-months | MQTT, CoAP | Telemetry + commands, queue-and-forward |
|
||
| 2 (edge compute) | > 1 MB | Wired, continuous | HTTP, MQTT | Local aggregation, gateway, edge inference |
|
||
|
||
## Constrained Protocols — MQTT and CoAP (P3, P5)
|
||
|
||
- **MQTT** is the canonical pub/sub protocol for constrained devices.
|
||
It is lightweight (2-byte header), broker-backed, and provides QoS
|
||
levels (0, 1, 2) that map to delivery semantics. MQTT is the
|
||
cross-process analog of message-queue delivery — see
|
||
`domains/messaging/queues` for the general queue/delivery-semantics
|
||
discipline; the cross-link is one-directional outward (edge →
|
||
messaging) per D-062 and D-026 extended.
|
||
- **CoAP** is the REST analog for constrained devices: UDP-based,
|
||
low-overhead, with confirmable (CON) and non-confirmable (NON)
|
||
message types. CoAP fits class-0 devices where TCP is too heavy.
|
||
- A blocking synchronous call on a constrained device with no
|
||
timeout is the `blocking-call-on-constrained-device` chaos
|
||
anti-pattern: it blocks the node, has no timeout (= hang), and
|
||
retries are unsafe without idempotency (P3 + P5 breach). Every
|
||
device operation must be async with a timeout, and every retried
|
||
operation must be idempotent.
|
||
|
||
```json
|
||
// MQTT publish/subscribe payload with QoS levels (P5 idempotency,
|
||
// P3 constrained protocol).
|
||
// QoS 0 — at-most-once: fire-and-forget, no ack. For telemetry
|
||
// where a dropped sample is acceptable (P3 economy of the
|
||
// constrained link).
|
||
{
|
||
"topic": "devices/sensor-7/temperature",
|
||
"qos": 0,
|
||
"payload": {
|
||
"device": "sensor-7",
|
||
"ts": 1700000000,
|
||
"value": 21.4,
|
||
"unit": "C"
|
||
}
|
||
}
|
||
|
||
// QoS 1 — at-least-once: acked, may duplicate. The consumer must
|
||
// be idempotent (P5) — dedup by (device, ts) or an idempotency key.
|
||
{
|
||
"topic": "devices/actuator-3/command",
|
||
"qos": 1,
|
||
"payload": {
|
||
"device": "actuator-3",
|
||
"idempotencyKey": "cmd-1700000000-1",
|
||
"command": "set-point",
|
||
"value": 22.0
|
||
}
|
||
}
|
||
|
||
// QoS 2 — exactly-once: four-step handshake, no duplication. The
|
||
// heaviest QoS; use only where duplicates are intolerable AND the
|
||
// device has the budget for the handshake (class-1+ only, P3).
|
||
{
|
||
"topic": "devices/actuator-3/irreversible-command",
|
||
"qos": 2,
|
||
"payload": {
|
||
"device": "actuator-3",
|
||
"idempotencyKey": "cmd-1700000000-2",
|
||
"command": "calibrate"
|
||
}
|
||
}
|
||
```
|
||
|
||
- The QoS choice is a P5 (idempotency) and P3 (resource) decision:
|
||
QoS 0 is cheapest (no ack) but lossy; QoS 1 requires consumer
|
||
idempotency (P5); QoS 2 is exactly-once but costs a four-step
|
||
handshake on a constrained link. The default for telemetry is QoS
|
||
0 or 1; the default for commands is QoS 1 with an idempotency key
|
||
(P5); QoS 2 is reserved for irreversible commands where the
|
||
device budget permits.
|
||
|
||
## Device Identity and Provisioning (P9 Identity is Constrained at the Edge)
|
||
|
||
- Every device holds a per-device identity: a unique device ID and a
|
||
scoped credential (X.509 certificate, API token, or rotating
|
||
key). No shared fleet credential — one compromise must not equal
|
||
a fleet compromise (P9). The credential scope is minimal: a
|
||
device can publish to `devices/<its-id>/+` and subscribe to
|
||
`devices/<its-id>/commands`, nothing else.
|
||
- Provisioning is the act of assigning a device identity at
|
||
enrollment time. The provisioning manifest declares the device,
|
||
its class, its allowed topics, and its credential. The manifest is
|
||
the P9 contract — a device operating outside its manifest scope is
|
||
a violation.
|
||
- A device that is provisioned with a shared fleet key (the
|
||
`shared-edge-device-credential` anti-pattern) is a P9 violation:
|
||
blast radius is unbounded. See `domains/security/secrets` for the
|
||
general secret-hygiene discipline (per-identity credentials,
|
||
rotation, minimal scope) that device provisioning builds on.
|
||
|
||
```yaml
|
||
# Device provisioning manifest (P9 per-device identity + scoped
|
||
# credentials). The manifest is the contract; the device operates
|
||
# only within its declared scope.
|
||
device:
|
||
id: sensor-7
|
||
class: 0 # P3 resource class
|
||
model: temp-sensor-v2
|
||
firmware: 1.4.2
|
||
identity:
|
||
cert: "sha256-of-device-cert"
|
||
credentialScope:
|
||
publish:
|
||
- "devices/sensor-7/temperature"
|
||
- "devices/sensor-7/status"
|
||
subscribe:
|
||
- "devices/sensor-7/commands"
|
||
# No wildcard, no fleet-wide topics (P9).
|
||
provisioning:
|
||
enrolledAt: 2024-01-15T00:00:00Z
|
||
rotatesEvery: 90d
|
||
# Per-device credential; never shared (P9, domains/security/secrets).
|
||
```
|
||
|
||
## Telemetry from Devices (P10 Edge Observability Survives Partition)
|
||
|
||
- Device telemetry is local-first (P10): the device buffers telemetry
|
||
on-node and forwards on reconnect. A fire-and-forget telemetry
|
||
pipeline loses data when the link drops; a buffered pipeline
|
||
survives. The buffer is bounded by the device class (P3): a
|
||
class-0 sensor buffers minutes of telemetry, not hours.
|
||
- Telemetry is observable in aggregate: the operator sees the fleet's
|
||
behavior, not just per-device. A device that has not reported in
|
||
its expected interval is itself a signal (a dead device, a
|
||
partitioned device, a drained battery). See
|
||
`domains/observability/metrics` for the generic structured-metrics
|
||
discipline; edge owns the partition-survivable, local-first angle.
|
||
- Telemetry must not be a secrets channel (P9 analog, see
|
||
`domains/observability/P6 No Secrets in Observability`): device
|
||
credentials, PII, and personally-identifying location must not
|
||
enter telemetry payloads.
|
||
|
||
## Command Idempotency (P5 Edge Operations are Idempotent)
|
||
|
||
- Device commands are retried by nature (the network is
|
||
partition-prone). Every command carries an idempotency key so a
|
||
retried command does not double-apply (P5). A `set-point` command
|
||
retried with the same idempotency key sets the point once, not
|
||
twice; an `open-valve` command retried is safe because the valve
|
||
is already open.
|
||
- Irreversible commands (a calibration burn-in, a firmware flash)
|
||
require stronger idempotency: the device tracks applied
|
||
idempotency keys and refuses re-application. A retried irreversible
|
||
command without idempotency tracking double-applies the effect
|
||
(P5 violation, possibly a physical-side-effect bug).
|
||
- The idempotency key is per-command, not per-device. A device that
|
||
dedups by device ID alone will drop distinct commands issued in
|
||
the same window. Use `(device, command-id, ts-window)` or a
|
||
UUID per command.
|
||
|
||
## Partial Degradation When Devices Drop (P7 Partial Degradation is Engineered)
|
||
|
||
- A fleet degrades when devices drop (battery exhaustion, partition,
|
||
hardware failure). The system must continue to operate with the
|
||
remaining devices; a whole-system crash on one device's failure is
|
||
a P7 violation. The degraded mode is documented: which
|
||
aggregations are valid with N-1 devices, which alerts fire, which
|
||
fallbacks engage.
|
||
- A device that drops is not an incident by itself — fleets expect
|
||
churn. The operator-facing signal is the *aggregate* health (X%
|
||
of devices reporting, Y% partitioned for >Z minutes), not the
|
||
per-device drop. Per-device drop alerts are noise; aggregate
|
||
degradation alerts are signal (see `domains/observability/metrics`).
|
||
- A command to a dropped device must time out (P5 — idempotent
|
||
retry) and degrade (P7 — the fleet continues without that
|
||
device). A command that blocks forever waiting for a dropped
|
||
device is the `blocking-call-on-constrained-device` chaos
|
||
anti-pattern (P3 + P5 breach).
|
||
|
||
## Cross-Link to Messaging (P5, cross-link messaging/queues)
|
||
|
||
- MQTT QoS 0/1/2 maps to at-most-once / at-least-once / exactly-once
|
||
delivery semantics — the same three-way tradeoff documented in
|
||
`domains/messaging/queues`. The cross-link is one-directional
|
||
outward (edge → messaging) per D-026 extended: edge owns the
|
||
constrained-device protocol angle; messaging owns the generic
|
||
cross-process delivery-semantics angle.
|
||
- This link dangles until P2 (the messaging domain is authored in
|
||
P2); it is verified bidirectional in P5 (ATELIER-114 per
|
||
IDEATE-40). Acceptable per D-053 (vertical-slice integrity — P1
|
||
ships the edge domain self-consistent; the messaging cross-link
|
||
resolves by the P6 ship).
|
||
- The parallel: a constrained device's QoS 1 publish is the
|
||
device-flavored instance of an at-least-once queue delivery — the
|
||
consumer (the broker or the downstream service) must be
|
||
idempotent (P5 here, `messaging/P3 Consumers are Idempotent`
|
||
there). The idempotency discipline is the same; the protocol and
|
||
failure model differ (constrained-device link vs broker-backed
|
||
network).
|
||
|
||
## What Violates IoT-at-the-Edge Discipline
|
||
|
||
| Violation | Principle |
|
||
|-----------|-----------|
|
||
| Undeclared device resource budget (assumes infinite battery/RAM) | P3 Resources are Constrained and Declared |
|
||
| Shared fleet credential (one key for all devices) | P9 Identity is Constrained at the Edge |
|
||
| Non-idempotent device command (retried command doubles the effect) | P5 Edge Operations are Idempotent |
|
||
| Blocking synchronous call on a constrained device with no timeout | P3, P5 (blocks the node; retry unsafe) |
|
||
| Fire-and-forget telemetry with no on-device buffer (lost on partition) | P10 Edge Observability Survives Partition |
|
||
| Whole-system crash on one device's failure (no degradation contract) | P7 Partial Degradation is Engineered |
|
||
| Device credential scope that includes fleet-wide topics (over-scoped) | P9, `domains/security/secrets` |
|
||
| QoS 2 used on a class-0 device (no budget for the handshake) | P3 Resources are Constrained and Declared |
|
||
| Per-device-drop alert (noise; aggregate degradation is the signal) | P7, `domains/observability/metrics` |
|
||
| Telemetry payload that includes device credentials or PII | P9, `domains/observability/P6 No Secrets in Observability` | |