# Birdhouse camera simulator: a HEART-embedded system you can run and watch

**A demonstration only.** It simulates a garden, the birds, a nest box camera and its HEART. There is no camera and no bird, and no picture is ever made.

One Python file, `birdhouseSimulator.py`, with only the standard library. Run it, register it on [HEART Hub](https://heartcortex.ai/monitor) with the code it prints, and watch its health on your own private page while it lives a simulated life: day and night, birds that visit (most at dawn and dusk, none at night), sometimes several at once, a pair that builds a nest, eggs, chicks that hatch and fledge, a solar battery, sun on the roof, a filling SD card and a Wi-Fi link that drops now and then.

It also shows **one thing a HEART may change by itself**: the camera's zoom, moved by two Playbooks the owner adopted, inside an envelope the owner set. And it shows **one thing it may not**: the infrared light, which shines on living birds, changes only when a person at the box presses the button.

The case study behind it, step by step for a real product, is [`guides/Birdhouse-Camera.md`](../guides/Birdhouse-Camera.md).

**On a real device:** [Birdhouse Camera, for real](https://heartcortex.ai/docs/real-devices/Birdhouse-Camera.md) shows the hardware we assume, where the HEART runs, the code to change, how to build and install it, the factory steps and what the owner sees. For illustration only.


| File | What it is |
|---|---|
| `birdhouseSimulator.py` | The simulator: garden, camera, HEART, zoom Playbooks, local HEART API, HEART Hub reporting |
| `test_e2e.py` | End-to-end test against a monitor site on your own computer |
| `README.md` | This file |

---

## 1. Download and run

You need Python 3.8 or newer. Nothing to install.

```sh
python3 birdhouseSimulator.py --scenario all
```

It starts at once and prints three things:

```
  HEART Hub: register this simulated birdhouse camera
    1. Sign in at https://heartcortex.ai/login with your email (a 6-digit code is sent to you).
    2. Open https://heartcortex.ai/register?id=5c1e9a07d3b2f468
    3. Enter the claim code  P4M7Q-2XRBA-8KTVW-HN3DZ
    4. Watch it at https://heartcortex.ai/device/5c1e9a07d3b2f468  (private to you until you share it)

10:46:12 HEART API on http://127.0.0.1:43280/heart/v1/pulse  (owner key for private views: ...)
10:46:27 07:05 dawn · birds 4 · nest eggs (eggs 2 seen @0.38) · zoom 2.0 · IR 30 % off · battery 19.8 % · box  9.3 Cel ·
         card  0.0 % free · Wi-Fi -63 dBm · 4 pictures today · HEART failing
```

Every 15 seconds it prints one line: the simulated time of day, what the camera sees and measures, and the HEART's status. When something goes wrong, its HEART says so, and when a Playbook moves the zoom, it says that too:

```
10:46:22 HEART error    storage.full: The card is full. No room on the card: new pictures are lost. Fit a new card.
10:46:47 HEART playbook   zoom-in-on-the-eggs: zoom 1.0 -> 2.0. When the camera has seen eggs with confidence 0.8 or
         more, and at most one bird, for 10 s, zoom in one step, towards 3.0.
10:46:53 HEART playbook   zoom-out-for-a-crowd: zoom 2.0 -> 1.0 REFUSED: only 124 s since the last automatic change:
         cooldownSec is 300
10:46:55 HEART resolved   storage.full: the card is full
```

Stop it with Ctrl-C. The device ID, claim code and key are kept in `birdhouse-monitor.json` (readable only by you), so the same simulated camera comes back next time. Delete that file to make a new device.

## 2. Register it and watch it on HEART Hub

1. **Sign in.** Open `https://heartcortex.ai/login` and enter your email address. A 6-digit code arrives; type it in. There are no passwords.
2. **Register.** Open the `register?id=...` link the simulator printed, and enter the **claim code**. The claim code is the proof that you hold the device: it is shown only on the simulator's console, and the site holds only its hash.
3. **Watch.** Your device's page, `https://heartcortex.ai/device/<ID>`, shows its status (healthy, degraded or failing) and its open and recent alerts, each with the plain-language meaning of its fault code. All your devices are at `https://heartcortex.ai/devices`.
4. **Share, if you want.** The page is private until you make a share link or make it public.
5. **Silence.** Stop the simulator, or wait for a Wi-Fi loss: after three missed Pulse intervals (90 seconds) the page shows a **Flatline**, and you get one email. The site checks for silent devices every 5 minutes, so the email can take a few minutes more. When it reports again, you get one more.

Until you register it, the site stores nothing: the simulator's reports are answered "not registered yet" and dropped. After registering, it can take up to five minutes for the first report to arrive, or less as soon as something changes.

**What the site sees.** Only the public Pulse and public alerts, and it keeps only the status and timing and each alert's fault code, severity and times. Never a reading, a bird count, an egg, the zoom, the visit timeline, and never a picture. The zoom Playbooks and their Actions stay on the camera, for the owner.

## 3. The staged faults

With `--scenario`, each fault starts at a set time, lasts a while, and is then fixed by the owner, so you can watch an alert open, escalate and resolve. Times are in simulated seconds: at `--speed 1` they are real seconds. Staged events happen whatever the simulated hour.

| Scenario | Starts | Lasts | What you see |
|---|---|---|---|
| `storage` | 1 min | 5 min | `storage.nearly-full` (warning) at once, then `storage.full` (error) and `system.main-function-lost` (critical, "pictures cannot be saved") after about 2.5 minutes; then the owner fits an empty card |
| `battery` | at once | about 19 min | A cloudy week: no solar charging and the drain sped up 40 times. `power.battery-low` (warning) within a minute or two; then the sun is back and the owner charges the battery |
| `heat` | 7 min | 3 min | A heatwave on the box: `performance.too-hot` (warning) above 40 Cel. The IR light cannot be raised while the box is 35 Cel or more (`409 guard-failed`) |
| `wifi` | 11 min | 4 min | `connectivity.link-lost` (warning). No reports while the link is down; at `--speed 1` the page shows a Flatline and you get an email, then it recovers |
| `camera` | 16 min | 3 min | The camera stops answering: `system.main-function-lost` (critical). The vision reports nothing while it is down |
| `crowd` | 5 min and 13 min | 2.5 min each | A flock of 4 to 6 birds on the box: the Playbook **zoom-out-for-a-crowd** fires (or is refused, if the last automatic change was under 300 s ago) |
| `eggs` | 2.5 min | to the end | The nest jumps to a fresh clutch of 1 to 6 eggs: the Playbook **zoom-in-on-the-eggs** zooms in, one step at a time, towards 3.0 |
| `all` | | | All of the above |

Without `--scenario`, life goes on at its own pace: birds come and go on random timers, a Wi-Fi drop every 6 to 18 hours, cloudy spells every few days. The nest's life takes real days: building 4 to 7 days, one egg a day, incubation 12 to 14 days, chicks 15 to 19 days. Use `--speed` to hurry it, or `--scenario eggs` to jump to the eggs.

## 4. Options

| Option | Default | Meaning |
|---|---|---|
| `--scenario` | none | `storage`, `battery`, `heat`, `camera`, `wifi`, `crowd`, `eggs` (comma-separated) or `all` |
| `--speed` | 1 | Simulated seconds per real second. `--speed 5` runs the staged events in about 4 minutes (too fast for Flatline emails, which need real minutes) |
| `--clock` | now | Start the simulated clock at this local time, `HH:MM`. Birds sleep at night, so `--clock 07:00` gives a busy dawn |
| `--monitor-url` | `https://heartcortex.ai` | Any HEART Hub site, including your own. `http` only for `127.0.0.1` |
| `--no-monitor` | | Report to no site: local only |
| `--state-file` | `birdhouse-monitor.json` | Where the device ID, claim code and key are kept |
| `--port` | 43280 | The local HEART API (falls back to 7685; `0` for none) |
| `--seed` | random | Repeat the same simulated life |
| `--duration` | until Ctrl-C | Stop after this many real seconds |

## 5. The local HEART API

The simulator also serves the HEART API on your own computer, as a real camera would:

```sh
curl http://127.0.0.1:43280/.well-known/heart-card.json         # the Card: what it is
curl http://127.0.0.1:43280/heart/v1/pulse                       # public health
curl http://127.0.0.1:43280/heart/v1/arrhythmias                 # public alerts: codes only
curl http://127.0.0.1:43280/heart/v1/properties                  # public values: battery only
KEY=...   # the owner key it printed
A="Authorization: Bearer $KEY"
curl -H "$A" http://127.0.0.1:43280/heart/v1/properties          # all values, the zoom and IR light included
curl -H "$A" http://127.0.0.1:43280/heart/v1/arrhythmias         # alerts with explanations
curl -H "$A" http://127.0.0.1:43280/heart/v1/visits              # the private visit timeline (counts, never pictures)
curl -H "$A" http://127.0.0.1:43280/heart/v1/tending/rules       # the two adopted Playbooks (heart.rule)
curl -H "$A" http://127.0.0.1:43280/heart/v1/tending/autonomy    # max, current, mode and the allowlist (heart.control)
curl -H "$A" http://127.0.0.1:43280/heart/v1/actions             # every change, by rule or by you (heart.action)
```

**The Playbooks, read only.** Both are `origin: owner`, `status: adopted`, `enabled: true`, and run as initiator `rule` on the capability `camera.zoom`:

| Playbook | When (held for 10 s) | Then |
|---|---|---|
| `zoom-out-for-a-crowd` | 3 or more birds in view | Zoom out one step (1.0) |
| `zoom-in-on-the-eggs` | Eggs seen with confidence 0.8 or more, and at most one bird in view (the derived signal `egg_view`) | Zoom in one step, towards 3.0 |

The allowlist has one entry: target `camera.zoom`, initiators `rule` only, envelope zoom 1.0 to 4.0, `maxStep` 1.0, `maxPerDay` 12, `cooldownSec` 300. Each time a Playbook fires, the HEART makes an **Action** (initiator `rule`, risk `write`, `physical: false`) and checks it against the entry. Inside the envelope it runs (`succeeded`, with the zoom before and after). Outside, it is **refused, not clipped**: the zoom stays where it is, and the Action is kept with status `denied` and the reason in `error`, for example `zoom 0.0 is outside the envelope 1.0 to 4.0` or `only 124 s since the last automatic change`. A rule fires once each time its condition comes to hold, and again only after `cooldownSec` while it keeps holding. Playbooks and the allowlist cannot be changed over the network here (`403 capability-not-allowed`): adopting or widening them is a destructive change to authority (API Standard 6.3).

**Your own changes.** The zoom is not physical, so with the owner key it runs at once, inside its range. The IR light is physical, so it waits for a person:

```sh
curl -H "$A" -X POST -d '{"arguments":{"zoom":2.5}}' http://127.0.0.1:43280/heart/v1/capabilities/camera.zoom/invoke
                                     # 200, succeeded. A zoom outside 1.0 to 4.0: 422 out-of-range
curl -H "$A" -X POST -d '{"arguments":{"level":45}}' http://127.0.0.1:43280/heart/v1/capabilities/camera.ir_level/invoke
                                     # 202, awaiting-approval; the console shows a 6-character code
curl -H "$A" -X POST -d '{"decision":"approve"}' http://127.0.0.1:43280/heart/v1/actions/<ID>/decision
                                     # 403 presence-required: the key alone cannot approve it
touch button.press                   # in the simulator's working directory: a person pressed the button. It runs.
curl -H "$A" -X POST http://127.0.0.1:43280/heart/v1/tending/stop    # Stop: no Playbook acts (or: touch button.hold)
curl -H "$A" -X PUT -d '{"max":4}' http://127.0.0.1:43280/heart/v1/tending/autonomy
                                     # 202: raising the ceiling gives authority back, so it waits for the button too
```

**Autonomy.** The camera promises level 4. A person sets the ceiling `max`; the current level is the lower of the two, and the mode follows from it: 4 is `act` (the Playbooks run), 2 or below is `observe`. Stop sets `max` to 2. `PUT /heart/v1/tending/autonomy` takes a `max` of 0 to 2 (the same as Stop, at once) or 4 (waits for the button); any other value answers `400 invalid-request`. The owner's Pulse shows `autonomy` with `max`, `current`, and `limitedBy: stopped` while `max` is below 4.

An IR level above 60 % (the firmware's own limit) is refused with `422 out-of-range`; raising the IR light while the box is 35 Cel or hotter is refused with `409 guard-failed`, also after the button. A waiting change expires after 10 minutes. Any other change gets `403 capability-not-allowed`.

Every message it serves is a valid HEART message: `test_e2e.py` checks each one against the official schemas.

## 6. How the code is built

```
Garden  ──►  Camera  ──(reads)──►  Heart  ──►  local HEART API      (127.0.0.1)
sun, birds    the product            watches     └►  HEART Hub    (outbound only)
nest life     vision, battery,       Playbooks ──(gate: allowlist + envelope)──► camera.zoom
              card, Wi-Fi, IR        owner ──(button at the box)──► camera.ir_level
```

| Part | In the file | In a real product |
|---|---|---|
| `Garden` | The simulated world: the hour, the sun, birds visiting on random timers, the nest's life over days | The real garden |
| `Camera` | The simulated product: pictures counted on each visit, on-chip vision (birds, eggs, confidence), battery and solar panel, box temperature, SD card, Wi-Fi, IR light, zoom, and the IR interlock | The camera's own firmware. **Pictures never pass through HEART** |
| `Heart.tend` | `RULES` turn the camera's values into alerts with fault codes; `escalate()` tells a person | **The code you add** |
| `Heart.run_playbooks` | The Tending engine: two adopted Playbooks, the allowlist check, an Action for every change | A HEART Full Core (the Rust Core's gate) |
| `Heart.button_press` | The proof of presence: approves the newest waiting Action, then the product's guards run again | The real button on the box |
| `monitor()` | Hello, then reports to HEART Hub; follows the site's `nextSec`; stops for a wrong key; says hello again if forgotten | The same, unchanged |
| `handler()` | The HEART API: Card, Pulse, alerts, properties, visits, Playbooks, control, Actions, invoke, decision, Stop | The same, behind TLS on a network |

The HEART can reach the camera through two setters only, `set_zoom` and `set_ir`, and each sits behind the gate: the allowlist for rules, the button for the IR light. Nothing else in `Camera` can be changed from outside.

## 7. What code to add to make a real product HEART-embedded

For a real birdhouse camera, HEART runs **on the camera itself**: an ESP32-S3 camera board has room for it (see Day 1 of the case study). Pick a starting point, then add four things.

**Starting points**

| Your camera | Start from |
|---|---|
| ESP32 camera board | The ESP32 Full starter `heart-templates/templates/esp32-full`: its signal table, main-job check, button, approvals, Stop and Monitor reporting. It has no Playbooks yet: see below |
| A Raspberry Pi or other Linux camera | The Python template `heart-templates/templates/python`, or this file's `Heart`, `monitor()` and `handler()`; or the Rust Core `heartd` (`heart-core-rs`), which has the full gate, allowlist and Trials, for HEART Full and Playbooks |
| A Node app on a Linux camera | The JavaScript template `heart-templates/templates/javascript-node` |

**The four things you add**

1. **Read the camera.** Replace `Garden` and `Camera` with reads from your firmware: the fuel gauge, the box sensor, the card, the Wi-Fi driver, the vision model's last result. HEART gets counts and confidences, never a picture.

   ```python
   class Readings:
       """CHANGE: fill these from your firmware. Read only, except the two settings behind the gate."""
       def refresh(self):
           f = your_firmware.status()             # your code
           self.battery, self.box_temp, self.sd_free = f.battery_pct, f.box_temp_c, f.sd_free_pct
           self.wifi_down, self.rssi = not f.wifi_up, f.rssi_dbm
           self.camera_ok, self.pictures_today = f.camera_ready, f.pictures_today
           self.birds_seen, self.eggs_seen, self.egg_confidence = f.vision.birds, f.vision.eggs, f.vision.confidence
   ```

2. **Your rules and thresholds.** Edit `RULES` and `EXPLAIN` with your own limits (the battery level that is really low, the box temperature that is really too hot). Keep the standard fault codes (Part 3). Keep `main_job_problem()` about the main job: an empty box is not a broken camera.

3. **Your escalation route.** Change `escalate()` to blink the LED outside the box and notify the owner's app, with no internet needed for the LED.

4. **Your privacy table.** Go through `properties()` and give every value its level (Step 2 of the case study). The visit timeline and the live bird count are `private`; only status, fault codes and the battery are `public`. Keep the location out of the device altogether.

**If you want the zoom Playbooks too**, they need HEART Full: the gate, the allowlist with its envelope, Action records, Stop and approval through the standard API, all tested. The Rust Core (`heart-core-rs`, `crates/heart-core/src/gate.rs`) has them; the ESP32 starter does not yet. Keep the IR light `physical`, so no rule and no key alone can ever change it.

Then keep `monitor()` as it is, store the device ID, claim code and key in the camera's encrypted flash, and print the claim code and a QR code of `https://<your monitor site>/register?id=<ID>` on a label inside the box lid. Serve the API over TLS (or keep it on loopback), run the HEART-Rating client (`heart-rating/client/heart_rating.py`) and Part 9's tests, and claim HEART Basic, Autonomy Level A1.

## 8. Test it

From the project's top folder, with Node 20+ and Python 3 with `pip install --user "jsonschema[format]>=4.18" referencing`:

```sh
python3 birdhouseSimulator/test_e2e.py
```

It starts a HEART Hub site on this computer (port 8801), runs the simulator with every staged event at 5x speed from a simulated 07:00 (local API on port 43301), signs in, registers the device through the site's own form, and checks for about four minutes that: every message is valid (Playbooks, control and Actions included); public views hold only public data; the owner's zoom runs at once and an out-of-range one is refused; an IR change waits, is refused over the network with `403 presence-required`, and runs after `touch button.press`; both zoom Playbooks fired and every automatic change stayed inside the envelope, with refused ones recorded and not clipped; every staged fault reaches the owner's page with its fault code; no reading, count or zoom reaches the site; and the page stays private.

---

Licence: Apache-2.0. Part of HEART, the open standard at [heartcortex.ai](https://heartcortex.ai).
