Tactiliest/Tactility/Source/service/development/DevelopmentService.cpp
Ken Van Hoeylandt 5cc5b50694
Merge develop into main (#312)
- Move various settings to `/data/settings` and `/sdcard/settings`
- Fix for `Gui` and `Statusbar` errors on startup (LVGL start)
- Implement Development service settings as properties file
- Rename `service::findManifestId()` to `service::findManifestById()`
- Renamed various classes like `BootProperties` to `BootSettings`
- Renamed `settings.properties` to `system.properties`. Code was moved to `settings` namespace/folder
- `DevelopmentSettings` is now in `settings` namespace/folder (moved from service)
2025-08-31 20:31:31 +02:00

296 lines
8.7 KiB
C++

#ifdef ESP_PLATFORM
#include <Tactility/service/development/DevelopmentService.h>
#include <Tactility/app/App.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/app/ElfApp.h>
#include <Tactility/file/File.h>
#include <Tactility/network/HttpdReq.h>
#include <Tactility/network/Url.h>
#include <Tactility/service/development/DevelopmentSettings.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/StringUtils.h>
#include <Tactility/TactilityHeadless.h>
#include <cstring>
#include <esp_wifi.h>
#include <ranges>
#include <sstream>
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(shouldEnableOnBoot());
}
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;
}
// 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")) {
if (!file::isFile(app_id)) {
TT_LOG_W(TAG, "[400] /app/run cannot find app %s", app_id.c_str());
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "app not found");
return ESP_FAIL;
}
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