#!/usr/bin/env bash # br-hw - BlackRoad OS Hardware Profile Manager # Detect, configure, and manage custom hardware across the fleet # Usage: br-hw [args] set -euo pipefail source "$HOME/.blackroad/config/nodes.sh" 2>/dev/null || true HW_DIR="$HOME/.blackroad/hardware" mkdir -p "$HW_DIR/profiles" "$HW_DIR/detected" usage() { cat < Create a custom hardware profile br-hw apply [node] Apply a hardware profile to a node br-hw info Show detected hardware for a node ${AMBER}HARDWARE PROFILES:${RESET} Profiles define device-specific configuration: - Power settings (governor, clock speed, GPU memory) - Peripheral support (Hailo, NVMe, cameras, I2C) - Service assignments (what runs where) - Custom udev rules and kernel parameters ${GREEN}EXAMPLES:${RESET} br-hw detect cecilia Probe Cecilia's hardware br-hw add my-pi5-hailo Create custom profile br-hw apply ai-accelerator cecilia br-hw scan Survey full fleet EOF } # Detect hardware on a node via SSH cmd_detect() { local node="${1:-}" if [[ -z "$node" ]]; then # Detect local (Mac) printf '%bDetecting local hardware...%b\n' "$AMBER" "$RESET" detect_local return fi printf '%bDetecting hardware on %s...%b\n' "$AMBER" "$node" "$RESET" if ! br_ssh_up "$node" 2>/dev/null; then printf '%b%s is offline%b\n' "$RED" "$node" "$RESET" return 1 fi local out_file="$HW_DIR/detected/${node}.yaml" # Collect hardware info via SSH local hw_info hw_info=$(br_ssh "$node" " echo '---' echo 'node: $node' echo 'detected: $(date -u +%Y-%m-%dT%H:%M:%SZ)' # Platform echo 'platform:' echo ' model: \"'$(cat /proc/device-tree/model 2>/dev/null | tr -d '\0' || echo 'unknown')'\"' echo ' arch: \"'$(uname -m)'\"' echo ' kernel: \"'$(uname -r)'\"' echo ' os: \"'$(. /etc/os-release 2>/dev/null && echo \"\$PRETTY_NAME\" || echo 'unknown')'\"' # CPU echo 'cpu:' echo ' cores: '$(nproc) echo ' governor: \"'$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo 'unknown')'\"' echo ' freq_mhz: '$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null | awk '{printf \"%.0f\", \$1/1000}' || echo '0') echo ' max_freq_mhz: '$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq 2>/dev/null | awk '{printf \"%.0f\", \$1/1000}' || echo '0') # Memory echo 'memory:' echo ' total_mb: '$(free -m | awk '/Mem:/{print \$2}') echo ' available_mb: '$(free -m | awk '/Mem:/{print \$7}') echo ' swap_mb: '$(free -m | awk '/Swap:/{print \$2}') # Storage echo 'storage:' lsblk -dn -o NAME,SIZE,TYPE,TRAN 2>/dev/null | while read -r dev size type tran; do echo \" - device: /dev/\$dev\" echo \" size: \\\"\$size\\\"\" echo \" type: \\\"\$type\\\"\" echo \" transport: \\\"\${tran:-unknown}\\\"\" done # GPU echo 'gpu:' echo ' mem_mb: '$(vcgencmd get_config gpu_mem 2>/dev/null | cut -d= -f2 || echo '0') # Temperature echo 'thermal:' echo ' temp_c: '$(vcgencmd measure_temp 2>/dev/null | grep -oP '[0-9.]+' || echo '0') echo ' throttled: \"'$(vcgencmd get_throttled 2>/dev/null | cut -d= -f2 || echo 'unknown')'\"' # Voltage echo ' voltage_v: '$(vcgencmd measure_volts 2>/dev/null | grep -oP '[0-9.]+' || echo '0') # Accelerators echo 'accelerators:' if [ -e /dev/hailo0 ]; then echo ' - type: hailo-8' echo ' device: /dev/hailo0' serial=\$(hailortcli fw-control identify 2>/dev/null | grep -i serial | awk '{print \$NF}' || echo 'unknown') echo \" serial: \\\"\$serial\\\"\" echo ' tops: 26' fi # USB devices echo 'usb_devices:' lsusb 2>/dev/null | grep -v 'root hub' | while read -r line; do echo \" - \\\"\$line\\\"\" done # I2C devices echo 'i2c_devices:' for bus in /dev/i2c-*; do [ -e \"\$bus\" ] || continue bus_num=\${bus##*-} i2cdetect -y \"\$bus_num\" 2>/dev/null | grep -oP '[0-9a-f]{2}' | while read -r addr; do echo \" - bus: \$bus_num\" echo \" address: 0x\$addr\" done done 2>/dev/null # Network echo 'network:' for iface in eth0 wlan0; do if ip addr show \"\$iface\" &>/dev/null; then ip_addr=\$(ip -4 addr show \"\$iface\" 2>/dev/null | grep -oP 'inet \K[0-9.]+' | head -1) echo \" \$iface: \\\"\${ip_addr:-down}\\\"\" fi done # Docker echo 'docker:' if command -v docker &>/dev/null; then echo ' installed: true' echo ' containers: '$(docker ps -q 2>/dev/null | wc -l) else echo ' installed: false' fi # Services echo 'services:' for svc in ollama docker tailscaled cloudflared nginx postgresql; do if systemctl is-active \"\$svc\" &>/dev/null; then echo \" \$svc: active\" fi done " 2>/dev/null) || { printf '%bFailed to detect hardware on %s%b\n' "$RED" "$node" "$RESET" return 1 } echo "$hw_info" > "$out_file" printf '%bHardware profile saved: %s%b\n' "$GREEN" "$out_file" "$RESET" # Display summary echo "$hw_info" } detect_local() { local out_file="$HW_DIR/detected/mac.yaml" cat > "$out_file" </dev/null || echo 'Mac')" arch: "$(uname -m)" kernel: "$(uname -r)" os: "$(sw_vers -productName 2>/dev/null) $(sw_vers -productVersion 2>/dev/null)" cpu: cores: $(sysctl -n hw.ncpu 2>/dev/null || echo 0) brand: "$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo 'unknown')" memory: total_mb: $(( $(sysctl -n hw.memsize 2>/dev/null || echo 0) / 1048576 )) storage: - device: /dev/disk0 size: "$(diskutil info / 2>/dev/null | grep 'Disk Size' | awk -F: '{print $2}' | xargs || echo 'unknown')" docker: installed: $(command -v docker &>/dev/null && echo true || echo false) containers: $(docker ps -q 2>/dev/null | wc -l | tr -d ' ') YAML printf '%bLocal hardware saved: %s%b\n' "$GREEN" "$out_file" "$RESET" cat "$out_file" } cmd_list() { printf '%b%-20s %s%b\n' "$BLUE" "PROFILE" "DESCRIPTION" "$RESET" printf '%-20s %s\n' "───────" "───────────" for f in "$HW_DIR/profiles/"*.yaml; do [[ -f "$f" ]] || continue name=$(basename "$f" .yaml) desc=$(grep '^description:' "$f" 2>/dev/null | sed 's/^description: *//' | head -1) printf '%-20s %s\n' "$name" "${desc:-—}" done echo "" printf '%bDetected:%b\n' "$AMBER" "$RESET" for f in "$HW_DIR/detected/"*.yaml; do [[ -f "$f" ]] || continue name=$(basename "$f" .yaml) detected=$(grep '^detected:' "$f" 2>/dev/null | awk '{print $2}') printf ' %-16s (scanned %s)\n' "$name" "${detected:-never}" done } cmd_add() { local name="$1" local profile="$HW_DIR/profiles/${name}.yaml" if [[ -f "$profile" ]]; then echo "Profile '$name' already exists." return 1 fi cat > "$profile" < " return 1 fi printf '%bApplying profile %s to %s...%b\n' "$AMBER" "$profile_name" "$node" "$RESET" if ! br_ssh_up "$node" 2>/dev/null; then printf '%b%s is offline%b\n' "$RED" "$node" "$RESET" return 1 fi # Copy profile to node local ssh_target ssh_target="$(br_ssh_target "$node")" scp -q "$profile" "$ssh_target:/tmp/br-hw-profile.yaml" 2>/dev/null # Apply power settings local governor governor=$(grep 'governor:' "$profile" | head -1 | awk '{print $2}') if [[ -n "$governor" && "$governor" != "null" ]]; then printf ' governor → %s: ' "$governor" br_ssh "$node" "echo '$governor' | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor >/dev/null 2>&1" && \ printf '%bok%b\n' "$GREEN" "$RESET" || printf '%bfail%b\n' "$RED" "$RESET" fi # Apply sysctl local swappiness swappiness=$(grep 'swappiness:' "$profile" | tail -1 | awk '{print $2}') if [[ -n "$swappiness" && "$swappiness" != "null" ]]; then printf ' swappiness → %s: ' "$swappiness" br_ssh "$node" "sudo sysctl -w vm.swappiness=$swappiness >/dev/null 2>&1" && \ printf '%bok%b\n' "$GREEN" "$RESET" || printf '%bfail%b\n' "$RED" "$RESET" fi printf '%bProfile %s applied to %s%b\n' "$GREEN" "$profile_name" "$node" "$RESET" } cmd_scan() { printf '%bScanning fleet hardware...%b\n\n' "$AMBER" "$RESET" # Detect local first detect_local echo "" # Then all Pi nodes for node in "${PI_NODES[@]}"; do cmd_detect "$node" 2>/dev/null || true echo "" done printf '%bFleet scan complete. Profiles in %s/detected/%b\n' "$GREEN" "$HW_DIR" "$RESET" } cmd_info() { local node="$1" local detected="$HW_DIR/detected/${node}.yaml" if [[ ! -f "$detected" ]]; then echo "No hardware data for '$node'. Run: br-hw detect $node" return 1 fi cat "$detected" } # Main case "${1:-}" in detect) cmd_detect "${2:-}" ;; list) cmd_list ;; scan) cmd_scan ;; add) cmd_add "${2:?profile name required}" ;; apply) cmd_apply "${2:?profile name required}" "${3:-}" ;; info) cmd_info "${2:?node name required}" ;; -h|--help|help|"") usage ;; *) echo "Unknown: $1"; usage; exit 1 ;; esac