CommandMgr: add wifi home flag + sleep interval command; control loop uses CFG.atHomeRequested, CFG.sleepSec

This commit is contained in:
2025-09-05 12:24:02 +10:00
parent 8f2f83ddb6
commit 14ebfa5451

View File

@ -1,7 +1,10 @@
#include "CommandMgr.hpp" #include "CommandMgr.hpp"
#include "HttpClient.hpp" #include "HttpClient.hpp"
#include "AppConfig.hpp"
extern AppConfig CFG;
namespace { namespace
{
String BASE; String BASE;
String IMEI; String IMEI;
uint32_t lastPoll = 0; uint32_t lastPoll = 0;
@ -10,97 +13,134 @@ namespace {
// Extract array of objects from a JSON string like: [{"id":..,"type":"..","payload":{...}}, ...] // Extract array of objects from a JSON string like: [{"id":..,"type":"..","payload":{...}}, ...]
// Well do a naive split on "},{" for small payloads; replace with proper JSON parsing later. // Well do a naive split on "},{" for small payloads; replace with proper JSON parsing later.
long extractId(const String& obj) { long extractId(const String &obj)
{
String pat = "\"id\":"; String pat = "\"id\":";
int k = obj.indexOf(pat); int k = obj.indexOf(pat);
if (k < 0) return 0; if (k < 0)
return 0;
k += pat.length(); k += pat.length();
int e = k; int e = k;
while (e < (int)obj.length() && (isDigit(obj[e]) || obj[e]=='-')) e++; while (e < (int)obj.length() && (isDigit(obj[e]) || obj[e] == '-'))
e++;
return obj.substring(k, e).toInt(); return obj.substring(k, e).toInt();
} }
String extractString(const String& obj, const String& key) { String extractString(const String &obj, const String &key)
{
String pat = "\"" + key + "\":"; String pat = "\"" + key + "\":";
int k = obj.indexOf(pat); int k = obj.indexOf(pat);
if (k < 0) return ""; if (k < 0)
return "";
k += pat.length(); k += pat.length();
while (k < (int)obj.length() && (obj[k]==' ' || obj[k]=='\"')) k++; while (k < (int)obj.length() && (obj[k] == ' ' || obj[k] == '\"'))
k++;
// read quoted string // read quoted string
int end = obj.indexOf('\"', k); int end = obj.indexOf('\"', k);
if (end <= k) return ""; if (end <= k)
return "";
return obj.substring(k, end); return obj.substring(k, end);
} }
String extractPayload(const String& obj) { String extractPayload(const String &obj)
{
// payload is often an object. Find "payload": and read balanced braces. // payload is often an object. Find "payload": and read balanced braces.
String pat = "\"payload\":"; String pat = "\"payload\":";
int k = obj.indexOf(pat); int k = obj.indexOf(pat);
if (k < 0) return ""; if (k < 0)
return "";
k += pat.length(); k += pat.length();
while (k < (int)obj.length() && (obj[k]==' ')) k++; while (k < (int)obj.length() && (obj[k] == ' '))
if (k >= (int)obj.length()) return ""; k++;
if (obj[k] == '{') { if (k >= (int)obj.length())
return "";
if (obj[k] == '{')
{
int depth = 0; int depth = 0;
int i = k; int i = k;
for (; i < (int)obj.length(); ++i) { for (; i < (int)obj.length(); ++i)
if (obj[i] == '{') depth++; {
else if (obj[i] == '}') { if (obj[i] == '{')
depth++;
else if (obj[i] == '}')
{
depth--; depth--;
if (depth == 0) { i++; break; } if (depth == 0)
{
i++;
break;
}
} }
} }
return obj.substring(k, i); return obj.substring(k, i);
} else if (obj[k] == '\"') { }
int end = obj.indexOf('\"', k+1); else if (obj[k] == '\"')
if (end > k) return obj.substring(k+1, end); {
int end = obj.indexOf('\"', k + 1);
if (end > k)
return obj.substring(k + 1, end);
} }
return ""; return "";
} }
// Very naive split into objects. Works if server returns a small array. // Very naive split into objects. Works if server returns a small array.
void parseArray(const String& json, std::vector<CommandMgr::Command>& out) { void parseArray(const String &json, std::vector<CommandMgr::Command> &out)
{
String s = json; String s = json;
s.trim(); s.trim();
if (!s.startsWith("[")) return; if (!s.startsWith("["))
return;
// strip [ and ] // strip [ and ]
if (s.length() >= 2 && s.endsWith("]")) s = s.substring(1, s.length()-1); if (s.length() >= 2 && s.endsWith("]"))
s = s.substring(1, s.length() - 1);
// crude split on "},{" boundaries // crude split on "},{" boundaries
int start = 0; int start = 0;
while (start < (int)s.length()) { while (start < (int)s.length())
{
int next = s.indexOf("},{", start); int next = s.indexOf("},{", start);
String obj; String obj;
if (next == -1) { if (next == -1)
{
obj = s.substring(start); obj = s.substring(start);
obj.trim(); obj.trim();
if (obj.startsWith("{") && obj.endsWith("}")) { if (obj.startsWith("{") && obj.endsWith("}"))
{
// ok // ok
} }
else if (!obj.isEmpty()) { else if (!obj.isEmpty())
{
// Try pad // Try pad
if (!obj.startsWith("{")) obj = "{" + obj; if (!obj.startsWith("{"))
if (!obj.endsWith("}")) obj += "}"; obj = "{" + obj;
if (!obj.endsWith("}"))
obj += "}";
} }
start = s.length(); start = s.length();
} else { }
else
{
obj = s.substring(start, next + 1); // include the trailing } obj = s.substring(start, next + 1); // include the trailing }
if (!obj.startsWith("{")) obj = "{" + obj; if (!obj.startsWith("{"))
obj = "{" + obj;
start = next + 2; // position after }, start = next + 2; // position after },
} }
obj.trim(); obj.trim();
if (obj.length() == 0) continue; if (obj.length() == 0)
continue;
CommandMgr::Command c; CommandMgr::Command c;
c.id = extractId(obj); c.id = extractId(obj);
c.type = extractString(obj, "type"); c.type = extractString(obj, "type");
c.payload = extractPayload(obj); c.payload = extractPayload(obj);
if (c.id != 0 && c.type.length()) out.push_back(c); if (c.id != 0 && c.type.length())
out.push_back(c);
} }
} }
bool sendReceipt(long id, const String& result, const String& detail) { bool sendReceipt(long id, const String &result, const String &detail)
{
// POST /api/device/{imei}/command-receipts // POST /api/device/{imei}/command-receipts
String path = "/api/device/" + IMEI + "/command-receipts"; String path = "/api/device/" + IMEI + "/command-receipts";
String body = String("{\"command_id\":") + id + ",\"result\":\"" + result + "\",\"detail\":\"" + detail + "\"}"; String body = String("{\"command_id\":") + id + ",\"result\":\"" + result + "\",\"detail\":\"" + detail + "\"}";
@ -109,76 +149,134 @@ namespace {
} // anon } // anon
namespace CommandMgr { namespace CommandMgr
{
void configure(const String& baseUrl, const String& imei) { void configure(const String &baseUrl, const String &imei)
BASE = baseUrl; {
IMEI = imei; BASE = baseUrl;
} IMEI = imei;
void poll(uint32_t minIntervalMs) {
uint32_t now = millis();
if (now - lastPoll < minIntervalMs) return;
lastPoll = now;
if (BASE.isEmpty() || IMEI.isEmpty()) return;
String path = "/api/device/" + IMEI + "/commands";
String body;
String fullUrl = BASE + path; // e.g., "http://laravel-server.lab.audasmedia.com.au" + "/api/device/IMEI/commands"
//bool ok =HttpClient::getJsonToFile(fullUrl, body);
bool ok = HttpClient::getJsonExact(BASE, path, body);
Serial.println("Commands JSON: " + body);
// TODO: replace with proper JSON parsing later
// Crude parse: split by "},{" for now and extract id/type/payload as before.
if (!ok || body.length()==0) return;
std::vector<Command> cmds;
parseArray(body, cmds);
for (auto& c : cmds) {
bool execOk = false;
String detail = "";
if (c.type == "lights") {
execOk = handleLights(c.payload);
detail = "lights handled";
} else if (c.type == "sleep") {
execOk = handleSleep(c.payload);
detail = "sleep handled";
} else if (c.type == "ring_fence") {
execOk = handleRingFence(c.payload);
detail = "ring fence handled";
} else {
execOk = true; // unknown; mark ok but do nothing
detail = "unknown type";
}
sendReceipt(c.id, execOk ? "ok" : "error", detail);
} }
}
bool handleLights(const String& payload) { void poll(uint32_t minIntervalMs)
// TODO: parse {"on":true} and toggle GPIO via your GpioCtrl later {
Serial.println("handleLights payload: " + payload); uint32_t now = millis();
bool on = (payload.indexOf("\"on\":true") != -1); if (now - lastPoll < minIntervalMs)
GpioCtrl::setLight(on); return;
return true; lastPoll = now;
}
bool handleSleep(const String& payload) { if (BASE.isEmpty() || IMEI.isEmpty())
// TODO: parse {"interval_sec":120} return;
Serial.println("handleSleep payload: " + payload);
return true;
}
bool handleRingFence(const String& payload) { String path = "/api/device/" + IMEI + "/commands";
// TODO: parse and apply ring fence String body;
Serial.println("handleRingFence payload: " + payload);
return true;
}
// } // namespace CommandMgr String fullUrl = BASE + path; // e.g., "http://laravel-server.lab.audasmedia.com.au" + "/api/device/IMEI/commands"
// bool ok =HttpClient::getJsonToFile(fullUrl, body);
bool ok = HttpClient::getJsonExact(BASE, path, body);
Serial.println("Commands JSON: " + body);
// TODO: replace with proper JSON parsing later
// Crude parse: split by "},{" for now and extract id/type/payload as before.
if (!ok || body.length() == 0)
return;
std::vector<Command> cmds;
parseArray(body, cmds);
for (auto &c : cmds)
{
bool execOk = false;
String detail = "";
if (c.type == "lights")
{
execOk = handleLights(c.payload);
detail = "lights handled";
}
else if (c.type == "sleep")
{
// parse {"interval_sec":N}
int pos = c.payload.indexOf("\"interval_sec\":");
if (pos != -1)
{
int i = pos + 15;
while (i < (int)c.payload.length() && (c.payload[i] == ' ' || c.payload[i] == ':'))
i++;
int j = i;
while (j < (int)c.payload.length() && isDigit(c.payload[j]))
j++;
if (j > i)
{
CFG.sleepSec = c.payload.substring(i, j).toInt();
execOk = true;
detail = "sleep interval updated";
}
else
{
execOk = false;
detail = "bad interval";
}
}
else
{
execOk = false;
detail = "missing interval";
}
}
else if (c.type == "ring_fence")
{
execOk = handleRingFence(c.payload);
detail = "ring fence handled";
}
else if (c.type == "wifi")
{
// payload expected: {"target":"home"}
extern AppConfig CFG; // declare once in CommandMgr.cpp top: extern AppConfig CFG;
if (c.payload.indexOf("\"home\"") != -1 || c.payload.indexOf("\"target\":\"home\"") != -1)
{
CFG.atHomeRequested = true;
execOk = true;
detail = "wifi home requested";
}
else
{
execOk = false;
detail = "unknown wifi target";
}
}
else
{
execOk = true; // unknown; mark ok but do nothing
detail = "unknown type";
}
sendReceipt(c.id, execOk ? "ok" : "error", detail);
}
}
bool handleLights(const String &payload)
{
// TODO: parse {"on":true} and toggle GPIO via your GpioCtrl later
Serial.println("handleLights payload: " + payload);
bool on = (payload.indexOf("\"on\":true") != -1);
GpioCtrl::setLight(on);
return true;
}
bool handleSleep(const String &payload)
{
// TODO: parse {"interval_sec":120}
Serial.println("handleSleep payload: " + payload);
return true;
}
bool handleRingFence(const String &payload)
{
// TODO: parse and apply ring fence
Serial.println("handleRingFence payload: " + payload);
return true;
}
// } // namespace CommandMgr