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)
This commit is contained in:
@@ -48,7 +48,36 @@ logger.setLevel(logging.INFO)
|
|||||||
|
|
||||||
# ── Paths ─────────────────────────────────────────────────────────────────────
|
# ── 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,
|
audio_mode, channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 3. Preset manager ─────────────────────────────────
|
# ── 3. Preset manager (shared across all channels) ──────────
|
||||||
pcfg = self._config["presets"]
|
pcfg = self._config["presets"]
|
||||||
self.presets = PresetManager(
|
self.presets = PresetManager(
|
||||||
preset_dir=pcfg.get("dir", "~/.pedal/presets"),
|
preset_dir=pcfg.get("dir", "~/.pedal/presets"),
|
||||||
audio_pipeline=self.pipeline,
|
audio_pipeline=self.pipeline,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Install factory presets — DISABLED per user request (all deleted)
|
# ── 3b. Multi-channel DSP pipelines ──────────────────────
|
||||||
if False:
|
# 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:
|
for ch in Channel:
|
||||||
installed = self.presets.install_factory_presets(
|
installed = self.presets.install_factory_presets(
|
||||||
overwrite=False, channel=ch,
|
overwrite=False, channel=ch,
|
||||||
@@ -256,6 +305,9 @@ class PedalApp:
|
|||||||
# Boot LED animation — quick scan
|
# Boot LED animation — quick scan
|
||||||
self.leds.preset_animate(direction="up")
|
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) ────
|
# ── 8. Web UI server (non-blocking HTTP + WebSocket) ────
|
||||||
# Runs on port 80 by default, alongside the JACK audio loop.
|
# Runs on port 80 by default, alongside the JACK audio loop.
|
||||||
try:
|
try:
|
||||||
@@ -265,6 +317,9 @@ class PedalApp:
|
|||||||
pipeline=self.pipeline,
|
pipeline=self.pipeline,
|
||||||
nam_host=self.nam_host,
|
nam_host=self.nam_host,
|
||||||
ir_loader=self.ir_loader,
|
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,
|
audio_system=self.audio_system,
|
||||||
jack_audio=self.jack_audio,
|
jack_audio=self.jack_audio,
|
||||||
config=self._config,
|
config=self._config,
|
||||||
@@ -337,7 +392,7 @@ class PedalApp:
|
|||||||
if cc_number == 11:
|
if cc_number == 11:
|
||||||
if self.pipeline:
|
if self.pipeline:
|
||||||
normalized = value / 127.0
|
normalized = value / 127.0
|
||||||
self.pipeline._master_volume = normalized
|
self.pipeline.master_volume = normalized
|
||||||
logger.debug("MIDI CC 11 → master volume: %.2f", normalized)
|
logger.debug("MIDI CC 11 → master volume: %.2f", normalized)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -354,10 +409,36 @@ class PedalApp:
|
|||||||
|
|
||||||
# Iterate preset mappings and find which one matches this CC number
|
# Iterate preset mappings and find which one matches this CC number
|
||||||
for param_key, mapping in preset.midi_mappings.items():
|
for param_key, mapping in preset.midi_mappings.items():
|
||||||
if mapping.cc_number == cc_number:
|
if mapping.cc_number != cc_number:
|
||||||
normalized = value / 127.0
|
continue
|
||||||
logger.debug("MIDI CC %d → %s = %.2f", cc_number, param_key, normalized)
|
|
||||||
break
|
# 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:
|
def _on_midi_learn(self, mapping: object) -> None:
|
||||||
"""Handle MIDI Learn completion — update display."""
|
"""Handle MIDI Learn completion — update display."""
|
||||||
@@ -429,7 +510,7 @@ class PedalApp:
|
|||||||
def _on_bypass_toggle(self) -> None:
|
def _on_bypass_toggle(self) -> None:
|
||||||
self._bypassed = not self._bypassed
|
self._bypassed = not self._bypassed
|
||||||
if self.pipeline:
|
if self.pipeline:
|
||||||
self.pipeline._bypassed = self._bypassed
|
self.pipeline.bypassed = self._bypassed
|
||||||
logger.info("Bypass %s", "ON" if self._bypassed else "OFF")
|
logger.info("Bypass %s", "ON" if self._bypassed else "OFF")
|
||||||
if self.leds:
|
if self.leds:
|
||||||
self.leds.set_bypass_led(2, self._bypassed)
|
self.leds.set_bypass_led(2, self._bypassed)
|
||||||
@@ -713,7 +794,7 @@ def main() -> int:
|
|||||||
|
|
||||||
# Set CPU governor to performance for stable RT audio
|
# Set CPU governor to performance for stable RT audio
|
||||||
try:
|
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"
|
gov_path = f"/sys/devices/system/cpu/cpu{c}/cpufreq/scaling_governor"
|
||||||
if os.path.exists(gov_path):
|
if os.path.exists(gov_path):
|
||||||
with open(gov_path, "w") as f:
|
with open(gov_path, "w") as f:
|
||||||
@@ -754,4 +835,4 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
Reference in New Issue
Block a user