#ifdef ESP_PLATFORM #include #include #include #include #include #include #include #include #include #include #include #include #include namespace tt::service::development { extern const ServiceManifest manifest; constexpr auto* TAG = "DevService"; bool DevelopmentService::onStart(ServiceContext& service) { std::stringstream stream; stream << "{"; stream << "\"cpuFamily\":\"" << CONFIG_IDF_TARGET << "\", "; stream << "\"osVersion\":\"" << TT_VERSION << "\", "; stream << "\"protocolVersion\":\"1.0.0\""; stream << "}"; deviceResponse = stream.str(); setEnabled(shouldEnableOnBoot()); return true; } void DevelopmentService::onStop(ServiceContext& service) { setEnabled(false); } // region Enable/disable void DevelopmentService::setEnabled(bool enabled) { auto lock = mutex.asScopedLock(); lock.lock(); if (enabled) { if (!httpServer.isStarted()) { httpServer.start(); } } else { if (httpServer.isStarted()) { httpServer.stop(); } } } bool DevelopmentService::isEnabled() const { auto lock = mutex.asScopedLock(); lock.lock(); return httpServer.isStarted(); } // region endpoints esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) { LOG_I(TAG, "GET /device"); if (httpd_resp_set_type(request, "application/json") != ESP_OK) { LOG_W(TAG, "Failed to send header"); return ESP_FAIL; } auto* service = static_cast(request->user_ctx); if (httpd_resp_sendstr(request, service->deviceResponse.c_str()) != ESP_OK) { LOG_W(TAG, "Failed to send response body"); return ESP_FAIL; } LOG_I(TAG, "[200] /device"); return ESP_OK; } esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) { LOG_I(TAG, "POST /app/run"); 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()) { LOG_W(TAG, "[400] /app/run id not specified"); httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified"); return ESP_FAIL; } char app_id[32]; AppInstanceId instance_id; // Warning: possible app closure between getting app id and instance id if ( app_manager_get_topmost_app_id(app_id, sizeof(app_id)) == ERROR_NONE && app_manager_get_topmost_instance_id(&instance_id) == ERROR_NONE ) { if (strcmp(id_key_pos->second.c_str(), app_id) == 0) { app_manager_stop(instance_id); } } app_manager_start(id_key_pos->second.c_str(), &instance_id); LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str()); httpd_resp_send(request, nullptr, 0); return ESP_OK; } esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { LOG_I(TAG, "PUT /app/install"); 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(); 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 boundary auto boundary_and_newlines_after_file = std::format("\r\n--{}--\r\n", boundary); auto file_size = content_left - boundary_and_newlines_after_file.length(); // Create tmp directory const std::string tmp_path = getTempPath(); if (!file::findOrCreateDirectory(tmp_path, 0777)) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create temp path"); return ESP_FAIL; } std::string safe_name = file::getLastPathSegment(filename_entry->second); if (safe_name.empty() || safe_name.find("..") != std::string::npos || safe_name.find('/') != std::string::npos || safe_name.find('\\') != std::string::npos) { httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "invalid filename"); return ESP_FAIL; } auto file_path = std::format("{}/{}", tmp_path, safe_name); if (network::receiveFile(request, file_size, file_path) != file_size) { file::deleteFile(file_path); httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file"); return ESP_FAIL; } content_left -= file_size; // Read and verify part if (!network::readAndDiscardOrSendError(request, boundary_and_newlines_after_file)) { return ESP_FAIL; } content_left -= boundary_and_newlines_after_file.length(); if (content_left != 0) { LOG_W(TAG, "We have more bytes at the end of the request parsing?!"); } if (app_install(file_path.c_str()) != ERROR_NONE) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to install"); return ESP_FAIL; } if (!file::deleteFile(file_path)) { LOG_W(TAG, "Failed to delete %s", file_path.c_str()); } LOG_I(TAG, "[200] /app/install -> %s", file_path.c_str()); httpd_resp_send(request, nullptr, 0); return ESP_OK; } esp_err_t DevelopmentService::handleAppUninstall(httpd_req_t* request) { LOG_I(TAG, "PUT /app/uninstall"); 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()) { LOG_W(TAG, "[400] /app/uninstall id not specified"); httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified"); return ESP_FAIL; } AppManifest manifest; if (app_manager_find_manifest(id_key_pos->second.c_str(), &manifest) != ERROR_NONE) { LOG_I(TAG, "[200] /app/uninstall %s (app wasn't installed)", id_key_pos->second.c_str()); httpd_resp_send(request, nullptr, 0); return ESP_OK; } if (app_uninstall(id_key_pos->second.c_str()) == ERROR_NONE) { LOG_I(TAG, "[200] /app/uninstall %s", id_key_pos->second.c_str()); httpd_resp_send(request, nullptr, 0); return ESP_OK; } else { LOG_W(TAG, "[500] /app/uninstall %s", id_key_pos->second.c_str()); httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to uninstall"); return ESP_FAIL; } } // endregion std::shared_ptr findService() { return std::static_pointer_cast( findServiceById(manifest.id) ); } extern const ServiceManifest manifest = { .id = "tactility.development", .createService = create }; } #endif // ESP_PLATFORM