Pivot from LP5562 to GPIO-direct PWM LEDs, add NTAG5 EH provisioning
LP5562 requires 2.7V min but NFC energy harvesting produces only 1.8V. New architecture: SAMD21 drives 6 red LEDs directly via TCC0/TCC1 hardware PWM through current-limiting resistors. Key changes: - Add TCC PWM driver (src/led/pwm.rs) for 6 GPIO-direct LED channels - Rewrite pattern engine: software waveform LUTs (sine/triangle/square/ heartbeat) replace LP5562 hardware execution engines - Add XBLK v2 EEPROM format with 16-byte pattern entries + playlist - Add TC4 50Hz ISR for animation, power governor for current budget - Add NTAG5 EH provisioning: persistent config + session trigger - Critical finding: SRAM passthrough in persistent EEPROM blocks all NFC access when MCU unpowered — CONFIG_1 must be session-only - Add provision_eh.py PCSC tool for ACR1552 reader - Update CLAUDE.md and STATUS.md for new architecture Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
166
tools/provision_eh.py
Normal file
166
tools/provision_eh.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Configure NTAG5Link energy harvesting for 1.8V automatic VOUT.
|
||||
|
||||
Writes EH_CONFIG to persistent EEPROM (block 0x3D) so the NTAG5
|
||||
automatically outputs 1.8V when an NFC field is present — powering
|
||||
the SAMD21 MCU without any firmware intervention.
|
||||
|
||||
Also sets ED_CONFIG (FD pin) for NFC-to-I2C SRAM pass-through so
|
||||
the MCU can detect SRAM writes from the phone.
|
||||
|
||||
Usage:
|
||||
# Read current EH config:
|
||||
python provision_eh.py --read
|
||||
|
||||
# Write 1.8V / 6.5mA EH config:
|
||||
python provision_eh.py --write
|
||||
|
||||
# Write with custom current limit:
|
||||
python provision_eh.py --write --current 4.0
|
||||
|
||||
# Also configure CONFIG_0 and CONFIG_1 for xblink:
|
||||
python provision_eh.py --write --full
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add ntag5sensor to path
|
||||
ntag5sensor_path = os.path.join(os.path.dirname(__file__), "..", "..", "ntag5sensor")
|
||||
sys.path.insert(0, ntag5sensor_path)
|
||||
|
||||
from reader.acr1552 import ACR1552
|
||||
from vicinity.iso15693 import ISO15693
|
||||
from vicinity.ntag5link import (
|
||||
Ntag5Link,
|
||||
NXP_EH_CONFIG_EH_VOUT_V_SEL_1_8,
|
||||
NXP_EH_CONFIG_EH_VOUT_V_SEL_2_4,
|
||||
NXP_EH_CONFIG_EH_VOUT_V_SEL_3_0,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_0_4,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_0_6,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_1_4,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_2_7,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_4_0,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_6_5,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_9_0,
|
||||
NXP_EH_CONFIG_EH_VOUT_I_SEL_12_5,
|
||||
NXP_ED_CONFIG_NFC_TO_I2C_PASS_THROUGH,
|
||||
NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH,
|
||||
NXP_CONFIG_1_ARBITER_MODE_SRAM_PASSTHROUGH,
|
||||
NXP_CONFIG_1_USE_CASE_CONF_I2C_SLAVE,
|
||||
)
|
||||
|
||||
# Current limit lookup: string -> constant
|
||||
CURRENT_MAP = {
|
||||
"0.4": NXP_EH_CONFIG_EH_VOUT_I_SEL_0_4,
|
||||
"0.6": NXP_EH_CONFIG_EH_VOUT_I_SEL_0_6,
|
||||
"1.4": NXP_EH_CONFIG_EH_VOUT_I_SEL_1_4,
|
||||
"2.7": NXP_EH_CONFIG_EH_VOUT_I_SEL_2_7,
|
||||
"4.0": NXP_EH_CONFIG_EH_VOUT_I_SEL_4_0,
|
||||
"6.5": NXP_EH_CONFIG_EH_VOUT_I_SEL_6_5,
|
||||
"9.0": NXP_EH_CONFIG_EH_VOUT_I_SEL_9_0,
|
||||
"12.5": NXP_EH_CONFIG_EH_VOUT_I_SEL_12_5,
|
||||
}
|
||||
|
||||
|
||||
def read_config(chip):
|
||||
"""Read and display current EH and general config."""
|
||||
print("=== NTAG5 Configuration ===\n")
|
||||
|
||||
info = chip.get_system_info()
|
||||
print(f"UID: {info['uid'].hex()}")
|
||||
|
||||
config = chip.get_config_info()
|
||||
print(f"\nCONFIG_0:")
|
||||
print(f" EH mode: {config.get('energy_harvesting_mode', '?')}")
|
||||
print(f" SRAM copy: {config.get('sram_copy_enabled', '?')}")
|
||||
print(f" Auto standby: {config.get('auto_standby_mode', '?')}")
|
||||
|
||||
print(f"\nCONFIG_1:")
|
||||
print(f" SRAM enable: {config.get('sram_enabled', '?')}")
|
||||
print(f" Arbiter mode: {config.get('arbiter_mode', '?')}")
|
||||
print(f" Use case: {config.get('use_case', '?')}")
|
||||
print(f" EH arbiter: {config.get('eh_arbiter_mode_enabled', '?')}")
|
||||
|
||||
eh = chip.get_eh_ed_config_info()
|
||||
print(f"\nEH_CONFIG (block 0x3D):")
|
||||
print(f" EH enable: {eh.get('eh_enable', '?')}")
|
||||
print(f" VOUT voltage: {eh.get('eh_vout_v_sel', '?')}V")
|
||||
print(f" VOUT current: {eh.get('eh_vout_i_sel', '?')}mA")
|
||||
print(f" Power check disabled: {eh.get('disable_power_check', '?')}")
|
||||
print(f" ED/FD config: {eh.get('ed_config', '?')}")
|
||||
|
||||
|
||||
def write_eh(chip, current_sel, full_config=False):
|
||||
"""Write EH config for 1.8V automatic come-up."""
|
||||
|
||||
print("Writing EH config: 1.8V, current limit = "
|
||||
f"{[k for k,v in CURRENT_MAP.items() if v == current_sel][0]}mA")
|
||||
print(f" ED/FD pin: NFC-to-I2C pass-through (SRAM write detect)")
|
||||
|
||||
chip.write_eh_ed_config(
|
||||
enable=True,
|
||||
disable_power_check=False,
|
||||
current=current_sel,
|
||||
voltage=NXP_EH_CONFIG_EH_VOUT_V_SEL_1_8,
|
||||
ed_config=NXP_ED_CONFIG_NFC_TO_I2C_PASS_THROUGH,
|
||||
)
|
||||
print(" EH_CONFIG written.")
|
||||
|
||||
if full_config:
|
||||
print("\nWriting CONFIG_0: EH mode = low field strength")
|
||||
chip.write_config0(
|
||||
eh_mode=NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH,
|
||||
)
|
||||
print(" CONFIG_0 written.")
|
||||
|
||||
print("Writing CONFIG_1: SRAM enable, arbiter=passthrough, use_case=I2C slave")
|
||||
chip.write_config1(
|
||||
sram_enable=True,
|
||||
arbiter_mode=NXP_CONFIG_1_ARBITER_MODE_SRAM_PASSTHROUGH,
|
||||
use_case=NXP_CONFIG_1_USE_CASE_CONF_I2C_SLAVE,
|
||||
)
|
||||
print(" CONFIG_1 written.")
|
||||
|
||||
# Verify
|
||||
print("\n--- Verify ---")
|
||||
read_config(chip)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Configure NTAG5Link energy harvesting for 1.8V")
|
||||
parser.add_argument("--read", action="store_true",
|
||||
help="Read current config (no writes)")
|
||||
parser.add_argument("--write", action="store_true",
|
||||
help="Write EH config for 1.8V automatic VOUT")
|
||||
parser.add_argument("--current", default="6.5",
|
||||
choices=list(CURRENT_MAP.keys()),
|
||||
help="VOUT current limit in mA (default: 6.5)")
|
||||
parser.add_argument("--full", action="store_true",
|
||||
help="Also write CONFIG_0 and CONFIG_1 for xblink")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.read and not args.write:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
reader = ACR1552()
|
||||
reader.connect()
|
||||
iso = ISO15693(reader)
|
||||
chip = Ntag5Link(iso)
|
||||
|
||||
if args.read:
|
||||
read_config(chip)
|
||||
|
||||
if args.write:
|
||||
current_sel = CURRENT_MAP[args.current]
|
||||
write_eh(chip, current_sel, full_config=args.full)
|
||||
|
||||
reader.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
XBLK pattern library serializer for xblink.
|
||||
XBLK v2 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.
|
||||
Converts pattern definitions to the XBLK v2 binary format (GPIO-direct PWM,
|
||||
16-byte pattern entries) and writes them to NTAG5 EEPROM blocks 256+
|
||||
(upper 1K) via ntag5sensor ISO15693 commands.
|
||||
|
||||
Usage:
|
||||
# Serialize built-in patterns to binary file:
|
||||
@@ -22,27 +23,23 @@ import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
# XBLK format constants (must match src/pattern/mod.rs)
|
||||
# XBLK v2 format constants (must match src/pattern/mod.rs)
|
||||
XBLK_MAGIC = b"XBLK"
|
||||
XBLK_VERSION = 0x01
|
||||
XBLK_VERSION = 0x02
|
||||
HEADER_SIZE = 16
|
||||
PATTERN_ENTRY_SIZE = 112
|
||||
MAX_PATTERNS = 9
|
||||
MAX_COMMANDS_PER_ENGINE = 16
|
||||
PATTERN_ENTRY_SIZE = 16
|
||||
MAX_PATTERNS = 61
|
||||
MAX_PLAYLIST = 32
|
||||
NUM_LEDS = 6
|
||||
|
||||
# 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,
|
||||
# Waveform IDs
|
||||
WAVEFORM_LOOKUP = {
|
||||
"sine": 0, "triangle": 1, "square": 2, "heartbeat": 3,
|
||||
}
|
||||
|
||||
LED_MODE_LOOKUP = {"rgbw": 0x00, "mono3": 0x01}
|
||||
|
||||
|
||||
def crc16(data: bytes) -> int:
|
||||
"""CRC-16/CCITT-FALSE."""
|
||||
@@ -58,215 +55,131 @@ def crc16(data: bytes) -> int:
|
||||
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([])
|
||||
waveform = WAVEFORM_LOOKUP.get(pat.get("waveform", "sine"), 0)
|
||||
cycle_len = pat.get("cycle_len", 125)
|
||||
phase = pat.get("phase", [0] * NUM_LEDS)
|
||||
envelope = pat.get("envelope", [255] * NUM_LEDS)
|
||||
repeat_count = pat.get("repeat_count", 0xFF)
|
||||
|
||||
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)
|
||||
# Pad/truncate to NUM_LEDS
|
||||
phase = (phase + [0] * NUM_LEDS)[:NUM_LEDS]
|
||||
envelope = (envelope + [255] * NUM_LEDS)[:NUM_LEDS]
|
||||
|
||||
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)
|
||||
buf[0] = waveform & 0xFF
|
||||
buf[1] = cycle_len & 0xFF
|
||||
buf[2:8] = bytes(phase)
|
||||
buf[8:14] = bytes(envelope)
|
||||
buf[14] = repeat_count & 0xFF
|
||||
buf[15] = 0 # reserved
|
||||
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def encode_library(config: dict) -> bytes:
|
||||
"""Encode a full XBLK library (header + patterns) to bytes."""
|
||||
"""Encode a full XBLK v2 library (header + patterns + playlist) 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)
|
||||
budget = config.get("budget", 50)
|
||||
active = config.get("active", 0) % len(patterns)
|
||||
playlist = config.get("playlist", [])
|
||||
has_playlist = len(playlist) > 0
|
||||
|
||||
# Build header (without CRC)
|
||||
if len(playlist) > MAX_PLAYLIST:
|
||||
raise ValueError(f"Playlist too long: {len(playlist)} (max {MAX_PLAYLIST})")
|
||||
|
||||
# Build header
|
||||
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)
|
||||
header[5] = 0x01 if has_playlist else 0x00 # flags
|
||||
header[6] = len(patterns)
|
||||
header[7] = active
|
||||
header[8] = budget & 0xFF
|
||||
header[9] = len(playlist) & 0xFF
|
||||
# bytes 10-13 reserved
|
||||
|
||||
# Compute CRC over header bytes 0-13
|
||||
crc = crc16(bytes(header[:14]))
|
||||
struct.pack_into(">H", header, 14, crc)
|
||||
|
||||
# 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)
|
||||
# Build playlist data
|
||||
playlist_data = bytes(playlist + [0] * (MAX_PLAYLIST - len(playlist)))
|
||||
|
||||
return bytes(header) + pat_data
|
||||
result = bytes(header) + pat_data
|
||||
if has_playlist:
|
||||
result += playlist_data[:MAX_PLAYLIST]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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",
|
||||
"budget": 50,
|
||||
"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]},
|
||||
{
|
||||
"name": "breathe",
|
||||
"waveform": "sine",
|
||||
"cycle_len": 125, # 2.5s
|
||||
"phase": [0, 0, 0, 0, 0, 0],
|
||||
"envelope": [255, 255, 255, 255, 255, 255],
|
||||
"repeat_count": 0xFF,
|
||||
},
|
||||
{
|
||||
"name": "heartbeat",
|
||||
"waveform": "heartbeat",
|
||||
"cycle_len": 80, # 1.6s
|
||||
"phase": [0, 0, 0, 0, 0, 0],
|
||||
"envelope": [255, 255, 255, 255, 255, 255],
|
||||
"repeat_count": 0xFF,
|
||||
},
|
||||
{
|
||||
"name": "wave_chase",
|
||||
"waveform": "sine",
|
||||
"cycle_len": 100, # 2s
|
||||
"phase": [0, 43, 85, 128, 170, 213],
|
||||
"envelope": [255, 255, 255, 255, 255, 255],
|
||||
"repeat_count": 0xFF,
|
||||
},
|
||||
{
|
||||
"name": "slow_pulse",
|
||||
"waveform": "triangle",
|
||||
"cycle_len": 250, # 5s
|
||||
"phase": [0, 0, 0, 0, 0, 0],
|
||||
"envelope": [200, 200, 200, 200, 200, 200],
|
||||
"repeat_count": 0xFF,
|
||||
},
|
||||
{
|
||||
"name": "alternating_blink",
|
||||
"waveform": "square",
|
||||
"cycle_len": 50, # 1s
|
||||
"phase": [0, 128, 0, 128, 0, 128],
|
||||
"envelope": [255, 255, 255, 255, 255, 255],
|
||||
"repeat_count": 0xFF,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -278,20 +191,16 @@ def write_to_ntag5(data: bytes):
|
||||
|
||||
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
|
||||
flags = ISO_FLAG_DATA_RATE | 0x08
|
||||
cmd = bytes([flags, 0x21]) + struct.pack("<H", block) + chunk
|
||||
reader.transmit_iso15693(cmd, True)
|
||||
|
||||
# EEPROM write cycle delay
|
||||
import time
|
||||
time.sleep(0.006)
|
||||
|
||||
@@ -302,11 +211,10 @@ def write_to_ntag5(data: bytes):
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="XBLK pattern library serializer")
|
||||
parser = argparse.ArgumentParser(description="XBLK v2 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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user