HEART Embedded Systems logoHEART

Guide 3: A HEART-Embedded birdhouse camera#

Reading tracks3 Advanced
Advanced (Stop 3A): adopted Playbooks inside an envelope. Which track am I?

A case study. A maker is building a birdhouse camera: a small camera inside a nest box on a pole in a garden, which takes pictures when a bird comes in and sends them to the owner's phone. They want it to be a HEART-embedded system: it should know when it is unwell, tell its owner, and never leak anything it should not.

This guide takes the camera from day 1 to a product that can claim HEART Basic, Autonomy Level A1, and shows what it would take to go further. It builds on the ESP32 Full starter (heart-templates/templates/esp32-full, release v1.2611.2).

A working demo comes with it: birdhouseSimulator/, one Python file that simulates a garden, the birds, the camera and its HEART, reports to HEART Hub, and shows two zoom Playbooks at work (Step 11).

Status. This is a design case study, not a built camera. The starter's HEART code is tested on Linux, but its ESP32 port has not yet run on a chip, and nobody has yet built this camera. The simulator is a demonstration on a computer: its garden, birds, camera and on-chip vision are all simulated. Each step marks what works today and what you would have to add.


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

HEART watches, explains, advises and acts with permission. It does not take the pictures or run the box.

Stays in your firmwareGoes to HEART
Camera driver, motion trigger, taking and storing picturesKnowing whether pictures are actually being taken
Sending pictures to the owner's appWatching the battery, storage, temperature and Wi-Fi
Infrared light and heater limits (bird welfare)Raising an alert with a standard fault code
Deep sleep and power managementTelling the owner, even with no internet (A1)
Anything that protects the birdsAnswering "how is the camera?" in one standard format

The rule is the same as for any product: if HEART crashes or is removed, the camera must keep working and the birds must stay safe.

Pictures never pass through HEART. They are the product's data, not signals. HEART reports that pictures are being taken, never what is in them.


Day 1: Choose the hardware#

PartChoiceWhy
BoardAn ESP32-S3 camera board with PSRAM and spare ADC pinsThe starter reads analogue values on ADC1. On the popular AI-Thinker ESP32-CAM, the camera and SD card use most free pins, and ADC2 cannot be read while Wi-Fi is on. Check your board's pin map before you choose.
CameraOV2640 or OV5640, driven by the Espressif camera componentYour firmware, not HEART
MotionA PIR sensor on a GPIO, or the camera's own motion detectionYour firmware
LightAn infrared LED (birds do not see it well)Your firmware sets it; HEART only watches it
PowerMains through the pole, or a battery with a solar panelSee Power below: this decides a lot
StoragemicroSD cardPictures wait here if Wi-Fi is down
ButtonA push button on the outside of the box, or at the foot of the poleApproving physical changes, and Stop (Step 6)
TemperatureA sensor inside the boxToo hot or too cold is bad for chicks and for the camera

Power. The starter keeps Wi-Fi and its HTTPS server running all the time. That suits a camera with mains power. A battery camera that sleeps between visits will look "silent" to HEART Hub and raise a Flatline (Step 8). Today the starter has no low-power mode, so either power the camera from the mains, or use a battery and solar panel big enough to stay awake, or set a long Pulse interval and accept the gaps. A sleeping HEART is a gap in the starter, listed at the end.


Step 1: Give the camera an identity#

In main/port_esp32.c, change the lines marked CHANGE. The system ID is a reverse-DNS name under a domain you own:

#define SYSTEM_ID "com.example.birdhouse"   /* CHANGE, with signals.json */
#define NAME      "Birdhouse Camera"        /* CHANGE */
#define VERSION   "0.1.0"                   /* CHANGE */

The Card the camera serves at /.well-known/heart-card.json then says what it is, at HEART level Basic, with its template ID heart/embeddedstandard/esp32-full/v1.2611.2.


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

Decide every value's privacy level before writing any code. A garden camera can reveal more than it seems: when people are home, where the house is, and sometimes the people themselves.

ValueSourcePrivacyWhy
Healthy or notDerived by HEARTpublicAnyone you share the page with may see it works
Battery levelFuel gauge or ADCpublicHarmless; anyone on the local network may read it
Temperature in the boxSensorinternalOwner only
Free space on the SD cardFirmwareinternal
Wi-Fi signal strengthFirmwareinternal
Pictures taken todayFirmwareinternal
Seconds since the last pictureFirmwareprivateA visit timeline can show when the garden is busy, and when people are about
Infrared light levelFirmwareinternal
ZoomFirmwareinternalThe only setting the zoom Playbooks may change (Step 11)
Nest stage, eggs seen and the vision's confidenceOn-chip visioninternalOwner only. Counts and a confidence, never a picture
Birds in view nowOn-chip visionprivateLike the visit timeline, it shows when the garden is busy
Location of the boxNot stored on the cameranoneThe camera does not need it. Keep it out of the device altogether: nests can be disturbed, and it gives away the house
Pictures themselvesCameranot a signalNever seen by HEART at all

Step 3: Write the signal table#

Every signal is declared once, in signals.json, and checked by a person before the device will start. Draft it, decide every open question, then seal and export it:

python3 heart_signals.py draft --system com.example.birdhouse --header firmware_signals.h > signals.json
python3 heart_signals.py check signals.json
python3 heart_signals.py seal signals.json --by "Your Name"
python3 heart_signals.py export signals.json --out main/heart_signals.h

The important lines of the table:

{"name": "com.example.birdhouse.battery_pct", "title": "Battery", "type": "number", "unit": "%",
 "range": {"min": 20, "max": 100}, "source": {"protocol": "on-chip", "symbol": "battery_pct"},
 "privacy": "public",
 "alerts": [{"when": "outside-range", "code": "power.battery-low", "severity": "warning"}]},

{"name": "com.example.birdhouse.box_temp", "title": "Temperature in the box", "type": "number", "unit": "Cel",
 "range": {"min": -10, "max": 40}, "source": {"protocol": "on-chip", "channel": 3, "scale": 0.0244, "offset": -20},
 "privacy": "internal",
 "alerts": [{"when": "outside-range", "code": "performance.too-hot", "severity": "warning"}]},

{"name": "com.example.birdhouse.sd_free_pct", "title": "Free space on the card", "type": "number", "unit": "%",
 "range": {"min": 10, "max": 100}, "source": {"protocol": "on-chip", "symbol": "sd_free_pct"},
 "privacy": "internal",
 "alerts": [{"when": "outside-range", "code": "storage.nearly-full", "severity": "warning"}]},

{"name": "com.example.birdhouse.ir_level", "title": "Infrared light", "type": "number", "unit": "%",
 "range": {"min": 0, "max": 60}, "source": {"protocol": "on-chip", "symbol": "ir_level"},
 "privacy": "internal", "writable": true, "physical": true}

Four choices to notice:

  • The range is the healthy range. The starter raises an alert when a value leaves its range. For the battery and the card, declare the range you consider healthy (battery above 20 %), so leaving it raises the right fault code. A separate warning threshold, apart from the range, is not in the starter yet.
  • The infrared light is physical. Light shining into a nest box affects living birds, so a change to it is treated like moving a motor: a person must be present to approve it (Step 6). Its range stops at 60 %, the most your firmware will ever allow.
  • The location is not in the table at all. The best way to keep a secret on a device is not to put it there. If the owner's app needs the location, it stays in the app.
  • The heater is not in the table as writable. If the box has a heater, it stays under the firmware's own limits. HEART may read its state, but nothing outside the box may turn it on.

Step 4: Say what "doing its main job" means#

The main job of a birdhouse camera is to take pictures when a bird comes in. In port_esp32.c:

/* CHANGE: return 1 while the product does its main job. */
int heart_port_main_job_ok(const char **why) {
    if (!camera_ready)            { *why = "The camera did not start."; return 0; }
    if (motion_without_picture)   { *why = "Motion was seen, but no picture was taken."; return 0; }
    if (sd_write_failed)          { *why = "Pictures cannot be saved to the card."; return 0; }
    return 1;
}

HEART checks this every 30 seconds, with no network and no model. When it fails, HEART raises system.main-function-lost with your reason, tells a person, and clears the alert by itself when the camera recovers.

Do not report "no birds today" as a fault. An empty box is not a broken camera.


Step 5: The faults it can recognise#

Every alert carries a fault code from Part 3, so any app, monitor site or installer reads it the same way.

What happensFault codeRaised by
Camera dead, or motion with no picturesystem.main-function-lostMain-job check (Step 4)
Battery below 20 %power.battery-lowSignal table range
Box too hot (in summer sun)performance.too-hotSignal table range
Card almost fullstorage.nearly-fullSignal table range
Card full: new pictures are loststorage.fullA second check on the card (the simulator has it; the starter's table gives one code per range)
Wi-Fi link lostconnectivity.link-lostThe Wi-Fi driver (the simulator has it)
Camera silent for three Pulse intervalssystem.flatlineHEART Hub (Step 8)

To tell a person (Autonomy Level A1 needs an escalation route), change heart_port_escalate(). With no internet, the simplest route is a blinking pattern on an LED outside the box; with internet, HEART Hub emails the owner (Step 8).


Step 6: Commands, approvals and Stop#

The owner can read signals and change non-physical settings with the owner key, shown once on the console at first start. A request to change the infrared light, which is physical, becomes an Action that waits: the device blinks and shows a 6-character code, and the owner checks the code and presses the button. An approval sent over the network is refused, so the key alone, or an AI agent holding it, can never shine a light into the nest.

The box is on a pole, so think about where the button goes:

OptionWhat it means
Button at the foot of the pole (recommended)A person walks to the camera to approve a physical change. Safe, and rare in practice.
Approve on HEART Hub (HEART_MONITOR_APPROVALS)The owner approves with an emailed code. The site signs the decision, and the device checks the signature against the site key it pinned. This covers non-physical waiting actions only.
Also physical changes on Monitor (HEART_MONITOR_APPROVALS_PHYSICAL)Convenient, but weaker: whoever controls the site's signing key could approve. Only turn this on if you accept that.

Stop: holding the button for 3 seconds stops all automatic action at once, with no login and no network. Resuming after Stop needs the button too, because it gives authority back.


Step 7: Explanations from a cloud model (optional)#

Set a Reasoning Gateway in menuconfig, and when an alert opens the camera asks a cloud model to explain it, for example why the card filled up so fast. The gateway holds the model's keys, so none are on the chip. Only public and internal values are sent; the visit timeline (private) never is, and no picture ever is. Every request leaves a receipt, within a daily budget. The model's words are shown to the owner as a suggestion and never acted on.


Step 8: Watch it from anywhere with HEART Hub#

Turn on HEART Hub in menuconfig. At first start the console shows a device ID and a claim code. For a product, print both, with a QR code, on a label inside the box lid. The owner registers the camera at https://heartcortex.ai, and its page at heartcortex.ai/device/<ID> shows its health to them alone until they share it.

Only the public Pulse and public alerts are sent, and the site keeps only the Pulse's status and timing and each alert's code, severity and times. The page shows whether the camera is healthy and which alerts are open, such as power.battery-low, but never a picture, a reading, the visit timeline or anything private. If the camera goes silent for three Pulse intervals (a flat battery, a fallen pole), the page shows a Flatline and the owner gets one email.


Step 9: Try it on a computer first#

The same HEART code runs on Linux with simulated parts:

cc -O2 -std=c99 -Wall -Imain -o heart host/port_linux.c main/heart_core.c main/heart_ed25519.c -lm
./heart                                     # http://127.0.0.1:43278
touch fault.flag                            # the main job "fails": the camera stops taking pictures
touch button.press                          # approve a waiting change
touch button.hold                           # Stop
HEART_MONITOR_URL=http://127.0.0.1:8787 ./heart   # report to a local monitor site

Use your own signals.json and check that each fault in Step 5 raises the right code.

Or, before you write any firmware, run the birdhouse simulator (Step 11): the same design as a whole simulated camera, in one Python file.


Step 10: Test it, rate it and claim only what passed#

  1. Run the HEART-Rating client (heart-rating/client/heart_rating.py) against the camera, and the hands-on tests of Part 9: break the camera on purpose, cut the internet, flatten the battery, fill the card.
  2. Publish a Declaration with every result.
  3. Claim HEART Basic, Autonomy Level A1, and nothing more until the tests for more have passed.
To claimYou would need
A2 (each alert says why, with a confidence)HEART Connected: put a HEART Gateway in front of the camera, or a Core with Rhythm events, so causes can point to evidence
A3 (suggests a fix and waits for a yes)HEART Full, with approval and Stop through the standard API
A4 (fixes known problems by itself)Adopted Playbooks, such as "card nearly full: delete the oldest pictures already sent to the app", each on the owner's allowlist for rules. The simulator shows the mechanism with the zoom (Step 11)
A5 (works out fixes for new problems)The ai and evolving modules: a model that proposes a fix, Trials that check it, and an allowlist for ai used only at A5. Never for the IR light, which is physical

Step 11: Try the simulator, and watch the zoom Playbooks#

The simulator, birdhouseSimulator/birdhouseSimulator.py, is this design as one Python file with only the standard library. Everything in it is simulated: the garden, the birds, the nest, the camera, its vision and its pictures, which are only counted and never made.

Run it, register it, watch it#

python3 birdhouseSimulator/birdhouseSimulator.py --scenario all --clock 07:00
  1. It prints a device ID and a claim code. Sign in at https://heartcortex.ai/login with your email, open the register?id=... link it printed, and enter the claim code.
  2. Watch its page at https://heartcortex.ai/device/<ID>, private to you until you share it. It shows the status and the fault codes of open and recent alerts, nothing else.
  3. With --scenario all, it stages each fault in turn, a few minutes apart, and each is then fixed by the owner: the card fills up (storage.nearly-full, then storage.full and system.main-function-lost), a cloudy week flattens the battery (power.battery-low), a heatwave cooks the box (performance.too-hot), Wi-Fi drops (connectivity.link-lost, and no reports meanwhile), and the camera stops answering (system.main-function-lost). It also brings flocks of birds (crowd) and a clutch of eggs (eggs), so the zoom Playbooks fire.
  4. Birds visit on random timers, most at dawn and dusk and none at night; --clock 07:00 starts the simulated day at dawn. Over simulated days a pair builds a nest, lays 1 to 6 eggs, and the chicks hatch and fledge: --speed hurries it.

The local HEART API, every option and the test are in the simulator's README.

The zoom Playbooks#

The zoom is a good first thing to let a HEART change by itself: it is not physical. It changes the picture, nothing in the box, and a wrong zoom costs a worse picture, nothing more. The owner adopted two Playbooks (origin: owner, status: adopted, enabled: true), each a plain rule with no model and no network:

PlaybookWhen (held for 10 s)Then
zoom-out-for-a-crowd3 or more birds in viewZoom out one step
zoom-in-on-the-eggsEggs seen with confidence 0.8 or more, and at most one bird in viewZoom in one step, towards 3.0

Both run as initiator rule on the capability camera.zoom, and the owner's allowlist has one entry for it: initiators rule only, with the envelope zoom 1.0 to 4.0, at most 1.0 per step, at most 12 changes in any 24 hours, at least 300 s apart.

  • Every change is an Action: initiator rule, risk write, physical: false, with the zoom before and after. The camera keeps them, and the owner reads them, with the Playbooks and the allowlist, at /heart/v1/actions, /heart/v1/tending/rules and /heart/v1/tending/autonomy. The console says each one as it happens: "tell a person afterwards".
  • Outside the envelope, it is refused, not clipped. A crowd at zoom 1.0 asks for 0.0: refused, the zoom stays at 1.0, and the Action says why. A change 120 s after the last one: refused, "only 120 s since the last automatic change". Refused Actions are kept with status denied and the reason.
  • Neither Playbook nor allowlist can be changed over the network. Adopting or widening them is a change to authority, which the standard treats as destructive (API Standard 6.3).
  • Stop (touch button.hold, or POST /heart/v1/tending/stop) stops both at once. Resuming gives authority back, so it waits for the button.

What a person may change, and how#

ChangePhysical?How it runs
The zoom, by the ownerNoAt once, with the owner key, inside 1.0 to 4.0 (outside: 422 out-of-range)
The zoom, by a PlaybookNoAt once if the allowlist envelope allows it; otherwise refused and recorded
The IR lightYesWaits for a person at the box: in the simulator, touch button.press in its working directory. An approval over the network with only the key is refused with 403 presence-required
Anything else (Playbooks, the allowlist, pictures, the heater)Refused: 403 capability-not-allowed

The IR light also keeps its guards in the product: never above 60 % (422 out-of-range), and never raised while the box is 35 Cel or hotter (409 guard-failed), even after the button.

What the simulator claims, and what it does not#

The simulator claims HEART Basic, Autonomy Level A1 (its Card says autonomy.level 1): it notices, explains and tells a person. The zoom Playbooks are a demonstration of the A4 mechanism (adopted rules, the allowlist for rule, the envelope, an Action for every change), on a setting that improves pictures rather than on a fault. They are not a claim of A4. To claim A4, a real camera needs HEART Full, with the gate, Stop and approval through the standard API, and the tests of Part 9 passed, for Playbooks that fix known problems. The Rust Core (heart-core-rs) has that gate; the ESP32 starter does not yet.

The simulator's privacy follows Step 2: only status, fault codes and the battery are public; the box temperature, card, Wi-Fi, pictures today, IR light, zoom, nest stage and eggs seen are internal; the birds in view, the seconds since the last picture and the visit timeline (counts and times, never pictures) are private. The location of the box is not stored anywhere.


What the case study shows#

  • HEART fits a hobby-sized product. The whole of it is one signal table, one main-job function, one escalation route and a button.
  • Privacy decides the design. The hardest questions were not technical: what a garden camera reveals about the people near it, and where the nest is. The signal table makes each answer explicit and checked by a person.
  • "Physical" covers living things. A light shining into a nest is a physical action, even though nothing moves. Treating it as one keeps an AI, or a stolen key, from disturbing the birds.
  • Autonomy can start small. The first thing to let a HEART change by itself is a setting where a mistake costs little: the zoom, inside an envelope the owner set, with every change recorded and anything outside the envelope refused.
  • It found gaps in the starter. Three things this product needs are not there yet:
    • a low-power mode, so a sleeping battery camera does not look dead;
    • warning thresholds apart from a signal's range;
    • a convenient approval for a device that people cannot easily reach.

What works today and what you would add#

PartStatus
HEART code (signals, alerts, approvals, Stop, Monitor, receipts)Tested on Linux in the starter
ESP32 port (Wi-Fi, HTTPS, button, LED)Written, not yet run on a chip
Camera, motion, pictures, sending to the appYours to write; not part of HEART
Low-power sleep, warning thresholdsNot in the starter yet
HEART Connected and A2Needs a HEART Gateway in front of the camera
The whole design, simulatedbirdhouseSimulator/, with an end-to-end test against a local HEART Hub site
Zoom Playbooks with an allowlist and envelopeDemonstrated in the simulator; on a real camera they need HEART Full (the Rust Core's gate), not yet in the ESP32 starter