12 Commits

Author SHA1 Message Date
998e67b246 Define global AppConfig CFG in .ino; extern in CommandMgr 2025-09-08 14:43:47 +10:00
d743a81069 Fix: remove duplicate Command struct from CommandMgr.cpp (use header definition) 2025-09-08 12:55:57 +10:00
f6d5bb58bd Add clean OTAMgr (check/apply via HTTPTOFS + Update stream); CommandMgr dispatch & receipts (wifi/sleep/periods) 2025-09-08 12:51:52 +10:00
12cc17544b Fix include path: CommandMgr includes ../app/AppConfig.hpp 2025-09-08 11:38:08 +10:00
66baea7a61 CommandMgr: wifi/sleep/periods/ota handlers; integrate OTAMgr check at boot; WiFi home flag flow 2025-09-05 14:06:07 +10:00
050d02d822 OTAMgr: check firmware endpoint; HTTPTOFS download; optional SHA256 verify; Update.apply + reboot 2025-09-05 13:17:42 +10:00
14ebfa5451 CommandMgr: add wifi home flag + sleep interval command; control loop uses CFG.atHomeRequested, CFG.sleepSec 2025-09-05 12:24:02 +10:00
8f2f83ddb6 Restore Telemetry::readGNSS (CGNSINF parser) and buildJson 2025-09-05 11:36:19 +10:00
ad0a1b1e3d Core loop: telemetry A sec, command poll B sec; car-power OFF -> no-power POST + sleep C; WiFi home connect placeholder; GNSS placeholders 2025-09-05 10:41:10 +10:00
c1cd3e653f CommandMgr: dispatch + receipts; exact-length SH GET integrated; Telemetry GNSS restore 2025-09-04 20:44:24 +10:00
2c09289eaa Add CommandMgr with HTTP GET queue + HttpClient.getJson 2025-09-03 16:47:09 +10:00
aac68aa77c Modular PR1: NetMgr, HttpClient, SmsMgr with working attach/HTTP/SMS 2025-09-02 09:20:23 +10:00
24 changed files with 1173 additions and 4 deletions

View File

@ -21,7 +21,8 @@
"/home/sam/.arduino15/packages/esp32/tools/esp-x32/2411/xtensa-esp-elf/include/c++/14.2.0/backward", "/home/sam/.arduino15/packages/esp32/tools/esp-x32/2411/xtensa-esp-elf/include/c++/14.2.0/backward",
"/home/sam/.arduino15/packages/esp32/tools/esp-x32/2411/lib/gcc/xtensa-esp-elf/14.2.0/include", "/home/sam/.arduino15/packages/esp32/tools/esp-x32/2411/lib/gcc/xtensa-esp-elf/14.2.0/include",
"/home/sam/.arduino15/packages/esp32/tools/esp-x32/2411/lib/gcc/xtensa-esp-elf/14.2.0/include-fixed", "/home/sam/.arduino15/packages/esp32/tools/esp-x32/2411/lib/gcc/xtensa-esp-elf/14.2.0/include-fixed",
"/home/sam/.arduino15/packages/esp32/tools/esp-x32/2411/xtensa-esp-elf/include" "/home/sam/.arduino15/packages/esp32/tools/esp-x32/2411/xtensa-esp-elf/include",
"/media/sam/8294CD2994CD2111/Users/Dell/Documents/Arduino/CAR_GPS_TRACKER/src/app"
], ],
"forcedInclude": [ "forcedInclude": [
"/home/sam/.arduino15/packages/esp32/hardware/esp32/3.2.0/cores/esp32/Arduino.h" "/home/sam/.arduino15/packages/esp32/hardware/esp32/3.2.0/cores/esp32/Arduino.h"

View File

@ -1,9 +1,119 @@
#include "app/App.hpp" #include "src/core/NetMgr.hpp"
#include "src/core/HttpClient.hpp"
#include "src/core/SmsMgr.hpp"
#include "src/core/CommandMgr.hpp"
#include "src/core/Telemetry.hpp"
#include "src/app/AppConfig.hpp"
#include "src/hw/PowerMgr.hpp"
#include "src/hw/WiFiMgr.hpp"
#include "src/core/OTAMgr.hpp"
#define FW_VERSION "1.0.0"
AppConfig CFG;
static String queryImei() {
String r = NetMgr::sendAT("AT+CGSN", 3000);
int s = -1;
for (int i = 0; i < (int)r.length() - 14; ++i) { if (isDigit(r[i])) { s = i; break; } }
if (s >= 0) { String imei = r.substring(s, s + 15); imei.trim(); return imei; }
return "";
}
void setup() { void setup() {
App::instance().setup(); Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, 16, 17);
// 1) Radio attach
NetMgr::powerOnSIM7080(4);
NetMgr::attachAndPdp(CFG.apn.c_str());
SmsMgr::setup();
OTAMgr::setCurrentVersion(FW_VERSION);
// 2) Identity
CFG.imei = queryImei();
if (CFG.imei.isEmpty()) CFG.imei = "860016049744324";
// 3) Init power sim
PowerMgr::init(true); // car power ON initially (simulate)
// 4) Command manager config
CommandMgr::configure(CFG.baseUrl, CFG.imei);
// GNSS power on (if applicable later)
NetMgr::sendAT("AT+CGNSPWR=1", 2000);
OTAMgr::setCurrentVersion(FW_VERSION);
String ver, url, sha;
if (OTAMgr::check(CFG.baseUrl, CFG.imei, ver, url, sha)) {
Serial.println("OTA available: " + ver + " -> " + url);
// Optionally apply now:
// OTAMgr::apply(url, sha);
}
} }
void loop() { void loop() {
App::instance().loop(); static uint32_t lastTelemetryMs = 0;
static uint32_t lastPollMs = 0;
static bool noPowerSent = false;
SmsMgr::pollUrc(); // SMS commands
bool carOn = PowerMgr::isCarPowerOn();
// When car power is ON
if (carOn) {
noPowerSent = false; // reset for next OFF cycle
// Telemetry every A seconds
if (millis() - lastTelemetryMs >= CFG.telemetryPeriodSec * 1000UL) {
double lat, lng, alt, spd, hdg;
Telemetry::readGNSS(lat, lng, alt, spd, hdg);
String json = Telemetry::buildJson(CFG.imei, lat, lng, alt, spd, hdg, carOn);
HttpClient::postJson(CFG.baseUrl, CFG.telemetryPath, json);
lastTelemetryMs = millis();
}
// Poll commands every B seconds
if (millis() - lastPollMs >= CFG.commandPollSec * 1000UL) {
CommandMgr::poll(CFG.commandPollSec * 1000UL); // uses its own timer, but call to force
lastPollMs = millis();
}
}
// When car power is OFF
else {
if (!noPowerSent) {
String np = Telemetry::buildNoPowerJson(CFG.imei);
HttpClient::postJson(CFG.baseUrl, CFG.telemetryPath, np);
noPowerSent = true;
}
// One last command poll to check instructions (e.g., connect WiFi, sleep C seconds)
CommandMgr::poll(0);
// Placeholder: if server requested home WiFi, connect
if (CFG.atHomeRequested) {
bool ok = WiFiMgr::connectHome("Aussie Broadband 8729", "Ffdfmunfca", 15000);
Serial.println(ok ? "Home WiFi connected" : "Home WiFi failed");
if (ok) {
// You can add local/LAN interactions here; then disconnect:
WiFiMgr::disconnect();
}
CFG.atHomeRequested = false;
}
// Go to deep sleep (placeholder currently just delay)
PowerMgr::deepSleep(CFG.sleepSec);
delay(CFG.sleepSec * 1000UL);
// On real deep sleep, code resumes at setup() on wake
// If simulating, continue loop
}
delay(100);
} }

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

@ -1,5 +1,6 @@
#include "App.hpp" #include "App.hpp"
#include "../core/HttpClient.hpp"
App& App::instance() { App& App::instance() {
static App inst; static App inst;

View File

@ -0,0 +1,21 @@
#pragma once
#include <Arduino.h>
struct AppConfig {
// periods (seconds)
uint32_t telemetryPeriodSec = 30; // A
uint32_t commandPollSec = 30; // B
uint32_t sleepSec = 300; // C
// device state
bool carPowerOn = true; // simulate for now
bool atHomeRequested = false; // server may request WiFi connect
// networking
String apn = "hologram";
String baseUrl = "http://laravel-server.lab.audasmedia.com.au";
String telemetryPath = "/api/gps";
// identity
String imei; // set at runtime
};

View File

@ -0,0 +1,181 @@
// 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 {
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<Command>& 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<Command> 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

View File

@ -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);
}

View File

@ -0,0 +1,93 @@
#include "HttpClient.hpp"
#include "NetMgr.hpp"
namespace HttpClient {
bool postJson(const String& baseUrl, const String& path, const String& jsonBody) {
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);
r = NetMgr::sendAT("AT+SHREQ=\"" + path + "\",3", 20000); // 3 = POST
int p = r.indexOf("+SHREQ:");
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 getJsonExact(const String& baseUrl, const String& path, String& outBody) {
outBody = "";
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=\"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;
}
}
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

@ -0,0 +1,7 @@
#pragma once
#include <Arduino.h>
namespace HttpClient {
bool postJson(const String& baseUrl, const String& path, const String& jsonBody);
bool getJsonExact(const String& baseUrl, const String& path, String& outBody);
}

View File

@ -0,0 +1,61 @@
#include "NetMgr.hpp"
namespace {
String at(const String& cmd, uint32_t timeoutMs) {
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();
}
}
resp.trim();
Serial.println("\n[Resp]\n" + resp);
return resp;
}
}
namespace NetMgr {
String sendAT(const String& cmd, uint32_t timeoutMs) { return at(cmd, timeoutMs); }
void powerOnSIM7080(uint8_t pwrkeyPin) {
pinMode(pwrkeyPin, OUTPUT);
digitalWrite(pwrkeyPin, LOW); delay(2000);
digitalWrite(pwrkeyPin, HIGH); delay(1000);
digitalWrite(pwrkeyPin, LOW); delay(30000);
}
bool waitLTE(uint32_t ms) {
uint32_t t0 = millis();
while (millis() - t0 < ms) {
String r = at("AT+CEREG?", 4000);
if (r.indexOf("+CEREG: 0,1") != -1 || r.indexOf("+CEREG: 0,5") != -1) return true;
delay(3000);
}
return false;
}
void attachAndPdp(const char* apn) {
at("AT", 2000);
at("AT+CMEE=1", 2000);
at("AT+CPIN?", 2000);
at("AT+CSQ", 2000);
at("AT+CNMP=38", 5000);
at("AT+CMNB=1", 5000);
at("AT+CGDCONT=1,\"IP\",\"" + String(apn) + "\"", 5000);
at("AT+CGATT=1", 10000);
if (!waitLTE()) { Serial.println("No LTE reg"); while (true) delay(1000); }
at("AT+CNCFG=0,1,\"" + String(apn) + "\"", 5000);
at("AT+CNACT=0,1", 45000); // may 767 if already active
at("AT+CNACT?", 3000);
at("AT+CDNSCFG=\"8.8.8.8\",\"1.1.1.1\"", 3000);
}
} // namespace NetMgr

View File

@ -0,0 +1,9 @@
#pragma once
#include <Arduino.h>
namespace NetMgr {
void powerOnSIM7080(uint8_t pwrkeyPin);
bool waitLTE(uint32_t ms = 120000);
void attachAndPdp(const char* apn);
String sendAT(const String& cmd, uint32_t timeoutMs);
}

View File

@ -0,0 +1,163 @@
// src/core/OTAMgr.cpp
#include "OTAMgr.hpp"
#include "NetMgr.hpp"
#include <Update.h> // Arduino Update (ESP32)
// SHA256 verify is disabled below to avoid mbedTLS dependency conflicts.
// Re-enable later if needed.
namespace {
String s_cur = "1.0.0"; // default current version
// Compare "1.2.3" vs "1.2.2"
int vercmp(const String& a, const String& b) {
int as[3]={0}, bs[3]={0};
sscanf(a.c_str(), "%d.%d.%d", &as[0], &as[1], &as[2]);
sscanf(b.c_str(), "%d.%d.%d", &bs[0], &bs[1], &bs[2]);
for (int i=0;i<3;i++) if (as[i]!=bs[i]) return (as[i]>bs[i])?1:-1;
return 0;
}
// Extract "key":"value" or key:value (string tokens only)
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]==':')) k++;
if (k < (int)j.length() && j[k]=='\"') {
int e = j.indexOf('\"', k+1);
if (e > k) return j.substring(k+1, e);
}
return "";
}
// Download URL to APFS path using HTTPTOFS
bool httpToFile(const String& url, const String& path) {
// delete old (ignore errors)
NetMgr::sendAT("AT+CFSDFILE=3,\"" + path + "\"", 2000);
// download
String r = NetMgr::sendAT("AT+HTTPTOFS=\"" + url + "\",\"" + path + "\"", 60000);
// Expect +HTTPTOFS: 200,<len>
return r.indexOf("+HTTPTOFS: 200,") != -1;
}
// Stream APFS file to Update (in chunks)
bool apfsStreamToUpdate(const String& path) {
// Probe size by reading header response (+CFSRFILE cannot query size directly here).
// Well stream until CFSRFILE returns no data.
const size_t chunk = 2048;
size_t total = 0;
if (!Update.begin(1024*1024)) { // 1MB max placeholder; adjust or use two-step begin with size if known
return false;
}
for (size_t offset = 0; ; offset += chunk) {
String cmd = "AT+CFSRFILE=3,\"" + path + "\"," + String(offset) + "," + String(chunk) + ",0";
String rf = NetMgr::sendAT(cmd, 8000);
int tag = rf.indexOf("+CFSRFILE:");
if (tag == -1) break;
int hdrEnd = rf.indexOf('\n', tag);
if (hdrEnd == -1 || hdrEnd + 1 >= (int)rf.length()) break;
String data = rf.substring(hdrEnd + 1);
int okPos = data.lastIndexOf("\nOK");
if (okPos >= 0) data = data.substring(0, okPos);
if (!data.length()) break;
size_t written = Update.write((uint8_t*)data.begin(), data.length());
if (written != (size_t)data.length()) { Update.end(); return false; }
total += written;
if (data.length() < chunk) break; // EOF
}
return Update.end(true);
}
}
namespace OTAMgr {
void setCurrentVersion(const String& v) { s_cur = v; }
const String& currentVersion() { return s_cur; }
// GET /api/device/{imei}/firmware -> { "version":"x.y.z", "url":"http://.../fw.bin", "sha256":"..." }
bool check(const String& baseUrl, const String& imei,
String& outVersion, String& outUrl, String& outSha256)
{
outVersion = outUrl = outSha256 = "";
// Reset SH state
NetMgr::sendAT("AT+SHDISC", 2000);
NetMgr::sendAT("AT+SHCHEAD", 2000);
if (NetMgr::sendAT("AT+SHCONF=\"URL\",\"" + baseUrl + "\"", 4000).indexOf("OK") == -1) return false;
if (NetMgr::sendAT("AT+SHCONN", 12000).indexOf("OK") == -1) return false;
NetMgr::sendAT("AT+SHCHEAD", 2000);
NetMgr::sendAT("AT+SHAHEAD=\"Accept\",\"application/json\"", 2000);
String path = "/api/device/" + imei + "/firmware";
String urc = NetMgr::sendAT("AT+SHREQ=\"" + path + "\",1", 20000);
// parse length 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;
}
}
String body;
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(); body = b;
}
}
NetMgr::sendAT("AT+SHDISC", 2000);
if (body.isEmpty()) return false;
outVersion = pickStr(body, "version");
outUrl = pickStr(body, "url");
outSha256 = pickStr(body, "sha256");
if (outVersion.isEmpty() || outUrl.isEmpty()) return false;
return vercmp(outVersion, s_cur) > 0;
}
// Download (HTTPTOFS) to APFS and stream to Update. Reboot on success.
bool apply(const String& url, const String& /*sha256*/) {
const String path = "/custapp/fw.bin";
if (!httpToFile(url, path)) return false;
bool ok = apfsStreamToUpdate(path);
NetMgr::sendAT("AT+CFSDFILE=3,\"" + path + "\"", 3000);
if (ok) {
Serial.println("OTA applied, restarting...");
delay(500);
ESP.restart();
}
return ok;
}
} // namespace OTAMgr

View File

@ -0,0 +1,17 @@
#pragma once
#include <Arduino.h>
namespace OTAMgr {
// Set current FW version string (e.g., "1.0.0")
void setCurrentVersion(const String& v);
// Check server for update: GET /api/device/{imei}/firmware
// Returns true if an update is available and populates outVersion/outUrl/outSha256.
bool check(const String& baseUrl, const String& imei,
String& outVersion, String& outUrl, String& outSha256);
// Download and apply firmware, then reboot. Returns true on success.
bool apply(const String& url, const String& sha256);
const String& currentVersion();
}

View File

@ -0,0 +1,89 @@
#include "SmsMgr.hpp"
#include "NetMgr.hpp"
namespace {
String readAndDelete(int idx) {
String r = NetMgr::sendAT("AT+CMGR=" + String(idx), 4000);
int okPos = r.lastIndexOf("\nOK");
String s = (okPos>0) ? r.substring(0, okPos) : r;
int lastLf = s.lastIndexOf('\n');
if (lastLf != -1) s = s.substring(lastLf + 1);
s.trim();
NetMgr::sendAT("AT+CMGD=" + String(idx), 2000);
return s;
}
void parseSimpleJson(const String& msg) {
auto extract = [&](const String& key)->String {
String pat = "\"" + key + "\":";
int k = msg.indexOf(pat);
if (k < 0) return "";
k += pat.length();
while (k < (int)msg.length() && (msg[k]==' '||msg[k]=='\"')) k++;
if (k>0 && msg[k-1]=='\"') {
int e = msg.indexOf('\"', k);
return (e>k) ? msg.substring(k, e) : "";
} else {
int e=k; while (e<(int)msg.length() && (isDigit(msg[e])||msg[e]=='.'||msg[e]=='-')) e++;
return msg.substring(k, e);
}
};
String deviceId = extract("device_id");
String speed = extract("speed");
Serial.println("SMS JSON parsed: device_id=" + deviceId + " speed=" + speed);
}
} // anon
namespace SmsMgr {
void setup() {
NetMgr::sendAT("AT+CMGF=1", 2000);
NetMgr::sendAT("AT+CPMS=\"SM\",\"SM\",\"SM\"", 2000);
NetMgr::sendAT("AT+CNMI=2,1,0,0,0", 2000);
}
void handleBody(const String& body) {
Serial.println("SMS body: " + body);
if (body.startsWith("{")) parseSimpleJson(body);
// else map plain-text commands here
}
void pollUrc() {
static String urc;
while (Serial2.available()) {
char c = (char)Serial2.read();
urc += c;
if (c=='\n') {
urc.trim();
if (urc.startsWith("+CMTI:")) {
int comma = urc.lastIndexOf(',');
if (comma != -1) {
int idx = urc.substring(comma+1).toInt();
String body = readAndDelete(idx);
handleBody(body);
}
}
urc = "";
}
}
}
void pollUnread() {
String list = NetMgr::sendAT("AT+CMGL=\"REC UNREAD\"", 3000);
int pos = 0;
while (true) {
int p = list.indexOf("+CMGL:", pos);
if (p == -1) break;
int comma = list.indexOf(',', p);
int idxStart = list.indexOf(' ', p);
if (comma==-1 || idxStart==-1 || idxStart>comma) break;
int idx = list.substring(idxStart+1, comma).toInt();
String body = readAndDelete(idx);
handleBody(body);
pos = comma + 1;
}
}
} // namespace SmsMgr

View File

@ -0,0 +1,9 @@
#pragma once
#include <Arduino.h>
namespace SmsMgr {
void setup();
void pollUrc(); // handle +CMTI
void pollUnread(); // optional periodic safety poll
void handleBody(const String& body); // your command handler entry
}

View File

@ -0,0 +1,52 @@
#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;
}
String buildNoPowerJson(const String& imei) {
return String("{\"device_id\":\"") + imei + "\",\"car_power\":false}";
}
} // namespace Telemetry namespace Telemetry

View File

@ -0,0 +1,9 @@
// Telemetry.hpp
#pragma once
#include <Arduino.h>
namespace Telemetry {
bool read(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, bool carPower);
String buildNoPowerJson(const String& imei);
}

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);
}

View File

@ -0,0 +1,16 @@
// PowerMgr.cpp
#include "PowerMgr.hpp"
namespace {
bool carOn = true;
}
namespace PowerMgr {
void init(bool carPowerInitial){ carOn = carPowerInitial; }
bool isCarPowerOn(){ return carOn; }
void setCarPower(bool on){ carOn = on; }
void deepSleep(uint32_t /*sec*/) {
// placeholder: implement esp_deep_sleep() later
// For now, just delay to simulate
}
}

View File

@ -0,0 +1,11 @@
// PowerMgr.hpp
#pragma once
#include <Arduino.h>
namespace PowerMgr {
void init(bool carPowerInitial);
bool isCarPowerOn();
void setCarPower(bool on); // simulate change
// called before deep sleep; placeholder
void deepSleep(uint32_t sec);
}

View File

@ -0,0 +1,19 @@
// WiFiMgr.cpp
#include "WiFiMgr.hpp"
#include <WiFi.h> // ESP32 WiFi
namespace WiFiMgr {
bool connectHome(const char* ssid, const char* psk, uint32_t timeoutMs) {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, psk);
uint32_t t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < timeoutMs) {
delay(200);
}
return WiFi.status() == WL_CONNECTED;
}
void disconnect() {
WiFi.disconnect(true, true);
WiFi.mode(WIFI_OFF);
}
}

View File

@ -0,0 +1,8 @@
// WiFiMgr.hpp
#pragma once
#include <Arduino.h>
namespace WiFiMgr {
bool connectHome(const char* ssid, const char* psk, uint32_t timeoutMs = 15000);
void disconnect();
}