diff --git a/Tactility/Private/Tactility/app/development/Development.h b/Tactility/Private/Tactility/app/development/Development.h new file mode 100644 index 00000000..77ec9363 --- /dev/null +++ b/Tactility/Private/Tactility/app/development/Development.h @@ -0,0 +1,7 @@ +#pragma once + +namespace tt::app::development { + +void start(); + +} diff --git a/Tactility/Private/Tactility/service/development/Development.h b/Tactility/Private/Tactility/service/development/DevelopmentService.h similarity index 70% rename from Tactility/Private/Tactility/service/development/Development.h rename to Tactility/Private/Tactility/service/development/DevelopmentService.h index 7f89b92b..6e28e2b9 100644 --- a/Tactility/Private/Tactility/service/development/Development.h +++ b/Tactility/Private/Tactility/service/development/DevelopmentService.h @@ -11,7 +11,7 @@ namespace tt::service::development { -class Development final : public Service { +class DevelopmentService final : public Service { Mutex mutex = Mutex(Mutex::Type::Recursive); httpd_handle_t server = nullptr; @@ -62,16 +62,35 @@ public: // 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); - bool isEnabled() const; + /** + * @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 findService(); +std::shared_ptr findService(); } diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 9a7ba056..4f8ba1cd 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -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,11 +74,13 @@ 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); addApp(app::applist::manifest); addApp(app::calculator::manifest); + addApp(app::development::manifest); addApp(app::display::manifest); addApp(app::filebrowser::manifest); addApp(app::fileselection::manifest); diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp new file mode 100644 index 00000000..cc6ee31c --- /dev/null +++ b/Tactility/Source/app/development/Development.cpp @@ -0,0 +1,156 @@ +#include "Tactility/app/AppManifest.h" +#include "Tactility/lvgl/Style.h" +#include "Tactility/lvgl/Toolbar.h" +#include "Tactility/service/development/DevelopmentService.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace tt::app::development { + +constexpr const char* TAG = "AddGps"; + +class DevelopmentApp final : public App { + + lv_obj_t* enableSwitch = nullptr; + lv_obj_t* enableOnBootSwitch = nullptr; + lv_obj_t* statusLabel = nullptr; + std::shared_ptr 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_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(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_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(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 { + timer.stop(); + } +}; + +extern const AppManifest manifest = { + .id = "Development", + .name = "Development", + .type = Type::Settings, + .createApp = create +}; + +void start() { + app::start(manifest.id); +} + +} // namespace \ No newline at end of file diff --git a/Tactility/Source/service/development/Development.cpp b/Tactility/Source/service/development/DevelopmentService.cpp similarity index 70% rename from Tactility/Source/service/development/Development.cpp rename to Tactility/Source/service/development/DevelopmentService.cpp index a6de8499..f163c9b0 100644 --- a/Tactility/Source/service/development/Development.cpp +++ b/Tactility/Source/service/development/DevelopmentService.cpp @@ -1,14 +1,16 @@ #ifdef ESP_PLATFORM -#include "Tactility/service/development/Development.h" +#include "Tactility/service/development/DevelopmentService.h" #include "Tactility/TactilityHeadless.h" #include "Tactility/service/ServiceManifest.h" #include "Tactility/service/ServiceRegistry.h" +#include "Tactility/service/wifi/Wifi.h" #include #include #include +#include namespace tt::service::development { @@ -24,14 +26,14 @@ static char* rest_read_buffer(httpd_req_t* request) { if (contentLength >= 1024) { // Respond with 500 Internal Server Error httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "content too long"); - return NULL; + return nullptr; } while (currentLength < contentLength) { received = httpd_req_recv(request, buffer + currentLength, contentLength); if (received <= 0) { // Respond with 500 Internal Server Error httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to post control value"); - return NULL; + return nullptr; } currentLength += received; } @@ -39,7 +41,7 @@ static char* rest_read_buffer(httpd_req_t* request) { return buffer; } -void Development::onStart(ServiceContext& service) { +void DevelopmentService::onStart(ServiceContext& service) { auto lock = mutex.asScopedLock(); lock.lock(); @@ -52,10 +54,10 @@ void Development::onStart(ServiceContext& service) { [this](kernel::SystemEvent) { onNetworkDisconnected(); } ); - setEnabled(true); + setEnabled(isEnabledOnStart()); } -void Development::onStop(ServiceContext& service) { +void DevelopmentService::onStop(ServiceContext& service) { auto lock = mutex.asScopedLock(); lock.lock(); @@ -69,21 +71,41 @@ void Development::onStop(ServiceContext& service) { // region Enable/disable -void Development::setEnabled(bool enabled) { +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 Development::isEnabled() const { +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 Development::startServer() { +void DevelopmentService::startServer() { auto lock = mutex.asScopedLock(); lock.lock(); @@ -117,7 +139,7 @@ void Development::startServer() { } } -void Development::stopServer() { +void DevelopmentService::stopServer() { auto lock = mutex.asScopedLock(); lock.lock(); @@ -133,13 +155,13 @@ void Development::stopServer() { server = nullptr; } -bool Development::isStarted() const { +bool DevelopmentService::isStarted() const { auto lock = mutex.asScopedLock(); lock.lock(); return server != nullptr; } -void Development::onNetworkConnected() { +void DevelopmentService::onNetworkConnected() { TT_LOG_I(TAG, "onNetworkConnected"); mutex.withLock([this] { if (isEnabled() && !isStarted()) { @@ -148,7 +170,7 @@ void Development::onNetworkConnected() { }); } -void Development::onNetworkDisconnected() { +void DevelopmentService::onNetworkDisconnected() { TT_LOG_I(TAG, "onNetworkDisconnected"); mutex.withLock([this] { if (isStarted()) { @@ -159,13 +181,13 @@ void Development::onNetworkDisconnected() { // region endpoints -esp_err_t Development::handleGetInfo(httpd_req_t* request) { +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(request->user_ctx); + auto* service = static_cast(request->user_ctx); if (httpd_resp_sendstr(request, service->deviceResponse.c_str()) != ESP_OK) { TT_LOG_W(TAG, "Failed to send response body"); @@ -176,13 +198,13 @@ esp_err_t Development::handleGetInfo(httpd_req_t* request) { return ESP_OK; } -esp_err_t Development::handleAppRun(httpd_req_t* request) { +esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) { httpd_resp_send(request, nullptr, 0); TT_LOG_I(TAG, "[200] /app/run"); return ESP_OK; } -esp_err_t Development::handleAppInstall(httpd_req_t* request) { +esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { httpd_resp_send(request, nullptr, 0); TT_LOG_I(TAG, "[200] /app/install"); return ESP_OK; @@ -190,15 +212,15 @@ esp_err_t Development::handleAppInstall(httpd_req_t* request) { // endregion -std::shared_ptr findService() { - return std::static_pointer_cast( +std::shared_ptr findService() { + return std::static_pointer_cast( findServiceById(manifest.id) ); } extern const ServiceManifest manifest = { .id = "Development", - .createService = create + .createService = create }; }