# Guide 3: A HEART-Embedded birdhouse camera

**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/`](../birdhouseSimulator/README.md), 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 firmware | Goes to HEART |
|---|---|
| Camera driver, motion trigger, taking and storing pictures | Knowing whether pictures are actually being taken |
| Sending pictures to the owner's app | Watching 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 management | Telling the owner, even with no internet (A1) |
| Anything that protects the birds | Answering "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

| Part | Choice | Why |
|---|---|---|
| Board | An **ESP32-S3 camera board** with PSRAM and spare ADC pins | The 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. |
| Camera | OV2640 or OV5640, driven by the Espressif camera component | Your firmware, not HEART |
| Motion | A PIR sensor on a GPIO, or the camera's own motion detection | Your firmware |
| Light | An infrared LED (birds do not see it well) | Your firmware sets it; HEART only watches it |
| Power | Mains through the pole, or a battery with a solar panel | See **Power** below: this decides a lot |
| Storage | microSD card | Pictures wait here if Wi-Fi is down |
| Button | A push button on the outside of the box, or at the foot of the pole | Approving physical changes, and Stop (Step 6) |
| Temperature | A sensor inside the box | Too 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:

```c
#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.

| Value | Source | Privacy | Why |
|---|---|---|---|
| Healthy or not | Derived by HEART | `public` | Anyone you share the page with may see it works |
| Battery level | Fuel gauge or ADC | `public` | Harmless; anyone on the local network may read it |
| Temperature in the box | Sensor | `internal` | Owner only |
| Free space on the SD card | Firmware | `internal` | |
| Wi-Fi signal strength | Firmware | `internal` | |
| Pictures taken today | Firmware | `internal` | |
| Seconds since the last picture | Firmware | `private` | A visit timeline can show when the garden is busy, and when people are about |
| Infrared light level | Firmware | `internal` | |
| Zoom | Firmware | `internal` | The only setting the zoom Playbooks may change (Step 11) |
| Nest stage, eggs seen and the vision's confidence | On-chip vision | `internal` | Owner only. Counts and a confidence, never a picture |
| Birds in view now | On-chip vision | `private` | Like the visit timeline, it shows when the garden is busy |
| Location of the box | Not stored on the camera | none | The camera does not need it. Keep it out of the device altogether: nests can be disturbed, and it gives away the house |
| Pictures themselves | Camera | not a signal | Never 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:

```sh
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:

```json
{"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`:

```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 happens | Fault code | Raised by |
|---|---|---|
| Camera dead, or motion with no picture | `system.main-function-lost` | Main-job check (Step 4) |
| Battery below 20 % | `power.battery-low` | Signal table range |
| Box too hot (in summer sun) | `performance.too-hot` | Signal table range |
| Card almost full | `storage.nearly-full` | Signal table range |
| Card full: new pictures are lost | `storage.full` | A second check on the card (the simulator has it; the starter's table gives one code per range) |
| Wi-Fi link lost | `connectivity.link-lost` | The Wi-Fi driver (the simulator has it) |
| Camera silent for three Pulse intervals | `system.flatline` | HEART 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:

| Option | What 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:

```sh
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 claim | You 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`](../birdhouseSimulator/README.md), 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

```sh
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](../birdhouseSimulator/README.md).

### 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:

| Playbook | When (held for 10 s) | Then |
|---|---|---|
| `zoom-out-for-a-crowd` | 3 or more birds in view | Zoom out one step |
| `zoom-in-on-the-eggs` | Eggs seen with confidence 0.8 or more, and at most one bird in view | Zoom 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

| Change | Physical? | How it runs |
|---|---|---|
| The zoom, by the owner | No | At once, with the owner key, inside 1.0 to 4.0 (outside: `422 out-of-range`) |
| The zoom, by a Playbook | No | At once if the allowlist envelope allows it; otherwise refused and recorded |
| The IR light | **Yes** | Waits 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

| Part | Status |
|---|---|
| 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 app | Yours to write; not part of HEART |
| Low-power sleep, warning thresholds | Not in the starter yet |
| HEART Connected and A2 | Needs a HEART Gateway in front of the camera |
| The whole design, simulated | `birdhouseSimulator/`, with an end-to-end test against a local HEART Hub site |
| Zoom Playbooks with an allowlist and envelope | Demonstrated in the simulator; on a real camera they need HEART Full (the Rust Core's gate), not yet in the ESP32 starter |
