# Pico Marquee – ESPHome MAX7219 MQTT Marquee
Pico Marquee is an ESPHome configuration for an ESP32-C6 driving a chain of MAX7219 LED matrices (e.g. 16× 8×8 modules → 128×8 display).
It turns your display into a **smart MQTT-driven marquee** with:
- A persistent **message queue** (with FIFO/LIFO modes)
- **Priority / temporary / sticky** messages
- **Fallback modes** when the queue is empty:
- Time (static, non-scrolling)
- Weather (from MQTT)
- News (from MQTT)
- Full control via **MQTT topics** and **Home Assistant entities**:
- Speed, brightness, direction, scroll mode, dwell time
- Invert display (negative video)
- Power, pause/resume, clear, pop front/back, replace, insert_at
- State persistence across reboots
This README documents how to use it from MQTT and Home Assistant.
---
## 1. Hardware & ESPHome Setup
### 1.1 Hardware assumptions
- **MCU**: ESP32-C6
- **Display**: MAX7219 8×8 matrices in a chain (16 chips → 128×8)
- **Connections** (from the YAML):
```yaml
spi:
clk_pin: GPIO6
mosi_pin: GPIO7
display:
- platform: max7219digit
id: marquee_display
cs_pin: GPIO21
num_chips: 16 # 16 * 8 = 128 pixels wideAdjust num_chips, pins, and dimensions as needed for your setup.
- Create a new ESPHome node (e.g.
pico-marquee.yaml). - Paste the provided configuration into that file.
- Ensure you have
marquee_utils.halongside the YAML (this contains helpers likeparse_bool,sanitize,trim_to_cap). - Provide your
wifi_ssid,wifi_password, and MQTT credentials insecrets.yaml.
Example secrets.yaml entries:
wifi_ssid: "YourSSID"
wifi_password: "YourWiFiPassword"
mqtt_host: "192.168.1.10"
mqtt_user: "mqttuser"
mqtt_pass: "mqttpassword"- Compile and flash via ESPHome.
The marquee maintains two parallel vectors:
msg_queue_text:std::vector<std::string>of messagesmsg_queue_hold:std::vector<int>per-message hold times (ms)
Key properties:
- Queue is persistent (saved to
queue_storein JSON), restored at boot. - Capacity is limited by
queue_cap(default 20, configurable). - Policy is FIFO or LIFO (
queue_policy).
When the queue is not empty:
-
The display scrolls messages one by one.
-
Each cycle’s duration is calculated from:
- Initial hold (
hold_ms_current) - Scroll steps (derived from text width and
current_speed_ms) - Tail dwell (
tail_dwell_ms)
- Initial hold (
When the queue is empty:
-
The device enters fallback mode (
in_fallback = true) and shows one of:- Time
- Weather
- News
fallback_mode (int):
0= Time1= Weather2= News
Time / Weather / News are updated on a 1-second / 15-second cadence via the interval blocks.
Behavior:
-
Time mode
- Shows a non-scrolling timestamp
- Format:
DD/MM HH:MM:SSAM/PM(e.g.15/11 08:49:12PM)
-
Weather mode
- Shows the current
weather_msgstring (from MQTT topicpico-marquee/weather-display)
- Shows the current
-
News mode
- Shows the current
news_msgstring (from MQTT topicpico-marquee/news-display)
- Shows the current
You can switch fallback mode from HA (Marquee Fallback Mode select) or via MQTT (pico-marquee/fallback_mode).
The global inverted flag flips the display:
- When off: normal bright text on dark background.
- When on: background lit, text “cut out” (negative/“inverse” look).
You can control it via:
- MQTT topic
pico-marquee/invert - HA switch
Marquee Invert
The node uses topic_prefix: pico-marquee. All topics below are relative to that.
-
Topic:
pico-marquee/power -
Payload: any boolean-ish string:
"1","true","on","ON","True"→ on"0","false","off"→ off
When off:
- MAX7219 is shut down via
turn_on_off(false) - Scroll is disabled, intensity set to 0
Example:
service: mqtt.publish
data:
topic: pico-marquee/power
payload: "off"- Topic:
pico-marquee/invert - Payload: boolean-ish (
"true","false", etc.)
Example:
service: mqtt.publish
data:
topic: pico-marquee/invert
payload: "true"- Topic:
pico-marquee/direction - Payload:
"left"or"right"
Example:
service: mqtt.publish
data:
topic: pico-marquee/direction
payload: "right"- Topic:
pico-marquee/fallback_mode - Payload:
"time","weather", or"news"(case-insensitive)
Example:
service: mqtt.publish
data:
topic: pico-marquee/fallback_mode
payload: "weather"This also syncs to the Marquee Fallback Mode select in HA.
- Topic:
pico-marquee/reboot - Payload: anything (ignored)
Example:
service: mqtt.publish
data:
topic: pico-marquee/reboot
payload: "1"These are used only in fallback modes:
-
Weather:
- Topic:
pico-marquee/weather-display - Payload: plain text (e.g.
"Weather: -3°C Snow ")
- Topic:
-
News:
- Topic:
pico-marquee/news-display - Payload: plain text (e.g.
"Slashdot: Linux 6.12 Released ")
- Topic:
When in fallback and the mode matches (Weather or News), updates to these topics immediately change the display.
Example (manual):
service: mqtt.publish
data:
topic: pico-marquee/weather-display
payload: " Weather: 5°C and sunny "All text payloads are run through marquee::sanitize to keep characters display-safe.
- Topic:
pico-marquee/append - Payload:
string
Appends a message with hold_ms = 0.
- If the queue was empty, it becomes the current message and starts showing immediately.
- Fallback mode is exited.
Example:
service: mqtt.publish
data:
topic: pico-marquee/append
payload: " Job 123 ready for pickup "Temporary priority (respecting revert_default):
-
Topic:
pico-marquee/priority -
Behavior:
-
If
revert_default = true(default):- Shows the message as a temporary priority
- After one cycle (
priority_cycles = 1), reverts to previous message/index
-
If
revert_default = false:- Inserts at front of queue (index 0) and makes that the current message (sticky)
-
Example temporary priority:
service: mqtt.publish
data:
topic: pico-marquee/revert_default
payload: "true"
---
service: mqtt.publish
data:
topic: pico-marquee/priority
payload: " !!! SYSTEM RESTART IN 5 MINUTES !!! "Priority Now (same as above but also ends the current cycle immediately):
- Topic:
pico-marquee/priority_now
Example:
service: mqtt.publish
data:
topic: pico-marquee/priority_now
payload: " High priority alert "Sticky priority (always inserted/front, no revert):
- Topic:
pico-marquee/priority_sticky
Example:
service: mqtt.publish
data:
topic: pico-marquee/priority_sticky
payload: " Welcome to the shop! "Temporary replace:
-
Topic:
pico-marquee/replace -
Behavior:
-
If
revert_default = true:- Temporarily replaces the current message; later reverts
-
If
revert_default = false:- Replaces current queue entry permanently
-
Example sticky replace:
service: mqtt.publish
data:
topic: pico-marquee/revert_default
payload: "false"
---
service: mqtt.publish
data:
topic: pico-marquee/replace
payload: " Updated text for current slot "Sticky replace (no revert, always writes to current index):
- Topic:
pico-marquee/replace_sticky
-
Topic:
pico-marquee/clear -
Payload:
"now"→ also forces scroll reset- anything else → clears queue and stays in fallback
Example:
service: mqtt.publish
data:
topic: pico-marquee/clear
payload: "now"- Topic:
pico-marquee/flush - Ends the current message cycle and advances to the next (if not paused).
Example:
service: mqtt.publish
data:
topic: pico-marquee/flush
payload: "1"- Topic:
pico-marquee/pause— pauses the cycle (shows static text) - Topic:
pico-marquee/resume— resumes scrolling from the current message
service: mqtt.publish
data:
topic: pico-marquee/pause
payload: "1"- Topic:
pico-marquee/pop_front— removes first message - Topic:
pico-marquee/pop_back— removes last message
Example:
service: mqtt.publish
data:
topic: pico-marquee/pop_front
payload: "1"- Topic:
pico-marquee/remove_index - Payload: integer index (0-based)
Example: Remove the 3rd message:
service: mqtt.publish
data:
topic: pico-marquee/remove_index
payload: "2"- Topic:
pico-marquee/capacity - Payload: integer (1–100)
Example:
service: mqtt.publish
data:
topic: pico-marquee/capacity
payload: "30"- Topic:
pico-marquee/policy - Payload:
"fifo"or"lifo"(case-sensitive in config, but you should stick to lowercase)
Example:
service: mqtt.publish
data:
topic: pico-marquee/policy
payload: "lifo"- Topic:
pico-marquee/insert_at - Payload (JSON):
Fields:
text(string, required)index(int, required)hold_ms(int, optional, default 0)
Example: Insert message at index 1 with 5s hold:
service: mqtt.publish
data:
topic: pico-marquee/insert_at
payload: >
{"text":"Middle message","index":1,"hold_ms":5000}-
Topic:
pico-marquee/instruction -
Payload (JSON): all fields optional
speed_ms: int, min 5intensity: int 0–15direction:"left"or"right"policy:"fifo"or"lifo"capacity: int 1–100dwell_ms: int (tail dwell)scroll_mode:"stop"or"loop"clear:trueto clear queue
Example: Adjust speed, brightness, and scroll mode:
service: mqtt.publish
data:
topic: pico-marquee/instruction
payload: >
{
"speed_ms":60,
"intensity":10,
"scroll_mode":"loop",
"dwell_ms":500
}- Topic:
pico-marquee/message - Payload (JSON):
Fields:
text(string, required)hold_ms(int, default 0, negative clamped to 0)replace(bool, default false)priority(bool, default false)revert_prev(bool, default true)cycles(int, min 1, default 1)
Semantics:
-
If
replaceorpriorityistrue:-
If
revert_prev = true:- Sets up a temporary message (priority) that will revert after
cyclescycles.
- Sets up a temporary message (priority) that will revert after
-
If
revert_prev = false:-
Sticky behavior:
replace = true→ replace current queue entry permanently.priority = true→ insert at front of queue.
-
-
-
Else (neither replace nor priority):
-
Append behavior:
- Insert at front if policy = LIFO, else at back.
-
Example: Temporary priority message, revert after 3 cycles:
service: mqtt.publish
data:
topic: pico-marquee/message
payload: >
{
"text": " !!! FIRE DRILL IN PROGRESS !!! ",
"priority": true,
"revert_prev": true,
"cycles": 3,
"hold_ms": 0
}Example: Sticky appended message with 2-second hold:
service: mqtt.publish
data:
topic: pico-marquee/message
payload: >
{
"text": "Thank you for visiting!",
"hold_ms": 2000
}Every 15 seconds, the node publishes a simple status JSON:
- Topic:
pico-marquee/status - Payload example:
{"queue_len": 4}You can use this to monitor queue depth in HA or other dashboards.
Because mqtt.discovery: true is enabled and template entities are defined, Home Assistant will see:
select.marquee_queue_policy— options:FIFO,LIFOselect.marquee_scroll_mode— options:STOP,LOOPselect.marquee_direction— options:left,rightselect.marquee_fallback_mode— options:Time,Weather,News
number.marquee_speed_ms(5–300ms)number.marquee_brightness(0–15)number.marquee_dwell_ms(0–3000ms)
switch.marquee_powerswitch.marquee_invert
button.marquee_reboot
type: entities
title: Pico Marquee
entities:
- entity: switch.marquee_power
- entity: switch.marquee_invert
- entity: select.marquee_fallback_mode
- entity: select.marquee_queue_policy
- entity: select.marquee_scroll_mode
- entity: select.marquee_direction
- entity: number.marquee_speed_ms
- entity: number.marquee_brightness
- entity: number.marquee_dwell_ms
- entity: button.marquee_rebootExample automation using a weather entity weather.home:
alias: Marquee – Update Weather
trigger:
- platform: state
entity_id: weather.home
action:
- service: mqtt.publish
data:
topic: pico-marquee/weather-display
retain: true
payload: >
{%- set s = states('weather.home') -%}
{%- set temp = state_attr('weather.home', 'temperature') -%}
{%- set cond = state_attr('weather.home', 'condition') -%}
Weather: {{ cond | title }} {{ temp }}°C With fallback_mode set to Weather, the marquee will display that string whenever the message queue is empty.
Example using some custom event.slashdot (or similar) entity:
alias: Marquee – Update Slashdot News
trigger:
- platform: state
entity_id: event.slashdot
action:
- service: mqtt.publish
data:
topic: pico-marquee/news-display
retain: true
payload: >
{%- set s = trigger.to_state -%}
{%- if s is not none -%}
{%- if 'headline' in s.attributes -%}
{{ " Slashdot: " ~ s.attributes.headline ~ " " }}
{%- elif 'summary' in s.attributes -%}
{{ " Slashdot: " ~ s.attributes.summary ~ " " }}
{%- else -%}
{{ " Slashdot: " ~ s.state ~ " " }}
{%- endif -%}
{%- else -%}
Slashdot: (no data)
{%- endif %}Set fallback mode to News to make this the default when the queue is empty.
alias: Marquee – Front Door Open
trigger:
- platform: state
entity_id: binary_sensor.front_door
from: "off"
to: "on"
action:
- service: mqtt.publish
data:
topic: pico-marquee/message
payload: >
{
"text": " Front door opened ",
"priority": true,
"revert_prev": true,
"cycles": 2
}Power:
- On:
topic: pico-marquee/power, payload"on" - Off:
topic: pico-marquee/power, payload"off"
Append message:
topic: pico-marquee/append
payload: " Hello World! "Temporary priority message, 3 cycles:
topic: pico-marquee/message
payload: >
{"text":" !!! ALERT !!! ","priority":true,"revert_prev":true,"cycles":3}Weather & News fallback:
- Weather text:
topic: pico-marquee/weather-display - News text:
topic: pico-marquee/news-display - Select fallback mode in HA:
Marquee Fallback Mode(Time / Weather / News)
Invert display:
topic: pico-marquee/invert
payload: "true"Change scroll speed:
topic: pico-marquee/instruction
payload: '{"speed_ms":60}'Clear queue now:
topic: pico-marquee/clear
payload: "now"That’s the whole mental model: messages queue when present, fall back to Time/Weather/News when empty, and everything is driven from MQTT or HA entities. If you want, I can also draft a shorter “user-facing” mini-cheatsheet you can print and stick near the marquee.