Files
XDJ100SX/tools/xdj_pi_dev/setup_cmds.py
T
Jeancarlo 8b0eb42fec add MCP developer tool with multi-unit support
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>
2026-05-08 01:24:15 -03:00

218 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
"""
Setup commands: SSH keys, DHCP server, hostname change, SSH recovery guide.
Extracted from xdj-pi-dev.py (lines 1102-1288).
"""
import subprocess
import textwrap
from pathlib import Path
import paramiko
from xdj_pi_dev._terminal import (
_C, Step, section, ok, warn, fail,
)
# Project-specific SSH key (won't affect other SSH usage)
SSH_KEY_PATH = Path.home() / ".ssh" / "xdj_pi_ed25519"
# ─── SSH key setup ────────────────────────────────────────────────────────────
def setup_ssh_keys(pi_client) -> None:
"""
Generate a project-specific ed25519 key pair and install it on the Pi.
After this, the tool authenticates without a password.
"""
section("SSH Key Setup")
# 1. Generate key locally if not present
if SSH_KEY_PATH.exists():
ok(f"Key already exists: {SSH_KEY_PATH}")
else:
print(f" Generating ed25519 key: {SSH_KEY_PATH}")
r = subprocess.run(
["ssh-keygen", "-t", "ed25519", "-f", str(SSH_KEY_PATH),
"-N", "", "-C", "xdj-pi-dev"],
capture_output=True, text=True
)
if r.returncode != 0:
fail(f"ssh-keygen failed: {r.stderr.strip()}")
return
ok(f"Key generated: {SSH_KEY_PATH}")
pub_key = (SSH_KEY_PATH.with_suffix(".pub")).read_text().strip()
# 2. Install on Pi
print(f" Installing public key on Pi ({pi_client.host})…")
install_cmd = (
f"mkdir -p ~/.ssh && chmod 700 ~/.ssh && "
f"grep -qxF '{pub_key}' ~/.ssh/authorized_keys 2>/dev/null || "
f"echo '{pub_key}' >> ~/.ssh/authorized_keys && "
f"chmod 600 ~/.ssh/authorized_keys && echo installed"
)
r2 = pi_client.exec(install_cmd)
if "installed" in r2["stdout"] or r2["rc"] == 0:
ok("Public key installed on Pi")
else:
fail(f"Install failed: {r2['stderr'].strip()}")
return
# 3. Test key auth works before offering to disable password
print(" Testing key auth…")
test_ssh = paramiko.SSHClient()
test_ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
test_ssh.connect(pi_client.host, username=pi_client.user,
key_filename=str(SSH_KEY_PATH),
look_for_keys=False, allow_agent=False, timeout=8)
test_ssh.close()
ok("Key auth confirmed working")
except Exception as e:
fail(f"Key auth test failed: {e}")
print(" Password auth is still active. Fix the key issue before disabling it.")
return
# 4. Offer to disable password auth (skip prompt in TUI — no stdin available)
from xdj_pi_dev._terminal import _tui_log_fn as _tui_fn
if _tui_fn:
ok("Key auth is working. Password auth left enabled (use CLI to disable).")
return
print()
print(" Key auth is working. Optionally disable SSH password auth on Pi")
print(" (more secure — only connections with this key will be accepted).")
answer = input(" Disable password auth now? [y/N] ").strip().lower()
if answer == "y":
r3 = pi_client.exec(
"sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' "
"/etc/ssh/sshd_config && sudo systemctl reload ssh && echo done"
)
if "done" in r3["stdout"]:
ok("Password auth disabled. Only key auth accepted.")
print()
print(" IMPORTANT: Keep the key file safe:")
print(f" {SSH_KEY_PATH}")
print(f" {SSH_KEY_PATH}.pub")
print()
print(" If you lose the key, use --restore-ssh to recover access.")
else:
fail(f"Could not update sshd_config: {r3['stderr'].strip()}")
else:
ok("Password auth left enabled (safe default)")
# ─── SSH recovery guide ───────────────────────────────────────────────────────
def restore_ssh() -> None:
"""Print step-by-step SSH recovery instructions — no network connection required."""
section("SSH Recovery Guide")
print(textwrap.dedent("""
If you're locked out of the Pi (lost key, disabled password auth accidentally):
── Option A: Physical console ─────────────────────────────────────────────
1. Connect a keyboard and HDMI monitor to the Pi.
2. Press Ctrl+Alt+F2 to switch to tty2 (away from Mixxx on tty1).
3. Log in: username xdj100sx, password xdj100sx
4. Re-enable password auth:
sudo nano /etc/ssh/sshd_config
→ Change: PasswordAuthentication no → yes
sudo systemctl reload ssh
5. Press Ctrl+Alt+F1 to return to Mixxx.
── Option B: Edit the SD card on another machine ──────────────────────────
1. Power off the Pi. Remove the SD card.
2. Mount the SD card on your machine (Linux/macOS native; Windows use DiskGenius or WSL).
3. Edit: /etc/ssh/sshd_config
→ PasswordAuthentication yes
4. Reinstall SD card, power on Pi.
── Option C: Install a new key without password auth ──────────────────────
If you have a different SSH key that still works:
1. ssh -i /path/to/other/key xdj100sx@<pi-ip>
2. Then run --setup-ssh-keys to install the project key.
── Recovering the Pi IP if unknown ────────────────────────────────────────
On the Pi console: ip addr show eth0
From this machine: python3 xdj-pi-dev.py --discover
"""))
# ─── Pi DHCP setup ───────────────────────────────────────────────────────────
def setup_pi_dhcp(pi_client) -> None:
"""
Configure the Pi to act as a DHCP server on eth0.
After this, any machine plugged directly into the Pi gets an IP automatically
— no manual network alias setup needed.
"""
section("Pi DHCP Server Setup")
print(" This installs dnsmasq on the Pi and configures it to auto-assign")
print(" IP addresses to machines connected via direct cable.")
print(" Pi's own IP stays at 192.168.10.2.")
print()
# Install dnsmasq
print(" Installing dnsmasq…")
r = pi_client.exec("sudo apt-get install -y dnsmasq 2>&1 | tail -3", timeout=120)
if r["rc"] != 0:
fail(f"apt-get failed: {r['stderr'].strip()}")
return
ok("dnsmasq installed")
# Write config
conf = textwrap.dedent("""\
# XDJ direct-cable DHCP — managed by xdj-pi-dev.py
interface=eth0
bind-interfaces
dhcp-range=192.168.10.100,192.168.10.200,255.255.255.0,12h
# No gateway, no DNS — pure IP assignment for direct cable
dhcp-option=3
dhcp-option=6
""")
r2 = pi_client.exec(
f"sudo mkdir -p /etc/dnsmasq.d && echo '{conf}' | sudo tee /etc/dnsmasq.d/xdj-direct.conf > /dev/null && echo ok"
)
if "ok" not in r2["stdout"]:
fail(f"Could not write dnsmasq config: {r2['stderr'].strip()}")
return
ok("dnsmasq config written (/etc/dnsmasq.d/xdj-direct.conf)")
# Enable and restart
r3 = pi_client.exec(
"sudo systemctl enable dnsmasq && sudo systemctl restart dnsmasq && echo ok"
)
if "ok" not in r3["stdout"]:
fail(f"dnsmasq restart failed: {r3['stderr'].strip()}")
return
ok("dnsmasq running and enabled on boot")
print()
print(" Done. Next time you plug in via direct cable:")
print(" • Your machine will get an IP in 192.168.10.100200 automatically")
print(" • No manual network config needed")
print(" • XDJ100SX.local still works too")
# ─── Hostname change ──────────────────────────────────────────────────────────
def set_hostname(pi_client, new_hostname: str) -> None:
"""Change the Pi's hostname and restart avahi so mDNS updates immediately."""
section(f"Set Hostname → {new_hostname}")
old = pi_client.exec("hostname").get("stdout", "").strip()
print(f" Current hostname: {old}")
r = pi_client.exec(
f"sudo hostnamectl set-hostname {new_hostname} && "
f"sudo sed -i 's/{old}/{new_hostname}/g' /etc/hosts && "
f"sudo systemctl restart avahi-daemon 2>/dev/null; echo ok"
)
if "ok" in r["stdout"]:
ok(f"Hostname changed to: {new_hostname}")
print(f" New mDNS address: {new_hostname}.local")
else:
fail(f"Failed: {r['stderr'].strip()}")