From af0f8759f40a44060b7b95ba0ec81b98b26e4003 Mon Sep 17 00:00:00 2001 From: Nathan Bradshaw Date: Thu, 16 Jul 2026 22:35:30 -0700 Subject: [PATCH 1/2] Add vibrato on/off toggle & increase dead zone - Add VIBRATO_SETTING menu item to enable/disable vibrato via CC1 (off by default) - Increase VIBRATO_DEAD_ZONE from 30 to 100 to prevent accidental triggering - Display vibrato as ON/OFF instead of numeric value in settings menu - Only send vibrato CC1 when enabled and settings menu is closed - Fix Embed.toml chip identifier to "STM32H750VBTx" - Add LM317L linear regulator circuit to schematic for clean 3.3V rail - Update .gitignore to exclude KiCad temporary files --- .gitignore | 2 + keyboard_keyboard/code/Embed.toml | 2 +- keyboard_keyboard/code/src/constants.rs | 3 +- keyboard_keyboard/code/src/display/screens.rs | 8 +- keyboard_keyboard/code/src/main.rs | 7 +- keyboard_keyboard/code/src/settings.rs | 14 +- keyboard_keyboard/kicad/#auto_saved_files# | 1 - .../kicad/keyboard_keyboard.kicad_sch | 1260 ++++++++++++++++- .../kicad/~keyboard_keyboard.kicad_pcb.lck | 1 - 9 files changed, 1236 insertions(+), 62 deletions(-) delete mode 100644 keyboard_keyboard/kicad/#auto_saved_files# delete mode 100644 keyboard_keyboard/kicad/~keyboard_keyboard.kicad_pcb.lck diff --git a/.gitignore b/.gitignore index a299ea0..8496804 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ keyboard_keyboard/kicad/~keyboard_keyboard.kicad_sch.lck .DS_Store keyboard_keyboard/.DS_Store .DS_Store +keyboard_keyboard/kicad/\#auto_saved_files\# +keyboard_keyboard/kicad/~keyboard_keyboard.kicad_pcb.lck diff --git a/keyboard_keyboard/code/Embed.toml b/keyboard_keyboard/code/Embed.toml index b7606e4..ae308d0 100644 --- a/keyboard_keyboard/code/Embed.toml +++ b/keyboard_keyboard/code/Embed.toml @@ -1,5 +1,5 @@ [default.general] -chip = "stm32h750v" +chip = "STM32H750VBTx" [default.rtt] enabled = true diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index d27de3a..c5118d4 100644 --- a/keyboard_keyboard/code/src/constants.rs +++ b/keyboard_keyboard/code/src/constants.rs @@ -74,7 +74,8 @@ pub const PITCH_BEND_INTERVAL_MS: u32 = 5; pub const VIBRATO_A: usize = 76; // HE77 → vibrato depth pub const VIBRATO_B: usize = 78; // HE79 → vibrato depth pub const VIBRATO_MAX_DELTA: u16 = 300; -pub const VIBRATO_DEAD_ZONE: u16 = 30; // noise floor below which output = 0 +pub const VIBRATO_DEAD_ZONE: u16 = 100; // press threshold below which output = 0 + // (was 30 — vibrato triggered far too easily) pub const VIBRATO_HYSTERESIS: u8 = 2; pub const VIBRATO_INTERVAL_MS: u32 = 10; diff --git a/keyboard_keyboard/code/src/display/screens.rs b/keyboard_keyboard/code/src/display/screens.rs index 7f34454..412e4d6 100644 --- a/keyboard_keyboard/code/src/display/screens.rs +++ b/keyboard_keyboard/code/src/display/screens.rs @@ -168,9 +168,13 @@ pub fn draw_settings(disp: &mut LcdDisplay, selected: usize, settings: &Settings .draw(disp) .ok(); - // Value: right-aligned + // Value: right-aligned (the vibrato toggle shows ON/OFF, not 0/1) let mut val_str: String<8> = String::new(); - write!(val_str, "{}", value).ok(); + if item_idx == crate::settings::VIBRATO_SETTING { + write!(val_str, "{}", if value != 0 { "ON" } else { "OFF" }).ok(); + } else { + write!(val_str, "{}", value).ok(); + } Text::with_alignment( val_str.as_str(), Point::new(127, y), diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index c79df75..4bfe7e8 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -435,7 +435,7 @@ mod app { recalibrate_show_until: u32 = 0, recalibrate_flashing: bool = false], shared = [tick_ms, switch_states, baselines, event_queue, midi_tx_flag, splash_done, - settings_active, recalibrate_pending, display_state], + settings_active, settings, recalibrate_pending, display_state], priority = 15 )] fn timer_handler(mut ctx: timer_handler::Context) { @@ -459,6 +459,7 @@ mod app { // touched by per-tick tracking. let anchor_baselines = ctx.shared.baselines.lock(|b| *b); let settings_active = ctx.shared.settings_active.lock(|a| *a); + let vibrato_enabled = ctx.shared.settings.lock(|s| s.vibrato_enabled); let mut pending: heapless::Vec<(usize, SwitchEvent), 32> = heapless::Vec::new(); // ── ADC scan ────────────────────────────────────────────────────────── @@ -593,8 +594,8 @@ mod app { ); } - // ── Vibrato → CC1 (dead zone + rate-limited, only when settings closed) ─ - if !settings_active && now % VIBRATO_INTERVAL_MS == 0 { + // ── Vibrato → CC1 (dead zone + rate-limited, only when enabled & settings closed) ─ + if vibrato_enabled && !settings_active && now % VIBRATO_INTERVAL_MS == 0 { let max_delta = vib_filt_a .abs_diff(ctx.local.dynamic_baselines[VIBRATO_A]) .max(vib_filt_b.abs_diff(ctx.local.dynamic_baselines[VIBRATO_B])) diff --git a/keyboard_keyboard/code/src/settings.rs b/keyboard_keyboard/code/src/settings.rs index 1ebaf1d..7392bf5 100644 --- a/keyboard_keyboard/code/src/settings.rs +++ b/keyboard_keyboard/code/src/settings.rs @@ -1,4 +1,7 @@ -pub const NUM_SETTINGS_ITEMS: usize = 6; +pub const NUM_SETTINGS_ITEMS: usize = 7; + +/// Menu index of the VIBRATO on/off toggle (rendered as ON/OFF, not a number). +pub const VIBRATO_SETTING: usize = 6; pub struct SettingsItem { pub name: &'static str, @@ -37,6 +40,11 @@ pub const SETTINGS_ITEMS: [SettingsItem; NUM_SETTINGS_ITEMS] = [ min: 0, max: 127, }, + SettingsItem { + name: "VIBRATO", + min: 0, + max: 1, + }, ]; #[derive(Clone, Copy, Debug)] @@ -47,6 +55,7 @@ pub struct Settings { pub pitch_bend_range: u8, // semitones 1–12 pub melody_program: u8, // 0–127, sent as PC on melody channel when settings closes pub drum_program: u8, // 0–127, sent as PC on drum channel when settings closes + pub vibrato_enabled: bool, // off by default — vibrato keys send CC1 only when on } impl Settings { @@ -58,6 +67,7 @@ impl Settings { pitch_bend_range: 2, melody_program: 0, drum_program: 0, + vibrato_enabled: false, } } @@ -70,6 +80,7 @@ impl Settings { 3 => self.pitch_bend_range as i16, 4 => self.melody_program as i16, 5 => self.drum_program as i16, + 6 => self.vibrato_enabled as i16, _ => 0, } } @@ -85,6 +96,7 @@ impl Settings { 3 => self.pitch_bend_range = v as u8, 4 => self.melody_program = v as u8, 5 => self.drum_program = v as u8, + 6 => self.vibrato_enabled = v != 0, _ => {} } } diff --git a/keyboard_keyboard/kicad/#auto_saved_files# b/keyboard_keyboard/kicad/#auto_saved_files# deleted file mode 100644 index 4bc4875..0000000 --- a/keyboard_keyboard/kicad/#auto_saved_files# +++ /dev/null @@ -1 +0,0 @@ -/Users/nathanbradshaw/Documents/GitHub/keyboard-keyboard/keyboard_keyboard/kicad/_autosave-keyboard_keyboard.kicad_sch diff --git a/keyboard_keyboard/kicad/keyboard_keyboard.kicad_sch b/keyboard_keyboard/kicad/keyboard_keyboard.kicad_sch index 47cbc8c..9dee8ab 100644 --- a/keyboard_keyboard/kicad/keyboard_keyboard.kicad_sch +++ b/keyboard_keyboard/kicad/keyboard_keyboard.kicad_sch @@ -1704,6 +1704,167 @@ ) (embedded_fonts no) ) + (symbol "Device:C_Polarized" + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "C_Polarized" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarized capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "cap capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "CP_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "C_Polarized_0_1" + (rectangle + (start -2.286 0.508) + (end 2.286 1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.778 2.286) (xy -0.762 2.286) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.27 2.794) (xy -1.27 1.778) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 2.286 -0.508) + (end -2.286 -1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type outline) + ) + ) + ) + (symbol "C_Polarized_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) (symbol "Device:LED" (pin_numbers (hide yes) @@ -4360,45 +4521,41 @@ ) (embedded_fonts no) ) - (symbol "power:+3.3VA" - (power) - (pin_numbers - (hide yes) - ) + (symbol "Regulator_Linear:LM317L_SOT-89" (pin_names - (offset 0) - (hide yes) + (offset 0.254) ) (exclude_from_sim no) (in_bom yes) (on_board yes) - (property "Reference" "#PWR" - (at 0 -3.81 0) + (property "Reference" "U" + (at -3.81 3.175 0) (effects (font (size 1.27 1.27) ) - (hide yes) ) ) - (property "Value" "+3.3VA" - (at 0 3.556 0) + (property "Value" "LM317L_SOT-89" + (at 0 3.175 0) (effects (font (size 1.27 1.27) ) + (justify left) ) ) - (property "Footprint" "" - (at 0 0 0) + (property "Footprint" "Package_TO_SOT_SMD:SOT-89-3" + (at 0 6.35 0) (effects (font (size 1.27 1.27) + (italic yes) ) (hide yes) ) ) - (property "Datasheet" "" + (property "Datasheet" "http://www.ti.com/lit/ds/symlink/lm317l.pdf" (at 0 0 0) (effects (font @@ -4407,7 +4564,7 @@ (hide yes) ) ) - (property "Description" "Power symbol creates a global label with name \"+3.3VA\"" + (property "Description" "100mA 35V Adjustable Linear Regulator, SOT-89" (at 0 0 0) (effects (font @@ -4416,7 +4573,7 @@ (hide yes) ) ) - (property "ki_keywords" "global power" + (property "ki_keywords" "Adjustable Voltage Regulator 1A Positive" (at 0 0 0) (effects (font @@ -4425,56 +4582,76 @@ (hide yes) ) ) - (symbol "+3.3VA_0_1" - (polyline - (pts - (xy -0.762 1.27) (xy 0 2.54) + (property "ki_fp_filters" "SOT?89*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) ) + (hide yes) + ) + ) + (symbol "LM317L_SOT-89_0_1" + (rectangle + (start -5.08 1.905) + (end 5.08 -5.08) (stroke - (width 0) + (width 0.254) (type default) ) (fill - (type none) + (type background) ) ) - (polyline - (pts - (xy 0 2.54) (xy 0.762 1.27) - ) - (stroke - (width 0) - (type default) + ) + (symbol "LM317L_SOT-89_1_1" + (pin power_in line + (at -7.62 0 0) + (length 2.54) + (name "VI" + (effects + (font + (size 1.27 1.27) + ) + ) ) - (fill - (type none) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) ) ) - (polyline - (pts - (xy 0 0) (xy 0 2.54) - ) - (stroke - (width 0) - (type default) + (pin input line + (at 0 -7.62 90) + (length 2.54) + (name "ADJ" + (effects + (font + (size 1.27 1.27) + ) + ) ) - (fill - (type none) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) ) ) - ) - (symbol "+3.3VA_1_1" - (pin power_in line - (at 0 0 90) - (length 0) - (name "~" + (pin power_out line + (at 7.62 0 180) + (length 2.54) + (name "VO" (effects (font (size 1.27 1.27) ) ) ) - (number "1" + (number "2" (effects (font (size 1.27 1.27) @@ -4485,7 +4662,7 @@ ) (embedded_fonts no) ) - (symbol "power:GND" + (symbol "power:+3.3VA" (power) (pin_numbers (hide yes) @@ -4498,7 +4675,7 @@ (in_bom yes) (on_board yes) (property "Reference" "#PWR" - (at 0 -6.35 0) + (at 0 -3.81 0) (effects (font (size 1.27 1.27) @@ -4506,8 +4683,133 @@ (hide yes) ) ) - (property "Value" "GND" - (at 0 -3.81 0) + (property "Value" "+3.3VA" + (at 0 3.556 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3VA\"" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "+3.3VA_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+3.3VA_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "power:GND" + (power) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) (effects (font (size 1.27 1.27) @@ -5137,6 +5439,12 @@ (color 0 0 0 0) (uuid "2beb8731-7cfc-4258-b966-b40bd5b04071") ) + (junction + (at 247.65 584.2) + (diameter 0) + (color 0 0 0 0) + (uuid "2c16eec2-0ffc-4b5e-b083-688de790a10d") + ) (junction (at 551.18 186.69) (diameter 0) @@ -5209,6 +5517,12 @@ (color 0 0 0 0) (uuid "3647e7b1-972b-4678-b389-85819a3b601c") ) + (junction + (at 234.95 551.18) + (diameter 0) + (color 0 0 0 0) + (uuid "3778349a-c6a3-4682-a9bc-f0fb7e46b685") + ) (junction (at 444.5 107.95) (diameter 0) @@ -5473,6 +5787,12 @@ (color 0 0 0 0) (uuid "5efa6a41-266b-47b5-8332-4023b9536e5d") ) + (junction + (at 222.25 560.07) + (diameter 0) + (color 0 0 0 0) + (uuid "6453b3e9-6808-47f7-9436-31e5b85d662b") + ) (junction (at 906.78 120.65) (diameter 0) @@ -5521,6 +5841,12 @@ (color 0 0 0 0) (uuid "6c4d7459-65a5-4325-832e-b88e22800ada") ) + (junction + (at 247.65 551.18) + (diameter 0) + (color 0 0 0 0) + (uuid "6cdb689b-b107-4c42-9464-5e1bb5f88584") + ) (junction (at 628.65 209.55) (diameter 0) @@ -5593,6 +5919,12 @@ (color 0 0 0 0) (uuid "7ecb93ca-1f2b-4bde-9f9e-c2c3be93d3f5") ) + (junction + (at 222.25 584.2) + (diameter 0) + (color 0 0 0 0) + (uuid "7f136f22-84de-4d9d-9858-c9b2e8f70837") + ) (junction (at 791.21 127) (diameter 0) @@ -5677,6 +6009,12 @@ (color 0 0 0 0) (uuid "8a05e2af-b60e-4d70-800e-ebbb9efcc6cb") ) + (junction + (at 204.47 551.18) + (diameter 0) + (color 0 0 0 0) + (uuid "8ba42fd1-9721-498d-a77a-73f44e48d31f") + ) (junction (at 829.31 127) (diameter 0) @@ -5905,6 +6243,12 @@ (color 0 0 0 0) (uuid "ab33170d-6f90-41a3-b9ed-8322413fb555") ) + (junction + (at 260.35 551.18) + (diameter 0) + (color 0 0 0 0) + (uuid "acafaccd-4f2e-489c-8342-bb02f3276d85") + ) (junction (at 304.8 107.95) (diameter 0) @@ -6997,6 +7341,16 @@ ) (uuid "0eefa4b8-6bdf-4ffb-bde2-b5764c4db216") ) + (wire + (pts + (xy 204.47 584.2) (xy 222.25 584.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "0f04bd86-4f74-4078-aeb2-ef4609aedbc3") + ) (wire (pts (xy 1027.43 121.92) (xy 1027.43 120.65) @@ -7177,6 +7531,16 @@ ) (uuid "1270a316-ceb8-4b27-b357-c11ddd1baef7") ) + (wire + (pts + (xy 222.25 560.07) (xy 222.25 561.34) + ) + (stroke + (width 0) + (type default) + ) + (uuid "1277cf03-e940-41f9-ac87-028adf7f4553") + ) (wire (pts (xy 398.78 116.84) (xy 411.48 116.84) @@ -7287,6 +7651,16 @@ ) (uuid "140d47be-1071-4d9f-8501-28cb628766e2") ) + (wire + (pts + (xy 260.35 571.5) (xy 260.35 584.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "141f4363-1a1f-4444-b3c9-d2e1dcf85a3b") + ) (wire (pts (xy 906.78 148.59) (xy 911.86 148.59) @@ -7797,6 +8171,16 @@ ) (uuid "1dafbebb-cacb-4629-b791-8be31352d109") ) + (wire + (pts + (xy 204.47 551.18) (xy 214.63 551.18) + ) + (stroke + (width 0) + (type default) + ) + (uuid "1ddc8d74-7464-400a-bb55-862e53bb3e81") + ) (wire (pts (xy 271.78 154.94) (xy 284.48 154.94) @@ -7847,6 +8231,16 @@ ) (uuid "1eecc4fd-7cab-4276-a972-1b21733d67a1") ) + (wire + (pts + (xy 260.35 584.2) (xy 247.65 584.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "1eef17f4-4351-422e-abe6-3d89476f15ec") + ) (wire (pts (xy 491.49 176.53) (xy 491.49 177.8) @@ -8127,6 +8521,16 @@ ) (uuid "25cc1853-016b-44fb-89f7-b84d1cfd1eee") ) + (wire + (pts + (xy 234.95 558.8) (xy 234.95 560.07) + ) + (stroke + (width 0) + (type default) + ) + (uuid "26275739-775d-4234-9628-9aa0b54a4e6b") + ) (wire (pts (xy 414.02 251.46) (xy 414.02 252.73) @@ -8287,6 +8691,16 @@ ) (uuid "2a21ee44-746c-45e3-b354-6ccaaa30d324") ) + (wire + (pts + (xy 222.25 568.96) (xy 222.25 572.77) + ) + (stroke + (width 0) + (type default) + ) + (uuid "2a4e75be-d912-4808-aeab-7657e5908157") + ) (wire (pts (xy 755.65 342.9) (xy 759.46 342.9) @@ -8687,6 +9101,16 @@ ) (uuid "35181544-31f0-49d9-b623-670912f89468") ) + (wire + (pts + (xy 234.95 551.18) (xy 247.65 551.18) + ) + (stroke + (width 0) + (type default) + ) + (uuid "35a2c3d8-7fb3-4283-97f9-cafbf1d18f75") + ) (wire (pts (xy 400.05 209.55) (xy 405.13 209.55) @@ -9627,6 +10051,16 @@ ) (uuid "4acfa322-5d58-4c01-9973-2ff847f55cf3") ) + (wire + (pts + (xy 247.65 551.18) (xy 247.65 563.88) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4af9fb1b-3a03-4ff6-abfc-939e8112d86e") + ) (wire (pts (xy 46.99 177.8) (xy 46.99 179.07) @@ -9737,6 +10171,16 @@ ) (uuid "4d8fd71f-8ca4-4874-a215-c2ba48c1b978") ) + (wire + (pts + (xy 222.25 580.39) (xy 222.25 584.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4d922138-32c0-473e-92f4-3a4fd7cdcd03") + ) (wire (pts (xy 806.45 181.61) (xy 806.45 182.88) @@ -9807,6 +10251,16 @@ ) (uuid "4f3b66de-61f3-48fe-8659-ccb1aa93f7f3") ) + (wire + (pts + (xy 222.25 558.8) (xy 222.25 560.07) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4fc31792-800f-4661-b5c9-c54fe3d05d11") + ) (wire (pts (xy 680.72 285.75) (xy 687.07 285.75) @@ -10207,6 +10661,16 @@ ) (uuid "576b2598-88b2-4b9d-a62e-bad517a103fb") ) + (wire + (pts + (xy 247.65 551.18) (xy 260.35 551.18) + ) + (stroke + (width 0) + (type default) + ) + (uuid "579930e2-19f8-42d4-8527-5aebbe093bda") + ) (wire (pts (xy 138.43 107.95) (xy 138.43 109.22) @@ -10257,6 +10721,16 @@ ) (uuid "58b1dc37-943e-4575-a792-d9432b25fc90") ) + (wire + (pts + (xy 229.87 551.18) (xy 234.95 551.18) + ) + (stroke + (width 0) + (type default) + ) + (uuid "58baf667-1ffd-4238-a037-449b6cc6e37c") + ) (wire (pts (xy 659.13 106.68) (xy 659.13 107.95) @@ -10487,6 +10961,16 @@ ) (uuid "5e5056ed-e05e-4763-bbe5-4e197e8479b4") ) + (wire + (pts + (xy 204.47 551.18) (xy 204.47 558.8) + ) + (stroke + (width 0) + (type default) + ) + (uuid "5e531168-a76f-48cc-84e0-fb764b897298") + ) (wire (pts (xy 306.07 147.32) (xy 306.07 146.05) @@ -11247,6 +11731,16 @@ ) (uuid "6d422e0c-f1b2-45d3-91f7-aff49bf88b9c") ) + (wire + (pts + (xy 260.35 551.18) (xy 260.35 563.88) + ) + (stroke + (width 0) + (type default) + ) + (uuid "6d71c9a5-c380-4592-ae3b-5645ae35243f") + ) (wire (pts (xy 793.75 182.88) (xy 806.45 182.88) @@ -12307,6 +12801,16 @@ ) (uuid "855008f6-d09b-4005-961f-0d92571f434a") ) + (wire + (pts + (xy 204.47 566.42) (xy 204.47 584.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "859b26f2-6ef8-49cd-b154-417fcec99467") + ) (wire (pts (xy 673.1 283.21) (xy 689.61 283.21) @@ -13417,6 +13921,16 @@ ) (uuid "a35dbac6-d6f0-4b32-a127-ac4bc312b0b6") ) + (wire + (pts + (xy 222.25 584.2) (xy 247.65 584.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "a3644d9e-86fd-4ecb-9862-90fadf8d4086") + ) (wire (pts (xy 317.5 414.02) (xy 320.04 414.02) @@ -15017,6 +15531,16 @@ ) (uuid "c844058e-0504-4da1-84d0-505672c65aec") ) + (wire + (pts + (xy 260.35 551.18) (xy 265.43 551.18) + ) + (stroke + (width 0) + (type default) + ) + (uuid "c846e777-6afc-46d6-ab42-b6252bd83a16") + ) (wire (pts (xy 985.52 200.66) (xy 990.6 200.66) @@ -15077,6 +15601,16 @@ ) (uuid "c97119d9-65cb-4b17-a921-8b35c8887d9b") ) + (wire + (pts + (xy 201.93 551.18) (xy 204.47 551.18) + ) + (stroke + (width 0) + (type default) + ) + (uuid "c9ce96bf-f5d7-4115-82f1-c3a173dad499") + ) (wire (pts (xy 647.7 152.4) (xy 647.7 153.67) @@ -16127,6 +16661,16 @@ ) (uuid "e154ba22-f72e-4888-bca1-f3dfd7ffb1fe") ) + (wire + (pts + (xy 247.65 571.5) (xy 247.65 584.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e17a787e-768e-482b-939a-cd8211f51ccf") + ) (wire (pts (xy 317.5 186.69) (xy 330.2 186.69) @@ -16567,6 +17111,16 @@ ) (uuid "ea7a181b-8f1f-44c9-9ac8-3b3f7a144f0a") ) + (wire + (pts + (xy 222.25 560.07) (xy 234.95 560.07) + ) + (stroke + (width 0) + (type default) + ) + (uuid "ea868d87-8266-4e6c-8002-4f965059bad5") + ) (wire (pts (xy 906.78 176.53) (xy 911.86 176.53) @@ -22539,6 +23093,28 @@ ) ) ) + (global_label "5v" + (shape input) + (at 201.93 551.18 180) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + (uuid "a8fd1790-05de-47b9-92f3-0bef37f09d0c") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 196.7677 551.18 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + (hide yes) + ) + ) + ) (global_label "HE63" (shape input) (at 236.22 243.84 90) @@ -23947,6 +24523,28 @@ ) ) ) + (global_label "vout" + (shape input) + (at 265.43 551.18 0) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + (uuid "ddd98805-ce88-4cb5-9389-06f4df46347e") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 272.4065 551.18 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + (hide yes) + ) + ) + ) (global_label "HE66" (shape input) (at 118.11 379.73 180) @@ -36078,6 +36676,76 @@ ) ) ) + (symbol + (lib_id "Device:C") + (at 204.47 562.61 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "2b3d9405-8409-4a1a-940a-8d293b666b60") + (property "Reference" "C1_" + (at 208.28 561.3399 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "0.1uf" + (at 208.28 563.8799 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 205.4352 566.42 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 204.47 562.61 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 204.47 562.61 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "33081c3f-f1c7-4efe-8934-bda9db83cbd0") + ) + (pin "2" + (uuid "8f452228-e1cf-4cfc-96e6-89efc2154bda") + ) + (instances + (project "keyboard_keyboard" + (path "/a04a08f6-0aff-4526-8b14-085b944c1df1" + (reference "C1_") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "Device:C") (at 114.3 151.13 0) @@ -49284,6 +49952,78 @@ ) ) ) + (symbol + (lib_id "Regulator_Linear:LM317L_SOT-89") + (at 222.25 551.18 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "68305e15-8cae-494a-aaa9-f5d2c867de7e") + (property "Reference" "U3" + (at 222.25 544.83 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "LM317T" + (at 222.25 547.37 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Package_TO_SOT_SMD:SOT-89-3" + (at 222.25 544.83 0) + (effects + (font + (size 1.27 1.27) + (italic yes) + ) + (hide yes) + ) + ) + (property "Datasheet" "http://www.ti.com/lit/ds/symlink/lm317l.pdf" + (at 222.25 551.18 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "100mA 35V Adjustable Linear Regulator, SOT-89" + (at 222.25 551.18 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "735dd26b-8bdd-4ac0-bf38-bbf3ae2634e3") + ) + (pin "3" + (uuid "71a37191-dfe9-402d-98f9-01ca4d7b5db5") + ) + (pin "2" + (uuid "7c8dcd07-536c-4ddd-91ac-92341886c227") + ) + (instances + (project "keyboard_keyboard" + (path "/a04a08f6-0aff-4526-8b14-085b944c1df1" + (reference "U3") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "power:+3.3VA") (at 351.79 176.53 0) @@ -56823,6 +57563,76 @@ ) ) ) + (symbol + (lib_id "Device:C_Polarized") + (at 260.35 567.69 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "8ea7ec3c-8858-472e-b7df-c353f9ee2085") + (property "Reference" "C153" + (at 264.16 565.5309 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "220uf" + (at 264.16 568.0709 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 261.3152 571.5 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 260.35 567.69 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarized capacitor" + (at 260.35 567.69 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "2e3306f0-6c4b-474a-a011-0bf768d6bee9") + ) + (pin "2" + (uuid "296d94d4-1a98-4433-b9e7-93e2b028bcd2") + ) + (instances + (project "keyboard_keyboard" + (path "/a04a08f6-0aff-4526-8b14-085b944c1df1" + (reference "C153") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "power:GND") (at 568.96 292.1 0) @@ -64921,6 +65731,76 @@ ) ) ) + (symbol + (lib_id "Device:R_US") + (at 222.25 565.15 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "b53fe0f6-d62f-4e3a-8ebf-8fdd4575b5a1") + (property "Reference" "R1_" + (at 224.79 563.8799 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "330" + (at 224.79 566.4199 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 223.266 565.404 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 222.25 565.15 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, US symbol" + (at 222.25 565.15 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "8a21daf9-773d-4b47-80f3-12347b6b6a31") + ) + (pin "1" + (uuid "b6af1990-0a06-4729-afb8-06cbf7cee242") + ) + (instances + (project "keyboard_keyboard" + (path "/a04a08f6-0aff-4526-8b14-085b944c1df1" + (reference "R1_") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "Device:C") (at 607.06 113.03 0) @@ -65729,6 +66609,72 @@ ) ) ) + (symbol + (lib_id "power:GND") + (at 222.25 584.2 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "b8c27738-560d-4fe2-bdca-bd8f7935eaed") + (property "Reference" "#PWR0312" + (at 222.25 590.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 222.25 589.28 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 222.25 584.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 222.25 584.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 222.25 584.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "b59c666e-d3f8-4641-a4af-d1edf8bafc64") + ) + (instances + (project "keyboard_keyboard" + (path "/a04a08f6-0aff-4526-8b14-085b944c1df1" + (reference "#PWR0312") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "Device:C") (at 377.19 182.88 0) @@ -69740,6 +70686,76 @@ ) ) ) + (symbol + (lib_id "Device:R_US") + (at 234.95 554.99 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "c9075752-e840-47e1-9935-d98bbd019467") + (property "Reference" "R2_" + (at 237.49 553.7199 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "220" + (at 237.49 556.2599 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 235.966 555.244 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 234.95 554.99 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, US symbol" + (at 234.95 554.99 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "7aca2d27-68b1-4e20-9999-c94ce6295224") + ) + (pin "1" + (uuid "3f81cfdb-bc43-4d72-b11e-79c2c9294c0f") + ) + (instances + (project "keyboard_keyboard" + (path "/a04a08f6-0aff-4526-8b14-085b944c1df1" + (reference "R2_") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "power:+3.3VA") (at 944.88 147.32 0) @@ -73025,6 +74041,76 @@ ) ) ) + (symbol + (lib_id "Device:R_US") + (at 222.25 576.58 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "d85cfd36-3ad8-4267-a1ae-b98a1e3fb218") + (property "Reference" "R10" + (at 224.79 575.3099 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "33" + (at 224.79 577.8499 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 223.266 576.834 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 222.25 576.58 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, US symbol" + (at 222.25 576.58 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "22f03db4-cc7e-4bfd-8b70-f0c91445c72f") + ) + (pin "1" + (uuid "0cc64079-5666-41a1-a9e8-53b0ca1d861a") + ) + (instances + (project "keyboard_keyboard" + (path "/a04a08f6-0aff-4526-8b14-085b944c1df1" + (reference "R10") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "Device:C") (at 91.44 182.88 0) @@ -78945,6 +80031,76 @@ ) ) ) + (symbol + (lib_id "Device:C_Polarized") + (at 247.65 567.69 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "f3f771c9-c773-465e-868e-ce3833cd8444") + (property "Reference" "C152" + (at 251.46 565.5309 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "1uf" + (at 251.46 568.0709 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 248.6152 571.5 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 247.65 567.69 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarized capacitor" + (at 247.65 567.69 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "4330859d-9ab6-4be6-8461-bba0015e335c") + ) + (pin "2" + (uuid "7bcaa45c-08e6-4cb7-ad4e-06d27b66c93a") + ) + (instances + (project "keyboard_keyboard" + (path "/a04a08f6-0aff-4526-8b14-085b944c1df1" + (reference "C152") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "Device:C") (at 306.07 248.92 0) diff --git a/keyboard_keyboard/kicad/~keyboard_keyboard.kicad_pcb.lck b/keyboard_keyboard/kicad/~keyboard_keyboard.kicad_pcb.lck deleted file mode 100644 index 5244761..0000000 --- a/keyboard_keyboard/kicad/~keyboard_keyboard.kicad_pcb.lck +++ /dev/null @@ -1 +0,0 @@ -{"hostname":"Mac","username":"nathanbradshaw"} \ No newline at end of file From 638fd11cd76eb85c1402c337b0e7c885f664c862 Mon Sep 17 00:00:00 2001 From: Nathan Bradshaw Date: Fri, 17 Jul 2026 13:40:14 -0700 Subject: [PATCH 2/2] Add persistent settings storage and factory reset Implement dual-slot QSPI flash storage for settings with CRC validation to safely persist changes across power cycles. Add emergency factory reset feature: holding Settings (HE74) and All Notes Off (HE76) keys together for 3 seconds triggers a reset to defaults. Add "RESET DEFAULTS" menu item for user-initiated factory reset. Settings are automatically saved when the settings menu closes. Load saved settings on boot, falling back to defaults if flash is empty or corrupted. Change default melody channel from 0 to 2. --- keyboard_keyboard/code/src/display/screens.rs | 2 + keyboard_keyboard/code/src/main.rs | 100 ++++++++++++-- keyboard_keyboard/code/src/settings.rs | 23 ++-- .../code/src/settings_storage.rs | 126 ++++++++++++++++++ 4 files changed, 232 insertions(+), 19 deletions(-) create mode 100644 keyboard_keyboard/code/src/settings_storage.rs diff --git a/keyboard_keyboard/code/src/display/screens.rs b/keyboard_keyboard/code/src/display/screens.rs index 412e4d6..e9730e7 100644 --- a/keyboard_keyboard/code/src/display/screens.rs +++ b/keyboard_keyboard/code/src/display/screens.rs @@ -172,6 +172,8 @@ pub fn draw_settings(disp: &mut LcdDisplay, selected: usize, settings: &Settings let mut val_str: String<8> = String::new(); if item_idx == crate::settings::VIBRATO_SETTING { write!(val_str, "{}", if value != 0 { "ON" } else { "OFF" }).ok(); + } else if item_idx == crate::settings::RESET_DEFAULTS_SETTING { + write!(val_str, "UP").ok(); } else { write!(val_str, "{}", value).ok(); } diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index 4bfe7e8..29a017b 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -11,6 +11,7 @@ mod display; mod hardware; mod midi; mod settings; +mod settings_storage; mod switch; mod types; @@ -25,7 +26,7 @@ mod app { i2c_bus_recovery, read_all_adcs, set_decoder, set_mux_channel, AdcPins, MuxRaw, }; use crate::midi::MidiSender; - use crate::settings::{Settings, NUM_SETTINGS_ITEMS}; + use crate::settings::{Settings, NUM_SETTINGS_ITEMS, RESET_DEFAULTS_SETTING}; use crate::switch::{ChannelFilter, SwitchEvent, SwitchState}; use crate::types::{DisplayState, LastEvent, LcdDisplay}; @@ -68,12 +69,15 @@ mod app { settings_active: bool, settings_selected: usize, recalibrate_pending: bool, + settings_changed: bool, + factory_reset_pending: bool, } // ── Local resources ─────────────────────────────────────────────────────── #[local] struct Local { audio: audio::Audio, + flash: libdaisy::flash::Flash, adc: Adc, adc_pins: AdcPins, enb_a: Daisy4>, // U1 A0 (ENB_A) @@ -113,6 +117,10 @@ mod app { let ccdr = system::System::init_clocks(device.PWR, device.RCC, &device.SYSCFG); let mut system = libdaisy::system_init!(core, device, ccdr, BLOCK_SIZE); + let settings = crate::settings_storage::load(&mut system.flash).unwrap_or_else(|| { + info!("no valid saved settings; using defaults"); + Settings::default() + }); let mut s0 = system .gpio @@ -325,6 +333,9 @@ mod app { "keyboard_keyboard ready: {} switches, {} muxes", NUM_SWITCHES, NUM_MUXES ); + let mut display_state = DisplayState::new(); + display_state.melody_channel = settings.melody_channel; + display_state.drum_channel = settings.drum_channel; ( Shared { @@ -333,15 +344,18 @@ mod app { baselines, event_queue: heapless::spsc::Queue::new(), midi_tx_flag: false, - display_state: DisplayState::new(), + display_state, splash_done: false, - settings: Settings::default(), + settings, settings_active: false, settings_selected: 0, recalibrate_pending: false, + settings_changed: false, + factory_reset_pending: false, }, Local { audio: system.audio, + flash: system.flash, adc, adc_pins, enb_a, @@ -433,9 +447,12 @@ mod app { last_pitch_bend, last_vibrato_cc, led1, led2, led3, midi_rx, led1_off_at, led2_off_at, recalibrate_show_until: u32 = 0, - recalibrate_flashing: bool = false], + recalibrate_flashing: bool = false, + reset_hold_ms: u16 = 0, + reset_armed: bool = true], shared = [tick_ms, switch_states, baselines, event_queue, midi_tx_flag, splash_done, - settings_active, settings, recalibrate_pending, display_state], + settings_active, settings, recalibrate_pending, display_state, + factory_reset_pending], priority = 15 )] fn timer_handler(mut ctx: timer_handler::Context) { @@ -548,6 +565,27 @@ mod app { } }); + // Emergency recovery: after the splash, hold Settings (HE74) and All Notes + // Off (HE76) together for three seconds. Requiring a post-calibration hold + // avoids treating keys held during power-on calibration as a reset request. + let splash_done = ctx.shared.splash_done.lock(|done| *done); + let reset_keys_down = ctx.shared.switch_states.lock(|states| { + !states[SETTINGS_OPEN].is_idle() && !states[ALL_NOTES_OFF_KEY].is_idle() + }); + if splash_done && reset_keys_down && *ctx.local.reset_armed { + *ctx.local.reset_hold_ms = ctx.local.reset_hold_ms.saturating_add(1); + if *ctx.local.reset_hold_ms >= 3000 { + *ctx.local.reset_armed = false; + ctx.shared + .factory_reset_pending + .lock(|pending| *pending = true); + process_events::spawn().ok(); + } + } else if !reset_keys_down { + *ctx.local.reset_hold_ms = 0; + *ctx.local.reset_armed = true; + } + // ── Pitch bend (rate-limited, only when settings is closed) ─────────── if !settings_active && now % PITCH_BEND_INTERVAL_MS == 0 { let delta_down = pb_filt_down.abs_diff(ctx.local.dynamic_baselines[PITCH_BEND_DOWN]); @@ -705,8 +743,9 @@ mod app { // ── MIDI output ─────────────────────────────────────────────────────────── #[task( shared = [event_queue, midi_tx_flag, display_state, splash_done, - settings, settings_active, settings_selected, recalibrate_pending], - local = [midi_sender], + settings, settings_active, settings_selected, recalibrate_pending, + settings_changed, factory_reset_pending], + local = [midi_sender, flash], priority = 2, capacity = 32 )] @@ -722,6 +761,17 @@ mod app { let mut did_send = false; let mut new_display_event: Option = None; let mut new_volume: Option = None; + let factory_reset = ctx + .shared + .factory_reset_pending + .lock(|pending| core::mem::replace(pending, false)); + if factory_reset { + settings = Settings::default(); + active = false; + selected = 0; + settings_dirty = true; + nav_dirty = true; + } ctx.shared.event_queue.lock(|queue| { while let Some((switch_idx, event)) = queue.dequeue() { @@ -804,14 +854,21 @@ mod app { nav_dirty = true; } SETTINGS_VAL_UP => { - settings.adjust(selected, 1); + if selected == RESET_DEFAULTS_SETTING { + settings = Settings::default(); + info!("factory defaults selected"); + } else { + settings.adjust(selected, 1); + } settings_dirty = true; nav_dirty = true; } SETTINGS_VAL_DOWN => { - settings.adjust(selected, -1); - settings_dirty = true; - nav_dirty = true; + if selected != RESET_DEFAULTS_SETTING { + settings.adjust(selected, -1); + settings_dirty = true; + nav_dirty = true; + } } _ => {} } @@ -881,6 +938,15 @@ mod app { // ── Write back mutations ─────────────────────────────────────────────── if settings_dirty { ctx.shared.settings.lock(|s| *s = settings); + ctx.shared.settings_changed.lock(|changed| *changed = true); + } + if factory_reset { + if crate::settings_storage::save(ctx.local.flash, settings) { + info!("factory defaults restored and saved"); + ctx.shared.settings_changed.lock(|changed| *changed = false); + } else { + warn!("factory reset save failed"); + } } if nav_dirty { ctx.shared.settings_active.lock(|a| *a = active); @@ -900,6 +966,18 @@ mod app { did_send = true; if !active { + let should_save = ctx + .shared + .settings_changed + .lock(|changed| core::mem::replace(changed, false)); + if should_save { + if crate::settings_storage::save(ctx.local.flash, settings) { + info!("settings saved"); + } else { + warn!("settings save failed"); + ctx.shared.settings_changed.lock(|changed| *changed = true); + } + } // Settings just closed — push updated parameters to the synth. ctx.local.midi_sender.set_channel(settings.melody_channel); ctx.local diff --git a/keyboard_keyboard/code/src/settings.rs b/keyboard_keyboard/code/src/settings.rs index 7392bf5..5395ffc 100644 --- a/keyboard_keyboard/code/src/settings.rs +++ b/keyboard_keyboard/code/src/settings.rs @@ -1,7 +1,8 @@ -pub const NUM_SETTINGS_ITEMS: usize = 7; +pub const NUM_SETTINGS_ITEMS: usize = 8; /// Menu index of the VIBRATO on/off toggle (rendered as ON/OFF, not a number). pub const VIBRATO_SETTING: usize = 6; +pub const RESET_DEFAULTS_SETTING: usize = 7; pub struct SettingsItem { pub name: &'static str, @@ -45,23 +46,28 @@ pub const SETTINGS_ITEMS: [SettingsItem; NUM_SETTINGS_ITEMS] = [ min: 0, max: 1, }, + SettingsItem { + name: "RESET DEFAULTS", + min: 0, + max: 0, + }, ]; #[derive(Clone, Copy, Debug)] pub struct Settings { - pub melody_channel: u8, // 0-indexed (0–15), displayed as 1–16 - pub drum_channel: u8, // 0-indexed (0–15), displayed as 1–16 - pub octave: i8, // 0–8; offset = (octave - 4) * 12 - pub pitch_bend_range: u8, // semitones 1–12 - pub melody_program: u8, // 0–127, sent as PC on melody channel when settings closes - pub drum_program: u8, // 0–127, sent as PC on drum channel when settings closes + pub melody_channel: u8, // 0-indexed (0–15), displayed as 1–16 + pub drum_channel: u8, // 0-indexed (0–15), displayed as 1–16 + pub octave: i8, // 0–8; offset = (octave - 4) * 12 + pub pitch_bend_range: u8, // semitones 1–12 + pub melody_program: u8, // 0–127, sent as PC on melody channel when settings closes + pub drum_program: u8, // 0–127, sent as PC on drum channel when settings closes pub vibrato_enabled: bool, // off by default — vibrato keys send CC1 only when on } impl Settings { pub const fn default() -> Self { Self { - melody_channel: 0, + melody_channel: 2, drum_channel: 9, octave: 2, pitch_bend_range: 2, @@ -81,6 +87,7 @@ impl Settings { 4 => self.melody_program as i16, 5 => self.drum_program as i16, 6 => self.vibrato_enabled as i16, + 7 => 0, _ => 0, } } diff --git a/keyboard_keyboard/code/src/settings_storage.rs b/keyboard_keyboard/code/src/settings_storage.rs new file mode 100644 index 0000000..2e8ca83 --- /dev/null +++ b/keyboard_keyboard/code/src/settings_storage.rs @@ -0,0 +1,126 @@ +//! Persistent settings stored in the final two sectors of the Daisy Seed QSPI flash. +//! +//! Two alternating records keep the previous settings intact if power is lost while +//! erasing or programming the next record. + +use crate::settings::Settings; +use libdaisy::flash::{Flash, FlashErase}; + +const SLOT_ADDRESSES: [u32; 2] = [0x7F_E000, 0x7F_F000]; +const RECORD_SIZE: usize = 32; +const MAGIC: [u8; 4] = *b"KKEY"; +const VERSION: u8 = 1; + +#[derive(Clone, Copy)] +struct Record { + generation: u32, + settings: Settings, +} + +/// Load the newest valid settings record, or return `None` for a fresh/corrupt flash. +pub fn load(flash: &mut Flash) -> Option { + newest_record(flash).map(|(_, record)| record.settings) +} + +/// Atomically save settings to the slot older than the current valid record. +pub fn save(flash: &mut Flash, settings: Settings) -> bool { + let (slot, generation) = match newest_record(flash) { + Some((current_slot, record)) => (1 - current_slot, record.generation.wrapping_add(1)), + None => (0, 0), + }; + let bytes = encode(Record { + generation, + settings, + }); + + if stm32h7xx_hal::nb::block!(flash.erase(FlashErase::Sector4K(SLOT_ADDRESSES[slot]))).is_err() { + return false; + } + stm32h7xx_hal::nb::block!(flash.program(SLOT_ADDRESSES[slot], &bytes)).is_ok() +} + +fn newest_record(flash: &mut Flash) -> Option<(usize, Record)> { + let a = read_slot(flash, 0); + let b = read_slot(flash, 1); + match (a, b) { + (Some(a), Some(b)) => { + // Wrapping comparison is safe because the two generations can differ by only one. + if b.generation.wrapping_sub(a.generation) < 0x8000_0000 { + Some((1, b)) + } else { + Some((0, a)) + } + } + (Some(a), None) => Some((0, a)), + (None, Some(b)) => Some((1, b)), + (None, None) => None, + } +} + +fn read_slot(flash: &mut Flash, slot: usize) -> Option { + let mut bytes = [0u8; RECORD_SIZE]; + flash.read(SLOT_ADDRESSES[slot], &mut bytes).ok()?; + decode(&bytes) +} + +fn encode(record: Record) -> [u8; RECORD_SIZE] { + let mut bytes = [0xFFu8; RECORD_SIZE]; + bytes[0..4].copy_from_slice(&MAGIC); + bytes[4] = VERSION; + bytes[5] = 7; + bytes[8..12].copy_from_slice(&record.generation.to_le_bytes()); + bytes[12] = record.settings.melody_channel; + bytes[13] = record.settings.drum_channel; + bytes[14] = record.settings.octave as u8; + bytes[15] = record.settings.pitch_bend_range; + bytes[16] = record.settings.melody_program; + bytes[17] = record.settings.drum_program; + bytes[18] = record.settings.vibrato_enabled as u8; + let checksum = crc32(&bytes[..28]); + bytes[28..32].copy_from_slice(&checksum.to_le_bytes()); + bytes +} + +fn decode(bytes: &[u8; RECORD_SIZE]) -> Option { + if bytes[0..4] != MAGIC || bytes[4] != VERSION || bytes[5] != 7 { + return None; + } + let expected = u32::from_le_bytes(bytes[28..32].try_into().ok()?); + if crc32(&bytes[..28]) != expected { + return None; + } + + let settings = Settings { + melody_channel: bytes[12], + drum_channel: bytes[13], + octave: bytes[14] as i8, + pitch_bend_range: bytes[15], + melody_program: bytes[16], + drum_program: bytes[17], + vibrato_enabled: bytes[18] != 0, + }; + if settings.melody_channel > 15 + || settings.drum_channel > 15 + || !(2..=5).contains(&settings.octave) + || !(1..=12).contains(&settings.pitch_bend_range) + || bytes[18] > 1 + { + return None; + } + + Some(Record { + generation: u32::from_le_bytes(bytes[8..12].try_into().ok()?), + settings, + }) +} + +fn crc32(bytes: &[u8]) -> u32 { + let mut crc = 0xFFFF_FFFFu32; + for &byte in bytes { + crc ^= byte as u32; + for _ in 0..8 { + crc = (crc >> 1) ^ (0xEDB8_8320u32 & (0u32.wrapping_sub(crc & 1))); + } + } + !crc +}