Guide 1: A HEART-Embedded air-conditioner controller#
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 hardware | Goes to HEART |
|---|---|
| Thermostat control loop | Watching temperatures, run times and faults |
| Compressor anti-short-cycle timer | Spotting short cycling and explaining it |
| High and low pressure trips | Raising alerts and escalating to the installer |
| Freeze protection, electrical protection | Answering "how is the unit?" in one standard format |
| Anything a safety standard covers | Changing 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 controller | What to do |
|---|---|
| Linux board, such as a Raspberry Pi Compute Module or i.MX | Run heartd beside your control program. Recommended. |
| Microcontroller only | Pair 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 building | Each 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": []
}
| Field | Why |
|---|---|
level: full | You want it to act on requests, and plain-language serving (core) needs Full |
modules | control lets a person stop all autonomy; properties and custom expose your values and commands |
shares | Anyone 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.
| Value | Source | Privacy | Why |
|---|---|---|---|
| Healthy or not | Derived by HEART | public | Anyone may know if it works |
| Room temperature | Register 40001 | internal | Owner and installer |
| Compressor running | Register 40011 | internal | |
| Setpoint and mode | Registers 40021, 40022 | internal | |
| Occupancy, daily schedule | Your firmware | private | Reveals when people are home |
| Wi-Fi password, installer PIN | Your firmware | never-share | Never 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}}" } }
}
| Setting | Effect |
|---|---|
minimum, maximum | 35 °C can never reach the board |
physical: true | Requests from an AI or another system always need a human's approval |
guards | Refuses rapid repeats that would wear the compressor |
callers | People 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_offcommand, "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:
| Rule | Watches | Result |
|---|---|---|
controller-silent | Board not answering 3 times in a minute | Alert: "Control board is not answering" |
short-cycling | 6 compressor starts in an hour | Alert, with advice |
weak-cooling | Supply 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:
- Put sockets in
/run/heart/. Unix socket paths must be shorter than 108 bytes. - Set
bindto the unit's address, and addtls_certandtls_key.heartdrefuses plain HTTP anywhere but loopback. - Install
packaging/heartd.service. It caps HEART at 32 MB of memory and 10% of one CPU, so it can never starve the controller. - 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.
| Who | Clearance | Kind | Scopes |
|---|---|---|---|
| Homeowner's app | internal | human | heart.read heart.ask heart.invoke |
| Installer | private | human | the above, plus heart.approve heart.tend heart.admin |
| Building management system | internal | peer | heart.read heart.send |
| Public dashboard | none needed | Sees 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:
| Request | Result |
|---|---|
| "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#
| Situation | Set-up |
|---|---|
| Each unit has its own IP address | Each listens on 43278. Nothing to coordinate |
| Several controllers on one Linux box | Run heartd gateway on that box. Each unit adds an [uplink] section and joins it |
| Microcontroller-only units | They 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#
| Test | How |
|---|---|
| Files are valid | heartd check |
| It detects a dead board | Stop the simulator: the alert appears within seconds, and clears when the board returns |
| Limits hold | Send celsius: 35: refused before reaching the board |
| It survives crashes | Kill heartd during a write: the action is never repeated on restart |
| Plain language | Write 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#
| When | What |
|---|---|
| Day 1 | Steps 1 and 2: identity, and the list of values with privacy levels |
| Week 1 | Steps 3 to 6 against a simulator: definitions, rules, host feed, running heartd |
| Week 2 | Step 7, and the app's approval screen |
| Week 3 | Step 11 on a real board, including pulling cables and power |
| Later | Steps 9 and 10, as the product needs them |