Initial monorepo — everything BlackRoad in one place
bin/ 230 CLI tools (ask-*, br-*, agent-*, roadid, carpool) scripts/ 99 automation scripts fleet/ Node configs and deployment workers/ Cloudflare Worker sources (roadpay, road-search, squad webhooks) roadc/ RoadC programming language roadnet/ Mesh network (5 APs, WireGuard) operator/ Memory system scripts config/ System configs dotfiles/ Shell configs docs/ Documentation BlackRoad OS — Pave Tomorrow. RoadChain-SHA2048: d1a24f55318d338b RoadChain-Identity: alexa@sovereign RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
This commit is contained in:
256
scripts/road-phone.sh
Executable file
256
scripts/road-phone.sh
Executable file
@@ -0,0 +1,256 @@
|
||||
#!/bin/bash
|
||||
# ☎️ RoadPhone — Pi-side landline to Alexandria
|
||||
#
|
||||
# Each Pi runs this as a service. It watches local files for changes,
|
||||
# then "dials" the Mac via rsync over SSH to drop off its payload.
|
||||
# Mac's ring daemon picks it up and pushes to git.
|
||||
#
|
||||
# Old school landline routing:
|
||||
# Pi detects change → picks up phone → dials Mac (rsync)
|
||||
# → Mac switchboard receives → routes to git
|
||||
#
|
||||
# Usage:
|
||||
# road-phone.sh # Run foreground
|
||||
# road-phone.sh --install # Install as systemd service
|
||||
# road-phone.sh --uninstall # Remove service
|
||||
# road-phone.sh --call # One-shot dial (no watching)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── identity ─────────────────────────────────────────────
|
||||
|
||||
HOSTNAME=$(hostname)
|
||||
SWITCHBOARD="alexa@192.168.4.28" # Alexandria Mac
|
||||
DROPOFF="~/local/fleet/$HOSTNAME"
|
||||
LOGFILE="/var/log/road-phone.log"
|
||||
PIDFILE="/tmp/road-phone.pid"
|
||||
RING_INTERVAL=60 # check every 60s
|
||||
CALL_COUNT=0
|
||||
|
||||
# Colors
|
||||
P='\033[38;5;205m' # pink
|
||||
G='\033[38;5;82m' # green
|
||||
A='\033[38;5;214m' # amber
|
||||
V='\033[38;5;135m' # violet
|
||||
R='\033[0m' # reset
|
||||
|
||||
log() { echo -e "${P}[phone:$HOSTNAME]${R} $(date '+%H:%M:%S') $1" | tee -a "$LOGFILE" 2>/dev/null || echo "$1"; }
|
||||
ring() { echo -e "${G}☎️ RING${R} $(date '+%H:%M:%S') $1" | tee -a "$LOGFILE" 2>/dev/null || echo "$1"; }
|
||||
busy() { echo -e "${A}📞 BUSY${R} $(date '+%H:%M:%S') $1" | tee -a "$LOGFILE" 2>/dev/null || echo "$1"; }
|
||||
tone() { echo -e "${V}📱 TONE${R} $(date '+%H:%M:%S') $1" | tee -a "$LOGFILE" 2>/dev/null || echo "$1"; }
|
||||
|
||||
# ── what to watch (each Pi's "phone book") ───────────────
|
||||
|
||||
WATCH_DIRS=(
|
||||
/opt/blackroad
|
||||
/etc/systemd/system
|
||||
)
|
||||
|
||||
SNAPSHOT_DIR="/tmp/road-phone-snapshot"
|
||||
mkdir -p "$SNAPSHOT_DIR"
|
||||
|
||||
# ── gather payload ───────────────────────────────────────
|
||||
|
||||
gather_payload() {
|
||||
local payload_dir="/tmp/road-phone-payload"
|
||||
rm -rf "$payload_dir"
|
||||
mkdir -p "$payload_dir"
|
||||
|
||||
# 1. Crontab for all users
|
||||
mkdir -p "$payload_dir/crontabs"
|
||||
for user in $(cut -d: -f1 /etc/passwd 2>/dev/null | head -20); do
|
||||
crontab -u "$user" -l > "$payload_dir/crontabs/$user.txt" 2>/dev/null || true
|
||||
done
|
||||
# Remove empty ones
|
||||
find "$payload_dir/crontabs" -empty -delete 2>/dev/null || true
|
||||
|
||||
# 2. Running services
|
||||
systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null \
|
||||
| awk '{print $1}' > "$payload_dir/services-running.txt" || true
|
||||
|
||||
# 3. System info snapshot
|
||||
cat > "$payload_dir/system-info.txt" << SYSEOF
|
||||
hostname: $(hostname)
|
||||
date: $(date -Iseconds)
|
||||
uptime: $(uptime)
|
||||
kernel: $(uname -r)
|
||||
temp: $(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null | awk '{printf "%.1f°C", $1/1000}' || echo "N/A")
|
||||
memory: $(free -h 2>/dev/null | awk '/Mem:/{print $3"/"$2}' || echo "N/A")
|
||||
disk: $(df -h / 2>/dev/null | awk 'NR==2{print $3"/"$2" ("$5")"}' || echo "N/A")
|
||||
load: $(cat /proc/loadavg 2>/dev/null | awk '{print $1, $2, $3}' || echo "N/A")
|
||||
throttle: $(vcgencmd get_throttled 2>/dev/null || echo "N/A")
|
||||
voltage: $(vcgencmd measure_volts core 2>/dev/null || echo "N/A")
|
||||
SYSEOF
|
||||
|
||||
# 4. /opt/blackroad scripts and configs
|
||||
if [ -d /opt/blackroad ]; then
|
||||
rsync -a --exclude='.git/' --exclude='node_modules/' --exclude='__pycache__/' \
|
||||
--exclude='*.db' --exclude='*.log' --exclude='venv/' \
|
||||
/opt/blackroad/ "$payload_dir/opt-blackroad/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 5. BlackRoad user scripts
|
||||
for user_home in /home/blackroad /home/pi /home/octavia; do
|
||||
if [ -d "$user_home" ]; then
|
||||
local uname=$(basename "$user_home")
|
||||
mkdir -p "$payload_dir/home-$uname"
|
||||
|
||||
# Grab shell scripts
|
||||
find "$user_home" -maxdepth 2 -name "*.sh" -size -100k 2>/dev/null \
|
||||
| while read -r f; do
|
||||
cp "$f" "$payload_dir/home-$uname/" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Grab .bashrc/.profile
|
||||
cp "$user_home/.bashrc" "$payload_dir/home-$uname/bashrc" 2>/dev/null || true
|
||||
cp "$user_home/.profile" "$payload_dir/home-$uname/profile" 2>/dev/null || true
|
||||
cp "$user_home/.bash_aliases" "$payload_dir/home-$uname/bash_aliases" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# 6. Key system configs
|
||||
mkdir -p "$payload_dir/etc"
|
||||
cp /boot/firmware/config.txt "$payload_dir/etc/config.txt" 2>/dev/null || \
|
||||
cp /boot/config.txt "$payload_dir/etc/config.txt" 2>/dev/null || true
|
||||
cp /etc/dhcpcd.conf "$payload_dir/etc/dhcpcd.conf" 2>/dev/null || true
|
||||
cp /etc/hosts "$payload_dir/etc/hosts" 2>/dev/null || true
|
||||
cp /etc/hostname "$payload_dir/etc/hostname" 2>/dev/null || true
|
||||
|
||||
# 7. Docker state
|
||||
if command -v docker &>/dev/null; then
|
||||
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' \
|
||||
> "$payload_dir/docker-containers.txt" 2>/dev/null || true
|
||||
docker images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}' \
|
||||
> "$payload_dir/docker-images.txt" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 8. Network state
|
||||
ip addr show 2>/dev/null | grep -E 'inet |state ' > "$payload_dir/network.txt" || true
|
||||
ss -tlnp 2>/dev/null > "$payload_dir/listening-ports.txt" || true
|
||||
|
||||
# 9. Ollama models (if running)
|
||||
if command -v ollama &>/dev/null; then
|
||||
ollama list > "$payload_dir/ollama-models.txt" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "$payload_dir"
|
||||
}
|
||||
|
||||
# ── detect changes ───────────────────────────────────────
|
||||
|
||||
has_changes() {
|
||||
local new_hash
|
||||
# Hash key files to detect changes
|
||||
new_hash=$(find /opt/blackroad /etc/systemd/system -name '*.sh' -o -name '*.service' -o -name '*.conf' -o -name '*.py' 2>/dev/null \
|
||||
| sort | head -200 | xargs md5sum 2>/dev/null | md5sum | awk '{print $1}')
|
||||
|
||||
local old_hash=""
|
||||
[ -f "$SNAPSHOT_DIR/last_hash" ] && old_hash=$(cat "$SNAPSHOT_DIR/last_hash")
|
||||
|
||||
if [ "$new_hash" != "$old_hash" ]; then
|
||||
echo "$new_hash" > "$SNAPSHOT_DIR/last_hash"
|
||||
return 0 # changed
|
||||
fi
|
||||
return 1 # no change
|
||||
}
|
||||
|
||||
# ── dial the switchboard ─────────────────────────────────
|
||||
|
||||
dial() {
|
||||
local payload_dir
|
||||
payload_dir=$(gather_payload)
|
||||
|
||||
tone "Dialing switchboard ($SWITCHBOARD)..."
|
||||
|
||||
# Ring ring — rsync payload to Mac
|
||||
if rsync -az --timeout=30 --delete \
|
||||
"$payload_dir/" \
|
||||
"$SWITCHBOARD:$DROPOFF/" 2>/dev/null; then
|
||||
|
||||
CALL_COUNT=$((CALL_COUNT + 1))
|
||||
ring "Call #$CALL_COUNT connected — payload delivered to $HOSTNAME line"
|
||||
rm -rf "$payload_dir"
|
||||
return 0
|
||||
else
|
||||
busy "Switchboard didn't pick up (Mac offline?)"
|
||||
rm -rf "$payload_dir"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── service install ──────────────────────────────────────
|
||||
|
||||
install_service() {
|
||||
local script_path
|
||||
script_path=$(readlink -f "$0")
|
||||
|
||||
cat > /tmp/road-phone.service << SVCEOF
|
||||
[Unit]
|
||||
Description=RoadPhone — BlackRoad landline to Alexandria
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=$script_path
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVCEOF
|
||||
|
||||
sudo mv /tmp/road-phone.service /etc/systemd/system/road-phone.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable road-phone.service
|
||||
sudo systemctl start road-phone.service
|
||||
log "Service installed and started"
|
||||
sudo systemctl status road-phone.service --no-pager
|
||||
}
|
||||
|
||||
uninstall_service() {
|
||||
sudo systemctl stop road-phone.service 2>/dev/null || true
|
||||
sudo systemctl disable road-phone.service 2>/dev/null || true
|
||||
sudo rm -f /etc/systemd/system/road-phone.service
|
||||
sudo systemctl daemon-reload
|
||||
log "Service uninstalled"
|
||||
}
|
||||
|
||||
# ── handle args ──────────────────────────────────────────
|
||||
|
||||
case "${1:-}" in
|
||||
--install) install_service; exit ;;
|
||||
--uninstall) uninstall_service; exit ;;
|
||||
--call) dial; exit ;;
|
||||
esac
|
||||
|
||||
# ── main loop ────────────────────────────────────────────
|
||||
|
||||
echo $$ > "$PIDFILE"
|
||||
trap 'rm -f "$PIDFILE"; log "Phone hanging up"; exit 0' INT TERM
|
||||
|
||||
log "☎️ RoadPhone online — $HOSTNAME picking up the landline"
|
||||
log "Switchboard: $SWITCHBOARD | Ring interval: ${RING_INTERVAL}s"
|
||||
|
||||
# Initial call on boot
|
||||
dial
|
||||
|
||||
# Watch loop
|
||||
SECONDS_SINCE_CALL=0
|
||||
while true; do
|
||||
sleep "$RING_INTERVAL"
|
||||
SECONDS_SINCE_CALL=$((SECONDS_SINCE_CALL + RING_INTERVAL))
|
||||
|
||||
if has_changes; then
|
||||
tone "Change detected on the line!"
|
||||
dial
|
||||
SECONDS_SINCE_CALL=0
|
||||
elif (( SECONDS_SINCE_CALL >= 900 )); then
|
||||
# Heartbeat call every 15 min even if nothing changed
|
||||
tone "Heartbeat ring (15m keepalive)"
|
||||
dial
|
||||
SECONDS_SINCE_CALL=0
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user