Compare commits
9 Commits
v0.4-comma
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 998e67b246 | |||
| d743a81069 | |||
| f6d5bb58bd | |||
| 12cc17544b | |||
| 66baea7a61 | |||
| 050d02d822 | |||
| 14ebfa5451 | |||
| 8f2f83ddb6 | |||
| ad0a1b1e3d |
3
.vscode/c_cpp_properties.json
vendored
3
.vscode/c_cpp_properties.json
vendored
@ -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/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/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": [
|
||||
"/home/sam/.arduino15/packages/esp32/hardware/esp32/3.2.0/cores/esp32/Arduino.h"
|
||||
|
||||
@ -2,51 +2,118 @@
|
||||
#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"
|
||||
|
||||
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";
|
||||
|
||||
#define FW_VERSION "1.0.0"
|
||||
|
||||
AppConfig CFG;
|
||||
|
||||
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);
|
||||
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() {
|
||||
Serial.begin(115200);
|
||||
Serial2.begin(9600, SERIAL_8N1, 16, 17); // RX=16, TX=17
|
||||
Serial.begin(115200);
|
||||
Serial2.begin(9600, SERIAL_8N1, 16, 17);
|
||||
|
||||
NetMgr::powerOnSIM7080(4); // PWRKEY pin 4
|
||||
NetMgr::attachAndPdp(APN); // LTE reg + CNACT + DNS
|
||||
SmsMgr::setup(); // enable text mode + URCs
|
||||
// 1) Radio attach
|
||||
NetMgr::powerOnSIM7080(4);
|
||||
NetMgr::attachAndPdp(CFG.apn.c_str());
|
||||
SmsMgr::setup();
|
||||
|
||||
DEVICE_IMEI = queryImei();
|
||||
if (DEVICE_IMEI.length()==0) DEVICE_IMEI = "860016049744324"; // fallback if needed
|
||||
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);
|
||||
}
|
||||
|
||||
CommandMgr::configure(BASE_URL, DEVICE_IMEI);
|
||||
|
||||
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(); // non‑blocking URC scan; handles +CMTI
|
||||
CommandMgr::poll(30000); // every 30s
|
||||
SmsMgr::pollUnread(); // optional safety poll
|
||||
delay(250);
|
||||
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 Wi‑Fi, sleep C seconds)
|
||||
CommandMgr::poll(0);
|
||||
|
||||
// Placeholder: if server requested home Wi‑Fi, 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);
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
@ -1,116 +1,136 @@
|
||||
// src/core/CommandMgr.cpp
|
||||
#include "CommandMgr.hpp"
|
||||
#include "HttpClient.hpp"
|
||||
#include "NetMgr.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
|
||||
// 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;
|
||||
@ -118,67 +138,44 @@ void configure(const String& baseUrl, const String& imei) {
|
||||
|
||||
void poll(uint32_t minIntervalMs) {
|
||||
uint32_t now = millis();
|
||||
if (now - lastPoll < minIntervalMs) return;
|
||||
if (minIntervalMs && (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;
|
||||
|
||||
Serial.println("Commands JSON: " + body);
|
||||
|
||||
std::vector<Command> cmds;
|
||||
parseArray(body, cmds);
|
||||
|
||||
for (auto& c : cmds) {
|
||||
bool execOk = false;
|
||||
String detail = "";
|
||||
|
||||
String detail;
|
||||
if (c.type == "lights") {
|
||||
execOk = handleLights(c.payload);
|
||||
detail = "lights handled";
|
||||
execOk = handleLights(c.payload); detail = "lights";
|
||||
} else if (c.type == "sleep") {
|
||||
execOk = handleSleep(c.payload);
|
||||
detail = "sleep handled";
|
||||
execOk = handleSleep(c.payload); detail = "sleep";
|
||||
} else if (c.type == "ring_fence") {
|
||||
execOk = handleRingFence(c.payload);
|
||||
detail = "ring fence handled";
|
||||
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; // unknown; mark ok but do nothing
|
||||
detail = "unknown type";
|
||||
execOk = true; detail = "unknown";
|
||||
}
|
||||
|
||||
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
|
||||
} // namespace CommandMgr
|
||||
@ -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).
|
||||
// We’ll 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
|
||||
@ -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();
|
||||
}
|
||||
@ -45,4 +45,8 @@ String buildJson(const String& imei, double lat, double lng, double alt, double
|
||||
return j;
|
||||
}
|
||||
|
||||
} // namespace Telemetry
|
||||
String buildNoPowerJson(const String& imei) {
|
||||
return String("{\"device_id\":\"") + imei + "\",\"car_power\":false}";
|
||||
}
|
||||
|
||||
} // namespace Telemetry namespace Telemetry
|
||||
@ -1,6 +1,9 @@
|
||||
// Telemetry.hpp
|
||||
#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);
|
||||
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);
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user