Add CommandMgr with HTTP GET queue + HttpClient.getJson
This commit is contained in:
@ -1,11 +1,26 @@
|
|||||||
#include "src/core/NetMgr.hpp"
|
#include "src/core/NetMgr.hpp"
|
||||||
#include "src/core/HttpClient.hpp"
|
#include "src/core/HttpClient.hpp"
|
||||||
#include "src/core/SmsMgr.hpp"
|
#include "src/core/SmsMgr.hpp"
|
||||||
|
#include "src/core/CommandMgr.hpp"
|
||||||
|
|
||||||
|
static String DEVICE_IMEI = "";
|
||||||
|
|
||||||
static const char* APN = "hologram";
|
static const char* APN = "hologram";
|
||||||
static const char* BASE_URL = "http://laravel-server.lab.audasmedia.com.au";
|
static const char* BASE_URL = "http://laravel-server.lab.audasmedia.com.au";
|
||||||
static const char* PATH = "/api/gps";
|
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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
Serial2.begin(9600, SERIAL_8N1, 16, 17); // RX=16, TX=17
|
Serial2.begin(9600, SERIAL_8N1, 16, 17); // RX=16, TX=17
|
||||||
@ -14,6 +29,11 @@ void setup() {
|
|||||||
NetMgr::attachAndPdp(APN); // LTE reg + CNACT + DNS
|
NetMgr::attachAndPdp(APN); // LTE reg + CNACT + DNS
|
||||||
SmsMgr::setup(); // enable text mode + URCs
|
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,"
|
String json = "{\"device_id\":\"sim7080g-01\",\"lat\":-33.865143,"
|
||||||
"\"lng\":151.2099,\"speed\":12.5,\"altitude\":30.2}";
|
"\"lng\":151.2099,\"speed\":12.5,\"altitude\":30.2}";
|
||||||
HttpClient::postJson(BASE_URL, PATH, json);
|
HttpClient::postJson(BASE_URL, PATH, json);
|
||||||
@ -21,6 +41,7 @@ void setup() {
|
|||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
SmsMgr::pollUrc(); // non‑blocking URC scan; handles +CMTI
|
SmsMgr::pollUrc(); // non‑blocking URC scan; handles +CMTI
|
||||||
|
CommandMgr::poll(30000); // every 30s
|
||||||
SmsMgr::pollUnread(); // optional safety poll
|
SmsMgr::pollUnread(); // optional safety poll
|
||||||
delay(250);
|
delay(250);
|
||||||
}
|
}
|
||||||
@ -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<CommandMgr::Command>& 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<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) {
|
||||||
|
// 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
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
@ -10,23 +10,58 @@ bool ensureHttpReady(const String& baseUrl) {
|
|||||||
String r = NetMgr::sendAT("AT+SHCONN", 15000);
|
String r = NetMgr::sendAT("AT+SHCONN", 15000);
|
||||||
if (r.indexOf("OK") == -1) return false;
|
if (r.indexOf("OK") == -1) return false;
|
||||||
NetMgr::sendAT("AT+SHCHEAD", 2000);
|
NetMgr::sendAT("AT+SHCHEAD", 2000);
|
||||||
NetMgr::sendAT("AT+SHAHEAD=\"Content-Type\",\"application/json\"", 2000);
|
|
||||||
return true;
|
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 {
|
namespace HttpClient {
|
||||||
|
|
||||||
bool postJson(const String& baseUrl, const String& path, const String& jsonBody) {
|
bool postJson(const String& baseUrl, const String& path, const String& jsonBody) {
|
||||||
if (!ensureHttpReady(baseUrl)) { Serial.println("SHCONN failed"); return false; }
|
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);
|
NetMgr::sendAT("AT+SHBOD=" + String(jsonBody.length()) + ",10000", 2000);
|
||||||
Serial2.print(jsonBody);
|
Serial2.print(jsonBody);
|
||||||
delay(200);
|
delay(200);
|
||||||
|
|
||||||
String r = NetMgr::sendAT("AT+SHREQ=\"" + path + "\",3", 20000); // 3=POST
|
String r = NetMgr::sendAT("AT+SHREQ=\"" + path + "\",3", 20000); // 3=POST
|
||||||
int p = r.indexOf("+SHREQ:");
|
int p = r.indexOf("+SHREQ:");
|
||||||
if (p != -1) Serial.println("Status/URC: " + r.substring(p));
|
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",<http_code>,<len>
|
||||||
|
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: <len>
|
||||||
|
// <body...>
|
||||||
|
// 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);
|
NetMgr::sendAT("AT+SHDISC", 3000);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,4 +3,5 @@
|
|||||||
|
|
||||||
namespace HttpClient {
|
namespace HttpClient {
|
||||||
bool postJson(const String& baseUrl, const String& path, const String& jsonBody);
|
bool postJson(const String& baseUrl, const String& path, const String& jsonBody);
|
||||||
|
bool getJson(const String& baseUrl, const String& path, String& outBody); // NEW
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user