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>
285 lines
11 KiB
Markdown
285 lines
11 KiB
Markdown
# xblink LED Pattern Editor — React Native App Design
|
||
|
||
**Date**: 2026-03-05
|
||
**Status**: Draft — to be refined as firmware milestones (M2–M6) 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**: 50ms–14s 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 | 1–63 | Per-step, multiplied by prescale |
|
||
| SRAM | 256 bytes (64x4B blocks) | Single-packet transfer for most patterns |
|
||
| EEPROM (patterns) | ~1024 bytes (blocks 256–511) | ~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 (0–255)
|
||
- **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 0–255 → opacity 0–1)
|
||
- 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?)
|