system.cpp
1 #include "api.h" 2 #include <config.h> 3 #include <services/system.h> 4 #include "services/http.h" 5 #include "networking/update.h" 6 7 #include <Arduino.h> 8 #include <AsyncJson.h> 9 #include <ArduinoJson.h> 10 11 #include <time.h> 12 13 namespace { 14 15 void handle_device_status(AsyncWebServerRequest *request) { 16 StorageKind storage_kind = StorageKind::SD; 17 if (request->hasParam("location")) { 18 String location = request->getParam("location")->value(); 19 if (location == "littlefs") storage_kind = StorageKind::LittleFS; 20 } 21 22 AsyncJsonResponse *response = new AsyncJsonResponse(); 23 JsonObject root = response->getRoot().to<JsonObject>(); 24 root["ok"] = true; 25 26 time_t now = time(nullptr); 27 if (now > 0) { 28 struct tm utc_time; 29 gmtime_r(&now, &utc_time); 30 char time_buf[32]; 31 strftime(time_buf, sizeof(time_buf), "%Y-%m-%dT%H:%M:%SZ", &utc_time); 32 root["time"] = time_buf; 33 } else { 34 root["time"] = ""; 35 } 36 37 SystemQuery query = { 38 .preferred_storage = storage_kind, 39 .snapshot = {}, 40 }; 41 services::system::accessSnapshot(&query); 42 43 JsonObject data = root["data"].to<JsonObject>(); 44 45 JsonObject device = data["device"].to<JsonObject>(); 46 device["hostname"] = query.snapshot.identity.hostname; 47 device["platform"] = config::PLATFORM; 48 device["sdk_version"] = query.snapshot.sdk_version; 49 device["idf_version"] = query.snapshot.idf_version; 50 device["arduino_version"] = query.snapshot.arduino_version; 51 device["chip_model"] = query.snapshot.chip_model; 52 device["chip_cores"] = query.snapshot.chip_cores; 53 device["chip_revision"] = query.snapshot.chip_revision; 54 device["cpu_mhz"] = query.snapshot.cpu_mhz; 55 device["sketch_md5"] = query.snapshot.sketch_md5; 56 device["sketch_size"] = query.snapshot.sketch_size; 57 device["sketch_free"] = query.snapshot.sketch_free; 58 device["flash_size"] = query.snapshot.flash_size; 59 device["flash_speed_mhz"] = query.snapshot.flash_speed_mhz; 60 61 JsonObject network = data["network"].to<JsonObject>(); 62 network["connected"] = query.snapshot.network.connected; 63 network["ssid"] = query.snapshot.network.ssid; 64 network["ipv4_address"] = query.snapshot.network.ip; 65 network["wifi_rssi"] = query.snapshot.network.rssi; 66 network["mac_address"] = query.snapshot.network.mac; 67 68 JsonObject runtime = data["runtime"].to<JsonObject>(); 69 char uptime_buf[32] = {0}; 70 services::system::formatUptime(uptime_buf, sizeof(uptime_buf), query.snapshot.uptime_seconds); 71 runtime["uptime"] = uptime_buf; 72 runtime["uptime_seconds"] = query.snapshot.uptime_seconds; 73 runtime["temperature_celsius"] = query.snapshot.chip_temperature_celsius; 74 runtime["memory_heap_free"] = query.snapshot.heap_free; 75 runtime["memory_heap_total"] = query.snapshot.heap_total; 76 runtime["memory_heap_min_free"] = query.snapshot.heap_min_free; 77 runtime["memory_heap_max_alloc"] = query.snapshot.heap_max_alloc; 78 runtime["memory_psram_total"] = query.snapshot.psram_total; 79 runtime["memory_psram_free"] = query.snapshot.psram_free; 80 81 JsonObject sleep = data["sleep"].to<JsonObject>(); 82 sleep["pending"] = query.snapshot.sleep.pending; 83 sleep["requested_duration_seconds"] = query.snapshot.sleep.requested_duration_seconds; 84 sleep["wake_cause"] = query.snapshot.sleep.wake_cause; 85 sleep["timer_wakeup_enabled"] = query.snapshot.sleep.timer_wakeup_enabled; 86 sleep["timer_wakeup_us"] = (unsigned long long)query.snapshot.sleep.timer_wakeup_us; 87 sleep["enabled"] = query.snapshot.sleep.config_enabled; 88 sleep["default_duration_seconds"] = query.snapshot.sleep.default_duration_seconds; 89 90 JsonObject logger = data["data_logger"].to<JsonObject>(); 91 logger["initialized"] = query.snapshot.data_logger.initialized; 92 logger["sd_ready"] = query.snapshot.data_logger.sd_ready; 93 logger["header_written"] = query.snapshot.data_logger.header_written; 94 logger["interval_ms"] = query.snapshot.data_logger.interval_ms; 95 logger["last_log_ms"] = query.snapshot.data_logger.last_log_ms; 96 logger["path"] = query.snapshot.data_logger.path; 97 98 JsonObject storage = data["storage"].to<JsonObject>(); 99 storage["location"] = (query.snapshot.storage.kind == StorageKind::LittleFS) ? "littlefs" : "sd"; 100 storage["mounted"] = query.snapshot.storage.mounted; 101 storage["total_bytes"] = query.snapshot.storage.total_bytes; 102 storage["used_bytes"] = query.snapshot.storage.used_bytes; 103 storage["free_bytes"] = query.snapshot.storage.free_bytes; 104 105 JsonObject sse = data["sse"].to<JsonObject>(); 106 sse["clients"] = services::http::sseClientCount(); 107 sse["avg_packets_waiting"] = services::http::sseAvgPacketsWaiting(); 108 109 response->setLength(); 110 request->send(response); 111 } 112 113 void handle_device_reset(AsyncWebServerRequest *request) { 114 request->send(200, asyncsrv::T_application_json, "{\"ok\":true,\"message\":\"rebooting\"}"); 115 xTaskCreate( 116 [](void *arg) { 117 (void)arg; 118 vTaskDelay(pdMS_TO_TICKS(100)); 119 ESP.restart(); 120 }, 121 "http-reset", 2048, nullptr, 1, nullptr); 122 } 123 124 void handle_sleep_config_get(AsyncWebServerRequest *request) { 125 SleepConfig config = {}; 126 if (!power::sleep::accessConfig(&config)) { 127 request->send(500, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"sleep config unavailable\"}"); 128 return; 129 } 130 131 AsyncJsonResponse *response = new AsyncJsonResponse(); 132 JsonObject root = response->getRoot().to<JsonObject>(); 133 root["ok"] = true; 134 JsonObject data = root["data"].to<JsonObject>(); 135 data["enabled"] = config.enabled; 136 data["duration_seconds"] = config.duration_seconds; 137 response->setLength(); 138 request->send(response); 139 } 140 141 } 142 143 void services::http::api::system::registerRoutes(AsyncWebServer &server, 144 AsyncRateLimitMiddleware &reset_limit, 145 AsyncRateLimitMiddleware &ota_limit) { 146 server.on("/api/system/device/status", HTTP_GET, handle_device_status); 147 server.on("/api/system/device/actions/reset", HTTP_POST, handle_device_reset) 148 .addMiddleware(&reset_limit); 149 server.on("/api/system/sleep/config", HTTP_GET, handle_sleep_config_get); 150 151 AsyncCallbackJsonWebHandler &sleep_config_handler = 152 server.on("/api/system/sleep/config", HTTP_POST, 153 [](AsyncWebServerRequest *request, JsonVariant &json) { 154 SleepConfig config = {}; 155 if (!power::sleep::accessConfig(&config)) { 156 request->send(500, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"sleep config unavailable\"}"); 157 return; 158 } 159 160 JsonObject body = json.as<JsonObject>(); 161 config.enabled = body["enabled"] | config.enabled; 162 config.duration_seconds = body["duration_seconds"] | config.duration_seconds; 163 164 if (!power::sleep::storeConfig(&config)) { 165 request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"invalid sleep config\"}"); 166 return; 167 } 168 169 if (!config.enabled) { 170 power::sleep::abortPending(); 171 } 172 173 AsyncJsonResponse *response = new AsyncJsonResponse(); 174 JsonObject root = response->getRoot().to<JsonObject>(); 175 root["ok"] = true; 176 JsonObject data = root["data"].to<JsonObject>(); 177 data["enabled"] = config.enabled; 178 data["duration_seconds"] = config.duration_seconds; 179 response->setLength(); 180 request->send(response); 181 }); 182 sleep_config_handler.setMaxContentLength(256); 183 184 server.on("/api/system/sleep/actions/trigger", HTTP_POST, 185 [](AsyncWebServerRequest *request) { 186 SleepConfig config = {}; 187 if (!power::sleep::accessConfig(&config) || config.duration_seconds == 0) { 188 request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"no sleep duration configured\"}"); 189 return; 190 } 191 192 SleepCommand command = { .duration_seconds = config.duration_seconds, .ok = false }; 193 power::sleep::request(&command); 194 195 AsyncJsonResponse *response = new AsyncJsonResponse(); 196 JsonObject root = response->getRoot().to<JsonObject>(); 197 root["ok"] = command.ok; 198 root["duration_seconds"] = config.duration_seconds; 199 response->setLength(); 200 request->send(response); 201 }); 202 203 server.on("/api/system/ota/rollback", HTTP_GET, 204 [](AsyncWebServerRequest *request) { 205 AsyncJsonResponse *response = new AsyncJsonResponse(); 206 JsonObject root = response->getRoot().to<JsonObject>(); 207 root["can_rollback"] = ::networking::update::canRollback(); 208 response->setLength(); 209 request->send(response); 210 }); 211 212 server.on("/api/system/ota/rollback", HTTP_POST, 213 [](AsyncWebServerRequest *request) { 214 bool ok = ::networking::update::rollback(); 215 AsyncJsonResponse *response = new AsyncJsonResponse(); 216 response->setCode(ok ? 200 : 400); 217 JsonObject root = response->getRoot().to<JsonObject>(); 218 root["ok"] = ok; 219 if (ok) root["message"] = "rollback set — reboot to activate"; 220 response->setLength(); 221 request->send(response); 222 }).addMiddleware(&ota_limit); 223 224 AsyncCallbackJsonWebHandler &ota_url_handler = 225 server.on("/api/system/ota/url", HTTP_POST, 226 [](AsyncWebServerRequest *request, JsonVariant &json) { 227 JsonObject body = json.as<JsonObject>(); 228 String url = body["url"] | ""; 229 if (url.isEmpty()) { 230 request->send(400, asyncsrv::T_application_json, "{\"ok\":false,\"error\":\"missing url\"}"); 231 return; 232 } 233 234 request->send(200, asyncsrv::T_application_json, "{\"ok\":true,\"message\":\"update started\"}"); 235 236 String url_copy = url; 237 xTaskCreate([](void *arg) { 238 String *update_url = (String *)arg; 239 bool ok = ::networking::update::applyFromURL(update_url->c_str()); 240 delete update_url; 241 if (ok) { 242 Serial.println(F("[ota] rebooting...")); 243 delay(500); 244 ESP.restart(); 245 } 246 vTaskDelete(nullptr); 247 }, "ota-url", 8192, new String(url_copy), 1, nullptr); 248 }); 249 ota_url_handler.setMaxContentLength(1024); 250 ota_url_handler.addMiddleware(&ota_limit); 251 252 server.on("/api/system/ota/sd", HTTP_POST, 253 [](AsyncWebServerRequest *request) { 254 AsyncWebServerRequestPtr weak = request->pause(); 255 256 xTaskCreate([](void *arg) { 257 AsyncWebServerRequestPtr *wp = (AsyncWebServerRequestPtr *)arg; 258 bool ok = ::networking::update::applyFromSD(); 259 260 auto request = wp->lock(); 261 delete wp; 262 if (request) { 263 if (ok) { 264 request->send(200, asyncsrv::T_application_json, 265 "{\"ok\":true,\"message\":\"rebooting\"}"); 266 } else { 267 request->send(400, asyncsrv::T_application_json, 268 "{\"ok\":false,\"error\":\"no update.bin on SD\"}"); 269 } 270 } 271 if (ok) { 272 delay(500); 273 ESP.restart(); 274 } 275 vTaskDelete(nullptr); 276 }, "ota-sd", 8192, new AsyncWebServerRequestPtr(weak), 1, nullptr); 277 }).addMiddleware(&ota_limit); 278 }