diff --git a/moonraker/plugins/power.py b/moonraker/plugins/power.py index 5270021..54a6d30 100644 --- a/moonraker/plugins/power.py +++ b/moonraker/plugins/power.py @@ -7,6 +7,7 @@ import logging import os import asyncio +import gpiod from tornado.ioloop import IOLoop from tornado import gen @@ -28,16 +29,14 @@ class PrinterPower: self.server.register_remote_method( "set_device_power", self.set_device_power) + self.chip_factory = GpioChipFactory() self.current_dev = None self.devices = {} prefix_sections = config.get_prefix_sections("power") - logging.info(f"Power plugin loading devices: f{prefix_sections}") - devices = {} + logging.info(f"Power plugin loading devices: {prefix_sections}") for section in prefix_sections: - dev = GpioDevice(config[section]) - devices[dev.name] = dev - ioloop = IOLoop.current() - ioloop.spawn_callback(self.initialize_devices, devices) + dev = GpioDevice(config[section], self.chip_factory) + self.devices[dev.get_name()] = dev async def _handle_list_devices(self, web_request): output = {"devices": list(self.devices.keys())} @@ -58,7 +57,7 @@ class PrinterPower: if req not in ("on", "off", "status"): raise self.server.error("Unsupported power request") if (await self._power_dev(dev, req)): - result[dev] = self.devices[dev].status + result[dev] = self.devices[dev].get_state() else: result[dev] = "device_not_found" return result @@ -67,8 +66,9 @@ class PrinterPower: if dev not in self.devices: return False if req in ["on", "off"]: - await self.devices[dev].power(req) - + ret = self.devices[dev].set_power(req) + if asyncio.iscoroutine(ret): + await ret self.server.send_event("gpio_power:power_changed", { "device": dev, "status": req @@ -80,17 +80,6 @@ class PrinterPower: await ret return True - async def initialize_devices(self, devices): - for name, device in devices.items(): - try: - await device.initialize() - except Exception: - logging.exception( - f"Power plugin: ERR Problem configuring the output pin for" - f" device {name}. Removing device") - continue - self.devices[name] = device - def set_device_power(self, device, state): status = None if isinstance(state, bool): @@ -109,122 +98,114 @@ class PrinterPower: if name in self.devices: raise self.server.error( f"Device [{name}] already configured") - await device.initialize() + ret = device.initialize() + if asyncio.iscoroutine(ret): + await ret self.devices[name] = device + async def close(self): + for device in self.devices.values(): + if hasattr(device, "close"): + ret = device.close() + if asyncio.iscoroutine(ret): + await ret + self.chip_factory.close() -class GPIO: - gpio_root = "/sys/class/gpio" - @staticmethod - def _set_gpio_option(pin, option, value): - GPIO._write( - os.path.join(GPIO.gpio_root, f"gpio{pin}", option), - value - ) +class GpioChipFactory: + def __init__(self): + self.chips = {} - @staticmethod - def _get_gpio_option(pin, option): - return GPIO._read( - os.path.join(GPIO.gpio_root, f"gpio{pin}", option) - ) - - @staticmethod - def _write(file, data): - with open(file, 'w') as f: - f.write(str(data)) - f.flush() - - @staticmethod - def _read(file): - with open(file, 'r') as f: - f.seek(0) - return f.read().strip() - - @staticmethod - async def verify_pin(pin, active_low=1): - gpiopath = os.path.join(GPIO.gpio_root, f"gpio{pin}") - if not os.path.exists(gpiopath): - logging.info(f"Re-intializing GPIO{pin}") - await GPIO.setup_pin(pin, active_low) - return - - if GPIO._get_gpio_option(pin, "active_low").strip() != str(active_low): - GPIO._set_gpio_option(pin, "active_low", active_low) - - if GPIO._get_gpio_option(pin, "direction").strip() != "out": - GPIO._set_gpio_option(pin, "direction", "out") - - @staticmethod - async def setup_pin(pin, active_low=1): - pin = int(pin) - active_low = 1 if active_low == 1 else 0 - - gpiopath = os.path.join(GPIO.gpio_root, f"gpio{pin}") - if not os.path.exists(gpiopath): - GPIO._write( - os.path.join(GPIO.gpio_root, "export"), - pin) - logging.info(f"Waiting for GPIO{pin} to initialize") - while os.stat(os.path.join( - GPIO.gpio_root, f"gpio{pin}", - "active_low")).st_gid == 0: - await gen.sleep(.1) - - if GPIO._get_gpio_option(pin, "active_low").strip() != str(active_low): - GPIO._set_gpio_option(pin, "active_low", active_low) - - if GPIO._get_gpio_option(pin, "direction").strip() != "out": - GPIO._set_gpio_option(pin, "direction", "out") - - @staticmethod - def is_pin_on(pin): - return "on" if int(GPIO._get_gpio_option(pin, "value")) else "off" - - @staticmethod - def set_pin_value(pin, active): - value = 1 if (active == 1) else 0 - GPIO._set_gpio_option(pin, "value", value) + def get_gpio_chip(self, chip_name): + if chip_name in self.chips: + return self.chips[chip_name] + chip = gpiod.Chip(chip_name, gpiod.Chip.OPEN_BY_NAME) + self.chips[chip_name] = chip + return chip + def close(self): + for chip in self.chips.values(): + chip.close() class GpioDevice: - def __init__(self, config): + def __init__(self, config, chip_factory): name_parts = config.get_name().split(maxsplit=1) if len(name_parts) != 2: raise config.error(f"Invalid Section Name: {config.get_name()}") self.name = name_parts[1] - self.status = None - cfg_pin = pin = config.get("pin") - self.invert = False + self.state = "init" + pin, chip_id, invert = self._parse_pin(config) + try: + chip = chip_factory.get_gpio_chip(chip_id) + self.line = chip.get_line(pin) + if invert: + self.line.request( + consumer="moonraker", type=gpiod.LINE_REQ_DIR_OUT, + flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW) + else: + self.line.request( + consumer="moonraker", type=gpiod.LINE_REQ_DIR_OUT) + except Exception: + self.state = "error" + logging.exception( + f"Unable to init {pin}. Make sure the gpio is not in " + "use by another program or exported by sysfs.") + raise config.error("Power GPIO Config Error") + self.set_power("off") + + def _parse_pin(self, config): + pin = cfg_pin = config.get("pin") + invert = False if pin[0] == "!": pin = pin[1:] - self.invert = True - self.consumer = "gpiochip0" + invert = True + chip_id = "gpiochip0" pin_parts = pin.split("/") - self.pin = "" if len(pin_parts) == 2: - self.consumer, self.pin = pin_parts + chip_id, pin = pin_parts elif len(pin_parts) == 1: - self.pin = pin_parts[0] + pin = pin_parts[0] # Verify pin - if not self.consumer.startswith("gpiochip") or \ - not self.consumer[-1].isdigit() or \ - not self.pin.startswith("gpio") or \ - not self.pin[4:].isdigit(): + if not chip_id.startswith("gpiochip") or \ + not chip_id[-1].isdigit() or \ + not pin.startswith("gpio") or \ + not pin[4:].isdigit(): raise config.error( f"Invalid Power Pin configuration: {cfg_pin}") - self.pin = int(self.pin[4:]) + pin = int(pin[4:]) + return pin, chip_id, invert - async def initialize(self): - await GPIO.setup_pin(self.pin, int(self.invert)) - self.refresh_status() + def initialize(self): + pass + + def get_name(self): + return self.name + + def get_state(self): + return self.state def refresh_status(self): - self.status = GPIO.is_pin_on(self.pin) + try: + val = self.line.get_value() + except Exception: + self.state = "error" + msg = f"Error Refeshing Device Status: {self.name}" + logging.exception(msg) + raise self.server.error(msg) from None + self.state = "on" if val else "off" - async def power(self, status): - await GPIO.verify_pin(self.pin, int(self.invert)) - GPIO.set_pin_value(self.pin, int(status == "on")) + def set_power(self, state): + try: + self.line.set_value(int(state == "on")) + except Exception: + self.state = "error" + msg = f"Error Toggling Device Power: {self.name}" + logging.exception(msg) + raise self.server.error(msg) from None + self.state = state + + def close(self): + self.line.release() # The power plugin has multiple configuration sections def load_plugin_multi(config):