Add clean OTAMgr (check/apply via HTTPTOFS + Update stream); CommandMgr dispatch & receipts (wifi/sleep/periods)
This commit is contained in:
@ -1,391 +1,185 @@
|
|||||||
|
// src/core/CommandMgr.cpp
|
||||||
#include "CommandMgr.hpp"
|
#include "CommandMgr.hpp"
|
||||||
#include "HttpClient.hpp"
|
#include "HttpClient.hpp"
|
||||||
|
#include "NetMgr.hpp"
|
||||||
|
|
||||||
|
// Access CFG set in .ino
|
||||||
#include "../app/AppConfig.hpp"
|
#include "../app/AppConfig.hpp"
|
||||||
#include "OTAMgr.hpp"
|
|
||||||
extern AppConfig CFG;
|
extern AppConfig CFG;
|
||||||
|
|
||||||
namespace
|
namespace CommandMgr {
|
||||||
{
|
|
||||||
String BASE;
|
|
||||||
String IMEI;
|
|
||||||
uint32_t lastPoll = 0;
|
|
||||||
|
|
||||||
// Minimal JSON parsing helpers (string-based)
|
struct Command {
|
||||||
// Extract array of objects from a JSON string like: [{"id":..,"type":"..","payload":{...}}, ...]
|
long id = 0;
|
||||||
// We’ll do a naive split on "},{" for small payloads; replace with proper JSON parsing later.
|
String type;
|
||||||
|
String payload;
|
||||||
|
};
|
||||||
|
|
||||||
static int pickInt(const String &j, const char *key)
|
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 + "\":";
|
String pat = String("\"") + key + "\":";
|
||||||
int k = j.indexOf(pat);
|
int k = j.indexOf(pat);
|
||||||
if (k < 0)
|
if (k < 0) return -1;
|
||||||
return -1;
|
|
||||||
k += pat.length();
|
k += pat.length();
|
||||||
while (k < (int)j.length() && (j[k] == ' ' || j[k] == ':'))
|
while (k < (int)j.length() && (j[k]==' '||j[k]==':')) k++;
|
||||||
k++;
|
|
||||||
int i = k;
|
int i = k;
|
||||||
while (i < (int)j.length() && isDigit(j[i]))
|
while (i < (int)j.length() && isDigit(j[i])) i++;
|
||||||
i++;
|
if (i <= k) return -1;
|
||||||
if (i <= k)
|
|
||||||
return -1;
|
|
||||||
return j.substring(k, i).toInt();
|
return j.substring(k, i).toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
static String pickStr(const String &j, const char *key)
|
static String pickStr(const String& j, const char* key) {
|
||||||
{
|
|
||||||
String pat = String("\"") + key + "\":";
|
String pat = String("\"") + key + "\":";
|
||||||
int k = j.indexOf(pat);
|
int k = j.indexOf(pat);
|
||||||
if (k < 0)
|
if (k < 0) return "";
|
||||||
return "";
|
|
||||||
k += pat.length();
|
k += pat.length();
|
||||||
while (k < (int)j.length() && (j[k] == ' ' || j[k] == '\"' || j[k] == ':'))
|
while (k < (int)j.length() && (j[k]==' '||j[k]==':'||j[k]=='\"')) k++;
|
||||||
k++;
|
if (k < (int)j.length() && j[k-1]=='\"') {
|
||||||
if (k > 0 && j[k - 1] == '\"')
|
|
||||||
{
|
|
||||||
int e = j.indexOf('\"', k);
|
int e = j.indexOf('\"', k);
|
||||||
if (e > k)
|
if (e > k) return j.substring(k, e);
|
||||||
return j.substring(k, e);
|
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
long extractId(const String &obj)
|
// naive split: [{".."},{".."}]
|
||||||
{
|
static void parseArray(const String& json, std::vector<Command>& out) {
|
||||||
String pat = "\"id\":";
|
String s = json; s.trim();
|
||||||
int k = obj.indexOf(pat);
|
if (!s.startsWith("[")) return;
|
||||||
if (k < 0)
|
if (s.length()>=2 && s.endsWith("]")) s = s.substring(1, s.length()-1);
|
||||||
return 0;
|
int start=0;
|
||||||
k += pat.length();
|
while (start<(int)s.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);
|
int next = s.indexOf("},{", start);
|
||||||
String obj;
|
String obj;
|
||||||
if (next == -1)
|
if (next == -1) {
|
||||||
{
|
obj = s.substring(start); obj.trim();
|
||||||
obj = s.substring(start);
|
if (!obj.startsWith("{")) obj = "{" + obj;
|
||||||
obj.trim();
|
if (!obj.endsWith("}")) obj += "}";
|
||||||
if (obj.startsWith("{") && obj.endsWith("}"))
|
|
||||||
{
|
|
||||||
// ok
|
|
||||||
}
|
|
||||||
else if (!obj.isEmpty())
|
|
||||||
{
|
|
||||||
// Try pad
|
|
||||||
if (!obj.startsWith("{"))
|
|
||||||
obj = "{" + obj;
|
|
||||||
if (!obj.endsWith("}"))
|
|
||||||
obj += "}";
|
|
||||||
}
|
|
||||||
start = s.length();
|
start = s.length();
|
||||||
|
} else {
|
||||||
|
obj = s.substring(start, next+1);
|
||||||
|
if (!obj.startsWith("{")) obj = "{" + obj;
|
||||||
|
start = next + 2;
|
||||||
}
|
}
|
||||||
else
|
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":
|
||||||
{
|
{
|
||||||
obj = s.substring(start, next + 1); // include the trailing }
|
String pat="\"payload\":";
|
||||||
if (!obj.startsWith("{"))
|
int k=obj.indexOf(pat);
|
||||||
obj = "{" + obj;
|
if (k>=0) {
|
||||||
start = next + 2; // position after },
|
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);
|
||||||
obj.trim();
|
} else if (k<(int)obj.length() && obj[k]=='\"') {
|
||||||
if (obj.length() == 0)
|
int e=obj.indexOf('\"', k+1);
|
||||||
continue;
|
if (e>k) c.payload = obj.substring(k+1, e);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (c.id>0 && c.type.length()) out.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool sendReceipt(long id, const String &result, const String &detail)
|
// ---------- receipts ----------
|
||||||
{
|
static bool sendReceipt(long id, const String& result, const String& detail) {
|
||||||
// POST /api/device/{imei}/command-receipts
|
|
||||||
String path = "/api/device/" + IMEI + "/command-receipts";
|
String path = "/api/device/" + IMEI + "/command-receipts";
|
||||||
String body = String("{\"command_id\":") + id + ",\"result\":\"" + result + "\",\"detail\":\"" + detail + "\"}";
|
String body = String("{\"command_id\":") + id + ",\"result\":\"" + result + "\",\"detail\":\"" + detail + "\"}";
|
||||||
return HttpClient::postJson(BASE, path, body);
|
return HttpClient::postJson(BASE, path, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // anon
|
// ---------- handlers ----------
|
||||||
|
bool handleLights(const String& /*payload*/) { return true; } // placeholder
|
||||||
|
|
||||||
namespace CommandMgr
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
void configure(const String &baseUrl, const String &imei)
|
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;
|
BASE = baseUrl;
|
||||||
IMEI = imei;
|
IMEI = imei;
|
||||||
}
|
}
|
||||||
|
|
||||||
void poll(uint32_t minIntervalMs)
|
void poll(uint32_t minIntervalMs) {
|
||||||
{
|
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
if (now - lastPoll < minIntervalMs)
|
if (minIntervalMs && (now - lastPoll < minIntervalMs)) return;
|
||||||
return;
|
|
||||||
lastPoll = now;
|
lastPoll = now;
|
||||||
|
|
||||||
if (BASE.isEmpty() || IMEI.isEmpty())
|
if (BASE.isEmpty() || IMEI.isEmpty()) return;
|
||||||
return;
|
|
||||||
|
|
||||||
String path = "/api/device/" + IMEI + "/commands";
|
String path = "/api/device/" + IMEI + "/commands";
|
||||||
String body;
|
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);
|
bool ok = HttpClient::getJsonExact(BASE, path, body);
|
||||||
|
if (!ok || body.length()==0) return;
|
||||||
|
|
||||||
Serial.println("Commands JSON: " + 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;
|
std::vector<Command> cmds;
|
||||||
parseArray(body, cmds);
|
parseArray(body, cmds);
|
||||||
for (auto &c : cmds)
|
|
||||||
{
|
for (auto& c : cmds) {
|
||||||
bool execOk = false;
|
bool execOk = false;
|
||||||
String detail = "";
|
String detail;
|
||||||
|
if (c.type == "lights") {
|
||||||
if (c.type == "lights")
|
execOk = handleLights(c.payload); detail = "lights";
|
||||||
{
|
} else if (c.type == "sleep") {
|
||||||
execOk = handleLights(c.payload);
|
execOk = handleSleep(c.payload); detail = "sleep";
|
||||||
detail = "lights handled";
|
} 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";
|
||||||
}
|
}
|
||||||
else if (c.type == "sleep")
|
|
||||||
|
|
||||||
{
|
|
||||||
// parse {"interval_sec":N}
|
|
||||||
int pos = c.payload.indexOf("\"interval_sec\":");
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
int i = pos + 15;
|
|
||||||
while (i < (int)c.payload.length() && (c.payload[i] == ' ' || c.payload[i] == ':'))
|
|
||||||
i++;
|
|
||||||
int j = i;
|
|
||||||
while (j < (int)c.payload.length() && isDigit(c.payload[j]))
|
|
||||||
j++;
|
|
||||||
if (j > i)
|
|
||||||
{
|
|
||||||
CFG.sleepSec = c.payload.substring(i, j).toInt();
|
|
||||||
execOk = true;
|
|
||||||
detail = "sleep interval updated";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
execOk = false;
|
|
||||||
detail = "bad interval";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
execOk = false;
|
|
||||||
detail = "missing interval";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c.type == "ring_fence")
|
|
||||||
{
|
|
||||||
execOk = handleRingFence(c.payload);
|
|
||||||
detail = "ring fence handled";
|
|
||||||
}
|
|
||||||
else if (c.type == "wifi")
|
|
||||||
{
|
|
||||||
// payload expected: {"target":"home"}
|
|
||||||
extern AppConfig CFG; // declare once in CommandMgr.cpp top: extern AppConfig CFG;
|
|
||||||
if (c.payload.indexOf("\"home\"") != -1 || c.payload.indexOf("\"target\":\"home\"") != -1)
|
|
||||||
{
|
|
||||||
CFG.atHomeRequested = true;
|
|
||||||
execOk = true;
|
|
||||||
detail = "wifi home requested";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
execOk = false;
|
|
||||||
detail = "unknown wifi target";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c.type == "ota")
|
|
||||||
{
|
|
||||||
|
|
||||||
String url = pickStr(c.payload, "url");
|
|
||||||
String sha = pickStr(c.payload, "sha256");
|
|
||||||
if (!url.length())
|
|
||||||
{
|
|
||||||
execOk = false;
|
|
||||||
detail = "missing url";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
execOk = OTAMgr::apply(url, sha);
|
|
||||||
detail = execOk ? "ota applied" : "ota failed";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c.type == "telemetry_sec")
|
|
||||||
{
|
|
||||||
int sec = pickInt(c.payload, "telemetry_sec");
|
|
||||||
if (sec > 0)
|
|
||||||
{
|
|
||||||
CFG.telemetryPeriodSec = (uint32_t)sec;
|
|
||||||
Serial.printf("Telemetry sec set to %u\n", CFG.telemetryPeriodSec);
|
|
||||||
execOk = true;
|
|
||||||
detail = "telemetry updated";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
execOk = false;
|
|
||||||
detail = "bad telemetry_sec";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c.type == "poll_sec")
|
|
||||||
{
|
|
||||||
int sec = pickInt(c.payload, "poll_sec");
|
|
||||||
if (sec > 0)
|
|
||||||
{
|
|
||||||
CFG.commandPollSec = (uint32_t)sec;
|
|
||||||
Serial.printf("Poll sec set to %u\n", CFG.commandPollSec);
|
|
||||||
execOk = true;
|
|
||||||
detail = "poll updated";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
execOk = false;
|
|
||||||
detail = "bad poll_sec";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c.type == "wifi")
|
|
||||||
{
|
|
||||||
String tgt = pickStr(c.payload, "target");
|
|
||||||
if (tgt.equalsIgnoreCase("home"))
|
|
||||||
{
|
|
||||||
CFG.atHomeRequested = true;
|
|
||||||
Serial.println("WiFi home connect requested");
|
|
||||||
execOk = true;
|
|
||||||
detail = "wifi home requested";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
execOk = false;
|
|
||||||
detail = "unknown wifi target";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
execOk = true; // unknown; mark ok but do nothing
|
|
||||||
detail = "unknown type";
|
|
||||||
}
|
|
||||||
|
|
||||||
sendReceipt(c.id, execOk ? "ok" : "error", detail);
|
sendReceipt(c.id, execOk ? "ok" : "error", detail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool handleLights(const String &payload)
|
} // namespace CommandMgr
|
||||||
{
|
|
||||||
// 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)
|
|
||||||
{
|
|
||||||
|
|
||||||
int sec = pickInt(payload, "interval_sec");
|
|
||||||
if (sec > 0)
|
|
||||||
{
|
|
||||||
CFG.sleepSec = (uint32_t)sec;
|
|
||||||
Serial.printf("Sleep sec set to %u\n", CFG.sleepSec);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
@ -1,12 +1,14 @@
|
|||||||
|
// src/core/OTAMgr.cpp
|
||||||
#include "OTAMgr.hpp"
|
#include "OTAMgr.hpp"
|
||||||
#include "NetMgr.hpp"
|
#include "NetMgr.hpp"
|
||||||
#include <Update.h> // Arduino Update (ESP32)
|
#include <Update.h> // Arduino Update (ESP32)
|
||||||
#include <mbedtls/sha256.h> // for SHA-256 verify (optional)
|
// SHA256 verify is disabled below to avoid mbedTLS dependency conflicts.
|
||||||
|
// Re-enable later if needed.
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
String s_cur = "1.0.0";
|
String s_cur = "1.0.0"; // default current version
|
||||||
|
|
||||||
// crude compare: "1.2.3" > "1.2.2"
|
// Compare "1.2.3" vs "1.2.2"
|
||||||
int vercmp(const String& a, const String& b) {
|
int vercmp(const String& a, const String& b) {
|
||||||
int as[3]={0}, bs[3]={0};
|
int as[3]={0}, bs[3]={0};
|
||||||
sscanf(a.c_str(), "%d.%d.%d", &as[0], &as[1], &as[2]);
|
sscanf(a.c_str(), "%d.%d.%d", &as[0], &as[1], &as[2]);
|
||||||
@ -15,108 +17,91 @@ namespace {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// simple JSON pickers (expects small JSON produced by your server)
|
// Extract "key":"value" or key:value (string tokens only)
|
||||||
String pick(const String& j, const char* key) {
|
String pickStr(const String& j, const char* key) {
|
||||||
String pat = String("\"") + key + "\":";
|
String pat = String("\"") + key + "\":";
|
||||||
int k = j.indexOf(pat);
|
int k = j.indexOf(pat);
|
||||||
if (k < 0) return "";
|
if (k < 0) return "";
|
||||||
k += pat.length();
|
k += pat.length();
|
||||||
while (k < (int)j.length() && (j[k]==' ' || j[k]=='\"')) k++;
|
while (k < (int)j.length() && (j[k]==' ' || j[k]==':')) k++;
|
||||||
if (j[k-1]=='\"') {
|
if (k < (int)j.length() && j[k]=='\"') {
|
||||||
int e = j.indexOf('\"', k);
|
int e = j.indexOf('\"', k+1);
|
||||||
if (e > k) return j.substring(k, e);
|
if (e > k) return j.substring(k+1, e);
|
||||||
} else {
|
|
||||||
int e = k;
|
|
||||||
while (e<(int)j.length() && (isAlphaNumeric(j[e]) || j[e]=='.' || j[e]==':' || j[e]=='/' || j[e]=='_' || j[e]=='-')) e++;
|
|
||||||
return j.substring(k, e);
|
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download URL to APFS path using HTTPTOFS
|
||||||
bool httpToFile(const String& url, const String& path) {
|
bool httpToFile(const String& url, const String& path) {
|
||||||
// delete old (ignore error)
|
// delete old (ignore errors)
|
||||||
NetMgr::sendAT("AT+CFSDFILE=3,\"" + path + "\"", 2000);
|
NetMgr::sendAT("AT+CFSDFILE=3,\"" + path + "\"", 2000);
|
||||||
|
// download
|
||||||
String r = NetMgr::sendAT("AT+HTTPTOFS=\"" + url + "\",\"" + path + "\"", 60000);
|
String r = NetMgr::sendAT("AT+HTTPTOFS=\"" + url + "\",\"" + path + "\"", 60000);
|
||||||
// optional: parse +HTTPTOFS: 200,<len>
|
// Expect +HTTPTOFS: 200,<len>
|
||||||
return r.indexOf("+HTTPTOFS: 200,") != -1;
|
return r.indexOf("+HTTPTOFS: 200,") != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool apfsReadAll(const String& path, Stream& to, size_t maxBytes = 1024*1024) {
|
// Stream APFS file to Update (in chunks)
|
||||||
// Read in chunks using CFSRFILE (SIM7080 returns content after header)
|
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;
|
const size_t chunk = 2048;
|
||||||
size_t offset = 0, total = 0;
|
size_t total = 0;
|
||||||
while (total < maxBytes) {
|
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 cmd = "AT+CFSRFILE=3,\"" + path + "\"," + String(offset) + "," + String(chunk) + ",0";
|
||||||
String rf = NetMgr::sendAT(cmd, 8000);
|
String rf = NetMgr::sendAT(cmd, 8000);
|
||||||
|
|
||||||
int tag = rf.indexOf("+CFSRFILE:");
|
int tag = rf.indexOf("+CFSRFILE:");
|
||||||
if (tag == -1) break;
|
if (tag == -1) break;
|
||||||
int hdrEnd = rf.indexOf('\n', tag);
|
int hdrEnd = rf.indexOf('\n', tag);
|
||||||
if (hdrEnd == -1 || hdrEnd + 1 >= (int)rf.length()) break;
|
if (hdrEnd == -1 || hdrEnd + 1 >= (int)rf.length()) break;
|
||||||
|
|
||||||
String data = rf.substring(hdrEnd + 1);
|
String data = rf.substring(hdrEnd + 1);
|
||||||
int okPos = data.lastIndexOf("\nOK");
|
int okPos = data.lastIndexOf("\nOK");
|
||||||
if (okPos >= 0) data = data.substring(0, okPos);
|
if (okPos >= 0) data = data.substring(0, okPos);
|
||||||
|
|
||||||
if (!data.length()) break;
|
if (!data.length()) break;
|
||||||
to.write((const uint8_t*)data.c_str(), data.length());
|
|
||||||
total += data.length();
|
size_t written = Update.write((uint8_t*)data.begin(), data.length());
|
||||||
offset += data.length();
|
if (written != (size_t)data.length()) { Update.end(); return false; }
|
||||||
if (data.length() < chunk) break; // end of file
|
total += written;
|
||||||
}
|
|
||||||
return total > 0;
|
if (data.length() < chunk) break; // EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional SHA-256 verify of the APFS file content
|
return Update.end(true);
|
||||||
bool verifySha256Apfs(const String& path, const String& hexExpected) {
|
|
||||||
// stream read into SHA-256
|
|
||||||
mbedtls_sha256_context ctx;
|
|
||||||
mbedtls_sha256_init(&ctx);
|
|
||||||
mbedtls_sha256_starts_ret(&ctx, 0);
|
|
||||||
|
|
||||||
// For simplicity, read into a local String buffer (not ideal for huge files)
|
|
||||||
String rf = NetMgr::sendAT("AT+CFSRFILE=3,\"" + path + "\",0,1048576,0", 15000);
|
|
||||||
int tag = rf.indexOf("+CFSRFILE:");
|
|
||||||
if (tag == -1) { mbedtls_sha256_free(&ctx); return false; }
|
|
||||||
int hdrEnd = rf.indexOf('\n', tag);
|
|
||||||
if (hdrEnd == -1 || hdrEnd + 1 >= (int)rf.length()) { mbedtls_sha256_free(&ctx); return false; }
|
|
||||||
String data = rf.substring(hdrEnd + 1);
|
|
||||||
int okPos = data.lastIndexOf("\nOK");
|
|
||||||
if (okPos >= 0) data = data.substring(0, okPos);
|
|
||||||
if (!data.length()) { mbedtls_sha256_free(&ctx); return false; }
|
|
||||||
|
|
||||||
mbedtls_sha256_update_ret(&ctx, (const unsigned char*)data.c_str(), data.length());
|
|
||||||
unsigned char out[32]; mbedtls_sha256_finish_ret(&ctx, out);
|
|
||||||
mbedtls_sha256_free(&ctx);
|
|
||||||
|
|
||||||
// hex compare
|
|
||||||
char hex[65]; for (int i=0;i<32;i++) sprintf(&hex[i*2], "%02x", out[i]);
|
|
||||||
hex[64] = 0;
|
|
||||||
String got(hex);
|
|
||||||
got.toLowerCase();
|
|
||||||
String exp = hexExpected; exp.toLowerCase();
|
|
||||||
return exp.length()==64 ? (got == exp) : true; // if no valid exp, skip
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace OTAMgr {
|
namespace OTAMgr {
|
||||||
|
|
||||||
void setCurrentVersion(const String& v){ s_cur = v; }
|
void setCurrentVersion(const String& v) { s_cur = v; }
|
||||||
const String& currentVersion(){ return s_cur; }
|
const String& currentVersion() { return s_cur; }
|
||||||
|
|
||||||
bool check(const String& baseUrl, const String& imei, String& outVersion, String& outUrl, String& outSha256) {
|
// 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 = "";
|
outVersion = outUrl = outSha256 = "";
|
||||||
|
|
||||||
// Use SH HTTP to GET /api/device/{imei}/firmware and read exact
|
// Reset SH state
|
||||||
// Reuse the same sequence as getJsonExact:
|
|
||||||
NetMgr::sendAT("AT+SHDISC", 2000);
|
NetMgr::sendAT("AT+SHDISC", 2000);
|
||||||
NetMgr::sendAT("AT+SHCHEAD", 2000);
|
NetMgr::sendAT("AT+SHCHEAD", 2000);
|
||||||
|
|
||||||
if (NetMgr::sendAT("AT+SHCONF=\"URL\",\"" + baseUrl + "\"", 4000).indexOf("OK") == -1) return false;
|
if (NetMgr::sendAT("AT+SHCONF=\"URL\",\"" + baseUrl + "\"", 4000).indexOf("OK") == -1) return false;
|
||||||
if (NetMgr::sendAT("AT+SHCONN", 12000).indexOf("OK") == -1) return false;
|
if (NetMgr::sendAT("AT+SHCONN", 12000).indexOf("OK") == -1) return false;
|
||||||
|
|
||||||
NetMgr::sendAT("AT+SHCHEAD", 2000);
|
NetMgr::sendAT("AT+SHCHEAD", 2000);
|
||||||
NetMgr::sendAT("AT+SHAHEAD=\"Accept\",\"application/json\"", 2000);
|
NetMgr::sendAT("AT+SHAHEAD=\"Accept\",\"application/json\"", 2000);
|
||||||
|
|
||||||
String path = "/api/device/" + imei + "/firmware";
|
String path = "/api/device/" + imei + "/firmware";
|
||||||
String urc = NetMgr::sendAT("AT+SHREQ=\"" + path + "\",1", 20000);
|
String urc = NetMgr::sendAT("AT+SHREQ=\"" + path + "\",1", 20000);
|
||||||
|
|
||||||
// Parse len
|
// parse length from URC
|
||||||
int len = -1;
|
int len = -1;
|
||||||
{
|
{
|
||||||
int start = 0;
|
int start = 0;
|
||||||
@ -150,43 +135,21 @@ bool check(const String& baseUrl, const String& imei, String& outVersion, String
|
|||||||
|
|
||||||
if (body.isEmpty()) return false;
|
if (body.isEmpty()) return false;
|
||||||
|
|
||||||
// Pick fields
|
outVersion = pickStr(body, "version");
|
||||||
outVersion = pick(body, "version");
|
outUrl = pickStr(body, "url");
|
||||||
outUrl = pick(body, "url");
|
outSha256 = pickStr(body, "sha256");
|
||||||
outSha256 = pick(body, "sha256");
|
|
||||||
|
|
||||||
if (outVersion.isEmpty() || outUrl.isEmpty()) return false;
|
if (outVersion.isEmpty() || outUrl.isEmpty()) return false;
|
||||||
// Compare
|
|
||||||
return vercmp(outVersion, s_cur) > 0;
|
return vercmp(outVersion, s_cur) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool apply(const String& url, const String& sha256) {
|
// 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";
|
const String path = "/custapp/fw.bin";
|
||||||
|
|
||||||
// Download to APFS
|
|
||||||
if (!httpToFile(url, path)) return false;
|
if (!httpToFile(url, path)) return false;
|
||||||
|
bool ok = apfsStreamToUpdate(path);
|
||||||
// Optional verify
|
|
||||||
if (sha256.length() == 64) {
|
|
||||||
if (!verifySha256Apfs(path, sha256)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream from APFS to Update
|
|
||||||
// Easiest: read entire file via CFSRFILE (limit ~1 MB here) and Update it
|
|
||||||
String rf = NetMgr::sendAT("AT+CFSRFILE=3,\"" + path + "\",0,1048576,0", 15000);
|
|
||||||
int tag = rf.indexOf("+CFSRFILE:");
|
|
||||||
if (tag == -1) return false;
|
|
||||||
int hdrEnd = rf.indexOf('\n', tag); if (hdrEnd == -1 || hdrEnd + 1 >= (int)rf.length()) return false;
|
|
||||||
String data = rf.substring(hdrEnd + 1);
|
|
||||||
int okPos = data.lastIndexOf("\nOK"); if (okPos >= 0) data = data.substring(0, okPos);
|
|
||||||
if (!data.length()) return false;
|
|
||||||
|
|
||||||
// Begin update
|
|
||||||
if (!Update.begin(data.length())) return false;
|
|
||||||
size_t written = Update.write((const uint8_t*)data.c_str(), data.length());
|
|
||||||
bool ok = (written == (size_t)data.length()) && Update.end(true);
|
|
||||||
|
|
||||||
// cleanup
|
|
||||||
NetMgr::sendAT("AT+CFSDFILE=3,\"" + path + "\"", 3000);
|
NetMgr::sendAT("AT+CFSDFILE=3,\"" + path + "\"", 3000);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user