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>
This commit is contained in:
@@ -42,13 +42,27 @@ Offset Size Field
|
||||
42 32 Engine 2 commands
|
||||
74 2 Engine 3 command count
|
||||
76 32 Engine 3 commands
|
||||
108 4 Reserved
|
||||
108 1 Next pattern index (0xFF = loop forever, 0-8 = chain to that index)
|
||||
109 1 Loop count (how many full cycles before chaining, 0 = use next_pattern as-is)
|
||||
110 2 Reserved
|
||||
```
|
||||
|
||||
Max capacity: `(1024 - 16) / 112 = 9` patterns.
|
||||
|
||||
The 16-command-per-engine limit is a hardware constraint of the LP5562 (48 bytes engine SRAM). The `branch` command enables infinite looping, so pattern duration is unlimited.
|
||||
|
||||
### Pattern Chaining
|
||||
|
||||
A pattern can specify a successor via `next_pattern_index`. When set to anything other than 0xFF, the pattern's engine programs should use finite-count `branch(0, N)` instead of `branch(0, 0)` (infinite loop). When all engines stop (detected by polling LP5562 STATUS register 0x0C for engine interrupt bits), the MCU loads the next pattern.
|
||||
|
||||
The LP5562 has no interrupt output pin — the MCU must poll STATUS periodically. During the idle loop, a poll every ~500ms is sufficient and costs negligible power vs. the LED current.
|
||||
|
||||
Chain examples:
|
||||
- `next=0xFF`: Loop forever (default, current behavior)
|
||||
- `next=1, loops=0`: Play once, then switch to pattern 1
|
||||
- `next=0, loops=3`: Play 3 full cycles, then switch to pattern 0 (can create A→B→A→B sequences)
|
||||
- Circular chains (A→B→A) create alternating pattern sequences
|
||||
|
||||
## Boot Sequence
|
||||
|
||||
1. Init LP5562 (enable, clock, current)
|
||||
|
||||
284
docs/plans/2026-03-05-led-pattern-editor-app.md
Normal file
284
docs/plans/2026-03-05-led-pattern-editor-app.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# 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?)
|
||||
Reference in New Issue
Block a user