/ firmware / src / services / http / api / system.cpp
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  }