A flexible and extensible LED matrix display system built with .NET 10, designed to run on Raspberry Pi with RGB LED matrices or in a simulated environment for development and testing.
- 🎨 Animated apps - Clocks, ambient scenes, visual effects and everyday boards, all built on a widget and animation toolkit
- 🖥️ Simulator mode - Develop and test apps without hardware, with a live browser preview (WebSocket)
- 🔌 Hardware support - Raspberry Pi RGB LED matrices via the rpi-rgb-led-matrix library
- 🌐 REST API - Control apps, brightness, settings, schedules and notifications
- 🗓️ Scheduler - Playlists that rotate apps, and rules such as "weekdays 07:15-08:45: commute" or "23:00-07:00: dim"
- 🔔 Overlays - Toasts, corner badges and alerts drawn over whatever is running
- 🛡️ Crash card - An app that throws shows its error on the panel instead of freezing
- 📱 Clients - Flutter mobile app and a Home Assistant integration
| Group | Apps (id) |
|---|---|
| Clocks | Clock (clock; style = Animated or Flip, with animated-clock and flip-clock as presets), Countdown Timer (countdown-timer) |
| Ambient | Home (home), Solid Color / mood light (solid_color), Scrolling Text (scrolling-text) |
| Visuals | Rainbow Spiral (rainbow-spiral), Geometric Patterns (geometric-patterns), Bouncing Balls (bouncing-balls), DVD Logo (dvd-logo), Matrix Rain (matrix-rain), Fire (fire), Equalizer (equalizer) |
| Everyday | Weather (weather, Open-Meteo, no key), Commute (commute), Calendar (calendar), Home Assistant tiles (ha-tiles), Spotify (spotify, needs API config) |
| Tube | Tube Departures (tube-departures), Tube Status (tube-status), Tube Line (tube-line) |
| Transport | Bus Arrivals (bus-arrivals), Rail Departures (rail-departures), Cycle Hub (cycle-hub), Road Disruptions (road-disruptions), Journey (journey) |
| Tracking & info | Plane Spotter (plane-spotter), ISS Tracker (iss-tracker), Air Quality (air-quality), Bin Day (bin-day), Morning Briefing (morning-briefing) |
| Custom | JSON screens (screen, see /api/screens) |
| Dev | Widget Demo (widget-demo) |
Apps with settings expose them through GET /api/apps/{id}/settings.
| Key | Used by |
|---|---|
Weather:Location |
Weather, Commute (place name or lat,lon) |
TFL:AppKey, Commute:StationId, Commute:WalkMinutes, TubeDeparturesApp:StationId |
Tube apps, Commute |
Calendar:IcsUrl |
Calendar (an .ics feed; webcal:// works) |
HomeAssistant:BaseUrl, HomeAssistant:Token, HomeAssistant:Entities |
Home Assistant tiles (long-lived access token) |
Display:Width, Display:Height |
Display size (default 256x64) |
Put secrets in appsettings.local.json, which is not committed.
- LedMatrixOS - ASP.NET Core host: DI, device selection, REST endpoints,
ScheduleRunner, WebSocket preview - LedMatrixOS.Core - Engine with no hardware or app specifics:
RenderEngine,FrameBuffer,AppManager,FrameContext, animation (Easing,Tween,Timeline), transitions,Poll<T>data, attribute settings,Scheduling/(playlists and rules),Overlays/,CrashGuard,FrameBroadcaster - LedMatrixOS.Graphics - Canvas helpers, fonts and text, the widget layer (
Node,Stack,Dock,Label,ListView,Pager,RollingNumber,WidgetApp), particles and post-effects - LedMatrixOS.Apps - The built-in apps (register new ones in
Apps.cs) - LedMatrixOS.Hardware.RpiLedMatrix - Raspberry Pi adapter (bindings to librgbmatrix.so)
- LedMatrixOS.Hardware.Simulator - Simulated display for development
- tests/LedMatrixOS.Tests - Unit and snapshot (golden PNG) tests
- flutter_app/, homeassistant/ - Clients of the REST API
- .NET 10.0 SDK or runtime
- Any platform (Windows, Linux, macOS)
- Raspberry Pi (tested on Pi 3/4)
- .NET 10.0 runtime (ARM)
- RGB LED Matrix panels
- rpi-rgb-led-matrix library installed
- Root privileges (for GPIO access)
git clone https://github.com/benfl3713/LedMatrixOS.git
cd LedMatrixOSdotnet buildEdit src/LedMatrixOS/appsettings.json to configure your matrix:
{
"Urls": "http://*:5005",
"Matrix": {
"Rows": 64,
"Cols": 64,
"HardwareMapping": "adafruit-hat-pwm",
"GpioSlowdown": 4,
"ChainLength": 4,
"PwmBits": 7,
"ShowRefreshRate": true
}
}For simulator mode, add appsettings.Development.json:
{
"Matrix": {
"UseSimulator": true
}
}cd src/LedMatrixOS
dotnet run --environment DevelopmentThen open your browser to http://localhost:5005 to see the web preview interface.
cd src/LedMatrixOS
sudo dotnet run --environment ProductionNote: Root privileges are required for GPIO access on Raspberry Pi.
The application exposes a RESTful API for control:
POST /api/apps/{id}- Activate an app by IDcurl -X POST http://localhost:5005/api/apps/animated-clock
-
GET /api/settings- Get current settings (width, height, brightness)curl http://localhost:5005/api/settings
-
POST /api/settings/brightness/{value}- Set brightness (0-100)curl -X POST http://localhost:5005/api/settings/brightness/50
POST /api/overlays/toast- banner over the running app:{"message":"Dinner","seconds":4,"color":"#000000","background":"#ffffff"}POST /api/overlays/badge- corner indicator until dismissed:{"id":"door","color":"#ff3c3c","pulsing":true}DELETE /api/overlays/{id}andDELETE /api/overlays- dismiss one / allPOST /api/notifications/message- full-screen alert, scrolls if long:{"message":"Washing done","color":{"r":255,"g":160,"b":0}}POST /api/notifications- red flash for 5 seconds
POST /api/schedule/reload- re-readschedule.jsonfrom the app directoryGET /api/schedule- what the schedule currently selects
schedule.json holds playlists (apps with durations, optional per-entry settings) and rules (time window, daysMask where Sun=1, Mon=2, ... Sat=64, optional brightnessOverride, priority). Times are local. See src/LedMatrixOS/schedule.example.json. A manual app switch sticks until the playlist next rotates.
GET /api/health- status, active app, display size, uptime
GET /ws/preview- WebSocket of binary frames:[width u16 LE][height u16 LE][RGB bytes], up to 30 fps, only when the picture changedGET /preview- Get current display as PNG (simulator mode only)curl http://localhost:5005/preview -o preview.png
Describe a tree of widgets once; the framework lays it out, animates and draws it. Settings come from attributes and data from Poll:
public class HelloApp : WidgetApp
{
public override string Id => "hello";
public override string Name => "Hello";
[Setting("Greeting", Description = "Text to show")]
public string Greeting { get; set; } = "HELLO";
protected override Node Build() => new Stack(Orientation.Vertical)
{
Children =
{
new Label(() => Greeting) { Style = new TextStyle(Fonts.Big, new Pixel(120, 220, 240)) },
new Clock("HH:mm", Time),
},
};
}Use Time and the frame context rather than DateTime.Now, so tests can drive the clock. Look at CommuteApp or HomeAssistantTilesApp for complete examples with polled data, and tests/LedMatrixOS.Tests/CommuteAppTests.cs for golden-image and allocation tests. Steady-state rendering should allocate nothing (cache TextRuns; rebuild strings only when data changes).
Apps can override these methods for lifecycle management:
OnActivatedAsync()- Called when app becomes active (initialize resources)Update()- Called every frame to update state (WidgetApphandles this andRender()for you)OnDeactivatedAsync()- Called when app is deactivated (cleanup resources)
For apps that need background work (like fetching data):
public override Task OnActivatedAsync((int height, int width) dimensions, CancellationToken cancellationToken)
{
// Start a background task
RunInBackground(async (ct) =>
{
while (!ct.IsCancellationRequested)
{
// Fetch data, etc.
await Task.Delay(TimeSpan.FromMinutes(5), ct);
}
});
return base.OnActivatedAsync(dimensions, cancellationToken);
}Add your app to BuiltInApps.GetAll() in src/LedMatrixOS.Apps/Apps.cs:
public static IEnumerable<Type> GetAll()
{
yield return typeof(MyCustomApp);
// ... other apps
}Configure in appsettings.json:
| Setting | Description | Default |
|---|---|---|
Rows |
Number of rows per panel | 64 |
Cols |
Number of columns per panel | 64 |
HardwareMapping |
Hardware mapping type | adafruit-hat-pwm |
GpioSlowdown |
GPIO slowdown factor (1-4) | 4 |
ChainLength |
Number of chained panels | 4 |
PwmBits |
PWM bits (1-11, lower = faster) | 7 |
ShowRefreshRate |
Show refresh rate on console | true |
Matrix:UseSimulator- Set totruefor simulator modeUrls- HTTP endpoint URL (default:http://*:5005)
LedMatrixOS/
├── src/
│ ├── LedMatrixOS/ # Main web application
│ │ ├── Program.cs # Entry point
│ │ ├── appsettings.json # Configuration
│ │ └── wwwroot/
│ │ └── index.html # Web preview UI
│ ├── LedMatrixOS.Core/ # Engine, scheduling, overlays, animation
│ ├── LedMatrixOS.Graphics/ # Canvas, fonts, widget layer, particles
│ ├── LedMatrixOS.Apps/ # Built-in apps
│ ├── LedMatrixOS.Hardware.RpiLedMatrix/ # Pi hardware
│ └── LedMatrixOS.Hardware.Simulator/ # Simulator
├── tests/LedMatrixOS.Tests/ # Unit and golden-image tests
├── flutter_app/ # Mobile client
├── homeassistant/ # Home Assistant integration
├── LedMatrixOS.sln # Solution file
└── Directory.Build.props # Common build settings
# Build entire solution
dotnet build
# Build specific project
dotnet build src/LedMatrixOS/LedMatrixOS.csproj
# Build for release
dotnet build -c ReleaseRun the automated tests (including golden-image snapshots) with dotnet test. If a visual change is intended, review the images and regenerate with UPDATE_SNAPSHOTS=1 dotnet test; set LED_PREVIEW_DIR to also write enlarged PNGs you can look at.
The simulator mode is perfect for trying apps without hardware:
- Set
Matrix:UseSimulatortotruein configuration - Run the application
- Open
http://localhost:5005in your browser - Use the web UI to switch between apps and adjust settings
Make sure the rpi-rgb-led-matrix library is installed and accessible:
sudo apt-get update
sudo apt-get install librgbmatrix-devLED matrix control requires root privileges:
sudo dotnet runThe LED library drops root privileges (to the daemon user) once the matrix is initialised, so files written next to the binary can fail. Either start the app with --led-no-drop-privs, or set DataDir (config or environment variable) to a directory that user can write, e.g. sudo mkdir -p /var/lib/ledmatrixos && sudo chown daemon:daemon /var/lib/ledmatrixos and DataDir=/var/lib/ledmatrixos. app-settings.json, screens.json and schedule.json are stored there, and existing copies next to the binary are copied over on first start.
Try adjusting these settings in appsettings.json:
- Increase
GpioSlowdown(values 1-4) - Decrease
PwmBitsfor faster refresh - Check power supply (LED matrices need significant power)
Make sure you're running in Development mode:
dotnet run --environment DevelopmentContributions are welcome! To contribute:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-app) - Commit your changes (
git commit -m 'Add amazing app') - Push to the branch (
git push origin feature/amazing-app) - Open a Pull Request
This project is provided as-is for educational and personal use.
- Built with .NET 10
- Uses SixLabors.ImageSharp for graphics
- Hardware support via rpi-rgb-led-matrix by Henner Zeller
- Inspired by the LED matrix community
For issues, questions, or suggestions, please open an issue on GitHub.
