Skip to content

Instantly share code, notes, and snippets.

@JonBons
Last active December 28, 2025 02:26
Show Gist options
  • Select an option

  • Save JonBons/2e87ef6778ad32ef6cc107903c38237c to your computer and use it in GitHub Desktop.

Select an option

Save JonBons/2e87ef6778ad32ef6cc107903c38237c to your computer and use it in GitHub Desktop.
esphome:
name: m5stickplus2-tally-cam1
friendly_name: m5-tally-cam1
platformio_options:
upload_speed: 115200
esp32:
board: m5stick-c
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "HA_API_KEY"
ota:
- platform: esphome
password: "OTA_KEY"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "m5-tally-cam1"
password: "AP_PASS"
# MQTT configuration
mqtt:
broker: !secret mqtt_broker
port: !secret mqtt_port
username: !secret mqtt_username
password: !secret mqtt_password
discovery: true # Auto-discover in Home Assistant
on_message:
# Subscribe to the JSON topic for full bus information (includes color)
- topic: 'tallyarbiter/device/${device_id}/bus/${program_bus_id}'
then:
- lambda: |-
// Parse JSON string manually to extract state and color
std::string payload = x.c_str();
bool is_active = payload.find("\"state\":\"ON\"") != std::string::npos;
// Extract busColor from JSON (look for "busColor":"#xxxxxx")
std::string color = "#000000"; // default
size_t color_pos = payload.find("\"busColor\":\"");
if (color_pos != std::string::npos) {
color_pos += 11; // length of "busColor":"
size_t color_end = payload.find("\"", color_pos);
if (color_end != std::string::npos) {
color = payload.substr(color_pos, color_end - color_pos);
}
}
id(program_tally).publish_state(is_active);
if (is_active) {
id(tally_state).publish_state("program");
id(tally_color).publish_state(color);
} else {
if (!id(preview_tally).state) {
id(tally_state).publish_state("empty");
id(tally_color).publish_state("#000000");
}
}
id(m5display).update();
- topic: 'tallyarbiter/device/${device_id}/bus/${preview_bus_id}'
then:
- lambda: |-
// Parse JSON string manually to extract state and color
std::string payload = x.c_str();
bool is_active = payload.find("\"state\":\"ON\"") != std::string::npos;
// Extract busColor from JSON
std::string color = "#000000"; // default
size_t color_pos = payload.find("\"busColor\":\"");
if (color_pos != std::string::npos) {
color_pos += 11; // length of "busColor":"
size_t color_end = payload.find("\"", color_pos);
if (color_end != std::string::npos) {
color = payload.substr(color_pos, color_end - color_pos);
}
}
id(preview_tally).publish_state(is_active);
if (is_active) {
if (!id(program_tally).state) {
id(tally_state).publish_state("preview");
id(tally_color).publish_state(color);
}
} else {
if (!id(program_tally).state) {
id(tally_state).publish_state("empty");
id(tally_color).publish_state("#000000");
}
}
id(m5display).update();
- topic: 'tallyarbiter/device/${device_id}/status'
then:
- lambda: |-
std::string payload = x.c_str();
id(mqtt_connected).publish_state(payload == "online");
# Web server for configuration
web_server:
port: 80
spi:
clk_pin: GPIO13
mosi_pin: GPIO15
# Display configuration for M5StickC Plus2 (135x240)
display:
- platform: st7789v
model: TTGO TDisplay 135x240
cs_pin: GPIO5
dc_pin: GPIO14
reset_pin: GPIO12
rotation: 270
width: 135
height: 240
offset_height: 40
offset_width: 52
update_interval: 500ms
id: m5display
lambda: |-
// Background color from MQTT (hex string like "#3fe481")
auto color_str = id(tally_color).state;
Color bg_color = Color(0, 0, 0); // Black default
// Parse hex color string to RGB
if (color_str.length() == 7 && color_str[0] == '#') {
// Convert hex string to integer
uint32_t color_hex = 0;
for (int i = 1; i < 7; i++) {
char c = color_str[i];
uint8_t val = 0;
if (c >= '0' && c <= '9') val = c - '0';
else if (c >= 'a' && c <= 'f') val = c - 'a' + 10;
else if (c >= 'A' && c <= 'F') val = c - 'A' + 10;
color_hex = (color_hex << 4) | val;
}
// Extract RGB components
uint8_t r = (color_hex >> 16) & 0xFF;
uint8_t g = (color_hex >> 8) & 0xFF;
uint8_t b = color_hex & 0xFF;
bg_color = Color(r, g, b);
} else {
// Fallback to state-based colors
auto state_str = id(tally_state).state;
if (state_str == "program") {
bg_color = Color(255, 0, 0); // Red
} else if (state_str == "preview") {
bg_color = Color(0, 255, 0); // Green
}
}
it.fill(bg_color);
// Get current tally state for display text
auto state = id(tally_state).state;
Color white = Color(255, 255, 255);
Color green = Color(0, 255, 0);
Color red = Color(255, 0, 0);
// Device name
it.printf(10, 10, id(font_small), white, "Device:");
it.printf(10, 25, id(font_medium), white, "%s", id(device_name).state.c_str());
// Tally state
int y_pos = 60;
if (state == "program") {
it.printf(10, y_pos, id(font_large), white, "PROGRAM");
} else if (state == "preview") {
it.printf(10, y_pos, id(font_large), white, "PREVIEW");
} else {
it.printf(10, y_pos, id(font_large), white, "OFF AIR");
}
// Battery info
int battery_y = 180;
it.printf(10, battery_y, id(font_small), white, "Battery: %.0f%%", id(battery_level).state);
// MQTT status
int status_y = 220;
if (id(mqtt_connected).state) {
it.printf(10, status_y, id(font_small), green, "MQTT: Online");
} else {
it.printf(10, status_y, id(font_small), red, "MQTT: Offline");
}
# Fonts
font:
- file: "gfonts://Roboto"
id: font_small
size: 12
- file: "gfonts://Roboto"
id: font_medium
size: 16
- file: "gfonts://Roboto"
id: font_large
size: 32
# Buttons
binary_sensor:
- platform: gpio
pin:
number: GPIO37
inverted: true
name: "Button A"
id: button_a
on_press:
- logger.log: "Button A pressed"
- lambda: |-
id(m5display).update();
- platform: gpio
pin:
number: GPIO39
inverted: true
name: "Button B"
id: button_b
on_press:
- logger.log: "Button B pressed"
- lambda: |-
id(m5display).update();
# Tally state binary sensors
- platform: template
name: "Program Tally"
id: program_tally
device_class: running
- platform: template
name: "Preview Tally"
id: preview_tally
device_class: running
- platform: template
name: "MQTT Connected"
id: mqtt_connected
device_class: connectivity
# Battery sensor
sensor:
- platform: adc
pin: GPIO38
attenuation: 12db
update_interval: 60s
name: "Battery Voltage"
id: battery_voltage
unit_of_measurement: "V"
filters:
- multiply: 2.0 # Voltage divider on Plus2
- platform: template
name: "Battery Level"
id: battery_level
unit_of_measurement: "%"
accuracy_decimals: 0
update_interval: 60s
lambda: |-
float voltage = id(battery_voltage).state;
// M5StickC Plus2 battery calculation
// Typically 3.0V (empty) to 4.2V (full)
float percentage = ((voltage - 3.0) / (4.2 - 3.0)) * 100.0;
if (percentage > 100.0) return 100.0;
if (percentage < 0.0) return 0.0;
return percentage;
# Text sensors
text_sensor:
- platform: template
name: "Tally State"
id: tally_state
lambda: |-
return {"empty"};
update_interval: 1s
- platform: template
name: "Device Name"
id: device_name
lambda: |-
return {"${display_device_name}"};
update_interval: 60s
- platform: template
name: "Tally Color"
id: tally_color
lambda: |-
return {"#000000"};
update_interval: 1s
# Brightness control
light:
- platform: monochromatic
name: "Screen Brightness"
id: screen_brightness
output: screen_brightness_output
default_transition_length: 0s
gamma_correct: 1.0
output:
- platform: ledc
pin: GPIO27 # Backlight pin on Plus2
inverted: false
id: screen_brightness_output
frequency: 1000Hz
# Configuration substitutions
substitutions:
device_name: "m5-tally-cam1" # Used for WiFi AP name
display_device_name: "Cam 1" # Device name displayed on screen
device_id: "ef22f2f4" # Replace with your TallyArbiter device ID
program_bus_id: "334e4eda" # Default Program bus ID
preview_bus_id: "e393251c" # Default Preview bus ID
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment