diff --git a/Cargo.lock b/Cargo.lock index afe9fa55..8a6a1829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "CoreFoundation-sys" @@ -714,6 +714,12 @@ dependencies = [ "void", ] +[[package]] +name = "embedded-storage" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -795,6 +801,7 @@ dependencies = [ "defmt-rtt", "embedded-graphics", "embedded-hal", + "embedded-storage", "fugit", "heapless", "is31fl3741", diff --git a/commands.md b/commands.md index 46322edb..a8caaff2 100644 --- a/commands.md +++ b/commands.md @@ -57,6 +57,14 @@ When no parameters are given, the current value is queried and returned. | SetPxCol | 0x16 | ` D ` | | 50 Bytes | Send a column of pixels | | FlushFB | 0x17 | ` D ` | | | Flush all columns | | Version | 0x20 | `LDM` | 3 Bytes | | Get firmware version | +| SavePattern | 0x30 | `L ` | 1 Byte | 1B Slot| Save display to flash | +| LoadPattern | 0x31 | `L ` | 1 Byte | 1B Slot| Load pattern from flash | +| DeletePattern| 0x32 | `L ` | 1 Byte | 1B Slot| Delete pattern from flash| +| ListPatterns | 0x33 | `L ` | 32 Bytes| | List all pattern slots | +| SaveConfig | 0x34 | `L ` | 1 Byte | | Save config to flash | +| GetConfig | 0x35 | `L ` | 16 Bytes| | Get stored config | +| ResetConfig | 0x36 | `L ` | 1 Byte | | Reset config to defaults | +| SetConfigVal | 0x37 | `L ` | 1 Byte | 3B K+Val | Set config value + save | #### Pattern (0x01) @@ -105,3 +113,102 @@ Byte 2: 1 if pre-release version, 0 otherwise | | | | 0 otherwise MMMMMMMM mmmmPPPP 0000000p ``` + +## Flash Storage Commands + +The LED Matrix module supports persistent storage of patterns and configuration +in flash memory. Storage is placed at the end of flash (before the serial number) +so it survives firmware updates. + +**Flash Layout:** +- Pattern Storage: 0x100E0000, 60KB (8 pattern slots) +- Config Storage: 0x100EF000, 64KB (wear-leveled configuration) +- Serial Number: 0x100FF000, 4KB (read-only, untouched) + +#### SavePattern (0x30) + +Save the current display to a flash pattern slot. + +Parameters: +- Byte 0: Slot number (0-7) + +Response: +- Byte 0: 1 if successful, 0 otherwise + +#### LoadPattern (0x31) + +Load and display a pattern from a flash slot. + +Parameters: +- Byte 0: Slot number (0-7) + +Response: +- Byte 0: 1 if successful (pattern found), 0 if slot empty + +#### DeletePattern (0x32) + +Delete a pattern from a flash slot (erases the flash sector). + +Parameters: +- Byte 0: Slot number (0-7) + +Response: +- Byte 0: 1 if successful, 0 otherwise + +#### ListPatterns (0x33) + +List metadata for all 8 pattern slots. + +Response (32 bytes): +For each slot (4 bytes per slot): +- Byte 0: 1 if occupied, 0 if empty +- Byte 1: Pattern type (0=static, 1=animation) +- Byte 2: Frame count +- Byte 3: Frame delay in milliseconds + +#### SaveConfig (0x34) + +Save current runtime settings to flash. + +Response: +- Byte 0: 1 if successful, 0 otherwise + +#### GetConfig (0x35) + +Get the stored configuration from flash. + +Response (16 bytes): +```plain +Byte 0: Config version (currently 1) +Byte 1: Default brightness (0-255) +Byte 2-3: Sleep timeout in seconds (little-endian, 0=disabled) +Byte 4-7: Animation period in microseconds (little-endian) +Byte 8: PWM frequency index (0-3) +Byte 9: Startup animation enabled (0 or 1) +Byte 10: Startup pattern slot (0-7, or 0xFF for none) +Byte 11-15: Reserved +``` + +#### ResetConfig (0x36) + +Reset configuration to factory defaults and save to flash. + +Response: +- Byte 0: 1 if successful, 0 otherwise + +#### SetConfigValue (0x37) + +Set a specific configuration value and save to flash. + +Parameters: +- Byte 0: Config key +- Byte 1-2: Value (little-endian u16) + +Config keys: +- 0x01: Default brightness (value: 0-255) +- 0x02: Sleep timeout (value: seconds, 0=disabled) +- 0x03: Startup pattern slot (value: 0-7, or 255 for none) +- 0x04: Startup animation (value: 0=disabled, 1=enabled) + +Response: +- Byte 0: 1 if successful, 0 otherwise diff --git a/docs/flash-storage.md b/docs/flash-storage.md new file mode 100644 index 00000000..3f84097b --- /dev/null +++ b/docs/flash-storage.md @@ -0,0 +1,431 @@ +# LED Matrix Flash Storage + +This document describes the persistent flash storage feature for the Framework Laptop 16 LED Matrix input module. This feature allows saving custom patterns and configuration settings that persist across power cycles and firmware updates. + +## Table of Contents + +- [Overview](#overview) +- [Flash Memory Layout](#flash-memory-layout) +- [Configuration Storage](#configuration-storage) + - [Stored Settings](#stored-settings) + - [Wear Leveling](#wear-leveling) + - [Startup Behavior](#startup-behavior) +- [Pattern Storage](#pattern-storage) + - [Pattern Slots](#pattern-slots) + - [Pattern Format](#pattern-format) +- [CLI Reference](#cli-reference) + - [Rust CLI](#rust-cli) + - [Python CLI](#python-cli) +- [USB Protocol](#usb-protocol) +- [Usage Examples](#usage-examples) + +## Overview + +The LED Matrix module uses the RP2040 microcontroller with 2MB of flash memory. The storage implementation reserves space at the end of flash (before the serial number region) for: + +- **Pattern Storage**: Save up to 8 custom display patterns +- **Configuration Storage**: Persist settings like brightness, sleep timeout, and startup behavior + +This placement ensures that storage data survives firmware updates, as the firmware occupies the beginning of flash while storage uses the end. + +## Flash Memory Layout + +``` +Flash Address Map (2MB total): + +0x10000000 ┌─────────────────────────┐ + │ Bootloader │ 256 bytes +0x10000100 ├─────────────────────────┤ + │ │ + │ Firmware │ ~896KB + │ (FLASH region) │ + │ │ +0x100E0000 ├─────────────────────────┤ + │ │ + │ Pattern Storage │ 60KB (8 slots × 7.5KB each) + │ │ +0x100EF000 ├─────────────────────────┤ + │ │ + │ Config Storage │ 64KB (wear-leveled) + │ │ +0x100FF000 ├─────────────────────────┤ + │ Serial Number │ 4KB (read-only, factory set) +0x10100000 └─────────────────────────┘ +``` + +### Memory Regions + +| Region | Address | Size | Purpose | +|-----------------|--------------|-------|----------------------------------| +| BOOT2 | 0x10000000 | 256B | RP2040 boot stage 2 | +| FLASH | 0x10000100 | 896KB | Firmware code and data | +| PATTERN_STORAGE | 0x100E0000 | 60KB | 8 pattern slots | +| CONFIG_STORAGE | 0x100EF000 | 64KB | Wear-leveled configuration | +| SERIALNUM | 0x100FF000 | 4KB | Factory-programmed serial number | + +## Configuration Storage + +### Stored Settings + +The configuration storage saves the following settings: + +| Setting | Type | Range | Default | Description | +|--------------------|--------|-------------|------------|------------------------------------------| +| `brightness` | u8 | 0-255 | 120 | Default LED brightness on startup | +| `sleep_timeout` | u16 | 0-65535 | 0 | Seconds before auto-sleep (0 = disabled) | +| `animation_period` | u32 | microseconds| 31250 | Animation frame period (~32 FPS) | +| `pwm_freq` | u8 | 0-3 | 0 | PWM frequency index (0=29kHz) | +| `startup_animation`| bool | true/false | true | Enable random animation on startup | +| `startup_pattern` | u8 | 0-7 or 0xFF | 0xFF | Pattern slot to display on startup | + +### Wear Leveling + +Flash memory has limited write endurance (typically 10,000-100,000 cycles per sector). To extend the lifespan of the configuration storage, a simple wear-leveling scheme is used: + +1. **Slot Rotation**: The 64KB config region is divided into 16KB pages, each containing multiple 16-byte config slots +2. **Sequential Writes**: New configs are written to the next available slot rather than erasing and rewriting the same location +3. **Page Cycling**: When a page fills up, the next page is erased and used +4. **Checksum Validation**: Each config slot includes a CRC8 checksum to detect corruption + +``` +Config Page Structure (4KB sectors, 16 pages total): + +┌────────────────────────────────────────┐ +│ Slot 0: [Magic][Config Data][Checksum] │ 16 bytes +│ Slot 1: [Magic][Config Data][Checksum] │ 16 bytes +│ Slot 2: [Magic][Config Data][Checksum] │ 16 bytes +│ ... │ +│ Slot 255: [Empty - 0xFF] │ +└────────────────────────────────────────┘ +``` + +When reading, the firmware scans for the last valid slot. When writing, it finds the next empty slot or erases the next page if the current one is full. + +### Startup Behavior + +On power-up, the firmware: + +1. Loads the stored configuration from flash (or uses defaults if none found) +2. Applies the saved brightness, animation period, and PWM frequency +3. Checks `startup_pattern`: + - If set (0-7): Loads and displays that pattern slot + - If unset (0xFF): Falls through to animation check +4. If `startup_animation` is enabled and no pattern was loaded: Plays a random built-in animation + +## Pattern Storage + +### Pattern Slots + +The pattern storage provides 8 independent slots for saving custom displays: + +| Slot | Address | Size | +|------|--------------|--------| +| 0 | 0x100E0000 | 8KB | +| 1 | 0x100E2000 | 8KB | +| 2 | 0x100E4000 | 8KB | +| 3 | 0x100E6000 | 8KB | +| 4 | 0x100E8000 | 8KB | +| 5 | 0x100EA000 | 8KB | +| 6 | 0x100EC000 | 8KB | +| 7 | 0x100ED800 | ~6KB | + +Each slot can store either: +- A **static pattern**: Single 9×34 greyscale frame (306 bytes) +- An **animation**: Up to 16 frames with configurable delay (not yet fully implemented in CLI) + +### Pattern Wear Leveling + +Like configuration storage, pattern storage uses wear leveling to extend flash life: + +1. **Entry Size**: Each pattern entry is 512 bytes (header + frame data) +2. **Entries Per Slot**: Each 8KB slot holds up to 16 entries +3. **Sequential Writes**: New patterns are written to the next empty entry position +4. **Sequence Numbers**: Each entry has a sequence number to identify the latest version +5. **Erase on Full**: Only when a slot is completely full is it erased + +``` +Pattern Slot Structure (8KB per slot): + +┌─────────────────────────────────────────────────────┐ +│ Entry 0: [Header 32B][Frame 306B][Padding] 512B │ +│ Entry 1: [Header 32B][Frame 306B][Padding] 512B │ +│ Entry 2: [Empty - 0xFF] 512B │ +│ ... │ +│ Entry 15: [Empty - 0xFF] 512B │ +└─────────────────────────────────────────────────────┘ +``` + +When saving a pattern: +1. Find current highest sequence number in the slot +2. Find next empty entry position +3. If slot is full, erase both pages and start at entry 0 +4. Write new entry with incremented sequence number + +When loading: +1. Scan all entries for valid headers (magic bytes) +2. Find entry with highest sequence number +3. Verify CRC and return pattern data + +This means you can write to the same slot ~16 times before triggering an erase cycle. + +### Pattern Format + +``` +Pattern Header (32 bytes): +┌─────────────────────────────────────────────────────┐ +│ magic[0] │ 0xAA - First magic byte │ +│ magic[1] │ 0x01 - Second magic byte │ +│ slot │ Slot number (0-7) │ +│ pattern_type │ 0 = static, 1 = animation │ +│ frame_count │ Number of frames (1-16) │ +│ frame_delay │ Delay between frames in ms │ +│ data_crc │ CRC16 of frame data (2 bytes) │ +│ sequence │ Sequence number for wear leveling │ +│ reserved │ Padding to 32 bytes │ +└─────────────────────────────────────────────────────┘ + +Frame Data (306 bytes per frame): +┌─────────────────────────────────────────────────────┐ +│ pixels[0][0..34] │ Column 0, rows 0-33 (greyscale)│ +│ pixels[1][0..34] │ Column 1, rows 0-33 │ +│ ... │ │ +│ pixels[8][0..34] │ Column 8, rows 0-33 │ +└─────────────────────────────────────────────────────┘ +``` + +## CLI Reference + +### Rust CLI + +The `inputmodule-control` tool provides the following storage-related options: + +#### Pattern Commands + +```bash +# Save the current display to a flash slot +inputmodule-control ledmatrix --save-pattern +# SLOT: 0-7 + +# Load and display a pattern from flash +inputmodule-control ledmatrix --load-pattern +# SLOT: 0-7 + +# Delete a pattern from flash +inputmodule-control ledmatrix --delete-pattern +# SLOT: 0-7 + +# List all pattern slots and their status +inputmodule-control ledmatrix --list-patterns +``` + +#### Configuration Commands + +```bash +# Save current runtime settings to flash +inputmodule-control ledmatrix --save-config + +# Display stored configuration +inputmodule-control ledmatrix --get-config + +# Reset configuration to factory defaults +inputmodule-control ledmatrix --reset-config + +# Set default brightness (saves immediately) +inputmodule-control ledmatrix --set-default-brightness <0-255> + +# Set sleep timeout in seconds (0 = disabled) +inputmodule-control ledmatrix --set-sleep-timeout + +# Set startup pattern slot (0-7, or 255 for none) +inputmodule-control ledmatrix --set-startup-pattern + +# Enable/disable startup animation +inputmodule-control ledmatrix --set-startup-animation +``` + +### Python CLI + +The Python library provides equivalent functions: + +```python +from inputmodule.inputmodule import find_devs +from inputmodule import ledmatrix + +# Find the LED matrix device +devs = find_devs() +dev = devs[0] + +# Pattern functions +ledmatrix.save_pattern(dev, slot=0) # Save current display +ledmatrix.load_pattern(dev, slot=0) # Load pattern +ledmatrix.delete_pattern(dev, slot=0) # Delete pattern +ledmatrix.list_patterns(dev) # List all slots + +# Configuration functions +ledmatrix.save_config(dev) # Save current settings +ledmatrix.get_config(dev) # Get stored config (returns dict) +ledmatrix.reset_config(dev) # Reset to defaults + +# Individual setting functions (save immediately) +ledmatrix.set_default_brightness(dev, 200) +ledmatrix.set_sleep_timeout(dev, 300) # 5 minutes +ledmatrix.set_startup_pattern(dev, 0) # Use slot 0 on startup +ledmatrix.set_startup_animation(dev, False) # Disable random animation +``` + +## USB Protocol + +All storage commands use the standard Framework input module protocol: + +``` +Request: [0x32, 0xAC, CommandID, ...parameters] +Response: 32 bytes (command-specific) +``` + +### Command Reference + +| Command | ID | Parameters | Response | +|---------------|------|-------------------|---------------------------------------| +| SavePattern | 0x30 | 1B: slot (0-7) | 1B: success (1) or failure (0) | +| LoadPattern | 0x31 | 1B: slot (0-7) | 1B: success (1) or not found (0) | +| DeletePattern | 0x32 | 1B: slot (0-7) | 1B: success (1) or failure (0) | +| ListPatterns | 0x33 | none | 32B: 8 slots × 4 bytes each | +| SaveConfig | 0x34 | none | 1B: success (1) or failure (0) | +| GetConfig | 0x35 | none | 16B: StoredConfig struct | +| ResetConfig | 0x36 | none | 1B: success (1) or failure (0) | +| SetConfigValue| 0x37 | 1B key + 2B value | 1B: success (1) or failure (0) | + +### ListPatterns Response Format + +``` +For each slot (4 bytes): + Byte 0: occupied (1) or empty (0) + Byte 1: type (0=static, 1=animation) + Byte 2: frame count + Byte 3: frame delay in ms +``` + +### GetConfig Response Format + +``` +Byte 0: Config version (currently 1) +Byte 1: Default brightness (0-255) +Bytes 2-3: Sleep timeout in seconds (little-endian u16) +Bytes 4-7: Animation period in microseconds (little-endian u32) +Byte 8: PWM frequency index (0-3) +Byte 9: Startup animation enabled (0 or 1) +Byte 10: Startup pattern slot (0-7, or 0xFF for none) +Bytes 11-15: Reserved +``` + +### SetConfigValue Keys + +| Key | Name | Value Range | +|------|-------------------|--------------------| +| 0x01 | DefaultBrightness | 0-255 | +| 0x02 | SleepTimeout | 0-65535 seconds | +| 0x03 | StartupPattern | 0-7, or 255 (none) | +| 0x04 | StartupAnimation | 0 (off) or 1 (on) | + +## Usage Examples + +### Example 1: Save a Custom Boot Logo + +```bash +# 1. Display your custom image +inputmodule-control ledmatrix --image-gray my-logo.png + +# 2. Save it to slot 0 +inputmodule-control ledmatrix --save-pattern 0 + +# 3. Set it as the startup pattern +inputmodule-control ledmatrix --set-startup-pattern 0 + +# 4. Disable the random animation +inputmodule-control ledmatrix --set-startup-animation false +``` + +Now your custom logo will display every time the laptop powers on. + +### Example 2: Configure Sleep Behavior + +```bash +# Set brightness to 50% for battery saving +inputmodule-control ledmatrix --set-default-brightness 128 + +# Auto-sleep after 5 minutes of inactivity +inputmodule-control ledmatrix --set-sleep-timeout 300 + +# View current configuration +inputmodule-control ledmatrix --get-config +``` + +### Example 3: Manage Multiple Patterns (Python) + +```python +from inputmodule.inputmodule import find_devs +from inputmodule import ledmatrix + +dev = find_devs()[0] + +# Save different patterns to different slots +ledmatrix.pattern(dev, "All LEDs on") +ledmatrix.save_pattern(dev, 0) + +ledmatrix.pattern(dev, "Zigzag") +ledmatrix.save_pattern(dev, 1) + +ledmatrix.show_string(dev, "HELLO") +ledmatrix.save_pattern(dev, 2) + +# List what we've saved +ledmatrix.list_patterns(dev) +# Output: +# Pattern slots: +# Slot 0: static +# Slot 1: static +# Slot 2: static +# Slot 3: empty +# ... + +# Switch between them +ledmatrix.load_pattern(dev, 1) # Show zigzag +``` + +### Example 4: Factory Reset + +```bash +# Reset all configuration to defaults +inputmodule-control ledmatrix --reset-config + +# Optionally clear all saved patterns +for i in {0..7}; do + inputmodule-control ledmatrix --delete-pattern $i +done +``` + +## Technical Notes + +### Flash Write Considerations + +- Flash erase operations require erasing entire 4KB sectors +- Write operations can only change bits from 1 to 0 +- Erasing sets all bits back to 1 (0xFF) +- Interrupts are disabled during flash operations to prevent conflicts with XIP (execute-in-place) + +### Firmware Update Compatibility + +The storage regions are placed after the firmware region, so updating firmware via UF2 or other methods will not erase saved patterns or configuration. However: + +- A firmware update that changes the storage format may need to reset configuration +- The config version field allows detection of incompatible formats +- Factory reset is always available via `--reset-config` + +### Error Handling + +All storage operations return success/failure status. Common failure cases: + +- Invalid slot number (must be 0-7) +- Flash write error (hardware failure) +- Corrupted data (CRC mismatch) - returns defaults instead +- Empty slot on load - returns failure, no display change diff --git a/fl16-inputmodules/Cargo.toml b/fl16-inputmodules/Cargo.toml index 4af886d8..b3ee8cad 100644 --- a/fl16-inputmodules/Cargo.toml +++ b/fl16-inputmodules/Cargo.toml @@ -9,6 +9,9 @@ cortex-m.workspace = true cortex-m-rt.workspace = true embedded-hal.workspace = true +# Flash storage traits +embedded-storage = "0.3" + defmt.workspace = true defmt-rtt.workspace = true diff --git a/fl16-inputmodules/src/control.rs b/fl16-inputmodules/src/control.rs index ed1fbd32..a4eac7a3 100644 --- a/fl16-inputmodules/src/control.rs +++ b/fl16-inputmodules/src/control.rs @@ -71,6 +71,30 @@ pub enum CommandVals { PwmFreq = 0x1E, DebugMode = 0x1F, Version = 0x20, + + // Flash storage commands (0x30-0x3B) + /// Save current grid to flash pattern slot + SavePattern = 0x30, + /// Load pattern from flash slot + LoadPattern = 0x31, + /// Delete pattern from flash slot + DeletePattern = 0x32, + /// List all pattern slots + ListPatterns = 0x33, + /// Save current config to flash + SaveConfig = 0x34, + /// Load config from flash (returns config bytes) + GetConfig = 0x35, + /// Reset config to defaults + ResetConfig = 0x36, + /// Set individual config value + SetConfigValue = 0x37, + /// Stage a frame for animation save + StageAnimationFrame = 0x38, + /// Commit staged frames as animation + CommitAnimation = 0x39, + /// Clear animation staging buffer + ClearAnimationStage = 0x3A, } #[derive(num_derive::FromPrimitive)] @@ -154,6 +178,24 @@ impl From for PwmFreq { } } +/// Configuration keys for SetConfigValue command +#[cfg(feature = "ledmatrix")] +#[derive(Copy, Clone, num_derive::FromPrimitive)] +pub enum ConfigKeyArg { + /// Default brightness (u8) + Brightness = 0x01, + /// Sleep timeout in seconds (u16) + SleepTimeout = 0x02, + /// Animation period in microseconds (u32) + AnimationPeriod = 0x03, + /// PWM frequency (u8, 0-3) + PwmFreq = 0x04, + /// Startup animation enabled (bool) + StartupAnimation = 0x05, + /// Startup pattern index (u8, 0xFF = none) + StartupPattern = 0x06, +} + // TODO: Reduce size for modules that don't require other commands pub enum Command { /// Get current brightness scaling @@ -211,6 +253,29 @@ pub enum Command { GetPwmFreq, SetDebugMode(bool), GetDebugMode, + // Flash storage commands + #[cfg(feature = "ledmatrix")] + SavePattern(u8), + #[cfg(feature = "ledmatrix")] + LoadPattern(u8), + #[cfg(feature = "ledmatrix")] + DeletePattern(u8), + #[cfg(feature = "ledmatrix")] + ListPatterns, + #[cfg(feature = "ledmatrix")] + SaveConfig, + #[cfg(feature = "ledmatrix")] + GetConfig, + #[cfg(feature = "ledmatrix")] + ResetConfig, + #[cfg(feature = "ledmatrix")] + SetConfigValue(ConfigKeyArg, u32), + #[cfg(feature = "ledmatrix")] + StageAnimationFrame(u8, [u8; HEIGHT]), + #[cfg(feature = "ledmatrix")] + CommitAnimation(u8, u8), + #[cfg(feature = "ledmatrix")] + ClearAnimationStage, _Unknown, } @@ -397,6 +462,61 @@ pub fn parse_module_command(count: usize, buf: &[u8]) -> Option { } else { Command::GetDebugMode }), + // Flash storage commands + Some(CommandVals::SavePattern) => { + arg.map(Command::SavePattern) + } + Some(CommandVals::LoadPattern) => { + arg.map(Command::LoadPattern) + } + Some(CommandVals::DeletePattern) => { + arg.map(Command::DeletePattern) + } + Some(CommandVals::ListPatterns) => Some(Command::ListPatterns), + Some(CommandVals::SaveConfig) => Some(Command::SaveConfig), + Some(CommandVals::GetConfig) => Some(Command::GetConfig), + Some(CommandVals::ResetConfig) => Some(Command::ResetConfig), + Some(CommandVals::SetConfigValue) => { + if count >= 5 { + let key: Option = FromPrimitive::from_u8(buf[3]); + if let Some(key) = key { + // Parse value based on remaining bytes + let value = match count - 4 { + 1 => buf[4] as u32, + 2 => u16::from_le_bytes([buf[4], buf[5]]) as u32, + 4 => u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]), + _ => buf[4] as u32, + }; + Some(Command::SetConfigValue(key, value)) + } else { + None + } + } else { + None + } + } + Some(CommandVals::StageAnimationFrame) => { + // Need: frame_index (1 byte) + column data (HEIGHT bytes per column) + // For simplicity, we stage one column at a time + if count >= 4 + HEIGHT { + let frame_idx = buf[3]; + let mut col_data = [0u8; HEIGHT]; + col_data.copy_from_slice(&buf[4..4 + HEIGHT]); + Some(Command::StageAnimationFrame(frame_idx, col_data)) + } else { + None + } + } + Some(CommandVals::CommitAnimation) => { + if count >= 5 { + let slot = buf[3]; + let frame_delay_ms = buf[4]; + Some(Command::CommitAnimation(slot, frame_delay_ms)) + } else { + None + } + } + Some(CommandVals::ClearAnimationStage) => Some(Command::ClearAnimationStage), _ => None, } } else { @@ -635,6 +755,121 @@ pub fn handle_command( response[0] = state.debug_mode as u8; Some(response) } + // Flash storage commands + Command::SavePattern(slot) => { + let mut response: [u8; 32] = [0; 32]; + response[0] = if crate::storage::save_pattern(*slot, &state.grid) { + 0x00 // Success + } else { + 0x01 // Error + }; + Some(response) + } + Command::LoadPattern(slot) => { + let mut response: [u8; 32] = [0; 32]; + if let Some(pattern) = crate::storage::load_pattern(*slot) { + state.grid = pattern.first_frame(); + response[0] = 0x00; // Success + response[1] = pattern.header.pattern_type as u8; + response[2] = pattern.header.frame_count; + response[3] = pattern.header.frame_delay_ms; + } else { + response[0] = 0x01; // Error: slot empty or invalid + } + Some(response) + } + Command::DeletePattern(slot) => { + let mut response: [u8; 32] = [0; 32]; + response[0] = if crate::storage::delete_pattern(*slot) { + 0x00 + } else { + 0x01 + }; + Some(response) + } + Command::ListPatterns => { + let mut response: [u8; 32] = [0; 32]; + let patterns = crate::storage::list_patterns(); + for (i, info) in patterns.iter().enumerate() { + let offset = i * 4; + if offset + 4 <= 32 { + response[offset] = if info.occupied { 0x01 } else { 0xFF }; + response[offset + 1] = info.pattern_type; + response[offset + 2] = info.frame_count; + response[offset + 3] = info.frame_delay_ms; + } + } + Some(response) + } + Command::SaveConfig => { + let mut response: [u8; 32] = [0; 32]; + // Build config from current state + let config = crate::storage::StoredConfig { + brightness: state.brightness, + animation_period_us: state.animation_period as u32 / 1000, // Convert to ms then to config format + pwm_freq: state.pwm_freq as u8, + ..crate::storage::StoredConfig::default() + }; + response[0] = if crate::storage::save_config(&config) { + 0x00 + } else { + 0x01 + }; + Some(response) + } + Command::GetConfig => { + let mut response: [u8; 32] = [0; 32]; + let config = crate::storage::load_config(); + let bytes = config.to_bytes(); + response[..16].copy_from_slice(&bytes); + Some(response) + } + Command::ResetConfig => { + let mut response: [u8; 32] = [0; 32]; + response[0] = if crate::storage::config::reset_config() { + 0x00 + } else { + 0x01 + }; + Some(response) + } + Command::SetConfigValue(key, value) => { + let mut response: [u8; 32] = [0; 32]; + let mut config = crate::storage::load_config(); + match key { + ConfigKeyArg::Brightness => config.brightness = *value as u8, + ConfigKeyArg::SleepTimeout => config.sleep_timeout_secs = *value as u16, + ConfigKeyArg::AnimationPeriod => config.animation_period_us = *value, + ConfigKeyArg::PwmFreq => config.pwm_freq = *value as u8, + ConfigKeyArg::StartupAnimation => config.startup_animation = *value != 0, + ConfigKeyArg::StartupPattern => config.startup_pattern_idx = *value as u8, + } + response[0] = if crate::storage::save_config(&config) { + 0x00 + } else { + 0x01 + }; + Some(response) + } + Command::StageAnimationFrame(_frame_idx, _col_data) => { + // Animation staging is handled in main.rs with the staging buffer + // This command just acknowledges receipt + let mut response: [u8; 32] = [0; 32]; + response[0] = 0x00; // Ack + Some(response) + } + Command::CommitAnimation(_slot, _frame_delay_ms) => { + // Animation commit is handled in main.rs + let mut response: [u8; 32] = [0; 32]; + response[0] = 0x00; // Ack + Some(response) + } + Command::ClearAnimationStage => { + // Clear staging buffer - handled in main.rs + let mut response: [u8; 32] = [0; 32]; + response[0] = 0x00; + Some(response) + } _ => handle_generic_command(command), } } diff --git a/fl16-inputmodules/src/lib.rs b/fl16-inputmodules/src/lib.rs index 8c445376..a4715155 100644 --- a/fl16-inputmodules/src/lib.rs +++ b/fl16-inputmodules/src/lib.rs @@ -23,6 +23,8 @@ pub mod animations; pub mod matrix; #[cfg(feature = "ledmatrix")] pub mod patterns; +#[cfg(feature = "ledmatrix")] +pub mod storage; #[cfg(feature = "b1display")] pub mod graphics; diff --git a/fl16-inputmodules/src/storage/config.rs b/fl16-inputmodules/src/storage/config.rs new file mode 100644 index 00000000..c8b42d0e --- /dev/null +++ b/fl16-inputmodules/src/storage/config.rs @@ -0,0 +1,245 @@ +//! Configuration storage with simple wear leveling +//! +//! Uses a simple page rotation scheme for wear leveling. +//! Each config write goes to the next available slot in a page, +//! and pages are erased when full. + +use crate::storage::flash::FlashStorage; +use crate::storage::{CONFIG_STORAGE_ADDR, CONFIG_STORAGE_SIZE, PAGE_SIZE}; + +/// Configuration format version (for future migrations) +pub const CONFIG_VERSION: u8 = 1; + +/// Default brightness (0-255) +pub const DEFAULT_BRIGHTNESS: u8 = 51; + +/// Default sleep timeout in seconds (60 seconds) +pub const DEFAULT_SLEEP_TIMEOUT_SECS: u16 = 60; + +/// Default animation period in microseconds (31,250 = 32 FPS) +pub const DEFAULT_ANIMATION_PERIOD_US: u32 = 31_250; + +/// Default PWM frequency (0 = 29kHz) +pub const DEFAULT_PWM_FREQ: u8 = 0; + +/// No startup pattern selected (use built-in random animation) +pub const NO_STARTUP_PATTERN: u8 = 0xFF; + +/// Config entry size including header (must be power of 2 for alignment) +const CONFIG_ENTRY_SIZE: usize = 32; + +/// Magic bytes to identify valid config entry +const CONFIG_MAGIC: [u8; 2] = [0xCF, 0x01]; + +/// Stored configuration structure +/// +/// This is stored in flash and persists across power cycles. +/// Total size: 16 bytes (padded to 32 for entry) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct StoredConfig { + /// Format version for future migrations + pub version: u8, + /// Default brightness (0-255) + pub brightness: u8, + /// Sleep timeout in seconds (0 = disabled) + pub sleep_timeout_secs: u16, + /// Animation period in microseconds + pub animation_period_us: u32, + /// PWM frequency setting (0-3 maps to PwmFreqArg) + pub pwm_freq: u8, + /// Enable random startup animation + pub startup_animation: bool, + /// Pattern slot index for startup (0xFF = none, use random) + pub startup_pattern_idx: u8, + /// Reserved for future use + pub _reserved: [u8; 5], +} + +impl Default for StoredConfig { + fn default() -> Self { + Self { + version: CONFIG_VERSION, + brightness: DEFAULT_BRIGHTNESS, + sleep_timeout_secs: DEFAULT_SLEEP_TIMEOUT_SECS, + animation_period_us: DEFAULT_ANIMATION_PERIOD_US, + pwm_freq: DEFAULT_PWM_FREQ, + startup_animation: true, + startup_pattern_idx: NO_STARTUP_PATTERN, + _reserved: [0; 5], + } + } +} + +impl StoredConfig { + /// Serialize config to bytes for storage + pub fn to_bytes(&self) -> [u8; 16] { + let mut bytes = [0u8; 16]; + bytes[0] = self.version; + bytes[1] = self.brightness; + bytes[2..4].copy_from_slice(&self.sleep_timeout_secs.to_le_bytes()); + bytes[4..8].copy_from_slice(&self.animation_period_us.to_le_bytes()); + bytes[8] = self.pwm_freq; + bytes[9] = self.startup_animation as u8; + bytes[10] = self.startup_pattern_idx; + // bytes[11..16] = reserved + bytes + } + + /// Deserialize config from bytes + pub fn from_bytes(bytes: &[u8]) -> Option { + if bytes.len() < 16 { + return None; + } + + let version = bytes[0]; + if version != CONFIG_VERSION { + // Future: handle migration from older versions + return None; + } + + Some(Self { + version, + brightness: bytes[1], + sleep_timeout_secs: u16::from_le_bytes([bytes[2], bytes[3]]), + animation_period_us: u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + pwm_freq: bytes[8], + startup_animation: bytes[9] != 0, + startup_pattern_idx: bytes[10], + _reserved: [0; 5], + }) + } +} + +/// Find the most recent valid config entry in flash +fn find_latest_config() -> Option<(usize, StoredConfig)> { + let flash = FlashStorage::new(CONFIG_STORAGE_ADDR, CONFIG_STORAGE_SIZE as u32); + let entries_per_page = PAGE_SIZE / CONFIG_ENTRY_SIZE; + let total_pages = CONFIG_STORAGE_SIZE / PAGE_SIZE; + let total_entries = entries_per_page * total_pages; + + // Scan backwards to find the most recent valid entry + // (most recent writes go to higher addresses until page is erased) + let mut latest_offset = None; + let mut latest_config = None; + + for entry_idx in 0..total_entries { + let offset = entry_idx * CONFIG_ENTRY_SIZE; + let mut entry = [0u8; CONFIG_ENTRY_SIZE]; + flash.read_at(offset as u32, &mut entry); + + // Check magic bytes + if entry[0] == CONFIG_MAGIC[0] && entry[1] == CONFIG_MAGIC[1] { + // Check if this is a valid (non-invalidated) entry + // An invalidated entry has byte[2] == 0x00 + if entry[2] != 0x00 { + if let Some(config) = StoredConfig::from_bytes(&entry[4..20]) { + // Calculate simple checksum + let stored_checksum = u16::from_le_bytes([entry[20], entry[21]]); + let calc_checksum = calculate_checksum(&entry[4..20]); + if stored_checksum == calc_checksum { + latest_offset = Some(offset); + latest_config = Some(config); + } + } + } + } + } + + latest_offset.map(|off| (off, latest_config.unwrap())) +} + +/// Find the next free slot for writing config +fn find_free_slot() -> Option { + let flash = FlashStorage::new(CONFIG_STORAGE_ADDR, CONFIG_STORAGE_SIZE as u32); + let entries_per_page = PAGE_SIZE / CONFIG_ENTRY_SIZE; + let total_pages = CONFIG_STORAGE_SIZE / PAGE_SIZE; + let total_entries = entries_per_page * total_pages; + + for entry_idx in 0..total_entries { + let offset = entry_idx * CONFIG_ENTRY_SIZE; + let mut magic = [0u8; 2]; + flash.read_at(offset as u32, &mut magic); + + // Empty slot (erased flash = 0xFF) + if magic[0] == 0xFF && magic[1] == 0xFF { + return Some(offset); + } + } + + None +} + +/// Calculate simple checksum for config data +fn calculate_checksum(data: &[u8]) -> u16 { + let mut sum: u16 = 0; + for byte in data { + sum = sum.wrapping_add(*byte as u16); + } + sum ^ 0xA5A5 +} + +/// Load configuration from flash +/// +/// Returns the stored configuration or default if none exists or on error. +pub fn load_config() -> StoredConfig { + match find_latest_config() { + Some((_, config)) => config, + None => StoredConfig::default(), + } +} + +/// Save configuration to flash +/// +/// Returns true on success, false on error. +pub fn save_config(config: &StoredConfig) -> bool { + let mut flash = FlashStorage::new(CONFIG_STORAGE_ADDR, CONFIG_STORAGE_SIZE as u32); + + // Find a free slot + let offset = match find_free_slot() { + Some(off) => off, + None => { + // No free slot, need to erase a page + // For simplicity, erase the first page + if flash.erase_sector(0).is_err() { + return false; + } + 0 + } + }; + + // Build the entry + let mut entry = [0xFFu8; CONFIG_ENTRY_SIZE]; + entry[0] = CONFIG_MAGIC[0]; + entry[1] = CONFIG_MAGIC[1]; + entry[2] = 0x01; // Valid marker + entry[3] = 0x00; // Reserved + + let config_bytes = config.to_bytes(); + entry[4..20].copy_from_slice(&config_bytes); + + let checksum = calculate_checksum(&config_bytes); + entry[20..22].copy_from_slice(&checksum.to_le_bytes()); + + // Write the entry + flash.write_at(offset as u32, &entry).is_ok() +} + +/// Reset configuration to defaults +/// +/// Erases the config storage region and saves default config. +pub fn reset_config() -> bool { + let mut flash = FlashStorage::new(CONFIG_STORAGE_ADDR, CONFIG_STORAGE_SIZE as u32); + + // Erase all config pages + let total_pages = CONFIG_STORAGE_SIZE / PAGE_SIZE; + for page in 0..total_pages { + let page_offset = (page * PAGE_SIZE) as u32; + if flash.erase_sector(page_offset).is_err() { + return false; + } + } + + // Save default config + save_config(&StoredConfig::default()) +} diff --git a/fl16-inputmodules/src/storage/flash.rs b/fl16-inputmodules/src/storage/flash.rs new file mode 100644 index 00000000..afe67bb8 --- /dev/null +++ b/fl16-inputmodules/src/storage/flash.rs @@ -0,0 +1,141 @@ +//! Low-level flash operations for RP2040 +//! +//! Uses RP2040 ROM functions for flash erase and program operations. +//! Flash operations must be performed with interrupts disabled. + +use rp2040_hal::rom_data; + +/// Flash base address (XIP region) +pub const FLASH_BASE: u32 = 0x1000_0000; + +/// RP2040 flash page size (256 bytes for programming) +const FLASH_PAGE_SIZE: usize = 256; + +/// RP2040 flash sector size (4KB for erasing) +const FLASH_SECTOR_SIZE: usize = 4096; + +/// Total flash size (2MB) +const FLASH_SIZE: usize = 2 * 1024 * 1024; + +/// Flash storage wrapper for a specific region +pub struct FlashStorage { + /// Start offset from FLASH_BASE + start_offset: u32, + /// Size of this storage region + size: u32, +} + +impl FlashStorage { + /// Create a new flash storage region + /// + /// # Arguments + /// * `start_addr` - Absolute start address (must be >= FLASH_BASE) + /// * `size` - Size of the storage region in bytes + /// + /// # Panics + /// Panics if start_addr is below FLASH_BASE or region exceeds flash size + pub fn new(start_addr: u32, size: u32) -> Self { + let start_offset = start_addr - FLASH_BASE; + assert!(start_addr >= FLASH_BASE); + assert!((start_offset + size) as usize <= FLASH_SIZE); + + Self { start_offset, size } + } + + /// Get the absolute address for a given offset within this region + fn absolute_addr(&self, offset: u32) -> u32 { + FLASH_BASE + self.start_offset + offset + } + + /// Get the flash offset for a given offset within this region + fn flash_offset(&self, offset: u32) -> u32 { + self.start_offset + offset + } + + /// Read bytes from flash at the given offset + /// + /// Flash is memory-mapped, so this is a direct memory read. + pub fn read_at(&self, offset: u32, bytes: &mut [u8]) { + if offset + bytes.len() as u32 > self.size { + return; + } + + let addr = self.absolute_addr(offset); + let ptr = addr as *const u8; + + // Flash is memory-mapped, just read directly + for (i, byte) in bytes.iter_mut().enumerate() { + *byte = unsafe { *ptr.add(i) }; + } + } + + /// Write bytes to flash at the given offset + /// + /// Data is written in 256-byte pages. If the data doesn't align to page + /// boundaries, it will be padded with 0xFF (erased state). + /// + /// # Important + /// The flash region must be erased before writing. + /// Writing to non-erased flash may produce incorrect data. + pub fn write_at(&mut self, offset: u32, bytes: &[u8]) -> Result<(), FlashError> { + if offset + bytes.len() as u32 > self.size { + return Err(FlashError::OutOfBounds); + } + + let flash_offset = self.flash_offset(offset); + + // Prepare a page-aligned buffer + let mut page_buf = [0xFFu8; FLASH_PAGE_SIZE]; + let data_len = bytes.len().min(FLASH_PAGE_SIZE); + page_buf[..data_len].copy_from_slice(&bytes[..data_len]); + + // Flash operations must be done with interrupts disabled + cortex_m::interrupt::free(|_| unsafe { + rom_data::flash_range_program(flash_offset, page_buf.as_ptr(), FLASH_PAGE_SIZE); + }); + + Ok(()) + } + + /// Erase a sector (4KB) at the given offset + /// + /// The offset must be sector-aligned (multiple of 4096). + pub fn erase_sector(&mut self, offset: u32) -> Result<(), FlashError> { + if offset + FLASH_SECTOR_SIZE as u32 > self.size { + return Err(FlashError::OutOfBounds); + } + + // Must be sector-aligned + if offset as usize % FLASH_SECTOR_SIZE != 0 { + return Err(FlashError::NotAligned); + } + + let flash_offset = self.flash_offset(offset); + + // Flash operations must be done with interrupts disabled + cortex_m::interrupt::free(|_| unsafe { + rom_data::flash_range_erase( + flash_offset, + FLASH_SECTOR_SIZE, + FLASH_SECTOR_SIZE as u32, + 0, + ); + }); + + Ok(()) + } + + /// Get the capacity of this storage region + pub fn capacity(&self) -> usize { + self.size as usize + } +} + +/// Flash operation error +#[derive(Debug)] +pub enum FlashError { + /// Operation would exceed storage region bounds + OutOfBounds, + /// Offset not properly aligned + NotAligned, +} diff --git a/fl16-inputmodules/src/storage/mod.rs b/fl16-inputmodules/src/storage/mod.rs new file mode 100644 index 00000000..24c843ec --- /dev/null +++ b/fl16-inputmodules/src/storage/mod.rs @@ -0,0 +1,39 @@ +//! Flash storage for persistent configuration and patterns +//! +//! This module provides wear-leveled flash storage for: +//! - Configuration settings (brightness, sleep timeout, etc.) +//! - Custom startup patterns (static frames or animations) +//! +//! Flash layout (from memory.x): +//! - PATTERN_STORAGE: 0x100E0000, 60KB (15 x 4KB pages) +//! - CONFIG_STORAGE: 0x100EF000, 64KB (16 x 4KB pages) +//! - SERIALNUM: 0x100FF000, 4KB (read-only, untouched) + +pub mod config; +pub mod flash; +pub mod patterns; + +pub use config::{load_config, save_config, StoredConfig}; +pub use flash::FlashStorage; +pub use patterns::{ + delete_pattern, list_patterns, load_pattern, save_pattern, PatternHeader, PatternSlotInfo, + StoredPattern, MAX_ANIMATION_FRAMES, MAX_PATTERN_SLOTS, +}; + +/// Flash page size for RP2040 (4KB) +pub const PAGE_SIZE: usize = 4096; + +/// Flash sector size for RP2040 (4KB, same as page) +pub const SECTOR_SIZE: usize = 4096; + +/// Pattern storage region start address +pub const PATTERN_STORAGE_ADDR: u32 = 0x100E_0000; + +/// Pattern storage region size (60KB = 15 pages) +pub const PATTERN_STORAGE_SIZE: usize = 60 * 1024; + +/// Config storage region start address +pub const CONFIG_STORAGE_ADDR: u32 = 0x100E_F000; + +/// Config storage region size (64KB = 16 pages) +pub const CONFIG_STORAGE_SIZE: usize = 64 * 1024; diff --git a/fl16-inputmodules/src/storage/patterns.rs b/fl16-inputmodules/src/storage/patterns.rs new file mode 100644 index 00000000..f0d4ef97 --- /dev/null +++ b/fl16-inputmodules/src/storage/patterns.rs @@ -0,0 +1,405 @@ +//! Pattern storage for custom startup patterns +//! +//! Stores static frames or short animations in flash for use as startup patterns. +//! Each pattern slot can hold a single frame or an animation up to MAX_ANIMATION_FRAMES. +//! +//! ## Wear Leveling +//! +//! Pattern storage uses a simple wear-leveling scheme to extend flash life: +//! - Each slot has space for multiple pattern entries +//! - New patterns are written to the next empty position (sequential write) +//! - A sequence number tracks which entry is the most recent +//! - When the slot is full, it's erased and writing starts fresh +//! +//! This reduces erase cycles since we only erase when the slot fills up, +//! not on every write. + +use crate::matrix::{Grid, HEIGHT, WIDTH}; +use crate::storage::flash::FlashStorage; +use crate::storage::{PAGE_SIZE, PATTERN_STORAGE_ADDR, PATTERN_STORAGE_SIZE}; + +/// Maximum number of pattern slots +pub const MAX_PATTERN_SLOTS: usize = 8; + +/// Maximum frames per animation +pub const MAX_ANIMATION_FRAMES: usize = 16; + +/// Size of a single frame (9 * 34 = 306 bytes) +pub const FRAME_SIZE: usize = WIDTH * HEIGHT; + +/// Pages per pattern slot (enough for header + 16 frames) +/// 16 frames * 306 bytes = 4896 bytes, so we need 2 pages (8KB) per slot +const PAGES_PER_SLOT: usize = 2; + +/// Size of each pattern slot +const SLOT_SIZE: usize = PAGES_PER_SLOT * PAGE_SIZE; + +/// Size of a single pattern entry (header + 1 frame, aligned to 512 bytes) +/// This allows ~16 entries per slot before needing to erase +const ENTRY_SIZE: usize = 512; + +/// Maximum entries per slot for wear leveling (8KB / 512 = 16) +const MAX_ENTRIES_PER_SLOT: usize = SLOT_SIZE / ENTRY_SIZE; + +/// Pattern type discriminator +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum PatternType { + /// Single static frame + Static = 0, + /// Multi-frame animation + Animation = 1, +} + +/// Magic bytes to identify valid pattern header +const PATTERN_MAGIC: [u8; 2] = [0xAA, 0x01]; + +/// Pattern header stored in flash (32 bytes) +#[derive(Clone, Copy, Debug)] +#[repr(C)] +pub struct PatternHeader { + /// Pattern slot index (0-7) + pub slot: u8, + /// Pattern type (static or animation) + pub pattern_type: PatternType, + /// Number of frames (1 for static, 2-16 for animation) + pub frame_count: u8, + /// Frame delay in milliseconds (for animations) + pub frame_delay_ms: u8, + /// CRC16 of the frame data for validation + pub data_crc: u16, + /// Sequence number for wear leveling (higher = newer) + pub sequence: u16, +} + +impl PatternHeader { + /// Serialize header to bytes (10 bytes of data, padded to 32) + pub fn to_bytes(&self) -> [u8; 32] { + let mut bytes = [0xFFu8; 32]; + bytes[0] = PATTERN_MAGIC[0]; + bytes[1] = PATTERN_MAGIC[1]; + bytes[2] = self.slot; + bytes[3] = self.pattern_type as u8; + bytes[4] = self.frame_count; + bytes[5] = self.frame_delay_ms; + bytes[6..8].copy_from_slice(&self.data_crc.to_le_bytes()); + bytes[8..10].copy_from_slice(&self.sequence.to_le_bytes()); + bytes + } + + /// Deserialize header from bytes + pub fn from_bytes(bytes: &[u8]) -> Option { + if bytes.len() < 32 { + return None; + } + + // Check magic bytes + if bytes[0] != PATTERN_MAGIC[0] || bytes[1] != PATTERN_MAGIC[1] { + return None; + } + + let pattern_type = match bytes[3] { + 0 => PatternType::Static, + 1 => PatternType::Animation, + _ => return None, + }; + + Some(Self { + slot: bytes[2], + pattern_type, + frame_count: bytes[4], + frame_delay_ms: bytes[5], + data_crc: u16::from_le_bytes([bytes[6], bytes[7]]), + sequence: u16::from_le_bytes([bytes[8], bytes[9]]), + }) + } +} + +/// Stored pattern with header and frame data +pub struct StoredPattern { + pub header: PatternHeader, + pub frames: [[u8; FRAME_SIZE]; MAX_ANIMATION_FRAMES], +} + +impl StoredPattern { + /// Create a new static pattern from a grid + pub fn from_grid(slot: u8, grid: &Grid, sequence: u16) -> Self { + let mut frames = [[0u8; FRAME_SIZE]; MAX_ANIMATION_FRAMES]; + + // Flatten the grid into the first frame + for x in 0..WIDTH { + for y in 0..HEIGHT { + frames[0][x * HEIGHT + y] = grid.0[x][y]; + } + } + + let crc = calculate_crc(&frames[0]); + + Self { + header: PatternHeader { + slot, + pattern_type: PatternType::Static, + frame_count: 1, + frame_delay_ms: 0, + data_crc: crc, + sequence, + }, + frames, + } + } + + /// Get the first frame as a Grid + pub fn first_frame(&self) -> Grid { + self.frame_to_grid(0) + } + + /// Convert a frame index to a Grid + pub fn frame_to_grid(&self, index: usize) -> Grid { + let mut grid = Grid::default(); + if index < self.header.frame_count as usize { + for x in 0..WIDTH { + for y in 0..HEIGHT { + grid.0[x][y] = self.frames[index][x * HEIGHT + y]; + } + } + } + grid + } + + /// Check if this is an animation + pub fn is_animation(&self) -> bool { + self.header.pattern_type == PatternType::Animation + } +} + +/// Information about a pattern slot (for listing) +#[derive(Clone, Copy, Debug, Default)] +pub struct PatternSlotInfo { + /// Whether the slot is occupied + pub occupied: bool, + /// Pattern type (if occupied) + pub pattern_type: u8, + /// Number of frames + pub frame_count: u8, + /// Frame delay in milliseconds + pub frame_delay_ms: u8, +} + +/// Calculate simple CRC16 for frame data +fn calculate_crc(data: &[u8]) -> u16 { + let mut sum: u16 = 0; + for byte in data { + sum = sum.wrapping_add(*byte as u16); + } + sum ^ 0xB5B5 +} + +/// Get the flash offset for a pattern slot +fn slot_offset(slot: u8) -> u32 { + (slot as usize * SLOT_SIZE) as u32 +} + +/// Get the flash offset for a specific entry within a slot +fn entry_offset(slot: u8, entry: usize) -> u32 { + slot_offset(slot) + (entry * ENTRY_SIZE) as u32 +} + +/// Find the latest valid entry in a slot (highest sequence number) +/// Returns (entry_index, sequence_number) or None if slot is empty +fn find_latest_entry(flash: &FlashStorage, slot: u8) -> Option<(usize, u16)> { + let mut latest: Option<(usize, u16)> = None; + + for entry in 0..MAX_ENTRIES_PER_SLOT { + let offset = entry_offset(slot, entry); + let mut header_bytes = [0u8; 32]; + flash.read_at(offset, &mut header_bytes); + + if let Some(header) = PatternHeader::from_bytes(&header_bytes) { + if header.slot == slot { + match latest { + None => latest = Some((entry, header.sequence)), + Some((_, seq)) if header.sequence > seq => { + latest = Some((entry, header.sequence)); + } + _ => {} + } + } + } + } + + latest +} + +/// Find the next empty entry in a slot +/// Returns entry index or None if slot is full +fn find_empty_entry(flash: &FlashStorage, slot: u8) -> Option { + for entry in 0..MAX_ENTRIES_PER_SLOT { + let offset = entry_offset(slot, entry); + let mut magic = [0u8; 2]; + flash.read_at(offset, &mut magic); + + // Empty entry has 0xFF magic bytes + if magic[0] == 0xFF && magic[1] == 0xFF { + return Some(entry); + } + } + None +} + +/// Save a pattern to flash +/// +/// Saves the current grid as a static pattern in the specified slot. +/// Uses wear leveling - writes to next empty entry, only erases when full. +pub fn save_pattern(slot: u8, grid: &Grid) -> bool { + if slot >= MAX_PATTERN_SLOTS as u8 { + return false; + } + + let mut flash = FlashStorage::new(PATTERN_STORAGE_ADDR, PATTERN_STORAGE_SIZE as u32); + + // Find current sequence number (if any pattern exists) + let next_sequence = match find_latest_entry(&flash, slot) { + Some((_, seq)) => seq.wrapping_add(1), + None => 1, + }; + + // Find empty entry, or erase slot if full + let entry_idx = match find_empty_entry(&flash, slot) { + Some(idx) => idx, + None => { + // Slot is full, erase and start fresh + let base_offset = slot_offset(slot); + for page in 0..PAGES_PER_SLOT { + let page_offset = base_offset + (page * PAGE_SIZE) as u32; + if flash.erase_sector(page_offset).is_err() { + return false; + } + } + 0 // Start at first entry after erase + } + }; + + let pattern = StoredPattern::from_grid(slot, grid, next_sequence); + save_pattern_at_entry(&mut flash, &pattern, entry_idx) +} + +fn save_pattern_at_entry(flash: &mut FlashStorage, pattern: &StoredPattern, entry: usize) -> bool { + let offset = entry_offset(pattern.header.slot, entry); + + // Write header (first 32 bytes) + let header_bytes = pattern.header.to_bytes(); + if flash.write_at(offset, &header_bytes).is_err() { + return false; + } + + // Write frame data starting at offset 32 + // For static patterns, we have 306 bytes of frame data + // Entry size is 512, so we have room for header (32) + frame (306) = 338 bytes + let frame_offset = offset + 32; + + // Write first 256 bytes of frame + if flash.write_at(frame_offset, &pattern.frames[0][..256]).is_err() { + return false; + } + + // Write remaining 50 bytes (306 - 256 = 50) + // Pad to 256 for write alignment + let mut remaining = [0xFFu8; 256]; + remaining[..50].copy_from_slice(&pattern.frames[0][256..]); + if flash.write_at(frame_offset + 256, &remaining).is_err() { + return false; + } + + true +} + +/// Load a pattern from flash +/// +/// Finds the latest valid entry in the slot and loads it. +pub fn load_pattern(slot: u8) -> Option { + if slot >= MAX_PATTERN_SLOTS as u8 { + return None; + } + + let flash = FlashStorage::new(PATTERN_STORAGE_ADDR, PATTERN_STORAGE_SIZE as u32); + + // Find the latest entry + let (entry_idx, _) = find_latest_entry(&flash, slot)?; + let offset = entry_offset(slot, entry_idx); + + // Read header + let mut header_bytes = [0u8; 32]; + flash.read_at(offset, &mut header_bytes); + + let header = PatternHeader::from_bytes(&header_bytes)?; + + // Read frame data + let mut frames = [[0u8; FRAME_SIZE]; MAX_ANIMATION_FRAMES]; + let frame_offset = offset + 32; + + // Read first 256 bytes + flash.read_at(frame_offset, &mut frames[0][..256]); + + // Read remaining 50 bytes + let mut remaining = [0u8; 256]; + flash.read_at(frame_offset + 256, &mut remaining); + frames[0][256..].copy_from_slice(&remaining[..50]); + + // Verify CRC + let calc_crc = calculate_crc(&frames[0]); + if calc_crc != header.data_crc { + return None; + } + + Some(StoredPattern { header, frames }) +} + +/// Delete a pattern from a slot +/// +/// Erases all entries in the slot. +pub fn delete_pattern(slot: u8) -> bool { + if slot >= MAX_PATTERN_SLOTS as u8 { + return false; + } + + let mut flash = FlashStorage::new(PATTERN_STORAGE_ADDR, PATTERN_STORAGE_SIZE as u32); + let base_offset = slot_offset(slot); + + // Erase the slot's pages + for page in 0..PAGES_PER_SLOT { + let page_offset = base_offset + (page * PAGE_SIZE) as u32; + if flash.erase_sector(page_offset).is_err() { + return false; + } + } + + true +} + +/// List all pattern slots +/// +/// Finds the latest entry in each slot to report status. +pub fn list_patterns() -> [PatternSlotInfo; MAX_PATTERN_SLOTS] { + let mut result = [PatternSlotInfo::default(); MAX_PATTERN_SLOTS]; + let flash = FlashStorage::new(PATTERN_STORAGE_ADDR, PATTERN_STORAGE_SIZE as u32); + + for slot in 0..MAX_PATTERN_SLOTS as u8 { + // Find the latest entry in this slot + if let Some((entry_idx, _)) = find_latest_entry(&flash, slot) { + let offset = entry_offset(slot, entry_idx); + let mut header_bytes = [0u8; 32]; + flash.read_at(offset, &mut header_bytes); + + if let Some(header) = PatternHeader::from_bytes(&header_bytes) { + result[slot as usize] = PatternSlotInfo { + occupied: true, + pattern_type: header.pattern_type as u8, + frame_count: header.frame_count, + frame_delay_ms: header.frame_delay_ms, + }; + } + } + } + + result +} diff --git a/inputmodule-control/src/inputmodule.rs b/inputmodule-control/src/inputmodule.rs index d2d4e83c..59c7adeb 100644 --- a/inputmodule-control/src/inputmodule.rs +++ b/inputmodule-control/src/inputmodule.rs @@ -50,6 +50,15 @@ enum Command { PwmFreq = 0x1E, DebugMode = 0x1F, Version = 0x20, + // Flash storage commands + SavePattern = 0x30, + LoadPattern = 0x31, + DeletePattern = 0x32, + ListPatterns = 0x33, + SaveConfig = 0x34, + GetConfig = 0x35, + ResetConfig = 0x36, + SetConfigValue = 0x37, } enum GameControlArg { @@ -246,6 +255,49 @@ pub fn serial_commands(args: &crate::ClapCli) { if ledmatrix_args.version { get_device_version(serialdev); } + + // Flash storage commands + if let Some(slot) = ledmatrix_args.save_pattern { + save_pattern_cmd(serialdev, slot); + } + if let Some(slot) = ledmatrix_args.load_pattern { + load_pattern_cmd(serialdev, slot); + } + if let Some(slot) = ledmatrix_args.delete_pattern { + delete_pattern_cmd(serialdev, slot); + } + if ledmatrix_args.list_patterns { + list_patterns_cmd(serialdev); + } + if ledmatrix_args.save_config { + save_config_cmd(serialdev); + } + if ledmatrix_args.get_config { + get_config_cmd(serialdev); + } + if ledmatrix_args.reset_config { + reset_config_cmd(serialdev); + } + if let Some(brightness) = ledmatrix_args.set_default_brightness { + set_config_value_cmd( + serialdev, + ConfigKey::DefaultBrightness, + brightness as u16, + ); + } + if let Some(timeout) = ledmatrix_args.set_sleep_timeout { + set_config_value_cmd(serialdev, ConfigKey::SleepTimeout, timeout); + } + if let Some(slot) = ledmatrix_args.set_startup_pattern { + set_config_value_cmd(serialdev, ConfigKey::StartupPattern, slot as u16); + } + if let Some(enable) = ledmatrix_args.set_startup_animation { + set_config_value_cmd( + serialdev, + ConfigKey::StartupAnimation, + if enable { 1 } else { 0 }, + ); + } } // Commands that block and need manual looping if ledmatrix_args.blinking { @@ -1182,3 +1234,229 @@ fn b1_display_pattern(serialdev: &str, pattern: B1Pattern) { B1Pattern::White => b1_display_color(serialdev, false), } } + +// Flash storage commands + +/// Config key identifiers for SetConfigValue command +#[derive(Clone, Copy)] +#[repr(u8)] +enum ConfigKey { + DefaultBrightness = 0x01, + SleepTimeout = 0x02, + StartupPattern = 0x03, + StartupAnimation = 0x04, +} + +fn save_pattern_cmd(serialdev: &str, slot: u8) { + if slot > 7 { + println!("Error: Pattern slot must be 0-7"); + return; + } + + let mut port = serialport::new(serialdev, 115_200) + .timeout(Duration::from_millis(500)) // Flash operations take longer + .open() + .expect("Failed to open port"); + + simple_cmd_port(&mut port, Command::SavePattern, &[slot]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + if response[0] == 1 { + println!("Pattern saved to slot {}", slot); + } else { + println!("Failed to save pattern to slot {}", slot); + } +} + +fn load_pattern_cmd(serialdev: &str, slot: u8) { + if slot > 7 { + println!("Error: Pattern slot must be 0-7"); + return; + } + + let mut port = serialport::new(serialdev, 115_200) + .timeout(Duration::from_millis(500)) + .open() + .expect("Failed to open port"); + + simple_cmd_port(&mut port, Command::LoadPattern, &[slot]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + if response[0] == 1 { + println!("Pattern loaded from slot {}", slot); + } else { + println!("No pattern found in slot {}", slot); + } +} + +fn delete_pattern_cmd(serialdev: &str, slot: u8) { + if slot > 7 { + println!("Error: Pattern slot must be 0-7"); + return; + } + + let mut port = serialport::new(serialdev, 115_200) + .timeout(Duration::from_millis(500)) + .open() + .expect("Failed to open port"); + + simple_cmd_port(&mut port, Command::DeletePattern, &[slot]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + if response[0] == 1 { + println!("Pattern deleted from slot {}", slot); + } else { + println!("Failed to delete pattern from slot {}", slot); + } +} + +fn list_patterns_cmd(serialdev: &str) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + simple_cmd_port(&mut port, Command::ListPatterns, &[]); + + // Response: 8 x 4 bytes (occupied, type, frame_count, delay_ms) + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + println!("Pattern slots:"); + for slot in 0..8 { + let base = slot * 4; + let occupied = response[base] == 1; + if occupied { + let pattern_type = if response[base + 1] == 0 { + "static" + } else { + "animation" + }; + let frame_count = response[base + 2]; + let delay_ms = response[base + 3]; + if frame_count > 1 { + println!( + " Slot {}: {} ({} frames, {}ms delay)", + slot, pattern_type, frame_count, delay_ms + ); + } else { + println!(" Slot {}: {}", slot, pattern_type); + } + } else { + println!(" Slot {}: empty", slot); + } + } +} + +fn save_config_cmd(serialdev: &str) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(Duration::from_millis(500)) + .open() + .expect("Failed to open port"); + + simple_cmd_port(&mut port, Command::SaveConfig, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + if response[0] == 1 { + println!("Configuration saved to flash"); + } else { + println!("Failed to save configuration"); + } +} + +fn get_config_cmd(serialdev: &str) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + simple_cmd_port(&mut port, Command::GetConfig, &[]); + + // Response: StoredConfig struct (16 bytes) + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let version = response[0]; + let brightness = response[1]; + let sleep_timeout = u16::from_le_bytes([response[2], response[3]]); + let animation_period = u32::from_le_bytes([response[4], response[5], response[6], response[7]]); + let pwm_freq = response[8]; + let startup_animation = response[9] == 1; + let startup_pattern = response[10]; + + println!("Stored Configuration (v{}):", version); + println!(" Default brightness: {}", brightness); + println!(" Sleep timeout: {}s (0 = disabled)", sleep_timeout); + println!(" Animation period: {}us", animation_period); + println!(" PWM frequency index: {}", pwm_freq); + println!(" Startup animation: {}", startup_animation); + if startup_pattern == 0xFF { + println!(" Startup pattern: none"); + } else { + println!(" Startup pattern: slot {}", startup_pattern); + } +} + +fn reset_config_cmd(serialdev: &str) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(Duration::from_millis(500)) + .open() + .expect("Failed to open port"); + + simple_cmd_port(&mut port, Command::ResetConfig, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + if response[0] == 1 { + println!("Configuration reset to defaults"); + } else { + println!("Failed to reset configuration"); + } +} + +fn set_config_value_cmd(serialdev: &str, key: ConfigKey, value: u16) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(Duration::from_millis(500)) + .open() + .expect("Failed to open port"); + + let value_bytes = value.to_le_bytes(); + simple_cmd_port( + &mut port, + Command::SetConfigValue, + &[key as u8, value_bytes[0], value_bytes[1]], + ); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let key_name = match key { + ConfigKey::DefaultBrightness => "default brightness", + ConfigKey::SleepTimeout => "sleep timeout", + ConfigKey::StartupPattern => "startup pattern", + ConfigKey::StartupAnimation => "startup animation", + }; + + if response[0] == 1 { + println!("Set {} and saved to flash", key_name); + } else { + println!("Failed to set {}", key_name); + } +} diff --git a/inputmodule-control/src/ledmatrix.rs b/inputmodule-control/src/ledmatrix.rs index 30ae5750..dab3c085 100644 --- a/inputmodule-control/src/ledmatrix.rs +++ b/inputmodule-control/src/ledmatrix.rs @@ -143,4 +143,49 @@ pub struct LedMatrixSubcommand { /// Get the device version #[arg(short, long)] pub version: bool, + + // Flash storage commands + /// Save current display to flash pattern slot (0-7) + #[arg(long)] + pub save_pattern: Option, + + /// Load pattern from flash slot (0-7) + #[arg(long)] + pub load_pattern: Option, + + /// Delete pattern from flash slot (0-7) + #[arg(long)] + pub delete_pattern: Option, + + /// List all saved patterns + #[arg(long)] + pub list_patterns: bool, + + /// Save current settings to flash + #[arg(long)] + pub save_config: bool, + + /// Get stored config from flash + #[arg(long)] + pub get_config: bool, + + /// Reset config to defaults + #[arg(long)] + pub reset_config: bool, + + /// Set default brightness (0-255) and save to flash + #[arg(long)] + pub set_default_brightness: Option, + + /// Set sleep timeout in seconds (0 = disabled) and save to flash + #[arg(long)] + pub set_sleep_timeout: Option, + + /// Set startup pattern slot (0-7, or 255 for none) + #[arg(long)] + pub set_startup_pattern: Option, + + /// Enable/disable startup animation and save to flash + #[arg(long)] + pub set_startup_animation: Option, } diff --git a/ledmatrix/src/main.rs b/ledmatrix/src/main.rs index c4082c72..e9e611c6 100644 --- a/ledmatrix/src/main.rs +++ b/ledmatrix/src/main.rs @@ -146,6 +146,7 @@ use fl16_inputmodules::games::{pong, snake}; use fl16_inputmodules::matrix::*; use fl16_inputmodules::patterns::*; use fl16_inputmodules::serialnum::{device_release, get_serialnum}; +use fl16_inputmodules::storage; // FRA - Framwork // KDE - C1 LED Matrix @@ -230,34 +231,50 @@ fn main() -> ! { let dip1 = pins.dip1.into_pull_up_input(); + // Load stored configuration from flash + let stored_config = storage::load_config(); + + // Convert stored PWM frequency to PwmFreqArg + let pwm_freq = match stored_config.pwm_freq { + 0 => PwmFreqArg::P29k, + 1 => PwmFreqArg::P3k6, + 2 => PwmFreqArg::P1k8, + 3 => PwmFreqArg::P900, + _ => PwmFreqArg::P29k, + }; + let mut state = LedmatrixState { grid: percentage(0), col_buffer: Grid::default(), animate: false, - brightness: 51, // Default to 51/255 = 20% brightness + brightness: stored_config.brightness, sleeping: SleepState::Awake, game: None, - animation_period: 31_250, // 31,250 us = 32 FPS - pwm_freq: PwmFreqArg::P29k, + animation_period: stored_config.animation_period_us as u64, + pwm_freq, debug_mode: false, upcoming_frames: None, }; state.debug_mode = dip1.is_low().unwrap(); - if show_startup_animation(&state) { - state.upcoming_frames = Some(match get_random_byte(&rosc) % 8 { - 0 => Animation::Percentage(StartupPercentageIterator::default()), - 1 => Animation::ZigZag(ZigZagIterator::default()), - 2 => Animation::Gof(GameOfLifeIterator::new(GameOfLifeStartParam::Pattern1, 200)), - 3 => Animation::Gof(GameOfLifeIterator::new( - GameOfLifeStartParam::BeaconToadBlinker, - 128, - )), - 4 => Animation::Gof(GameOfLifeIterator::new(GameOfLifeStartParam::Glider, 128)), - 5 => Animation::Breathing(BreathingIterator::default()), - 6 => Animation::Pong(PongIterator::default()), - 7 => Animation::Snake(SnakeIterator::default()), - _ => unreachable!(), - }); + + // Check if a saved startup pattern should be displayed + let has_startup_pattern = stored_config.startup_pattern_idx != storage::config::NO_STARTUP_PATTERN; + + if has_startup_pattern { + // Try to load the saved startup pattern + if let Some(pattern) = storage::load_pattern(stored_config.startup_pattern_idx) { + state.grid = pattern.first_frame(); + // TODO: If pattern is an animation, set up upcoming_frames + } else { + // Pattern slot is empty, fall back to default behavior + if show_startup_animation(&state) && stored_config.startup_animation { + state.upcoming_frames = Some(random_startup_animation(&rosc)); + } else { + state.grid = percentage(100); + } + } + } else if show_startup_animation(&state) && stored_config.startup_animation { + state.upcoming_frames = Some(random_startup_animation(&rosc)); } else { // If no startup animation, keep display always on state.grid = percentage(100); @@ -585,6 +602,23 @@ fn show_startup_animation(state: &LedmatrixState) -> bool { STARTUP_ANIMATION && !debug_mode(state) } +fn random_startup_animation(rosc: &RingOscillator) -> Animation { + match get_random_byte(rosc) % 8 { + 0 => Animation::Percentage(StartupPercentageIterator::default()), + 1 => Animation::ZigZag(ZigZagIterator::default()), + 2 => Animation::Gof(GameOfLifeIterator::new(GameOfLifeStartParam::Pattern1, 200)), + 3 => Animation::Gof(GameOfLifeIterator::new( + GameOfLifeStartParam::BeaconToadBlinker, + 128, + )), + 4 => Animation::Gof(GameOfLifeIterator::new(GameOfLifeStartParam::Glider, 128)), + 5 => Animation::Breathing(BreathingIterator::default()), + 6 => Animation::Pong(PongIterator::default()), + 7 => Animation::Snake(SnakeIterator::default()), + _ => Animation::Percentage(StartupPercentageIterator::default()), + } +} + fn assign_sleep_reason( previous: Option, current: Option, diff --git a/memory.x b/memory.x index 070eac78..0c9ae631 100644 --- a/memory.x +++ b/memory.x @@ -1,9 +1,24 @@ MEMORY { BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 - FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 + /* Firmware code - reduced to leave room for storage before serial number */ + FLASH : ORIGIN = 0x10000100, LENGTH = 896K - 0x100 + /* Pattern storage: 60KB (15 x 4KB pages) for ~8 pattern slots */ + PATTERN_STORAGE : ORIGIN = 0x100E0000, LENGTH = 60K + /* Config storage: 64KB (16 x 4KB pages) for wear-leveled configuration */ + CONFIG_STORAGE : ORIGIN = 0x100EF000, LENGTH = 64K + /* Serial number - programmed at manufacturing, read-only */ + SERIALNUM : ORIGIN = 0x100FF000, LENGTH = 4K RAM : ORIGIN = 0x20000000, LENGTH = 256K } +/* + * Note: The FLASH region is sized to leave room for: + * - PATTERN_STORAGE at 0x100E0000 (60KB) + * - CONFIG_STORAGE at 0x100EF000 (64KB) + * - SERIALNUM at 0x100FF000 (4KB, read-only) + * If firmware grows too large, the linker will error. + */ + EXTERN(BOOT2_FIRMWARE) SECTIONS { diff --git a/python/inputmodule/inputmodule/__init__.py b/python/inputmodule/inputmodule/__init__.py index 7fb67f8b..5051c036 100644 --- a/python/inputmodule/inputmodule/__init__.py +++ b/python/inputmodule/inputmodule/__init__.py @@ -37,6 +37,15 @@ class CommandVals(IntEnum): PwmFreq = 0x1E DebugMode = 0x1F Version = 0x20 + # Flash storage commands + SavePattern = 0x30 + LoadPattern = 0x31 + DeletePattern = 0x32 + ListPatterns = 0x33 + SaveConfig = 0x34 + GetConfig = 0x35 + ResetConfig = 0x36 + SetConfigValue = 0x37 class Game(IntEnum): diff --git a/python/inputmodule/inputmodule/ledmatrix.py b/python/inputmodule/inputmodule/ledmatrix.py index 7b351bdc..5e44c3ee 100644 --- a/python/inputmodule/inputmodule/ledmatrix.py +++ b/python/inputmodule/inputmodule/ledmatrix.py @@ -10,9 +10,18 @@ FWK_MAGIC, send_serial, brightness, + RESPONSE_SIZE, ) from inputmodule.gui.gui_threading import get_status, set_status + +class ConfigKey: + """Config key identifiers for SetConfigValue command""" + DefaultBrightness = 0x01 + SleepTimeout = 0x02 + StartupPattern = 0x03 + StartupAnimation = 0x04 + WIDTH = 9 HEIGHT = 34 PATTERNS = [ @@ -456,3 +465,174 @@ def show_symbols(dev, symbols): font_items.append(s) show_font(dev, font_items) + + +# Flash storage commands + +def save_pattern(dev, slot: int): + """Save current display to flash pattern slot (0-7)""" + if slot < 0 or slot > 7: + print("Error: Pattern slot must be 0-7") + return False + res = send_command(dev, CommandVals.SavePattern, [slot], with_response=True) + if res and res[0] == 1: + print(f"Pattern saved to slot {slot}") + return True + else: + print(f"Failed to save pattern to slot {slot}") + return False + + +def load_pattern(dev, slot: int): + """Load pattern from flash slot (0-7)""" + if slot < 0 or slot > 7: + print("Error: Pattern slot must be 0-7") + return False + res = send_command(dev, CommandVals.LoadPattern, [slot], with_response=True) + if res and res[0] == 1: + print(f"Pattern loaded from slot {slot}") + return True + else: + print(f"No pattern found in slot {slot}") + return False + + +def delete_pattern(dev, slot: int): + """Delete pattern from flash slot (0-7)""" + if slot < 0 or slot > 7: + print("Error: Pattern slot must be 0-7") + return False + res = send_command(dev, CommandVals.DeletePattern, [slot], with_response=True) + if res and res[0] == 1: + print(f"Pattern deleted from slot {slot}") + return True + else: + print(f"Failed to delete pattern from slot {slot}") + return False + + +def list_patterns(dev): + """List all pattern slots""" + res = send_command(dev, CommandVals.ListPatterns, with_response=True) + if not res: + print("Failed to get pattern list") + return None + + patterns = [] + print("Pattern slots:") + for slot in range(8): + base = slot * 4 + occupied = res[base] == 1 + if occupied: + pattern_type = "static" if res[base + 1] == 0 else "animation" + frame_count = res[base + 2] + delay_ms = res[base + 3] + if frame_count > 1: + print(f" Slot {slot}: {pattern_type} ({frame_count} frames, {delay_ms}ms delay)") + else: + print(f" Slot {slot}: {pattern_type}") + patterns.append({ + 'slot': slot, + 'occupied': True, + 'type': pattern_type, + 'frame_count': frame_count, + 'delay_ms': delay_ms + }) + else: + print(f" Slot {slot}: empty") + patterns.append({'slot': slot, 'occupied': False}) + return patterns + + +def save_config(dev): + """Save current settings to flash""" + res = send_command(dev, CommandVals.SaveConfig, with_response=True) + if res and res[0] == 1: + print("Configuration saved to flash") + return True + else: + print("Failed to save configuration") + return False + + +def get_config(dev): + """Get stored config from flash""" + res = send_command(dev, CommandVals.GetConfig, with_response=True) + if not res: + print("Failed to get config") + return None + + config = { + 'version': res[0], + 'brightness': res[1], + 'sleep_timeout': int.from_bytes(res[2:4], 'little'), + 'animation_period_us': int.from_bytes(res[4:8], 'little'), + 'pwm_freq': res[8], + 'startup_animation': res[9] == 1, + 'startup_pattern': res[10] if res[10] != 0xFF else None + } + + print(f"Stored Configuration (v{config['version']}):") + print(f" Default brightness: {config['brightness']}") + print(f" Sleep timeout: {config['sleep_timeout']}s (0 = disabled)") + print(f" Animation period: {config['animation_period_us']}us") + print(f" PWM frequency index: {config['pwm_freq']}") + print(f" Startup animation: {config['startup_animation']}") + if config['startup_pattern'] is None: + print(" Startup pattern: none") + else: + print(f" Startup pattern: slot {config['startup_pattern']}") + + return config + + +def reset_config(dev): + """Reset config to defaults""" + res = send_command(dev, CommandVals.ResetConfig, with_response=True) + if res and res[0] == 1: + print("Configuration reset to defaults") + return True + else: + print("Failed to reset configuration") + return False + + +def set_config_value(dev, key: int, value: int): + """Set a config value and save to flash""" + value_bytes = value.to_bytes(2, 'little') + res = send_command(dev, CommandVals.SetConfigValue, [key, value_bytes[0], value_bytes[1]], with_response=True) + + key_names = { + ConfigKey.DefaultBrightness: "default brightness", + ConfigKey.SleepTimeout: "sleep timeout", + ConfigKey.StartupPattern: "startup pattern", + ConfigKey.StartupAnimation: "startup animation", + } + key_name = key_names.get(key, f"config key {key}") + + if res and res[0] == 1: + print(f"Set {key_name} and saved to flash") + return True + else: + print(f"Failed to set {key_name}") + return False + + +def set_default_brightness(dev, brightness_val: int): + """Set default brightness (0-255) and save to flash""" + return set_config_value(dev, ConfigKey.DefaultBrightness, brightness_val) + + +def set_sleep_timeout(dev, timeout_secs: int): + """Set sleep timeout in seconds (0 = disabled) and save to flash""" + return set_config_value(dev, ConfigKey.SleepTimeout, timeout_secs) + + +def set_startup_pattern(dev, slot: int): + """Set startup pattern slot (0-7, or 255 for none) and save to flash""" + return set_config_value(dev, ConfigKey.StartupPattern, slot) + + +def set_startup_animation(dev, enable: bool): + """Enable/disable startup animation and save to flash""" + return set_config_value(dev, ConfigKey.StartupAnimation, 1 if enable else 0)