// src/core/CommandMgr.cpp #include "CommandMgr.hpp" #include "HttpClient.hpp" #include "NetMgr.hpp" // Access CFG set in .ino #include "../app/AppConfig.hpp" extern AppConfig CFG; namespace CommandMgr { struct Command { long id = 0; String type; String payload; }; static String BASE; static String IMEI; static uint32_t lastPoll = 0; // ---------- tiny JSON helpers ---------- static int pickInt(const String& j, const char* key) { String pat = String("\"") + key + "\":"; int k = j.indexOf(pat); if (k < 0) return -1; k += pat.length(); while (k < (int)j.length() && (j[k]==' '||j[k]==':')) k++; int i = k; while (i < (int)j.length() && isDigit(j[i])) i++; if (i <= k) return -1; return j.substring(k, i).toInt(); } static String pickStr(const String& j, const char* key) { String pat = String("\"") + key + "\":"; int k = j.indexOf(pat); if (k < 0) return ""; k += pat.length(); while (k < (int)j.length() && (j[k]==' '||j[k]==':'||j[k]=='\"')) k++; if (k < (int)j.length() && j[k-1]=='\"') { int e = j.indexOf('\"', k); if (e > k) return j.substring(k, e); } return ""; } // naive split: [{".."},{".."}] static void parseArray(const String& json, std::vector& out) { String s = json; s.trim(); if (!s.startsWith("[")) return; if (s.length()>=2 && s.endsWith("]")) s = s.substring(1, s.length()-1); int start=0; while (start<(int)s.length()) { int next = s.indexOf("},{", start); String obj; if (next == -1) { obj = s.substring(start); obj.trim(); if (!obj.startsWith("{")) obj = "{" + obj; if (!obj.endsWith("}")) obj += "}"; start = s.length(); } else { obj = s.substring(start, next+1); if (!obj.startsWith("{")) obj = "{" + obj; start = next + 2; } if (obj.length()==0) continue; // extract id, type, payload Command c; c.id = pickInt(obj, "id"); c.type = pickStr(obj, "type"); // payload: assume object; take substring from "payload": { String pat="\"payload\":"; int k=obj.indexOf(pat); if (k>=0) { k += pat.length(); while (k<(int)obj.length() && obj[k]==' ') k++; if (k<(int)obj.length() && obj[k]=='{') { int depth=0, i=k; for (; i<(int)obj.length(); ++i) { if (obj[i]=='{') depth++; else if (obj[i]=='}') { depth--; if (depth==0) { i++; break; } } } c.payload = obj.substring(k, i); } else if (k<(int)obj.length() && obj[k]=='\"') { int e=obj.indexOf('\"', k+1); if (e>k) c.payload = obj.substring(k+1, e); } } } if (c.id>0 && c.type.length()) out.push_back(c); } } // ---------- receipts ---------- static bool sendReceipt(long id, const String& result, const String& detail) { String path = "/api/device/" + IMEI + "/command-receipts"; String body = String("{\"command_id\":") + id + ",\"result\":\"" + result + "\",\"detail\":\"" + detail + "\"}"; return HttpClient::postJson(BASE, path, body); } // ---------- handlers ---------- bool handleLights(const String& /*payload*/) { return true; } // placeholder bool handleSleep(const String& payload) { int sec = pickInt(payload, "interval_sec"); if (sec>0) { CFG.sleepSec = (uint32_t)sec; Serial.printf("Sleep sec -> %u\n", CFG.sleepSec); return true; } return false; } bool handleRingFence(const String& payload) { Serial.println("Ring fence payload: " + payload); return true; } // wifi: set home connect flag static bool handleWifi(const String& payload) { String tgt = pickStr(payload, "target"); if (tgt.equalsIgnoreCase("home")) { CFG.atHomeRequested = true; Serial.println("WiFi home requested"); return true; } return false; } // telemetry/poll periods static bool handleTelemetrySec(const String& payload) { int sec = pickInt(payload, "telemetry_sec"); if (sec>0) { CFG.telemetryPeriodSec=(uint32_t)sec; Serial.printf("Telemetry sec -> %u\n", CFG.telemetryPeriodSec); return true; } return false; } static bool handlePollSec(const String& payload) { int sec = pickInt(payload, "poll_sec"); if (sec>0) { CFG.commandPollSec=(uint32_t)sec; Serial.printf("Poll sec -> %u\n", CFG.commandPollSec); return true; } return false; } // ---------- public API ---------- void configure(const String& baseUrl, const String& imei) { BASE = baseUrl; IMEI = imei; } void poll(uint32_t minIntervalMs) { uint32_t now = millis(); if (minIntervalMs && (now - lastPoll < minIntervalMs)) return; lastPoll = now; if (BASE.isEmpty() || IMEI.isEmpty()) return; String path = "/api/device/" + IMEI + "/commands"; String body; bool ok = HttpClient::getJsonExact(BASE, path, body); if (!ok || body.length()==0) return; Serial.println("Commands JSON: " + body); std::vector cmds; parseArray(body, cmds); for (auto& c : cmds) { bool execOk = false; String detail; if (c.type == "lights") { execOk = handleLights(c.payload); detail = "lights"; } else if (c.type == "sleep") { execOk = handleSleep(c.payload); detail = "sleep"; } else if (c.type == "ring_fence") { execOk = handleRingFence(c.payload); detail = "ring_fence"; } else if (c.type == "wifi") { execOk = handleWifi(c.payload); detail = "wifi"; } else if (c.type == "telemetry_sec") { execOk = handleTelemetrySec(c.payload); detail = "telemetry_sec"; } else if (c.type == "poll_sec") { execOk = handlePollSec(c.payload); detail = "poll_sec"; } else if (c.type == "ota") { // reserved; readd OTAMgr later execOk = false; detail = "ota disabled"; } else { execOk = true; detail = "unknown"; } sendReceipt(c.id, execOk ? "ok" : "error", detail); } } } // namespace CommandMgr