Files
xblink/docs/plans/2026-03-05-led-pattern-editor-app.md
michael 2fade7ad8b Add pattern chaining support to XBLK format
Pattern entry bytes 108-109 now hold next_pattern (0xFF=loop forever,
0-8=chain to index) and loop_count. MCU will poll LP5562 STATUS register
to detect engine completion and load the successor pattern.

Updated XBLK format spec, Rust Pattern struct (serializer + deserializer),
and React Native app design doc (binary format, data model, resolved
open questions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:52:33 -08:00

285 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# xblink LED Pattern Editor — React Native App Design
**Date**: 2026-03-05
**Status**: Draft — to be refined as firmware milestones (M2M6) progress
## Context
The xblink implant needs a companion phone app for creating and transferring LED animation patterns. The hardware constraints are tight: LP5562 has 3 engines x 16 steps each, NTAG5Link has 256-byte SRAM (transfer buffer) and ~1KB usable EEPROM (after NDEF). This design translates those constraints into an intuitive "hybrid timeline" editor — like a step sequencer for light brightness — with animated preview, preset patterns, and NFC transfer.
## Decisions Made
- **UI model**: Hybrid timeline (per-channel, tap to add set/ramp/wait steps)
- **Timing range**: 50ms14s full LP5562 capability
- **Transfer**: NTAG5 SRAM as 256-byte mailbox, EEPROM as persistent storage
- **Storage**: ~1KB EEPROM for patterns (~8 slots), MCU SRAM as runtime cache
- **Magnet toggle**: Tap-to-cycle through patterns (flashlight-style)
- **Preview**: Animated RGBW circles in-app, real-time as you edit
- **Stack**: Expo + react-native-nfc-manager + react-native-dt-theme + react-native-reanimated
## Hardware Constraints Reference
| Constraint | Value | UI Impact |
|-----------|-------|-----------|
| Engines | 3 (16 steps each) | R/G/B get timelines, W is static-only |
| Max user steps/engine | 14 (2 reserved for branch+end) | Step counter per channel |
| Increment max | 127 PWM steps/ramp | Large ramps consume 2 engine slots |
| Prescale | Fast 0.49ms, Slow 15.6ms | Per-step selector in editor |
| Step time | 163 | Per-step, multiplied by prescale |
| SRAM | 256 bytes (64x4B blocks) | Single-packet transfer for most patterns |
| EEPROM (patterns) | ~1024 bytes (blocks 256511) | ~8 patterns at 122B each |
| Pattern size | 112B fixed-size entry (3x34B engines + 6B header + 4B chain/reserved) | Fits in one SRAM packet |
| White channel | Direct I2C PWM only, no engine | Static brightness slider, no timeline |
## App Screens
### 1. Pattern Library (Home)
Ordered list of up to 8 patterns. Order = magnet cycle order.
```
PATTERN LIBRARY [+ Add]
"Tap magnet to cycle"
[drag] 1. Breathing [mini-preview]
R:wave G:wave B:-- W:50%
Duration: 2.4s | 6/14 steps
[drag] 2. Heartbeat [mini-preview]
R:pulse G:-- B:-- W:0%
Duration: 1.0s | 10/14 steps
[drag] 3. (empty slot)
Memory: 244/1024 bytes
[========= Sync to Implant =========]
```
- **Cards**: DTCard per pattern, drag handle for reorder, swipe-left to delete
- **Add button**: "Create New" or "From Preset" bottom sheet. Disabled at 8 patterns
- **Memory bar**: DTProgressBar, warning at 80%, error at 95%
- **Sync button**: Opens NFC transfer flow
- **Tap card**: Opens pattern editor
### 2. Pattern Editor
The core UI. 3 engine timelines (R, G, B) + static W slider + animated preview.
```
[< Back] Breathing [Save]
PREVIEW
(R) (G) (B) (W) (MIXED)
[> Play] [1x] [loop: on]
COMBINED COLOR STRIP
|====gradient showing mixed RGBW over time====|
0ms 2400ms
R 6/14 steps
|[SET 0]|===RAMP UP===|===RAMP DN===|[BR->0]|
G 6/14 steps
|[SET 0]|===RAMP UP===|===RAMP DN===|[BR->0]|
B 0/14 steps
(tap to add steps)
W Static: [=======|===] 128
(no engine — direct PWM only)
```
**Timeline blocks**:
- **Set PWM**: Narrow bar, shows brightness value (0255)
- **Ramp**: Wide bar proportional to duration, sloped fill (up/down)
- **Wait**: Wide bar, flat, hatched fill
- **Branch**: Arrow icon pointing to target step, shows loop count or infinity
**Interactions**:
- Tap empty space → Add Step sheet (Set / Ramp / Wait)
- Tap existing step → Step Editor modal
- Long press → Delete confirmation
- Step counter: DTChip, warning at 12+, error at 14
**Step Editor Modal**:
```
Edit Step: Ramp Up [Delete]
Type: [Set PWM] [Ramp] [Wait]
Target Brightness: [====|===] 200 (0-255)
Prescale: [Fast 0.49ms] [Slow 15.6ms]
Step Time: [--] 32 [++] (1-63)
Duration: 502ms
Note: This ramp uses 2 engine slots (delta > 127)
[Cancel] [Apply]
```
- For ramps, user sets absolute target brightness; app calculates increment + direction
- If delta > 127, show warning and deduct 2 from step budget
- Duration displayed as calculated value, updates live
**White channel**: Single slider. No timeline. Label explains constraint.
**Preview area**:
- 5 circles: R, G, B, W (individual) + MIXED (additive blend)
- Animated via react-native-reanimated (PWM 0255 → opacity 01)
- Play/pause, speed selector (0.25x/0.5x/1x/2x), loop toggle
- Playback cursor synced across all timelines
### 3. Preset Browser
Built-in patterns users can add to their library:
| Preset | Engines Used | Steps/Engine | Duration | Description |
|--------|-------------|-------------|----------|-------------|
| Breathing | 1 (all RGB) | 6 | ~4.0s | Slow ramp 0→255→0, all channels |
| Heartbeat | 1 (R only) | 10 | ~1.0s | Double pulse, red |
| Color Cycle | 3 (R+G+B) | 7 each | ~3.0s | Sequential fade with trigger sync |
| Strobe | 1 (all RGB) | 5 | ~100ms | Fast on/off, 10Hz |
| Candle | 2 (R+G) | 8 each | ~200ms | Warm flicker, pseudo-random ramps |
| Solid Color | 0 (all direct) | 0 | N/A | Static RGBW, color picker UI |
Each preset card shows animated preview circles.
### 4. NFC Transfer Flow
4-phase modal:
1. **Pre-flight**: List patterns to sync, total bytes, confirm
2. **Hold**: Pulsing NFC icon, "Hold phone against implant"
3. **Transferring**: Progress bar, per-pattern status, "DO NOT MOVE"
4. **Complete/Error**: Success with magnet-cycle reminder, or retry
## Data Model
```typescript
interface PatternLibrary {
version: 1;
patterns: Pattern[]; // Ordered, index = magnet cycle position
globalCurrent: number; // LED current 0-255
}
interface Pattern {
id: string;
name: string; // Max 32 chars
presetId: string | null;
engines: [EngineProgram, EngineProgram, EngineProgram];
channelAssignment: { red: EngineRef; green: EngineRef; blue: EngineRef; white: 'direct' };
whiteLevel: number; // 0-255
nextPattern: number; // 0xFF = loop forever, 0-8 = chain to index
loopCount: number; // Cycles before chaining (0 = chain on stop)
}
interface EngineProgram {
steps: EngineStep[]; // Max 14 user steps
}
type EngineStep =
| { type: 'set_pwm'; brightness: number }
| { type: 'ramp'; targetBrightness: number; prescale: 'fast' | 'slow'; stepTime: number }
| { type: 'wait'; prescale: 'fast' | 'slow'; stepTime: number };
type EngineRef = 'engine1' | 'engine2' | 'engine3' | 'direct';
```
**Step-to-LP5562 mapping**:
- `set_pwm``EngineCommand::set_pwm(value)` — 1 slot
- `ramp` → compute delta from current PWM; if <= 127: 1 `ramp_wait` (1 slot), if > 127: 2 commands (2 slots)
- `wait``EngineCommand::wait(prescale, stepTime)` — 1 slot
- Auto-appended: `branch(0, 0)` for infinite loop, or `branch(0, N)` for chained patterns — 1 reserved slot
## SRAM Transfer Protocol
**Packet format** (256 bytes max):
```
Byte Field
0 command (0x01=SYNC_START, 0x02=PATTERN_DATA, 0x03=SYNC_END)
1 sequence number (0-255)
2 flags (bit0=ACK_REQ, bit1=LAST_CHUNK, bit2=ERASE_FIRST)
3 total_patterns
4 pattern_index
5 chunk_index
6 chunk_count
7 reserved
8-9 payload_length (u16 LE)
10-11 crc16 (over header + payload)
12-255 payload (up to 244 bytes)
```
A single 3-engine pattern (122 bytes) fits in one packet. Transfer flow:
1. SYNC_START (global settings)
2. PATTERN_DATA x N (one per pattern, usually single-chunk)
3. SYNC_END (MCU commits to EEPROM, activates pattern 0)
**MCU response** (SRAM byte 0 = 0xFF):
```
0: 0xFF (response marker), 1: echo seq, 2: status (0=OK), 3: active_pattern
```
## Pattern Binary Format
Matches firmware spec `docs/plans/2026-03-05-eeprom-pattern-format.md`:
```
Header (16 bytes, blocks 256-259):
[0-3] Magic "XBLK"
[4] Version 0x01
[5] Pattern count (1-9)
[6] Active pattern index (0-based, wraps at count)
[7] LED current (0-255, 0.1mA/step)
[8] LED mode (0x00=RGBW, 0x01=Mono3)
[9-13] Reserved
[14-15] CRC-16 over bytes 0-13 + all pattern data
Pattern Entry (112 bytes each, fixed-size):
[0] Engine count (0-3)
[1] LED_MAP register byte
[2-5] Direct PWM [B, G, R, W]
[6-7] Engine 1 command count (BE)
[8-39] Engine 1 commands (16x2 bytes BE)
[40-41] Engine 2 command count
[42-73] Engine 2 commands
[74-75] Engine 3 command count
[76-107] Engine 3 commands
[108] Next pattern index (0xFF=loop forever, 0-8=chain to that index)
[109] Loop count (cycles before chaining, 0=chain on engine stop)
[110-111] Reserved
```
Max capacity: 9 patterns in 1024 bytes (upper 1K EEPROM).
## Reusable Code from Sibling Projects
| File | What to reuse |
|------|--------------|
| `dangerous-things-nfc-identifier/src/services/nfc/commands.ts` | `parseApduResponse`, `hexToBytes`, `bytesToHex`, NTAG5 command patterns |
| `dangerous-things-nfc-identifier/src/services/nfc/NFCManager.ts` | NFC session management |
| `dangerous-things-nfc-identifier/src/services/detection/ntag5sensor.ts` | SRAM read/write, energy harvesting control |
| `react-native-dt-theme/src/components/` | DTCard, DTButton, DTChip, DTModal, DTDrawer, DTProgressBar, DTQuantityStepper, DTSwitch, DTTextInput |
| `react-native-dt-theme/src/theme/` | Colors, typography, DTThemeProvider |
## Implementation Phases
1. **Project scaffold + data model** — Expo init, types, serialization, presets
2. **Pattern Library screen** — List, reorder, add/delete, memory bar
3. **Timeline Editor** — Channel timelines, step editor modal, W slider, step budget
4. **Animated Preview** — Reanimated circles, color strip, playback controls
5. **Binary Serialization** — XBLK encoder, CRC-16, SRAM packet framing
6. **NFC Transfer** — react-native-nfc-manager, ISO15693, transfer flow UI
7. **Polish** — Undo/redo, export/import, settings, onboarding
## Resolved Questions
- **EEPROM layout**: Lower 1K (blocks 0-255) = NFC/NDEF, upper 1K (blocks 256-511) = XBLK pattern library. Decided in M5.
- **Magnet cycle index**: Persists in EEPROM header byte [6] (active_pattern_index). Single block write on switch (~5ms).
- **Non-looping patterns**: Supported via pattern chaining (next_pattern field). Engine programs use finite `branch(0, N)` instead of infinite `branch(0, 0)`. MCU polls LP5562 STATUS register to detect engine completion, then loads the next pattern. No INT pin on LP5562 — polling only.
## Open Questions
- Trigger command exposure in UI (v1 hides it; presets use it internally)
- Pattern chaining UI: how to visualize A→B→C chains in the library screen (arrows? numbered sequence?)