Compare commits

...

15 Commits

Author SHA1 Message Date
GitHub Actions
e5d941c460 Bump version to 2024.12.4 [skip-versioning] 2024-12-22 01:12:50 +00:00
Edward Firmo
9b134a786a Merge pull request #50 from edwardtfn/swipe-priority
Rebuild Button click engine
2024-12-22 02:12:39 +01:00
Edward Firmo
be0fc353f6 Simplify relay handler 2024-12-22 02:03:35 +01:00
Edward Firmo
fbc5cb79cc Reduce BUTTON_MULTI_CLICK_DELAY to 250ms 2024-12-22 01:46:28 +01:00
Edward Firmo
c65acc3298 Pass parameters to button_click_event 2024-12-22 01:37:07 +01:00
Edward Firmo
4d8107faf2 Remove relay from binary sensor 2024-12-22 01:15:26 +01:00
Edward Firmo
65bbd901b1 Clean-up get_button_from_position 2024-12-21 23:32:15 +01:00
Edward Firmo
8c610a406a Log long-press from component
If this works fine, it could replace this part on the new implementation.
2024-12-21 23:17:42 +01:00
Edward Firmo
696e3bf28d Component to provide button number 2024-12-21 23:10:16 +01:00
Edward Firmo
5ae4564e6a CodeRabbitAI Nitpick 2 2024-12-21 22:08:19 +01:00
Edward Firmo
857c43940d CodeRabbitAI Nitpick 2024-12-21 21:54:54 +01:00
Edward Firmo
8668fe8dc9 Fix another bad reference 2024-12-21 21:29:05 +01:00
Edward Firmo
976a903118 Fix references 2024-12-21 21:25:29 +01:00
Edward Firmo
35cc0c4a8e Reorganize scripts for better clarity 2024-12-21 21:18:54 +01:00
Edward Firmo
5c4581563e Rebuild Button click engine
The native engine from ESPHome cannot be used here as a button press event is triggered on touch, which would trigger the click even on swipe.
Other projects solved this by tracking only single click and only at release, so we decided to implement this in a different way, so we can handle multiple or long-clicks also.
This should finally solve #14, but some test is required to ensure nothing else was broken.
2024-12-21 21:14:41 +01:00
13 changed files with 241 additions and 189 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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
...

View File

@@ -74,7 +74,7 @@ improv_serial:
id: serial_improv
logger:
level: INFO
level: DEBUG
ota:
platform: esphome

View File

@@ -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:

View File

@@ -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:

View File

@@ -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<uint8_t>(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<uint8_t>(touch.x));
- script.execute:
id: touch_on_press
touch_x: !lambda return static_cast<uint8_t>(touch.x);
const uint8_t touch_position = static_cast<uint8_t>(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
...

View File

@@ -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

View File

@@ -13,6 +13,7 @@ DEPENDENCIES = ['uart']
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 @@ CONFIG_SCHEMA = cv.Schema({
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(),

View File

@@ -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<uint8_t>((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<int, UART_RECEIVED_BYTES_SIZE> &uart_received_bytes) {
@@ -133,6 +163,8 @@ namespace esphome {
TouchPoint TxUltimateEasy::get_touch_point(const std::array<int, UART_RECEIVED_BYTES_SIZE> &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<uint8_t>(tp.x));
tp.state = this->get_touch_state(uart_received_bytes);
switch (tp.state) {
case TOUCH_STATE_RELEASE:

View File

@@ -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<int, UART_RECEIVED_BYTES_SIZE> &bytes);
@@ -70,6 +78,8 @@ namespace esphome {
Trigger<TouchPoint> trigger_multi_touch_release_;
Trigger<TouchPoint> trigger_long_touch_release_;
uint8_t gang_count_ = 1;
}; // class TxUltimateEasy
} // namespace tx_ultimate_easy

View File

@@ -1 +1 @@
2024.12.3
2024.12.4

View File

@@ -1 +1 @@
version: 2024.12.3
version: 2024.12.4