diff --git a/docs/plans/2026-03-05-eeprom-pattern-format.md b/docs/plans/2026-03-05-eeprom-pattern-format.md index 068446c..d6d34bb 100644 --- a/docs/plans/2026-03-05-eeprom-pattern-format.md +++ b/docs/plans/2026-03-05-eeprom-pattern-format.md @@ -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) diff --git a/docs/plans/2026-03-05-led-pattern-editor-app.md b/docs/plans/2026-03-05-led-pattern-editor-app.md new file mode 100644 index 0000000..1878fae --- /dev/null +++ b/docs/plans/2026-03-05-led-pattern-editor-app.md @@ -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?) diff --git a/src/pattern/mod.rs b/src/pattern/mod.rs index 8cc45af..27afad5 100644 --- a/src/pattern/mod.rs +++ b/src/pattern/mod.rs @@ -28,6 +28,10 @@ pub struct Pattern { pub map_g: LedMapping, pub map_r: LedMapping, pub map_w: LedMapping, + /// Next pattern index after this one completes (0xFF = loop forever). + pub next_pattern: u8, + /// Number of full cycles before chaining (0 = chain immediately on engine stop). + pub loop_count: u8, } // --------------------------------------------------------------------------- @@ -157,6 +161,8 @@ pub fn rgb_cycle(mode: LedMode) -> Pattern { map_g: LedMapping::Engine2, map_r: LedMapping::Engine3, map_w: LedMapping::I2c, + next_pattern: 0xFF, + loop_count: 0, }, LedMode::Mono3 => Pattern { engine1: Some(rgb_cycle_engine1()), @@ -167,6 +173,8 @@ pub fn rgb_cycle(mode: LedMode) -> Pattern { map_g: LedMapping::Engine2, map_r: LedMapping::Engine3, map_w: LedMapping::I2c, + next_pattern: 0xFF, + loop_count: 0, }, } } @@ -264,6 +272,8 @@ pub fn color_wash(mode: LedMode) -> Pattern { map_g: LedMapping::Engine2, map_r: LedMapping::Engine3, map_w: LedMapping::I2c, + next_pattern: 0xFF, + loop_count: 0, }, LedMode::Mono3 => Pattern { engine1: Some(color_wash_engine1()), @@ -273,6 +283,8 @@ pub fn color_wash(mode: LedMode) -> Pattern { map_g: LedMapping::Engine2, map_r: LedMapping::Engine3, map_w: LedMapping::I2c, + next_pattern: 0xFF, + loop_count: 0, }, } } @@ -291,6 +303,8 @@ fn single_engine_pattern(prog: EngineProgram, mode: LedMode) -> Pattern { map_g: LedMapping::Engine1, map_r: LedMapping::Engine1, map_w: LedMapping::I2c, + next_pattern: 0xFF, + loop_count: 0, } } @@ -425,6 +439,8 @@ pub fn parse_pattern_entry(buf: &[u8; PATTERN_ENTRY_SIZE]) -> Option { map_g, map_r, map_w, + next_pattern: buf[108], + loop_count: buf[109], }) } @@ -498,6 +514,10 @@ pub fn serialize_pattern_entry(p: &Pattern, buf: &mut [u8; PATTERN_ENTRY_SIZE]) serialize_engine(&p.engine1, &mut buf[6..40]); serialize_engine(&p.engine2, &mut buf[40..74]); serialize_engine(&p.engine3, &mut buf[74..108]); + + // Chaining + buf[108] = p.next_pattern; + buf[109] = p.loop_count; } /// Serialize an optional engine program into a 34-byte section.