#!/usr/bin/env bash set -e source .env ## Spore Control Script ## Usage: ./ctl.sh [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 - OTA update specific node ## ota all - OTA update all nodes in cluster ## cluster members - List cluster members ## node wifi [ip] - Configure WiFi on node ## node label set [ip] - Set a label on node ## node label delete [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 [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 [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 [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}