8 コミット b0fa8743d1 ... d20f67037d

作者 SHA1 メッセージ 日付
  digital d20f67037d moved controllers from pin.py to controller.py, worked in ButtonController and created file errmsg.py for error messages 7 年 前
  digital a448f96ad7 typo fix 7 年 前
  digital 6e4e2b3b32 declaring variable _pins_for_cleanup now 7 年 前
  digital 2f33210b97 moved gpio/__init__.py to gpio/pin.py. gpio/__init__ only imports now. 7 年 前
  digital 0e7d1f8f8a started to implement a controller manager and worked on the controller baseclass 7 年 前
  digital a72ebc6c1f renamed setmode to configure 7 年 前
  digital 7f0b60306f removed blinker import from gpio 7 年 前
  digital b5410a5032 duplicated submodule pin to gpio and started rewriting it. the gpio warpper is in an extra file now. instead of input and output read and write are used 7 年 前

+ 55 - 0
src/digilib/gpio/__init__.py

@@ -0,0 +1,55 @@
+#!/usr/bin/env python3.5
+# Copyright 2017 Digital
+#
+# This file is part of DigiLib.
+#
+# DigiLib 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.
+#
+# DigiLib 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 DigiLib.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import .wrapper.*
+import .pin.*
+import .controller.*
+import .ctrl_manager.*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#

+ 319 - 0
src/digilib/gpio/controller.py

@@ -0,0 +1,319 @@
+# Copyright 2017 Digital
+#
+# This file is part of DigiLib.
+#
+# DigiLib 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.
+#
+# DigiLib 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 DigiLib.  If not, see <http://www.gnu.org/licen
+
+import logging
+log = logging.getLogger(__name__+"")
+lctrl = logging.getLogger(__name__+".ctrl")
+
+import time
+
+import curio
+import digilib.network
+import digilib.misc
+import digilib.gpio.wrapper
+
+
+class ControllerBase(object):
+    """
+    ControllerBase is the baseclass for Controller. All collectors need to inherit from CollectorBase or provide the same methods.
+
+    A collector collects information form sensors and puts them in a pipe, so the CtrlManager can access it. The minutely, hourly and daily methods are easy to use but there execution time depends on when the core was started. use curio's execute_at feature to execute a function at a specific time.
+
+    Parameters
+    ----------
+    """
+    def __init__(self):
+        """
+        during initilazion, a Controller should register its methods.
+        """
+        super().__init__()
+
+    async def collect_loadcell_data(self):
+        """
+        This method collects data from a sensor.
+        """
+
+    async def daily(self):
+        """
+        This method is called once every day by the core.
+        """
+
+    async def hourly(self):
+        """
+        This method is called once every hour by the core.
+        """
+
+    async def main_loop(self):
+        """
+        This is the main loop of the Controller class.
+        """
+
+    async def minutely(self):
+        """
+        This method is called once every minute by the core.
+        """
+
+    async def on_startup(self):
+        """
+        This method is called by the core when it starts. This is a good entry point for a Controller class, however the main_loop should be in a different method wich can be called here.
+        """
+        # self.main_loop()
+
+    async def on_shutdown(self):
+        """
+        This method is called by the core when it shuts down.
+        """
+
+class ButtonController(object):
+    """
+    ButtonController can be used with a hardware push button. It provides events you can register a callback to, join it or test the buttons state.
+
+    .. A collector collects information form sensors and puts them in a pipe, so the CtrlManager can access it. The minutely, hourly and daily methods are easy to use but there execution time depends on when the core was started. use curio's execute_at feature to execute a function at a specific time.
+
+    Parameters
+    ----------
+    pin_num: int
+        the number of the pin wich is connected to the button and to logical HIGH through an appropriate resistor.
+    time_short_press: int
+        the maximum time the button needs to be pressed to be registered as a short press.
+    time_long_press: int
+        the maximum time the button needs to be pressed to be registered as a long press. if ``time_short_press`` is greater the long press feature is disabled.
+
+    Attributes
+    ----------
+    STATE_PRESSED: int
+        the value returned from gpio.read if the button is pressed
+    STATE_RELEASED: int
+        the value returned from gpio.read if the button is released
+    """
+    def __init__(self,pin_num,time_short_press,time_long_press,):
+        """
+        """
+        super().__init__()
+        self.pin_num = pin_num
+        self.time_short_press = time_short_press
+        self.time_long_press = time_long_press
+        digilib.gpio.wrapper.setup(self.pin_num,digilib.gpio.wrapper.OUT)
+
+    async def read_button_state(self):
+        """
+        This method reads the current state from the button's gpio pin.
+        """
+        return digilib.gpio.wrapper.read(self.pin_num)
+
+    async def daily(self):
+        """
+        This method is called once every day by the core.
+        """
+
+    async def hourly(self):
+        """
+        This method is called once every hour by the core.
+        """
+
+    async def main_loop(self):
+        """
+        The main loop executes registered callbacks if the button state was changed, was pressed for ``self.time_short_press`` or ``self.time_long_press``.
+
+        Attributes
+        ----------
+        prev_state: int
+            the state of the button during the last loop.
+        time_pressed: int
+            the time when the button was pressed, taken from time.time()
+        time_released: int
+            the time when the button was released, taken from time.time()
+        """
+        prev_state = None
+        time_pressed = 0
+        time_released = 0
+        try:
+            while True:
+                state = self.read_button_state()
+                if state != prev_state:
+                    if state = self.STATE_PRESSED:
+                        # the button was pressed just now
+                        time_pressed = time.time()
+                        # TODO execute registered methods
+                    else:
+                        # the button was released just now
+                        time_released = time.time()
+                        # TODO execute registered methods
+                        if ( time_released - time_pressed
+                                >= self.time_short_press ):
+                            # the button was pressed for a short time.
+                            # TODO execute registered methods
+                            pass
+                        elif ( time_released - time_pressed
+                                >= self.time_long_press ):
+                            # the button was pressed for a short time.
+                            # TODO execute registered methods
+                            pass
+                prev_state = state
+                await curio.sleep(0.1)
+
+    async def minutely(self):
+        """
+        This method is called once every minute by the core.
+        """
+
+    async def on_startup(self):
+        """
+        This method is called by the core when it starts. This is a good entry point for a Controller class, however the main_loop should be in a different method wich can be called here.
+        """
+        # self.main_loop()
+
+    async def on_shutdown(self):
+        """
+        This method is called by the core when it shuts down.
+        """
+
+class LED(ControllerBase):
+    """
+    Controllerbase controlls a normal LED.
+
+    Parameters
+    ----------
+    pin_num: int
+        number of the led's pin
+
+    Attributes
+    ----------
+    lock: threading.Lock
+        The lock used to protect gpio operations.
+    """
+    lock = None
+
+    def __init__(self,pin_num):
+        super().__init__()
+        self.pin = pin.DigitalPin(pin_num,_gpio.OUT)
+        self.lock = threading.Lock()
+        self.on()
+
+    async def on(self,args=[],command=None,respond=None):
+        if self.lock.locked():
+            response("This LED is already in use")
+            return
+        with self.lock:
+            self.write(True)
+
+    async def off(self,args=[],command=None,respond=None):
+        if self.lock.locked():
+            respond("This LED is already in use")
+            return
+        with self.lock:
+            self.write(False)
+
+    async def set(self,args=[],command=None,respond=None):
+        if len(args) != 1:
+            respond("one missing argument: state")
+            return
+        [state] = args
+        if self.lock.locked():
+            response("This LED is already in use")
+            return
+        with self.lock:
+            self.write(state)
+
+
+class StatusLED(PinControllerBase):
+
+    def __init__(self,pin_red,pin_green):
+        super(StatusLED,self).__init__([pin_red,pin_green])
+        self.pin_red = DigitalPin(pin_red,_gpio.OUT)
+        self.pin_green = DigitalPin(pin_green,_gpio.OUT)
+        self.green()
+
+    def red(self,args=[],command=None,respond=None):
+        if len(args) > 1:
+            respond(errmsg.args(command,"one","optional","<state>"))
+            return
+        elif len(args) == 1:
+            state = digilib.misc.parse_to_int_list(*args)
+        else:
+            state = 1
+        self.pin_red.write(state)
+        self.pin_green.write(int(not state))
+
+    def green(self,args=[],command=None,respond=None):
+        if len(args) > 1:
+            respond(ERROR_TAKES_ARGUMENTS.format(
+                command,"one","optional","<state>"))
+            return
+        elif len(args) == 1:
+            state = int(*args)
+        else:
+            state = 1
+        self.pin_green.write(state)
+        self.pin_red.write(int(not state))
+
+class DebugPinController(PinControllerBase):
+
+    def write(self,args=[],command=None,respond=None):
+        if len(args) != 2:
+            respond(ERROR_TAKES_ARGUMENTS.format(
+                    command, "two", "positional", "<name>"))
+            return False
+        pins = digilib.misc.parse_to_int_list(args[0])
+        [state] = digilib.misc.parse_to_int_list(args[1])
+        _gpio.write(pins,state)
+
+    def read(self,args=[],command=None,respond=None):
+        if len(args) != 2:
+            respond(ERROR_TAKES_ARGUMENTS.format(
+                    command, "two", "positional", "<name>"))
+            return False
+        pins = digilib.misc.parse_to_int_list(args[0])
+        [state] = digilib.misc.parse_to_int_list(args[1])
+        rv = _gpio.read(pins,state)
+        lgpio.debug(rv)
+        respond(str(rv))
+
+    def raise_exc(self,args=[],command=None,respond=None):
+        raise Exception("Test Exception")
+
+    async def araise_exc(self,args=[],command=None,respond=None):
+        state = digilib.misc.parse_to_int_list("1,2,3,4")
+        a = 1+2
+        raise Exception("Test Async Exception")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#

+ 169 - 0
src/digilib/gpio/ctrl_manager.py

@@ -0,0 +1,169 @@
+# Copyright 2017 Digital
+#
+# This file is part of DigiLib.
+#
+# DigiLib 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.
+#
+# DigiLib 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 DigiLib.  If not, see <http://www.gnu.org/licen
+
+class CtrlManager(object):
+    """
+    The CtrlManager collects data from controllers and analyses it.
+
+    Parameters
+    ----------
+    config: dict
+        dictionary holding config information. for more information see :doc:`/configure`
+
+    Attributes
+    ----------
+    self.timed_tg: curio.TaskGroup
+        A ``curio.TaskGroup`` in which all timed tasks will be spawned
+    """
+    def __init__(self):
+        super().__init__()
+        self.timed_tg = curio.TaskGroup(name="timed tasks")
+
+    async def astart(self):
+        """
+        This method starts the core. It does the setup and calls the run method.
+        """
+        # lets run the timed callers
+        await self.timed_tg.run(self.minutely())
+        await self.timed_tg.run(self.hourly())
+        await self.timed_tg.run(self.daily())
+
+    async def astop(self):
+        """
+        asynchronous stop method. cancels timed tasks and calls stop handlers
+        """
+        lcore.debug("canceling remaining timed tasks")
+        try:
+            self.timed_tg.cancel_remaining()
+            self.timed_tg.join()
+        except Exception as exc:
+            lcore.error("an error occured in a timed caller:",exc_info=exc)
+        # call the shutdown handlers
+        self.exec_coll_methods("shutdown")
+
+    async def call(self, ctrl_name, func_name, kwargs, respond):
+        """
+        The call method takes commands and calls the corresponding function with ``args`` and ``respond``. It treats all functions as asynchronous and logs the traceback if an exception occured.
+
+        Parameters
+        ----------
+        ctrl_name: str
+            name of the controller
+        func_name: str
+            name of the function to call
+        kwargs: dict
+            the keyword arguments for the function
+        respond: function or method
+            a function to send error messages
+        """
+        ctrl = beewatch._controllers.get(ctrl_name,False)
+        if not ctrl:
+            self.respond("can't call function '{}' of controller '{}', "
+                "there is no such controller!")
+            return
+        func = getattr(ctrl,func_name,False)
+        if not func:
+            self.respond("can't call function '{}' of controller '{}', "
+                "the controller doesn have this function!")
+            return
+        try:
+            await func(**kwargs)
+        except Exception as e:
+            lch.error(
+                "an error was raised when calling func {}:".format(func),
+                exc_info=e)
+            tb = traceback.format_exc()
+            await self.respond(tb,log_msg="traceback of '{}'"
+                .format(e.__cause__))
+        # # task joins iself to suppress the "task not joined" warning
+        # cur_task = await curio.current_task()
+        # await curio.ignore_after(0,cur_task.wait)
+
+    async def daily(self):
+        """
+        This method is calls the collectors daily method once every day
+        """
+        try:
+            while True:
+                await self.exec_coll_methods("daily")
+                # sleep one day
+                await curio.sleep(24*60*60*1000)
+        except TaskCancelled as exc:
+            # we catch this so when we join the timed_tg later we only get
+            # unexpected exceptions
+            lcore.debug("Daily loop was canceled")
+
+    async def exec_coll_methods(self,name):
+        """
+        This method calls the method of every controller with the name inside th name parameter
+
+        Parameters
+        ----------
+        name: str
+            The name of the controllers method which is to be called.
+        """
+        lcore.debug("executing every collector's {} function!".format(name))
+        for c in beewatch._collectors:
+            try:
+                method = getattr(c,name)
+                await method()
+            except TaskCancelled as exc:
+                raise
+            except Exception as exc:
+                lcore.error(
+                "an error occured when calling {}'s {} method!"
+                .format(repr(c),name))
+
+    async def hourly(self):
+        """
+        This method is calls the collectors hourly method once every hour
+        """
+        try:
+            while True:
+                await self.exec_coll_methods("hourly")
+                # sleep one hour
+                await curio.sleep(60*60*1000)
+        except TaskCancelled as exc:
+            # we catch this so when we join the timed_tg later we only get
+            # unexpected exceptions
+            lcore.debug("Hourly loop was canceled")
+
+    async def minutely(self):
+        """
+        This method is calls the collectors minutely method once every minute
+        """
+        try:
+            while True:
+                await self.exec_coll_methods("minutely")
+                # sleep one minute
+                await curio.sleep(60*1000)
+        except TaskCancelled as exc:
+            # we catch this so when we join the timed_tg later we only get
+            # unexpected exceptions
+            lcore.debug("Minutely loop was canceled")
+
+    def start(self):
+        """
+        synchronous start method wich calls astart asynchronously.
+        """
+        curio.run(self.async_start)
+
+    def stop(self):
+        """
+        synchronous stop method wich calls astop asynchronously.
+        """
+        curio.run(self.async_stop)

+ 77 - 0
src/digilib/gpio/errmsg.py

@@ -0,0 +1,77 @@
+# Copyright 2017 Digital
+#
+# This file is part of DigiLib.
+#
+# DigiLib 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.
+#
+# DigiLib 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 DigiLib.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Error messages
+--------------
+Some error messages often used in gpio. They should be used with ``.format()``.
+"""
+
+
+
+def args(command=None,amount=None,optional=None,syntax=None):
+    retmsg = ""
+    if command:
+        retmsg += "'{}' ".format(command)
+    retmsg += "takes "
+    if amount:
+        retmsg += "{} ".format(amount)
+    if optional:
+        retmsg += "{} ".format(optional)
+    retmsg += "argument(s): "
+    if syntax:
+        retmsg += "{} ".format(syntax)
+    return retmsg
+
+ERROR_TAKES_ARGUMENTS = "{} takes {} {} argument(s): {}"
+
+
+if __name__ == "__main__":
+    print(args("cmd","one","optional","<state>"))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#

+ 105 - 0
src/digilib/gpio/pin.py

@@ -0,0 +1,105 @@
+#!/usr/bin/env python3.5
+# Copyright 2017 Digital
+#
+# This file is part of DigiLib.
+#
+# DigiLib 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.
+#
+# DigiLib 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 DigiLib.  If not, see <http://www.gnu.org/licenses/>.
+
+# Python modules
+import atexit
+import logging
+import threading
+import traceback
+# Third party modules
+import curio
+import digilib.network
+import digilib.misc
+
+import .errmsg
+log = logging.getLogger(__name__+"")
+lgpio = logging.getLogger(__name__+".gpio")
+
+# Error messages
+# ERROR_TAKES_ARGUMENTS = "{} takes {} {} argument(s): {}"
+# respond(ERROR_TAKES_ARGUMENTS.format(
+#         command, "one", "positional", "<name>"))
+
+
+class PinBase(object):
+    """
+    PinBase is the base class for all classes representing a gpio pin
+
+    Parameters
+    ----------
+    pin_number: int
+        number of the pin
+    mode: gpio.OUT or gpio.IN
+        ``gpio.IN`` if the pin is an read pin or ``gpio.OUT`` if the pin is an write pin
+    """
+    pin_number = None
+    def __init__(self,pin_number,mode):
+        super(PinBase,self).__init__()
+        self.pin_number = pin_number
+        _gpio.setup(self.pin_number,_gpio.OUT)
+
+    def write(self,value):
+        [value] = digilib.misc.parse_to_int_list(value)
+        _gpio.write(self.pin_number,value)
+
+    def read(self):
+        value = _gpio.read(self.pin_number,value)
+        return value
+
+class DigitalPin(PinBase):
+    def __init__(self,pin_number,mode):
+        super(DigitalPin,self).__init__(pin_number,mode)
+
+class AnalogPin(PinBase):
+    def __init__(self,pin_number):
+        super(AnalogPin,self).__init__(pin_number)
+
+
+if __name__ == "__main__":
+    pass
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#

+ 143 - 0
src/digilib/gpio/wrapper.py

@@ -0,0 +1,143 @@
+#!/usr/bin/env python3.5
+# Copyright 2017 Digital
+#
+# This file is part of DigiLib.
+#
+# DigiLib 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.
+#
+# DigiLib 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 DigiLib.  If not, see <http://www.gnu.org/licenses/>.
+
+lgpio = logging.getLogger(__name__)
+
+try:
+    import RPi.gpio as _gpio
+    OUT = _gpio.OUT
+    IN = _gpio.IN
+    BCM = _gpio.BCM
+except ImportError:
+    lgpio.debug("failed to import RPi.gpio")
+    _gpio = None
+    OUT = "out"
+    IN = "in"
+    BCM = "bcm"
+
+_pins_for_cleanup = set()
+
+
+def cleanup(self,*args):
+    """
+    Calls _gpio.cleanup for every pin used. The module automatically registers this function to the `atexit` handler, so the user doesn't need to call it.
+    """
+    lgpio.debug("cleanup! ({})".format(args))
+    if _gpio:
+        # _gpio.cleanup wants a list or tuple, but _pins_for_cleanup is
+        # a set. we have to convert it first.
+        _gpio.cleanup(list(_pins_for_cleanup))
+
+def write(self,pins,state):
+    """
+    sets pin `pin` to `state`.
+
+    Parameters
+    ----------
+    pins: list or int
+        the pins which will be written to.
+    state: int or float
+        what will be written to the pins. float is only for analog pins.
+    """
+    lgpio.debug("setting pin(s) {} to value {}"
+        .format(pins,state))
+    if type(pins) is int:
+        pins = [pins]
+    _pins_for_cleanup.update(pins)
+    if _gpio:
+        _gpio.output(pins,state)
+
+def read(self,pins):
+    """
+    sets pin `pin` to `state`.
+
+    Parameters
+    ----------
+    pins: list or int
+        the pins which will be read from.
+
+    Returns
+    -------
+    value: list
+        the values read from the gpio pins in the same order as `pins`. if RPi.GPIO could not be imported, -1 is used instead of the pins actual value.
+    """
+    values = []
+    for p in pins:
+        if _gpio:
+            values.append(_gpio.input(p))
+        else:
+            values.append(-1)
+    lgpio.debug("reading pins {}: {}".format(
+        pins,values))
+    return values
+
+def configure():
+    """
+    Sets the gpio numbering mode to broadcom.
+    """
+    lgpio.debug("setting pin numbering to broadcom")
+    atexit.register(cleanup)
+    if _gpio:
+        _gpio.setmode(_gpio.BCM)
+
+def setup(self,pins,value):
+    """
+    wrapper for ``gpio.setup()``. used to configure pins as input or output
+
+    Parameters
+    ----------
+    pins: list
+        the pins which will be configured.
+    value: `IN` or `OUT`
+        wether the pins will be configured as input or output.
+    """
+    lgpio.debug("setting pin(s) {} to {}".format(pins,value))
+    if _gpio:
+        _gpio.setup(pins,value)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#