ugpio plugins

This commit is contained in:
Devaev Maxim 2020-09-06 08:47:43 +03:00
parent e8bd1e2648
commit a6dac4bd84
18 changed files with 321 additions and 75 deletions

View File

@ -40,6 +40,7 @@ from ..plugins.auth import get_auth_service_class
from ..plugins.hid import get_hid_class from ..plugins.hid import get_hid_class
from ..plugins.atx import get_atx_class from ..plugins.atx import get_atx_class
from ..plugins.msd import get_msd_class from ..plugins.msd import get_msd_class
from ..plugins.ugpio import get_ugpio_driver_class
from ..yamlconf import ConfigError from ..yamlconf import ConfigError
from ..yamlconf import make_config from ..yamlconf import make_config
@ -174,6 +175,16 @@ def _patch_dynamic( # pylint: disable=too-many-locals
rebuild = True rebuild = True
if load_gpio: if load_gpio:
for (driver, params) in { # type: ignore
"gpio": {},
**(raw_config.get("kvmd", {}).get("gpio", {}).get("drivers", {})),
}.items():
driver_type = valid_stripped_string_not_empty(params.get("type", "gpio"))
scheme["kvmd"]["gpio"]["drivers"][driver] = {
"type": Option(driver_type, type=valid_stripped_string_not_empty),
**get_ugpio_driver_class(driver_type).get_plugin_options()
}
for (channel, params) in raw_config.get("kvmd", {}).get("gpio", {}).get("scheme", {}).items(): for (channel, params) in raw_config.get("kvmd", {}).get("gpio", {}).get("scheme", {}).items():
try: try:
mode = valid_ugpio_mode(params.get("mode", "")) mode = valid_ugpio_mode(params.get("mode", ""))
@ -181,6 +192,7 @@ def _patch_dynamic( # pylint: disable=too-many-locals
pass pass
finally: finally:
ch_scheme: Dict = { ch_scheme: Dict = {
"driver": Option("gpio"),
"pin": Option(-1, type=valid_gpio_pin), "pin": Option(-1, type=valid_gpio_pin),
"mode": Option("", type=valid_ugpio_mode), "mode": Option("", type=valid_ugpio_mode),
"inverted": Option(False, type=valid_bool), "inverted": Option(False, type=valid_bool),
@ -197,6 +209,7 @@ def _patch_dynamic( # pylint: disable=too-many-locals
}, },
}) })
scheme["kvmd"]["gpio"]["scheme"][channel] = ch_scheme scheme["kvmd"]["gpio"]["scheme"][channel] = ch_scheme
rebuild = True
return rebuild return rebuild
@ -326,6 +339,7 @@ def _get_config_scheme() -> Dict:
"gpio": { "gpio": {
"state_poll": Option(0.1, type=valid_float_f01), "state_poll": Option(0.1, type=valid_float_f01),
"drivers": {}, # Dynamic content
"scheme": {}, # Dymanic content "scheme": {}, # Dymanic content
"view": { "view": {
"header": { "header": {

View File

@ -61,16 +61,16 @@ def _clear_gpio(config: Section) -> None:
("streamer/cap", config.streamer.cap_pin), ("streamer/cap", config.streamer.cap_pin),
("streamer/conv", config.streamer.conv_pin), ("streamer/conv", config.streamer.conv_pin),
*([ # *([
(f"gpio/{channel}", params.pin) # (f"gpio/{channel}", params.pin)
for (channel, params) in config.gpio.scheme.items() # for (channel, params) in config.gpio.scheme.items()
if params.mode == "output" # if params.mode == "output"
]), # ]),
]: ]:
if pin >= 0: if pin >= 0:
logger.info("Writing 0 to GPIO pin=%d (%s)", pin, name) logger.info("Writing 0 to GPIO pin=%d (%s)", pin, name)
try: try:
gpio.set_output(pin, initial=False) gpio.set_output(pin, False)
except Exception: except Exception:
logger.exception("Can't clear GPIO pin=%d (%s)", pin, name) logger.exception("Can't clear GPIO pin=%d (%s)", pin, name)

View File

@ -54,13 +54,18 @@ class ExportApi:
self.__user_gpio.get_state(), self.__user_gpio.get_state(),
]) ])
rows: List[str] = [] rows: List[str] = []
self.__append_prometheus_rows(rows, atx_state["enabled"], "pikvm_atx_enabled") self.__append_prometheus_rows(rows, atx_state["enabled"], "pikvm_atx_enabled")
self.__append_prometheus_rows(rows, atx_state["leds"]["power"], "pikvm_atx_power") self.__append_prometheus_rows(rows, atx_state["leds"]["power"], "pikvm_atx_power")
for mode in ["input", "output"]: for mode in ["input", "output"]:
for (channel, gch) in gpio_state[f"{mode}s"].items(): for (channel, ch_state) in gpio_state[f"{mode}s"].items():
self.__append_prometheus_rows(rows, gch["state"], f"pikvm_gpio_input_{channel}") for key in ["online", "state"]:
self.__append_prometheus_rows(rows, ch_state["state"], f"pikvm_gpio_{mode}_{key}_{channel}")
if hw_state is not None: if hw_state is not None:
self.__append_prometheus_rows(rows, hw_state["health"], "pikvm_hw") self.__append_prometheus_rows(rows, hw_state["health"], "pikvm_hw")
return Response(text="\n".join(rows)) return Response(text="\n".join(rows))
def __append_prometheus_rows(self, rows: List[str], value: Any, path: str) -> None: def __append_prometheus_rows(self, rows: List[str], value: Any, path: str) -> None:

View File

@ -59,5 +59,6 @@ class UserGpioApi:
async def __pulse_handler(self, request: Request) -> Response: async def __pulse_handler(self, request: Request) -> Response:
channel = valid_ugpio_channel(request.query.get("channel")) channel = valid_ugpio_channel(request.query.get("channel"))
delay = valid_float_f0(request.query.get("delay", "0")) delay = valid_float_f0(request.query.get("delay", "0"))
await self.__user_gpio.pulse(channel, delay) wait = valid_bool(request.query.get("wait", "0"))
await self.__user_gpio.pulse(channel, delay, wait)
return make_json_response() return make_json_response()

View File

@ -106,20 +106,21 @@ class StreamerResolutionNotSupported(OperationError):
# ===== # =====
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class _Component: class _Component: # pylint: disable=too-many-instance-attributes
name: str name: str
event_type: str event_type: str
obj: object obj: object
sysprep: Optional[Callable[[], None]] = None
systask: Optional[Callable[[], Coroutine[Any, Any, None]]] = None
get_state: Optional[Callable[[], Coroutine[Any, Any, Dict]]] = None get_state: Optional[Callable[[], Coroutine[Any, Any, Dict]]] = None
poll_state: Optional[Callable[[], AsyncGenerator[Dict, None]]] = None poll_state: Optional[Callable[[], AsyncGenerator[Dict, None]]] = None
systask: Optional[Callable[[], Coroutine[Any, Any, None]]] = None
cleanup: Optional[Callable[[], Coroutine[Any, Any, Dict]]] = None cleanup: Optional[Callable[[], Coroutine[Any, Any, Dict]]] = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
if isinstance(self.obj, BasePlugin): if isinstance(self.obj, BasePlugin):
object.__setattr__(self, "name", f"{self.name} ({self.obj.get_plugin_name()})") object.__setattr__(self, "name", f"{self.name} ({self.obj.get_plugin_name()})")
for field in ["get_state", "poll_state", "systask", "cleanup"]: for field in ["sysprep", "systask", "get_state", "poll_state", "cleanup"]:
object.__setattr__(self, field, getattr(self.obj, field, None)) object.__setattr__(self, field, getattr(self.obj, field, None))
if self.get_state or self.poll_state: if self.get_state or self.poll_state:
assert self.event_type, self assert self.event_type, self
@ -278,7 +279,9 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
# ===== SYSTEM STUFF # ===== SYSTEM STUFF
def run(self, **kwargs: Any) -> None: # type: ignore # pylint: disable=arguments-differ def run(self, **kwargs: Any) -> None: # type: ignore # pylint: disable=arguments-differ
self.__hid.start() for component in self.__components:
if component.sysprep:
component.sysprep()
aioproc.rename_process("main") aioproc.rename_process("main")
super().run(**kwargs) super().run(**kwargs)
@ -307,7 +310,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
async def wrapper() -> None: async def wrapper() -> None:
try: try:
await method(*args) await method(*args)
raise RuntimeError(f"Dead system task: {method.__name__}" raise RuntimeError(f"Dead system task: {method}"
f"({', '.join(getattr(arg, '__name__', str(arg)) for arg in args)})") f"({', '.join(getattr(arg, '__name__', str(arg)) for arg in args)})")
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass

View File

@ -141,8 +141,8 @@ class Streamer: # pylint: disable=too-many-instance-attributes
**params_kwargs: Any, **params_kwargs: Any,
) -> None: ) -> None:
self.__cap_pin = (gpio.set_output(cap_pin) if cap_pin >= 0 else -1) self.__cap_pin = (gpio.set_output(cap_pin, False) if cap_pin >= 0 else -1)
self.__conv_pin = (gpio.set_output(conv_pin) if conv_pin >= 0 else -1) self.__conv_pin = (gpio.set_output(conv_pin, False) if conv_pin >= 0 else -1)
self.__sync_delay = sync_delay self.__sync_delay = sync_delay
self.__init_delay = init_delay self.__init_delay = init_delay

View File

@ -30,62 +30,89 @@ from typing import Optional
from ...logging import get_logger from ...logging import get_logger
from ...plugins.ugpio import GpioError
from ...plugins.ugpio import GpioOperationError
from ...plugins.ugpio import GpioDriverOfflineError
from ...plugins.ugpio import BaseUserGpioDriver
from ...plugins.ugpio import get_ugpio_driver_class
from ... import aiotools from ... import aiotools
from ... import gpio
from ...yamlconf import Section from ...yamlconf import Section
from ...errors import OperationError
from ...errors import IsBusyError from ...errors import IsBusyError
# ===== # =====
class GpioChannelNotFoundError(OperationError): class GpioChannelNotFoundError(GpioOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("GPIO channel is not found") super().__init__("GPIO channel is not found")
class GpioSwitchNotSupported(OperationError): class GpioSwitchNotSupported(GpioOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("This GPIO channel does not support switching") super().__init__("This GPIO channel does not support switching")
class GpioPulseNotSupported(OperationError): class GpioPulseNotSupported(GpioOperationError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("This GPIO channel does not support pulsing") super().__init__("This GPIO channel does not support pulsing")
class GpioChannelIsBusyError(IsBusyError): class GpioChannelIsBusyError(IsBusyError, GpioError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("Performing another GPIO operation on this channel, please try again later") super().__init__("Performing another GPIO operation on this channel, please try again later")
# ===== # =====
class _GpioInput: class _GpioInput:
def __init__(self, channel: str, config: Section, reader: gpio.BatchReader) -> None: def __init__(
self,
channel: str,
config: Section,
driver: BaseUserGpioDriver,
) -> None:
self.__channel = channel self.__channel = channel
self.__pin: int = config.pin self.__pin: int = config.pin
self.__inverted: bool = config.inverted self.__inverted: bool = config.inverted
self.__reader = reader self.__driver = driver
self.__driver.register_input(self.__pin)
def get_scheme(self) -> Dict: def get_scheme(self) -> Dict:
return {} return {}
def get_state(self) -> Dict: def get_state(self) -> Dict:
return {"state": (self.__reader.get(self.__pin) ^ self.__inverted)} (online, state) = (True, False)
try:
state = (self.__driver.read(self.__pin) ^ self.__inverted)
except GpioDriverOfflineError:
online = False
return {
"online": online,
"state": state,
}
def __str__(self) -> str: def __str__(self) -> str:
return f"Input({self.__channel}, pin={self.__pin})" return f"Input({self.__channel}, driver={self.__driver.get_instance_name()}, pin={self.__pin})"
__repr__ = __str__ __repr__ = __str__
class _GpioOutput: # pylint: disable=too-many-instance-attributes class _GpioOutput: # pylint: disable=too-many-instance-attributes
def __init__(self, channel: str, config: Section, notifier: aiotools.AioNotifier) -> None: def __init__(
self,
channel: str,
config: Section,
driver: BaseUserGpioDriver,
notifier: aiotools.AioNotifier,
) -> None:
self.__channel = channel self.__channel = channel
self.__pin: int = config.pin self.__pin: int = config.pin
self.__inverted: bool = config.inverted self.__inverted: bool = config.inverted
self.__initial: bool = config.initial
self.__switch: bool = config.switch self.__switch: bool = config.switch
@ -95,6 +122,9 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
self.__busy_delay: float = config.busy_delay self.__busy_delay: float = config.busy_delay
self.__driver = driver
self.__driver.register_output(self.__pin, (config.initial ^ config.inverted))
self.__region = aiotools.AioExclusiveRegion(GpioChannelIsBusyError, notifier) self.__region = aiotools.AioExclusiveRegion(GpioChannelIsBusyError, notifier)
def get_scheme(self) -> Dict: def get_scheme(self) -> Dict:
@ -105,20 +135,31 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
"min_delay": (self.__min_pulse_delay if self.__pulse_delay else 0), "min_delay": (self.__min_pulse_delay if self.__pulse_delay else 0),
"max_delay": (self.__max_pulse_delay if self.__pulse_delay else 0), "max_delay": (self.__max_pulse_delay if self.__pulse_delay else 0),
}, },
"hw": {
"driver": self.__driver.get_instance_name(),
"pin": self.__pin,
},
} }
def get_state(self) -> Dict: def get_state(self) -> Dict:
busy = self.__region.is_busy() busy = self.__region.is_busy()
(online, state) = (True, False)
if not busy:
try:
state = self.__read()
except GpioDriverOfflineError:
online = False
return { return {
"state": (self.__read() if not busy else False), "online": online,
"state": state,
"busy": busy, "busy": busy,
} }
def cleanup(self) -> None: def cleanup(self) -> None:
try: try:
gpio.write(self.__pin, False) self.__driver.write(self.__pin, (self.__initial ^ self.__inverted))
except Exception: except Exception:
get_logger().exception("Can't cleanup GPIO %s", self) get_logger().exception("Can't cleanup %s", self)
async def switch(self, state: bool) -> bool: async def switch(self, state: bool) -> bool:
if not self.__switch: if not self.__switch:
@ -126,17 +167,21 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
async with self.__region: async with self.__region:
if state != self.__read(): if state != self.__read():
self.__write(state) self.__write(state)
get_logger(0).info("Switched %s to %d", self, state) get_logger(0).info("Switched %s to state=%d", self, state)
await asyncio.sleep(self.__busy_delay) await asyncio.sleep(self.__busy_delay)
return True return True
await asyncio.sleep(self.__busy_delay) await asyncio.sleep(self.__busy_delay)
return False return False
@aiotools.atomic @aiotools.atomic
async def pulse(self, delay: float) -> None: async def pulse(self, delay: float, wait: bool) -> None:
if not self.__pulse_delay: if not self.__pulse_delay:
raise GpioPulseNotSupported() raise GpioPulseNotSupported()
delay = min(max((delay or self.__pulse_delay), self.__min_pulse_delay), self.__max_pulse_delay) delay = min(max((delay or self.__pulse_delay), self.__min_pulse_delay), self.__max_pulse_delay)
if wait:
async with self.__region:
await self.__inner_pulse(delay)
else:
await aiotools.run_region_task( await aiotools.run_region_task(
f"Can't perform pulse of {self} or operation was not completed", f"Can't perform pulse of {self} or operation was not completed",
self.__region, self.__inner_pulse, delay, self.__region, self.__inner_pulse, delay,
@ -153,13 +198,13 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
get_logger(0).info("Pulsed %s with delay=%.2f", self, delay) get_logger(0).info("Pulsed %s with delay=%.2f", self, delay)
def __read(self) -> bool: def __read(self) -> bool:
return (gpio.read(self.__pin) ^ self.__inverted) return (self.__driver.read(self.__pin) ^ self.__inverted)
def __write(self, state: bool) -> None: def __write(self, state: bool) -> None:
gpio.write(self.__pin, (state ^ self.__inverted)) self.__driver.write(self.__pin, (state ^ self.__inverted))
def __str__(self) -> str: def __str__(self) -> str:
return f"Output({self.__channel}, pin={self.__pin})" return f"Output({self.__channel}, driver={self.__driver.get_instance_name()}, pin={self.__pin})"
__repr__ = __str__ __repr__ = __str__
@ -170,27 +215,23 @@ class UserGpio:
self.__view = config.view self.__view = config.view
self.__state_notifier = aiotools.AioNotifier() self.__state_notifier = aiotools.AioNotifier()
self.__reader = gpio.BatchReader(
pins=[ self.__drivers = {
( driver: get_ugpio_driver_class(drv_config.type)(**drv_config._unpack(ignore=["type"]))
gpio.set_input(ch_config.pin) for (driver, drv_config) in config.drivers.items()
if ch_config.mode == "input" else }
gpio.set_output(ch_config.pin, (ch_config.initial ^ ch_config.inverted))
)
for ch_config in config.scheme.values()
],
interval=config.state_poll,
notifier=self.__state_notifier,
)
self.__inputs: Dict[str, _GpioInput] = {} self.__inputs: Dict[str, _GpioInput] = {}
self.__outputs: Dict[str, _GpioOutput] = {} self.__outputs: Dict[str, _GpioOutput] = {}
for (channel, ch_config) in sorted(config.scheme.items(), key=operator.itemgetter(0)): for (channel, ch_config) in sorted(config.scheme.items(), key=operator.itemgetter(0)):
driver = self.__drivers.get(ch_config.driver)
if driver is None:
raise RuntimeError(f"Missing User-GPIO driver configuration: {ch_config.driver}")
if ch_config.mode == "input": if ch_config.mode == "input":
self.__inputs[channel] = _GpioInput(channel, ch_config, self.__reader) self.__inputs[channel] = _GpioInput(channel, ch_config, driver)
else: # output: else: # output:
self.__outputs[channel] = _GpioOutput(channel, ch_config, self.__state_notifier) self.__outputs[channel] = _GpioOutput(channel, ch_config, driver, self.__state_notifier)
async def get_model(self) -> Dict: async def get_model(self) -> Dict:
return { return {
@ -216,13 +257,26 @@ class UserGpio:
prev_state = state prev_state = state
await self.__state_notifier.wait() await self.__state_notifier.wait()
def sysprep(self) -> None:
get_logger().info("Preparing User-GPIO drivers ...")
for (_, driver) in sorted(self.__drivers.items(), key=operator.itemgetter(0)):
driver.prepare(self.__state_notifier)
async def systask(self) -> None: async def systask(self) -> None:
get_logger(0).info("Polling User-GPIO inputs ...") get_logger(0).info("Running User-GPIO drivers ...")
await self.__reader.poll() await asyncio.gather(*[
driver.run()
for (_, driver) in sorted(self.__drivers.items(), key=operator.itemgetter(0))
])
async def cleanup(self) -> None: async def cleanup(self) -> None:
for gout in self.__outputs.values(): for gout in self.__outputs.values():
gout.cleanup() gout.cleanup()
for driver in self.__drivers.values():
try:
driver.cleanup()
except Exception:
get_logger().exception("Can't cleanup driver %r", driver.get_instance_name())
async def switch(self, channel: str, state: bool) -> bool: async def switch(self, channel: str, state: bool) -> bool:
gout = self.__outputs.get(channel) gout = self.__outputs.get(channel)

View File

@ -24,7 +24,7 @@ import asyncio
import contextlib import contextlib
from typing import Tuple from typing import Tuple
from typing import List from typing import Set
from typing import Generator from typing import Generator
from typing import Optional from typing import Optional
@ -48,7 +48,7 @@ def bcm() -> Generator[None, None, None]:
logger.info("GPIO cleaned") logger.info("GPIO cleaned")
def set_output(pin: int, initial: bool=False) -> int: def set_output(pin: int, initial: Optional[bool]) -> int:
assert pin >= 0, pin assert pin >= 0, pin
GPIO.setup(pin, GPIO.OUT, initial=initial) GPIO.setup(pin, GPIO.OUT, initial=initial)
return pin return pin
@ -71,10 +71,10 @@ def write(pin: int, state: bool) -> None:
class BatchReader: class BatchReader:
def __init__(self, pins: List[int], interval: float, notifier: aiotools.AioNotifier) -> None: def __init__(self, pins: Set[int], interval: float, notifier: aiotools.AioNotifier) -> None:
self.__pins = pins self.__pins = sorted(pins)
self.__flags: Tuple[Optional[bool], ...] = (None,) * len(pins) self.__flags: Tuple[Optional[bool], ...] = (None,) * len(self.__pins)
self.__state = {pin: read(pin) for pin in pins} self.__state = {pin: read(pin) for pin in self.__pins}
self.__interval = interval self.__interval = interval
self.__notifier = notifier self.__notifier = notifier

View File

@ -62,8 +62,8 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
self.__power_led_pin = gpio.set_input(power_led_pin) self.__power_led_pin = gpio.set_input(power_led_pin)
self.__hdd_led_pin = gpio.set_input(hdd_led_pin) self.__hdd_led_pin = gpio.set_input(hdd_led_pin)
self.__power_switch_pin = gpio.set_output(power_switch_pin) self.__power_switch_pin = gpio.set_output(power_switch_pin, False)
self.__reset_switch_pin = gpio.set_output(reset_switch_pin) self.__reset_switch_pin = gpio.set_output(reset_switch_pin, False)
self.__power_led_inverted = power_led_inverted self.__power_led_inverted = power_led_inverted
self.__hdd_led_inverted = hdd_led_inverted self.__hdd_led_inverted = hdd_led_inverted
@ -75,7 +75,7 @@ class Plugin(BaseAtx): # pylint: disable=too-many-instance-attributes
self.__region = aiotools.AioExclusiveRegion(AtxIsBusyError, self.__state_notifier) self.__region = aiotools.AioExclusiveRegion(AtxIsBusyError, self.__state_notifier)
self.__reader = gpio.BatchReader( self.__reader = gpio.BatchReader(
pins=[self.__power_led_pin, self.__hdd_led_pin], pins=set([self.__power_led_pin, self.__hdd_led_pin]),
interval=state_poll, interval=state_poll,
notifier=self.__state_notifier, notifier=self.__state_notifier,
) )

View File

@ -32,8 +32,8 @@ from .. import get_plugin_class
# ===== # =====
class BaseHid(BasePlugin): class BaseHid(BasePlugin):
def start(self) -> None: def sysprep(self) -> None:
pass raise NotImplementedError
async def get_state(self) -> Dict: async def get_state(self) -> Dict:
raise NotImplementedError raise NotImplementedError

View File

@ -74,7 +74,7 @@ class Plugin(BaseHid):
"noop": Option(False, type=valid_bool), "noop": Option(False, type=valid_bool),
} }
def start(self) -> None: def sysprep(self) -> None:
self.__keyboard_proc.start() self.__keyboard_proc.start()
self.__mouse_proc.start() self.__mouse_proc.start()

View File

@ -175,7 +175,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
multiprocessing.Process.__init__(self, daemon=True) multiprocessing.Process.__init__(self, daemon=True)
self.__reset_pin = gpio.set_output(reset_pin) self.__reset_pin = gpio.set_output(reset_pin, False)
self.__reset_delay = reset_delay self.__reset_delay = reset_delay
self.__device_path = device_path self.__device_path = device_path
@ -217,9 +217,9 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
"noop": Option(False, type=valid_bool), "noop": Option(False, type=valid_bool),
} }
def start(self) -> None: def sysprep(self) -> None:
get_logger(0).info("Starting HID daemon ...") get_logger(0).info("Starting HID daemon ...")
multiprocessing.Process.start(self) self.start()
async def get_state(self) -> Dict: async def get_state(self) -> Dict:
state = await self.__state_flags.get() state = await self.__state_flags.get()

View File

@ -165,8 +165,8 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
reset_delay: float, reset_delay: float,
) -> None: ) -> None:
self.__target_pin = gpio.set_output(target_pin) self.__target_pin = gpio.set_output(target_pin, False)
self.__reset_pin = gpio.set_output(reset_pin) self.__reset_pin = gpio.set_output(reset_pin, False)
self.__device_path = device_path self.__device_path = device_path
self.__init_delay = init_delay self.__init_delay = init_delay

View File

@ -0,0 +1,77 @@
# ========================================================================== #
# #
# KVMD - The main Pi-KVM daemon. #
# #
# Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
from typing import Type
from typing import Optional
from ...errors import OperationError
from ... import aiotools
from .. import BasePlugin
from .. import get_plugin_class
# =====
class GpioError(Exception):
pass
class GpioOperationError(OperationError, GpioError):
pass
class GpioDriverOfflineError(GpioOperationError):
def __init__(self, driver: "BaseUserGpioDriver") -> None:
super().__init__(f"GPIO driver {driver.get_instance_name()!r} is offline")
# =====
class BaseUserGpioDriver(BasePlugin):
def get_instance_name(self) -> str:
raise NotImplementedError
def register_input(self, pin: int) -> None:
raise NotImplementedError
def register_output(self, pin: int, initial: Optional[bool]) -> None:
raise NotImplementedError
def prepare(self, notifier: aiotools.AioNotifier) -> None:
raise NotImplementedError
async def run(self) -> None:
raise NotImplementedError
def cleanup(self) -> None:
pass
def read(self, pin: int) -> bool:
raise NotImplementedError
def write(self, pin: int, state: bool) -> None:
raise NotImplementedError
# =====
def get_ugpio_driver_class(name: str) -> Type[BaseUserGpioDriver]:
return get_plugin_class("ugpio", name) # type: ignore

View File

@ -0,0 +1,86 @@
# ========================================================================== #
# #
# KVMD - The main Pi-KVM daemon. #
# #
# Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
from typing import Dict
from typing import Set
from typing import Optional
from ... import aiotools
from ... import gpio
from ...yamlconf import Option
from ...validators.basic import valid_float_f01
from . import BaseUserGpioDriver
# =====
class Plugin(BaseUserGpioDriver):
def __init__(self, state_poll: float) -> None: # pylint: disable=super-init-not-called
self.__state_poll = state_poll
self.__input_pins: Set[int] = set()
self.__output_pins: Dict[int, Optional[bool]] = {}
self.__reader: Optional[gpio.BatchReader] = None
@classmethod
def get_plugin_options(cls) -> Dict:
return {
"state_poll": Option(0.1, type=valid_float_f01),
}
def get_instance_name(self) -> str:
return "gpio"
def register_input(self, pin: int) -> None:
self.__input_pins.add(pin)
def register_output(self, pin: int, initial: Optional[bool]) -> None:
self.__output_pins[pin] = initial
def prepare(self, notifier: aiotools.AioNotifier) -> None:
assert self.__reader is None
self.__reader = gpio.BatchReader(
pins=set([
*map(gpio.set_input, self.__input_pins),
*[
gpio.set_output(pin, initial)
for (pin, initial) in self.__output_pins.items()
],
]),
interval=self.__state_poll,
notifier=notifier,
)
async def run(self) -> None:
assert self.__reader
await self.__reader.poll()
def read(self, pin: int) -> bool:
assert self.__reader
return self.__reader.get(pin)
def write(self, pin: int, state: bool) -> None:
assert self.__reader
gpio.write(pin, state)

View File

@ -87,6 +87,7 @@ def main() -> None:
"kvmd.plugins.atx", "kvmd.plugins.atx",
"kvmd.plugins.msd", "kvmd.plugins.msd",
"kvmd.plugins.msd.otg", "kvmd.plugins.msd.otg",
"kvmd.plugins.ugpio",
"kvmd.clients", "kvmd.clients",
"kvmd.apps", "kvmd.apps",
"kvmd.apps.kvmd", "kvmd.apps.kvmd",

View File

@ -29,7 +29,7 @@ from kvmd import gpio
@pytest.mark.parametrize("pin", [0, 1, 13]) @pytest.mark.parametrize("pin", [0, 1, 13])
def test_ok__loopback_initial_false(pin: int) -> None: def test_ok__loopback_initial_false(pin: int) -> None:
with gpio.bcm(): with gpio.bcm():
assert gpio.set_output(pin) == pin assert gpio.set_output(pin, False) == pin
assert gpio.read(pin) is False assert gpio.read(pin) is False
gpio.write(pin, True) gpio.write(pin, True)
assert gpio.read(pin) is True assert gpio.read(pin) is True
@ -53,6 +53,6 @@ def test_ok__input(pin: int) -> None:
def test_fail__invalid_pin() -> None: def test_fail__invalid_pin() -> None:
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
gpio.set_output(-1) gpio.set_output(-1, False)
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
gpio.set_input(-1) gpio.set_input(-1)

View File

@ -38,6 +38,11 @@ kvmd:
- "--no-log-colors" - "--no-log-colors"
gpio: gpio:
drivers:
gpio2:
type: gpio
state_poll: 0.3
scheme: scheme:
host1: # any name like foo_bar_baz host1: # any name like foo_bar_baz
pin: 1 pin: 1