Merge pull request #184 from jgoguen/add-beta-release-setting
feat: Allow OTA update to beta releases
This commit is contained in:
@@ -32,14 +32,46 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract env
|
- name: Extract env
|
||||||
run: |
|
run: |
|
||||||
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
VERSION="$(python3 - <<'PY'
|
||||||
echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
|
import configparser
|
||||||
echo "RC_TAG=${GITHUB_REF_NAME#release/}-rc.${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}" >> $GITHUB_ENV
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read("platformio.ini")
|
||||||
|
version = config["crosspoint"]["version"].strip()
|
||||||
|
if version.count(".") == 1:
|
||||||
|
version = f"{version}.0"
|
||||||
|
print(version)
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
NEXT_RC="$(python3 - "$VERSION" <<'PY'
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
version = sys.argv[1]
|
||||||
|
pattern = re.compile(rf"^{re.escape(version)}-rc\.(\d+)(?:\.\d+)?$")
|
||||||
|
|
||||||
|
highest = 0
|
||||||
|
tags = subprocess.check_output(
|
||||||
|
["git", "tag", "-l", f"{version}-rc.*"],
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
for tag in tags.splitlines():
|
||||||
|
match = pattern.match(tag)
|
||||||
|
if match:
|
||||||
|
highest = max(highest, int(match.group(1)))
|
||||||
|
|
||||||
|
print(highest + 1)
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
echo "RC_TAG=${VERSION}-rc.${NEXT_RC}" >> $GITHUB_ENV
|
||||||
echo "RELEASE_NOTES_PATH=${RUNNER_TEMP}/release-notes.md" >> $GITHUB_ENV
|
echo "RELEASE_NOTES_PATH=${RUNNER_TEMP}/release-notes.md" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build CrossPoint Release Candidate
|
- name: Build CrossPoint Release Candidate
|
||||||
env:
|
env:
|
||||||
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
|
CROSSPOINT_RC_VERSION: ${{ env.RC_TAG }}
|
||||||
run: pio run -e gh_release_rc
|
run: pio run -e gh_release_rc
|
||||||
|
|
||||||
- name: Patch min_chip_rev_full to 0
|
- name: Patch min_chip_rev_full to 0
|
||||||
|
|||||||
@@ -107,12 +107,6 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, OpfCac
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const unsigned long streamMs = millis() - streamStart;
|
const unsigned long streamMs = millis() - streamStart;
|
||||||
LOG_DBG("EBP",
|
|
||||||
"content.opf stream=%lu ms, parser.write_calls=%zu, bytes=%zu, parse_buffer=%lu ms, manifest_open=%lu ms, "
|
|
||||||
"spine_open=%lu ms, guide_open=%lu ms, itemrefs=%zu, itemref_lookup=%lu ms, create_spine=%lu ms",
|
|
||||||
streamMs, opfParser.stats.writeCalls, opfParser.stats.bytesParsed, opfParser.stats.parseBufferMs,
|
|
||||||
opfParser.stats.manifestOpenMs, opfParser.stats.spineOpenMs, opfParser.stats.guideOpenMs,
|
|
||||||
opfParser.stats.itemRefCount, opfParser.stats.itemRefLookupMs, opfParser.stats.createSpineEntryMs);
|
|
||||||
|
|
||||||
// Grab data from opfParser into epub
|
// Grab data from opfParser into epub
|
||||||
bookMetadata.title = opfParser.title;
|
bookMetadata.title = opfParser.title;
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ STR_REFRESH_FREQ: "Refresh Frequency"
|
|||||||
STR_REFRESH_AFTER_IMAGE_PAGES: "Refresh after image pages"
|
STR_REFRESH_AFTER_IMAGE_PAGES: "Refresh after image pages"
|
||||||
STR_KOREADER_SYNC: "KOReader Sync"
|
STR_KOREADER_SYNC: "KOReader Sync"
|
||||||
STR_CHECK_UPDATES: "Check for updates"
|
STR_CHECK_UPDATES: "Check for updates"
|
||||||
|
STR_INCLUDE_BETA_UPDATES: "Include beta updates"
|
||||||
STR_LANGUAGE: "Language"
|
STR_LANGUAGE: "Language"
|
||||||
STR_CLEAR_READING_CACHE: "Clear Reading Cache"
|
STR_CLEAR_READING_CACHE: "Clear Reading Cache"
|
||||||
STR_USERNAME: "Username"
|
STR_USERNAME: "Username"
|
||||||
@@ -246,6 +247,7 @@ STR_NEW_VERSION: "New Version: "
|
|||||||
STR_UPDATING: "Updating..."
|
STR_UPDATING: "Updating..."
|
||||||
STR_NO_UPDATE: "No update available"
|
STR_NO_UPDATE: "No update available"
|
||||||
STR_UPDATE_FAILED: "Update failed"
|
STR_UPDATE_FAILED: "Update failed"
|
||||||
|
STR_RELEASE_METADATA_TOO_LARGE: "Release metadata too large"
|
||||||
STR_UPDATE_COMPLETE: "Update complete"
|
STR_UPDATE_COMPLETE: "Update complete"
|
||||||
STR_POWER_ON_HINT: "Press and hold power button to turn back on"
|
STR_POWER_ON_HINT: "Press and hold power button to turn back on"
|
||||||
STR_RESTARTING_HINT: "Restarting... If device does not restart, hold the power button for a few seconds."
|
STR_RESTARTING_HINT: "Restarting... If device does not restart, hold the power button for a few seconds."
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ build_flags =
|
|||||||
extends = base
|
extends = base
|
||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${base.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
|
||||||
-DENABLE_SERIAL_LOG
|
-DENABLE_SERIAL_LOG
|
||||||
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
|
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ def categorize_commit(subject: str) -> tuple[str, str]:
|
|||||||
return "other", subject
|
return "other", subject
|
||||||
|
|
||||||
commit_type = match.group("type").lower()
|
commit_type = match.group("type").lower()
|
||||||
description = match.group("description")
|
description = f'{commit_type}: {match.group("description")}'
|
||||||
if match.group("breaking"):
|
if match.group("breaking"):
|
||||||
description = f"BREAKING: {description}"
|
description = f"BREAKING: {description}"
|
||||||
|
|
||||||
|
|||||||
+115
-12
@@ -1,13 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
PlatformIO pre-build script: inject git branch into CROSSPOINT_VERSION for
|
PlatformIO pre-build script: inject Git metadata into preprocessor defines.
|
||||||
the default (dev) environment.
|
|
||||||
|
|
||||||
Results in a version string like: 1.1.0-dev+feat-koysnc-xpath
|
- The default (dev) environment gets CROSSPOINT_VERSION with a branch suffix like:
|
||||||
Release environments are unaffected; they set CROSSPOINT_VERSION in the ini.
|
1.1.0-dev+feat-koysnc-xpath
|
||||||
|
- The gh_release_rc environment gets CROSSPOINT_VERSION with an RC tag from CI metadata
|
||||||
|
when available, or a local fallback like: 1.1.0-rc+local
|
||||||
|
- All environments get CROSSPOINT_GIT_REPOSITORY, resolved from CI metadata
|
||||||
|
or local Git remotes. A safe fallback is defined in src/network/OtaUpdater.h in case
|
||||||
|
resolution here fails.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -16,18 +21,26 @@ def warn(msg):
|
|||||||
print(f'WARNING [git_branch.py]: {msg}', file=sys.stderr)
|
print(f'WARNING [git_branch.py]: {msg}', file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def get_git_branch(project_dir):
|
def run_git_command(*args: str, project_dir: str) -> str:
|
||||||
try:
|
try:
|
||||||
branch = subprocess.check_output(
|
return subprocess.check_output(
|
||||||
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
['git', *args],
|
||||||
text=True, stderr=subprocess.PIPE, cwd=project_dir
|
text=True, stderr=subprocess.PIPE, cwd=project_dir
|
||||||
).strip()
|
).strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
warn('git not found on PATH')
|
||||||
|
raise
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
warn(f'git command "git {" ".join(args)}" failed (exit {e.returncode}): {e.stderr.strip()}')
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_branch(project_dir):
|
||||||
|
try:
|
||||||
|
branch = run_git_command('rev-parse', '--abbrev-ref', 'HEAD', project_dir=project_dir)
|
||||||
# Detached HEAD — show the short SHA instead
|
# Detached HEAD — show the short SHA instead
|
||||||
if branch == 'HEAD':
|
if branch == 'HEAD':
|
||||||
branch = subprocess.check_output(
|
branch = run_git_command('rev-parse', '--short', 'HEAD', project_dir=project_dir)
|
||||||
['git', 'rev-parse', '--short', 'HEAD'],
|
|
||||||
text=True, stderr=subprocess.PIPE, cwd=project_dir
|
|
||||||
).strip()
|
|
||||||
# Strip characters that would break a C string literal
|
# Strip characters that would break a C string literal
|
||||||
return ''.join(c for c in branch if c not in '"\\')
|
return ''.join(c for c in branch if c not in '"\\')
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -41,6 +54,74 @@ def get_git_branch(project_dir):
|
|||||||
return 'unknown'
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_remotes(project_dir: str) -> list[str]:
|
||||||
|
try:
|
||||||
|
remotes = run_git_command('remote', project_dir=project_dir)
|
||||||
|
return remotes.splitlines()
|
||||||
|
except FileNotFoundError:
|
||||||
|
warn('git not found on PATH; cannot read git remotes')
|
||||||
|
return []
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
warn(f'git command failed (exit {e.returncode}): {e.stderr.strip()}; cannot read git remotes')
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
warn(f'Unexpected error reading git remotes: {e}; cannot read git remotes')
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def parse_git_repository(remote_url: str) -> str | None:
|
||||||
|
# Match strings like:
|
||||||
|
# - https://github.com/owner/repo.git
|
||||||
|
# - https://code.example.com/owner/repo
|
||||||
|
# - git+ssh://vcs.example.org:owner/repo.git
|
||||||
|
# - codeberg.org:owner/repo.git
|
||||||
|
match = re.search(r'^(?:.+)?(?:://)?[^:/]+[:/]([^/]+)/([^/]+?)(?:\.git)?$', remote_url.strip())
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
owner = match.group(1)
|
||||||
|
repo = match.group(2)
|
||||||
|
if not owner or not repo:
|
||||||
|
return None
|
||||||
|
return f'{owner}/{repo}'
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_remote_url(project_dir, remote_name):
|
||||||
|
try:
|
||||||
|
return run_git_command('remote', 'get-url', remote_name, project_dir=project_dir)
|
||||||
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_repository(project_dir):
|
||||||
|
# Other CI systems (Forgejo, Codeberg) may set GITHUB_REPOSITORY for compatibility
|
||||||
|
# with GHA. We could also check for other CI-specific env vars to expand support
|
||||||
|
# later, such as:
|
||||||
|
# - FORGEJO_REPOSITORY
|
||||||
|
# - CI_REPOSITORY_URL (this one is a full URL, likely will work with parse_git_repository)
|
||||||
|
# - BITBUCKET_REPO_FULL_NAME
|
||||||
|
ci_repository = os.environ.get('GITHUB_REPOSITORY')
|
||||||
|
if ci_repository:
|
||||||
|
return ci_repository
|
||||||
|
|
||||||
|
remotes = get_all_remotes(project_dir)
|
||||||
|
# 'origin' is most likely to be the primary remote, so always check for it first
|
||||||
|
if 'origin' in remotes:
|
||||||
|
remotes = ['origin'] + [r for r in remotes if r != 'origin']
|
||||||
|
for remote_name in remotes:
|
||||||
|
remote_url = get_git_remote_url(project_dir, remote_name)
|
||||||
|
if not remote_url:
|
||||||
|
continue
|
||||||
|
repository = parse_git_repository(remote_url)
|
||||||
|
if repository:
|
||||||
|
return repository
|
||||||
|
|
||||||
|
warn(
|
||||||
|
'Could not resolve a repository from CI metadata or git remotes; '
|
||||||
|
'falling back to compile-time default.'
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_base_version(project_dir):
|
def get_base_version(project_dir):
|
||||||
ini_path = os.path.join(project_dir, 'platformio.ini')
|
ini_path = os.path.join(project_dir, 'platformio.ini')
|
||||||
if not os.path.isfile(ini_path):
|
if not os.path.isfile(ini_path):
|
||||||
@@ -54,13 +135,35 @@ def get_base_version(project_dir):
|
|||||||
return config.get('crosspoint', 'version')
|
return config.get('crosspoint', 'version')
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_semver_patch(version: str) -> str:
|
||||||
|
version = version.strip()
|
||||||
|
if version.count('.') == 1:
|
||||||
|
return f'{version}.0'
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
def inject_version(env):
|
def inject_version(env):
|
||||||
|
project_dir = env['PROJECT_DIR']
|
||||||
|
git_repository = get_git_repository(project_dir)
|
||||||
|
if git_repository:
|
||||||
|
env.Append(CPPDEFINES=[('CROSSPOINT_GIT_REPOSITORY', f'\\"{git_repository}\\"')])
|
||||||
|
print(f'CrossPoint Git repository: {git_repository}')
|
||||||
|
|
||||||
|
# Release candidate builds use the CI-provided RC tag when available, but
|
||||||
|
# keep local gh_release_rc builds identifiable instead of leaving the
|
||||||
|
# firmware version empty.
|
||||||
|
if env['PIOENV'] == 'gh_release_rc':
|
||||||
|
base_version = normalize_semver_patch(get_base_version(project_dir))
|
||||||
|
version_string = os.environ.get('CROSSPOINT_RC_VERSION') or f'{base_version}-rc.0+local'
|
||||||
|
env.Append(CPPDEFINES=[('CROSSPOINT_VERSION', f'\\"{version_string}\\"')])
|
||||||
|
print(f'CrossPoint build version: {version_string}')
|
||||||
|
return
|
||||||
|
|
||||||
# Only applies to the dev (default) environment; release envs set the
|
# Only applies to the dev (default) environment; release envs set the
|
||||||
# version via build_flags in platformio.ini and are unaffected.
|
# version via build_flags in platformio.ini and are unaffected.
|
||||||
if env['PIOENV'] != 'default':
|
if env['PIOENV'] != 'default':
|
||||||
return
|
return
|
||||||
|
|
||||||
project_dir = env['PROJECT_DIR']
|
|
||||||
base_version = get_base_version(project_dir)
|
base_version = get_base_version(project_dir)
|
||||||
branch = get_git_branch(project_dir)
|
branch = get_git_branch(project_dir)
|
||||||
version_string = f'{base_version}-dev+{branch}'
|
version_string = f'{base_version}-dev+{branch}'
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ class CrossPointSettings {
|
|||||||
uint8_t useClock = 0;
|
uint8_t useClock = 0;
|
||||||
// Show the Weather home screen menu item (1 = enabled, 0 = hidden)
|
// Show the Weather home screen menu item (1 = enabled, 0 = hidden)
|
||||||
uint8_t useWeather = 1;
|
uint8_t useWeather = 1;
|
||||||
|
// Include release candidate builds when checking for OTA updates.
|
||||||
|
uint8_t includeBetaUpdates = 0;
|
||||||
|
|
||||||
// Configurable actions for short / double / long press on each logical button.
|
// Configurable actions for short / double / long press on each logical button.
|
||||||
// BTN_DEFAULT means "use the button's normal built-in behaviour".
|
// BTN_DEFAULT means "use the button's normal built-in behaviour".
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ inline const std::vector<SettingInfo> list = {
|
|||||||
StrId::STR_CAT_SYSTEM),
|
StrId::STR_CAT_SYSTEM),
|
||||||
SettingInfo::Toggle(StrId::STR_SHOW_FILE_EXTENSIONS, &CrossPointSettings::showFileExtensions, "showFileExtensions",
|
SettingInfo::Toggle(StrId::STR_SHOW_FILE_EXTENSIONS, &CrossPointSettings::showFileExtensions, "showFileExtensions",
|
||||||
StrId::STR_CAT_SYSTEM),
|
StrId::STR_CAT_SYSTEM),
|
||||||
|
SettingInfo::Toggle(StrId::STR_INCLUDE_BETA_UPDATES, &CrossPointSettings::includeBetaUpdates, "includeRcUpdates",
|
||||||
|
StrId::STR_CAT_SYSTEM),
|
||||||
|
|
||||||
// Will be dealt with separately , so do receive none of the main categories to be visible in the web UI but not the
|
// Will be dealt with separately , so do receive none of the main categories to be visible in the web UI but not the
|
||||||
// device UI
|
// device UI
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ void OtaUpdateActivity::render(RenderLock&&) {
|
|||||||
case OtaUpdater::OOM_ERROR:
|
case OtaUpdater::OOM_ERROR:
|
||||||
reason = "Out of memory";
|
reason = "Out of memory";
|
||||||
break;
|
break;
|
||||||
|
case OtaUpdater::METADATA_TOO_LARGE_ERROR:
|
||||||
|
reason = tr(STR_RELEASE_METADATA_TOO_LARGE);
|
||||||
|
break;
|
||||||
case OtaUpdater::INTERNAL_UPDATE_ERROR:
|
case OtaUpdater::INTERNAL_UPDATE_ERROR:
|
||||||
reason = "Internal update error";
|
reason = "Internal update error";
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ void SettingsActivity::onEnter() {
|
|||||||
|
|
||||||
bool sawReaderFontSection = false;
|
bool sawReaderFontSection = false;
|
||||||
bool insertedFontDownload = false;
|
bool insertedFontDownload = false;
|
||||||
|
bool sawIncludeBetaUpdates = false;
|
||||||
|
SettingInfo includeBetaUpdatesSetting{};
|
||||||
|
|
||||||
auto insertFontDownloadBelowFontSection = [&]() {
|
auto insertFontDownloadBelowFontSection = [&]() {
|
||||||
auto fontDownload = SettingInfo::Action(StrId::STR_FONT_DOWNLOAD, SettingAction::DownloadFonts);
|
auto fontDownload = SettingInfo::Action(StrId::STR_FONT_DOWNLOAD, SettingAction::DownloadFonts);
|
||||||
@@ -88,6 +90,11 @@ void SettingsActivity::onEnter() {
|
|||||||
enriched.enumLabels.reserve(n);
|
enriched.enumLabels.reserve(n);
|
||||||
for (uint8_t i = 0; i < n; i++) enriched.enumLabels.push_back(fontFamilyOptionLabel(i));
|
for (uint8_t i = 0; i < n; i++) enriched.enumLabels.push_back(fontFamilyOptionLabel(i));
|
||||||
}
|
}
|
||||||
|
if (enriched.nameId == StrId::STR_INCLUDE_BETA_UPDATES) {
|
||||||
|
includeBetaUpdatesSetting = enriched;
|
||||||
|
sawIncludeBetaUpdates = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const bool isReaderFontEntry =
|
const bool isReaderFontEntry =
|
||||||
enriched.category == StrId::STR_CAT_READER && (enriched.subcategory == StrId::STR_MENU_READER_FONT ||
|
enriched.category == StrId::STR_CAT_READER && (enriched.subcategory == StrId::STR_MENU_READER_FONT ||
|
||||||
enriched.submenu == StrId::STR_MENU_READER_FONT_SETTINGS);
|
enriched.submenu == StrId::STR_MENU_READER_FONT_SETTINGS);
|
||||||
@@ -150,6 +157,10 @@ void SettingsActivity::onEnter() {
|
|||||||
addToMoved(systemSettings, lastSystemSub,
|
addToMoved(systemSettings, lastSystemSub,
|
||||||
std::move(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates)
|
std::move(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates)
|
||||||
.withSubcategory(StrId::STR_MENU_SYS_SYSTEM)));
|
.withSubcategory(StrId::STR_MENU_SYS_SYSTEM)));
|
||||||
|
if (sawIncludeBetaUpdates) {
|
||||||
|
addToMoved(systemSettings, lastSystemSub,
|
||||||
|
std::move(includeBetaUpdatesSetting.withSubcategory(StrId::STR_MENU_SYS_SYSTEM)));
|
||||||
|
}
|
||||||
addToMoved(systemSettings, lastSystemSub,
|
addToMoved(systemSettings, lastSystemSub,
|
||||||
std::move(SettingInfo::Action(StrId::STR_SD_FIRMWARE_UPDATE, SettingAction::SdFirmwareUpdate)
|
std::move(SettingInfo::Action(StrId::STR_SD_FIRMWARE_UPDATE, SettingAction::SdFirmwareUpdate)
|
||||||
.withSubcategory(StrId::STR_MENU_SYS_SYSTEM)));
|
.withSubcategory(StrId::STR_MENU_SYS_SYSTEM)));
|
||||||
|
|||||||
@@ -2,21 +2,31 @@
|
|||||||
|
|
||||||
#include <climits>
|
#include <climits>
|
||||||
|
|
||||||
HttpClientStream::HttpClientStream(esp_http_client_handle_t client, int64_t contentLength)
|
HttpClientStream::HttpClientStream(esp_http_client_handle_t client, int64_t contentLength, size_t maxBytes)
|
||||||
: client(client), contentLength(contentLength) {}
|
: client(client), contentLength(contentLength), maxBytes(maxBytes) {}
|
||||||
|
|
||||||
int HttpClientStream::available() {
|
int HttpClientStream::available() {
|
||||||
if (hasError() || endOfStream) {
|
if (hasError() || endOfStream) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (contentLength < 0) {
|
if (contentLength < 0) {
|
||||||
|
if (maxBytes > 0 && bytesRead >= maxBytes) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const int64_t remaining = contentLength - bytesRead;
|
const int64_t remaining = contentLength - bytesRead;
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return remaining > INT_MAX ? INT_MAX : static_cast<int>(remaining);
|
size_t availableBytes = remaining > INT_MAX ? INT_MAX : static_cast<size_t>(remaining);
|
||||||
|
if (maxBytes > 0) {
|
||||||
|
const size_t remainingLimit = bytesRead >= maxBytes ? 0 : maxBytes - bytesRead;
|
||||||
|
if (availableBytes > remainingLimit) {
|
||||||
|
availableBytes = remainingLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<int>(availableBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
int HttpClientStream::read() {
|
int HttpClientStream::read() {
|
||||||
@@ -33,6 +43,19 @@ size_t HttpClientStream::readBytes(char* buffer, size_t length) {
|
|||||||
if (buffer == nullptr || length == 0) {
|
if (buffer == nullptr || length == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (maxBytes > 0) {
|
||||||
|
if (bytesRead >= maxBytes) {
|
||||||
|
limitExceeded = true;
|
||||||
|
endOfStream = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const size_t remainingLimit = maxBytes - bytesRead;
|
||||||
|
if (length > remainingLimit) {
|
||||||
|
length = remainingLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const int readLen = esp_http_client_read(client, buffer, static_cast<int>(length));
|
const int readLen = esp_http_client_read(client, buffer, static_cast<int>(length));
|
||||||
if (readLen == 0) {
|
if (readLen == 0) {
|
||||||
endOfStream = true;
|
endOfStream = true;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
class HttpClientStream final : public Stream {
|
class HttpClientStream final : public Stream {
|
||||||
public:
|
public:
|
||||||
explicit HttpClientStream(esp_http_client_handle_t client, int64_t contentLength);
|
explicit HttpClientStream(esp_http_client_handle_t client, int64_t contentLength, size_t maxBytes = 0);
|
||||||
|
|
||||||
int available() override;
|
int available() override;
|
||||||
int read() override;
|
int read() override;
|
||||||
@@ -19,11 +19,14 @@ class HttpClientStream final : public Stream {
|
|||||||
size_t bytesReadCount() const { return bytesRead; }
|
size_t bytesReadCount() const { return bytesRead; }
|
||||||
bool hasError() const { return lastReadError < 0; }
|
bool hasError() const { return lastReadError < 0; }
|
||||||
int lastError() const { return lastReadError; }
|
int lastError() const { return lastReadError; }
|
||||||
|
bool isLimitExceeded() const { return limitExceeded; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
esp_http_client_handle_t client;
|
esp_http_client_handle_t client;
|
||||||
int64_t contentLength;
|
int64_t contentLength;
|
||||||
|
size_t maxBytes;
|
||||||
size_t bytesRead = 0;
|
size_t bytesRead = 0;
|
||||||
int lastReadError = 0;
|
int lastReadError = 0;
|
||||||
bool endOfStream = false;
|
bool endOfStream = false;
|
||||||
|
bool limitExceeded = false;
|
||||||
};
|
};
|
||||||
|
|||||||
+190
-74
@@ -1,10 +1,13 @@
|
|||||||
#include "OtaUpdater.h"
|
#include "OtaUpdater.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
#include "HttpClientStream.h"
|
#include "HttpClientStream.h"
|
||||||
#include "bootloader_common.h"
|
#include "bootloader_common.h"
|
||||||
#include "esp_flash_partitions.h"
|
#include "esp_flash_partitions.h"
|
||||||
@@ -16,9 +19,13 @@
|
|||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char latestReleaseUrl[] = "https://api.github.com/repos/jpirnay/crosspoint-reader/releases/latest";
|
constexpr char latestReleaseUrl[] = "https://api.github.com/repos/" CROSSPOINT_GIT_REPOSITORY "/releases/latest";
|
||||||
|
constexpr char releaseListUrl[] = "https://api.github.com/repos/" CROSSPOINT_GIT_REPOSITORY "/releases?per_page=1";
|
||||||
constexpr int httpRxBufferSize = 2048;
|
constexpr int httpRxBufferSize = 2048;
|
||||||
constexpr int httpTxBufferSize = 512;
|
constexpr int httpTxBufferSize = 512;
|
||||||
|
constexpr int otaHttpMaxAttempts = 3;
|
||||||
|
constexpr unsigned long otaInitialRetryDelayMs = 1000;
|
||||||
|
constexpr size_t releaseMetadataMaxBytes = 128 * 1024;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* When esp_crt_bundle.h included, it is pointing wrong header file
|
* When esp_crt_bundle.h included, it is pointing wrong header file
|
||||||
@@ -41,6 +48,32 @@ struct HttpClientCleaner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const char* getReleaseApiUrl() { return SETTINGS.includeBetaUpdates ? releaseListUrl : latestReleaseUrl; }
|
||||||
|
|
||||||
|
void delayBeforeRetry(const char* operation, int attempt) {
|
||||||
|
const unsigned long delayMs = otaInitialRetryDelayMs << static_cast<unsigned int>(attempt - 1);
|
||||||
|
LOG_ERR("OTA", "%s failed on attempt %d/%d, retrying in %lu ms", operation, attempt, otaHttpMaxAttempts, delayMs);
|
||||||
|
delay(delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonVariantConst selectRelease(const JsonDocument& doc) {
|
||||||
|
if (doc.is<JsonArrayConst>()) {
|
||||||
|
for (JsonObjectConst release : doc.as<JsonArrayConst>()) {
|
||||||
|
if (release["draft"] | false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
return JsonVariantConst();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.is<JsonObjectConst>()) {
|
||||||
|
return doc.as<JsonObjectConst>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonVariantConst();
|
||||||
|
}
|
||||||
} /* namespace */
|
} /* namespace */
|
||||||
|
|
||||||
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
||||||
@@ -56,8 +89,10 @@ OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
|||||||
totalSize = 0;
|
totalSize = 0;
|
||||||
render = false;
|
render = false;
|
||||||
|
|
||||||
|
const char* releaseApiUrl = getReleaseApiUrl();
|
||||||
|
|
||||||
esp_http_client_config_t client_config = {
|
esp_http_client_config_t client_config = {
|
||||||
.url = latestReleaseUrl,
|
.url = releaseApiUrl,
|
||||||
.timeout_ms = 10000,
|
.timeout_ms = 10000,
|
||||||
/* Default HTTP client buffer size 512 byte only */
|
/* Default HTTP client buffer size 512 byte only */
|
||||||
.buffer_size = httpRxBufferSize,
|
.buffer_size = httpRxBufferSize,
|
||||||
@@ -66,81 +101,129 @@ OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
|||||||
.keep_alive_enable = true,
|
.keep_alive_enable = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
esp_http_client_handle_t client_handle = esp_http_client_init(&client_config);
|
if (SETTINGS.includeBetaUpdates) {
|
||||||
if (!client_handle) {
|
filter[0]["tag_name"] = true;
|
||||||
LOG_ERR("OTA", "HTTP Client Handle Failed");
|
filter[0]["draft"] = true;
|
||||||
return INTERNAL_UPDATE_ERROR;
|
filter[0]["assets"][0]["name"] = true;
|
||||||
}
|
filter[0]["assets"][0]["browser_download_url"] = true;
|
||||||
HttpClientCleaner clientCleaner = {client_handle};
|
filter[0]["assets"][0]["size"] = true;
|
||||||
|
} else {
|
||||||
esp_err = esp_http_client_set_header(client_handle, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
filter["tag_name"] = true;
|
||||||
if (esp_err != ESP_OK) {
|
filter["assets"][0]["name"] = true;
|
||||||
LOG_ERR("OTA", "esp_http_client_set_header Failed : %s", esp_err_to_name(esp_err));
|
filter["assets"][0]["browser_download_url"] = true;
|
||||||
return INTERNAL_UPDATE_ERROR;
|
filter["assets"][0]["size"] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err = esp_http_client_set_header(client_handle, "Accept", "application/vnd.github+json");
|
for (int attempt = 1; attempt <= otaHttpMaxAttempts; ++attempt) {
|
||||||
if (esp_err != ESP_OK) {
|
doc.clear();
|
||||||
LOG_ERR("OTA", "esp_http_client_set_header Failed : %s", esp_err_to_name(esp_err));
|
|
||||||
return INTERNAL_UPDATE_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err = esp_http_client_open(client_handle, 0);
|
esp_http_client_handle_t client_handle = esp_http_client_init(&client_config);
|
||||||
if (esp_err != ESP_OK) {
|
if (!client_handle) {
|
||||||
LOG_ERR("OTA", "esp_http_client_open Failed : %s", esp_err_to_name(esp_err));
|
LOG_ERR("OTA", "HTTP Client Handle Failed");
|
||||||
return HTTP_ERROR;
|
return INTERNAL_UPDATE_ERROR;
|
||||||
}
|
}
|
||||||
|
HttpClientCleaner clientCleaner = {client_handle};
|
||||||
|
|
||||||
const int64_t headerContentLength = esp_http_client_fetch_headers(client_handle);
|
esp_err = esp_http_client_set_header(client_handle, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||||
if (headerContentLength < 0) {
|
if (esp_err != ESP_OK) {
|
||||||
LOG_ERR("OTA", "esp_http_client_fetch_headers Failed : %lld", headerContentLength);
|
LOG_ERR("OTA", "esp_http_client_set_header Failed : %s", esp_err_to_name(esp_err));
|
||||||
return HTTP_ERROR;
|
return INTERNAL_UPDATE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int statusCode = esp_http_client_get_status_code(client_handle);
|
esp_err = esp_http_client_set_header(client_handle, "Accept", "application/vnd.github+json");
|
||||||
if (statusCode != 200) {
|
if (esp_err != ESP_OK) {
|
||||||
LOG_ERR("OTA", "Release metadata request failed: HTTP %d", statusCode);
|
LOG_ERR("OTA", "esp_http_client_set_header Failed : %s", esp_err_to_name(esp_err));
|
||||||
return HTTP_ERROR;
|
return INTERNAL_UPDATE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool chunked = esp_http_client_is_chunked_response(client_handle);
|
esp_err = esp_http_client_open(client_handle, 0);
|
||||||
const int64_t contentLength = chunked ? -1 : esp_http_client_get_content_length(client_handle);
|
if (esp_err != ESP_OK) {
|
||||||
LOG_DBG("OTA", "Release metadata headers: content_length=%lld chunked=%s heap=%u largest=%u", contentLength,
|
LOG_ERR("OTA", "esp_http_client_open Failed on attempt %d/%d: %s", attempt, otaHttpMaxAttempts,
|
||||||
chunked ? "yes" : "no", heap_caps_get_free_size(MALLOC_CAP_8BIT),
|
esp_err_to_name(esp_err));
|
||||||
heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
|
if (attempt < otaHttpMaxAttempts) {
|
||||||
|
delayBeforeRetry("Release metadata connection", attempt);
|
||||||
filter["tag_name"] = true;
|
continue;
|
||||||
filter["assets"][0]["name"] = true;
|
}
|
||||||
filter["assets"][0]["browser_download_url"] = true;
|
|
||||||
filter["assets"][0]["size"] = true;
|
|
||||||
|
|
||||||
HttpClientStream responseStream(client_handle, contentLength);
|
|
||||||
const DeserializationError error = deserializeJson(doc, responseStream, DeserializationOption::Filter(filter));
|
|
||||||
if (error) {
|
|
||||||
if (responseStream.hasError()) {
|
|
||||||
LOG_ERR("OTA", "HTTP stream read failed after %zu bytes: %d", responseStream.bytesReadCount(),
|
|
||||||
responseStream.lastError());
|
|
||||||
return HTTP_ERROR;
|
return HTTP_ERROR;
|
||||||
}
|
}
|
||||||
LOG_ERR("OTA", "JSON parse failed after %zu bytes: %s", responseStream.bytesReadCount(), error.c_str());
|
|
||||||
|
const int64_t headerContentLength = esp_http_client_fetch_headers(client_handle);
|
||||||
|
if (headerContentLength < 0) {
|
||||||
|
LOG_ERR("OTA", "esp_http_client_fetch_headers Failed on attempt %d/%d: %lld", attempt, otaHttpMaxAttempts,
|
||||||
|
headerContentLength);
|
||||||
|
if (attempt < otaHttpMaxAttempts) {
|
||||||
|
delayBeforeRetry("Release metadata headers", attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return HTTP_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int statusCode = esp_http_client_get_status_code(client_handle);
|
||||||
|
if (statusCode != 200) {
|
||||||
|
LOG_ERR("OTA", "Release metadata request failed on attempt %d/%d: HTTP %d", attempt, otaHttpMaxAttempts,
|
||||||
|
statusCode);
|
||||||
|
if (statusCode >= 500 && attempt < otaHttpMaxAttempts) {
|
||||||
|
delayBeforeRetry("Release metadata HTTP status", attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return HTTP_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool chunked = esp_http_client_is_chunked_response(client_handle);
|
||||||
|
const int64_t contentLength = chunked ? -1 : esp_http_client_get_content_length(client_handle);
|
||||||
|
LOG_DBG("OTA", "Release metadata headers: content_length=%lld chunked=%s heap=%u largest=%u", contentLength,
|
||||||
|
chunked ? "yes" : "no", heap_caps_get_free_size(MALLOC_CAP_8BIT),
|
||||||
|
heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
|
||||||
|
|
||||||
|
if (contentLength > static_cast<int64_t>(releaseMetadataMaxBytes)) {
|
||||||
|
LOG_ERR("OTA", "Release metadata too large: %lld bytes", contentLength);
|
||||||
|
return METADATA_TOO_LARGE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpClientStream responseStream(client_handle, contentLength, releaseMetadataMaxBytes);
|
||||||
|
const DeserializationError error = deserializeJson(doc, responseStream, DeserializationOption::Filter(filter));
|
||||||
|
if (error) {
|
||||||
|
if (responseStream.isLimitExceeded() || error == DeserializationError::NoMemory) {
|
||||||
|
LOG_ERR("OTA", "Release metadata too large after %zu bytes: %s", responseStream.bytesReadCount(),
|
||||||
|
error.c_str());
|
||||||
|
return METADATA_TOO_LARGE_ERROR;
|
||||||
|
}
|
||||||
|
if (responseStream.hasError()) {
|
||||||
|
LOG_ERR("OTA", "HTTP stream read failed on attempt %d/%d after %zu bytes: %d", attempt, otaHttpMaxAttempts,
|
||||||
|
responseStream.bytesReadCount(), responseStream.lastError());
|
||||||
|
if (attempt < otaHttpMaxAttempts) {
|
||||||
|
delayBeforeRetry("Release metadata stream", attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return HTTP_ERROR;
|
||||||
|
}
|
||||||
|
LOG_ERR("OTA", "JSON parse failed after %zu bytes: %s", responseStream.bytesReadCount(), error.c_str());
|
||||||
|
return JSON_PARSE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonVariantConst release = selectRelease(doc);
|
||||||
|
if (release.isNull()) {
|
||||||
|
LOG_ERR("OTA", "No release found in response");
|
||||||
return JSON_PARSE_ERROR;
|
return JSON_PARSE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!doc["tag_name"].is<std::string>()) {
|
if (!release["tag_name"].is<std::string>()) {
|
||||||
LOG_ERR("OTA", "No tag_name found");
|
LOG_ERR("OTA", "No tag_name found");
|
||||||
return JSON_PARSE_ERROR;
|
return JSON_PARSE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!doc["assets"].is<JsonArray>()) {
|
if (!release["assets"].is<JsonArrayConst>()) {
|
||||||
LOG_ERR("OTA", "No assets found");
|
LOG_ERR("OTA", "No assets found");
|
||||||
return JSON_PARSE_ERROR;
|
return JSON_PARSE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
latestVersion = doc["tag_name"].as<std::string>();
|
latestVersion = release["tag_name"].as<std::string>();
|
||||||
|
|
||||||
for (JsonObjectConst asset : doc["assets"].as<JsonArrayConst>()) {
|
for (JsonObjectConst asset : release["assets"].as<JsonArrayConst>()) {
|
||||||
const char* name = asset["name"] | "";
|
if (asset["name"] == "firmware.bin") {
|
||||||
if (strcmp(name, "firmware.bin") == 0) {
|
|
||||||
otaUrl = asset["browser_download_url"].as<std::string>();
|
otaUrl = asset["browser_download_url"].as<std::string>();
|
||||||
otaSize = asset["size"].as<size_t>();
|
otaSize = asset["size"].as<size_t>();
|
||||||
totalSize = otaSize;
|
totalSize = otaSize;
|
||||||
@@ -154,7 +237,7 @@ OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
|||||||
return NO_UPDATE;
|
return NO_UPDATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("OTA", "Found update: %s", latestVersion.c_str());
|
LOG_DBG("OTA", "Found %s update: %s", SETTINGS.includeBetaUpdates ? "beta" : "stable", latestVersion.c_str());
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,14 +246,24 @@ bool OtaUpdater::isUpdateNewer() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int currentMajor, currentMinor, currentPatch;
|
int currentMajor = 0, currentMinor = 0, currentPatch = 0, currentBetaRelease = 0, currentBetaBuild = 0;
|
||||||
int latestMajor, latestMinor, latestPatch;
|
int latestMajor = 0, latestMinor = 0, latestPatch = 0, latestBetaRelease = 0, latestBetaBuild = 0;
|
||||||
|
|
||||||
const auto currentVersion = CROSSPOINT_VERSION;
|
const auto currentVersion = CROSSPOINT_VERSION;
|
||||||
|
const bool currentIsBeta = strstr(currentVersion, "-rc.") != nullptr;
|
||||||
|
const bool latestIsBeta = latestVersion.find("-rc.") != std::string::npos;
|
||||||
|
|
||||||
// semantic version check (only match on 3 segments)
|
// Semantic version check with optional RC suffix. `sscanf()` will stop when
|
||||||
sscanf(latestVersion.c_str(), "%d.%d.%d", &latestMajor, &latestMinor, &latestPatch);
|
// it reaches part of the input string that doesn't match the format, so this
|
||||||
sscanf(currentVersion, "%d.%d.%d", ¤tMajor, ¤tMinor, ¤tPatch);
|
// format string works for versions like "1.31", "1.34.2", "1.35.0-rc.1", and
|
||||||
|
// "1.36.0-rc.2.5".
|
||||||
|
// This does not handle versions using the old "rc.<hash>" format, but
|
||||||
|
// considering that people will need to manually install this release or later
|
||||||
|
// to get this functionality anyway that should be fine.
|
||||||
|
sscanf(latestVersion.c_str(), "%d.%d.%d-rc.%d.%d", &latestMajor, &latestMinor, &latestPatch, &latestBetaRelease,
|
||||||
|
&latestBetaBuild);
|
||||||
|
sscanf(currentVersion, "%d.%d.%d-rc.%d.%d", ¤tMajor, ¤tMinor, ¤tPatch, ¤tBetaRelease,
|
||||||
|
¤tBetaBuild);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Compare major versions.
|
* Compare major versions.
|
||||||
@@ -191,13 +284,30 @@ bool OtaUpdater::isUpdateNewer() const {
|
|||||||
*/
|
*/
|
||||||
if (latestPatch != currentPatch) return latestPatch > currentPatch;
|
if (latestPatch != currentPatch) return latestPatch > currentPatch;
|
||||||
|
|
||||||
// If we reach here, it means all segments are equal.
|
/*
|
||||||
// One final check, if we're on an RC build (contains "-rc"), we should consider the latest version as newer even if
|
* If we reach here, the stable version segments are equal. A stable release
|
||||||
// the segments are equal, since RC builds are pre-release versions.
|
* is newer than an RC with the same version.
|
||||||
if (strstr(currentVersion, "-rc") != nullptr) {
|
*/
|
||||||
|
if (!latestIsBeta && currentIsBeta) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (latestIsBeta && !currentIsBeta) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If both versions are RCs, compare their RC release and build numbers.
|
||||||
|
*/
|
||||||
|
if (latestIsBeta && currentIsBeta) {
|
||||||
|
if (latestBetaRelease != currentBetaRelease) {
|
||||||
|
return latestBetaRelease > currentBetaRelease;
|
||||||
|
}
|
||||||
|
if (latestBetaBuild != currentBetaBuild) {
|
||||||
|
return latestBetaBuild > currentBetaBuild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,17 +357,23 @@ OtaUpdater::OtaUpdaterError OtaUpdater::beginInstallUpdate() {
|
|||||||
.http_client_init_cb = http_client_set_header_cb,
|
.http_client_init_cb = http_client_set_header_cb,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* For better timing and connectivity, we disable power saving for WiFi */
|
for (int attempt = 1; attempt <= otaHttpMaxAttempts; ++attempt) {
|
||||||
esp_wifi_set_ps(WIFI_PS_NONE);
|
/* For better timing and connectivity, we disable power saving for WiFi */
|
||||||
|
esp_wifi_set_ps(WIFI_PS_NONE);
|
||||||
|
|
||||||
esp_err_t esp_err = esp_https_ota_begin(&ota_config, &otaHandle);
|
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));
|
return UPDATE_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERR("OTA", "HTTP OTA Begin Failed on attempt %d/%d: %s", attempt, otaHttpMaxAttempts, esp_err_to_name(esp_err));
|
||||||
cleanupUpdate();
|
cleanupUpdate();
|
||||||
return INTERNAL_UPDATE_ERROR;
|
if (attempt < otaHttpMaxAttempts) {
|
||||||
|
delayBeforeRetry("Firmware OTA connection", attempt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UPDATE_IN_PROGRESS;
|
return INTERNAL_UPDATE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Writes the otadata entry to boot from the most recently flashed OTA partition,
|
/* Writes the otadata entry to boot from the most recently flashed OTA partition,
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#ifndef CROSSPOINT_GIT_REPOSITORY
|
||||||
|
#define CROSSPOINT_GIT_REPOSITORY "jpirnay/crosspoint-reader"
|
||||||
|
#endif
|
||||||
|
|
||||||
// Avoid pulling in esp_https_ota.h here — it transitively includes lwip/sockets.h
|
// 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.
|
// which defines INADDR_NONE as a numeric macro, conflicting with Arduino's IPAddress.h.
|
||||||
typedef void* esp_https_ota_handle_t;
|
typedef void* esp_https_ota_handle_t;
|
||||||
@@ -27,6 +31,7 @@ class OtaUpdater {
|
|||||||
UPDATE_OLDER_ERROR,
|
UPDATE_OLDER_ERROR,
|
||||||
INTERNAL_UPDATE_ERROR,
|
INTERNAL_UPDATE_ERROR,
|
||||||
OOM_ERROR,
|
OOM_ERROR,
|
||||||
|
METADATA_TOO_LARGE_ERROR,
|
||||||
UPDATE_CANCELLED,
|
UPDATE_CANCELLED,
|
||||||
UPDATE_IN_PROGRESS,
|
UPDATE_IN_PROGRESS,
|
||||||
VALIDATE_FAILED,
|
VALIDATE_FAILED,
|
||||||
|
|||||||
Reference in New Issue
Block a user