Empire Node Network, The Mothership, Activation phase of your Empire Node network.

Empire Node Network, The Mothership, Activation phase of your Empire Node network.

Activation phase
of your Empire Node network.


Here’s a good step plan for your next few sessions to keep it tight and avoid chaos on the desk:




🧩 1. Jetson Assembly Checklist (for #2 and #3)


Goal: Achieve identical baseline configurations.


  • Mount the Jetson boards securely in the NanoCube using the proper nylon spacers and M2.5 screws.
  • Attach the Waveshare AC8265 WiFi/Bluetooth NICs to the M.2 E-key slot.
  • Snap in both antenna leads (MAIN + AUX) — those go on the small gold u.FL connectors.
  • Mount antennas through the case holes before tightening the board fully.
  • Plug in the microSD with the flashed JetPack / Ubuntu image.
  • Connect HDMI, keyboard/mouse, and USB-C 5V 4A power (or barrel jack if provided).



⚙️ 2. Raspberry Pi Prep


Goal: Stage all OS cards for a clean boot sequence.


  • Verify each Pi’s SD card boots cleanly (Ubuntu or Raspberry Pi OS Lite).
  • Label cards physically: Pi-DB, Pi-Comms, Pi-Control, Pi-Backup, Pi-Test.
  • Run updates:

    sudo apt update && sudo apt upgrade -y
  • Enable SSH and VNC via raspi-config.



🔌 3. Network Alignment


Goal: Create a single controlled subnet before powering all units.


  • Assign static IPs or reserve DHCP leases via your switch/router.
  • Label Ethernet cables per node.
  • If you’re running a managed switch, enable PoE for Pis only.
  • Jetsons → use their 1 GbE RJ45 ports (no PoE).



⚡ 4. “Alive” Sequence


Goal: Bring up each device in sequence and verify inter-node communication.


  1. Power up one Jetson, connect via SSH (ssh ubuntu@<ip>).
  2. Ping your laptop, router, and one Pi.
  3. Bring up MQTT or RabbitMQ broker (whichever is first in your comms plan).
  4. Run a small heartbeat script to confirm node-to-node latency and response.



🧽 5. Desk Cleanup & Cable Management


  • Route all USB-C and Ethernet through a central channel.
  • Label both ends of every cable.
  • Mount your power strips off the desk surface.
  • Leave enough airflow under each NanoCube.



🧠 6. Node Naming Convention


Use a naming scheme that feels cinematic and keeps it clear later:

JETSON01 – Governor
JETSON02 – Elder
JETSON03 – Archivist
PI01 – Comms
PI02 – Database
PI03 – SensorHub
PI04 – Monitor
PI05 – Backup





Once your desk is cleaned and every node is alive on the network, you’ll officially have your first Empire Mesh Cluster.
Would you like me to generate a Node Activation Script Pack next — including hostname setup, SSH key exchange, and a heartbeat status dashboard (Python + MQTT + Web UI)?
 
Node Activation Script Pack — full code + filenames, commented, and ready to drop in a folder and run.


Here’s your Node Activation Script Pack — full code + filenames, commented, and ready to drop in a folder and run.

📦 Folder Layout
activation-pack/
├─ README.md
├─ config/
│ ├─ nodes.env
│ └─ mqtt.env
├─ scripts/
│ ├─ 00_bootstrap.sh
│ ├─ 01_set_hostname.sh
│ ├─ 02_setup_user_ssh.sh
│ ├─ 03_push_authorized_keys.sh
│ ├─ 04_install_mosquitto.sh
│ └─ 05_enable_heartbeat_service.sh
├─ services/
│ ├─ heartbeat-client.service
│ └─ dashboard.service
└─ python/
├─ requirements.txt
├─ heartbeat_client.py
├─ dashboard_app.py
├─ templates/
│ └─ index.html
└─ static/
└─ style.css
🧭 How to Use (Quick Start)
Edit config/nodes.env with your IPs and hostnames (sample included below).

On each node (Jetsons + Pis):

scripts/00_bootstrap.sh (packages, Python venv, paho-mqtt, etc.)

scripts/01_set_hostname.sh <NewHostName> (sets hostname + /etc/hosts)

scripts/02_setup_user_ssh.sh (generates SSH key if missing)

On your admin laptop:

scripts/03_push_authorized_keys.sh (copies your public key to all nodes for passwordless SSH)

Choose a broker node (recommend PI01):

SSH into PI01 → run scripts/04_install_mosquitto.sh

Start heartbeat clients on every node:

scripts/05_enable_heartbeat_service.sh client

Start the dashboard on one node (recommend JETSON01):

scripts/05_enable_heartbeat_service.sh dashboard

Open dashboard in your browser: http://<JETSON01-IP>:8080

README.md
# Empire Node – Activation Pack

This pack brings your Jetsons + Raspberry Pis online with consistent hostnames, SSH keys, an MQTT broker, and a live heartbeat dashboard.

## Steps
1. Edit `config/nodes.env` and `config/mqtt.env`.
2. On each node:
```bash
sudo ./scripts/00_bootstrap.sh
sudo ./scripts/01_set_hostname.sh <HOSTNAME>
./scripts/02_setup_user_ssh.sh
From your admin machine:

./scripts/03_push_authorized_keys.sh
On broker node (e.g., PI01):

sudo ./scripts/04_install_mosquitto.sh
On all nodes:

sudo ./scripts/05_enable_heartbeat_service.sh client
On dashboard node (e.g., JETSON01):

sudo ./scripts/05_enable_heartbeat_service.sh dashboard
Visit: http://<dashboard-node-ip>:8080

Notes
Works on Ubuntu-based JetPack (Jetsons) and Ubuntu/Raspberry Pi OS (Pis).

Default user assumed ubuntu (Ubuntu) or pi (Raspberry Pi OS); adjust in nodes.env as needed.


---

## `config/nodes.env` (EDIT ME)

```ini
# Comma-separated list of nodes in the form HOSTNAME|IP|USER
NODES="JETSON01|192.168.50.21|ubuntu,\
JETSON02|192.168.50.22|ubuntu,\
JETSON03|192.168.50.23|ubuntu,\
PI01|192.168.50.31|ubuntu,\
PI02|192.168.50.32|ubuntu,\
PI03|192.168.50.33|ubuntu,\
PI04|192.168.50.34|ubuntu,\
PI05|192.168.50.35|ubuntu"

# For /etc/hosts population (optional extra entries)
MGMT_GATEWAY_IP="192.168.50.1"
MGMT_DOMAIN="empire.lan"
config/mqtt.env (EDIT ME)
# Broker details (used by both heartbeat client and dashboard)
MQTT_BROKER_HOST="192.168.50.31" # e.g., PI01
MQTT_BROKER_PORT="1883"
MQTT_USERNAME=""
MQTT_PASSWORD=""
MQTT_TOPIC_PREFIX="empire/heartbeat"
DASHBOARD_PORT="8080"
scripts/00_bootstrap.sh
#!/usr/bin/env bash
set -euo pipefail
# Purpose: Install base packages, Python venv, and prerequisites on a node.
# Works on Ubuntu / Debian variants (Jetson + Raspberry Pi OS based on Debian).

if [[ $EUID -ne 0 ]]; then
echo "Run as root: sudo ./scripts/00_bootstrap.sh"
exit 1
fi

export DEBIAN_FRONTEND=noninteractive

apt-get update
apt-get install -y --no-install-recommends \
ca-certificates curl git wget vim htop net-tools dnsutils \
python3 python3-venv python3-pip python3-dev \
build-essential pkg-config \
jq unzip

# Prepare app directory
APP_DIR="/opt/empire"
mkdir -p "$APP_DIR"
cp -r ./python "$APP_DIR/"
cp -r ./config "$APP_DIR/"
chown -R ${SUDO_USER:-$USER}:${SUDO_USER:-$USER} "$APP_DIR"

# Create venv and install Python deps
python3 -m venv "$APP_DIR/venv"
source "$APP_DIR/venv/bin/activate"
pip install --upgrade pip
pip install -r "$APP_DIR/python/requirements.txt"
deactivate

echo "[OK] Bootstrap complete on $(hostname)"
scripts/01_set_hostname.sh
#!/usr/bin/env bash
set -euo pipefail
# Purpose: Set hostname and populate /etc/hosts with the cluster map.

if [[ $EUID -ne 0 ]]; then
echo "Run as root: sudo ./scripts/01_set_hostname.sh <HOSTNAME>"
exit 1
fi

NEW_HOSTNAME="${1:-}"
if [[ -z "$NEW_HOSTNAME" ]]; then
echo "Usage: sudo ./scripts/01_set_hostname.sh <HOSTNAME>"
exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$ROOT_DIR/config/nodes.env"

echo "$NEW_HOSTNAME" > /etc/hostname
hostnamectl set-hostname "$NEW_HOSTNAME"

# Build hosts file entries
{
echo "127.0.0.1 localhost"
echo "::1 localhost ip6-localhost ip6-loopback"
echo "ff02::1 ip6-allnodes"
echo "ff02::2 ip6-allrouters"
# Management gateway hint
[[ -n "${MGMT_GATEWAY_IP:-}" ]] && echo "$MGMT_GATEWAY_IP gateway.${MGMT_DOMAIN:-local}"

# Cluster nodes
IFS=',' read -ra ARR <<< "$NODES"
for rec in "${ARR[@]}"; do
IFS='|' read -r H IP U <<< "$rec"
echo "$IP ${H}.${MGMT_DOMAIN:-local} $H"
done
} > /etc/hosts

echo "[OK] Hostname set to $NEW_HOSTNAME and /etc/hosts updated."
scripts/02_setup_user_ssh.sh
#!/usr/bin/env bash
set -euo pipefail
# Purpose: Ensure the current (non-root) user has an SSH key and correct perms.

if [[ $EUID -eq 0 ]]; then
USERNAME="${SUDO_USER:-ubuntu}"
else
USERNAME="$USER"
fi

HOME_DIR="$(eval echo "~$USERNAME")"
SSH_DIR="$HOME_DIR/.ssh"
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
if [[ ! -f "$SSH_DIR/id_rsa" && ! -f "$SSH_DIR/id_ed25519" ]]; then
# Prefer Ed25519 keys
ssh-keygen -t ed25519 -N "" -f "$SSH_DIR/id_ed25519" -C "$USERNAME@$(hostname)"
KEYFILE="$SSH_DIR/id_ed25519.pub"
else
KEYFILE="$(ls "$SSH_DIR"/*.pub | head -n1)"
fi

touch "$SSH_DIR/authorized_keys"
chmod 600 "$SSH_DIR/authorized_keys"
chown -R "$USERNAME":"$USERNAME" "$SSH_DIR"

echo "[OK] SSH ready for $USERNAME."
echo "Public key:"
cat "$KEYFILE"
scripts/03_push_authorized_keys.sh
#!/usr/bin/env bash
set -euo pipefail
# Purpose: From your admin laptop, push your local public key to all nodes' authorized_keys.

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT_DIR/config/nodes.env"

# Determine which public key to push
DEFAULT_KEY="${HOME}/.ssh/id_ed25519.pub"
[[ ! -f "$DEFAULT_KEY" ]] && DEFAULT_KEY="${HOME}/.ssh/id_rsa.pub"
if [[ ! -f "$DEFAULT_KEY" ]]; then
echo "No public key found in ~/.ssh. Run ssh-keygen first."
exit 1
fi

PUBKEY_CONTENT="$(cat "$DEFAULT_KEY")"

IFS=',' read -ra ARR <<< "$NODES"
for rec in "${ARR[@]}"; do
IFS='|' read -r HOST IP USER <<< "$rec"
echo "[*] Pushing key to $HOST ($IP) as $USER..."
ssh -o StrictHostKeyChecking=no ${USER}@${IP} "mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && grep -qxF \"$PUBKEY_CONTENT\" ~/.ssh/authorized_keys || echo \"$PUBKEY_CONTENT\" >> ~/.ssh/authorized_keys"
echo "[OK] $HOST"
done
scripts/04_install_mosquitto.sh
#!/usr/bin/env bash
set -euo pipefail
# Purpose: Install Mosquitto MQTT broker with a minimal config (no-auth by default; edit for credentials).

if [[ $EUID -ne 0 ]]; then
echo "Run as root: sudo ./scripts/04_install_mosquitto.sh"
exit 1
fi

apt-get update
apt-get install -y mosquitto mosquitto-clients

# Minimal listener on 1883, anonymous enabled (LAN only; lock down later)
cat >/etc/mosquitto/conf.d/empire.conf <<'EOF'
listener 1883 0.0.0.0
allow_anonymous true
persistence true
persistence_location /var/lib/mosquitto/
EOF

systemctl enable mosquitto
systemctl restart mosquitto

echo "[OK] Mosquitto installed and running on port 1883."
scripts/05_enable_heartbeat_service.sh
#!/usr/bin/env bash
set -euo pipefail
# Purpose: Enable either the heartbeat client or the dashboard as a systemd service.

ROLE="${1:-client}" # client | dashboard
APP_DIR="/opt/empire"
SERV_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../services" && pwd)"

if [[ $EUID -ne 0 ]]; then
echo "Run as root: sudo ./scripts/05_enable_heartbeat_service.sh [client|dashboard]"
exit 1
fi

# Copy services
cp "$SERV_DIR/heartbeat-client.service" /etc/systemd/system/
cp "$SERV_DIR/dashboard.service" /etc/systemd/system/

systemctl daemon-reload

if [[ "$ROLE" == "client" ]]; then
systemctl enable heartbeat-client
systemctl restart heartbeat-client
echo "[OK] Heartbeat client enabled."
elif [[ "$ROLE" == "dashboard" ]]; then
systemctl enable empire-dashboard
systemctl restart empire-dashboard
echo "[OK] Dashboard enabled."
else
echo "Unknown role: $ROLE (use: client | dashboard)"
exit 2
fi
services/heartbeat-client.service
[Unit]
Description=Empire Heartbeat Client
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/empire
EnvironmentFile=/opt/empire/config/mqtt.env
ExecStart=/opt/empire/venv/bin/python /opt/empire/python/heartbeat_client.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
Change User=ubuntu to pi on Raspberry Pi OS if needed.

services/dashboard.service
[Unit]
Description=Empire Heartbeat Dashboard (Flask)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/empire
EnvironmentFile=/opt/empire/config/mqtt.env
ExecStart=/opt/empire/venv/bin/python /opt/empire/python/dashboard_app.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
Change User=ubuntu to pi on Raspberry Pi OS if needed.

python/requirements.txt
Flask==3.0.3
paho-mqtt==2.1.0
gunicorn==22.0.0
python/heartbeat_client.py
#!/usr/bin/env python3
"""
Heartbeat client:
- Publishes a JSON heartbeat every 5 seconds to MQTT.
- Topic: <prefix>/<hostname>
- Payload includes: hostname, ip, ts, cpu load, mem, uptime, and model hint.
"""

import os, json, time, socket, subprocess, datetime
import paho.mqtt.client as mqtt

BROKER = os.getenv("MQTT_BROKER_HOST", "127.0.0.1")
PORT = int(os.getenv("MQTT_BROKER_PORT", "1883"))
USER = os.getenv("MQTT_USERNAME", "")
PWD = os.getenv("MQTT_PASSWORD", "")
TOPIC_PREFIX = os.getenv("MQTT_TOPIC_PREFIX", "empire/heartbeat")

HOST = socket.gethostname()

def get_ip():
try:
# Gets primary IP without external calls
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return "0.0.0.0"

def sh(cmd):
return subprocess.check_output(cmd, shell=True, text=True).strip()

def model_hint():
# Quick hardware hint (Jetson vs Pi vs generic)
try:
cpuinfo = sh("cat /proc/cpuinfo | head -n 20")
if "NVIDIA" in cpuinfo or "Jetson" in cpuinfo:
return "Jetson"
if "Raspberry Pi" in sh("cat /proc/device-tree/model 2>/dev/null || true"):
return "Raspberry Pi"
except Exception:
pass
return "Linux"

def sys_metrics():
# CPU load average (1 min), mem usage %, uptime minutes
try:
load1 = float(os.getloadavg()[0])
except Exception:
load1 = -1.0

mem_total = 0
mem_free = 0
try:
with open("/proc/meminfo") as f:
for line in f:
if line.startswith("MemTotal"):
mem_total = int(line.split()[1]) # kB
elif line.startswith("MemAvailable"):
mem_free = int(line.split()[1]) # kB
mem_used_pct = round(100.0 * (1 - (mem_free / max(1, mem_total))), 2)
except Exception:
mem_used_pct = -1.0

try:
uptime_s = float(open("/proc/uptime").read().split()[0])
except Exception:
uptime_s = 0.0
return load1, mem_used_pct, int(uptime_s // 60)

def main():
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"hb-{HOST}")
if USER:
client.username_pw_set(USER, PWD)

client.connect(BROKER, PORT, keepalive=30)
client.loop_start()

topic = f"{TOPIC_PREFIX}/{HOST}"
while True:
load1, mem_used_pct, uptime_m = sys_metrics()
payload = {
"host": HOST,
"ip": get_ip(),
"ts": datetime.datetime.utcnow().isoformat() + "Z",
"load1": load1,
"mem_used_pct": mem_used_pct,
"uptime_m": uptime_m,
"model": model_hint(),
}
client.publish(topic, json.dumps(payload), qos=0, retain=True)
time.sleep(5)

if __name__ == "__main__":
main()
python/dashboard_app.py
#!/usr/bin/env python3
"""
Flask dashboard:
- Subscribes to MQTT <prefix>/#
- Maintains in-memory state per host (last payload + last seen)
- Serves a dark UI table at /
"""

import os, json, time, threading
from datetime import datetime, timezone
from flask import Flask, render_template
import paho.mqtt.client as mqtt

BROKER = os.getenv("MQTT_BROKER_HOST", "127.0.0.1")
PORT = int(os.getenv("MQTT_BROKER_PORT", "1883"))
USER = os.getenv("MQTT_USERNAME", "")
PWD = os.getenv("MQTT_PASSWORD", "")
TOPIC_PREFIX = os.getenv("MQTT_TOPIC_PREFIX", "empire/heartbeat")
WEB_PORT = int(os.getenv("DASHBOARD_PORT", "8080"))

state = {} # host -> {payload:dict, last: epoch_secs}

def on_connect(client, userdata, flags, reason_code, properties=None):
client.subscribe(f"{TOPIC_PREFIX}/#")

def on_message(client, userdata, msg):
try:
payload = json.loads(msg.payload.decode("utf-8"))
host = payload.get("host", "unknown")
state[host] = {
"payload": payload,
"last": time.time()
}
except Exception:
pass

def mqtt_thread():
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="empire-dashboard")
if USER:
client.username_pw_set(USER, PWD)
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, PORT, keepalive=30)
client.loop_forever()

app = Flask(__name__, template_folder="templates", static_folder="static")

@app.route("/")
def index():
rows = []
now = time.time()
for host, rec in sorted(state.items()):
p = rec["payload"]
age = now - rec["last"]
rows.append({
"host": p.get("host"),
"ip": p.get("ip"),
"model": p.get("model"),
"load1": p.get("load1"),
"mem_used_pct": p.get("mem_used_pct"),
"uptime_m": p.get("uptime_m"),
"ts": p.get("ts"),
"age_s": int(age),
"status": "OK" if age < 20 else ("LATE" if age < 60 else "DOWN")
})
return render_template("index.html", rows=rows, updated=datetime.now(timezone.utc).isoformat())

if __name__ == "__main__":
t = threading.Thread(target=mqtt_thread, daemon=True)
t.start()
app.run(host="0.0.0.0", port=WEB_PORT)
python/templates/index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Empire Mesh – Heartbeat</title>
<link rel="stylesheet" href="/static/style.css">
<meta http-equiv="refresh" content="5">
</head>
<body>
<header>
<h1>Empire Mesh – Heartbeat</h1>
<p class="subtitle">Live node status | Auto-refresh every 5s</p>
</header>
<main>
<table>
<thead>
<tr>
<th>Host</th>
<th>IP</th>
<th>Model</th>
<th>Load(1m)</th>
<th>Mem Used %</th>
<th>Uptime (min)</th>
<th>Last Beat (UTC)</th>
<th>Age (s)</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for r in rows %}
<tr class="{{ r.status }}">
<td>{{ r.host }}</td>
<td>{{ r.ip }}</td>
<td>{{ r.model }}</td>
<td>{{ r.load1 }}</td>
<td>{{ r.mem_used_pct }}</td>
<td>{{ r.uptime_m }}</td>
<td>{{ r.ts }}</td>
<td>{{ r.age_s }}</td>
<td><span class="badge {{ r.status }}">{{ r.status }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</main>
<footer>
<small>Updated: {{ updated }}</small>
</footer>
</body>
</html>
python/static/style.css
/* Dark, neon-adjacent vibe without being loud */
:root {
--bg: #000d1a;
--panel: #061b2b;
--text: #e6f7ff;
--muted: #8fbcd4;
--ok: #19c37d;
--late: #f6c343;
--down: #ef4146;
--accent: #00ccff;
}

* { box-sizing: border-box; }
body {
margin: 0; padding: 24px;
background: radial-gradient(1200px 800px at 20% 10%, #001223, var(--bg));
color: var(--text);
font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
header { margin-bottom: 16px; }
h1 { margin: 0; color: var(--accent); text-shadow: 0 0 8px rgba(0,204,255,0.3); }
.subtitle { color: var(--muted); margin-top: 4px; }

table {
width: 100%;
border-collapse: collapse;
background: var(--panel);
border: 1px solid #07344c;
box-shadow: 0 0 24px rgba(0,204,255,0.08);
border-radius: 12px;
overflow: hidden;
}
thead th {
text-align: left;
padding: 12px;
color: var(--accent);
border-bottom: 1px solid #07344c;
}
tbody td {
padding: 10px 12px;
border-top: 1px solid #072638;
}
tr.OK { background: linear-gradient(0deg, rgba(25,195,125,0.06), transparent); }
tr.LATE { background: linear-gradient(0deg, rgba(246,195,67,0.06), transparent); }
tr.DOWN { background: linear-gradient(0deg, rgba(239,65,70,0.06), transparent); }

.badge {
padding: 2px 8px; border-radius: 10px;
border: 1px solid currentColor; font-weight: 600;
}
.badge.OK { color: var(--ok); }
.badge.LATE { color: var(--late); }
.badge.DOWN { color: var(--down); }

footer { margin-top: 12px; color: var(--muted); }
💡 Extra Ideas (Optional, plug-and-play later)
Retention: Heartbeat uses retain=true, so the dashboard shows last-known state even after restarts.

Auth: Flip Mosquitto to username/password and TLS once you’re stable (we can generate a secure config next).

Metrics: Extend client to include disk usage, GPU temperature (Jetson), and throttling flags (Pi).

Alerts: Add a tiny notifier that sends SMS via Twilio if a node is DOWN > 60s.
 
Back
Top