| name | hermes-display |
| description | Show anything on the CrowPanel rotary display: build a deck of 360x360 round screens from HTML, images or native widgets, push it over the LAN, and react when the user turns or presses the knob. |
| version | 1.0.0 |
| author | adam |
| license | MIT |
| platforms | ["linux","macos"] |
| metadata | {"hermes":{"tags":["Display","Smart-Home","ESP32","Ambient","HMI"],"homepage":"https://github.com/Incipiens/HermesDisplay"}} |
| prerequisites | {"commands":["python3"]} |
Hermes Display
A 1.46-inch round touchscreen with a rotary knob, sitting on a desk somewhere on
the LAN. You decide what it shows by writing a deck, which is an ordered list of
full-screen cards, and pushing it over HTTP. The user turns the knob to move
between cards and presses to act on one.
Use this whenever the user asks to show, put, or display something on the screen,
the panel, the display, the knob, or "the little round thing".
Screen size drives everything
At 1.46 inches across, a card holds one idea, a headline of four or five words,
and no paragraphs. If the answer needs a paragraph, put a summary on the screen
and send the paragraph to the chat.
How to drive it
Write a deck spec to a temp file and push it:
cat > /tmp/deck.json <<'EOF'
{ "deck": "living-room", "timeout_s": 300, "cards": [ ... ] }
EOF
"$HERMESDISPLAY_HOME/host/hermesdisp.py" push /tmp/deck.json
HERMESDISPLAY_HOME defaults to ~/HermesDisplay. The panel address comes from
HERMESDISPLAY_HOST, or is found by mDNS if that is unset.
Other commands:
| Command | Purpose |
|---|
hermesdisp.py health | Is it awake, what is it showing, how much room is left |
hermesdisp.py clear | Drop the deck, return to the idle clock |
hermesdisp.py brightness 40 | 0-100 |
hermesdisp.py discover | Find panels on the LAN |
Run health first if you have not talked to the panel this session. It tells you
limits.max_cards and confirms the panel is reachable.
When there is more content than there are cards
Expect this most of the time. A network sweep finds 75 hosts and the panel holds
ten. Curate the list yourself rather than truncating it or asking the user to
choose:
- Pick the most useful items and say what rule you applied, such as "I picked
the marquee infrastructure since only 8 fit".
- Put the rest in your chat reply, grouped sensibly, so nothing is lost.
- Offer to swap the selection.
Split the content between the two surfaces instead of repeating it on both. The
panel gets what is worth glancing at; the chat gets the long tail, the detail,
and the follow-up question.
Keeping decks
Save every deck you push to $HERMESDISPLAY_HOME/decks/<deck-id>.json and tell
the user the path. It lets them tweak a selection by hand, and lets you re-push
or amend a deck later without rebuilding it from scratch:
mkdir -p "$HERMESDISPLAY_HOME/decks"
cp /tmp/deck.json "$HERMESDISPLAY_HOME/decks/network-online.json"
Deck spec
{
"deck": "xda-latest",
"timeout_s": 900,
"wrap": true,
"transition": "slide",
"cards": [ { "id": "a1", "type": "html", "html": "...", "label": "...", "action": "..." } ]
}
| Field | Meaning |
|---|
deck | Short id, echoed back in every event |
timeout_s | Seconds of no interaction before the panel returns to its clock. Always set this. Use 300 for a glance, 900 for something to browse, 0 to pin it |
wrap | Knob wraps past the last card back to the first. Default true |
transition | slide or none |
cards | 1 to 10 cards, in knob order |
Every card takes id, label (human name, used in events) and action (an
opaque string handed back to you when the user presses the knob on that card).
Card types
Pick the cheapest type that does the job.
metric — a single number drawn on the device
No browser or image upload, near-instant. Use for temperature, power draw,
counts, prices, anything that is one big number.
{ "id": "t", "type": "metric", "label": "LIVING ROOM",
"value": "21.4", "unit": "degrees", "sub": "updated 12:04", "color": "#4FA3FF" }
text — a heading and a short body, drawn on the device
Also browser-free. Use for a status line, a reminder, a short answer.
{ "id": "n", "type": "text", "title": "Bin day", "body": "Blue bin goes out tonight" }
html — anything else
Rendered to a 360x360 PNG by headless Chromium on this machine, then uploaded.
Use it when you need images, colour, layout, charts, or a logo. Remote <img>
URLs are fetched during rendering, so article thumbnails and album art work.
{ "id": "a1", "type": "html", "label": "Proxmox tips",
"action": "open:https://www.xda-developers.com/...",
"html": "<div class=\"safe\"><img class=\"thumb\" src=\"https://...\"><div class=\"eyebrow\">XDA</div><div class=\"title\">5 Proxmox settings I change</div></div>" }
image_url — a picture, fitted to the screen
{ "id": "p", "type": "image_url", "url": "https://...", "fit": "cover" }
Writing HTML for a round screen
The canvas is 360x360, but the corners sit behind a bezel. Put everything inside
<div class="safe">, a 272x236 band through the middle that is guaranteed
visible. The base stylesheet is already loaded; these classes exist:
| Class | Use |
|---|
.safe | The container. Always the outermost element |
.eyebrow | Small uppercase label above a headline |
.title | Headline, clamped to 3 lines (2 if a .thumb is present) |
.body | Supporting text, clamped to 4 lines |
.huge | A giant number |
.unit | The unit next to a .huge |
.thumb | 272x118 cover-cropped image |
.avatar | 92px circular image |
.rule | Short accent underline |
.row / .dim | Label-and-value rows for a small table |
Rules:
- Never go below 14px. Use 17px body, 21-23px headline, 76px for a hero number.
- Use a dark background with light text. The panel is an IPS screen in a dark
shell and it lives on a desk, so black backgrounds look intentional and white
ones glare.
- One accent colour per deck. Default
#4FA3FF.
- Pass extra CSS in the card's
style field rather than inline <style> tags.
- A card whose
html starts with <html bypasses the base stylesheet entirely.
Only do this when drawing something full-bleed, like a photo or a chart.
Preview before pushing when the layout is unusual:
hermesdisp.py preview /tmp/deck.json --out /tmp/preview writes the PNGs so you
can check them.
Reacting to the knob
The panel emits events over a WebSocket. If a listener is wired up, you receive
JSON like:
{"v":1,"seq":12,"device":"A4B1C2","type":"select","deck":"xda-latest",
"index":2,"card":"a3","action":"open:https://...","label":"ESP32 projects"}
type | Meaning |
|---|
ready | A deck was rendered and is now on screen |
scroll | The user turned the knob to a different card |
select | The user pressed the knob, or tapped the screen |
back | The user held the knob down; the deck was dismissed |
timeout | The deck expired and the clock came back |
Design your action strings so you can act on them without keeping extra state.
open:<url>, hass:light.turn_on:living_room and reply:card-3 all work. The
panel never interprets them; it just hands them back.
Recipes
"What's online on my network" — sweep, work out what each host does rather than
listing bare IPs, pick the eight most useful, one text card each, and list the
remainder in chat.
"What's on XDA right now" — fetch the feed, take the top 3-5, one html card
each with .thumb + .eyebrow + .title, action set to open:<url>,
timeout_s 900.
"Show the temperature in the living room" — one metric card. Do not reach for
HTML.
"How's the server doing" — one html card using .row for CPU / RAM / disk,
timeout_s 300.
"Remind me what I said I'd do today" — one text card per task, up to 10,
action set to done:<task-id> so a press can tick it off.
Failure modes
- More than 10 cards is rejected. Trim, or split into decks.
- A card image over 256 KB is downscaled automatically, but a full-bleed photo
with lots of detail may end up as a JPEG. Prefer flat colour.
html cards need Playwright: pip install playwright && playwright install chromium. If it is missing, fall back to metric / text cards and say so.
- Panel not found: run
discover. If nothing comes back, the panel is on a
different VLAN or asleep. Say so rather than retrying.
Reference
references/deck-schema.md: every field
references/card-html-guide.md: worked HTML examples with previews