8b0eb42fec
First developer tool for the XDJ-100SX project. Connects Claude Code directly to the Pi over SSH — push skin files, take screenshots, restart Mixxx, flash Pico firmware, and more without leaving the editor. Available MCP tools: - run_command, read_file, write_file, list_files - push_skin, pull_skin, push_skin_file, pull_skin_file - push_midi, pull_midi - take_screenshot, navigate_panel - restart_mixxx - check (preflight: SSH, Mixxx, Pico, audio) - pico_bootloader, pico_flash - discover_units — scan network for all reachable XDJ Pi units - select_unit — switch active connection mid-session (multi-unit support) Also adds --about flag and TUI About modal with authors and credits, and fixes scrolling/close behavior on Help and About modals. By: Jeancarlo Cardoso de Faria Filho (jaianlab) <jaianlabworks@gmail.com>
128 lines
3.8 KiB
Python
128 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
"""
|
|
Config layer — owns all path resolution and the config file.
|
|
|
|
In dev mode (python3 xdj-pi-dev.py): paths resolve from __file__ inside the repo.
|
|
In standalone mode (PyInstaller frozen binary): paths come from ~/.xdj-pi-dev/config.json.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# True only when running inside a PyInstaller bundle
|
|
_FROZEN: bool = getattr(sys, "frozen", False)
|
|
|
|
CONFIG_PATH = Path.home() / ".xdj-pi-dev" / "config.json"
|
|
|
|
# Keys stored in the config file
|
|
KEYS = ["skin_dir", "midi_dir", "firmware_dir", "backup_dir", "host", "board", "ssh_user", "ssh_pass"]
|
|
|
|
# Supported boards — only "pico" has full feature support right now
|
|
BOARDS = ["pico", "pico2", "teensy", "arduino", "unknown"]
|
|
PICO_BOARDS = {"pico", "pico2"} # boards that support UF2 bootloader + flash
|
|
|
|
|
|
def load_config() -> dict:
|
|
"""Read config file; return {} if missing or corrupt."""
|
|
try:
|
|
return json.loads(CONFIG_PATH.read_text())
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return {}
|
|
|
|
|
|
def save_config(data: dict) -> None:
|
|
"""Write config to disk, creating parent dir if needed."""
|
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
CONFIG_PATH.write_text(json.dumps(data, indent=2))
|
|
|
|
|
|
def update_config(**kwargs) -> None:
|
|
"""Merge kwargs into existing config and save."""
|
|
cfg = load_config()
|
|
cfg.update(kwargs)
|
|
save_config(cfg)
|
|
|
|
|
|
def config_complete() -> bool:
|
|
"""True when all required path keys have values."""
|
|
cfg = load_config()
|
|
return all(cfg.get(k) for k in ["skin_dir", "midi_dir"])
|
|
|
|
|
|
def get_board() -> str:
|
|
"""Return board name from config; default 'pico' in dev mode."""
|
|
if _FROZEN:
|
|
return load_config().get("board", "unknown")
|
|
return load_config().get("board", "pico")
|
|
|
|
|
|
def is_pico_board() -> bool:
|
|
return get_board() in PICO_BOARDS
|
|
|
|
|
|
# ─── Path getters ─────────────────────────────────────────────────────────────
|
|
|
|
def _repo_root() -> Path:
|
|
"""Repo root — only valid in dev mode."""
|
|
return Path(__file__).resolve().parent.parent.parent
|
|
|
|
|
|
def get_skin_dir() -> Path:
|
|
if _FROZEN:
|
|
p = load_config().get("skin_dir")
|
|
if not p:
|
|
raise RuntimeError("Skin folder not configured — open Settings.")
|
|
return Path(p)
|
|
return _repo_root() / "mixxx" / "SKIN" / "XDJ100SX"
|
|
|
|
|
|
def get_midi_dir() -> Path:
|
|
if _FROZEN:
|
|
p = load_config().get("midi_dir")
|
|
if not p:
|
|
raise RuntimeError("MIDI folder not configured — open Settings.")
|
|
return Path(p)
|
|
return _repo_root() / "mixxx" / "MIDI"
|
|
|
|
|
|
def get_firmware_dir() -> Path:
|
|
if _FROZEN:
|
|
p = load_config().get("firmware_dir")
|
|
if not p:
|
|
raise RuntimeError("Firmware folder not configured — open Settings.")
|
|
return Path(p)
|
|
return _repo_root() / "arduino" / "pico"
|
|
|
|
|
|
def get_backup_dir() -> Path:
|
|
if _FROZEN:
|
|
p = load_config().get("backup_dir")
|
|
if p:
|
|
return Path(p)
|
|
# Default: cwd for dev, ~/Downloads/XDJ-Backups for standalone
|
|
if _FROZEN:
|
|
d = Path.home() / "Downloads" / "XDJ-Backups"
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
return d
|
|
return _repo_root()
|
|
|
|
|
|
def get_saved_host() -> str | None:
|
|
"""Return the remembered Pi host, or None if not set."""
|
|
return load_config().get("host") or None
|
|
|
|
|
|
def get_ssh_user() -> str:
|
|
"""SSH username: config file → XDJ_USER env → built-in default."""
|
|
v = load_config().get("ssh_user")
|
|
return v if v else os.environ.get("XDJ_USER", "xdj100sx")
|
|
|
|
|
|
def get_ssh_pass() -> str:
|
|
"""SSH password: config file → XDJ_PASS env → built-in default."""
|
|
v = load_config().get("ssh_pass")
|
|
return v if v else os.environ.get("XDJ_PASS", "xdj100sx")
|