# Controlling Hanover LED signs

The platform drives **Hanover Displays** LED destination signs from the *same* render pipeline that
drives Luminator/Mobitec panels. Nothing about the authoring changes: routes, destinations, LED
templates and announcement sign text are authored once in the Creator, the engine renders them to
pixels once, and a **per-vendor protocol adaptor** frames those pixels for whichever sign is bolted
to the vehicle. A mixed fleet — Luminator front sign, Hanover side sign — runs from one build, one
configuration, one publish.

This page is the integration guide: what the engine emits for Hanover signs, on which MQTT topics,
how the wire format works, how to hook up the physical sign, and how the implementation was
verified. The primary references are the vendor specifications in [`Hanover/`](Hanover/) (HCPS
protocol manual, *Guide to SuperX*, the Hilde format note) — every framing rule below is tested
byte-exact against the worked examples in those documents.

---

## 1. One render pass, two vendors

```mermaid
flowchart LR
  subgraph Authoring
    CR["Creator<br/>LED templates · playlists"]
  end
  subgraph Engine
    REN["Render<br/>one 1-bpp bitmap per sign"]
    FF["Mobitec FF encoder<br/>engine/ff"]
    HAN["Hanover HCPS/SuperX encoder<br/>engine/hanover"]
  end
  subgraph Vehicle
    GW["RS-485 gateway<br/>or Hanover infohub"]
    SIGN["Hanover sign<br/>DIP address 0–F"]
  end
  CR --> REN
  REN --> FF
  REN --> HAN
  FF -- "…/display/ff topics" --> GW
  HAN -- "…/display/hanover topics" --> GW
  GW --> SIGN
```

The Hanover adaptor (`src/engine/hanover/hanoverEncoder.ts`) is deliberately downstream of
everything: fonts, RTL shaping, abbreviations, scrolling decisions, template rules and destination
lists have all already happened by the time it runs. It receives finished pixels and returns wire
bytes. That is what makes the output **pixel-identical across vendors** — and it is asserted, not
assumed: a pipeline test decodes the published Hanover frame and compares it bit-for-bit against
the sign bitmap published for the same announcement.

## 2. What the engine publishes

Hanover output is **on by default** (`DISPLAY_HANOVER_ENABLED`, set `false` to disable) and runs in
parallel with the FF topics — enabling it changes nothing for existing Mobitec installations.
Three paths, mirroring the FF ones:

| Output | Topic (default pattern) | Content |
|---|---|---|
| **Interior sign** | `{tenant}/{vehicleId}/pis/0/display/hanover` + `…/hanover/{address}` | The announcement sign text as one HCPS graphic frame, addressed to the interior sign (default address **4**). If the text is wider than the panel it carries a SuperX scroll (`\sl`, repeat forever) so the *sign hardware* scrolls it. The same frame mirrors on the address subtopic. |
| **Exterior faces** | `{tenant}/{vehicleId}/pis/0/display/exterior/hanover` + `…/hanover/{address}` | Front, side and rear as **three self-addressed frames concatenated** into one payload (defaults: front **0**, side **1**, rear **2**), published retained so a (re)connecting gateway immediately has the current faces. Each face also lands on its own address subtopic, so a per-sign controller subscribes to just its DIP/rotary address. |
| **Template signs** | `{tenant}/{vehicleId}/pis/0/display/signs/hanover` + per-sign `…/display/signs/{slug}/hanover` + per-address `…/display/signs/hanover/{address}` | LED-template (MatrixRenderer) signs whose roster entry declares `protocol: 'hanover'`. The aggregate carries every Hanover-protocol sign concatenated; each sign also gets its own subtopic (`{slug}` = kebab-cased position, deduped with `-{address}`); and each **bus address** gets its own raw-frame topic carrying only that address's frames (signs sharing an address concatenate) — a sign controller subscribes to its DIP/rotary address instead of filtering the aggregate. Signature-deduped — a frame is republished only when its pixels, address or protocol actually change; stale retained subtopics (slug *and* address) are cleared with empty payloads when the roster changes. |

A sign's **address** is the DIP/rotary switch on the Hanover sign itself (one hex digit, `0`–`F`).
The engine's address map is configuration (§6); make it match what is set on the hardware.

## 3. The wire format in one page

Every payload is one or more **HCPS frames**:

```
STX(0x02) · command('0') · address(one ASCII hex digit) · body · ETX(0x03) · CK1 CK2
```

`CK1 CK2` are the two's-complement 8-bit sum of everything after STX through ETX, as two uppercase
ASCII-hex characters. Worked example from the vendor's Hilde note — reproduced byte-exact by the
test suite:

```
[STX]01345<Køge St.<Øster Banevej[ETX]F3
```

The body is either:

- **A SuperX picture message** (what the engine emits for all graphic output):
  `{\mode0[\sl|\sr\repN]{\pic\picwW\pichH HEX}}` — the bitmap packed as a **vertical raster**
  (column-major, `ceil(rows/8)` bands per column, LSB = top row of each band) and hex-encoded.
  A 2×5 bitmap packs to two bytes, e.g. `{\mode0{\pic\picw2\pich5 1506}}`. `\sl\rep0` scrolls
  left forever. A picture over SuperX's **32 767-byte** `\bin` cap makes the encoder throw rather
  than ship a frame the sign would truncate.
- **Hilde text** (`encodeHanoverText`) — plain CP1252 text with `<` line separators, for simple
  text-mode signs and for clearing (an all-spaces body is the documented "blank the sign" frame).
  Unmappable characters degrade to `?`; the engine's graphic path never needs this (it ships
  pixels, so RTL/Arabic/diacritics work exactly as rendered).

## 4. Hooking up the hardware

The engine speaks MQTT; Hanover signs speak RS-485. Two integration patterns:

**A. RS-485 gateway (direct).** A small onboard process subscribes to the `…/hanover` topics and
writes the payload to the serial line (electrical parameters per the sign's manual in
[`Hanover/`](Hanover/)). Concatenated payloads split without any framing metadata: SuperX bodies
are pure printable ASCII — never STX/ETX — so the receiver scans for `ETX`, takes the two checksum
characters after it, and that is one frame:

```ts
for (let start = 0; start < payload.length;) {
  const etx = payload.indexOf(0x03, start);
  const frame = payload.subarray(start, etx + 3);   // ETX + CK1 CK2
  start = etx + 3;
}
```

This split contract is pinned by a test, and `decodeHanover` (exported by the encoder module) is
available to any TypeScript gateway for validation — it parses a frame back to address, body,
bitmap and scroll flags and reports `checksumOk` so a corrupted byte is *flagged*, never silently
displayed.

**B. Hanover infohub (MQTT bridge).** Fleets running Hanover's own infohub/DPI ecosystem publish
the frame wrapped in the infohub JSON envelope instead of raw bytes. The encoder module exports
both halves, matching the vendor SDK exactly:

```
topic:   infohub/dpi/sign/request/{address}/json     (status/response mirror it)
payload: {"requestId":null,"requestData":{"hanover_hcp":{"command":"0","message":"<frame>"}}}
```

## 5. Authoring — nothing new to learn

- **Announcement sign text** (interior) and the **exterior destination faces** need no authoring
  change at all: with Hanover output enabled they are twinned onto the Hanover topics
  automatically.
- **LED-template signs**: set `protocol: 'hanover'` on the sign's roster entry (alongside its
  `address`) and that face publishes HCPS frames instead of FF. Signs without the field default to
  `ff`, so a roster can mix vendors face by face.

## 6. Configuration reference

| Variable | Default | Meaning |
|---|---|---|
| `DISPLAY_HANOVER_ENABLED` | `true` | Master switch for all three Hanover outputs |
| `DISPLAY_HANOVER_ADDR_FRONT` | `0` | Exterior front face → sign DIP address |
| `DISPLAY_HANOVER_ADDR_SIDE` | `1` | Exterior side face → sign DIP address |
| `DISPLAY_HANOVER_ADDR_REAR` | `2` | Exterior rear face → sign DIP address |
| `DISPLAY_HANOVER_ADDR_INTERIOR` | `4` | Interior announcement sign → DIP address |
| `MQTT_DISPLAY_HANOVER_TOPIC_PATTERN` | `{tenant}/{vehicleId}/pis/0/display/hanover` | Interior topic |
| `MQTT_EXTERIOR_DISPLAY_HANOVER_TOPIC_PATTERN` | `{tenant}/{vehicleId}/pis/0/display/exterior/hanover` | Exterior faces topic |
| `MQTT_LED_SIGNS_HANOVER_TOPIC_PATTERN` | `{tenant}/{vehicleId}/pis/0/display/signs/hanover` | Template-sign aggregate topic |

## 7. How this is verified — and what is not, yet

The adaptor ships with **15 dedicated encoder tests** plus integration tests in the pipeline and
template suites:

- **Byte-exact against the vendor docs**: both checksum examples from the Hilde/HCPS note and the
  16×16 raster worked example from *Guide to SuperX* §9 reproduce exactly.
- **Round-trip**: any rendered bitmap encodes → decodes to identical pixels; a flipped byte on the
  wire is detected (`checksumOk: false`), which is the checksum's whole job on a shared RS-485 line.
- **Pipeline parity**: the published interior Hanover frame decodes to the *same pixels* as the
  published sign bitmap, scroll flags mirrored.
- **Protocol split**: a `protocol: 'hanover'` roster sign gets an HCPS frame and no FF, and the
  concatenated exterior payload splits cleanly on ETX (`matrixParity`, `hanoverEncoder` suites).
- **Limits enforced**: the 32 767-byte SuperX cap throws instead of truncating; out-of-range
  addresses are rejected at encode time.

**Honest gap:** everything above proves conformance to the vendor's written specification. The
adaptor has not yet been driven against a physical Hanover sign or a live infohub hub — that bench
test (especially large picture messages through the hub's own RS-485 framing) is the remaining
step before production sign-off, and the first thing to schedule when hardware is available.
