Arksine / moonraker

Web API Server for Klipper
https://moonraker.readthedocs.io
GNU General Public License v3.0
1.09k stars 417 forks source link

Add support for firmata power devices #507

Open brunnels opened 2 years ago

brunnels commented 2 years ago

Is your feature request related to a problem? Please describe

In some use cases all the GPIO pins on a RPi are being utilized and additional pins are needed for power control. In my specific case I have a DPI LCD display with touch screen. DPI uses almost all of the pins and the rest are used for the SPI touch screen.

Describe the solution you'd like

I would like the ability to utilize devices running firmata firmware as power devices in moonraker.

Firmata essentially gives you a port expander on any arduino avr device, pi pico, or esp32.

I did some looking and it seems the most modern and easiest to implement would be telemetrix

It has firmware available for arduino, pi-pico, and esp32.

Describe alternatives you've considered

Existing power libraries require more software infrastructure or hardware that I don't own. I already have several arduino nanos, esp32, and pi-pico's on hand.

Additional information

No response

brunnels commented 2 years ago

This looks like it could be pretty easy to add. I might be able to submit a merge request but the requirements for contributing seem complicated.

Something like this should probably work but I haven't tested yet.


from telemetrix import telemetrix

class TelemetrixDevice(GpioDevice):
    def __init__(self, config: ConfigHelper):
        super().__init__(config, initial_val=0)
        self.board = telemetrix.Telemetrix(
            com_port=config.get("com_port"),
            arduino_instance_id=config.get("arduino_instance_id", 1),
            arduino_wait=config.get("arduino_wait", 4)
        )
        self.board.set_pin_mode_digital_output(self.gpio_out.value)

    def set_power(self, state) -> None:
        if self.timer_handle is not None:
            self.timer_handle.cancel()
            self.timer_handle = None
        try:
            self.board.digital_write(
                self.gpio_out.value,
                1 if state == "on" else 0
            )
        except Exception:
            self.board.shutdown()
            self.state = "error"
            msg = f"Error Toggling Device Power: {self.name}"
            logging.exception(msg)
            raise self.server.error(msg) from None
        self.state = state
        self._check_timer()
brunnels commented 2 years ago

I just tested and what I had was close. Needed to derive the gpio pin number from the gpio_out.name.

This works:

class TelemetrixDevice(GpioDevice):
    def __init__(self, config: ConfigHelper):
        super().__init__(config, initial_val=0)
        self.board = telemetrix.Telemetrix(
            com_port=config.get("com_port"),
            arduino_instance_id=config.get("arduino_instance_id", 1),
            arduino_wait=config.get("arduino_wait", 4)
        )
        # we could use the chip_id to derive the arduino_instance_id but
        # arduino_instance_id starts at 1 and chip_id starts at 0 so I don't
        # think it's worth the confusion to save a config param
        chip_id, pin = self.gpio_out.name.split(":")
        self.pin_number = int(pin[4:])
        self.board.set_pin_mode_digital_output(self.pin_number)

    def set_power(self, state) -> None:
        if self.timer_handle is not None:
            self.timer_handle.cancel()
            self.timer_handle = None
        try:
            self.board.digital_write(self.pin_number, int(state == "on"))
        except Exception:
            self.board.shutdown()
            self.state = "error"
            msg = f"Error Toggling Device Power: {self.name}"
            logging.exception(msg)
            raise self.server.error(msg) from None
        self.state = state
        self._check_timer()
brunnels commented 2 years ago

Looking through the code a bit more. I should probably utilize the TelemetrixAIO instead so as not to block the event loop.

I was looking through the project PR's and saw another that you has rejected as it had an external dependency and that wasn't using async.

Are you still unwilling to accept any new power devices that come with additional dependencies, even if they are async? If so I'll just maintain my own branch for the time being and skip submitting a PR.

Arksine commented 2 years ago

As long as the dependency is a pure python module and does not block I don't have an issue adding it. The biggest issue we run into is building wheels on some SBC distros, modules with C or Rust dependencies often fail.

The telemetrix-aio seems to be pure python and its only dependency is pyserial, so it looks ok to me.

brunnels commented 2 years ago

@Arksine sounds good. I'll work on the PR this weekend and send it. I think firmata/telemetrix support will provide a lot of additional power device options since it's runs on pretty much anything that can connect with serial/usb. It might also be any easy integration point to add support for external environmental sensors that I have seen people asking for. Telemetrix has baked in support for quite a few sensors.