[SOLVED] How to make Easy Deployment Mode enable just before shutdown!

In a quest not to need an external button press, have the system boot as soon as DC power is available, and allow the system to shutdown gracefully, I have developed the following method and want to share for those that will benefit.

This aims to:

  1. Safely (gracefully) shutdown the pi
  2. Enter EDM mode, just before halt, reboot, or poweroff (as catch alls, the behavior is the same)
  3. Allow the system to boot without external intervention as soon as power is applied (no button press)

As mentioned in other posts, a “shutdown” using this method will result in a system reboot. This is intentional and desired behavior in my use case. Alter as needed. When no DC power is connected, the system will shutdown (into EDM) and power down all connected peripherals.; if DC is (re)connected, the system will boot.

The EDM function does NOT shut the system down or bring the runlevel down to 0 gracefully, it simply cuts power and puts the HAT into EDM. That’s bad, and this is how to avoid it.

The method works by starting a service that stays running that must be stopped before the systemd reboot.target, halt.target, etc., are allowed to be entered. This is when the stop command runs. This is the last possible moment to execute something on the system and the system is considered to be in a safe state. All other programs have safely shutdown (and files closed), by design. This is the safest possible method to enter EDM.

The basics of this method consist of:

  1. Writing an enter_edm.py script
  2. Writing a service file to call the script when the service is killed (just before shutdown)
  3. Enabling the service
  4. Verifying behavior

** EDIT: ** Sometimes the ‘remove_all_scheduled_events’ fails and the script exits erroneously, I have had better luck without it, but I still remove the events in a different script, but strictly speaking it is not necessary to make that function call. I have a ‘watch’ script that monitors inputs, signals temp to the system (so that fan automation works), and initiates a shutdown sequence when DC current is 0 and battery powered (high power use can draw battery power, or has for me in the past). This script may interfere with the shutdown command in my use case, but this change helps make my system work as expected.

1. Writing an enter_edm.py script
$ sudo nano /usr/local/sbin/enter_edm.py
and add:

#! /usr/bin/python3
import sys
sys.path.append(’/home/pi/gits/sixfab-power-python-api/’)
from power_api import SixfabPower, Definition, Event
api=SixfabPower()
#api.remove_all_scheduled_events(200) # removed because if can fail
while True: api.set_edm_status(1, 1000)

2. Writing a service file to call the script when the service is killed (just before shutdown)
$ sudo nano /lib/systemd/system/shutdown_edm.service
and add:

[Unit]
Description=script to put UPS HAT in Easy Deployment Mode
Before=reboot.target halt.target poweroff.target unmount.target

[Service]
User=pi
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/true
ExecStop=/bin/sh -c ‘/usr/bin/python3 /usr/local/sbin/enter_edm.py’
TimeoutStopSec=10

[Install]
WantedBy=default.target reboot.target halt.target poweroff.target

3. Enabling the service
$ sudo systemctl enable shutdown_edm.service
$ sudo systemctl start shutdown_edm.service

3a. Disabling the service
$ sudo systemctl disable shutdown_edm.service
(Don’t run the stop, it will immediately poweroff/reboot the system)

4. Verifying behavior
$ sudo systemctl status shutdown_edm.service

The command should show that the service is active.

When shutdown on DC power, the system will reboot. This is by design of the Easy Deployment Mode.

When shutdown not on DC power, the system will shutdown and all attached hardware will power down (including GPIO, USB, everything).

When power is reapplied to the UPS hat, the system will immediately boot by leaving EDM.

Hope this helps others. I don’t think anything needs to be made executable, at minimum, everything needs to be
chmod 644’d

Shawn

1 Like

I have noticed, when running a USB hub (for which the power is cut at the same moment) it sinks power that does not allow the HAT to switch fast enough, or cleanly enough, to battery power to keep the pi powered; it immediately shuts down. In a limited number of cases in tests without the USB (and attached devices like Wifi adapter and phone) hub connected the system was able to enter battery mode quickly enough and the system did not lose power.

I mention this to elicit additional testing if this method does not work perfectly.

Because the hub can also be DC powered I plan to build a “no DC over USB adapter” from USB->terminal block connectors. I am hoping this will sufficiently isolate the USB hub and prevent the power spike that it causes to the system at the critical moment.

1 Like

Turns out, disconnecting the +5V between the Pi and the powered hub and powering the +5V line from another source solves all of my problems (so far).

The +5V still needs to be hooked up to something, even if the hub is powered (by 12V, or other); the +5V signal provides the signal to begin communication. The Pi has no way of knowing that it’s 5V out has been interrupted and the hub doesn’t care where the 5V is coming from.

The problem is the amount of current that the hub tried to sink from the Pi in the moment that 12V is cut to both the HAT and the hub. The HAT not able to make the switch to battery power, power, the Pi and provide for the spike in USB current to the hub.

Bonus is, the USB devices charge quickly, and the above describe method works perfectly to get the HAT into Easy Deployment Mode successfully. If you too have an issue with high current killing the pi after DC disconnect, try this method.

Along with the change to the enter_edm.py script noted above.

From an architecture standpoint, I strongly recommend that you run your Pi read-only in any embedded IOT application. Ideally you would never write to the boot image at all and would store data in a cloud server.

Note there is another way to do the above using overlay file systems that comes out of the box on current Raspberry Pi OS.

If you do the above it is always safe to power down.

My use case does not have an internet connection. Cloud storage has no use to me. The method I describe will work on a file system that has been made read only as long as it is configured first.

This is just a way to trigger EDM on shutdown. I use this as my car stereo.

For those that might be wanting it, here is my auto-started script (that’s up to you to implement):

read_sensors.py

import os
import sys
import time
import datetime
import subprocess
sys.path.append('./')

from power_api import SixfabPower, Definition, Event

api = SixfabPower()

ENABLE_SHUTDOWN = True

ENABLE_SHUTDOWN_EVENT = False
ENABLE_SHUTDOWN_COMMAND = True

BLANK_LCD = False

FORCE_RTC_SET = False
FORCE_SYS_SET = False

print("|***** Input Sensors *****|**** System Sensors *****|**** Battery Sensors ****|")
print("|     Temp: %5.2f °C      |       Temp: %5.2f °C    |       Temp: %5.2f °C    |" % (api.get_input_temp(), api.get_system_temp(), api.get_battery_temp()))
print("|  Voltage: %5.2f V       |    Voltage: %5.2f V     |    Voltage: %5.2f V     |"  % (api.get_input_voltage(), api.get_system_voltage(), api.get_battery_voltage()))
print("|  Current: %5.2f A       |    Current: %5.2f A     |    Current: %5.2f A     |"  % (api.get_input_current(), api.get_system_current(), api.get_battery_current()))
print("|    Power: %5.2f W       |      Power: %5.2f W     |      Power: %5.2f W     |"  % (api.get_input_power(), api.get_system_power(), api.get_battery_power()))
print("|********** GPS **********|********** Fan **********|  Level/Max:   %3d %%/%3d |"   % (api.get_battery_level(), api.get_battery_max_charge_level()))
print("|  Status:                |     Health:  %7s    |     Health:   %3d %%     |"  % ("healthy" if api.get_fan_health() == 1 else "broken", api.get_battery_health()))
print("|    Mode:                |      Speed: %5d RPM   |   Capacity:  %4d mAh   |" % (api.get_fan_speed(), api.get_battery_design_capacity()))
print("|     Lat:      Err y:    |       Mode: %5d       |                         |" % (api.get_fan_mode()))
print("|     Lon:      Err x:    | Thresholds:[%3d,%3d] °C |                         |" % (api.get_fan_automation()[0], api.get_fan_automation()[1]))
print("|Altitude:      Err v:    |***** Sys Date Time *****|***** RTC Date Time *****|")
t1 = datetime.datetime.fromtimestamp(int(time.time()))
t2 = int(api.get_rtc_time(Definition.TIME_FORMAT_EPOCH))
print("|DateTime:      Err t:    |     Epoch :  %s |     Epoch :  %s |" % (str(int(time.time())), t2))
print("|   Speed:      Err s:    |   -> date :  %s |   -> date :  %s |" % (t1.strftime("%Y-%m-%d"), api.get_rtc_time(Definition.TIME_FORMAT_DATE)))
print("|   Track:      Err d:    |   -> time :   %s  |   -> time :   %s  |" % (t1.strftime("%H:%M:%S"), api.get_rtc_time(Definition.TIME_FORMAT_TIME)))
print("|   Climb:      Err c:    | lc %s  | lc %s  |"% (t1, datetime.datetime.fromtimestamp(t2) ) )
print("|*************************|*************************|*************************|")


if int(time.time()) - 2 > t2 or FORCE_RTC_SET:
    print("sys -> RTC")
    api.set_rtc_time(int(time.time()))
    print("api.set_rtc_time(int(time.time()+time.timezone))")
if t2  - 2 > time.time() or FORCE_SYS_SET:
    print("RTC -> sys")
    cmd = str("sudo timedatectl set-ntp no")
    print(cmd)
    os.system(cmd)
    dt = datetime.datetime.fromtimestamp(api.get_rtc_time(Definition.TIME_FORMAT_EPOCH))
    cmd = str("sudo timedatectl set-time '"+str("%s" % (dt))+"'")
    print(cmd)
    os.system(cmd)
    cmd = str("sudo timedatectl set-ntp yes")
    print(cmd)
    os.system(cmd)
    
api.send_system_temp()

if ENABLE_SHUTDOWN and api.get_working_mode() == Definition.BATTERY_POWERED and api.get_input_power() <= 0.:
    print("Result removing all Scheduled Event: " + str(api.remove_all_scheduled_events(200)))
        
    print("Powering down")
    """
    level, status = 50, 2
    api.set_safe_shutdown_battery_level(level)
    api.set_safe_shutdown_status(status)
    """

    sleep_time, run_time = 1439, 0
    api.set_power_outage_params(sleep_time, run_time)
    
    status = 2
    api.set_lpm_status(status)

    # create power off event
    
    if ENABLE_SHUTDOWN_EVENT:
        print("Creating event")
        event = Event()
        event.id = 1
        event.schedule_type = Definition.EVENT_INTERVAL
        event.repeat = Definition.EVENT_ONE_SHOT
        event.time_interval = 5
        event.interval_type = Definition.INTERVAL_TYPE_SEC
        event.action = Definition.HARD_POWER_OFF

        result = api.create_scheduled_event_with_event(event, 200)
    else:
        print("Shutdown event disabled")

    if ENABLE_SHUTDOWN_COMMAND:
        print("Sending command")
        if subprocess.run(["systemctl", "is-active --quiet shutdown_edm.service || systemctl start shutdown_edm.service"]) == 0:
            print("EDM service active")  
        os.system("sleep 1 && sudo shutdown -h now")
    else:
        print("Shutdown command disabled")

    if BLANK_LCD:
        os.system("vcgencmd set_backlight 0 0")
        os.system("vcgencmd display_power 0 0")
    else:
        print("Keeping LCD on for now")

    """
    status = 1
    api.set_edm_status(status)
    """
    print("Good byte")
    exit()