#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