Skip to content

funsocietyirc/esp-marquee

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

# 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 wide

Adjust num_chips, pins, and dimensions as needed for your setup.

1.2 ESPHome project

  • Create a new ESPHome node (e.g. pico-marquee.yaml).
  • Paste the provided configuration into that file.
  • Ensure you have marquee_utils.h alongside the YAML (this contains helpers like parse_bool, sanitize, trim_to_cap).
  • Provide your wifi_ssid, wifi_password, and MQTT credentials in secrets.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.

2. High-Level Behavior

2.1 Message queue

The marquee maintains two parallel vectors:

  • msg_queue_text: std::vector<std::string> of messages
  • msg_queue_hold: std::vector<int> per-message hold times (ms)

Key properties:

  • Queue is persistent (saved to queue_store in 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)

When the queue is empty:

  • The device enters fallback mode (in_fallback = true) and shows one of:

    • Time
    • Weather
    • News

2.2 Fallback modes

fallback_mode (int):

  • 0 = Time
  • 1 = Weather
  • 2 = 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_msg string (from MQTT topic pico-marquee/weather-display)
  • News mode

    • Shows the current news_msg string (from MQTT topic pico-marquee/news-display)

You can switch fallback mode from HA (Marquee Fallback Mode select) or via MQTT (pico-marquee/fallback_mode).

2.3 Inverted 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

3. MQTT API

The node uses topic_prefix: pico-marquee. All topics below are relative to that.

3.1 Basic control topics (string payloads)

Power

  • 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"

Invert

  • Topic: pico-marquee/invert
  • Payload: boolean-ish ("true", "false", etc.)

Example:

service: mqtt.publish
data:
  topic: pico-marquee/invert
  payload: "true"

Direction

  • Topic: pico-marquee/direction
  • Payload: "left" or "right"

Example:

service: mqtt.publish
data:
  topic: pico-marquee/direction
  payload: "right"

Fallback mode

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

Reboot

  • Topic: pico-marquee/reboot
  • Payload: anything (ignored)

Example:

service: mqtt.publish
data:
  topic: pico-marquee/reboot
  payload: "1"

3.2 Weather & News display topics

These are used only in fallback modes:

  • Weather:

    • Topic: pico-marquee/weather-display
    • Payload: plain text (e.g. "Weather: -3°C Snow ")
  • News:

    • Topic: pico-marquee/news-display
    • Payload: plain text (e.g. "Slashdot: Linux 6.12 Released ")

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  "

3.3 Queue operations – simple string topics

All text payloads are run through marquee::sanitize to keep characters display-safe.

Append

  • 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 "

Priority (temporary or sticky)

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! "

Replace (current message)

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

3.4 Queue maintenance topics

Clear

  • 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"

Flush current cycle

  • 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"

Pause / Resume

  • 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"

Pop front / back

  • 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"

Remove by index

  • 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"

Capacity (queue size)

  • Topic: pico-marquee/capacity
  • Payload: integer (1–100)

Example:

service: mqtt.publish
data:
  topic: pico-marquee/capacity
  payload: "30"

Policy (FIFO/LIFO)

  • 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"

3.5 JSON topics

/insert_at – precise insertion

  • 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}

/instruction – global configuration

  • Topic: pico-marquee/instruction

  • Payload (JSON): all fields optional

    • speed_ms: int, min 5
    • intensity: int 0–15
    • direction: "left" or "right"
    • policy: "fifo" or "lifo"
    • capacity: int 1–100
    • dwell_ms: int (tail dwell)
    • scroll_mode: "stop" or "loop"
    • clear: true to 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
    }

/message – rich message API

  • 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 replace or priority is true:

    • If revert_prev = true:

      • Sets up a temporary message (priority) that will revert after cycles cycles.
    • 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
    }

3.6 Status topic

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.


4. Home Assistant Integration

Because mqtt.discovery: true is enabled and template entities are defined, Home Assistant will see:

4.1 Selects

  • select.marquee_queue_policy — options: FIFO, LIFO
  • select.marquee_scroll_mode — options: STOP, LOOP
  • select.marquee_direction — options: left, right
  • select.marquee_fallback_mode — options: Time, Weather, News

4.2 Numbers

  • number.marquee_speed_ms (5–300ms)
  • number.marquee_brightness (0–15)
  • number.marquee_dwell_ms (0–3000ms)

4.3 Switches

  • switch.marquee_power
  • switch.marquee_invert

4.4 Button

  • button.marquee_reboot

4.5 Example: Basic HA control card (Lovelace)

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_reboot

5. Example Home Assistant Automations

5.1 Weather → pico-marquee/weather-display

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

5.2 News → pico-marquee/news-display

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.

5.3 Send a one-off message when a door opens

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
        }

6. Quick Cheat Sheet

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.

About

ESPHome Marquee (Max7219Digit 126*8)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors