mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Fixes
This commit is contained in:
parent
80a0906867
commit
6baf92581b
@ -52,36 +52,59 @@ def read_installed_tools_hash(version_directory):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def extract(archive_data, target_directory):
|
class Update:
|
||||||
"""Extracts next to target_directory and then replaces it, refusing entries outside of it"""
|
"""Stages every component next to its installed folder, so a failed download leaves the installation untouched"""
|
||||||
staging_directory = f"{target_directory}.new"
|
|
||||||
shutil.rmtree(staging_directory, ignore_errors=True)
|
def __init__(self):
|
||||||
os.makedirs(staging_directory)
|
# (target directory, staging directory, metadata path, metadata)
|
||||||
root = os.path.realpath(staging_directory)
|
self.staged = []
|
||||||
try:
|
|
||||||
with zipfile.ZipFile(io.BytesIO(archive_data)) as archive:
|
def stage(self, archive_data, target_directory, metadata_path, metadata):
|
||||||
for member in archive.infolist():
|
"""Extracts into a staging folder, refusing entries outside of it"""
|
||||||
destination = os.path.realpath(os.path.join(root, member.filename))
|
staging_directory = f"{target_directory}.new"
|
||||||
if not destination.startswith(root + os.sep):
|
|
||||||
fail(f"invalid entry in the archive: {member.filename}")
|
|
||||||
archive.extractall(root)
|
|
||||||
except zipfile.BadZipFile:
|
|
||||||
shutil.rmtree(staging_directory, ignore_errors=True)
|
shutil.rmtree(staging_directory, ignore_errors=True)
|
||||||
fail("the downloaded archive is invalid")
|
os.makedirs(staging_directory)
|
||||||
shutil.rmtree(target_directory, ignore_errors=True)
|
self.staged.append((target_directory, staging_directory, metadata_path, metadata))
|
||||||
os.replace(staging_directory, target_directory)
|
root = os.path.realpath(staging_directory)
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(io.BytesIO(archive_data)) as archive:
|
||||||
|
for member in archive.infolist():
|
||||||
|
destination = os.path.realpath(os.path.join(root, member.filename))
|
||||||
|
if not destination.startswith(root + os.sep):
|
||||||
|
fail(f"invalid entry in the archive: {member.filename}")
|
||||||
|
archive.extractall(root)
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
fail("the downloaded archive is invalid")
|
||||||
|
|
||||||
|
def discard(self):
|
||||||
|
for _, staging_directory, _, _ in self.staged:
|
||||||
|
shutil.rmtree(staging_directory, ignore_errors=True)
|
||||||
|
self.staged = []
|
||||||
|
|
||||||
def install_platform(platform_directory, archive_data, commit):
|
def commit(self):
|
||||||
extract(archive_data, os.path.join(platform_directory, "TactilitySDK"))
|
"""Replaces the installed folders, and puts the previous ones back when that fails"""
|
||||||
with open(os.path.join(platform_directory, "commit"), "w") as file:
|
replaced = []
|
||||||
file.write(commit)
|
try:
|
||||||
|
for target_directory, staging_directory, _, _ in self.staged:
|
||||||
|
backup_directory = f"{target_directory}.old"
|
||||||
def install_tools(version_directory, archive_data, sha256):
|
shutil.rmtree(backup_directory, ignore_errors=True)
|
||||||
extract(archive_data, os.path.join(version_directory, "tools"))
|
if os.path.isdir(target_directory):
|
||||||
with open(os.path.join(version_directory, "tools.sha256"), "w") as file:
|
os.replace(target_directory, backup_directory)
|
||||||
file.write(sha256)
|
replaced.append((target_directory, backup_directory))
|
||||||
|
os.replace(staging_directory, target_directory)
|
||||||
|
except OSError as error:
|
||||||
|
for target_directory, backup_directory in reversed(replaced):
|
||||||
|
shutil.rmtree(target_directory, ignore_errors=True)
|
||||||
|
if os.path.isdir(backup_directory):
|
||||||
|
os.replace(backup_directory, target_directory)
|
||||||
|
self.discard()
|
||||||
|
fail(f"failed to install the update: {error}")
|
||||||
|
for _, _, metadata_path, metadata in self.staged:
|
||||||
|
with open(metadata_path, "w") as file:
|
||||||
|
file.write(metadata)
|
||||||
|
for _, backup_directory in replaced:
|
||||||
|
shutil.rmtree(backup_directory, ignore_errors=True)
|
||||||
|
self.staged = []
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -99,22 +122,28 @@ def main():
|
|||||||
fail("the downloaded index.json is invalid")
|
fail("the downloaded index.json is invalid")
|
||||||
commit = index.get("gitCommit", "")
|
commit = index.get("gitCommit", "")
|
||||||
available = index.get("platforms", {})
|
available = index.get("platforms", {})
|
||||||
updated = False
|
update = Update()
|
||||||
tools = index.get("tools")
|
try:
|
||||||
if tools is not None and read_installed_tools_hash(version_directory) != tools.get("sha256"):
|
tools = index.get("tools")
|
||||||
print(f"Updating the tools of SDK {version}")
|
if tools is not None and read_installed_tools_hash(version_directory) != tools.get("sha256"):
|
||||||
archive_data = fetch(f"{CDN_URL}/sdk/{version}/{tools['file']}")
|
print(f"Downloading the tools of SDK {version}", flush=True)
|
||||||
if hashlib.sha256(archive_data).hexdigest() != tools["sha256"]:
|
archive_data = fetch(f"{CDN_URL}/sdk/{version}/{tools['file']}")
|
||||||
fail("the downloaded tools don't match their checksum")
|
if hashlib.sha256(archive_data).hexdigest() != tools["sha256"]:
|
||||||
install_tools(version_directory, archive_data, tools["sha256"])
|
fail("the downloaded tools don't match their checksum")
|
||||||
updated = True
|
update.stage(archive_data, os.path.join(version_directory, "tools"), os.path.join(version_directory, "tools.sha256"), tools["sha256"])
|
||||||
for platform in installed_platforms(version_directory):
|
for platform in installed_platforms(version_directory):
|
||||||
platform_directory = os.path.join(version_directory, f"platform-{platform}")
|
platform_directory = os.path.join(version_directory, f"platform-{platform}")
|
||||||
if read_commit(platform_directory) == commit or platform not in available:
|
if read_commit(platform_directory) == commit or platform not in available:
|
||||||
continue
|
continue
|
||||||
print(f"Updating SDK {version} for {platform}")
|
print(f"Downloading SDK {version} for {platform}", flush=True)
|
||||||
install_platform(platform_directory, fetch(f"{CDN_URL}/sdk/{version}/{available[platform]}"), commit)
|
archive_data = fetch(f"{CDN_URL}/sdk/{version}/{available[platform]}")
|
||||||
updated = True
|
update.stage(archive_data, os.path.join(platform_directory, "TactilitySDK"), os.path.join(platform_directory, "commit"), commit)
|
||||||
|
except SystemExit:
|
||||||
|
update.discard()
|
||||||
|
print(f"SDK {version} was not changed", file=sys.stderr)
|
||||||
|
raise
|
||||||
|
updated = len(update.staged) > 0
|
||||||
|
update.commit()
|
||||||
with open(os.path.join(version_directory, "index.json"), "wb") as file:
|
with open(os.path.join(version_directory, "index.json"), "wb") as file:
|
||||||
file.write(index_data)
|
file.write(index_data)
|
||||||
print(f"SDK {version} was updated" if updated else f"SDK {version} is up to date")
|
print(f"SDK {version} was updated" if updated else f"SDK {version} is up to date")
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Fails when the files of tt changed since a base revision, but TT_VERSION in tt.py wasn't increased.
|
"""Fails when the files of tt or its packaging changed since a base revision, but TT_VERSION in tt.py wasn't increased.
|
||||||
Usage: check-tt-version.py <base revision>, e.g. origin/main"""
|
Usage: check-tt-version.py <base revision>, e.g. origin/main"""
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
@ -10,6 +10,8 @@ import sys
|
|||||||
|
|
||||||
ROOT_PATH = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
ROOT_PATH = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||||
VERSION_PATTERN = re.compile(r'^TT_VERSION = "([^"]+)"', re.MULTILINE)
|
VERSION_PATTERN = re.compile(r'^TT_VERSION = "([^"]+)"', re.MULTILINE)
|
||||||
|
# Changes to what tt.zip contains, e.g. an added or removed file
|
||||||
|
PACKAGING_FILE_NAME = "Buildscripts/release-tt.py"
|
||||||
|
|
||||||
|
|
||||||
def load_file_names():
|
def load_file_names():
|
||||||
@ -39,7 +41,7 @@ def main():
|
|||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
base = sys.argv[1]
|
base = sys.argv[1]
|
||||||
file_names = load_file_names()
|
file_names = load_file_names()
|
||||||
diff = git("diff", "--name-only", f"{base}...HEAD", "--", *file_names)
|
diff = git("diff", "--name-only", f"{base}...HEAD", "--", *file_names, PACKAGING_FILE_NAME)
|
||||||
if diff.returncode != 0:
|
if diff.returncode != 0:
|
||||||
print(f"git diff failed: {diff.stderr.strip()}", file=sys.stderr)
|
print(f"git diff failed: {diff.stderr.strip()}", file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
## Higher Priority
|
## Higher Priority
|
||||||
|
|
||||||
- Remove screenshot app and replace with tt.py + screenshot.py: "tt screenshot [filepath]". if filepath isn't specified, save as screenshot-[yyyy-mm-dd].png
|
|
||||||
- BlueTooth as radio backend for MeshCore/Meshtastic?
|
- BlueTooth as radio backend for MeshCore/Meshtastic?
|
||||||
- ESP-NOW as radio backend. Use it for MeshCore/Meshtastic?
|
- ESP-NOW as radio backend. Use it for MeshCore/Meshtastic?
|
||||||
- Find out why the first build of an app with a clean build cache folder always fails
|
- Find out why the first build of an app with a clean build cache folder always fails
|
||||||
|
|||||||
@ -40,6 +40,7 @@ static int report_error(const char* action, error_t error) {
|
|||||||
/**
|
/**
|
||||||
* Queues an operation and waits for its result.
|
* Queues an operation and waits for its result.
|
||||||
* The subscription is made before queueing, so the operation can't finish unnoticed.
|
* The subscription is made before queueing, so the operation can't finish unnoticed.
|
||||||
|
* It times out after OPERATION_TIMEOUT without events of the operation, so a long update of many apps doesn't.
|
||||||
* @param[in] finished_type the event that ends the operation and carries its result
|
* @param[in] finished_type the event that ends the operation and carries its result
|
||||||
* @param[in] on_event called for the other events of the operation (optional)
|
* @param[in] on_event called for the other events of the operation (optional)
|
||||||
*/
|
*/
|
||||||
@ -62,7 +63,7 @@ static error_t run_and_wait(
|
|||||||
error = operation(id, &operation_id);
|
error = operation(id, &operation_id);
|
||||||
if (error == ERROR_NONE) {
|
if (error == ERROR_NONE) {
|
||||||
error = ERROR_TIMEOUT;
|
error = ERROR_TIMEOUT;
|
||||||
const TickType_t start = get_ticks();
|
TickType_t start = get_ticks();
|
||||||
TickType_t elapsed = 0;
|
TickType_t elapsed = 0;
|
||||||
bool finished = false;
|
bool finished = false;
|
||||||
while (!finished && elapsed < OPERATION_TIMEOUT) {
|
while (!finished && elapsed < OPERATION_TIMEOUT) {
|
||||||
@ -72,6 +73,7 @@ static error_t run_and_wait(
|
|||||||
if (event.operation_id != operation_id) {
|
if (event.operation_id != operation_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
start = get_ticks();
|
||||||
if (event.type == finished_type) {
|
if (event.type == finished_type) {
|
||||||
error = event.result;
|
error = event.result;
|
||||||
finished = true;
|
finished = true;
|
||||||
|
|||||||
@ -20,5 +20,8 @@ bool http_parse_status_line(const std::string& line, int& statusCode);
|
|||||||
/** Parses a header line such as "Content-Length: 42". The name is returned in lower case, the value without surrounding whitespace. */
|
/** Parses a header line such as "Content-Length: 42". The name is returned in lower case, the value without surrounding whitespace. */
|
||||||
bool http_parse_header_line(const std::string& line, std::string& name, std::string& value);
|
bool http_parse_header_line(const std::string& line, std::string& name, std::string& value);
|
||||||
|
|
||||||
|
/** Parses a Content-Length value: decimal digits only, without sign or overflow */
|
||||||
|
bool http_parse_content_length(const std::string& value, long& length);
|
||||||
|
|
||||||
/** Parses the size line of a chunk in a chunked body, e.g. "1a2b" or "1a2b;name=value" */
|
/** Parses the size line of a chunk in a chunked body, e.g. "1a2b" or "1a2b;name=value" */
|
||||||
bool http_parse_chunk_size(const std::string& line, size_t& size);
|
bool http_parse_chunk_size(const std::string& line, size_t& size);
|
||||||
|
|||||||
@ -299,6 +299,8 @@ HttpDownloadEvent http_download_run(const std::string& url, const std::string& c
|
|||||||
|
|
||||||
// -1 means the length is unknown: the body is chunked or ends when the server closes the connection
|
// -1 means the length is unknown: the body is chunked or ends when the server closes the connection
|
||||||
long contentLength = -1;
|
long contentLength = -1;
|
||||||
|
std::string contentLengthValue;
|
||||||
|
bool hasContentLength = false;
|
||||||
bool chunked = false;
|
bool chunked = false;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!connection.readLine(line)) {
|
if (!connection.readLine(line)) {
|
||||||
@ -313,7 +315,8 @@ HttpDownloadEvent http_download_run(const std::string& url, const std::string& c
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (name == "content-length") {
|
if (name == "content-length") {
|
||||||
contentLength = strtol(value.c_str(), nullptr, 10);
|
contentLengthValue = value;
|
||||||
|
hasContentLength = true;
|
||||||
} else if (name == "transfer-encoding" && value.find("chunked") != std::string::npos) {
|
} else if (name == "transfer-encoding" && value.find("chunked") != std::string::npos) {
|
||||||
chunked = true;
|
chunked = true;
|
||||||
}
|
}
|
||||||
@ -322,6 +325,10 @@ HttpDownloadEvent http_download_run(const std::string& url, const std::string& c
|
|||||||
if (statusCode < 200 || statusCode >= 300) {
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
return http_download_make_error_event("Server response is not OK", statusCode);
|
return http_download_make_error_event("Server response is not OK", statusCode);
|
||||||
}
|
}
|
||||||
|
// A chunked body ignores the Content-Length
|
||||||
|
if (hasContentLength && !chunked && !http_parse_content_length(contentLengthValue, contentLength)) {
|
||||||
|
return http_download_make_error_event("Invalid Content-Length", statusCode);
|
||||||
|
}
|
||||||
if (http_download_is_cancelled(link)) {
|
if (http_download_is_cancelled(link)) {
|
||||||
return http_download_make_cancelled_event(statusCode);
|
return http_download_make_cancelled_event(statusCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#include <http/private/http_parsing.h>
|
#include <http/private/http_parsing.h>
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <climits>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
@ -110,6 +111,22 @@ bool http_parse_header_line(const std::string& line, std::string& name, std::str
|
|||||||
return !name.empty();
|
return !name.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool http_parse_content_length(const std::string& value, long& length) {
|
||||||
|
if (value.empty() || value.find_first_not_of("0123456789") != std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
long result = 0;
|
||||||
|
for (const char c : value) {
|
||||||
|
const long digit = c - '0';
|
||||||
|
if (result > (LONG_MAX - digit) / 10) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
result = result * 10 + digit;
|
||||||
|
}
|
||||||
|
length = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool http_parse_chunk_size(const std::string& line, size_t& size) {
|
bool http_parse_chunk_size(const std::string& line, size_t& size) {
|
||||||
const std::string digits = trim(line.substr(0, line.find(';')));
|
const std::string digits = trim(line.substr(0, line.find(';')));
|
||||||
if (digits.empty() || digits.size() > sizeof(size_t) * 2) {
|
if (digits.empty() || digits.size() > sizeof(size_t) * 2) {
|
||||||
|
|||||||
@ -46,6 +46,21 @@ TEST_CASE("http_parse_header_line lowercases the name and trims the value") {
|
|||||||
CHECK_FALSE(http_parse_header_line(": empty name", name, value));
|
CHECK_FALSE(http_parse_header_line(": empty name", name, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("http_parse_content_length accepts decimal digits only") {
|
||||||
|
long length = -1;
|
||||||
|
REQUIRE(http_parse_content_length("42", length));
|
||||||
|
CHECK_EQ(length, 42);
|
||||||
|
REQUIRE(http_parse_content_length("0", length));
|
||||||
|
CHECK_EQ(length, 0);
|
||||||
|
CHECK_FALSE(http_parse_content_length("", length));
|
||||||
|
CHECK_FALSE(http_parse_content_length("-5", length));
|
||||||
|
CHECK_FALSE(http_parse_content_length("+5", length));
|
||||||
|
CHECK_FALSE(http_parse_content_length("12abc", length));
|
||||||
|
CHECK_FALSE(http_parse_content_length("1 2", length));
|
||||||
|
CHECK_FALSE(http_parse_content_length("99999999999999999999999", length));
|
||||||
|
CHECK_EQ(length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("http_parse_chunk_size reads hexadecimal sizes and ignores extensions") {
|
TEST_CASE("http_parse_chunk_size reads hexadecimal sizes and ignores extensions") {
|
||||||
size_t size = 0;
|
size_t size = 0;
|
||||||
REQUIRE(http_parse_chunk_size("1a2B", size));
|
REQUIRE(http_parse_chunk_size("1a2B", size));
|
||||||
|
|||||||
@ -27,7 +27,7 @@ public:
|
|||||||
|
|
||||||
explicit TimeZoneView(OnSelected onSelected);
|
explicit TimeZoneView(OnSelected onSelected);
|
||||||
|
|
||||||
/** Waits for a running detection, which can take a few seconds. Destroy it without holding the LVGL lock. */
|
/** Cancels a running detection and waits for its lookup to end, which can take a few seconds */
|
||||||
~TimeZoneView();
|
~TimeZoneView();
|
||||||
|
|
||||||
TimeZoneView(const TimeZoneView&) = delete;
|
TimeZoneView(const TimeZoneView&) = delete;
|
||||||
@ -59,6 +59,8 @@ private:
|
|||||||
std::unique_ptr<Timer> filterTimer;
|
std::unique_ptr<Timer> filterTimer;
|
||||||
std::unique_ptr<Thread> detectThread;
|
std::unique_ptr<Thread> detectThread;
|
||||||
std::atomic<bool> detecting = false;
|
std::atomic<bool> detecting = false;
|
||||||
|
// Set when the view is destroyed, so a running detection ends without using it
|
||||||
|
std::atomic<bool> cancelled = false;
|
||||||
|
|
||||||
// Valid while the widgets exist, guarded by the LVGL lock
|
// Valid while the widgets exist, guarded by the LVGL lock
|
||||||
lv_obj_t* textarea = nullptr;
|
lv_obj_t* textarea = nullptr;
|
||||||
|
|||||||
@ -90,6 +90,40 @@ void updatePageIndicatorWidth(void* data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A page indicator below the grid gets the width of its parent.
|
||||||
|
// It shows the page number when its dots are wider than its max width, which must be in pixels.
|
||||||
|
void updateCenteredPageIndicatorWidth(lv_obj_t* indicator) {
|
||||||
|
const int32_t margins = lv_obj_get_style_margin_left(indicator, LV_PART_MAIN) + lv_obj_get_style_margin_right(indicator, LV_PART_MAIN);
|
||||||
|
const int32_t max_width = std::max<int32_t>(0, lv_obj_get_content_width(lv_obj_get_parent(indicator)) - margins);
|
||||||
|
// Setting a style marks the layout as dirty, which would resize the parent and call this again
|
||||||
|
if (lv_obj_get_style_max_width(indicator, LV_PART_MAIN) != max_width) {
|
||||||
|
lv_obj_set_style_max_width(indicator, max_width, LV_STATE_DEFAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onCenteredPageIndicatorParentSizeChanged(lv_event_t* event) {
|
||||||
|
updateCenteredPageIndicatorWidth(static_cast<lv_obj_t*>(lv_event_get_user_data(event)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void onCenteredPageIndicatorDeleted(lv_event_t* event) {
|
||||||
|
lv_obj_t* indicator = lv_event_get_target_obj(event);
|
||||||
|
lv_obj_remove_event_cb_with_user_data(lv_obj_get_parent(indicator), onCenteredPageIndicatorParentSizeChanged, indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call after moving the page indicator below the grid */
|
||||||
|
void centerPageIndicator(lv_obj_t* indicator) {
|
||||||
|
lv_obj_set_width(indicator, LV_PCT(100));
|
||||||
|
lv_obj_add_event_cb(lv_obj_get_parent(indicator), onCenteredPageIndicatorParentSizeChanged, LV_EVENT_SIZE_CHANGED, indicator);
|
||||||
|
lv_obj_add_event_cb(indicator, onCenteredPageIndicatorDeleted, LV_EVENT_DELETE, nullptr);
|
||||||
|
updateCenteredPageIndicatorWidth(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call before moving the page indicator away from below the grid */
|
||||||
|
void uncenterPageIndicator(lv_obj_t* indicator) {
|
||||||
|
lv_obj_remove_event_cb_with_user_data(lv_obj_get_parent(indicator), onCenteredPageIndicatorParentSizeChanged, indicator);
|
||||||
|
lv_obj_remove_event_cb(indicator, onCenteredPageIndicatorDeleted);
|
||||||
|
}
|
||||||
|
|
||||||
void onBottomBarSizeChanged(lv_event_t* event) {
|
void onBottomBarSizeChanged(lv_event_t* event) {
|
||||||
lv_async_call_cancel(updatePageIndicatorWidth, lv_event_get_target_obj(event));
|
lv_async_call_cancel(updatePageIndicatorWidth, lv_event_get_target_obj(event));
|
||||||
lv_async_call(updatePageIndicatorWidth, lv_event_get_target_obj(event));
|
lv_async_call(updatePageIndicatorWidth, lv_event_get_target_obj(event));
|
||||||
@ -448,7 +482,7 @@ void TileGrid::createWidgets(lv_obj_t* parent) {
|
|||||||
createGrid(parent);
|
createGrid(parent);
|
||||||
// Centered below the grid
|
// Centered below the grid
|
||||||
pageIndicator = lvgl_page_indicator_create(parent);
|
pageIndicator = lvgl_page_indicator_create(parent);
|
||||||
lv_obj_set_width(pageIndicator, LV_PCT(100));
|
centerPageIndicator(pageIndicator);
|
||||||
// Keeps the dots off the screen's bottom edge
|
// Keeps the dots off the screen's bottom edge
|
||||||
lv_obj_set_style_margin_bottom(pageIndicator, lvgl_theme_is_compact() ? LV_DPX(2) : LV_DPX(8), LV_STATE_DEFAULT);
|
lv_obj_set_style_margin_bottom(pageIndicator, lvgl_theme_is_compact() ? LV_DPX(2) : LV_DPX(8), LV_STATE_DEFAULT);
|
||||||
finishCreate(parent);
|
finishCreate(parent);
|
||||||
@ -531,8 +565,7 @@ void TileGrid::setBarSideContainer(lv_obj_t* sideContainer) {
|
|||||||
// The page indicator is centered below the grid
|
// The page indicator is centered below the grid
|
||||||
lv_obj_set_parent(pageIndicator, barParent);
|
lv_obj_set_parent(pageIndicator, barParent);
|
||||||
lv_obj_move_to_index(pageIndicator, lv_obj_get_index(grid) + 1);
|
lv_obj_move_to_index(pageIndicator, lv_obj_get_index(grid) + 1);
|
||||||
lv_obj_set_width(pageIndicator, LV_PCT(100));
|
centerPageIndicator(pageIndicator);
|
||||||
lv_obj_remove_local_style_prop(pageIndicator, LV_STYLE_MAX_WIDTH, LV_PART_MAIN);
|
|
||||||
// Keeps the dots off the screen's bottom edge
|
// Keeps the dots off the screen's bottom edge
|
||||||
lv_obj_set_style_margin_bottom(pageIndicator, lvgl_theme_is_compact() ? 1 : 2, LV_STATE_DEFAULT);
|
lv_obj_set_style_margin_bottom(pageIndicator, lvgl_theme_is_compact() ? 1 : 2, LV_STATE_DEFAULT);
|
||||||
} else {
|
} else {
|
||||||
@ -543,9 +576,13 @@ void TileGrid::setBarSideContainer(lv_obj_t* sideContainer) {
|
|||||||
lv_obj_remove_local_style_prop(bottomBar, LV_STYLE_BG_OPA, LV_PART_MAIN);
|
lv_obj_remove_local_style_prop(bottomBar, LV_STYLE_BG_OPA, LV_PART_MAIN);
|
||||||
lv_obj_remove_local_style_prop(bottomBar, LV_STYLE_PAD_ROW, LV_PART_MAIN);
|
lv_obj_remove_local_style_prop(bottomBar, LV_STYLE_PAD_ROW, LV_PART_MAIN);
|
||||||
lv_obj_set_hidden(barSpacer, false);
|
lv_obj_set_hidden(barSpacer, false);
|
||||||
|
uncenterPageIndicator(pageIndicator);
|
||||||
lv_obj_set_parent(pageIndicator, bottomBar);
|
lv_obj_set_parent(pageIndicator, bottomBar);
|
||||||
lv_obj_set_width(pageIndicator, LV_SIZE_CONTENT);
|
lv_obj_set_width(pageIndicator, LV_SIZE_CONTENT);
|
||||||
lv_obj_remove_local_style_prop(pageIndicator, LV_STYLE_MARGIN_BOTTOM, LV_PART_MAIN);
|
lv_obj_remove_local_style_prop(pageIndicator, LV_STYLE_MARGIN_BOTTOM, LV_PART_MAIN);
|
||||||
|
// The bar's size can stay the same, which wouldn't update the max width
|
||||||
|
lv_async_call_cancel(updatePageIndicatorWidth, bottomBar);
|
||||||
|
lv_async_call(updatePageIndicatorWidth, bottomBar);
|
||||||
}
|
}
|
||||||
// The keys move through the bar where it's shown: below the statusbar's icons in the side strip, or below the grid
|
// The keys move through the bar where it's shown: below the statusbar's icons in the side strip, or below the grid
|
||||||
setGroupOrder(grid, bottomBar, sideContainer != nullptr);
|
setGroupOrder(grid, bottomBar, sideContainer != nullptr);
|
||||||
|
|||||||
@ -78,6 +78,7 @@ TimeZoneView::TimeZoneView(OnSelected onSelected) : onSelected(std::move(onSelec
|
|||||||
|
|
||||||
TimeZoneView::~TimeZoneView() {
|
TimeZoneView::~TimeZoneView() {
|
||||||
filterTimer->stop();
|
filterTimer->stop();
|
||||||
|
cancelled = true;
|
||||||
if (detectThread != nullptr) {
|
if (detectThread != nullptr) {
|
||||||
detectThread->join();
|
detectThread->join();
|
||||||
}
|
}
|
||||||
@ -268,7 +269,16 @@ void TimeZoneView::detect() {
|
|||||||
failure = "The time zone couldn't be detected. Check the internet connection and try again.";
|
failure = "The time zone couldn't be detected. Check the internet connection and try again.";
|
||||||
}
|
}
|
||||||
|
|
||||||
lvgl_lock();
|
// Waits for the LVGL lock without blocking the destructor, which can be called with the lock held
|
||||||
|
while (!lvgl_try_lock(pdMS_TO_TICKS(50))) {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cancelled) {
|
||||||
|
lvgl_unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
detecting = false;
|
detecting = false;
|
||||||
mutex.lock();
|
mutex.lock();
|
||||||
message = failure;
|
message = failure;
|
||||||
|
|||||||
40
tt.py
40
tt.py
@ -20,7 +20,7 @@ import time
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
TT_VERSION = "1.1.0"
|
TT_VERSION = "1.1.1"
|
||||||
CDN_URL = os.environ.get("TT_CDN", "https://cdn.tactilityproject.org")
|
CDN_URL = os.environ.get("TT_CDN", "https://cdn.tactilityproject.org")
|
||||||
HOME_DIRECTORY = os.path.join(os.path.expanduser("~"), ".tactility")
|
HOME_DIRECTORY = os.path.join(os.path.expanduser("~"), ".tactility")
|
||||||
INDEX_VALIDITY_SECONDS = 3600
|
INDEX_VALIDITY_SECONDS = 3600
|
||||||
@ -173,15 +173,47 @@ def install_platforms(version, version_directory, index, platforms):
|
|||||||
file.write(index.get("gitCommit", ""))
|
file.write(index.get("gitCommit", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def read_text(path):
|
||||||
|
try:
|
||||||
|
with open(path) as file:
|
||||||
|
return file.read().strip()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_installed_build(version_directory, index):
|
||||||
|
"""True when the installed tools and platforms are of the build that the index describes"""
|
||||||
|
installed_tools_hash = read_text(os.path.join(version_directory, "tools.sha256"))
|
||||||
|
if installed_tools_hash is not None and installed_tools_hash != index.get("tools", {}).get("sha256"):
|
||||||
|
return False
|
||||||
|
for name in os.listdir(version_directory):
|
||||||
|
commit = read_text(os.path.join(version_directory, name, "commit"))
|
||||||
|
if name.startswith("platform-") and commit is not None and commit != index.get("gitCommit", ""):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def prepare_sdk(version, platforms):
|
def prepare_sdk(version, platforms):
|
||||||
"""Installs what's missing of the SDK version. Installed tools and platforms are only replaced by "tt update-sdk", so they stay from the same build."""
|
"""Installs what's missing of the SDK version. Installed tools and platforms are only replaced by "tt update-sdk", so they stay from the same build."""
|
||||||
version_directory = version_path(HOME_DIRECTORY, version)
|
version_directory = version_path(HOME_DIRECTORY, version)
|
||||||
|
index = None
|
||||||
|
# "target.platforms=all" means every platform of the SDK
|
||||||
|
if platforms == ["all"]:
|
||||||
|
os.makedirs(version_directory, exist_ok=True)
|
||||||
|
index = load_index(version, version_directory)
|
||||||
|
platforms = sorted(index.get("platforms", {}))
|
||||||
has_tools = os.path.isdir(os.path.join(version_directory, "tools"))
|
has_tools = os.path.isdir(os.path.join(version_directory, "tools"))
|
||||||
missing_platforms = [platform for platform in platforms if not os.path.isdir(os.path.join(version_directory, f"platform-{platform}", "TactilitySDK"))]
|
missing_platforms = [platform for platform in platforms if not os.path.isdir(os.path.join(version_directory, f"platform-{platform}", "TactilitySDK"))]
|
||||||
if has_tools and not missing_platforms:
|
if has_tools and not missing_platforms:
|
||||||
return version_directory
|
return version_directory
|
||||||
os.makedirs(version_directory, exist_ok=True)
|
os.makedirs(version_directory, exist_ok=True)
|
||||||
index = load_index(version, version_directory)
|
if index is None:
|
||||||
|
index = load_index(version, version_directory)
|
||||||
|
# The CDN only has the latest build of a version, so a newer build can't be mixed with the installed one
|
||||||
|
if not is_installed_build(version_directory, index):
|
||||||
|
if has_tools:
|
||||||
|
fail(f"SDK {version} was published again since it was installed: run 'tt update-sdk' first")
|
||||||
|
fail(f"SDK {version} was published again since it was installed: remove {version_directory} and try again")
|
||||||
if not has_tools:
|
if not has_tools:
|
||||||
install_tools(version, version_directory, index)
|
install_tools(version, version_directory, index)
|
||||||
install_platforms(version, version_directory, index, missing_platforms)
|
install_platforms(version, version_directory, index, missing_platforms)
|
||||||
@ -255,7 +287,9 @@ def main(arguments):
|
|||||||
fail(f"tools not found: {tools_directory}")
|
fail(f"tools not found: {tools_directory}")
|
||||||
else:
|
else:
|
||||||
sdk_path = HOME_DIRECTORY
|
sdk_path = HOME_DIRECTORY
|
||||||
tools_directory = os.path.join(prepare_sdk(version, platforms), "tools")
|
# update-sdk replaces the installed build, so it only needs the tools
|
||||||
|
required_platforms = [] if command == "update-sdk" else platforms
|
||||||
|
tools_directory = os.path.join(prepare_sdk(version, required_platforms), "tools")
|
||||||
environment = tool_environment(version, sdk_path, is_local)
|
environment = tool_environment(version, sdk_path, is_local)
|
||||||
if is_help:
|
if is_help:
|
||||||
print_help(version, tools_directory)
|
print_help(version, tools_directory)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user