From b404852fc72464e7f09df7e51d56d642bf9744b7 Mon Sep 17 00:00:00 2001 From: 0x1d Date: Wed, 15 Oct 2025 22:36:20 +0200 Subject: [PATCH] feat: GET node config endpoint --- ctl.sh | 111 +++++++++++++++++++++++++++ include/spore/services/NodeService.h | 1 + src/spore/services/NodeService.cpp | 62 +++++++++++++++ 3 files changed, 174 insertions(+) diff --git a/ctl.sh b/ctl.sh index 682d7e2..235c90c 100755 --- a/ctl.sh +++ b/ctl.sh @@ -17,6 +17,7 @@ source .env ## 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 ## @@ -28,6 +29,8 @@ source .env ## ./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 @@ -316,6 +319,114 @@ function node { ${@:-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 "Discovery Interval: $(echo "$response_body" | jq -r '.cluster.discovery_interval_ms // "N/A"') ms" + 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 "Member Info Update Interval: $(echo "$response_body" | jq -r '.cluster.member_info_update_interval_ms // "N/A"') ms" + echo "Print Interval: $(echo "$response_body" | jq -r '.cluster.print_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}" diff --git a/include/spore/services/NodeService.h b/include/spore/services/NodeService.h index 748840b..e930373 100644 --- a/include/spore/services/NodeService.h +++ b/include/spore/services/NodeService.h @@ -21,4 +21,5 @@ private: void handleRestartRequest(AsyncWebServerRequest* request); void handleEndpointsRequest(AsyncWebServerRequest* request); void handleConfigRequest(AsyncWebServerRequest* request); + void handleGetConfigRequest(AsyncWebServerRequest* request); }; diff --git a/src/spore/services/NodeService.cpp b/src/spore/services/NodeService.cpp index 520462d..003ec07 100644 --- a/src/spore/services/NodeService.cpp +++ b/src/spore/services/NodeService.cpp @@ -37,6 +37,11 @@ void NodeService::registerEndpoints(ApiServer& api) { ParamSpec{String("labels"), true, String("body"), String("json"), {}, String("")} }); + // Config endpoint for getting node configuration (without WiFi password) + api.registerEndpoint("/api/node/config", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleGetConfigRequest(request); }, + std::vector{}); + // Generic local event endpoint api.registerEndpoint("/api/node/event", HTTP_POST, [this](AsyncWebServerRequest* request) { @@ -233,3 +238,60 @@ void NodeService::handleConfigRequest(AsyncWebServerRequest* request) { request->send(500, "application/json", "{\"error\":\"Failed to save configuration\"}"); } } + +void NodeService::handleGetConfigRequest(AsyncWebServerRequest* request) { + JsonDocument doc; + + // WiFi Configuration (excluding password for security) + JsonObject wifiObj = doc["wifi"].to(); + wifiObj["ssid"] = ctx.config.wifi_ssid; + wifiObj["connect_timeout_ms"] = ctx.config.wifi_connect_timeout_ms; + wifiObj["retry_delay_ms"] = ctx.config.wifi_retry_delay_ms; + + // Network Configuration + JsonObject networkObj = doc["network"].to(); + networkObj["udp_port"] = ctx.config.udp_port; + networkObj["api_server_port"] = ctx.config.api_server_port; + + // Cluster Configuration + JsonObject clusterObj = doc["cluster"].to(); + clusterObj["discovery_interval_ms"] = ctx.config.discovery_interval_ms; + clusterObj["heartbeat_interval_ms"] = ctx.config.heartbeat_interval_ms; + clusterObj["cluster_listen_interval_ms"] = ctx.config.cluster_listen_interval_ms; + clusterObj["status_update_interval_ms"] = ctx.config.status_update_interval_ms; + clusterObj["member_info_update_interval_ms"] = ctx.config.member_info_update_interval_ms; + clusterObj["print_interval_ms"] = ctx.config.print_interval_ms; + + // Node Status Thresholds + JsonObject thresholdsObj = doc["thresholds"].to(); + thresholdsObj["node_active_threshold_ms"] = ctx.config.node_active_threshold_ms; + thresholdsObj["node_inactive_threshold_ms"] = ctx.config.node_inactive_threshold_ms; + thresholdsObj["node_dead_threshold_ms"] = ctx.config.node_dead_threshold_ms; + + // System Configuration + JsonObject systemObj = doc["system"].to(); + systemObj["restart_delay_ms"] = ctx.config.restart_delay_ms; + systemObj["json_doc_size"] = ctx.config.json_doc_size; + + // Memory Management + JsonObject memoryObj = doc["memory"].to(); + memoryObj["low_memory_threshold_bytes"] = ctx.config.low_memory_threshold_bytes; + memoryObj["critical_memory_threshold_bytes"] = ctx.config.critical_memory_threshold_bytes; + memoryObj["max_concurrent_http_requests"] = ctx.config.max_concurrent_http_requests; + + // Custom Labels + if (!ctx.config.labels.empty()) { + JsonObject labelsObj = doc["labels"].to(); + for (const auto& kv : ctx.config.labels) { + labelsObj[kv.first.c_str()] = kv.second; + } + } + + // Add metadata + doc["version"] = "1.0"; + doc["retrieved_at"] = millis(); + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +}