X728 kit for Raspberry Pi 4

As part of my small project of movng my Z-Wave Hub to a Raspberry PI, I got an X728 kit. This has:

  • UPS controller board
    • RTC circuit
    • Battery and Power control board
  • Case
    • Button
    • Cooling fan
    • Additional Battery holder

The case has holes for wall-mounting.

The Geekworm X728 kit is very easy to build. There is a video to show how to do this:

Otherwise, refer to the hardware guide here.

In my case, before the build, I took the disassembled case to measure the holes needed for wall-mounting the case. You need fairly small screws for this. I actually had to bend the case slighly for my screws to work.

Also, I set the jumper to automatic Power-on and a few cable ties to fix a USB Hub to the case.

To test the hardware I downloaded a 64-bit Raspberry OS Lite image from Raspberrypi.com and image an micro-SD card.

  • Boot the Raspberry OS. The first boot will resize the filesystems, so please wait. Also, it will let you configure the default user and password.
  • Enable the i2c function:
    • sudo raspi-config
    • Go to Interfacing Options -> I2C - Enable/Disable automatic loading.
    • While you are at-it, you may also enable SSH.
  • Alternatively, you can a manual install by:
    • Modify the config.txt in the /boot partition:
    • Add at the end:
    • [all]
    • dtparam=i2c_arm=on
  • Install pre-requisites:
    • sudo apt-get update
    • sudo apt-get upgrade
    • sudo apt-get -y install i2c-tools
    • This is only needed for i2cdetect.
  • Reboot the system.
  • Check if the hardware is detected:
    • sudo i2cdetect -y 1
    • screenshot
    • #36 - the address of the battery fuel gauging chip
    • #68 - the address of the RTC chip
    • Different x728 versions may have different values. Mine used these values.

I personally did not like the example software. This can be found in github. Specifically, the shutdown functionality seemed to have race-conditions.

However, using it is not that complicated. So I wrote my own software, but you can do your own thing:

RTC functionality

The RTC functionality is supported by the Raspberry OS kernel. You need to enable the i2c functionality in /boot/config.txt by adding the line:

dtparam=i2c_arm=on

With that enabled, you need to add the kernel modules:

i2c-dev
rtc-ds1307

You need to enable it in the bus:

echo ds1307 0x68 > /sys/class/i2c-adapter/i2c-1/new_device

From then on, you can use the standard hwclock command.

These can be added to rc.local which is how the sample code does. I prefer to do this from a script run from systemd unit file:

# file: /etc/systemd/system/x728clock.service

[Unit]
Description=Restore / save X728 clock
DefaultDependencies=no
Before=sysinit.target shutdown.target
Conflicts=shutdown.target

[Service]
ExecStart=/etc/x728/clock.sh start
ExecStop=/etc/x728/clock.sh stop
Type=oneshot
RemainAfterExit=yes

[Install]
WantedBy=sysinit.target

After saving this file and creating a /etc/x728/clock.sh script you can:

systemctl daemon-reload
systemctl enable x728clock
systemctl start x728clock
systemctl disable fake-hwclock
systemctl stop fake-hwclock

GPIO assignments

Pin Function Direction Comment
#6 PLD in 1: A/C lost, 0: A/C OK
#5 5hutdown in Sense button press
#12 Boot out Control SW/HW controlled button
#20 Buzzer out
#26 Button out Simulate power button press
  • Reading PLD detects if the A/C power is available or not. If it reads 1 A/C power was lost. 0 if A/C power is available.
  • Buzzer if set to 1 it will sound a rather loud beep. 0 for off.
  • Button simulates pressing the hardware Off button. If you set Button to 1 for 6 seconds, the system will poweroff.
  • Shutdown is used to read the status of the hardware button. This works only if Boot is set to 1. Otherwise, Shutdown doesn't seem to work. When Boot is set to 1, it would read 1 if pressed, 0 if released. Weirdly enough, the Shutdown button is not very sensitive. It takes about 3 seconds to register the button press. The button release takes bout 50 seconds to detect.

GPIO programming

Programming GPIO is quite easy. It can be done from shell scripting using the sys file-system.

gpioIO() {
  local pin=$1
  if [ $# -eq 1 ] ; then
    cat /sys/class/gpio/gpio$pin/value
  else
    echo "$2" > /sys/class/gpio/gpio$pin/value
  fi
}

gpioInit() {
  local name="$1" pin="$2" dir="$3"

  [ ! -d /sys/class/gpio/gpio$pin ] && echo "$pin" > /sys/class/gpio/export
  echo $dir > /sys/class/gpio/gpio$pin/direction

  eval "gpio${name}() { gpioIO $pin \"\$@\" ; }"
}
ticks() {
  echo $(date +%s)$(date +%N | cut -c-2)
}

beep() {
  local len="$1" ; shift

  gpioBUZZER 1
  sleep "$len"
  gpioBUZZER 0

  [ $# -eq 0 ] && return

  local repeat="$1" idle
  [ $# -gt 1 ] && idle="$2" || idle="$len"

  while [ $repeat -gt 1 ]
  do
    repeat=$(expr $repeat - 1)
    sleep "$idle"
    gpioBUZZER 1
    sleep "$len"
    gpioBUZZER 0
  done
}

gpioInit SHUTDOWN 5 in
gpioInit PLD 6 in
gpioInit BOOT 12 out
gpioInit BUZZER 20 out
gpioInit BUTTON 26 out

Afterwards, you can just:

  • gpio[PIN] to read, i.e:
    • gpioSHUTDOWN
    • gpioPLD
  • gpio[PIN] {1|0} to write i.e.:
    • gpioBOOT 1
    • gpioBUZER 0

Reading Battery status

You can read battery voltage and battery charge from the smbus. To read the smbus I am using an example from [rpi-examples]((https://github.com/leon-anavi/rpi-examples/tree/master/BMP180/c). Specifically, I am only using the files smbus.c and smbus.h from that repository.

The code outline is:

  • open /dev/i2c-1 in read/write mode.
  • ioctl(fd, I2C_SLAVE, I2C_ADDRESS) where I2C_ADDRESS = 0x36.
  • From smbus.c read i2c_smbus_read_word_data(fd, address), and byte swap.
  • Voltage can be read from address 2:
    • Voltage = (swapped) * 1.25 / 1000 / 16
  • Battery charge can be read from address 4:
    • Battery = (swapped) / 256

The code to do this can be found on github.

A precompiled 64bit static binary can be found there too.

Power down

The x728 will turn off power by holding down the power button for around 6 seconds. Doing this will skip the shutdown process. Also, if you execute the poweroff command, the Raspberry Pi will shutdown but power will not go OFF until you hold the power button for 6 seconds.

For this to work properly, I am adding this small script to /lib/systemd/system-shutdown/gpio-poweroff:

#!/bin/sh
#
# file: /lib/systemd/system-shutdown/gpio-poweroff
# $1 will be either "halt", "poweroff", "reboot" or "kexec"
#

BUTTON=26

op_poweroff() {
  echo $BUTTON > /sys/class/gpio/export
  echo out > /sys/class/gpio/gpio$BUTTON/direction
  echo 1 > /sys/class/gpio/gpio$BUTTON/value
  sync;sync;sync
  sleep 7
  echo 0 > /sys/class/gpio/gpio$BUTTON/value
  sleep 3
}

case "$1" in
  poweroff) op_poweroff ;;
esac

This hooks into systemd's shutdown target and uses the BUTTON pin to simulate holding the button for 6 seconds to force the UPS board to power off.

UPS management

In addition, I wrote a small script to:

  • graceful shutdown when power button is pressed.
    • hold the power button, after approximately 3 seconds, you will hear 2 beeps. YOu can release the power button then. The system will do a graceful shutdown and power down.
  • When A/C power is lost:
    • If battery status can not be determined, the system will do a graceful powerdown.
    • If battery status can be read, it will beep once every 60 seconds until power is restored.
    • If battery is low, it will do a graceful powerdown.
  • Events are written to /dev/kmsg, so they could be forwarded to a syslog server.

The files to do this are:

Home Assistant

I am using the X728 kit for creating a Home Assistant installation. Home Assistant has a "managed Operating System" called Home Assistant OS which is a mostly read-only installation. This makes it complicated to add your own "low-level" customizations. To include these scripts and making them persistant accross upgrades I am hooking into the RAUC OTA upgrade subsystem.

For that, I hook up to the System handlers which makes use of a Handler Interface.

With these scripts, I am able to move the customizations from a previous image to the new upgraded image.

For this, I have a script haos-x728 that injects the customizations into a new installation image. This script also modifies /etc/rauc/system.conf so that the customization handler is called during an upgrade.

The post-install handler re-adds the handler to /etc/rauc/system.conf and copies the necessary files to the updated image.

The customization scripts are not X728 specific, and essentially lets you copy all the files in a directory to the custom image. As such, I am using to not only inject these X728 scripts, but also the vcgencmd and a muninlite agent. Also, the dependant binaries for the post-install handler are injected in the same way.

For the post-install handler to work properly you need to copy binaries for:

  • gensquashfs
  • sqfs2tar

And dependant shared libraries (that are not part of the Home Assistant OS image:

  • liblz4.so.1
  • liblz4.so.1.9.3
  • liblzma.so.5
  • liblzma.so.5.2.5
  • liblzo2.so.2
  • liblzo2.so.2.0.0
  • libselinux.so.1
  • libsquashfs.so.1
  • libsquashfs.so.1.1.0
  • libzstd.so.1
  • libzstd.so.1.4.8

The simplest way to get these is to install squashfs-tools-ng on standard Raspberry PI OS and copy those files from there.

The full set of customization files that I am using can be found here.

It contains:

RAUC handler:

  • lib/rauc/post-install

squashfs-tools-ng (dependancy to RAUC handler)

  • bin/gensquashfs
  • bin/sqfs2tar
  • lib/liblz4.so.1
  • lib/liblz4.so.1.9.3
  • lib/liblzma.so.5
  • lib/liblzma.so.5.2.5
  • lib/liblzo2.so.2
  • lib/liblzo2.so.2.0.0
  • lib/libselinux.so.1
  • lib/libsquashfs.so.1
  • lib/libsquashfs.so.1.1.0
  • lib/libzstd.so.1
  • lib/libzstd.so.1.4.8

Actual X728 support scripts.

  • bin/x728batt
  • etc/x728/clock.sh
  • etc/x728/upsmon.sh
  • etc/systemd/system/x728clock.service
  • etc/systemd/system/x728ups.service
  • etc/systemd/system/sysinit.target.wants/x728clock.service
  • etc/systemd/system/multi-user.target.wants/x728ups.service
  • lib/systemd/system-shutdown/gpio-poweroff

Munin node:

  • bin/munin-node
  • etc/systemd/system/sockets.target.wants/munin-node.socket
  • etc/systemd/system/munin-node.socket
  • etc/systemd/system/[email protected]
  • etc/muninlite.conf