diff --git a/README.md b/README.md
index 9fae8532..ccd8b74a 100644
--- a/README.md
+++ b/README.md
@@ -181,7 +181,7 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \
| Plugin | Description | Preview |
|--------|-------------|---------|
-| [Hello World](./plugins/hello-world/) | Plugin development example and starter template | |
+| [Hello World](./plugins/hello-world/) | Plugin development example and starter template |
|
---
diff --git a/docs/assets/hello-world/colors.png b/docs/assets/hello-world/colors.png
new file mode 100644
index 00000000..dbac319e
Binary files /dev/null and b/docs/assets/hello-world/colors.png differ
diff --git a/docs/assets/hello-world/hero.png b/docs/assets/hello-world/hero.png
new file mode 100644
index 00000000..df72947c
Binary files /dev/null and b/docs/assets/hello-world/hero.png differ
diff --git a/docs/assets/hello-world/message-length.png b/docs/assets/hello-world/message-length.png
new file mode 100644
index 00000000..d17f292c
Binary files /dev/null and b/docs/assets/hello-world/message-length.png differ
diff --git a/docs/assets/hello-world/panel-sizes.png b/docs/assets/hello-world/panel-sizes.png
new file mode 100644
index 00000000..a28af521
Binary files /dev/null and b/docs/assets/hello-world/panel-sizes.png differ
diff --git a/docs/assets/hello-world/shots.json b/docs/assets/hello-world/shots.json
new file mode 100644
index 00000000..471dcd67
--- /dev/null
+++ b/docs/assets/hello-world/shots.json
@@ -0,0 +1,220 @@
+{
+ "plugin": "hello-world",
+ "defaults": {
+ "width": 128,
+ "height": 32,
+ "scale": 6,
+ "freeze_time": "2026-09-02T20:26:00+00:00",
+ "config": {
+ "enabled": true
+ }
+ },
+ "shots": [
+ {
+ "name": "hero"
+ },
+ {
+ "name": "time-on",
+ "config": {
+ "show_time": true
+ },
+ "standalone": false
+ },
+ {
+ "name": "time-off",
+ "config": {
+ "show_time": false
+ },
+ "standalone": false
+ },
+ {
+ "name": "col-default",
+ "standalone": false
+ },
+ {
+ "name": "col-amber",
+ "config": {
+ "color": [
+ 255,
+ 176,
+ 0
+ ],
+ "time_color": [
+ 255,
+ 90,
+ 0
+ ]
+ },
+ "standalone": false
+ },
+ {
+ "name": "col-green",
+ "config": {
+ "color": [
+ 0,
+ 255,
+ 120
+ ],
+ "time_color": [
+ 0,
+ 160,
+ 90
+ ]
+ },
+ "standalone": false
+ },
+ {
+ "name": "col-mono",
+ "config": {
+ "color": [
+ 255,
+ 255,
+ 255
+ ],
+ "time_color": [
+ 255,
+ 255,
+ 255
+ ]
+ },
+ "standalone": false
+ },
+ {
+ "name": "msg-short",
+ "config": {
+ "message": "Hi"
+ },
+ "standalone": false
+ },
+ {
+ "name": "msg-default",
+ "standalone": false
+ },
+ {
+ "name": "msg-long",
+ "config": {
+ "message": "Welcome to the workshop"
+ },
+ "standalone": false
+ },
+ {
+ "name": "p-64",
+ "width": 64,
+ "height": 32,
+ "scale": 8,
+ "standalone": false
+ },
+ {
+ "name": "p-128",
+ "width": 128,
+ "height": 32,
+ "scale": 8,
+ "standalone": false
+ },
+ {
+ "name": "p-12864",
+ "width": 128,
+ "height": 64,
+ "scale": 6,
+ "standalone": false
+ },
+ {
+ "name": "p-256",
+ "width": 256,
+ "height": 32,
+ "scale": 4,
+ "standalone": false
+ }
+ ],
+ "composites": [
+ {
+ "name": "show-time",
+ "columns": 2,
+ "cells": [
+ {
+ "shot": "time-on",
+ "label": "show_time: true",
+ "sublabel": "the default; message above, clock below"
+ },
+ {
+ "shot": "time-off",
+ "label": "show_time: false",
+ "sublabel": "message alone, on the centre line"
+ }
+ ]
+ },
+ {
+ "name": "colors",
+ "columns": 2,
+ "cells": [
+ {
+ "shot": "col-default",
+ "label": "The defaults",
+ "sublabel": "white message, cyan time"
+ },
+ {
+ "shot": "col-amber",
+ "label": "Amber",
+ "sublabel": "color and time_color"
+ },
+ {
+ "shot": "col-green",
+ "label": "Green",
+ "sublabel": ""
+ },
+ {
+ "shot": "col-mono",
+ "label": "All white",
+ "sublabel": "both set the same"
+ }
+ ]
+ },
+ {
+ "name": "message-length",
+ "columns": 1,
+ "cells": [
+ {
+ "shot": "msg-short",
+ "label": "\"Hi\"",
+ "sublabel": "short messages sit centred with room to spare"
+ },
+ {
+ "shot": "msg-default",
+ "label": "\"Hello, World!\"",
+ "sublabel": "the default, which just fits a 128-wide panel"
+ },
+ {
+ "shot": "msg-long",
+ "label": "\"Welcome to the workshop\"",
+ "sublabel": "too long: the plugin does not shrink or wrap, so the ends are clipped"
+ }
+ ]
+ },
+ {
+ "name": "panel-sizes",
+ "columns": 1,
+ "cells": [
+ {
+ "shot": "p-64",
+ "label": "64 x 32",
+ "sublabel": "the default message no longer fits"
+ },
+ {
+ "shot": "p-128",
+ "label": "128 x 32",
+ "sublabel": "the common two-panel chain"
+ },
+ {
+ "shot": "p-12864",
+ "label": "128 x 64",
+ "sublabel": "the same content, more breathing room"
+ },
+ {
+ "shot": "p-256",
+ "label": "256 x 32",
+ "sublabel": "a long chain; the text stays centred"
+ }
+ ]
+ }
+ ]
+}
diff --git a/docs/assets/hello-world/show-time.png b/docs/assets/hello-world/show-time.png
new file mode 100644
index 00000000..3d8e3592
Binary files /dev/null and b/docs/assets/hello-world/show-time.png differ
diff --git a/plugins.json b/plugins.json
index f900d57c..59b45b3e 100644
--- a/plugins.json
+++ b/plugins.json
@@ -308,10 +308,10 @@
"plugin_path": "plugins/hello-world",
"stars": 0,
"downloads": 0,
- "last_updated": "2026-05-15",
+ "last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
- "latest_version": "1.0.3"
+ "latest_version": "1.1.0"
},
{
"id": "hockey-scoreboard",
diff --git a/plugins/hello-world/README.md b/plugins/hello-world/README.md
index 9ba1e03c..e4787d85 100644
--- a/plugins/hello-world/README.md
+++ b/plugins/hello-world/README.md
@@ -1,65 +1,136 @@
-# Hello World Plugin
+# Hello World
-A minimal LEDMatrix plugin that displays a customizable greeting and the
-current time. It's primarily here as a working starter template you can
-copy when building your own plugin.
+A deliberately tiny plugin that shows a message and the time. It exists for two
+reasons: to prove your plugin setup works end to end, and to be **the thing you
+copy when starting a new plugin**.
-## What it does
+
-- Displays a configurable message
-- Optionally shows the current time underneath
-- Lets you set the colors of both lines
+*Every image in this README is real plugin output, rendered at the true panel
+size against a frozen clock and then scaled up so the pixels stay pixels.*
+
+---
+
+## Table of Contents
+
+1. [Installation](#installation)
+2. [Checking It Loaded](#checking-it-loaded)
+3. [Configuration Reference](#configuration-reference)
+ - [message and show_time](#message-and-show_time)
+ - [Colours](#colours)
+ - [Examples](#examples)
+4. [Panel Sizes](#panel-sizes)
+5. [Using This as a Template](#using-this-as-a-template)
+6. [Troubleshooting](#troubleshooting)
+7. [Development](#development)
+8. [Support](#support)
+
+---
## Installation
-The Hello World plugin ships with the default Plugin Store, so the easiest
-way to install it is from the LEDMatrix web UI:
+**From the Plugin Store (recommended).** Open the LEDMatrix web interface at
+`http://:5000`, go to **Plugin Manager**, find **Hello World** in
+the **Plugin Store** section, and click **Install**.
+
+**Manually.** Copy this directory into your LEDMatrix `plugin-repos/` and
+restart the display service.
+
+Unlike most plugins here, `enabled` defaults to **`true`** — it is meant to show
+something the moment it is installed.
+
+A ready-made config block is in
+[`example_config.json`](example_config.json):
+
+```json
+{
+ "hello-world": {
+ "enabled": true,
+ "message": "Hello, World!",
+ "show_time": true,
+ "color": [255, 255, 255],
+ "time_color": [0, 255, 255],
+ "display_duration": 10
+ }
+}
+```
+
+---
+
+## Checking It Loaded
-1. Open the web interface (`http://your-pi-ip:5000`)
-2. Open the **Plugin Manager** tab
-3. Find **Hello World** in the **Plugin Store** section and click **Install**
-4. Toggle it on, then click **Restart Display Service** on the **Overview**
- tab
+The quickest check is the **Plugin Manager** tab: installed plugins appear
+under **Installed Plugins**, and a `hello-world` tab appears in the plugin row
+at the top.
-If you'd rather install it from source for local development, copy this
-directory into your LEDMatrix installation's configured plugins
-directory (default `plugin-repos/`):
+From SSH, tail the display log:
```bash
-cp -r plugins/hello-world ~/LEDMatrix/plugin-repos/
-sudo systemctl restart ledmatrix
+sudo journalctl -u ledmatrix -f | grep hello-world
```
-## Configuration
+You should see something like:
+
+```text
+Discovered plugin: hello-world v1.1.0
+Loaded plugin: hello-world
+Hello World plugin initialized with message: 'Hello, World!'
+```
+
+To see it immediately rather than waiting for the rotation, open its tab in the
+web UI and click **Run On-Demand**.
+
+---
+
+## Configuration Reference
+
+Six settings, and all six do something:
+
+| Option | Type | Default | What it does |
+|--------|------|---------|--------------|
+| `enabled` | boolean | `true` | Whether the plugin runs at all |
+| `message` | string | `"Hello, World!"` | The greeting text |
+| `show_time` | boolean | `true` | Show the clock beneath the message |
+| `color` | array | `[255, 255, 255]` | Message colour, `[R, G, B]` |
+| `time_color` | array | `[0, 255, 255]` | Clock colour, `[R, G, B]` |
+| `display_duration` | number | `10` | Seconds on screen before the rotation moves on |
+
+### `message` and `show_time`
+
+
+
+With `show_time` on, the message sits a third of the way down and the clock two
+thirds. With it off, the message alone is drawn on the centre line.
+
+**The message is not shrunk or wrapped.** It is drawn centred at whatever size
+the font gives, so a message wider than the panel is clipped at both ends:
+
+
-Once installed, configuration lives in the plugin's tab in the web UI.
-Under the hood it's stored in `config/config.json` under the `hello-world`
-key.
+The default `Hello, World!` just fits a 128-wide panel. On a 64-wide panel it
+does not — keep the message short, or use the
+[Scrolling Text](../text-display/) plugin, which scrolls and can auto-size.
-| Option | Type | Default | Description |
-|---|---|---|---|
-| `enabled` | boolean | `true` | Enable/disable the plugin |
-| `message` | string | `"Hello, World!"` | The greeting message (1–50 chars) |
-| `show_time` | boolean | `true` | Show current time below message |
-| `color` | `[r, g, b]` | `[255, 255, 255]` | RGB color for the message (white) |
-| `time_color` | `[r, g, b]` | `[0, 255, 255]` | RGB color for the time (cyan) |
-| `display_duration` | number | `10` | Seconds the plugin holds the screen (1–300) |
+### Colours
-The full schema lives in [`config_schema.json`](config_schema.json) and is
-what the web UI's form is generated from.
+
+
+Both are `[R, G, B]` arrays of integers from 0 to 255. The defaults deliberately
+differ so the two lines read as separate things at a glance.
### Examples
-**Minimal:**
+**Minimal** — everything else takes its default:
+
```json
-{
- "hello-world": {
- "enabled": true
- }
-}
+{ "hello-world": { "enabled": true } }
```
-**Custom message and color:**
+**A custom message and colour:**
+
```json
{
"hello-world": {
@@ -71,7 +142,8 @@ what the web UI's form is generated from.
}
```
-**Message only, no time:**
+**Message only, no clock:**
+
```json
{
"hello-world": {
@@ -83,70 +155,122 @@ what the web UI's form is generated from.
}
```
-## Verifying the plugin loaded
+---
-The fastest way is the **Plugin Manager** tab — installed plugins show up
-under **Installed Plugins** and a tab for `hello-world` appears in the
-plugin row at the top.
+## Panel Sizes
-From SSH you can also tail the display log:
+
-```bash
-sudo journalctl -u ledmatrix -f | grep hello-world
-```
+Text is centred horizontally and placed by fractions of the panel height, so
+the layout holds at any size. The only real constraint is message width — see
+above.
-You should see something like:
+---
-```text
-Discovered plugin: hello-world v1.0.2
-Loaded plugin: hello-world
-Hello World plugin initialized with message: 'Hello, World!'
-```
+## Using This as a Template
+
+This plugin is intentionally small enough to read in one sitting, which is why
+it is the recommended starting point for a new plugin.
-To run the plugin once on demand instead of waiting for it in the
-rotation, open its tab in the web UI and click **Run On-Demand**.
+| File | What it is |
+|------|-----------|
+| [`manager.py`](manager.py) | `HelloWorldPlugin`, implementing `update()` and `display()` from `BasePlugin` |
+| [`manifest.json`](manifest.json) | Metadata, entry point, and class name — `class_name` must match the class in `manager.py` exactly |
+| [`config_schema.json`](config_schema.json) | JSON Schema that generates the web UI settings form |
+| [`requirements.txt`](requirements.txt) | Dependencies the plugin loader installs on first run |
+| [`example_config.json`](example_config.json) | A config block to paste into `config/config.json` |
-## Using this as a template
+To start a new plugin: copy this directory, rename it, update `manifest.json`
+(especially `id`, `class_name` and `entry_point`), and replace the bodies of
+`update()` and `display()`.
-Hello World is intentionally tiny so you can read the whole thing in one
-sitting.
+**Two things worth copying deliberately**, because both are easy to get wrong
+and this plugin got them wrong until recently:
-- [`manager.py`](manager.py) — `HelloWorldPlugin` class implementing
- `update()` and `display()` from `BasePlugin`
-- [`manifest.json`](manifest.json) — plugin metadata, entry point, and
- class name (must match the class in `manager.py` exactly)
-- [`config_schema.json`](config_schema.json) — JSON Schema that drives
- the web UI configuration form
-- [`requirements.txt`](requirements.txt) — Python dependencies the
- plugin loader will install on first run
+- **`draw_text(x=...)` is the left edge, not the centre.** To centre text,
+ either omit `x` entirely — the display manager centres it for you — or pass
+ `centered=True` alongside it. Passing `x=width // 2` on its own starts the
+ text at the midpoint and runs it off the right side.
+- **Pass `color` on every `draw_text` call.** It is tempting to branch on which
+ font you got and only pass the colour in one branch; the other branch then
+ silently falls back to white, and the bug only shows up on installs where the
+ font manager is available.
-To start a new plugin, copy this directory, rename it, update
-`manifest.json` (especially `id`, `class_name`, and `entry_point`), and
-replace the body of `update()` / `display()`.
+Fetch in `update()` and draw in `display()` — never hit the network from
+`display()`, which runs every frame.
-For deeper details see the LEDMatrix docs:
+For deeper details, see the LEDMatrix core docs:
- [Plugin Development Guide](https://github.com/ChuckBuilds/LEDMatrix/blob/main/docs/PLUGIN_DEVELOPMENT_GUIDE.md)
- [Plugin API Reference](https://github.com/ChuckBuilds/LEDMatrix/blob/main/docs/PLUGIN_API_REFERENCE.md)
- [Plugin Architecture Spec](https://github.com/ChuckBuilds/LEDMatrix/blob/main/docs/PLUGIN_ARCHITECTURE_SPEC.md)
+- [Advanced Plugin Development](https://github.com/ChuckBuilds/LEDMatrix/blob/main/docs/ADVANCED_PLUGIN_DEVELOPMENT.md)
+
+and this repository's own
+[plugin development docs](../../docs/plugin-development/).
+
+---
## Troubleshooting
-**Plugin doesn't appear in the rotation**
-- Make sure it's enabled in **Plugin Manager** and that you restarted the
- display service afterward.
-- Check the **Logs** tab in the web UI (or `journalctl -u ledmatrix`) for
- errors mentioning `hello-world`.
+**The plugin does not appear in the rotation.**
+Check it is enabled in **Plugin Manager** and that you restarted the display
+service afterwards. Then check the **Logs** tab, or
+`journalctl -u ledmatrix`, for errors mentioning `hello-world`.
+
+**`Class HelloWorldPlugin not found in module`.**
+`class_name` in `manifest.json` must match the class in `manager.py` exactly —
+case-sensitive, no spaces. This is the single most common mistake when copying
+this plugin to start a new one.
+
+**The message is cut off at both ends.**
+It is wider than the panel. The plugin centres but does not resize or wrap; use
+a shorter message or a wider panel.
+
+**Colours look wrong.**
+Each value must be a three-element array of integers from 0 to 255. The
+settings form rejects anything else, but a hand-edited `config.json` will not.
+
+---
+
+## Development
+
+### Project structure
+
+```text
+hello-world/
+├── manifest.json # Plugin metadata and version history
+├── manager.py # HelloWorldPlugin
+├── config_schema.json # Settings schema; source of truth for defaults
+├── example_config.json # A config block to copy
+├── requirements.txt # None beyond the core
+├── QUICK_START.md # Enabling it and verifying it on a Pi
+└── README.md
+```
+
+[`QUICK_START.md`](QUICK_START.md) covers getting it running on a real Pi and
+checking it through the web API; this README covers what the settings do and
+how to build on it.
+
+### Regenerating the images in this README
+
+```bash
+python scripts/render_docs_assets.py --plugin hello-world
+```
+
+`--check` verifies the committed images still match. The clock is frozen in the
+shot list so the time readout does not change on every run.
-**`Class HelloWorldPlugin not found in module`**
-- The `class_name` field in `manifest.json` must exactly match the class
- defined in `manager.py`. They are case-sensitive and must not contain
- spaces.
+---
-**Colors look wrong**
-- Each color value must be a 3-element array of integers from `0` to
- `255`. The form rejects anything else.
+## Support
-## License
+- YouTube:
+- Instagram:
+- Discord:
+- Sponsor: [GitHub Sponsors](https://github.com/sponsors/ChuckBuilds) ·
+ [Buy Me a Coffee](https://buymeacoffee.com/chuckbuilds) ·
+ [Ko-fi](https://ko-fi.com/chuckbuilds/)
-GPL-3.0, same as the LEDMatrix project.
+Released under the GNU General Public License v3.0 — see [LICENSE](LICENSE).
diff --git a/plugins/hello-world/manager.py b/plugins/hello-world/manager.py
index a96208e7..2c945778 100644
--- a/plugins/hello-world/manager.py
+++ b/plugins/hello-world/manager.py
@@ -150,63 +150,50 @@ def display(self, force_clear=False):
except Exception as e:
self.logger.warning(f"Error getting fonts from font manager: {e}")
- # Calculate positions for centered text
+ # draw_text treats x as the LEFT edge unless centered=True is
+ # passed, so x=width // 2 alone starts the text at the midpoint and
+ # runs it off the right. Passing centered=True makes x the centre,
+ # which is what the layout below assumes.
if self.show_time:
# Display message at top, time at bottom
message_y = height // 3
time_y = (2 * height) // 3
- # Draw the greeting message
- if message_font:
- self.display_manager.draw_text(
- self.message,
- x=width // 2,
- y=message_y,
- font=message_font
- )
- else:
- self.display_manager.draw_text(
- self.message,
- x=width // 2,
- y=message_y,
- color=self.color,
- font=self.bdf_font
- )
+ # Draw the greeting message. The font manager's face is used
+ # when there is one, and the bundled BDF otherwise -- but the
+ # configured colour is passed either way. Selecting the font in
+ # a branch that also dropped `color` is how a custom colour
+ # silently stopped applying on any install that has a font
+ # manager.
+ self.display_manager.draw_text(
+ self.message,
+ x=width // 2,
+ y=message_y,
+ color=self.color,
+ font=message_font or self.bdf_font,
+ centered=True
+ )
# Draw the current time
if self.current_time_str:
- if time_font:
- self.display_manager.draw_text(
- self.current_time_str,
- x=width // 2,
- y=time_y,
- font=time_font
- )
- else:
- self.display_manager.draw_text(
- self.current_time_str,
- x=width // 2,
- y=time_y,
- color=self.time_color,
- font=self.bdf_font
- )
- else:
- # Display message centered
- if message_font:
- self.display_manager.draw_text(
- self.message,
- x=width // 2,
- y=height // 2,
- font=message_font
- )
- else:
self.display_manager.draw_text(
- self.message,
+ self.current_time_str,
x=width // 2,
- y=height // 2,
- color=self.color,
- font=self.bdf_font
+ y=time_y,
+ color=self.time_color,
+ font=time_font or self.bdf_font,
+ centered=True
)
+ else:
+ # Message only, on the vertical centre line
+ self.display_manager.draw_text(
+ self.message,
+ x=width // 2,
+ y=height // 2,
+ color=self.color,
+ font=message_font or self.bdf_font,
+ centered=True
+ )
# Update the physical display
self.display_manager.update_display()
diff --git a/plugins/hello-world/manifest.json b/plugins/hello-world/manifest.json
index 4d6d76fa..40666285 100644
--- a/plugins/hello-world/manifest.json
+++ b/plugins/hello-world/manifest.json
@@ -1,7 +1,7 @@
{
"id": "hello-world",
"name": "Hello World",
- "version": "1.0.3",
+ "version": "1.1.0",
"author": "ChuckBuilds",
"description": "A simple test plugin that displays a customizable message",
"entry_point": "manager.py",
@@ -16,6 +16,11 @@
"hello-world"
],
"versions": [
+ {
+ "released": "2026-09-02",
+ "version": "1.1.0",
+ "ledmatrix_min": "2.0.0"
+ },
{
"released": "2026-05-15",
"version": "1.0.3",
@@ -32,7 +37,7 @@
"ledmatrix_min_version": "2.0.0"
}
],
- "last_updated": "2026-05-15",
+ "last_updated": "2026-09-02",
"stars": 0,
"downloads": 0,
"verified": true,