HEART Embedded Systems logoHEART

Guide 1: A HEART-Embedded air-conditioner controller#

Reading tracks3 Advanced
Advanced (Stop 3A): HEART Full with heartd beside the control firmware. Which track am I?

This guide takes an AC temperature controller from day 1 to a unit that knows itself, looks after itself and answers in one standard language.

The finished example is in heart-core-rs/examples/ac-controller. Every command shown here has been run against it with a simulated control board.


Before you start: what HEART will and will not do#

HEART watches, explains, advises and acts with permission. It does not control the compressor.

Stays in your firmware or hardwareGoes to HEART
Thermostat control loopWatching temperatures, run times and faults
Compressor anti-short-cycle timerSpotting short cycling and explaining it
High and low pressure tripsRaising alerts and escalating to the installer
Freeze protection, electrical protectionAnswering "how is the unit?" in one standard format
Anything a safety standard coversChanging the setpoint or mode, through checks and approval

This split is required by the standard (API Standard, Section 15). If HEART crashes, hangs or is removed, the unit must keep cooling safely.


Day 1: Decide the hardware#

The reference Core today needs embedded Linux (class Core-M). In testing it used about 5.6 MB of memory at rest, and its program is 2.9 MB.

Your controllerWhat to do
Linux board, such as a Raspberry Pi Compute Module or i.MXRun heartd beside your control program. Recommended.
Microcontroller onlyPair it with a small Linux module, or use a building Gateway (see Step 10). The microcontroller-only Core (Core-S) is not built yet.
Many units in one buildingEach unit runs heartd on its own IP address, or a building controller runs a HEART Gateway for all of them

Your control program and heartd talk through one of two routes:

  • Control-board registers. In the example, the control board exposes Modbus TCP registers. Modbus TCP is supported today.
  • The host feed. Your firmware writes one line of JSON per event to a local socket.

If your board speaks something else, such as UART, CAN or BACnet, you will need to add an adapter in crates/heartd/src/adapters.rs. Only modbus-tcp and process adapters exist today.


Step 1: Give the unit an identity (the HEART Card)#

Create card.json. The system ID must be a reverse-DNS name under a domain you own.

{
  "system": { "id": "com.example.cool-air.ac-controller", "name": "CoolAir AC Controller",
              "version": "0.1.0", "alias": "ac-unit", "owner": "Example Climate Ltd" },
  "level": "full",
  "bindings": ["http-server", "unix-socket"],
  "pulse": { "intervalSec": 30 },
  "modules": ["control", "properties", "custom", "core"],
  "shares": { "pulse": "public", "rhythm": "internal", "userData": "never-share" },
  "valve": { "llmCeiling": "internal" },
  "capabilities": []
}
FieldWhy
level: fullYou want it to act on requests, and plain-language serving (core) needs Full
modulescontrol lets a person stop all autonomy; properties and custom expose your values and commands
sharesAnyone may see that the unit is healthy; only trusted parties see its event history

Step 2: List what the unit knows, and how private each item is#

Write down every value worth knowing, and give each a privacy level before writing any code.

ValueSourcePrivacyWhy
Healthy or notDerived by HEARTpublicAnyone may know if it works
Room temperatureRegister 40001internalOwner and installer
Compressor runningRegister 40011internal
Setpoint and modeRegisters 40021, 40022internal
Occupancy, daily scheduleYour firmwareprivateReveals when people are home
Wi-Fi password, installer PINYour firmwarenever-shareNever leaves the unit, not even to an AI

Each readable value becomes a property definition, as in custom/room-temperature.json:

{
  "name": "com.example.hvac.room_temperature",
  "kind": "property",
  "title": "Room temperature",
  "description": "Current room temperature measured by the return-air sensor.",
  "valueSchema": { "type": "number" },
  "unit": "Cel",
  "privacy": "internal",
  "native": {
    "protocol": "modbus-tcp",
    "target": "tcp:127.0.0.1:1502:1",
    "request": { "function": 3, "register": 40001, "count": 1 },
    "response": { "extract": { "value": { "from": "registers[0]", "scale": 0.1 } } }
  }
}

The board stores tenths of a degree, and scale turns 245 into 24.5. HEART also polls each property, so it notices when the board stops answering.

Write titles and descriptions the way a homeowner talks. HEART uses them to understand plain requests.


Step 3: Define what the unit can be asked to do#

Each command becomes a command definition, with limits, as in custom/set-setpoint.json:

{
  "name": "com.example.hvac.set_setpoint",
  "kind": "command",
  "title": "Set temperature",
  "description": "Set the target room temperature in degrees Celsius.",
  "inputSchema": { "type": "object", "required": ["celsius"], "additionalProperties": false,
                   "properties": { "celsius": { "type": "integer", "minimum": 16, "maximum": 30 } } },
  "risk": "write",
  "physical": true,
  "privacy": "internal",
  "callers": ["human", "ai"],
  "guards": { "minIntervalSec": 10, "maxPerHour": 30 },
  "native": { "protocol": "modbus-tcp", "target": "tcp:127.0.0.1:1502:1",
              "request": { "function": 6, "register": 40021, "value": "{{arguments.celsius}}" } }
}
SettingEffect
minimum, maximum35 °C can never reach the board
physical: trueRequests from an AI or another system always need a human's approval
guardsRefuses rapid repeats that would wear the compressor
callersPeople and AI may ask. Other systems may not

The example defines three commands (set temperature, set mode, turn off) and two properties (room temperature, compressor running).

Pitfall found while testing. Without its own turn_off command, "Turn the AC off" matched HEART's standard "stop all autonomous action" operation. That is safe, but it does not do what the person meant, and it blocks later automatic actions. Give every common request its own command with a plain title.


Step 4: Teach the unit its reflexes (Tending rules)#

Rules let the unit spot and explain problems with no network and no AI. They go in rules.json:

RuleWatchesResult
controller-silentBoard not answering 3 times in a minuteAlert: "Control board is not answering"
short-cycling6 compressor starts in an hourAlert, with advice
weak-coolingSupply and return air too close in temperature"Check the filter, then call a technician"
{ "id": "short-cycling", "enabled": true,
  "description": "The compressor is starting too often, which wears it out",
  "when": { "event": "compressor.started", "count": 6, "withinSec": 3600 },
  "then": { "raise": "com.example.hvac.short-cycling", "severity": "warning",
            "summary": "Compressor started 6 times in an hour" } }

Start with alerts only. Add automatic actions later, and only write ones you have listed in core.allowlist. HEART never runs destructive actions without a person.


Step 5: Report events from your firmware (the host feed)#

Your control program tells HEART what happened by writing one JSON object per line to the host socket. HEART replies ok, or error with a reason.

{"event": "compressor.started", "component": "compressor"}
{"vital": "deltaTCelsius", "value": 8.4}
{"state": "cooling"}
{"event": "filter.reminder", "severity": "warning", "summary": "Filter due in 7 days"}

In C, this is a connect() to /run/heart/host.sock and a write(). The socket is readable and writable by the product's own user only.

Event names are dotted lower case. Names starting circulation., security. or valve. are refused, because they are reserved for HEART.


Step 6: Run it#

cd heart-core-rs
cargo build --release -p heartd
./target/release/heartd check examples/ac-controller/heartd.toml     # prints "configuration is valid"

For development, create an issuer key and tokens:

python3 scripts/dev-token.py keygen examples/ac-controller >> examples/ac-controller/heartd.toml
python3 scripts/dev-token.py token examples/ac-controller owner internal human heart.read heart.ask

On the real unit:

  1. Put sockets in /run/heart/. Unix socket paths must be shorter than 108 bytes.
  2. Set bind to the unit's address, and add tls_cert and tls_key. heartd refuses plain HTTP anywhere but loopback.
  3. Install packaging/heartd.service. It caps HEART at 32 MB of memory and 10% of one CPU, so it can never starve the controller.
  4. Reserve the port: net.ipv4.ip_local_reserved_ports = 43278.

Then check the result:

curl -s http://127.0.0.1:43278/heart/v1/pulse                     # anyone: {"status": "healthy", ...}
curl -s -H "Authorization: Bearer $TOKEN" \
     http://127.0.0.1:43278/heart/v1/properties/com.example.hvac.room_temperature   # 24.5 Cel

Step 7: Decide who may do what#

Tokens come from your OAuth 2.0 authorisation server. Each carries a clearance, a kind and scopes.

WhoClearanceKindScopes
Homeowner's appinternalhumanheart.read heart.ask heart.invoke
Installerprivatehumanthe above, plus heart.approve heart.tend heart.admin
Building management systeminternalpeerheart.read heart.send
Public dashboardnone neededSees only public fields

Start the unit in mode = "suggest". HEART then proposes actions and a person approves them. Switch to act once you trust its rules. Anyone with heart.tend can stop all autonomy at once with POST /heart/v1/tending/stop.


Step 8: Plain-language requests#

With no AI model at all, these are the tested results:

RequestResult
"Set the temperature to 22"Understood: set_setpoint, celsius: 22, waiting for approval
"What is the room temperature?"Answered at once: 24.5 Cel
"Turn the AC off"Understood: turn_off, waiting for approval
"It is too hot in here"Not understood. It needs a model, and HEART does not guess

Anything that changes the unit always waits for confirmation in your app, even from plain language, because any interpretation can be wrong.


Step 9: Add AI later, if you want it#

Add a [model] section pointing at your organisation's Reasoning Gateway, with a daily budget. HEART then sends the model only what the Valve allows (llmCeiling = internal), never occupancy or secrets, and records a receipt of every call. "It is too hot in here" can then be understood. The AI still cannot change anything without approval.


Step 10: Many units in one building#

SituationSet-up
Each unit has its own IP addressEach listens on 43278. Nothing to coordinate
Several controllers on one Linux boxRun heartd gateway on that box. Each unit adds an [uplink] section and joins it
Microcontroller-only unitsThey push their Pulse to the building Gateway

The building then answers heart://building-controller/systems with every unit's Card, and heart://building-controller/systems/ac-unit/pulse for one unit.


Step 11: Test before shipping#

TestHow
Files are validheartd check
It detects a dead boardStop the simulator: the alert appears within seconds, and clears when the board returns
Limits holdSend celsius: 35: refused before reaching the board
It survives crashesKill heartd during a write: the action is never repeated on restart
Plain languageWrite 30 requests your customers would actually say, and check that none changes anything without approval

conformance/run.py shows how to automate all of these.


What is not ready yet#

  • Adapters other than Modbus TCP and argument-list processes.
  • A microcontroller-only Core, and its C interface.
  • Signed updates of rules and definitions, and model regression tests. HEART Full requires both, so the reference cannot fully claim Full yet.
  • Testing on real AC hardware.

Timeline#

WhenWhat
Day 1Steps 1 and 2: identity, and the list of values with privacy levels
Week 1Steps 3 to 6 against a simulator: definitions, rules, host feed, running heartd
Week 2Step 7, and the app's approval screen
Week 3Step 11 on a real board, including pulling cables and power
LaterSteps 9 and 10, as the product needs them