commit 017ebda429ec30aca096ff2eae4124bbc397aa54 Author: Scott Alfter Date: Fri Mar 28 12:39:52 2025 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/README.md b/README.md new file mode 100644 index 0000000..456d0f5 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +UPS software for Geekworm X728 +============================== + +Install prerequisites: + +```sudo apt update && sudo apt install -y python3 python3-smbus python3-pip && sudo pip3 install --break-system-packages rpi-lgpio``` + +Install the software: + +```sudo cp x728ups x728off /usr/local/bin && sudo cp x728ups.service /lib/systemd/system && sudo systemctl daemon-reload``` + +Enable the software: + +```sudo systemctl enable x728ups.service && sudo systemctl start x728ups.service``` + +Monitor the daemon while it's running: + +```journalctl -fu x728ups``` + +Ways to do a clean shutdown +--------------------------- + +* ```sudo x728off``` (don't use ```shutdown -h now```) +* send SIGUSR1 (this is what x728off does) +* hold down the button for 4 seconds +* unplug power and wait for the battery to get to 20% remaining + +Ways to reboot +-------------- + +* the old standby: ```shutdown -r now``` +* hold down the button for 1 second + +Kill the buzzer when on battery +------------------------------- + +* send SIGUSR2: ```sudo pkill -USR2 -u root -f x728ups``` + +Notes +----- + +Tested on a Raspberry Pi 4B running Armbian Bookworm, with a Geekworm X728 v2.5. diff --git a/x728off b/x728off new file mode 100755 index 0000000..d07c890 --- /dev/null +++ b/x728off @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +source /etc/profile +sudo pkill -USR1 -u root -f x728ups diff --git a/x728ups b/x728ups new file mode 100755 index 0000000..1c5ebd9 --- /dev/null +++ b/x728ups @@ -0,0 +1,112 @@ +#!/usr/bin/env python +import struct +import RPi.GPIO as GPIO +import smbus +import time +import subprocess +import sys +import signal + +GPIO_BUZZER_PIN=20 # controls the buzzer +GPIO_ON_BATT=6 # set when running on battery +GPIO_PWR_CTRL=26 # toggle this for 2 seconds before shutdown, or else the board won't respond to the first power-on event +GPIO_BTN=5 # button signal: 500ms pulse for reboot, stays high for shutdown +GPIO_BOOT=12 # enable the button +I2C_ADDR=0x36 # the chip that reads battery charge state is here +CRIT_BATT=20 # shut off at this percentage + +class X728UPS: + + def __init__(self): + GPIO.setmode(GPIO.BCM) + GPIO.setup(GPIO_BUZZER_PIN, GPIO.OUT, initial=GPIO.LOW) + GPIO.setup(GPIO_PWR_CTRL, GPIO.OUT, initial=GPIO.LOW) + GPIO.setup(GPIO_ON_BATT, GPIO.IN) + GPIO.setup(GPIO_BOOT, GPIO.OUT, initial=GPIO.HIGH) + GPIO.setup(GPIO_BTN, GPIO.IN) + GPIO.add_event_detect(GPIO_BTN, GPIO.RISING, callback=lambda ch: self.ButtonCallback()) + signal.signal(signal.SIGUSR1, self.Shutdown) + self.buzzer_on=True + signal.signal(signal.SIGUSR2, self.ToggleBuzzer) + + def ToggleBuzzer(self, signum, frame): + self.buzzer_on=not self.buzzer_on + signal.signal(signal.SIGUSR2, self.ToggleBuzzer) + + def Shutdown(self, signum, frame): + print("shutdown requested by SIGUSR1", flush=True) + GPIO.output(GPIO_PWR_CTRL, GPIO.HIGH) + time.sleep(2) + GPIO.output(GPIO_PWR_CTRL, GPIO.LOW) + subprocess.run(["shutdown", "-h", "now"]) + sys.exit() + + def ButtonCallback(self): + if GPIO.input(GPIO_BTN): + t=0 + while GPIO.input(GPIO_BTN) and t<1000: + time.sleep(0.01) + t=t+10 + if t>=200 and t<=600: # nominal 500ms pulse for reboot, or stays high for shutdown until powered off + print("reboot requested", flush=True) + time.sleep(1) # so the message above gets logged + subprocess.run(["shutdown", "-r", "now"]) + sys.exit() + else: + print("shutdown requested", flush=True) + GPIO.output(GPIO_PWR_CTRL, GPIO.HIGH) + time.sleep(2) + GPIO.output(GPIO_PWR_CTRL, GPIO.LOW) + subprocess.run(["shutdown", "-h", "now"]) + sys.exit() + + def readVoltage(self, bus): + address = I2C_ADDR + read = bus.read_word_data(address, 2) + swapped = struct.unpack("H", read))[0] + voltage = swapped * 1.25 /1000/16 + return voltage + + def readCapacity(self, bus): + address = I2C_ADDR + read = bus.read_word_data(address, 4) + swapped = struct.unpack("H", read))[0] + capacity = swapped/256 + return capacity + + def main(self): + bus=smbus.SMBus(1) + + while True: + v=self.readVoltage(bus) + c=self.readCapacity(bus) + if GPIO.input(GPIO_ON_BATT): + p="OB" + else: + p="OL" + if c