Raspberry PI mini server with UPS

They can be pretty inexpensive, all things considered. The Ender 3, for example, is well under US$200, it’s pretty popular (I have an Ender 3 Pro, which adds a few upgrades), and you can do a lot with it. Be sure to set up OctoPi when you get it:

1 Like

I probably used the wrong word “big purchase” i think it’s more along the lines of justifying tech to someone who doesn’t see a use as they haven’t used it although i have to give her credit when i met her about 10years ago they only thing she used her computer for was ms exel and solitaire.

To be quite honest it’s sometimes good to have that voice saying “and we need this why” it’s made me prioritise spending to projects i really like and have stopped me wasting time on some things which in hindsight turned out to be a money pit

So I did that, and the tl;dr seems to be that there’s a bug in how the UPS board’s reporting things. Here’s the script I wrote:

#!/usr/bin/python3
import smbus2
DEVICE_BUS = 1
DEVICE_ADDR = 0x17

bus = smbus2.SMBus(DEVICE_BUS)

aReceiveBuf = []
aReceiveBuf.append(0x00)   # Placeholder

for i in range(1,32):
    aReceiveBuf.append(bus.read_byte_data(DEVICE_ADDR, i))

print(" ".join(hex(n) for n in aReceiveBuf))

bReceiveBuf = []
bReceiveBuf = bus.read_i2c_block_data(DEVICE_ADDR, 0, 32)

print(" ".join(hex(n) for n in bReceiveBuf))

It reads aReceiveBuf byte-by-byte, just as the original script did (though only for 32 bytes), and reads bReceiveBuf as a block. Here’s the output, with the charger unplugged:

0x0 0xf2 0xc 0x2 0x14 0x37 0x10 0xc9 0x14 0x3 0x0 0x36 0x0 0xce 0x10 0x74 0xe 0x74 0xe 0x5e 0x0 0x2 0x0 0x1 0x7a 0x1 0xdb 0x0 0xad 0x2f 0x0 0x0
0x1 0xf2 0xc 0x2 0x14 0x37 0x10 0xc9 0x14 0x3 0x0 0x36 0x0 0xce 0x10 0x74 0xe 0x74 0xe 0x5e 0x0 0x2 0x0 0x1 0x7a 0x1 0xdb 0x0 0xad 0x2f 0x0 0x0

Note that, except for byte 0 (which is unused anyway), the output is identical.

According to the UPS-Plus wiki, and according to the original code, bytes 7 and 8 contain the USB-C charging port voltage, LSB first, in mV. Unless I can’t count any more, this reads 0x14c9 mV, or 5321 mV–an entirely reasonable reading for a 5V-nominal charger. Except that the charger is unplugged.

Edit:

3 Likes

Probably are going to be tempted to write it in C ;
Though one of the objectives is actually to have a fun project and learn some python on the go.

@danb35, without having a board to test this what i’d would try first:

3 Likes

Haven’t tried your suggestions yet, but there’s also discussion of this script on the Raspberry Pi forums:
https://www.raspberrypi.org/forums/viewtopic.php?f=28&t=316171

Sounds like they’re echoing some of your comments about opening resources excessively.

3 Likes

I’m thinking about to structure the code/script as I’m used to do for code running on mcu’s.
Which means two or more, old school, nested state machines. Probably mealy machines as they are easier to code.

Tried to do this in python and to my surprise python has no switch/case statement. :sob:
Here some demo-code (heavily influenced by my C habits) for two nested state machines:

#!/usr/bin/python3

""" 
First go on programming two nested (moore) state machines in python.
"""
import sys
from enum import Enum
from time import sleep

# DEMOCODE
test_counter = 1
test_exception_counter = 13


class Main (Enum):
    Init = 0
    Running = 1
    Powerdown = 2
    Exception = 3


class Running (Enum):
    Init = 0
    Read = 1
    UpdateDisplay = 2


def set_main_state(state):
    global main_state
    main_state = state


def set_running_state(state):
    global running_state
    running_state = state


main_state = Main.Init
running_state = Running.Init

# Main state machine
while True:

    if (main_state == Main.Init):

        try:
            print("\nInitializing\n")
            set_main_state(Main.Running)
            # DEMOCODE
            sleep(1)
            if ((test_exception_counter % 13) == 0):
                raise Exception("I do not know Python!")

        except Exception as error:
            print("Somthing went wrong while Initilazing ")
            the_error = error
            set_main_state(Main.Exception)
            pass

    elif (main_state == Main.Running):

        # Nested Running state machine
        while (main_state == Main.Running):

            if (running_state == Running.Init):
                print("Setting up running statemachine\n")
                set_running_state(Running.Read)

            elif (running_state == Running.Read):
                try:
                    print("Reading Data")
                    # DEMOCODE
                    if (test_counter >= 9):
                        test_counter = 0
                        set_main_state(Main.Powerdown)
                    else:
                        set_running_state(Running.UpdateDisplay)

                    test_exception_counter += 1
                    if ((test_exception_counter % 13) == 0):
                        # Introduce a bug of our own making
                        something = unknown

                except Exception as error:
                    print("\nERROR: Unable to Read")
                    the_error = error
                    set_main_state(Main.Exception)
                    pass

            elif (running_state == Running.UpdateDisplay):
                print("Updating Display")
                # DEMOCODE
                if ((test_counter % 3) == 0):
                    set_running_state(Running.Read)

            else:
                the_error = "PANIC: Unknown Running state"
                set_main_state(Main.Exception)

            # DEMOCODE
            test_counter += 1
            sleep(0.1)

        # End Nested Running state machine

    elif (main_state == Main.Powerdown):
        print("\nGracefully powering down the system\n")
        # DEMOCODE
        set_main_state(Main.Running)

    else: # catch all including Main.Exception
        if (main_state != Main.Exception) :
            the_error = "PANIC: Unknown Main state"
            
        print(f"\nAn exception occurred : {the_error}")
        print("We logging and trying to fix it\n")
        set_main_state(Main.Init)
        # DEMOCODE
        test_exception_counter = 1

    sleep(1)

# END Main state machine

Codding in state machines result’s in very ringed and reliably code, specially because you have a clear flow of your code. Hence it is used in the embedded space very often. :slight_smile:

EDIT: experimented with exception handling ans introduced a bug of my own making, catch it and don’t die… :rofl:

2 Likes

The UPS and display arrived , unfortuany 18650 batteries come in two sizes (ca 65 mm and 70 mm) and the 70 mm iv got do not fit… (you need 65 mm long batteries…)

display is working and observed that writing the 64x128 pixels is quite expensive
and this is also done on the same i2c bus, hence have enabled i2c bus 3 by adding

dtoverlay=i2c-gpio,i2c_gpio_sda=5,i2c_gpio_scl=6,bus=3
to /boot/config.txt

Then pin GPIO-5 (pin 29) is sda and GPIO-6 (pin 31) is scl of the i2c_bus 3.
and connect the display pins to those, hoping with the reduced traffic on i2c bus 1 it will gain stability.

in the Python script one obviously need to initialize the display with i2c bus 3 too:
disp = Adafruit_SSD1306.SSD1306_128_64(rst=None, i2c_bus=3 )

3 Likes

@danb35 made a extra test script :

#!/usr/bin/python3

from collections import namedtuple
import Adafruit_GPIO.I2C as I2C
from ina219 import INA219, DeviceRangeError

DEVICE_BUS = 1
DEVICE_ADDR = 0x17
INA_DEVICE_ADDR = 0x40
INA_BATT_ADDR = 0x45

ups_i2c = I2C.get_i2c_device(DEVICE_ADDR, busnum=DEVICE_BUS)
ina_i2c = INA219(0.00725, address=INA_DEVICE_ADDR, busnum=DEVICE_BUS)
ina_i2c.configure()
ina_batt_i2c = INA219(0.005, address=INA_BATT_ADDR, busnum=DEVICE_BUS)
ina_batt_i2c.configure()


def read_ups():
    global ups_i2c
    global ina_i2c
    global ina_batt_i2c

    UpsData = namedtuple("UpsData",
                         "McuVccVolt PogoPinVolt BatPinCVolt UsbCVolt UsbMicroVolt "
                         "BatTemperature BatFullVolt BatEmptyVolt BatProtectVolt BatRemaining "
                         "SampleTime AutoPowerOn FullRunTime FullChargeTime RunningTime Version "
                         "PiVolt PiCurrent PiPower BattVolt BattCurrent BattPower")

    buf = []

    # UPS Register Addresses from
    # https://wiki.52pi.com/index.php/UPS_Plus_SKU:_EP-0136?spm=a2g0o.detail.1000023.17.4bfb6b35vkFvoW#USB_Plus_V5.0_Register_Mapping_Chart

    buf = ups_i2c.readList(0x00, 0x20)
    McuVccVolt = int.from_bytes([buf[0x01], buf[0x02]], byteorder='little')
    PogoPinVolt = int.from_bytes([buf[0x03], buf[0x04]], byteorder='little')
    BatPinCVolt = int.from_bytes([buf[0x05], buf[0x06]], byteorder='little')
    UsbCVolt = int.from_bytes([buf[0x07], buf[0x08]], byteorder='little')
    UsbMicroVolt = int.from_bytes([buf[0x09], buf[0x0A]], byteorder='little')
    BatTemperature = int.from_bytes([buf[0x0B], buf[0x0C]], byteorder='little')
    BatFullVolt = int.from_bytes([buf[0x0D], buf[0x0E]], byteorder='little')
    BatEmptyVolt = int.from_bytes([buf[0x0F], buf[0x10]], byteorder='little')
    BatProtectVolt = int.from_bytes([buf[0x11], buf[0x12]], byteorder='little')
    BatRemaining = int.from_bytes([buf[0x13], buf[0x14]], byteorder='little')
    SampleTime = int.from_bytes([buf[0x15], buf[0x16]], byteorder='little')
    AutoPowerOn = buf[0x19]

    buf = buf + ups_i2c.readList(0x1C, 0x2A)
    FullRunTime = int.from_bytes(
        [buf[0x1C], buf[0x1D], buf[0x1E], buf[0x1F]], byteorder='little')
    FullChargeTime = int.from_bytes(
        [buf[0x20], buf[0x21], buf[0x22], buf[0x23]], byteorder='little')
    RunningTime = int.from_bytes(
        [buf[0x24], buf[0x25], buf[0x26], buf[0x27]], byteorder='little')
    Version = int.from_bytes([buf[0x28], buf[0x29]], byteorder='little')

    # Read both ina219 powermonitors
    PiVolt = int(ina_i2c.voltage() * 1000)
    try:
        PiCurrent = int(ina_i2c.current())
        PiPower = int(ina_i2c.power())
    # FIXME : What is DeviceRangeError ?
    except DeviceRangeError:
        PiCurrent = 0
        PiPower = 0

    BattVolt = int(ina_batt_i2c.voltage() * 1000)
    try:
        BattCurrent = int(ina_batt_i2c.current())
        BattPower = int(ina_batt_i2c.power())
    # FIXME : What is DeviceRangeError ?
    except DeviceRangeError:
        BattCurrent = 0
        BattPower = 0

    return UpsData(McuVccVolt, PogoPinVolt, BatPinCVolt, UsbCVolt, UsbMicroVolt,
                   BatTemperature, BatFullVolt, BatEmptyVolt, BatProtectVolt, BatRemaining,
                   SampleTime, AutoPowerOn, FullRunTime, FullChargeTime, RunningTime, Version,
                   PiVolt, PiCurrent, PiPower, BattVolt, BattCurrent, BattPower)


if __name__ == "__main__":

    print("------------------")
    print("UPS READ DATA TEST")
    print("------------------\n")

    upsdata = read_ups()
    print(upsdata)

    print("\n------------------")

And branched of wip to testing,
(note: sysinfo stats is based on psutils, on Raspberry OS needed psutil installed with pip3)

3 Likes

OK, revisiting this topic a bit. I don’t know if my UPS board is broken or not (only one of the two I ordered ever worked), but there’s simply no way to turn it off–and the nice, compact case design makes it near-impossible, not only to initially assemble, but also to disassemble, and particularly to remove the batteries. Nice concept, questionable (at best) execution.

Yeah–the design means you’re assembling and disassembling the whole system, in very tight quarters, using metal tools, under power. The lack of a hard power switch on the UPS board is a fatally stupid design decision.

2 Likes