1 / 5
Smart Tactile Desk Buttons — zdjęcie 1
1 / 5

Smart Tactile Desk Buttons

Zamów wydruk tego modelu

Obliczymy ostateczną cenę na podstawie wybranego pliku. Sprawdzisz ją i zaakceptujesz przed płatnością.

Zapisane na tym urządzeniu.

A compact, tactile 5‑button controller with integrated OLED displays for Home Assistant (or custom firmware)

Ocena
0 / 5
Polubienia
13
Pobrania
16
Wydruki
0
Wyświetlenia
286
Oceny
0

Aktualizacja u źródła:

Informacje o modelu

Źródło
Printables
Kategoria
Tools
Licencja
Creative Commons — Attribution — Noncommercial

A compact desk-mounted control unit with 5 physical buttons, each with its own OLED label, designed for use with Home Assistant. It has a really satisfying button action and click.

DIY and soldering skills required.
I might make more detailed instructions or think about selling semi-assembled units later, if there is enough interest. 


My Use Case

I made this little button unit for my Home Assistant setup, because I wanted to have the ability to physically trigger anything I want with a nice feel and look. The LED is extended to the front with a nylon string and reacts to trigger or status.

  • Integrates via ESPHome

  • Each button can trigger:

    • Home Assistant entities

    • Scenes

    • Scripts or automations

  • Button labels are the friendly Label of the selected entity.

  • Powered via USB

You can implement your own ideas, let me know about it in the comments or a leave a Make.
Would love to hear about it! 


Instructions

  • Easy wiring: Daisy-chain Ground for the Buttons and 3.3v/Ground for the OLED Screens

  • The three slits on the bottom are for 5 cent (€) coins, to add weight.

  • OLEDS click right into the Buttons, pins facing the top side of the button.

Parts:

5x OLED 64x32

I2C Screen

5x Micro-switch

TCA9548A I2C IIC

Multiplexer 8-Ch

5x Springs
Diameter: 7mm

Thickness: 0.5mm

Length: 20mm

ESP32-C3 SuperMini

Misc:

  • 1mm Nylon

  • 5x2mm Magnets (4x)

  • Rubber Feet cutout

  • 22AWG-28AWG

    wire


Code:

Configuration.yaml:

template:
  - binary_sensor:
      - name: "Desk Button 1 – Button 1 on"
        unique_id: desk_button_1_button_1_on
        state: >
          {% set ent_id = states('input_text.desk_button_1_target_1') | trim %}
          {% if ent_id in states %}
            {{ is_state(ent_id, 'on') }}
          {% else %}
            false
          {% endif %}
  - name: "Desk Button 1 – Button 2 on"
    unique_id: desk_button_1_button_2_on
    state: >
      {% set ent_id = states('input_text.desk_button_1_target_2') | trim %}
      {% if ent_id in states %}
        {{ is_state(ent_id, 'on') }}
      {% else %}
        false
      {% endif %}

  - name: "Desk Button 1 – Button 3 on"
    unique_id: desk_button_1_button_3_on
    state: >
      {% set ent_id = states('input_text.desk_button_1_target_3') | trim %}
      {% if ent_id in states %}
        {{ is_state(ent_id, 'on') }}
      {% else %}
        false
      {% endif %}

  - name: "Desk Button 1 – Button 4 on"
    unique_id: desk_button_1_button_4_on
    state: >
      {% set ent_id = states('input_text.desk_button_1_target_4') | trim %}
      {% if ent_id in states %}
        {{ is_state(ent_id, 'on') }}
      {% else %}
        false
      {% endif %}

  - name: "Desk Button 1 – Button 5 on"
    unique_id: desk_button_1_button_5_on
    state: >
      {% set ent_id = states('input_text.desk_button_1_target_5') | trim %}
      {% if ent_id in states %}
        {{ is_state(ent_id, 'on') }}
      {% else %}
        false
      {% endif %}
  • sensor:
    • name: "Desk Button 1 Label 1" unique_id: desk_button_1_label_1 state: > {% set ent = states('input_text.desk_button_1_target_1') | trim %} {% if ent | regex_match('^[a-z_]+\.[a-z0-9_]+$') %} {{ state_attr(ent, 'friendly_name') or ent }} {% else %} BTN 1 {% endif %}

    • name: "Desk Button 1 Label 2" unique_id: desk_button_1_label_2 state: > {% set ent = states('input_text.desk_button_1_target_2') | trim %} {% if ent | regex_match('^[a-z_]+\.[a-z0-9_]+$') %} {{ state_attr(ent, 'friendly_name') or ent }} {% else %} BTN 2 {% endif %}

    • name: "Desk Button 1 Label 3" unique_id: desk_button_1_label_3 state: > {% set ent = states('input_text.desk_button_1_target_3') | trim %} {% if ent | regex_match('^[a-z_]+\.[a-z0-9_]+$') %} {{ state_attr(ent, 'friendly_name') or ent }} {% else %} BTN 3 {% endif %}

    • name: "Desk Button 1 Label 4" unique_id: desk_button_1_label_4 state: > {% set ent = states('input_text.desk_button_1_target_4') | trim %} {% if ent | regex_match('^[a-z_]+\.[a-z0-9_]+$') %} {{ state_attr(ent, 'friendly_name') or ent }} {% else %} BTN 4 {% endif %}

    • name: "Desk Button 1 Label 5" unique_id: desk_button_1_label_5 state: > {% set ent = states('input_text.desk_button_1_target_5') | trim %} {% if ent | regex_match('^[a-z_]+\.[a-z0-9_]+$') %} {{ state_attr(ent, 'friendly_name') or ent }} {% else %} BTN 5 {% endif %}

input_text: desk_button_1_target_1: name: Desk Button 1 Target 1 desk_button_1_target_2: name: Desk Button 1 Target 2 desk_button_1_target_3: name: Desk Button 1 Target 3 desk_button_1_target_4: name: Desk Button 1 Target 4 desk_button_1_target_5: name: Desk Button 1 Target 5

scripts.yaml:

desk_button_1_press:
  alias: Desk Button 1 Press
  mode: queued
  fields:
    button:
      description: "Button number 1-5"
      example: "1"

sequence: - variables: target: "{{ states('input_text.desk_button_1_target_' ~ button) | trim }}" domain: "{{ target.split('.')[0] if '.' in target else '' }}"

# DEBUG breadcrumb in HA logs (so you can see what target it tried)
- service: system_log.write
  data:
    level: info
    message: "DeskButton1 press: button={{ button }}, target='{{ target }}', domain='{{ domain }}'"

- choose:
    # empty target
    - conditions: "{{ target == '' }}"
      sequence:
        - service: system_log.write
          data:
            level: warning
            message: "DeskButton1: button={{ button }} has EMPTY target (input_text not set)"

    # scene target
    - conditions: "{{ domain == 'scene' }}"
      sequence:
        - service: scene.turn_on
          target:
            entity_id: "{{ target }}"

    # script target
    - conditions: "{{ domain == 'script' }}"
      sequence:
        - service: script.turn_on
          target:
            entity_id: "{{ target }}"

  default:
    - service: homeassistant.toggle
      target:
        entity_id: "{{ target }}"</code></pre><p></p><h5>desk-button-1.yaml (ESPHome):</h5><p>After adding the ESP32 to ESPHome, paste your device-specific sections/passwords in the top section.</p><pre><code>esphome:

name: desk-button-1 friendly_name: Desk Button 1 on_boot: priority: -10 then: - script.execute: led_press_flash - component.update: display1 - component.update: display2 - component.update: display3 - component.update: display4 - component.update: display5 - script.execute: led_apply_baseline

esp32: board: esp32-c3-devkitm-1 framework: type: esp-idf

logger: level: INFO

api: encryption: key: "____"

ota:

  • platform: esphome password: "___"

wifi: ssid: !secret wifi_ssid password: !secret wifi_password power_save_mode: LIGHT ap: ssid: "Desk-Button-1 Fallback Hotspot" password: "___"

captive_portal:

---------------- LED ----------------

light:

  • platform: esp32_rmt_led_strip name: "Desk Button 1 RGB" id: deskbtn_rgb pin: GPIO8 num_leds: 1 chipset: ws2812 rgb_order: GRB restore_mode: ALWAYS_OFF internal: true

globals:

  • id: led_r type: float restore_value: true initial_value: "0.0"
  • id: led_g type: float restore_value: true initial_value: "1.0"
  • id: led_b type: float restore_value: true initial_value: "0.0"

script:

  • id: led_press_flash mode: restart then:

    • light.turn_on: id: deskbtn_rgb transition_length: 500ms red: 0% green: 40% blue: 100% brightness: 100%
    • delay: 1.0s
    • script.execute: led_apply_baseline
  • id: led_apply_baseline mode: restart then:

    • light.turn_on: id: deskbtn_rgb transition_length: 500ms red: !lambda "return id(led_r);" green: !lambda "return id(led_g);" blue: !lambda "return id(led_b);" brightness: 80%

---- Visible entities on the HA device page (diagnostics) ----

button:

  • platform: restart name: "Desk Button 1 Restart"

sensor:

  • platform: wifi_signal name: "Desk Button 1 WiFi Signal" update_interval: 90s

#- platform: uptime

name: "Desk Button 1 Uptime"

update_interval: 60s

select:

  • platform: template name: "Display Font" id: sel_font optimistic: true restore_value: true options:
    • Helvetica
    • Roboto
    • Inter
    • SourceSans
    • Noto
    • RobotoCondensed
    • InterTight
    • ArchivoNarrow
    • JetBrainsMono
    • IBMPlexMono on_value: then:
      • component.update: display1
      • component.update: display2
      • component.update: display3
      • component.update: display4
      • component.update: display5

---- Font ----

font:

  • file: "gfonts://Helvetica" id: font_helv size: 15 glyphsets: ["GF_Latin_Core"]

  • file: "gfonts://Roboto" id: font_roboto size: 15 glyphsets: ["GF_Latin_Core"]

  • file: "gfonts://Source+Sans+3" id: font_source size: 15 glyphsets: ["GF_Latin_Core"]

  • file: "gfonts://Noto+Sans" id: font_noto size: 15 glyphsets: ["GF_Latin_Core"]

  • file: "gfonts://Roboto+Condensed" id: font_roboc size: 16 glyphsets: ["GF_Latin_Core"]

  • file: "gfonts://Inter" id: font_inter size: 15 glyphsets: ["GF_Latin_Core"]

  • file: "gfonts://Inter+Tight" id: font_intertight size: 15 glyphsets: ["GF_Latin_Core"]

  • file: "gfonts://Archivo+Narrow" id: font_archivo size: 16 glyphsets: ["GF_Latin_Core"]

  • file: "gfonts://JetBrains+Mono" id: font_jbmono size: 14 glyphsets: ["GF_Latin_Core"]

  • file: "gfonts://IBM+Plex+Mono" id: font_ibmmono size: 14 glyphsets: ["GF_Latin_Core"]

---- I2C + TCA9548A ----

i2c: id: I2C_0 sda: GPIO04 scl: GPIO03 scan: false frequency: 200kHz

tca9548a:

  • address: 0x70 id: multiplex0 i2c_id: I2C_0 channels:
    • bus_id: ch1 channel: 0
    • bus_id: ch2 channel: 1
    • bus_id: ch3 channel: 2
    • bus_id: ch4 channel: 3
    • bus_id: ch5 channel: 4

---- 5 text inputs shown under THIS device in HA ----

entity_id like: switch.studio_lamps

ESPHome writes it into HA input_text.desk_button_1_target_X

text:

  • platform: template name: "Button 1 Target Entity" id: txt_btn1_target optimistic: true restore_value: true mode: text on_value: then: - homeassistant.service: service: input_text.set_value data: entity_id: input_text.desk_button_1_target_1 value: !lambda "return x;" - component.update: display1

  • platform: template name: "Button 2 Target Entity" id: txt_btn2_target optimistic: true restore_value: true mode: text on_value: then: - homeassistant.service: service: input_text.set_value data: entity_id: input_text.desk_button_1_target_2 value: !lambda "return x;" - component.update: display2

  • platform: template name: "Button 3 Target Entity" id: txt_btn3_target optimistic: true restore_value: true mode: text on_value: then: - homeassistant.service: service: input_text.set_value data: entity_id: input_text.desk_button_1_target_3 value: !lambda "return x;" - component.update: display3

  • platform: template name: "Button 4 Target Entity" id: txt_btn4_target optimistic: true restore_value: true mode: text on_value: then: - homeassistant.service: service: input_text.set_value data: entity_id: input_text.desk_button_1_target_4 value: !lambda "return x;" - component.update: display4

  • platform: template name: "Button 5 Target Entity" id: txt_btn5_target optimistic: true restore_value: true mode: text on_value: then: - homeassistant.service: service: input_text.set_value data: entity_id: input_text.desk_button_1_target_5 value: !lambda "return x;" - component.update: display5

---- Pull labels from HA ----

text_sensor:

  • platform: homeassistant id: ha_target1 entity_id: input_text.desk_button_1_target_1 on_value: then: - text.set: id: txt_btn1_target value: !lambda "return x;" - component.update: display1

  • platform: homeassistant id: ha_target2 entity_id: input_text.desk_button_1_target_2 on_value: then: - text.set: id: txt_btn2_target value: !lambda "return x;" - component.update: display2

  • platform: homeassistant id: ha_target3 entity_id: input_text.desk_button_1_target_3 on_value: then: - text.set: id: txt_btn3_target value: !lambda "return x;" - component.update: display3

  • platform: homeassistant id: ha_target4 entity_id: input_text.desk_button_1_target_4 on_value: then: - text.set: id: txt_btn4_target value: !lambda "return x;" - component.update: display4

  • platform: homeassistant id: ha_target5 entity_id: input_text.desk_button_1_target_5 on_value: then: - text.set: id: txt_btn5_target value: !lambda "return x;" - component.update: display5

  • platform: homeassistant id: b1_label entity_id: sensor.desk_button_1_label_1 on_value: then: - component.update: display1

  • platform: homeassistant id: b2_label entity_id: sensor.desk_button_1_label_2 on_value: then: - component.update: display2

  • platform: homeassistant id: b3_label entity_id: sensor.desk_button_1_label_3 on_value: then: - component.update: display3

  • platform: homeassistant id: b4_label entity_id: sensor.desk_button_1_label_4 on_value: then: - component.update: display4

  • platform: homeassistant id: b5_label entity_id: sensor.desk_button_1_label_5 on_value: then: - component.update: display5

---- Binary sensors: status, HA on-state, GPIO presses ----

binary_sensor:

  • platform: status name: "Desk Button 1 Status" id: esp_status on_state: then: - if: condition: binary_sensor.is_on: esp_status then: - lambda: |- id(led_r)=0.0; id(led_g)=1.0; id(led_b)=0.0; - script.execute: led_apply_baseline else: - lambda: |- id(led_r)=1.0; id(led_g)=0.0; id(led_b)=0.0; - script.execute: led_apply_baseline

  • platform: homeassistant id: b1_on entity_id: binary_sensor.desk_button_1_button_1_on on_state: then: - component.update: display1

  • platform: homeassistant id: b2_on entity_id: binary_sensor.desk_button_1_button_2_on on_state: then: - component.update: display2

  • platform: homeassistant id: b3_on entity_id: binary_sensor.desk_button_1_button_3_on on_state: then: - component.update: display3

  • platform: homeassistant id: b4_on entity_id: binary_sensor.desk_button_1_button_4_on on_state: then: - component.update: display4

  • platform: homeassistant id: b5_on entity_id: binary_sensor.desk_button_1_button_5_on on_state: then: - component.update: display5

---- Physical buttons ----

  • platform: gpio pin: number: GPIO21 mode: INPUT_PULLUP inverted: true id: button1 internal: true filters:

    • delayed_on: 20ms
    • delayed_off: 20ms on_press: then:
      • logger.log: "BTN1 pressed"
      • script.execute: led_press_flash
      • homeassistant.service: service: script.desk_button_1_press data: button: "1"
  • platform: gpio pin: number: GPIO20 mode: INPUT_PULLUP inverted: true id: button2 internal: true filters:

    • delayed_on: 20ms
    • delayed_off: 20ms on_press: then:
      • logger.log: "BTN2 pressed"
      • script.execute: led_press_flash
      • homeassistant.service: service: script.desk_button_1_press data: button: "2"
  • platform: gpio pin: number: GPIO10 mode: INPUT_PULLUP inverted: true id: button3 internal: true filters:

    • delayed_on: 20ms
    • delayed_off: 20ms on_press: then:
      • logger.log: "BTN3 pressed"
      • script.execute: led_press_flash
      • homeassistant.service: service: script.desk_button_1_press data: button: "3"
  • platform: gpio pin: number: GPIO9 mode: INPUT_PULLUP inverted: true id: button4 internal: true filters:

    • delayed_on: 20ms
    • delayed_off: 20ms on_press: then:
      • logger.log: "BTN4 pressed"
      • script.execute: led_press_flash
      • homeassistant.service: service: script.desk_button_1_press data: button: "4"
  • platform: gpio pin: number: GPIO7 mode: INPUT_PULLUP inverted: true id: button5 internal: true filters:

    • delayed_on: 20ms
    • delayed_off: 20ms on_press: then:
      • logger.log: "BTN5 pressed"
      • script.execute: led_press_flash
      • homeassistant.service: service: script.desk_button_1_press data: button: "5"

---- OLEDs ----

display:

  • platform: ssd1306_i2c model: "SSD1306 64x32" id: display1 address: 0x3C i2c_id: ch1 update_interval: never lambda: |- auto label = id(b1_label).state; if (label.empty()) label = "Button 1"; for (auto &c : label) { if (c == '_' || c == '-') c = ' '; } auto trim = [](std::string &s) { while (!s.empty() && s.front() == ' ') s.erase(s.begin()); while (!s.empty() && s.back() == ' ') s.pop_back(); }; auto collapse_spaces = [&](std::string &s) { std::string out; bool prev_space = false; for (char c : s) { bool is_space = (c == ' '); if (is_space) { if (!prev_space) out.push_back(' '); } else { out.push_back(c); } prev_space = is_space; } s = out; }; trim(label); collapse_spaces(label);

    const char* f = id(sel_font).current_option(); auto font = &id(font_helv); if (f) { if (!strcmp(f, "Roboto")) font = &id(font_roboto); else if (!strcmp(f, "Inter")) font = &id(font_inter); else if (!strcmp(f, "SourceSans")) font = &id(font_source); else if (!strcmp(f, "Noto")) font = &id(font_noto); else if (!strcmp(f, "RobotoCondensed")) font = &id(font_roboc); else if (!strcmp(f, "InterTight")) font = &id(font_intertight); else if (!strcmp(f, "ArchivoNarrow")) font = &id(font_archivo); else if (!strcmp(f, "JetBrainsMono")) font = &id(font_jbmono); else if (!strcmp(f, "IBMPlexMono")) font = &id(font_ibmmono); }

    const int MAX1 = 8; // max character. const int MAX2 = 8;

    auto split_words = [](const std::string &s) { std::vector<std::string> w; std::string cur; for (char c : s) { if (c == ' ') { if (!cur.empty()) { w.push_back(cur); cur.clear(); } } else cur.push_back(c); } if (!cur.empty()) w.push_back(cur); return w; }; auto ellipsize = [](const std::string &s, int maxlen) -> std::string { if ((int)s.size() <= maxlen) return s; if (maxlen <= 1) return "…"; return s.substr(0, maxlen - 1) + "…"; }; std::string l1, l2; auto words = split_words(label);

    if (words.size() == 2) { l1 = ellipsize(words[0], MAX1); l2 = ellipsize(words[1], MAX2); } else { for (size_t i = 0; i < words.size(); i++) { std::string cand = l1.empty() ? words[i] : (l1 + " " + words[i]); if ((int)cand.size() <= MAX1) l1 = cand; else break; } size_t used = split_words(l1).size(); for (size_t i = used; i < words.size(); i++) { std::string cand = l2.empty() ? words[i] : (l2 + " " + words[i]); if ((int)cand.size() <= MAX2) l2 = cand; else break; } if (l1.empty()) l1 = ellipsize(label, MAX1); if (l2.empty() && (int)label.size() > MAX1) { std::string rest = label.substr(std::min((int)label.size(), MAX1)); trim(rest); l2 = ellipsize(rest.empty() ? label : rest, MAX2); } } it.printf(32, 7, *font, TextAlign::CENTER, "%s", l1.c_str()); it.printf(32, 22, *font, TextAlign::CENTER, "%s", l2.c_str());

  • platform: ssd1306_i2c model: "SSD1306 64x32" id: display2 address: 0x3C i2c_id: ch2 update_interval: never lambda: |- auto label = id(b2_label).state; if (label.empty()) label = "Button 2"; for (auto &c : label) { if (c == '_' || c == '-') c = ' '; } auto trim = [](std::string &s) { while (!s.empty() && s.front() == ' ') s.erase(s.begin()); while (!s.empty() && s.back() == ' ') s.pop_back(); }; auto collapse_spaces = [&](std::string &s) { std::string out; bool prev_space = false; for (char c : s) { bool is_space = (c == ' '); if (is_space) { if (!prev_space) out.push_back(' '); } else { out.push_back(c); } prev_space = is_space; } s = out; }; trim(label); collapse_spaces(label);

    const char* f = id(sel_font).current_option(); auto font = &id(font_helv); if (f) { if (!strcmp(f, "Roboto")) font = &id(font_roboto); else if (!strcmp(f, "Inter")) font = &id(font_inter); else if (!strcmp(f, "SourceSans")) font = &id(font_source); else if (!strcmp(f, "Noto")) font = &id(font_noto); else if (!strcmp(f, "RobotoCondensed")) font = &id(font_roboc); else if (!strcmp(f, "InterTight")) font = &id(font_intertight); else if (!strcmp(f, "ArchivoNarrow")) font = &id(font_archivo); else if (!strcmp(f, "JetBrainsMono")) font = &id(font_jbmono); else if (!strcmp(f, "IBMPlexMono")) font = &id(font_ibmmono); }

    const int MAX1 = 8; // max character. const int MAX2 = 8;

    auto split_words = [](const std::string &s) { std::vector<std::string> w; std::string cur; for (char c : s) { if (c == ' ') { if (!cur.empty()) { w.push_back(cur); cur.clear(); } } else cur.push_back(c); } if (!cur.empty()) w.push_back(cur); return w; }; auto ellipsize = [](const std::string &s, int maxlen) -> std::string { if ((int)s.size() <= maxlen) return s; if (maxlen <= 1) return "…"; return s.substr(0, maxlen - 1) + "…"; }; std::string l1, l2; auto words = split_words(label);

    if (words.size() == 2) { l1 = ellipsize(words[0], MAX1); l2 = ellipsize(words[1], MAX2); } else { for (size_t i = 0; i < words.size(); i++) { std::string cand = l1.empty() ? words[i] : (l1 + " " + words[i]); if ((int)cand.size() <= MAX1) l1 = cand; else break; } size_t used = split_words(l1).size(); for (size_t i = used; i < words.size(); i++) { std::string cand = l2.empty() ? words[i] : (l2 + " " + words[i]); if ((int)cand.size() <= MAX2) l2 = cand; else break; } if (l1.empty()) l1 = ellipsize(label, MAX1); if (l2.empty() && (int)label.size() > MAX1) { std::string rest = label.substr(std::min((int)label.size(), MAX1)); trim(rest); l2 = ellipsize(rest.empty() ? label : rest, MAX2); } } it.printf(32, 7, *font, TextAlign::CENTER, "%s", l1.c_str()); it.printf(32, 22, *font, TextAlign::CENTER, "%s", l2.c_str());

  • platform: ssd1306_i2c model: "SSD1306 64x32" id: display3 address: 0x3C i2c_id: ch3 update_interval: never lambda: |- auto label = id(b3_label).state; if (label.empty()) label = "Button 3"; for (auto &c : label) { if (c == '_' || c == '-') c = ' '; } auto trim = [](std::string &s) { while (!s.empty() && s.front() == ' ') s.erase(s.begin()); while (!s.empty() && s.back() == ' ') s.pop_back(); }; auto collapse_spaces = [&](std::string &s) { std::string out; bool prev_space = false; for (char c : s) { bool is_space = (c == ' '); if (is_space) { if (!prev_space) out.push_back(' '); } else { out.push_back(c); } prev_space = is_space; } s = out; }; trim(label); collapse_spaces(label);

    const char* f = id(sel_font).current_option(); auto font = &id(font_helv); if (f) { if (!strcmp(f, "Roboto")) font = &id(font_roboto); else if (!strcmp(f, "Inter")) font = &id(font_inter); else if (!strcmp(f, "SourceSans")) font = &id(font_source); else if (!strcmp(f, "Noto")) font = &id(font_noto); else if (!strcmp(f, "RobotoCondensed")) font = &id(font_roboc); else if (!strcmp(f, "InterTight")) font = &id(font_intertight); else if (!strcmp(f, "ArchivoNarrow")) font = &id(font_archivo); else if (!strcmp(f, "JetBrainsMono")) font = &id(font_jbmono); else if (!strcmp(f, "IBMPlexMono")) font = &id(font_ibmmono); }

    const int MAX1 = 8; // max character. const int MAX2 = 8;

    auto split_words = [](const std::string &s) { std::vector<std::string> w; std::string cur; for (char c : s) { if (c == ' ') { if (!cur.empty()) { w.push_back(cur); cur.clear(); } } else cur.push_back(c); } if (!cur.empty()) w.push_back(cur); return w; }; auto ellipsize = [](const std::string &s, int maxlen) -> std::string { if ((int)s.size() <= maxlen) return s; if (maxlen <= 1) return "…"; return s.substr(0, maxlen - 1) + "…"; }; std::string l1, l2; auto words = split_words(label);

    if (words.size() == 2) { l1 = ellipsize(words[0], MAX1); l2 = ellipsize(words[1], MAX2); } else { for (size_t i = 0; i < words.size(); i++) { std::string cand = l1.empty() ? words[i] : (l1 + " " + words[i]); if ((int)cand.size() <= MAX1) l1 = cand; else break; } size_t used = split_words(l1).size(); for (size_t i = used; i < words.size(); i++) { std::string cand = l2.empty() ? words[i] : (l2 + " " + words[i]); if ((int)cand.size() <= MAX2) l2 = cand; else break; } if (l1.empty()) l1 = ellipsize(label, MAX1); if (l2.empty() && (int)label.size() > MAX1) { std::string rest = label.substr(std::min((int)label.size(), MAX1)); trim(rest); l2 = ellipsize(rest.empty() ? label : rest, MAX2); } } it.printf(32, 7, *font, TextAlign::CENTER, "%s", l1.c_str()); it.printf(32, 22, *font, TextAlign::CENTER, "%s", l2.c_str());

  • platform: ssd1306_i2c model: "SSD1306 64x32" id: display4 address: 0x3C i2c_id: ch4 update_interval: never lambda: |- auto label = id(b4_label).state; if (label.empty()) label = "Button 4"; for (auto &c : label) { if (c == '_' || c == '-') c = ' '; } auto trim = [](std::string &s) { while (!s.empty() && s.front() == ' ') s.erase(s.begin()); while (!s.empty() && s.back() == ' ') s.pop_back(); }; auto collapse_spaces = [&](std::string &s) { std::string out; bool prev_space = false; for (char c : s) { bool is_space = (c == ' '); if (is_space) { if (!prev_space) out.push_back(' '); } else { out.push_back(c); } prev_space = is_space; } s = out; }; trim(label); collapse_spaces(label);

    const char* f = id(sel_font).current_option(); auto font = &id(font_helv); if (f) { if (!strcmp(f, "Roboto")) font = &id(font_roboto); else if (!strcmp(f, "Inter")) font = &id(font_inter); else if (!strcmp(f, "SourceSans")) font = &id(font_source); else if (!strcmp(f, "Noto")) font = &id(font_noto); else if (!strcmp(f, "RobotoCondensed")) font = &id(font_roboc); else if (!strcmp(f, "InterTight")) font = &id(font_intertight); else if (!strcmp(f, "ArchivoNarrow")) font = &id(font_archivo); else if (!strcmp(f, "JetBrainsMono")) font = &id(font_jbmono); else if (!strcmp(f, "IBMPlexMono")) font = &id(font_ibmmono); }

    const int MAX1 = 8; // max character. const int MAX2 = 8;

    auto split_words = [](const std::string &s) { std::vector<std::string> w; std::string cur; for (char c : s) { if (c == ' ') { if (!cur.empty()) { w.push_back(cur); cur.clear(); } } else cur.push_back(c); } if (!cur.empty()) w.push_back(cur); return w; }; auto ellipsize = [](const std::string &s, int maxlen) -> std::string { if ((int)s.size() <= maxlen) return s; if (maxlen <= 1) return "…"; return s.substr(0, maxlen - 1) + "…"; }; std::string l1, l2; auto words = split_words(label);

    if (words.size() == 2) { l1 = ellipsize(words[0], MAX1); l2 = ellipsize(words[1], MAX2); } else { for (size_t i = 0; i < words.size(); i++) { std::string cand = l1.empty() ? words[i] : (l1 + " " + words[i]); if ((int)cand.size() <= MAX1) l1 = cand; else break; } size_t used = split_words(l1).size(); for (size_t i = used; i < words.size(); i++) { std::string cand = l2.empty() ? words[i] : (l2 + " " + words[i]); if ((int)cand.size() <= MAX2) l2 = cand; else break; } if (l1.empty()) l1 = ellipsize(label, MAX1); if (l2.empty() && (int)label.size() > MAX1) { std::string rest = label.substr(std::min((int)label.size(), MAX1)); trim(rest); l2 = ellipsize(rest.empty() ? label : rest, MAX2); } } it.printf(32, 7, *font, TextAlign::CENTER, "%s", l1.c_str()); it.printf(32, 22, *font, TextAlign::CENTER, "%s", l2.c_str());

  • platform: ssd1306_i2c model: "SSD1306 64x32" id: display5 address: 0x3C i2c_id: ch5 update_interval: never lambda: |- auto label = id(b5_label).state; if (label.empty()) label = "Button 5"; for (auto &c : label) { if (c == '_' || c == '-') c = ' '; } auto trim = [](std::string &s) { while (!s.empty() && s.front() == ' ') s.erase(s.begin()); while (!s.empty() && s.back() == ' ') s.pop_back(); }; auto collapse_spaces = [&](std::string &s) { std::string out; bool prev_space = false; for (char c : s) { bool is_space = (c == ' '); if (is_space) { if (!prev_space) out.push_back(' '); } else { out.push_back(c); } prev_space = is_space; } s = out; }; trim(label); collapse_spaces(label);

    const char* f = id(sel_font).current_option(); auto font = &id(font_helv); if (f) { if (!strcmp(f, "Roboto")) font = &id(font_roboto); else if (!strcmp(f, "Inter")) font = &id(font_inter); else if (!strcmp(f, "SourceSans")) font = &id(font_source); else if (!strcmp(f, "Noto")) font = &id(font_noto); else if (!strcmp(f, "RobotoCondensed")) font = &id(font_roboc); else if (!strcmp(f, "InterTight")) font = &id(font_intertight); else if (!strcmp(f, "ArchivoNarrow")) font = &id(font_archivo); else if (!strcmp(f, "JetBrainsMono")) font = &id(font_jbmono); else if (!strcmp(f, "IBMPlexMono")) font = &id(font_ibmmono); }

    const int MAX1 = 8; // max character. const int MAX2 = 8;

    auto split_words = [](const std::string &s) { std::vector<std::string> w; std::string cur; for (char c : s) { if (c == ' ') { if (!cur.empty()) { w.push_back(cur); cur.clear(); } } else cur.push_back(c); } if (!cur.empty()) w.push_back(cur); return w; }; auto ellipsize = [](const std::string &s, int maxlen) -> std::string { if ((int)s.size() <= maxlen) return s; if (maxlen <= 1) return "…"; return s.substr(0, maxlen - 1) + "…"; }; std::string l1, l2; auto words = split_words(label);

    if (words.size() == 2) { l1 = ellipsize(words[0], MAX1); l2 = ellipsize(words[1], MAX2); } else { for (size_t i = 0; i < words.size(); i++) { std::string cand = l1.empty() ? words[i] : (l1 + " " + words[i]); if ((int)cand.size() <= MAX1) l1 = cand; else break; } size_t used = split_words(l1).size(); for (size_t i = used; i < words.size(); i++) { std::string cand = l2.empty() ? words[i] : (l2 + " " + words[i]); if ((int)cand.size() <= MAX2) l2 = cand; else break; } if (l1.empty()) l1 = ellipsize(label, MAX1); if (l2.empty() && (int)label.size() > MAX1) { std::string rest = label.substr(std::min((int)label.size(), MAX1)); trim(rest); l2 = ellipsize(rest.empty() ? label : rest, MAX2); } } it.printf(32, 7, *font, TextAlign::CENTER, "%s", l1.c_str()); it.printf(32, 22, *font, TextAlign::CENTER, "%s", l2.c_str());

Tagi