Implement http-module (#637)
Create http-module and implement it in App Hub and App Hub Details apps.
This commit is contained in:
committed by
GitHub
parent
d2442bedb4
commit
bcbf18e363
@@ -174,6 +174,7 @@ def main():
|
||||
add_module(target_path, "app-module")
|
||||
add_module(target_path, "crypt-module")
|
||||
add_module(target_path, "gps-module")
|
||||
add_module(target_path, "http-module")
|
||||
add_module(target_path, "lvgl-module")
|
||||
add_module(target_path, "lvgl-window-manager-module")
|
||||
add_module(target_path, "service-module")
|
||||
|
||||
@@ -11,6 +11,12 @@ CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH=y
|
||||
# EmbedTLS
|
||||
# Use TLS 1.2 because 1.3 conflicts with MbedTLS dynamic buffer
|
||||
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
|
||||
# Frees TLS IN/OUT buffers between reads/writes instead of holding them for the whole session.
|
||||
# Internal-RAM-only devices need this: mbedtls_ssl_setup() otherwise fails with ALLOC_FAILED
|
||||
# (-0x7F00).
|
||||
# Keep MBEDTLS_SSL_IN_CONTENT_LEN at its 16384 default: servers may send TLS records up to that
|
||||
# size, and a smaller buffer fails mid-transfer with MBEDTLS_ERR_SSL_INVALID_RECORD (-0x7200).
|
||||
CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
|
||||
# LVGL
|
||||
CONFIG_LV_USE_USER_DATA=y
|
||||
CONFIG_LV_USE_FS_STDIO=y
|
||||
|
||||
@@ -101,6 +101,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
|
||||
add_subdirectory(Modules/lvgl-module)
|
||||
add_subdirectory(Modules/crypt-module)
|
||||
add_subdirectory(Modules/gps-module)
|
||||
add_subdirectory(Modules/http-module)
|
||||
add_subdirectory(Modules/service-module)
|
||||
add_subdirectory(Modules/app-module)
|
||||
add_subdirectory(Modules/lvgl-window-manager-module)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw
|
||||
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
|
||||
MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQw
|
||||
MDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZp
|
||||
Y2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARvzTr+
|
||||
Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4mMTG6J7/EFmP
|
||||
LCaY9eYmJbsPAvpWo4H+MIH7MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr
|
||||
BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU
|
||||
kHeSNWfE/6jMqeZ72YB5e8yT+TgwHwYDVR0jBBgwFoAUgEzW63T/STaj1dj8tT7F
|
||||
avCUHYwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAChhhodHRwOi8vaS5wa2ku
|
||||
Z29vZy9yNC5jcnQwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2MucGtpLmdvb2cv
|
||||
ci9yNC5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwCgYIKoZIzj0EAwMDaAAwZQIx
|
||||
AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V
|
||||
sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN
|
||||
7uJW
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw
|
||||
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
|
||||
MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQw
|
||||
MDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZp
|
||||
Y2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARvzTr+
|
||||
Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4mMTG6J7/EFmP
|
||||
LCaY9eYmJbsPAvpWo4H+MIH7MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr
|
||||
BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU
|
||||
kHeSNWfE/6jMqeZ72YB5e8yT+TgwHwYDVR0jBBgwFoAUgEzW63T/STaj1dj8tT7F
|
||||
avCUHYwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAChhhodHRwOi8vaS5wa2ku
|
||||
Z29vZy9yNC5jcnQwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2MucGtpLmdvb2cv
|
||||
ci9yNC5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwCgYIKoZIzj0EAwMDaAAwZQIx
|
||||
AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V
|
||||
sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN
|
||||
7uJW
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -18,4 +18,6 @@ display.size=3.5"
|
||||
display.shape=rectangle
|
||||
display.dpi=165
|
||||
|
||||
cdn.warningMessage=This device has display driver and memory issues. App Hub doesn't work.
|
||||
|
||||
lvgl.colorDepth=16
|
||||
|
||||
@@ -16,4 +16,6 @@ display.dpi=165
|
||||
touch.calibrationSupported=true
|
||||
touch.calibrationRequired=false
|
||||
|
||||
cdn.warningMessage=This device has display driver and memory issues. App Hub doesn't work.
|
||||
|
||||
lvgl.colorDepth=16
|
||||
|
||||
@@ -2,17 +2,12 @@
|
||||
|
||||
## Before release
|
||||
|
||||
- Add `// SPDX-License-Identifier: GPL-3.0-only` and `// SPDX-License-Identifier: Apache-2.0` to individual files in the project
|
||||
- Elecrow Basic & Advance 3.5" memory issue: not enough memory for App Hub
|
||||
- App Hub crashes if you close it while an app is being installed
|
||||
- Calculator bugs (see GitHub issue)
|
||||
- Try out speed optimizations: https://docs.espressif.com/projects/esp-faq/en/latest/software-framework/peripherals/lcd.html
|
||||
(relates to CONFIG_ESP32S3_DATA_CACHE_LINE_64B that is in use for RGB displays via the `device.properties` fix/workaround)
|
||||
|
||||
## Higher Priority
|
||||
|
||||
- Add tests for app stdin/stdout
|
||||
- AppHubApp: Prevent download callbacks from accessing a destroyed view. Create a "download task" concept that emits events.
|
||||
- CrashDiagnostics shouldn't show a QR when there's no callstack
|
||||
- Apps currently have a `Context` object with an `appInstanceId` in it, purely for being able to close the app.
|
||||
Change it so that the app has its own termination signal that it waits for in the loop, it should subscribe to the event group.
|
||||
@@ -80,7 +75,6 @@
|
||||
- Mutex: Implement give/take from ISR support (works only for non-recursive ones)
|
||||
- Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials.
|
||||
- Remove flex_flow from app_container in Gui.cpp
|
||||
- ElfAppManifest: change name (remove "manifest" as it's confusing), remove icon and title, publish snapshot SDK on CDN
|
||||
- Bug: CYD 2432S032C screen rotation fails due to touch driver issue
|
||||
- Calculator app should show regular text input field on non-touch devices that have a keyboard (Cardputer, T-Lora Pager)
|
||||
- Allow for WSAD keys to navigate LVGL (this is extra nice for cardputer, but just handy in general)
|
||||
|
||||
@@ -100,6 +100,7 @@ else ()
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
http-module
|
||||
gps-generic-module
|
||||
gps-meshtastic-module
|
||||
service-module
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
list(APPEND REQUIRES_LIST
|
||||
TactilityKernel
|
||||
)
|
||||
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
list(APPEND REQUIRES_LIST
|
||||
esp_http_client
|
||||
)
|
||||
list(FILTER SOURCE_FILES EXCLUDE REGEX ".*download_mock\\.cpp$")
|
||||
else ()
|
||||
list(FILTER SOURCE_FILES EXCLUDE REGEX ".*download_esp\\.cpp$")
|
||||
endif ()
|
||||
|
||||
tactility_add_module(http-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
PRIV_INCLUDE_DIRS private/
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES ${REQUIRES_LIST}
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Identifies how a download finished, as delivered to http_download_poll(). */
|
||||
enum HttpDownloadEventType {
|
||||
HTTP_DOWNLOAD_EVENT_SUCCESS, // no data
|
||||
HTTP_DOWNLOAD_EVENT_ERROR, // struct HttpDownloadErrorEvent
|
||||
HTTP_DOWNLOAD_EVENT_CANCELLED, // no data - see http_download_cancel()
|
||||
};
|
||||
|
||||
#define HTTP_DOWNLOAD_ERROR_MESSAGE_MAX_LEN 64
|
||||
|
||||
/** Data for HTTP_DOWNLOAD_EVENT_ERROR. */
|
||||
struct HttpDownloadErrorEvent {
|
||||
char message[HTTP_DOWNLOAD_ERROR_MESSAGE_MAX_LEN];
|
||||
};
|
||||
|
||||
/** Event delivered through http_download_poll() once a download's task finishes. */
|
||||
struct HttpDownloadEvent {
|
||||
enum HttpDownloadEventType type;
|
||||
/** Microseconds since boot, from get_micros_since_boot(). */
|
||||
uint64_t timestamp;
|
||||
/** The server's HTTP response status code, or 0 if no response was ever received
|
||||
* (e.g. failed to open the connection, or platform doesn't support downloads). */
|
||||
int32_t status_code;
|
||||
/** Valid only when type == HTTP_DOWNLOAD_EVENT_ERROR. */
|
||||
struct HttpDownloadErrorEvent error;
|
||||
};
|
||||
|
||||
/** Internal, refcounted state shared between a subscription and its download task - lets
|
||||
* http_download_unsubscribe() sever the link safely at any time, even mid-download (see its
|
||||
* own doc). Defined only in download.cpp; opaque here like Module::internal (tactility/module.h). */
|
||||
struct HttpDownloadLink;
|
||||
|
||||
/**
|
||||
* Caller-owned subscription node for one download's outcome, registered with
|
||||
* http_download_subscribe() and polled with http_download_poll() - same subscribe/await/poll
|
||||
* shape as TactilityKernel's system_event and app-module's app_event. Unlike those (which
|
||||
* multiplex many emitters/subscribers by type or app instance), one subscription always belongs
|
||||
* to exactly one download and receives exactly one terminal event.
|
||||
* @warning Fields other than `bit` are for internal use only; do not read or write them directly.
|
||||
*/
|
||||
struct HttpDownloadSubscription {
|
||||
/** Set by http_download_subscribe(). Read-only for the caller: OR it into a
|
||||
* task_event_group_wait() mask (alongside other subscriptions sharing the same
|
||||
* `internal.event_group`) to block on this subscription and other event sources with one
|
||||
* call. */
|
||||
uint32_t bit;
|
||||
|
||||
struct {
|
||||
/** Caller-owned, borrowed; set by http_download_subscribe(). */
|
||||
struct TaskEventGroup* event_group;
|
||||
struct HttpDownloadEvent event;
|
||||
bool pending;
|
||||
struct HttpDownloadLink* link;
|
||||
} internal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a poll subscription for one download's outcome.
|
||||
* @warning Does not work in ISR context.
|
||||
* @warning Must be called before http_download_start(), so the download task can never finish
|
||||
* before a subscriber exists to notify.
|
||||
* @param[in,out] sub subscription to register; caller owns the storage and must keep it alive
|
||||
* (and stationary) until http_download_unsubscribe()
|
||||
* @param[in] event_group caller-owned group to wait on; must outlive @a sub. To block for the
|
||||
* outcome, call task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit
|
||||
* into the mask, or use _wait_any() to include every subscription sharing it), then poll with
|
||||
* http_download_poll().
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered
|
||||
* @retval ERROR_OUT_OF_MEMORY failed to allocate @a sub's internal link; @a sub was not registered
|
||||
*/
|
||||
error_t http_download_subscribe(struct HttpDownloadSubscription* sub, struct TaskEventGroup* event_group);
|
||||
|
||||
/**
|
||||
* Remove a previously registered subscription. Safe to call at any time, including while the
|
||||
* download is still in flight (e.g. right after http_download_cancel()) - the download task
|
||||
* never touches @a sub or its event_group again once this returns, so both may be destructed
|
||||
* immediately afterward without waiting for the download to actually finish.
|
||||
* @warning Does not work in ISR context.
|
||||
* @return ERROR_NONE on success, ERROR_NOT_FOUND if @a sub isn't currently subscribed
|
||||
*/
|
||||
error_t http_download_unsubscribe(struct HttpDownloadSubscription* sub);
|
||||
|
||||
/**
|
||||
* Non-blocking: check whether the download's terminal event has arrived.
|
||||
* @warning Never blocks. To wait, block in task_event_group_wait()/task_event_group_wait_any()
|
||||
* on @a sub's bit first (see http_download_subscribe()), then call this.
|
||||
* @param[in,out] sub subscription to poll, as passed to http_download_subscribe()
|
||||
* @param[out] out_event set to the download's outcome
|
||||
* @retval ERROR_NONE the terminal event arrived - it is copied into @a out_event
|
||||
* @retval ERROR_TIMEOUT the download hasn't finished yet
|
||||
*/
|
||||
error_t http_download_poll(struct HttpDownloadSubscription* sub, struct HttpDownloadEvent* out_event);
|
||||
|
||||
/**
|
||||
* Starts a download on its own dedicated task: issues a GET request for @a url, verifying the
|
||||
* server against the PEM certificate at @a cert_path, and writes the response body to
|
||||
* @a target_path.
|
||||
* @param[in] url the URL to download
|
||||
* @param[in] cert_path path to a PEM certificate file used to verify the server
|
||||
* @param[in] target_path path to write the downloaded file to
|
||||
* @param[in] sub subscription to notify on completion, already registered via
|
||||
* http_download_subscribe(); must stay alive (and stationary) at least until
|
||||
* http_download_unsubscribe() is called
|
||||
* @retval ERROR_NONE the download task was started
|
||||
* @retval ERROR_INVALID_ARGUMENT @a sub is not currently subscribed
|
||||
* @retval ERROR_OUT_OF_MEMORY failed to allocate the download task
|
||||
*/
|
||||
error_t http_download_start(
|
||||
const char* url,
|
||||
const char* cert_path,
|
||||
const char* target_path,
|
||||
struct HttpDownloadSubscription* sub
|
||||
);
|
||||
|
||||
/**
|
||||
* Request cancellation of @a sub's in-flight download. Best-effort and asynchronous: the
|
||||
* download task only checks for this periodically (between I/O steps), so it keeps running for
|
||||
* a short while after this returns. If you still want the outcome, poll as usual - it finishes
|
||||
* with HTTP_DOWNLOAD_EVENT_CANCELLED. If you don't, http_download_unsubscribe() may be called
|
||||
* right away instead of waiting for that - see its own doc.
|
||||
* @param[in,out] sub subscription for the download to cancel, as passed to
|
||||
* http_download_subscribe()
|
||||
* @retval ERROR_NONE the cancellation request was recorded
|
||||
* @retval ERROR_INVALID_STATE @a sub is not currently subscribed
|
||||
*/
|
||||
error_t http_download_cancel(struct HttpDownloadSubscription* sub);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module http_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <http/download.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
// Shared between a subscription and its download task, so http_download_unsubscribe() can sever
|
||||
// the link at any time without racing the task's access to the subscription/event_group.
|
||||
// Refcounted via `owners`: starts at 1 for the subscriber, gains 1 when http_download_start()
|
||||
// spawns the task, and whichever side releases last deletes this.
|
||||
// `mutex` also guards the subscription's `internal.event`/`pending`/`event_group`/`bit`. The task
|
||||
// only touches those while `subscribed` is true, under this same lock, so that check is a hard
|
||||
// guarantee those fields are still live.
|
||||
struct HttpDownloadLink {
|
||||
Mutex mutex {};
|
||||
bool subscribed = true;
|
||||
bool cancel_requested = false;
|
||||
int owners = 1;
|
||||
|
||||
HttpDownloadLink() { mutex_construct(&mutex); }
|
||||
~HttpDownloadLink() { mutex_destruct(&mutex); }
|
||||
};
|
||||
|
||||
inline HttpDownloadEvent http_download_make_error_event(const char* message, int32_t status_code = 0) {
|
||||
HttpDownloadEvent event {};
|
||||
event.type = HTTP_DOWNLOAD_EVENT_ERROR;
|
||||
event.timestamp = get_micros_since_boot();
|
||||
event.status_code = status_code;
|
||||
snprintf(event.error.message, sizeof(event.error.message), "%s", message);
|
||||
return event;
|
||||
}
|
||||
|
||||
inline HttpDownloadEvent http_download_make_success_event(int32_t status_code) {
|
||||
HttpDownloadEvent event {};
|
||||
event.type = HTTP_DOWNLOAD_EVENT_SUCCESS;
|
||||
event.timestamp = get_micros_since_boot();
|
||||
event.status_code = status_code;
|
||||
return event;
|
||||
}
|
||||
|
||||
inline HttpDownloadEvent http_download_make_cancelled_event(int32_t status_code = 0) {
|
||||
HttpDownloadEvent event {};
|
||||
event.type = HTTP_DOWNLOAD_EVENT_CANCELLED;
|
||||
event.timestamp = get_micros_since_boot();
|
||||
event.status_code = status_code;
|
||||
return event;
|
||||
}
|
||||
|
||||
inline bool http_download_is_cancelled(HttpDownloadLink* link) {
|
||||
mutex_lock(&link->mutex);
|
||||
bool cancelled = link->cancel_requested;
|
||||
mutex_unlock(&link->mutex);
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/** Implemented in download_esp.cpp (ESP_PLATFORM) or download_mock.cpp (otherwise). */
|
||||
HttpDownloadEvent http_download_run(
|
||||
const std::string& url,
|
||||
const std::string& certPath,
|
||||
const std::string& targetPath,
|
||||
HttpDownloadLink* link
|
||||
);
|
||||
@@ -0,0 +1,190 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <http/download.h>
|
||||
#include <http/private/download.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto* TAG = "http-download";
|
||||
constexpr size_t DOWNLOAD_TASK_STACK_DEPTH = 4608 / sizeof(StackType_t);
|
||||
|
||||
struct DownloadContext {
|
||||
std::string url;
|
||||
std::string certPath;
|
||||
std::string targetPath;
|
||||
HttpDownloadSubscription* subscription;
|
||||
HttpDownloadLink* link;
|
||||
};
|
||||
|
||||
// Delivers `event` to `sub` if the subscriber hasn't unsubscribed in the meantime, then releases
|
||||
// this task's share of `link`, deleting it if the subscriber already released theirs.
|
||||
void finish(HttpDownloadLink* link, HttpDownloadSubscription* sub, const HttpDownloadEvent& event) {
|
||||
mutex_lock(&link->mutex);
|
||||
if (link->subscribed) {
|
||||
sub->internal.event = event;
|
||||
sub->internal.pending = true;
|
||||
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||
}
|
||||
bool last = (--link->owners == 0);
|
||||
mutex_unlock(&link->mutex);
|
||||
|
||||
if (last) {
|
||||
delete link;
|
||||
}
|
||||
}
|
||||
|
||||
void download_task_main(void* raw_context) {
|
||||
auto* context = static_cast<DownloadContext*>(raw_context);
|
||||
LOG_I(TAG, "Downloading %s to %s", context->url.c_str(), context->targetPath.c_str());
|
||||
|
||||
auto event = http_download_run(context->url, context->certPath, context->targetPath, context->link);
|
||||
if (event.type == HTTP_DOWNLOAD_EVENT_ERROR) {
|
||||
LOG_E(TAG, "Download of %s failed: %s", context->url.c_str(), event.error.message);
|
||||
} else if (event.type == HTTP_DOWNLOAD_EVENT_CANCELLED) {
|
||||
LOG_I(TAG, "Download of %s cancelled", context->url.c_str());
|
||||
} else {
|
||||
LOG_I(TAG, "Downloaded %s to %s", context->url.c_str(), context->targetPath.c_str());
|
||||
}
|
||||
|
||||
finish(context->link, context->subscription, event);
|
||||
delete context;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t http_download_subscribe(HttpDownloadSubscription* sub, TaskEventGroup* event_group) {
|
||||
uint32_t bit;
|
||||
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
|
||||
if (claim_result != ERROR_NONE) {
|
||||
return claim_result;
|
||||
}
|
||||
|
||||
auto* link = new (std::nothrow) HttpDownloadLink();
|
||||
if (link == nullptr) {
|
||||
task_event_group_release_bit(event_group, bit);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
sub->bit = bit;
|
||||
sub->internal.event_group = event_group;
|
||||
sub->internal.pending = false;
|
||||
sub->internal.link = link;
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t http_download_unsubscribe(HttpDownloadSubscription* sub) {
|
||||
HttpDownloadLink* link = sub->internal.link;
|
||||
if (link == nullptr) {
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
// Caller-exclusive: the task never reads this field, so it's safe to clear without the lock.
|
||||
sub->internal.link = nullptr;
|
||||
|
||||
TaskEventGroup* event_group;
|
||||
uint32_t bit;
|
||||
|
||||
mutex_lock(&link->mutex);
|
||||
// Read/clear under the lock: finish() reads these same fields under that lock too,
|
||||
// while `subscribed` is true, so this must not race it.
|
||||
event_group = sub->internal.event_group;
|
||||
bit = sub->bit;
|
||||
sub->internal.event_group = nullptr;
|
||||
link->subscribed = false;
|
||||
bool last = (--link->owners == 0);
|
||||
mutex_unlock(&link->mutex);
|
||||
|
||||
// Safe outside the lock: the critical section above already decided whether finish() will
|
||||
// ever signal this bit, so nothing will touch it again from here on.
|
||||
task_event_group_release_bit(event_group, bit);
|
||||
|
||||
if (last) {
|
||||
delete link;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t http_download_poll(HttpDownloadSubscription* sub, HttpDownloadEvent* out_event) {
|
||||
HttpDownloadLink* link = sub->internal.link;
|
||||
if (link == nullptr) {
|
||||
return ERROR_TIMEOUT;
|
||||
}
|
||||
|
||||
mutex_lock(&link->mutex);
|
||||
bool pending = sub->internal.pending;
|
||||
if (pending) {
|
||||
*out_event = sub->internal.event;
|
||||
sub->internal.pending = false;
|
||||
}
|
||||
mutex_unlock(&link->mutex);
|
||||
|
||||
return pending ? ERROR_NONE : ERROR_TIMEOUT;
|
||||
}
|
||||
|
||||
error_t http_download_start(
|
||||
const char* url,
|
||||
const char* cert_path,
|
||||
const char* target_path,
|
||||
HttpDownloadSubscription* sub
|
||||
) {
|
||||
HttpDownloadLink* link = sub->internal.link;
|
||||
if (link == nullptr) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
auto* context = new(std::nothrow) DownloadContext {
|
||||
.url = url,
|
||||
.certPath = cert_path,
|
||||
.targetPath = target_path,
|
||||
.subscription = sub,
|
||||
.link = link
|
||||
};
|
||||
if (context == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
mutex_lock(&link->mutex);
|
||||
link->owners++; // the task's own share, released by finish()
|
||||
mutex_unlock(&link->mutex);
|
||||
|
||||
TaskHandle_t task_handle;
|
||||
if (xTaskCreate(download_task_main, "http-download", DOWNLOAD_TASK_STACK_DEPTH, context, tskIDLE_PRIORITY + 1, &task_handle) != pdPASS) {
|
||||
delete context;
|
||||
|
||||
mutex_lock(&link->mutex);
|
||||
bool last = (--link->owners == 0);
|
||||
mutex_unlock(&link->mutex);
|
||||
if (last) {
|
||||
delete link;
|
||||
}
|
||||
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t http_download_cancel(HttpDownloadSubscription* sub) {
|
||||
HttpDownloadLink* link = sub->internal.link;
|
||||
if (link == nullptr) {
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
mutex_lock(&link->mutex);
|
||||
link->cancel_requested = true;
|
||||
mutex_unlock(&link->mutex);
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <http/private/download.h>
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_http_client.h>
|
||||
|
||||
#include <tactility/freertos/task.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto* TAG = "http-download";
|
||||
|
||||
// RAII: guarantees esp_http_client_close()/_cleanup() run on every exit path below.
|
||||
class EspDownloadClient {
|
||||
esp_http_client_handle_t client = nullptr;
|
||||
bool isOpen = false;
|
||||
|
||||
public:
|
||||
~EspDownloadClient() {
|
||||
if (isOpen) {
|
||||
esp_http_client_close(client);
|
||||
}
|
||||
if (client != nullptr) {
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
}
|
||||
|
||||
bool init(const esp_http_client_config_t& config) {
|
||||
client = esp_http_client_init(&config);
|
||||
return client != nullptr;
|
||||
}
|
||||
|
||||
bool open() {
|
||||
if (esp_http_client_open(client, 0) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
isOpen = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fetchHeaders() const { return esp_http_client_fetch_headers(client) >= 0; }
|
||||
|
||||
int getStatusCode() const { return esp_http_client_get_status_code(client); }
|
||||
|
||||
int getContentLength() const { return esp_http_client_get_content_length(client); }
|
||||
|
||||
int read(char* buffer, int size) const { return esp_http_client_read(client, buffer, size); }
|
||||
|
||||
bool isComplete() const { return esp_http_client_is_complete_data_received(client); }
|
||||
};
|
||||
|
||||
// esp_http_client_config_t::cert_pem needs a NUL-terminated buffer.
|
||||
bool read_certificate(const std::string& certPath, std::string& outCertificate) {
|
||||
auto* file = fopen(certPath.c_str(), "rb");
|
||||
if (file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fseek(file, 0, SEEK_END);
|
||||
long size = ftell(file);
|
||||
fseek(file, 0, SEEK_SET);
|
||||
if (size <= 0) {
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
|
||||
outCertificate.resize(static_cast<size_t>(size));
|
||||
size_t read_bytes = fread(outCertificate.data(), 1, static_cast<size_t>(size), file);
|
||||
fclose(file);
|
||||
return read_bytes == static_cast<size_t>(size);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
HttpDownloadEvent http_download_run(const std::string& url, const std::string& certPath, const std::string& targetPath, HttpDownloadLink* link) {
|
||||
if (http_download_is_cancelled(link)) {
|
||||
return http_download_make_cancelled_event();
|
||||
}
|
||||
|
||||
std::string certificate;
|
||||
if (!read_certificate(certPath, certificate)) {
|
||||
return http_download_make_error_event("Failed to read certificate file");
|
||||
}
|
||||
|
||||
esp_http_client_config_t config {};
|
||||
config.url = url.c_str();
|
||||
config.auth_type = HTTP_AUTH_TYPE_NONE;
|
||||
config.cert_pem = certificate.c_str();
|
||||
config.cert_len = certificate.size() + 1;
|
||||
config.tls_version = ESP_HTTP_CLIENT_TLS_VER_TLS_1_2;
|
||||
config.method = HTTP_METHOD_GET;
|
||||
config.timeout_ms = 5000;
|
||||
config.transport_type = HTTP_TRANSPORT_OVER_SSL;
|
||||
|
||||
// Total free can look fine while a fragmented heap still can't satisfy one large-enough
|
||||
// allocation. Logging both makes a future ALLOC_FAILED here diagnosable from the log alone.
|
||||
LOG_I(TAG, "Free internal heap before connecting: %u bytes (largest block: %u bytes)",
|
||||
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
|
||||
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)));
|
||||
|
||||
EspDownloadClient client;
|
||||
if (!client.init(config)) {
|
||||
return http_download_make_error_event("Failed to initialize HTTP client");
|
||||
}
|
||||
if (!client.open()) {
|
||||
return http_download_make_error_event("Failed to open connection");
|
||||
}
|
||||
if (!client.fetchHeaders()) {
|
||||
return http_download_make_error_event("Failed to fetch response headers");
|
||||
}
|
||||
|
||||
auto status_code = client.getStatusCode();
|
||||
if (status_code < 200 || status_code >= 300) {
|
||||
return http_download_make_error_event("Server response is not OK", status_code);
|
||||
}
|
||||
|
||||
if (http_download_is_cancelled(link)) {
|
||||
return http_download_make_cancelled_event(status_code);
|
||||
}
|
||||
|
||||
// Downloaded to a sibling ".tmp" file first and only renamed onto targetPath on success, so a
|
||||
// failed/cancelled download (or a crash mid-write) never leaves a partial file at targetPath.
|
||||
auto tempPath = targetPath + ".tmp";
|
||||
auto* file = fopen(tempPath.c_str(), "wb");
|
||||
if (file == nullptr) {
|
||||
return http_download_make_error_event("Failed to open target file", status_code);
|
||||
}
|
||||
|
||||
// -1 means the length is unknown (e.g. a chunked response). Read until the client reports
|
||||
// the body done instead of counting down a length that was never given.
|
||||
auto content_length = client.getContentLength();
|
||||
bool length_known = content_length >= 0;
|
||||
auto bytes_left = content_length;
|
||||
char buffer[512];
|
||||
while (!length_known || bytes_left > 0) {
|
||||
if (http_download_is_cancelled(link)) {
|
||||
fclose(file);
|
||||
remove(tempPath.c_str());
|
||||
return http_download_make_cancelled_event(status_code);
|
||||
}
|
||||
int data_read = client.read(buffer, sizeof(buffer));
|
||||
if (data_read < 0) {
|
||||
fclose(file);
|
||||
remove(tempPath.c_str());
|
||||
return http_download_make_error_event("Failed to read response data", status_code);
|
||||
}
|
||||
if (data_read == 0) {
|
||||
break;
|
||||
}
|
||||
if (length_known) {
|
||||
bytes_left -= data_read;
|
||||
}
|
||||
if (fwrite(buffer, 1, static_cast<size_t>(data_read), file) != static_cast<size_t>(data_read)) {
|
||||
fclose(file);
|
||||
remove(tempPath.c_str());
|
||||
return http_download_make_error_event("Failed to write downloaded data", status_code);
|
||||
}
|
||||
taskYIELD();
|
||||
}
|
||||
|
||||
// Distinguishes a clean end (chunked terminator seen, or a known length fully read) from a
|
||||
// connection that just stopped producing data early.
|
||||
if (!client.isComplete()) {
|
||||
fclose(file);
|
||||
remove(tempPath.c_str());
|
||||
return http_download_make_error_event("Response body was incomplete", status_code);
|
||||
}
|
||||
|
||||
if (fclose(file) != 0) {
|
||||
remove(tempPath.c_str());
|
||||
return http_download_make_error_event("Failed to finalize downloaded file", status_code);
|
||||
}
|
||||
|
||||
// Some embedded filesystems (e.g. FATFS) reject rename() onto an existing path instead of
|
||||
// replacing it like POSIX does. Only clear the way if the plain rename actually needed it, so
|
||||
// targetPath is never removed unless the new file is confirmed ready to replace it.
|
||||
if (rename(tempPath.c_str(), targetPath.c_str()) != 0) {
|
||||
remove(targetPath.c_str());
|
||||
if (rename(tempPath.c_str(), targetPath.c_str()) != 0) {
|
||||
remove(tempPath.c_str());
|
||||
return http_download_make_error_event("Failed to finalize downloaded file", status_code);
|
||||
}
|
||||
}
|
||||
|
||||
return http_download_make_success_event(status_code);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <http/private/download.h>
|
||||
|
||||
HttpDownloadEvent http_download_run(const std::string&, const std::string&, const std::string&, HttpDownloadLink* link) {
|
||||
if (http_download_is_cancelled(link)) {
|
||||
return http_download_make_cancelled_event();
|
||||
}
|
||||
return http_download_make_error_event("HTTP downloads are not supported on this platform");
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <http/download.h>
|
||||
#include <http/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
static const ModuleSymbol http_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(http_download_subscribe),
|
||||
DEFINE_MODULE_SYMBOL(http_download_unsubscribe),
|
||||
DEFINE_MODULE_SYMBOL(http_download_poll),
|
||||
DEFINE_MODULE_SYMBOL(http_download_start),
|
||||
DEFINE_MODULE_SYMBOL(http_download_cancel),
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
Module http_module = {
|
||||
.name = "http",
|
||||
.start = nullptr,
|
||||
.stop = nullptr,
|
||||
.drivers = nullptr,
|
||||
.symbols = http_module_symbols,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
project(HttpModuleTests)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||
add_executable(HttpModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
|
||||
target_include_directories(HttpModuleTests PRIVATE ${DOCTESTINC})
|
||||
|
||||
add_test(NAME HttpModuleTests COMMAND HttpModuleTests)
|
||||
|
||||
target_link_libraries(HttpModuleTests PUBLIC
|
||||
http-module
|
||||
TactilityKernel
|
||||
freertos_kernel
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <http/download.h>
|
||||
|
||||
TEST_CASE("http_download_start requires a subscribed subscription") {
|
||||
HttpDownloadSubscription sub {};
|
||||
CHECK_EQ(http_download_start("http://example.com/file", "/cert.pem", "/target.bin", &sub), ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
TEST_CASE("http_download_poll times out before the download finishes") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
HttpDownloadSubscription sub {};
|
||||
CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
|
||||
HttpDownloadEvent event {};
|
||||
CHECK_EQ(http_download_poll(&sub, &event), ERROR_TIMEOUT);
|
||||
|
||||
http_download_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
TEST_CASE("http_download_start on this (non-ESP-IDF) platform always fails the download") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
HttpDownloadSubscription sub {};
|
||||
CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
CHECK_EQ(http_download_start("http://example.com/file", "/cert.pem", "/target.bin", &sub), ERROR_NONE);
|
||||
|
||||
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
|
||||
HttpDownloadEvent event {};
|
||||
CHECK_EQ(http_download_poll(&sub, &event), ERROR_NONE);
|
||||
CHECK_EQ(event.type, HTTP_DOWNLOAD_EVENT_ERROR);
|
||||
|
||||
http_download_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
TEST_CASE("http_download_unsubscribe reports ERROR_NOT_FOUND when not subscribed") {
|
||||
HttpDownloadSubscription sub {};
|
||||
CHECK_EQ(http_download_unsubscribe(&sub), ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST_CASE("http_download_cancel reports ERROR_INVALID_STATE when not subscribed") {
|
||||
HttpDownloadSubscription sub {};
|
||||
CHECK_EQ(http_download_cancel(&sub), ERROR_INVALID_STATE);
|
||||
}
|
||||
|
||||
TEST_CASE("http_download_unsubscribe is safe to call right after start, without waiting for the download to finish") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
HttpDownloadSubscription sub {};
|
||||
CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
CHECK_EQ(http_download_start("http://example.com/file", "/cert.pem", "/target.bin", &sub), ERROR_NONE);
|
||||
|
||||
// No cancel, no poll for the terminal event - just unsubscribe and tear down immediately,
|
||||
// racing whatever the download task is doing.
|
||||
CHECK_EQ(http_download_unsubscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(http_download_unsubscribe(&sub), ERROR_NOT_FOUND);
|
||||
|
||||
// The bit is free again immediately, even though the download task may still be running.
|
||||
uint32_t reclaimed_bit;
|
||||
CHECK_EQ(task_event_group_claim_bit(&event_group, &reclaimed_bit), ERROR_NONE);
|
||||
task_event_group_release_bit(&event_group, reclaimed_bit);
|
||||
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
|
||||
TEST_CASE("http_download_cancel before start makes the download finish as cancelled") {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
HttpDownloadSubscription sub {};
|
||||
CHECK_EQ(http_download_subscribe(&sub, &event_group), ERROR_NONE);
|
||||
CHECK_EQ(http_download_cancel(&sub), ERROR_NONE);
|
||||
CHECK_EQ(http_download_start("http://example.com/file", "/cert.pem", "/target.bin", &sub), ERROR_NONE);
|
||||
|
||||
CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
|
||||
HttpDownloadEvent event {};
|
||||
CHECK_EQ(http_download_poll(&sub, &event), ERROR_NONE);
|
||||
CHECK_EQ(event.type, HTTP_DOWNLOAD_EVENT_CANCELLED);
|
||||
|
||||
http_download_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT
|
||||
#include "doctest.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
|
||||
typedef struct {
|
||||
int argc;
|
||||
char** argv;
|
||||
int result;
|
||||
} TestTaskData;
|
||||
|
||||
void test_task(void* parameter) {
|
||||
auto* data = (TestTaskData*)parameter;
|
||||
|
||||
doctest::Context context;
|
||||
|
||||
context.applyCommandLine(data->argc, data->argv);
|
||||
|
||||
// overrides
|
||||
context.setOption("no-breaks", true); // don't break in the debugger when assertions fail
|
||||
|
||||
data->result = context.run();
|
||||
|
||||
vTaskEndScheduler();
|
||||
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
TestTaskData data = {
|
||||
.argc = argc,
|
||||
.argv = argv,
|
||||
.result = 0
|
||||
};
|
||||
|
||||
BaseType_t task_result = xTaskCreate(
|
||||
test_task,
|
||||
"test_task",
|
||||
8192,
|
||||
&data,
|
||||
1,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (task_result != pdPASS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
vTaskStartScheduler();
|
||||
|
||||
return data.result;
|
||||
}
|
||||
|
||||
// NOTE: This is normally provided by the platform kernel module, but that's not loaded for http-module
|
||||
extern "C" {
|
||||
// Required for FreeRTOS
|
||||
void vAssertCalled(unsigned long line, const char* const file) {
|
||||
std::fprintf(stderr, "assert failed at %s:%lu\n", file, line);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ list(APPEND REQUIRES_LIST
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
http-module
|
||||
gps-generic-module
|
||||
gps-meshtastic-module
|
||||
service-module
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Tactility/TactilityCore.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <dirent.h>
|
||||
#include <functional>
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include <gps/module.h>
|
||||
#include <gps_generic/module.h>
|
||||
#include <gps_meshtastic/module.h>
|
||||
#include <http/module.h>
|
||||
|
||||
#include <crypt/module.h>
|
||||
#include <lvgl/devices/keyboard.h>
|
||||
@@ -475,6 +476,7 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) {
|
||||
check(module_ensure_started(&gps_module) == ERROR_NONE);
|
||||
check(module_ensure_started(&gps_generic_module) == ERROR_NONE);
|
||||
check(module_ensure_started(&gps_meshtastic_module) == ERROR_NONE);
|
||||
check(module_ensure_started(&http_module) == ERROR_NONE);
|
||||
// Registers the APP_LOCATION_MEMORY app loader (boot/launcher need it below).
|
||||
check(module_ensure_started(&app_module) == ERROR_NONE);
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <Tactility/app/apphub/AppHubEntry.h>
|
||||
#include <Tactility/app/apphubdetails/AppHubDetailsApp.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/network/Http.h>
|
||||
#include <Tactility/service/wifi/Wifi.h>
|
||||
|
||||
#include <app/event.h>
|
||||
@@ -12,6 +11,8 @@
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <http/download.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
@@ -22,6 +23,7 @@
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <format>
|
||||
|
||||
namespace tt::app::apphub {
|
||||
@@ -43,6 +45,20 @@ struct Context {
|
||||
|
||||
// Survives across a bury/resurface cycle (e.g. opening AppHubDetailsApp and returning),
|
||||
int32_t scrollY = 0;
|
||||
// Set by createWidgets(), consumed by the first showApps() after resurfacing. The refresh
|
||||
// itself runs async (see requestRefresh() below), so when showApps() first populates the
|
||||
// list, contentWrapper is still an empty spinner; scrollY must come from here instead of a
|
||||
// live read off it.
|
||||
bool restoreScrollOnNextShow = false;
|
||||
// Only the first createWidgets() call triggers a network refresh. The rest uses the cached file.
|
||||
bool needsInitialRefresh = true;
|
||||
|
||||
TaskEventGroup* eventGroup = nullptr;
|
||||
uint32_t refreshRequestedBit = 0;
|
||||
std::atomic<bool> refreshRequested {false};
|
||||
|
||||
HttpDownloadSubscription downloadSub {};
|
||||
bool downloadInProgress = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -66,9 +82,14 @@ void onAppPressed(lv_event_t* e) {
|
||||
ctx->mutex.unlock();
|
||||
}
|
||||
|
||||
void requestRefresh(Context* ctx) {
|
||||
ctx->refreshRequested = true;
|
||||
task_event_group_signal(ctx->eventGroup, ctx->refreshRequestedBit);
|
||||
}
|
||||
|
||||
void onRefreshPressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
refresh(ctx);
|
||||
requestRefresh(ctx);
|
||||
}
|
||||
|
||||
void showRefreshFailedError(Context* ctx, const char* message) {
|
||||
@@ -88,7 +109,13 @@ void showNoInternet(Context* ctx) {
|
||||
void showApps(Context* ctx) {
|
||||
// Refresh rebuilds the list from scratch (cached copy, then again once the network fetch
|
||||
// lands), which would otherwise reset the user's scroll position each time.
|
||||
auto scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
|
||||
int32_t scrollY;
|
||||
if (ctx->restoreScrollOnNextShow) {
|
||||
scrollY = ctx->scrollY;
|
||||
ctx->restoreScrollOnNextShow = false;
|
||||
} else {
|
||||
scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
|
||||
}
|
||||
lv_obj_clean(ctx->contentWrapper);
|
||||
ctx->mutex.lock();
|
||||
if (parseJson(ctx->cachedAppsJsonFile, ctx->entries)) {
|
||||
@@ -130,42 +157,82 @@ void showApps(Context* ctx) {
|
||||
ctx->mutex.unlock();
|
||||
}
|
||||
|
||||
// Runs on appMain()'s own task (triggered via requestRefresh()), never directly from the LVGL task.
|
||||
void refresh(Context* ctx) {
|
||||
// Buried (e.g. AppHubDetailsApp is open): destroyWidgets() already released these. A refresh
|
||||
// request queued just before burying could still land here, so re-check rather than assume
|
||||
// requestRefresh() and refresh() always run against a live window.
|
||||
if (ctx->downloadInProgress || ctx->contentWrapper == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
lvgl_lock();
|
||||
lv_obj_clean(ctx->contentWrapper);
|
||||
auto* spinner = lvgl_spinner_create(ctx->contentWrapper);
|
||||
lv_obj_align(spinner, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
lv_obj_add_flag(ctx->refreshButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lvgl_unlock();
|
||||
|
||||
if (service::wifi::getRadioState() != service::wifi::RadioState::ConnectionActive) {
|
||||
lvgl_lock();
|
||||
showNoInternet(ctx);
|
||||
lvgl_unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
if (file::isFile(ctx->cachedAppsJsonFile)) {
|
||||
lvgl_lock();
|
||||
showApps(ctx);
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
// These callbacks run on a background network thread and reach back into this app's
|
||||
// widgets via the captured ctx pointer - same convention as AppHubDetailsApp.cpp's
|
||||
// download callback for the sibling "install/update" flow.
|
||||
network::http::download(
|
||||
getAppsJsonUrl(),
|
||||
CERTIFICATE_PATH,
|
||||
ctx->cachedAppsJsonFile,
|
||||
[ctx] {
|
||||
LOG_I(TAG, "Request success");
|
||||
lvgl_lock();
|
||||
showApps(ctx);
|
||||
lvgl_unlock();
|
||||
},
|
||||
[ctx](const char* error) {
|
||||
LOG_E(TAG, "Request failed: %s", error);
|
||||
lvgl_lock();
|
||||
showRefreshFailedError(ctx, "Cannot reach server");
|
||||
lvgl_unlock();
|
||||
}
|
||||
);
|
||||
if (http_download_subscribe(&ctx->downloadSub, ctx->eventGroup) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to subscribe to download events");
|
||||
lvgl_lock();
|
||||
showRefreshFailedError(ctx, "Cannot reach server");
|
||||
lvgl_unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
auto url = getAppsJsonUrl();
|
||||
if (http_download_start(url.c_str(), CERTIFICATE_PATH, ctx->cachedAppsJsonFile.c_str(), &ctx->downloadSub) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to start download");
|
||||
http_download_unsubscribe(&ctx->downloadSub);
|
||||
lvgl_lock();
|
||||
showRefreshFailedError(ctx, "Cannot reach server");
|
||||
lvgl_unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx->downloadInProgress = true;
|
||||
}
|
||||
|
||||
// Called from appMain()'s loop once http_download_poll() reports the download's terminal event.
|
||||
void onDownloadFinished(Context* ctx, const HttpDownloadEvent& event) {
|
||||
ctx->downloadInProgress = false;
|
||||
http_download_unsubscribe(&ctx->downloadSub);
|
||||
|
||||
bool succeeded = event.type == HTTP_DOWNLOAD_EVENT_SUCCESS;
|
||||
ctx->needsInitialRefresh = !succeeded;
|
||||
if (succeeded) {
|
||||
LOG_I(TAG, "Request success (status %d)", event.status_code);
|
||||
} else {
|
||||
LOG_E(TAG, "Request failed (status %d): %s", event.status_code, event.error.message);
|
||||
}
|
||||
|
||||
if (ctx->contentWrapper == nullptr) {
|
||||
// Buried (e.g. AppHubDetailsApp is open): destroyWidgets() already released the widgets
|
||||
// above. createWidgets() picks this up via needsInitialRefresh on resurface instead.
|
||||
return;
|
||||
}
|
||||
|
||||
lvgl_lock();
|
||||
if (succeeded) {
|
||||
showApps(ctx);
|
||||
} else {
|
||||
showRefreshFailedError(ctx, "Cannot reach server");
|
||||
}
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
@@ -186,9 +253,15 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_style_pad_all(ctx->contentWrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_ver(ctx->contentWrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
refresh(ctx);
|
||||
|
||||
lv_obj_scroll_to_y(ctx->contentWrapper, ctx->scrollY, LV_ANIM_OFF);
|
||||
ctx->restoreScrollOnNextShow = true;
|
||||
if (ctx->needsInitialRefresh) {
|
||||
requestRefresh(ctx);
|
||||
} else {
|
||||
// Resurfacing (e.g. returning from AppHubDetailsApp): redisplay the cache already
|
||||
// loaded this session instead of hitting the network again. window_manager calls
|
||||
// createWidgets() with the LVGL lock already held, so this can touch widgets directly.
|
||||
showApps(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
void destroyWidgets(void* userData) {
|
||||
@@ -205,6 +278,10 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
ctx.eventGroup = &event_group;
|
||||
if (task_event_group_claim_bit(&event_group, &ctx.refreshRequestedBit) != ERROR_NONE) {
|
||||
LOG_W(TAG, "Failed to claim a refresh-requested bit; refresh button won't work");
|
||||
}
|
||||
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
@@ -224,8 +301,24 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (shouldClose) break;
|
||||
}
|
||||
|
||||
if (ctx.downloadInProgress) {
|
||||
HttpDownloadEvent download_event {};
|
||||
if (http_download_poll(&ctx.downloadSub, &download_event) == ERROR_NONE) {
|
||||
onDownloadFinished(&ctx, download_event);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldClose && ctx.refreshRequested.exchange(false)) {
|
||||
refresh(&ctx);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.downloadInProgress) {
|
||||
http_download_cancel(&ctx.downloadSub);
|
||||
http_download_unsubscribe(&ctx.downloadSub);
|
||||
ctx.downloadInProgress = false;
|
||||
}
|
||||
|
||||
window_manager_remove(window);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <Tactility/app/apphub/AppHub.h>
|
||||
#include <Tactility/app/apphub/AppHubEntry.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/network/Http.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/install.h>
|
||||
@@ -13,6 +12,8 @@
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <http/download.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
@@ -51,6 +52,13 @@ struct Context {
|
||||
std::atomic<uint32_t> installDialogId = 0;
|
||||
std::atomic<uint32_t> uninstallDialogId = 0;
|
||||
std::atomic<uint32_t> updateDialogId = 0;
|
||||
|
||||
// doInstall()/onDownloadFinished() and the poll for them both run on appMain()'s own task
|
||||
// (triggered via confirm-dialog APP_EVENT_RESULTs), so HttpDownloadSubscription's
|
||||
// subscribe/start/poll are naturally all on one consistent task already.
|
||||
TaskEventGroup* eventGroup = nullptr;
|
||||
HttpDownloadSubscription downloadSub {};
|
||||
bool downloadInProgress = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -95,36 +103,68 @@ void uninstallApp(Context* ctx) {
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
void doInstall(Context* ctx) {
|
||||
auto url = apphub::getDownloadUrl(ctx->entry.file);
|
||||
// Path doInstall() downloads to and onDownloadFinished() installs from - deterministic from ctx->entry,
|
||||
// which doesn't change once this app instance is running, so it's recomputed at each use instead of stored.
|
||||
std::string getTempFilePath(Context* ctx) {
|
||||
auto file_name = file::getLastPathSegment(ctx->entry.file);
|
||||
auto temp_file_path = std::format("{}/{}", getTempPath(), file_name);
|
||||
network::http::download(
|
||||
url,
|
||||
apphub::CERTIFICATE_PATH,
|
||||
temp_file_path,
|
||||
[ctx, temp_file_path] {
|
||||
app_install(temp_file_path.c_str());
|
||||
return std::format("{}/{}", getTempPath(), file_name);
|
||||
}
|
||||
|
||||
if (!file::deleteFile(temp_file_path)) {
|
||||
LOG_W(TAG, "Failed to remove %s", temp_file_path.c_str());
|
||||
} else {
|
||||
LOG_I(TAG, "Deleted temporary file %s", temp_file_path.c_str());
|
||||
}
|
||||
void doInstall(Context* ctx) {
|
||||
if (ctx->downloadInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
lvgl_lock();
|
||||
updateViews(ctx);
|
||||
lvgl_unlock();
|
||||
},
|
||||
[ctx, temp_file_path](const char* errorMessage) {
|
||||
LOG_E(TAG, "Download failed: %s", errorMessage);
|
||||
if (http_download_subscribe(&ctx->downloadSub, ctx->eventGroup) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to subscribe to download events");
|
||||
alertdialog::start(ctx->appInstanceId, "Error", "Failed to install app");
|
||||
return;
|
||||
}
|
||||
|
||||
auto url = apphub::getDownloadUrl(ctx->entry.file);
|
||||
auto temp_file_path = getTempFilePath(ctx);
|
||||
if (http_download_start(url.c_str(), apphub::CERTIFICATE_PATH, temp_file_path.c_str(), &ctx->downloadSub) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to start download");
|
||||
http_download_unsubscribe(&ctx->downloadSub);
|
||||
alertdialog::start(ctx->appInstanceId, "Error", "Failed to install app");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx->downloadInProgress = true;
|
||||
}
|
||||
|
||||
// Called from appMain()'s loop once http_download_poll() reports the download's terminal event.
|
||||
void onDownloadFinished(Context* ctx, const HttpDownloadEvent& event) {
|
||||
ctx->downloadInProgress = false;
|
||||
http_download_unsubscribe(&ctx->downloadSub);
|
||||
|
||||
auto temp_file_path = getTempFilePath(ctx);
|
||||
|
||||
if (event.type == HTTP_DOWNLOAD_EVENT_SUCCESS) {
|
||||
error_t install_result = app_install(temp_file_path.c_str());
|
||||
if (install_result != ERROR_NONE) {
|
||||
LOG_E(TAG, "Install of %s failed", temp_file_path.c_str());
|
||||
alertdialog::start(ctx->appInstanceId, "Error", "Failed to install app");
|
||||
|
||||
if (file::isFile(temp_file_path) && !file::deleteFile(temp_file_path.c_str())) {
|
||||
LOG_W(TAG, "Failed to remove %s", temp_file_path.c_str());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!file::deleteFile(temp_file_path)) {
|
||||
LOG_W(TAG, "Failed to remove %s", temp_file_path.c_str());
|
||||
} else {
|
||||
LOG_I(TAG, "Deleted temporary file %s", temp_file_path.c_str());
|
||||
}
|
||||
|
||||
lvgl_lock();
|
||||
updateViews(ctx);
|
||||
lvgl_unlock();
|
||||
} else {
|
||||
LOG_E(TAG, "Download failed (status %d): %s", event.status_code,
|
||||
event.type == HTTP_DOWNLOAD_EVENT_ERROR ? event.error.message : "Cancelled");
|
||||
alertdialog::start(ctx->appInstanceId, "Error", "Failed to install app");
|
||||
|
||||
if (file::isFile(temp_file_path) && !file::deleteFile(temp_file_path.c_str())) {
|
||||
LOG_W(TAG, "Failed to remove %s", temp_file_path.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void installApp(Context* ctx) {
|
||||
@@ -234,6 +274,7 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
ctx.eventGroup = &event_group;
|
||||
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
@@ -267,6 +308,21 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
}
|
||||
if (shouldClose) break;
|
||||
}
|
||||
|
||||
if (ctx.downloadInProgress) {
|
||||
HttpDownloadEvent download_event {};
|
||||
if (http_download_poll(&ctx.downloadSub, &download_event) == ERROR_NONE) {
|
||||
onDownloadFinished(&ctx, download_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.downloadInProgress) {
|
||||
// Safe to call immediately, even mid-download - no need to wait for the terminal event
|
||||
// first, so app close doesn't block on the network.
|
||||
http_download_cancel(&ctx.downloadSub);
|
||||
http_download_unsubscribe(&ctx.downloadSub);
|
||||
ctx.downloadInProgress = false;
|
||||
}
|
||||
|
||||
window_manager_remove(window);
|
||||
|
||||
@@ -19,6 +19,7 @@ target_link_libraries(TactilityTests PRIVATE
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
http-module
|
||||
gps-generic-module
|
||||
gps-meshtastic-module
|
||||
service-module
|
||||
|
||||
@@ -9,6 +9,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/TactilityKernel/tests ${CMAKE_CURRENT_BINAR
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Tactility/Tests ${CMAKE_CURRENT_BINARY_DIR}/Tactility)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/crypt-module/tests ${CMAKE_CURRENT_BINARY_DIR}/crypt-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/app-module/tests ${CMAKE_CURRENT_BINARY_DIR}/app-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/http-module/tests ${CMAKE_CURRENT_BINARY_DIR}/http-module)
|
||||
|
||||
add_custom_target(build-tests)
|
||||
add_dependencies(build-tests ServiceModuleTests)
|
||||
@@ -17,3 +18,4 @@ add_dependencies(build-tests TactilityTests)
|
||||
add_dependencies(build-tests TactilityKernelTests)
|
||||
add_dependencies(build-tests CryptModuleTests)
|
||||
add_dependencies(build-tests AppModuleTests)
|
||||
add_dependencies(build-tests HttpModuleTests)
|
||||
|
||||
Reference in New Issue
Block a user