M5 complete: boot-from-EEPROM with self-provisioning
XBLK binary format for pattern library in NTAG5 upper 1K EEPROM: - 16-byte header (magic, version, pattern count, active index, CRC-16) - Up to 9 × 112-byte fixed-size pattern entries - Deserializer + serializer in src/pattern/mod.rs - MCU self-provisions hardcoded patterns on first boot (no XBLK found) - Subsequent boots load active pattern directly from EEPROM - Python serializer tool for future PCSC-based pattern uploads Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# xblink Project Status
|
# xblink Project Status
|
||||||
|
|
||||||
**Current Milestone**: M5 — Boot-from-EEPROM
|
**Current Milestone**: M6 — SRAM Mailbox
|
||||||
**Last Updated**: 2026-03-05
|
**Last Updated**: 2026-03-05
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -55,13 +55,15 @@
|
|||||||
- [x] Verified on hardware: config check passes, NDEF readable via NFC
|
- [x] Verified on hardware: config check passes, NDEF readable via NFC
|
||||||
- [ ] Register map module (`src/ntag5/registers.rs`) — deferred, constants in mod.rs for now
|
- [ ] Register map module (`src/ntag5/registers.rs`) — deferred, constants in mod.rs for now
|
||||||
|
|
||||||
### M5: Boot-from-EEPROM
|
### M5: Boot-from-EEPROM (COMPLETE)
|
||||||
|
|
||||||
- [ ] Design binary pattern format (informed by M2-M3 experience)
|
- [x] Design XBLK binary pattern format (`docs/plans/2026-03-05-eeprom-pattern-format.md`)
|
||||||
- [ ] Implement pattern deserializer (`src/pattern/mod.rs`)
|
- [x] Implement XBLK deserializer (`src/pattern/mod.rs`: parse_header, parse_pattern_entry)
|
||||||
- [ ] Implement engine program builder (`src/pattern/engine.rs`)
|
- [x] Implement XBLK serializer (`src/pattern/mod.rs`: serialize_pattern_entry, serialize_header, crc16)
|
||||||
- [ ] Update main.rs: boot → read EEPROM → parse → program LP5562 → idle
|
- [x] Update main.rs: boot → read EEPROM → parse → program LP5562 → idle, with fallback
|
||||||
- [ ] Write Python serializer tool for PCSC pattern uploads
|
- [x] MCU self-provisioning: write hardcoded patterns to EEPROM on first boot
|
||||||
|
- [x] Write Python serializer tool (`tools/xblk_serialize.py`) for PCSC pattern uploads
|
||||||
|
- [x] Verified on hardware: self-provisioning writes XBLK, subsequent boots load from EEPROM
|
||||||
|
|
||||||
### M6: SRAM Mailbox
|
### M6: SRAM Mailbox
|
||||||
|
|
||||||
|
|||||||
106
docs/plans/2026-03-05-eeprom-pattern-format.md
Normal file
106
docs/plans/2026-03-05-eeprom-pattern-format.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# EEPROM Pattern Library Format (M5)
|
||||||
|
|
||||||
|
## Memory Layout
|
||||||
|
|
||||||
|
The NTP53x2 has 2048 bytes (512 x 4-byte blocks) of user EEPROM.
|
||||||
|
|
||||||
|
| Region | Blocks | Bytes | Purpose |
|
||||||
|
|--------|--------|-------|---------|
|
||||||
|
| NFC/NDEF | 0-255 | 0-1023 | CC + NDEF data (phone-visible, MCU doesn't touch) |
|
||||||
|
| Pattern library | 256-511 | 1024-2047 | XBLK header + up to 9 patterns |
|
||||||
|
|
||||||
|
I2C base address for the pattern library: `0x0100` (block 256).
|
||||||
|
|
||||||
|
## Header (16 bytes, blocks 256-259)
|
||||||
|
|
||||||
|
```
|
||||||
|
Offset Size Field
|
||||||
|
0 4 Magic: "XBLK" (0x58 0x42 0x4C 0x4B)
|
||||||
|
4 1 Version: 0x01
|
||||||
|
5 1 Pattern count (1-9)
|
||||||
|
6 1 Active pattern index (0-based, wraps at count)
|
||||||
|
7 1 LED current (0-255, 0.1mA/step)
|
||||||
|
8 1 LED mode (0x00 = RGBW, 0x01 = Mono3)
|
||||||
|
9 5 Reserved (0x00)
|
||||||
|
14 2 CRC-16 over bytes 0-13 + all pattern data
|
||||||
|
```
|
||||||
|
|
||||||
|
Current and LED mode are global -- all patterns share them.
|
||||||
|
|
||||||
|
## Pattern Entry (112 bytes, 28 blocks each)
|
||||||
|
|
||||||
|
Fixed-size entries for direct seeking: `offset = 16 + (index * 112)`.
|
||||||
|
|
||||||
|
```
|
||||||
|
Offset Size Field
|
||||||
|
0 1 Engine count (0-3)
|
||||||
|
1 1 LED_MAP register value (raw LP5562 register byte)
|
||||||
|
2 4 Direct PWM [B, G, R, W] for I2C-mapped channels
|
||||||
|
6 2 Engine 1 command count (big-endian, 0 = unused)
|
||||||
|
8 32 Engine 1 commands (up to 16 x 2 bytes BE, pad with 0x0000)
|
||||||
|
40 2 Engine 2 command count
|
||||||
|
42 32 Engine 2 commands
|
||||||
|
74 2 Engine 3 command count
|
||||||
|
76 32 Engine 3 commands
|
||||||
|
108 4 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.
|
||||||
|
|
||||||
|
## Boot Sequence
|
||||||
|
|
||||||
|
1. Init LP5562 (enable, clock, current)
|
||||||
|
2. Read header at `0x0100` (4 blocks)
|
||||||
|
3. If magic = "XBLK" and CRC valid:
|
||||||
|
- Read pattern at `active_pattern_index`
|
||||||
|
- Deserialize into `Pattern` struct
|
||||||
|
- Load into LP5562 engines
|
||||||
|
4. If invalid (no magic, CRC mismatch, I2C error):
|
||||||
|
- Fall back to hardcoded breathe pattern (compiled into firmware)
|
||||||
|
5. Config check via session registers, report via LED blinks (no NDEF write)
|
||||||
|
6. Sleep (future: STANDBY)
|
||||||
|
|
||||||
|
## Pattern Switching (future, hall sensor)
|
||||||
|
|
||||||
|
1. Wake on hall EIC interrupt
|
||||||
|
2. Read header, increment `active_pattern_index` (wrap at `pattern_count`)
|
||||||
|
3. Write back the single block containing the index (1 EEPROM write, ~5ms)
|
||||||
|
4. Read and load the new pattern
|
||||||
|
5. Sleep
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- EEPROM read fails at boot: use hardcoded fallback
|
||||||
|
- CRC mismatch: use hardcoded fallback
|
||||||
|
- Pattern data malformed (engine_count > 3, cmd_count > 16): skip to next, wrap around; if all bad, use fallback
|
||||||
|
- Active index >= pattern count: reset to 0
|
||||||
|
|
||||||
|
## Python Serializer
|
||||||
|
|
||||||
|
`tools/xblk_serialize.py` converts JSON pattern definitions to binary and writes to EEPROM via ntag5sensor/PCSC.
|
||||||
|
|
||||||
|
Input JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"current": 20,
|
||||||
|
"mode": "rgbw",
|
||||||
|
"active": 0,
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "breathe",
|
||||||
|
"led_map": {"b": "engine1", "g": "engine1", "r": "engine1", "w": "direct"},
|
||||||
|
"direct_pwm": [0, 0, 0, 0],
|
||||||
|
"engines": [
|
||||||
|
[18176, 18176, 26368, 26368, 15408, 40960],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Engine commands are raw u16 values. The serializer validates constraints, builds the binary blob, computes CRC-16, and writes to EEPROM blocks 256+ via I2C.
|
||||||
200
src/main.rs
200
src/main.rs
@@ -71,15 +71,15 @@ fn main() -> ! {
|
|||||||
|
|
||||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
||||||
|
|
||||||
// 2 mA per channel (20 × 0.1 mA) — realistic for EH power budget
|
// Default LED current: 2 mA per channel (20 × 0.1 mA)
|
||||||
const LED_CURRENT: u8 = 20;
|
let mut led_current: u8 = 20;
|
||||||
|
|
||||||
// Verify LP5562 is reachable before entering pattern loop
|
// Verify LP5562 is reachable before continuing
|
||||||
let i2c_ok = (|| -> Result<(), led::lp5562::Error<_>> {
|
let i2c_ok = (|| -> Result<(), led::lp5562::Error<_>> {
|
||||||
lp.enable()?;
|
lp.enable()?;
|
||||||
delay.delay_ms(1u32); // >500us startup
|
delay.delay_ms(1u32); // >500us startup
|
||||||
lp.init_direct_control(ClockSource::Internal)?;
|
lp.init_direct_control(ClockSource::Internal)?;
|
||||||
lp.set_all_current(LED_CURRENT, LED_CURRENT, LED_CURRENT, LED_CURRENT)?;
|
lp.set_all_current(led_current, led_current, led_current, led_current)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -89,69 +89,167 @@ fn main() -> ! {
|
|||||||
led.set_low().unwrap();
|
led.set_low().unwrap();
|
||||||
delay.delay_ms(500u32);
|
delay.delay_ms(500u32);
|
||||||
led.set_high().unwrap();
|
led.set_high().unwrap();
|
||||||
delay.delay_ms(1000u32); // 1s gap between status signals
|
delay.delay_ms(1000u32);
|
||||||
|
|
||||||
// LED mode: Rgbw for EVM RGB LED, Mono3 for 3 separate LEDs
|
// --- Try to load pattern library from NTAG5 EEPROM ---
|
||||||
let mode = LedMode::Rgbw;
|
|
||||||
|
|
||||||
let patterns = [
|
|
||||||
pattern::breathe(mode),
|
|
||||||
pattern::heartbeat(mode),
|
|
||||||
pattern::slow_pulse(mode),
|
|
||||||
pattern::rgb_cycle(mode),
|
|
||||||
pattern::color_wash(mode),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Load first pattern — LP5562 engines run autonomously
|
|
||||||
let _ = pattern::load_pattern(&mut lp, &patterns[0], LED_CURRENT, &mut delay);
|
|
||||||
|
|
||||||
// Release I2C from LP5562 (engines keep running) for NTAG5 check
|
|
||||||
let i2c = lp.release();
|
let i2c = lp.release();
|
||||||
let mut ntag = Ntag5Link::new(i2c, ntag5::DEFAULT_ADDRESS);
|
let mut ntag = Ntag5Link::new(i2c, ntag5::DEFAULT_ADDRESS);
|
||||||
|
|
||||||
// NTAG5 config check — read session registers, write NDEF result
|
// Config check (LED blinks only, no NDEF write)
|
||||||
// If NTAG5 isn't connected, this will fail silently (I2C NAK)
|
match ntag.check_config() {
|
||||||
match ntag.check_and_write_ndef(&mut delay) {
|
Ok(result) => {
|
||||||
Ok(true) => {
|
if result.all_ok {
|
||||||
// Config OK — 2 short blinks
|
blink(&mut led, &mut delay, 2, 100);
|
||||||
blink(&mut led, &mut delay, 2, 100);
|
} else {
|
||||||
|
blink(&mut led, &mut delay, 5, 60);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
Err(_) => {
|
||||||
// Config mismatch — 5 fast blinks (warning)
|
|
||||||
blink(&mut led, &mut delay, 5, 60);
|
|
||||||
}
|
|
||||||
Err(ntag5::Error::VerifyFailed) => {
|
|
||||||
// EEPROM write-verify failed — SOS pattern (3 rapid, pause, 3 rapid)
|
|
||||||
blink(&mut led, &mut delay, 3, 40);
|
|
||||||
delay.delay_ms(300u32);
|
|
||||||
blink(&mut led, &mut delay, 3, 40);
|
|
||||||
}
|
|
||||||
Err(ntag5::Error::I2c(_)) => {
|
|
||||||
// NTAG5 not reachable — 1 long blink (not fatal)
|
// NTAG5 not reachable — 1 long blink (not fatal)
|
||||||
led.set_low().unwrap();
|
led.set_low().unwrap();
|
||||||
delay.delay_ms(800u32);
|
delay.delay_ms(800u32);
|
||||||
led.set_high().unwrap();
|
led.set_high().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
delay.delay_ms(500u32);
|
||||||
|
|
||||||
// Reclaim I2C for LP5562
|
// Try reading XBLK library header from upper 1K
|
||||||
let i2c = ntag.release();
|
let mut header_buf = [0u8; pattern::HEADER_SIZE];
|
||||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
let eeprom_ok = ntag.read_memory(pattern::LIBRARY_BASE_BLOCK, &mut header_buf).ok()
|
||||||
|
.and_then(|()| pattern::parse_header(&header_buf));
|
||||||
|
|
||||||
// Pattern cycle loop — LP5562 engines run autonomously, no MCU LED indication
|
// Read active pattern if header is valid
|
||||||
let mut idx = 1; // Already loaded pattern 0 above
|
let eeprom_pattern = eeprom_ok.as_ref().and_then(|hdr| {
|
||||||
loop {
|
led_current = hdr.current;
|
||||||
if pattern::load_pattern(
|
let pat_block = pattern::LIBRARY_BASE_BLOCK
|
||||||
&mut lp, &patterns[idx], LED_CURRENT, &mut delay,
|
+ (pattern::HEADER_SIZE as u16 / 4)
|
||||||
).is_err() {
|
+ (hdr.active_index as u16 * (pattern::PATTERN_ENTRY_SIZE as u16 / 4));
|
||||||
// I2C error loading pattern — fast blink
|
let mut pat_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
||||||
blink(&mut led, &mut delay, 10, 50);
|
ntag.read_memory(pat_block, &mut pat_buf).ok()?;
|
||||||
|
pattern::parse_pattern_entry(&pat_buf)
|
||||||
|
});
|
||||||
|
|
||||||
|
match eeprom_pattern {
|
||||||
|
Some(pat) => {
|
||||||
|
// Reclaim I2C for LP5562
|
||||||
|
let i2c = ntag.release();
|
||||||
|
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
||||||
|
|
||||||
|
// EEPROM pattern loaded — 3 fast blinks
|
||||||
|
blink(&mut led, &mut delay, 3, 80);
|
||||||
|
|
||||||
|
if pattern::load_pattern(&mut lp, &pat, led_current, &mut delay).is_err() {
|
||||||
|
blink(&mut led, &mut delay, 10, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LP5562 runs autonomously — MCU idles (future: STANDBY)
|
||||||
|
loop {
|
||||||
|
delay.delay_ms(60000u32);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
// No XBLK in EEPROM — self-provision hardcoded patterns
|
||||||
|
// 2 long blinks = provisioning
|
||||||
|
blink(&mut led, &mut delay, 2, 300);
|
||||||
|
|
||||||
// MCU idles while LP5562 runs the pattern
|
let mode = LedMode::Rgbw;
|
||||||
delay.delay_ms(15000u32);
|
let patterns = [
|
||||||
|
pattern::breathe(mode),
|
||||||
|
pattern::heartbeat(mode),
|
||||||
|
pattern::slow_pulse(mode),
|
||||||
|
pattern::rgb_cycle(mode),
|
||||||
|
pattern::color_wash(mode),
|
||||||
|
];
|
||||||
|
|
||||||
idx = (idx + 1) % patterns.len();
|
// Serialize all pattern entries and write to EEPROM
|
||||||
|
let pat_base_block = pattern::LIBRARY_BASE_BLOCK
|
||||||
|
+ (pattern::HEADER_SIZE as u16 / 4);
|
||||||
|
let mut all_pat_data = [0u8; pattern::PATTERN_ENTRY_SIZE * 5];
|
||||||
|
let mut provision_ok = true;
|
||||||
|
|
||||||
|
for (i, pat) in patterns.iter().enumerate() {
|
||||||
|
let mut entry_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
||||||
|
pattern::serialize_pattern_entry(pat, &mut entry_buf);
|
||||||
|
|
||||||
|
// Copy into combined buffer for CRC
|
||||||
|
let offset = i * pattern::PATTERN_ENTRY_SIZE;
|
||||||
|
all_pat_data[offset..offset + pattern::PATTERN_ENTRY_SIZE]
|
||||||
|
.copy_from_slice(&entry_buf);
|
||||||
|
|
||||||
|
// Write 112 bytes as 28 x 4-byte blocks
|
||||||
|
let entry_block = pat_base_block
|
||||||
|
+ (i as u16 * (pattern::PATTERN_ENTRY_SIZE as u16 / 4));
|
||||||
|
for blk in 0..28u16 {
|
||||||
|
let bo = (blk as usize) * 4;
|
||||||
|
let chunk = [entry_buf[bo], entry_buf[bo+1], entry_buf[bo+2], entry_buf[bo+3]];
|
||||||
|
if ntag.write_verify_block(entry_block + blk, &chunk, &mut delay).is_err() {
|
||||||
|
provision_ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !provision_ok { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if provision_ok {
|
||||||
|
// Write header last (so partial writes don't look valid)
|
||||||
|
let mut hdr_buf = [0u8; pattern::HEADER_SIZE];
|
||||||
|
pattern::serialize_header(
|
||||||
|
5, 0, led_current, 0x00,
|
||||||
|
&all_pat_data,
|
||||||
|
&mut hdr_buf,
|
||||||
|
);
|
||||||
|
for blk in 0..4u16 {
|
||||||
|
let bo = (blk as usize) * 4;
|
||||||
|
let chunk = [hdr_buf[bo], hdr_buf[bo+1], hdr_buf[bo+2], hdr_buf[bo+3]];
|
||||||
|
if ntag.write_verify_block(
|
||||||
|
pattern::LIBRARY_BASE_BLOCK + blk, &chunk, &mut delay,
|
||||||
|
).is_err() {
|
||||||
|
provision_ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if provision_ok {
|
||||||
|
// Success — 4 fast blinks, then load pattern 0 from EEPROM
|
||||||
|
blink(&mut led, &mut delay, 4, 80);
|
||||||
|
|
||||||
|
// Re-read active pattern from what we just wrote
|
||||||
|
let mut pat_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
||||||
|
let loaded = ntag.read_memory(pat_base_block, &mut pat_buf).ok()
|
||||||
|
.and_then(|()| pattern::parse_pattern_entry(&pat_buf));
|
||||||
|
|
||||||
|
let i2c = ntag.release();
|
||||||
|
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
||||||
|
|
||||||
|
if let Some(pat) = loaded {
|
||||||
|
if pattern::load_pattern(&mut lp, &pat, led_current, &mut delay).is_err() {
|
||||||
|
blink(&mut led, &mut delay, 10, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
delay.delay_ms(60000u32);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Provisioning failed — fall back to hardcoded cycle
|
||||||
|
blink(&mut led, &mut delay, 8, 50);
|
||||||
|
|
||||||
|
let i2c = ntag.release();
|
||||||
|
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
|
loop {
|
||||||
|
if pattern::load_pattern(
|
||||||
|
&mut lp, &patterns[idx], led_current, &mut delay,
|
||||||
|
).is_err() {
|
||||||
|
blink(&mut led, &mut delay, 10, 50);
|
||||||
|
}
|
||||||
|
delay.delay_ms(15000u32);
|
||||||
|
idx = (idx + 1) % patterns.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|||||||
@@ -344,3 +344,229 @@ where
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// XBLK EEPROM pattern library format
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// EEPROM base block for the pattern library (upper 1K, block 256).
|
||||||
|
pub const LIBRARY_BASE_BLOCK: u16 = 0x0100;
|
||||||
|
|
||||||
|
/// Magic bytes identifying a valid XBLK library.
|
||||||
|
pub const XBLK_MAGIC: [u8; 4] = *b"XBLK";
|
||||||
|
|
||||||
|
/// Format version.
|
||||||
|
pub const XBLK_VERSION: u8 = 0x01;
|
||||||
|
|
||||||
|
/// Header size in bytes.
|
||||||
|
pub const HEADER_SIZE: usize = 16;
|
||||||
|
|
||||||
|
/// Pattern entry size in bytes (fixed for direct seeking).
|
||||||
|
pub const PATTERN_ENTRY_SIZE: usize = 112;
|
||||||
|
|
||||||
|
/// Maximum patterns that fit in 1024 bytes: (1024 - 16) / 112 = 9.
|
||||||
|
pub const MAX_PATTERNS: usize = 9;
|
||||||
|
|
||||||
|
/// Parsed XBLK library header.
|
||||||
|
pub struct LibraryHeader {
|
||||||
|
pub pattern_count: u8,
|
||||||
|
pub active_index: u8,
|
||||||
|
pub current: u8,
|
||||||
|
pub led_mode: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a 16-byte XBLK header. Returns None if magic or version is invalid.
|
||||||
|
pub fn parse_header(buf: &[u8; HEADER_SIZE]) -> Option<LibraryHeader> {
|
||||||
|
if buf[0..4] != XBLK_MAGIC {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if buf[4] != XBLK_VERSION {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let count = buf[5];
|
||||||
|
if count == 0 || count as usize > MAX_PATTERNS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(LibraryHeader {
|
||||||
|
pattern_count: count,
|
||||||
|
active_index: buf[6] % count, // wrap if out of range
|
||||||
|
current: buf[7],
|
||||||
|
led_mode: buf[8],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a 112-byte pattern entry into a Pattern struct.
|
||||||
|
/// Returns None if the data is malformed.
|
||||||
|
pub fn parse_pattern_entry(buf: &[u8; PATTERN_ENTRY_SIZE]) -> Option<Pattern> {
|
||||||
|
let engine_count = buf[0];
|
||||||
|
if engine_count > 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let led_map_reg = buf[1];
|
||||||
|
// Decode LED_MAP register: 2 bits per channel [W(7:6), R(5:4), G(3:2), B(1:0)]
|
||||||
|
let map_b = led_map_byte_to_mapping(led_map_reg & 0x03)?;
|
||||||
|
let map_g = led_map_byte_to_mapping((led_map_reg >> 2) & 0x03)?;
|
||||||
|
let map_r = led_map_byte_to_mapping((led_map_reg >> 4) & 0x03)?;
|
||||||
|
let map_w = led_map_byte_to_mapping((led_map_reg >> 6) & 0x03)?;
|
||||||
|
|
||||||
|
// Direct PWM values at bytes 2-5 (unused for now, engines override)
|
||||||
|
// let _direct_pwm = [buf[2], buf[3], buf[4], buf[5]];
|
||||||
|
|
||||||
|
let engine1 = parse_engine_program(&buf[6..40])?;
|
||||||
|
let engine2 = parse_engine_program(&buf[40..74])?;
|
||||||
|
let engine3 = parse_engine_program(&buf[74..108])?;
|
||||||
|
|
||||||
|
Some(Pattern {
|
||||||
|
engine1,
|
||||||
|
engine2,
|
||||||
|
engine3,
|
||||||
|
map_b,
|
||||||
|
map_g,
|
||||||
|
map_r,
|
||||||
|
map_w,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a 34-byte engine section: 2 bytes command count (BE) + 32 bytes commands.
|
||||||
|
/// Returns Some(None) for unused engines, Some(Some(prog)) for valid, None for malformed.
|
||||||
|
fn parse_engine_program(buf: &[u8]) -> Option<Option<EngineProgram>> {
|
||||||
|
let cmd_count = ((buf[0] as u16) << 8 | buf[1] as u16) as usize;
|
||||||
|
if cmd_count == 0 {
|
||||||
|
return Some(None);
|
||||||
|
}
|
||||||
|
if cmd_count > 16 {
|
||||||
|
return None; // malformed
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut commands = [0u16; 16];
|
||||||
|
for i in 0..cmd_count {
|
||||||
|
let offset = 2 + i * 2;
|
||||||
|
commands[i] = (buf[offset] as u16) << 8 | buf[offset + 1] as u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prog = EngineProgram::new();
|
||||||
|
for i in 0..cmd_count {
|
||||||
|
let _ = prog.push(commands[i]);
|
||||||
|
}
|
||||||
|
Some(Some(prog))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a 2-bit LED_MAP field to a LedMapping enum value.
|
||||||
|
fn led_map_byte_to_mapping(val: u8) -> Option<LedMapping> {
|
||||||
|
match val {
|
||||||
|
0b00 => Some(LedMapping::I2c),
|
||||||
|
0b01 => Some(LedMapping::Engine1),
|
||||||
|
0b10 => Some(LedMapping::Engine2),
|
||||||
|
0b11 => Some(LedMapping::Engine3),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the raw LED_MAP register byte from a Pattern's mapping fields.
|
||||||
|
pub fn pattern_to_led_map_byte(p: &Pattern) -> u8 {
|
||||||
|
(p.map_b as u8)
|
||||||
|
| ((p.map_g as u8) << 2)
|
||||||
|
| ((p.map_r as u8) << 4)
|
||||||
|
| ((p.map_w as u8) << 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// XBLK serializer (MCU-side, for self-provisioning)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Serialize a Pattern into a 112-byte XBLK entry buffer.
|
||||||
|
pub fn serialize_pattern_entry(p: &Pattern, buf: &mut [u8; PATTERN_ENTRY_SIZE]) {
|
||||||
|
// Zero the buffer
|
||||||
|
for b in buf.iter_mut() {
|
||||||
|
*b = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engine count
|
||||||
|
let mut count = 0u8;
|
||||||
|
if p.engine1.is_some() { count += 1; }
|
||||||
|
if p.engine2.is_some() { count += 1; }
|
||||||
|
if p.engine3.is_some() { count += 1; }
|
||||||
|
buf[0] = count;
|
||||||
|
|
||||||
|
// LED_MAP register byte
|
||||||
|
buf[1] = pattern_to_led_map_byte(p);
|
||||||
|
|
||||||
|
// Direct PWM [B, G, R, W] — bytes 2-5, leave as 0 for engine patterns
|
||||||
|
|
||||||
|
// Engine programs
|
||||||
|
serialize_engine(&p.engine1, &mut buf[6..40]);
|
||||||
|
serialize_engine(&p.engine2, &mut buf[40..74]);
|
||||||
|
serialize_engine(&p.engine3, &mut buf[74..108]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize an optional engine program into a 34-byte section.
|
||||||
|
fn serialize_engine(eng: &Option<EngineProgram>, buf: &mut [u8]) {
|
||||||
|
match eng {
|
||||||
|
None => {
|
||||||
|
buf[0] = 0;
|
||||||
|
buf[1] = 0;
|
||||||
|
}
|
||||||
|
Some(prog) => {
|
||||||
|
let len = prog.len();
|
||||||
|
buf[0] = (len >> 8) as u8;
|
||||||
|
buf[1] = len as u8;
|
||||||
|
let bytes = prog.as_bytes(); // 32-byte SRAM image, big-endian
|
||||||
|
buf[2..34].copy_from_slice(&bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CRC-16/CCITT-FALSE (matching Python serializer).
|
||||||
|
pub fn crc16(data: &[u8]) -> u16 {
|
||||||
|
let mut crc: u16 = 0xFFFF;
|
||||||
|
for &b in data {
|
||||||
|
crc ^= (b as u16) << 8;
|
||||||
|
for _ in 0..8 {
|
||||||
|
if crc & 0x8000 != 0 {
|
||||||
|
crc = (crc << 1) ^ 0x1021;
|
||||||
|
} else {
|
||||||
|
crc <<= 1;
|
||||||
|
}
|
||||||
|
crc &= 0xFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crc
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize the XBLK header into a 16-byte buffer.
|
||||||
|
/// CRC is computed over header bytes 0-13 + pattern data.
|
||||||
|
pub fn serialize_header(
|
||||||
|
pattern_count: u8,
|
||||||
|
active_index: u8,
|
||||||
|
current: u8,
|
||||||
|
led_mode: u8,
|
||||||
|
pattern_data_crc_input: &[u8],
|
||||||
|
buf: &mut [u8; HEADER_SIZE],
|
||||||
|
) {
|
||||||
|
buf[0..4].copy_from_slice(&XBLK_MAGIC);
|
||||||
|
buf[4] = XBLK_VERSION;
|
||||||
|
buf[5] = pattern_count;
|
||||||
|
buf[6] = active_index;
|
||||||
|
buf[7] = current;
|
||||||
|
buf[8] = led_mode;
|
||||||
|
for i in 9..14 {
|
||||||
|
buf[i] = 0;
|
||||||
|
}
|
||||||
|
// CRC over header[0..14] + pattern data
|
||||||
|
let mut crc = crc16(&buf[0..14]);
|
||||||
|
// Continue CRC over pattern data
|
||||||
|
for &b in pattern_data_crc_input {
|
||||||
|
crc ^= (b as u16) << 8;
|
||||||
|
for _ in 0..8 {
|
||||||
|
if crc & 0x8000 != 0 {
|
||||||
|
crc = (crc << 1) ^ 0x1021;
|
||||||
|
} else {
|
||||||
|
crc <<= 1;
|
||||||
|
}
|
||||||
|
crc &= 0xFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf[14] = (crc >> 8) as u8;
|
||||||
|
buf[15] = crc as u8;
|
||||||
|
}
|
||||||
|
|||||||
339
tools/xblk_serialize.py
Normal file
339
tools/xblk_serialize.py
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
XBLK pattern library serializer for xblink.
|
||||||
|
|
||||||
|
Converts pattern definitions to the XBLK binary format and writes them
|
||||||
|
to NTAG5 EEPROM blocks 256+ (upper 1K) via ntag5sensor ISO15693 commands.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Serialize built-in patterns to binary file:
|
||||||
|
python xblk_serialize.py --output patterns.bin
|
||||||
|
|
||||||
|
# Write to NTAG5 via PCSC reader:
|
||||||
|
python xblk_serialize.py --write
|
||||||
|
|
||||||
|
# Load from JSON file:
|
||||||
|
python xblk_serialize.py --json patterns.json --write
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# XBLK format constants (must match src/pattern/mod.rs)
|
||||||
|
XBLK_MAGIC = b"XBLK"
|
||||||
|
XBLK_VERSION = 0x01
|
||||||
|
HEADER_SIZE = 16
|
||||||
|
PATTERN_ENTRY_SIZE = 112
|
||||||
|
MAX_PATTERNS = 9
|
||||||
|
MAX_COMMANDS_PER_ENGINE = 16
|
||||||
|
|
||||||
|
# EEPROM block offset for pattern library (upper 1K)
|
||||||
|
LIBRARY_BASE_BLOCK = 256
|
||||||
|
|
||||||
|
# LED_MAP encoding helpers
|
||||||
|
LED_MAP_LOOKUP = {
|
||||||
|
"direct": 0b00, "i2c": 0b00,
|
||||||
|
"engine1": 0b01, "e1": 0b01,
|
||||||
|
"engine2": 0b10, "e2": 0b10,
|
||||||
|
"engine3": 0b11, "e3": 0b11,
|
||||||
|
}
|
||||||
|
|
||||||
|
LED_MODE_LOOKUP = {"rgbw": 0x00, "mono3": 0x01}
|
||||||
|
|
||||||
|
|
||||||
|
def crc16(data: bytes) -> int:
|
||||||
|
"""CRC-16/CCITT-FALSE."""
|
||||||
|
crc = 0xFFFF
|
||||||
|
for b in data:
|
||||||
|
crc ^= b << 8
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 0x8000:
|
||||||
|
crc = (crc << 1) ^ 0x1021
|
||||||
|
else:
|
||||||
|
crc <<= 1
|
||||||
|
crc &= 0xFFFF
|
||||||
|
return crc
|
||||||
|
|
||||||
|
|
||||||
|
def encode_led_map(mapping: dict) -> int:
|
||||||
|
"""Encode {"b": "engine1", "g": "engine1", ...} to LP5562 LED_MAP register byte."""
|
||||||
|
b = LED_MAP_LOOKUP.get(mapping.get("b", "direct"), 0)
|
||||||
|
g = LED_MAP_LOOKUP.get(mapping.get("g", "direct"), 0)
|
||||||
|
r = LED_MAP_LOOKUP.get(mapping.get("r", "direct"), 0)
|
||||||
|
w = LED_MAP_LOOKUP.get(mapping.get("w", "direct"), 0)
|
||||||
|
return b | (g << 2) | (r << 4) | (w << 6)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_pattern(pat: dict) -> bytes:
|
||||||
|
"""Encode a single pattern dict to PATTERN_ENTRY_SIZE bytes."""
|
||||||
|
engines = pat.get("engines", [[], [], []])
|
||||||
|
while len(engines) < 3:
|
||||||
|
engines.append([])
|
||||||
|
|
||||||
|
engine_count = sum(1 for e in engines if len(e) > 0)
|
||||||
|
led_map_reg = encode_led_map(pat.get("led_map", {}))
|
||||||
|
direct_pwm = pat.get("direct_pwm", [0, 0, 0, 0])
|
||||||
|
while len(direct_pwm) < 4:
|
||||||
|
direct_pwm.append(0)
|
||||||
|
|
||||||
|
buf = bytearray(PATTERN_ENTRY_SIZE)
|
||||||
|
buf[0] = engine_count
|
||||||
|
buf[1] = led_map_reg
|
||||||
|
buf[2:6] = bytes(direct_pwm[:4])
|
||||||
|
|
||||||
|
for eng_idx, cmds in enumerate(engines[:3]):
|
||||||
|
if len(cmds) > MAX_COMMANDS_PER_ENGINE:
|
||||||
|
raise ValueError(f"Engine {eng_idx+1} has {len(cmds)} commands (max {MAX_COMMANDS_PER_ENGINE})")
|
||||||
|
base = 6 + eng_idx * 34 # 2 bytes count + 32 bytes commands
|
||||||
|
struct.pack_into(">H", buf, base, len(cmds))
|
||||||
|
for i, cmd in enumerate(cmds):
|
||||||
|
struct.pack_into(">H", buf, base + 2 + i * 2, cmd & 0xFFFF)
|
||||||
|
|
||||||
|
return bytes(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_library(config: dict) -> bytes:
|
||||||
|
"""Encode a full XBLK library (header + patterns) to bytes."""
|
||||||
|
patterns = config.get("patterns", [])
|
||||||
|
if len(patterns) == 0:
|
||||||
|
raise ValueError("No patterns defined")
|
||||||
|
if len(patterns) > MAX_PATTERNS:
|
||||||
|
raise ValueError(f"Too many patterns: {len(patterns)} (max {MAX_PATTERNS})")
|
||||||
|
|
||||||
|
current = config.get("current", 20)
|
||||||
|
mode = LED_MODE_LOOKUP.get(config.get("mode", "rgbw"), 0x00)
|
||||||
|
active = config.get("active", 0) % len(patterns)
|
||||||
|
|
||||||
|
# Build header (without CRC)
|
||||||
|
header = bytearray(HEADER_SIZE)
|
||||||
|
header[0:4] = XBLK_MAGIC
|
||||||
|
header[4] = XBLK_VERSION
|
||||||
|
header[5] = len(patterns)
|
||||||
|
header[6] = active
|
||||||
|
header[7] = current & 0xFF
|
||||||
|
header[8] = mode
|
||||||
|
# bytes 9-13 reserved
|
||||||
|
# bytes 14-15 CRC (filled below)
|
||||||
|
|
||||||
|
# Build pattern data
|
||||||
|
pat_data = b""
|
||||||
|
for pat in patterns:
|
||||||
|
pat_data += encode_pattern(pat)
|
||||||
|
|
||||||
|
# Compute CRC over header (bytes 0-13) + all pattern data
|
||||||
|
crc = crc16(bytes(header[:14]) + pat_data)
|
||||||
|
struct.pack_into(">H", header, 14, crc)
|
||||||
|
|
||||||
|
return bytes(header) + pat_data
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Built-in patterns (matching src/pattern/mod.rs)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# LP5562 EngineCommand helpers (matching lp5562.rs encoding)
|
||||||
|
def ramp_wait(prescale_slow: bool, step_time: int, up: bool, increment: int) -> int:
|
||||||
|
prescale_bit = 0x4000 if prescale_slow else 0
|
||||||
|
sign_bit = 0 if up else 0x0080
|
||||||
|
return prescale_bit | ((step_time & 0x3F) << 8) | sign_bit | (increment & 0x7F)
|
||||||
|
|
||||||
|
def wait(prescale_slow: bool, step_time: int) -> int:
|
||||||
|
prescale_bit = 0x4000 if prescale_slow else 0
|
||||||
|
return prescale_bit | ((step_time & 0x3F) << 8)
|
||||||
|
|
||||||
|
def set_pwm(value: int) -> int:
|
||||||
|
return 0x4000 | value # Actually: 0x40xx format
|
||||||
|
# Wait, let me check the actual encoding...
|
||||||
|
|
||||||
|
def branch(step: int, loop_count: int) -> int:
|
||||||
|
return 0xA000 | ((loop_count & 0x3F) << 7) | (step & 0x7F)
|
||||||
|
|
||||||
|
def trigger(wait_mask: int, send_mask: int) -> int:
|
||||||
|
return 0xE000 | ((wait_mask & 0x07) << 7) | (send_mask & 0x07)
|
||||||
|
|
||||||
|
|
||||||
|
def builtin_patterns() -> dict:
|
||||||
|
"""Return the 5 built-in patterns as a config dict."""
|
||||||
|
# Breathe: 1 engine, all RGB channels
|
||||||
|
breathe_cmds = [
|
||||||
|
ramp_wait(True, 1, True, 127), # 0→128
|
||||||
|
ramp_wait(True, 1, True, 127), # 128→255
|
||||||
|
ramp_wait(True, 1, False, 127), # 255→127
|
||||||
|
ramp_wait(True, 1, False, 127), # 127→0
|
||||||
|
wait(True, 48), # pause
|
||||||
|
branch(0, 0), # loop
|
||||||
|
]
|
||||||
|
|
||||||
|
# Heartbeat: 1 engine, double-pulse
|
||||||
|
heartbeat_cmds = [
|
||||||
|
0x40FF, # set_pwm(255)
|
||||||
|
wait(False, 20), # hold ~10ms
|
||||||
|
0x4000, # set_pwm(0)
|
||||||
|
wait(False, 40), # gap ~20ms
|
||||||
|
0x40FF, # set_pwm(255)
|
||||||
|
wait(False, 20), # hold ~10ms
|
||||||
|
0x4000, # set_pwm(0)
|
||||||
|
wait(True, 63), # rest ~1.0s
|
||||||
|
wait(True, 32), # rest ~0.5s
|
||||||
|
branch(0, 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Slow pulse: 1 engine, very gentle
|
||||||
|
slow_pulse_cmds = [
|
||||||
|
ramp_wait(True, 4, True, 127), # 0→128
|
||||||
|
ramp_wait(True, 4, True, 127), # 128→255
|
||||||
|
wait(True, 32), # hold
|
||||||
|
ramp_wait(True, 4, False, 127), # 255→127
|
||||||
|
ramp_wait(True, 4, False, 127), # 127→0
|
||||||
|
wait(True, 63), # pause
|
||||||
|
branch(0, 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
# RGB cycle: 3 engines with trigger sync
|
||||||
|
rgb_e1 = [
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
trigger(0, 0b010), # send to E2
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
wait(True, 63),
|
||||||
|
branch(0, 0),
|
||||||
|
]
|
||||||
|
rgb_e2 = [
|
||||||
|
trigger(0b001, 0), # wait for E1
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
trigger(0, 0b100), # send to E3
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
wait(True, 32),
|
||||||
|
branch(0, 0),
|
||||||
|
]
|
||||||
|
rgb_e3 = [
|
||||||
|
trigger(0b010, 0), # wait for E2
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
wait(True, 32),
|
||||||
|
branch(0, 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Color wash: 3 engines, free-running with different periods
|
||||||
|
wash_e1 = [
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
branch(0, 0),
|
||||||
|
]
|
||||||
|
wash_e2 = [
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
wait(True, 32),
|
||||||
|
branch(0, 0),
|
||||||
|
]
|
||||||
|
wash_e3 = [
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, True, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
ramp_wait(True, 1, False, 127),
|
||||||
|
wait(True, 63),
|
||||||
|
branch(0, 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
single_rgb_map = {"b": "engine1", "g": "engine1", "r": "engine1", "w": "direct"}
|
||||||
|
triple_map = {"b": "engine1", "g": "engine2", "r": "engine3", "w": "direct"}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"current": 20,
|
||||||
|
"mode": "rgbw",
|
||||||
|
"active": 0,
|
||||||
|
"patterns": [
|
||||||
|
{"name": "breathe", "led_map": single_rgb_map, "engines": [breathe_cmds, [], []]},
|
||||||
|
{"name": "heartbeat", "led_map": single_rgb_map, "engines": [heartbeat_cmds, [], []]},
|
||||||
|
{"name": "slow_pulse", "led_map": single_rgb_map, "engines": [slow_pulse_cmds, [], []]},
|
||||||
|
{"name": "rgb_cycle", "led_map": triple_map, "engines": [rgb_e1, rgb_e2, rgb_e3]},
|
||||||
|
{"name": "color_wash", "led_map": triple_map, "engines": [wash_e1, wash_e2, wash_e3]},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_ntag5(data: bytes):
|
||||||
|
"""Write binary data to NTAG5 EEPROM blocks 256+ via ntag5sensor."""
|
||||||
|
# Add ntag5sensor to path
|
||||||
|
ntag5sensor_path = os.path.join(os.path.dirname(__file__), "..", "..", "ntag5sensor")
|
||||||
|
sys.path.insert(0, ntag5sensor_path)
|
||||||
|
|
||||||
|
from vicinity.iso15693 import ISO15693, ISO_FLAG_DATA_RATE
|
||||||
|
from vicinity.ntag5link import Ntag5Link
|
||||||
|
|
||||||
|
reader = ISO15693()
|
||||||
|
chip = Ntag5Link(reader)
|
||||||
|
|
||||||
|
print(f"Writing {len(data)} bytes to EEPROM blocks {LIBRARY_BASE_BLOCK}-{LIBRARY_BASE_BLOCK + len(data)//4 - 1}")
|
||||||
|
|
||||||
|
# Write in 4-byte blocks
|
||||||
|
for i in range(0, len(data), 4):
|
||||||
|
block = LIBRARY_BASE_BLOCK + i // 4
|
||||||
|
chunk = data[i:i+4]
|
||||||
|
if len(chunk) < 4:
|
||||||
|
chunk = chunk + b'\x00' * (4 - len(chunk))
|
||||||
|
|
||||||
|
# Use ISO15693 WRITE SINGLE BLOCK (unaddressed)
|
||||||
|
# Block address needs protocol extension for blocks > 255
|
||||||
|
flags = ISO_FLAG_DATA_RATE | 0x08 # data rate + protocol extension
|
||||||
|
cmd = bytes([flags, 0x21]) + struct.pack("<H", block) + chunk
|
||||||
|
reader.transmit_iso15693(cmd, True)
|
||||||
|
|
||||||
|
# EEPROM write cycle delay
|
||||||
|
import time
|
||||||
|
time.sleep(0.006)
|
||||||
|
|
||||||
|
if (i // 4) % 10 == 0:
|
||||||
|
print(f" Block {block}...")
|
||||||
|
|
||||||
|
print("Done!")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="XBLK pattern library serializer")
|
||||||
|
parser.add_argument("--json", help="JSON pattern definition file")
|
||||||
|
parser.add_argument("--output", "-o", help="Output binary file")
|
||||||
|
parser.add_argument("--write", action="store_true", help="Write to NTAG5 via PCSC")
|
||||||
|
parser.add_argument("--builtin", action="store_true", help="Use built-in patterns (default if no --json)")
|
||||||
|
parser.add_argument("--dump", action="store_true", help="Hex dump the binary")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
with open(args.json) as f:
|
||||||
|
config = json.load(f)
|
||||||
|
else:
|
||||||
|
config = builtin_patterns()
|
||||||
|
|
||||||
|
data = encode_library(config)
|
||||||
|
print(f"Library: {len(config['patterns'])} patterns, {len(data)} bytes")
|
||||||
|
print(f"CRC-16: 0x{struct.unpack_from('>H', data, 14)[0]:04X}")
|
||||||
|
|
||||||
|
if args.dump:
|
||||||
|
for i in range(0, len(data), 16):
|
||||||
|
hex_str = " ".join(f"{b:02X}" for b in data[i:i+16])
|
||||||
|
ascii_str = "".join(chr(b) if 32 <= b < 127 else "." for b in data[i:i+16])
|
||||||
|
print(f" {i:04X}: {hex_str:<48s} {ascii_str}")
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
with open(args.output, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
print(f"Written to {args.output}")
|
||||||
|
|
||||||
|
if args.write:
|
||||||
|
write_to_ntag5(data)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user