From 2c09289eaab025f6d92b014f1c464b85512f47de Mon Sep 17 00:00:00 2001 From: sam rolfe Date: Wed, 3 Sep 2025 16:47:09 +1000 Subject: [PATCH] Add CommandMgr with HTTP GET queue + HttpClient.getJson --- CAR_GPS_TRACKER.ino | 21 +++++ src/core/CommandMgr.cpp | 173 ++++++++++++++++++++++++++++++++++++++++ src/core/CommandMgr.hpp | 22 +++++ src/core/HttpClient.cpp | 39 ++++++++- src/core/HttpClient.hpp | 1 + 5 files changed, 254 insertions(+), 2 deletions(-) diff --git a/CAR_GPS_TRACKER.ino b/CAR_GPS_TRACKER.ino index 520edd4..0299f5f 100644 --- a/CAR_GPS_TRACKER.ino +++ b/CAR_GPS_TRACKER.ino @@ -1,11 +1,26 @@ #include "src/core/NetMgr.hpp" #include "src/core/HttpClient.hpp" #include "src/core/SmsMgr.hpp" +#include "src/core/CommandMgr.hpp" + +static String DEVICE_IMEI = ""; static const char* APN = "hologram"; static const char* BASE_URL = "http://laravel-server.lab.audasmedia.com.au"; static const char* PATH = "/api/gps"; +static String queryImei() { + String r = NetMgr::sendAT("AT+CGSN", 3000); + // Response often includes the IMEI line + OK; take the first 15-digit line + int s = 0; + for (int i=0;i<(int)r.length();++i) { + if (isDigit(r[i])) { s = i; break; } + } + String imei = r.substring(s, s+15); + imei.trim(); + return imei; +} + void setup() { Serial.begin(115200); Serial2.begin(9600, SERIAL_8N1, 16, 17); // RX=16, TX=17 @@ -14,6 +29,11 @@ void setup() { NetMgr::attachAndPdp(APN); // LTE reg + CNACT + DNS SmsMgr::setup(); // enable text mode + URCs + DEVICE_IMEI = queryImei(); + if (DEVICE_IMEI.length()==0) DEVICE_IMEI = "860016049744324"; // fallback if needed + + CommandMgr::configure(BASE_URL, DEVICE_IMEI); + String json = "{\"device_id\":\"sim7080g-01\",\"lat\":-33.865143," "\"lng\":151.2099,\"speed\":12.5,\"altitude\":30.2}"; HttpClient::postJson(BASE_URL, PATH, json); @@ -21,6 +41,7 @@ void setup() { void loop() { SmsMgr::pollUrc(); // non‑blocking URC scan; handles +CMTI + CommandMgr::poll(30000); // every 30s SmsMgr::pollUnread(); // optional safety poll delay(250); } \ No newline at end of file diff --git a/src/core/CommandMgr.cpp b/src/core/CommandMgr.cpp index e69de29..4da3b13 100644 --- a/src/core/CommandMgr.cpp +++ b/src/core/CommandMgr.cpp @@ -0,0 +1,173 @@ +#include "CommandMgr.hpp" +#include "HttpClient.hpp" + +namespace { + String BASE; + String IMEI; + uint32_t lastPoll = 0; + + // Minimal JSON parsing helpers (string-based) + // Extract array of objects from a JSON string like: [{"id":..,"type":"..","payload":{...}}, ...] + // We’ll do a naive split on "},{" for small payloads; replace with proper JSON parsing later. + + long extractId(const String& obj) { + String pat = "\"id\":"; + int k = obj.indexOf(pat); + if (k < 0) return 0; + k += pat.length(); + int e = k; + while (e < (int)obj.length() && (isDigit(obj[e]) || obj[e]=='-')) e++; + return obj.substring(k, e).toInt(); + } + + String extractString(const String& obj, const String& key) { + String pat = "\"" + key + "\":"; + int k = obj.indexOf(pat); + if (k < 0) return ""; + k += pat.length(); + while (k < (int)obj.length() && (obj[k]==' ' || obj[k]=='\"')) k++; + // read quoted string + int end = obj.indexOf('\"', k); + if (end <= k) return ""; + return obj.substring(k, end); + } + + String extractPayload(const String& obj) { + // payload is often an object. Find "payload": and read balanced braces. + String pat = "\"payload\":"; + int k = obj.indexOf(pat); + if (k < 0) return ""; + k += pat.length(); + while (k < (int)obj.length() && (obj[k]==' ')) k++; + if (k >= (int)obj.length()) return ""; + if (obj[k] == '{') { + int depth = 0; + int i = k; + for (; i < (int)obj.length(); ++i) { + if (obj[i] == '{') depth++; + else if (obj[i] == '}') { + depth--; + if (depth == 0) { i++; break; } + } + } + return obj.substring(k, i); + } else if (obj[k] == '\"') { + int end = obj.indexOf('\"', k+1); + if (end > k) return obj.substring(k+1, end); + } + return ""; + } + + // Very naive split into objects. Works if server returns a small array. + void parseArray(const String& json, std::vector& out) { + String s = json; + s.trim(); + if (!s.startsWith("[")) return; + // strip [ and ] + if (s.length() >= 2 && s.endsWith("]")) s = s.substring(1, s.length()-1); + + // crude split on "},{" boundaries + 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.endsWith("}")) { + // ok + } + else if (!obj.isEmpty()) { + // Try pad + if (!obj.startsWith("{")) obj = "{" + obj; + if (!obj.endsWith("}")) obj += "}"; + } + start = s.length(); + } else { + obj = s.substring(start, next + 1); // include the trailing } + if (!obj.startsWith("{")) obj = "{" + obj; + start = next + 2; // position after }, + } + + obj.trim(); + if (obj.length() == 0) continue; + + CommandMgr::Command c; + c.id = extractId(obj); + c.type = extractString(obj, "type"); + c.payload = extractPayload(obj); + if (c.id != 0 && c.type.length()) out.push_back(c); + } + } + + bool sendReceipt(long id, const String& result, const String& detail) { + // POST /api/device/{imei}/command-receipts + String path = "/api/device/" + IMEI + "/command-receipts"; + String body = String("{\"command_id\":") + id + ",\"result\":\"" + result + "\",\"detail\":\"" + detail + "\"}"; + return HttpClient::postJson(BASE, path, body); + } + +} // anon + +namespace CommandMgr { + +void configure(const String& baseUrl, const String& 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; + bool ok = HttpClient::getJson(BASE, path, body); + if (!ok || body.length()==0) return; + + 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 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) { + // TODO: parse {"on":true} and toggle GPIO via your GpioCtrl later + Serial.println("handleLights payload: " + payload); + 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 \ No newline at end of file diff --git a/src/core/CommandMgr.hpp b/src/core/CommandMgr.hpp index e69de29..575b64e 100644 --- a/src/core/CommandMgr.hpp +++ b/src/core/CommandMgr.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +namespace CommandMgr { + + struct Command { + long id = 0; + String type; // "lights","camera","sleep","ring_fence","custom" + String payload; // raw JSON payload + }; + + void configure(const String& baseUrl, const String& imei); + + // Poll the server for queued commands and execute them. + void poll(uint32_t minIntervalMs = 30000); + + // (Optional) simple handlers you can expand later. + bool handleLights(const String& payload); + bool handleSleep(const String& payload); + bool handleRingFence(const String& payload); + +} \ No newline at end of file diff --git a/src/core/HttpClient.cpp b/src/core/HttpClient.cpp index 43b0ee5..c162052 100644 --- a/src/core/HttpClient.cpp +++ b/src/core/HttpClient.cpp @@ -10,23 +10,58 @@ bool ensureHttpReady(const String& baseUrl) { String r = NetMgr::sendAT("AT+SHCONN", 15000); if (r.indexOf("OK") == -1) return false; NetMgr::sendAT("AT+SHCHEAD", 2000); - NetMgr::sendAT("AT+SHAHEAD=\"Content-Type\",\"application/json\"", 2000); return true; } +String readAll(uint32_t chunk = 2048) { + // Try to read body; some servers may close early (CME 3). That’s OK. + String b = NetMgr::sendAT("AT+SHREAD=0," + String(chunk), 4000); + return b; } +} // anon + namespace HttpClient { bool postJson(const String& baseUrl, const String& path, const String& jsonBody) { if (!ensureHttpReady(baseUrl)) { Serial.println("SHCONN failed"); return false; } + NetMgr::sendAT("AT+SHAHEAD=\"Content-Type\",\"application/json\"", 2000); + NetMgr::sendAT("AT+SHBOD=" + String(jsonBody.length()) + ",10000", 2000); Serial2.print(jsonBody); delay(200); + String r = NetMgr::sendAT("AT+SHREQ=\"" + path + "\",3", 20000); // 3=POST int p = r.indexOf("+SHREQ:"); if (p != -1) Serial.println("Status/URC: " + r.substring(p)); - NetMgr::sendAT("AT+SHREAD=0,2048", 4000); // non-fatal if CME 3 + readAll(); + NetMgr::sendAT("AT+SHDISC", 3000); + return true; +} + +bool getJson(const String& baseUrl, const String& path, String& outBody) { + outBody = ""; + if (!ensureHttpReady(baseUrl)) return false; + + String r = NetMgr::sendAT("AT+SHREQ=\"" + path + "\",1", 20000); // 1=GET + // Expect +SHREQ: "GET",, + int s = r.indexOf("+SHREQ:"); + if (s != -1) Serial.println("Status/URC: " + r.substring(s)); + + // Read response (best effort) + String raw = readAll(); + // raw looks like: + // +SHREAD: + // + // Parse body after the first newline following +SHREAD: + int tag = raw.indexOf("+SHREAD:"); + if (tag != -1) { + int nl = raw.indexOf('\n', tag); + if (nl != -1 && nl + 1 < (int)raw.length()) { + outBody = raw.substring(nl + 1); + outBody.trim(); + } + } NetMgr::sendAT("AT+SHDISC", 3000); return true; } diff --git a/src/core/HttpClient.hpp b/src/core/HttpClient.hpp index 2228a96..f686379 100644 --- a/src/core/HttpClient.hpp +++ b/src/core/HttpClient.hpp @@ -3,4 +3,5 @@ namespace HttpClient { bool postJson(const String& baseUrl, const String& path, const String& jsonBody); + bool getJson(const String& baseUrl, const String& path, String& outBody); // NEW } \ No newline at end of file