diff --git a/CAR_GPS_TRACKER.ino b/CAR_GPS_TRACKER.ino index d7e4baa..5176fa2 100644 --- a/CAR_GPS_TRACKER.ino +++ b/CAR_GPS_TRACKER.ino @@ -6,6 +6,9 @@ #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" static AppConfig CFG; @@ -26,6 +29,8 @@ void setup() { NetMgr::attachAndPdp(CFG.apn.c_str()); SmsMgr::setup(); + OTAMgr::setCurrentVersion(FW_VERSION); + // 2) Identity CFG.imei = queryImei(); if (CFG.imei.isEmpty()) CFG.imei = "860016049744324"; diff --git a/src/core/CommandMgr.cpp b/src/core/CommandMgr.cpp index 760bf72..991436a 100644 --- a/src/core/CommandMgr.cpp +++ b/src/core/CommandMgr.cpp @@ -246,6 +246,13 @@ namespace CommandMgr detail = "unknown wifi target"; } } + else if (c.type == "ota") + { + String url = /* parse from payload */; + String sha = /* parse if provided */; + execOk = OTAMgr::apply(url, sha); + detail = execOk ? "ota applied" : "ota failed"; + } else { execOk = true; // unknown; mark ok but do nothing diff --git a/src/core/OTAMgr.cpp b/src/core/OTAMgr.cpp index e69de29..35e924a 100644 --- a/src/core/OTAMgr.cpp +++ b/src/core/OTAMgr.cpp @@ -0,0 +1,200 @@ +#include "OTAMgr.hpp" +#include "NetMgr.hpp" +#include // Arduino Update (ESP32) +#include // 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, + 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 \ No newline at end of file diff --git a/src/core/OTAMgr.hpp b/src/core/OTAMgr.hpp index e69de29..98b5041 100644 --- a/src/core/OTAMgr.hpp +++ b/src/core/OTAMgr.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +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(); +} \ No newline at end of file