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

11 KiB
Raw Blame History

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

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_pwmEngineCommand::set_pwm(value) — 1 slot
  • ramp → compute delta from current PWM; if <= 127: 1 ramp_wait (1 slot), if > 127: 2 commands (2 slots)
  • waitEngineCommand::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?)