diff --git a/.test/esphome_ard_basic.yaml b/.test/esphome_ard_basic.yaml index dbda8d8..ed32a8f 100644 --- a/.test/esphome_ard_basic.yaml +++ b/.test/esphome_ard_basic.yaml @@ -1,6 +1,6 @@ --- packages: - basic_package: !include ../TX-Ultimate-Easy-ESPHome.yaml # Basic package + basic_package: !include ../ESPHome/TX-Ultimate-Easy-ESPHome_core.yaml # Basic package esp32: framework: diff --git a/.test/esphome_idf53_bluetooth_proxy.yaml b/.test/esphome_idf53_bluetooth_proxy.yaml index 1e8643f..a8d3868 100644 --- a/.test/esphome_idf53_bluetooth_proxy.yaml +++ b/.test/esphome_idf53_bluetooth_proxy.yaml @@ -1,6 +1,6 @@ --- packages: - basic_package: !include ../TX-Ultimate-Easy-ESPHome.yaml # Core package + core_package: !include ../ESPHome/TX-Ultimate-Easy-ESPHome_core.yaml addon_bluetooth_proxy: !include ../ESPHome/TX-Ultimate-Easy-ESPHome_addon_ble_proxy.yaml esp32: diff --git a/.test/esphome_idf_bluetooth_proxy.yaml b/.test/esphome_idf_bluetooth_proxy.yaml index a2f9a0b..3e434a5 100644 --- a/.test/esphome_idf_bluetooth_proxy.yaml +++ b/.test/esphome_idf_bluetooth_proxy.yaml @@ -1,5 +1,5 @@ --- packages: - basic_package: !include ../TX-Ultimate-Easy-ESPHome.yaml # Core package + core_package: !include ../ESPHome/TX-Ultimate-Easy-ESPHome_core.yaml addon_bluetooth_proxy: !include ../ESPHome/TX-Ultimate-Easy-ESPHome_addon_ble_proxy.yaml ... diff --git a/ESPHome/TX-Ultimate-Easy-ESPHome_core.yaml b/ESPHome/TX-Ultimate-Easy-ESPHome_core.yaml index 12ecd7d..5911a02 100644 --- a/ESPHome/TX-Ultimate-Easy-ESPHome_core.yaml +++ b/ESPHome/TX-Ultimate-Easy-ESPHome_core.yaml @@ -74,7 +74,7 @@ improv_serial: id: serial_improv logger: - level: INFO + level: DEBUG ota: platform: esphome diff --git a/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_buttons.yaml b/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_buttons.yaml index 2e8a8a0..20eb5a7 100644 --- a/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_buttons.yaml +++ b/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_buttons.yaml @@ -20,111 +20,53 @@ substitutions: BUTTON_3_ACTION_TEXT: "Relay 3 (toggle)" BUTTON_4_ACTION_TEXT: "Relay 4 (toggle)" -binary_sensor: - - id: bs_button_1 - name: Button 1 - icon: mdi:gesture-tap-box - internal: false - platform: template - on_click: - then: - - script.execute: - id: button_action - component: bs_button_1 - event: click - on_double_click: - then: - - script.execute: - id: button_action - component: bs_button_1 - event: double_click - on_multi_click: - - timing: &long_click-timing - - ON for at least 0.8s - invalid_cooldown: ${invalid_cooldown} - then: - - script.execute: - id: button_action - component: bs_button_1 - event: long_click + BUTTON_CLICK_MIN_LENGTH: '50' # The minimum duration the click should last, in msec + BUTTON_CLICK_MAX_LENGTH: '350' # The maximum duration the click should last, in msec + BUTTON_MULTI_CLICK_DELAY: '250' # The time to wait for another click, in msec + BUTTON_PRESS_TIMEOUT: '10000' # Ignore if button is pressed for longer than this time, in msec + BUTTON_LONG_PRESS_DELAY: '800' # The time to wait to consider a long press, in msec - - id: bs_button_2 +binary_sensor: + - &binary_sensor_button_base + id: bs_button_2 name: Button 2 icon: mdi:gesture-tap-box - internal: true platform: template - on_click: - then: - - script.execute: - id: button_action - component: bs_button_2 - event: click - on_double_click: - then: - - script.execute: - id: button_action - component: bs_button_2 - event: double_click - on_multi_click: - - timing: *long_click-timing - invalid_cooldown: ${invalid_cooldown} - then: - - script.execute: - id: button_action - component: bs_button_2 - event: long_click + internal: true - id: bs_button_3 name: Button 3 - icon: mdi:gesture-tap-box - internal: true - platform: template - on_click: - then: - - script.execute: - id: button_action - component: bs_button_3 - event: click - on_double_click: - then: - - script.execute: - id: button_action - component: bs_button_3 - event: double_click - on_multi_click: - - timing: *long_click-timing - invalid_cooldown: ${invalid_cooldown} - then: - - script.execute: - id: button_action - component: bs_button_3 - event: long_click + <<: *binary_sensor_button_base - id: bs_button_4 name: Button 4 - icon: mdi:gesture-tap-box - internal: true - platform: template - on_click: - then: - - script.execute: - id: button_action - component: bs_button_4 - event: click - on_double_click: - then: - - script.execute: - id: button_action - component: bs_button_4 - event: double_click - on_multi_click: - - timing: *long_click-timing - invalid_cooldown: ${invalid_cooldown} - then: - - script.execute: - id: button_action - component: bs_button_4 - event: long_click + <<: *binary_sensor_button_base + + - id: bs_button_1 + name: Button 1 + internal: false + <<: *binary_sensor_button_base + +globals: + - id: button_press_button + type: uint8_t + restore_value: false + initial_value: '0' + + - id: button_press_position + type: uint8_t + restore_value: false + initial_value: '0' + + - id: button_press_start_time + type: uint32_t + restore_value: false + initial_value: '0' + + - id: click_counter + type: uint8_t + restore_value: false + initial_value: '0' script: - id: !extend boot_initialize @@ -162,9 +104,31 @@ script: parameters: component: string event: string - then: # There's nothing here so far - # Extended by: - # - core_api + then: + # Extended by: + # - core_api + - lambda: |- + ESP_LOGI("core_hw_buttons", "Button '%s' action: '%s'", component.c_str(), event.c_str()); + id(button_press_button) = 0; + id(click_counter) = 0; + id(button_press_start_time) = 0; + buttons_release->execute(); + + - id: button_click_event + mode: restart + parameters: + button_id: uint8_t + click_count: uint8_t + then: + - delay: + milliseconds: ${BUTTON_MULTI_CLICK_DELAY} + - lambda: |- + const std::string button_name = "bs_button_" + std::to_string(button_id); + std::string event_name; + if (click_count == 1) event_name = "click"; + else if (click_count == 2) event_name = "double_click"; + else event_name = std::to_string(click_count) + "_click"; + button_action->execute(button_name.c_str(), event_name.c_str()); - id: buttons_release mode: restart @@ -187,42 +151,78 @@ script: then: - script.execute: id: touch_on_press_buttons - touch_x: !lambda return touch_x; + touch_position: !lambda return touch_position; - id: touch_on_press_buttons mode: restart parameters: - touch_x: uint8_t + touch_position: uint8_t then: - lambda: |- + id(button_press_start_time) = millis(); + id(button_press_position) = touch_position; + uint8_t button = 0; auto model_index = sl_tx_model_gang->active_index(); if (model_index.has_value()) { - uint8_t model_idx = model_index.value() + 1; - switch (model_idx) { - case 1: // 1 Gang - bs_button_1->publish_state(true); - break; - case 2: // 2 Gang - if (touch_x <= 5) bs_button_1->publish_state(true); - else bs_button_2->publish_state(true); - break; - case 3: // 3 Gang - if (touch_x <= 3) bs_button_1->publish_state(true); - else if (touch_x <= 7) bs_button_2->publish_state(true); - else bs_button_3->publish_state(true); - break; - case 4: // 4 Gang - if (touch_x <= 2) bs_button_1->publish_state(true); - else if (touch_x <= 5) bs_button_2->publish_state(true); - else if (touch_x <= 8) bs_button_3->publish_state(true); - else bs_button_4->publish_state(true); - break; - } + const uint8_t model_idx = model_index.value() + 1; // Increment for 1-based indexing + if (model_idx == 1) { + button = 1; // Single button, always 1 + } else { + const uint8_t width = ${TOUCH_POSITION_MAX_VALUE} / model_idx; // Width of each button region + ESP_LOGV("core_hw_buttons", "Button regions: width=%" PRIu8 ", touch_position=%" PRIu8, + width, touch_position); + button = (touch_position / width) + 1; // Determine button region + if (button > model_idx) + button = model_idx; // Clamp to max button count + } + } + // Update binary sensor + switch (button) { + case 1: + bs_button_1->publish_state(true); + break; + case 2: + bs_button_2->publish_state(true); + break; + case 3: + bs_button_3->publish_state(true); + break; + case 4: + bs_button_4->publish_state(true); + break; + } + // Update counters + if (id(button_press_button) == button) { + id(click_counter)++; + } else { + id(click_counter) = 1; + id(button_press_button) = button; } - id: !extend touch_on_release then: - - script.execute: buttons_release + - lambda: |- + uint32_t current_time = millis(); + buttons_release->execute(); + if (id(button_press_start_time) > 0 and + id(button_press_start_time) < current_time) { + uint32_t press_duration = current_time - id(button_press_start_time); + // Handle overflow (optional, since it's unlikely to happen here) + ESP_LOGI("core_hw_buttons", "Button press duration: %" PRIu32 " ms", press_duration); + if (press_duration < ${BUTTON_CLICK_MIN_LENGTH}) { + ESP_LOGW("core_hw_buttons", "Ignoring button press (too short)"); + } else if (press_duration >= ${BUTTON_CLICK_MIN_LENGTH} and + press_duration <= ${BUTTON_CLICK_MAX_LENGTH}) { // Short/normal click + button_click_event->execute(id(button_press_button), id(click_counter)); + } else if (press_duration >= ${BUTTON_LONG_PRESS_DELAY} and press_duration <= ${BUTTON_PRESS_TIMEOUT}) { + button_action->execute(("bs_button_" + std::to_string(id(button_press_button))).c_str(), "long_click"); + } else if (press_duration > ${BUTTON_PRESS_TIMEOUT}) { // Timeout or invalid + ESP_LOGW("core_hw_buttons", "Button press cancelled or timed out after ${BUTTON_PRESS_TIMEOUT} ms"); + } + } else { + ESP_LOGW("core_hw_buttons", "Press event timestamp not recorded yet"); + } + id(button_press_start_time) = 0; - id: !extend touch_swipe_left then: diff --git a/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_relays.yaml b/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_relays.yaml index 79a92ce..dc59f0a 100644 --- a/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_relays.yaml +++ b/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_relays.yaml @@ -11,55 +11,6 @@ ##### - For normal system use, modifications to this file are NOT required. ##### #################################################################################################### --- -binary_sensor: - - id: !extend bs_button_1 - on_click: - then: - - if: - condition: - - lambda: return sl_button_1_action->active_index().has_value(); - - lambda: return sl_button_1_action->active_index().value() == 1; - - and: &button_click_no_other_click - - lambda: return not bs_multi_touch->state; - - lambda: return not bs_swipe_left->state; - - lambda: return not bs_swipe_down->state; - - lambda: return not bs_swipe_right->state; - then: - - switch.toggle: sw_relay_1 - - - id: !extend bs_button_2 - on_click: - then: - - if: - condition: - - lambda: return sl_button_2_action->active_index().has_value(); - - lambda: return sl_button_2_action->active_index().value() == 1; - - and: *button_click_no_other_click - then: - - switch.toggle: sw_relay_2 - - - id: !extend bs_button_3 - on_click: - then: - - if: - condition: - - lambda: return sl_button_3_action->active_index().has_value(); - - lambda: return sl_button_3_action->active_index().value() == 1; - - and: *button_click_no_other_click - then: - - switch.toggle: sw_relay_3 - - - id: !extend bs_button_4 - on_click: - then: - - if: - condition: - - lambda: return sl_button_4_action->active_index().has_value(); - - lambda: return sl_button_4_action->active_index().value() == 1; - - and: *button_click_no_other_click - then: - - switch.toggle: sw_relay_4 - globals: - id: boot_initialization_relays type: bool @@ -159,6 +110,45 @@ script: } id(boot_initialization_relays) = true; + - id: !extend button_click_event + then: + - lambda: |- + // Handle only single clicks + if (click_count != 1) + return; + + // Ignore if other touch events are active + if (bs_multi_touch->state || + bs_swipe_left->state || + bs_swipe_down->state || + bs_swipe_right->state) { + return; + } + + // Toggle relay if corresponding button action is enabled + switch (button_id) { + case 1: + if (sl_button_1_action->active_index().has_value() and + sl_button_1_action->active_index().value() == 1) + sw_relay_1->toggle(); + break; + case 2: + if (sl_button_2_action->active_index().has_value() and + sl_button_2_action->active_index().value() == 1) + sw_relay_2->toggle(); + break; + case 3: + if (sl_button_3_action->active_index().has_value() and + sl_button_3_action->active_index().value() == 1) + sw_relay_3->toggle(); + break; + case 4: + if (sl_button_4_action->active_index().has_value() and + sl_button_4_action->active_index().value() == 1) + sw_relay_4->toggle(); + break; + } + - id: show_relay_status mode: restart then: diff --git a/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_touch.yaml b/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_touch.yaml index 08c6490..0a47ab3 100644 --- a/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_touch.yaml +++ b/ESPHome/TX-Ultimate-Easy-ESPHome_core_hw_touch.yaml @@ -11,6 +11,9 @@ ##### - For normal system use, modifications to this file are NOT required. ##### #################################################################################################### --- +substitutions: + TOUCH_POSITION_MAX_VALUE: '10' # Maximum touch position value returned by the touch pad via uart + binary_sensor: - id: bs_multi_touch name: Multi-touch @@ -61,7 +64,7 @@ external_components: - source: type: git url: https://github.com/edwardtfn/TX-Ultimate-Easy - ref: ${version} + ref: v${version} refresh: 1h components: - tx_ultimate_easy @@ -124,7 +127,7 @@ script: - id: touch_on_press mode: restart parameters: - touch_x: uint8_t + touch_position: uint8_t then: # Extended by: # - HW Buttons @@ -203,16 +206,28 @@ tx_ultimate_easy: id: tx_ultimate uart: uart_touch + on_long_touch_release: + - lambda: |- + const uint8_t touch_position = static_cast(touch.x); + if (touch_position > ${TOUCH_POSITION_MAX_VALUE}) { // Check for valid range + ESP_LOGE("tx_ultimate_easy", "Invalid long-touch position: %" PRIu8, touch_position); + } else { + ESP_LOGI("tx_ultimate_easy", "Long-touch released at position %" PRIu8, touch_position); + } + on_multi_touch_release: - lambda: ESP_LOGI("tx_ultimate_easy", "Multi-touch released"); - script.execute: touch_on_multi_touch_release on_press: - lambda: |- - ESP_LOGI("tx_ultimate_easy", "Pressed at position %" PRIu8, static_cast(touch.x)); - - script.execute: - id: touch_on_press - touch_x: !lambda return static_cast(touch.x); + const uint8_t touch_position = static_cast(touch.x); + if (touch_position > ${TOUCH_POSITION_MAX_VALUE}) { // Check for valid range + ESP_LOGE("tx_ultimate_easy", "Invalid touch position: %" PRIu8, touch_position); + } else { + ESP_LOGI("tx_ultimate_easy", "Pressed at position %" PRIu8, touch_position); + touch_on_press->execute(touch_position); + } on_release: - lambda: ESP_LOGI("tx_ultimate_easy", "Released"); @@ -233,8 +248,8 @@ tx_ultimate_easy: ESP_LOGD("tx_ultimate_easy", " Position: %i", touch.x); uart: - id: uart_touch - tx_pin: GPIO19 - rx_pin: GPIO22 - baud_rate: 115200 + - id: uart_touch + tx_pin: GPIO19 + rx_pin: GPIO22 + baud_rate: 115200 ... diff --git a/TX-Ultimate-Easy-ESPHome.yaml b/TX-Ultimate-Easy-ESPHome.yaml index 3b95ea8..0befff0 100644 --- a/TX-Ultimate-Easy-ESPHome.yaml +++ b/TX-Ultimate-Easy-ESPHome.yaml @@ -16,7 +16,7 @@ wifi: packages: remote_package: url: https://github.com/edwardtfn/TX-Ultimate-Easy - ref: dev + ref: latest refresh: 30s files: - ESPHome/TX-Ultimate-Easy-ESPHome_core.yaml diff --git a/components/tx_ultimate_easy/__init__.py b/components/tx_ultimate_easy/__init__.py index 3a598ad..7120ead 100644 --- a/components/tx_ultimate_easy/__init__.py +++ b/components/tx_ultimate_easy/__init__.py @@ -13,6 +13,7 @@ CONF_TX_ULTIMATE_EASY = "tx_ultimate_easy" CONF_UART = "uart" +CONF_GANG_COUNT = "gang_count" CONF_ON_TOUCH_EVENT = "on_touch_event" CONF_ON_PRESS = "on_press" @@ -33,6 +34,7 @@ cv.GenerateID(): cv.declare_id(TxUltimateTouch), cv.Required(CONF_UART): cv.use_id(uart), + cv.Optional(CONF_GANG_COUNT, default=1): cv.int_range(min=1, max=4), cv.Optional(CONF_ON_TOUCH_EVENT): automation.validate_automation(single=True), cv.Optional(CONF_ON_PRESS): automation.validate_automation(single=True), @@ -49,6 +51,9 @@ async def register_tx_ultimate_easy(var, config): uart_component = await cg.get_variable(config[CONF_UART]) cg.add(var.set_uart_component(uart_component)) + if CONF_GANG_COUNT in config: + cg.add(var.set_gang_count(config[CONF_GANG_COUNT])) + if CONF_ON_TOUCH_EVENT in config: await automation.build_automation( var.get_trigger_touch_event(), diff --git a/components/tx_ultimate_easy/tx_ultimate_easy.cpp b/components/tx_ultimate_easy/tx_ultimate_easy.cpp index 84150db..640f3db 100644 --- a/components/tx_ultimate_easy/tx_ultimate_easy.cpp +++ b/components/tx_ultimate_easy/tx_ultimate_easy.cpp @@ -48,6 +48,35 @@ namespace esphome { void TxUltimateEasy::dump_config() { ESP_LOGCONFIG(TAG, "TX Ultimate Easy"); + ESP_LOGCONFIG(TAG, " Gang count: %" PRIu8, this->gang_count_); + } + + bool TxUltimateEasy::set_gang_count(const uint8_t gang_count) { + // Hardware supports maximum of 4 touch-sensitive buttons + if (gang_count < 1 or gang_count > 4) + return false; + this->gang_count_ = gang_count; + return true; + } + + uint8_t TxUltimateEasy::get_button_from_position(const uint8_t position) { + // Validate position bounds + if (position > TOUCH_MAX_POSITION) + return 0; + + // Special case for single gang (only one button exists) + if (this->gang_count_ == 1) + return 1; + + // Calculate button number + const uint8_t width = (TOUCH_MAX_POSITION + 1) / this->gang_count_; // Width of each button region + if (width < 1 or width > this->gang_count_) // Invalid width - and prevents division by zero + return 0; + const uint8_t button = std::min( + static_cast((position / width) + 1), // Convert position to button index + this->gang_count_ // Clamp to max gang count + ); + return button; } void TxUltimateEasy::send_touch_(TouchPoint tp) { @@ -103,7 +132,8 @@ namespace esphome { state == TOUCH_STATE_SWIPE_LEFT || state == TOUCH_STATE_SWIPE_RIGHT || state == TOUCH_STATE_MULTI_TOUCH) && - (uart_received_bytes[6] >= 0 || state == TOUCH_STATE_MULTI_TOUCH); + // Multi-touch events may have x < 0, all other events require valid x position + (uart_received_bytes[6] >= 0 || state == TOUCH_STATE_MULTI_TOUCH); } int TxUltimateEasy::get_touch_position_x(const std::array &uart_received_bytes) { @@ -133,6 +163,8 @@ namespace esphome { TouchPoint TxUltimateEasy::get_touch_point(const std::array &uart_received_bytes) { TouchPoint tp; tp.x = this->get_touch_position_x(uart_received_bytes); + if (tp.x >= 0) + tp.button = this->get_button_from_position(static_cast(tp.x)); tp.state = this->get_touch_state(uart_received_bytes); switch (tp.state) { case TOUCH_STATE_RELEASE: diff --git a/components/tx_ultimate_easy/tx_ultimate_easy.h b/components/tx_ultimate_easy/tx_ultimate_easy.h index 5d7b3a0..cfb9e8b 100644 --- a/components/tx_ultimate_easy/tx_ultimate_easy.h +++ b/components/tx_ultimate_easy/tx_ultimate_easy.h @@ -13,6 +13,9 @@ namespace esphome { namespace tx_ultimate_easy { + // Touch Max Position + constexpr uint8_t TOUCH_MAX_POSITION = 10; + // Touch State Constants constexpr uint8_t TOUCH_STATE_RELEASE = 0x01; constexpr uint8_t TOUCH_STATE_PRESS = 0x02; @@ -32,6 +35,7 @@ namespace esphome { static const char *TAG = "tx_ultimate_easy"; struct TouchPoint { + uint8_t button = 0; int8_t x = -1; int8_t state = -1; std::string state_str = "Unknown"; @@ -53,6 +57,10 @@ namespace esphome { void loop() override; void dump_config() override; + uint8_t get_gang_count() { return this->gang_count_; } + bool set_gang_count(const uint8_t gang_count); + uint8_t get_button_from_position(const uint8_t position); + protected: void send_touch_(TouchPoint tp); void handle_touch(const std::array &bytes); @@ -70,6 +78,8 @@ namespace esphome { Trigger trigger_multi_touch_release_; Trigger trigger_long_touch_release_; + uint8_t gang_count_ = 1; + }; // class TxUltimateEasy } // namespace tx_ultimate_easy