What would be the proper way to add a power button for BalenaOS

Using a Raspberry Pi 3B+'s GPIO pinout, what would be the best way to add a button that can interact with the balenaOS to “Halt” balenaOS. Raspberry pi is able to Halt in raspbian:

https://www.quartoknows.com/page/raspberry-pi-shutdown-button

Essentially it’s a sudo shutdown -h now command. I’m not sure that this would work for BalenaOS.

What are your thoughts?

Hi,

I can point you to the code on how our supervisor performs a shutdown: https://github.com/balena-io/balena-supervisor/blob/6b5617be155c368f8ff02f044ef1c9935901426b/src/lib/systemd.ts#L85-L95. It is basically using the HostOS dbus to send a PowerOff command. I’m not an OS guy, but afaik this is not an official interface so this might break in the future.

If you plan to build this with balenaCloud where you have a supervisor available. I would suggest to use the supervisor api to perform the shutdown. https://www.balena.io/docs/reference/supervisor/supervisor-api/#post-v1shutdown

Best Regards,

Basically what I’m trying to achieve is the ability to use one button to both sleep the device and once pressed again, to turn on the device. Does Balena have any suggestions for achieving this? Even if it isn’t a true sleep but shuts down as many things as possible but doesn’t actually cut power

I seem to recal a GPIO pin that, when grounded/pulled high, would start a Pi from a halt state. You could wire this up and then run a service to hook the GPIO event (maybe in Python) and then call the DBUS power off command mentioned above. That’s the only thing I can think of right now, I hope it helps.

So how would I implement that into a python script like this:

#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import subprocess

GPIO.setmode(GPIO.BOARD)
# set pin 5 to input, and enable the internal pull-up resistor
GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_UP)

oldButtonState1 = True
while True:
    buttonState1 = GPIO.input(5)

    if buttonState1 != oldButtonState1 and buttonState1 == False :
        # print "Button 1 pressed"
        subprocess.call("halt", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    oldButtonState1 = buttonState1

    time.sleep(.1)

@KingstonSteele what I would try is basically call the following:

dbus-send --system \
        --print-reply=literal \
        --dest=org.freedesktop.login1 \
        /org/freedesktop/login1 \
        org.freedesktop.login1.Manager.Suspend

where your script calls subprocess.call(..., I’m not sure if your device supports suspend to ram, but that might work. I couldn’t find the halt equivalent on the login1.Manager dbus interface, suspend seems the closest

So would this require an internet connection to turn off then? The Balena poweroff function or shutdown is actually a halt. The device CAN be turned back on by shorting pins 5 and 6, the i2c pins. That is working as of right now. That’s just an inherent feature of all Raspberry Pis. What I need to be able to do is to push the same button to send a poweroff or shutdown command, preferably all locally and without a dependency of an internet connection.

Hi @KingstonSteele, you could run the dbus-send command locally – without an internet connection.

I tested and see that this command works as well:

dbus-send --system \
    --print-reply=literal \
    --dest=org.freedesktop.login1 \
    /org/freedesktop/login1 \
   "org.freedesktop.login1.Manager.PowerOff" boolean:true

Let us know if we could help further.

@gelbal That looks correct to me. How would I implement that into the python script above? What would that look like? Sorry for the Noob question.

#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import subprocess

GPIO.setmode(GPIO.BOARD)
# set pin 5 to input, and enable the internal pull-up resistor
GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_UP)

oldButtonState1 = True
while True:
    buttonState1 = GPIO.input(5)

    if buttonState1 != oldButtonState1 and buttonState1 == False :
        # print "Button 1 pressed"
        subprocess.call(
        dbus-send --system \
    --print-reply=literal \
    --dest=org.freedesktop.login1 \
    /org/freedesktop/login1 \
   "org.freedesktop.login1.Manager.PowerOff" boolean:true)

    oldButtonState1 = buttonState1

    time.sleep(.1)

Hey there! it’s probably worth checking out this library: https://pypi.org/project/dbus-python/ . I 'd experiment with some dbus-python examples onlite too. Hope it helps!

@shaunmulligan @gelbal What if you were running this inside the HostOS instead of a container? Would that simplify things or would I still need to use dbus?

Hi @KingstonSteele, you could run shutdown command on hostOS to shut down RPi. As for simplifying things, it depends on how you’d like to setup this. Coding such control in the service container would probably make it easier to maintain it. As the setup will be part of the application code.

Having said these, I’m not sure if RPi supports sleep mode. Searching online, I see several notes about RPi not having any sophisticated power management capabilities. (this is one of the things balenaFin addresses)

I was able to get back onto this. I’m committed to finding a solution and sharing it for others.
So I was able to make some stuff happen and get some error logs, I went through them all (things like exporting dbus, etc) but now I have no log at all when I press the button. So no errors but also no shutdown just this:

<Interface <ProxyObject wrapping <dbus._dbus.SystemBus (system) at 0x769f41b0> :1.1 /org/freedesktop/login1 at 0x769ef690> implementing 'org.freedesktop.login1.Manager.PowerOff' at 0x769ef830>

dockercompose

  button:
    build: ./button
    restart: always
    network_mode: host
    privileged: true
    labels:
      io.balena.features.dbus: '1'
      io.balena.features.firmware: '1'
    environment:
      - DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket

dockerfile:

#!/usr/bin/env bash

## connect to the host's system bus from the application container
export DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket

python button.py

button.py

import RPi.GPIO as GPIO
import time
import dbus

# Set GPIO mode: GPIO.BCM or GPIO.BOARD
GPIO.setmode(GPIO.BOARD)

# Set pin 5 an an input, and enable the internal pull-up resistor
GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_UP)

oldButtonState1 = True

while True:
    buttonState1 = GPIO.input(5)

    if buttonState1 != oldButtonState1 and buttonState1 == False :
        bus = dbus.SystemBus()
        boolean = dbus.Boolean(True)
        remote_object = bus.get_object('org.freedesktop.login1', '/org/freedesktop/login1')
        interface = dbus.Interface(remote_object, 'org.freedesktop.login1.Manager.PowerOff')

        print interface

    oldButtonState1 = buttonState1

time.sleep(1)

Once this is solved it will wake using the same button. I can power the device down from the dashboard/balena cloud and wake it back up whenever by just pressing the button. So when this get’s solved, it’ll be a pretty cool, handy feature.

Hi @KingstonSteele that would indeed be a very handy feature. One thing that might help troubleshoot is to run the 'org.freedesktop.login1.Manager.PowerOff' command from within your container manually using dbus-send and simultaneously tail the journal logs on the hostOS. If that doesn’t work we know there is probably some problem on the OS level

One other thing I noticed is that you said you can shutdown the pi from the dashboard and bring it back via the button. If that works, you could wire the button call the supervisor shutdown API endpoint from your container (this does NOT need an internet connection as its all over local docker network) and that will shutdown the device in the same way it does from the dashboard

Sending this command within the hostOS terminal works:

dbus-send --system \
    --print-reply=literal \
    --dest=org.freedesktop.login1 \
    /org/freedesktop/login1 \
   "org.freedesktop.login1.Manager.PowerOff" boolean:true

Sending that same command inside the button service results in this error:

bash: dbus-send: command not found

Hi there,

You’ll need to install the dbus package for that command to exist in your container.

If you are using balena base images, you can do that by adding a line to your Dockerfile like so (also see https://www.balena.io/docs/reference/base-images/base-images/#features-overview & https://www.balena.io/docs/reference/base-images/base-images/#installing-packages):

RUN install_packages dbus

I’ve added the line like so:

FROM resin/%%RESIN_MACHINE_NAME%%-python

#switch on systemd init system in container
ENV INITSYSTEM on

RUN install_packages dbus

ENV DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket

# pip install python deps from requirements.txt
# For caching until requirements.txt changes
COPY ./requirements.txt /requirements.txt
RUN pip install -r /requirements.txt

COPY . /usr/src/app
WORKDIR /usr/src/app

CMD ["python", "button.py"]

But I get this error during build:

[button]        /bin/sh: 1: install_packages: not found
[button]        
[button]        Removing intermediate container 91a25804abde
[button]        The command '/bin/sh -c install_packages dbus' returned a non-zero code: 127

Hi again,

I did not realize you were using the resin base images still. Since those images are no longer supported, we recommend switching to the newer balenalib images: https://www.balena.io/docs/reference/base-images/legacy-base-images/.

You can install the dbus package using the package management system, which in your case seems to be apt-get. Something like RUN apt-get update && apt-get install -y dbus should do the right thing, though we really recommend switching base images as soon as possible. Do note all the breaking changes listed here: https://www.balena.io/docs/reference/base-images/base-images/#major-changes.

Well I changed up my dockerfile quite a bit here:

FROM balenalib/%%BALENA_MACHINE_NAME%%-debian-python:3.7.4

# Enable systemd init system
ENV INITSYSTEM on

# Set the working directory
WORKDIR /usr/src/app

RUN install_packages dbus

# Upgrade pip
RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip install --user -r requirements.txt --no-cache-dir --disable-pip-version-check \
                --index-url https://www.piwheels.org/simple

# Copy everything into the container
COPY . ./
#Make sure scripts in .local are usable:
ENV PATH=/root/.local/bin:$PATH
ENV DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket
# Start application
CMD ["bash", "start.sh"]

start.sh

#!/usr/bin/env bash

## connect to the host's system bus from the application container
export DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket

python button.py

button.py

import RPi.GPIO as GPIO
import time
import dbus

# Set GPIO mode: GPIO.BCM or GPIO.BOARD
GPIO.setmode(GPIO.BOARD)

# Set pin 5 an an input, and enable the internal pull-up resistor
GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_UP)

oldButtonState1 = True

while True:
    buttonState1 = GPIO.input(5)

    if buttonState1 != oldButtonState1 and buttonState1 == False :
        bus = dbus.SystemBus()
        boolean = dbus.Boolean(True)
        remote_object = bus.get_object('org.freedesktop.login1', '/org/freedesktop/login1')
        interface = dbus.Interface(remote_object, 'org.freedesktop.login1.Manager.PowerOff')

        print (interface)

    oldButtonState1 = buttonState1

time.sleep(1)

The result is still:

<Interface <ProxyObject wrapping <dbus._dbus.SystemBus (system) at 0x768e2fc0> :1.0 /org/freedesktop/login1 at 0x768f13d0> implementing 'org.freedesktop.login1.Manager.PowerOff' at 0x76582910>