CommandMgr: dispatch + receipts; exact-length SH GET integrated; Telemetry GNSS restore

This commit is contained in:
2025-09-04 20:44:24 +10:00
parent 2c09289eaa
commit c1cd3e653f
10 changed files with 437 additions and 73 deletions

View File

@ -10,38 +10,43 @@ 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;
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
Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, 16, 17); // RX=16, TX=17
NetMgr::powerOnSIM7080(4); // PWRKEY pin 4
NetMgr::attachAndPdp(APN); // LTE reg + CNACT + DNS
SmsMgr::setup(); // enable text mode + URCs
NetMgr::powerOnSIM7080(4); // PWRKEY pin 4
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
DEVICE_IMEI = queryImei();
if (DEVICE_IMEI.length()==0) DEVICE_IMEI = "860016049744324"; // fallback if needed
CommandMgr::configure(BASE_URL, DEVICE_IMEI);
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);
double lat,lng,alt,spd,hdg;
Telemetry::readGNSS(lat,lng,alt,spd,hdg);
String json = Telemetry::buildJson(DEVICE_IMEI, lat,lng,alt,spd,hdg);
//String json =
// "{\"device_id\":\"sim7080g-01\",\"lat\":-33.865143,"
// "\"lng\":151.2099,\"speed\":12.5,\"altitude\":30.2}";
HttpClient::postJson(BASE_URL, PATH, json);
}
void loop() {
SmsMgr::pollUrc(); // nonblocking URC scan; handles +CMTI
CommandMgr::poll(30000); // every 30s
SmsMgr::pollUnread(); // optional safety poll
delay(250);
SmsMgr::pollUrc(); // nonblocking URC scan; handles +CMTI
CommandMgr::poll(30000); // every 30s
SmsMgr::pollUnread(); // optional safety poll
delay(250);
}

View File

@ -0,0 +1,47 @@
#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
NetMgr::powerOnSIM7080(4); // PWRKEY pin 4
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);
}
void loop() {
SmsMgr::pollUrc(); // nonblocking URC scan; handles +CMTI
CommandMgr::poll(30000); // every 30s
SmsMgr::pollUnread(); // optional safety poll
delay(250);
}

View File

@ -0,0 +1,209 @@
#include <HardwareSerial.h>
#define RXD2 16
#define TXD2 17
#define PWRKEY 4
int parseLenFromSHREQ(const String& buf) {
int start = 0;
while (start < (int)buf.length()) {
int end = buf.indexOf('\n', start);
if (end == -1) end = buf.length();
String line = buf.substring(start, end);
line.trim();
if (line.startsWith("+SHREQ:")) {
int lastComma = line.lastIndexOf(',');
if (lastComma < 0) return -1;
int i = lastComma + 1;
while (i < (int)line.length() && line[i] == ' ') i++;
int j = i;
while (j < (int)line.length() && isDigit(line[j])) j++;
if (j <= i) return -1;
return line.substring(i, j).toInt();
}
start = end + 1;
}
return -1;
}
String at(const String& cmd, uint32_t timeoutMs=5000, uint32_t urcWindowMs=500) {
Serial.print(">> "); Serial.println(cmd);
while (Serial2.available()) (void)Serial2.read();
Serial2.println(cmd);
String resp;
uint32_t t0 = millis();
while (millis()-t0 < timeoutMs) {
while (Serial2.available()) {
char c = (char)Serial2.read();
resp += c; Serial.print(c);
t0 = millis(); // extend on activity
}
}
// Short URC window to catch lines like +SHREQ: "GET",200,<len>
uint32_t u0 = millis();
while (millis()-u0 < urcWindowMs) {
while (Serial2.available()) {
char c = (char)Serial2.read();
resp += c; Serial.print(c);
u0 = millis(); // extend on activity
}
}
resp.trim();
Serial.println("\n[Resp]\n" + resp);
return resp;
}
void powerOnSIM7080() {
pinMode(PWRKEY, OUTPUT);
digitalWrite(PWRKEY, LOW); delay(2000);
digitalWrite(PWRKEY, HIGH); delay(1000);
digitalWrite(PWRKEY, LOW); delay(30000);
}
bool waitLTE(uint32_t ms=120000) {
uint32_t t0=millis();
while (millis()-t0 < ms) {
String r = at("AT+CEREG?", 3000);
if (r.indexOf("+CEREG: 0,1")!=-1 || r.indexOf("+CEREG: 0,5")!=-1) return true;
delay(2000);
}
return false;
}
void attachPDP() {
at("AT",2000);
at("AT+CMEE=1",2000);
at("AT+CPIN?",2000);
at("AT+CSQ",2000);
at("AT+CNMP=38",3000);
at("AT+CMNB=1",3000);
at("AT+CGDCONT=1,\"IP\",\"hologram\"",3000);
at("AT+CGATT=1",10000);
if (!waitLTE()) { Serial.println("No LTE"); while(true) delay(1000); }
at("AT+CNCFG=0,1,\"hologram\"",3000);
at("AT+CNACT=0,1",20000);
at("AT+CNACT?",2000);
at("AT+CDNSCFG=\"8.8.8.8\",\"1.1.1.1\"",2000);
}
// Pure SH GET for one host+path, prints raw SHREAD output(s)
void shGet(const String& base, const String& path) {
Serial.println("\n=== SH GET ===");
Serial.println("Base: " + base);
Serial.println("Path: " + path);
at("AT+SHDISC", 2000); // clear any stale session (ignore CME 3)
at("AT+SHCHEAD", 2000); // clear headers
at("AT+SHCONF=\"URL\",\"" + base + "\"", 4000);
//at("AT+SHCONF=\"BODYLEN\",1024", 2000);
//at("AT+SHCONF=\"HEADERLEN\",350", 2000);
String r = at("AT+SHCONN", 12000);
Serial.println("---- CONNECTION !!----");
Serial.println(r);
if (r.indexOf("OK")==-1) { Serial.println("SHCONN failed"); return; }
at("AT+SHCHEAD", 2000);
// Optionally set UA if your server cares:
//at("AT+SHAHEAD=\"User-Agent\",\"SIM7080G\"",2000);
at("AT+SHAHEAD=\"User-Agent\",\"IOE Client\"",2000);
// at("AT+SHAHEAD=\"User-Agent\",\"curl/7.47.0\"",2000);
at("AT+SHAHEAD=\"Cache-control\",\"no-cache\"",2000);
at("AT+SHAHEAD=\"Connection\",\"keep-alive\"",2000);
// at("AT+SHAHEAD=\"Content-Type\",\"application/json\"",2000);
// at("AT+SHAHEAD=\"Content-Type\",\"text/html\"",2000);
at("AT+SHAHEAD=\"Accept\",\"text/html,*/*\"",2000);
//at("AT+SHAHEAD=\"Accept-encoding\",\"gzip, deflate\"",2000);
String u = at("AT+SHREQ=\"" +path + "\",1", 20000); // 1 = GET
Serial.println("---- REQUESTED !!----");
Serial.println(u);
//String u = at("AT+SHREQ=\"" + path + "\",1", 20000);
int bodyLen = parseLenFromSHREQ(u);
Serial.print("Parsed body length: "); Serial.println(bodyLen);
if (bodyLen > 0) {
String body = at("AT+SHREAD=0," + String(bodyLen), 5000);
Serial.println("---- SHREAD(N) RAW ----");
Serial.println(body);
}
// First read
//String body0 = at("AT+SHREAD=0,8", 3000);
//Serial.println("---- SHREAD(8) RAW ----");
//Serial.println(body0);
//String body1 = at("AT+SHREAD=0,2048", 4000);
//Serial.println("01 ---- SHREAD(2048) RAW ----");
//Serial.println(body1);
// Second smaller read
//String body2 = at("AT+SHREAD=0,512", 3000);
//Serial.println("---- SHREAD(512) RAW ----");
//Serial.println(body2);
//String body3 = at("AT+SHREAD=0,20", 3000);
//Serial.println("---- SHREAD(20) RAW ----");
//Serial.println(body3);
at("AT+SHDISC", 2000);
}
void setup() {
Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2);
powerOnSIM7080();
attachPDP();
// 1) Your Laravel endpoint
shGet("http://laravel-server.lab.audasmedia.com.au",
"/api/device/860016049744324/commands");
//shGet("http://httpforever.com/","/");
// 2) Example.com root
// shGet("http://example.com", "/");
// 3) Adafruit WiFi test page
// shGet("http://wifitest.adafruit.com", "/testwifi/index.html");
// 4) httpbin GET
// shGet("http://httpbin.org", "/get");
Serial.println("\n--- All GETs attempted ---");
}
void loop() {
delay(1000);
}

View File

@ -125,7 +125,16 @@ void poll(uint32_t minIntervalMs) {
String path = "/api/device/" + IMEI + "/commands";
String body;
bool ok = HttpClient::getJson(BASE, path, 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;
@ -155,6 +164,8 @@ void poll(uint32_t minIntervalMs) {
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;
}
@ -170,4 +181,4 @@ bool handleRingFence(const String& payload) {
return true;
}
} // namespace CommandMgr
// } // namespace CommandMgr

View File

@ -1,69 +1,93 @@
#include "HttpClient.hpp"
#include "NetMgr.hpp"
namespace {
bool ensureHttpReady(const String& baseUrl) {
NetMgr::sendAT("AT+SHCONF=\"URL\",\"" + baseUrl + "\"", 5000);
NetMgr::sendAT("AT+SHCONF=\"BODYLEN\",1024", 2000);
NetMgr::sendAT("AT+SHCONF=\"HEADERLEN\",350", 2000);
String r = NetMgr::sendAT("AT+SHCONN", 15000);
if (r.indexOf("OK") == -1) return false;
NetMgr::sendAT("AT+SHCHEAD", 2000);
return true;
}
String readAll(uint32_t chunk = 2048) {
// Try to read body; some servers may close early (CME 3). Thats 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+SHDISC", 2000);
NetMgr::sendAT("AT+SHCHEAD", 2000);
if (NetMgr::sendAT("AT+SHCONF=\"URL\",\"" + baseUrl + "\"", 4000).indexOf("OK") == -1)
return false;
String r = NetMgr::sendAT("AT+SHCONN", 12000);
if (r.indexOf("OK") == -1) return false;
NetMgr::sendAT("AT+SHCHEAD", 2000);
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
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));
readAll();
NetMgr::sendAT("AT+SHDISC", 3000);
if (p != -1) Serial.println("POST status URC: " + r.substring(p));
// optional read (non-fatal)
NetMgr::sendAT("AT+SHREAD=0,2048", 4000);
NetMgr::sendAT("AT+SHDISC", 2000);
return true;
}
bool getJson(const String& baseUrl, const String& path, String& outBody) {
bool getJsonExact(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));
NetMgr::sendAT("AT+SHDISC", 2000);
NetMgr::sendAT("AT+SHCHEAD", 2000);
// 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();
if (NetMgr::sendAT("AT+SHCONF=\"URL\",\"" + baseUrl + "\"", 4000).indexOf("OK") == -1)
return false;
String r = NetMgr::sendAT("AT+SHCONN", 12000);
if (r.indexOf("OK") == -1) return false;
NetMgr::sendAT("AT+SHCHEAD", 2000);
NetMgr::sendAT("AT+SHAHEAD=\"Connection\",\"keep-alive\"", 2000);
NetMgr::sendAT("AT+SHAHEAD=\"Accept\",\"application/json\"", 2000);
// GET + parse length
String urc = NetMgr::sendAT("AT+SHREQ=\"" + path + "\",1", 20000);
// parse len from URC
int len = -1;
{
int start = 0;
while (start < (int)urc.length()) {
int end = urc.indexOf('\n', start);
if (end == -1) end = urc.length();
String line = urc.substring(start, end);
line.trim();
if (line.startsWith("+SHREQ:")) {
int lastComma = line.lastIndexOf(',');
if (lastComma >= 0) {
int i = lastComma + 1;
while (i < (int)line.length() && line[i]==' ') i++;
int j = i;
while (j < (int)line.length() && isDigit(line[j])) j++;
if (j > i) len = line.substring(i, j).toInt();
}
break;
}
start = end + 1;
}
}
NetMgr::sendAT("AT+SHDISC", 3000);
return true;
if (len > 0) {
String raw = NetMgr::sendAT("AT+SHREAD=0," + String(len), 6000);
int nl = raw.indexOf('\n');
if (nl != -1) {
String b = raw.substring(nl + 1);
int okPos = b.lastIndexOf("\nOK");
if (okPos >= 0) b = b.substring(0, okPos);
b.trim();
outBody = b;
}
}
NetMgr::sendAT("AT+SHDISC", 2000);
return outBody.length() > 0;
}
} // namespace HttpClient

View File

@ -3,5 +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
bool getJsonExact(const String& baseUrl, const String& path, String& outBody);
}

View File

@ -0,0 +1,48 @@
#include "Telemetry.hpp"
#include "NetMgr.hpp"
namespace Telemetry {
bool readGNSS(double& lat, double& lng, double& alt, double& speed, double& heading) {
lat=lng=alt=speed=heading=0;
String r = NetMgr::sendAT("AT+CGNSINF", 3000);
// +CGNSINF: 1,1,yyyyMMddhhmmss.sss,lat,lng,alt, ... ,speed_over_ground, ... ,course
int p = r.indexOf("+CGNSINF:");
if (p < 0) return false;
// crude split
int first = r.indexOf('\n', p); // skip header if present
String line = (first!=-1) ? r.substring(p, first) : r.substring(p);
// safer: take last line
int lastNl = r.lastIndexOf('\n');
line = (lastNl!=-1) ? r.substring(lastNl+1) : line;
line.trim();
// tokenization
int commas[10]; int count=0;
for (int i=0;i<(int)line.length() && count<10;i++) if (line[i]==',') commas[count++]=i;
if (count < 6) return false;
lat = line.substring(commas[1]+1, commas[2]).toDouble();
lng = line.substring(commas[2]+1, commas[3]).toDouble();
alt = line.substring(commas[3]+1, commas[4]).toDouble();
// speed (km/h) field may be later; do a find:
int next = line.indexOf(',', commas[4]+1);
next = line.indexOf(',', next+1);
next = line.indexOf(',', next+1);
int speedStart = next+1;
int speedEnd = line.indexOf(',', speedStart);
if (speedStart>0 && speedEnd>speedStart) speed = line.substring(speedStart, speedEnd).toDouble();
// heading/course similarly if needed
return true;
}
String buildJson(const String& imei, double lat, double lng, double alt, double speed, double heading) {
String j = String("{\"device_id\":\"") + imei + "\","
+ "\"lat\":" + String(lat,6) + ","
+ "\"lng\":" + String(lng,6) + ","
+ "\"speed\":" + String(speed,2) + ","
+ "\"altitude\":" + String(alt,2) + "}";
return j;
}
} // namespace Telemetry

View File

@ -0,0 +1,6 @@
#pragma once
#include <Arduino.h>
namespace Telemetry {
bool readGNSS(double& lat, double& lng, double& alt, double& speed, double& heading);
String buildJson(const String& imei, double lat, double lng, double alt, double speed, double heading);
}

View File

@ -0,0 +1,8 @@
#include "GpioCtrl.hpp"
namespace {
uint8_t lp = 255;
}
namespace GpioCtrl {
void init(uint8_t lightPin) { lp = lightPin; pinMode(lp, OUTPUT); digitalWrite(lp, LOW); }
void setLight(bool on) { if (lp != 255) digitalWrite(lp, on ? HIGH : LOW); }
}

View File

@ -0,0 +1,6 @@
#pragma once
#include <Arduino.h>
namespace GpioCtrl {
void init(uint8_t lightPin);
void setLight(bool on);
}