Installing Linux Mint 21 on a Late 2013 iMac with Fusion Drive

I’ve had my old Late 2013 27″ iMac sitting in its box in our back room ever since I bought my Apple Silicon MacBook Pro at the end of 2021, I did some cleaning and tidying the other weekend and ended having enough space to be able to set the iMac up on the desk with a keyboard and mouse. While it’s not a retina-quality screen, it is a quite nice display, and there have been a few occasions when I’ve been doing something out in the back room and wanted to look something up but it was awkward doing it on my phone.

Once I set the iMac up I turned it on, it still works fine but the latest OS it can run is Mac OS X 10.15 Catalina which is ooold. The latest Firefox still works on it, but the rest of the OS would be a Swiss cheese of security holes so I figured I would do what I do to basically all my old Macs at some point or another and install Linux on it. 😛

I remember trying to install Linux on this machine a few years ago and having zero luck at even booting it, though I mostly just gave up and went and did something else at that point, there wasn’t much troubleshooting actually done! As with my previous adventures installing it on my old MacBook Air, I went with Linux Mint because I’ve found it to be the easiest to use and has wide hardware support.

During my research I found this repository on GitHub which claims to be a guide but doesn’t actually have any guides to follow, just a selection of software versions of various things. Even so, it was quite helpful because it seems that anything that uses kernel version newer than 5.x will have problems on the iMac’s specific hardware, and the latest Linux Mint 22 uses the 6.x line. Linux Mint 21 still uses the 5.x kernel, so I grabbed the latest as of writing 21.3 Cinnamon Edition from their main downloads page and imaged it onto a portable SSD using Raspberry Pi Imager (even though the iMac has an SD card slot, you can’t boot from an SD card sadly).

Step 1: Splitting the Fusion Drive

My model of iMac was one that has Apple’s “Fusion Drive”, which is marketing speak for a smaller SSD combined with a large hard disk that appears as one single drive to the OS, and where the OS puts the system and applications onto the SSD for fast access. The size of the SSD shrunk in later iMac revisions before they went all-SSD, but the one in the Late 2013 iMac is 128GB which is plenty for just installing Linux onto, and I was going to partition it so I could still have a Mac OS X installation.

The first thing is that the Fusion Drive needs to be split up so it appears as its two separate drives in order to partition them, so boot into Recovery Mode by holding down Cmd-R as soon as you press the power button, then go into the Utilities menu and open Terminal.

Run diskutil apfs list and you’ll see an entry that says something like APFS Container Reference: disk3 (Fusion). That disk number (disk3) is the identifier for the Fusion Drive, so run disktuil apfs deletecontainer disk3 to break the Fusion Drive apart, then you can close the Terminal window and open Disk Utility to format or otherwise partition the drives. I split the SSD into a 75GB partition for Linux Mint and the rest for Mac OS X, and partitioned the hard drive exactly in half, half of which will be used to mount my home folder under Linux Mint, and the other half for use by Mac OS X.

Step 2: Install Linux Mint

At this point it’s pretty straightforwards, reboot and hold down Option to select the “EFI Boot” disk that is the Linux Mint live image on the external SSD and follow the installer steps to format the partitions from Step 1 above, I set the 75GB portion of the SSD to be mounted at / and the half of the hard disk as /home.

Once it’s all installed, it’ll prompt you to reboot.

Step 3: Getting wifi working

Fortunately unlike some distributions (looking at you, Debian) Linux Mint actually includes the wifi drivers that the iMac needs, but because they’re proprietary it doesn’t install them by default. You install them by running the Driver Manager, but if you simply open Driver Manager from the application menu it complains that you’re offline and doesn’t give you any options. It points out that you can also use installation media, but for whatever reason even though the portable SSD was still plugged in, it wasn’t actually mounted so Driver Manager couldn’t see it.

Instead of mucking around with mount commands, you can just open a terminal window and type mintdrivers, which will open the Driver Manager but detect that the installation media is there and offer to mount it for you. Once that’s done you can enable both the NVIDIA drivers and the Broadcom wifi drivers and reboot when prompted, and you’ll be up and running!

Miscellanous odds and ends

Linux’s equivalent of macOS’s Night Shift

I had been using F.lux for many many years to tint my screen orange at night to make it easier on the eyes after dark, and switched to the macOS/iOS native “Night Shift” when that was added a number of years back. F.lux exists for Linux but it hasn’t been updated since 2013 and doesn’t work anymore. Linux Mint 22 has a new thing called Night Light that’s the exact same thing as Night Shift and gives you a nice UI to control everything from, but unfortunately it doesn’t come with Mint 21. There is an older application called Redshift but the automatic sunset/sunrise detection relies on another executable geoclue2 that isn’t being updated either and the Redshift applet installed with Mint doesn’t give you any configuration options. Fortunately you can configure Redshift manually, so create a file ~/.config/redshift.conf and drop the following contents into it:

[redshift]
temp-day=6500
temp-night=3700
gamma=0.8
fade=1
adjustment-method=randr
location-provider=manual

[manual]
lat=-33.8
lon=151.2

You may need to adjust the temp-* values to taste, lower is warmer, and the [manual] section at the bottom contains the latitude and longitude for your current location. fade=1 will mean the colour will change gradually when sunset happens, if it’s set to 0 it’s an immediately switch from day to night mode.

Once that’s created, find Redshift in the application menu and open it and if it’s night time, you’ll see the nice orange-tinted colour!

Changing keyboard shortcuts to match the Mac’s with Toshy

One of my biggest dislikes when using Linux (or Windows for that matter) is that the primary shortcut key is Control. Apart from the muscle memory of my entire life worth of using Macs, using Control as the primary key for shortcuts is ergonomically extremely awkward. Plus you end up with the really stupid situation of copy being Ctrl-C except for if you’re in a terminal session and it’s Ctrl-Shift-C because Ctrl-C interferes with the standard UNIX shortcut for terminating the currently-running process.

A little while back I discovered Toshy, which is a whole package that modifies all the standard Linux keyboard shortcuts to be remapped to use Command instead of Control as the primary shortcut, so it feels exactly like you’re on a Mac and aren’t constantly breaking muscle memory. It works extremely well to the point where as I type this on my Asahi Linux-equipped M1 MacBook Pro, I’m frequently forgetting that I’m even using Linux at all because all my shortcuts are just working completely seamlessly!

Migrating from iTunes to Navidrome

I posted of my woes with reliably syncing music onto my iPhone back in October last year, and after I had written that post I discovered an additional irritation in that a bunch of random albums just… didn’t have their album artwork showing when viewed on my iPhone. The art was there in iTunes, it was there embedded in the damn music file, but the iPhone just stubbornly refused to show it even after another full remove-all-the-music-and-resync (which took well over twelve hours — ?!? — because apparently iTunes won’t transcode and then sync more than one single file at once and it does them in serial). I even nuked the iTunes artwork cache and rebuilt it then resynced, to no avail. At this point I decided I wanted to seriously start looking at alternatives.

I had already been dabbling a little bit with Navidrome, although it seemed like it was a lot more focused on streaming rather than what I wanted to do (have all my music synced to my iPhone so I didn’t need to rely on a decent internet connection when listening to music during my commute). Navidrome uses the OpenSubsonic API and so any client that uses that protocol can talk to it, and it turns out that the API has a download endpoint and you can have it set up whereby that download will be transcoded to a smaller format first, which is great because an increasingly-large portion of my music library is lossless and so it won’t actually fit on my phone anymore. At this point I figured I’d go ahead and see about migrating for real.

Initial music import

Navidrome is quite simple, you point it at a directory of music and it’ll read all the metadata for album/artist/etc. and give you a nice shiny UI to listen to it with. It has no ability to make any changes to the music files and is strictly read-only, so I started out by simply pointing it to the existing music directory on my laptop and seeing what happened.

The first obvious thing to check was to compare the number of songs that iTunes reported and the number that Navidrome had picked up, and these did not actually match! I poked around and discovered two reasons for this. First and most obvious, there were a couple of albums that I somehow had sitting my music directory that hadn’t been added to iTunes, but of course Navidrome happily picked up. 🤦🏻‍♀️

Secondly, there were a number of random files that had a .m4p extension, which is the extension that iTunes used for the old DRM-protected music files that you used to buy from the iTunes Store before Apple did away with DRMing them. Navidrome saw this extension and was like “Nope, not even going to try to read this!”

I definitely did not have any DRM music left, I’d redownloaded all my DRMed stuff and replaced it with better quality non-DRM files pretty much as soon as Apple allowed that, but for some reason some (but not all!) of the files’ extensions had remained as .m4p. A quick rename to .m4a and Navidrome happily picked those files up, and at this point the song counts between iTunes and Navidrome matched.

Importing play counts and ratings from iTunes

Similar to iTunes, Navidrome keeps track of play counts across individual songs and supports ratings and favouriting items, but Navidrome also aggregates up to the album and artist level so you can see how many plays of a given album or overall artist you’ve had. My next order of business was figuring out how to get my play counts and ratings imported from iTunes into Navidrome, because I’ve been using iTunes for my music ever since I got my original iPod way back in 2001 and I didn’t want to lose all that history.

Given Navidrome is open source I figured someone had to have written something so I went digging, and found a Python script from three years ago that someone had written to take the XML export of an iTunes library, read it, and write that data into the SQLite file that Navidrome uses. It didn’t actually work because Navidrome’s database structure had changed between the version of Navidrome that the script had been written to run against and what was current, but it was a good excuse to do some Python hacking. Over the course of quite a few weeks’ worth of evenings I got it working and made a number of improvements, plus tidied up the code and added a lot of clarifying comments to explain what was going on, and have published the code over on Codeberg.

The main issue I ran into when I ran it against my full music library was XML encoding problems with file names, I described this in the README and how to work around them, but otherwise it was quite seamless once I’d worked out all the kinks in the script itself.

Sound Check and ReplayGain

iTunes has long had an option called “Sound Check” that you can enable that analyses the actual loudness of every song and then adds a tag into that song file to tell iTunes how much to adjust the playback volume by so your whole library plays back at the same relative loudness. This is extra good when you tend to listen to your whole library on shuffle like I do where albums from different eras were mastered at different volumes: Sound Check saves you from having your ears blasted out when a song that’s much louder than another comes on and you have to scramble to adjust the volume down (and then back up when a quiet song comes on, and so on).

There’s a thing called ReplayGain that does the same thing as Sound Check but better and more accurately, and I have long been using a piece of software called iVolume that analyses your music using ReplayGain then overwrites iTunes’ own Sound Check tag with the ReplayGain value. That tag is an iTunes-specific thing and not any sort of standard that any other player uses (because of course), so I needed to run my whole music library through something that would update the files with the actual ReplayGain tag that players will read.

It turns out there are quite a few options to choose from, I ended up using the rsgain command line utility installed via Homebrew and letting it loose it in easy mode against my entire music directory. And as I subsequently discovered later on, MusicBrainz Picard has a plugin system and one of those plugins is called “ReplayGain 2.0” which will use your rsgain installation and allows you to generate the ReplayGain tags as part of updating the rest of the metadata.

Mobile client apps

On desktop I can just use Navidrome’s UI, but I needed a mobile client for when I’m commuting or just otherwise out of the house and listening to music. The first batch I found were quite janky open-source-feeling ones, but then I discovered first Nautiline which actually feels like a properly native iOS app, and then subsequently Arpeggi. Both are highly recommended if you’re on iPhone or iPad.

Transcoding

Because my music library has quite a few lossless files in it, I can’t just actually fit my whole library onto my phone anymore without transcoding to a smaller lossy format.

(Before anything else, since it’s the machine that Navidrome is running on that’s responsible for the transcoding, you need to have ffmpeg installed, brew install ffmpeg will do the trick if you’re using Homebrew.)

Navidrome has a number of options for controlling transcoding, but it’s very confusing because the client app you’re using also can set options, and it’s been rather difficult to figure out the exact incantation of options configured everywhere to do what I want, namely always transcode to Opus at around 160Kbps and download in that same format.

For Navidrome itself, the only configuration setting I’ve tweaked has been to set the AutoTranscodeDownload option to true. The client side is more confusing though, because there are separate options for downloading, plus transcoding based on whether you’re on a mobile connection or wifi and a lot of the transcoding decision seems to come down to the client app’s request.

In Arpeggi, I’ve gotten it working how I want by going into the Playback settings and setting the “Cache Type” to “Download”, and the “Cellular” and “WiFi” options under “Transcoding” to both be Opus at 160Kbps, with the “Only transcode lossless files” option off.

For Nautiline, the magic configuration seems to be going into Settings > Transcoding, setting the Format to “Opus”, the “Connection Quality” for both Cellular and Wi-Fi to “Low”, then configuring both High Quality and Low Quality to 160Kbps and “Transcode Lossless” under “High Quality” to be “Always”. Then under Settings > Downloads & Cache, “Transcode Lossless” should be on.

Running Navidrome as a service

Up until this point I had been running Navidrome just on my local laptop by manually running the navidrome executable in a terminal window, but to actually move this into “production” as it were, Navidrome would need to be running a service at all times on our home server Mac mini, and all the music files would need to be transferred to it as well.

I copied across all of the music via rsync, plus the Navidrome SQLite database file containing my imported iTunes data, fired up Navidrome on the Mini and added it as a server in my iOS client app, but when I tried to play music… nothing happened. After a bunch of confusion, I figured out what was going on: VNCing into the Mac mini revealed that it was sitting at the “Do you want to allow navidrome access to removable disks?” prompt, because all of the music was on the external hard disk attached to the Mac mini, and until I clicked yes Navidrome couldn’t actually read the files. 🤦🏻‍♀️ I also discovered that this gets reset on every Navidrome upgrade, so each time Navidrome is upgraded it’ll require another trip into VNC to allow it to access the external drive.

With that out of the way, it was time to keep Navidrome itself up and running, and launched at boot. The Navidrome installation instructions for macOS helpfully include an example LaunchAgent plist file you can use, but when running it like this I found that the location that Homebrew installed ffmpeg into wasn’t actually in the $PATH that Navidrome uses so it wasn’t able to transcode anything. Navidrome has a specific FFmpegPath configuration option that can be set in the configuration file, so I just pointed that at the ffmpeg executable (/opt/homebrew/bin/ffmpeg in my case).

I also discovered that the trusty old launchctl load and launchctl unload commands to start and stop a LaunchAgent are actually considered “legacy” and there’s a new and significantly more confusing way of doing it. This blog post has a good overview, but the tl;dr is:

  • Use launchctl bootstrap gui/$(id -u) <PATH_TO_PLIST> to start a LaunchAgent
  • Use launchctl bootout gui/$(id -u) <PATH_TO_PLIST> to stop it

To save myself having to remember this ridiculous incantation, I wrote a quick shell script called navidromectl and call it with navidromectl start or navidromectl stop:

#!/usr/bin/env bash

if [[ $# -eq 0 ]] ; then
    echo "Usage: navidromectl [start|stop]"
    exit 1
fi

ACTION=$1

case $ACTION in
    "start")
        launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/navidrome.plist
        ;;
    "stop")
        launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/navidrome.plist
        ;;
esac

Automatically organising newly-imported music

iTunes has a pair of options, “Copy files to Media folder when adding to library” and “Keep Media folder organised” that when enabled mean that files added to iTunes will be copied to wherever your music is stored and automatically sorted into an $ARTIST/$ALBUM folder structure, with the files themselves named as $TRACK_NUMBER $TITLE. I wanted some way of replicating this once I was fully on Navidrome, but since Navidrome is read-only and does no organisation of anything on its own, I would need some other solution.

I went digging and discovered this frankly ridiculous Python package called Beets. It does basically everything and the kitchen sink, and has a whole system to write your own plugins as well in case it doesn’t do what you need it to. As well as doing the automatic organisation and file naming I was after, it can also fetch file metadata from MusicBrainz (the same as what I’m doing with MusicBrainz Picard), calculate ReplayGain values, and write it all to the files before it organises them.

I didn’t actually need the vast majority of what it does, but it does do the things I need it to well (i.e. point it at an “Import” directory and have it automatically move and rename things), and after a bunch of fiddling with configuration options, I’ve settled on this beets.yaml configuration file:

# The directory where the music library lives
directory: /Volumes/Multifarious/Music/virtualwolf
library: ~/Library/Application Support/beets/library.db

# This is required for the disc_and_track template below
# to work
plugins:
  - inline

item_fields:
  # Include the disc number at the beginning of the
  # filename ONLY if there is more than one disc
  disc_and_track: f"{disc:01d}-{track:02d}" if disctotal > 1 else f"{track:02d}"

# Set the desired file and folder organisation
paths:
  default: $albumartist/$album%aunique{}/$disc_and_track $title
  comp: Compilations/$album%aunique{}/$disc_and_track $title

import:
  log: ~/Library/Application Support/beets/beets.log
  quiet: true

  # This is important to allow the "Import anything
  # dropped into this directory" workflow 
  incremental: true

  # Don't write any tags to files
  autotag: false

  # Move the files from the import directory
  move: true
  
  # Don't write tag changes to files since I'm handling
  # that with MusicBrainz Picard earlier
  write: false

The last missing magic ingredient here was the ability to automatically have Beets run when I drop music into the Import directory (with the setup above, I’d need to SSH into the Mac mini, activate the Python virtualenv that the beet package is installed in, and then run the import command).

As it turns out, there is a built-in solution in macOS to automatically run a script or perform an action when something is added to a folder: Folder Actions. Firstly the action itself needs to be created, and then it gets attached to a folder.

To create the action, open the Automator application, go File menu > New, and choose “Folder Action”. In the right-hand pane at the top where it says “Folder Action receives files and folders added to” click on “Choose folder” and select the folder that’s going to be using to drop the music into to be auto-imported, then find the “Run Shell Script” action in the left-hand pane and double-click on it to add that action. Set the “Shell” dropdown to /bin/bash, set the “Pass input” dropdown to “as arguments”, and add the beet import command into there. Mine looks like this because I’m also using a virtualenv to install Beets into, and that directory argument to beet import on the second line is the same directory as the “Folder Action receives files and folders added to” dropdown:

. /Users/virtualwolf/Library/Application\ Support/beets/.venv/bin/activate
beet import "/Volumes/Multifarious/Music/Import/"

Save the workflow as “Import Music” or whatever, then in the Finder go to that directory you’ll be using for imports, right-click on it, and go to Services > Folder Actions Set-Up…”, then click “Run Service” at the prompt. It shows a list of actions, one of which will be the shiny new Import Music option. Select that, and job done! Once a directory full of music is dropped into that Import directory, Beets will trigger and automatically grab the files, read their metadata, and move them into the correct locations in your music directory.

One thing to note here is that Beets keeps a log of which directories it’s already imported, in the state.pickle file in its config directory. I was tripped up by this when I was trying to test that everything was working because I was repeatedly using the same directory to test the importing, and once it had been imported by Beets one time, that was it, it would be ignored from then on and I couldn’t figure out out what on earth was going on. So if you’re doing testing, make sure you use unique directory names before you drop them into the import directory that Beets is looking at!

UPDATE: One thing I realised with this is that it falls over if you’re copying files into the Import directory rather than just moving them from another location on the same drive. If you copy them, they don’t all arrive in the Import directory all at once and so as soon as the first file or two have successfully copied over, the folder action triggers, Beets dutifully runs and imports the files that are there, and then ignores the rest when they’re finished. So this is a bit of a two-step process now, I copy onto the drive that all my music lives on, and then move it into the Import directory in one hit so Beets can do its thing.

Smart playlists and one-click downloading

I suspect I’m probably an outlier amongst music-listeners, but I don’t tend to use regular playlists at all. In iTunes I have a handful of smart playlists set up, and by far my most heavily-used one is my “Random” playlist which is every song that I haven’t listened to in the last two years. If I’m not listening to a specific album, typically I’ll just chuck that playlist on shuffle.

Navidrome has the concept of smart playlists as well, though they’re currently in beta and are not currently exposed through the UI, but you can manually create files with the correct syntax, drop them somewhere in the music directory, and they’ll show up.

To recreate my Random playlist, I used this:

{
  "all": [
    { "notInTheLast": { "lastplayed": 730 } }
  ],
  "sort": "lastplayed",
  "comment": "All songs that have not been played in the last two years."
}

The other thing I wanted was an easy way to just download (and thus transcode) everything from Navidrome into my mobile client app. There isn’t any direct way to do that in either Nautiline or Arpeggi, but creating a smart playlist that lists every single song did the trick:

{
  "all": [{ "gt": { "playCount": -1 } }],
  "sort": "album",
  "comment": "Every single song, to one-click download for offline listening."
}

Opening that playlist then downloading the contents worked a treat, and as a bonus Arpeggi has an “Auto-download” option for playlists, so as soon as new music is imported into Navidrome, Arpeggi picks that up and downloads those new songs!

Making Navidrome accessible from outside the local network

The very last piece that I needed to set up was making it possible to access Navidrome outside of our local network. I didn’t want to expose it directly to the internet via port forwarding, but I also couldn’t set up Wireguard so my devices could VPN in because I wanted to be able to access Navidrome via my work machine so I can listen to music on my fancy speakers while working, and my workplace uses Cloudflare WARP which is Wireguard under the covers (and I suspect the security team would very much frown on me running a VPN to somewhere random).

A while back I had set up a Cloudflare Tunnel to my internal Grafana and InfluxDB instances, which meant I had a publicly-accessible domain that I could get to on my work laptop, but actually loading it required a login through Cloudflare (specifically a 2FA code via email). It was straightforward enough to set that up for Navidrome too, but the wrinkle was that the mobile client apps didn’t know anything about Cloudflare and so if that externally-available address was added to them they’d be redirected to Cloudflare instead of getting the expected OpenSubsonic API response back.

Fortunately this sort of problem had been anticipated on both the Cloudflare and Navidrome client end of things! Cloudflare lets you create a service token to be sent in a header with each request, and in both Nautiline and Arpeggi you can add a custom header to be sent on every request when you add a server address.

Getting the service token created on the Cloudflare side was a bit fiddly, their documentation is here and you need to follow the “Authenticate with a single header” to make a couple of manual curl calls against their API to update the application configuration to allow the single-header configuration.

That’s only a one-time setup though, and once that was done I was able to add the externally-accessible domain to the Navidrome client apps and configure it to include that authentication header, and now I’m able to be completely disconnected from our local network and still access Navidrome!

Custom backgrounds for the login screen

One fun thing that Navidrome allows is custom backgrounds on the login page. By default it uses various music-related images from this Unsplash collection, but you can specify a configuration option UILoginBackgroundUrl to choose your own image. There isn’t a built-in way to do the same “Select a random image from a selection of them” that Navidrome does by default, so I wrote a PHP script that I dropped onto my VPS alongside a group of images to choose from that loads the filenames of those images, then redirects to a random one:

<?php

    $files = glob('*.jpg');
    $file = array_rand($files);

    header('Location: ' . "https://virtualwolf.org/files/navidrome/$files[$file]")
?>

By setting the UILoginBackgroundUrl option to this script, I get a different random image of my own from the ones I uploaded!

Dealing with log files

Navidrome logs a fair bit of stuff as it’s working, and under Linux you’d typically use logrotate to automatically compress log files and eventually delete the oldest ones so you don’t end up with all of your disk space being eaten up by logs. BSD and thus macOS has its own log rotation tool called newsyslog but unfortunately the version that’s installed with macOS doesn’t support the R configuration flag to specify a path to a shell command to run instead of using a file that contains the process ID of the process to reload, and as a result Navidrome never gets reloaded and will just keep writing to the rotated file and not the new one.

Rather than screwing around installing logrotate, I decided to take the lazy way out and added a cronjob to just empty out the Navidrome log once a week, at midnight on Monday:

0 0 * * MON cat /dev/null > /Users/virtualwolf/Library/Application\ Support/navidrome/navidrome.log

Phew!

When I started this post I wasn’t expecting to write over three and a half thousand words on it, and yet here we are! I’m very excited to see how this all goes over the next while, and I’m already really enjoying that Navidrome (and as a result both Nautiline and Apeggi too) are a lot more focused on individual albums and displaying their wonderful artwork, it’s nice to just browse through and see what strikes my fancy.

Farewell TypeScript, bye Sass

Around this time-ish in 2019, I wrote a post about having migrated my website from Javascript and Sails.js to TypeScript and Express.js. Now six years later, I’ve just spent a bit over two months doing another rewrite, but from TypeScript back to Javascript, and along the way I ditched the CSS preprocessor Sass that I’d been using since 2013 with the previous Perl-based incarnation of my website!

Why ditch TypeScript?

I’d been getting a little irritated with TypeScript for a while now, mostly around dependency upgrade time and problems with the TypeScript compiler tsc complaining about types not matching when the definitions hadn’t been updated to match the library. It also meant a bunch more dev dependencies in my package.json and just generally more places for things to go wrong. I can definitely see the value in TypeScript for larger codebases, but when it comes down to it, my website is just a little hobby project worked on by one person (even if it does have comprehensive test coverage and a deployment pipeline in Bitbucket Pipelines) and I decided it’d be nice to simplify things as much as possible, especially since a lot of the modern tech stack is decreed by huge tech companies that have far more complex requirements than I do.

The process

Most of the conversion process was just renaming files from .ts to .js, updating the import statements to include the .js in the filename, and removing all the type information. And having my existing test suite made it far easier to make sure I hadn’t accidentally ballsed anything up! It look a while to get through everything but it wasn’t particularly arduous, just time-consuming.

As part of the simplification process, I also tackled the page rendering process as well. Back in August of last year I moved from EJS templates to Mustache — which was an absolute delight, the templates are so much easier to read now — and I wanted to see what else I could do.

I’d been using the library Pikaday as the date picker for the Memories pages on my website and hadn’t upgraded it in ages, so I figured that would be a good first start. I found that the maintainer had marked the library as read-only and recommended that people use the native HTML date picker which I didn’t even know existed! A few changes later to handle dates being set via query parameter, and I could completely ditch Pikaday. I also discovered that the charting library I’m using on the Weather pages, D3, has its own built-in date/time parsing, and as a result I was able to entirely ditch Moment.js on the frontend. Now the only frontend Javascript is the aforementioned D3, along with Fancybox on the Media, Photos, and Memories pages to give a nice full-screen lightbox when clicking on images (and if Javascript is entirely disabled, it’ll just link to the original image file instead).

The only extra complexity I added with all of this was moving from Linkify (for simple conversion of URLs to add an <a> tag so they can clicked on) to the Marked library to do full Markdown rendering of my Media posts. All my posts are sent to my GoToSocial instance which supports Markdown, but it was getting increasingly ugly having the raw Markdown showing on my website.

Ditching Sass

As I mentioned in the intro paragraph, I’d been using Sass since 2013 when I first started mucking around with theming on my website because it made it quite simple to have a base file with most of the styling in it, and just include overrides for the different themes.

I knew CSS had gotten a fair few new features over the years, but I hadn’t realised just how many, and with the help of this guide I was able to migrate to CSS variables and remove the need for Sass at all. I also discovered the color-mix() function which I used to make automatic colour theme matching for blockquotes and the background of <pre> blocks!

Moving from VS Code to WebStorm

During this whole rewrite period I’ve also been making another parallel switch, from Microsoft’s VS Code IDE to JetBrains’ WebStorm as part of my ongoing project to migrate away from the huge US tech companies as much as possible (and Microsoft’s jamming of AI into everything it can get its hands on in particular, hence my migration from GitHub to Codeberg a few months ago).

The developers at work use JetBrains’ products a lot and we’ve got a site license for it, and I was quite delighted to discover that WebStorm is actually free for non-commercial use. I’ve had to import a keyboard mapping so it’d match VS Code so I didn’t get frustrated because none of my muscle memory worked for the keyboard shortcuts (I should probably just suck it up and put up a cheatsheet of the default keys and make myself learn it), but WebStorm has been really nice to use. It was far easier than VS Code to set up the run/debug configurations, and it’s got really neat stuff like database integration such that it can look at my opaque SQL query strings in the codebase (literally strings in the editor surrounded by backticks) and actually analyse them and let me know if I’ve made an error or if a table or column doesn’t exist!

Onwards!

I have a few items in my backlog that came out of all of this, mostly some other areas that could do with a tidy-up, but it’s been far nicer to work on the codebase now and should be less fiddly to keep things updated.

Excluding package-lock.json because it’s absurdly large, there was a fair bit of change:

$ git diff --stat 9.15.4 10.0.0 -- . ':(exclude)package-lock.json'
 [...]
 175 files changed, 3990 insertions(+), 4482 deletions(-)

(9.15.4 being the version of my website I was on prior to all of this, and 10.0.0 being the initial release with all of these changes included.)

Goodbye Linode, hello Binary Lane

Way back in 2011 when I started on different support role at work that involved more sysadmin-type duties, I signed up with the VPS provider Linode so I would have my own remote Linux box that I could muck around with and break and generally learn about Linux a bit more on. At that point, my actual website was hosted on Dreamhost but I slowly expanded what I was using my Linode box for, and in 2017 I eventually ditched Dreamhost and migrated everything to the Linode. Linode’s hosting has been absolutely rock solid, I think in the 14 years I’ve been with them there have been… maybe two outages? And from memory both of them were when I had my VPS in one of their older datacentres California prior to them starting up a datacentre in Sydney and me migrating to it.

Towards the end of 2023 I posted about how I’d used Ansible to keep the configuration of the system under version control, and set up a whole migration script to migrate from one Linode box to a new one. Since then I’ve also started up a second smaller (1GB RAM) Linode to experiment with the fediverse software GoToSocial as an lightweight alternative to Mastodon that I could host myself instead of being on someone else’s server. I dutifully reused my existing Ansible playbooks and extended them for this purpose, and Linode have a whole collection that allows you to manage the actual VPSes and their external firewall as well.

Unfortunately with the orange idiot in charge of the US and all his tariff nonsense, the Australian dollar has gone right downhill compared to the USD and so a 1GB Linode + 2GB Linode and automatic backups were costing around AU$37/month. With that and the whole vibe of “Hey maybe we shouldn’t depend so much on US tech companies” lately, I figured maybe some local Australian company might have a decent VPS offering. I saw some recommendations for Binary Lane, did some poking around and even the people on Whingepool liked them. The price was certainly good, around AU$4 for a 1GB VPS versus US$5 with Linode (which works out to be over AU$8 once you do the currency conversion and tack on GST).

Binary Lane don’t have anything as fancy as Linode’s Ansible offerings, though the actual process of creating the VPS in the first place really isn’t something that happens very often so it’s not a big deal. The larger downside I discovered was that while Linode have a very robust external firewall that you can use Ansible to configure programmatically (and you bet your bippy I did!), Binary Lane’s offering is far less capable. I decided I’d need to add a local firewall instead that runs on the actual VPS itself, and found ufw which Ansible conveniently has a module for. After a bunch of trial and error I got it all figured out, and started modifying my existing Ansible migration script to consolidate the two Linode boxes into the one single Binary Lane one.

One interesting stumbling block I ran into when I first tried to migrate my GoToSocial instance — which runs in a Docker container and uses PostgreSQL that’s running directly on the box as its database — was that it turned out GoToSocial couldn’t actually contact PostgreSQL because it was being blocked by ufw! I did a whole lot of reading and figuring things out, and eventually it ended up being as easy as allowing the IP range that Docker Compose uses by default to connect to 172.17.0.1:

ufw allow from 172.16.0.0/16 to 172.17.0.1

Once that was figured out, it was relatively smooth sailing, though with a reasonable amount of trial and error and testing various bits of the migration. I had previously updated my Ansible playbooks to put all the static parts of my DNS configuration (things like MX/DKIM/SPF records and CNAMEs and so forth, all the parts that don’t point at a specific IP address) under version control and use Cloudflare’s† DNS Ansible collection to update it, and I was able to extend this to have full control of DNS records that actually point to an individual server and update that all dynamically from my playbooks!

†Yes I know Cloudflare are also a US tech company. One thing at a time. 😛

I ran the final migration last Sunday, and while there were a few hiccups there were far fewer than when I had originally migrated back in 2023 and I should be much better prepared next time I migrate boxes again.

One weird post-migration issue that I ran into that really stumped me for a while was that trying to load the URL of the website I run my IRC client through was giving me an invalid certificate error where it was trying to load one of the Cloudflare-proxied domain SSL certificates instead, and when I clicked through the warning I ended up on the domain for my GoToSocial instance instead.

After a whole lot of scratching my head, I figured out that while I had IPv6 enabled for the new Binary Lane VPS it turns out that Nginx won’t use IPv6 unless you explicitly turn it on in the virtual host configuration with a listen directive like on line 3 here:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name blog.virtualwolf.org;
    [...]

With that all figured out and everything running along nicely, my next projects are going to be to set up fail2ban for Nginx (looking at my logs, there were almost ten THOUSAND requests to /xmlrpc.php on this blog URL, just within the last day of having it running 😑), and migrating my aus.social Mastodon account over to my GoToSocial at toot.virtualwolf.org!

Now, let’s see if publishing this blog post via the WordPress ActivityPub plugin makes my new VPS crumble…

Playing with LaunchBar Actions: Converting epoch times to human dates

I’ve been a user of Objective Development’s most excellent utility LaunchBar since 2003 (!), I primarily use it for quickly launching applications (type Cmd-Space, start typing some letters, and hit Return à la Spotlight, but more powerful because it remembers the specific letters you’ve used: I type “ph” to launch Photos.app but “phs” for Photoshop for example) and clipboard history, but it has a ton of other functionality and to be honest I probably should take more advantage of it. 😅

One of the things Objective Development added relatively recently is Actions, basically a way to extend LaunchBar with your own custom code to accomplish various tasks, and it occurred to me that I could probably write up something that I often run into at work: needing to convert an epoch timestamp into a human-readable one.

An epoch timestamp is given as the number of seconds (or milliseconds) since the 1st of January 1970 UTC — for example as I type this the current epoch time is 1739007449 — and they’re quite heavily used in all sorts of computery things, including for things like times on logs entries and so on. The difficulty though is that you can’t just look at it and know what the actual human-readable time is, you need to convert it. There’s a website I’ve used for a while, epochconverter.com, which works well, but it’s an extra few steps, so I decided to scratch this itch and write a LaunchBar Action to do it!

You can download it here, unzip it and double-click the resulting .lbaction file to install it into LaunchBar. If called without any input, it will give the current epoch time in seconds and milliseconds. If given an epoch time as input, it will display the human-readable date and time in your local timezone as well as UTC!

I’ve put the code up on Codeberg if anyone is interested.

Monitoring indoor air quality, and outdoor temperature sensor woes

Previously on Monitor (And Automate) All The Things:

At the end of my last post on the subject I mentioned I was going to add an ENS160 air quality sensor to my arsenal of home monitoring sensors. I actually got that done only a couple of weeks after that blog post was published and it was pretty straightforwards, though I ended up forking the ENS160 library I’d found so I could more easily configure it and have it work better for my ESP32 code architecture.

Because I2C devices like the BME280 can be daisy-chained together and Core Electronics’ sensors thoughtfully have two STEMMA QT connectors on them, I was able to refactor my esp32-sensor-reader-mqtt code to allow for multiple sensors per ESP32 board (which in turn required refactoring of my Pi Home Dashboard Admin page as well). So now we still need only a single ESP32 board each in the office and lounge room, but both of them have a temperature/humidity and indoor air quality sensor attached to them! It’s really interesting watching the huge increase in indoor air quality we get as soon as we open the windows and let the fresh air in (when the weather is amenable to that happening, of course), and having this data is making me much more eager to crack open even just a couple of windows whenever we can.


For my birthday this year I got a little starter kit I got from Core Electronics that has a bunch of sensors and wires and bits and bobs in it, and one of those bits and bobs was a board with three LEDs on it and a STEMMA QT connector. I was pondering what I could do with it, then hit upon the great idea of using it to give a visual indication of what the outdoor dew point, outdoor air quality, and indoor quality was without needing to display full numbers (ranging from blue being the best, through to yellow being not great, and red being actively bad). I used my esp32-sensor-reader-mqtt code as a base and wrote some code (Update April 2024: I’ve rolled this into the main esp32-sensor-reader-mqtt codebase) to listen to the relevant MQTT topics and change the LED colours based on thresholds for the values it received.

I wanted this whole setup to be fairly compact, not much larger than the LED board itself, and I discovered that Adafruit makes an ESP32 that is TINY and has a built-in STEMMA QT connector so I wouldn’t need to do any soldering: the QT Py ESP32 Pico. I also realised I would need some way of holding this all neatly together instead of being an ugly pile of wires everywhere, and so had my first foray into 3D printing! I used Tinkercad to design a holder, and a friend has a 3D printer and so was able to print out the prototype for me. It turned out the measurements weren’t quite correct so I did a bit of tweaking, got a second printing, and after a coating with black paint via my airbrush, hey presto!

A photo of two small black cube-shaped objects with a vertical slot cut out of the front of them. The one on the right has a larger slot which is holding a blue circuit board with three bright LEDs lit up in blue, green, and yellow. The one on the left has a smaller slot which is holding a smaller circuit board (the ESP32) with a USB-C port on one side of it. (The holder on the left is showing what's on the back side of the holder on the right.) There are three short wires looping out to connect from the ESP32 on the back to the LED module on the front.
The holder from behind on the left, showing the ESP32 board, and the front on the right with the LEDs lit up.

We have two of these LED boards set up, sitting next to the Raspberry Pis that show the temperature/humidity dashboard in the lounge room and the office. I also hooked the ESP32s into my MQTT topic setup that the Raspberry Pis listen to to turn their display backlight off each night at midnight and back on in the morning at 7am so we don’t have blindingly bright LEDs lighting up the whole lounge room and office at night.

These have been running since the end of May with zero problems, just quietly displaying their data and turning off and back on as required!


Also in my previous post, I said that I’d switched to the Bosch BME280 temperature/humidity sensors because the DHT22 that was outside would become saturated after a long enough period of high humidity and would display wildly incorrect humidity readings. Unfortunately I subsequently discovered that the same thing happens with the BME280 because it turns out they actually use the same type of technology for their humidity sensor as the DHT22. 😑 They’re definitely more accurate indoors and I’m happily using them in the lounge room and office, but we had a spate of really wet days in winter this year and the BME280 that was outside ended up in the exact same state as the DHT22s, despite not actually ever being directly exposed to rain.

Queue a bunch more research, and I finally found something that uses a totally different type of technology for measuring humidity and is designed to actually work outside, and it even has a little built-in heater than you can trigger to dry the sensor out, the Sensiron SHT-30 (which I will never not read as “sense-iron” rather than what I assume they were going for, “sensy-ron”). Adafruit has packaged this up into a weatherproofed mesh package so I bought one of those, updated my esp32-sensor-reader-mqtt code again (I continue to remain delighted at how the architecture for this whole project has allowed me to extremely easily add additional sensors without a whole lot of effort), and it’s been live “in production” as it were since August of this year with absolutely zero problems:

A photo of our extremely ghetto "weather station": it's a piece of wood attached to the outside of our back room with a tupperware over the end to protect the sensors from direct rain. The new sensor is dangling down underneath the tupperware and looks almost like it has a microphone on the end of it (it's actually the weatherproofing mesh).

I had to make a minor change to the SHT-30 library I’d found to allow for turning the built-in heater on and off so have forked it myself, though I think I could probably get away with not having the heater functionality after all… a friend of mine has the same sensor packed up into a little commercially-purchasable box without the heater being used and his has been sitting outside for over a year with no problems.

One difference between the BME280 and the SHT-30 is that the BME280 calculates the dew point, whereas the SHT-30 only sends temperature and humidity. However, I discovered that you can actually calculate the dew point yourself given a temperature and humidity value, so I was able to have an option to send dew point data for both the DHT22 and the SHT-30!

With all of the changes I made to the ESP32 admin page to account for the additional options (including software and firmware updates), it’s rather significantly longer now:

And my Grafana dashboard has gotten equally elaborate!

Better environmental monitoring with the BME280 temperature sensor

Previously on Monitor (And Automate) All The Things:

Ever since I originally moved to the Raspberry Pi setup for our temperature monitoring back in 2017, I’ve been using DHT22 temperature/humidity sensors. They work well enough but they’re quite low-cost and we were noticing that as soon as the temperature starting cooling down outside, the humidity reading would shoot up until it hit 100% even though that clearly was not the case just from standing outside and feeling that it wasn’t muggy. Unless it was properly low humidity, this 100% reading would persist throughout the night until after the sun came up, even after replacing the sensor with a brand new one because the old one got wet in a particularly heavy downpour.

I started investigating alternative sensors and then remembered I’d previously come across the Bosch BME280 which in addition to temperature and humidity also measures atmospheric pressure. A bare sensor by itself obviously isn’t particularly useful, but I found a mob just up in Newcastle called Core Electronics who actually manufacture their own circuit boards to put sensors and other little “maker” things on. Conveniently one of the sensors they offer is the aforementioned BME280 on this nifty little compact board that has a common connector known as “PiicoDev” from them so you don’t need to solder anything. (The connector is also known as “STEMMA QT” or “Qwiic” depending on which company is making it, but they’re all physically identical.)

I found a few MicroPython libraries for the BME280 and settled on this one because it also calculates the dew point for you. After that I ordered a couple of the sensors and got to hacking on my esp32-sensor-reader-mqtt project to add the ability to select which sensor type you’re using.

(I also ended up also rolling my esp32-air-quality-reader code in to allow usage of the PMS5003 air quality sensor we have because that repository was using an old and creaky version of the code I was busily hacking on, and given I had added support for the BME280 already it was a trivial matter to also do the PMS5003.)

As all of that progressed, the number of options the esp32-sensor-reader-mqtt code could use was increasing and I realised I needed some way of changing settings that was easier to use than having to remember a bunch of mosquitto_pub CLI commands and the specific JSON payload and MQTT topic each one used. I’d previously started writing an admin frontend for them as part of my pi-home-dashboard project but hadn’t finished it, so dusted that off and ended up with this lovely setup!

A screenshot of the Pi Home Dashboard Admin page, with buttons along the top to select which client is being configured, a bunch of text boxes underneath for setting the configuration, and a "Logs" box at the bottom showing all the logs being emitted from the board.

It’s all in a single HTML page, uses MQTT.js to talk directly to the MQTT broker that’s running on the Raspberry Pi, Vue.js for the interactivity, and Bootstrap to make it look nice. It also uses MQTT’s Last Will and Testament functionality to track whether or not the board is online (it restarts if you update a setting) and disable the field inputs if it’s not, plus it allows for remote software updating directly from GitHub so I don’t need to physically plug the boards into the computer like some sort of barbarian when I want to update them! And it’ll grab the latest Git commit hash and save it so I know which code is actually running on the board.

With that all done, I updated our Grafana dashboard with the new data.

A screenshot showing spark graphs with the current temperature and humidity for the four ESP32s we have, our power usage and consumption, outdoor air quality, and the two new bits, two gauges showing the current dew point and atmospheric pressure.

Pleasingly, all of the new readings have been very close to what the Bureau of Meteorology reports!

And finally for my birthday later this month I’m getting Core Electronics’ PiicoDev Starter Kit which has several other sensors in it, and on top of that I included an ENS160 air quality sensor which measures VOCs and eCO2 indoors so I have a lot more more hacking fun to come. 😁

Automating Raspberry Pi setup (and ESP32, and Linode) with Ansible

(Update April 2024: When I originally wrote this blog post, the official Raspberry Pi distribution of Debian hadn’t been updated for Debian 12 and so I was still on Debian 11. They’ve since updated it, so I ran a trial run on my spare Raspberry Pi 4B+ and made a couple of minor changes to the Ansible playbooks, and flashing the “production” 4B+ to Debian 12 with the full Ansible setup was an absolute unqualified success! 🎉)

Back in 2017 when I first moved off the old and busted NinjaBlocks platform to a Raspberry Pi for my temperature sensor setup, I said:

[…] if the hardware itself died I’d be stuck; yes, it was all built on “open hardware” but I didn’t know enough about it all to be able to recreate it.

I definitely have no problems with the hardware now (and with my move to ESP32s and MQTT for the temperature sensors themselves it’s even simpler), but I recently realised that the software configuration on the Raspberry Pis was still a problem: the configuration and setup of everything had had a steady pace of organic updates and tweaks and if one of the SD cards died or had a problem and I had to reformat it, I’d have a hell of a time recreating it afresh. On the “main” Raspberry Pi 4B+ alone I would need to:

  • Install all the various software packages (vim, git, tmux, Docker, etc.)
  • Add my custom shell configuration and bashmarks
  • Install Chrony and configure it to allow access from the ESP32s
  • Configure the drivers for the JustBoom DAC HAT and install and configure shairport-sync to allow for AirPlay to the big speakers in the lounge room
  • Configure and run the Mosquitto Docker container to allow all my temperature, humidity, air quality, and power data to flow to all the various places it needs to go
  • Configure and run the pi-home-dashboard Docker container so the Raspberry Pi Zero Ws can display our little temperature dashboards
  • Configure and run the powerwall-to-pvoutput-uploader Docker container so our power usage data can be both sent to PV Output and also be read by InfluxDB and the Raspberry Pi Zero W dashboard

In addition to all of that, the Raspberry Pi Zero Ws that display our little dashboards required a whole bunch of constant tweaks and changes to get them working properly (they run a full Linux desktop and display the dashboard in Chromium, and Chromium has an extremely irritating habit of giving a “Chromium didn’t shut down properly” message instead of loading the page you tell it to if there were basically any issues at all; fixing that required a whole lot of trial and error until I narrowed down the specific incantations to get it to stop).

Setting all of that back up from scratch would have been an absolute nightmare so I figured I should probably automate it in some fashion. And in addition, it means I would be able to keep future changes under version control as well so no matter what I’ve done I’d be able to restore it if necessary. At work we use a piece of software called Ansible to bring up the required software and configuration on the Amazon EC2 instances we run, so I thought that’d be a good place to start.

Even though it’s developed by RedHat and isn’t just some random open-source piece of software (although it is open-source), I still found the documentation to be not great in terms of explaining everything step-by-step and building on that knowledge in subsequent steps. But after a bunch of reading and trial and error, plus several weeks of getting it all working, I have my entire Raspberry Pi setup for all the Pis we have at home fully automated! I can wipe the SD card, reflash it with a fresh copy of Raspbian, then run Ansible and it gets everything installed and configured and working exactly how I need it.

I uploaded the whole shebang to Codeberg to hopefully help others out as well. It’s obviously completely customised for our setup, but it at least gives a reasonable idea of how everything works.

It starts by creating an inventory, basically a list of the hostnames you want to run playbooks against (a “playbook” is a file that describes the list of steps to run in order to get to the desired state you need). Alongside that, you can group the hosts together so you can target a playbook to run on a specific set of hosts. For example, the server group only consists of the main Raspberry Pi 4B+ described above, but the dashboards consists of all three of the Pi Zero Ws that are running the dashboards.

One of the really handy things with Ansible is that you can set variables that will be reused in various places, and you can configure them for all hosts, or per group, or per individual host. I’m using a combination of those, and inside the server group it will actually look up items from within 1Password so I can commit the code to source control and not be storing secrets in it. You can also set variables per individual host as well, which I use to specify the dashboard URL that each of the Pi Zero Ws should load.

The playbooks themselves live in the playbooks directory, and they specify a set of hosts to run against, and a series of roles to run. The roles are reusable sets of tasks, so for example I run the initialisation role against all of the Raspberry Pis no matter what they’re ultimately going to be doing, for doing the initial things like setting the hostname and updating all the software packages, configuring Git, etc.

After the initialisation is done, the server playbook will run all the various steps to get Docker installed, NVM and Node.js installed, then get everything else configured and installed that needs to be configured and installed. Compare to the dashboards playbook that will also run the same initialisation steps, but then runs the dashboard role which installs the drivers for the HyperPixel display and various other things that need doing, and will configure the autostart file so on boot Chromium comes up with the correct URL depending on which Raspberry Pi it’s running on! The dashboard_url variable in that template file is set in the host_vars directory per specific hostname, so I can customise it for each one.

After my complete success here, I decided I wanted to do the same for when I needed to reflash my ESP32s, because previous it relied on me remembering to update a configuration file with the name of the ESP32 and I had definitely messed that up on a couple of occasions. That was relatively straightforwards as well, and I added it to my esp32-sensor-reader-mqtt repository (and included a playbook to just erase a board because I never did it quite often enough to remember what the specific steps were).

And then finally after that, I decided I should also automate my Linode setup. I’d posted back in 2019 about using Linode’s StackScripts to set everything up, but the problem with that is that you run it at the very beginning and your Linode is set up appropriately at that point, but any changes you make after that aren’t saved anywhere, so you’re essentially back to square one again. The final sentence in that blog post was this:

As long as I’m disciplined about remembering to update my StackScript when I make software changes, whenever the next big move to a new VM is should be a hell of a lot simpler.

But in news that will surprise nobody, I was not at all disciplined. 😅 The other problem is that you can’t test the StackScript as you’re going (they only run when you first spin up the Linode afresh), you have to update it and hope those steps work in future. With Ansible, the idea is that everything is idempotent, so you can run everything as many times as you want and it won’t change if something has been configured already, so it enables you to easily test out parts of the playbook updates without needing to wipe the whole damn thing. It’s taken a bit over a month of working on it after dinner and on weekends, but now the whole Linode setup is fully automated as well.

However, the other wrinkle is that where the Raspberry Pis don’t store any data on them and can just be wiped without problem, my Linode hosts this blog, my website and all its images, all the various images I’ve posted to Ars Technica over the years, and a bunch of other things too. I ended up splitting the Ansible process into two, there’s the configuration and then another separate playbook that copies everything over from the old Linode onto the new one and then registers a new SSL certificate with Let’s Encrypt and updates Cloudflare to point the DNS of my website and blog and the general server hostname to the new Linode.

That bit was a bit nerve-wracking, I tested the process a bunch of times and pulled the trigger for real last weekend, then had a minor panic attack when I realised that the database dump of my website hadn’t been reimported since I originally tested the process back on the 9th (I suspect it didn’t import because there was already data in the database, but unfortunately it didn’t actually error out so I didn’t know), so I didn’t have any of the posts I’d made nor any of the temperature data since then. 😬 Fortunately I realised this the morning after I’d done the migration and had wisely left the old Linode up and running, so I renamed the database on the new Linode so I could harvest the temperature data that been sent since the migration, dumped the old database from the old Linode and imported it into the correct location on the new Linode, and then exported and reimported just the missing temperature data, and we’re back in business.

This time I should be able to revisit this in three or four years when I next do a big upgrade and it should actually be quite painless (famous last words, I know, but I’m much more confident this time).

Replacing the hard disk in a PowerBook G3 “Pismo”, and other fun with Mac OS 9

Replacing the hard disk in a PowerBook G3 “Pismo”, and other fun with Mac OS 9

I posted nearly five years ago about my shiny new Power Mac G4 and how much I was enjoying the nostalgia. Unfortunately the power supply in it has since started to die, and the machine will randomly turn itself off after an increasingly short period of time. Additionally, I’d forgotten just how noisy those machines were, and how hot they ran! I’ve bought a replacement power supply for it, but it involves rearranging the output pins from a standard ATX PSU to what the G4 needs, and that’s so daunting that I still haven’t tackled it yet. I decided to go back to the trusty old PowerBook G3, as I’ve since gotten a new desk and computer setup that has much more room on it, and having a much more compact machine has been very helpful.

One thing I was a bit concerned about was the longevity of the hard disk in it and I started investing the possibility of putting a small SSD into it. Thankfully such a thing is eminently possible by way of a 128GB mSATA SSD and an mSATA to IDE adapter! I followed the iFixit guide — though steps 6 through to 11 were entirely unnecessary — and now have a shiny new and nearly entirely silent PowerBook G3 (though it’s disconcerting at just how quiet it is given it’s an old machine… I realised I’m so subconsciously used to hearing the clicking of the hard disk).

A photo of a black PowerBook G3 sitting on a desk, booted to the Mac OS 9 desktop. The machine is big and chunky, but also has subtle curves to it, and the trackpad is HILARIOUSLY tiny compared to modern Macs.

I even had the original install discs from the year 2000 when mum first bought this machine, and they worked perfectly (though a few years ago I’d had to replace the original DVD drive with a slot-loading one because the original one died and it stopped reading discs entirely).

One I had it up and running, another sticking point is actually getting files onto it. As I mentioned in my previous post, Macintosh Repository has a whole ton of old software and if you load it up with a web browser from within Mac OS 9 it’ll load without HTTPS, but even so it’s pretty slow. Sometimes it’s nicer just to do all the searching and downloading from a fast modern machine and then transfer the resulting files over.

Mac OS 9 uses AFP for sharing files, and the AFP server that used to be built into Mac OS X was removed a few versions ago. Fortunately there’s an open-source implementation called Netatalk, and some kindly soul packed it all up into a Docker container.

I also stumbled across a project called Webone a while ago, which acts essentially as an SSL-stripping proxy that you run on a modern machine and point your old machine’s web browser to for its proxy setting. Old browsers are utterly unable to do anything with the modern web thanks to newer versions of encryption in HTTPS, but this lets you at least somewhat manage to view websites, even if they often don’t actually render properly.

Both Netatalk and Webone required a bit of configuration, and I rather than setting them up and then forgetting how I did so, I’ve made a Git repository called Mac OS 9 Toolbox with docker-compose.yml files and setup for both projects in them, plus a README so future-me knows what I’ve done and why. 😛 In particular getting write permissions to write from the Mac OS 9 machine to the one running Netatalk was tricky.

I also included a couple of other things in there too, and will continue to expand on it as I go. One thing is how to convert the PICT format screenshots from Mac OS 9 into PNG, since basically nothing will read PICTs anymore. It also includes a Mastodon client called Macstodon:

A screenshot of a multi-pane Mac OS 9 application showing the Mastodon Home and Local Timelines and Notifications at the top, and the details of a selected toot at the bottom.

And also the game Escape Velocity: Override (which I’m very excited to note is getting a modern remaster from the main guy who worked on the original):

A screenshot of a top-down 2D space trading/combat game with quite basic graphics. A planet is in the middle of the screen along with several starships of various sizes.

I mentioned both the Marathon and Myth games in my previous post, but those actually run quite happily on modern hardware since Bungie was nice enough to open-source them many years ago. Marathon lives on with Aleph One, and Myth via Project Magma.

Upping my monitoring game with MQTT

Previously on Monitor All The Things:

(The display that used to show the air quality has been changed to show a clock instead, and the air quality monitoring is done via another ESP32 now. I’m also sensing a definite theme with my blog post titles here).

I hadn’t blogged about it, but I also have all of this (indoor and outdoor temperature and humidity, power usage and generation plus battery charge, and outdoor air quality) going into InfluxDB for visualising in Grafana. The dashboard I made looks like this:

Pretty spiffy, eh?

It’d very much evolved rather organically as I went though, so lots of different things on different hosts sending HTTP calls all over the place, including my own slightly dodgy system for getting the ESP32s that are connected to the temperature sensor to save their readings onto the local filesystem if my website couldn’t be contacted (for example if we had an internet outage), as well as two separate things hitting the Powerwall’s local API every five seconds to pull the power data (one for the little HyperPixel display at the front of the house, and one for the visualisation stuff above).

I figured there had to be a cleaner and more elegant way of doing this. At work I deal with Amazon’s Simple Queue Service (SQS) quite a lot and use it in one of the services I built, so I wondered if there was a way to accomplish something similar myself, so I could just have everything drop messages onto a queue and have the things that need to read them pick those messages up from the queue.

Turns out there is, and it’s called MQTT!

It’s an absurdly simple and lightweight protocol, you have a central server called a “broker”, a publisher that sends messages to a given topic on the broker, and as many subscribers as you want that also connect to the broker and each listen on a topic or topics, and the broker ensures those messages get from the publisher to each subscriber. There’s also quality of service settings where you can have it guarantee that the message is received by the subscriber at least once, and it’ll queue up the messages for the subscribers if they drop offline and the messages will all be sent once the subscriber comes back.

Interestingly, you can also have a broker on one machine connect to a broker on another machine, and have it send messages on a particular topic to the remote broker, which seemed like it’d be a good way to get weather updates to my website.

There’s a guy who wrote an MQTT client library in MicroPython for the ESP32, mqtt_as, so that would take care of the ESP32 side of things, I’d use a popular open-source MQTT broker called Mosquitto, and there’s a Javascript MQTT client called MQTT.js that would be used for my website and all the other TypeScript parts of the setup.

I did a bunch of brainstorming in draw.io and came up with this elaborate diagram:

(Mechanise is the hostname of my Linode, which my website runs on, and PVOutput is a website for sending your solar power generation data to, a bunch of people at work also do the same and we’re all in the same “team” so we can see how much combined we’ve all generated together).

After that, it just involved a whole bunch of coding (as well as ordering two spare ESP32s so I could test that my code worked without having to pull apart my existing setup), which I’ve uploaded to GitHub:

Despite having written up a careful plan and done what I thought was getting all my ducks in a row to make a quick switchover this morning, there were a number of things I ran into that caused it to take a few hours to get going (things like forgetting to configure PostgreSQL on the Raspberry Pi 4B to allow things running in Docker to access it, needing to add an extra published port on the Linode so my website could connect to Mosquitto, and most annoyingly of all, a recent VSCode update breaking Pymakr and having to revert to an old version of both pieces of software). I got everything up and running in the end, and now if I add any new monitoring things, it’ll be quite simple to publish the data to Mosquitto and slurp it up into InfluxDB!