Smart Tactile Desk Buttons
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 Springs Thickness: 0.5mm Length: 20mm | ![]() | Misc:
|


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());



