8 Commits b0fa8743d1 ... d20f67037d

Author SHA1 Message Date
  digital d20f67037d moved controllers from pin.py to controller.py, worked in ButtonController and created file errmsg.py for error messages 7 years ago
  digital a448f96ad7 typo fix 7 years ago
  digital 6e4e2b3b32 declaring variable _pins_for_cleanup now 7 years ago
  digital 2f33210b97 moved gpio/__init__.py to gpio/pin.py. gpio/__init__ only imports now. 7 years ago
  digital 0e7d1f8f8a started to implement a controller manager and worked on the controller baseclass 7 years ago
  digital a72ebc6c1f renamed setmode to configure 7 years ago
  digital 7f0b60306f removed blinker import from gpio 7 years ago
  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 years ago

+ 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)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#