|
|
|
|
@ -0,0 +1,200 @@
|
|
|
|
|
#include "OTAMgr.hpp"
|
|
|
|
|
#include "NetMgr.hpp"
|
|
|
|
|
#include <Update.h> // Arduino Update (ESP32)
|
|
|
|
|
#include <mbedtls/sha256.h> // for SHA-256 verify (optional)
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
String s_cur = "1.0.0";
|
|
|
|
|
|
|
|
|
|
// crude compare: "1.2.3" > "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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// simple JSON pickers (expects small JSON produced by your server)
|
|
|
|
|
String pick(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 (j[k-1]=='\"') {
|
|
|
|
|
int e = j.indexOf('\"', k);
|
|
|
|
|
if (e > k) return j.substring(k, 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 "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool httpToFile(const String& url, const String& path) {
|
|
|
|
|
// delete old (ignore error)
|
|
|
|
|
NetMgr::sendAT("AT+CFSDFILE=3,\"" + path + "\"", 2000);
|
|
|
|
|
String r = NetMgr::sendAT("AT+HTTPTOFS=\"" + url + "\",\"" + path + "\"", 60000);
|
|
|
|
|
// optional: parse +HTTPTOFS: 200,<len>
|
|
|
|
|
return r.indexOf("+HTTPTOFS: 200,") != -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool apfsReadAll(const String& path, Stream& to, size_t maxBytes = 1024*1024) {
|
|
|
|
|
// Read in chunks using CFSRFILE (SIM7080 returns content after header)
|
|
|
|
|
const size_t chunk = 2048;
|
|
|
|
|
size_t offset = 0, total = 0;
|
|
|
|
|
while (total < maxBytes) {
|
|
|
|
|
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;
|
|
|
|
|
to.write((const uint8_t*)data.c_str(), data.length());
|
|
|
|
|
total += data.length();
|
|
|
|
|
offset += data.length();
|
|
|
|
|
if (data.length() < chunk) break; // end of file
|
|
|
|
|
}
|
|
|
|
|
return total > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// optional SHA-256 verify of the APFS file content
|
|
|
|
|
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 {
|
|
|
|
|
|
|
|
|
|
void setCurrentVersion(const String& v){ s_cur = v; }
|
|
|
|
|
const String& currentVersion(){ return s_cur; }
|
|
|
|
|
|
|
|
|
|
bool check(const String& baseUrl, const String& imei, String& outVersion, String& outUrl, String& outSha256) {
|
|
|
|
|
outVersion = outUrl = outSha256 = "";
|
|
|
|
|
|
|
|
|
|
// Use SH HTTP to GET /api/device/{imei}/firmware and read exact
|
|
|
|
|
// Reuse the same sequence as getJsonExact:
|
|
|
|
|
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 len
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// Pick fields
|
|
|
|
|
outVersion = pick(body, "version");
|
|
|
|
|
outUrl = pick(body, "url");
|
|
|
|
|
outSha256 = pick(body, "sha256");
|
|
|
|
|
|
|
|
|
|
if (outVersion.isEmpty() || outUrl.isEmpty()) return false;
|
|
|
|
|
// Compare
|
|
|
|
|
return vercmp(outVersion, s_cur) > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool apply(const String& url, const String& sha256) {
|
|
|
|
|
const String path = "/custapp/fw.bin";
|
|
|
|
|
|
|
|
|
|
// Download to APFS
|
|
|
|
|
if (!httpToFile(url, path)) return false;
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
if (ok) {
|
|
|
|
|
Serial.println("OTA applied, restarting...");
|
|
|
|
|
delay(500);
|
|
|
|
|
ESP.restart();
|
|
|
|
|
}
|
|
|
|
|
return ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace OTAMgr
|