Add OTA updates
This commit is contained in:
parent
b39ce22e54
commit
f55eef50f1
@ -39,6 +39,7 @@ lib_deps =
|
|||||||
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
||||||
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
||||||
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
||||||
|
ArduinoJson
|
||||||
|
|
||||||
[env:default]
|
[env:default]
|
||||||
extends = base
|
extends = base
|
||||||
|
|||||||
247
src/activities/settings/OtaUpdateActivity.cpp
Normal file
247
src/activities/settings/OtaUpdateActivity.cpp
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
#include "OtaUpdateActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <InputManager.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "network/OtaUpdater.h"
|
||||||
|
|
||||||
|
void OtaUpdateActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<OtaUpdateActivity*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
|
||||||
|
exitActivity();
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
Serial.printf("[%lu] [OTA] WiFi connection failed, exiting\n", millis());
|
||||||
|
goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [OTA] WiFi connected, checking for update\n", millis());
|
||||||
|
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = CHECKING_FOR_UPDATE;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
const auto res = updater.checkForUpdate();
|
||||||
|
if (res != OtaUpdater::OK) {
|
||||||
|
Serial.printf("[%lu] [OTA] Update check failed: %d\n", millis(), res);
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = FAILED;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updater.isUpdateNewer()) {
|
||||||
|
Serial.printf("[%lu] [OTA] No new update available\n", millis());
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = NO_UPDATE;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = WAITING_CONFIRMATION;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::onEnter() {
|
||||||
|
ActivityWithSubactivity::onEnter();
|
||||||
|
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
xTaskCreate(&OtaUpdateActivity::taskTrampoline, "OtaUpdateActivityTask",
|
||||||
|
2048, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority
|
||||||
|
&displayTaskHandle // Task handle
|
||||||
|
);
|
||||||
|
|
||||||
|
// Turn on WiFi immediately
|
||||||
|
Serial.printf("[%lu] [OTA] Turning on WiFi...\n", millis());
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
|
// Launch WiFi selection subactivity
|
||||||
|
Serial.printf("[%lu] [OTA] Launching WifiSelectionActivity...\n", millis());
|
||||||
|
enterNewActivity(new WifiSelectionActivity(renderer, inputManager,
|
||||||
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::onExit() {
|
||||||
|
ActivityWithSubactivity::onExit();
|
||||||
|
|
||||||
|
// Turn off wifi
|
||||||
|
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
||||||
|
delay(100); // Allow disconnect frame to be sent
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
delay(100); // Allow WiFi hardware to fully power down
|
||||||
|
|
||||||
|
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
render();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::render() {
|
||||||
|
if (subActivity) {
|
||||||
|
// Subactivity handles its own rendering
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float updaterProgress = 0;
|
||||||
|
if (state == UPDATE_IN_PROGRESS) {
|
||||||
|
Serial.printf("[%lu] [OTA] Update progress: %d / %d\n", millis(), updater.processedSize, updater.totalSize);
|
||||||
|
updaterProgress = static_cast<float>(updater.processedSize) / static_cast<float>(updater.totalSize);
|
||||||
|
// Only update every 2% at the most
|
||||||
|
if (static_cast<int>(updaterProgress * 50) == lastUpdaterPercentage / 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastUpdaterPercentage = static_cast<int>(updaterProgress * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
|
||||||
|
renderer.clearScreen();
|
||||||
|
renderer.drawCenteredText(READER_FONT_ID, 10, "Update", true, BOLD);
|
||||||
|
|
||||||
|
if (state == CHECKING_FOR_UPDATE) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 300, "Checking for update...", true, BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == WAITING_CONFIRMATION) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 200, "New update available!", true, BOLD);
|
||||||
|
renderer.drawText(UI_FONT_ID, 20, 250, "Current Version: " CROSSPOINT_VERSION);
|
||||||
|
renderer.drawText(UI_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str());
|
||||||
|
|
||||||
|
renderer.drawRect(25, pageHeight - 40, 106, 40);
|
||||||
|
renderer.drawText(UI_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_FONT_ID, "Cancel")) / 2, pageHeight - 35,
|
||||||
|
"Cancel");
|
||||||
|
|
||||||
|
renderer.drawRect(130, pageHeight - 40, 106, 40);
|
||||||
|
renderer.drawText(UI_FONT_ID, 130 + (105 - renderer.getTextWidth(UI_FONT_ID, "Update")) / 2, pageHeight - 35,
|
||||||
|
"Update");
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == UPDATE_IN_PROGRESS) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 310, "Updating...", true, BOLD);
|
||||||
|
renderer.drawRect(20, 350, pageWidth - 40, 50);
|
||||||
|
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42);
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 420, (std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str());
|
||||||
|
renderer.drawCenteredText(
|
||||||
|
UI_FONT_ID, 440, (std::to_string(updater.processedSize) + " / " + std::to_string(updater.totalSize)).c_str());
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == NO_UPDATE) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 300, "No update available", true, BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FAILED) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 300, "Update failed", true, BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FINISHED) {
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 300, "Update complete", true, BOLD);
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 350, "Press and hold power button to turn back on");
|
||||||
|
renderer.displayBuffer();
|
||||||
|
state = SHUTTING_DOWN;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdateActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == WAITING_CONFIRMATION) {
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
|
Serial.printf("[%lu] [OTA] New update available, starting download...\n", millis());
|
||||||
|
int lastUpdatePercentage = 0;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = UPDATE_IN_PROGRESS;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
const auto res = updater.installUpdate([this, &lastUpdatePercentage](const size_t progress, const size_t total) {
|
||||||
|
// Only trigger display updates every 2% at most
|
||||||
|
updateRequired = true;
|
||||||
|
// vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res != OtaUpdater::OK) {
|
||||||
|
Serial.printf("[%lu] [OTA] Update failed: %d\n", millis(), res);
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = FAILED;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
state = FINISHED;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FAILED) {
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == NO_UPDATE) {
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == SHUTTING_DOWN) {
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/activities/settings/OtaUpdateActivity.h
Normal file
40
src/activities/settings/OtaUpdateActivity.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "network/OtaUpdater.h"
|
||||||
|
|
||||||
|
class OtaUpdateActivity : public ActivityWithSubactivity {
|
||||||
|
enum State {
|
||||||
|
WIFI_SELECTION,
|
||||||
|
CHECKING_FOR_UPDATE,
|
||||||
|
WAITING_CONFIRMATION,
|
||||||
|
UPDATE_IN_PROGRESS,
|
||||||
|
NO_UPDATE,
|
||||||
|
FAILED,
|
||||||
|
FINISHED,
|
||||||
|
SHUTTING_DOWN
|
||||||
|
};
|
||||||
|
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
bool updateRequired = false;
|
||||||
|
const std::function<void()> goBack;
|
||||||
|
State state = WIFI_SELECTION;
|
||||||
|
unsigned int lastUpdaterPercentage = 111; // Can't initialize this to 0 or the first render doesn't happen
|
||||||
|
OtaUpdater updater;
|
||||||
|
|
||||||
|
void onWifiSelectionComplete(bool success);
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render();
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit OtaUpdateActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& goBack)
|
||||||
|
: ActivityWithSubactivity("OtaUpdate", renderer, inputManager), goBack(goBack), updater() {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
@ -4,16 +4,19 @@
|
|||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
#include "OtaUpdateActivity.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
// Define the static settings list
|
// Define the static settings list
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int settingsCount = 3;
|
constexpr int settingsCount = 4;
|
||||||
const SettingInfo settingsList[settingsCount] = {
|
const SettingInfo settingsList[settingsCount] = {
|
||||||
// Should match with SLEEP_SCREEN_MODE
|
// Should match with SLEEP_SCREEN_MODE
|
||||||
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
||||||
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}},
|
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}},
|
||||||
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}}};
|
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}},
|
||||||
|
{"Check for updates", SettingType::ACTION, nullptr, {}},
|
||||||
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void SettingsActivity::taskTrampoline(void* param) {
|
void SettingsActivity::taskTrampoline(void* param) {
|
||||||
@ -41,7 +44,7 @@ void SettingsActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SettingsActivity::onExit() {
|
void SettingsActivity::onExit() {
|
||||||
Activity::onExit();
|
ActivityWithSubactivity::onExit();
|
||||||
|
|
||||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
@ -54,6 +57,11 @@ void SettingsActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SettingsActivity::loop() {
|
void SettingsActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle actions with early return
|
// Handle actions with early return
|
||||||
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
toggleCurrentSetting();
|
toggleCurrentSetting();
|
||||||
@ -81,7 +89,7 @@ void SettingsActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SettingsActivity::toggleCurrentSetting() const {
|
void SettingsActivity::toggleCurrentSetting() {
|
||||||
// Validate index
|
// Validate index
|
||||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
||||||
return;
|
return;
|
||||||
@ -96,6 +104,16 @@ void SettingsActivity::toggleCurrentSetting() const {
|
|||||||
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
|
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
|
||||||
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
|
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
|
||||||
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
|
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
|
||||||
|
} else if (setting.type == SettingType::ACTION) {
|
||||||
|
if (std::string(setting.name) == "Check for updates") {
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
exitActivity();
|
||||||
|
enterNewActivity(new OtaUpdateActivity(renderer, inputManager, [this] {
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
}));
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Only toggle if it's a toggle type and has a value pointer
|
// Only toggle if it's a toggle type and has a value pointer
|
||||||
return;
|
return;
|
||||||
@ -107,7 +125,7 @@ void SettingsActivity::toggleCurrentSetting() const {
|
|||||||
|
|
||||||
void SettingsActivity::displayTaskLoop() {
|
void SettingsActivity::displayTaskLoop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (updateRequired) {
|
if (updateRequired && !subActivity) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
render();
|
render();
|
||||||
@ -152,6 +170,8 @@ void SettingsActivity::render() const {
|
|||||||
|
|
||||||
// Draw help text
|
// Draw help text
|
||||||
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to toggle, BACK to save & exit");
|
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to toggle, BACK to save & exit");
|
||||||
|
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
|
||||||
|
pageHeight - 30, CROSSPOINT_VERSION);
|
||||||
|
|
||||||
// Always use standard refresh for settings screen
|
// Always use standard refresh for settings screen
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -3,16 +3,15 @@
|
|||||||
#include <freertos/semphr.h>
|
#include <freertos/semphr.h>
|
||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
|
|
||||||
enum class SettingType { TOGGLE, ENUM };
|
enum class SettingType { TOGGLE, ENUM, ACTION };
|
||||||
|
|
||||||
// Structure to hold setting information
|
// Structure to hold setting information
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
@ -22,7 +21,7 @@ struct SettingInfo {
|
|||||||
std::vector<std::string> enumValues;
|
std::vector<std::string> enumValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsActivity final : public Activity {
|
class SettingsActivity final : public ActivityWithSubactivity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
@ -32,11 +31,11 @@ class SettingsActivity final : public Activity {
|
|||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void render() const;
|
void render() const;
|
||||||
void toggleCurrentSetting() const;
|
void toggleCurrentSetting();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
|
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
|
||||||
: Activity("Settings", renderer, inputManager), onGoHome(onGoHome) {}
|
: ActivityWithSubactivity("Settings", renderer, inputManager), onGoHome(onGoHome) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
165
src/network/OtaUpdater.cpp
Normal file
165
src/network/OtaUpdater.cpp
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
#include "OtaUpdater.h"
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <Update.h>
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr char latestReleaseUrl[] = "https://api.github.com/repos/daveallie/crosspoint-reader/releases/latest";
|
||||||
|
}
|
||||||
|
|
||||||
|
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
||||||
|
const std::unique_ptr<WiFiClientSecure> client(new WiFiClientSecure);
|
||||||
|
client->setInsecure();
|
||||||
|
HTTPClient http;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [OTA] Fetching: %s\n", millis(), latestReleaseUrl);
|
||||||
|
|
||||||
|
http.begin(*client, latestReleaseUrl);
|
||||||
|
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||||
|
|
||||||
|
const int httpCode = http.GET();
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
Serial.printf("[%lu] [OTA] HTTP error: %d\n", millis(), httpCode);
|
||||||
|
http.end();
|
||||||
|
return HTTP_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
const DeserializationError error = deserializeJson(doc, http.getStream());
|
||||||
|
http.end();
|
||||||
|
if (error) {
|
||||||
|
Serial.printf("[%lu] [OTA] JSON parse failed: %s\n", millis(), error.c_str());
|
||||||
|
return JSON_PARSE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["tag_name"].is<std::string>()) {
|
||||||
|
Serial.printf("[%lu] [OTA] No tag_name found\n", millis());
|
||||||
|
return JSON_PARSE_ERROR;
|
||||||
|
}
|
||||||
|
if (!doc["assets"].is<JsonArray>()) {
|
||||||
|
Serial.printf("[%lu] [OTA] No assets found\n", millis());
|
||||||
|
return JSON_PARSE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
latestVersion = doc["tag_name"].as<std::string>();
|
||||||
|
|
||||||
|
for (int i = 0; i < doc["assets"].size(); i++) {
|
||||||
|
if (doc["assets"][i]["name"] == "firmware.bin") {
|
||||||
|
otaUrl = doc["assets"][i]["browser_download_url"].as<std::string>();
|
||||||
|
otaSize = doc["assets"][i]["size"].as<size_t>();
|
||||||
|
totalSize = otaSize;
|
||||||
|
updateAvailable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updateAvailable) {
|
||||||
|
Serial.printf("[%lu] [OTA] No firmware.bin asset found\n", millis());
|
||||||
|
return NO_UPDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [OTA] Found update: %s\n", millis(), latestVersion.c_str());
|
||||||
|
return OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OtaUpdater::isUpdateNewer() {
|
||||||
|
if (!updateAvailable || latestVersion.empty() || latestVersion == CROSSPOINT_VERSION) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// semantic version check (only match on 3 segments)
|
||||||
|
const auto updateMajor = stoi(latestVersion.substr(0, latestVersion.find('.')));
|
||||||
|
const auto updateMinor = stoi(
|
||||||
|
latestVersion.substr(latestVersion.find('.') + 1, latestVersion.find_last_of('.') - latestVersion.find('.') - 1));
|
||||||
|
const auto updatePatch = stoi(latestVersion.substr(latestVersion.find_last_of('.') + 1));
|
||||||
|
|
||||||
|
std::string currentVersion = CROSSPOINT_VERSION;
|
||||||
|
const auto currentMajor = stoi(currentVersion.substr(0, currentVersion.find('.')));
|
||||||
|
const auto currentMinor = stoi(currentVersion.substr(
|
||||||
|
currentVersion.find('.') + 1, currentVersion.find_last_of('.') - currentVersion.find('.') - 1));
|
||||||
|
const auto currentPatch = stoi(currentVersion.substr(currentVersion.find_last_of('.') + 1));
|
||||||
|
|
||||||
|
if (updateMajor > currentMajor) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (updateMajor < currentMajor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateMinor > currentMinor) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (updateMinor < currentMinor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatePatch > currentPatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& OtaUpdater::getLatestVersion() { return latestVersion; }
|
||||||
|
|
||||||
|
OtaUpdater::OtaUpdaterError OtaUpdater::installUpdate(const std::function<void(size_t, size_t)>& onProgress) {
|
||||||
|
if (!isUpdateNewer()) {
|
||||||
|
return UPDATE_OLDER_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::unique_ptr<WiFiClientSecure> client(new WiFiClientSecure);
|
||||||
|
client->setInsecure();
|
||||||
|
HTTPClient http;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [OTA] Fetching: %s\n", millis(), otaUrl.c_str());
|
||||||
|
|
||||||
|
http.begin(*client, otaUrl.c_str());
|
||||||
|
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||||
|
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||||
|
const int httpCode = http.GET();
|
||||||
|
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
Serial.printf("[%lu] [OTA] Download failed: %d\n", millis(), httpCode);
|
||||||
|
return HTTP_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get length and stream
|
||||||
|
const size_t contentLength = http.getSize();
|
||||||
|
|
||||||
|
if (contentLength != otaSize) {
|
||||||
|
Serial.printf("[%lu] [OTA] Invalid content length\n", millis());
|
||||||
|
return HTTP_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Begin the ESP-IDF Update process
|
||||||
|
if (!Update.begin(otaSize)) {
|
||||||
|
Serial.printf("[%lu] [OTA] Not enough space. Error: %s\n", millis(), Update.errorString());
|
||||||
|
return INTERNAL_UPDATE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->totalSize = otaSize;
|
||||||
|
Serial.printf("[%lu] [OTA] Update started\n", millis());
|
||||||
|
Update.onProgress([this, onProgress](const size_t progress, const size_t total) {
|
||||||
|
this->processedSize = progress;
|
||||||
|
this->totalSize = total;
|
||||||
|
onProgress(progress, total);
|
||||||
|
});
|
||||||
|
const size_t written = Update.writeStream(*client);
|
||||||
|
|
||||||
|
if (written == otaSize) {
|
||||||
|
Serial.printf("[%lu] [OTA] Successfully written %u bytes\n", millis(), written);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [OTA] Written only %u/%u bytes. Error: %s\n", millis(), written, contentLength,
|
||||||
|
Update.errorString());
|
||||||
|
return INTERNAL_UPDATE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Update.end() && Update.isFinished()) {
|
||||||
|
Serial.printf("[%lu] [OTA] Update complete\n", millis());
|
||||||
|
return OK;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [OTA] Error Occurred: %s\n", millis(), Update.errorString());
|
||||||
|
return INTERNAL_UPDATE_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/network/OtaUpdater.h
Normal file
30
src/network/OtaUpdater.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class OtaUpdater {
|
||||||
|
bool updateAvailable = false;
|
||||||
|
std::string latestVersion;
|
||||||
|
std::string otaUrl;
|
||||||
|
size_t otaSize;
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum OtaUpdaterError {
|
||||||
|
OK = 0,
|
||||||
|
NO_UPDATE,
|
||||||
|
HTTP_ERROR,
|
||||||
|
JSON_PARSE_ERROR,
|
||||||
|
UPDATE_OLDER_ERROR,
|
||||||
|
INTERNAL_UPDATE_ERROR,
|
||||||
|
OOM_ERROR,
|
||||||
|
};
|
||||||
|
size_t processedSize = 0;
|
||||||
|
size_t totalSize = 0;
|
||||||
|
|
||||||
|
OtaUpdater() = default;
|
||||||
|
bool isUpdateNewer();
|
||||||
|
const std::string& getLatestVersion();
|
||||||
|
OtaUpdaterError checkForUpdate();
|
||||||
|
OtaUpdaterError installUpdate(const std::function<void(size_t, size_t)>& onProgress);
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user