Merge pull request #118 from jpirnay/fix-ota-updater
feat: Try to support CaptivePortals
This commit is contained in:
@@ -28,6 +28,9 @@ jobs:
|
|||||||
- name: Build CrossPoint
|
- name: Build CrossPoint
|
||||||
run: pio run -e gh_release
|
run: pio run -e gh_release
|
||||||
|
|
||||||
|
- name: Patch min_chip_rev_full to 0
|
||||||
|
run: python3 scripts/patch_min_chip_rev.py .pio/build/gh_release/firmware.bin
|
||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ jobs:
|
|||||||
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
|
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
|
||||||
run: pio run -e gh_release_rc
|
run: pio run -e gh_release_rc
|
||||||
|
|
||||||
|
- name: Patch min_chip_rev_full to 0
|
||||||
|
run: python3 scripts/patch_min_chip_rev.py .pio/build/gh_release_rc/firmware.bin
|
||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -534,3 +534,7 @@ STR_STAR_PAGE: "Star Page"
|
|||||||
STR_NO_STARRED_PAGES: "No starred pages"
|
STR_NO_STARRED_PAGES: "No starred pages"
|
||||||
STR_RENAME: "Rename"
|
STR_RENAME: "Rename"
|
||||||
STR_PAGE_PREFIX: "p"
|
STR_PAGE_PREFIX: "p"
|
||||||
|
STR_CAPTIVE_PORTAL_DETECTED: "Login Required"
|
||||||
|
STR_CAPTIVE_PORTAL_HINT_1: "Network requires browser login. On another device,"
|
||||||
|
STR_CAPTIVE_PORTAL_HINT_2: "visit the URL below to authorize, then press OK."
|
||||||
|
STR_CAPTIVE_PORTAL_DONE: "I'm authorized"
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ default_envs = default
|
|||||||
extra_configs = platformio.local.ini
|
extra_configs = platformio.local.ini
|
||||||
|
|
||||||
[crosspoint]
|
[crosspoint]
|
||||||
version = 1.38
|
version = 1.36
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
Patch min_chip_rev_full to 0 in a firmware.bin and recompute the appended
|
||||||
|
SHA-256 digest so esp_image_verify() still passes.
|
||||||
|
|
||||||
|
esp_image_header_t layout (24 bytes, all packed):
|
||||||
|
offset 0 magic (0xE9)
|
||||||
|
offset 1 segment_count
|
||||||
|
offset 2 spi_mode
|
||||||
|
offset 3 spi_speed:4 | spi_size:4
|
||||||
|
offset 4-7 entry_addr (uint32 LE)
|
||||||
|
offset 8 wp_pin
|
||||||
|
offset 9-11 spi_pin_drv[3]
|
||||||
|
offset 12-13 chip_id (uint16 LE)
|
||||||
|
offset 14 min_chip_rev (uint8, legacy)
|
||||||
|
offset 15-16 min_chip_rev_full (uint16 LE) <-- patch target
|
||||||
|
offset 17-18 max_chip_rev_full (uint16 LE)
|
||||||
|
offset 19-22 reserved[4]
|
||||||
|
offset 23 hash_appended (uint8)
|
||||||
|
|
||||||
|
When hash_appended == 1, the last 32 bytes of the image are the SHA-256 over
|
||||||
|
bytes [0 .. len-33] (everything except the digest itself). We recompute and
|
||||||
|
overwrite those 32 bytes after patching the header.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
MIN_CHIP_REV_FULL_OFFSET = 15 # byte offset of min_chip_rev_full in esp_image_header_t
|
||||||
|
HASH_LEN = 32
|
||||||
|
HASH_APPENDED_OFFSET = 23 # byte offset of hash_appended flag
|
||||||
|
|
||||||
|
|
||||||
|
def patch(path: str) -> None:
|
||||||
|
with open(path, "r+b") as f:
|
||||||
|
data = bytearray(f.read())
|
||||||
|
|
||||||
|
if data[0] != 0xE9:
|
||||||
|
print(f"ERROR: {path}: not a valid ESP image (magic=0x{data[0]:02x})", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
old = struct.unpack_from("<H", data, MIN_CHIP_REV_FULL_OFFSET)[0]
|
||||||
|
print(f"min_chip_rev_full: v{old // 100}.{old % 100} -> v0.0")
|
||||||
|
struct.pack_into("<H", data, MIN_CHIP_REV_FULL_OFFSET, 0)
|
||||||
|
|
||||||
|
hash_appended = data[HASH_APPENDED_OFFSET]
|
||||||
|
if hash_appended == 1:
|
||||||
|
# Digest covers everything except the last 32 bytes.
|
||||||
|
digest = hashlib.sha256(data[:-HASH_LEN]).digest()
|
||||||
|
data[-HASH_LEN:] = digest
|
||||||
|
print(f"SHA-256 recomputed and written to last {HASH_LEN} bytes")
|
||||||
|
else:
|
||||||
|
print("hash_appended=0, no digest to update")
|
||||||
|
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
print(f"Patched: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <firmware.bin>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
patch(sys.argv[1])
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
#include "WifiSelectionActivity.h"
|
#include "WifiSelectionActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
#include <NetworkClient.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_mac.h>
|
#include <esp_mac.h>
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@
|
|||||||
#include "activities/util/KeyboardEntryActivity.h"
|
#include "activities/util/KeyboardEntryActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/QrUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -263,6 +266,35 @@ void WifiSelectionActivity::attemptConnection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WifiSelectionActivity::checkCaptivePortal() {
|
||||||
|
// Probe a known HTTP endpoint that returns 204 on open internet.
|
||||||
|
// Captive portals intercept this and return a redirect (3xx) or 200 with a login page.
|
||||||
|
NetworkClient client;
|
||||||
|
HTTPClient http;
|
||||||
|
http.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
|
||||||
|
http.setTimeout(5000);
|
||||||
|
if (!http.begin(client, "http://connectivitycheck.gstatic.com/generate_204")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int code = http.GET();
|
||||||
|
String location = http.getLocation();
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
if (code < 0) {
|
||||||
|
LOG_DBG("WIFI", "Captive portal probe failed (connection error %d)", code);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == 204) {
|
||||||
|
return false; // Open internet, no captive portal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any redirect or unexpected 200 means a captive portal is intercepting.
|
||||||
|
captivePortalUrl = location.length() > 0 ? location.c_str() : "http://connectivitycheck.gstatic.com/generate_204";
|
||||||
|
LOG_DBG("WIFI", "Captive portal detected (HTTP %d), URL: %s", code, captivePortalUrl.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::checkConnectionStatus() {
|
void WifiSelectionActivity::checkConnectionStatus() {
|
||||||
if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) {
|
if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) {
|
||||||
return;
|
return;
|
||||||
@@ -285,6 +317,13 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
WIFI_STORE.setLastConnectedSsid(selectedSSID);
|
WIFI_STORE.setLastConnectedSsid(selectedSSID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for captive portal before declaring success
|
||||||
|
if (checkCaptivePortal()) {
|
||||||
|
state = WifiSelectionState::CAPTIVE_PORTAL;
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If we entered a new password, ask if user wants to save it
|
// If we entered a new password, ask if user wants to save it
|
||||||
// Otherwise, immediately complete so parent can start web server
|
// Otherwise, immediately complete so parent can start web server
|
||||||
if (!usedSavedPassword && !enteredPassword.empty()) {
|
if (!usedSavedPassword && !enteredPassword.empty()) {
|
||||||
@@ -404,6 +443,24 @@ void WifiSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle captive portal state - user must authorize on another device
|
||||||
|
if (state == WifiSelectionState::CAPTIVE_PORTAL) {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
|
// User says they've completed browser auth - proceed as connected
|
||||||
|
if (!usedSavedPassword && !enteredPassword.empty()) {
|
||||||
|
state = WifiSelectionState::SAVE_PROMPT;
|
||||||
|
savePromptSelection = 0;
|
||||||
|
requestUpdate();
|
||||||
|
} else {
|
||||||
|
onComplete(true);
|
||||||
|
}
|
||||||
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
WiFi.disconnect();
|
||||||
|
startWifiScan();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle connected state (should not normally be reached - connection
|
// Handle connected state (should not normally be reached - connection
|
||||||
// completes immediately)
|
// completes immediately)
|
||||||
if (state == WifiSelectionState::CONNECTED) {
|
if (state == WifiSelectionState::CONNECTED) {
|
||||||
@@ -539,6 +596,9 @@ void WifiSelectionActivity::render(RenderLock&&) {
|
|||||||
case WifiSelectionState::FORGET_PROMPT:
|
case WifiSelectionState::FORGET_PROMPT:
|
||||||
renderForgetPrompt();
|
renderForgetPrompt();
|
||||||
break;
|
break;
|
||||||
|
case WifiSelectionState::CAPTIVE_PORTAL:
|
||||||
|
renderCaptivePortal();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
@@ -719,6 +779,90 @@ void WifiSelectionActivity::renderForgetPrompt() const {
|
|||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WifiSelectionActivity::renderCaptivePortal() const {
|
||||||
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||||
|
const Rect contentRect = UITheme::getContentRect(renderer, true, false);
|
||||||
|
|
||||||
|
const int pageWidth = renderer.getScreenWidth();
|
||||||
|
const int maxWidth = pageWidth - metrics.contentSidePadding * 2;
|
||||||
|
const int lh12 = renderer.getLineHeight(UI_12_FONT_ID);
|
||||||
|
const int lh10 = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
|
const int lhSmall = renderer.getLineHeight(SMALL_FONT_ID);
|
||||||
|
const int sp = metrics.verticalSpacing;
|
||||||
|
constexpr int QR_SIZE = 320;
|
||||||
|
|
||||||
|
// Pre-compute URL line count so we can vertically centre everything
|
||||||
|
const char* url = captivePortalUrl.c_str();
|
||||||
|
int urlLineCount = 0;
|
||||||
|
{
|
||||||
|
int rem = static_cast<int>(captivePortalUrl.size());
|
||||||
|
int off = 0;
|
||||||
|
while (rem > 0) {
|
||||||
|
int lo = 1, hi = rem;
|
||||||
|
while (lo < hi) {
|
||||||
|
const int mid = (lo + hi + 1) / 2;
|
||||||
|
char tmp[512];
|
||||||
|
snprintf(tmp, sizeof(tmp), "%.*s", mid, url + off);
|
||||||
|
if (renderer.getTextWidth(SMALL_FONT_ID, tmp) <= maxWidth)
|
||||||
|
lo = mid;
|
||||||
|
else
|
||||||
|
hi = mid - 1;
|
||||||
|
}
|
||||||
|
urlLineCount++;
|
||||||
|
off += lo;
|
||||||
|
rem -= lo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int totalHeight = lh12 + sp // title
|
||||||
|
+ lh10 // hint line 1
|
||||||
|
+ lh10 + sp // hint line 2
|
||||||
|
+ QR_SIZE + sp // QR code
|
||||||
|
+ urlLineCount * lhSmall;
|
||||||
|
|
||||||
|
// contentRect covers the full screen minus button hints; subtract the header
|
||||||
|
// and sub-header that render() always draws above us.
|
||||||
|
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight;
|
||||||
|
const int contentBottom = contentRect.y + contentRect.height;
|
||||||
|
int y = contentTop + (contentBottom - contentTop - totalHeight) / 2;
|
||||||
|
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, y, tr(STR_CAPTIVE_PORTAL_DETECTED), true, EpdFontFamily::BOLD);
|
||||||
|
y += lh12 + sp;
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CAPTIVE_PORTAL_HINT_1));
|
||||||
|
y += lh10;
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CAPTIVE_PORTAL_HINT_2));
|
||||||
|
y += lh10 + sp;
|
||||||
|
|
||||||
|
const int qrX = contentRect.x + (contentRect.width - QR_SIZE) / 2;
|
||||||
|
QrUtils::drawQrCode(renderer, Rect{qrX, y, QR_SIZE, QR_SIZE}, captivePortalUrl);
|
||||||
|
y += QR_SIZE + sp;
|
||||||
|
|
||||||
|
// Split URL into as many lines as needed
|
||||||
|
int remaining = static_cast<int>(captivePortalUrl.size());
|
||||||
|
int offset = 0;
|
||||||
|
while (remaining > 0) {
|
||||||
|
int lo = 1, hi = remaining;
|
||||||
|
while (lo < hi) {
|
||||||
|
const int mid = (lo + hi + 1) / 2;
|
||||||
|
char tmp[512];
|
||||||
|
snprintf(tmp, sizeof(tmp), "%.*s", mid, url + offset);
|
||||||
|
if (renderer.getTextWidth(SMALL_FONT_ID, tmp) <= maxWidth)
|
||||||
|
lo = mid;
|
||||||
|
else
|
||||||
|
hi = mid - 1;
|
||||||
|
}
|
||||||
|
char line[512];
|
||||||
|
snprintf(line, sizeof(line), "%.*s", lo, url + offset);
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, y, line);
|
||||||
|
y += lhSmall;
|
||||||
|
offset += lo;
|
||||||
|
remaining -= lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_CAPTIVE_PORTAL_DONE), "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::onComplete(const bool connected) {
|
void WifiSelectionActivity::onComplete(const bool connected) {
|
||||||
ActivityResult result;
|
ActivityResult result;
|
||||||
result.isCancelled = !connected;
|
result.isCancelled = !connected;
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ enum class WifiSelectionState {
|
|||||||
CONNECTED, // Successfully connected
|
CONNECTED, // Successfully connected
|
||||||
SAVE_PROMPT, // Asking user if they want to save the password
|
SAVE_PROMPT, // Asking user if they want to save the password
|
||||||
CONNECTION_FAILED, // Connection failed
|
CONNECTION_FAILED, // Connection failed
|
||||||
FORGET_PROMPT // Asking user if they want to forget the network
|
FORGET_PROMPT, // Asking user if they want to forget the network
|
||||||
|
CAPTIVE_PORTAL // Connected but network requires web-based login
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,14 +88,18 @@ class WifiSelectionActivity final : public Activity {
|
|||||||
void renderSavePrompt() const;
|
void renderSavePrompt() const;
|
||||||
void renderConnectionFailed() const;
|
void renderConnectionFailed() const;
|
||||||
void renderForgetPrompt() const;
|
void renderForgetPrompt() const;
|
||||||
|
void renderCaptivePortal() const;
|
||||||
|
|
||||||
void startWifiScan();
|
void startWifiScan();
|
||||||
void processWifiScanResults();
|
void processWifiScanResults();
|
||||||
void selectNetwork(int index);
|
void selectNetwork(int index);
|
||||||
void attemptConnection();
|
void attemptConnection();
|
||||||
void checkConnectionStatus();
|
void checkConnectionStatus();
|
||||||
|
bool checkCaptivePortal();
|
||||||
std::string getSignalStrengthIndicator(int32_t rssi) const;
|
std::string getSignalStrengthIndicator(int32_t rssi) const;
|
||||||
|
|
||||||
|
std::string captivePortalUrl;
|
||||||
|
|
||||||
void onComplete(bool connected);
|
void onComplete(bool connected);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
|
|||||||
LOG_DBG("OTA", "Update check failed: %d", res);
|
LOG_DBG("OTA", "Update check failed: %d", res);
|
||||||
{
|
{
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
|
failureReason = res;
|
||||||
state = FAILED;
|
state = FAILED;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -126,6 +127,35 @@ void OtaUpdateActivity::render(RenderLock&&) {
|
|||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
} else if (state == FAILED) {
|
} else if (state == FAILED) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD);
|
||||||
|
const char* reason = "";
|
||||||
|
switch (failureReason) {
|
||||||
|
case OtaUpdater::HTTP_ERROR:
|
||||||
|
reason = "Network error (HTTP request failed)";
|
||||||
|
break;
|
||||||
|
case OtaUpdater::JSON_PARSE_ERROR:
|
||||||
|
reason = "Could not parse release info";
|
||||||
|
break;
|
||||||
|
case OtaUpdater::UPDATE_OLDER_ERROR:
|
||||||
|
reason = "Available version is not newer";
|
||||||
|
break;
|
||||||
|
case OtaUpdater::OOM_ERROR:
|
||||||
|
reason = "Out of memory";
|
||||||
|
break;
|
||||||
|
case OtaUpdater::INTERNAL_UPDATE_ERROR:
|
||||||
|
reason = "Internal update error";
|
||||||
|
break;
|
||||||
|
case OtaUpdater::NO_UPDATE:
|
||||||
|
reason = "No firmware asset found";
|
||||||
|
break;
|
||||||
|
case OtaUpdater::VALIDATE_FAILED:
|
||||||
|
reason = "Bootloader incompatible - reflash via USB with PlatformIO";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (reason[0] != '\0') {
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, top + height + metrics.verticalSpacing, reason);
|
||||||
|
}
|
||||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
} else if (state == FINISHED) {
|
} else if (state == FINISHED) {
|
||||||
@@ -140,22 +170,18 @@ void OtaUpdateActivity::loop() {
|
|||||||
// TODO @ngxson : refactor this logic later
|
// TODO @ngxson : refactor this logic later
|
||||||
if (updater.getRender()) {
|
if (updater.getRender()) {
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
updater.clearRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == WAITING_CONFIRMATION) {
|
if (state == WAITING_CONFIRMATION) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
LOG_DBG("OTA", "New update available, starting download...");
|
LOG_DBG("OTA", "New update available, starting download...");
|
||||||
{
|
const auto beginResult = updater.beginInstallUpdate();
|
||||||
RenderLock lock(*this);
|
if (beginResult != OtaUpdater::UPDATE_IN_PROGRESS) {
|
||||||
state = UPDATE_IN_PROGRESS;
|
LOG_DBG("OTA", "Update begin failed: %d", beginResult);
|
||||||
}
|
|
||||||
requestUpdateAndWait();
|
|
||||||
const auto res = updater.installUpdate();
|
|
||||||
|
|
||||||
if (res != OtaUpdater::OK) {
|
|
||||||
LOG_DBG("OTA", "Update failed: %d", res);
|
|
||||||
{
|
{
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
|
failureReason = beginResult;
|
||||||
state = FAILED;
|
state = FAILED;
|
||||||
}
|
}
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -164,9 +190,10 @@ void OtaUpdateActivity::loop() {
|
|||||||
|
|
||||||
{
|
{
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
state = FINISHED;
|
state = UPDATE_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
@@ -176,6 +203,46 @@ void OtaUpdateActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state == UPDATE_IN_PROGRESS) {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
updater.cancelUpdate();
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto res = updater.performInstallUpdateStep();
|
||||||
|
if (res == OtaUpdater::UPDATE_IN_PROGRESS) {
|
||||||
|
if (updater.getRender()) {
|
||||||
|
requestUpdate();
|
||||||
|
updater.clearRender();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == OtaUpdater::OK) {
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
state = FINISHED;
|
||||||
|
}
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == OtaUpdater::UPDATE_CANCELLED) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("OTA", "Update failed: %d", res);
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
failureReason = res;
|
||||||
|
state = FAILED;
|
||||||
|
}
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state == FAILED) {
|
if (state == FAILED) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
finish();
|
finish();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class OtaUpdateActivity : public Activity {
|
|||||||
State state = WIFI_SELECTION;
|
State state = WIFI_SELECTION;
|
||||||
unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE;
|
unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE;
|
||||||
OtaUpdater updater;
|
OtaUpdater updater;
|
||||||
|
OtaUpdater::OtaUpdaterError failureReason = OtaUpdater::OK;
|
||||||
|
|
||||||
void onWifiSelectionComplete(bool success);
|
void onWifiSelectionComplete(bool success);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <builtinFonts/all.h>
|
#include <builtinFonts/all.h>
|
||||||
|
#include <esp_ota_ops.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@@ -180,6 +181,13 @@ void setupDisplayAndFonts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
|
{
|
||||||
|
esp_ota_img_states_t otaState;
|
||||||
|
const esp_partition_t* running = esp_ota_get_running_partition();
|
||||||
|
if (esp_ota_get_state_partition(running, &otaState) == ESP_OK && otaState == ESP_OTA_IMG_PENDING_VERIFY) {
|
||||||
|
esp_ota_mark_app_valid_cancel_rollback();
|
||||||
|
}
|
||||||
|
}
|
||||||
HalSystem::begin();
|
HalSystem::begin();
|
||||||
gpio.begin();
|
gpio.begin();
|
||||||
powerManager.begin();
|
powerManager.begin();
|
||||||
|
|||||||
+148
-51
@@ -3,8 +3,12 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include "bootloader_common.h"
|
||||||
|
#include "esp_flash_partitions.h"
|
||||||
#include "esp_http_client.h"
|
#include "esp_http_client.h"
|
||||||
#include "esp_https_ota.h"
|
#include "esp_https_ota.h"
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
|
#include "esp_partition.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -28,45 +32,41 @@ esp_err_t http_client_set_header_cb(esp_http_client_handle_t http_client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t event_handler(esp_http_client_event_t* event) {
|
esp_err_t event_handler(esp_http_client_event_t* event) {
|
||||||
/* We do interested in only HTTP_EVENT_ON_DATA event only */
|
/* We are only interested in HTTP_EVENT_ON_DATA event */
|
||||||
if (event->event_id != HTTP_EVENT_ON_DATA) return ESP_OK;
|
if (event->event_id != HTTP_EVENT_ON_DATA) return ESP_OK;
|
||||||
|
|
||||||
if (!esp_http_client_is_chunked_response(event->client)) {
|
if (event->data == nullptr || event->data_len == 0) {
|
||||||
int content_len = esp_http_client_get_content_length(event->client);
|
return ESP_OK;
|
||||||
int copy_len = 0;
|
|
||||||
|
|
||||||
if (local_buf == NULL) {
|
|
||||||
/* local_buf life span is tracked by caller checkForUpdate */
|
|
||||||
local_buf = static_cast<char*>(calloc(content_len + 1, sizeof(char)));
|
|
||||||
output_len = 0;
|
|
||||||
if (local_buf == NULL) {
|
|
||||||
LOG_ERR("OTA", "HTTP Client Out of Memory Failed, Allocation %d", content_len);
|
|
||||||
return ESP_ERR_NO_MEM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
copy_len = min(event->data_len, (content_len - output_len));
|
|
||||||
if (copy_len) {
|
|
||||||
memcpy(local_buf + output_len, event->data, copy_len);
|
|
||||||
}
|
|
||||||
output_len += copy_len;
|
|
||||||
} else {
|
|
||||||
/* Code might be hits here, It happened once (for version checking) but I need more logs to handle that */
|
|
||||||
int chunked_len;
|
|
||||||
esp_http_client_get_chunk_length(event->client, &chunked_len);
|
|
||||||
LOG_DBG("OTA", "esp_http_client_is_chunked_response failed, chunked_len: %d", chunked_len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int newSize = output_len + event->data_len + 1;
|
||||||
|
char* newBuf = static_cast<char*>(realloc(local_buf, static_cast<size_t>(newSize)));
|
||||||
|
if (newBuf == nullptr) {
|
||||||
|
LOG_ERR("OTA", "HTTP Client Out of Memory Failed, Allocation %d", newSize);
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_buf = newBuf;
|
||||||
|
memcpy(local_buf + output_len, event->data, event->data_len);
|
||||||
|
output_len += event->data_len;
|
||||||
|
local_buf[output_len] = '\0';
|
||||||
|
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
} /* event_handler */
|
} /* event_handler */
|
||||||
} /* namespace */
|
} /* namespace */
|
||||||
|
|
||||||
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
||||||
|
// Reset globals so retries start clean regardless of previous outcome
|
||||||
|
local_buf = nullptr;
|
||||||
|
output_len = 0;
|
||||||
|
|
||||||
JsonDocument filter;
|
JsonDocument filter;
|
||||||
esp_err_t esp_err;
|
esp_err_t esp_err;
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
|
|
||||||
esp_http_client_config_t client_config = {
|
esp_http_client_config_t client_config = {
|
||||||
.url = latestReleaseUrl,
|
.url = latestReleaseUrl,
|
||||||
|
.timeout_ms = 10000,
|
||||||
.event_handler = event_handler,
|
.event_handler = event_handler,
|
||||||
/* Default HTTP client buffer size 512 byte only */
|
/* Default HTTP client buffer size 512 byte only */
|
||||||
.buffer_size = 8192,
|
.buffer_size = 8192,
|
||||||
@@ -199,28 +199,41 @@ bool OtaUpdater::isUpdateNewer() const {
|
|||||||
|
|
||||||
const std::string& OtaUpdater::getLatestVersion() const { return latestVersion; }
|
const std::string& OtaUpdater::getLatestVersion() const { return latestVersion; }
|
||||||
|
|
||||||
OtaUpdater::OtaUpdaterError OtaUpdater::installUpdate() {
|
void OtaUpdater::cleanupUpdate() {
|
||||||
|
if (otaHandle) {
|
||||||
|
const esp_err_t err = esp_https_ota_finish(otaHandle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
LOG_ERR("OTA", "esp_https_ota_finish on cleanup: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
otaHandle = nullptr;
|
||||||
|
}
|
||||||
|
cancelRequested = false;
|
||||||
|
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdater::cancelUpdate() {
|
||||||
|
if (otaHandle) {
|
||||||
|
cleanupUpdate();
|
||||||
|
} else {
|
||||||
|
cancelRequested = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OtaUpdater::OtaUpdaterError OtaUpdater::beginInstallUpdate() {
|
||||||
if (!isUpdateNewer()) {
|
if (!isUpdateNewer()) {
|
||||||
return UPDATE_OLDER_ERROR;
|
return UPDATE_OLDER_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_https_ota_handle_t ota_handle = NULL;
|
cleanupUpdate();
|
||||||
esp_err_t esp_err;
|
|
||||||
/* Signal for OtaUpdateActivity */
|
|
||||||
render = false;
|
render = false;
|
||||||
|
cancelRequested = false;
|
||||||
|
|
||||||
esp_http_client_config_t client_config = {
|
esp_http_client_config_t client_config = {
|
||||||
.url = otaUrl.c_str(),
|
.url = otaUrl.c_str(),
|
||||||
.timeout_ms = 30000,
|
.timeout_ms = 10000,
|
||||||
/* Default HTTP client buffer size 512 byte only
|
|
||||||
* not sufficient to handle URL redirection cases or
|
|
||||||
* parsing of large HTTP headers.
|
|
||||||
*/
|
|
||||||
.max_redirection_count = 5,
|
.max_redirection_count = 5,
|
||||||
.buffer_size = 8192,
|
.buffer_size = 8192,
|
||||||
.buffer_size_tx = 8192,
|
.buffer_size_tx = 8192,
|
||||||
/* GitHub release assets redirect to objects.githubusercontent.com CDN.
|
|
||||||
* Without max_redirection_count, esp_https_ota downloads 0 bytes and stalls. */
|
|
||||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||||
.keep_alive_enable = true,
|
.keep_alive_enable = true,
|
||||||
};
|
};
|
||||||
@@ -233,38 +246,122 @@ OtaUpdater::OtaUpdaterError OtaUpdater::installUpdate() {
|
|||||||
/* For better timing and connectivity, we disable power saving for WiFi */
|
/* For better timing and connectivity, we disable power saving for WiFi */
|
||||||
esp_wifi_set_ps(WIFI_PS_NONE);
|
esp_wifi_set_ps(WIFI_PS_NONE);
|
||||||
|
|
||||||
esp_err = esp_https_ota_begin(&ota_config, &ota_handle);
|
esp_err_t esp_err = esp_https_ota_begin(&ota_config, &otaHandle);
|
||||||
if (esp_err != ESP_OK) {
|
if (esp_err != ESP_OK) {
|
||||||
LOG_DBG("OTA", "HTTP OTA Begin Failed: %s", esp_err_to_name(esp_err));
|
LOG_DBG("OTA", "HTTP OTA Begin Failed: %s", esp_err_to_name(esp_err));
|
||||||
|
cleanupUpdate();
|
||||||
return INTERNAL_UPDATE_ERROR;
|
return INTERNAL_UPDATE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
return UPDATE_IN_PROGRESS;
|
||||||
esp_err = esp_https_ota_perform(ota_handle);
|
}
|
||||||
processedSize = esp_https_ota_get_image_len_read(ota_handle);
|
|
||||||
/* Sent signal to OtaUpdateActivity */
|
/* Writes the otadata entry to boot from the most recently flashed OTA partition,
|
||||||
render = true;
|
* bypassing esp_ota_set_boot_partition()'s image_validate() call.
|
||||||
delay(100); // TODO: should we replace this with something better?
|
* Used when esp_https_ota_finish() returns ESP_ERR_OTA_VALIDATE_FAILED on
|
||||||
} while (esp_err == ESP_ERR_HTTPS_OTA_IN_PROGRESS);
|
* unsigned Arduino builds (boot_comm efuse revision check false-positive). */
|
||||||
|
int OtaUpdater::forceSetOtaBootPartition() {
|
||||||
|
const esp_partition_t* newPartition = esp_ota_get_next_update_partition(nullptr);
|
||||||
|
if (newPartition == nullptr) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_partition_t* otaDataPartition =
|
||||||
|
esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_OTA, nullptr);
|
||||||
|
if (otaDataPartition == nullptr) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_ota_select_entry_t otadata[2];
|
||||||
|
esp_err_t err = esp_partition_read(otaDataPartition, 0, &otadata[0], sizeof(esp_ota_select_entry_t));
|
||||||
|
if (err != ESP_OK) return err;
|
||||||
|
err = esp_partition_read(otaDataPartition, otaDataPartition->erase_size, &otadata[1], sizeof(esp_ota_select_entry_t));
|
||||||
|
if (err != ESP_OK) return err;
|
||||||
|
|
||||||
|
int activeSlot = bootloader_common_get_active_otadata(otadata);
|
||||||
|
int nextSlot = (activeSlot == -1) ? 0 : (~activeSlot & 1);
|
||||||
|
|
||||||
|
uint8_t otaAppCount = 0;
|
||||||
|
while (esp_partition_find_first(ESP_PARTITION_TYPE_APP,
|
||||||
|
static_cast<esp_partition_subtype_t>(ESP_PARTITION_SUBTYPE_APP_OTA_MIN + otaAppCount),
|
||||||
|
nullptr) != nullptr) {
|
||||||
|
otaAppCount++;
|
||||||
|
}
|
||||||
|
if (otaAppCount == 0) return ESP_ERR_NOT_FOUND;
|
||||||
|
|
||||||
|
const uint8_t subTypeId = newPartition->subtype & 0x0F;
|
||||||
|
uint32_t newSeq;
|
||||||
|
if (activeSlot == -1) {
|
||||||
|
newSeq = subTypeId + 1;
|
||||||
|
} else {
|
||||||
|
uint32_t currentSeq = otadata[activeSlot].ota_seq;
|
||||||
|
newSeq = currentSeq;
|
||||||
|
while (newSeq % otaAppCount != static_cast<uint32_t>(subTypeId)) {
|
||||||
|
newSeq++;
|
||||||
|
}
|
||||||
|
if (newSeq == currentSeq) newSeq += otaAppCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
otadata[nextSlot].ota_seq = newSeq;
|
||||||
|
otadata[nextSlot].ota_state = ESP_OTA_IMG_VALID;
|
||||||
|
otadata[nextSlot].crc = bootloader_common_ota_select_crc(&otadata[nextSlot]);
|
||||||
|
|
||||||
|
err = esp_partition_erase_range(otaDataPartition, otaDataPartition->erase_size * static_cast<uint32_t>(nextSlot),
|
||||||
|
otaDataPartition->erase_size);
|
||||||
|
if (err != ESP_OK) return err;
|
||||||
|
|
||||||
|
return esp_partition_write(otaDataPartition, otaDataPartition->erase_size * static_cast<uint32_t>(nextSlot),
|
||||||
|
&otadata[nextSlot], sizeof(esp_ota_select_entry_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
OtaUpdater::OtaUpdaterError OtaUpdater::performInstallUpdateStep() {
|
||||||
|
if (cancelRequested) {
|
||||||
|
cleanupUpdate();
|
||||||
|
return UPDATE_CANCELLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!otaHandle) {
|
||||||
|
return INTERNAL_UPDATE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_err = esp_https_ota_perform(otaHandle);
|
||||||
|
processedSize = esp_https_ota_get_image_len_read(otaHandle);
|
||||||
|
render = true;
|
||||||
|
|
||||||
|
if (esp_err == ESP_ERR_HTTPS_OTA_IN_PROGRESS) {
|
||||||
|
return UPDATE_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
/* Return back to default power saving for WiFi in case of failing */
|
|
||||||
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
|
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
|
||||||
|
|
||||||
if (esp_err != ESP_OK) {
|
if (esp_err != ESP_OK) {
|
||||||
LOG_ERR("OTA", "esp_https_ota_perform Failed: %s", esp_err_to_name(esp_err));
|
LOG_ERR("OTA", "esp_https_ota_perform Failed: %s", esp_err_to_name(esp_err));
|
||||||
esp_https_ota_finish(ota_handle);
|
cleanupUpdate();
|
||||||
return HTTP_ERROR;
|
return HTTP_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!esp_https_ota_is_complete_data_received(ota_handle)) {
|
if (!esp_https_ota_is_complete_data_received(otaHandle)) {
|
||||||
LOG_ERR("OTA", "esp_https_ota_is_complete_data_received Failed: %s", esp_err_to_name(esp_err));
|
LOG_ERR("OTA", "esp_https_ota_is_complete_data_received Failed");
|
||||||
esp_https_ota_finish(ota_handle);
|
cleanupUpdate();
|
||||||
return INTERNAL_UPDATE_ERROR;
|
return INTERNAL_UPDATE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err = esp_https_ota_finish(ota_handle);
|
esp_err_t finish_err = esp_https_ota_finish(otaHandle);
|
||||||
if (esp_err != ESP_OK) {
|
otaHandle = nullptr;
|
||||||
LOG_ERR("OTA", "esp_https_ota_finish Failed: %s", esp_err_to_name(esp_err));
|
if (finish_err == ESP_ERR_OTA_VALIDATE_FAILED) {
|
||||||
|
/* Arduino unsigned builds fail boot_comm validation even though the image
|
||||||
|
* is fully written. Force the boot partition to the new OTA slot by writing
|
||||||
|
* the otadata entry directly, bypassing image_validate(). */
|
||||||
|
LOG_INF("OTA", "Validation failed (expected for unsigned Arduino builds) - forcing boot partition");
|
||||||
|
finish_err = forceSetOtaBootPartition();
|
||||||
|
if (finish_err != ESP_OK) {
|
||||||
|
LOG_ERR("OTA", "forceSetOtaBootPartition failed: %s", esp_err_to_name(finish_err));
|
||||||
|
cleanupUpdate();
|
||||||
|
return VALIDATE_FAILED;
|
||||||
|
}
|
||||||
|
} else if (finish_err != ESP_OK) {
|
||||||
|
LOG_ERR("OTA", "esp_https_ota_finish Failed: %s", esp_err_to_name(finish_err));
|
||||||
|
cleanupUpdate();
|
||||||
return INTERNAL_UPDATE_ERROR;
|
return INTERNAL_UPDATE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
// Avoid pulling in esp_https_ota.h here — it transitively includes lwip/sockets.h
|
||||||
|
// which defines INADDR_NONE as a numeric macro, conflicting with Arduino's IPAddress.h.
|
||||||
|
typedef void* esp_https_ota_handle_t;
|
||||||
|
|
||||||
class OtaUpdater {
|
class OtaUpdater {
|
||||||
bool updateAvailable = false;
|
bool updateAvailable = false;
|
||||||
std::string latestVersion;
|
std::string latestVersion;
|
||||||
@@ -11,6 +15,8 @@ class OtaUpdater {
|
|||||||
size_t processedSize = 0;
|
size_t processedSize = 0;
|
||||||
size_t totalSize = 0;
|
size_t totalSize = 0;
|
||||||
bool render = false;
|
bool render = false;
|
||||||
|
esp_https_ota_handle_t otaHandle = nullptr;
|
||||||
|
bool cancelRequested = false;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum OtaUpdaterError {
|
enum OtaUpdaterError {
|
||||||
@@ -21,6 +27,9 @@ class OtaUpdater {
|
|||||||
UPDATE_OLDER_ERROR,
|
UPDATE_OLDER_ERROR,
|
||||||
INTERNAL_UPDATE_ERROR,
|
INTERNAL_UPDATE_ERROR,
|
||||||
OOM_ERROR,
|
OOM_ERROR,
|
||||||
|
UPDATE_CANCELLED,
|
||||||
|
UPDATE_IN_PROGRESS,
|
||||||
|
VALIDATE_FAILED,
|
||||||
};
|
};
|
||||||
|
|
||||||
size_t getOtaSize() const { return otaSize; }
|
size_t getOtaSize() const { return otaSize; }
|
||||||
@@ -30,10 +39,19 @@ class OtaUpdater {
|
|||||||
size_t getTotalSize() const { return totalSize; }
|
size_t getTotalSize() const { return totalSize; }
|
||||||
|
|
||||||
bool getRender() const { return render; }
|
bool getRender() const { return render; }
|
||||||
|
void clearRender() { render = false; }
|
||||||
|
|
||||||
|
bool isUpdateInProgress() const { return otaHandle != nullptr; }
|
||||||
|
|
||||||
OtaUpdater() = default;
|
OtaUpdater() = default;
|
||||||
bool isUpdateNewer() const;
|
bool isUpdateNewer() const;
|
||||||
const std::string& getLatestVersion() const;
|
const std::string& getLatestVersion() const;
|
||||||
OtaUpdaterError checkForUpdate();
|
OtaUpdaterError checkForUpdate();
|
||||||
OtaUpdaterError installUpdate();
|
OtaUpdaterError beginInstallUpdate();
|
||||||
|
OtaUpdaterError performInstallUpdateStep();
|
||||||
|
void cancelUpdate();
|
||||||
|
void cleanupUpdate();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int forceSetOtaBootPartition();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user