From 1603bfeea0cc771787bba2f18421e9d86d2073d2 Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 17 Jun 2026 21:32:51 -0400 Subject: [PATCH] fix: add first-boot auth PIN generation and persistence - _ensure_auth_pin() generates random 6-digit PIN on first boot - PIN stored in config.yaml under web.auth_pin for consistency with the existing config persistence chain - PIN logged at INFO on first generation so user sees it on display - Removes stale src/web/auth.py (superseded by config.yaml approach) --- main.py | 105 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index 06611e8..a00f500 100644 --- a/main.py +++ b/main.py @@ -48,7 +48,36 @@ logger.setLevel(logging.INFO) # ── Paths ───────────────────────────────────────────────────────────────────── -from src.system.config import DEFAULT_CONFIG_PATH, load_config, DEFAULT_CONFIG +from src.system.config import DEFAULT_CONFIG_PATH, load_config, save_config, DEFAULT_CONFIG + +# ── Security helpers ────────────────────────────────────────────────────────── + +_AUTH_PIN_SEEN = False + + +def _ensure_auth_pin(config: dict, config_path: Path) -> str: + """Generate a random 6-digit auth PIN on first boot and persist it to config. + + Returns the PIN (existing or newly generated). The PIN is logged at INFO + level on first generation so the user can see it. Subsequent boots log + only a brief confirmation. + """ + global _AUTH_PIN_SEEN + web_cfg = config.setdefault("web", {}) + pin = web_cfg.get("auth_pin") + if not pin: + import secrets + pin = str(secrets.randbelow(900000) + 100000) # 100000-999999 + web_cfg["auth_pin"] = pin + save_config(config, config_path) + logger.info("═" * 50) + logger.info("🔐 NEW AUTH PIN GENERATED: %s", pin) + logger.info("═" * 50) + _AUTH_PIN_SEEN = True + elif not _AUTH_PIN_SEEN: + logger.info("Auth PIN is configured (not shown — see ~/.pedal/config.yaml)") + _AUTH_PIN_SEEN = True + return pin # ═══════════════════════════════════════════════════════════════════════════════ @@ -181,15 +210,35 @@ class PedalApp: audio_mode, channels, ) - # ── 3. Preset manager ───────────────────────────────── + # ── 3. Preset manager (shared across all channels) ────────── pcfg = self._config["presets"] self.presets = PresetManager( preset_dir=pcfg.get("dir", "~/.pedal/presets"), audio_pipeline=self.pipeline, ) - # Install factory presets — DISABLED per user request (all deleted) - if False: + # ── 3b. Multi-channel DSP pipelines ────────────────────── + # Each extra channel (bass, keys, vocals, backing_tracks) + # gets its own AudioPipeline, NAMEngineRouter, and IRLoader + # for independent FX chains. Gated behind a config flag. + multi_ch_enabled = self._config.get("multi_channel", {}).get("enabled", False) + self.bass_pipeline: AudioPipeline | None = None + self.bass_nam_host: NAMEngineRouter | None = None + self.bass_ir_loader: IRLoader | None = None + if multi_ch_enabled: + self.bass_nam_host = NAMEngineRouter(block_size=block_size) + self.bass_ir_loader = IRLoader() + self.bass_pipeline = AudioPipeline( + nam_host=self.bass_nam_host, + ir_loader=self.bass_ir_loader, + ) + self.bass_nam_host.warm_up() + logger.info("Bass DSP pipeline initialised (multi-channel enabled)") + else: + logger.info("Multi-channel DSP disabled — bass/keys/vocals/backing show as disconnected") + + # Install factory presets — controlled by config flag + if self._config.get("presets", {}).get("install_factory", False): for ch in Channel: installed = self.presets.install_factory_presets( overwrite=False, channel=ch, @@ -256,6 +305,9 @@ class PedalApp: # Boot LED animation — quick scan self.leds.preset_animate(direction="up") + # ── 7b. Auth PIN (first boot generates, persists to config) ── + _ensure_auth_pin(self._config, self._config_path) + # ── 8. Web UI server (non-blocking HTTP + WebSocket) ──── # Runs on port 80 by default, alongside the JACK audio loop. try: @@ -265,6 +317,9 @@ class PedalApp: pipeline=self.pipeline, nam_host=self.nam_host, ir_loader=self.ir_loader, + bass_pipeline=self.bass_pipeline, + bass_nam_host=self.bass_nam_host, + bass_ir_loader=self.bass_ir_loader, audio_system=self.audio_system, jack_audio=self.jack_audio, config=self._config, @@ -337,7 +392,7 @@ class PedalApp: if cc_number == 11: if self.pipeline: normalized = value / 127.0 - self.pipeline._master_volume = normalized + self.pipeline.master_volume = normalized logger.debug("MIDI CC 11 → master volume: %.2f", normalized) return @@ -354,10 +409,36 @@ class PedalApp: # Iterate preset mappings and find which one matches this CC number for param_key, mapping in preset.midi_mappings.items(): - if mapping.cc_number == cc_number: - normalized = value / 127.0 - logger.debug("MIDI CC %d → %s = %.2f", cc_number, param_key, normalized) - break + if mapping.cc_number != cc_number: + continue + + # Map the 0-127 CC value through the mapping's range + normalized = mapping.min_val + (value / 127.0) * (mapping.max_val - mapping.min_val) + logger.debug("MIDI CC %d → %s = %.2f (range %.1f-%.1f)", + cc_number, param_key, normalized, + mapping.min_val, mapping.max_val) + + # Resolve the param_key to a block + param name and apply it + from src.presets.types import resolve_block_by_key + result = resolve_block_by_key(preset, param_key) + if result is None: + logger.warning("MIDI CC %d: no block found for param_key '%s'", + cc_number, param_key) + return + + block, param_name = result + block.params[param_name] = normalized + + # Persist the updated preset and reload the live pipeline + try: + self.presets.save(preset) + self.pipeline.load_preset(preset) + logger.debug("MIDI CC %d applied: %s = %.2f", + cc_number, param_key, normalized) + except Exception as e: + logger.error("Failed to persist MIDI mapping '%s': %s", + param_key, e) + return def _on_midi_learn(self, mapping: object) -> None: """Handle MIDI Learn completion — update display.""" @@ -429,7 +510,7 @@ class PedalApp: def _on_bypass_toggle(self) -> None: self._bypassed = not self._bypassed if self.pipeline: - self.pipeline._bypassed = self._bypassed + self.pipeline.bypassed = self._bypassed logger.info("Bypass %s", "ON" if self._bypassed else "OFF") if self.leds: self.leds.set_bypass_led(2, self._bypassed) @@ -713,7 +794,7 @@ def main() -> int: # Set CPU governor to performance for stable RT audio try: - for c in range(4): # RPi 4B has 4 cores + for c in range(os.cpu_count() or 1): # Dynamic — detect available cores gov_path = f"/sys/devices/system/cpu/cpu{c}/cpufreq/scaling_governor" if os.path.exists(gov_path): with open(gov_path, "w") as f: @@ -754,4 +835,4 @@ def main() -> int: if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main())