I picked up a Keybow Mini from Sheffield Hackspace a few weeks ago, which is a small 3-key mechanical keyboard or "macro pad".
It had been donated to the hackspace by Pimoroni; I also picked up a donated Pi Zero and a lying 1024GB SD card I'd picked up a few weeks ago, and assembled it.
Assembly
Unfortunately, this is a text only-blog. I assure you that the pictures of me putting keycaps and circuitboards together are incredibly riveting — but you'll just have to imagine.
end result
After following the assembly guide and the creating macros guide, I had a little 3-key keyboard which could increase, decrease, or mute my volume (via media keys). I wanted to have more options, so I wrote a little layout in Lua in which the left button switches "layout", and the other two buttons do different actions based on which layout is currently enabled. The keys I started with are:
Here's the Lua script for what I described. I think it's pretty readable.
require "keybow"
-- do loads of things --
-- left button switches between mode
-- do not use F13 as by default it opens settings
-- red: media controller
-- blue: send F15 and F16
-- green: send F17 and F18
Kbsetup = 0
TotKbSetups = 3
function setup()
keybow.use_mini()
keybow.auto_lights(false)
keybow.clear_lights()
keybow.set_pixel(0, 255, 0, 0)
keybow.set_pixel(1, 255, 0, 0)
keybow.set_pixel(2, 255, 0, 0)
end
-- left key
function handle_minikey_02(pressed)
if (pressed) then
Kbsetup = (Kbsetup + 1) % TotKbSetups
end
if (Kbsetup == 0) then
-- print("setup 1")
keybow.set_pixel(0, 255, 0, 0)
keybow.set_pixel(1, 255, 0, 0)
keybow.set_pixel(2, 255, 0, 0)
elseif (Kbsetup == 1) then
-- print("setup 2")
keybow.set_pixel(0, 0, 255, 0)
keybow.set_pixel(1, 0, 255, 0)
keybow.set_pixel(2, 0, 255, 0)
elseif (Kbsetup == 2) then
-- print("setup 3")
keybow.set_pixel(0, 0, 0, 255)
keybow.set_pixel(1, 0, 0, 255)
keybow.set_pixel(2, 0, 0, 255)
end
end
-- middle key
function handle_minikey_01(pressed)
if Kbsetup == 0 then
keybow.set_media_key(keybow.MEDIA_VOL_DOWN, pressed)
elseif Kbsetup == 1 then
keybow.set_key(keybow.F15, pressed)
elseif Kbsetup == 2 then
keybow.set_key(keybow.F17, pressed)
end
end
-- right key
function handle_minikey_00(pressed)
if Kbsetup == 0 then
keybow.set_media_key(keybow.MEDIA_VOL_UP, pressed)
elseif Kbsetup == 1 then
keybow.set_key(keybow.F16, pressed)
elseif Kbsetup == 2 then
keybow.set_key(keybow.F17, pressed)
end
end
Final notes
The only annoying thing is that to reprogram the PiBow Mini, you've got to unplug it, remove the SD card, plug it into a laptop (in my case as only my laptop has an SD reader), copy the new Lua files over, unplug the SD card, plug it in, test the new code, and repeat if it doesn't work as intended.
That's why I opted to send function keys, which I can turn into hotkeys using AutoKey, then I only have to change a Python script, and not redo a whole device firmware.
I've been interested in "doing something" with the ical format for a while, as it's quite a simple format, but can be shared easily to create events in other people's calendars.
Here is an example of an ical event, from the wikipedia page
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:uid1@example.com
ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
DTSTAMP:19970701T100000Z
DTSTART:19970714T170000Z
DTEND:19970715T040000Z
SUMMARY:Bastille Day Party
GEO:48.85299;2.36885
END:VEVENT
END:VCALENDAR
As you can see, it's fairly human-readable. You can see a description of the event as SUMMARY, and a start/end date-time as DTSTART/DTEND. You can imagine writing a file like this manually (though probably don't start doing that).
As for parsing them, you can use awk pretty well. Create a file called parse_ics.awk a bit like this:
If you want to do something more… machiney… you could change the output to be more machine-readable (i.e., JSON) by changing the printing section in awk, e.g., to…
So… if someone publishes their event as an .ics file… you can do some nice scripting with it!
For example, I created a script which parses the Sheffield United football fixtures and sends a message on Discord whenver there is one coming up later in the day (as Sheffield Hackspace is near the football ground so it affects parking for car-brains).
I am a director of Sheffield Hackspace and part of my role is to keep track of the membership.
It's currently via a spreadsheet where the membership payments come via bank transactions, and I match them up to the sheet. This involves a lot of finding names with "CTRL+F", and then changing a specific column.
I'd like to be able to "jump to column N", but I couldn't find an appropriate shortcut (I would love something like "Alt" then press "N", but alas).
After some web-searching, I found AutoKey, a Linux program that sounds like a colder version of AutoHotKey. I installed it, and it worked great. I'd tried to use xdotool to send key commands before, but it didn't work when set as a keyboard shortcut.
What I wanted to do using the keyboard was:
open the cell "Name Box" (the bit that says "A59" or "H14")
change the first letter to an "N" (i.e., turn "A57" into "N57")
press Enter to go to that cell
After reading the the documentation, I figured a script to do this:
import time
keyboard.send_keys("<ctrl>+<shift>+T")
time.sleep(0.25)
keyboard.send_keys("<left>")
keyboard.send_keys("<shift>+N")
keyboard.send_key("<delete>")
keyboard.send_key("<enter>")
(the 0.25 second wait is to let LibreOffice Calc recognise the shortcut and move the cursor to the Name Box)
…and used the GUI to set the script to run whenever I typed "Ctrl+F8".
It worked great :]
I see myself using this program more into the future.
I often make curl requests. I often want to see the HTTP headers and also the content.
Most of the time, you can just add -i or --include to include the headers in the printout
curl -i https://alifeee.co.uk/
In writing this (and looking at man curl), you can also use -v or --verbose to view the headers.
But, if you want to downloadboth the headers and the content in one request (e.g., for a particularly large file or a server you don't want to hammer), you can use a script like this:
# customisation
url="https://alifeee.co.uk/"
file="alifeee.html"
filenom="${file%.*}"
fileext="${file#*.}"
# request
curl -i -o "${filepath}" "${url}"
# get index of first blank line
blank=$(cat "${filepath}" | awk '/^\r?$/ {print NR; exit}')
# cut file up to first blank line
head -n "$(($blank - 1))" "${filepath}" > "${filenom}_HEADERS.txt"
# cut file after first blank line
tail -n "+$(($blank + 1))" "${filepath}" > "${filenom}_RESPONSE.${fileext}"
go to "servers" and click "power on" for the server titled "Minecraft server" (minecraft box)
wait a bit for it to power up, and connect via ssh (sometimes I just run `while true; do ssh minecraft; sleep 2; done)
start a tmux session and run ./run to turn on the Minecraft server
disconnect from the tmux session and from the minecraft box
connect to my web server and change map.mc.alifeee.net to 301 redirect to livemap.mc.alifeee.net
Turning it off
Then, on Wednesday mornings (if I remember), I:
ssh onto the minecraft box
connect to the tmux session and stop the Minecraft server with CTRL+C
connect to my web server and use rsync to copy the world and dynmap files as backups (this takes about 3 minutes)
(still on the web server) switch off the 301 redirect
run sudo shutdown on the minecraft box to turn it off
The problems
Each of these steps can take a few seconds to run, so I am often multitasking, and I often forget things (like forgetting the backup, forgetting to actually run shutdown after all is done).
So, I've tried to automate it.
Doing it automatically
I found out that Kamatera (the server host) has an API that you can use to remotely turn on/off servers, which is the only thing that I was really missing.
cron tasks - web server
Here are the cron tasks on my web server:
# turn Minecraft server server on/off
45 16 * * 2 /home/alifeee/minecraft/togglepower.sh on >> /home/alifeee/minecraft/cron.log 2>&1
5 4 * * 3 /home/alifeee/minecraft/rsync_backup.sh on >> /home/alifeee/minecraft/cron.log 2>&1
15 4 * * 3 /home/alifeee/minecraft/togglepower.sh off >> /home/alifeee/minecraft/cron.log 2>&1
$ cat rsync_backup.sh
#!/bin/bash
date
echo "saving cron log"
rsync minecraft:/usr/alifeee/minecraft/cron.log cron_minecraft.log
date
echo "saving world"
rsync -r minecraft:/usr/alifeee/minecraft/world/ world/
date
echo "saving dynmap"
rsync -r minecraft:/usr/alifeee/minecraft/dynmap/web/ dynmap/web/
date
echo "done!"
What about the map?
Well, I figured this was too annoying to automate, so I just wrote a front page to pick whether you wanted the "dead map" or the "live map" (on https://map.mc.alifeee.net/ – link probably dead).
The HTML for this simple picker makes quite a nice page:
Today, I wanted to have an overlay (think Discord voice chat overlay, or when you pop-out a video in Firefox, or when you use chat heads on mobile) which showed me who was online on the server.
Querying the Minecraft server status
After seeing an "enable status" option in the server's server.properties file, and searching up what it meant (it allows services to "query the status of the server), I'd used https://mcsrvstat.us/ before to check the status of the server, which shows you the player list in a browser.
But a local overlay would need a local way to query the server status. So I did some web searching, found a Python script which wasn't great (and written for Python 2), then a self-hostable server status API, which led me to mcstatus, a Python API (with command line tool) for fetching server status.
Next, a way of having an overlay. Searching for "linux x simple text overlay" led me to xmessage, which can show simple windows, but they're more like confirmation windows, not like long-lasting status windows (i.e., it's hard to update the text).
I was also led to discover conky, which – if nothing else – has a great name. It's designed to be a "system monitor", i.e., a thing wot shows you your CPU temperature, uptime, RAM usage, et cetera. The configuration is also written in Lua, which is super neat! I still want to get more into Lua.
Using conky
By modifying the default configuration (in /etc/conky/conky.conf) like so:
…when we run conky it opens a small window which contains the output of the script ~/temp/minecraft/check.sh (the 5 after execpi means it runs every 5 seconds). If this script was just echo "hi!" then that conky window looks a bit like:
———————+x
| |
| hi! |
|_______|
I use Pop!_OS, which uses Gnome/X for all the windows. With that (by default), I can right click the top bar of a window and click "Always on Top", which effectively makes the little window into an overlay, as it always displays on top of other windows, with the added bonus that I can easily drag it around.
Writing a script for conky to use
Now, I can change the script to use the above Minecraft server status JSON information to output something which conky can use as an input, like:
I often want to get my current WiFi name (SSID) and password.
How to get name/password manually
Sometimes, it's for a microcontroller. Sometimes, to share it. This time, it's for setting up an info-beamer device with WiFi.
Before today, I would usually open my phone and go to "share" under the WiFi settings, and copy the password manually, and also copy the SSID manually.
It's finally time to write a way to do it with bash!
How to get name/password with bash
After some web-searching, these commands do what I want:
alias wifi=iwgetid -r
alias wifipw=sudo cat "/etc/NetworkManager/system-connections/$(wifi).nmconnection" | pcregrep -o1 "^psk=(.*)"
How to use
…and I can use them like:
$ wifi
the wood raft (2.4G)
$ wifipw
[sudo] password for alifeee:
**************
atuin dotfiles alias set wifi 'iwgetid -r'
atuin dotfiles alias set wifipw 'sudo cat "/etc/NetworkManager/system-connections/$(wifi).nmconnection" | pcregrep -o1 "^psk=(.*)"'
Now, they will automatically be enabled on all my computers that use Atuin. This is actually not… amazingly helpful as my other computers all use ethernet, not WiFi, but… it's mainly about having the aliases all in the same place (and "backed up", if you will).
Once again, I start by downloading the JSON files, so that (in theory) I can make only one request to each SpaceAPI endpoint, and then work with the data locally (instead of requesting the JSON from the web every time I interact with it).
This script is modified from last time I did it, adding some better feedback of why some endpoints fail.
# download spaces
tot=0; got=0
echo "code,url" > failed.txt
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
while read double; do
tot=$(($tot+1))
name=$(echo "${double}" | awk -F';' '{print $1}');
url=$(echo "${double}" | awk -F';' '{print $2}');
fn=$(echo "${name}" | sed 's+/+-+g')
echo "saving '${name}' - <${url}> to ./spaces/${fn}.json";
# skip unless manually deleted
if [ -f "./spaces/${fn}.json" ]; then
echo -e " ${YELLOW}already saved${NC} this URL!" >> /dev/stderr
got=$(($got+1))
continue
fi
# get, skipping if HTTP status >= 400
code=$(curl -L -s --fail --max-time 5 -o "./spaces/${fn}.json" --write-out "%{http_code}" "${url}")
if [[ "${?}" -ne 0 ]] || [[ "${code}" -ne 200 ]]; then
echo "${code},${url}" >> failed.txt
echo -e " ${RED}bad${NC} status code (${code}) for this url!" >> /dev/stderr
continue
fi
echo -e " ${GREEN}fetched${NC}! maybe it's bad :S" >> /dev/stderr
got=$(($got+1))
done <<<$(cat directory.json | jq -r 'to_entries | .[] | (.key + ";" + .value)')
echo "done, got ${got} of ${tot} files, $(($tot-$got)) failed with HTTP status >= 400"
echo "codes from failed.txt:"
cat failed.txt | awk -F',' 'NR>1{a[$1]+=1} END{printf " "; for (i in a) {printf "%s (%i) ", i, a[i]}; printf "\n"}'
# some JSON files are malformed (i.e., not JSON) - just remove them
rem=0
for file in spaces/*.json; do
cat "${file}" | jq > /dev/null
if [[ "${?}" -ne 0 ]]; then
echo "=== ${file} does not parse as JSON... removing it... ==="
rm -v "${file}"
rem=$(( $rem + 1 ))
fi
done
echo "removed ${rem} malformed json files"
Extracting contact information
This is basically copied from last time I did it, changing membership_plans? to contact?, and changing the jq format afterwards.
We can filter this file to only the "mastodon:" lines, and then extract the server with a funky regex, and get a list of which instances are most common.
Sheffield city council publishes a list of HMO (House in Multiple Occupation) licences on their HMO page, along with other information about HMOs (in brief, an HMO is a shared house/flat with more than 3 non-family members, and it must be licenced if this number is 5 or more).
How accessible is the data on HMO licences
They provide a list of licences as an Excel spreadsheet (.xlsx). I've asked them before if they could (also) provide a CSV, but they told me that was technically impossible. I also asked if they had historical data (i.e., previous spreadsheets), but they said they deleted it every time they uploaded a new one.
Therefore, as I'm interested in private renting in Sheffield, I've been archiving the data in a GitHub repository, as CSVs. I also add additional data like lat/long coordinates (via geocoding), and parse the data into geographical formats like .geojson, .gpx, and .kml (which can be viewed on a map!).
Calculating statistics from the data
What I hadn't done yet was any statistics on the data (I'd only been interested in visualising it on a map) so that's what I've done now.
I spent the afternoon writing some scripts to parse CSV data and calculate things like mean occupants, most common postcodes, number of expiring licences by date, et cetera.
General Statisitcs
I find shell scripting interesting, but I'm not so sure everyone else does (the script for the interested). So I'm not going to put the scripts here, but I will say that I used these command line tools (CLI tools) this many times:
cat 7 times, tail 8 times, wc 1 time, csvtool 7 times, awk 4 times, sort 7 times, head 3 times, echo 7 times, uniq 2 times, sed 3 times.
Anyway, here are the statistics from the script (in text form, as is most shareable):
the mean number of occupants may be rising or it may be statistically insignificant
there are either a lot of houses becoming "not HMOs" overall (and on roads like Crookesmoor Road) or there are becoming a lot of unlicenced HMOs
Statistics on issuing and expiry dates
I also did some statistics on the licence issue and expiry dates with a second stats script, which – as it parses nearly 5,000 dates – takes longer than "almost instantly" to run. As above, this used:
date 10 times, while 6 times, cat 3 times, csvtool 2 times, tail 5 times, wc 3 times, echo 23 times, sort 11 times, uniq 4 times, sed 4 times, awk 9 times, head 2 times
The script outputs:
hmos_2024-09-09.csv
1745 dates in 1745 lines (627 unique issuing dates)
637 expired
1108 active
Licence Issue Dates:
Sun 06 Jan 2019, Sun 06 Jan 2019, … … … Wed 12 Jun 2024, Tue 09 Jul 2024,
Monday (275), Tuesday (440), Wednesday (405), Thursday (352), Friday (256), Saturday (5), Sunday (12),
2019 (84), 2020 (311), 2021 (588), 2022 (422), 2023 (183), 2024 (157),
Licence Expiry Dates:
Mon 09 Sep 2024, Mon 09 Sep 2024, … … … Mon 11 Jun 2029, Sun 08 Jul 2029,
2024 (159), 2025 (824), 2026 (263), 2027 (225), 2028 (185), 2029 (89),
hmos_2025-01-28.csv
1459 dates in 1459 lines (561 unique issuing dates)
334 expired
1125 active
Licence Issue Dates:
Mon 28 Oct 2019, Mon 04 Nov 2019, … … … Mon 06 Jan 2025, Tue 14 Jan 2025,
Monday (243), Tuesday (380), Wednesday (338), Thursday (272), Friday (211), Saturday (6), Sunday (9),
2019 (2), 2020 (130), 2021 (567), 2022 (406), 2023 (181), 2024 (170), 2025 (3),
Licence Expiry Dates:
Thu 30 Jan 2025, Fri 31 Jan 2025, … … … Mon 22 Oct 2029, Wed 28 Nov 2029,
2025 (681), 2026 (264), 2027 (225), 2028 (184), 2029 (105),
hmos_2025-03-03.csv
1315 dates in 1315 lines (523 unique issuing dates)
189 expired
1126 active
Licence Issue Dates:
Mon 28 Oct 2019, Mon 04 Nov 2019, … … … Wed 05 Mar 2025, Wed 05 Mar 2025,
Monday (217), Tuesday (339), Wednesday (314), Thursday (244), Friday (189), Saturday (4), Sunday (8),
2019 (2), 2020 (64), 2021 (494), 2022 (399), 2023 (177), 2024 (170), 2025 (9),
Licence Expiry Dates:
Fri 07 Mar 2025, Fri 07 Mar 2025, … … … Mon 22 Oct 2029, Wed 28 Nov 2029,
2025 (533), 2026 (262), 2027 (225), 2028 (184), 2029 (111),
Potential conclusions on dates
Again, draw your own conclusions (homework!), but some could be:
Sheffield council's most productive day of the week is Tuesday
Something is slowing down the process or the data uploaded in March wasn't up to date or the distribution of licences isn't even across the year, as only 9 licences are marked as issued in 2025
Why is this interesting
I started collecting HMO data originally because I wanted to visualise the licences on a map. Over a short time, I have created my own archive of licence history (as the council do not provide such).
Since I had multiple months of data, I could make some comparison, so I made these statistics. I don't find them incredibly useful, but there could be people who do.
Perhaps as time goes on, the long-term comparison (over years) could be interesting. I think the above data might not be greatly useful as it seems that Sheffield council are experiencing delays over licensing at the moment, so the decline in licences probably doesn't reflect general housing trends.
However, further down, it lists how to access it, which says:
You can search for and view open records on our partner site Findmypast.co.uk (charges apply). A version of the 1939 Register is also available at Ancestry.co.uk (charges apply), and transcriptions without images are on MyHeritage.com (charges apply). It is free to search for these records, but there is a charge to view full transcriptions and download images of documents. Please note that you can view these records online free of charge in the reading rooms at The National Archives in Kew.
So… charges apply.
Anyway, for a while in April 2025 (until May 8th), FindMyPast is giving free access to the 1939 data.
Of course, family history is hard, and what's much easier is "who lived in my house in 1939". For that you can use: