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>
11 KiB
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:
- Pre-flight: List patterns to sync, total bytes, confirm
- Hold: Pulsing NFC icon, "Hold phone against implant"
- Transferring: Progress bar, per-pattern status, "DO NOT MOVE"
- Complete/Error: Success with magnet-cycle reminder, or retry
Data Model
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 slotramp→ compute delta from current PWM; if <= 127: 1ramp_wait(1 slot), if > 127: 2 commands (2 slots)wait→EngineCommand::wait(prescale, stepTime)— 1 slot- Auto-appended:
branch(0, 0)for infinite loop, orbranch(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:
- SYNC_START (global settings)
- PATTERN_DATA x N (one per pattern, usually single-chunk)
- 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
- Project scaffold + data model — Expo init, types, serialization, presets
- Pattern Library screen — List, reorder, add/delete, memory bar
- Timeline Editor — Channel timelines, step editor modal, W slider, step budget
- Animated Preview — Reanimated circles, color strip, playback controls
- Binary Serialization — XBLK encoder, CRC-16, SRAM packet framing
- NFC Transfer — react-native-nfc-manager, ISO15693, transfer flow UI
- 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 infinitebranch(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?)