Files
spore/ctl.sh
2025-10-19 13:48:13 +02:00

494 lines
18 KiB
Bash
Executable File

#!/usr/bin/env bash
set -e
source .env
## Spore Control Script
## Usage: ./ctl.sh <command> [options]
##
## Commands:
## build [target] - Build firmware for target (base, d1_mini, etc.)
## flash [target] - Flash firmware to device
## uploadfs [target] - Upload filesystem to device
## ota update <ip> <target> - OTA update specific node
## ota all <target> - OTA update all nodes in cluster
## cluster members - List cluster members
## node wifi <ssid> <password> [ip] - Configure WiFi on node
## node label set <key=value> [ip] - Set a label on node
## node label delete <key> [ip] - Delete a label from node
## node config get [ip] - Get node configuration
## node status [ip] - Get node status and information
## monitor - Monitor serial output
##
## Examples:
## ./ctl.sh build base
## ./ctl.sh flash d1_mini
## ./ctl.sh node wifi "MyNetwork" "MyPassword"
## ./ctl.sh node wifi "MyNetwork" "MyPassword" 192.168.1.100
## ./ctl.sh node label set "environment=production"
## ./ctl.sh node label set "location=office" 192.168.1.100
## ./ctl.sh node label delete "environment"
## ./ctl.sh node config get
## ./ctl.sh node config get 192.168.1.100
## ./ctl.sh node status
## ./ctl.sh node status 192.168.1.100
function info {
sed -n 's/^##//p' ctl.sh
}
function build {
function target {
echo "Building project for $1..."
pio run -e $1
}
function all {
pio run
}
${@:-all}
}
function flash {
function target {
echo "Flashing firmware for $1..."
pio run --target upload -e $1
}
${@:-info}
}
function mkfs {
~/bin/mklittlefs -c data \
-s 0x9000 \
-b 4096 \
-p 256 \
littlefs.bin
}
function flashfs {
esptool.py --port /dev/ttyUSB0 \
--baud 115200 \
write_flash 0xbb000 littlefs.bin
}
function uploadfs {
echo "Uploading files to LittleFS..."
pio run -e $1 -t uploadfs
}
function ota {
function update {
echo "Updating node at $1 with $2... "
curl -X POST \
-F "file=@.pio/build/$2/firmware.bin" \
http://$1/api/node/update | jq -r '.status'
}
function all {
echo "Updating all nodes..."
curl -s http://$API_NODE/api/cluster/members | jq -r '.members.[].ip' | while read -r ip; do
ota update $ip $1
done
}
${@:-info}
}
function cluster {
function members {
curl -s http://$API_NODE/api/cluster/members | jq -r '.members[] | "\(.hostname) \(.ip)"'
}
${@:-info}
}
function node {
function wifi {
if [ $# -lt 2 ]; then
echo "Usage: $0 node wifi <ssid> <password> [node_ip]"
echo " ssid: WiFi network name"
echo " password: WiFi password"
echo " node_ip: Optional IP address (defaults to API_NODE from .env)"
return 1
fi
local ssid="$1"
local password="$2"
local node_ip="${3:-$API_NODE}"
echo "Configuring WiFi on node $node_ip..."
echo "SSID: $ssid"
# Configure WiFi using the API endpoint
response=$(curl -s -w "\n%{http_code}" -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "ssid=$ssid&password=$password" \
"http://$node_ip/api/network/wifi/config" 2>/dev/null || echo -e "\n000")
# Extract HTTP status code and response body
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | head -n -1)
# Check if curl succeeded
if [ "$http_code" = "000" ] || [ -z "$response_body" ]; then
echo "Error: Failed to connect to node at $node_ip"
echo "Please check:"
echo " - Node is powered on and connected to network"
echo " - IP address is correct"
echo " - Node is running Spore firmware"
return 1
fi
# Check HTTP status code
if [ "$http_code" != "200" ]; then
echo "Error: HTTP $http_code - Server error"
echo "Response: $response_body"
return 1
fi
# Parse and display the response
status=$(echo "$response_body" | jq -r '.status // "unknown"')
message=$(echo "$response_body" | jq -r '.message // "No message"')
config_saved=$(echo "$response_body" | jq -r '.config_saved // false')
restarting=$(echo "$response_body" | jq -r '.restarting // false')
connected=$(echo "$response_body" | jq -r '.connected // false')
ip=$(echo "$response_body" | jq -r '.ip // "N/A"')
echo "Status: $status"
echo "Message: $message"
echo "Config saved: $config_saved"
if [ "$restarting" = "true" ]; then
echo "Restarting: true"
echo "Note: Node will restart to apply new WiFi settings"
fi
echo "Connected: $connected"
if [ "$connected" = "true" ]; then
echo "IP Address: $ip"
fi
# Return appropriate exit code
if [ "$status" = "success" ]; then
echo "WiFi configuration completed successfully!"
return 0
else
echo "WiFi configuration failed!"
return 1
fi
}
function label {
function set {
if [ $# -lt 1 ]; then
echo "Usage: $0 node label set <key=value> [node_ip]"
echo " key=value: Label key and value in format 'key=value'"
echo " node_ip: Optional IP address (defaults to API_NODE from .env)"
return 1
fi
local key_value="$1"
local node_ip="${2:-$API_NODE}"
# Parse key=value format
if [[ ! "$key_value" =~ ^[^=]+=.+$ ]]; then
echo "Error: Label must be in format 'key=value'"
echo "Example: environment=production"
return 1
fi
local key="${key_value%%=*}"
local value="${key_value#*=}"
echo "Setting label '$key=$value' on node $node_ip..."
# First get current labels
current_labels=$(curl -s "http://$node_ip/api/node/status" | jq -r '.labels // {}')
# Add/update the new label
updated_labels=$(echo "$current_labels" | jq --arg key "$key" --arg value "$value" '. + {($key): $value}')
# Send updated labels to the node
response=$(curl -s -w "\n%{http_code}" -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "labels=$updated_labels" \
"http://$node_ip/api/node/config" 2>/dev/null || echo -e "\n000")
# Extract HTTP status code and response body
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | head -n -1)
# Check if curl succeeded
if [ "$http_code" = "000" ] || [ -z "$response_body" ]; then
echo "Error: Failed to connect to node at $node_ip"
echo "Please check:"
echo " - Node is powered on and connected to network"
echo " - IP address is correct"
echo " - Node is running Spore firmware"
return 1
fi
# Check HTTP status code
if [ "$http_code" != "200" ]; then
echo "Error: HTTP $http_code - Server error"
echo "Response: $response_body"
return 1
fi
# Parse and display the response
status=$(echo "$response_body" | jq -r '.status // "unknown"')
message=$(echo "$response_body" | jq -r '.message // "No message"')
echo "Status: $status"
echo "Message: $message"
# Return appropriate exit code
if [ "$status" = "success" ]; then
echo "Label '$key=$value' set successfully!"
return 0
else
echo "Failed to set label!"
return 1
fi
}
function delete {
if [ $# -lt 1 ]; then
echo "Usage: $0 node label delete <key> [node_ip]"
echo " key: Label key to delete"
echo " node_ip: Optional IP address (defaults to API_NODE from .env)"
return 1
fi
local key="$1"
local node_ip="${2:-$API_NODE}"
echo "Deleting label '$key' from node $node_ip..."
# First get current labels
current_labels=$(curl -s "http://$node_ip/api/node/status" | jq -r '.labels // {}')
# Check if key exists
if [ "$(echo "$current_labels" | jq -r --arg key "$key" 'has($key)')" != "true" ]; then
echo "Warning: Label '$key' does not exist on node"
return 0
fi
# Remove the key
updated_labels=$(echo "$current_labels" | jq --arg key "$key" 'del(.[$key])')
# Send updated labels to the node
response=$(curl -s -w "\n%{http_code}" -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "labels=$updated_labels" \
"http://$node_ip/api/node/config" 2>/dev/null || echo -e "\n000")
# Extract HTTP status code and response body
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | head -n -1)
# Check if curl succeeded
if [ "$http_code" = "000" ] || [ -z "$response_body" ]; then
echo "Error: Failed to connect to node at $node_ip"
echo "Please check:"
echo " - Node is powered on and connected to network"
echo " - IP address is correct"
echo " - Node is running Spore firmware"
return 1
fi
# Check HTTP status code
if [ "$http_code" != "200" ]; then
echo "Error: HTTP $http_code - Server error"
echo "Response: $response_body"
return 1
fi
# Parse and display the response
status=$(echo "$response_body" | jq -r '.status // "unknown"')
message=$(echo "$response_body" | jq -r '.message // "No message"')
echo "Status: $status"
echo "Message: $message"
# Return appropriate exit code
if [ "$status" = "success" ]; then
echo "Label '$key' deleted successfully!"
return 0
else
echo "Failed to delete label!"
return 1
fi
}
${@:-info}
}
function config {
function get {
local node_ip="${1:-$API_NODE}"
echo "Getting configuration for node $node_ip..."
# Get node configuration
response=$(curl -s -w "\n%{http_code}" "http://$node_ip/api/node/config" 2>/dev/null || echo -e "\n000")
# Extract HTTP status code and response body
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | head -n -1)
# Check if curl succeeded
if [ "$http_code" = "000" ] || [ -z "$response_body" ]; then
echo "Error: Failed to connect to node at $node_ip"
echo "Please check:"
echo " - Node is powered on and connected to network"
echo " - IP address is correct"
echo " - Node is running Spore firmware"
return 1
fi
# Check HTTP status code
if [ "$http_code" != "200" ]; then
echo "Error: HTTP $http_code - Server error"
echo "Response: $response_body"
return 1
fi
# Parse and display the response in a nice format
echo ""
echo "=== Node Configuration ==="
echo "Node IP: $node_ip"
echo "Retrieved at: $(date)"
echo ""
# WiFi Configuration
echo "=== WiFi Configuration ==="
echo "SSID: $(echo "$response_body" | jq -r '.wifi.ssid // "N/A"')"
echo "Connect Timeout: $(echo "$response_body" | jq -r '.wifi.connect_timeout_ms // "N/A"') ms"
echo "Retry Delay: $(echo "$response_body" | jq -r '.wifi.retry_delay_ms // "N/A"') ms"
echo "Password: [HIDDEN]"
echo ""
# Network Configuration
echo "=== Network Configuration ==="
echo "UDP Port: $(echo "$response_body" | jq -r '.network.udp_port // "N/A"')"
echo "API Server Port: $(echo "$response_body" | jq -r '.network.api_server_port // "N/A"')"
echo ""
# Cluster Configuration
echo "=== Cluster Configuration ==="
echo "Heartbeat Interval: $(echo "$response_body" | jq -r '.cluster.heartbeat_interval_ms // "N/A"') ms"
echo "Cluster Listen Interval: $(echo "$response_body" | jq -r '.cluster.cluster_listen_interval_ms // "N/A"') ms"
echo "Status Update Interval: $(echo "$response_body" | jq -r '.cluster.status_update_interval_ms // "N/A"') ms"
echo ""
# Node Status Thresholds
echo "=== Node Status Thresholds ==="
echo "Active Threshold: $(echo "$response_body" | jq -r '.thresholds.node_active_threshold_ms // "N/A"') ms"
echo "Inactive Threshold: $(echo "$response_body" | jq -r '.thresholds.node_inactive_threshold_ms // "N/A"') ms"
echo "Dead Threshold: $(echo "$response_body" | jq -r '.thresholds.node_dead_threshold_ms // "N/A"') ms"
echo ""
# System Configuration
echo "=== System Configuration ==="
echo "Restart Delay: $(echo "$response_body" | jq -r '.system.restart_delay_ms // "N/A"') ms"
echo "JSON Doc Size: $(echo "$response_body" | jq -r '.system.json_doc_size // "N/A"') bytes"
echo ""
# Memory Management
echo "=== Memory Management ==="
echo "Low Memory Threshold: $(echo "$response_body" | jq -r '.memory.low_memory_threshold_bytes // "N/A"') bytes"
echo "Critical Memory Threshold: $(echo "$response_body" | jq -r '.memory.critical_memory_threshold_bytes // "N/A"') bytes"
echo "Max Concurrent HTTP Requests: $(echo "$response_body" | jq -r '.memory.max_concurrent_http_requests // "N/A"')"
echo ""
# Custom Labels
labels=$(echo "$response_body" | jq -r '.labels // {}')
if [ "$labels" != "{}" ] && [ "$labels" != "null" ]; then
echo "=== Custom Labels ==="
echo "$labels" | jq -r 'to_entries[] | "\(.key): \(.value)"'
echo ""
else
echo "=== Custom Labels ==="
echo "No custom labels set"
echo ""
fi
# Metadata
echo "=== Metadata ==="
echo "Configuration Version: $(echo "$response_body" | jq -r '.version // "N/A"')"
echo "Retrieved Timestamp: $(echo "$response_body" | jq -r '.retrieved_at // "N/A"')"
echo ""
echo "=== Raw JSON Response ==="
echo "$response_body" | jq '.'
return 0
}
${@:-info}
}
function status {
local node_ip="${1:-$API_NODE}"
echo "Getting status for node $node_ip..."
# Get node status
response=$(curl -s -w "\n%{http_code}" "http://$node_ip/api/node/status" 2>/dev/null || echo -e "\n000")
# Extract HTTP status code and response body
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | head -n -1)
# Check if curl succeeded
if [ "$http_code" = "000" ] || [ -z "$response_body" ]; then
echo "Error: Failed to connect to node at $node_ip"
echo "Please check:"
echo " - Node is powered on and connected to network"
echo " - IP address is correct"
echo " - Node is running Spore firmware"
return 1
fi
# Check HTTP status code
if [ "$http_code" != "200" ]; then
echo "Error: HTTP $http_code - Server error"
echo "Response: $response_body"
return 1
fi
# Parse and display the response in a nice format
echo ""
echo "=== Node Status ==="
echo "Hostname: $(echo "$response_body" | jq -r '.hostname // "N/A"')"
echo "IP Address: $node_ip"
echo "Free Heap: $(echo "$response_body" | jq -r '.freeHeap // "N/A"') bytes"
echo "Chip ID: $(echo "$response_body" | jq -r '.chipId // "N/A"')"
echo "SDK Version: $(echo "$response_body" | jq -r '.sdkVersion // "N/A"')"
echo "CPU Frequency: $(echo "$response_body" | jq -r '.cpuFreqMHz // "N/A"') MHz"
echo "Flash Size: $(echo "$response_body" | jq -r '.flashChipSize // "N/A"') bytes"
# Display labels if present
labels=$(echo "$response_body" | jq -r '.labels // {}')
if [ "$labels" != "{}" ] && [ "$labels" != "null" ]; then
echo ""
echo "=== Labels ==="
echo "$labels" | jq -r 'to_entries[] | "\(.key): \(.value)"'
else
echo ""
echo "=== Labels ==="
echo "No labels set"
fi
echo ""
echo "=== Raw JSON Response ==="
echo "$response_body" | jq '.'
return 0
}
${@:-info}
}
function monitor {
pio run --target monitor
}
${@:-info}