mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-02-18 10:53:17 +00:00
Merge Develop into Main (#298)
Various improvements and new internal APIs including a new Development service+app which allows `tactility.py` to upload and run applications remotely.
This commit is contained in:
parent
d06197a6aa
commit
ab4cf79a47
@ -1,5 +1,6 @@
|
||||
# TODOs
|
||||
|
||||
- Bug: When a Wi-Fi SSID is too long, then it fails to save the credentials
|
||||
- Add a Keyboard setting app to override the behaviour of soft keyboard hiding (e.g. keyboard hardware is present, but user wants soft keyboard)
|
||||
- HAL for display touch calibration
|
||||
- Start using non_null (either via MS GSL, or custom)
|
||||
|
||||
@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||
|
||||
list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minmea esp_wifi nvs_flash spiffs vfs fatfs lwip)
|
||||
list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minmea esp_wifi nvs_flash spiffs vfs fatfs lwip esp_http_server)
|
||||
if ("${IDF_TARGET}" STREQUAL "esp32s3")
|
||||
list(APPEND REQUIRES_LIST esp_tinyusb)
|
||||
endif ()
|
||||
|
||||
@ -31,10 +31,7 @@ void setElfAppManifest(
|
||||
*/
|
||||
std::string getElfAppId(const std::string& filePath);
|
||||
|
||||
/**
|
||||
* @return true when registration was done, false when app was already registered
|
||||
*/
|
||||
bool registerElfApp(const std::string& filePath);
|
||||
void registerElfApp(const std::string& filePath);
|
||||
|
||||
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest);
|
||||
|
||||
|
||||
29
Tactility/Include/Tactility/network/HttpdReq.h
Normal file
29
Tactility/Include/Tactility/network/HttpdReq.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value);
|
||||
|
||||
bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary);
|
||||
|
||||
bool getQueryOrSendError(httpd_req_t* request, std::string& query);
|
||||
|
||||
std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead);
|
||||
|
||||
std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator);
|
||||
|
||||
std::map<std::string, std::string> parseContentDisposition(const std::vector<std::string>& input);
|
||||
|
||||
bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead);
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
19
Tactility/Include/Tactility/network/Url.h
Normal file
19
Tactility/Include/Tactility/network/Url.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
/**
|
||||
* Parse a query from a URL
|
||||
* @param[in] query
|
||||
* @return a map with key-values
|
||||
*/
|
||||
std::map<std::string, std::string> parseUrlQuery(std::string query);
|
||||
|
||||
std::string urlEncode(const std::string& input);
|
||||
|
||||
std::string urlDecode(const std::string& input);
|
||||
|
||||
} // namespace
|
||||
@ -113,6 +113,11 @@ void setScanRecords(uint16_t records);
|
||||
*/
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* @return the IPv4 address or empty string
|
||||
*/
|
||||
std::string getIp();
|
||||
|
||||
/**
|
||||
* @brief Connect to a network. Disconnects any existing connection.
|
||||
* Returns immediately but runs in the background. Results are through pubsub.
|
||||
|
||||
11
Tactility/Private/Tactility/app/development/Development.h
Normal file
11
Tactility/Private/Tactility/app/development/Development.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
namespace tt::app::development {
|
||||
|
||||
void start();
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Tactility/service/Service.h"
|
||||
|
||||
#include <Tactility/Mutex.h>
|
||||
|
||||
#include <esp_event.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <Tactility/kernel/SystemEvents.h>
|
||||
|
||||
namespace tt::service::development {
|
||||
|
||||
class DevelopmentService final : public Service {
|
||||
|
||||
Mutex mutex = Mutex(Mutex::Type::Recursive);
|
||||
httpd_handle_t server = nullptr;
|
||||
bool enabled = false;
|
||||
kernel::SystemEventSubscription networkConnectEventSubscription = 0;
|
||||
kernel::SystemEventSubscription networkDisconnectEventSubscription = 0;
|
||||
std::string deviceResponse;
|
||||
|
||||
httpd_uri_t handleGetInfoEndpoint = {
|
||||
.uri = "/info",
|
||||
.method = HTTP_GET,
|
||||
.handler = handleGetInfo,
|
||||
.user_ctx = this
|
||||
};
|
||||
|
||||
httpd_uri_t appRunEndpoint = {
|
||||
.uri = "/app/run",
|
||||
.method = HTTP_POST,
|
||||
.handler = handleAppRun,
|
||||
.user_ctx = this
|
||||
};
|
||||
|
||||
httpd_uri_t appInstallEndpoint = {
|
||||
.uri = "/app/install",
|
||||
.method = HTTP_PUT,
|
||||
.handler = handleAppInstall,
|
||||
.user_ctx = this
|
||||
};
|
||||
|
||||
void onNetworkConnected();
|
||||
void onNetworkDisconnected();
|
||||
|
||||
void startServer();
|
||||
void stopServer();
|
||||
|
||||
static esp_err_t handleGetInfo(httpd_req_t* request);
|
||||
static esp_err_t handleAppRun(httpd_req_t* request);
|
||||
static esp_err_t handleAppInstall(httpd_req_t* request);
|
||||
|
||||
public:
|
||||
|
||||
// region Overrides
|
||||
|
||||
void onStart(ServiceContext& service) override;
|
||||
void onStop(ServiceContext& service) override;
|
||||
|
||||
// endregion Overrides
|
||||
|
||||
// region Internal API
|
||||
|
||||
/**
|
||||
* Enabling the service means that the user is willing to start the web server.
|
||||
* @return true when the service is enabled
|
||||
*/
|
||||
bool isEnabled() const;
|
||||
|
||||
/**
|
||||
* Enabling the service means that the user is willing to start the web server.
|
||||
* @param[in] enabled
|
||||
*/
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* @return true if the service will enable itself when it is started (e.g. on boot, or manual start)
|
||||
*/
|
||||
bool isEnabledOnStart() const;
|
||||
|
||||
/**
|
||||
* Set whether the service should auto-enable when it is started.
|
||||
* @param enabled
|
||||
*/
|
||||
void setEnabledOnStart(bool enabled);
|
||||
|
||||
bool isStarted() const;
|
||||
|
||||
// region Internal API
|
||||
};
|
||||
|
||||
std::shared_ptr<DevelopmentService> findService();
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -33,9 +33,10 @@ namespace app {
|
||||
namespace addgps { extern const AppManifest manifest; }
|
||||
namespace alertdialog { extern const AppManifest manifest; }
|
||||
namespace applist { extern const AppManifest manifest; }
|
||||
namespace boot { extern const AppManifest manifest; }
|
||||
namespace calculator { extern const AppManifest manifest; }
|
||||
namespace chat { extern const AppManifest manifest; }
|
||||
namespace boot { extern const AppManifest manifest; }
|
||||
namespace development { extern const AppManifest manifest; }
|
||||
namespace display { extern const AppManifest manifest; }
|
||||
namespace filebrowser { extern const AppManifest manifest; }
|
||||
namespace fileselection { extern const AppManifest manifest; }
|
||||
@ -73,6 +74,7 @@ namespace app {
|
||||
|
||||
// endregion
|
||||
|
||||
// List of all apps excluding Boot app (as Boot app calls this function indirectly)
|
||||
static void registerSystemApps() {
|
||||
addApp(app::addgps::manifest);
|
||||
addApp(app::alertdialog::manifest);
|
||||
@ -109,6 +111,7 @@ static void registerSystemApps() {
|
||||
#ifdef ESP_PLATFORM
|
||||
addApp(app::chat::manifest);
|
||||
addApp(app::crashdiagnostics::manifest);
|
||||
addApp(app::development::manifest);
|
||||
#endif
|
||||
|
||||
if (getConfiguration()->hardware->power != nullptr) {
|
||||
|
||||
@ -20,6 +20,7 @@ namespace service::gps { extern const ServiceManifest manifest; }
|
||||
namespace service::wifi { extern const ServiceManifest manifest; }
|
||||
namespace service::sdcard { extern const ServiceManifest manifest; }
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace service::development { extern const ServiceManifest manifest; }
|
||||
namespace service::espnow { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
|
||||
@ -33,6 +34,7 @@ static void registerAndStartSystemServices() {
|
||||
addService(service::sdcard::manifest);
|
||||
addService(service::wifi::manifest);
|
||||
#ifdef ESP_PLATFORM
|
||||
addService(service::development::manifest);
|
||||
addService(service::espnow::manifest);
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -177,7 +177,7 @@ std::string getElfAppId(const std::string& filePath) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
bool registerElfApp(const std::string& filePath) {
|
||||
void registerElfApp(const std::string& filePath) {
|
||||
if (findAppById(filePath) == nullptr) {
|
||||
auto manifest = AppManifest {
|
||||
.id = getElfAppId(filePath),
|
||||
@ -187,7 +187,6 @@ bool registerElfApp(const std::string& filePath) {
|
||||
};
|
||||
addApp(manifest);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest) {
|
||||
|
||||
163
Tactility/Source/app/development/Development.cpp
Normal file
163
Tactility/Source/app/development/Development.cpp
Normal file
@ -0,0 +1,163 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Tactility/app/AppManifest.h"
|
||||
#include "Tactility/lvgl/Style.h"
|
||||
#include "Tactility/lvgl/Toolbar.h"
|
||||
#include "Tactility/service/development/DevelopmentService.h"
|
||||
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/service/wifi/Wifi.h>
|
||||
#include <cstring>
|
||||
#include <lvgl.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/service/loader/Loader.h>
|
||||
#include <Tactility/service/wifi/Wifi.h>
|
||||
|
||||
namespace tt::app::development {
|
||||
|
||||
constexpr const char* TAG = "Development";
|
||||
|
||||
class DevelopmentApp final : public App {
|
||||
|
||||
lv_obj_t* enableSwitch = nullptr;
|
||||
lv_obj_t* enableOnBootSwitch = nullptr;
|
||||
lv_obj_t* statusLabel = nullptr;
|
||||
std::shared_ptr<service::development::DevelopmentService> service;
|
||||
|
||||
Timer timer = Timer(Timer::Type::Periodic, [this] {
|
||||
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||
if (lock.lock(lvgl::defaultLockTime)) {
|
||||
updateViewState();
|
||||
}
|
||||
});
|
||||
|
||||
static void onEnableSwitchChanged(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
auto* widget = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||
bool is_on = lv_obj_has_state(widget, LV_STATE_CHECKED);
|
||||
auto* app = static_cast<DevelopmentApp*>(lv_event_get_user_data(event));
|
||||
bool is_changed = is_on != app->service->isEnabled();
|
||||
if (is_changed) {
|
||||
app->service->setEnabled(is_on);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void onEnableOnBootSwitchChanged(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
auto* widget = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||
bool is_on = lv_obj_has_state(widget, LV_STATE_CHECKED);
|
||||
auto* app = static_cast<DevelopmentApp*>(lv_event_get_user_data(event));
|
||||
bool is_changed = is_on != app->service->isEnabledOnStart();
|
||||
if (is_changed) {
|
||||
app->service->setEnabledOnStart(is_on);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateViewState() {
|
||||
if (!service->isEnabled()) {
|
||||
lv_label_set_text(statusLabel, "Service disabled");
|
||||
} else if (!service->isStarted()) {
|
||||
lv_label_set_text(statusLabel, "Waiting for connection...");
|
||||
} else { // enabled and started
|
||||
auto ip = service::wifi::getIp();
|
||||
if (ip.empty()) {
|
||||
lv_label_set_text(statusLabel, "Waiting for IP...");
|
||||
} else {
|
||||
std::string status = std::string("Available at ") + ip;
|
||||
lv_label_set_text(statusLabel, status.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void onCreate(AppContext& appContext) override {
|
||||
service = service::development::findService();
|
||||
if (service == nullptr) {
|
||||
TT_LOG_E(TAG, "Service not found");
|
||||
service::loader::stopApp();
|
||||
}
|
||||
}
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Toolbar
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
|
||||
|
||||
enableSwitch = lvgl::toolbar_add_switch_action(toolbar);
|
||||
lv_obj_add_event_cb(enableSwitch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||
|
||||
if (service->isEnabled()) {
|
||||
lv_obj_add_state(enableSwitch, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_remove_state(enableSwitch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
// Wrappers
|
||||
|
||||
lv_obj_t* secondary_flex = lv_obj_create(parent);
|
||||
lv_obj_set_width(secondary_flex, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(secondary_flex, 1);
|
||||
lv_obj_set_flex_flow(secondary_flex, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_border_width(secondary_flex, 0, 0);
|
||||
lv_obj_set_style_pad_all(secondary_flex, 0, 0);
|
||||
lv_obj_set_style_pad_gap(secondary_flex, 0, 0);
|
||||
lvgl::obj_set_style_bg_invisible(secondary_flex);
|
||||
|
||||
// align() methods don't work on flex, so we need this extra wrapper
|
||||
lv_obj_t* wrapper = lv_obj_create(secondary_flex);
|
||||
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lvgl::obj_set_style_bg_invisible(wrapper);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
|
||||
// Enable on boot
|
||||
|
||||
lv_obj_t* enable_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(enable_label, "Enable on boot");
|
||||
lv_obj_align(enable_label, LV_ALIGN_TOP_LEFT, 0, 6);
|
||||
|
||||
enableOnBootSwitch = lv_switch_create(wrapper);
|
||||
lv_obj_add_event_cb(enableOnBootSwitch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||
lv_obj_align(enableOnBootSwitch, LV_ALIGN_TOP_RIGHT, 0, 0);
|
||||
if (service->isEnabledOnStart()) {
|
||||
lv_obj_add_state(enableOnBootSwitch, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_remove_state(enableOnBootSwitch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
statusLabel = lv_label_create(wrapper);
|
||||
lv_obj_align(statusLabel, LV_ALIGN_TOP_LEFT, 0, 50);
|
||||
|
||||
updateViewState();
|
||||
|
||||
timer.start(1000);
|
||||
}
|
||||
|
||||
void onHide(AppContext& appContext) override {
|
||||
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||
// Ensure that the update isn't already happening
|
||||
lock.lock();
|
||||
timer.stop();
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Development",
|
||||
.name = "Development",
|
||||
.type = Type::Settings,
|
||||
.createApp = create<DevelopmentApp>
|
||||
};
|
||||
|
||||
void start() {
|
||||
app::start(manifest.id);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -118,7 +118,7 @@ static void statusbar_pubsub_event(TT_UNUSED const void* message, void* obj) {
|
||||
}
|
||||
}
|
||||
|
||||
static void onNetworkConnected(TT_UNUSED kernel::SystemEvent event) {
|
||||
static void onTimeChanged(TT_UNUSED kernel::SystemEvent event) {
|
||||
if (statusbar_data.mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
statusbar_data.time_update_timer->stop();
|
||||
statusbar_data.time_update_timer->start(5);
|
||||
@ -139,7 +139,7 @@ static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj)
|
||||
statusbar_data.time_update_timer->start(50 / portTICK_PERIOD_MS);
|
||||
statusbar_data.systemEventSubscription = kernel::subscribeSystemEvent(
|
||||
kernel::SystemEvent::Time,
|
||||
onNetworkConnected
|
||||
onTimeChanged
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
160
Tactility/Source/network/HttpdReq.cpp
Normal file
160
Tactility/Source/network/HttpdReq.cpp
Normal file
@ -0,0 +1,160 @@
|
||||
#include "Tactility/network/HttpdReq.h"
|
||||
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <Tactility/Log.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#define TAG "network"
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value) {
|
||||
size_t header_size = httpd_req_get_hdr_value_len(request, name.c_str());
|
||||
if (header_size == 0) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "header missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto header_buffer = std::make_unique<char[]>(header_size + 1);
|
||||
if (header_buffer == nullptr) {
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
httpd_resp_send_500(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (httpd_req_get_hdr_value_str(request, name.c_str(), header_buffer.get(), header_size + 1) != ESP_OK) {
|
||||
httpd_resp_send_500(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
value = header_buffer.get();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary) {
|
||||
std::string content_type_header;
|
||||
if (!getHeaderOrSendError(request, "Content-Type", content_type_header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto boundary_index = content_type_header.find("boundary=");
|
||||
if (boundary_index == std::string::npos) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "boundary not found in Content-Type");
|
||||
return false;
|
||||
}
|
||||
|
||||
boundary = content_type_header.substr(boundary_index + 9);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getQueryOrSendError(httpd_req_t* request, std::string& query) {
|
||||
size_t buffer_length = httpd_req_get_url_query_len(request);
|
||||
if (buffer_length == 0) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto buffer = std::make_unique<char[]>(buffer_length + 1);
|
||||
if (buffer.get() == nullptr || httpd_req_get_url_query_str(request, buffer.get(), buffer_length + 1) != ESP_OK) {
|
||||
httpd_resp_send_500(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
query = buffer.get();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead) {
|
||||
assert(length > 0);
|
||||
bytesRead = 0;
|
||||
|
||||
auto result = std::make_unique<char[]>(length);
|
||||
|
||||
while (bytesRead < length) {
|
||||
size_t read_size = length - bytesRead;
|
||||
size_t bytes_received = httpd_req_recv(request, result.get() + bytesRead, read_size);
|
||||
if (bytes_received <= 0) {
|
||||
TT_LOG_W(TAG, "Received %zu / %zu", bytesRead + bytes_received, length);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bytesRead += bytes_received;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator) {
|
||||
size_t read_index = 0;
|
||||
std::stringstream result;
|
||||
while (!result.str().ends_with(terminator)) {
|
||||
char buffer;
|
||||
size_t bytes_read = httpd_req_recv(request, &buffer, 1);
|
||||
if (bytes_read <= 0) {
|
||||
return "";
|
||||
} else {
|
||||
read_index += bytes_read;
|
||||
}
|
||||
|
||||
result << buffer;
|
||||
}
|
||||
|
||||
return result.str();
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> parseContentDisposition(const std::vector<std::string>& input) {
|
||||
std::map<std::string, std::string> result;
|
||||
static std::string prefix = "Content-Disposition: ";
|
||||
|
||||
// Find header
|
||||
auto content_disposition_header = std::ranges::find_if(input, [](const std::string& header) {
|
||||
return header.starts_with(prefix);
|
||||
});
|
||||
|
||||
// Header not found
|
||||
if (content_disposition_header == input.end()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto parseable = content_disposition_header->substr(prefix.size());
|
||||
auto parts = string::split(parseable, "; ");
|
||||
for (auto part : parts) {
|
||||
auto key_value = string::split(part, "=");
|
||||
if (key_value.size() == 2) {
|
||||
// Trim trailing newlines
|
||||
auto value = string::trim(key_value[1], "\r\n");
|
||||
if (value.size() > 2) {
|
||||
result[key_value[0]] = value.substr(1, value.size() - 2);
|
||||
} else {
|
||||
result[key_value[0]] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead) {
|
||||
size_t bytes_read;
|
||||
auto buffer = receiveByteArray(request, toRead.length(), bytes_read);
|
||||
if (bytes_read != toRead.length()) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "failed to read discardable data");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (memcmp(buffer.get(), toRead.c_str(), bytes_read) != 0) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "discardable data mismatch");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
82
Tactility/Source/network/Url.cpp
Normal file
82
Tactility/Source/network/Url.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
#include "Tactility/network/Url.h"
|
||||
|
||||
#include <Tactility/Log.h>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
std::map<std::string, std::string> parseUrlQuery(std::string query) {
|
||||
std::map<std::string, std::string> result;
|
||||
|
||||
if (query.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t current_index = query[0] == '?' ? 1U : 0U;
|
||||
auto equals_index = query.find_first_of('=', current_index);
|
||||
while (equals_index != std::string::npos) {
|
||||
auto index_boundary = query.find_first_of('&', equals_index + 1);
|
||||
if (index_boundary == std::string::npos) {
|
||||
index_boundary = query.size();
|
||||
}
|
||||
auto key = query.substr(current_index, (equals_index - current_index));
|
||||
auto decodedKey = urlDecode(key);
|
||||
auto value = query.substr(equals_index + 1, (index_boundary - equals_index - 1));
|
||||
auto decodedValue = urlDecode(value);
|
||||
|
||||
result[decodedKey] = decodedValue;
|
||||
|
||||
// Find next token
|
||||
current_index = index_boundary + 1;
|
||||
equals_index = query.find_first_of('=', current_index);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Adapted from https://stackoverflow.com/a/29962178/3848666
|
||||
std::string urlEncode(const std::string& input) {
|
||||
std::string result = "";
|
||||
const char* characters = input.c_str();
|
||||
char hex_buffer[10];
|
||||
size_t input_length = input.length();
|
||||
|
||||
for (size_t i = 0;i < input_length;i++) {
|
||||
unsigned char c = characters[i];
|
||||
// uncomment this if you want to encode spaces with +
|
||||
if (c==' ') {
|
||||
result += '+';
|
||||
} else if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
result += c;
|
||||
} else {
|
||||
sprintf(hex_buffer, "%%%02X", c); //%% means '%' literal, %02X means at least two digits, paddable with a leading zero
|
||||
result += hex_buffer;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Adapted from https://stackoverflow.com/a/29962178/3848666
|
||||
std::string urlDecode(const std::string& input) {
|
||||
std::string result;
|
||||
size_t conversion_buffer, input_length = input.length();
|
||||
|
||||
for (size_t i = 0; i < input_length; i++) {
|
||||
if (input[i] != '%') {
|
||||
if (input[i] == '+') {
|
||||
result += ' ';
|
||||
} else {
|
||||
result += input[i];
|
||||
}
|
||||
} else {
|
||||
sscanf(input.substr(i + 1, 2).c_str(), "%x", &conversion_buffer);
|
||||
char c = static_cast<char>(conversion_buffer);
|
||||
result += c;
|
||||
i = i + 2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
301
Tactility/Source/service/development/DevelopmentService.cpp
Normal file
301
Tactility/Source/service/development/DevelopmentService.cpp
Normal file
@ -0,0 +1,301 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Tactility/service/development/DevelopmentService.h"
|
||||
|
||||
#include "Tactility/network/HttpdReq.h"
|
||||
#include "Tactility/network/Url.h"
|
||||
#include "Tactility/TactilityHeadless.h"
|
||||
#include "Tactility/service/ServiceManifest.h"
|
||||
#include "Tactility/service/ServiceRegistry.h"
|
||||
#include "Tactility/service/wifi/Wifi.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <esp_wifi.h>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <Tactility/Preferences.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/app/ElfApp.h>
|
||||
#include <Tactility/app/ManifestRegistry.h>
|
||||
|
||||
namespace tt::service::development {
|
||||
|
||||
extern const ServiceManifest manifest;
|
||||
|
||||
constexpr const char* TAG = "DevService";
|
||||
|
||||
void DevelopmentService::onStart(ServiceContext& service) {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
networkConnectEventSubscription = kernel::subscribeSystemEvent(
|
||||
kernel::SystemEvent::NetworkConnected,
|
||||
[this](kernel::SystemEvent) { onNetworkConnected(); }
|
||||
);
|
||||
networkConnectEventSubscription = kernel::subscribeSystemEvent(
|
||||
kernel::SystemEvent::NetworkDisconnected,
|
||||
[this](kernel::SystemEvent) { onNetworkDisconnected(); }
|
||||
);
|
||||
|
||||
setEnabled(isEnabledOnStart());
|
||||
}
|
||||
|
||||
void DevelopmentService::onStop(ServiceContext& service) {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
kernel::unsubscribeSystemEvent(networkConnectEventSubscription);
|
||||
kernel::unsubscribeSystemEvent(networkDisconnectEventSubscription);
|
||||
|
||||
if (isEnabled()) {
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
// region Enable/disable
|
||||
|
||||
void DevelopmentService::setEnabled(bool enabled) {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
this->enabled = enabled;
|
||||
|
||||
// We might already have an IP address, so in case we do, we start the server manually
|
||||
// Or we started the server while it shouldn't be
|
||||
if (enabled && !isStarted() && wifi::getRadioState() == wifi::RadioState::ConnectionActive) {
|
||||
startServer();
|
||||
} else if (!enabled && isStarted()) {
|
||||
stopServer();
|
||||
}
|
||||
}
|
||||
|
||||
bool DevelopmentService::isEnabled() const {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool DevelopmentService::isEnabledOnStart() const {
|
||||
Preferences preferences = Preferences(manifest.id.c_str());
|
||||
bool enabled_on_boot = false;
|
||||
preferences.optBool("enabledOnBoot", enabled_on_boot);
|
||||
return enabled_on_boot;
|
||||
}
|
||||
|
||||
void DevelopmentService::setEnabledOnStart(bool enabled) {
|
||||
Preferences preferences = Preferences(manifest.id.c_str());
|
||||
preferences.putBool("enabledOnBoot", enabled);
|
||||
}
|
||||
|
||||
// region Enable/disable
|
||||
|
||||
void DevelopmentService::startServer() {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
if (isStarted()) {
|
||||
TT_LOG_W(TAG, "Already started");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting server");
|
||||
|
||||
std::stringstream stream;
|
||||
stream << "{";
|
||||
stream << "\"cpuFamily\":\"" << CONFIG_IDF_TARGET << "\", ";
|
||||
stream << "\"osVersion\":\"" << TT_VERSION << "\", ";
|
||||
stream << "\"protocolVersion\":\"1.0.0\"";
|
||||
stream << "}";
|
||||
deviceResponse = stream.str();
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
|
||||
config.server_port = 6666;
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
|
||||
if (httpd_start(&server, &config) == ESP_OK) {
|
||||
httpd_register_uri_handler(server, &handleGetInfoEndpoint);
|
||||
httpd_register_uri_handler(server, &appRunEndpoint);
|
||||
httpd_register_uri_handler(server, &appInstallEndpoint);
|
||||
TT_LOG_I(TAG, "Started on port %d", config.server_port);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to start");
|
||||
}
|
||||
}
|
||||
|
||||
void DevelopmentService::stopServer() {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
if (!isStarted()) {
|
||||
TT_LOG_W(TAG, "Not started");
|
||||
return;
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "Stopping server");
|
||||
if (httpd_stop(server) != ESP_OK) {
|
||||
TT_LOG_W(TAG, "Error while stopping");
|
||||
}
|
||||
server = nullptr;
|
||||
}
|
||||
|
||||
bool DevelopmentService::isStarted() const {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
return server != nullptr;
|
||||
}
|
||||
|
||||
void DevelopmentService::onNetworkConnected() {
|
||||
TT_LOG_I(TAG, "onNetworkConnected");
|
||||
mutex.withLock([this] {
|
||||
if (isEnabled() && !isStarted()) {
|
||||
startServer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DevelopmentService::onNetworkDisconnected() {
|
||||
TT_LOG_I(TAG, "onNetworkDisconnected");
|
||||
mutex.withLock([this] {
|
||||
if (isStarted()) {
|
||||
stopServer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// region endpoints
|
||||
|
||||
esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) {
|
||||
if (httpd_resp_set_type(request, "application/json") != ESP_OK) {
|
||||
TT_LOG_W(TAG, "Failed to send header");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
auto* service = static_cast<DevelopmentService*>(request->user_ctx);
|
||||
|
||||
if (httpd_resp_sendstr(request, service->deviceResponse.c_str()) != ESP_OK) {
|
||||
TT_LOG_W(TAG, "Failed to send response body");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "[200] /device");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
|
||||
std::string query;
|
||||
if (!network::getQueryOrSendError(request, query)) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
auto parameters = network::parseUrlQuery(query);
|
||||
auto id_key_pos = parameters.find("id");
|
||||
if (id_key_pos == parameters.end()) {
|
||||
TT_LOG_W(TAG, "[400] /app/run id not specified");
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
auto app_id = id_key_pos->second;
|
||||
if (app_id.ends_with(".app.elf")) {
|
||||
app::registerElfApp(app_id);
|
||||
app_id = app::getElfAppId(app_id);
|
||||
} else if (!app::findAppById(app_id.c_str())) {
|
||||
TT_LOG_W(TAG, "[400] /app/run app not found");
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "app not found");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
app::start(app_id);
|
||||
TT_LOG_I(TAG, "[200] /app/run %s", app_id.c_str());
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
std::string boundary;
|
||||
if (!network::getMultiPartBoundaryOrSendError(request, boundary)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t content_left = request->content_len;
|
||||
|
||||
// Skip newline after reading boundary
|
||||
auto content_headers_data = network::receiveTextUntil(request, "\r\n\r\n");
|
||||
content_left -= content_headers_data.length();
|
||||
auto content_headers = string::split(content_headers_data, "\r\n")
|
||||
| std::views::filter([](const std::string& line) {
|
||||
return line.length() > 0;
|
||||
})
|
||||
| std::ranges::to<std::vector>();
|
||||
|
||||
auto content_disposition_map = network::parseContentDisposition(content_headers);
|
||||
if (content_disposition_map.empty()) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: invalid content disposition");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
auto name_entry = content_disposition_map.find("name");
|
||||
auto filename_entry = content_disposition_map.find("filename");
|
||||
if (
|
||||
name_entry == content_disposition_map.end() ||
|
||||
filename_entry == content_disposition_map.end() ||
|
||||
name_entry->second != "elf"
|
||||
) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: name or filename parameter missing or mismatching");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Receive file
|
||||
size_t content_read;
|
||||
auto part_after_file = std::format("\r\n--{}--\r\n", boundary);
|
||||
auto file_size = content_left - part_after_file.length();
|
||||
auto buffer = network::receiveByteArray(request, file_size, content_read);
|
||||
if (content_read != file_size) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: file data not received");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
content_left -= content_read;
|
||||
|
||||
// Write file
|
||||
auto file_path = std::format("/sdcard/{}", filename_entry->second);
|
||||
auto* file = fopen(file_path.c_str(), "wb");
|
||||
auto file_bytes_written = fwrite(buffer.get(), 1, file_size, file);
|
||||
fclose(file);
|
||||
if (file_bytes_written != file_size) {
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Read and verify part
|
||||
if (!network::readAndDiscardOrSendError(request, part_after_file)) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
content_left -= part_after_file.length();
|
||||
|
||||
if (content_left != 0) {
|
||||
TT_LOG_W(TAG, "We have more bytes at the end of the request parsing?!");
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "[200] /app/install -> %s", file_path.c_str());
|
||||
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
std::shared_ptr<DevelopmentService> findService() {
|
||||
return std::static_pointer_cast<DevelopmentService>(
|
||||
findServiceById(manifest.id)
|
||||
);
|
||||
}
|
||||
|
||||
extern const ServiceManifest manifest = {
|
||||
.id = "Development",
|
||||
.createService = create<DevelopmentService>
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -1,5 +1,6 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <lwip/esp_netif_net_stack.h>
|
||||
#include "Tactility/service/wifi/Wifi.h"
|
||||
|
||||
#include "Tactility/TactilityHeadless.h"
|
||||
@ -12,6 +13,7 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <Tactility/kernel/SystemEvents.h>
|
||||
#include <sys/cdefs.h>
|
||||
|
||||
namespace tt::service::wifi {
|
||||
@ -71,6 +73,7 @@ public:
|
||||
};
|
||||
bool pause_auto_connect = false; // Pause when manually disconnecting until manually connecting again
|
||||
bool connection_target_remember = false; // Whether to store the connection_target on successful connection or not
|
||||
esp_netif_ip_info_t ip_info;
|
||||
|
||||
RadioState getRadioState() const {
|
||||
auto lock = dataMutex.asScopedLock();
|
||||
@ -230,6 +233,19 @@ void disconnect() {
|
||||
getMainDispatcher().dispatch([wifi]() { dispatchDisconnectButKeepActive(wifi); });
|
||||
}
|
||||
|
||||
void clearIp() {
|
||||
auto wifi = wifi_singleton;
|
||||
if (wifi == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = wifi->dataMutex.asScopedLock();
|
||||
if (!lock.lock(10 / portTICK_PERIOD_MS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&wifi->ip_info, 0, sizeof(esp_netif_ip_info_t));
|
||||
}
|
||||
void setScanRecords(uint16_t records) {
|
||||
TT_LOG_I(TAG, "setScanRecords(%d)", records);
|
||||
auto wifi = wifi_singleton;
|
||||
@ -463,6 +479,7 @@ static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32
|
||||
}
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
TT_LOG_I(TAG, "eventHandler: disconnected");
|
||||
clearIp();
|
||||
switch (wifi->getRadioState()) {
|
||||
case RadioState::ConnectionPending:
|
||||
wifi->connection_wait_flags.set(WIFI_FAIL_BIT);
|
||||
@ -476,8 +493,10 @@ static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32
|
||||
}
|
||||
wifi->setRadioState(RadioState::On);
|
||||
publish_event_simple(wifi, EventType::Disconnected);
|
||||
kernel::publishSystemEvent(kernel::SystemEvent::NetworkDisconnected);
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
auto* event = static_cast<ip_event_got_ip_t*>(event_data);
|
||||
memcpy(&wifi->ip_info, &event->ip_info, sizeof(esp_netif_ip_info_t));
|
||||
TT_LOG_I(TAG, "eventHandler: got ip:" IPSTR, IP2STR(&event->ip_info.ip));
|
||||
if (wifi->getRadioState() == RadioState::ConnectionPending) {
|
||||
wifi->connection_wait_flags.set(WIFI_CONNECTED_BIT);
|
||||
@ -485,6 +504,7 @@ static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32
|
||||
// TODO: Make thread-safe
|
||||
wifi->pause_auto_connect = false; // Resume auto-connection
|
||||
}
|
||||
kernel::publishSystemEvent(kernel::SystemEvent::NetworkConnected);
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) {
|
||||
auto* event = static_cast<wifi_event_sta_scan_done_t*>(event_data);
|
||||
TT_LOG_I(TAG, "eventHandler: wifi scanning done (scan id %u)", event->scan_id);
|
||||
@ -874,6 +894,15 @@ void onAutoConnectTimer() {
|
||||
}
|
||||
}
|
||||
|
||||
std::string getIp() {
|
||||
auto wifi = std::static_pointer_cast<Wifi>(wifi_singleton);
|
||||
|
||||
auto lock = wifi->dataMutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
return std::format("{}.{}.{}.{}", IP2STR(&wifi->ip_info.ip));
|
||||
}
|
||||
|
||||
class WifiService final : public Service {
|
||||
|
||||
public:
|
||||
|
||||
@ -135,6 +135,10 @@ int getRssi() {
|
||||
}
|
||||
}
|
||||
|
||||
std::string getIp() {
|
||||
return "192.168.1.2";
|
||||
}
|
||||
|
||||
// endregion Public functions
|
||||
|
||||
class WifiService final : public Service {
|
||||
|
||||
@ -36,7 +36,7 @@ bool contains(const char* ssid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool key_exists = nvs_find_key(handle, ssid, NULL) == ESP_OK;
|
||||
bool key_exists = nvs_find_key(handle, ssid, nullptr) == ESP_OK;
|
||||
credentials_nvs_close(handle);
|
||||
|
||||
return key_exists;
|
||||
|
||||
@ -19,13 +19,76 @@
|
||||
#include "tt_timer.h"
|
||||
#include "tt_wifi.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <ctype.h>
|
||||
#include <private/elf_symbol.h>
|
||||
#include <esp_log.h>
|
||||
#include <cassert>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Hidden functions work-around
|
||||
extern void* _Znwj(uint32_t size);
|
||||
extern void _ZdlPvj(void* p, uint64_t size);
|
||||
extern double __adddf3(double a, double b);
|
||||
extern double __subdf3(double a, double b);
|
||||
extern double __muldf3 (double a, double b);
|
||||
extern double __divdf3 (double a, double b);
|
||||
extern int __nedf2 (double a, double b);
|
||||
|
||||
const struct esp_elfsym elf_symbols[] {
|
||||
// Hidden functions work-around
|
||||
ESP_ELFSYM_EXPORT(_ZdlPvj), // new?
|
||||
ESP_ELFSYM_EXPORT(_Znwj), // delete?
|
||||
ESP_ELFSYM_EXPORT(__adddf3), // Routines for floating point emulation:
|
||||
ESP_ELFSYM_EXPORT(__subdf3), // See https://gcc.gnu.org/onlinedocs/gccint/Soft-float-library-routines.html
|
||||
ESP_ELFSYM_EXPORT(__muldf3),
|
||||
ESP_ELFSYM_EXPORT(__nedf2),
|
||||
ESP_ELFSYM_EXPORT(__divdf3),
|
||||
// <cassert>
|
||||
ESP_ELFSYM_EXPORT(__assert_func),
|
||||
// <cstdio>
|
||||
ESP_ELFSYM_EXPORT(fclose),
|
||||
ESP_ELFSYM_EXPORT(feof),
|
||||
ESP_ELFSYM_EXPORT(ferror),
|
||||
ESP_ELFSYM_EXPORT(fflush),
|
||||
ESP_ELFSYM_EXPORT(fgetc),
|
||||
ESP_ELFSYM_EXPORT(fgetpos),
|
||||
ESP_ELFSYM_EXPORT(fgets),
|
||||
ESP_ELFSYM_EXPORT(fopen),
|
||||
ESP_ELFSYM_EXPORT(fputc),
|
||||
ESP_ELFSYM_EXPORT(fputs),
|
||||
ESP_ELFSYM_EXPORT(fprintf),
|
||||
ESP_ELFSYM_EXPORT(fread),
|
||||
ESP_ELFSYM_EXPORT(fseek),
|
||||
ESP_ELFSYM_EXPORT(fsetpos),
|
||||
ESP_ELFSYM_EXPORT(fscanf),
|
||||
ESP_ELFSYM_EXPORT(ftell),
|
||||
ESP_ELFSYM_EXPORT(fwrite),
|
||||
ESP_ELFSYM_EXPORT(getc),
|
||||
ESP_ELFSYM_EXPORT(putc),
|
||||
ESP_ELFSYM_EXPORT(puts),
|
||||
ESP_ELFSYM_EXPORT(printf),
|
||||
ESP_ELFSYM_EXPORT(sscanf),
|
||||
ESP_ELFSYM_EXPORT(snprintf),
|
||||
ESP_ELFSYM_EXPORT(sprintf),
|
||||
ESP_ELFSYM_EXPORT(vsprintf),
|
||||
// cstring
|
||||
ESP_ELFSYM_EXPORT(strlen),
|
||||
ESP_ELFSYM_EXPORT(strcmp),
|
||||
ESP_ELFSYM_EXPORT(strncpy),
|
||||
ESP_ELFSYM_EXPORT(strcpy),
|
||||
ESP_ELFSYM_EXPORT(strcat),
|
||||
ESP_ELFSYM_EXPORT(strstr),
|
||||
ESP_ELFSYM_EXPORT(memset),
|
||||
ESP_ELFSYM_EXPORT(memcpy),
|
||||
// ctype
|
||||
ESP_ELFSYM_EXPORT(isdigit),
|
||||
// ESP-IDF
|
||||
ESP_ELFSYM_EXPORT(esp_log_write),
|
||||
ESP_ELFSYM_EXPORT(esp_log_timestamp),
|
||||
// Tactility
|
||||
ESP_ELFSYM_EXPORT(tt_app_register),
|
||||
ESP_ELFSYM_EXPORT(tt_app_get_parameters),
|
||||
@ -139,7 +202,10 @@ const struct esp_elfsym elf_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(lv_event_get_user_data),
|
||||
ESP_ELFSYM_EXPORT(lv_event_get_target_obj),
|
||||
ESP_ELFSYM_EXPORT(lv_event_get_target),
|
||||
ESP_ELFSYM_EXPORT(lv_event_get_current_target_obj),
|
||||
// lv_obj
|
||||
ESP_ELFSYM_EXPORT(lv_obj_create),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_delete),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_add_event_cb),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_align),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_align_to),
|
||||
@ -157,7 +223,13 @@ const struct esp_elfsym elf_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(lv_obj_remove_event_cb),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_get_user_data),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_user_data),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_remove_flag),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_add_flag),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_pos),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_flex_align),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_flex_flow),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_flex_grow),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_bg_color),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_margin_hor),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_margin_ver),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_margin_top),
|
||||
@ -172,19 +244,36 @@ const struct esp_elfsym elf_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_bottom),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_left),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_right),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_column),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_row),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_border_width),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_border_opa),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_border_post),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_border_side),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_border_color),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_align),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_x),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_y),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_size),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_width),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_height),
|
||||
ESP_ELFSYM_EXPORT(lv_theme_get_color_primary),
|
||||
ESP_ELFSYM_EXPORT(lv_theme_get_color_secondary),
|
||||
// lv_button
|
||||
ESP_ELFSYM_EXPORT(lv_button_create),
|
||||
// lv_buttonmatrix
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_create),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_button_text),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_map),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_one_checked),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_selected_button),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_button_ctrl),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_button_ctrl_all),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_ctrl_map),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_map),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_one_checked),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_button_width),
|
||||
ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_selected_button),
|
||||
// lv_label
|
||||
ESP_ELFSYM_EXPORT(lv_label_create),
|
||||
ESP_ELFSYM_EXPORT(lv_label_cut_text),
|
||||
@ -251,6 +340,18 @@ const struct esp_elfsym elf_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(lv_textarea_set_placeholder_text),
|
||||
ESP_ELFSYM_EXPORT(lv_textarea_set_text),
|
||||
ESP_ELFSYM_EXPORT(lv_textarea_set_text_selection),
|
||||
// lv_palette
|
||||
ESP_ELFSYM_EXPORT(lv_palette_main),
|
||||
ESP_ELFSYM_EXPORT(lv_palette_darken),
|
||||
ESP_ELFSYM_EXPORT(lv_palette_lighten),
|
||||
// lv_display
|
||||
ESP_ELFSYM_EXPORT(lv_display_get_horizontal_resolution),
|
||||
ESP_ELFSYM_EXPORT(lv_display_get_vertical_resolution),
|
||||
ESP_ELFSYM_EXPORT(lv_display_get_physical_horizontal_resolution),
|
||||
ESP_ELFSYM_EXPORT(lv_display_get_physical_vertical_resolution),
|
||||
// lv_pct
|
||||
ESP_ELFSYM_EXPORT(lv_pct),
|
||||
ESP_ELFSYM_EXPORT(lv_pct_to_px),
|
||||
// delimiter
|
||||
ESP_ELFSYM_END
|
||||
};
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
// Alloc
|
||||
#define LOG_MESSAGE_ALLOC_FAILED "Memory allocation failed"
|
||||
|
||||
// Mutex
|
||||
#define LOG_MESSAGE_MUTEX_LOCK_FAILED "Mutex acquisition timeout"
|
||||
#define LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT "Mutex acquisition timeout (%s)"
|
||||
|
||||
@ -64,4 +64,12 @@ bool isAsciiHexString(const std::string& input);
|
||||
/** @return the first part of a file name right up (and excluding) the first period character. */
|
||||
std::string removeFileExtension(const std::string& input);
|
||||
|
||||
/**
|
||||
* Remove the given characters from the start and end of the specified string.
|
||||
* @param[in] input the text to trim
|
||||
* @param[in] characters the characters to remove from the input
|
||||
* @return the input where the specified characters are removed from the start and end of the input string
|
||||
*/
|
||||
std::string trim(const std::string& input, const std::string& characters);
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -13,7 +13,6 @@ class Timer {
|
||||
public:
|
||||
|
||||
typedef std::function<void()> Callback;
|
||||
// typedef std::function<void(uint32_t)> PendingCallback;
|
||||
typedef void (*PendingCallback)(void* context, uint32_t arg);
|
||||
|
||||
private:
|
||||
|
||||
@ -96,4 +96,14 @@ bool isAsciiHexString(const std::string& input) {
|
||||
}).empty();
|
||||
}
|
||||
|
||||
std::string trim(const std::string& input, const std::string& characters) {
|
||||
auto index = input.find_first_not_of(characters);
|
||||
if (index == std::string::npos) {
|
||||
return "";
|
||||
} else {
|
||||
auto end_index = input.find_last_not_of(characters);
|
||||
return input.substr(index, end_index - index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
61
Tests/Tactility/UrlTest.cpp
Normal file
61
Tests/Tactility/UrlTest.cpp
Normal file
@ -0,0 +1,61 @@
|
||||
#include "doctest.h"
|
||||
#include <Tactility/network/Url.h>
|
||||
|
||||
using namespace tt;
|
||||
|
||||
TEST_CASE("parseUrlQuery can handle a single key-value pair") {
|
||||
auto map = network::parseUrlQuery("?key=value");
|
||||
CHECK_EQ(map.size(), 1);
|
||||
CHECK_EQ(map["key"], "value");
|
||||
}
|
||||
|
||||
TEST_CASE("parseUrlQuery can handle empty value in the middle") {
|
||||
auto map = network::parseUrlQuery("?a=1&b=&c=3");
|
||||
CHECK_EQ(map.size(), 3);
|
||||
CHECK_EQ(map["a"], "1");
|
||||
CHECK_EQ(map["b"], "");
|
||||
CHECK_EQ(map["c"], "3");
|
||||
}
|
||||
|
||||
TEST_CASE("parseUrlQuery can handle empty value at the end") {
|
||||
auto map = network::parseUrlQuery("?a=1&b=");
|
||||
CHECK_EQ(map.size(), 2);
|
||||
CHECK_EQ(map["a"], "1");
|
||||
CHECK_EQ(map["b"], "");
|
||||
}
|
||||
|
||||
TEST_CASE("parseUrlQuery returns empty map when query s questionmark with a key without a value") {
|
||||
auto map = network::parseUrlQuery("?a");
|
||||
CHECK_EQ(map.size(), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("parseUrlQuery returns empty map when query is a questionmark") {
|
||||
auto map = network::parseUrlQuery("?");
|
||||
CHECK_EQ(map.size(), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("parseUrlQuery should url-decode the value") {
|
||||
auto map = network::parseUrlQuery("?key=Test%21Test");
|
||||
CHECK_EQ(map.size(), 1);
|
||||
CHECK_EQ(map["key"], "Test!Test");
|
||||
}
|
||||
|
||||
TEST_CASE("parseUrlQuery should url-decode the key") {
|
||||
auto map = network::parseUrlQuery("?Test%21Test=value");
|
||||
CHECK_EQ(map.size(), 1);
|
||||
CHECK_EQ(map["Test!Test"], "value");
|
||||
}
|
||||
|
||||
TEST_CASE("urlDecode") {
|
||||
auto input = std::string("prefix!*'();:@&=+$,/?#[]<>%-.^_`{}|~ \\");
|
||||
auto expected = std::string("prefix%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D%3C%3E%25-.%5E_%60%7B%7D%7C~+%5C");
|
||||
auto encoded = network::urlEncode(input);
|
||||
CHECK_EQ(encoded, expected);
|
||||
}
|
||||
|
||||
TEST_CASE("urlDecode") {
|
||||
auto input = std::string("prefix%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D%3C%3E%25-.%5E_%60%7B%7D%7C~+%5C");
|
||||
auto expected = std::string("prefix!*'();:@&=+$,/?#[]<>%-.^_`{}|~ \\");
|
||||
auto decoded = network::urlDecode(input);
|
||||
CHECK_EQ(decoded, expected);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user