{
  "uid": "cogitave.learn.build-a-diyar-solution.the-device-profile",
  "kind": "moduleUnit",
  "href": "/modules/build-a-diyar-solution/the-device-profile/",
  "title": "The signed device profile",
  "summary": "",
  "type": null,
  "products": [],
  "roles": [],
  "levels": [],
  "subjects": [],
  "headings": [
    "One signed document, loaded at boot",
    "What it configures",
    "Signing and boot refusal",
    "Append-only evolution within a major",
    "The HAL seam: the same core, different wiring",
    "Field-bus transports and the mandatory watchdog",
    "What is not yet field-validated"
  ],
  "source": "Every Diyar edge boots from exactly one signed document that describes the hardware it drives - not a hard-coded build, and not a per-customer fork. This unit covers what that document configures, how the edge enforces it before doing anything else, and how the same platform reaches different field hardware through one narrow seam.\n\n## One signed document, loaded at boot\n\nA **device profile** is an Ed25519-signed JSON document, one per solution, that parametrically configures the edge (`edge-app`) at startup. It is not application config in the ordinary sense: the profile decides measurement conditions and safety-relevant pin maps that end up inside the evidence record, so Diyar signs it with the same detached-signature primitive it uses for its other signed documents (the update manifest, the license, the roster).\n\nThe profile is identified by a DTMI-style id and a monotonic revision, for example `profile_id: \"diyar:profile:pressure-hold;1\"` at `revision: 1`. `profile_id` must parse as `diyar:profile:<name>;<major>` - the trailing `;<major>` is a real major-version boundary, not decoration - and `revision` is a monotonic anti-rollback counter.\n\n## What it configures\n\n| Section | What it names |\n| --- | --- |\n| `channels[]` | one entry per sensor: id, ADC index/mux or protocol address, kind (probe, cold junction, ambient, disabled), unit, calibration, a plausibility band |\n| `actuators[]` | one entry per output: id, role (`heat_source`, `airflow`, `indicator`), pin or protocol write target, and a `fail_safe: deenergized` default |\n| `safety` | the absolute safety limit — a signal-bound limit with a `Max`/`Min` direction and a bound in that signal's own unit (an over-temperature ceiling for a heat process, an over-pressure limit for a pressure one) — and the maximum allowed rate of change |\n| `process` | the process-rule parameters the named engine requires — for a threshold-hold engine, the in-band range and the number of consecutive in-band samples; sampling interval and max steps are common to every engine |\n| `verdict_binding` | which verdict engine this profile names, and how its channels map onto that engine's expected input (the next unit covers what a profile is and is not allowed to name here) |\n| `workflow` / `capabilities` | ordered phase ids, and which contract operations this product exposes |\n\nThe profile itself lives as `profile.json` inside an app package directory named by `DIYAR_APP_DIR` - the next unit but one covers the other two signed documents that sit next to it.\n\n## Signing and boot refusal\n\n`edge-app` parses the profile with strict serde (`deny_unknown_fields` - an unrecognized field is a load error, never silently ignored), verifies its Ed25519 signature over a domain-tagged preimage, and checks the revision. Any unverifiable or invalid profile is a **loud refuse-to-start**, not a degraded run. The rule is stated directly: an app package directory that is set but not fully present or verifiable must not boot.\n\nThis is the same posture as the `DIYAR_HAL=real` gate for direct hardware: a real backend requires a valid signed device profile before it will start at all. The edge also cross-checks the profile's channel count against the compiled-in constant its verdict engine expects, so a profile that doesn't match the hardware it claims to describe is rejected before a single reading is taken.\n\n> [!NOTE]\n> An embedded default/simulation profile with today's exact values ships with the edge, so existing tests stay green without any signing infrastructure. An unsigned profile is only ever accepted behind an explicit, loud insecure-dev escape hatch - never on a path that could reach production hardware.\n\n## Append-only evolution within a major\n\nA profile's `profile_id` major is a compatibility boundary. Moving to the next `revision` inside the same major is allowed in only one direction: **channels and actuators may be appended, never removed or renamed.** This is enforced in `validate()`, not by convention - the check compares the previous and next profile and rejects:\n\n- a changed `profile_id` presented as an upgrade (that is a new profile, not an upgrade);\n- a `revision` that does not strictly increase;\n- a channel that was removed;\n- a channel or actuator whose index/kind/role/offset changed in place.\n\nPractically: a physical rewire that adds a probe is a new revision of the same profile. Renumbering or dropping a probe requires a new profile major, and reusing an old `profile_id` to mean something structurally different is refused outright.\n\n## The HAL seam: the same core, different wiring\n\nA new physical device configuration is a signed profile, not a fork of the codebase. Above the profile, everything the measurement engine and safety logic depend on comes through three narrow traits - the HAL seam:\n\n```rust\ntrait TempSource { fn sample(&mut self) -> Result<ReadingSet, HalError>; } // one acquisition sweep\ntrait RelayBank  { fn set(&mut self, relay: Relay, energized: bool); fn de_energize_all(&mut self); }\ntrait MonotonicClock { /* timing independent of wall clock */ }\n```\n\nA timeout or a bad read surfaces as a typed `HalError`, never stale data quietly reused. Today's implementors are direct hardware (an SPI ADC and GPIO relays) plus protocol-backed field devices reached over **Modbus** (TCP and RTU) - each is just another pair of `TempSource`/`RelayBank` implementors behind the same seam. Nothing above the HAL - the measurement engine, the safety typestate, the evidence journal, the verdict oracle - changes when the transport changes.\n\n## Field-bus transports and the mandatory watchdog\n\nWhen a profile drives a field device instead of direct hardware, the signed profile carries an additional `transport` block naming the protocol, the connection parameters, and how each profile channel/actuator maps onto a register or coil. A Modbus RTU example:\n\n```json\n{\n  \"protocol\": \"modbus_rtu\",\n  \"connection\": {\n    \"path\": \"/dev/ttyUSB0\",\n    \"baud_rate\": 19200,\n    \"unit_id\": 1,\n    \"request_timeout_ms\": 1000\n  },\n  \"channels\": {\n    \"t1\": { \"register\": \"input\", \"address\": 100, \"data_type\": \"i16\", \"scale\": 0.1 }\n  },\n  \"actuators\": {\n    \"heater\": { \"target\": \"coil\", \"address\": 1, \"energized_value\": 1 }\n  },\n  \"watchdog\": {\n    \"register\": \"holding\", \"address\": 900, \"period_ms\": 1000, \"expiry_ms\": 3000\n  }\n}\n```\n\n> [!WARNING]\n> A lost network or serial link is comms loss: the edge cannot guarantee its \"off\" command ever arrives. Profile validation **rejects** any Modbus transport that actuates a `heat_source` without a `watchdog` block - the field device itself must de-energize the output if it stops seeing a heartbeat within `expiry_ms`. This is a load-time refusal, not optional configuration.\n\n`request_timeout_ms` is mandatory for the same reason: a hung read has to surface as a fault that cuts the heater, never as a stale value fed into a verdict.\n\n## What is not yet field-validated\n\nProvisioning a field-bus device is not the same as running it against real equipment. The boot preflight for a Modbus (TCP or RTU) transport checks - without a physical device attached - that the profile is present, every non-disabled channel is bound, and no actuator role collides on the map. Having confirmed the wiring is complete, the edge still exits with status 2: the networked run path stays gated until a real field device's comms-loss watchdog and register map are validated on the bench. This is the same gate `DIYAR_HAL=real` applies to direct hardware pending hardware characterization: a signed profile makes a device's *configuration* real, but it does not by itself certify a *run* on real hardware.\n",
  "partOf": "cogitave.learn.build-a-diyar-solution",
  "durationInMinutes": 8,
  "quiz": null
}