# HEART Signal Tables

**Every signal in a product, declared once, checked by a person**

**Author:** Milind K. Patil, Syncaissa Systems Inc.
**Status:** Working draft 1, November 2026. To become part of Part 8 (Product Fit) of the [HEART Standards Family](HEART-Standards-Family.md).
**Version:** 1.0 (draft)
**Schema:** [`schemas/1.0/signal-table.schema.json`](schemas/1.0/signal-table.schema.json) · **Example:** [`examples/signal-table.json`](examples/signal-table.json)
**Track:** Intermediate: all of it, if your product serves many signals (Modbus, CAN, firmware variables).

---

## In one minute

A product is full of **signals**: temperatures, speeds, currents, switch positions, counters. A HEART that knows them can notice problems early, explain them, and give an AI model the facts it needs.

A **signal table** (`signals.json`) lists every signal the HEART may read, with its unit, range, privacy and whether it may ever be changed. Three rules make it safe:

1. **Declared or invisible.** A signal not in the table is never served or sent anywhere.
2. **Careful by default.** Nothing is writable unless a person says so. A safety signal is never writable.
3. **A person checks it.** A HEART MUST NOT load a table that has not been reviewed.

Nobody has to write the table by hand. The **generator** drafts it from files engineers already have: a Modbus register map, a CAN database, or the firmware's own variables. A person then checks the draft and seals it.

---

## 1. What a signal is

| Field | Meaning | Example |
|---|---|---|
| `name` | The system ID, then a lower-case signal name | `com.example.cool-air.room_temp` |
| `title`, `description` | For people, and for AI models | "Room temperature" |
| `type` | `number`, `integer`, `boolean`, `string` or `enum` | `number` |
| `unit` | The UCUM code, the standard list of units | `Cel` (degrees Celsius) |
| `range` | The possible values. Outside them, the sensor is probably broken. | `{"min": -20, "max": 60}` |
| `source` | Where it comes from: a bus address, or a pin or variable on the chip | Modbus input register 0 (`table: input`, `register: 0`), scale 0.1 |
| `sampleSec`, `historySec` | How often to read it, and how much history to keep | `5`, `3600` |
| `privacy` | `public`, `internal`, `private` or `never-share` (Part 5) | `internal` |
| `writable` | May HEART ever change it? Default: no. | `false` |
| `physical` | Does changing it move, heat, power or switch something? | `true` for a setpoint |
| `safety` | Is it part of a safety function? If so: read-only, always. | `true` for a pressure trip |
| `derive` | Summaries to keep, from the shared statistic list: `value` (latest), `min`, `max`, `mean`, `stddev`, `trend` (units per minute, least squares), `overshoot` (% of the setpoint step), `count` (events in the window) | `["mean", "trend"]` |
| `alerts` | When to raise a fault code by itself | out of range, `sensors.out-of-range` |

A table MAY also carry `$comment`, free text for people such as a licence line. HEART ignores it.

### Where signals can come from

| `protocol` | Fields | Example |
|---|---|---|
| `modbus-tcp`, `modbus-rtu` | `table` (`holding`, `input`, `coil`, `discrete`), `register` (the 0-based protocol address within `table`, as sent on the wire; old reference numbers such as 30001 or 40021 MUST NOT be used), `scale`, `offset` | An air conditioner's control board |
| `can` | `frameId`, `startBit`, `bits`, `byteOrder`, `signed`, `scale`, `offset` | A vehicle, a chiller, a battery |
| `on-chip` | `symbol` (a firmware variable) or `channel` (an ADC channel or pin) | A microcontroller reading its own sensors |
| Any adapter of API Standard 14.3 | Following that adapter's request format | BACnet, OPC UA, I²C |

---

## 2. The rules

1. **Declared or invisible.** A HEART MUST NOT read, serve or send a signal that is not in its table.
2. **Reviewed before use.** A table is usable only when it has a `reviewed` entry (who, and when) and no signal is still marked `draft`.
3. **Writable only on purpose.** `writable` defaults to `false`. Writing a signal passes the approval gate like any action (Part 6), and writing a `physical` signal counts as a physical action.
4. **Safety signals are read-only.** A signal marked `safety` MUST NOT be writable, and the schema rejects one that is. HEART watches safety signals; it never controls them (Part 6, Section 9).
5. **Watching never disturbs control.** A HEART reads signals at low priority, from copies or shadow registers where possible, never from inside a control loop.
6. **Personal signals are private.** A signal about a person, such as occupancy, location, voice or camera, MUST be `private` or `never-share`.
7. **Budgets are fixed.** Sampling and history have fixed limits, so watching never fills the device's memory or wears out its flash.

---

## 3. From what you have to a sealed table

```
register map (CSV) ─┐
CAN database (.dbc) ├──► draft ──► a person checks ──► seal ──► use
firmware header (.h)┘    (all signals marked draft,           (reviewed: who, when)
                          nothing writable)
```

The generator, `heart-templates/tools/heart_signals.py`, has four steps:

| Step | Command | What it does |
|---|---|---|
| **Draft** | `heart_signals.py draft --system com.acme.chiller --modbus regs.csv --dbc bus.dbc --header signals.h > signals.json` | Turns every register, CAN signal and annotated variable into a draft signal. Converts units to UCUM. |
| **Check** | `heart_signals.py check signals.json` | Lists errors, and every decision still to make |
| **Seal** | `heart_signals.py seal signals.json --by "A. Engineer"` | Marks the table reviewed. Refused while anything is a draft or wrong. |
| **Export** | `heart_signals.py export signals.json --out custom/` | Writes property definitions that `heartd` loads (Modbus signals, today) |
| **Export as C** | `heart_signals.py export signals.json --out main/heart_signals.h` | Writes every signal as a C table for a device, so the chip needs no JSON parser. Used by the `esp32-full` template. |

**What the generator does on its own, so the reviewer doesn't have to start from nothing:**

- Every signal is a **draft**, and **nothing is writable**, even if the register map says it can be written. The reviewer is told, and decides.
- Names that suggest a **person** (occupancy, location, voice) are made **private**.
- Names that suggest a **safety function** (trip, interlock, emergency stop) are marked **safety**, so they can never be made writable.
- Names that suggest an **actuator** (motor, valve, setpoint) get a note: "if made writable, also set physical".
- A missing **range** gets a note, because without one a broken sensor cannot be noticed.

### Annotating firmware variables

The generator reads simple C declarations, with optional hints after `// heart:`

```c
volatile float room_temp_c;       // heart: unit=Cel range=-40..85 title="Room temperature"
volatile uint16_t fan_rpm;        // heart: unit=rpm range=0..3000
volatile float fan_speed_target;  // heart: unit=% range=0..100 writable
```

`writable` here is only a suggestion to the reviewer. It never makes a signal writable by itself.

---

## 4. How a signal table fits the rest of HEART

| HEART part | How signals appear |
|---|---|
| **Part 3 (Information Model)** | Each signal is a **property**: readable at `/heart/v1/properties/{name}`, with its unit, quality and time |
| **Part 3, fault codes** | `alerts` raise standard codes such as `sensors.out-of-range` and `sensors.stuck` |
| **Part 2 (Autonomy Levels)** | Signals give the evidence for "why" at A2, and the trends a Playbook watches at A4 |
| **Part 5 (Privacy)** | Each signal's `privacy` decides who may read it, and whether it may ever reach an AI model |
| **Part 6 (Human Control)** | Writing a signal is an action, and physical writes need a person's proof of presence |
| **Templates** | The Full starter templates (first: `esp32-full`) use the sealed table, exported as C, and serve each signal as a property |

---

## 5. Open items

| Item | Why it matters |
|---|---|
| **`physical` on property definitions** | The custom definition schema does not yet list `physical` for properties, and the reference implementation does not yet gate a physical property write as a physical action. Both must be fixed before writable physical signals are used. |
| **Export for CAN and on-chip sources** | Export writes Modbus definitions today. CAN definitions need the CAN adapter's request format to be published. |
| **More source formats** | OPC UA node sets, BACnet object lists, EtherCAT ESI files and ELF symbol tables would cover most other products. |
| **Derived signals on small chips** | `mean`, `trend` and `stddev` need fixed-memory formulas for microcontrollers. |
