Encrypted Kubuntu 24.10 with ZFS and Hibernation

For a while now I have published manual installation steps for pretty much any Ubuntu release since 18.10. Since, with Plasma 6, I switched to KDE and Kubuntu, I am abandoning that series. This guide will be the first one with manual installation steps for Kubuntu.

First, the big question: Why manual? Well, I view two things as absolutely mandatory for my Linux desktop: ZFS and encryption. One that I really like to have is hibernation. Default installation uses ZFS’ native encryption which, while nice and fast, doesn’t encrypt metadata. I am not saying that ZFS encryption is bad - I am just saying I am not necessarily comfortable with it. And swap size is way too small for hibernation to work. Thus, I like to do it all manually.

That said, if GUI installation leaves the system in state you like, there is no reason to follow this guide. Save yourself a bit of time.

As previously noted, this guide is for Kubuntu - an Ubuntu variant using KDE and not Gnome. While I was Gnome user for quite a long time, it was more of a stockholm syndrom rather than love. With KDE Plasma 6, I found something I actually like. As for the guide, Ubuntu and Kubuntu have a huge overlap and thus most of the steps are actually the same up to GUI install. It should be easy enough to look into an old Ubuntu guide and adjust the steps. I will probably still update Ubuntu steps for long-term releases (last one was 24.04)

With prologue done, what am I actually trying to achieve? Well, I want a minimal Kubuntu installation for my Framework 13 laptop that supports ZFS and hibernation. Both data and hibernation files are to be protected by LUKS encryption.

Finally, let’s go over all the steps to make it happen.

The first step is to boot into the USB installation and use “Try Ubuntu” option. Once on the a desktop, we want to open a terminal, and, since all further commands are going to need root access, we can start with that.

sudo -i

Then we add a few packets that come by default with Ubuntu install but are extras on Kubuntu.

apt update
apt install -y gdisk zfsutils-linux

Next step should be setting up a few variables - disk, hostname, and username. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system.

DISK=/dev/disk/by-id/<disk>
HOST=<host>
USERNAME=<user>

For partition, I like to have 4 of them. The first two partitions are unencrypted and in charge of booting (boot + EFI). While I love encryption, I almost never encrypt the boot partitions in order to make my life easier as you cannot seamlessly integrate the boot partition password prompt with the later password prompt. Thus encrypted boot would require you to type the password twice (or thrice if you decide to use native ZFS encryption on top of that).

Third partition is swap that we need for hibernation support. I will use 64GB here because my laptop can have up to 96GB. While swap only requires about 40% of RAM to be backed by swap, having a few gigs extra will not hurt.

The last partition is the largest and will contain all user data.

One extra calculation is needed to figure out 4K aligned sector count.

LASTDISKSECTOR=$(( `blockdev --getsz $DISK` / 2048 * 2048 - 1 ))

And then we can create all the partitions needed:

blkdiscard -f $DISK 2>/dev/null
sgdisk --zap-all                               $DISK
sgdisk -n1:1M:+255M          -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+1792M          -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:+64G            -t3:8200 -c3:Swap $DISK
sgdisk -n4:0:$LASTDISKSECTOR -t4:8309 -c4:LUKS $DISK
sgdisk --print                                 $DISK

To ease commands used later, here we can get partition UUIDs. While we can use disk names directly (as I often did before), using partition UUIDs helps if we ever clone disk to another physical drive.

PART1=`blkid -s PARTUUID -o value $DISK-part1`
PART2=`blkid -s PARTUUID -o value $DISK-part2`
PART3=`blkid -s PARTUUID -o value $DISK-part3`
PART4=`blkid -s PARTUUID -o value $DISK-part4`

The next step is to setup all LUKS partitions. If you paid attention, that means we need to repeat formatting a total of 2 times. Unless you want to deal with multiple password prompts, make sure to use the same password for both:

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i /dev/disk/by-partuuid/$PART4
cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i /dev/disk/by-partuuid/$PART3

Since creating encrypted partitions doesn’t mount them, we do need this as a separate step. I like to name my LUKS devices based on partition names so we can recognize them more easily:

cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    /dev/disk/by-partuuid/$PART4 $PART4
cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    /dev/disk/by-partuuid/$PART3 $PART3

Finally, we can set up our ZFS pool with an optional step of setting quota to roughly 85% of disk capacity. Since we’re using LUKS, there’s no need to setup any ZFS keys. Name of the pool will match name of the host and it will contain several datasets to start with. Most of my stuff goes to either Data dataset for general use or to VirtualBox dataset for virtual machines. Consider this just a suggestion and a good starting point, adjust as needed.

zpool create \
    -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O quota=3200G \
    -O canmount=off -O mountpoint=none -R /mnt/install \
    ${HOST^} /dev/mapper/$PART4
zfs create \
    -o reservation=100G \
    -o 28433:snapshot=72 \
    -o devices=on \
    -o canmount=noauto -o mountpoint=/ \
    ${HOST^}/System
zfs mount ${HOST^}/System
zfs create \
    -o 28433:snapshot=240 \
    -o canmount=noauto -o mountpoint=/home \
    ${HOST^}/Home
zfs mount ${HOST^}/Home
zfs set canmount=on ${HOST^}/Home
zfs create \
    -o 28433:snapshot=360 \
    -o canmount=noauto -o mountpoint=/Data \
    ${HOST^}/Data
zfs set canmount=on ${HOST^}/Data
zfs create \
    -o recordsize=32K \
    -o 28433:snapshot=72 \
    -o canmount=noauto -o mountpoint=/VirtualBox \
    ${HOST^}/VirtualBox
zfs set canmount=on ${HOST^}/VirtualBox
zfs set devices=off ${HOST^}

With ZFS done, we might as well setup boot, EFI, and swap partitions too:

yes | mkfs.ext4 /dev/disk/by-partuuid/$PART2
mkdir /mnt/install/boot/
mount /dev/disk/by-partuuid/$PART2 /mnt/install/boot/
mkfs.msdos -F 32 -n EFI -i 4d65646f /dev/disk/by-partuuid/$PART1
mkdir /mnt/install/boot/efi/
mount /dev/disk/by-partuuid/$PART1 /mnt/install/boot/efi/
mkswap /dev/mapper/$PART3

At this time, I also often disable IPv6 as I’ve noticed that on some misconfigured IPv6 networks it takes ages to download packages. This step is both temporary (i.e., IPv6 is disabled only during installation) and fully optional:

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
sysctl -w net.ipv6.conf.lo.disable_ipv6=1

To start the fun we need to debootstrap our OS. As of this step, you must be connected to the Internet:

apt update
apt dist-upgrade --yes
apt install --yes debootstrap
debootstrap --components=main,restricted,universe,multiverse \
    oracular /mnt/install/

We can use our live system to update a few files on our new installation:

echo $HOST > /mnt/install/etc/hostname
sed "s/kubuntu/$HOST/" /etc/hosts > /mnt/install/etc/hosts
cp /etc/netplan/*.yaml /mnt/install/etc/netplan/

At last, we’re ready to chroot into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install /usr/bin/env \
    PART1=$PART1 PART2=$PART2 PART3=$PART3 PART4=$PART4 \
    HOST=$HOST USERNAME=$USERNAME USERID=$USERID \
    bash --login

With our newly installed system running, let’s not forget to set up locale and time zone:

locale-gen --purge "en_US.UTF-8"
update-locale LANG=en_US.UTF-8 LANGUAGE=en_US
dpkg-reconfigure --frontend noninteractive locales
dpkg-reconfigure tzdata

To download some stuff later, wget is useful.

apt install -y wget

In order for decryption to work, we do need to set up crypttab so our encrypted partitions:

echo -n > /etc/crypttab
echo "$PART3 PARTUUID=$PART3 none luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "$PART4 PARTUUID=$PART4 none luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab

cat /etc/crypttab

To mount all those partitions, we also need some fstab entries too. ZFS entries are not strictly needed. I just like to add them in order to hide our LUKS encrypted ZFS from the file manager (habit from Nautilus days):

echo -n > /etc/fstab
echo "PARTUUID=$PART2    /boot     ext4 noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "PARTUUID=$PART1    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "/dev/mapper/$PART3 none      swap sw,nofail                                  0 0" >> /etc/fstab
echo "/dev/mapper/$PART4 none      auto nofail,nosuid,nodev,noauto                 0 0" >> /etc/fstab

cat /etc/fstab

Now we’re ready to onboard the latest Linux kernel. Since this is not a LTS release, generic kernel will do.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

On systems with a lot of RAM, I like to adjust memory settings a bit. This is inconsequential in the grand scheme of things, but I like to do it anyway. Think of it as wearing “lucky” socks:

echo "vm.swappiness=10" >> /etc/sysctl.conf
echo "vm.min_free_kbytes=1048576" >> /etc/sysctl.conf
cat /etc/sysctl.conf

Now we can create the boot environment:

apt install --yes zfs-initramfs cryptsetup keyutils plymouth-theme-spinner
update-initramfs -c -k all

And then, we can get grub going. Do note we also set up booting from swap (needed for hibernation) here too.

apt install --yes grub-efi-amd64-signed shim-signed

sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    mem_sleep_default=deep \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/$PART3)\"/" \
    /etc/default/grub

update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi \
    --bootloader-id=Ubuntu --recheck --no-floppy

cat /boot/grub/grub.cfg  | grep 'linux' | grep 'ZFS' | head -1 | xargs | cut -d' ' -f3-

I don’t like snap so I preemptively banish it from ever being installed:

apt remove --yes snapd 2>/dev/null

echo 'Package: snapd'    > /etc/apt/preferences.d/snapd
echo 'Pin: release *'   >> /etc/apt/preferences.d/snapd
echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/snapd

apt update

And now, finally, we can install our minimal desktop environment:

apt install --yes kde-plasma-desktop man-db

Since I have a framework laptop, I like to allow trim operation on its external USB drives.

cat << EOF | tee /etc/udev/rules.d/42-framework-storage.rules
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="13fe", ATTRS{idProduct}=="6500", ATTR{provisioning_mode}="unmap"
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0005", ATTR{provisioning_mode}="unmap"
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0010", ATTR{provisioning_mode}="unmap"
EOF

In order to have hibernation to work properly, we need to disable a few devices. Fortunately, we can create script to do it for us every time system goes to sleep.

cat << EOF | tee /usr/lib/systemd/system-sleep/framework
#!/bin/sh
case \$1 in
  pre)
    for DRIVER_LINK in \$(find /sys/devices/ -name "driver" -print); do
      DEVICE_PATH=\$(dirname \$DRIVER_LINK)
      if [ ! -f "\$DEVICE_PATH/power/wakeup" ]; then continue; fi
      DRIVER=\$( basename \$(readlink -f \$DRIVER_LINK) )
      if [ "\$DRIVER" = "i2c_hid_acpi" ] || [ "\$DRIVER" = "xhci_hcd" ]; then
        echo disabled > \$DEVICE_PATH/power/wakeup
     fi
    done
  ;;
esac
EOF

sudo chmod +x /usr/lib/systemd/system-sleep/framework

For hibernation, I like to change sleep settings so that hibernation kicks in automatically after 13 minutes of sleep.

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/'                           /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/'                   /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/'               /etc/systemd/sleep.conf

For that we also need to do a minor lid switch configuration adjustment. I also like to set my power button as a hibernation trigger.

apt install -y pm-utils
sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/'                          /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/'           /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=ignore/' /etc/systemd/logind.conf
sed -i 's/.*HoldoffTimeoutSec=.*/HoldoffTimeoutSec=13s/'                          /etc/systemd/logind.conf

Since Firefox is a snapd package (and we banished it), we can install it manually.

add-apt-repository --yes ppa:mozillateam/ppa

cat << 'EOF' | sed 's/^    //' | tee /etc/apt/preferences.d/mozillateamppa
    Package: firefox*
    Pin: release o=LP-PPA-mozillateam
    Pin-Priority: 501
EOF

apt update && apt install --yes firefox

Chrome aficionados, can install it too:

pushd /tmp
wget --inet4-only https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome-stable_current_amd64.deb
popd

Lastly, we need to have a user too.

adduser --disabled-password --gecos '' -u $USERID $USERNAME
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USERNAME
echo "$USERNAME ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME
passwd $USERNAME

It took a while, but we can finally exit our debootstraped environment:

exit

Let’s clean all mounted partitions and get ZFS ready for next boot:

zfs set devices=off ${HOST^}/System

sync
umount /mnt/install/boot/efi
umount /mnt/install/boot
mount | grep -v zfs | tac | awk '/\/mnt/ {print $3}' | xargs -i{} umount -lf {}
zpool export -a

After reboot, we should be done and our new system should boot with a password prompt.

reboot

Once system has booted, we can tell it to use local BIOS clock. It helps with dual-boot and I actually prefer to have local time in BIOS.

sudo timedatectl set-local-rtc 1 --adjust-system-clock

Of course, we can end with a hibernation test. If everything went correctly, this should work nicely.

sudo systemctl hibernate

Just the Hibernation Steps, Please

For a while now I use Ubuntu family of Linux on my Framework. While the exact flavor might change (Kubuntu rules! :)), two things remain the same. ZFS and hibernation. If you want to install system from scratch, you can find post for many different Ubuntu releases. What I don’t have is a separate post with just a hibernation steps. So, here it is.

First, let’s discuss prerequisites. You MUST have a separate swap partition if you want to hibernate to a file on ZFS. My setup is usually something like this:

PartitionTypeSize (MB)Description
EFIEF00FAT32255Small EFI partition
Boot8300EXT41,792Linux boot partition
Swap8200-65,536Swap, minimum 40% of RAM
(else)8309(whatever)one or more partitions for system and data

If you don’t have a separate partition, you can stop reading now and find somebody smarter. And no, the swap partition doesn’t need to be same size or larger than the amount of RAM you have. Minimum is actually 40% of your RAM (controlled by image_size parameter). Now, I’ve been guilty of having the swap the same size as RAM too. But that is not a requirement.

You also must have the Secure Boot disabled. If you don’t have it disabled, it will all seemingly work but system will never restore.

Steps I am giving here are for encrypted swap. I personally think that you should NEVER have unencrypted data on disk. NEVER. However, I am aware that most of people don’t care and see password entry as a chore. In one of the next posts, I will add instructions for non-encrypted swap too. So, if you are “one of those”, stay tuned.

Order of operations is quite fungible but, for the purpose of this guide, I will start with config files. There are two and whether you will modify them both depends on what you want to achieve.

In sleep.conf, I adjust suspend settings to allow for Suspend-then-Hibernate setup. My preference is to have system hibernate after 13 minutes of sleep but you can change that to your liking.

sudo sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/'                           /etc/systemd/sleep.conf
sudo sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/'                   /etc/systemd/sleep.conf
sudo sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' /etc/systemd/sleep.conf
sudo sed -i 's/.*HibernateMode=.*/HibernateMode=platform shutdown/'           /etc/systemd/sleep.conf
sudo sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/'               /etc/systemd/sleep.conf

Second config file is to reassign power button to hibernation. This is just my preference and, if you want power button to stay as-is, you can omit this step. For this, you need to install pm-utils package, if not already present on your system. Again, these are settings I like so adjust as needed.

sudo apt install -y pm-utils
sudo sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/'                          /etc/systemd/logind.conf
sudo sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/'           /etc/systemd/logind.conf
sudo sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=ignore/' /etc/systemd/logind.conf
sudo sed -i 's/.*HoldoffTimeoutSec=.*/HoldoffTimeoutSec=13s/'                          /etc/systemd/logind.conf

With this sorted out, you need to make sure computer doesn’t wake up. This step can be skipped quite often, but not so with Framework. With Framework laptops you need to manually disable wakeup for i2c_hid_acpi and xhci_hcd devices.

These commands will generate script I personally use and allow for its execution upon sleep.

cat << EOF | sudo tee /usr/lib/systemd/system-sleep/framework
#!/bin/sh
case \$1 in
  pre)
    for DRIVER_LINK in \$(find /sys/devices/ -name "driver" -print); do
      DEVICE_PATH=\$(dirname \$DRIVER_LINK)
      if [ ! -f "\$DEVICE_PATH/power/wakeup" ]; then continue; fi
      DRIVER=\$( basename \$(readlink -f \$DRIVER_LINK) )
      if [ "\$DRIVER" = "i2c_hid_acpi" ] || [ "\$DRIVER" = "xhci_hcd" ]; then
        echo disabled > \$DEVICE_PATH/power/wakeup
     fi
    done
  ;;
esac
EOF
sudo chmod +x /usr/lib/systemd/system-sleep/framework

Now, we can setup swap. For this I strongly recommend using variables as to avoid any naming issues. Replacewith your partition (e.g. /dev/disk/by-id/whatever-part3).

PART=<part>
UUID=`sudo blkid -s PARTUUID -o value $PART`

Assuming your swap is not initialized, you need to do so.

sudo cryptsetup luksFormat -q --type luks2 \
  --sector-size 4096 \
  --cipher aes-xts-plain64 --key-size 256 \
  --pbkdf argon2i /dev/disk/by-partuuid/$UUID
sudo cryptsetup luksOpen \
  --persistent --allow-discards \
  --perf-no_write_workqueue --perf-no_read_workqueue \
  /dev/disk/by-partuuid/$UUID $UUID
mkswap /dev/mapper/$UUID

Of course, adding this to both crypttab and fstab is also needed for swap to work properly.

echo "$UUID PARTUUID=$UUID none luks,discard,initramfs,keyscript=decrypt_keyctl" | sudo tee -a /etc/crypttab
echo "/dev/mapper/$UUID none swap sw,nofail 0 0" | sudo tee -a /etc/fstab

The final step is adding swap as RESUME into grub. Note that swap will be identified based on partition UUID.

sudo sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
  rtc_cmos.use_acpi_alarm=1 \
  RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/$UUID)\"/" \
  /etc/default/grub

With these things in place, you should be able to use hibernate. To check, use systemctl.

sudo systemctl hibernate

And yes, you can probably use these steps with any laptop, not just Framework. However, I tested this on Framework and thus will not make other claims. :)

Sweet Dreams, My Dear Framework

Setting up sleep on my Framework 13 was a bit annoying but, once set, it worked perfectly. However, the same solution didn’t work on my Framework 16. While my installation is far from standard, the hibernation steps are quite straighforward:

  1. Setup swap partition RESUME variable in grub loader
  2. Adjust sleep.conf
  3. Disable wakeup for troublesome components so your laptop doesn’t wake immediately

And it was the step 3 that presented the problem - the darn thing kept waking up.

Since I sorted this out with Framework 13, I figured I can do the same for Framework 16. Even better, I found a forum post that actually told me which components need more of a sleep.

echo disabled > /sys/devices/pci0000:00/0000:00:08.1/0000:c1:00.3/usb1/1-4/1-4.3/power/wakeup
echo disabled > /sys/devices/platform/AMDI0010:03/i2c-1/i2c-PIXA3854:00/power/wakeup
echo disabled >/sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:4b/PNP0C09:00/PNP0C0D:00/power/wakeup

Yes, it was different than my approach of disabling them in udev rules, but the same idea overall. And it even has the same suspects as for Framework 13, most notably Touchpad (i2c_hid_acpi), lid switch (button), and USB xHCI (xhci_hcd). Full of hope, I tried that and my computer still woke up.

My next step was to check the file (yes, I blindly copy/pasted it) and problem was obvious. My devices were at a different path. So I adjusted, tried it again, finished up a blog post, and called it a day. I mean, everything was working perfectly. Or so I thought.

After a few days, I placed computer into hibernate only for the darn thing to wake up on me. What the heck? I though I solved that issue. So I checked and noticed my devices were at slightly different location. Hm, maybe in all the fuss around finishing up blog post I accidentally made an error. So I addjusted paths and, with everything working correctly, called it a day.

But, guess what, in a few days I got the same issue again. And this time I was certain I had it done correctly. However, device paths were changed again. With so many independent USB devices, plug-and-play was moving stuff around every time system was rebooted.

So, I needed to script my /usr/lib/systemd/system-sleep/framework a bit smarter. At the end I ended up with this:

#!/bin/sh
case $1 in
  pre)
    for DRIVER_LINK in $(find /sys/devices/ -name "driver" -print); do
      DEVICE_PATH=$(dirname $DRIVER_LINK)
      if [ ! -f "$DEVICE_PATH/power/wakeup" ]; then continue; fi
      DRIVER=$( basename $(readlink -f $DRIVER_LINK) )
      if [ "$DRIVER" = "i2c_hid_acpi" ] || [ "$DRIVER" = "xhci_hcd" ]; then
        echo disabled > $DEVICE_PATH/power/wakeup
     fi
    done
  ;;
esac

This will search for naughty devices every time hibernate is called upon and turn off wakeup. If PnP moves them, no worries, script will find them again.

And yes, the same script works for both Framework 13 and 16.


P.S.: While I mention button driveer in text, script actually doesn’t disable wakeup on lid switch. I kinda like computer to wake when I open it.

Dark Mode Trickery

One addition to these pages you might have noticed is, at the very bottom, an icon allowing you to switch between dark and light mode. And it’s not just a simple switch, it’s a tri-switch! While it allows for fixed light and dark modes, it also includes an automatic mode (i.e., based on your system settings).

And yes, there are quite a few ways that smarter people have used, but neither one worked exactly how I wanted. So, let’s see yet another way to do the same thing.

First step is, of course, setting up CSS. All colors for light scheme get to be defined in section with prefers-color-scheme: light, while dark colors get their prefers-color-scheme: dark section. I personally like to use these to setup variables to be used later, but you can define styles directly too.

@media (prefers-color-scheme: light) {
  /* styles */
}
@media (prefers-color-scheme: dark) {
  /* styles */
}

Next step is setting up a “button” for switching between themes. While we define three links, neither one of them is shown by default - we’ll sort that out later in the code.

<span>
  <a href="" id="color-scheme-auto" style="display: none;">Auto</a>
  <a href="" id="color-scheme-dark" style="display: none;">Dark</a>
  <a href="" id="color-scheme-light" style="display: none;">Light</a>
</span>

And yes, for my pages I don’t actually use text but icons. Below are links for Lucide icons I use currently, but you can go with whichever set you want.

  •  (automatic)
  •  (dark)
  •  (light)

Lastly, we come to the code and I’m just gonna drop the whole thing here. Explanations as to what each section does will be below.

<script>
  let systemScheme = 'light';
  if (window.matchMedia('(prefers-color-scheme: light)').matches) { systemScheme = 'light'; }
  if (window.matchMedia('(prefers-color-scheme: dark)').matches) { systemScheme = 'dark'; }

  let savedScheme = localStorage.getItem("color-scheme");
  let currentScheme = systemScheme;
  switch (savedScheme) {
    case "light":
      currentScheme = "light";
      break;
    case "dark":
      currentScheme = "dark";
      break;
    default:
      savedScheme = "auto";
      break;
  }

  if (currentScheme !== systemScheme) { // swap at start so there's no flash
    for (var s = 0; s < document.styleSheets.length; s++) {
      try {
        for (var i = 0; i < document.styleSheets[s].cssRules.length; i++) {
          rule = document.styleSheets[s].cssRules[i];
          if (rule && rule.media && rule.media.mediaText.includes("prefers-color-scheme")) {
            ruleMedia = rule.media.mediaText;
            if (ruleMedia.includes("light")) {
              newRuleMedia = ruleMedia.replace("light", "dark");
            } else if (ruleMedia.includes("dark")) {
              newRuleMedia = ruleMedia.replace("dark", "light");
            }
            if (newRuleMedia !== null) {
              rule.media.deleteMedium(ruleMedia);
              rule.media.appendMedium(newRuleMedia);
            }
          }
        }
      } catch (e) { }
    }
  }

  function nextColorScheme() {
    switch (savedScheme) {
      case "light": localStorage.removeItem("color-scheme"); break;
      case "dark": localStorage.setItem("color-scheme", "light"); break;
      default: localStorage.setItem("color-scheme", "dark"); break;
    }
    window.location.reload();  // to force button update
  }

  function updateButtons() {
    switch (savedScheme) {
      case "light": document.getElementById("color-scheme-light").style.display = 'inline'; break;
      case "dark": document.getElementById("color-scheme-dark").style.display = 'inline'; break;
      default: document.getElementById("color-scheme-auto").style.display = 'inline'; break;
    }
  }

  document.addEventListener('DOMContentLoaded', function() {
    document.getElementById('color-scheme-auto').addEventListener('click', nextColorScheme);
    document.getElementById('color-scheme-dark').addEventListener('click', nextColorScheme);
    document.getElementById('color-scheme-light').addEventListener('click', nextColorScheme);
    updateButtons();
  });
</script>

The first portion just determines system scheme and stores it in systemScheme variable. Variable will contain whatever system tells the preferred scheme should be - either light or dark.

Next portion is all about loading what user (maybe) saved the last time. For this purpose we’re using localStorage and the result gets stored in savedScheme variable. Its state will set currentScheme variable to match either what is stored or the system scheme if we have no better idea (i.e., automatic mode).

End result of this variable game is decision if currentScheme differs from systemScheme. If they are different, we simply swap dark and light settings around. This swap is actually what does all the heavy lifting.

The nextColorScheme method checks the current state (savedState variable) and moves to the next one. States are written as light and dark. For automatic handling, code simply deletes storage altogether. Once that is done, it won’t attempt to sort out any swaps needed to get colors in line. Nope, it will simply reload the page and let the loading code sort it out.

The updateButtons is what displays whatever scheme is selected for a bit of user feedback.

The last portion of code will add an event listener to the click event of each scheme “button” (identified by id) so that each click calls nextColorScheme method. Here is also where we call updateButtons method to show the current state.

With all this in place, our theme switching should just work.

AuxPower1U: Mistakes were made

This is a post 11 in the series (previous: Main Controller PCB).

This post is sponsored by PCBWay.


With AuxPower1U being an actual physical object, I can see there were some mistakes. Most of them I worked around for and they’re already fixed in the repository for potential future version. But some of them will remain as a design choice.

First one has nothing to do with electronics but with my aluminium plate (aka “heat sink”). I planned for tapping holes so I can screw power supplies directly. Unfortunately, my model actually swapped diameter and radius and thus I got holes much bigger than I wanted. Classic error. So I used just screws and washers to get over that. But that actually made me think - do I really want to make all those tapping holes? Answer is no - longer screw and nut are good enough.

With that out of way, next portion are the PCB errors that were fixed courtesy of PCBWay respinning my desing. While the first revision board could be fixed using bodge wires, and I did so for firmware development, errors were big enough that I really needed a new PCB.

Current monitoring was the bigest miss here. I used my trusty ZXCT1009 - something I used many times ago. So, of course, I forgot to include ground resistor which made its readings go wild. Connecting resistor via bodge wire fixed that error. But, only when testing at 30V released the magic smoke, I checked the data sheet more closely. Yes, ZXCT1009 is limited to 20V. This never came up as most of my designs stick to 12V at most. But it became problem now.

However, finding a chip that goes up to 60V proved to be a difficult task. There actually are not too many simple chips that go that high and I didn’t want something that has more than 3 pins. Well, I think I found my new favorite current monitor - HV7801.

While HV7801 has more than 3 pins, it still comes in SOT-23 package and requires no external components. It actually occupies less PCB realestate than ZXCT1009. Downside is that its gain is fixed to 5x thus requiring me to use 1.024V ADC range. With ZXCT1009 it’s much easier to get resistor dividers than are nicely “rounded” for ADC and fit almost any range. With HV7801 you get what you get. Despite all of this, if I need current measurement that doesn’t need to be precise and I have 12-bit ADC, this chip is awesome.

Speaking of current measurement, I used 0.1Ω 3W resistor at first. It’s the same resistor I used in many other projects and it was always an overkill. What I temporarily forgot is that my low voltage ranges (e.g., 15V) will need to pass a lot of current. Thus, my trusty 0.1Ω resistor was getting way more heat than appropriate. After actually running the numbers, I decided to go with 0.025Ω 3W. Still gives nice figures for microcontroller calculations while producing way less heat.

Unfortunately, since my MCP9701A temperature sensor outputs around 800 mA at the room temperature, usage of HV7801 also meant I have to switch ADC ranges every time I measure the current. MCP9701A at 1.024V ADC range would only allow measurement up to 32°C. Even its 9700A brother (that I didn’t have in stock) would only allow up to 62°C. Switching to 2.048V just for temperature measurement is not the end of the world but it does mean slower measurement and it probably has implications for precision too.

One other case of “the forgotten resistor” is a pull-up for output MOSFETs. Early in the design phase I decided that resetting the board should not bring outputs down. Idea behind it is that, even if my board misbehaves, current will flow. There are merits on starting the system with all devices off but, since this would control power to my wireless, I decided it should fail-closed. And for that you need pull-ups. That I have forgotten.

Since we’re talking about resistors, I also had to increased ones used for LEDs. Those things were just too bright. While this means absolutely nothing once box it closed, it meant a lot during debugging since I could actually look at the PCB without burning my retinas. See, high-efficiency LEDs are not always good. :)

Speaking of debugging, I originally had my ICSP (i.e., debugging) lines shared with UART. This was necessary as I didn’t have enough pins and had to make some share functionality. Later in the project I added an I²C extender and thus aleviated the issue. However, I forgot to decouple UART lines from ICSP thus making the debugging of UART communication impossible. Solution was simple enough - just move those lines.

With all these errors, you can see why revision B was needed. While many of errors could be sorted by bodging wire here and there, and indeed they were for the purpose of firmware development, it was just too many to ever trust that board. Revision B saved the day.