# DreamStack Panel IR — Dynamic UI Streaming to ESP32

Stream DreamStack apps to ESP32-P4 touchscreen panels as a compact **Panel IR** over WebSocket. The on-device LVGL runtime renders the UI locally. Touch is instant. New UIs arrive dynamically — no reflashing.

## Architecture

```
HUB (Pi / laptop)                              ESP32-P4 PANEL (Waveshare 10.1")
──────────────────                              ─────────────────────────────────

DreamStack compiler                             Panel Runtime (~500 lines C)
  ds compile app.ds --target panel                LVGL 9 graphics library
  ↓                                               ↓
ir_emitter.rs → Panel IR (JSON)  ──→ WiFi 6 ──→  Parse IR → create widgets
                                                  Bind signal reactivity
                                                  Handle touch locally (< 5ms)

Signal update: { "t":"sig", "s":{"0":75} } ──→   Update bound labels instantly
Touch event:   { "t":"evt", "n":3, "e":"click" } ←── LVGL callback fires
New screen:    Full IR blob              ──→       Destroy old UI, build new
```

## Why not pixel streaming?

| | Pixel streaming | Panel IR (this spec) |
|---|---|---|
| Touch latency | 30-50ms (network round-trip) | **< 5ms (local)** |
| WiFi disconnect | Screen freezes | **App stays interactive** |
| Bandwidth | 20-50 Kbps continuous | ~100 bytes/event |
| Dynamic UIs | ✅ | ✅ |
| Hub CPU load | Renders for all panels | **Near zero** |
| Offline | ❌ | ✅ (for local signals) |

## Message Protocol

Three message types over WebSocket (JSON):

### 1. `ui` — Full UI tree

Sent when app loads, screen changes, or hub pushes a new app.

```json
{
  "t": "ui",
  "signals": [
    { "id": 0, "v": 72, "type": "int" },
    { "id": 1, "v": true, "type": "bool" },
    { "id": 2, "v": "Kitchen", "type": "str" }
  ],
  "root": {
    "t": "col", "gap": 10, "pad": 20,
    "c": [
      { "t": "lbl", "id": 0, "text": "{2}: {0}°F", "size": 24 },
      { "t": "btn", "id": 1, "text": "Lights",
        "on": { "click": { "op": "toggle", "s": 1 } } },
      { "t": "sld", "id": 2, "min": 60, "max": 90, "bind": 0 }
    ]
  },
  "derived": [
    { "id": 3, "expr": "s0 * 2", "deps": [0] }
  ]
}
```

### 2. `sig` — Signal update (hub → panel)

```json
{ "t": "sig", "s": { "0": 75 } }
```

### 3. `evt` — Event (panel → hub)

```json
{ "t": "evt", "n": 1, "e": "click" }
```

## IR Node Types

| IR type | DreamStack source | LVGL widget | Key props |
|---|---|---|---|
| `col` | `column [...]` | `lv_obj` + `LV_FLEX_FLOW_COLUMN` | gap, pad, bg |
| `row` | `row [...]` | `lv_obj` + `LV_FLEX_FLOW_ROW` | gap, pad |
| `stk` | `stack [...]` | `lv_obj` (absolute pos) | — |
| `lbl` | `text "..."` | `lv_label` | text, color, size |
| `btn` | `button "..." {}` | `lv_btn` + `lv_label` | text, on.click |
| `inp` | `input` | `lv_textarea` | placeholder, bind |
| `sld` | `slider` | `lv_slider` | min, max, bind |
| `img` | `image` | `lv_img` | src |
| `bar` | `progress` | `lv_bar` | min, max, bind |
| `sw`  | `toggle` | `lv_switch` | bind |
| `lst` | `list [...]` | `lv_list` | — |
| `pnl` | `panel [...]` | `lv_obj` (styled card) | title, bg, radius |

## Signal Binding

Text fields use `{N}` for signal interpolation:

```
"text": "{2}: {0}°F"    →    "Kitchen: 72°F"
                              signal 2  signal 0
```

When signal 0 changes to 75, the runtime re-expands the template and calls `lv_label_set_text()`. Only affected labels update.

Sliders and inputs use `"bind": N` for two-way binding. When the user moves a slider, the runtime updates signal N locally AND sends a `sig` message to the hub.

## Event Actions

Simple operations execute locally on the ESP32 (instant). Complex logic forwards to the hub.

| Action | IR | Runs on |
|---|---|---|
| Set value | `{ "op": "set", "s": 0, "v": 5 }` | ESP32 (local) |
| Toggle bool | `{ "op": "toggle", "s": 1 }` | ESP32 (local) |
| Increment | `{ "op": "inc", "s": 0 }` | ESP32 (local) |
| Decrement | `{ "op": "dec", "s": 0 }` | ESP32 (local) |
| Navigate | `{ "op": "nav", "screen": "settings" }` | Hub → sends new IR |
| Remote call | `{ "op": "remote", "name": "fetch_weather" }` | Hub → result as sig |

## Styling

Each node can have optional style props:

```json
{
  "t": "lbl", "id": 0, "text": "Hello",
  "style": {
    "bg": "#1a1a2e",
    "fg": "#e0e0ff",
    "size": 18,
    "radius": 8,
    "pad": 12,
    "align": "center",
    "font": "bold"
  }
}
```

The runtime maps these to `lv_obj_set_style_*()` calls.

## Implementation Plan

### Phase 1: Compiler Backend

**New file:** `compiler/ds-codegen/src/ir_emitter.rs`

```rust
pub fn emit_panel_ir(program: &Program, graph: &SignalGraph) -> String {
    // Walk AST → build IR JSON
    // Map signal names → integer IDs
    // Convert OnHandler → action opcodes
    // Convert string interpolations → {N} references
}
```

**Modify:** `compiler/ds-cli/src/main.rs` — add `--target panel` flag

### Phase 2: ESP32 LVGL Runtime

**New files in** `devices/waveshare-p4-panel/main/`:

| File | Lines | Purpose |
|---|---|---|
| `ds_runtime.h` | ~60 | API: build, destroy, signal_update, event_send |
| `ds_runtime.c` | ~400 | JSON parser (cJSON), widget factory, signal table, text formatter, event dispatch |

Core functions:
```c
void ds_ui_build(const char *ir_json);     // Parse IR, create LVGL tree
void ds_ui_destroy(void);                  // Tear down for screen change
void ds_signal_update(uint16_t id, const char *value);  // Update + refresh
void ds_event_send(uint16_t node_id, const char *event); // Send to hub
```

### Phase 3: Hub Server

**New file:** `engine/ds-stream/src/panel_server.rs`

WebSocket server that:
1. Compiles `.ds` → IR on startup
2. Sends IR to connecting panels
3. Forwards signal diffs bidirectionally
4. Watches `.ds` file → recompile → push new IR (hot reload)

### Phase 4: Pixel fallback (already built)

The existing `ds_codec.c` + `ds_protocol.h` remain as a fallback for cases where pixel streaming IS needed (streaming non-DreamStack content, camera feeds, etc).

## Hardware

| Component | Role | Price |
|---|---|---|
| Waveshare ESP32-P4-WIFI6 10.1" | Panel display + runtime | ~$85 |
| Raspberry Pi 5 (or any Linux box) | Hub: compile + serve IR | ~$60 |
| **Total POC** | | **~$145** |

## Example: Complete Flow

### 1. Write the app
```
// kitchen.ds
let temperature = 72
let lights_on = true

view main {
  text { "Kitchen: {temperature}°F" }
  button "Lights" {
    on click { lights_on = !lights_on }
  }
  slider { min: 60, max: 90, bind: temperature }
}
```

### 2. Compile to IR
```bash
ds compile kitchen.ds --target panel > kitchen.ir.json
```

### 3. Hub serves it
```bash
ds serve --panel kitchen.ds --port 9100
# Panel connects → receives IR → renders UI
# Tap button → lights_on toggles locally
# Move slider → temperature updates locally + syncs to hub
# Hub pushes weather API data → { "t":"sig", "s":{"0": 68} }
# Panel label updates: "Kitchen: 68°F"
```

### 4. Push a new app
```bash
ds serve --panel settings.ds --port 9100
# Hub sends new IR → panel destroys kitchen UI → builds settings UI
# No reflash. Instant.
```
