Single power button for Klipper printers

Disclaimer: I have no idea what I’m doing, please be careful with mains voltage.

Motivation

Any 3D printer running klipper needs some machine running Linux acting as the host. Most, like me, use a Raspberry Pi for this.
If you’re building one, you’d want to be able to turn it on, so you put a big fat power switch on the back. Done!
Well, at some point, you might want to turn it off again. Just toggeling the power button you installed is not advisable, since just cutting power to the Raspberry Pi might corrupt the filesystem, causing it to not boot the next time you turn it on. Besides that, maybe you forgot to check that the hotend is cold enough, and cutting power would result in a clogged heatbreak.

The initial idea

To solve both of these problems, I wanted to shut down the printer through software, instead of just cutting power. Then, the Raspberry Pi could calmly shut itself down. Once that’s done, however, I still needed to actually cut the power to the rest of the system.

For that, I wanted to use a 2-coil latching relay. A relay is basically an electromechanical switch, containing an electromagnet. If it’s powered, the magnet activates the switch and current can flow (in case of a normally-open relay) or it seizes to flow (normally-closed relay).

A 2-coil latching relay is a particular kind of relay that is triggered not by continous current flowing through the coil of the electromagnet, but through a single pulse. Since it has two coils, one coil closes the switch, and the other one opens it. It’s somewhat analogous to a SR-Flipflop.

After I found a nice 2-coil latching relay on digikey, I went ahead and put together a circut to do what I want: schematic

Let’s walk through how it was supposed to work:

Initially, the printer is off. Then, I press a momentary (non-latching) switch, mounted to the front of my printer. That pulls SWITCH_ON to ground, energizing the set-coil (pins 1 & 2) of the relay. Now, the printer is on, even if I let go of the button. Great!

Now, how do I turn it off again? Just pressing the button again has no effect, since it would only energize the set-coil, doing absolutely nothing. For this, I did some research, and found the gpio-poweroff dtoverlay for the Raspberry Pi. It’s a module which can be configured to pull a GPIO pin high when the Raspberry Pi is completely powered off. That GPIO pin was supposed to be connected to RPI_GPIO in the schematic. Then, once the Raspberry Pi is powered off, the GPIO goes high, the N-channel mosfet switches, energizing the reset-coil of the relay, and the power is killed.

I bought the parts, I ordered the PCB and I put it all together. And after adding a few pullup/pulldown resistors here and there, it did seem to work.

However, I have no background in electrical engineering, and having a DIY design switch mains voltage was scary to me. I did put care into the PCB design, using the right trace widths, paying attention to creepages & clearances, but I couldn’t get myself to trust my creation.

Time to explore alternatives!

(almost) off-the-shelf solution

After discussing this project with a few people, using some sort of ““smart-home”“ IoT device was recommended to me. It was clear to me that I didn’t want to deal with any cloud bullshit, so I was sceptical at first. However, that turned out to be no problem at all, since (to my relieve) there seems to be a vibrant ecosystem of hackable non-cloud IoT devices.

After searching for a bit, I ended up ordering a Shelly Plus 2PM. It’s a tiny relay, intended to be permanently installed in your wall. The “2” in it’s name indicates that it has two channels, the “PM” that it can measure power draw.

For this use-case, one channel would have been enough, but I wasn’t yet sure if I might want a second one in the future. Therefore, using a Shelly Plus 1PM would have been the cheaper choice.

While these relays do have inputs for buttons & switches, their input does need to be capable of handling mains voltage. Therefore, I wouldn’t be able to use the gpio_poweroff approach described above. Instead, I decided to just initiate a shutdown on the Raspberry Pi, waiting for 20 seconds, and turning the relay off.

How it works

I bought a nice vandal-resistant, 230V-rated and illuminated momentary switch from digikey. That switch is connected to the Shelly Plus 2PM. Once the switch is pressed, a script on the Shelly would run.

If the printer was already off, the script would turn the power on.

If the printer is currently on, the script would send a message over the network to the Raspberry Pi. The Raspberry now has the chance to decide what to do, and it can refuse to do anything if the printer still has to cool down (That, I don’t have implemented yet). Then, The Raspberry sends a message back to the Shelly, and shuts itself off. Finally, after a couple of seconds, the Shelly kills the power.

To illustrate this a bit, here’s a rough wiring diagram of what I ended up with:

diagram

Shelly Script

As alluded in the last section, there are two scripts - one running on the Shelly itself, and an other one running on the Raspberry Pi. Let’s start with the one running on the Shelly. The stock firmware on it supports scripts in a subset of JavaScript, mJS.

function setSwitch(id, on) {
    Shelly.call("Switch.Set", {id: id, on: on}, function(response, err, err_msg, ctx) {
        if(err !== 0) {
            die(err_msg);
        }
        if(ctx.on !== response.was_on) {
            print(
            "Channel", ctx.id, "turned", 
            ctx.on ? "on" : "off", 
            );
        }

    }, {id: id, on: on});
}

function isSwitchOn(id, callback, ctx) {
    Shelly.call("Switch.GetStatus", {id: id}, function(response, err, err_msg, x) {
        if(err !== 0) {
            die(err_msg);
        }
        x.callback(response.output, x.ctx);
    }, {callback: callback, ctx: ctx});
}

function setSwitches(on) {
    setSwitch(0, on);
    setSwitch(1, on);
}

function initiateShutdown(req, res, arg) {
    print("initiateShutdown");
    Timer.set(30 * 1000, false, function() {
        setSwitches(false);
    }, null);

    res.status = 200;
    res.send();
}

function onButton() {
    print("Button pressed!");
    isSwitchOn(0, function(isOn, ctx) {
        if(isOn) {
            print("Switch is on, sending shutdown request to Raspberry..");
            Shelly.emitEvent("RPI_shutdown_request", {});
        } else {
            print("Switch is off, turning on..");
            setSwitches(true);
        }
    }, null);
}

HTTPServer.registerEndpoint(
    "shutdown",
    initiateShutdown,
    null
);

let last_button_press = 0;
Shelly.addEventHandler(function(e) {
    if(e.info.event === "btn_up") {
        let delta = e.now - last_button_press;
        if(delta < 1) {
            return;
        }
        last_button_press = e.now;
        onButton();
    }
});

setSwitches(true);

When switching anything, it always switches both channels, since I’m using a Shelly Plus 2PM. For a 1PM, there’s only one channel to switch, so a good portion of this script can be removed.

Raspberry Pi Script

On the Pi, I hacked together an awful Python script, which runs after boot:

import asyncio
import json
import websockets
import os
import requests
import time

SRC = "RAVEN_RASPBERRY_PI"
HOSTNAME = "shellyplus2pm-b8d61a8bc250.local"

def shutdown():
    print("executing SHUTTING_DOWN GCode macro..")
    requests.post(f"http://127.0.0.1/printer/gcode/script?script=SHUTTING_DOWN")

    print("waiting 1s..")
    time.sleep(1)

    print("sending shutdown confirmation..")
    response = requests.get(f"http://{HOSTNAME}/script/1/shutdown")
    if response.status_code != 200:
        raise Exception(f"Non-200 status code: {response.status_code}")

    print("shutting down..")
    os.system("shutdown -h -P now")

async def handle_msg(msg):
    msg = json.loads(msg)
    if "params" not in msg or "events" not in msg["params"]:
        return
    if not any([x for x in msg["params"]["events"] if x["event"] == "RPI_shutdown_request"]):
        return
    shutdown()

async def start():
    print("connecting..")
    async with websockets.connect(f"ws://{HOSTNAME}/rpc") as websocket:
        print("sending initial message..")
        await websocket.send(json.dumps({
            "src": SRC, "id": 420, "method": "Switch.GetStatus", "params": {"id": 0}
        }))
        
        print("receiving initial response..")
        await websocket.recv()

        print("subscribing..")
        while True:
            msg = await websocket.recv()
            await handle_msg(msg)


async def main():
    while True:
        try:
            await start()
        except Exception as e:
            print("Error:")
            print(e)

            
asyncio.run(main())

When the script receives a shutdown-request over a websocket connection, it runs the GCode macro SHUTTING_DOWN through moonraker.

Then, after waiting for a second, it replies to the request using an HTTP call, after which the Pi shuts down.

To get it to start after the Pi has booted, I added this to /etc/rc.local:

screen -L -Logfile /home/pi/shelly-shutdown.log -dmS shelly-shutdown bash -c "python3 /home/pi/klipper_config/shelly-shutdown.py"

This starts a new screen session, runs the script (located in /home/pi/klipper_config/shelly-shutdown.py) and logs the output to the home directory.

Conclusion

After I gave up on my DIY-solution, I’m really happy with how this project turned out!

While I’m not entirely happy with the 30s delay before killing power, I do believe it’s a sensible approach.

Alternatively, I was contemplating adding relay module for the Raspberry Pi. With that, I could switch 230V mains when the shutdown is complete (through gpio_poweroff), turning the Shelly off at the right time.

However, I was running out of space in my electronics compartment, where 3 huge PSUs are taking all available space, so I’ll leave it at that.