diff --git a/docs/ui/screen_bypass.svg b/docs/ui/screen_bypass.svg new file mode 100644 index 0000000..6d3591b --- /dev/null +++ b/docs/ui/screen_bypass.svg @@ -0,0 +1,8 @@ + + + + ORBIT ECHO + + + BYPASSED + diff --git a/docs/ui/screen_feedback.svg b/docs/ui/screen_feedback.svg new file mode 100644 index 0000000..2a5c60b --- /dev/null +++ b/docs/ui/screen_feedback.svg @@ -0,0 +1,15 @@ + + + + ORBIT ECHO + + + FEEDBACK + + 50% + + + + + + diff --git a/docs/ui/screen_mix.svg b/docs/ui/screen_mix.svg new file mode 100644 index 0000000..e1e6615 --- /dev/null +++ b/docs/ui/screen_mix.svg @@ -0,0 +1,15 @@ + + + + ORBIT ECHO + + + MIX + + 35% + + + + + + diff --git a/targets/embedded_esp32s3/main/app_main.cpp b/targets/embedded_esp32s3/main/app_main.cpp index 7957090..c1f12b9 100644 --- a/targets/embedded_esp32s3/main/app_main.cpp +++ b/targets/embedded_esp32s3/main/app_main.cpp @@ -130,16 +130,10 @@ void audioCallback(void* userData, const int32_t* inInterleaved, int32_t* outInt } void uiTick(void* userData) { - auto* app = static_cast(userData); - static uint32_t tick = 0; - ++tick; - - AudioParams p; - p.mix = 0.30f + 0.15f * ((tick / 120) % 2); - p.feedback = 0.40f; - p.toneHz = 6500.0f; - p.smearAmount = 0.20f; - app->params.publish(p); + // Opcional: A UI task já trata seus inputs e manda os parâmetros pro bridge. + // O uiTick agora poderia ser usado apenas para side-effects ou heartbeats, + // mas o loop principal da UI task gerencia o estado através de UiTft::updateState(). + // Removendo a lógica dummy que alterava os parâmetros. } } // namespace @@ -190,6 +184,7 @@ extern "C" void app_main(void) { uiCfg.refreshPeriodMs = board::ui::kRefreshPeriodMs; uiCfg.core = board::ui::kTaskCore; uiCfg.priority = board::ui::kTaskPriority; + uiCfg.paramBridge = &app.params; if (!app.ui.start(uiCfg, uiTick, &app)) { ESP_LOGW(kTag, "UI não iniciou; áudio segue ativo"); diff --git a/targets/embedded_esp32s3/main/ui_tft.cpp b/targets/embedded_esp32s3/main/ui_tft.cpp index 98ceac5..e7c23e0 100644 --- a/targets/embedded_esp32s3/main/ui_tft.cpp +++ b/targets/embedded_esp32s3/main/ui_tft.cpp @@ -3,6 +3,11 @@ #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "driver/gpio.h" +#include "board_config.h" + +#include +#include namespace orbit::embedded { namespace { @@ -22,6 +27,21 @@ bool UiTft::start(const Config& config, UiTickCallback callback, void* userData) callback_ = callback; userData_ = userData; + // Configuração dos GPIOs para o Encoder e Botão Bypass + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << board::encoder::kA) | + (1ULL << board::encoder::kB) | + (1ULL << board::encoder::kSwitch) | + (1ULL << board::controls::kBypassButton); + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + gpio_config(&io_conf); + + // Incializa o estado do encoder + lastEncoderA_ = gpio_get_level(static_cast(board::encoder::kA)); + if (!stoppedSignal_) { stoppedSignal_ = xSemaphoreCreateBinary(); if (!stoppedSignal_) { @@ -72,14 +92,139 @@ void UiTft::taskEntry(void* ctx) { static_cast(ctx)->taskLoop(); } +void UiTft::processInputs() { + uint32_t now = xTaskGetTickCount() * portTICK_PERIOD_MS; + + // Encoder - Leitura baseada em polling de quadratura + int a = gpio_get_level(static_cast(board::encoder::kA)); + int b = gpio_get_level(static_cast(board::encoder::kB)); + + if (a != lastEncoderA_) { + if (a == 0) { // Na borda de descida de A + if (b == 1) { + encoderPos_++; // CW + } else { + encoderPos_--; // CCW + } + } + lastEncoderA_ = a; + } + + // Encoder Switch (Debounced) + bool switchState = gpio_get_level(static_cast(board::encoder::kSwitch)); + if (switchState != lastSwitchState_) { + if (now - switchDebounceTime_ > 50) { // 50ms debounce + if (switchState == 0) { // Pressionado (pull-up) + switchPressed_ = true; + } + lastSwitchState_ = switchState; + switchDebounceTime_ = now; + } + } + + // Bypass Button (Debounced) + bool bypassState = gpio_get_level(static_cast(board::controls::kBypassButton)); + if (bypassState != lastBypassState_) { + if (now - bypassDebounceTime_ > 50) { // 50ms debounce + if (bypassState == 0) { // Pressionado (pull-up) + bypassPressed_ = true; + } + lastBypassState_ = bypassState; + bypassDebounceTime_ = now; + } + } +} + +void UiTft::updateState() { + bool paramsUpdated = false; + + // Trata o botão Bypass + if (bypassPressed_) { + isBypassed_ = !isBypassed_; + bypassPressed_ = false; + + // Em caso de bypass, podemos mudar o readMode ou zerar o mix. + // Aqui assumimos zerar o mix como forma de bypass. + if (isBypassed_) { + preBypassMix_ = currentParams_.mix; + currentParams_.mix = 0.0f; + } else { + currentParams_.mix = preBypassMix_; + } + paramsUpdated = true; + } + + // Trata o botão do Encoder (Muda a página) + if (switchPressed_) { + int nextPage = static_cast(currentPage_) + 1; + if (nextPage >= static_cast(Page::Count)) { + nextPage = 0; + } + currentPage_ = static_cast(nextPage); + switchPressed_ = false; + } + + // Trata a rotação do Encoder (Muda os valores da página atual) + if (encoderPos_ != 0) { + if (!isBypassed_) { // Ignora rotações no bypass, ou desabilita bypass? + switch (currentPage_) { + case Page::Mix: + currentParams_.mix = std::clamp(currentParams_.mix + (encoderPos_ * 0.05f), 0.0f, 1.0f); + break; + case Page::Feedback: + currentParams_.feedback = std::clamp(currentParams_.feedback + (encoderPos_ * 0.05f), 0.0f, 1.0f); + break; + case Page::Time: + // 24000.0f is kMaxDelaySamples. It would be better to share it, but for now we clamp against it. + currentParams_.offsetSamples = std::clamp(currentParams_.offsetSamples + (encoderPos_ * 1000.0f), 0.0f, 24000.0f); + break; + case Page::Tone: + currentParams_.toneHz = std::clamp(currentParams_.toneHz + (encoderPos_ * 500.0f), 200.0f, 20000.0f); + break; + default: + break; + } + paramsUpdated = true; + } + encoderPos_ = 0; + } + + // Publica se houve mudança real e o ParameterBridge estiver configurado + if (paramsUpdated && config_.paramBridge) { + config_.paramBridge->publish(currentParams_); + } +} + +void UiTft::drawDisplay() { + // Stub: Simular o desenho do display + // Em uma implementação real usaria SPI para enviar os dados da tela +} + + void UiTft::taskLoop() { const TickType_t periodTicks = pdMS_TO_TICKS(config_.refreshPeriodMs); TickType_t lastWake = xTaskGetTickCount(); + // Sincroniza parâmetros iniciais + if (config_.paramBridge) { + // Se o paramBridge tivesse método getter, a gente puxava. + // Vamos inicializar com alguns valores padrão conhecidos, + // alinhados com o estado default da struct AudioParams + currentParams_.mix = 0.35f; + currentParams_.feedback = 0.35f; + currentParams_.offsetSamples = 1200.0f; + currentParams_.toneHz = 8000.0f; + } + while (running_) { + processInputs(); + updateState(); + drawDisplay(); + if (callback_) { callback_(userData_); } + vTaskDelayUntil(&lastWake, periodTicks); } diff --git a/targets/embedded_esp32s3/main/ui_tft.h b/targets/embedded_esp32s3/main/ui_tft.h index dff7740..50e849b 100644 --- a/targets/embedded_esp32s3/main/ui_tft.h +++ b/targets/embedded_esp32s3/main/ui_tft.h @@ -5,6 +5,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" +#include "parameter_bridge.h" namespace orbit::embedded { @@ -15,6 +16,7 @@ class UiTft { uint32_t stackWords = 4096; uint32_t priority = 3; int core = 1; + ParameterBridge* paramBridge = nullptr; }; using UiTickCallback = void (*)(void* userData); @@ -29,12 +31,42 @@ class UiTft { static void taskEntry(void* ctx); void taskLoop(); + void processInputs(); + void updateState(); + void drawDisplay(); + Config config_{}; UiTickCallback callback_ = nullptr; void* userData_ = nullptr; TaskHandle_t taskHandle_ = nullptr; SemaphoreHandle_t stoppedSignal_ = nullptr; bool running_ = false; + + // Encoder State + int lastEncoderA_ = 1; + int encoderPos_ = 0; + bool switchPressed_ = false; + bool lastSwitchState_ = true; + uint32_t switchDebounceTime_ = 0; + + // Bypass State + bool bypassPressed_ = false; + bool isBypassed_ = false; + bool lastBypassState_ = true; + uint32_t bypassDebounceTime_ = 0; + float preBypassMix_ = 0.35f; + + // UI Parameters + enum class Page { + Mix = 0, + Feedback, + Time, + Tone, + Count + }; + Page currentPage_ = Page::Mix; + + AudioParams currentParams_; }; } // namespace orbit::embedded