Lazarus on the Raspberry Pi: A Native Dashboard for MQTT Telemetry
Bjørn T. Nøstdahl
~5 min read
Regular readers know that MQTT runs most of my home lab. For years the dashboard showing those readings was a web page served from the Raspberry Pi and displayed in a browser on the same Pi. It worked, but it was heavy: a full browser, a web server, a bundle of JavaScript, all to draw a few lines. This article replaces it with a single native executable built in Lazarus on the Pi itself.
If MQTT is new to you, my older MQTT for dummies article explains the protocol. The short version: devices publish messages to topics on a broker, and clients subscribe to the topics they care about.
Building on the Pi
The simplest way to get Lazarus on a Raspberry Pi is the distribution package:
sudo apt update
sudo apt install lazarus
The version is a release or two behind, but for this project it does not matter. If you want the latest IDE and compiler, fpcupdeluxe has an ARM build and will compile everything from source in an hour or so on a Pi 4.
Cross-compiling from a Windows or Linux desktop to the Pi is possible, and it is the way to go once a project is bigger, but it means setting up a cross-compiler and copying the LCL libraries for the target. For a first project, building on the device avoids all of that. Compile times on a Pi 4 are a few seconds for a project this size, which is still faster than most of my cloud builds.
The broker
I run Mosquitto on the same Pi:
sudo apt install mosquitto mosquitto-clients
mosquitto_sub -t 'lab/#' -v
The second command is the quickest sanity check in IoT: if messages scroll past, the sensors and the broker are fine and any problem is in your code. My sensors publish to topics like lab/boiler/temperature with a plain number as the payload.
Receiving messages without freezing the form
MQTT clients wait for messages, and waiting is exactly what a UI thread must never do. So the client lives in a TThread, and every message is handed to the form through TThread.Queue, which runs a method on the main thread without blocking the worker.
There are several MQTT client libraries for Free Pascal. I used a binding to the native Mosquitto client library, which is already installed when you install the broker, but the structure below is the same whichever library you choose: one line to connect, one to subscribe, and a callback when a message arrives.
type
TReadingEvent = procedure(const Topic: string; Value: Double) of object;
TTelemetryThread = class(TThread)
private
FHost: string;
FOnReading: TReadingEvent;
FTopic, FPayload: string;
procedure Deliver;
procedure HandleMessage(const Topic, Payload: string);
protected
procedure Execute; override;
public
constructor Create(const AHost: string; AOnReading: TReadingEvent);
end;
constructor TTelemetryThread.Create(const AHost: string; AOnReading: TReadingEvent);
begin
inherited Create(False);
FHost := AHost;
FOnReading := AOnReading;
FreeOnTerminate := False;
end;
procedure TTelemetryThread.HandleMessage(const Topic, Payload: string);
begin
// Called on the worker thread by the MQTT library.
FTopic := Topic;
FPayload := Payload;
Synchronize(@Deliver);
end;
procedure TTelemetryThread.Deliver;
var
Value: Double;
Fmt: TFormatSettings;
begin
Fmt := DefaultFormatSettings;
Fmt.DecimalSeparator := '.'; // payloads always use a dot, whatever the Pi's locale
if Assigned(FOnReading) and TryStrToFloat(FPayload, Value, Fmt) then
FOnReading(FTopic, Value);
end;
procedure TTelemetryThread.Execute;
begin
// Connect to FHost, subscribe to 'lab/#' and point the library's
// message callback at HandleMessage. Then loop until Terminated,
// letting the library process network traffic in short slices.
while not Terminated do
ProcessNetworkForUpTo(100); // milliseconds; name depends on your library
end;
Synchronize is used here rather than Queue because the thread reuses its fields for each message; Queue would let a second message overwrite the first before the form had read it. If you want Queue, allocate a small record per message and let the handler free it.
Plotting with TAChart
TAChart ships with Lazarus and runs fine on the Pi. I keep one TLineSeries per topic in a dictionary (TSeriesMap below is specialize TDictionary<string, TLineSeries> from Generics.Collections) and add a point on every reading:
procedure TfrmDashboard.OnReading(const Topic: string; Value: Double);
var
Series: TLineSeries;
begin
if not FSeries.TryGetValue(Topic, Series) then
begin
Series := TLineSeries.Create(chtLab);
Series.Title := Topic;
Series.SeriesColor := NextColour;
chtLab.AddSeries(Series);
FSeries.Add(Topic, Series);
end;
Series.AddXY(Now, Value);
while (Series.Count > 0) and (Series.XValue[0] < Now - OneHour) do
Series.Delete(0);
lblLatest.Caption := Format('%s: %.1f', [Topic, Value]);
end;
The while loop keeps one hour of history per series, which keeps memory flat and the chart readable. Set the bottom axis to a date-time formatter and the chart looks after itself.
Running it as a kiosk
A dashboard on a wall-mounted screen should fill the screen and start by itself. Two properties on the form take care of the first part:
procedure TfrmDashboard.FormCreate(Sender: TObject);
begin
BorderStyle := bsNone;
WindowState := wsFullScreen;
FSeries := TSeriesMap.Create;
FTelemetry := TTelemetryThread.Create('localhost', @OnReading);
end;
procedure TfrmDashboard.FormDestroy(Sender: TObject);
begin
FTelemetry.Terminate;
FTelemetry.WaitFor;
FTelemetry.Free;
FSeries.Free;
end;
For autostart, a .desktop file in ~/.config/autostart launches the program when the desktop session starts. If the Pi runs without a desktop, a systemd service with a minimal X session does the same job, but that is a tutorial of its own.
What it costs
The finished executable is a few megabytes, starts in about a second, and sits at a fraction of the memory the browser used. CPU usage is close to zero between messages. The Pi fans are quieter, and the screen no longer shows a "page unresponsive" dialog once a week. Was the web dashboard ever the right tool for a single screen on a wall?
Questions for you
- Which MQTT library do you use from Free Pascal, and would you recommend it? I would like to compare notes before I commit to one for a larger project.
- Do you build on the Pi, or cross-compile from a desktop? If the latter, what did your setup look like?
- What else is on your lab wall that would be better as a native application than a browser tab?
Email me at bjorn.nostdahl@nostdahl.com. The threading pattern in this article deserves a closer look, so that is the next topic in the series.