MacSE Weather Display Network
We have a dog and a toddler, do not own a car and bike everywhere. This means that we are affected by weather. A lot. Usually near the entrance, where we put on (the toddler's) clothes.
Questions we usually want answered, several times a day, are:
- Will it rain?
- Will it sun?
- What temperatures can we expect?
- What is the actual temperature we are currently experiencing?
For that reasons I have designed and built a weather reporting station that sits near our entrance, hosted in the shell of an old Macintosh SE. Because I'm a freak and this is one of the computers I grew up with (I'm not as old as that suggests, but I did not have access to current computers for a very long time due to not being massively rich), I also decided to make the UI look somewhat period typical.
The weather station displays two temperature readings, one from each side of the house, and a meteogram from yr.no.
The source code for both the display system and the sensors is available on SourceHut. I'm not a fan of the git-send-mail workflow so I don't use SourceHut for anything that requires collaboration, but for a personal hobby project it's fine.
Design
I run Tokio with whatever's the default number of threads, to power the tiny REST API to receive temperature readings, using Axum. That's extremely overkill, and I'm seriously considering paring it down to just Hyper, but that's a lot of work, probably.
My interface is conceptually extremely simple since it takes no inputs, needs no accessibility, and must fit on precisely one screen with content of a fixed size. The entire front end can just be a reactive rendering of whatever data the back end put in. I need precisely two fonts, both of which are embedded in the binary.
This is surprisingly complicated to implement. Slint, being platform independent and, even worse, targeting several backend languages, has its own markup language. Unfortunately this means anything inexpressible in that language cannot be done reactively. This includes any handling of dates or times as well as string formatting of numbers.
Fortunately this can be handled in Rust using callbacks marked as pure (eg without side effects), but that requires
- writing the Rust function
- Registering it as a callback on the root application window during startup
- Declaring it as a callback on the root application window
- Threading it through the tree of components, down to where the function is actually needed
Some of this spaghetti looks like this:
export component AppWindow inherits Window {
in property <duration> now;
pure callback render_time(duration) -> string;
Clock {
now: now;
render_time(duration) => {
render_time(duration)
}
}
}
component Clock inherits Rectangle {
in property <duration> now;
pure callback render_time(duration) -> string;
Text {
text: "\{render_time(now)}";
}
}
And then, on the Rust side, in main():
let app = AppWindow::new().unwrap();
app.on_render_time(|unix_utc_stamp| {
DateTime::from_timestamp_millis(unix_utc_stamp)
.unwrap()
.with_timezone(&chrono_tz::Europe::Stockholm)
.format("%H:%M %A week %V")
.to_shared_string()
});
Note that due to the two-language problem, we have to work with raw Unix timestamps, since chrono's complicated types can never make its way into Slint land. Many Rust concepts you want are also inexpressible, including Options, which would have been great for uninitialised values. Presumably this is because Slint targets C++.
A second issue is Slint isn't good about failures (yet). The usual failure mode is "the thing renders or runs incorrectly and you don't know why". This includes when I tried to use inheritance to remove code duplication between the "windows". After a couple of hours I just gave up getting their contents to draw at all and inlined the common bits. It's also picky about what happens on which thread, which is helpfully stated in the documentation. It is however nontrivial to figure out what code will run where due to some of the helpful frameworking.
I run three threads:
- Tokio × Axum for the REST API, modulo whatever circus of threads Tokio sets up
- One thread which downloads, modifies, rasterises, and dithers te meteogram, then sleeps
- One thread which runs UI stuff
Is this more threads than necessary? Yes. Does it matter? No.
Sensor nodes
The sensor nodes are meant to be dirt simple. They're ESP32's (one of them is a C model or other, I forget which one). They essentially run an infinite loop of the type: read temperature, POST temperature. If failure, try again forever with some sort of backoff. They do not keep time, since that is annoying. Instead, the server that receives the readings time stamps it.
Since there are no guarantees in networks, this means times can be off, but in reality, if our network is that broken we have worse problems.
Experiences
This is one of the things Rust doesn't do well. In particular, I would ideally use my big honking developer machine to compile the Program and later run it on the terribly underpowered Raspberry Pi 3. That's not an option. I cannot stress this enough. I've used virtual machines. I've used Linux machines of various kinds. I've tried Windows Subsystem For Linux™. I've tried all of this in a jenga tower of docker containers that promise to be batteries included does all of it for you in one easy step etc etc. It. Does. Not. Work.
And it doesn't work in the worst possible way; you do a lot of work for one cryptic error message that gives you nothing to go on.
Problems I've run into:
- The framebuffer backend for Slint requires, for some reason, libudev, which isn't available anywhere and requires a terrible
-syscrate that of course is murder to link - Lol, you need a cross linker idiot, gl hf finding it it isn't available anywhere. Oh and the ones that are don't work. Why? Who knows.
- libc doesn't match and so you get dynamic linking error
- For some reason the Wayland program I'm compiling needs to link to some libx11 or other, and that's of course the wrong version
- Ok ok you've managed to get past all of the above and it does compile and run, but somehow the linked Wayland libraries are the wrong version and now it doesn't work with the Wayland that ships with Raspbian (or, if it does work now, good luck having it work in a year)
I've literally spent more time trying to compile and run a minimal program I myself wrote than actually writing the program.
My conclusion from this is: do. Not. Depend. Do the hundred rabbits thing and only write code in your weird assembler for your own weird VM. Manually blit pixels onto a framebuffer you get directly from whatever device descriptor Linux exposes. Libc? Don't know her.
Build a log cabin. Live off the land. Write a manif... ok I see where this is going.
How I solved it? Uh, by compiling on the Raspberry Pi. Over night. With one job, with link time optimisation turned off, because otherwise somewhere in the literally 500 dependencies the 1 GB RAM runs out and swapping on the SD card wasn't exactly good.