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.

Fixing iPhone music sync issues: “The file was not found” or “The file could not be converted”

(If you don’t care about the background and just want to see the fix, click here.)

I’m one of that dying breed of people who doesn’t use streaming services to listen to music and instead prefers to actually own it, with digital files that live on my computer and whose availability isn’t at the whim of some record label or artist who might suddenly decide to take it down. And when I’m commuting to work, I also don’t want to have to depend on the vagaries of my phone signal to listen to music were I to set up some sort of streaming-from-home thing. As a result, I manually plug my iPhone in to my laptop and sync my entire music library to it, so I have a full copy of everything that doesn’t require an internet connection at all.

Back when Apple first started using AAC as its audio format of choice for the iTunes Store, I ripped all my CDs at 160Kbps to save space and improve audio quality over the MP3s I’d previous been using, but it turns out on good headphones (or good speakers) 160Kbps is actually not all that good quality, particularly around the highs. Around 2018 or so I decided to re-rip all of my CDs again this time in Apple Lossless format so they would be a fully archival-quality copy and if anything came along that improved on AAC I could just encode a copy of the original lossless in that new format.

In 2017 I had started buying music primarily from Bandcamp, and a couple of years after that I realised that they offered downloads in Apple Lossless format too, so I redownloaded everything as lossless and the size of my iTunes library quickly ballooned, to the point where I needed to enable the “Convert higher bit rate songs” option for syncing the music to my iPhone or it wasn’t all going to fit, even on a 256GB iPhone.

With all of those points above, I’d been doing all sorts of screwing around with the underlying music files to ensure that I could keep all of the Music.app playcounts and date added metadata and such but with the underlying file being the new lossless version. I don’t know if there’s a better way of doing it, but my process for the Bandcamp-sourced files involved quitting Music entirely, completely deleting the original crummy format files (as in moving them to the Trash then emptying it) and dropping the new lossless version in its place, then opening Music.app and doing a Get Info on the first song in the album. It pops up with a “The file could not be found, do you want to locate it?” message, then I’d navigate to the new lossless version and select that as the file for that song. Rinse and repeat for each song in the album.

And just to add some more screwing around to the mix, recently I’ve been checking out the self-hosted music server software Navidrome and getting my tagging in order with MusicBrainz Picard, since the MusicBrainz database is primarily what Navidrome uses to identify songs, which in turn involved getting Music.app to refresh its metadata from the underlying files.

Yesterday I decided to make a clean start with the music syncing to my iPhone and turned it off entirely so all the music would be deleted, then turned it back on again and kicked off a brand new sync. I’ve been syncing music like this since the days of the original iPod when there was no other option to get music onto your iDevice, but with the rise of Apple Music it appears that Apple has been paying very little attention to making sure the sync process works flawlessly and my sync wasn’t problem-free, out of a library of 8586 songs I ended up with 21 songs that failed to sync with a “<SONG> could not be copied to the iPhone because the file could not be converted” error, and another 44 that failed with “<SONG> could not be copied to the iPhone because the file could not be found”.

Fixing the failures

The files were all there in Music.app and could all be played perfectly fine on my computer, so it was clearly something weird going on with the sync process. I went through and checked the “File could not be converted” songs first, and it turned out to be about half the songs off just three of the albums I had ripped from CD as Apple Lossless, and one single song I had ripped from another CD. Looking at the underlying song files in the Finder, I noticed a strange detail: all of the ones that failed ended in ” 1.m4a”, so the song “Tear Down The Walls” was 01 Tear Down The Walls 1.m4a instead of what I’d have expected which was 01 Tear Down The Walls.m4a (the 01 coming from it being the first track on the album). I have Music.app set to automatically manage and name the underlying files based on the ID3 tags, so I did a Get Info on the whole album and added an exclamation mark to the end of the album name, which then triggered iTunes to update the folder and filenames to match the ID3 tags and removed the ” 1″ at the end of the filenames. I then did another Get Info and removed the exclamation mark, repeated the process for the other problem albums and then resynced, and all of them synced successfully.

Even more strangely, this also fixed all of the entirely separate set of songs that had failed with “File could not be found” errors! I got a new set of songs with that same error, but this time it was one single album that I’d just re-bought as lossless from Bandcamp and had forgotten to do the “relocate files” dance I described above and once I fixed that it worked fine too. I have no idea why that didn’t show up in the initial sync though.

So I guess in summary this is less of a “This is a 100% foolproof fix” and more of a “Trying fiddling with the metadata and maybe it’ll kick things into gear” suggestion. 😅

Improving wifi signal with Ubiquiti’s U6+ access points

Last month I bought a set of modern ESP32 boards with four times more memory than the ones I had been using that date from ~2017:

I have a bit of functionality in my MicroPython code where I can trigger the board to update itself remotely by pulling the files down from GitHub, but unfortunately with the overhead of running MicroPython plus my code itself, I only end up with about 80KB or so of free memory after a fresh reboot and so the update-from-GitHub code frequently runs out of memory and I end up needing to update the code by plugging it directly into the computer. (Which in turns means needing to find out a damn Micro USB to USB-C adapter or cable because all my computers are USB-C now.)

I got the indoor ESP32s for the office and lounge room all set up and working fine, but when I got the ones in the back room set up, I realised that they were constantly dropping off the wifi for minutes at a time, or longer. It turns out the wifi signal strength there is very marginal, and I think the sheer compactness of the new boards meant they couldn’t pick hang on to a signal as well as the older boards.

While investigating this, I realised that with my recent upgrade to an iPhone 16 Pro I could take advantage of Ubiquiti’s WiFiman app which lets you generate a map of your wireless signal strength but it will also use the ARKit functionality in newer iPhones to pick up where the walls in your house are and actually generate a rough floor plan! I ran it twice, once for the 2.4GHz and once for the 5GHz network:

A screenshot of both the 2.4GHz and 5GHz network signal strength. The 2.4GHz isn't bad, inside it's mostly green except for the very front of the house, but the back room is decidedly orange and getting close to red. The 5GHz is a shit-show, the room that the router is in green and everything else is yellow at MOST, with the front of the house being almost red, and the back room being completely red.

The room with the green signal strength on the 5GHz map is where the router was actually located, our home office is the room directly above that, the lounge room is at the bottom right and the very top-right bit that’s poking out is the back room that’s out in our back yard where the outdoor sensors live.

Unsurprisingly the 5GHz signal doesn’t penetrate through walls very well, but I was surprised at how little it reached the very front of the house where Kristina and I sit and use our computers when we’re not in the home office despite there only being a single wall in the way (though that also explains why I’ve gotten some pretty rubbish speeds when copying files over the network from my machine when sitting in the lounge room).

The router itself is a Ubiquiti Dream Router, and I knew Ubiquiti offered all sorts of additional access points that you could just plug into the network and get running with very little additional mucking around. After a bunch of reading I settled on a pair of the no-frills U6+ APs, and they arrived on the 4th of this month. My plan was to relocate the Dream Router itself to the home office and have one AP at the very front of the house in the lounge room and the other in the back room.

However, I had neglected to notice that the U6+ doesn’t actually have any way of getting power to it except for via Power over Ethernet (PoE) where both data and power is supplied via ethernet cable, which in turn requires a device that can inject power into the ethernet cable. Thankfully the Dream Router has two PoE-capable ports on it, but I wanted to keep the Raspberry Pi 4B+ on ethernet since it’s the heart of our temperature monitoring/display setup and is rather critical to the whole thing, so I needed to buy a separate PoE injector for the AP for the lounge room. Moving the main Dream Router to the home office also meant we needed to get the electrician out to do a bunch of recabling, and after four hours of hard work on Tuesday doing a bunch of extremely hot and difficult work in the ceiling, followed by three hours on Tuesday evening on my part pulling everything in the home office apart to tidy it up, we are in business!

The actual process of adding the new APs to the network could not have been easier: you plug them in, they start up and pop up in the Dream Router’s web interface, you click “Adopt”, and they update their firmware and are automatically configured, even figuring out which wifi channel they should be on so they don’t interfere with each other. You can also lock a specific device to only ever connect to one specific access point, so I’ve got everything that’s permanently in the lounge room set to only connect to the lounge room access point, and the same for the Dream Router and the back room access point as well.

I ran another WiFiman signal strength scan earlier today for both the 5GHz and 2.4GHz networks, and you can clearly see the effect of the router being in the office and having the new AP at the front of the house. The back room signal looks weird because my iPhone took a little while to roam from the prior access point to the one in the back room so it’d already partially mapped some signal strength but you can still see the difference from above:

An image of the signal strength of our 5GHz network with the new access points, the home office lounge room are both nicely green, as is the back room.

I haven’t included the 2.4GHz one here because the signal strength throughout the whole thing was sufficient that the iPhone never roamed from the original Dream Router I’d started out being connected to. 😅 But looking at the list of signal strengths of all of the clients in the Dream Router’s UI, they’re all absolutely excellent now and I’ve had zero dropouts on any of my ESP32s!

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!

More fun with temperature sensors: ESP32 microcontrollers and MicroPython

More fun with temperature sensors: ESP32 microcontrollers and MicroPython

I’ve blagged previously about our temperature/humidity sensor setup and how they’re attached to my Raspberry Pis, and they’ve been absolutely rock-solid in the three-and-a-half years since then. A few months ago, a colleague at work had mentioned doing some stuff with an ESP32 microcontroller and just recently I decided to actually look up what that was and what one can do with it, because it sounded like it might be a fun new project to play with!

From Wikipedia: ESP32 is a series of low-cost, low-power system on a chip microcontrollers with integrated Wi-Fi and dual-mode Bluetooth.

So it’s essentially a tiny single-purpose computer that you write code for and then flash that code onto the board, rather than like with the Raspberry Pi where it has an entire Linux OS running on it. It runs at a blazing fast 240MHz and has 320KB of RAM. The biggest draw for me was that it has built-in wifi so I could do networked stuff easily. There’s a ton of different boards and options and it was all a bit overwhelming, but I ended getting two of Adafruit’s HUZZAH32s which come with the headers for attaching the temperature sensors we have already soldered on. Additionally, they have 520KB of RAM and 4MB of storage.

Next up, I needed to find out how to actually program the thing. Ordinarily you’d write in C like with an Arduino and I wasn’t too keen on that, but it turns out there’s a distribution of Python called MicroPython that’s written explicitly for embedded microcontrollers like the ESP32. I’ve never really done much with Python before, because the utter tyre fire that is the dependency/environment management always put me off (this xkcd comic is extremely relevant). However, with MicroPython on the ESP32 I wouldn’t be having to deal with any of that, I’d just write the Python and upload it to the board! Additionally, it turns out MicroPython has built-in support for the DHT22 temperature/humidity sensor that I’ve already been using with the Raspberry Pis. Score!

There was a lot of searching over many different websites trying to find how to get all this going, so I’m including it all here in the hopes that maybe it’ll help somebody else in future.

Installing MicroPython

At least on macOS, first you need to install the USB to UART driver or your ESP32 won’t even be recognised. Grab it from Silicon Labs’ website and get it installed.

Once that’s done, follow the Getting Started page on the MicroPython website to flash the ESP32 with MicroPython, substituting /dev/ttyUSB0 in the commands for /dev/tty.SLAB_USBtoUART.

Using MicroPython

With MicroPython, there’s two files that are always executed when the board starts up, boot.py which is run once at boot time and is generally where you’d put your connect-to-the-wifi-network code, and main.py which is run after boot.py and will generally be the entry point to your code. To get these files onto the board, you can use a command-line tool called ampy, but it’s a bit clunky and also not supported anymore.

However, there is a better way!

Setting up the development environment

There are two additional tools that make writing your Python code in Visual Studio Code and uploading to the ESP32 an absolute breeze.

The first one is micropy-cli, which is a command-line tool to generate the skeleton of a VSCode project and set it up for full autocompletion and Intellisense of your MicroPython code. Make sure you add the ESP32 stubs first before creating a new micropy project.

The second is a VSCode extension called Pymakr. It gives you a terminal to connect directly to the board and run commands and read output, and also gives you a one-click button to upload your fresh code, and it’s smart enough not to re-upload files that haven’t changed.

There were a couple of issues I ran into when trying to get Pymakr to recognise the ESP32 though. To fix them, bring up the VSCode command palette with Cmd-Shift-P and find “Pymakr > Global Settings”. Update the address field from the default IP address to /dev/tty.SLAB_USBtoUART, and edit the autoconnect_comport_manufacturers array to add Silicon Labs.

Replacing the Raspberry Pis with ESP32s

After I had all of that set up and working, it was time to start coding! As I mentioned earlier I’ve not really done any Python before, so it was quite the learning experience. It was a good few weeks of coding and learning and iterating, but in the end I fully-replicated my Pi Sensor Reader setup with the ESP32s, and with some additional bits besides.

One of the things my existing Pi Sensor Reader setup did was to have a local webserver running so I could periodically hit the Pi and display the data elsewhere. Under Node.js this is extremely easily accomplished with Express, but using MicroPython the options were more limited. There are a number of little web frameworks that people have written for it, but they all seemed quite overkill.

I decided to just use raw sockets to write my own, though one thing I didn’t appreciate until this point was how Node.js’s everything-is-asynchronous-and-non-blocking makes doing this kind of thing very easy, you don’t have to worry about a long-running function causing everything else to grind to a halt while it waits for that function to finish. Python has a thing called asyncio but I was struggling to get my head around how to use it for the webserver part of things until I stumbled across this extremely helpful repository where someone had shown an example of how to do exactly that! (I even ended up making a pull request to fix an issue I discovered with it, which I’m pretty stoked with).

One of the things I most wanted to do was to have some sort of log file accessible in case of errors. With the Raspberry Pi I can just SSH in and check the Docker logs, but once the ESP32s were plugged into power and running, you can’t easily do a similar thing. I ended up writing the webserver with several endpoints to read the log, clear it, reset the board, and view and clear the queue of failed updates.

The whole thing has been uploaded to Codeberg with a proper README of how it works, and they’ve been running connected to the actual indoor and outdoor temperature sensors and posting data to my website for just under a week now, and it’s been absolutely flawless!

(Update October 2021: The dodgy HTTP setup described in this post has been replaced by a much more elegant MQTT one, and all my development efforts have been put towards the MQTT version of my sensor reader code.)