How to turn on/off GPIO LED based on state

I have LEDs on my printer that I want to have ON while printing, but turn off while the printer is idle.

The following works for turning OFF the LED because when the print is finished it sends an M84 to disable the motors.

[gcode_macro LEDON]
gcode:  SET_PIN PIN=LED VALUE=1

[gcode_macro LEDOFF]
gcode:  SET_PIN PIN=LED VALUE=0

[gcode_macro M84]
rename_existing: M84.1
gcode: 
    M84.1 { rawparams }
    LEDOFF

However, how would you turn ON the led based on an event?

Things I’ve tried

  1. Modify START_PRINT macro to run my LEDON (fails to run for some reason)

    [gcode_macro START_PRINT]
    gcode:
        # Turn On LED.  # THIS DOES NOT WORK FOR SOME REASON
        LEDON
        {% set BED_TEMP = params.BED_TEMP|default(60)|float %}
        {% set EXTRUDER_TEMP = params.EXTRUDER_TEMP|default(190)|float %}
        # Start bed heating
        M140 S{BED_TEMP}
        # Use absolute coordinates
        G90
        # Reset the G-Code Z offset (adjust Z offset if needed)
        SET_GCODE_OFFSET Z=0.0
        # Home the printer
        G28
        # Move the nozzle near the bed
        G1 Z5 F3000
        # Move the nozzle very close to the bed
        G1 Z0.15 F300
        # Wait for bed to reach temperature
        M190 S{BED_TEMP}
        # Set and wait for nozzle to reach temperature
        M109 S{EXTRUDER_TEMP}
    
  2. Modify M17 [Engage motors]. Fails because klipper does not implement M17

    # Does not work
    [gcode_macro M17]
    rename_existing: M17.1
    gcode: 
        M17.1 { rawparams }
        LEDON
    
  3. Modify M140 [ Heat Bed]. This works if I manually run M140, however klipper/fluidd does not send an M140 to turn on the heater, instead it runs SET_HEATER_TEMPERATURE )

    # Does not work. Prints send `SET_HEATER_TEMPERATURE` not M140
    [gcode_macro M140]
    rename_existing: M140.1
    gcode:
        LEDON
        M140.1 { rawparams }
    

How can I automate turning ON the LEDs?

I am considering bypassing macros entirely and using the klipper API to subscribe to the idle_timeout api, however that comes with its own set of problems.

{"id": 123, "method": "objects/query", "params": {"objects": {"idle_timeout": [], "webhooks": null}}}
{"id": 123, "method": "objects/subscribe", "params": {"objects":{"idle_timeout": ["state"], "webhooks": ["state"]}, "response_template":{}}}

I have neopixel LEDs on my printhead and I turn them on in the START_PRINT macro as you have done. If you execute your LEDON macro a little later in the START_PRINT macro (say before the G28), do you get the same result?.

For your 3rd option, you should be able to do a rename_existing on SET_HEATER_TEMPERATURE macro, exactly as you have done for M140

I’ve tried using/abusing delayed_gcode in a recursive loop as a test.

# The following loop works but has unfortunate downsides. I don't recomend it

[delayed_gcode event_loop]
initial_duration: 5.
gcode:
    LEDON
    G4 P250 # sleep 250 miliseconds
    LEDOFF
    # Recursively call the event loop after 1 second
    UPDATE_DELAYED_GCODE ID=event_loop DURATION=1

While this sort of works in that it allows me to run a jinja template every second, it has the following side effects

  1. Loop is paused when other gcode is running (e.g. homing the bed)
  2. Doesn’t allow machine to go into idle state (which keeps fans running on my machine)
  3. Unpreditable how many times the loop will run when the loop is unblocked (e.g. runs repeatedly)

Solution 1

Assuming an LED is connected to pin 35 of a raspberry pi using a mosfet, the following works (with a few caveats)

It runs a loop every second that checks printer.print_stats.state

[output_pin LED]
pin: rpi:gpio19 #BCM=19, wPI=24, Physical=35

[gcode_macro LEDOFF]
gcode:  SET_PIN PIN=LED VALUE=0

[gcode_macro LEDON]
gcode:  SET_PIN PIN=LED VALUE=1

# Run a loop every second to check if the printer is idle
[delayed_gcode event_loop]
initial_duration: 1.
gcode:
    check_state DEBUG=0
    UPDATE_DELAYED_GCODE ID=event_loop DURATION=1

# Adds check_state macro that can be called manually or on a schedule
[gcode_macro check_state]
gcode:
    # Log to the console if human is manually calling `check_status`
    # Dont log to the console if the event_loop is calling since it will clutter up console
    {% set debug = params.DEBUG|default(1)|int %}
    {% if debug == 1 %}
        # Print out the state ('cancelled', 'standby', 'printing', 'paused', 'error')
        { action_respond_info(printer.print_stats.state) }
    {% endif %}

    # Turn on the LED if the printer is busy (not standby)
    # Note: 'cancelled' will stay 'cancelled' until a human clicks 'clear print' in the fluidd/mainsail gui
    # Note: This won't turn on during homing or when print bed is heating since those are blocking states
    {% if printer.print_stats.state == "printing" %}
        LEDON
    {% elif printer.print_stats.state == "paused" %}
        LEDON
    {% elif printer.print_stats.state == "cancelled" %}
        LEDON
    {% elif printer.print_stats.state == "error" %}
        LEDON
    {% else %}
        LEDOFF
    {% endif %}

While this does work, it has the following side effects

  1. Bed Heat and Homeing are blocking so sometimes they won’t trigger the LED (It usually turns on when the print starts)

Solution 2

Instead of doing gcode in a loop, its possible to use controller_fan to toggle a GPIO pin based on an event.
controller_fan is enabled when a heater/stepper motor is engaged.

Here I have a LED strip attached to pin 35 of a raspberry pi. This turns on the LED any time a motor moves, and turns off the LED 30 seconds after idle_timeout is reached (600 seconds after last motor movement by default)

[controller_fan led_bar]
pin: rpi:gpio19

Note: If you wish to have the LED be controllable from the GUI, I’ve added an output_pin and also a duplicate_pin_override to prevent errors.

# Define the LED as a toggle in the GUI
[output_pin LED]
pin: rpi:gpio19

[controller_fan led_bar]
pin: rpi:gpio19

[duplicate_pin_override]
pins: rpi:gpio19

Note: While this does work exactly how I want. it has the following downsides

  1. LED shows up twice in the outputs pane
  2. If the controller_fan turns on/off the LED, the state is not updated in the outputs section of the gui

FYI, as of PR #5374 ( Automated LED updates and LED animations by KevinOConnor · Pull Request #5374 · Klipper3d/klipper · GitHub ) it is possible to define a display_template for automatic led color changes.

-Kevin