GeistHaus
log in · sign up

M0YNG.uk Feed

Part of m0yng.uk

M0YNG is the callsign of a radio amateur in Gloucestershire. This is a smolnet website available via Gopher and Gemini and HTTP.

stories primary
A story of storage and suffering

Let me tell you a story of storage and suffering (and also some notes for future self)

Several years ago I bought a buffalo NAS, it has/had 4×2TB drives in it.

After I got frustrated with the built in OS I forced it to run Debian using Debian_on_Buffalo and foolishly made the main storage pool RAID5.

It worked well enough, kinda slow (the CPU was not up to the task of RAID5!) but it worked and hummed along being an NFS and SMB server that gradually filled up with photo and backups and music and the things people put on a NAS.

I kept it updated, but over time I noticed that it was no longer getting new packages, then I realised it was still running Debian 11, and 13 was out and I was running out of space (to the point where I now had a couple of USB drives hanging off the "servers") but it was still working. Replacing it seemed expensive, why pay for the same storage I already had? And if I wanted a chunk more storage it was a big chunk more money, especially if I wanted RAID5 still. So I didn't.

Then, it sort of vanished from the network. It was still running, and connected, but I couldn't SSH into it or access any files.

I rebooted it, it didn't boot. It failed to initiate one of the SATA controllers.

The Initial "oh shit"

OK, this sucks, but maybe we can recover it simply.

If the controller won't come up, maybe it's the NAS hardware and the drives and data are fine.

I have two "servers" (that a Lenovo ThinkCentre and a Dell whatever-the-number-is (ultra?) small form factor things) so I don't NEED a dedicated NAS, I can just connect the drives directly to one of those.

A bit of rummaging around later and I conclude the "ORICO 5Bay Hard Drive Enclosure Type-C to SATA 3.5inch Enclosure Magnetic Tool-Free External HDD SSD Enclosure Storage Case Built-in Fan for Data Backup, NAS Expansion Up to 90TB(5x18) - DS500C3" seems like a good option.

A quick review of the ORICO 5 Bay enclosure

Pros:

  • fair price?
  • 5 slots, plenty of space for many needs
  • quiet
  • USB-C is standard so I don't need some weird micro-USB-3 thing
  • only one plug needed to power 5 drives

Cons:

  • drives don't fit snug in the holes, can cause noise / vibration
  • smaller than 3.5" drives have no support and will flop about
  • weird power connector
  • lid is stupidly shiny and shows all fingerprints
  • one key feature is lacking … read on!
How do I RAID?

It arrived!

Great, remove the drives from the buffalo - discover they are the OEM drives so must be quite a many year old now - pop them into the enclosure, connect it to an computer, spend quite some time trying to work out how you re-connect an existing software RAID to a different computer.

Re-connect an existing RAID array
  1. install mdadm
  2. sudo mdadm --assemble --scan # this finds and "makes" the arrays
  3. sudo mdadm --query --detail /dev/md* # this tells you what it found

That's it. Your array is (in theory) now ready to mount or do whatever.

That wasn't the problem

mdadm reports the State as degraded. One of the drives is dead. Not the NAS.

At this point I probably should have just ordered a new 2TB drive, popped the working 3 and the new one back in the NAS and left it to re-sync.

I did not do that.

Why not?

  1. The NAS hardware struggled to read and write the RAID5 array, I assumed it would struggle to repair it
  2. RAID5 with 4×2TB only gives around 6TB of storage, I was already using all of it, I wanted to take the chance to get more space.

So, onto eBay I go and find two very well priced refurb (maybe) 8TB HGST drives - an order is placed and a few days later I have them in hand.

Problems multiply - part 1: XFSv4

Remember I said I hadn't updated Debian on the Buffalo?

Yeah, I'd also (for reasons I can't recall) used XFS for the file system on the array.

Guess what is no longer supported on Debian 13? Yeah, XFSv4.

How do you upgrade to XFSv5 you ask? Start again.

Literally, back it up, format in v5, and put the data back.

At this point I think I thought I couldn't boot the buffalo with 3 drives, or something. I can't recall, but even if I could I suspected it would struggle with the degraded array.

So I go through various older versions of Debian as live systems on my laptop until I find one that is new enough to handle the WiFi and old enough to do XFSv4 (12.4 is what I used)

Success, I can boot that, assemble the array, mount it, and see the data! It sucks, but it works.

Problems multiply - Part 2: 4kn

Remember I said there was a secret bonus "con" for the 5 bay enclosure?

The drives I got off eBay are from a server, they are dated 2019, they have a sticker saying "4KN".

I remove the failed drive, and pop the two new ones into the remaining slots. I connect the enclosure and they are detected! Huzzah!

BUT! I can't format them, I can't partition them, I get a flood of errors in dmesg.

Yeah, 4KN means they use 4096 byte sectors (4k) native (n).

The USB bit of the enclosure can't do that. it needs 512, or at least for the drive to pretend to be 512 (some drives are 4k but can present as 512e and emulate being older.)

Much searching and stress later I conclude that these drives cannot pretend, they are 4kn and that's that. So I need an enclosure that can handle 4kn drives.

Reminder - I don't have a desktop computer with spare SATA connections, just a laptop and these two tiny computers.

This (supporting 4kn drives) it seems, would be a helpful attribute to list in your product description.

It is not, it seems, an attribute any product mentions having (or lacking.)

For your reference I can confirm that these devices DO NOT SUPPORT 4KN DRIVES:

  • ORICO 5bay (because I couldn't use it) (and probably all other ORICO enclosures)
  • Every TERRAMASTER device (because I asked them)

I can confirm that the "SABRENT EC-DFLT" DOES support 4KN drives.

Because, after a lot more stress and searching and cross checking tables of supported drives to data sheets I took a chance (they are about £21 each).

A quick review of the SABRENT EC-DFLT

Pros:

  • supports 4KN drives
  • the drive doesn't flop about, even if it's small
  • super simple to use
  • kinda normal barrel plug power
  • kinda normal USB-B connection (the v3 one with extra bit on top)
  • the power button is a toggle so will still be on if the power goes out and comes back

Cons:

  • No cooling (but also no fan noise)
  • the power button is on the top and easy to press - immediately removing power from the drive (ask me how I know) (I may 3d print a little hat to protect it)
  • the LED is pretty bright (and is blue)
Failed side-quest

No, I could not get the Buffalo to boot with the two new drives. Mainly because it stores it's OS on the drives, and these drives did not have an OS on them, and at this point I didn't have anything that would work with them at all so I couldn't even prime them somehow.

In short, a couple of hours wasted and much stress (but the buffalo software works fine in Wine.)

A start to janky recovery

At this point I almost have it working.

I have the 5 bay enclosure with 3 working drives in it, connected via USB-C to my laptop, which is running a live Debian 12.4 off a USB stick.

I have the new drives connected via USB to the "server" (luckily I had enough spare sockets to power these all!)

I have formatted and partitioned and RAID-ed the two new 8TB drives as RAID1

(Two main points chose RAID1 for me, 1: I don't want to lose my data, 2: I can't justify more drives and enclosures and etc. right now)

I start copying data from the degraded array, over WiFi (no the laptop doesn't have a (full size) Ethernet port) to the new drives.

It's going slow, but it's going.

Then I come back in the morning to find the laptop wasn't as plugged into power as I thought it was, and has shut down.

Also the new RAID1 array is "degraded" with a drive missing. I force the drive back into the array, resume rsync.

A bit later, the drive as failed again.

I can't get SMART data out of it (oh yeah, another thing, the SMART database doesn't have the SABRENT enclosure in it (yet, as far as Debian 12 knows) so I have to update the SMART thing and then put usb-storage.quirks=152d:a578:u into /etc/default/grub) and even after that I can't get SMART info and the "server" won't get past BIOS with the drive connected.

At this point it's Saturday, I'm a little over a week into the madness, and about £350 in.

I try my best to be rational, but at the same time I've got data I don't want to lose, and various things don't work right now (pretty much all our music for one).

I take a slightly rash decision, I try to buy 2 8TB drives from Currys. There is only one in stock. I buy one, collection in an hour, 4% cashback with my bank. At least I can secure the data AND a brand new drive is likely to have a much longer lifespan before failure. I hope.

I go collect it, and some pastries.

A quick review of the WD Elements External Hard Drive - 8 TB, Black

Pros:

  • was in stock and a fair price
  • kinda normal barrel plug for power
  • stands upright, so takes up less desk/shelf space
  • looks quite nice? The LED is subtle.
  • can probably break the drive out of the cage if needed

Cons:

  • Stupid micro-USB3 connector
  • people will probably tell me WD are bad
It's working, don't touch it!

Copying data over WiFi was always stupid, I could connect all the drives to my laptop and it will be faster. I don't have enough ports though, so a USB3 hub is redeployed - I now have 5 hard drives totalling 22TB of raw storage connected to a laptop and I think I know what I'm doing. I also have most of this on my desk with the keyboard pushed to the back and the mouse nowhere to be seen.

I connect the new drive, I format it, I nuke the old array and create it fresh.

I format the new array EXT4 - I'm not messing around with edge case fancyness, I just want my data to remain available.

I mount the old array, I mount the new array, I start rsync (--progress --archive)

It starts copying.

I watch it for a bit.

I disable the laptop from going to sleep.

I keep watching it.

It seems to be working.

I keep checking on it far more often than I want to admit.

It keeps working.

I'm sending updates of the progress copying data and the array re-sync.

It's Sunday morning, still going.

It's Sunday evening, still going.

It's Monday and I need the desk for work. I carefully move a couple of things around - having a wireless split keyboard comes in handy when you have a hard drive in the middle of the desk. I literally "work around it".

It's Monday evening, still going.

It's Tuesday morning, I wonder if I the re-sync will ever finish - I search a bit and conclude it's probably fine if it doesn't, it should resume when I connect it to the "server".

I decide to pause the re-sync.

echo 0 > /proc/sys/dev/raid/speed_limit_max

rsync speeds up from 13MB/s to 20MB/s! This could have been finished yesterday if I'd done this to start with! Also the drives are a lot quieter when they're not doing two things at the same time.

It's Tuesday afternoon, rsync stops.

df -h says there is the same amount of data on both arrays. Everything seems OK.

I shut down my laptop and disconnect the enclosures.

I carefully place the drives beside the "server" in the "rack" (shelf) and route the power cables nicely.

The drives are detected, the array is assembled, the file system mounted, the array configuration added to /etc/mdadm/mdadm.conf and the mount to /etc/fstab.

Jellyfin and Navidrome are pointed to the new locations, scans are run, things I bought on Bandcamp Friday are downloaded and added, everything seems to be working.

Happy ending?

In the end I have:

  • 8TBish of RAID1 storage
    • slightly more than I had before
    • uses less electricity usage (about ⅖)
    • is a lot faster
  • one fewer computer to update
  • 3 2TB drives that are probably about to fail
  • 1 2TB drive that is dead
  • 1 8TB drive that is dead
    • seller has offered refund
  • 1 unused hard drive enclosure
    • I'll keep this because it was cheap and I'm sure it will be useful
  • 1 5 bay enclosure that I have no use for
    • I've initiated a refund for this too as it's not compatible with my needs
  • More experience
  • guilt for spending money
    • total end cost will be around £310 (once I've returned and refunded bits)
    • but also, what is the price of family photos and music from Bandcamp / CDs vs subscriptions?
    • Spotify premium family would cost £263.88 a year

Ultimately this was something I wanted to do, just not with the stress and pressure of a failing array and not being able to access my data.

Obligatory "RAID is not a backup" and 3-2-1 etc. goes here.

I may try and reactivate the Buffalo with new(er) drives and new(er) Debian and deploy it with my parents for "offsite" "backup" - but that's a future project, for now I have new LukHash and Revengeday to listen to.

https://m0yng.uk/2026/02/A-story-of-storage-and-suffering/
My bike doesn't make sense - Mycle Charge (+ Comfort)

I've posted about the Charge before, in 2022, when I'd had it a few months and done 160km on it.

Since then I've done over 2000km and it doesn't make sense to me.

Charge

The Mycle Charge is a "fat tyre folding electric bike".

It has fat tyres which come with "puncture resistant tyres" (spoiler, they're not that resistant) which are nobbly. Great for loose surfaces, mud, etc.

But that doesn't make sense, because they're 20 inches, and the bike is heavy (26kg) so I'm not using it offroad. I've swapped the tyres for some Schwalbe "Super Moto-X" which are significantly better for most of my riding on cycle paths, roads, etc.

Ok, so it folds and has 20" wheels so doesn't need booking on a train. But it's also heavy so hard to lift onto a train, especially when folded (it doesn't hold itself folded either) and it doesn't fit into any of the storage on the train because it folds so poorly - it also only just fits in my car boot!

Ok, so it's not great for folding and taking with you, but it's also got fat tyres which won't fit into the bicycle holders on the train, or the racks in town, or any number of other situations.

Comfort

This is a newer addition, after one of the two charges got stolen. It's a more traditional bicycle design and has 700c wheels and the battery is integrated into the frame in a more subtle way.

This bike also has confusing features. The main one is that there is no "ignition". On the chrage the battery can be locked to the frame, but power disabled, with a key. But on the comfort the key is only there to lock the battery to the frame, so you can't deactivate the power without removing the battery, which isn't very convenient for parking in town.

The rear rack is weird too, it's welded and part of the frame, which is good, but it's thicker than the charge and the pannier bags I got won't fit on, nor will the child seat.

It's also prone to be skitty, possibly due to the geometry being a V rather than a full triangle, and a wobble can propogate through the frame easily. You also need the handle bars quite a lot higher and further back than you'd initially expect otherwise your weight is pushed into the front wheel which results in the front becoming unsettled. However, it is the only bike I can ride zero-handed (although not for long, yet) so there is some inherent stability there.

https://m0yng.uk/2025/03/My-bike-doesn't-make-sense---Mycle-Charge-(+-Comfort)/
Tracking the benefits of Solar and Battery

On / In my house I have a 4.4kW (peak) solar array (facing West), a 3.6kW (peak) inverter, and a 9.6kWh battery for storage.

I've posted about this system before, so have a look at the tags to find other posts.

So, we generate electricity in the sun and store it in a battery, we can also charge the battery from the grid, and send both back to the grid.

We can also track the real time cost of our electricity (thanks to the Octopus Energy API) and the real time "carbon intensity" of the grid (thanks to the Carbon Intensity API).

About a year ago I set up a bunch of "helpers" in Home Assistant to meld various combinations of these to give me four key statistics:

  • How much money the battery has saved us
  • How much CO₂ the battery has avoided being emitted
  • The same, but for the solar panels
TL;DR, give me the figures

After one year (01 March 2024 to 28 February 2025)

The battery saved £452.81 + Solar saved £272.67 = £725.48

The battery avoided 129.2kgCO₂ + Solar 154.1kgCO₂ = 283.3kgCO₂

We exported 1815.2kWh, at £0.15 = £272.28

During this period the panels produced 3397kWh and the battery dischard 3580kWh.

This gives a total (estimated) "benefit" of £997.76

Some of the export is during "saving sessions" where we get paid even more to export and that isn't included, if it is then we're easily over £1000 saving.

This gives a financial "Return on Investment" of around 11 years. Considering the solar panels are expected to last much longer than that and the battery has a 10 year warrenty, this feels like "a good deal". Plus we're only buying a net 2700 kWh across the entire year, 2348 kWh of which is the heat pump, and all of which is bought in at slightly less than 50% the "standard/capped rate".

It's interesting to observe that the battery saves most money and the solar panels avoid emitting the most CO₂. This because generally the CO₂ intensity of the grid is highest during the day, when the solar panels are working to generate power (and we're using it) and that the battery is "net"; charging it from the grid costs money and "costs" CO₂. Even so the money savings are much higher with the battery than the solar panels, and the gap between CO₂ savings isn't particularly big. If you're pondering solar without batery, I'd suggest getting battery without solar!

I want to know how you've worked this out

It's mostly just melding the right things in the right way.

Ingredients
  • Carbon Intensity API for my area
  • Python script to get and send this to Home Assistant every 5 minutes
  • The Octopus Energy API for my tariff
  • A Home Assistant plugin that exposes this
  • A couple of "Helpers" that provide an "instantanious" value for carbon and cost
  • A few more helpers that act as "utility meters" and track this over the day, and in total
Method

The solar helpers are simple, any solar power going directly to the house is tracked. Power going to the battery is (effectively) 0 carbon and 0 cost, so it's not tracked. Power going out to the grid is also not tracked (although it could be, and it would be a nice addition especially for "saving sessions" or days when it's sunny but not windy and the grid is using a lot of gas.)

The code just multiplies the power flowing from the panels to the house by the current carbon intensity of the grid, divided by 1000 because that's in kW and the others are in watts.

{{
    float(states.sensor.solar_to_house.state)
    * (float(states.sensor.national_grid_carbon_intensity_for_west_midlands_gco2_kwh.state) / 1000)
}}

The battery helpers need to be "net" trackers, if the battery is discharging we increase the "benefit", if we're charging from the grid we decrement the "benefit" (as it's costing us money to buy the power, and generating it emits CO₂).

Our code subtracts the "grid to battery" from the "battery to house" and them multiplies it by the carbon intensity of the grid.

{{
    (float(states.sensor.battery_to_house.state) - float(states.sensor.grid_to_battery.state))
    * (float(states.sensor.national_grid_carbon_intensity_for_west_midlands_gco2_kwh.state) / 1000)
}}

The "today" and "total" helpers are just "Utility Meters" pointed at the relevant "instant" helpers.

That's pretty much it, now you can just see it happening.

Notifications

This is easy enough to add to the dashboard, but it's also nice to get a summary each day.

There is an automation that fires at 2359 and send a summary via Matrix, which looks something like:

Saved

🔋 £3.05 | 13.4kWh | 1810.75gCO₂

☀️ £1.05 | 11.4kWh | 674.92gCO₂

plus exported ⚡ 1.8kWh.

Optimisation

We also have a heat pump, this is where a lot of the battery savings come from during the cold months as it's common for the pump to take longer to warm the house up than the 3 hours of cheaper electricity we get every morning.

On these days the heat pump will come on and start warming the house when the grid tariff is in a low price time at 0400, at the same time the battery will charge too. Ideally the house is warm and the pump stops before the price goes up at 0700, but if it hasn't the pump can keep running off the battery for another couple of hours and we'll still have stored power to last us until the next cheap time at 1300.

Because we're in a slighly perverted situation where we can buy electricity from the grid for less than we can sell it back (for 8 hours every day the price is about 12.5p and we can sell back at any time for 15p) it doesn't make financial sense to run off solar. It makes financial sense to charge the battery up from the grid when it's cheap and then sell everything we generate back netting a 2.5p (ish) profit per kWh. So we do that!

In theory we can fill the battery three times every day, 3.6kW charge for 8 hours is 28.8kWh, or exacly 3 times our 9.6kWh battery capacity. But, most of the time, we don't use 28.8kWh in a day, 46kWh is the highest day I can find (11th January 2025) 34.3kWh of that was the heat pump and 18kWh went into and out of the battery - less than the theoretical maximum (remember we can just use power directly from the grid, especially when it's cheap.)

Price is one thing to optimise, it's easy enough, is the import cost less than the export price? Great, charge.

But what about CO₂?

The grid carbon intensity API also gives a forecast of how intensive the generation over the next 24 hours will be (it's one of many ways generation is balanced and planned between wind, solar, hydro, gas, etc.)

What if we try and charge the battery when the grid carbon is lowest? Trickle charging when it's high and shoving those electrons in as fast as possible when it's low?

Ingredients

The glue here is that both APIs give data per half hour, so we can simply generate a list of time slots with their cost and CO₂, then sort the list by cost, then by CO₂. Easy.

My script is very janky, but it then goes through every slot and checks to see if its cost is below a "threshold" (the 15p export) and if it is, that slot gets marked as "chargeEnable" and the CO₂ value is compaired to the highest and lowest values identified so far. We end up with a every half hour in the next 24 hours ranked by cost, CO₂, marked as suitable for charging, and also the highest and lowest CO₂ of the charging slots.

The really janky bit is when it works out a "variance" for the charge rate. It then scales the maximum possible charge rate by how much each slot's CO₂ varies from the other slots. Thus, if there are some slots which are very high and others that are very low it will try to charge the battery as fast as possible when it's low, and slowly (or not at all) when the CO₂ is high. It also means that is there isn't much variation (e.g. super windy all day, or everything generated by gas) it will pretty much just charge close to full speed all the time - because there is no reason not to.

The script runs twice a day and puts the charge rates into a file, and then cron runs every half hour to read the file and send the charge rate to Home Assistant.

The script also outputs a chart to show what slots have been allocated what charge speed. It's a complex chart I would tell other people off for making, but it shows me what I want to see, and as I'm pretty much the only user this seems acceptable.

Here are some recent examples, the charge rate is shown with the blue bars using a scale of kW, the grid CO₂ intensity is the pink line using a scale of grams per kWh (times 100), the purple line shows the cost to import from the grid in pence, and the dashed green line is the "threshold cost" which is used to determine when to enable charging.

The Carbon intnsity is fairly similar all day, the charge rate is in the top 70% plus most of the time, but it drops off sharply at the end of the morning session as the carbon intensity jumps up from 50g per kWh to 200g per kWh

Carbon is moderate and climbs in the evening, then drops off over night, the afternoon and evening charges are fairly conservative ranging from a few hundret watts to 1.5kW, but there is a big jump in the morning when carbon intensity is relatively low and the battery is charging at full speed

The charts have the extra benefit of making it easy to know when is best to put on other loads like the tumble dryer, rather than trying to decifer a list of times and carbon intensity values.

https://m0yng.uk/2025/03/Tracking-the-benefits-of-Solar-and-Battery/
Amateur Radio Inclusivity Pledge Accessibility Check

The Amateur Radio Inclusivity Pledge is great! I've been meaning to add it to my site for ages now (one moment ... done! See About) and Mastodon.Radio has been listed as an Organisation we love for some time.

Accessibility is a key foundation for inclusivity, but it sounds like there may be some issues ("I'm very much an amateur on the web design front") so let's see if we can help.

Although I am a professional accessibility consultant, this is not a paid for audit and I am not using all my usual tools (as I'm not at work!) so it's probable that I've missed things. I've also not looked at every facet of the service so the whole thing should be checked for anything I do identify.

I've linked everything I can to the international standard, please refer to WCAG Quick Reference for more specifics.

This check was undertaken on amateurradioinclusivitypledge.org

I did not check the docs as these were made with "Just the Docs".

Findings Language of page not set

3.1.1 This means screen readers and other assistive technology might not know how to read the text, especially if the user's default language isn't English (like the page.)

<html lang="en">
No H1

1.3.1 The title of the page is an H2, but there should be one (and only one) H1 on every page.

Unlabelled input (theme switch)

1.3.5 The checkbox has a element BUT it only contains two SVG images, which are both marked aria-hidden so the user does not know they are there and will not be able to use the checkbox.

As a minimum you should include some (visually hidden) text, or use the aria-label attribute, that reads "switch site theme".

As this changes something on the page, consider using telling the user what state it is in/will change to.

e.g. "Change theme to light" and "Change theme to dark".

Theme not automatically set

You can use prefers-color-scheme to detect and apply the user's preferred theme.

ARIP image has poor alternative text.

1.1.1

Currently it reads "Author's picture or avatar or logo", and also has a title of "ARIP Logo".

Set the alt to "AIRP Logo" and remove the title (title attributes are not well supported for assistive technology.)

List with one item

The intro quote is a list item, in a list with only one item. This should not be a list.

Justified text used for paragraph text

Just left justify it as justified text can be hard to read.

Insufficient contrast in footer

1.4.3

This text is rgb(128, 128, 128) / #808080 which has a ratio of 3.49:1 against the white background.

Change to #757575 at minimum.

"Go back" link lacks alternative text

1.1.1 the link (e.g. on the about page) to go back is just an SVG, so a screen reader user will not know where the link takes them.

Consider adding "go back" either as aria-label, visually hidden text, or inline of the SVG: Accessible SVGs

Post's date etc. doesn't need to be H4

On the posts page every post has it's title as H3 and the date as H4. The date should not be a heading.

No "call to action"

Consider adding a "Sign the Pledge" link to the front page so people can quickly know what you want them to do.

https://m0yng.uk/2024/12/Amateur-Radio-Inclusivity-Pledge-Accessibility-Check/
flohmarkt accessibility check

flohmarkt is "A decentral federated small advertisement platform", think classified ads like eBay used to be.

midzer asked for help checking the accessibility.

Although I am a professional accessibility consultant, this is not a paid for audit and I am not using all my usual tools (as I'm not at work!) so it's probable that I've missed things. I've also not looked at every facet of the service so the whole thing should be checked for anything I do identify.

I've linked everything I can to the international standard, please refer to WCAG Quick Reference for more specifics.

This check was undertaken on https://fleamarket.neilzone.co.uk and https://fedi.markets

Findings 2.4.2

The page title on the hompage is "None".

2.4.3

The header has a different visual order to keyboard focus order:

keyboard focus: Home link, sign in button(s), search visual: Home link, search, sign in buttons(s)

The items have a reading order that doesn't match the visual order. However, I'd suggest that's OK - if the order is:

  • Item title
  • Price
  • New/Old/etc.
  • Description

The image does not need alt (it should be "null") as it adds no value (unless you allow people to add descriptions!), you also don't need title attribute on the image,

2.1.1

Home, Local, All, tabs cannot be accessed using keyboard

1.4.3

Colour contrast, Colour Contrast.cc is great for checking this.

"New" badge has insufficient contrast, #dc322f on #000610 (dark theme) is 4.39.

"Old" badge has insufficient contrast, #976b00 is 4.28.

"Popular" badge has insufficient contrast, #627600 is 3.98.

"Read more about this instance" has insufficient contrast in dark mode, backgound (acutal) is #182d17 with #4e8429 text is 3.26.

Incorrect use of <aside>

Each item is within an <aside> but that's not how that element should be used: MDN: Aside

1.4.11

Search input has insufficient contrast on its border, it's really hard to spot in light theme.

3.3.2

Account input on item page is unlabelled.

1.1.1

User avatar has alt text of the username, which doesn't add any value. Consider "[username] avatar" or null.

https://m0yng.uk/2024/11/flohmarkt-accessibility-check/
delete all the tweets

Fuck twitter, anyway I've just deleted every tweet I posted since I joined in 2008 before deleting my accounts.

I threw this code together to do it, it sucks, it doesn't work without needing to refresh the entire page often, but it does work.

go to your profile, then "with replies", then open developer tools and paste this into the console, then when it seems to stop working refresh and do it again

document.querySelectorAll('[data-testid="unretweet"]').forEach(unretweet => {
    unretweet.click();
    setTimeout(confirmUnRetweet, 300);
});

function confirmUnRetweet() {
    document.querySelectorAll('[data-testid="unretweetConfirm"]').forEach(confirmUnRetweetElement => {
        confirmUnRetweetElement.click();
    })
}

function openMenu() {
    document.querySelectorAll('[data-testid="cellInnerDiv"] [aria-label="More"]').forEach(menuEl => {
        menuEl.click();
        setTimeout(clickDelete, 300);
    })
}

function clickDelete() {
    try {
        Array.from(
            document.querySelectorAll('[role="menuitem"]')).find(el => el.textContent.includes('Delete')).click();
            setTimeout(confirmDelete, 300);
    } catch (error) {
        console.error('can\'t click delete as there is no delete on this menu');
    }
}

function confirmDelete() {
    document.querySelector('[data-testid="confirmationSheetConfirm"]').click();
    setTimeout(openMenu, 300);
}
openMenu();
https://m0yng.uk/2024/11/delete-all-the-tweets/
Hexagonal Stickers for my new laptop

My long suffering and much used (and abused) MacBook Pro 13" (retina) from 2013 finally gave up recently. It's had memory issues for some time but mostly was able to keep going. Untill it couldn't and I get a full screen error one day and it would no longer boot.

At some point it had gained a "retro" rainbow apple sticker over the apple logo, and a wood veneer (real wood!) which had been stickered over a bit not complete coverage.

I've now got a Lenovo ThinkPad X390, refurbished, grade C, intel Core i5 8265U, 8GiB RAM, 256GB NVME storage, USB C port that does power data and display, so far I like it a lot. But it does lack stickers...

I have recently aquired a Brother QL-560 label printer. Ideas formed.

The following stickers are designed to follow the Hexagon Sticker Standard of 5.08cm diagonal plus or minus 1mm. They have also been optimised to print on my specific printer which can do black, white, and 8 shades of grey in between by dithering the black and white. Some have colour in them which is original, others it has been modified to suit the printer and may look odd when viewed/printed in colour.

My ThinkBook with a tesselation of stickers applied - aligned with and overlapping the ThinkBook logo

They make use of the following fonts:

License?

I'm making these available for your own personal use, you're welcome to adjust them to suit your own printer/situation/etc. but please don't sell them.

This is the base SVG file which is the correct size to make your own sticker designs in. base.svg

Stickers go here

Here follows the stickers, I've attempted to make some kind of grouping / category but it's not exact!

Linux / "nerd" Mastodon.Radio

Alex with heart eyes, lineart of Alex on a laptop, and another of them waving, Mastodon.Radio in the middle mastodonRadioAlex.svg

A circle of all the Alex emoji with Mastodon.Radio in the middle mastodonRadioAlexEmoji.svg

WCAG 2.2

Started as a basic idea because WCAG 2.2AA and GTFO are all 4 letters (ish) and morphed because Success Critera 3.1.4 requires abbreviations to be expanded and the joke kept expanding from there to also include the "or the latest applicable version of these guidelines" bit. Originally it was triangle shape. "Fudge" so you can put it onto a work laptop.

WCAG 22AA or GTFO in big, around the outside "Web Content Accessibility Guidelines Version 2.2* Level AA or Get The Fudge Out" and under the main text even smaller "* or the latest applicable version of these guidleines" WCAGGTFO.svg WCAGGTFOx4.svg

Craft & Code

Based on the tagline / sticker from Future Insights / Future of Web Design (FOWD) conference (quite a few years ago). I've had the big circle sticker on a netbook I took there ever since and it really resonates with me, the melding of craft, design, humans, and code.

This one uses Dank Mono but you could swap the bottom bit for any monospace or programming font.

Lineart person with cartoon style text reading Craft & Code followed by { ... } craftCode.svg

Disable IPv6

Inspired by this toot by Rail

If the solution is to disable IPv6, it is still broken disableIPv6.svg

I Void Warranties

With apologies to Jilles Groenendijk

I VOID WARRANTIES wrapped around the inside of the hexagon, with a skull and crossed soldering iron and screwdriver in the middle, and terminal prompt, multi-meter, oscilloscope, and binary/chip icons along the bottom iVoidWarrenties.svg

Warnings Danger - No Microsoft

Based on this toot or one like it

DANGER bent arcross the top, Do not connect this machine to a microsoft account no matter how much it asks, with a crossed out microsoft/windows logo at the bottom noMS.svg

Warning - Spicy Pillow

Inspired by this toot by @freakazoid@retro.social commenting that

Everything with a lithium battery should come with a big orange sticker that says "discontinue use if the battery starts to swell".

Warning triangle with text Contains Lithium Cells (which is crossed out and a comic font adds A Spicy Pillow) Discontinue use if battery (cross out, Pillow added) starts to swell lithiumCells.svg

I do not consent to the search of this device

Based on the EFF original sticker, but in the hex format.

Black with white text, I DO NOT CONSENT TO THE SEARCH OF THIS DEVICE noSearch.svg

DHMO

More for my DHMO containment vessles than laptop, but it's hexagonal... PS, do not expose your laptop to DHMO unless it has been specifically hardened againt this corrosive chemical.

WARNING Dihydrogen Monoxide Can cause serious injury or death if inhaled. Prolonged exposure to solid form causes severe tissue damage and necrosis. Exposure to gasious form can cause immediate burns and death. Do not combine with group 1 Alkali metals. QR Code for DHMO.org warningDHMO.svg

LGBTQ+

It's hard to do rainbows with shades of grey!

Trans Rights

Greyscale trans flag background with Trans Rights transRights.svg

Pronouns

A couple for myself/friends, I may make more or automate the process later... I'd suggest printing these directly from the SVG files so the QR codes don't get desroyed by being trurned into PNG files / or export them with a much higher DPI value.

Pronouns logo, My pronouns are He or They FYI with QR codes linking to mypronouns.fyi pronounsHeThey.svg

Pronouns logo, My pronouns are She or They FYI with QR codes linking to mypronouns.fyi pronounsSheThey.svg

The world is better with you in it

Doesn't really fit into any other the categories...

The world is better with you in it worldIsBetter.svg

"Political"

In quotes because I don't personally think any of this is controversial...

Black Lives Matter

This uses the Martin font.

Black Lives Matter BLM.svg

Join a union

It's a great day to JOIN A UNION joinAUnion.svg

No human is illegal

No human is illegal noHumanIllegal.svg

F Brexit

EU Flag stars surround "FUCK BREXIT" FBrexit.svg

And a slightly milder version (kinda)

EU Flag stars surround "I still think ... FUCK BREXIT" stillFBrexit.svg

Jolly Good Idea

Lots of text, but the message is clear...

I personally think it would be a jolly good idea to take the catestrophic collapse of the global climatge seriously and do something about it, for example just stopping oil would be a good start to rebel against the extinction of humanity jollyGoodIdeaClimate.svg

Abolish Billionaires

Ok, maybe SOME Humans should be illegal...

Bold text ABOLISH normal text Billionaires abolishBillionaires.svg

"Fan"? Debian

This just rips the logo from the Debian website obviously this logo falls under Debian's copyright and licensing.

Debian logo and text debian.svg

Xenia

This art by Cathodegaytube, find (out) more at Xenia, the Linux mascot, obviously the copyright and license for this image is up to Cathodegaytube.

Xenia the Linux Fox drawn by CathodeGayTube xenia.svg

Rocinante

Uses BankGothic and Open Sans Condensed.

Ship outline found in various places without any speicific license attached, but I've probably missed it.

Black with white text, ROCINANTE, an outline of the ship, and the text from the ship's placard rocinante.svg

https://m0yng.uk/2024/10/Hexagonal-Stickers-for-my-new-laptop/
Consistent device names with udev

I've often got multiple things plugged into my computer, for example a USB to serial cable for the TNC and a different USB to serial cable for CAT control of my HF radio. These usually show up as something like /dev/ttyUSB0 and /dev/ttyUSB1.

The problem is that the order is not always the same. Sometimes a reboot results in the devices swapping number and suddenly your software can't talk to the radio anymore.

Let's fix that!

(I'm doing this on Debian 12)

Identify the devices

We need to know which device is connected to what, the easiest way to do this is use lsusb then plug or unplug the device and see what appears/disappears.

For example

lsusb
Bus 001 Device 006: ID 08bb:29c6 Texas Instruments PCM2906C Audio CODEC
Bus 001 Device 004: ID 067b:23a3 Prolific Technology, Inc. ATEN Serial Bridge
Bus 001 Device 003: ID 10c4:ea60 Silicon Labs CP210x UART Bridge

Then I unplug the CAT cable

lsusb
Bus 001 Device 006: ID 08bb:29c6 Texas Instruments PCM2906C Audio CODEC
Bus 001 Device 004: ID 067b:23a3 Prolific Technology, Inc. ATEN Serial Bridge

Now I know that 067b:23a3 is the TNC and 10c4:ea60 is the CAT.

Tell udev what to do.

Create or edit /etc/udev/rules.d/99-usbtty.rules and populate it with something like this:

SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="ttyUSBIcomCat"
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="23a3", SYMLINK+="TNC"

We're tell it to look for a device with the vendor id 10c4 and product id ea60 and then create a symbolic link called ttyUSBIcomCat to point to the actual device.

This way we can tell our software to always look for /dev/ttyUSBIcomCat and it doesn't matter if it's number 0, 1, or 6352528.

You'll need to reboot or kick udev somehow, then check and see if it's worked:

ls -lh /dev/
crw-rw---- 1 root dialout 188,  1 Jul 12 21:08 /dev/ttyUSB1
lrwxrwxrwx 1 root root          7 Jul 12 14:41 /dev/ttyUSBIcomCat -> ttyUSB1

You're looking for something with whatever name you specified (ttyUSBIcomCat) followed by -> pointing to ttyUSBsomething. If it's there, it worked. If it didn't ... I don't know, check the IDs or something.

WSJT-X

WSJT-X didn't give the new symlink as an option for me. However you can just type the path in, you don't have to pick from the drop down list.

https://m0yng.uk/2024/07/Consistent-device-names-with-udev/
Modding the Dell DA-2 into a radio power supply

The Dell DA-2 is a power supply for, I don't actually know. A Dell computer of some sort I assume. They're popular for running external GPUs, and can be easily modified to supply 12v at up to 18 amps, aka 200 watts with no fan and (in theory) decent performance etc.

So, let's modify one to use as a radio power supply! (Please ignore the continuity errors, I took the photos after doing the mod and whilst putting it back together.)

To open it up you'll need a "security" bit, S2 T10, it's a 6 pointed star with a bit in the middle. The screws are all under the rubber feet.

Closeup of the screw with the bit I used to unscrew it

Inside the case you'll find everything wrapped up in metal which is held with tape, and shimmed into the case with two flat bits down the sides. Slide those out and the rest should come free easily.

The metal wrapped power supply, held closed by kapton tape

Cut the tape and you can unfold the metal and slip the thing out.

The metal unfolded but still containing the circuit

There is a big flat copper thing that connects the earth/ground/0v from one side to the other. You'll need to unsolder this to remove it.

A big copper rectangle on top of a black plastic wrap

Yet more unwrapping! Unwrap the plastic thing and you'll finally see the actual circuit.

This is where you need to tin your soldering iron and make sure it's hot as there are two bundles of wires that need removing and it'll take a lot of heat. Be patient and make sure all the solder is liquid before pulling them out.

There are also two smaller wires that need disconnecting.

back of the circuit board with the things that need de-soldering highlighted, it's the two big chunky mountains but you knew that already

Decision time. The supply can be switched by connecting pin 5 to ground. Although not any more as we just removed that wire... If you want it to always be on you can just short the through hole marked "remote" to ground. Or you can do what I did and wire in a switch.

I've no idea what "red" is for. I didn't connect anything to it.

Time to get the soldering iron to work again and connect your +ve and -ve wires. Use something chunky enough to handle 18 amps! I used some of the cable that came with my Icom IC-706 which I'd already cut in bits and fitted Power Poles onto.

Other side of the circuit board with chunky red and black wires and thinner wires added

Now, put it all back together! Don't forget the copper thing. Plastic wrap, copper, metal, tape, shims down side, case, screws. Be careful you don't trap any of your new wires on the screw posts.

https://m0yng.uk/2024/07/Modding-the-Dell-DA-2-into-a-radio-power-supply/
Solar Panels and Battery and Heat Pump Info Dump

I got solar panels, and a battery, and a heat pump! Be prepared for an info dump.

I've been writing this blog post for a month now, so if there are things missing or questions unanswered ... sorry? Maybe I'll do an update next year.

What do I have, what is it?
  • 11 x 400w Solar Panels
  • 1 x 3.6kW Inverter
  • 1 x 9.6kWh LiFePO4 Battery

It makes and stores electricity, the battery can be charged from and discharge to the grid.

Panels are West facing in Gloucester, UK.

No "Isolation" from grid. Grid goes offline, we go offline.

  • 8kW (output) Heat Pump
  • 9 new radiators
  • 1 x hot water cylinder

It's an Air Conditioner, running backwards, and dumping heat into either hot water, or hot water ... for radiators. Some new radiators needed as HP works at lower temperatures than gas boiler - lower delta between radiator and air means increased surface area needed to transfer the same heat.

Why?

Would prefer to avoid murdering the planet.

Cost, I guess? Having our own generation insulates us from the cost (a bit) and heat pump is more efficient than gas boiler.

Getting it
  • All from Octopus Energy
  • Solar was fairly easy
  • Heat Pump was more involved / annoying
Checking my privilege

Lucky to be able to do this.

  • We have space!
  • We have time!
  • We own the house!
  • We had money thanks to inheritance from Granddad!
Solar
  • Initial enquiry
  • Phone call to discuss what we want and what they offer
  • Survey
  • Proposal / plan with specifics and costs
  • Book install date
  • Scaffolding! Came a few days before
  • Installation took a week
  • Solar panels on roof - quite noisy having mounting hardware drilled into the rafters!
  • Battery and Inverter in garage - quite noisy having holes drilled in the wall for cables!
  • Needed upgraded smoke alarms - we now have a connected detector in the garage that will alert us if anything goes badly wrong.
  • Scaffolding removal, left a few days after

The panels are only on the West roof, we did get a quote including the East roof but it would have been disproportionately more money for little (although not nothing!) extra generation. It wasn't in budget at the time, but maybe we can add more later.

Total cost of install about £11k.

It was installed in October 2023, so I only have about 5 months experience with this, and all of that was in the "low" season.

Heat Pump
  • Initial enquiry
  • Phone call to discuss
  • Survey
  • Proposal / plan with specifics and costs
  • ! Need "confirmation" from council that we're "allowed" a pump because the Heat Pump is "highway facing" (it's not, there is a wall) this was a pain, no one at the council thinks it's an issue so getting a "confirmation" was hard. Apparently the rules are relaxing.
  • Various issues getting confirmation as no one at the council thinks this needs their permission
  • Eventually get someone from planning to "officially" point me at permitted development
  • Book install (months later!)
  • Pre-install visit to tweak plan
  • Installation took a week and was very invasive
  • HP out front - pretty easy
  • Hot water tank, buffer tank, controller all in garage - we did move them to be more out of the way, which was worth it.
  • 9 out of 11 radiators replaced - this is fairly invasive as basically every room had a radiator replaced.
  • old boiler removed
  • old hot water cylinder removed
  • Floor taken up to route new pipes from garage to existing pipes - this was absolutely worth doing, but quite disruptive during the work and it's ruined the underlay.
  • no heating or hot water for most of the week - it was cold. The canal froze over. Would not recommend.
  • system issue a few days later! Engineer visit.
  • post-install check-up / filter clean 4 weeks later

Total cost £10,500 - but government "Boiler Upgrade Scheme" paid £7,500 of that = total "out of pocket" £3000 (aka the cost of a new boiler.)

The pump was installed in Mid January 2024, at this point we've had it less than 2 months, although they are the coldest months of the year with my garden temperature not getting above 15 and averaging around 10.

(Note: the BUS is only available for heating, if you want a system that can also cool you in summer - that isn't eligible. Or so I'm told. Although good luck cooling with convection heaters (aka "radiators".))

Living With it Heat Pump

Boring.

It just works.

Seriously, I barely think about it.

The only reason I do think about it is because it uses electricity and I'm obsessed with optimising the solar + battery usage.

Rooms are warm, water is hot, it blows freezing air even when the air is already freezing.

We've got the thermostat set to 19°C, but it's by the front door and a bit colder than average, which keeps the whole house a comfortable 20°C ish.

One downside of having the hot water tank in the garage is the hot water is now further away from the taps, so you have to wait a bit longer for it to come through. Previously the tank was in the geographic center of the house so there wasn't much distance to travel for the hot water, but now it has to come from the back of the garage, under the floor of the study, and the landing, before it gets to the starting point of the old tank. However, I'm assured that water costs less than electricity and the reduction in efficiency would be significant enough that this works out the better solution.

Sometimes the hot water runs out, unlike the old gas boiler that would wait for the next scheduled time to make more the heat pump will kick in and get heating. This is good, because we have more hot water, but it's bad if we're running low on stored power and it means we draw expensive power from the grid.

The water is set to the higher of the two default presets, due to the longer run, and we could probably save a bit / improve the efficiency by using the lower setting. Maybe in the summer!

Does it work when it's cold? Yes. Obviously. The week we had it installed the canal froze over. We had the hot water enabled first and we could have hot showers that same day, whilst it was cold enough for the canal to freeze from side to side. We had the house heating turned on in the last afternoon and were back to normal internal temperatures that same evening.

Lifetime COP (Coefficient Of Performance) is 3 (so far), for heating and water. For every 1kWh of electricity put in it has pumped 3kWh of heat. Hot water is less efficient than heating - but the combined total is 3.

I've done the maths and it is costing less to run than a gas boiler.

Looking at a bill from December, we used 443.7kWh of gas, at 6.49p/kWh for a total of £28.81. At 90% efficiency (an A-Rated boiler) that's 399.33kWh of heat output. At COP of 3 the Heat Pump would use 133.11kWh of electricity to do the same. We're paying 17p/kWh (on "Cozy Octopus") or £22.63.

Heck, even if the gas boiler was 100% efficient the heat pumt would cost £25.14 for the same heat output. Which is still less than gas!

Admittedly we did use some gas for cooking, but it will not be £6 worth.

We've also scrapped the gas hob and installed induction. We're completely off the gas grid. Not paying the standing charge will save over £95 a year, and nearly £115 with the new price cap come April. https://www.ofgem.gov.uk/energy-price-cap

Stop Press: The April prices have been announced. Gas will be 6p and "cozy" electric will be 11.676p, which is less than 2 times the unit cost of gas. That same example would be £26.62 as gas and £15.54 with the heat pump.

Not to mention we're now not putting all the combustion by-products of gas into the kitchen when we cook.

And on a sunny afternoon we pay £0. Yes, this does happen, even in February. It's worth a reminder that the Heat Pump and Solar are different systems, we didn't get one to support the other. They do overlap and help each other (well mostly the solar helps the pump!) but running costs etc. should be considered in isolation because yes, solar won't produce enough to run the pump when we most need it in winter.

The financial break even point on this is negative, it's already about the same as buying a new boiler and the running costs are less. Admittedly we didn't NEED a new boiler...

The climate break even point is harder to know, but still encouraging. I asked Octopus sales, and the installation team, and the manufacturer, none can tell me the embedded emissions of manufacturing this system and installing it. HOWEVER, given the worst common situation where most of the electricity on the grid is generated by gas - we're still emitting less CO2 by using that electricity to pump heat rather than burn it for heat. I don't know exact numbers, but the losses in the electric system would have to be massive to counteract a 300% efficiency. Plus, we're with Octopus so (at least in theory) 100% of our electricity is renewable, and when the sun shines it's our own solar being used.

I'm a massive nerd so you might expect me to have connected this to my Home Assistant system, but I haven't. The options are all bad, there is no open source integration because the manufacturer has an NDA (Non-Disclosure Agreement) before you get access to the API, so it's impossible to do an open source anything with it. Plus the app has options for logging in with Facebook ... and fuck that, if you're going to even give that as an option I do not trust you with my home heating.

However, I have got a clamp energy monitor on the power supply so I can at least monitor the power separately from the rest of the house, in Home Assistant.

The control panel in the garage does give some data, for example kWh used, kWh heat generated, etc. which is where the figures here have come from.

Checking my privilege again

Given that 9.6 million households in the UK are living on incomes below the minimum income standard and in poorly insulated homes.

And these households have most to gain from more efficient heating.

But the government grant is only available to homes with a valid energy performance certificate for the property which does not recommend installing loft or cavity wall insulation.

And it's not just heating for comfort.

The Building Research Establishment Group (BRE) has calculated that the very worst housing in England – approximately 720,000 homes with a category one hazard of excess cold (typically insulated to bands F or G) cost the NHS £0.5 billion per year in first-year treatment costs alone.

That's probably more than a million people (assuming more than one person lives in a home, on average) who are so cold they end up needing medical treatment.

We urgently need to insulate Britain and roll out heat pumps.

Solar + Battery

I am a massive nerd and obsess over this ... a lot. I suspect that if I'd lived in the past I'd have been the one obsessing over having enough fire wood, and water, etc. for the village.

There are a few different ways to live with solar and storage, one is to basically ignore it and carry on as normal. This is somewhat pointless as you'll inevitably end up using power at different times to when it's being generated. This is mitigated somewhat by having storage, now you can store power for use later, especially important for things that can't really move about much, like cooking dinner. Some care is needed for this to work - the inverter can only provide 3.6kW, which is plenty to boil the kettle, run the oven, dishwasher, etc. but not at the same time and this is where you can start drawing from the grid for example if you boil the kettle whilst cooking, or the tumble dryer at the same time as the washing machine. I've started to get cleaver with this and monitor the energy draw of the washing machine as once it has heated the water it doesn't use much power, then I can turn the tumble dryer on and run them at the same time. It's risky, there isn't any overhead if someone turns on the toaster!

In some ways this is the least impressive thing because I really enjoy the days when it's overcast, even raining, and we're generating more electricity than the house is using. It's great to sit at my desk with music playing, lights on, computers running, and be doing it all from solar. In the rain. In December.

Most of the time we don't worry much about this, and with some care to make the most of the generated solar and charge when it's cheap, and not overdo things when cooking dinner, it's just like a normal house. But with the nice realisation that you're watching TV at night on the electric you generated that afternoon.

The battery has a 9.6kWh capacity, so enough to run the house for 24 hours, although admittedly not with the heat pump! Although in the summer I expect we'll be able to operate without using the grid at all, we've already done in multiple days in October!

It's really intuitive, if the suns out we're making power. It's even possible to have a sense of exactly how much and if it will last, the brighter it is the more we're making, and the cloudier it is or if the weather is blowing in we'll probably not have it for long. Clouds make a real difference, even small fluffy ones can drop generation from 2.2kW to 400w whilst they pass.

Because the panels are all West facing there is a significant delay in the mornings before we start generating the big watts, today (3rd March 2024) generation started around 7am, and by 10am it was still only making 275w. But then the sun was in a position to illuminate the panels directly and generation shot up to over 2.2kW by 12. It will stay high until late afternoon / evening (weather permitting) until it falls off a cliff when the sun goes behind some tall trees across the road. This is noticeable in winter, but a near vertical drop in summer.

Data and control of Solar + Battery

The battery and inverter are made by GivEnergy, I was made a cloud account for the system when it was installed (as in, the engineer handed me my username and password!) but it also has complete local control via the GivTCP software / Home Assistant integration. As a result I have a lot of data and full control of the system.

I also use Forecast.Solar to get predictions of generation which helps me plan usage and charging, especially when combined with the Unofficial Octopus Energy integration.

I have this data almost constantly visible, I've curated a dashboard on Home Assistant which gives me quite a lot of detail, including power flows to/from the solar, battery, grid, house, and heat pump. Forecast generation, "running totals" of generation, usage, import, export, tariff, cost, grid carbon, "benefit" (saving in money/carbon) etc.

I've got an e-ink display on my desk than updates every minute with key bits (plus other house stuff like temperature) and I've also got some widgets on my phone home screen so I can pretty much always know power going to/from the grid, solar, and battery, plus battery charge level, grid carbon, top grid fuel, and outside temperature.

One of my favourite (but also I have complicated emotions about it) is the "cost per kWh generation". It's a simple "sensor" which divides the total system cost by how many kWh we've generated - so far every single kWh of solar generation has cost £18.82 which is quite a lot! This is a kind of "break-even-o-meter" as once this gets to the same cost per kWh as buying from the grid, we've broken even.

I've also got a bunch of notifications that are sent over Matrix.

This one is sent at the start of "cheap times"

Electric import is about to be £0.168739 Target charge 72% (currently 40%, 4.7 kWh solar left today) and/or forcast generation in the next hour is 1.5 kWh starting at 13:00:00 and ending 16:00:00

At the end of the (solar) day we'll get one like this

☀️ production today 13.5kWh 🔋 battery is 88% charged. 🔮 Forcast generation tomorrow 5.897kWh

At midnight the daily summary will be delivered (the numbers often don't add up, I blame rounding)

Saved £3.13 and 3085.34gCO2 today by using the battery (£2.12 | 8.6kWh | 1577.60gCO2) and solar (£1.30 | 11.0kWh | 1612.31gCO2), plus exported 2.5kWh.

Making Money with the Battery

Yeah, really.

Well, kind of.

For a couple of months we were on Octopus' "Intelligent Flux" tariff which takes control of the battery and charges it overnight when electricity is cheap / carbon is low, and then tops it off during the day using solar or the grid. It then pays extra for your exports during peak time, by about 10p per kWh. With a 9kWh battery you can in theory make 90p per day, just by charging and discharging the battery. In reality the charging algorithm they use isn't great, and it never discharged as much or as fast as I thought it could, and I never managed to get someone to talk to me about it.

There are other tariffs that are "manual" but also work in a similar way.

Exporting usually pays about 15p per kWh, which is a lot less than buying it costs, so it's not usually worth charging and then discharging it later on normal tariffs. However, Saving Sessions / Demand Flexibility are times it can be worth it, especially if you've got some solar in the store. I've got my system set to automatically dump the battery to the grid at full power during these sessions. We don't usually draw from the grid during peak times any more but it still gives us points as negative "use" is still a "reduction" on nothing!

We're now on the "cozy" tariff which is specifically for Heat Pump users, it charges the normal amount most of the day, with two 3 hour periods of 40% cheaper power for running the heat hump. BUT we also charge the battery in this time, which means we're saving 40% all day long. This is especially useful because there is also a peak period which costs more, during the early evening. Cooking off battery is saving us paying 45p per kWh during that time! My rudimentary sensor in Home Assistant says we've saved about £35 per month by charging when cheap and running off battery the rest of the day, although admittedly some of that power was from the solar - not much though as this was late January to late February. During the same period we saved nearly £19 by using solar generation straight into the house.

We could possibly save more by using the "Agile" tariff, which changes per half hour. But I've not done anything to work out for sure.

Other suppliers have tariffs that vary by time of day and/or grid capacity so I'm pretty sure most people could save money with just a battery. Even the old Economy 7 tariffs would work here as you could charge the battery up over night and reliably have power during the day.

Stop Press: The new prices for April have been announced and we'll be paid more to export (15p) than import during the "cozy" times (11.676p) so this completely changes the calculations on what's "worth" exporting (everything, all the time.)

Return / Break Even

The financial return on this system depends a lot on how we use it. During planning the prediction was we would generate about 53% of our yearly usage, which doesn't sound great. But cutting an annual bill in half doesn't sound like a bad idea! Our average usage in February was 17.4kWh per day (including the heat pump), and the estimated generation in summer is 17kWh so I suspect we'll be exporting quite a lot too.

17.4kWh per day for a year is 4623.8, at our cozy rate we're looking at a worst case total year cost of £786 (excluding standing charge.)

The design pack predicts 3627kWh generation per year, which is 78.4% of that figure. Or about £616 of electricity we're not paying for every year.

On average days we use up to 10kWh excluding the heat pump, meaning we could be exporting 7kWh a day in summer, netting an amazing £1.19 per day! That's £71 over 60 days of peak generation!

One issue is that peak production is during summer and peak use is winter, BUT Octopus pay us a fixed 15p / kWh, and we pay 17p / kWh on the cozy times, so on average, we're paying 2p / kWh.

Design pack predicts a break even point of 6 years 6 months. But we'll have to see how this adjusts based on our real world usage.

The battery has a guaranteed 10 year life, and the solar panels are predicted to last 25 years before performance degrades below 80%. Even then I expect the battery will keep working, and could be replaced and/or upgraded at much reduced cost in 10 years, and even 80% efficiency isn't that bad. Point being, all of this is way past that 6.5 year prediction.

But we didn't do this for the money, although it is nice to have lower and more predictable expenses.

Like the Heat Pump, it's impossible to know the embedded environmental cost of the system. The panels were made in China and no one knows anything about this process. We even had a visit from Octopus France who are starting to supply solar soon and wanted to see how it was being done here, and I was being told that in France they have a legal requirement to know the whole like carbon cost, and the Chinese manufacturing is causing problems because no one knows! Then there is the issues of lithium cells and the significant issues of the mining of the raw materials used in them. All I can really do here is trust that Octopus have done the work and the entire supply chain is slavery free, low carbon etc. GivEnergy claim the battery is "Ethically sourced and cobalt-free." I did ask, and it's the same story as the Heat Pump, no one knows.

In the last week (in February 2024) we've avoided 4kg of CO2 emissions by using solar directly rather than from the grid. Or 7.2 seconds of Private Jet flight. (See my post on High Draw and Low Carbon Automation for the details of that maths.) At time of writing we've generated 600kWh, which would have prevented 145kg of CO2 emissions at the current generation cost of 234gCO2/kWh. It's not much, but it's part of my rebellion against extinction.

Checking my privilege again again

There was zero chance of my affording this system on my own. We could only do it due to inheritance from my Granddad. I think he's be fascinated by the technology (he was always keen to show me his new computer, laptop, gadget, etc.) and the knowledge that he's helping provide our power - just like he did when he delivered coal back when he was younger than I am now.

I'm also very aware that I live in and own a house we can modify like this, that has a roof suitable, and space in the garage. We're (disappointingly, everyone deserves somewhere affordable and safe to live) very fortunate to be in this position.

Optimising Battery Charge

We're in the most difficult period of the year for optimising this (writing in February/March), as the solar generation isn't very predictable, but it also isn't nothing.

The charging is a bit stupid, if the battery is in "eco" mode any solar we don't use immediately in the house goes to the battery, and if it's full, the spare goes to the grid. But if it's in charge mode it draws from the solar AND the grid, up to a set target. Once it reaches the target it will stop adding more to the battery, even if there is capacity and the solar is generating.

SO, I want to maximise the solar we capture, but also not end up running from the grid when it's expensive. I want to set that target to shove as much solar as possible in there, and top it off with cheap grid, even though we often have to grid charge first. But doesn't put so much grid power in there we run out of storage and send some back to the grid.

This is my current "algorithm" that runs at 59 minutes past every hour.

[100  - (float(states.sensor.energy_production_today_remaining.state) / 9.6 * 90)
      + (float(states.sensor.energy_next_hour.state) / 9.6 * 100)
      | round,
      20] | max

What?

  • Start with 100%, a fully charged battery
  • take away the forecast solar production left today, but only 90% of it, we're pessimistic
  • add back the forecast solar production in the next hour, we want that going into the battery not the grid.
  • round that to a whole number
  • pick whichever number is largest, the calculated value or 20

In tandem I have a script which looks at the next period, and if the cost of import is low (below 20p) it will schedule the charge at that cheap time. But it also checks if the current charge is lower than the target, because if it is we don't want to enable "charging" because the solar will go into the grid not the battery. Which is pointless. But it also checks the forecast generation for the hour, if that's very low or nothing we'll still enable "charging" even if it means we just run from the grid for that time, because any high loads (i.e. the heat pump) will quickly drain the battery leaving us with nothing for when the price goes back up.

It's not perfect and I suspect that this is a situation where some sort of self-adjusting model would work well, especially if it can also consider the average or likely demand for power (is it cold? The we'll need heat.) and even the forecast carbon intensity of the grid (can we charge later in the cheap period and save some CO2 emissions?) but I have no idea where to start with this.

Stop Press: With the April prices it's going to be pointless to do any of this, I may as well stuff the battery from the grid during "cozy" time and export everything I generate, getting "paid" 3.324p for the difference.

Energy isn't Fungible

Like money or monkeys, we've dealt with electricity for a long time like it hasn't mattered where it comes from or what it does. But it does. 1kWh from coal isn't the same as 1kWh from wind. And it doesn't matter how green the generation is if the 1kWh is used to mine crypto when it could have been used to heat someone's home.

Ultimately I think we have the tools to mitigate many climate issues, and now I've got a bunch of them in my house. Heat Pumps are more efficient, they cost less to run and emit much less / or no / carbon. Batteries enable us to flatten the curve on demand, meaning we can minimise the most polluting generation and store the least polluting for later.

We need a significant shift in attitudes from everyone, especially politicians, to make this change. But my house shows we can do it.

https://m0yng.uk/2024/03/Solar-Panels-and-Battery-and-Heat-Pump-Info-Dump/
Beepy charge mods for infinite battery life

Well, effectively infinite anyway.

I <3 my Beepy and use it a lot, something which is hindered by the fact that the stock charging current is limited to 100mA due to a 10k resistor on the TP4054 charging IC.

So, I ordered a bunch of tiny resistors and got the soldering iron out.

Increasing charge current

The charge IC is limited to 500mA so although the 2000mAh cell could in theory take a full 2A of charge current, we're going to stick with around 500mA for now.

You will need:

  • A working Beepy
  • A 2k 0603 surface mount resistor
  • Soldering iron
  • Flux, solder, etc.

The resistor to change is R4 just by the charge controller.

A tiny resistor highlighted with a pink ring, it's on the positive side of the battery connector above a 5 pin component and is marked 103

Simply (ha!) add some flux, heat the pads, and slide the component off with a little force - not too much, we don't want to damage the pads.

Clean it up however you want, if you want, and maybe add some fresh solder and flux ready for the new resistor.

Use tweezers to position the new resistor and melt the solder on one end, then the other, hopefully it lines itself up with the pad and you're done. Just clean up the flux with some IPA.

A tiny resistor marked 308

It was at this point I took a photo and noticed the resistor I'd got from a multipack wasn't actually the right value and I didn't have a 2k resistor in the set! I fudged it with the closest value I had to hand and repeated the process. No photos, it wasn't as pretty the second time!

EDIT: I've been told that it is actually a 30B resistor which IS 2k at 1%, but I've never worked with components like this before and that's my excuse. Now you hopefully won't make the same mistake. (p.s. this edit made on the beepy!)

Beepy in a bright pink case connected via USB to a power monitor reading 0.5A

It works!

"Infinite" battery life

Ok, great, now we can charge it faster than it discharges itself at idle, pretty good.

But, what if, Qi?

Qi is the name of the standard most of us know as "wireless charging" and you can buy bare bones receiving coils and circuits quite cheaply, and small enough to hide in the Beepy.

I ordered a 1cm by 1.5cm circuit and coil for £6.65 - it can deliver 0.6A and charge the Beepy fine when it's turned off, but struggled and stops working after a few minutes when the Beepy is running so you may want one that can do around 1A. If they exist.

Back of the Beepy PCB facing towards the USB C connector. Arrows point to small wires soldered to the far right pin of the USB C port which is ground and the bottom of a resistor which is 5v

Solder the ground wire to any ground pin, I used the end pin of the USB C Port.

Solder the 5v wire to the top of Diode 1 (D1) this has a direct connection to the USB C port's 5v line so DO NOT CONNECT Qi AND USB AT THE SAME TIME.

That's basically it, I wrapped the Qi PCB in heat shrink (yes, I had to unsolder it because I forgot!) and tucked it all into the case.

The coil is placed on the battery, which probably isn't ideal due to heat, but there isn't much space and you need the coil in the middle so when you naturally place the Beepy down on the charger it is in the right place. You WILL need the little black pad behind the coil for it to work (in my experiments.) Maybe a suitable case could place the battery lower down and have a slot of the coil in the middle?

Now you have basically infinite battery life! Just pop the Beepy down on the charger when not using it and it will charge, the pick it up to use without having to faff with wires.

Beepy in a bright pink case on a Qi charging thing with a power monitor reading 0.78A

(This reads more than over USB due to the inherent inefficiencies of Qi charging.)

https://m0yng.uk/2023/09/Beepy-charge-mods-for-infinite-battery-life/
High draw low carbon automation with Home Assistant

I'll admit this is a bad a title, but there isn't a snappy way to describe this.

Why do?

Not all electricity is created equal

Maybe you've never thought about this, but not all electricity is created the same way, and every method has a different environmental impact. Plus the way the electricity delivered to your home is generated changes throughout the day, week, month, season, and over years.

For example, as I write this, Scotland is generating 68% of its electricity from Hydro, 11.4% from wind, 10% nuclear, and the rest is imported - a mixture that emits 21 gCO₂ equivalent per kWh of electricity.

In contrast, South Wales is generating 96.9% by gas and 3.2% by wind, emitting 381 gCO₂eq/kWh.

(You can see the real time info for loads of countries at Electricity Maps)

So, if I have a dish washer to run or a load of washing to do right now, it would be best to do that using the Scottish electricity and not the South Wales stuff.

But you can't just plug into different bits of the grid at different times, but you can wait until the local generation is producing as little CO₂eq as possible.

Think of this as "flattening the curve" (remember that from Covid?) we aren't trying to use less electricity, but we are trying to use it when it's most abundant so we don't need to use costly methods of generation. The National Grid experimented with this last winter, paying people for every kWh they didn't use during peak times (compared to that home's average usage at that time of day) when the grid was at risk of having to bring coal plants online.

How do?

There are some problems to solve here:

  1. How do I know how much CO₂eq is being emitted by the grid right now?
  2. How do I know how much is likely to be emitted in the future, so I can decide if I should wait or do it now?
  3. How do I communicate that information?
1 - CO₂eq NOW

This is fairly easy, Carbon Intensity API exists and will give you this info for the current 30 minute chunk and it super easy to use, you can just give it a postcode!

For example, pop over to https://api.carbonintensity.org.uk/regional/postcode/RG10 and you'll get a JSON blob with the CO₂eq value, an "index" from "Very Low" to "Very High" and even a breakdown of what generation sources are in use.

(Electricity Maps also have an API if you're not in the UK, it's not as specific as the National Grid data, and you need a free API key - it's called CO2 Signal)

I have a python script which is run by cron to grab this data and shove it into MQTT where Home Assistant does stuff with it - more on that later.

'''
Gets the current Carbon Intensity data for the region
'''
import requests
import configparser
import paho.mqtt.publish as publish
# load the config
config = configparser.ConfigParser()
config.read('config.ini')
# we need this for every request to the API
headers = {
  'Accept': 'application/json'
}
# a request for the current carbon data
r = requests.get(f"{config['ngci']['baseURL']}regionid/{config['ngci']['regionid']}", headers = headers)
# convert that to something we can use
currentData = r.json()
# print out the data
print(currentData)
# the "messages" we're going to send to MQTT (aka the data from the API)
messages = [
# basic intensity info
        {
            'topic': f"{config['mqtt']['topic']}current/gco2",
            'payload': currentData['data'][0]['data'][0]['intensity']['forecast']
        },
        {
            'topic': f"{config['mqtt']['topic']}current/index",
            'payload': currentData['data'][0]['data'][0]['intensity']['index']
        }
]
# add a message for each of the generation types
for fuel in currentData['data'][0]['data'][0]['generationmix']:
    messages.append({
            'topic': f"{config['mqtt']['topic']}current/{fuel['fuel']}",
            'payload': fuel['perc']
            })
# show me the messages!
print(messages)

# Send the messages off to MQTT
publish.multiple(messages, hostname=config['mqtt']['server'], auth={'username': config['mqtt']['username'], 'password': config['mqtt']['password']})
2 - Future

OK, great, but what if it's currently "Medium" intensity, do I run the dishwasher now because that's as good as it will get, or wait because it's going to get lower later?

It's important for the National Grid to predict demand so they can match it with supply, and to do that they need to be able to predict supply too so there is some pretty good data for this.

Most renewables can be forecast pretty accurately, wind will use weather forecast data for the wind, solar will use a mix of weather, time of day, time of year, etc. and the same API also has endpoints for predicted CO₂eq per 30 minute chunk for the next 24+ hours.

I have another python script which checks this API each morning and sends the data via MQTT to Home Assistant, which sends it on to me via a Matrix room.

'''
Gets the forecasted Carbon Intensity data for the next 24 hours
'''
import configparser
import requests
from datetime import date
from datetime import datetime
from dateutil import tz
import paho.mqtt.publish as publish

config = configparser.ConfigParser()
config.read("config.ini")

to_zone = tz.gettz('Europe/London')

forcastAPI = f'{config["ngci"]["baseURL"]}intensity/{datetime.utcnow().replace(microsecond=0).isoformat()}Z/fw24h/regionid/{config["ngci"]["regionid"]}'

lastHalfIndex = ''
combinedText = ''

r = requests.get(forcastAPI)
for halfhour in r.json()['data']['data']:
    if lastHalfIndex != halfhour['intensity']['index']:
        dtUTC = datetime.strptime(halfhour['from'][:-1], '%Y-%m-%dT%H:%M')
        combinedText += halfhour['intensity']['index'] + ' from ' + dtUTC.astimezone(to_zone).strftime('%H:%M') + '\n'
        lastHalfIndex = halfhour['intensity']['index']

print(combinedText)

publish.single(f"{config['mqtt']['topic']}forecast", payload=combinedText, hostname=config['mqtt']['server'], auth={'username':config['mqtt']['username'], 'password':config['mqtt']['password']})
3 - Communicate

There are two main ways I communicate this, a bot that sits in a Matrix room, and switches attached to the "high draw" machines I want use at different times.

Matrix is triggered by Home Assistant automations looking for changes to those MQTT topics, if the forecast changes, it tells me, if the index goes High or Low it tells me that too. I'm sure there are many ways you can tell yourself stuff and you'll probably already have a favourite.

Each "high draw" machine (dish washer, clothes washing machine, tumble dryer) has an energy monitoring connected switch attached. These are all dumb appliances and I want to keep it that way.

I'm using these tasmota energy monitor switches from Local Bytes as they came pre-flashed, from the UK, and I had already destroyed an identical looking and more expensive device trying to flash ESPHome onto it before.

The main way these switches communicate is by turning the devices on, or off, depending on the index value. The basic logic is simple.

IF index is High or Very High:
    turn switches off
ELSE IF index is Low or Very Low:
    turn switches on
You'll notice there is no consideration for "Medium" intensity, I don't feel this is a "bad enough" or "good enough" index to cause a change, and often the index will go back to what it was before anyway.

This makes the logic slightly more complex because we can't just slam it on or off regardless, so I made a "helper toggle" in Home Assistant called "High Draw Active" and the logic checks it before doing anything, this also prevents it turning off switches that are already off if the index goes from High to Very High.

IF index is High or Very High:
    and "High Draw" is active:
        turn switches off
        turn "High Draw" off
        tell me it was turned off
ELSE IF index is Low or Very Low:
    and "High Draw" is off:
        turn switches on
        turn "High Draw" on
        tell me it was turned on

I could probably break this out into three bits that all listen to or change the "high draw" toggle, but I haven't felt the need ... yet.

It also checks to see if a machine is running before turning it off. I have some "Dropdown" helpers that store the state of each machine (running, finished) and the energy monitoring switches allow me to detect when the machine is running, and when it has finished (and notify me via matrix) Obviously I don't want to turn off a machine that is running so the automation won't do that, but it will tell me that it hasn't turned it off because it is running.

What do?

It has adjusted our habits, more than I thought it might.

The motivation for, and implementation of, this is supported by other people in the house and it has a good "spouse approval rating" - which is helped by the fact that all switches can be overriden if needed.

What impact does it have though?

Some numbers

My dishwasher seems to use around 1kWh per cycle, which is helpful for doing maths.

Since I installed my monitoring sockets the dishwasher has used about 50kWh, so if we use the numbers from before the emissions would be:

  • 19,050 gCO₂eq for 381g in South Wales
  • 1,050 gCO₂eq for 21g in Scotland

That's an 18kg difference in emissions.

Now, I'll admit that I don't have actual numbers for my real usage, in theory I could work them out but I haven't got my head around the maths for that yet (I have dyscalculia) but I think it makes the point!

Completely Pointless

However, this exercise is completely pointless, depending on how you look at it, because:

  • I'm with Octopus Energy so all my electricity is renewable anyway
  • I don't pay anything different for using low carbon electricity
Completely and utterly pointless

Seriously, it's completely and utterly pointless.

The average private jet emits two tonnes of carbon an hour Flying shame: the scandalous rise of private jets - The Guardian

Therefore that 18kg hypothetical difference is 54 seconds of a private jet.

I'd have to run the dish washer 5,249 times at 381gCO₂eq/kWh to make up 1 hour of private jet use.

If I didn't care and only used the dish washer when the grid is incredibly carbon intensive, and did it every day, at 381gCO₂eq/kWh, it would take 14 years and 4 months and 17 days to emit as much CO₂eq as one hour of private jet usage.

It's the billionaires, there is no way I can make even the smallest scratch in the tiniest dent on the climate crisis when billionaires are emitting a million times more greenhouse gases than the average person - we cannot capitalism our way out of this.

What's next?

We're in the process of getting solar panels and battery storage.

I expect I'll swap the triggers for this to be when the panels are generating, at least in the summer.

Another aspect is cost, if we go on an "economy 7" tariff we get 7 hours of cheaper electricity overnight, which we could use to charge up the battery (especially useful during winter) and run things like the dish washer.

Maybe there will be three triggers to balance, solar generation, electricity cost, and the emissions of the grid.

I'd like to make a "smart carbon meter", like the smart energy meters that show you how much electric/gas you've used and give a budget for the day/week/month it might be interesting to set a "carbon budget" and have a display that shows the emissions, the budget, and the status of the switches and the grid generation intensity. In theory I have all the data, and the hardware (a Lilygo T-Display) but I lack the time/skills to get this working.

https://m0yng.uk/2023/08/High-draw-low-carbon-automation-with-Home-Assistant/
TPU with the Anycubic Vyper

The Anycubic Vyper is a pretty good 3d printer, in my experience of only ever using two 3d printers. It has a bowden tube, which means the extruder (that pushes the filament into the hot end) is a good 30+ cm away from the hot end so, can it print using TPU, a flexible and soft plastic?

YES!

Vyper mid TPU print, with pink filament dropping slightly as it heads into the extruder

Go slow

This seems to be key, print slow, very slow. I'm finding 20mm/s works well (20 being the top speed, the outside is going at just 10mm/s)

I'm using Overture TPU which says it's suitable for "direct extrusion" - oops! The "shore hardness" is apparently 95A which is very flexible, maybe you can go faster with a more firm filament.

I'm mostly using the Cura preset "Generic TPU 95A" with 0.2mm layer height, 2 walls, 4 bottom and top layers, zig-zag pattern, 228 hot end, heated bed off, part cooling fan at 100%, retraction enabled, and a skirt.

I also find the experimental / hidden settings of "Infill travel optimisation" and a "combing mode" of "Not on Outer Surface" help avoid stringing from nozzle oozing as it travels between areas - because these both reduce or hide that travel.

With these setting the tyres for this tractor model have basically no stringing in the middle and only the odd hair on the outside.

A stack of four pink tractor tyres

The cali dragon doesn't do quite as well, but to be fair it is designed to highlight issues like this! I find a gentle heat from a lighter or hot air gun can tidy up the strings pretty well.

A small pink dragon with some blobby bits of filament on the horns and some blackened bits where the flame got too close

Size can also help, I find larger models print much cleaner, possibly because the layers have time to cool. For example this owl model prints perfectly until the top of the head where holes usually appear.

The number one tip for quality 3d printing seems to be "keep the filament dry" and this is even more the case with TPU. It is stupidly hygroscopic and will start spitting and causing issues really quickly when exposed to the air. An initial dry in the filament dryer helped a lot and acted really quickly, I've been keeping mine in the filament dryer box since with some silica gel packets (and no heat) and it seems to be keeping dry enough.

Clogs

Clogs seem to be different with TPU, and much more common. With a hard filament if the hot end gets clogged the force get's passed to the extruder which will slip on the filament, making clicking sounds and breaking off bits of the filament where the teeth have bit in then slipped. I've only had one with PLA so far, and that was due to running a bunch of prints at a temperature that was too cool for the filament.

TPU doesn't transfer the force and there will be no audible warning of a clog. The things to look for are under extrusion (obviously, but harder to spot) and corkscrewing of the filament in the bowden tube near the extruder. Because the extruder will keep pushing and the filament has nowhere to go it will start twisting around and bunching up inside the tube, looking like a corkscrew. This seems a good reason to use a clear bowden tube to me!

Failed owl print due to a clog, the deposited filament becomes less and less dense with increasing voids especially visible in the infill

Luckily I've found clogs to be easy to clear, prodding the nozzle with the thing poking thing the printer came with will spray molten plastic onto your hand and free up the backlogged filament to ooze out into a blob on the build plate.

Squishy Models

The tractor tyres are a little bit squashy, and a clog let me feel them only partly printed, and I liked the feel!

The owl prints great at 10% grid infill and is super squishy!

It seems to always end up with holes in the top though, and these can be good as it lets air in and out when you squash - giving a very satisfying feel. But I like to rub a 300 degree soldering iron over the holes (maybe with some spare oozed filament) to "weld" them shut which gives more resistance to a squeeze and a more robust feel. Although finding and welding all the holes seems impossible 🤷

A pink owl with welded patched on the head, although they aren't obvious unless you're looking for them

https://m0yng.uk/2023/07/TPU-with-the-Anycubic-Vyper/
Mycle Charge Fat Tyre Folding Electric Bike

I've had this bike for a few months and covered around 160km (100 miles) so thought I'd share some thoughts.

What is it?

It's a bike of many features! I won't copy the entire sales pitch but the things that sold it to me are:

  • It's electric
  • It has good range - claimed 65km!
  • Fat tyres for various terrain and bad roads
  • Folding

It retails for £1,599 but I got it on the Cycle To Work scheme, which means it costs me less and I pay over 12 months, but I don't actually OWN it until I buy it outright after 12 months OR extend the "hire" for 5 years at no cost.

What's it like to ride?

It's very different to a normal bike, which comes from a combination of factors.

It's HEAVY at 26kg.

It has fat tyres, which give you confidence on rough or loose ground, but also feel weird and are noisy on the flat road, I don't feel like I can lean into corners as much.

The wheels are 20 inches, which means it can go on a train without booking when folded (in the UK.) small wheels usually mean you can speed up and change direction easier, as they have less mass and less inertia, BUT these wheels are fat and heavy, which undoes all of that I think.

Front tyre next to an Anytone 878 radio, the tyre is nearly twice as wide

I feel like I need to have it on the first level of power assist all the time just to overcome the rolling resistance of the fat tyres and heavy bike. It does feel like you're constantly fighting the motor at this level and it's hard to get the bike going faster by peddling alone. Generally I ride with level two assist, it is a good speed for shared pedestrian and cycle paths, and I can push it faster if I want most of the time.

Level three assist is the "sod it" mode (2 is "Let's go already"), it easily keeps up to the legal limit of 25km/h even up reasonable hills. I have taken it up a 1 in 4 ony then did it fail to get me up there, even with me peddling in first gear.

At the top end of speed, the fat tyres increase rolling resistance, the small wheels limit top speed, and the assist is limited to 25km/h, so it has a very obvious limit to how fast you can go. Much more than 30km/h on the flat is very hard work.

To reiterate the "very different" feel, the first ride I went on, I wiped out and dislocated my shoulder. I think the combination of weight, disk breaks (which were new to me), and suspension (which moved the weight around in a way I wasn't used to) conspired against me and it just went from under me. I'm still recovering from that accident, and would not recommend dislocating your shoulder.

On a train?

YES!

Well, mostly.

On the UK you can take a folding bicycle with maximum 20" wheels on a train without booking.

Mycle says it folds down in 10 seconds, which is not true.

It also weighs 26kg, and you probably have a bag or two as well.

26kg is a lot to heft on and off a train, from the platform, and through a door, whilst also holding a bag or two. There is no obvious place to hold the bike when doing this, and it doesn't clip shut or anything so you have to balance it such that it doesn't swing unfolded again.

When folded it isn't that small either, I could only just fit it into the bottom of a large luggage cubby - that's a lie, it stuck out. So no, you can't really use it like a "normal" folding bicycle. Mycle say it's great for "folding into your car on weekend trips away" ... and that might work if you don't also have bags or anyone else wanting to bring their bike...

Folded bike taking up the whole of a car boot

But, bicycle reservations are free! On most journeys so far I've (tried to) book a cycle reservation. There are two types of on train cycle space I've encountered so far.

One is in the rear of the train and the staff have to unlock it for you, inside there are maybe four of five places where you can wedge the front wheel of the bike. But not with this! The fat tyres are too big to fit, so you have to improvise, put the stand down, and hope it doesn't fall over. It always falls over.

The other is in the main train, and you're supposed to store the bike vertically and hang the front wheel from the hook provided. WHich this bike also can't use. So you end up with it sticking out of the designated space. To be fair, the spaces aren't that well designed and others have commented on how they must have been designed by someone who has never actually taken a bike on a train.

Bike that doesn't qute fit in the train cubby

Utility cycling

I think this is where the bike shines, it can do a lot of different stuff and can replace a car for most things.

The rack can carry 25kg, which is a fair bit of shopping in pannier bags, or a child in a seat. It's not as good as an actual cargo bike, but it can do a lot of common journeys easily.

On that note, rack mounted child seats should absolutely be the normal thing! The ones that attach to the seat tube are a right faff, wobble all over, and generally suck. The ones that mount onto the rack are really sturdy, really simple to fit and swap between bikes, and are better in every way. And yes, you can fit a child seat like this to a 20" bike, don't let the people in the shop say otherwise.

Bike with child seat fitted and child in it, their face is covered with a grinning Alex emoji

I also have a trailer that can carry 30kg and has a larger physical space than just bags. With this you can do loads! A week of shopping? No problem. A bunch of stuff (including an old single mattress) for the local municipal recycling center (aka Tip)? Can do.

Bike with trailer attached, a rolled mattress is strapped to the top of the trailer

Does it do 65km? Maybe? I've done nearly 20km with little regard for looking after the battery and was still showing 2 of 3 lights on the battery itself.

Things it lacks

When I ordered the bike the only option for the control was a basic unit with some LEDs on it, 3 indicate the boost level, and some others show the battery level. It has an on button and buttons to reduce and increase boost, hold up and it turns the lights on or off. That's it.

You can now get a fancy display for £100 extra, which does ... something? Mycle don't really say. But it would be great to have a display showing speed, distance, etc. I have a wireless CAT "computer" that does this, but I have to remember to turn it on, it has different batteries, and doesn't work reliably with the bikes electrics.

It would be great if there was a way to interface with the bikes data, I assume it has some, in an open way.

The battery also uses a 3-pin XLR connector for reasons I don't understand at all. That's a connector used for microphones and lights. It makes absolutely no sense to me to use a connector like this for that.

It's clear that all the stuff in this bike is just off the shelf components from China, combined to make this specific bike. The instructions for the charger are clearly white-label (they don't mention Mycle at all) and the controller says Mycle on it, but the manual also doesn't mention Mycle.

The controller is apparently made by lsdzs, but I can't find it on their website. It says it is:

  • 36V
  • 17A max
  • 8A rated current
  • 30V low voltage protection
  • Throttle adjustment voltage between 1.2 and 4.4V

Close photo of the controller which is a metal rectangle with diagonal details and a damaged sticker

Should you get one?

It depends?

If you're ok with the compromise (and there are a few) and it suits your needs, maybe!

However, if you need something for the train, maybe get a bike more focussed on small folding. If you want to carry stuff, get a cargo. If you want to ride around town, get a traditional (electric) bike. If it's going off road, maybe a mountain or trail bike. BUT if you want something that can do most of these things fairly well, then the charge might be good for you.

Overall, I'm pretty happy with it!

https://m0yng.uk/2022/12/Mycle-Charge-Fat-Tyre-Folding-Electric-Bike/
December update on TwitterMigration

It's been around a month since my "Thoughts on the TwitterMigration" post and things seem to have calmed down again, so I thought I'd take some time to pull together some thoughts and numbers for you all.

Previously ...

After 4 years of fairly steady increase in users mastodon.radio got an influx.

We went from less than 100 new users per month to 111 joining in the final 4 days of October, or 28 per day.

I wasn't enjoying the constant barrage of notifications and requests.

Some numbers

In November 2022 mastodon.radio added 1071 new accounts. That's more than double the total accounts that joined in the entire previous 4 years. It's 34.5 per day.

Impressively 64% of those new accounts have logged in during the first 10 days of December, which is pretty impressive "retention" considering we're early in the month.

We now have nearly 1,700 "Active users".

New users create a lot of email, before we'd comfortably sent fewer than 1000 emails each month, in November we sent nearly 14,000

Also on the backend is Sidekiq, it does all the fetching and sharing of posts, likes, boosts, etc. and it took some tweaking to get working well under the new load.

For the previous months Sidekiq processed around 50-60k jobs per day, now it hovers between 750k and 1.2 million jobs per day.

We've also seen a HUGE increase in donations. I have nearly £1,500 in an account ready to pay for all those emails and server upgrades.

Server upgrade

This was inevitable, even if we hadn't had any more users there is now a lot more traffic on the network and sidekiq is busy, so we needed a server upgrade to keep up.

Mythic Beasts have hosted us from the start and when I emailed to ask what they could offer I got a pretty quick reply and a very good offer. We were basically able to double the server power (now we have 4 CPU cores!) and pay only a little bit more.

Some quick maths shows it costs just under 20p a year per user to pay for the server and domain and a modest amount of email.

Process updates

There were a few things that made the initial influx very hard work. For one I personally welcome every new user to the server, and approve all the accounts.

I made some changes to the way I do this, the main one being to make our mascot, Alex, send the welcome messages. This freed up my personal timeline and mentions to be usable and not just a flood of "welcome to mastodon.radio!" messages. I also made some changes to the script to streamline the actual process of approving the accounts.

This helped a lot, I still get emails when a new account is requested, but I'm much more lax about dealing with them now and don't jump on approving every one as soon as I can.

I've also turned off a lot of notifications, so I don't get pings for every boost or like etc. which makes the experience of not using mastodon.radio (you know, the time I'm NOT on the laptop or phone) much more pleasant.

More instances

One of the big changes was a bunch of other instances starting up, including mastodon.hams.social, qth.social, and hackers.radio. I hear rumours of national radio associations also looking to set up instances for their members.

This is great as is spreads the community our and stops mastodon.radio being THE mastodon server for radio people.

Behind the scenes we've got a good alliance of admins who talk about issues, moderation, etc. which is very helpful.

We've also got a relay set up which ensures posts from mastodon.radio get to hackers.radio, and vice versa, without everyone having to follow everyone on every server. It also helps posts with hashtags be reliably "findable" across the servers connected to the relay.

The relay server also runs LibreTranslate so we have some basic machine translation of toots.

You can find a list of radio related servers, where they are located, and if they are on the relay at fediverse.radio

How I'm feeling now

Much more comfortable, if mastodon.radio collapses in flames then so what? It's social media not a bank. I'm confident I could stand it back up again, even if we didn't keep all the data for whatever reason (the daily database backup files are now 2.5G gzipped) plus there are other servers that people could jump to if they REALLY can't not have mastodon for a bit.

It's really good to see new instances coming online and sharing the ethos.

It's still just me running the server, which isn't ideal, but it's absolutely not just me running amateur radio fediverse servers.

https://m0yng.uk/2022/12/December-update-on-TwitterMigration/
Thoughts on the TwitterMigration

It's been about a week since "The Bird Site" was purchased, lots has been written about that, and lots has been written about Mastodon, but I've not spotted much about the impact of the influx on servers like mastodon.radio, so here are some thoughts. A ramble, so beware.

Brief History

Mastodon.radio started in 2018, for a long time it ran on a single core VPS with 4Gb ram. Then when an influx happened in the past I finally upgraded it to the current specification, which is 2 CPUs, 8Gb ram, and 160Gb storage.

For a long time I've had it set to require approval for new accounts, this is mostly to stop spam and people who aren't actually wanting a radio amateur focused server (maybe they are a DJ, etc.), and it works fairly well. A week ago we had 807 accounts, and around half were active, the statistics say we have 44% of our users still using the site after 6 months (on average) which is pretty good!

For the most part we got a handful of new users each week, and the community was pretty stable. I follow everyone and could keep up with the local timeline easily.

Chart showing a couple of peaks in 2020 and 2021 but 2022 is significantly higher overall

When I was clearing out my emails at the start of October I grabbed this chart from Thunderbird showing the number of emails the server sent me about new accounts per month. You can see it's mostly stable with a couple of spikes, and a large increase in 2022 when Smitty mentioned the server on the Ham Radio Workbench Podcast - also around the time Musk was talking of buying the bird site. Then it trails off back to the usual lower level by October - presumably as people catch up with the podcast!

The specific numbers are:

  • May 109
  • June 65
  • July 64
  • August 42
  • September 27
The Eternal September of October 2022

It's a little tricky to get the maths right, but I think Musk paid approximately $111.11 per monthly active user on the bird site.

Apparently people didn't like this.

In October we had 138 new users.

Reader - I hated it. It has completely ruined my relationship with mastodon.radio.

It used to be a cozy space where I could read about cool things people were doing, see fluffy pets, I could read EVERYTHING - I could refresh my home timeline and there be no new posts.

But then people wanted accounts. If we do some quick maths and assume we would have had as many people join in October as September without the bird situation and compare that to what we had:

138 - 27 = 111 extra new users.

Over four days.

That's nearly 28 per day, on average, and there are only 24 hours in a day.

Remember that I get an email every time I need to approve an account. Even though I've streamlined the approval process quite a lot with my "mastoWelcomer" script I still have to:

  1. get a notification
  2. notice if it's an account request
  3. dismiss the notification
  4. open up my ssh client/app
  5. connect to my home server
  6. go up to the last command
  7. run the last command
  8. wait for it to load in the account info
  9. scroll back and read the username, reason, check the callsign, etc. and make a decision
  10. type the number of the account(s) to approve
  11. confirm I want to approve those accounts
  12. close the app
  13. go to mastodon and check it worked
  14. refresh the local timeline every so often to see if they have posted an introduction
  15. boost their introduction
  16. notice, and dismiss, people liking/commenting/etc. on the welcome post

By which point I'd sometimes have had another two or three account requests come in!

After a few days I'd stopped trying to approve accounts as fast as possible and I'm now only checking and doing it if I'm either already at the computer or every few hours, e.g. morning, lunch, before dinner, again in the evening, before bed. I'm still getting around 10 accounts whilst I sleep.

I've also modified my "welcomer" script to only show me accounts that have confirmed their email address. There is no point approving an account if they won't know its been done. Some people confirm really quickly, some hours or days later, some seem to enter invalid addresses that I then try and track down a correction for if it seems easy, e.g. they have a callsign I can look up elsewhere.

I've always introduced everyone to the server, and although I've automated it a bit, I still actively care about and welcome every new users. This has however made my account somewhat useless for me recently, as I have over 200 direct messages and quite a lot of "please welcome" public posts. I can't even find messages I posted myself a few days ago. I'm sure many people who follow me have probably stopped or muted me by now!

The "problem" (for want of a better word) is that none of this is "chunky". It happens all the time. People don't request accounts only between 0900 and 1700, they do it whenever suits them, at least one every hour. And people respond to these new accounts when they see them, not immediately when I post, so there is a long tail of notifications that never seems to stop. I've turned off notifications on my phone, they're still there but it doesn't beep or vibrate. This helps a little, but I've also missed phone calls so it's not a perfect system.

The rest of my life hasn't stopped either, so balancing this with that has been ... difficult at times.

Some statistics?

We've had 225 new user, up 704% on the average number of new users, pushing us over 1000 accounts on the server.

We now have 562 "active users", up 90% (although will they stick around?)

15,602 interactions, up 309%, although that's probably mostly me mentioning all the new users.

We've also had zero reports - which is ... good but worrying? Have I curated our block list and membership that well? Maybe! I hope so!

The daily database backup is now over 1.6G with gzip compression!

We've also had a massive increase in donations. The server and domain renew in August but we've already got everything we need to pay for it sitting in a savings account. Which is amazing!

Although we might need that money soon as the next version of the mastodon software has translation capabilities, if paired with a translation service so that's an extra cost for either a service or another VPS (because I think we need to spread the load a bit)

So what?

I don't know.

If the pace of new users keeps up, and they all stick around, we're going to have a very different server than we had before. But that's not a bad thing.

What is a bad thing is if the server crashes, I can't fix it, something else happens.

I feel a lot of pressure to look after the experience of 1.03k users (so says the about page as I write this.)

It's the same install of Debian I put on the server in 2018, it's been updated sure, but I still worry about things going wrong and my ability to bring it back. I've never done a full clean install and recover from backups, even on a test server. I probably should, and I'm sure Smitty would lend me one, but then do I want to send all our data to the USA?

To be clear, moderation is not a burden, approving accounts at the usual cadence is not a problem. But I do worry that people think this is a proper enterprise and there is a team of people who know what they're doing. Maybe it should be. But I've struggled to work out how I make the leap from "I own everything, I trust me" to "I can die tomorrow and know a team will keep the server running." Who do I trust with the keys to the kingdom? Who do the users of the server trust? Everyone just trusts me to keep hold of the money they donate and spend it on the server, but no one knows if I do or not. Wouldn't it be better to have a coop run this? Hold the money, be a legal entity that can be trusted with everything?

But I have NO IDEA how to make that a thing.

Plus, I don't have the time! I don't discuss my personal life online, but I have people who depend on me IRL, and a full time job, and I'm the editor for my IRL radio club newsletter, and at some point I might want to actually DO some radio. How do I fit "set up mastodon.radio as a cooperative, find, vet, and induct people to help, etc." into everything else?

Mastodon.radio was a fun idea 4 years ago, now it's a burden, and I don't know how to share the burden.

Do I regret starting mastodon.radio? Absolutely not.

Do I regret not having a more robust technical architecture that could more easily scale etc. yes.

Do I regret not sharing the admin burden sooner? Kind of. I still feel like I'd need someone who I know IRL, has the skills required, is interested and committed to help, and can be trusted - I'm not sure I know anyone who fits that specification.

I've had three, no, four account requests since I started writing this post.

https://m0yng.uk/2022/11/Thoughts-on-the-TwitterMigration/
Pending Projects and ideas 2022

I have a few project ideas currently pending parts, budget, time, etc. and I wanted to write some of them down so they can get out of my head! There is a lot of ESP stuff in here ...

Page Content Penkesu Computer

This is documented elsewhere on the site, but as it stands I've not got any further mostly due to the lack of Raspberry Pi availability, and my falling out with the 3d printer I was baby sitting (which has since been returned)

I'd still like to make it ... but the MNT Pocket Reform seems to be becoming a reality and might be a better idea

so... do you want a cute open hardware 7" computer with mechanical keyboard and optional LTE/5G connectivity?! what would you do with it? minute CEO of MNT Research

ESP Solar Server

For some time I've had a 100% solar powered APRS setup. But it's possible to have an ESP act as a web server, and they use basically no power, and you can get MicroSD card things for them, and this entire site is currently +checks+ 21M so could easily fit on an microSD. There also exists a "wide input" thing that would happily take the variable 12-ish-volts from the solar system to run the ESP.

I could then get my VPS to connect back into my house, check if the ESP is online or not. If so, proxy that, if not, fall back to serving the site in the same way it does now. Could easily have a solar subdomain that always (tries) to serve from the ESP.

I could even add Gemini using Astrrra / ESP8266GeminiServer

Sure, I could use the raspberry pi that's doing the APRS thing ... but ...

Fix the APRS Solar Setup

The APRS station isn't actually online right now.

A few things failed, and I haven't got it all back (yet / ever?)

Firstly, the 12v -> USB converter seems to have failed in a way that kills raspberry pis - which isn't ideal. The Pi was working, until I had to (re)boot it, then it wasn't. I assumed SD card issues, which was the case. New SD card, still no boot. "Spare" Pi, new SD, boot success! But it only stayed on the WiFi for a few minutes, then vanished, and then wouldn't boot anymore.

The SD was ok, and when I connected it to the only Pi I had left not already doing something (although I did have plans for it, just not this) it booted OK on a different power supply. neither of the other PIs would boot with the same SD and power supply.

When I connected up the third Pi to the solar system I didn't use the pi-killing-converter and went for a wide-input-shim I'd got for using with the 3d printer and octopi. So far this has worked fine and I would absolutely recommend using something like that over an ebay sourced buck converter.

I've also set up log2ram so hopefully that SD card and Pi will survive for longer than the last two. Although to be fair, the original solar powered pi did run consistently (but not constantly, night time and winter in the UK isn't great for 100% solar things) for at least a year but was let down by the power supply.

If anyone knows how I might bring the other two Pis back to life, that would be great!

One key thing the solar setup is missing now is some way to monitor it all. I was using three INA260s which worked really well. But now they seem to have decided to stop working. One has a short to ground on the clock pin and the other two don't respond to i2cdetect (and make it run super slowly) my eBay basket has some INA226 modules - but they are nearly £7 each and I'm not sure I care enough about this specific project right now.

However, it would be super good to get this working again, and combined with the ESP Server idea I could do something like LOW<-Tech Magazine and show the battery charge level somehow, like how they use the background.

ESPHome Video Doorbell

This one is potentially really easy to do, and also useful!

  • LILYGO make an ESP32 camera device with PIR, camera, button, etc. It's basically screaming to be made into a video doorbell
  • petrepa/ESPHome-VideoDoorbell exists and uses this device
"Smart" Laundry basket

I started this and it half works...

  • Take an ESP8266
  • Connect an HC-SR04 ultrasonic distance measuring module
  • Connect a tilt switch to the reset pin
  • attach it to the inside of the laundry basket lid
  • code it to read the distance and send that over MQTT then go into deep sleep
  • Make HomeAssistant show that distance and alert me when the basket is getting full

Some problems, probably of my own making...

  • Ultrasonic distance sensors work by using sound. Which is absorbed by fabric. Which leads to unreliable distance readings (especially when the distance is short / basket is getting very full)
  • Doesn't seem to actually ... do it more than once? The tilt-to-reset doesn't seem to work as expected and deepsleep for 60 minutes never seems to wake up (yes I have the pins connected)
  • I should probably just use ESPHome
  • not everyone is careful when shoving stuff into the basket and the pins have been bent and connections pulled out quite often
Remote "HomeScreen"

I have a PiZeroW powered e-paper display in the hall. It runs various python scripts on a cron defined schedule and can do things like:

  • Show everyone's calendar and weather forecast in the morning
  • Provide random Aeropress recipies at 10am
  • Show charts of energy use, temperature, solar power (if it was working), grid fossil fuel %, etc. every hour during the day
  • Various info about stuff to summarise the day in the evening

It can do all of this on-demand too using a 6 key mechanical keyboard (each key runs a differnt script, some depend on the time of day), plus some other stuff only on-demand:

  • Show a random image from pexels.com
  • Show an image uploaded to it
  • Check and show if my computers/Pis/servers are online and if they have updates
  • fetch car Tax/MOT status from DVLA
  • tell me how many visitors my website has had recently (on Gopher, Gemini, and HTTP)

It would be very cool to have a remote screen that can display some/all of this but from another location, for example a friend or relative's house (yes, I'm being vague for privacy reasons.)

LILYGO (them again!) make a 4.7 inch e-paper screen, which has an ESP32 stuck on the back, and buttons, and the ability to run off (and charge!) a 18650 cell.

trombik/esp_wireguard exists, which would allow me to VPN back into my house and download pre-made images for the remote display to display. I could even add an environment sensor to get temperature etc. and scan for devices on the remote WiFi to determine if people I care about got home safely after work and send it all to my Home Assistant! (with their permission of course.)

I got as far as making the display connect to WiFi and download a PNG... but the LILYGO screen is a higher resolution than my Pi-powered one! So it can't just take the image file and display it, some conversion seems required, and I haven't worked out how to do that yet. Plus my C skills are lacking, and the edit-compile-upload-run-debug cycle is VERY SLOW. I could pre-convert the image on the pi, but I have no idea how to make it download and display the .h file the example code produces. I've not tried putting WireGuard and the other stuff on yet.

Remote WLED

On a similar thread, I have modified a small dinosaur light and shoved some controllable RGB LEDs in it, and connected it up with an ESP8266 and WLED. It's great! I can control it from Home Assistant!

BUT - I want to give this to a friend, so they can have it in their home and it sync with/be controlled by my Home Assistant.

I've repurposed a PiZeroW to use RaspAP to abuse AP-STA so I can connect to their WiFi AND provide an AP for the WLED device to connect to, plus WireGuard to connect everything on that AP back into my home.

But, although it seems to work (I can connect to devices in my home via the AP) I can't work out how to do it the other way around and get Home Assistant to "discover" the remote WLED device. The only suggestion I've had so far it to run another Home Assistant instance and make it a "remote" to my local one, but as I've killed two Pi 3b+ I don't have the spare hardware for that! Until issue 2634 is implemented I don't think I'll be getting this working any time soon.

Like the remote display, it's really hard to debug and work this stuff out when you have multiple devices and complex webs of connections. My at home debugging setup was

  • Laptop connected to RaspAP WiFi
  • PiZeroW connected to my phone's WiFi hotspot, which connects to Mobile network
  • PiZeroW using Wireguard to connect to my home network

It's REALLY EASY to not spot when something has re-connected to the better WiFi - aka your home WiFi and is no longer on the Pi's AP. Or the Pi needs to restart something and the AP goes away. Or the phone has weak signal strength so the 3g is very slow. or etc.

DishWasher Monitor

I have a dumb dishwasher, I like this. But it is "integrated" so has no external sign that it is running, finished, or whatever. I bought an energy monitoring "smart socket" that has an ESP8266 hiding in it, it was an absolute pain to open up and get to the ESP. Once I had it out I can't get it into flash mode.

I'd LIKE to do something like Giovanni did with their washing machine - but I'd really like the slim profile of something round and about the size of the plug. Not that it matters, it's all hidden in the cupboard anyway 🤷

A more compact device would be more acceptable for devices that have visible plugs though, like a bread machine on the kitchen counter...

ESPHome Flashing Service

ESPHome is great, and I've been using it a lot with commercial devices that have ESPs in them.

But getting ESPHome onto the device is often a pain, involving disassembly and sometimes soldering. Once installed all the updates and configuration can be done over WiFi.

I suspect there is a market for people who want to use ESPHome but don't want to do the initial hardware flashing work. That was me when I first started playing!

I wonder how much luck I'd have with an eBay store selling sonoff and other devices I'd purchased in bulk for less than retail price + a service to flash it with ESPHome and your provided WiFi details and charge people about retail price for the bundle.

ESPHome environment light switches

Lets say I have some ESPHome powered light switches (I do!) and I wanted a neat way to know the temperature in every room of my house.

Well, I've already got most of the solution stuck to the wall, right?

Can I add a small i2c temperature sensor to the ESP inside my light switches?

https://m0yng.uk/2022/06/Pending-Projects-and-ideas-2022/
mastoWelcomer
Page Content Background

I run mastodon.radio, and I have it set to require approval for new accounts. I also like to welcome every new user.

This can result in delays, as I need to do a complex dance, something like:

  • look at pending accounts
  • confirm email is confirmed (not visible on approval page!)
  • confirm I'm happy with reason
  • maybe check the callsign too
  • select for approval
  • copy usernames (using snippet in dev console)
  • approve
  • copy old welcome toot
  • paste usernames into welcome toot
  • send toot
  • find accounts again
  • follow the new accounts

I wrote a snippet of JavaScript to run in the dev console to copy out the selected usernames, which helped but for security reasons you can't put stuff on the clipboard without user interaction so I still had to manually select and copy the string... the entire process is a faff.

Well, mastodon has an API, and there exists Mastodon.py

Guess what comes next?

That's right! SCOPE CREEP!

Proof of concept

I wrote a small python script to fetch a list of pending accounts, show me the info, and let me pick which ones to approve. It then does that, and follows each account, and toots a welcome message.

It worked well!

Script output listing 3 pending accounts, details redacted

But ...

  • It was hard to use on my phone (copying long ID strings)
  • It wasn't obvious that the email was confirmed
  • The date time thing is too long
  • It didn't check if the resulting welcome toot is too long, so it could fail to send
Current version

So, I extended it a bit.

Now it also:

  • formats the relative date/time
  • tries to find callsigns in the username, email, and request text
  • looks for my "rules canary" (more below)
  • highlights if email is confirmed
  • checks QRZ.com for any found callsigns
  • lets me type single digits to select accounts!
  • sends individual welcome messages, plus a public welcome

Script output listing 3 pending accounts, with the above improvements

The Code

It's probably not much use to anyone else, but if you remove the callsign and QRZ lookups it might be a good base for your own?

Note I've kept the permissions as minimal as possible, but they are still ADMIN tokens lying around on your computer. Due caution is required.

from datetime import datetime, timezone
from mastodon import Mastodon
from random import choice
from qrz import QRZ
import humanize
import pprint
import re

callsignRegex = '[a-zA-Z0-9]{1,3}[0-9][a-zA-Z0-9]{0,3}[a-zA-Z]'
ruleCanary = 'hippo cornflakes'

qrz = QRZ(cfg='./settings.cfg')

pp = pprint.PrettyPrinter(indent=4)

''' create app - do this once
Mastodon.create_app(
     'mastoWelcome',
     scopes=['admin:read:accounts', 'admin:write:accounts', 'write:statuses', 'write:follows'],
     api_base_url = 'https://mastodon.radio',
     to_file = 'mastoWelcome_clientcred.secret'
)
'''

''' authorize - do this once
mastodon = Mastodon(
    client_id = 'mastoWelcome_clientcred.secret',
    api_base_url = 'https://mastodon.radio'
)
'''

''' log in - do this as often as needed
mastodon.log_in(
    'username',
    'password',
    scopes=['admin:read:accounts', 'admin:write:accounts', 'write:statuses', 'write:follows'],
    to_file = 'mastoWelcome_usercred.secret'
)
'''

# connect with saved credentials
mastodon = Mastodon(
    access_token = 'mastoWelcome_usercred.secret',
    api_base_url = 'https://mastodon.radio'
)

pendingAccounts = mastodon.admin_accounts(status='pending')

print(str(len(pendingAccounts)) + ' Pending accounts')
i = 0
for pAccount in pendingAccounts:
    # easy to type ID
    print(f'\n{i}')
    # attempt to find callsigns
    allPossibleCallsigns = re.findall(callsignRegex, pAccount['username'])
    allPossibleCallsigns.extend(re.findall(callsignRegex, pAccount['email']))
    allPossibleCallsigns.extend(re.findall(callsignRegex, pAccount['invite_request']))
    # tell me about the account
    print(f"Username: {pAccount['username']}")
    print(f'{humanize.naturaltime(datetime.now(timezone.utc) - pAccount["created_at"])} {pAccount["email"]}')
    print(f'{pAccount["invite_request"]}')
    if ruleCanary in pAccount["invite_request"]:
        print(f'\033[93mRule Canary: {ruleCanary in pAccount["invite_request"]}\033[0m')
    else:
        print(f'\033[95mRule Canary: {ruleCanary in pAccount["invite_request"]}\033[0m')
    if pAccount["confirmed"]:
        print(f'\033[93mEmail confirmed: {pAccount["confirmed"]}\033[0m')
    else:
        print(f'\033[95mEmail confirmed: {pAccount["confirmed"]}\033[0m')
    # dedupe list
    possibleCallsigns = []
    for call in allPossibleCallsigns:
        lowerCall = call.casefold()
        if lowerCall not in possibleCallsigns:
            possibleCallsigns.append(lowerCall)
    print(f'Possible Callsigns: {possibleCallsigns}')
    # get info from QRZ
    print('QRZ info')
    for call in possibleCallsigns:
        result = qrz.callsign(call)
        pp.pprint(result)
    i+=1

# ask me what accounts look OK
approveWhat = input("Which accounts to approve?: ")
# split the space separated list
approveWhat = approveWhat.split()

print('\nYou want to approve these account:')
for aAccount in approveWhat:
    print(pendingAccounts[int(aAccount)]['username'])

# make a case sensitive YES to make sure I'm paying attention
confirmYES = ''.join(map(choice, zip('yes', 'YES')))
confirm = input(f"type '{confirmYES}' to confirm: ")

# store the usernames in "@ + name" format
userNames = ''

if confirm == confirmYES:
    print('Approving the accounts...')
    for aAccount in approveWhat:
        updatedAccount = mastodon.admin_account_approve(pendingAccounts[int(aAccount)]['id'])
        print(f'Approved {updatedAccount["username"]}!')
        mastodon.account_follow(pendingAccounts[int(aAccount)]['id'])
        print('Followed them!')
        userNames += f'@{updatedAccount["username"]}\n'
        # YOU'LL WANT TO CHANGE THIS MESSAGE (I assume)
        mastodon.toot(f'Hello @{updatedAccount["username"]}\nPlease toot an introduction (using # introductions) so we can get to know you :alex_grin: add some hashtags too so we know what interests you\n\nYou can find more people on our server using the local timeline & directory\nhttps://mastodon.radio/web/directory\n\nThis list of radio amateurs on mastodon, may be of interest: https://pad.dc7ia.eu/p/radio_amateurs_on_the_fediverse\n\nI recommend the third-party apps\nhttps://joinmastodon.org/apps\nRemember to describe your images (ask for help!) + fill out your bio before following people', visibility='unlisted')
        print('Tooting to welcome them!')
    templateToot = f'Everyone, please welcome\n{userNames}\n\n#introductions'
    print('Tooting bulk welcome message...')
    mastodon.toot(templateToot)
else:
    print('\nOk, DOING NOTHING.')
Rules Canary?

mastodon.radio isn't a big corporate server, we have rules that we enforce. Some are pretty common (don't be a dick) some are important for the community (describe your images) and I see people not describing their images. Which makes me think they haven't read the rules. So I wondered how I could check up on this.

Maybe you've heard of a warrant canary? The idea being a message says that "The FBI has not been here" and is removed if they have been, even if an organisation is forbidden from saying, for example, that the FBI HAS been there.

Based on this idea I added "rule 7" to mastodon.radio, it reads

When apply for an account, if you've actually read these rules, please end your "Why do you want to join?" with the phrase "hippo cornflakes"

Very few requests mention cornflakes, or hippos. But that's not really the point, I'm not denying entry to people who don't. Yet. But it is interesting to see, from the administrator's view, who actually read the rules AND bothered to follow them.

Some workflow optimisations [November 2022]

November 2022 has seen a lot more applications for accounts than ever before to mastodon.radio, as a result I've made some changes to the script which remove some of the manual checks and speed up the process.

  • Only show confirmed emails - before I had to eyeball which accounts had confirmed their email address, now it only shows me accounts with a confirmed email address.
  • Rule Canary - the script now creates an "Auto Selected" list of accounts which have confirmed their email AND have included the rule canary text (plus I made it not case sensitive!)
  • Stop showing if email is confirmed and rule canary present, as we're automatically deciding so don't need to see them
  • Added some try/except statements to avoid the entire script failing part way through

I could automate the decision further, specifically if a callsign is found and gets at least one result in QRZ lookup. But I've held off that for now because I'd have to make more significant code changes to move that lookup around, AND I'm not convinced that a bad actor couldn't just pop in a valid callsign and then I'm automating the approval of spam. I know the rule canary could have this issue too, but it's easy to change that and it remain effective (for a time) but the only mitigation to "callsign stuffing" would be to remove the automation again.

Make Alex do it [November 2022]

For four years I've welcomed everyone personally, when I wrote this script I was able to easily send a direct message to every new account with some tips.

This worked fine, until we had literally hundreds of new users and my mentions and direct messages became impossible to use.

The solution? Make Alex do it! Alex is the mastodon.radio mascot and has had an account for a while which has always been restricted, with no followers, and not posting anything. But I've opened that up and now it's Alex who welcomes every new user.

It adds some complexity to the script because now it has to log in twice, but it makes my life a lot easier and my use of mastodon.radio a LOT nicer. It also means that it doesn't matter who does the approval, everyone gets a consistent welcome and everyone else has one place to look for these welcome messages.

I'm logged in as Alex on my phone so I can spot anyone asking questions to them, and so far it has been very well received - with replies like:

A mascot! Now that's what I'm talking about!

Who's a good bot!? You're a good bot!

Plus now more people get to see our cool mascot! If you want to know about new arrivals on mastodon.radio give Alex a follow.

The updated code [November 2022]

This is the newly refined code with more Alex

'''
ALL THE SAME STUFF AS BEFORE BUT WITH TWO ACCOUNTS
'''
# connect with saved credentials
mastodon = Mastodon(
    access_token = 'mastoWelcome_usercred.secret',
    api_base_url = 'https://mastodon.radio'
)
# THIS IS NEW
mastodonMascot = Mastodon(
    access_token = 'mastoWelcome_usercredMascot.secret',
    api_base_url = 'https://mastodon.radio'
)
pendingAccounts = mastodon.admin_accounts(status='pending')

print(str(len(pendingAccounts)) + ' Pending accounts')
autoSelected = []
i = 0
for pAccount in pendingAccounts:
    if pAccount["confirmed"]:
        # THIS IS NEW
        if ruleCanary in pAccount["invite_request"].casefold():
            autoSelected.append(i)
        else:
            print(f'\n{i}')
            allPossibleCallsigns = re.findall(callsignRegex, pAccount['username'])
            allPossibleCallsigns.extend(re.findall(callsignRegex, pAccount['email']))
            allPossibleCallsigns.extend(re.findall(callsignRegex, pAccount['invite_request']))
            print(f"\033[93mUsername: {pAccount['username']}\033[0m")
            print(f'{humanize.naturaltime(datetime.now(timezone.utc) - pAccount["created_at"])} {pAccount["email"]}')
            print(f'\033[95m{pAccount["invite_request"]}\033[0m')
            # dedupe list
            possibleCallsigns = []
            for call in allPossibleCallsigns:
                lowerCall = call.casefold()
                if lowerCall not in possibleCallsigns:
                    possibleCallsigns.append(lowerCall)
            print(f'Possible Callsigns: {possibleCallsigns}')
            # get info from QRZ
            print('QRZ info')
            for call in possibleCallsigns:
                try:
                    result = qrz.callsign(call)
                    pp.pprint(result)
                except:
                    print(f'{call} not found')
    i+=1
# THIS IS NEW
print(f'Auto Selected {len(autoSelected)} accounts')
for aAccount in autoSelected:
    print(pendingAccounts[int(aAccount)]['username'])

approveWhat = input("Which accounts to approve?: ")

approveWhat = approveWhat.split()

print(f'\nYou want to approve these {len(approveWhat)} accounts:')
for aAccount in approveWhat:
    print(pendingAccounts[int(aAccount)]['username'])

# make a case sensitive YES to make sure I'm paying attention
confirmYES = ''.join(map(choice, zip('yes', 'YES')))
confirm = input(f"type '{confirmYES}' to confirm: ")

userNames = ''

if confirm == confirmYES:
    print('Approving the accounts...')
    for aAccount in autoSelected + approveWhat:
        updatedAccount = mastodon.admin_account_approve(pendingAccounts[int(aAccount)]['id'])
        print(f'Approved {updatedAccount["username"]}!')
        mastodon.account_follow(pendingAccounts[int(aAccount)]['id'])
        print('Followed them!')
        userNames += f'@{updatedAccount["username"]}\n'
        try:
            mastodonMascot.status_post(f'Welcome to mastodon.radio @{updatedAccount["username"]}!\nAny questions, your admin is M0YNG\n\nPlease toot an #introduction with topic hashtags so we can get to know you :alex_grin:\n\nYou can find more people on our server using the local timeline\nhttps://mastodon.radio/web/public/local\nPlease fill out your bio before following people.\n\nThis list of radio amateurs on mastodon may be of interest\nhttps://pad.dc7ia.eu/p/radio_amateurs_on_the_fediverse\n\nI recommend the third-party apps\nhttps://joinmastodon.org/apps\n\nRemember to describe your images!', visibility='direct')
            print('Tooting to welcome them!')
        except:
            print(f'Failed to welcome {updatedAccount["username"]}')
    templateToot = f'Everyone, please welcome\n{userNames}\n\n#introduction'
    print('Tooting bulk welcome message...')
    mastodonMascot.toot(templateToot)
else:
    print('\nOk, DOING NOTHING.')
https://m0yng.uk/2022/05/mastoWelcomer/
Brain transplant on CALEX 4-way to ESPHome

A while ago I spotted some "Smart" 4-way power plugs "Reduced to Clear" in Tesco from £30 to £7.50 - so I bought two.

They are named "CALEX Holland Smart Power Plug 4-way / 4 USB", and are still listed on the Tesco website at £30

A black box with an image of the device and various logos

They use tuya - as so many things these days do, and I excitedly began the process of using Tuya-Convert to convert them to use the ESPHome firmware. Sadly these devices don't work with Tuya-Convert (at time of writing) due to the known issue of a New PSK Format and/or that they use a "WB3S" microcontroller which is not an ESP device but a custom controller tuya have started using (possibly to thwart people like me, probably for cost reasons.)

I read (somewhere) that the WB3S and the ESP8266 have the same pinout so it is theoretically possible to perform a "brain transplant" by removing the WB3S and replacing it with an ESP8266.

Before we start I need to remind you that this device has MAINS VOLTAGE in it, and you should only do this if you are confident you know what you are doing. Don't come crying to me if you die in the process, etc.

TL;DR
  • I works
  • It's a good job I bought two
Performing surgery

This is a fairly simple process, but has some nuances

  1. Unscrew case
  2. Identify WB3S
  3. Remove WB3S
  4. Replace with pre-installed ESP8266
  5. Close up case

Let's go through it all step by step!

You will need
  • An CALEX device
  • A Tri-Wing screwdriver or bit
  • A drill with countersink bit
  • A precision screwdriver / set
  • An ESP8266
  • Equipment to desolder - e.g. hot air gun, desoldering braid, soldering iron, flux, tweezers, spudger
  • Equipment to solder - eg. soldering iron, solder
  • A clamp to hold stuff still
  • Multimeter or similar to test connections
Programme the ESP8266

Before I began this I installed a very basic ESPHome firmware on the ESP8266 using the USB development board it came on.

ESP8266 developlment board connected via USB

I was able to confirm the ESP was working, and booting up, connecting to the WiFi, etc. before I did anything nasty to it.

I'll put a full configuration example below for you to start from.

Unscrew the case

Two problems here, one is that the case uses tri-wing screws and the other is that the screws are sufficiently recessed that my bit wouldn't reach. NOTE: you do not need a precision bit, these are fairly chunky screws. I had to buy a set of "security" bits and use the smallest which is much bigger than my largest precision tri-wing.

Bottom of case with six screw holes highlighted

To gain access to the screws, remove the six rubber feet. I couldn't reach the screws so used a countersink bit to drill out the top a bit, enough to reach the screws but not enough to prevent me fitting everything back together. Go slowly, too fast and you will melt the plastic rather than cutting it away. I do want to use this after I'm done and as it has mains voltage inside I don't want the cover hanging off!

Close view of drilled out screw hole - like a small impact crater

Once you can undo the screws it is simply a case of unscrewing them all and separating the cover halves. The PCB is attached to the back.

The case is open and we can see the PCB, bus bars, etc.

Identify the WB3S

The WB3S is located on the back of the PCB, when we first see the board. Remove the four small screws holding it in place and free the earth and neutral bus bars, and the four live connections from the case. You may find it helpful to unscrew the cable retainer which is using the same tri-wing screws as the case.

Turn the PCB over and look for the white rectangle overhanding the main PCB on the opposite end to the USB sockets.

The pcb is out of the case and the WB3S is highlighted

Remove the WB3S

This is a little tricky and I don't know the best way. I DO know that prying it off whilst trying to heat each pad with a soldering iron will damage the traces (possibly tearing them off the board) and destroy the main PCB, so don't do that. From the factory these boards are mostly attached with solder UNDER, not to the side, so it is very hard to get it to all melt.

I was successful by using desoldering braid and a soldering iron to remove the majority of the solder, then a heat gun to simultaneously melt the solder on all the pads and lift the entire blue PCB off the main green PCB very carefully using tweezers.

The PCB is in a clamp and the WB3S is no longer attached

I then had to repeat the process on the ESP8266 as it seems to be much easier and cheaper to buy them on development boards than as just the ESP (at least in the UK, if you don't want to wait for shipping directly from China.) Mine didn't have an overlap so I used a spudger to gently get in between the ESP's PCB and the development board and extremely carefully lifted it off whilst applying hot air.

The PCB is in a clamp and the ESP8266 is no longer attached

Replace the WB3S with the ESP8266

Hopefully this is harder to get wrong!

I placed some solder onto the now empty pads and placed the ESP8266 roughly in position. I then did a mix of melting pads directly with the soldering iron, and heating with the heat gun and pressing down with the tweezers.

This seemed to work ok, but I also flowed a little extra solder onto each pad to be sure, looking for it to appear inside the little hole in the pad.

The PCB is in a clamp and the ESP8266 has replaced the WB3S

I then went around and checked for continuity from each pad to the main PCB - ideally to the next component or connection I could see.

Close up the case

I know it's bad to close the case up before testing, but as we have mains voltage and bus bars held in place using only flexible wires I STRONGLY recommend you put everything back, screw it up, and not plug anything in until you are SURE there is no risk of the live and neutral shorting out or shocking you.

Now is the time to plug it in! If it worked you should see the LED by the USB light up, and one or two of the sockets might also have a lit LED (depending on the configuration (or lack of) you installed). It should also show up in ESPHome / on your WiFi - just like it did before when you tested it (you did test it, right?)

The rubber feet should still fit, and you could probably file/sand the rough plastic to make a nice flat finish.

Don't forget to add labels to remind you this one has ESPHome in it, and to identify the socket numbers.

Back of the closed case, it now has a Dymo label reading "ESPHOME" Front of the closed case, each socket has a Dymo label number on from 4 to 1

ESPHome configuration

It worked! Now what?

I had a "spare" PCB so I was able to trace the relays back to the controller and identify which pins control which relay. I don't think it is possible to control the USB ports, but I didn't ever try the tuya firmware so maybe I'm missing something.

You will have noticed the ribbon cable, it has seven wires. Four are used to control relays, one is ground, one is VCC, one is possibly also (a different) VCC. So there doesn't seem to be be one available to control the USB, and there isn't a relay for it, and looking at the PCB the USB ports seem to get a direct feed which is shared with the relays.

Each socket has an LED, which is tied to the relay so we don't need to control these in addition (like the Sonoff switch) and the LEDs around the power button are all hard wired too, so we can't control that either.

Your basic configuration could look something like this:

esphome:
  name: calex-4gang

esp8266:
  board: esp01_1m

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
  password: "set this / auto generated"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Calex-4Gang Fallback Hotspot"
    password: "set this / auto generated"

captive_portal:

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO16
      mode:
        input: true
      inverted: true
    name: "CALEX Button"
    on_press:
      - switch.toggle: CALEX_Relay_1
      - switch.toggle: CALEX_Relay_2
      - switch.toggle: CALEX_Relay_3
      - switch.toggle: CALEX_Relay_4

switch:
  - platform: gpio
    name: "CALEX Relay 1"
    pin: GPIO14
    id: CALEX_Relay_1
  - platform: gpio
    name: "CALEX Relay 2"
    pin: GPIO5
    id: CALEX_Relay_2
  - platform: gpio
    name: "CALEX Relay 3"
    pin: GPIO15
    id: CALEX_Relay_3
  - platform: gpio
    name: "CALEX Relay 4"
    pin: GPIO4
    id: CALEX_Relay_4

It showed up in Home Assistant looking like this (after it was discovered and "configured")

Home Assistant UI showing four CALEX Relays and a button

Having Lights

I'm going to use two of these sockets to control lights, so I'd like Home Assistant to see them as lights, not switches.

To do that you need to alter the configuration slightly, remove the sockets you want to have controlling lights from the "switch" section, create a new "outputs" section and put them in there, don't give them a name (so they don't show up individually in HA). Then add a "light" section which calls on the outputs by ID, and does have a name. Like this:

switch:
  - platform: gpio
    name: "CALEX Relay 1"
    pin: GPIO14
    id: CALEX_Relay_1
  - platform: gpio
    name: "CALEX Relay 2"
    pin: GPIO5
    id: CALEX_Relay_2

output:
  - platform: gpio
    pin: GPIO15
    id: CALEX_Relay_3
  - platform: gpio
    pin: GPIO4
    id: CALEX_Relay_4

light:
  - platform: binary
    output: CALEX_Relay_3
    id: cloud
    name: "Cloud lights"
  - platform: binary
    output: CALEX_Relay_4
    id: stars
    name: "Star Lights"

Now it looks like this in Home Assistant

Home Assistant UI showing two CALEX Relays and two lights named Cloud and Star

Conclusion

What have we learned from all of this?

  • It is possible to replace the tuya controller with an ESP8266 and use ESPHome
  • Tuya probably don't want you to (they did use "security screws")
  • You probably don't want to do this unless you are willing to potentially kill a working bit of kit
  • I should have bought a hot air gun years ago

Is it worth it? Only you can answer that for yourself really. For me, the device was £7.50 (ok, I killed one but I can possibly patch it back together) and I like the privacy and control of having everything on my own network and not talking to random servers just to turn lights on and off. Plus I've learned a lot, had success, and some fun, and now I have a working useful device to replace multiple individual remote sockets in one room and a USB charger. So yes, for me, it was worth it.

https://m0yng.uk/2022/04/Brain-transplant-on-CALEX-4-way-to-ESPHome/
koda keyboard

I've wanted to build a keyboard for ages now, and the Penkesu computer needs a keyboard, so my first forray into the custom keyboard world was to build a koda keyboard by odd-rocket

Small 4 row by 12 keyboard with black square key caps with white lettering and a pink glow

Sourcing components PCB

I needed to get PCB(s) made, which isn't something I've done before. I went with JLCPCB on the basis that people like Big Clive dot com use them. The process wasn't too complex, but I did end up guessing on some options (like surface finish) or just picking based on price. In the end I ordered:

  • 15
  • Purple (because why not?)
  • HASL(with lead)

Now, when Great Scott says his JLCPCB order came within the week, I suspect he gets the special sponsor treatment.

My timeline was:

  • Place order 18/02
  • Begin production 19/02
  • Complete production 23/02
  • Packed for delivery 24/02
  • Picked up by FedEx 25/02 (after cut off!)
  • Delivered 04/03

The PCBs themselves seem to be of good quality, and the one I have used works fine. I'd buy from them again.

The PCBs themselves cost £18.74 and the cheapest shipping was £19.15 for a total of £37.89 or about £2.53 per board. Did I need 15? No. Did it change the total price much? Also no. On that note, if you are in the UK and want one, let me know!

Everything else

I got the rest of the stuff from mechboards in the UK, and it wasn't cheap!

The total with shipping was £115!

I then got to test their support. The first set of keycaps I received had two "D" keys, and no "C". They asked me to post the set back and replaced it once they received them, which was slow but ok I guess.

However, when I went to install the keycaps I realised that I had no down key! In the pack it looked like a down key, but it was actually a left/right that had been rotated 90, and as the keycaps can only be installed one way (well, also upside down) I don't currently have a down arrow key. Luckily the set has a few dots that I can borrow, whilst I wait for mechboards to decide what to do next.

Building

I don't have many photos of this because I was doing it between other things, and after waiting this long I was keen to get it working!

The process is fairly simple,

  1. Snip the socket into just the side rails so we can fit the controller
  2. Shove the pins into the socket and position them into the controller - with the USB side towards the PCB
  3. Solder the pins into the controller
  4. Solder all the diodes to the under side of the PCB
  5. Solder the socket to the underside of the PCB
  6. Solder all the switches - beware of bent pins!

Later I used some brass M2 standoffs and screws with another PCB and some sticky feet to make a more robust (no exposed diodes and microcontroller) setup.

Flashing

I had to do the initial flash with the microcontroller not installed, so I could reach the reset button, or you can short RST to GND using a screwdriver.

I cloned the penkkesu repo and copied the keyboard stuff into ~/qmk_firmware/keyboards/penkesu

From there I could cd into ~/qmk_firmware/keyboards/penkesu/keymaps/default and run

qmk compile
qmk flash

Once you have QMK installed getting the keyboard into flash mode is simply a case of holding the top left and bottom left keys when connecting it to the computer, in this case esc and the bottom left dot.

Mods

Obviously I had to make some tweaks! So far just two...

RGB

Odd-rocket says

koda has no leds. two extra data pins with GND and VCC were broken out for RGB underlighting. 

These are the four through holes in a line below the controller.

  • TOP - Data
  • VCC
  • GND
  • Bottom - Data

underside of the keyboard PCB showing LED tape lit up in the middle and most of the width of the keyboard

I connected some addressable RGB LED tape to the top data connection and made the following configuration changes;

# config.h

/* Define pins etc. for RGB lights */
#define RGB_DI_PIN C6 /* specify physical data pin */
#define RGBLED_NUM 12 /* specify number of LEDs */
#define RGBLIGHT_ANIMATIONS
#define RGBLIGHT_SLEEP /* turn LEDs off when host sleeps */
# rules.mk

BACKLIGHT_ENABLE ?= no # Enable keyboard backlight functionality
RGBLIGHT_ENABLE ?= yes # Enable RGB LEDs
# keymaps/default/keymap.c
# In the third KEYMAP (the "raise" key on penkesu, to the right of space) on the second row (tab, a, s, d, f)

RGB_TOG, RGB_MOD, RGB_HUI, RGB_SAI, RGB_VAI,

This lets to turn the RGB on/off RGB_TOG, change the mode RGB_MOD, then change the Hue, Saturation, and Intensity/Brightness.

These settings are remembered between use.

It looks good, making the middle of the board glow, but it doesn't help identify keys in the dark.

the keyboard in the dark with purple glow, some letters are slightly visible

Keymap

Since writing this post I've added a few more convenience keys, including an entire layer for function keys and "forced" keys that should type what I want regardless of the keyboard layout. For example the tilde (~) is all over the place between mac and windows keyboards, US and UK layout, etc. Now I can type it with confidence. But also ‽ … 🐍 ` # ~ | €

  • make backspace perform a forward delete when holding raise
  • up/down be page up/down when holding raise
  • left/right be home/end when holding raise
  • up/down be volume up/down when holding left raise
  • (and the RGB controls)

My full keymap file now looks like

#include "koda.h"

enum unicode_names {
    BANG,
    EURO,
    SNEK,
    TILDE,
    PIPE,
    HASH,
    GRAVE,
    ELIP
};

const uint32_t PROGMEM unicode_map[] = {
    [BANG]  = 0x203D,  // ‽
    [SNEK]  = 0x1F40D, // 🐍
    [EURO]  = 0x20AC,  // € 
    [TILDE]  = 0x007E,  // ~
    [PIPE]  = 0x007C,  // |
    [HASH]  = 0x0023,  // #
    [GRAVE]  = 0x0060,  // `
    [ELIP]  = 0x2026,  // …

};

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {

    KEYMAP(
        KC_ESC, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_BSPC, 
        KC_TAB, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, 
        KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_UP, KC_ENT, 
        MO(3), KC_LCTL, KC_LALT, KC_LGUI, MO(1), KC_SPC, KC_SPC, MO(2), KC_SLSH, KC_LEFT, KC_DOWN, KC_RGHT),

    KEYMAP(
        KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_TRNS, 
        KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_MINS, KC_EQL, KC_LBRC, KC_RBRC, KC_SCLN, KC_QUOT, 
        KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_BSLS, KC_COMM, KC_DOT, KC_TRNS, KC_TRNS, 
        KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_SPC, KC_SPC, MO(2), KC_SLSH, KC_TRNS, KC_TRNS, KC_TRNS),

    KEYMAP(
        KC_TILD, KC_EXLM, KC_AT, KC_HASH, KC_DLR, KC_PERC, KC_CIRC, KC_AMPR, KC_ASTR, KC_LPRN, KC_RPRN, KC_DELETE, 
        RGB_TOG, RGB_MOD, RGB_HUI, RGB_SAI, RGB_VAI, KC_NONUS_HASH, KC_UNDS, KC_PLUS, KC_LCBR, KC_RCBR, KC_COLN, KC_DQUO, 
        KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_PIPE, KC_LT, KC_GT, KC_PAGE_UP, KC_TRNS, 
        KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, MO(1), KC_TRNS, KC_TRNS, KC_TRNS, KC_QUES, KC_HOME, KC_PAGE_DOWN, KC_END),

    KEYMAP(
        KC_F12, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, 
        KC_TRNS, KC_TRNS, KC_TRNS, X(EURO), KC_TRNS, KC_TRNS, KC_TRNS, X(SNEK), X(GRAVE), X(HASH), X(TILDE), X(PIPE),
        KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, X(ELIP), KC_KB_VOLUME_UP, KC_TRNS, 
        KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_SPC, KC_SPC, MO(2), X(BANG), KC_TRNS, KC_KB_VOLUME_DOWN, KC_TRNS)

};
Review

I'm using it now, as a stand-alone keyboard. I've never used an ortholinear keyboard before so it is taking some getting used to the layout.

I like the switches, they feel good and sound good. I like the keycaps, they look and feel nice. But they should for the price!

I've also not used QMK before, and I didn't find it easy to get started. There seems to be a lot of older info out there, and there are many different keyboards and configurations possible, so it can be hard to pin down specifics. I've found the official documentation to be the best.

My build is pretty solid and looks nice, and I could pop it into a bag to bring with me easily enough, but it is going to take time to o get my typing speed back up!

After using it for even more longer, the main issue is that it's too light and moves around the desk when typing. Maybe more grippy feet would help this.

Ultimately I still want to install this into the penkesu when I build it, but it is a nice keyboard on its own. Maybe I can have a passthrough on the penkesu so I can connect it to a host computer and just use the keyboard.

https://m0yng.uk/2022/03/koda-keyboard/
GEEETech i3 3d printer

I'm currently babysitting a 3d printer, it's a "GEETech i3 Pro C" - apparently.

The end goal is to make the Penkesu Computer, but first I need to get the printer working as well as possible so I can print the case.

It's a fairly basic device from what I understand, manual bed levelling for example, but has a heated bed and two extruders. Aligning the nozzles for these to be exactly the same height off the bed is ... not fun.

After quite a few small burns (and one that really hurt and still hurt after 4 hours under water) I think I'm finally getting it dialled in to work well with both extruders simultaneously.

This is the latest "XYZ calibration cube" printed with extruder 0 (purple) for the perimeters, and extruder 1 (yellow) for the infill.

The slicer preview looks like this A purple sided cube with yellow top. X and Y on the sides and Z on the top

The resulting cube looks like this A purple sided cube with threads of yellow poking out of the Y side and some gloop on the X side

Photos of all the sides are at the end of the page, if you care or want to help me improve the results!

I think I'm trying to do something quite hard here, use both extruders on every layer, but I really want to have my computer case have purple edges and yellow middle, because if I'm making it as a one off just for myself why shouldn't I have it look how I want? Other than that being hard.

To answer the inevitable "yeah, but can it even print with just one extruder anyway?" - Yes, it can. These two cubes were printed with exactly the same settings but using only one extruder, the purple loaded into extruder 0 and yellow into extruder 1. The only difference is that I grabbed a nearby fan to aid general cooling for the yellow cube as the purple one had droopy soft warm infill.

Purple and yellow cubes, both are good quality prints on the sides. The purple has a messy top but the yellow is fine

I'm using "PrusaSlicer" with the following changes to the default printer configuration - using "expert" mode because that is required to unlock multiple extruders.

PrusaSlier Printer Settings General
  • Capabilities - Extruders - 2
Extruder 1
  • Extra length on restart - 0.1mm (ensures the extruder is extruding when it re-starts on the next layer)
  • Retraction when tool is disabled - 15mm, 0.2mm extra on restart (ensures the extruder is extruding when it re-starts on the next layer)
Extruder 2
  • Extruder offset - x: 33mm, y: 0.5mm (because the firmware doesn't handle this)
  • Extra length on restart - 0.1mm
  • Retraction when tool is disabled - 15mm, 0.3mm extra on restart
PrusaSlicer Print Settings

I find these (nominally per print) settings essential too

Layers and Perimeters
  • Vertical Shells - Perimeters - at least 3 (to prime the extruder before it starts the first layer)
  • Quality - Avoid crossing perimeters - enabled (avoid ooze from the nozzles being dragged onto the model)
Infill
  • Fill pattern - Gyroid
  • Top/Bottom fill pattern - Octagram Spiral
Skirt and Brim
  • Loops (minimum) - 6 (primes both extruders before they start working on the first layer)
Multiple Extruders
  • Perimeter extruder - 1
  • Infill and Solid infill extruder - 2
Photos of the test cube

Every side of the cube, if you like that sort of thing.

Base (opposite Z) of the cube, a star pattern is visible in yellow with some gaps and splodges

X side in purple with a blob of yellow in the bottom left, some layers are uneven or have blobs or strings

Y side in purple with 19 little bits of yellow poking out and some strings, the purple is the same as on the X side

Back side (opposite X) in purple, a few layers are wavy and some are misaligned so you can see yellow

Left (opposite Y) side in purple has lots of strings of extra purple

https://m0yng.uk/2022/02/GEEETech-i3-3d-printer/
Sonoff S26 R2 + ESPHome

Who wants a smart home that stops working if the internet is down, or the OEM goes bust, or wants to charge you extra, or ... Not me!

I have a couple of TP-Link Kasa "Smart Plugs" because they were fairly cheap and they can be controlled from the command line by using python-kasa or Thomas Breydo's thing however, they still (try to) talk to their OEM a lot and at the time I needed to use the app to do the initial configuration.

Then I discovered that ESPHome supports pretty much every Sonoff product, and that they are fairly cheap - I paid £32.82 for four sockets (£8.205 each) delivered from the UK.

But what I got was the S26 R2 - which I haven't seen documented elsewhere, and there are some slight differences compared with the older version.

The documentation for S20 is basically the same - so do check that too: Sonoff S20 ESP Documentation

Open it up

WARNING This is a mains powered device. DO NOT OPEN IT WHEN IT IS PLUGGED IN.

Pop off the ring around the plugs, and remove the three screws, the case should come off into two parts.

S26 R2 with cover removed

If you look closely at the writing on the chip on the daughter board you may worry that it is not an ESP8266 - but don't panic, it still works.

Flashing

There are no handy holes for the UART connections, so you WILL need a soldering iron for this.

Pull out the circuit board and look on the bottom by the daughter board which is sticking up and you should see three pads marked "V", "TX", & "RX". "GND" is on the other side of the daughter board.

You will need to solder some suitable wires to these pads (or try holding four wires very still!)

S26 R2 with wires soldered to the UART pads

On my devices TX needed to connect to RX on my serial interface, and vice versa. (yours might be different, but there it won't break anything if you get this wrong initially)

Note: don't be scared off by the term "UART", I was successful using a normal USB->Serial interface This USB to RS232 at £6 on ebay

To get the device into "flash mode" hold down the button when you connect it to the computer - no lights should come on, if they do it didn't work and you can just try again. But it is worth giving it power without flash mode to check if the device is dead on arrival!

I am using ESPHome with Home Assistant and generated the firmware from there. I named each one "s26-last four characters of serial number" and used "Pick specific board" -> "Generic ESP8266 (for example Sonoff)" as the device type.

If you are using a chromium based browser you can just flash straight from the browser! But I found it more reliable to download the firmware image and use "esphomeflasher" to shove it over to the device.

Once the flash is complete, unplug from your computer and plug it back in again. Hopefully it will boot up and connect to your network and appear in ESPHome and/or Home Assistant.

Configuration

In my experience, the generated configuration file lacked some basic functionality. Sure - you can see if the button is pressed, turn the relay on and off, and the LED on and off, but that isn't quite a complete switch!

I want to be able to:

  • Turn the relay on and off remotely
  • Turn the relay on and off by pressing the button
  • Have the state correctly relayed to Home Assistant
  • Have the LED turn on and off depending on the relay state

My configuration files look like this:

###
# All your normal WiFi Stuff here
###

# The button
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO0
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Sonoff S26 Button"
    # what to do when it is pressed
    on_press:
      - switch.toggle: relayandled
    # tell Home Assistant what is going on
  - platform: status
    name: "Sonoff S26 Status"
  - platform: gpio
    pin: GPIO2
    name: "Sonoff S26 Sensor"

# This is the relay
switch:
  - platform: gpio
    name: "Sonoff S26 Relay"
    pin: GPIO12
    id: s20_relay
    # the thing we do when the button is pressed
  - platform: template
    name: "Penguin Relay"
    optimistic: true
    id: relayandled
    turn_on_action:
    - switch.turn_on: s26_relay
    - light.turn_on: s26_status_led
    turn_off_action:
    - switch.turn_off: s26_relay
    - light.turn_off: s26_status_led

output:
  # Register the LED as a dimmable output ....
  - platform: esp8266_pwm
    id: s26_led
    pin:
      number: GPIO13
      inverted: true

light:
  # ... and then make a light out of it.
  - platform: monochromatic
    name: "Sonoff S26 LED"
    output: s26_led
    id: s26_status_led

TIP you can add this to your configuration BEFORE initial flashing, to avoid doing everything twice.

Tidying up Home Assistant

Each switch should now offer you some "things"

  • The relay itself (switch)
  • The LED (light)
  • The Button (sensor)
  • The state (sensor)
  • The template we called "Penguin Relay" (switch)

If this is too much clutter for your dashboard (it is for me!) you can disable most without any issue (in my experience)

  • Go to "Configuration"
  • Go to "Devices"
  • Find your switch and click to open its configuration
  • Find the thing you want to disable, for example "binary_sensor.sonoff_s26_button"
  • Click to open the Settings modal dialogue
  • Toggle the "Enable entity" switch
  • Cick UPDATE

Enable entity is disabled

https://m0yng.uk/2021/12/Sonoff-S26-R2-+-ESPHome/
Solar power in autumn

Note: This is not a full write-up of my solar power APRS radio, I will do that one day, honest!

I've been running my APRS RF iGate purely off solar power for a few months now, and we're at an interesting time of year for solar stuff, so I thought I'd quickly note down some observations as the seasons change.

wet solar panel basking under a rainbow

Quick history

Some time ago I wondered if I could press a 20w solar panel and 7.5Ah SLAB into service doing APRS, the answer was YES - but only just.

I was happy for stuff to turn off when it runs out of power, reasoning that APRS traffic is light when there is least light so it wouldn't matter too much.

However, the 20w panel could only just keep up with the power demands of the system on a slightly dull summer day, and the 7.5Ah SLAB could only just keep it all running overnight in summer after a sunny day.

I could have left the experiment there, but I spotted a reduced-to-clear 100w panel on eBay and bought it, then bought a 55Ah battery on sale at the local motor factors (yea, I know it's the wrong type of battery.)

Since getting all that I've had everything running fine 24/7 from the moment I connected it all up.

Quick setup overview

It's not optimised, but it's also not fancy.

  • 100w solar panel
  • Charge controller
  • 55Ah battery
  • Raspberry Pi 3b+
  • TNC-Pi HAT
  • Small woxum mobile radio running low power
  • 2/70 Collinear
  • APRX software
So, Autumn?

Up until now, even with three or four rainy days in a row the system has never run low enough to power off, until this last week. We had a few VERY dark days, which ran the battery down enough that the charge controller shut off the output. Even after a sunny day there isn't enough juice going into the battery to keep everything alive overnight in early November.

Let's look at a graph/chart, and see if you can spot what is going on (answers after the break!)

Chart of amps and volts, discussed in detail below

This graph/chart is from today, so the data doesn't extend to midnight, because it isn't midnight yet. However, there is another big gap between around 0300h and 0900h where the system was offline. Which is fine, that's part of the design and "expected operation".

We can see that from some point in the morning the panel has been generating power and charging the battery, as we're at around 12.5v before we get any data (because the Pi is monitoring its own voltages) and it is coming it a respectable (if not amazing) 2 Amps. The panel is rated 100w at 5A, so we're running around 40% here, which for an autumn morning isn't too bad.

Can you guess what happens next though? From around 1000h to 1300h we have basically no solar generation. Any ideas why? Yup, my garden is north facing and even though I have the solar panel as far away from the house as possible there is still a big house shaped shadow that falls on the panel for these three hours. I suspect a lot of potential power is being lost here because it is the hight of the day and when the solar panel could be generating the most power.

This theory is supported by the nearly 4 Amps generated almost immediately the shadow moves off the panel. (The data is read every 30s but grouped up to 15 minutes for the chart.)

If we look at my historic solar data we can see how much power is generated and used and stored every day since late June 2021. The highlights are:

  • Good days produce 18Ah of solar power
  • Average days use 8Ah
  • Once we get to days that start with 0 stored power the table is not helpful

This is because if we start with 0 power, and the next day also starts with 0 power, clearly the system is only able to use the power generated that day. On that point, it's also not that useful for days where there is lots of sun because the battery can only store solar power in the "space" made by using it overnight.

Compared with summer

Let's look at another graph/chart and see how this compares with a summer day, this is from September

Chart of amps and volts, it looks like you'd think it should!

In this chart you can see the voltage is following the "proper" cycle of charging upto 14.25 volts (maybe there was a cloud around 1000?), then dropping off to hold at 13.8 volts for the rest of the day. We also start earlier with the voltage picking up from 0700 and dropping off again at 1700. These times are mostly dictated by neighbouring houses and the shadows they cast. The panel was also closer to my house at this point, but as the sun was higher in the sky it didn't matter, unlike now!

The raw power generated is actually less, but that's because we're just topping up what was used overnight, and providing some to run directly, rather than sucking up every single amp available.

A dark day

Chart of very few amps and volts

If I Remember Correctly [IIRC] this was a dark day that rained a lot, amazingly the solar panel still managed to produce 2.77 Ah that day, but barely managed to prevent the battery voltage from ending the "sun lit day" lower than it started.

What am I going to do?

We're always going to struggle with the triangle of power, to keep this thing up overnight I need to do one or more of:

  • Increase storage
  • Increase production
  • Decrease usage

Decreasing usage isn't that easy, other than tuning the pi a little by turning off USB+Ethernet, turning off HDMI, and turning off LEDs that I'm not using. I could also underclock the CPU, but it is already running the ondemand governor which turns down the clock speed when it is idle. I can't reduce the frequency it does APRS stuff either, as that is the entire point of it existing.

Increasing production could be done by adding more panels, but that's overkill for most of the year and expensive. I am considering moving the panel again so it gets sun later in the morning, but isn't shadowed during the peak of the day. This should help boost production a little. But at the expense of a later "start up"? I could move the panel to the house roof, but a) I'm not climbing up there, b) I don't have any way to attach it, c) I don't have long enough cables, d) the roof faces East and West, not South so we'd still have the shadow problem.

Increasing storage probably ins't useful in this context, we're not making enough to keep going until the next sunrise so more storage wouldn't do much.

What was the point of all this?

Nothing really, I just thought it was interesting and I look at these graphs every day so wanted to share!

https://m0yng.uk/2021/11/Solar-power-in-autumn/
Complex 19 + Licences

Today I made the source of Complex 19 available, so I thought it would be a good time to write a little bit about Complex 19 itself, and why it took me so long to release the code.

What is Complex 19?

Complex 19 is a Static Site Generator which takes in Markdown and outputs Gopher, Gemini, and HTML.

It can do:

  • Blog posts (like this one)
  • Pages (that you want to stick around for longer)
  • Make a page showing all the posts by Tag
  • Make a page showing all the posts by date
  • Make RSS and Atom files
  • Do all of that for Gopher format
  • Also do it for Gemini format
  • It does HTML files too!
  • Cope with some static files, e.g. your gpg key
  • Optimise images

It relied on many FLOSS projects and I'd like to acknowledge the awesome shoulders I stand on every time I can pip install something.

Why? Well because I could and because I wanted to. Will it be any use to you? Probably not.

Want to play? I've also installed gitea on my VPS to host it (I have gitea on my home network too so updates may be lumpy.)

Complex 19 git repo

A quick history

I've had a website since mumble years ago, and I've used a lot of things to make websites, from Micro$oft Publisher, Front Page, Dreamweaver, Drupal, Wordpress, and most recently hexo.

I wanted to try making my site available via Gopher, and as I already had my content in markdown it should be easy, right? Well as things tend to do with me it spiralled out of control. If you spot any references to "gopherStaticMaker" in the code, this is why.

I also wanted to support Gemini, and it was pretty easy. Yay for Open Source.

At this point I wondered why I was still using hexo to make HTML files, so I extended it to do that too.

I've been using it for a while now and think it does everything I need and want it to.

Picking a licence

I've been holding off releasing the code for one main reason, what licence?

I'm very aware of the ethics of software, I've gained massively from FLOSS over the years and Complex 19 is built on it. However, I also cannot feel comfortable releasing software that could be used for harm.

I run mastodon.radio, and like many instances it is a great place with friendly and supportive people. However, there are other instances that also use mastodon for things I find abhorrent. As I write this post the exploitative practices of Facebook are (yet again) being laid bare and parallels to the tobacco industry are obvious. Facebook is built on FLOSS, Gab is built on FLOSS.

I cannot feel comfortable releasing code that could be used in ways that harm people without any protection against that. Maybe there is a vanishingly small chance anyone would use Complex 19 for their hate site, but it's still not zero. I have snippets of code on my site that could more directly cause harm, for example python scripts that interface with mastodon, and I have been considering how to licence these too.

I know licences don't fix the problem, I know a bad actor will just ignore it, but I can try, right? We know that Google won't use AGPL licenced software so it can have an impact.

I'm not alone, there are quite a few Ethical Licences, (Organization for Ethical Source have a few options.)

After much consideration, deliberation, and procrastination, and having published the code I decided to "Just pick a license!111!!!" (thanks @erebion@chaos.social) and have picked The Hippocratic License 3.0 (or later.) Because it seems "good enough" for what I want, and has been around for a while and still seems to be cared about. I can always change it later, right?

I'm probably going to keep using the GNU GPL for some stuff, but I'm using the Hippocratic License for Complex 19, the code snippets on my site, and probably most of the other code I release in the future.

https://m0yng.uk/2021/10/Complex-19-+-Licences/
Talking IRC via XMPP

mastodon.radio has an XMPP server, which is mostly there because I wanted to see if I could. WIth some help from Mike it even authenticates using the user's mastodon account (mastodon xmpp config file) but none of that is relevant to this!

Danie van der Merwe wrote

Matrix is interesting in that it bridges to lots of other networks. So we have a #hamradio-za IRC channel at Libera.Chat, but it can be fully and easily accessed from Matrix by just joining the room from there. IRC Libera.Chat does already have a global #hamradio channel going too.

And I wondered if we could do XMPP to IRC and things looked hopeful because mod_irc exists. But I couldn't find any documentation on it, and Харпер in the XMPP Service Operators MUC said it was depreciated and the modern option was biboumi.

I patched together some bits from the biboumi documentation and this post by copyninja to get it working, and here follows an idiot's guide to remind me what to do next time (if there is one.)

A quick note on why

There isn't really any obvious advantage to doing this rather than just directly connecting via IRC, but some benefits to me are:

  • Extra privacy, as the connection goes via the mastodon.radio server rather than showing my home connection IP to the IRC server
  • Persistance, as long as I'm connected to XMPP somehow the IRC connection stays alive
  • Persistent authentication, identify with nickserv once on my laptop and I'm identified on my phone too (because the IRC server only sees one connection)
  • Seamless multiple devices, I can jump between laptop and mobile and miss nothing
  • Battery, blabber uses very little power on my phone (3% over 20 hours)
  • I can just keep using the same clients
Configure ejabberd

We need to tell ejabberd about our "external component", which we add to the listen section

listen:
    -
        port: 5347
        ip: "127.0.0.1"
        module: ejabberd_service
        access: all
        hosts:
            "biboumi.mastodon.radio"
                password: yourpasswordgoeshere

This password is for biboumi to talk to ejabberd and is needed in the biboumi configuration file.

You can restart ejabberd now and hopefully it will start up happy.

Database configuration

I already have postgres as mastodon uses it, so it was easy to just add a new user and database for this.

First get a root prompt on postgres:

sudo -u postgres psql

Then create our user, give it a password, and create a database:

CREATE USER biboumi CREATEDB;
ALTER USER biboumi WITH PASSWORD 'yourDBpasswordHere';
CREATE DATABASE biboumi OWNER biboumi;
\q
Configure biboumi

Mine is very mich like the example...

hostname=biboumi.mastodon.radio
password=yourpasswordgoeshere
xmpp_server_ip=127.0.0.1
port=5347
admin=m0yng@mastodon.radio
db_name=postgresql://biboumi:yourDBpasswordHere@localhost/biboumi
realname_customization=true
realname_from_jid=false

At this point I would run biboumi manually to see if it works and look for any issues and errors (I had a few and this is the final working version.)

sudo biboumi /etc/biboumi/biboumi.cfg

Assuming everything is ok, we can enable and start the service

sudo systemctl enable biboumi.service
sudo systemctl start biboumi.service
Connecting to an IRC channel

From the XMPP side of things we are joining a Multi User Channel (MUC) but the format is slightly verbose, for example to join the hamradio channel use this magic string:

#hamradio%irc.libera.chat@biboumi.mastodon.radio

#hamradio <- is the channel name
%irc.libera.chat <- is the IRC server
@biboumi.mastodon.radio <- is the local transport we set in the configuration

From there everything just sort of worked for me, I'm even able to send and receive messages on my laptop (using dino) and mobile phone (using blabber.im) and a terminal (using profanity.)

In order to speak in the #hamradio room on libera.chat you need to have registered your nickname. Which is fairly easy.

You can't do the usual /msg NickServ so you need to start a chat with NickServ directly. Their "address" is basically the same as we used to join the channel:

nickserv%irc.libera.chat@biboumi.mastodon.radio

(note the lack of a #, 'cos it's not a channel.)

Then you can just message them directly with the usual commands

REGISTER <password> <email>

which will hopefully send you an email with instructions to confirm the registration. Then you need to identify the next time you connect - directly messaging NickServ like you did to register.

IDENTIFY <nick> <password>

If you have any questions, and think I might be able to help, feel free to ping my via XMPP m0yng@mastodon.radio or mastodon

https://m0yng.uk/2021/09/Talking-IRC-via-XMPP/
Printing my eQSLs

eQSL is a cool idea, I have some reservations about it (most useful features are paid for, control over cards is limited, card size is small, the site is clunky) but it provides all the thrill of QSL cards without the delay ... or something.

Anyway, I can see my cards in cloudlog, which is great, but there isn't an easy way to just browse the cards (that I know of) or get a copy of them. When you view the cards in the eQSL website there is an option to print them (which doesn't work when I tried) but all that does it show just the image, and it is all very manual and labour intensive with lots of clicks.

I had a rummage around and found an old python script to download the card images (eqsl-downloader-python) but couldn't get it working, I think some of the URLs have changed. CloudLog can fetch the cards too, so by mixing up the code from both I was able to hack together a script that would download the cards and send them off to my thermal printer. Some time later, I'd run out of paper and only done half of the QSL cards!

two reels of paper, one unwrapped to show eQSL cards in black and white

You could obviously just use the script to download all your eQSL cards, but then they still only exist as data on your computer and not on a long reel of paper!

The eQSL "API"

eQSL doesn't have a proper API for getting cards, there are the following steps:

  1. Call a URL with your username and password as query parameters
  2. Get an HTML page with a link to the ADI or TXT file of your log
  3. Download the log file
  4. Parse the log
  5. For each log entry call a url with the QSO's date year, date month, date day, time hour, time minute, mode, band, and remote call (oh, and your username and password)
  6. Get an HTML page with a link to the image
  7. Download the image
Printing the cards

This was fairly simple, Adafruit have a nice library for the printer that can handle printing suitably sized images so all I needed to do was rotate the file to better fit the paper and resize it (which wasn't actually needed, because the cards are such low resolution already!)

You may not want to rotate the image, as you can see here the card from E7TT is still readable and takes up much less space than the full size card from OH5BM.

E7TT and OH5BM cards printed onto paper

You will also notice that many of the cards don't look great, obviously a thermal printer designed for text doesn't handle images with lots of colour and detail very well. Plus, I vastly overestimates the paper I had available / underestimated how many cards and how much paper they would need!

two strips of paper showing cards from EAGLE OM5XX F1RUM F4GUK S53AK

The script

This is far from clean code, but it does work, for me at least.

import requests
import re
import adif_io
import os
# you can cut these two if you don't want to print them out
from Adafruit_Thermal import *
from PIL import Image


BASE_URL = 'http://www.eqsl.cc'
baseURL = 'https://www.eQSL.cc/qslcard/DownloadInBox.cfm?'
username = ''
password = ''

# you can cut this bit if you don't want to print them out
printer = Adafruit_Thermal("/dev/ttyUSB0", 19200, timeout=5)

eqslPageURI = baseURL + 'UserName=' + username + '&Password=' + urllib.parse.quote(password)
eqslPage = requests.get(eqslPageURI)
adiURI = re.search('<A HREF="(.*)">.ADI file</A>', eqslPage.text)

adiData = requests.get('https://eQSL.cc/qslcard/'+adiURI.group(1))

qsos, header =  adif_io.read_from_string(adiData.text)

for qso in qsos:
    outputFile = 'cards/' + qso['QSO_DATE'] + '-' + qso['TIME_ON'] + '-' + qso['BAND'] + '-' + qso['MODE'] + '-' + qso['CALL'].replace('/','-') + ".png"
    print(qso)
    if (not os.path.isfile(outputFile)) :
        card_url = 'https://www.eqsl.cc/qslcard/GeteQSL.cfm?Callsign=' + qso['CALL'] + '&CallsignFrom=' + qso['CALL'] + '&QSOYear=' + qso['QSO_DATE'][0:4] + '&QSOMonth=' + qso['QSO_DATE'][4:6] + '&QSODay=' + qso['QSO_DATE'][6:8] + '&QSOHour=' + qso['TIME_ON'][0:2] + '&QSOMinute=' + qso['TIME_ON'][2:4] + '&QSOBand=' + qso['BAND'] + '&QSOMode=' + qso['MODE'] + '&UserName=' + username + '&Password=' + urllib.parse.quote(password)
        cardPage = requests.get(card_url)
        cardImageURL = re.search(' src="/CFFileServlet/_cf_image/([a-zA-Z0-9_\-.]*)"',cardPage.text).group(1)
        print(cardImageURL)
        with requests.get('https://www.eQSL.cc/CFFileServlet/_cf_image/' + cardImageURL, stream = True) as incoming:
            with open(outputFile, 'wb') as outgoing:
                for chunk in incoming.iter_content(chunk_size=16*1024):
                    outgoing.write(chunk)
        # you can cut this to the end if you don't want to print them out
        image = Image.open(outputFile)
        image = image.rotate(90, expand=True) # only if you want them bigger
        image.thumbnail([384,100000])
        image.save(outputFile+'-thermal.png')
        printer.printImage(outputFile+'-thermal.png')
Conclusion?

This is almost certainly pointless and a waste of paper, BUT it is a fun wa to review past contacts in a tactile way (I only keep electronic logs) and it is cool to see SWL reports too which I don't get any other way (that I know of...)

I'm sure there are things I could do to enhance the quality of the prints, probably by some sort of processing of the image before but I'm not sure exactly what that would be.

I'm also unsure what to do with the tens of meters of printed out QSL cards I now have, maybe decorate the shack somehow?

https://m0yng.uk/2021/08/Printing-my-eQSLs/
Raspberry Pi Pico Display Pride

I couldn't resist the raspberry pi pico any longer, and I also got a pico display to do with it. This is a pico sized hat that sits directly onto the pico and provides a nice full colour screen, RGB LED, and 4 buttons.

After playing with the demo code I wanted to try something myself, so I thought I'd try making a name badge of sorts, and using the pride flag as the background. Then I wanted to use the buttons, so made each button show a different flag. The LED also changes, trying to show some key colours from the flag.

It can show:

  • Pride rainbow
  • Trans
  • Intersex
  • Bi

In this quick demo I've made it run through the flags automatically.

GIF of Pico display showing the flags

Code uses the Pimoroni Pico Firmware

import time
import utime
import picodisplay as display

width = display.get_width()
height = display.get_height()

display_buffer = bytearray(width * height * 2)  # 2-bytes per pixel (RGB565)
display.init(display_buffer)

display.set_backlight(1.0)

display.set_led(255,255,0)
display.set_pen(255,255,0)
display.clear()

def clear():
    display.set_pen(0, 0, 0)
    display.clear()
    display.update()

def draw_trans():
    clear()
    stripe_height = int(height/5)
    display.set_pen(91,207,250)
    display.rectangle(0,0,width,stripe_height)
    display.rectangle(0,stripe_height*4,width,stripe_height)
    display.set_pen(245, 171, 185)
    display.rectangle(0,stripe_height,width,stripe_height)
    display.rectangle(0,stripe_height*3,width,stripe_height)
    display.set_pen(255, 255, 255)
    display.rectangle(0,stripe_height*2,width,stripe_height)
    display.set_led(255,0,255)
    display.set_pen(0,0,0)

def draw_pride():
    clear()
    stripe_height = int(height/6)
    display.set_pen(229,0,0)
    display.rectangle(0,stripe_height*0,width,stripe_height)
    display.set_pen(255, 141, 0)
    display.rectangle(0,stripe_height*1,width,stripe_height)
    display.set_pen(255, 238, 0)
    display.rectangle(0,stripe_height*2,width,stripe_height)
    display.set_pen(0, 129, 33)
    display.rectangle(0,stripe_height*3,width,stripe_height)
    display.set_pen(0,76,255)
    display.rectangle(0,stripe_height*4,width,stripe_height)
    display.set_pen(118, 1, 136)
    display.rectangle(0,stripe_height*5,width,stripe_height)
    display.set_led(100,100,100)
    display.set_pen(0,0,0)

def draw_intersex():
    clear()
    display.set_pen(255, 217, 0)
    display.clear()
    display.set_pen(122, 0, 172)
    display.circle(int(width/2), int(height/2), int(height/3))
    display.set_pen(255, 217, 0)
    display.circle(int(width/2), int(height/2), int(height/4))
    display.set_led(122, 0, 172)
    display.set_pen(0,0,0)

def draw_bi():
    clear()
    stripe_height = int(height/5)
    display.set_pen(214,2,112)
    display.rectangle(0,stripe_height*0, width, stripe_height*2)
    display.set_pen(155,79,150)
    display.rectangle(0,stripe_height*2, width, stripe_height)
    display.set_pen(0,56,168)
    display.rectangle(0,stripe_height*3, width, stripe_height*2)
    display.set_led(155,0,150)
    display.set_pen(0,0,0)


def draw_details():
    display.set_pen(0,0,0)
    display.text("Christopher", 5, 10, 100, 4)
    display.text("M0YNG", 20, 40, 100, 7)
    display.update()


while True:
    if display.is_pressed(display.BUTTON_A):
        draw_trans()
        display.text("Trans Rights!", 20,40,100,7)
        display.update()
        utime.sleep(2)
        draw_trans()
    elif display.is_pressed(display.BUTTON_B):
        draw_pride()
        display.text("Stonewall was a riot", 10,10,100,4)
        display.update()
        utime.sleep(2)
        draw_pride()
    elif display.is_pressed(display.BUTTON_X):
        draw_intersex()
        display.text("", 5, 10, 100, 7)
        display.update()
        utime.sleep(2)
        draw_intersex()
    elif display.is_pressed(display.BUTTON_Y):
        draw_bi()
        display.text("", 5, 10, 100, 7)
        display.update()
        utime.sleep(2)
        draw_bi()
    draw_details()
    utime.sleep(0.1)
https://m0yng.uk/2021/07/Raspberry-Pi-Pico-Display-Pride/
prefers-color-scheme

prefers-color-scheme is the magic sauce to get CSS to care about the user's preferred colour scheme - aka DARK MODE.

For a little while you have been able to toggle the theme on the HTTP version of my site between a dark theme and a light theme, but I wanted to respect your choice and present the theme you are most likely to want based on this CSS property.

Obviously this won't work with older browsers that don't support this property, or if you haven't set it, or something else is weird. But in these cases the CSS just gets ignored and you get my preference, a dark theme.

So, how does it work?

The CSS file has the default dark colours coded in as normal, which means they should always work as a fallback if this doesn't work, then we add some extra code, which overrides the dark colours if your browser presents a preference for a light theme.

@media(prefers-color-scheme: light) {
    body {
        background-color: #efefef;
        color: darkmagenta;
    }
    .themeToggles button,
    header,
    footer,
    h2,
    h3,
    a {
        color: darkmagenta;
    }
    img {
        border-color: darkmagenta;
    }
    *:focus, .themeToggles button:focus {
        background-color: #efefef;
    }
}

I don't do anything special if you prefer a dark theme, because that's the default and would be pointless extra code, although I have added a few comments into the CSS where those defaults are so I remember that may get overridden later and I need to make changes in other places too.

For now, I have just removed the ability to toggle the theme because I can't see a clean way to both follow prefers-color-scheme AND allow you to change it on the fly without just duplicating CSS. Hopefully this isn't an issue because we're trying to respect your preference so you're less likely to want to change the theme.

https://m0yng.uk/2021/07/prefers-color-scheme/
BashFu to show count of visitors today

'cos I can't document this in just one toot...

#!/bin/bash

# Tell the server hostname and today's date
echo "Visitors to $(hostname) on $(date +%d/%b/%Y)"
# Search nginx log file for today's date and slice it up to get just IP addresses, then sort them, then get just the unique ones, then count them
HTTPV=$(grep "\[`date +%d/%b/%Y`" /var/log/nginx/access.log |  cut -d" " -f1 | sort | uniq | wc -l)
# same for gopher
GOPHERV=$(grep "\[`date +%d/%b/%Y`" /var/log/gopher |  cut -d" " -f1 | sort | uniq | wc -l)
# same for gemini
GEMINIV=$(grep "`date +'%b %d'`" /var/log/gemini.log | cut -d" " -f11 | sort | uniq | wc -l)
# Tell the counts
echo "HTTP      Gopher  Gemini"
echo "$HTTPV    $GOPHERV        $GEMINIV"

Gives an output like this

Visitors to server.m0yng.uk on 10/Jun/2021
HTTP    Gopher  Gemini
43      2       6

A slightly more advanced version, this tries to get the data for yesterday too (although consistently fails for me with nginx probably due to logrotate), looks for stuff that says it is a bot, and tabulates the output for consistent output.

#!/bin/bash

# Tell the server hostname and today's date
echo "Visitors to $(hostname) on $(date +%d/%b/%Y)"

# Search nginx log file for today's date and slice it up to get just IP addresses, then sort them, then get just the unique ones, then count them
HTTPV=$(grep "\[`date +%d/%b/%Y`" /var/log/nginx/access.log | cut -d" " -f1 | sort | uniq | wc -l)
# What about yesterday?
HTTPY=$(grep "\[`date --date='yesterday' +%d/%b/%Y`" /var/log/nginx/access.log | cut -d" " -f1 | sort | uniq | wc -l)
# Count bots and crawlers 
HTTPVBOT=$(grep "\[`date +%d/%b/%Y`" /var/log/nginx/access.log | grep -iE 'bot|crawler' | cut -d" " -f1 | sort | uniq | wc -l)
HTTPYBOT=$(grep "\[`date --date='yesterday' +%d/%b/%Y`" /var/log/nginx/access.log | grep -iE 'bot|crawler' | cut -d" " -f1 | sort | uniq | wc -l)
# same for gopher (but not the bots, are there bots on gopher?)
GOPHERV=$(grep "\[`date +%d/%b/%Y`" /var/log/gopher | cut -d" " -f1 | sort | uniq | wc -l)
GOPHERY=$(grep "\[`date --date='yesterday' +%d/%b/%Y`" /var/log/gopher | cut -d" " -f1 | sort | uniq | wc -l)
# same for gemini
GEMINIV=$(grep "`date +'%b %d'`" /var/log/gemini.log | cut -d" " -f11 | sort | uniq | wc -l)
GEMINIY=$(grep "`date --date='yesterday' +'%b %d'`" /var/log/gemini.log | cut -d" " -f11 | sort | uniq | wc -l)
GEMINIVBOT=$(grep "`date +'%b %d'`" /var/log/gemini.log | grep -iE 'bot|crawler' | cut -d" " -f11 | sort | uniq | wc -l)
GEMINIYBOT=$(grep "`date --date='yesterday' +'%b %d'`" /var/log/gemini.log | grep -iE 'bot|crawler' | cut -d" " -f11 | sort | uniq | wc -l)

# Tell the counts
echo -e "\e[1;33m" # make it yellow
printf "%-7s%-7s%-7s%-7s%-7s\n" "HTTP" "Bot?" "Gopher" "Gemini" "Bot?" # %-7s = left aligned 7 characters wide string.
echo -e "\e[0m-----------------------------------" # stop being yellow
printf "%-7s%-7s%-7s%-7s%-7s\n" $HTTPV $HTTPVBOT $GOPHERV $GEMINIV $GEMINIVBOT
printf "%-7s%-7s%-7s%-7s%-7s%-9s\n" $HTTPY $HTTPYBOT $GOPHERY $GEMINIY $GEMINIYBOT "Yesterday"

exit

Gives an output like this

Visitors to server.m0yng.uk on 13/Jul/2021

HTTP   Bot?   Gopher Gemini Bot?   
-----------------------------------
585    498    0      9      0      
0      0      0      18     0      Yesterday
https://m0yng.uk/2021/06/BashFu-to-show-count-of-visitors-today/
Autopsy - Breaking a server

Ok, I have to hold my hands up here. I screwed up. I broke the mastodon.radio server and I shouldn't have.

I'm sorry.

Here I'll try to walk through what happened, and how I fixed it.

The initial mistake

I thought it would be a good idea to add nitter as a service to mastodon.radio, I thought as people using the server are actively NOT using twitter they might like a proxy to view the odd tweet, and the pubic nitter servers are very often overloaded.

This wasn't a mistake, but it did quickly lead to one, that then led to another, and then a cascading failure leading me to a fairly bad place.

Nitter needs nim - but it needs version >= 1.2, which isn't available in debian stable (at time of writing that's 0.19.4). Nor is it in the (relatively safe) backports repo (at time of writing that's 1.0.4). But testing has 1.4.0! So knowing that I can selectively install just one package from backports I thought I could do the same from testing.

Now would have been a good point to stop, and say it's not worth it. Or try docker, or something else.

Stupidity causes cascading failure

I added testing, and apt said that nearly everything needed updating. Which wasn't a shock. But wasn't what I planned.

I thought I would try "pinning" everything, debian has a concept of priorities where different packages can be sourced from, so I set up some preferences which put the testing files as a low priority.

The file looks a little like this

Package: *
Pin: release a=testing
Pin-Priority: 900

This kinda worked, apt no longer wanted to update everything.

So, I tentatively try running apt install again, this time asking to get nim from testing. This listed a bunch of dependencies, which I thought was probably too long but I didn't read it in full.

Now would have been a good point to stop. At this point I've broken it, I just don't know it yet.

I told apt to go ahead and install stuff. And it did.

Then I noticed that my connection to the xmpp server had gone, and I was getting lots of authentication failures in the logs. I restarted the server and the auth script (which uses python) failed. I tried running it manually and it was missing a library. So I tried installing it with pip and that failed too.

I think it was this point I decided to try backing out of this.

Making it even worse

If I remember correctly I tried to uninstall nim at this point, and then I tried to make apt downgrade things back to stable. This failed, and I got an ominous message amongst the failure.

error while loading shared libraries: libcrypt.so.1: cannot open shared object file: No such file or directory

So I tried again, but sudo said no

sudo: account validation failure, is your account locked?

I tried to ssh in again

kex_exchange_identification: read: Connection reset by peer

At this point I still had an SSH connection, but couldn't sudo, couldn't create a new connection, couldn't su, basically I couldn't do ANYTHING.

I could ssh OUT so I began backing up some data to another server (this one!)

I also made use of the VPS snapshot facility, and created a snapshot of the broken server.

Trying to recover

I sent an email to mythic-beasts who host the VPS and they were both helpful and a little pessimistic

that's probably quite tricky to recover from.

They suggested booting a live cd (virtual, of course) and I took this advice!

I booted systemrescuecd and connect via their VNC thing, which let me mount the filesystem. I used wget to download the libc6 deb which contains libcrypt.so.1 and manually unwrapped the file, and extracted the data over the server's filesystem.

I crossed my fingers and rebooted.

It worked! I was able to log in! (still using VNC, ssh wasn't running) and tried running apt again, which gave me a long list of broken packages. It suggested running apt --fixall, so I did. But it didn't work, saying that apt-listchanges was at fault. So I tried installing it again, but apt was still not happy and said to run fixall. Anything I did with apt said the same. apt uses python, and debian 10 expects python3.7, which it seemed I didn't have anymore. I couldn't install anything using apt, so I started downloading the packages manually and using dpkg to install them, which has the advantage of telling me about dependencies that are missing.

I don't know what went wrong, but at some point things stopped working. Almost every command triggered a "stack smashing detected" error and terminated, even ls!

So, I started restoring the snapshot.

Then I had dinner.

Very careful manual packages

Once the snapshot was restored, I booted up systemrescuecd again and this time tried to go straight into a chroot of the server, which didn't work because I didn't have libcrypt.so.1

So I backed out of that, extracted the file deb again, and tried the chroot again. This time apt worked! But still had many issues. So I carefully ran through the dependency tree of python3 on debian, and worked from the bottom up downloading the deb files (oh, did I mention I couldn't paste? So I was manually typing the URLs) and using dpkg to install them until I had python3 available as a command.

I then ran apt --fixall again, and it worked!

I then quickly removed the testing repo, removed the pinning, and ran apt update, then apt upgrade, then apt dist-upgrade, and then apt autoremove, until nothing else was complaining.

And I crossed my fingers, and rebooted.

Tidying up the mess

Some things still weren't working, but at least the server was running and mastodon had (mostly) started.

I had not removed the pinning for stable completely, and nodejs on debian is v10 and mastodon-streaming needs v12 so it wasn't starting. I removed the stable pin completely and apt installed v12. Huzzah! mastodon-streaming started!

Seen as this server is mastodon.radio I thought at this point we were ok, and I broke away to write this post.

ejabberd is still complaining, but it's not mission critical and I can fix it later.

https://m0yng.uk/2021/02/Autopsy---Breaking-a-server/
Gophers, Gemini, and new things

I've been playing with the Gopher Protocol and the Gemini Protocol and thought it would be interesting to make some things accessible over Gopher. I was interested both from a "gopher seems fun, maybe it could work over the radio" and "I have an old netbook here that might be interesting to use rather than a full laptop" and "this looks like cool tech". Here follows a story that could be used to define "Scope Creep" 😆

Mastodon over Gopher

Mastodon has a great API that lets you do everything, for some reason I had started writing a command line client for mastodon using this API (in Python) a while ago. It got as far as streaming the user's home timeline to the terminal, which is interesting but not all that useful.

Inspired by the idea of offering mastodon over Packet Radio I thought that gopher would be a good protocol to do this, I'm probably wrong but ¯\(ツ)

Using the code I already had I was able to extend it a little, and cut down other bits, to allow anyone to view the home timeline, public timeline, profile directory, and info about the server - all over gopher! This seemed to go down well when I tooted about it.

Assuming it hasn't fallen over or I didn't re-start it after a server reboot etc. you can give it a try at gopher://mastodon.radio:7777

It would be nice and a logical extension to replicate this to work over Gemini, especially as (in theory) you could "log in" by associating a client certificate with the server and an API key so you could view your own timeline, send toots, etc. all via Gemini. Maybe that's a thing for later...

My own website over Gopher

The next thing I wanted to try was making my own website available over Gopher. I use(ed?) Hexo as a static site generator for m0yng.uk which means I have a directory full of markdown files with some metadata, which seemed like a good starting point.

A quick search found some hopeful looking python libraries (such as markdown-full-yaml-metadata) that would probably allow me to find these files, extract the metadata, and then the rest was just some logic to glue it all together.

Initially I was using the GoFish gopher server, because my laptop uses Manjaro which is based on Arch and the Arch Wiki suggested GoFish. This worked OK until I wanted to share it and the server I had to hand runs Debian, which doesn't have GoFish available. So I tried pygoperd because that was suggested for Debian, but then that doesn't support IPv6. So I moved onto Gophernicus which is available in testing. But at least it means that my thing is tested with a few different options!

There wasn't too much to making this work, the basic logic is:

  1. Look for markdown files in a directory
  2. Read every file and extract the metadata (e.g. title and tags)
  3. Create a text file from the markdown, and save it somewhere sensible
  4. Keep a record of what pages you have and what their tags are
  5. Create some menus that list the things by date, tags, etc.
  6. Put all of that somewhere that a server can see

So, now you can view this site via Gopher! Pop over to gopher://m0yng.uk

Scope Creep - What about Gemini?

Now I wondered, I have basically done all the hard work here, it shouldn't be difficult to add md2gemini into the mix and start making Gemini stuff too.

It wasn't too much extra work, but at this point I also started to organise my code a little better, which made it easier to work with but was also more work. At this point I introduced chevron which let me use mustache templates rather than just concatenating strings together in the python code.

It was at this point that I really started to appreciate the appeal of Gemini. The base stuff isn't that different to Gopher, but the output is so much nicer to consume. Adding in the markdown-ish headings makes everything so much nicer! Plus I am keen to maximise accessibility, and suspect that fancy banners look good but are hard to understand if you are using a screen reader so I was mostly avoiding them which resulted in the gemini pages being a bit boring and flat. But Gemini makes headers into headings, which seems good for accessibility and generally making the pages nicer to look at and read.

Plus Gemini supports links better, sure they need their own line but a menu and a blog post can both have links and images linked from them easily using Gemini, but Gopher only supports that on gophermap pages (aka menus). I'm get to work out the best way of making images etc. work on the Gopher side of things, without hacking the existing markdown->html->txt flow I have for Gopher. Maybe I should go markdown->gemini->gopher?

So now I had a thing that would take my markdown files and generate a Gopher hole and a Gemini Capsule of my web site.

More Scope Creep - Hosting stuff

I asked my web host if they did or could support Gopher and Gemini as well as HTTP, they said "no", I'd need a VPS or something.

So now I have a VPS. I went for a fairly cheap option from Inception Hosting that gives me a full Debain 10 install, with IPv4 and IPv6, and more than enough power to run a Gopher and Gemini server.

Seen as I wanted to host Gopher, Gemini, and HTTP versions of my site at the same domain I have also moved my web site over to the new VPS, which foreshadowed the next bit of scope creep.

Setting up agate was pretty simple, I followed a guide by Chris Ware (gemini link) and was done pretty quickly.

Initially I used pygopherd, but that doesn't support IPv6 as far as I can tell, so I jumped onto Gophernicus and had a short battle with config files (and ended up emailing the package maintainer Ryan Kavanagh (gopher link) who was very patient and helpful) to get it working, but it was pretty simple really.

I also stood up nginx for the HTTP stuff (probably overkill, but I will probably be moving more things to this server later.) By far, this was the most painful to get working with certificates (yay for Let's Encrypt!) and virtual hosts and redirects from www etc.

Yet more scope creep - I could do HTML now too

For a while I've wanted to tweak the theme of my website, but I was scared off by the approach to theming used by Hexo. But in front of me I had some code that was already doing almost everything I needed, all I had to do was make some template files for HTML and add some more code to generate HTML pages too.

So I did.

Now I was reminded by how hard it is to make a really slick looking website (I don't think I've managed it), apparently it's been a while since I seriously wrote any HTML and CSS! I had some requirements of myself here:

  • Minimal and thus fast to load (less code, no web fonts)
  • Light and Dark themes (user selectable)
  • Monospace font, but also sans (user selectable)
  • Mobile and ... not-mobile friendly

So I set about making templates for HTML pages and a stylesheet for it and then adding code syntax highlighting and making it work well on mobile too and adding in theme toggles and this all took a while!

Keeping URIs consistent with the existing site was tricky, I'd been using underscores for whitespace but hexo uses hyphens, and it also collapses multiple hyphens (e.g. "thing - thing" becomes "thing-thing" not "thing---thing") and I've not worked out an easy way to do this so I'm just ignoring the problem for now, I doubt anyone has any of my pages bookmarked anyway.

I have attempted to implement some sort of feed, although I've ended up with rss not atom (so it's in a different place and format) and it only links to posts rather than contains them. I expect this won't be good enough for some, but it's a first step. And I have for for Gemini too, 'cos that's a thing right? (I don't know tbh.)

Anyway, it exists now, but is currently still kind-of dependent on Hexo to do things like creating pages and posts, although you can do all of that manually. I'll probably look at adding this functionality too.

A summary of the scope creep

What started as "I wonder if I can make my website visible over gopher?" became:

  • A somewhat extensible static site generator that creates Gopher (for three different servers), Gemini, and HTML
  • A new server

I would include screenshots of the different versions of the site, but you can see them yourself by visiting:

I'll probably share the code at some point (maybe on a git server on my new VPS?) but I doubt it will be of any use to anyone else, and it's far from polished at the moment.

Browsers?

An obvious thing to mention is how I access these things, what browsers do I use to view Gopher and Gemini things?

Mostly I use Lagrange but a mostly complete list would be:

  • Lagrange for Gopher and Gemini
  • Castor for Gopher and Gemini (but less)
  • Lynx for Gopher on the command line
  • Waffle for Gopher (but I've not had it working recently)
  • Ariane for Gemini on Mobile
  • Pocket Gopher for Gopher on Mobile
  • DeeDum for Gemini on Mobile
https://m0yng.uk/2021/02/Gophers,-Gemini,-and-new-things/
Building F4GOH DRA818 APRS Tracker
What is it?

TL;DR - a small low cost APRS tracker, which uses an arduino and a radio module.

Recently I was handed two kits with the understanding that if I could get both working, I could keep one.

F4GOH's blog post has more info, but not quite enough to make it easy to just pick up and build.

I have one working, now I'm going to make the other, and document the process to help anyone else do the same.

Parts List

NOTE: there are multiple versions, my PCB is marked with V1.2 and does have C8 and R7

The PCB looks like this Green PCB with F4GOH written on it Underside of Green PCB with F4GOH written on it

This is taken from the "Bill of Materials" for V2, from the zip file "DRAPRSV2", from F4GOH DRAPRS repo on github, but I've added the extra bits that my PCB has too.

Resistors (all surface mount)
  • 2 R1,R6 1K
  • 2 R2,R4 1.8K
  • 2 R3,R5 3.3K
  • 2 R10,R11 4.7k
  • 4 R7, R12-R14 10k
  • 2 R15,R16 330
Capacitors
  • 6 C1,C2,C5-C8 100nF (surface mount)
  • 1 C3 1uF
  • 1 C4 100uF
Integrated Circuits
  • 1 U1 DRA818 (surface mount)
  • 1 U2 ARDUINO NANO
Transistors
  • 1 Q1 2N7002 (surface mount)
Diodes
  • 1 D1 1N4007 (surface mount)
  • 1 D2 LED3MM (surface mount)
Miscellaneous
  • 1 L3 VK200 (Inductor - the bits of wire in a black thing)
  • 1 RV1 10K (potentiometer)
  • 1 J3 BNC/SMA (for connecting aerial)
  • 2 J1,J6 CONN-SIL10 (to join split boards)
  • 2 J4,J9 CONN-SIL4 (for connecting OLED screen and GPS)
  • 2 J7,J8 CONN-SIL6 (for the optional SD Card and other "accessories")
  • 1 J5 ALIM (for 5v power to the board)
  • Several standoffs / screws
  • 1 J2 CONN-SIL2 (Jumper to set output power. If present 0.2w, else 1w)
  • 16 PT1-PT8,PT10-PT17 ENTRETOISE (Probably the through hole mounting points?)
  • 1 PT9 GND (??)
Let's get soldering!

I have split this PCB in two, but you don't have to. I will refer to the arduino board (right half) and the TRX board (left half).

Surface Mount Capacitors

All the surface mount capacitors are the same, so let's start there.

  • C1 and C2 are on the top of the TRX board
  • C5, C6, C7, C8 are on the underside of the arduino board

PCB with capacitors highlighted

Surface Mount Resistors
  • R1 and R6 are 1K (1001) one is by C2 on TRX board and one by the LED on arduino board
  • R2, R4 are 1.8K (1801) both are on the TRX board bottom and top
  • R3 and R5 are 3.3k (3301) both are on the TRX board, close to R2 and R4
  • R10 and R11 are 4.7k (4701) both are on the underside of the arduino board
  • R7, R12, R13, R14 are 10k (1002) all are on the underside of the arduino board
  • R15 and R16 are 330 (3300) and are together on the underside of the arduino board

underside arduino PCB with resistors highlighted TRX PCB with resistors highlighted arduino PCB with resistors highlighted

Surface mount diodes
  • D1 is on the bottom on the TRX board, on mine the lettering is the "right way up" if it's oriented correctly
  • D2 is on the right side of the arduino board, on mine the green end of the LED is down
Surface mount Transistors
  • Q1 is on the TRX board

TRX PCB with diode and transistor highlighted

Through Hole Capacitors

Now we're onto the easy bits!

  • C3 is 1uF and on the top of the TRX board, the negative leg is on the outside edge (note: I installed mine on the other side and folded over to help reduce height)
  • C4 is 100uF and on the arduino board
Miscellaneous

I'm leaving the ICs for last...

  • L3 is an inductor, bottom left of the TRX board (note: I also installed this on the back side)
  • RV1 is a potentiometer, on the arduino board
  • J3 is the aerial connector on the top of the TRX board (note: also back side)
  • J1 is the 10 pin thing to join the split boards, it needs to go on the under side of the TRX board, but only if you have split it
  • J6 is the other side of that 10 pin thing, it goes on the underside of the arduino board
Arduino Time!

Now is a good point to add the arduino, before any other headers are added around it to make life difficult.

I wanted everything to be as low profile as possible, so I soldered my arduino directly onto the headers. Be sure you want this, and maybe test the arduino first, before you do it!

Arduino PCB with arduino and other components fitted

More Miscellaneous
  • J4 and J9 are 4 pin headers, both on the arduino board, one by the capacitor and one on the right
  • J7 and J8 are 6 pin headers, both on the arduino board, next to J4 and J9 (I didn't do J7)
  • J5 is the screw terminal thing, top of the arduino board

I don't plan to use the SD or low power, so left those headers off

DRA818

This is the main bit, the radio module.

A little tricky to get in position, because it's so big AND surface mount. Hold it in place with tweezers and solder one corner pin, then the opposite, and hope it's straight!

It's ok if the top three pins on the left connect to the shield, as they are all ground, but probably best to avoid? The bottom left pin is power, so DO NOT connect that to the shield.

TRX PCB with module fitted

Quick Tests

Some quick tests, look for continuity between:

  • The bottom pin of the TRX board and the bottom left side pin of the DRA818
  • The top pin of the TRX board and anything that should be grounded, e.g. DRA818 shield, SMA outer.
  • VCC terminal on the arduino board and the bottom pin of J8, VIN on the arduino
  • GND terminal on the arduino board and anything that should be grounded, e.g. gnd on the arduino, headers, etc.

Put your multimeter in diode test mode, and it should light the LED with positive on the top, but it will be very dim

Stacking the boards

This is fairly simple, but some tips:

  • The TRX board needs to be flipped, the DRA818 should be exposed on the underside with the arduino on the top
  • If you are using standoffs, put some on the bottom too so it can stand off the floor
  • If you put anything on the middle (like me) it may need insulating from the underside of the arduino board (I used some spare foam from the screen's packing)
  • Metal connectors will help carry GND, but plastic also work

Stacked PCBs viewed from the top Stacked PCBs viewed from the bottom

GPS and Screen

In most cases, the wires of these won't match the PCB nicely, so you will need to make up a wiring harness thing.

GPS

I have VK2828U7G5LF and only the middle four wires are needed. I won't give colours because they seem to change...

Counting from the left:

  1. E - not used
  2. G - Ground, goes to left pin on PCB
  3. R - RX on GPS, goes to TX on the PCB
  4. T - TX on GPS, goes to RX on the PCB
  5. V - Volts in, goes to the remaining pin on the PCB
  6. B - PPS signal, not used

Remember to put the heat shrink on before soldering stuff together!

Stacked PCBs with GPS modules connected in foreground

OLED Screen

For me this was easier because I didn't have to solder wires...

From the top of the arduino board:

  1. GND - GND on screen
  2. VDD on screen
  3. SDA on screen
  4. SCK on screen
Build Done!

At this point the hardware is done!

I'm not going to cover connecting power etc. as that is entirely up to you, and there are many ways it can be done.

Next time I'll talk about software.

https://m0yng.uk/2020/10/Building-F4GOH-DRA818-APRS-Tracker/
A simple hit counter

I don't have any analytics on this site, but it would still be nice to know if anyone is looking... and something reminded me of the hit counters we used to have back on the old web.

They were almost always a graphic which showed a number that increased every time that image was loaded, I don't think I ever made one so I'm not sure how exactly they worked.

I wondered if I could combine a bit of PHP, with a JSON file, some SVG, and good old HTTP headers to make a very simple page counter that would just work, and automatically handle new pages being added to the site.

Well, it seems that yes, I can.

Requirements / Limitations
  • I wanted this to be as small and fast as possible.
  • It should not need to know about a page before it starts counting.
  • Ideally it would be accessible, meaning anyone can know the value.
  • It should not require databases, etc.
The code The PHP

This is the code that does all the work, I'm sure it could be better!

<?php
// tell the browser to expect an svg
header('Content-Type: image/svg+xml');
// set some default text
$thisCount = 'ERROR!';
// if we have a referer
if (isset($_SERVER['HTTP_REFERER'])) {
    // and it is OUR website
    if (strpos($_SERVER['HTTP_REFERER'], 'm0yng.uk') > 0) {
        // strip out index.html so it is counted the same as /
        if (substr($_SERVER['HTTP_REFERER'], -10) === 'index.html') {
            $_SERVER['HTTP_REFERER'] = substr($_SERVER['HTTP_REFERER'], 0, strlen($_SERVER['HTTP_REFERER']) - 10);
        }
        // remove any trailing /s
        $_SERVER['HTTP_REFERER'] = rtrim($_SERVER['HTTP_REFERER'], '/');
// sanitize the string
        $pageHash = filter_var($_SERVER['HTTP_REFERER'], FILTER_SANITIZE_STRING);
        // load the json file of counts
        $counterData = json_decode(file_get_contents("counterData.json"), true);
        // if it already exists
        if (isset($counterData[$pageHash])) {
            // add one to it
            $counterData[$pageHash]++;
        } else {
            // otherwise, start counting at one
            $counterData[$pageHash] = 1;
        }
        // cast that number to a string for later use
        $thisCount = (string) $counterData[$pageHash];
        // save the counting
        file_put_contents("counterData.json", json_encode($counterData));
    }
}
// below we have the SVG ... and inject our count with left padded zeros into the text area using php
?>

<svg height="15px" width="50px" fill="#c9cacc" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 32 32"><text x="-34px" y="28px" font-family="monospace" font-size="32px"><?php echo str_pad($thisCount, 5, '0', STR_PAD_LEFT); ?></text></svg>
The HTML

To use the counter simply use an <img> tag like so

<img src="/counter.php" alt="page hit counter"/>
The JSON

It ends up looking a bit like this, which means I can grab the file and read it myself if I want, without having to visit every page to see a number.

{
  "https:\/\/m0yng.uk": 3,
  "https:\/\/m0yng.uk\/2020\/08\/The-Portable-Web": 2
}
Improvements?

It would be great to embed the SVG itself into the page, this would allow it to inherit font colours, etc. and be accessible as text to accessibility technology.

However, I'm not sure how to do that, whilst keeping it very simple to use.

https://m0yng.uk/2020/10/A-simple-hit-counter/
aptChecker - Part 2 - Remote Control

In part one I knocked up a thing to let me know when various things in my house needed updating. This works well as everything I wanted to keep an eye on is running Debian (or a variation of it) and is on the same network. But, what if I wanted to keep an eye on something not on the same network?

I could open the firewall so the remote system could directly POST updates, but that wouldn't be very secure, so I could add HTTPS but then I have to worry about certificates. I could get them to SSH info back, but then I have to worry about keys...

I ended up choosing to use SSH, and running commands on the remote system, this is fairly easy and I was already using key based authentication so didn't need extra setup. The only change needed was to allow the user to run apt update using sudo without a password.

This is simple, just add a line like this using visudo (possibly sudo visudo):

christopher ALL=(ALL) NOPASSWD:/usr/bin/apt update

That's it, all the setup needed on the remote system is done!

I then wrote a new script on myserver (on the internal network), called /etc/cron.daily/aptChecker-remote that looks like this:

#!/bin/bash

# this will run as root, so we need to use the correct ssh id
sshid="/home/christopher/.ssh/id_rsa"

# list of user+host combos to connect to
hosts=( "christopher@example.com" "root@another.example.server" "christopher@youget.the.idea" )

for host in "${hosts[@]}"
do
  echo ${host}
  # get fully qualified hostname
  hostname=$(ssh -i ${sshid} ${host} hostname -f)
  # run update
  ssh -i ${sshid} ${host} sudo apt update
  # count updates
  updatecount=$(ssh -i ${sshid} ${host} "apt list --upgradeable | wc -l")
  # correct for always getting at least one, even if there are none
  updatecount=$((updatecount - 1))
  echo $updatecount
  # get the current date+time
  datetime=$(date --iso-8601=seconds)
  # record the details
  curl -X POST -F "hostname=$hostname" -F "datetime=$datetime" -F "updatecount=$updatecount" http://myserver/aptCounter.php
done

I also updated the php that shows the results to tell me if the check was run today, or not:

$today = date('Y-m-d');
# ---
foreach ($aptCounterData as $host) {
  $host['goodbad'] = ($host['updatecount'] > 0 ? 'bad' : 'good');
  $host['updatedtoday'] = (strpos($host['datetime'], $today) === 0);
  echo "<tr><td>${host['hostname']}</td>";
  echo ($host['updatedtoday'] ? '<td class="good">today</td>' : "<td class='bad'>${host['datetime']}</td>");
  echo "<td class='${host['goodbad']}'>${host['updatecount']}</td></tr>";
}
https://m0yng.uk/2020/08/aptChecker---Part-2---Remote-Control/
aptChecker - Keeping too many things updated

It's an obvious problem to me that I have too many computers, and keeping on top of which have been updated recently is... not simple.

I've finally got around to making a semi solution, using a bash script and some PHP! Is there a better way? Almost certainly! Would it work easily and quickly on even a raspberry pi v1 and zero without stopping them doing the thing I want them to do? Maybe not.

Update

I've added some new bits (remote server checks), detailed in part two

Pager showing that radiopi-10 has 18 updates available

The Bits
  • A bash script which runs apt update and then apt list --upgradeable to count how many packages could be updated.
  • That file is in /etc/cron.daily/ so should run every day
  • A PHP file that gets info from the script and stores it in a json file, but also:
  • Sends a page via DAPNET if there are more than 0 packages
  • Provides a quick reference html page
  • Also json

The page is simple, and looks like this (I could probably format the date time, but meh.) Screenshot white text on black in a table showing update counts for different systems

The code aptChecker.sh
#!/bin/bash

hostname=$(hostname)
datetime=$(date --iso-8601=seconds)

apt update

updatecount=$(apt list --upgradeable | wc -l)

# correct for always getting at least one, even if there are none
updatecount=$((updatecount - 1))

echo $updatecount

curl -X POST -F "hostname=$hostname" -F "datetime=$datetime" -F "updatecount=$updatecount" http://myserver/aptCounter.php
aptCounter.php
<?php

$aptCounterData = json_decode(file_get_contents("aptCounter.json"), true);
if (!empty($_POST)) {
    $newEntry = (object) [
        "hostname" => (string) $_POST['hostname'],
        "datetime" => (string) $_POST['datetime'],
        "updatecount" => (int) $_POST['updatecount'],
    ];

    $hostname = $newEntry->hostname;

    $aptCounterData[$newEntry->hostname] = $newEntry;

    file_put_contents("aptCounter.json", json_encode($aptCounterData));
    if ($newEntry->updatecount > 0) {
        echo $newEntry->updatecount;
        $url = 'http://www.hampager.de:8080/calls';
        $data = array('text' => "M0YNG: $newEntry->hostname has $newEntry->updatecount updates available.", 'callSignNames' => ['m0yng'], 'transmitterGroupNames' => ['UK-ALL'], 'emergency' => 'false');
        $auth = base64_encode('m0yng:mypassword');

        $options = array(
            'http' => array(
                'header' => "Content-type: application/json\r\nAuthorization: Basic $auth",
                'method' => 'POST',
                'content' => json_encode($data),
            ),
        );
        $context = stream_context_create($options);
        $result = file_get_contents($url, false, $context);

        var_dump($result);

    }

    echo "Done.";
    exit();
} else if ($_SERVER['QUERY_STRING'] == 'format=json') {
    header('Content-Type: application/json');

    echo json_encode($aptCounterData);
} else {
    usort($aptCounterData, function ($a, $b) {return $a['updatecount'] < $b['updatecount'];});
    ?>

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="initial-scale=1.0">
  <title>aptChecker Counts</title>
  <style>
* {
  box-sizing: border-box;
}

body {
  background-color: #1d1f21;
  color: #c9cacc;
  font-family: "Fira Code", monospace;
}

main,
header {
  max-width: 70rem;
  padding: 1rem;
  margin: 0 auto;
}

table {
  width: 100%;
  text-align: left;
}
table td {
  padding: 0.5em;
}
table td:last-of-type {
  font-weight: bold;
}

table td.good {
  color: #8f8;
}
table td.bad {
  color: #f88;
}
  </style>
</head>
<body>
<header>
<h1>aptChecker Counts</h1>
</header>
<main>
<table>
<tr>
<th>Hostname</th><th>DateTime</th><th>Update Count</th>
</tr>
<?php
foreach ($aptCounterData as $host) {
        if (host['updatecount'] > 0) {

        }
        $host['goodbad'] = ($host['updatecount'] > 0 ? 'bad' : 'good');
        echo "<tr><td>${host['hostname']}</td><td>${host['datetime']}</td><td class='${host['goodbad']}'>${host['updatecount']}</td></tr>";
    }
    ?>
</table>
</main>

</body>
</html>

<?php
}
?>
https://m0yng.uk/2020/08/aptChecker---Keeping-too-many-things-updated/
The Portable Web?

Recently I discovered some spare parts from past projects, and ended up with this, a battery powered Raspberry Pi Zero W. But what to do with it?

A Green case with ZeroLipo Hat and LiPo cell connected

A few ideas came to mind or were suggested, such as a portable DMR hotspot, POCSAG transmitter, and others. But one idea came together from a few other bits.

What if I could have a WiFi hotspot that served content, rather than the Internet? What if the Pi was an isolated network that followed me around?

Basic Setup

The basic setup has the following parts

  • An open WiFi hotspot (using RaspAP)
  • A "Captive Portal" to introduce users to the "Network" (using NoDogSplash)
  • Lighttpd to host stuff on HTTP
  • dnsmasq to resolve domains to our Pi
  • USB network gadget mode to load content etc.

I won't go into detail of how this is configured, because it isn't anything special. Basically I set a few domains to resolve to the fixed IP of the Pi, and lighttpd is set to serve static files from the PI's SD.

When a user joins the network they are greeted with a page introducing the idea, and a button to connect. They should then be directed to the custom domain home.portableweb which provides some more info and links to services available on the Pi.

What can it do?

Not much, yet!

My website

I have a full mirror of my website on the Pi. As it is all static html files this is really easy to do, I just copy the files to the Pi and it "Just Works".

Cheltenham Amateur Radio Association News Archive

The CARA website is larger, and more complex. However, I have a simple page with some info on the club, and an archive of CARA News in PDF format.

A guestbook

I've made a very basic PHP guestbook that lets users read and post messages for me, and future users. It is very simple and just stores everything in a json file.

USB Sync

The Pi is configured to act as a USB network "gadget" when connected to another computer using the "data" USB port. This way I can run a small bash script that runs a handful of rsync commands to sync updates to the various "sites" to the Pi, and copy the guestbook off the Pi.

But Why?

Honestly? Because I thought I could, but wasn't sure, and it seemed fun.

Maybe you are reading this whilst connected to my "Portable Web"? If so, please put something in my guestbook and tell me what you think!

This is also a kind of experiment. It seems that every website these days has huge piles of javascript, tracking, cookies, adverts, and requires a farm of servers to make one page display. This is different, one tiny computer, no infrastructure, just you and me! That your access to the information goes away when the two of us are no longer close makes the experience mean more. Maybe. I don't know, but it seemed worth exploring.

Being an open WiFi, and the impracticality of using HTTPS, everything is unencrypted. My guestbook has some security in that PHP filters out "bad stuff" the user submits, but basically this is all open. Which suits the experimental nature of amateur radio quite nicely. How long that will even be practical, I don't know, I'm sure browsers will soon start warning users not to touch anything that isn't HTTPS.

Are "portable webs" going to take over? I seriously doubt it.

Could they become a thing? Maybe? Has someone else already done this before, I'm sure they have! Could they be a fun way to provide more information at events, clubs, etc., maybe. (the Physical Web sort of does this, but in a more "cloud" way) Could I find a better name for it? I really wish I had done before using it so many times...

What next?

I want to add some more services to the Pi. Some ideas are:

  • Telnet server to play Snakes and Ladders
  • Gopher server with similar content to the http stuff. I'd probably need to provide a gopher client too!
  • An e-paper display so I can see if there are new entries in the guestbook, connected users, etc.
  • Some live info on the Pi, such as uptime, for the users to see (as proof it really is real?)
  • Some basic contact logging software that would show what I'm doing when /P and then sync with my main log later
  • A "Portable Web Protocol" that can let units chat or something? Like StreetPass for the 3DS?
  • Improve the DNS so it catches non existent domains and returns you to some help, rather than just failing.
  • A way to trick Android (and others) to think they DO have Internet, so they don't use use mobile data and ignore the Pi
  • More info on Amateur Radio, maybe clone Wikipedia? Some videos from Essex Ham?
Conclusion

In the end, this is probably pointless and I'll never have anyone spontaneously use it, and I doubt anyone else will try to replicate the idea.

However, it was fun to build, and with some hot glue to hold the wire and LiPo in place it looks pretty neat!

A Green case with ZeroLipo Hat and LiPo cell glued behind

If you do have thoughts, come talk to me on mastodon! I'm @M0YNG@mastodon.radio

https://m0yng.uk/2020/08/The-Portable-Web/
APRS to Mastodon

Who doesn't want to post messages to a social network using APRS?

It's an idea I've been not-really working on for a while, and I've had various iterations.

One would watch the log files of APRX and toot weather reports heard off the air.

This one connects to the APRS Internet Stream and lets people send an APRS message to M0YNG-15, then have it tooted by @APRSBot (it toots "Followers only" so it might appear silent.)

The code is fairly simple, and almost certainly has issues!

The Code publicTooter.py
#!/usr/bin/env python3

"""
APRS -> Fediverse gateway
Maybe.

M0YNG 2020
"""
# imports (obviously)
import aprslib
import configparser
from mastodon import Mastodon

# Load the config
config = configparser.ConfigParser()
config.read('config.ini')

# hashes of already tooted messages
pastToots = []

# connect to mastodon
mastodonBot = Mastodon(
    access_token=config['mastodon']['access_token'],
    api_base_url=config['mastodon']['app_url']
)

# function to check and toot APRS packets
def callback(packet):
    if 'addresse' in packet and packet['addresse'] == config['aprs']['callsign'] and packet['format'] == 'message':
        # Construct toot text
        tootText = 'From ' + str(packet['from']) + ' via ' + \
            str(packet['path']) + '\n\n' + packet['message_text']
        tootHash = hash(tootText)
        if tootHash not in pastToots:
            # log it
            print(tootText)
            # toot it
            postedToot = mastodonBot.status_post(tootText)
            # log it
            print(postedToot['url'] + '\n')
            # record it
            pastToots.append(tootHash)
            # let the user know it worked
            AIS.sendall(config['aprs']['callsign'] +
                        '>APRS,TCPIP*::'+str(packet['from']) + ' : Message tooted!')
        else:
            print('Already tooted this message')


# connect to the stream
AIS = aprslib.IS(config['aprs']['callsign'], passwd=config['aprs']['passcode'])
AIS.connect()
# do something with each packet
AIS.consumer(callback)
config.ini
[mastodon]
access_token = youraccesstoken
app_url = https://your.domain.here
[aprs]
callsign = callsign-ssid
passcode = passcodde
The Future

Things this could do;

  • Have "registered users" who's messages could be tooted by specific accounts, e.g. if it comes from M0YNG it gets tooted by @M0YNG-APRS
  • Be used at a hamfest or club event, and display the messages somewhere fun
https://m0yng.uk/2020/08/APRS-to-Mastodon/
Owntracks in Docker

For a while I've used Traccar for friendly stalking of things, and it works well. But it is very feature rich, which isn't always what you want.

I'd heard good things about Owntracks, and it seemed to have a good balance of features and performance.

However, the documentation is as far as I can tell mostly a list of things you can do, rather than details of how to actually set anything up, and the project appears to be a collection of technologies rather than one coherent offering.

I have got the thing working, but it was a pain, so I've tried to write it down here, so I can try and do it again later.

NOTE! I have not tested this - just written down the config I ended up with, so it might not work, or may need steps doing in a different order.

What we're going to do

I've used a €2 VPS running Debian 10 for this, but it should work on other similar setups.

At the end of the day we should have:

  • Multiple Android phones
  • Who can see the location of each other
  • A web UI where we can see history of locations

We'll need to setup;

  • MQTT in docker-compose
  • Owntracks Recorder in docker-compose
  • Owntracks UI in docker-compose
  • nginx proxying HTTP
  • Encrypted connections using Let's Encrypt
  • User accounts for access to the web UI and for the clients
  • Created some "user cards"

I assume you can / have already:

  • Setup your server with Debian
  • Installed docker-compose
  • Installed nginx
  • Setup your domain
  • Installed Certbot
nginx + reverse proxy + basic auth + certificates

There isn't much to getting certificates, just use certbot to get the certificate for your domain.

I use nginx to reverse proxy the docker containers, but also provide some security because the owncloud services don't have any concept of what a user is (MQTT does), so we'll use basic auth to stop anyone we don't want seeing our location. Some guides put this just on the API, but I want it on the entire domain. This also gives us https, which we'd not get otherwise.

All of this config was put in the default site, at /etc/nginx/sites-available/default

server {
  auth_basic              "OwnTracks";
  auth_basic_user_file    /usr/local/etc/nginx/owntracks.htpasswd;

To create the first user run

htpasswd -c /usr/local/etc/nginx/owntracks.htpasswd christopher

To add more users, drop -c so:

htpasswd /usr/local/etc/nginx/owntracks.htpasswd anotheruser

You may need to apt install apache2-utils to get the htpasswd command.

The following configuration was tweaked from elsewhere, but I can't remember which guide now (I went through a lot!)

location /owntracks/ws {
    rewrite ^/owntracks/(.*)    /$1 break;
    proxy_pass      http://127.0.0.1:8083;
    proxy_http_version  1.1;
    proxy_set_header    Upgrade $http_upgrade;
    proxy_set_header    Connection "upgrade";
    proxy_set_header    Host $host;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
}

location / {
    proxy_pass http://127.0.0.1:8080/;

    proxy_http_version  1.1;
    proxy_set_header    Host $host;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header    X-Real-IP $remote_addr;
}

location /owntracks/ {
    proxy_pass      http://127.0.0.1:8083/;
    proxy_http_version  1.1;
    proxy_set_header    Host $host;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header    X-Real-IP $remote_addr;
}

# OwnTracks Recorder Views
location /owntracks/view/ {
      proxy_buffering         off;            # Chrome
      proxy_pass              http://127.0.0.1:8083/view/;
      proxy_http_version      1.1;
      proxy_set_header        Host $host;
      proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header        X-Real-IP $remote_addr;
}
location /owntracks/static/ {
      proxy_pass              http://127.0.0.1:8083/static/;
      proxy_http_version      1.1;
      proxy_set_header        Host $host;
      proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header        X-Real-IP $remote_addr;
}
# HTTP Mode
location /owntracks/pub {
    proxy_pass              http://127.0.0.1:8083/pub;
    proxy_http_version      1.1;
    proxy_set_header        Host $host;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header        X-Real-IP $remote_addr;

    # Optionally force Recorder to use username from Basic
    # authentication user. Whether or not client sets
    # X-Limit-U and/or uses ?u= parameter, the user will
    # be set to $remote_user.
    proxy_set_header        X-Limit-U $remote_user;
}
Dock some containers

Awesome, let's get the actual stuff running.

Owntracks is a bit of a veneer, mostly the work is done by MQTT which handles all the location data, and passes it between clients, etc.

I'd suggest creating a directory for all of this, and then make a few directories to put stuff into;

mkdir recorder-data ui-data mosquitto-data

Then we'll need a docker-compose.yml file has three containers, one for the recorder (which records locations), one for mosquitto (which is MQTT and gets and shares locations), and one for the fancy UI (which has more features that the recorder UI);

version: "3"

services:
  otrecorder:
    image: owntracks/recorder
    ports:
      - 127.0.0.1:8083:8083 # only expose the unencrypted connection locally (so we can proxy it)
    volumes:
      - ./recorder-data/config:/config
      - ./recorder-data/store:/store
    restart: unless-stopped

  mosquitto:
    image: eclipse-mosquitto
    ports:
      - 8883:8883 # expose the TLS port to the world
    volumes:
      - ./mosquitto-data/data:/mosquitto/data
      - ./mosquitto-data/logs:/mosquitto/logs
      - ./mosquitto-data/conf:/mosquitto/config
      - /etc/letsencrypt:/mosquitto/certs #this lets us use the let's encrypt certs directly
    restart: unless-stopped

  owntracks-ui:
    image: owntracks/frontend
    ports:
      - 127.0.0.1:8080:80 # only expose the unencrypted connection locally (so we can proxy it)
    volumes:
      - ./ui-data/config.js:/usr/share/nginx/html/config/config.js
    environment:
      - SERVER_HOST=otrecorder # the UI needs to know where the recorder is
      - SERVER_PORT=8083
    restart: unless-stopped

We'll need to create some configs to get us going too;

recorder-data/confg/recorder.conf
#(@)ot-recorder.default
#
# Specify global configuration options for the OwnTracks Recorder
# and its associated utilities to override compiled-in defaults.

OTR_TOPICS = "owntracks/#"
OTR_HTTPHOST = "0.0.0.0" # ideally this would be IPv6 too...
OTR_HOST = "your.domain.here"
OTR_PORT = 8883
OTR_USER = "recorder" # the user for connecting to mosquitto
OTR_PASS = "password" # the password for that user
OTR_CAPATH = "/etc/ssl/certs/" # where can it find a root certificate to connect using TLS?
mosquitto-data/conf/mosquitto.conf
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log

allow_anonymous false # require the user to log in
password_file /mosquitto/config/passwd # where to find the users/passwords

listener 8883 # set the listener on the default TLS port
cafile /mosquitto/certs/live/tracks.your.domain/chain.pem
certfile /mosquitto/certs/live/tracks.your.domain/cert.pem
keyfile /mosquitto/certs/live/tracks.your.domain/privkey.pem
ui-data/config.js

(I don't use this, but you might want to)

// Here you can overwite the default configuration values
window.owntracks = window.owntracks || {};
window.owntracks.config = {};

The first run will probably fail, because we haven't created our recorder user in mosquitto yet.

There are two ways to do that, both are a pain.

  1. Run the mosquitto container, and then exec into it, and create the user there
  2. Install required software outside the container, and create the user outside
Create Users

Let's create some users so we can record and read locations, we'll do this inside the mosquitto container, so run:

docker-compose exec mosquitto sh

Then we can create our first user with:

mosquitto_passwd -c /mosquitto/config/passwd recorder

Like with htpasswd we need to use -c to create the file the first time, then drop it for more users

mosquitto_passwd /mosquitto/config/passwd christopher

To be sure, I'd suggest grabbing a copy of the generated file just in case docker doesn't magic it outside the container correctly, just run this and copy and paste the output somewhere.

cat /mosquitto/config/passwd

Be sure to put the new password for the recorder user in the config file.

You will need to completely restart the containers when config files are changed, I do that with this incantation:

docker-compose down && docker-compose up -d && docker-compose logs -f

Which means I can be sure everything shuts down, then starts up fresh (as a daemon with -d), but I can still see the logs. Crucially I can stop seeing the logs without stopping the containers.

If we want the users to be able to use the UI we'll also need to create accounts for nginx, so don't forget to do that if you haven't already.

Setup Android clients

It's not obvious how to configure your client apps to connect, so for quick reference:

  1. Install and open the "Owntracks" app
  2. Open the menu in the left
  3. Select "Preferences"
  4. Select "Connection"
  5. Set "Mode" to "MQTT"
  6. Set "Host" to
  7. host: your full domain
  8. port 8883
  9. do not use websockets
  10. Set "Identification" to
  11. username and password as you configured for mosquitto
  12. "device ID" something like "phone" (it will become the full user/id e.g. christopher/phone)
  13. "tracker ID" whatever you like, maybe an initial and number, e.g. "c1"
  14. Set "Security" to TLS, but leave the rest blank (our phone should trust Let's Encrypt certificates)
  15. Leave "Parameters" empty
  16. Tap the i icon to see any status / error messages

Note: Some guides say to use HTTP for connection from the android app. This does work, but it does not let you see the location of other users in the app. This was the key feature I needed, so wasted time trying to work out why it didn't work.

Make users look nice

In theory, at this point we should be able to see locations on the UI, and on the app. But it won't be pretty, just c1 or similar.

However, we can create "cards" that represent our users in a nicer way. We'll want an image, and the image2card script:

wget https://github.com/owntracks/recorder/raw/master/contrib/faces/image2card.sh
./image-card.sh user-image.png UserName > username-card.json
mosquitto_pub -t owntracks/user/phone -f username-card.json -r -p 8883 --capath /etc/ssl/certs -h yourtracker.url -u recorder -P recorderpassword
The end bit

Hopefully you should now have the ability to see where you are, and where other people are too. Yay!

If not, sorry. Maybe I missed something, maybe I screwed up an instruction. I've not actually tested this guide, just extracted what I have that does work.

https://m0yng.uk/2020/08/Owntracks-in-Docker/
Reverse Engineering the Baofeng dm1702 codeplug
A note from the future that goes at the start

I started working on this nearly two month ago, and I've not had time to get any further with it. So I'm going to post this now, incomplete as it is, in the hopes it might be useful to someone else, or my future self if I get back to it at some point.

Some background

Recently I was too tempted by the seemingly very good price of the Baofeng dm1702 DMR radio.

I paid £65 for the radio, which included delivery, which seemed reasonable considering the features

  • 2m + 70cm
  • Tier II DMR
  • Analogue FM
  • GPS
  • 5w output
  • Programming over standard USB!

I didn't do enough research though, I just assumed that there would be tools available to help me code it up in Linux, as there are for so many DMR radios, but sadly all I can find right now is some stuff to write data to and from the radio, which works, but doesn't help me change any settings like dmrconfig does for my Anytone.

So I resorted to the official software, which only works in windows, which means I had to kick my offline VM into gear. But oh my, is that software bad, like, really, really bad.

When editing a table of channels, you can't tab between them, it sorts by name so adding a new row goes wherever the default text fits in the sorting order, etc. etc. etc.

It is so bad I decided to break out a hex editor and try to work out what is going on in the codeplug itself.

An initial look

Now, I'm not an expert at reverse engineering anything, so I don't expect to be able to actually do this task…

A first look wasn't promising, there is lots of empty space, lots of FFFF, lots of repeating data that doesn't appear to store anything meaningful.

Next I made one small change to the codeplug, and tried to diff it, here I have changed my hotspot frequency from 438.8MHz to 136.12345MHz on RX and 444.67899 on TX:

$ diff  <(xxd codeplug-justfreq.data) <(xxd codeplug.data)
833c833
< 00003400: 4523 6113 9978 4644 4000 0000 0000 1100  E#a..xFD@.......
> 00003400: 4523 6113 9978 4644 4200 0000 0000 1100  E#a..xFDB.......

Thanks to a great tip from SP6MR who identified the values as being stored as unsigned 32bit I started to get somewhere. Maybe.

A change of tense

You might notice a tense change now, as I'm writing this post as I work, mostly to keep notes for myself!

We can also see that changing the TX power level from low to high changes the value after the frequency from 40 to 42

< 00003400: 4523 6113 9978 4644 4000 0000 0000 1100  E#a..xFD@.......
> 00003400: 4523 6113 9978 4644 4200 0000 0000 1100  E#a..xFDB.......

Changing the timeslot from 2 to 1 and the last chunk changes from 11 to 10

< 00003400: 4523 6113 9978 4644 4000 0000 0000 1100  E#a..xFD@.......
> 00003400: 4523 6113 9978 4644 4200 0000 0000 0100  E#a..xFDB.......

But wait! changing the colour code from 1 to 2 makes that 11 or 10 into 12 or 02, so we must be storing the timeslot in one bit, counting from zero

< 00003400: 4523 6113 9978 4644 4000 0000 0000 1100  E#a..xFD@.......
> 00003400: 4523 6113 9978 4644 4200 0000 0000 1200  E#a..xFDB.......

Which makes me wonder about our conclusion for the TX power…

< 00003400: 4523 6113 9978 4644 4000 0000 0000 1100  E#a..xFD@.......
> 00003400: 4523 6113 9978 4644 0000 0000 0000 1200  E#a..xFD........

Based on that, the 40 is actually 4 for DMR/Digital and 0 for analogue, with the other bit storing the TX power level as 0 or 2.

We also see another line change much further through the file:

< 0001e020: 000e 0009 000b 0100 000d 0100 0000 ffff  ................
> 0001e020: 000e 0009 000b 0100 000d 0000 0000 ffff  ................

Only one bit seems to change though, so I don't know what it's for (yet?)

Next I changed it back to a digital channel, and set the "TX Contact Name"

< 0001e020: 000e 0009 000b 0100 000d 0100 0000 ffff  ................
> 0001e020: 000e 0009 000b 0100 000d 0005 0000 ffff  ................

It seems that the 6th chunk has some bits that changes to a number referring to the contact (somehow).

Around 0001F000 we see what looks like the name of contacts

0001f000: ffff 5357 204f 6869 6f20 4b34 5553 4400  ..SW Ohio K4USD.
0001f010: 0000 ffa2 7a00 04ff ffff 3331 3035 3537  ....z.....310557
0001f020: 204d 6169 6e00 0000 0000 ff1d bd04 04ff   Main...........
0001f030: ffff 3331 3639 204d 6964 5765 7374 0000  ..3169 MidWest..
0001f040: 0000 ff61 0c00 04ff ffff 4172 6561 2038  ...a......Area 8
0001f050: 2033 3130 3938 0000 0000 ff7a 7900 04ff   31098.....zy...
0001f060: ffff 3331 3020 5441 4300 0000 0000 0000  ..310 TAC.......
0001f070: 0000 ff36 0100 04ff ffff 3331 3120 5441  ...6......311 TA
0001f080: 4300 0000 0000 0000 0000 ff37 0100 04ff  C..........7....
0001f090: ffff 3331 3220 5441 4300 0000 0000 0000  ..312 TAC.......
0001f0a0: 0000 ff38 0100 04ff ffff 3331 3030 2055  ...8......3100 U
0001f0b0: 5341 0000 0000 0000 0000 ff1c 0c00 04ff  SA..............
0001f0c0: ffff 3933 204e 2041 6d65 7269 6361 0000  ..93 N America..
0001f0d0: 0000 ff5d 0000 04ff ffff 3331 3339 204f  ...]......3139 O
0001f0e0: 6869 6f20 5769 6465 0000 ff43 0c00 04ff  hio Wide...C....
0001f0f0: ffff 576f 726c 6445 6e67 6c69 7368 2039  ..WorldEnglish 9

Each Name seems to be 16 characters long, and have something after the text. This repeats for 800 calls, ending at 00023B40, where we have FF until 00024000 where we get 00 until 00025020.

Changing the call ID causes a change, but it's not clear how the data is stored, it doesn't appear to be unsigned 32bit. Here I've changed the ID from 310 to 111

< 0001f070: 0000 ff36 0100 04ff ffff 3331 3120 5441  ...6......311 TA
> 0001f070: 0000 ff6f 0000 04ff ffff 3331 3120 5441  ...o......311 TA

Based on Andy Valencia's keen eyes the ID is being stored in "endian shuffled order", as can be seen here with the ID 11111111 as c7, then 8a, and a9, read each two together, but backwards and we get 0xa98ac7

0001f070: 5354 ffc7 8aa9 04ff ffff 3331 3120 5441  ST........311 TA

If we change the ID to 11111113 we have even more confidence we're doing it right

0001f070: 5354 ffc9 8aa9 04ff ffff 3331 3120 5441  ST........311 TA
Structure of a contact

At this point, I think the structure of a contact is:

  • 16 bytes text (Name)
  • 1 byte padding/space (FF)
  • 3 bytes in "endian shuffled order" (call ID)
  • 1 byte either 03 (Private Call) 04 (Group Call) or 05 (All Call)
  • 3 bytes padding/space (FF FF FF)

Making 24 bytes in total per contact.

Structure of a memory
  • 4 bytes unsigned 32bit (RX Frequency)
  • 4 bytes unsigned 32bit (TX Frequency)
  • 0 for analogue, 4 for digital.
    • Add 2 if "RX Only" (2 or 6)
    • Add 1 if Squelch is "Stringent"
  • A bit for power, Channel Spacing, and "Lone Worker"

    • 0 for low power, 4 for high power.
    • If "Lone Worker" 8 for low and a for high power.
    • 0 Low + Narrow spacing (Analogue) OR Low Digital Power
    • 1 Low + Wide spacing (Analogue)
    • 2 High + Narrow spacing (Analogue)
    • 3 High + Wide spacing (Analogue)
    • 4 High Digital Power
    • 8 Low + Narrow spacing (Analogue) + Lone Worker OR Low Digital Power + Lone Worker
    • 9 Low + Wide spacing (Analogue) + Lone Worker
    • a High + Narrow spacing (Analogue) + Lone Worker OR High Digital Power + Lone Worker
    • b High + Wide spacing (Analogue) + Lone Worker
  • 8 for "Auto Scan Start", or 0 for not (only used if scan list set)

  • 1 for the "Scan List" reference, or 0 if none
  • 00 Is for "TX Admit"
    • 00 "Always" (Analogue or Digital)
    • 01 "Analogue Channel Free"
    • 02 "Analogue CTCSS/CDCSS Correct"
    • 03 "Analogue CTCSS/CDCSS Incorrect"
    • 08 "Digital Channel Free"
    • 10 "Digital Color Code Free"
  • 8 for "Emergency Alarm Indication", c if also "Emergency Alarm ACK", e if also "Emergency Call Indication", 2 if not "Alarm" but "Call"
  • 1 for the "Emergency System" reference, or 0 if none
  • 0 - Doesn't appear to change
  • 0 - Doesn't appear to change
  • 0 for "Private Call Confirmed" off, 2 for on. 3 for "Short Data Significance Bit" in "Confirming Mode", 1 if "Private Call Confirmed" is off
  • 0 - Doesn't appear to change
  • 0 for Slot 0, 1 for Slot 2, 3 for "Double Capacity Mode"
  • 0 - f for the colour code
  • 0 for no encryption, 8 for encryption on
  • 0 - f for the Encryption Key reference

3410 offset

  • 00 - ff "GPS System" reference
  • 00 - ff "RX Group List" Reference
  • 1 - Doesn't appear to change
  • 0 or 8 ??
  • ff ff "CTCSS/CDCSS Decode" in endian shuffled order (e.g. 70 06 for 67.0, 41 25 for 254.1) ffff if none.
  • ff ff Same again for Encode.
  • 1 for VOX, 0 if not
  • 0 - Doesn't appear to change
  • 8 for "PTT ID Display", 0 for off
  • 000 - Doesn't appear to change
  • 0 if "Talk Around Status" is OFF, 1 if OFF. Add 4 if ENABLED (4 and 5)
  • 000 - Doesn't appear to change
  • 0000 0000 - Doesn't appear to change

0001e020 offset

  • 3 bytes 0
  • 0100
  • 0000
  • 0
  • 0
  • 00 - ff "TX Contact Name" reference
Make a memory by hand

In theory we know enough to try making a memory by hand now, I'm going to try GB3CG, a local analogue repeater on 2m.

  • 145.725MHz TX
  • 145.125MHz RX
  • ‘J’ 118.8 Hz CTCSS
  • Analogue
  • High Power + Narrow spacing
  • TX admit always
  • everything else off

I think it will look like this:

0025 5714 0025 5114 0200 0000 0000 0000
0000 1881 1188 1000 0000 0000 0000 0000

I manually edited the codeplug in Okteta and wrote it to the radio, and it half worked!

CTCSS was wrong, it was enabled but had the wrong value, so I changed it on the radio and read back the changes

< 00003410: 0000 1881 1188 1000 0000 4000 0000 0000  ..........@.....
> 00003410: 0000 1888 1188 1100 0000 4000 0000 0000  ..........@.....

When I remove just the RX CTCSS we get

< 00003410: 0000 1881 1188 1000 0000 4000 0000 0000  ..........@.....
> 00003410: 0000 18ff ff88 1100 0000 4000 0000 0000  ..........@.....

When I remove just the TX CTCSS we get

> 00003410: 0000 18ff ffff ff00 0000 4000 0000 0000  ..........@.....

This suggests that I've not got the CTCSS structure quite right, but what's really happened is I've messed up understanding the structure. The one above is actually the revised version, not what I originally had. Byte 3 on this line needs to be something else, and I'd missed it completely so everything was offset.

https://m0yng.uk/2020/06/Reverse-Engineering-the-Baofeng-dm1702-codeplug/
CloudPusher - logging WSJT-X to Cloudlog

Recently I managed to have some success with FT8, which was great! But I didn't want to have to log contact manually. I've also started using CloudLog (which you can view at https://log.m0yng.uk)

I looked around and didn't find anything simple and native for Linux to do this log uploading (I'm sure there are things I didn't find, but 🤷)

Previously I've (ab)used a handy Python script by Giampaolo Rodola' which find and watches log files watch_log.py, and I knew that WSJT-X generates an adi file, and that CloudLog had an API that can accept adi lines.

CloudPusher Code
#!/usr/bin/env python3

# Imports for stuff
import configparser
import watch_log
import requests

# read in the config
config = configparser.ConfigParser()
config.read('config.ini')

# Some fixed common things
qsourl = '/index.php/api/qso'

# What to do when we find a new contact
def pushContact(filename, lines):
    for line in lines:
        r = requests.post(
            config['cloudlog']['host'] + qsourl,
            json={"key": config['cloudlog']['key'],
                  "type": "adif",
                  "string": line
                  }
        )
        if r.status_code == 201:
            print('😃 log created!')
        else:
            print('😟 Something went wrong')
            print(r.text)


# tell it what to watch
l = watch_log.LogWatcher(config['cloudlog']['filepath'], pushContact)
# start the watching
l.loop()

I've used a config file, which looks like this

```config config.ini [cloudlog] host = https://yourlog.server key = yourkeyhere filepath = /home/pi/.local/share/WSJT-X

I needed to change line 29 of `watch_log.py` to monitor `.adi` files, not just `.log`:

```python watch_log.py extract (spot the difference)
def __init__(self, folder, callback, extensions=["log"], tail_lines=0):

def __init__(self, folder, callback, extensions=["adi"], tail_lines=0):

Conclusion

I'm sure there are better ways of doing this, but it was quick, simple, and it works.

Sure, I have to remember to start the script first, but it's not too big of an issue, and I could make it start automatically on boot...

It would be nice to show on the CLI some info about the contact, e.g. 😃 contact with G5BK created!, but that would require parsing the adi text and it's just not that important to me (for now.)

https://m0yng.uk/2020/05/CloudPusher---logging-WSJT-X-to-Cloudlog/
Pythons and Ladders

I get bored playing games like Snakes and Ladders, there is no skill, just luck with a die.

I wondered if I could save the time and effort of actually playing the game, and just let the computer tell me who wins. Not by random selection, but by actually playing the game for me.

So, using Python (obviously) I knocked up a thing to play it for me.

I based my code on this travel game I used to play.

A magnetic travel set of Snakes and Ladders

It gives an output like this

$ ./index.py Player1 Player2 Player3
Play Snakes and Ladders, without actually playing.
We have 3 players!
Let's play!
Player1 rolled a 4 and moved to space 4
Space 4 was a ladder! Player1 moves to 14
Player2 rolled a 2 and moved to space 2
Player3 rolled a 6 and moved to space 6
Player1 rolled a 4 and moved to space 18
Player2 rolled a 3 and moved to space 5
Player3 rolled a 6 and moved to space 12
Player1 rolled a 2 and moved to space 20
Player2 rolled a 2 and moved to space 7
Player3 rolled a 4 and moved to space 16
Space 16 was a snake! Player3 moves to 6
Player1 rolled a 6 and moved to space 26
Player2 rolled a 6 and moved to space 13
Player3 rolled a 6 and moved to space 12
Player1 rolled a 6 and moved to space 32
Player2 rolled a 4 and moved to space 17
Player3 rolled a 1 and moved to space 13
Player1 rolled a 5 and moved to space 37
Player2 rolled a 6 and moved to space 23
Player3 rolled a 6 and moved to space 19
Player1 rolled a 5 and moved to space 42
Player2 rolled a 4 and moved to space 27
Player3 rolled a 4 and moved to space 23
Player1 rolled a 1 and moved to space 43
Player2 rolled a 3 and moved to space 30
Player3 rolled a 5 and moved to space 28
Space 28 was a ladder! Player3 moves to 84
Player1 rolled a 2 and moved to space 45
Player2 rolled a 2 and moved to space 32
Player3 rolled a 5 and moved to space 89
Player1 rolled a 5 and moved to space 50
Player2 rolled a 6 and moved to space 38
Player3 rolled a 1 and moved to space 90
Player1 rolled a 3 and moved to space 53
Player2 rolled a 5 and moved to space 43
Player3 rolled a 1 and moved to space 91
Player1 rolled a 3 and moved to space 56
Space 56 was a snake! Player1 moves to 53
Player2 rolled a 1 and moved to space 44
Player3 rolled a 6 and moved to space 97
Player1 rolled a 3 and moved to space 56
Space 56 was a snake! Player1 moves to 53
Player2 rolled a 3 and moved to space 47
Space 47 was a snake! Player2 moves to 26
Player3 rolled a 3 and moved to space 100
Player3 has won!
The code

And it works like this (I'm sure it's not optimal, but that wasn't the point)

#!/usr/bin/env python3
import random
import sys
print('Play Snakes and Ladders, without actually playing.')

if len(sys.argv) == 1:
    print('No players?.')
    print('try: snakesandladders player1 player2 player3')
    sys.exit()
elif len(sys.argv) == 2:
    print('We need more than one player!')
    sys.exit()

players = sys.argv[1:]

print('We have ' + str(len(players)) + ' players!')

print('Let\'s play!')

playerPositions = {}

# Board properties
boardSize = 100
snakesandladders = {
    1: {'goto': 38, 'type': 'ladder'},
    4: {'goto': 14, 'type': 'ladder'},
    9: {'goto': 31, 'type': 'ladder'},
    16: {'goto': 6, 'type': 'snake'},
    21: {'goto': 42, 'type': 'ladder'},
    28: {'goto': 84, 'type': 'ladder'},
    36: {'goto': 44, 'type': 'ladder'},
    47: {'goto': 26, 'type': 'snake'},
    49: {'goto': 11, 'type': 'snake'},
    51: {'goto': 67, 'type': 'ladder'},
    56: {'goto': 53, 'type': 'snake'},
    62: {'goto': 19, 'type': 'snake'},
    64: {'goto': 60, 'type': 'snake'},
    71: {'goto': 91, 'type': 'ladder'},
    80: {'goto': 100, 'type': 'ladder'},
    87: {'goto': 24, 'type': 'snake'},
    93: {'goto': 73, 'type': 'snake'},
    95: {'goto': 75, 'type': 'snake'},
    98: {'goto': 78, 'type': 'snake'},
}


# setup player positions
for player in players:
    playerPositions[player] = 0


def doTurn(player):
    thisRoll = random.randint(1, 6)
    playerPositions[player] += thisRoll
    print(player + ' rolled a ' + str(thisRoll) +
          ' and moved to space ' + str(playerPositions[player]))
    if playerPositions[player] in snakesandladders:
        print('Space ' + str(playerPositions[player]) + ' was a ' +
              snakesandladders[playerPositions[player]]['type'] + '! ' + player +
              ' moves to ' + str(snakesandladders[playerPositions[player]]['goto']))
        playerPositions[player] = snakesandladders[playerPositions[player]]['goto']
    if playerPositions[player] >= boardSize:
        print(player + ' has won!')
        return True
    else:
        return False


def playGame():
    for player in players:
        if doTurn(player):
            sys.exit()
    playGame()


playGame()

Sadly there isn't a ladder Emoji (yet) so I couldn't jazz up the output with a ladder to match the 🐍

https://m0yng.uk/2020/01/Pythons-and-Ladders/
📟 Pager notifications from Mastodon 🐘

A little while ago SP6MR pointed out that Pi-Star now supports POCSAG, a pager standard. I know pagers are basically dead, but who cares, this is Amateur Radio!

So, after a couple of false starts with pagers bought off eBay that I couldn't modify, I bit the bullet and ordered a new pager from China 🇨🇳 which was suitable for use on the 70cm amateur radio band. Easy!

I'll spare you the details of getting it coded up, the software isn't great, only works on Windows, and the drivers aren't signed.

So, now I have a pager, it's registered on DAPNET, and my Pi-Star is too. I can send and receive pages!

What now?

Mastodon.Radio notifications!

Obviously (it's not obvious, is it?) I want to get notifications of things happening on Mastodon.Radio via my new pager!

The following code is hacked together, basic, has no error handling, and will break. Do not use it in production. It uses Python3 and is based on https://github.com/DL7FL/DAPNET/tree/master/DAPNET

You will need to setup an app on your mastodon server, and grab the access_token to put into your config file.

It uses the streaming API of Mastodon, so often the pager get's a notification faster than Tusky on my phone!

Main file
#!/usr/bin/env python3

# --------------------------------------
# Mastodon Notifications -> DAPNET Pager
# Maybe.
#
# M0YNG 2019
# --------------------------------------

# Requirements
import dapnet
import configparser
from mastodon import Mastodon
from mastodon.streaming import StreamListener

# Load the config
config = configparser.ConfigParser()
config.read('config.ini')

# Setup logging
import sys
import logging
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s;%(levelname)s;%(message)s")
logger = logging.getLogger(sys.argv[0])

# Connect to mastodon server
mastodon = Mastodon(
    access_token = config['mastodon']['access_token'],
    api_base_url = config['mastodon']['api_base_url']
)

# Setup what to do with streams
class myListener(StreamListener):
    def on_notification(self, notification):
        # Construct a message to send
        message = 'New ' + notification['type'] + ' from ' + notification['account']['acct']
        print(message)
        # Send it
        dapnet.send(message, config['dapnet']['callsign_list'].split(','), config['dapnet']['username'], config['dapnet']['password'], config['dapnet']['serverURL'], config['dapnet']['txgroup'])

# Connect the listener to the stream
listener = myListener()
mastodon.stream_user(listener)
config.ini
; Your Mastodon account credentials
[mastodon]
access_token = token_here
api_base_url = https://mastodon.radio
;  Your DAPNET account credentials
[dapnet]
username = callsign
password = password
serverURL = http://www.hampager.de:8080/calls
txgroup = uk-all
callsign_list = M0YNG

Like I said, entirely pointless, but maybe you find the idea amusing enough to try yourself? If you do, please light my pager up by sending me a toot! @M0YNG@mastodon.radio

https://m0yng.uk/2019/11/📟-Pager-notifications-from-Mastodon-🐘/
APRS Talk - a companion

In September 2019 I gave at talk at The Cheltenham Amateur Radio Association about APRS. I'm far from an expert on the topic, but I was told I didn't say anything that wasn't true, so take from that what you will.

This post won't try and replicate the talk, but it will try to be a companion to the talk, and hopefully cover the same material for those who weren't there.

some radio equipment sits in front of a projector screen

What is APRS?

It is "Automatic Packet Reporting System" (Not "Amateur"). It was designed by Bob Bruninga, WB4APR (who absolutely didn't make up the name to match his callsign.)

It's not ADSB, although they do share some key similarities and I often get them confused, for example:

  • 50% the same letters
  • Both use one frequency for all stations
  • Both mostly used for position data
What is it for?

The most common use is location reporting/tracking. You may have hard of or used aprs.fi, but there are other sites out there such as aprsdirect.com and FindU

a trace of KC0RJX's journey from aprsdirect.com

APRS is not a vehicle tracking system. It is a two-way tactical real-time digital communications system between all assets in a network sharing information about everything going on in the local area. APRS.org

Given that, what else can it do?

Weather

G0LFP>APRS,TCPIP*,qAC,CWOP-3:@161930z5154.95N/00203.15W_357/ 002g003t060r000p000P000h84b10228eCumulusFO

This is compressed, because APRS has a limited number of characters that can fit into one message. However, it can be expanded to show more details.

At this point I read off from a real weather report I'd received in the last half hour. A printout showing the wEATHER

Telemetry

A fellow CARA member has an APRS station in Yorkshire, although a good position for RF, there isn't any mains power available, so he's rigged it up with solar panels and a battery. The station reports its own battery status, solar generation, temperature, etc. via APRS.

A graph of MB7UHT's battery level

You can view the graphs yourself by looking at the MB7UHT Telemetry page of APRS.fi.

Finding local radio things

I posted on mastodon.radio that I'd be doing this talk, and asked people what features of APRS should I mention that are less well known. Bryan KC0RJX provided most of this list;

  • Other Amateur
  • Repeaters
  • BBS
  • Club meetings

These are all really useful if you are new, or visiting, an area. In an era before the Internet, this was a great way to find out how and who you could talk to, and it is still useful now. Sure, you can find clubs online, but it's quite common for amateurs transmitting their location to include a frequency or other way to talk to them, and you know they are likely to be around to talk to if they are transmitting this information.

Repeaters and clubs are interesting too, I've seen a few listed with details of the frequency and CTCSS, or next meeting details. It's quite an interesting and fun way to use radio, for doing radio things.

We keep showing online tools that ingest APRS info, but all of this can be done directly off the air, and in theory you could listen for maybe an hour and know everything radio nearby without having to touch the Internet at all.

Short Messages

This bit was risky, and I was really excited that it (mostly) worked!

APRS can be used to send short textual messages to other APRS stations, and in this live demo I attempted to send and receive a message from SP6MR - Michał in Wrocław, Poland.

I used the following setup; * APRSDroid on my phone * A baofeng handheld radio * A Yaesu FT-1900 2m radio connected to * A TNC Pi * A Raspberry Pi 3 b+ * APRX software * A WiFi hotspot on my phone

I explained that in theory the message would flow like this;

Message → Radio → RF → Radio → TNC → iGate → Internet → SR6NDZ → RF → SP6MR-7

SR6NDZ is an APRS repeater near SP6MR that messages had previously been transmitted through in our tests.

For the fun of a live demo I transmitted the message using "sound", I held the PTT on the radio, and hit "send" on my phone, which made noises that were picked up by the radio's microphone. I was able to see that the packet had been received by my Pi, and then showed the audience the message showing up on APRS.fi, proof that it had entered the Internet.

Later in the talk I was excited to see that SP6MR had let me know via mastodon.radio that the message had arrived, and for bonus points I was able to read out and show the reply "Greetings from Poland", which had passed through SR6NDZ!

A printout showing the message

Because I didn't have the radio connected to my phone we didn't see the reply get decoded by APRSDroid, but we did see it print out and be sent over RF by the "base station", so that was cool.

But how does it work?

Here I got into the more technical side, having teased people with what APRS can do, and showing it doing it, I thought it would a good time to explain how that actually worked.

It's like, but not, packet radio

Ok, this isn't quite fair. APRS is packet radio, it uses AX.25 after all. But, it uses the "Unconnected broadcast" feature and we don't expect packets to be received by anyone else, so it's different to traditional packet radio where we seek acknowledgement for all of our packets.

Everyone is on the same frequency

144.8MHz in 🇪🇺, other places are different. You can look it up if you need to know.

Packets have a "path" and a "life"

We don't expect our packets to be heard by anyone else, but we hope they are. These days it is often the goal of the operator to get their packet into an iGate, and thus onto the Internet for people to see. We might be far from civilization, using low power, etc. so APRS has a feature built in to repeat our packets from more powerful and capable stations. This way a handheld radio on low power can hopefully be heard by a bigger station nearby, which can repeat the packet for more stations to hear it, hopefully one of these is an iGate and the packet is passed onto the Internet.

Obviously we don't want these packets to keep bouncing around forever, so we give them a path, which dictates their lifespan. Commonly we'd use WIDE2-2, this means we'd like the packet to be repeated twice. It might look like this;

  • Station 1 transmits a packet with path WIDE2-2
  • Station 2 hears it, and repeats the packet. It adds in its own callsign, and reduces the count. Now the path looks like WIDE2-1 STN2
  • Station 3 hears it, and repeats the packet. It also adds its own callsign, and reduces the count. WIDE2-0 STN2 STN3
  • Station 2 hears the packet repeated by station 3, but it sees its own callsign so doesn't repeat the packet.
  • Station 7 hears the packet, and is an iGate, so sends it up to the Internet.
  • Any other stations hearing the packet won't repeat it, because the path has been used up.

iGates are often in suboptimal RF locations, due to the nature of where reliable power and Internet can be found (aka people's houses) and generally this system works well.

The path can work the other way too, so if a packet destined for Station 1 is seen by Station 7 on the Internet, it knows it has heard Station 1 "recently" and will pluck the packet off the Internet stream and transmit it via RF, Station 3 and 2 will also know they can pass on the packet and repeat it. Eventually, in theory, the packet gets to Station 1.

SSIDs and Icons

Stations can set an icon to show what they are. They don't transmit the icon, just a letter which represents which icon should be shown by the receiving station / website / etc.

These examples are taken from the APRS.fi icon set on Github.

a table of APRS Symbols

SSIDs, or Secondary Station Identifiers, are needed because you only have one callsign and it can't be used by your home station, and your car, and your handheld, at the same time. There is a convention for the numbers.

This list is taken from aprs.org

  • 0 Your primary station
  • 1-4 generic additional station, digi, mobile, wx, etc
  • 5 Other networks (Dstar, Iphones, Androids etc)
  • 6 Special activity, Satellite ops, etc
  • 7 walkie talkies, HT's or other human portable
  • 8 boats, sailboats, RV's or second main mobile
  • 9 Primary Mobile (usually message capable)
  • 10 internet, Igates, echolink, winlink, AVRS, APRN
  • 11 balloons, aircraft, spacecraft, etc
  • 12 one-way trackers, etc
  • 13 Weather stations
  • 14 Truckers or generally full time drivers
  • 15 generic additional station, digi, mobile, wx, etc
Yet more uses!

You can also send short messages to the special recipient "EMAIL" to send an email, which is useful if you don't have access to the Internet for any reason and you want to communicate with someone who might not have a radio turned on right now.

You can also send an SMS to a mobile phone! I've never got either of these to work though...

However, I am working on an APRS-to-Fediverse thing that would allow you to post status updates to mastodon.radio via APRS.

How do I do APRS?

At this point I hoped people were interested enough to try doing APRS for real!

The easiest way to get started is probably to go grab APRSDroid and just use the Internet directly. It's kinda pointless, but it does work.

The next step up would be to either connect your phone to a handheld radio using a wire (not like I did in the demo!) and do APRS over RF.

You could also seek out a mobile/handheld radio that does APRS, but be careful. I mentioned that I had the AnyTone 868 for this reason, but didn't realise until later that it only did "sending location" APRS, and couldn't do messages or receive anything. So, check what you want to do, and that the radio can do that. G6USL and G0TMP were in the audience and added that they used the Kenwood TH-D72 and TH-D7E and that these were properly equipped APRS handhelds.

Direwolf is a good option too, it is a soundcard TNC so all you need to do if connect the output of your radio to the line in of your computer soundcard and Direwolf can start decoding packets. However, I did say that I'd not had much luck getting transmit to work, as you either rely on VOX or have to make a way to control the PTT using e.g. serial connections, which can be a pain.

Lastly I showed my TNCPi, a hardware TNC that sits atop my Pi computer. It fixes all the issues with PTT, and allows me to talk directly via serial, KISS, etc. which is a lot easier. I also suggested that I was sure lots of the audience would have a TNC gathering dust in the loft that could be revived.

Questions

In this part it people asked about things like different frequencies for the USA, etc. but other interesting things came up too.

There are a few satellites which have APRS on them, which is cool and a fun way to send your packets far and wide. The ISS also has APRS, but it's not always available, on, etc.

RAYNET make a lot of use of APRS, and it's very popular. However, since the loss of GB3Uk and other repeaters locally, one being APRS, RAYNET have had to work harder to provide coverage enough to make full use of APRS.

Throughout this you will notice I mention printing things out, and the photos are of a receipt roll. Well, this is exactly that. I threw together some basic python to read the log files using APRSLib, and use a borrowed thermal printer to spit out the APRS messages received. It's a great fun way to engage people, very few people look at screens on a stand, but will ask about something that randomly spits out a bit of paper! G6USL seemed to take much amusement in transmitting his location and seeing the printer work!

If you have any questions, think something I've said here is wrong, etc. please send me a toot! I'm @M0YNG@mastodon.radio

https://m0yng.uk/2019/09/APRS-Talk---a-companion/
Being Static

It's been nearly a year since I last posted on this blog. Oops.

In my defence, I've spent a lot of that last year working on mastodon.radio, a twitter-like social network for the Amateur Radio community. I should write about that too...

I've been inspired by a range of other cool blogs and websites I've seen in the last year that are static, or very minimal in both design and code. That, plus the ongoing burden of maintaining wordpress, has made me want to move towards a simpler website, one that hopefully loads faster, and is generally easier to use and maintain.

At the moment, I'm experimenting with hexo to generate this site, and if you are reading this on m0yng.uk then I have already moved wordpress off, and put the static/generated site that hexo made in its place.

Migrating the content from wordpress wasn't too bad, a helpful plugin (hexo-migrator-wordpress) did most of the work, then I just needed to manaully tidy up the lack of line breaks, links, and images, that hadn't been touched in the migration. Oh, and download the images from the old site to put into the new.

For now I'm using the cactus theme, I might look at rolling my own in the future, but I suspect that will never happen, and this theme is pretty close to what I'd like anyway.

Hopefully the next post on here will be a companion to the talk I gave at CARA this week on APRS. I don't plan to replicate the whole talk, but I think there are bits that could be expanded on more (e.g. specifics of my setup) and things that I wasn't able to show properly on the night (because for some reason none of my images worked.)

Let's hope that does happen, and the next post isn't in a year with the title "I should post more often".

73

https://m0yng.uk/2019/09/Being-Static/
Is Amateur Radio on Life Support?

This Opinion Piece was written for the October Edition of "CARA News" (the Cheltenham Amateur Radio Association's newsletter), but I wanted to share it more widelly, and see what people make of it.

This toot by Adam MU0WLV got me thinking the other day:

FT8 is great for learning propagation but it can be like watching fish in a tank.. interesting yet boring… it's most definitely not the death of radio as something else will just come along and replace it… in fact it's probably helping keep people active given the general conditions :-) Adam MU0WLV

It got me thinking, are things like FT8, WSPR, and Network Radio, Life Support for our hobby?

Let me make my case on this … when I was new to the hobby, I was promised that you could talk to the world with a bit of wet string and a strong wind. Experienced amateurs spoke, misty eyed, about the time they were chatting to someone in Japan with an amazing signal, or how they regularly spoke to their friend in California.

But, when did this last happen? My log is stuffed full of European stations, and I've barely worked the USA twice.

The fact of the matter is that we are at a Solar Minimum, the bands are all consistently poor (at best!) and it's very hard for new amateurs to get interesting contacts. I can, and have, sat calling CQ on 40m SSB, 20m SSB, any other band I find a signal on, and get nothing back. When do I get a reply? During a contest, then it's just "59" and some other unknown bit of information, often I don't even know which contest people are taking part in.

When else do I make a contact? Using data modes. I've not personally used FT8 (yet), but I've sat down at the computer (after spending ages trying to get it to talk to the radio), turned on JT65, or PSK31, and had more contacts in half an hour than an entire field weekend of SSB. I've got ONE Irish station in my log using 40m SSB, but I can turn on Network Radio and talk to someone using 2m mobile into one of the Southern Irish Repeaters.

Some people complain that data modes aren't "real radio", that having your computer do the decoding isn't proper operating. But, to be honest, who cares? Back in the day, I bet we had the same arguments about the move from Spark Gap radios, when SSB started to become a popular mode for voice I bet people bemoaned that it wasn't as good as AM.

Some point out that using data modes doesn't help the argument that we need the spectrum, if we use it too efficiently we won't get to keep it. But the move from AM to SSB did the same, and I don't see anyone saying that CW is "too efficient on spectrum".

Some people say that Network Radio isn't "proper radio" (and, I have sympathy with this, it's not), but it does facilitate "proper radio". At the last field day I was talking to Alan M0NRO, and Derek G3NKS, via the "Engineering Channel" of Network Radio, as we attempted to make three way contact on 40m, 80m, 60m, and finally had success on 10m. Without the Engineering Channel, we'd have given up very quickly, and never found 10m to work so well. Listening to the Southern Irish Repeaters inspired me to make the effort to turn on the radio in my car, which I have to admit I'd not done for ages, and talk to people using our own local repeaters. These are both "Real Radio", that wouldn't have happened without the UnReal Radio being there to help.

Maybe it helps to think of modes like FT8, or Network Radio, or whatever comes next, as "Life Support" for the hobby. Nobody wants to see their loved ones on life support, with a machine keeping them alive. But, given the absolutely horrific HF conditions, isn't life support better than the alternative? If we can't keep people interested in the hobby, interested because they can make contacts with people far away, in challenging conditions, even if it's just on their screen, what will happen when the solar minimum ends and the conditions improve? Give it a few years and we might have amazing HF conditions which mean I can talk to Brazil on 10m using 5 watts of SSB and a wet string, but if I've sold the radio because I only even spoke to someone down the road and that Italian station with too much power, I won't see the benefit.

I'd pick SSB over a data mode any day, you just can't compare the human interaction of hearing a voice to seeing a callsign on a screen. Yes, it's messy and you have to repeat things, and explain things slowly, but that's what we all enjoy, talking to people. Our current data mode obsession isn't because it's better, but because it's essential to fulfil the promise sold to us by the carefully selected memories of people who were active in the heights of the hobby, and the heights of HF conditions. Once those conditions come back, so will SSB, and the bands will be full of human voices again, because then FT8 will be boring, and not the only way.

At the end of the day, if someone is sat in front of a radio, talking to someone else, having fun with their hobby, does it matter if it's not "proper radio"?

https://m0yng.uk/2018/10/Is-Amateur-Radio-on-Life-Support/
Rule 0 - don't be on fire 🔥 18650 Cells for /P

When I first visited a hackspace I was introduced to the rules, most of which I have since forgotten, but rule 0 stuck with me.

Don't be on fire.

The standard power source for portable operation is the SLAB, always heavy, often bulky, but reliable and pretty resilient to charging and high drain operation. However, mine weighs 3kg and only gives me 7.5Ah of power. It's also almost as big as the radio.

As electronic cigarets have grown in popularity over the last few years, so has the availability of cheap, quality, Lithium based cells to power them. And, in my mind, these cells put out 3.7v, so 4 x 3.7 = 14.8v. Well within the range of my radio, and with each cell packing 2500mAh, I'd get even more juice for much less weight and space. Also, the 18650 is basically what Tesla use for their sports cars, so this must be able to drive my radio, right!?

These cells have got a reputation for catching fire, but I've had basically the same technology in my laptop, phone, and various other things for years, and these have never caught fire. I think the key thing here is to get quality cells, and charge them properly, to avoid being on fire. So far, I've been ok, so what would be different here?

I tried to spec this circuit to run the radio at full 100% TX power, which is 20 amps according to the manual. I don't plan to run it at this power (often) but I thought it would be nice to have the option if I wanted to shout at an interesting station and avoid accidentally melting something if the power was turned up too high / left from before / etc.

Sadly it turns out, I was wrong on two things, both of them maths..

3.7v doesn't mean 3.7v

3.7v is the "nominal voltage" of these cells, which in reality appears to be close to the minimum safe voltage (if you over discharge a lithium cell, it can be dead for good.)

Fully charged each cell produces 4.2v, and 16.8v is way out of the safe input range for my radio. So I had to drop it down a bit. Searching around a bit, I didn't find much information, or many people using these cells for this job.

I did find one American "prepper" / amateur, who was keen to use these cells to avoid the end of the world, or something, and was using a diode to provide 0.7v drop. He was happy to operate above the rated range of his rig, but I wasn't, so decided on a cunning plan to connect TWO diodes, dropping me from 16.8 to 15.4v, back in range. My circuit also includes two jumper wires, allowing me to switch out one or two diodes as the cell voltages drop during use. That way I can keep the supply to the rig within range at all times. I have a voltage display in the circuit so I can keep an eye on this as I operate, and chunky switches so I feel powerful :)

In reality, the cells drop voltage like a ton of bricks when given a load, which disappointed me.

The cells I bought were, 18650s, and rated for 22 - 35A. However, under light load they drop voltage, and under full 100% TX power load they drop from 16.8v to nearly 12v. They do recover when the load is removed, but I was expecting more.

More testing is probably needed, but it's a worrying start.

AmpHours don't work like that

Sadly I was overenthusiastic with my maths, connecting cells in series does not combine their Amp Hours. So I can do enough volts, and enough amps, but I only have 2.5Ah of juice.

That's 7 and a half minutes of full power TX.

Not quite what I was hoping for.

However, the cells are so small and light I can easily carry more, and either swap them out or wire them in parallel and not worry.

To be fair, this was just me not checking things properly before hand.

What Next?

This experiment cost around £60, so I'd like to make sure it works and I make use of it.

Annoyingly, the diodes resisted being soldered, and everything ended up looking quite messy and I had loose connections almost immediately after building it. I may need to try non-lead-free-solder... I've also wrapped all the connections in electrical tape, so hopefully nothing shorts out. Ideally I'd have a power connection on the tin, rather than having the wires fixed, but as a first try, it's not too much hassle. I also didn't have the proper tools, so the holes were cut with a Stan Lee knife, and aren't the right size, or un-sharpened. (you might have notices I specify 10amp diodes, but they are rated for much higher peak amps) I may not even need the diodes considering the voltage drop the radio causes, and the capacity is pretty poor right now.

However, I've not used it in anger, yet, so I still need to try it out in the field and see how long it really lasts and how well it performs.

Future versions may benefit from a battery management circuit, to ensure the cells stay balanced, and can be charged without needing to be removed. A buck converter may be more suitable for regulating the voltage, but I can't find any that work at 20amps. (yes, maybe I should learn morse and operate at low power)

Also, decent cells are somewhat expensive, but is that worth it for the loss of weight and bulk? Is the risk of fire worth it? Probably, a SLAB can deliver enough power to kill you and melt things if you short it out, so this isn't much different. Just handle with care.

https://m0yng.uk/2017/09/Rule-0---don't-be-on-fire-🔥-18650-Cells-for-P/
CARA Fun - Some short hops

After deciding that 7+kg handing off one arm wasn't ideal, I've invested in a sturdy rucksack.

It's 28 litre, so "small" but a good size to fit the radio and kit into, without too much room for things to flop about and break. It has lots of sections and pockets, which is great for organising stuff, and MOLLE loops, so in theory I can hang stuff off it too. Oh, and it's black "British Terrain Pattern" camo, because why not.

More importantly, it weighs slightly less than the box, and spreads the weight more evenly, so it's much easier to carry everything.

The first proper outing was to Woolstone hill, which just keeps going. I'm sure we reached the top three times before spotting the Trig Point. It turns out that Trig Points are brilliant for operating radio from, just the right height and enough space. Almost like they were specifically placed there for putting equipment on top of.

Once there, the view was pretty impressive, and the signals were good too. Initially I happened upon the Worked All Britain frequency, of 7.160. Apparently "on this frequency we give mobiles priority" so I didn't stick around for long. I also had a quick 2m FM qso.

Two more UK contacts kept me entertained for a while, including an interesting Special Event station GB1AFV in Scunthorpe. You can listen to a bit of GB1AFV, and see the view, in this short video (sorry, it's YouTube for now.)

As I was about to pack up I heard EI99WAW, which was a nice extra point and way to end the expedition, even if all my contacts were quite short hops.

Icom radio, battery, and aerial pole, attached to the trig point with rolling hills behind

https://m0yng.uk/2017/09/CARA-Fun---Some-short-hops/
CARA Fun - Up a Hill

Continuing my attempts to get out and about operating radio (and points) I've been up my local "big hill" and activated Cleeve Hill.

Radio, on a bench, with the trig point behind

Supporting the aerial was a bit easier than on the beach, a couple of bungee chords around the bench and the pole was nice and stable. The bench also proved more comfortable than the beach pebbles!

It was also interesting to see how many people stopped to talk and ask what I was doing, I think I may need a reference sheet / leaflet for future outings to public places.

I was quite successful on the hill, with contacts in Italy and a 40m mobile station in the UK. I got points for activating a SOTA hill, activating a Cotswolds Hill, and having a QSO with a mobile station.

However, I took the short but steep route up the hill. I had to stop and rest a few times, and once I got home I measured the box at over 7kg. The next step will be using a rucksack to help spread the weight more, and give more space.

I also found that the battery was running out a bit too quickly, but after checking the charging voltages on the side I noticed that the charger I had purchased wasn't really giving it enough juice, so that's another thing to investigate. Nothing spoils the effort of climbing a hill more than running out of power.

Looking out from Cleeve Hill, heavy clouds and clear skies over Bishop's Cleeve below

https://m0yng.uk/2017/09/CARA-Fun---Up-a-Hill/
CARA Fun - Working high and low

Recently CARA launched a Fun Activity Challenge, with the intent of getting members old and new on the air.

Now, I have to confess, I've not been on the air much, and the idea of ticking off some fun places and challenges appeals. So, I got me an ICOM IC-706 from a SK auction, and started trying to do some ticking.

Activate a Castle

Technically this required me to be "within 100m of a standing wall", but as I was at Portchester Castle, I took the opportunity to do it properly, from the top of the keep!

The view from the top of Portchester Castle Keep, looking down to the grassy grounds.

I suspected that setting up a HF station with 40m dipole wouldn't be popular or practical from the keep, so I used a handheld to call CQ on 2m. Sadly this wasn't answered, so I hopped onto GB3IW the repeater in the Isle of Wight. This worked brilliantly, with a very clear signal, however, still no reply to my call. Helpfully M6SGK came to my rescue and we had a QSO from castle grounds to castle keep, via the Isle of Wight.

But it counts, so that was one point!

Activate a beach

This was another fun idea, and I managed to escape to a nearby beach one evening and succeeded in digging a deep enough hole and big enough pile in the pebbles to stop the pole falling over.

A radio balanced on a box, with fishing rod supporting the aerial and the sea stretching off into the distanceGlowing radio in the foreground and moon low in the sky

Although a great spot to sit and fiddle, I didn't manage many contacts on HF (after all the effort and the pole falling down three times.) The most interesting was to France, and ended quite swiftly due to the approaching thunderstorms ruining the bands, and necessitating aerial disconnection for my French contact.

I also spoke for some time via GB3IW to a station on the Isle of Wight, which was fun, as was watching the day fade and the moon come up. It's incredible how many ships can be seen by their lights on the English Channel at night, compared to the day.

Packing up was a bit more fun than usual as it was properly dark by this point, but I don't think I left anything behind.

So, two point, and a promising start to my new /p kit.

https://m0yng.uk/2017/09/CARA-Fun---Working-high-and-low/
Trying again - take 4 for the TYT TH-9800

I got myself a TYT TH-9800 when I passed my intermediate exam, and although I like the radio, it's been a rocky ride for the two of us! Mine was one of the first batch produced, at least it hadn't been out long when I bought it. Inevitably, there were some teething troubles. For me, this manifested as the 2m Power Amplifier failing, in total, this has happened three times, with increasingly significant action by the seller.

  1. Repair / replace the PA module
  2. Replace the entire radio circuitry (same box)
  3. Replace the whole radio (this time I'd *smelt* the radio fail!)

I now own a "plus" model, which I understand (from reading the internet) comes with uprated components, and improved firmware. There also seems to be a variation which covers 4m, rather than 6m.

Reasonably pleased with finding that TYT had hopefully resolved my problems, and that it wasn't anything I was doing that had caused them, I shoved everything back under my seat in the car, and hit the road.

Sadly, and much to the confusion of the other station, and to my own frustration, I found that the new radio had a "busy lock" enabled. This stops the radio transmitting when it is receiving a signal. This meant that I was unable to reply to my contact using the repeater, until the repeater had stopped transmitting. Which it didn't, because I kept being asked if I was still there…

This "feature" is not something you can change in the menu of the radio, nor via Chirp, nor via the TYT supplied software. It is, however, superb at making the radio almost completely useless as a mobile rig, repeaters are impossible, and squeezing into a net isn't much better!

If you search online for "TYT TH-9800 Busy Lock", you will find quite a few results. Not least of which is N6PET's very useful page. On it, he helpfully provides a bit of software to upgrade the firmware / fix the issue. Hooray! I've kept hold of a copy, in case his ever vanishes...

No TX when Busy Light on

Sadly, the software is only provided for Windows, so I spent the majority of a day arguing with combinations of Virtualbox, Windows 7, Windows 10, Prolific Drivers, an old laptop, the radio, other bits of software, and myself, I was successful in applying the fix.

To speed things up for everyone else, RTFM. Follow the instructions, put your radio into the correct mode, and everything will work.

It will, however, erase any of your settings, so I'd suggest running Chirp before, and after, to save you manually re-programming everything.

With that all done, hopefully this one will last longer than the rest! I've got a page with more info on the TYT TH-9800, and my use of it, which I'll soon update with things to remind me how to do this, if I ever need to again!

https://m0yng.uk/2015/12/Trying-again---take-4-for-the-TYT-TH-9800/
A trip to Yorkshire

A couple of weeks ago I went to Yorkshire to visit family, and I took some wires with me.

Initially, I threw up the remains of my 10m vertical, which still tuned up ok for 40m. Sadly, it wasn't much use on top band for the Hornsea club net!

I was also able to get along to the Hornsea club meeting on the Wednesday, which was great. A very interesting talk on the merits of CW (I'm still almost completely useless) and good to see another club in action. I was very envious of their club shack!

Later in the week I was able to shove my radio, battery, 10/20 Sotabeam aerial, and a young foundation licensed relative into the car and trundle up to Beverley Westwood.

Car boot radio

Despite all the bands sounding very busy, no one answered my CQ calls, and I couldn't find anything to call into either. After a while I gave up, and headed back for fish and chips.

https://m0yng.uk/2015/11/A-trip-to-Yorkshire/
I'm one year old!

With the RSGB Convention taking place last weekend, I was aware that is is "about" a year since I took my final amateur radio exam, at last year's convention.

On a whim, I just pulled my licence paperwork out of my exceptionally well organised (ha) filing system, and guess what? It's EXACTLY one year today since my licence was issued!

So, what have I done in my first year as a fully licensed amateur?

Looking back at Simple Log, I've recorded 288 QSOs, although I've almost certainly made more as I don't record every QSO, especially if it happens when I'm driving.

I've also run my first special event station, GB0RWC, gone through at least three HF aerials at home, one vertical, one horisontal, and one useless.

I've taken part in my first contest, CQWW SSB, coming 28th in England for the "Single Operator - Low Power - ALL BANDS" category. (I only operated for 5.5 hours!) and finished 5th in the Data category of the CARA Challenge 2015

Although I've still not got anywhere with learning CW, I've continued my experimentation with data modes, making about 40% of my contacts this year using some form of data. (Note, the image above is a screenshot from SimpleLog, which calculates the wavelength of a frequency, rather than showing the conventional band name) Maybe I'll try the CW thing next year?

The world of radio is pretty sizeable, and hopefully this is just the first of many years to try it all :-)

https://m0yng.uk/2015/10/I'm-one-year-old!/
Getting it up

Icom radio, battery, and aerial pole, attached to the trig point with rolling hills behind

After much weeping and gnashing of teeth, lost tennis balls, and probably very confused and suspicious neighbours, I had all but given up on my plan to get the half sized G5RV up in the air.

Then a friend came to visit! And I know he really is a friend, because he didn't think the idea of recovering a tennis ball which had got stuck on the roof with a 10m carbon fibre fishing rod was completely stupid.

My plan had been, in light of the lack of required space to fit the G5RV in, to throw a tennis ball, with rope attached, over the house roof, attach the end of the aerial to it, then pull that up and over as far as needed. However, tennis balls and string are surprisingly hard to throw successfully over a house, and the tennis ball had got stuck, several times, when trying to pull it back again.

The new plan was to tie the string to the 10m fishing rod, and carefully lift it over the house. This didn't work, the string kept falling off the rod and getting stuck, so we gave up on that.

Then my friend had a great idea, to throw the tennis ball over a part of the house that was lower down, then use the rod to lift it over the very top. It worked, very well!

The best bit about this was we got out the PMR handsets, which came in very helpful when we began to hoist the aerial over the roof, as there was a house in the way, neither of us could see everything that was going on.

So, what do I have now?

A half sized G5RV, one end attached to a telescopic aluminium pole, with a flag on it, and the other end going over the top of my roof, and about 20% of the way down the other side again. The feeder is pretty much the perfect length to reach the remove matching unit, so it has all fitted quite well.

Performance wise, the aerial is doing quite well. I've had no issue working people on 20m data (PSK and JT65) even hearing and being heard in Brazil and Chile, but haven't yet had any voice contacts.

https://m0yng.uk/2015/07/Getting-it-up/
Going Hoizontal

After my vertical fell over a bit, I've been experimenting with horizontal aerials.

The first attempt was using James' SOTABeam hooked over the guttering and supported at the other end using another fishing rod. This time, I was sure to collapse the rod when not in use!

The setup worked reasonably well, with more success inter-G than I've had before, which is to be expected. It proved that this way of rigging an aerial had potential, and I probably had enough space.

However, it wasn't a long term solution, so with the assistance of the fishing rod, I hooked some twine over the TV aerial bracket on the gable end of the house, making a slightly shoddy hoist for one end of an aerial.

Thanks to M0PCB I started work with a proper telescopic aluminium pole, that stands approximately quite tall.

Two pulleys on a pole

I drilled two holes in the top section, and attached some pulleys, as you can see in the photo. I then hammered it into the ground next to a fence post, and proceeded to attach it with some bent shelf brackets, holding the pole in place.

Next, I attached some rope to the pulleys, and attached one end of a half size G5RV (thanks again to M0PCB!) With the other end attached to my makeshift hoist and at the full height of the house, I optimistically began to raise the other on my newly installed pole.

Sadly, this didn't go as well as hoped.

My pole was not far enough away to produce anything other than a very limp V, with the feeder trailing on the ground. Not what I had planned.

Further measuring, well, some measuring, showed that my available space wasn't quite enough to fit the Half G5RV in place. I'll need to experiment with having the house end of the aerial over the top of the roof and attached down the other side somehow.

For now, the pole is making a very nice flagpole, and hasn't fallen over yet!

https://m0yng.uk/2015/06/Going-Hoizontal/
Not what I meant by a "Compromise" aerial

I'm familiar with the concept that every aerial is a compromise, especially at HF. However, this "horizontal modification" isn't quite the compromise I had in mind! Today was very gusty, with the forecast showing 40kph winds, and I know I got this advise:

Just keep eye for heavier winds and drop down couple of section till wind drops - Ray on Twitter

It's hard to know what "heavier winds" are, and the whip has stood up to similar in the past. Dropping sections also got harder the day I sealed the joints to keep the rain out. Today, the wind must have been heavier than before, it seems that two of the three sets of cable ties holding the aerial firmly to the fence post had worked loose, and the aerial itself had too much movement available. And it snapped.

The break in the vertical aerial, some of the fibre glass still holding on!Close up of the broken vertical pole, once  removed from the fence and on the floor. It's very spiky.

On the plus side, the car survived, and no one was in the way at the time. I've managed to recover all but two sections (the very base section is ok, but now the sizes don't match) so I'll hopefully be able to use it again.

At least it's something to talk about! I've been told that if your aerial doesn't fall down in the wind it isn't big enough, so now I know what size to aim for!

https://m0yng.uk/2015/03/Not-what-I-meant-by-a-Compromise-aerial/
Award Winning Simple Log!

Update: SimpleLog is no longer online (as of September 2019), it might come back one day.

Simple Log is designed to be a very simple logging application for Radio Armatures, be they new to the hobby, or not interested in recording every last detail of every contact they have.

Cheltenham Amateur Radio Association held their annual constructors context and exhibit last week and I entered Simple Log in the new "Software / Computer related" category.

Reaction was mixed, most people seemed positive, although a fair number of questions related to the pico-projector I was using to throw my screen onto the wall!

I was accused of "reinventing the wheel" a few times, but that is kinda the point, I don't think the current offering of logging applications is very good, and at least isn't suitable for what I want.

However, I did find at least one new bug to squash! In the end however, I was awarded the G0UPU cup, despite/because of being the only entry in the category (although I was assured I did deserve it.)

It was great to show off, and get recognition for, my work, and I'm hoping my entry will inspire more people to enter next year. If you've not tried it yet, go have a play! (don't, it's not online now.)

https://m0yng.uk/2015/03/Award-Winning-Simple-Log!/
Experiments in Non-RF Communication

It's some time since I wrote a blog post on the Internet, but a year after I got my foundation license I think it's time to start documenting my experiences, experiments, successes and failures in Amateur Radio.

So, hello from Christopher, M0YNG

https://m0yng.uk/2015/03/Experiments-in-Non-RF-Communication/