Pārlūkot izejas kodu

led and mute button support for Rpi

nico 8 gadi atpakaļ
vecāks
revīzija
473df782a6

+ 40 - 0
Docs/settings.md

@@ -323,5 +323,45 @@ And use variables in your neurons:
           password: "{{password}}"
 ```
 
+## Raspberry LED and mute button
+LEDs connected to GPIO port of your Raspberry can be used to know current status of Kalliope.
+A button can also be added in order to pause the trigger process. Kalliope does not listen for the hotword anymore when pressed.
+
+A Dictionary called `rpi` can be declared which contains pin number to use following the mapping bellow
+
+| Value name        | Description                                                                                                |
+|-------------------|------------------------------------------------------------------------------------------------------------|
+| pin_mute_button   | Pin connected to a mute button. When pressed the trigger process of kalliope is paused                     |
+| pin_led_started   | Pin switched to "on" when Kalliope is running                                                              |
+| pin_led_muted     | Pin switched to "on" when the mute button is pressed                                                       |
+| pin_led_talking   | Pin switched to "on" when Kalliope is talking                                                              |
+| pin_led_listening | Pin switched to "on" when Kalliope is readu to listen an order after a trigger detection ("Say something") |
+
+**Example config**
+```yml
+rpi:
+  pin_mute_button: 6
+  pin_led_started: 5
+  pin_led_muted: 17
+  pin_led_talking: 27
+  pin_led_listening: 22
+```
+
+**Example circuit**
+
+You will be using one of the ‘ground’ (GND) pins to act like the ‘negative’ or 0 volt ends of a battery. 
+The ‘positive’ end of the battery will be provided by a GPIO pin.
+
+<p align="center">
+    <img style="width: 200px;" src="../images/led_kalliope_circuit.png">
+</p>
+
+
+>**Note:** You must ALWAYS use resistors to connect LEDs up to the GPIO pins of the Raspberry Pi. 
+The Raspberry Pi can only supply a small current (about 60mA). T
+he LEDs will want to draw more, and if allowed to they will burn out the Raspberry Pi. 
+Therefore putting the resistors in the circuit will ensure that only this small current will flow and the Pi will not be damaged.
+
+
 ## Next: configure the brain of Kalliope
 Now your settings are ok, you can start creating the [brain](brain.md) of your assistant.

+ 2 - 1
Tests/test_models.py

@@ -353,7 +353,8 @@ class TestModels(unittest.TestCase):
                 'ttss': ['ttts'],
                 'variables': {'key1': 'val1'},
                 'resources': None,
-                'triggers': ['snowboy']
+                'triggers': ['snowboy'],
+                'rpi_settings': None
             }
 
             self.assertDictEqual(expected_result_serialize, setting1.serialize())

BIN
images/led_kalliope_circuit.png


+ 15 - 2
kalliope/__init__.py

@@ -148,10 +148,23 @@ def main():
             # catch signal for killing on Ctrl+C pressed
             signal.signal(signal.SIGINT, signal_handler)
             # start the state machine
-            MainController(brain=brain)
+            try:
+                MainController(brain=brain)
+            except (KeyboardInterrupt, SystemExit):
+                Utils.print_info("Ctrl+C pressed. Killing Kalliope")
+            finally:
+                # we need to switch GPIO pin to default status if we are using a Rpi
+                if settings.rpi_settings:
+                    logger.debug("Clean GPIO")
+                    import RPi.GPIO as GPIO
+                    GPIO.cleanup()
 
     if parser.action == "gui":
-        ShellGui(brain=brain)
+        try:
+            ShellGui(brain=brain)
+        except (KeyboardInterrupt, SystemExit):
+            Utils.print_info("Ctrl+C pressed. Killing Kalliope")
+            sys.exit(0)
 
 
 def configure_logging(debug=None):

+ 31 - 2
kalliope/core/ConfigurationManager/SettingLoader.py

@@ -2,6 +2,7 @@ import logging
 import os
 from six import with_metaclass
 
+from kalliope.core.Models.RpiSettings import RpiSettings
 from .YAMLLoader import YAMLLoader
 from kalliope.core.Models.Resources import Resources
 from kalliope.core.Utils.Utils import Utils
@@ -113,6 +114,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
         default_synapse = self._get_default_synapse(settings)
         resources = self._get_resources(settings)
         variables = self._get_variables(settings)
+        rpi_settings = self._get_rpi_settings(settings)
 
         # Load the setting singleton with the parameters
         setting_object.default_tts_name = default_tts_name
@@ -131,6 +133,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
         setting_object.default_synapse = default_synapse
         setting_object.resources = resources
         setting_object.variables = variables
+        setting_object.rpi_settings = rpi_settings
 
         return setting_object
 
@@ -452,7 +455,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
                 # check the CORS request settings
                 allowed_cors_origin = False
                 if "allowed_cors_origin" in rest_api:
-                     allowed_cors_origin = rest_api["allowed_cors_origin"]
+                    allowed_cors_origin = rest_api["allowed_cors_origin"]
 
             except KeyError as e:
                 raise SettingNotFound("%s settings not found" % e)
@@ -604,7 +607,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
         return play_on_ready_notification
 
     @staticmethod
-    def _get_on_ready_answers( settings):
+    def _get_on_ready_answers(settings):
         """
         Return the list of on_ready_answers string from the settings.
         :param settings: The YAML settings file
@@ -662,4 +665,30 @@ class SettingLoader(with_metaclass(Singleton, object)):
             # User does not provide this settings
             return dict()
 
+    @staticmethod
+    def _get_rpi_settings(settings):
+        """
+        return RpiSettings object
+        :param settings: The loaded YAML settings file
+        :return: 
+        """
 
+        try:
+            rpi_settings_dict = settings["rpi"]
+            rpi_settings = RpiSettings()
+            # affect pin if there are declared
+            if "pin_mute_button" in rpi_settings_dict:
+                rpi_settings.pin_mute_button = rpi_settings_dict["pin_mute_button"]
+            if "pin_led_started" in rpi_settings_dict:
+                rpi_settings.pin_led_started = rpi_settings_dict["pin_led_started"]
+            if "pin_led_muted" in rpi_settings_dict:
+                rpi_settings.pin_led_muted = rpi_settings_dict["pin_led_muted"]
+            if "pin_led_talking" in rpi_settings_dict:
+                rpi_settings.pin_led_talking = rpi_settings_dict["pin_led_talking"]
+            if "pin_led_listening" in rpi_settings_dict:
+                rpi_settings.pin_led_listening = rpi_settings_dict["pin_led_listening"]
+
+            return rpi_settings
+        except KeyError:
+            logger.debug("[SettingsLoader] No Rpi config")
+            return None

+ 37 - 11
kalliope/core/MainController.py

@@ -12,6 +12,7 @@ from kalliope.core.Players import Mplayer
 from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
 from kalliope.core.SynapseLauncher import SynapseLauncher
 from kalliope.core.TriggerLauncher import TriggerLauncher
+from kalliope.core.Utils.RpiUtils import RpiUtils
 from kalliope.neurons.say.say import Say
 
 logging.basicConfig()
@@ -43,6 +44,19 @@ class MainController:
         # Starting the rest API
         self._start_rest_api()
 
+        self.rpi_utils = None
+        if self.settings.rpi_settings:
+            if self.settings.rpi_settings.pin_mute_button:
+                # start the listening for button pressed thread only if the user set a pin
+                self.rpi_utils = RpiUtils(self.settings.rpi_settings, self.muted_button_pressed)
+                self.rpi_utils.daemon = True
+                self.rpi_utils.start()
+        # switch high the start led, as kalliope is started. Only if the setting exist
+        if self.settings.rpi_settings:
+            if self.settings.rpi_settings.pin_led_started:
+                logger.debug("[MainController] Switching pin_led_started to ON")
+                RpiUtils.switch_pin_to_on(self.settings.rpi_settings.pin_led_started)
+
         # save an instance of the trigger
         self.trigger_instance = None
         self.trigger_callback_called = False
@@ -84,7 +98,7 @@ class MainController:
         """
         This function will start the trigger thread that listen for the hotword
         """
-        logger.debug("Entering state: %s" % self.state)
+        logger.debug("[MainController] Entering state: %s" % self.state)
         self.trigger_instance = self._get_default_trigger()
         self.trigger_callback_called = False
         self.trigger_instance.daemon = True
@@ -96,7 +110,7 @@ class MainController:
         """
         Play a sound when Kalliope is ready to be awaken at the first start
         """
-        logger.debug("Entering state: %s" % self.state)
+        logger.debug("[MainController] Entering state: %s" % self.state)
         if (not self.on_ready_notification_played_once and self.settings.play_on_ready_notification == "once") or \
                         self.settings.play_on_ready_notification == "always":
             # we remember that we played the notification one time
@@ -113,7 +127,7 @@ class MainController:
         """
         Method to print in debug that the main process is waiting for a trigger detection
         """
-        logger.debug("Entering state: %s" % self.state)
+        logger.debug("[MainController] Entering state: %s" % self.state)
         Utils.print_info("Waiting for trigger detection")
         # this loop is used to keep the main thread alive
         while not self.trigger_callback_called:
@@ -124,10 +138,13 @@ class MainController:
         """
         Method to print in debug that the main process is waiting for an order to analyse
         """
-        logger.debug("Entering state: %s" % self.state)
+        logger.debug("[MainController] Entering state: %s" % self.state)
         # this loop is used to keep the main thread alive
         while not self.order_listener_callback_called:
             sleep(0.1)
+        if self.settings.rpi_settings:
+            if self.settings.rpi_settings.pin_led_listening:
+                RpiUtils.switch_pin_to_off(self.settings.rpi_settings.pin_led_listening)
         self.next_state()
 
     def trigger_callback(self):
@@ -135,7 +152,7 @@ class MainController:
         we have detected the hotword, we can now pause the Trigger for a while
         The user can speak out loud his order during this time.
         """
-        logger.debug("Trigger callback called, switching to the next state")
+        logger.debug("[MainController] Trigger callback called, switching to the next state")
         self.trigger_callback_called = True
 
     def stop_trigger_process(self):
@@ -143,7 +160,7 @@ class MainController:
         The trigger has been awaken, we don't needed it anymore
         :return: 
         """
-        logger.debug("Entering state: %s" % self.state)
+        logger.debug("[MainController] Entering state: %s" % self.state)
         self.trigger_instance.stop()
         self.next_state()
 
@@ -151,7 +168,7 @@ class MainController:
         """
         Start the STT engine thread
         """
-        logger.debug("Entering state: %s" % self.state)
+        logger.debug("[MainController] Entering state: %s" % self.state)
         # start listening for an order
         self.order_listener_callback_called = False
         self.order_listener = OrderListener(callback=self.order_listener_callback)
@@ -164,7 +181,7 @@ class MainController:
         Play a sound or make Kalliope say something to notify the user that she has been awaken and now
         waiting for order
         """
-        logger.debug("Entering state: %s" % self.state)
+        logger.debug("[MainController] Entering state: %s" % self.state)
         # if random wake answer sentence are present, we play this
         if self.settings.random_wake_up_answers is not None:
             Say(message=self.settings.random_wake_up_answers)
@@ -180,7 +197,7 @@ class MainController:
         :param order: the sentence received
         :type order: str
         """
-        logger.debug("order listener callback called. Order to process: %s" % order)
+        logger.debug("[MainController] Order listener callback called. Order to process: %s" % order)
         self.order_to_process = order
         self.order_listener_callback_called = True
 
@@ -188,7 +205,7 @@ class MainController:
         """
         Start the order analyser with the caught order to process
         """
-        logger.debug("order in analysing_order_thread %s" % self.order_to_process)
+        logger.debug("[MainController] order in analysing_order_thread %s" % self.order_to_process)
         SynapseLauncher.run_matching_synapse_from_order(self.order_to_process,
                                                         self.brain,
                                                         self.settings,
@@ -217,7 +234,7 @@ class MainController:
         """
         # take first randomly a path
         random_path = random.choice(random_wake_up_sounds)
-        logger.debug("Selected sound: %s" % random_path)
+        logger.debug("[MainController] Selected sound: %s" % random_path)
         return Utils.get_real_file_path(random_path)
 
     def _start_rest_api(self):
@@ -234,3 +251,12 @@ class MainController:
                                  allowed_cors_origin=self.settings.rest_api.allowed_cors_origin)
             flask_api.daemon = True
             flask_api.start()
+
+    def muted_button_pressed(self, muted=False):
+        logger.debug("[MainController] Mute button pressed. Switch trigger process to muted: %s" % muted)
+        if muted:
+            self.trigger_instance.pause()
+            Utils.print_info("Kalliope now muted")
+        else:
+            self.trigger_instance.unpause()
+            Utils.print_info("Kalliope now listening for trigger detection")

+ 35 - 0
kalliope/core/Models/RpiSettings.py

@@ -0,0 +1,35 @@
+
+
+class RpiSettings(object):
+
+    def __init__(self, pin_mute_button=None, pin_led_started=None, pin_led_muted=None,
+                 pin_led_talking=None, pin_led_listening=None):
+        self.pin_mute_button = pin_mute_button
+        self.pin_led_started = pin_led_started
+        self.pin_led_muted = pin_led_muted
+        self.pin_led_talking = pin_led_talking
+        self.pin_led_listening = pin_led_listening
+
+    def __str__(self):
+        return str(self.serialize())
+
+    def serialize(self):
+        """
+        This method allows to serialize in a proper way this object        
+        """
+
+        return {
+            'pin_mute_button': self.pin_mute_button,
+            'pin_led_started': self.pin_led_started,
+            'pin_led_muted': self.pin_led_muted,
+            'pin_led_talking': self.pin_led_talking,
+            'pin_led_listening': self.pin_led_listening,
+        }
+
+    def __eq__(self, other):
+        """
+        This is used to compare 2 objects
+        :param other:
+        :return:
+        """
+        return self.__dict__ == other.__dict__

+ 5 - 2
kalliope/core/Models/Settings.py

@@ -24,7 +24,8 @@ class Settings(object):
                  cache_path=None,
                  default_synapse=None,
                  resources=None,
-                 variables=None):
+                 variables=None,
+                 rpi_settings=None):
 
         self.default_tts_name = default_tts_name
         self.default_stt_name = default_stt_name
@@ -44,6 +45,7 @@ class Settings(object):
         self.variables = variables
         self.machine = platform.machine()   # can be x86_64 or armv7l
         self.kalliope_version = current_kalliope_version
+        self.rpi_settings = rpi_settings
 
     def serialize(self):
         """
@@ -71,7 +73,8 @@ class Settings(object):
             'resources': self.resources,
             'variables': self.variables,
             'machine': self.machine,
-            'kalliope_version': self.kalliope_version
+            'kalliope_version': self.kalliope_version,
+            'rpi_settings': self.rpi_settings.serialize() if self.rpi_settings is not None else None
         }
 
     def __str__(self):

+ 1 - 1
kalliope/core/Models/__init__.py

@@ -5,4 +5,4 @@ from .Brain import Brain
 from .Order import Order
 from .Synapse import Synapse
 from .Neuron import Neuron
-
+from .RpiSettings import RpiSettings

+ 21 - 0
kalliope/core/NeuronModule.py

@@ -10,6 +10,7 @@ from kalliope.core import OrderListener
 from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
 from kalliope.core.Models.MatchedSynapse import MatchedSynapse
 from kalliope.core.OrderAnalyser import OrderAnalyser
+from kalliope.core.Utils.RpiUtils import RpiUtils
 from kalliope.core.Utils.Utils import Utils
 
 logging.basicConfig()
@@ -159,9 +160,15 @@ class NeuronModule(object):
                                                                         module_name=self.tts.name,
                                                                         parameters=self.tts.parameters,
                                                                         resources_dir=tts_folder)
+            # Kalliope will talk, turn on the LED
+            self.switch_on_led_talking(rpi_settings=self.settings.rpi_settings, on=True)
+
             # generate the audio file and play it
             tts_module_instance.say(tts_message)
 
+            # Kalliope has finished to talk, turn off the LED
+            self.switch_on_led_talking(rpi_settings=self.settings.rpi_settings, on=False)
+
     def _get_message_from_dict(self, message_dict):
         """
         Generate a message that can be played by a TTS engine from a dict of variable and the jinja template
@@ -287,3 +294,17 @@ class NeuronModule(object):
 
         logger.debug("NeuroneModule: TTS args: %s" % tts_object)
         return tts_object
+
+    @staticmethod
+    def switch_on_led_talking(rpi_settings, on):
+        """
+        Call the Rpi utils class to switch the led talking if the setting has been specified by the user
+        :param rpi_settings: Rpi
+        :param on: True if the led need to be switched to on
+        """
+        if rpi_settings:
+            if rpi_settings.pin_led_talking:
+                if on:
+                    RpiUtils.switch_pin_to_on(rpi_settings.pin_led_talking)
+                else:
+                    RpiUtils.switch_pin_to_off(rpi_settings.pin_led_talking)

+ 0 - 13
kalliope/core/ShellGui.py

@@ -17,19 +17,6 @@ logging.basicConfig()
 logger = logging.getLogger("kalliope")
 
 
-def signal_handler(signal, frame):
-    """
-    Used to catch a keyboard signal like Ctrl+C in order to kill the kalliope program
-    :param signal: signal handler
-    :param frame: execution frame
-    """
-    print("\n")
-    Utils.print_info("Ctrl+C pressed. Killing Kalliope")
-    sys.exit(0)
-
-signal.signal(signal.SIGINT, signal_handler)
-
-
 class ShellGui:
     def __init__(self, brain=None):
         """

+ 91 - 0
kalliope/core/Utils/RpiUtils.py

@@ -0,0 +1,91 @@
+from threading import Thread
+
+try:
+    # only import if we are on a Rpi
+    import RPi.GPIO as GPIO
+except RuntimeError:
+    pass
+import time
+
+import logging
+
+from kalliope.core.Models.RpiSettings import RpiSettings
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+
+class RpiUtils(Thread):
+
+    def __init__(self, rpi_settings=None, callback=None):
+        """
+        
+        :param rpi_settings: Settings object with GPIO pin number to use
+        :type rpi_settings: RpiSettings
+        :param callback: Callback function from the main controller to call when the mute button is pressed
+        """
+        super(RpiUtils, self).__init__()
+        GPIO.setmode(GPIO.BCM)  # Use GPIO name
+        GPIO.setwarnings(False)
+        self.rpi_settings = rpi_settings
+        self.callback = callback
+        self.init_gpio(self.rpi_settings)
+
+    def run(self):
+        # run the main thread
+        try:
+            while True:  # keep the thread alive
+                time.sleep(0.1)
+        except (KeyboardInterrupt, SystemExit):
+            self.destroy()
+        self.destroy()
+
+    def switch_kalliope_mute_led(self, event):
+        """
+        Switch the state of the MUTE LED
+        :param event: 
+        :return: 
+        """
+        logger.debug("[RpiUtils] Event button caught. Switching mute led")
+        # get led status
+        led_mute_kalliope = GPIO.input(self.rpi_settings.pin_led_muted)
+        # switch state
+        if led_mute_kalliope == GPIO.HIGH:
+            logger.debug("[RpiUtils] Switching pin_led_muted to OFF")
+            self.switch_pin_to_off(self.rpi_settings.pin_led_muted)
+            self.callback(muted=False)
+        else:
+            logger.debug("[RpiUtils] Switching pin_led_muted to ON")
+            self.switch_pin_to_on(self.rpi_settings.pin_led_muted)
+            self.callback(muted=True)
+
+    def destroy(self):
+        logger.debug("[RpiUtils] Cleanup GPIO configuration")
+        GPIO.cleanup()
+
+    def init_gpio(self, rpi_settings):
+        # All led are off by default
+        if self.rpi_settings.pin_led_muted:
+            GPIO.setup(rpi_settings.pin_led_muted, GPIO.OUT, initial=GPIO.LOW)
+        if self.rpi_settings.pin_led_started:
+            GPIO.setup(rpi_settings.pin_led_started, GPIO.OUT, initial=GPIO.LOW)
+        if self.rpi_settings.pin_led_listening:
+            GPIO.setup(rpi_settings.pin_led_listening, GPIO.OUT, initial=GPIO.LOW)
+        if self.rpi_settings.pin_led_talking:
+            GPIO.setup(rpi_settings.pin_led_talking, GPIO.OUT, initial=GPIO.LOW)
+
+        # MUTE button
+        GPIO.setup(rpi_settings.pin_mute_button, GPIO.IN, pull_up_down=GPIO.PUD_UP)
+        GPIO.add_event_detect(rpi_settings.pin_mute_button, GPIO.FALLING,
+                              callback=self.switch_kalliope_mute_led,
+                              bouncetime=500)
+
+    @classmethod
+    def switch_pin_to_on(cls, pin_number):
+        logger.debug("[RpiUtils] Switching pin number %s to ON" % pin_number)
+        GPIO.output(pin_number, GPIO.HIGH)
+
+    @classmethod
+    def switch_pin_to_off(cls, pin_number):
+        logger.debug("[RpiUtils] Switching pin number %s to OFF" % pin_number)
+        GPIO.output(pin_number, GPIO.LOW)

+ 10 - 0
kalliope/settings.yml

@@ -162,3 +162,13 @@ default_synapse: "default-synapse"
 #var_files:
 #  - variables.yml
 #  - variables2.yml
+
+# ---------------------------
+# Raspberry Pi GPIO settings
+# ---------------------------
+#rpi:
+#  pin_mute_button: 24
+#  pin_led_started: 23
+#  pin_led_muted: 17
+#  pin_led_talking: 27
+#  pin_led_listening: 22

+ 10 - 1
kalliope/stt/Utils.py

@@ -4,7 +4,8 @@ from time import sleep
 import logging
 import speech_recognition as sr
 
-from kalliope import Utils
+from kalliope import Utils, SettingLoader
+from kalliope.core.Utils.RpiUtils import RpiUtils
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -24,6 +25,10 @@ class SpeechRecognition(Thread):
         self.kill_yourself = False
         self.audio_stream = None
 
+        # get global configuration
+        sl = SettingLoader()
+        self.settings = sl.settings
+
         if audio_file is None:
             # audio file not set, we need to capture a sample from the microphone
             with self.microphone as source:
@@ -40,6 +45,10 @@ class SpeechRecognition(Thread):
         """
         if self.audio_stream is None:
             Utils.print_info("Say something!")
+            # Turn on the listening led if we are on a Raspberry
+            if self.settings.rpi_settings:
+                if self.settings.rpi_settings.pin_led_listening:
+                    RpiUtils.switch_pin_to_on(self.settings.rpi_settings.pin_led_listening)
             self.stop_thread = self.recognizer.listen_in_background(self.microphone, self.callback)
             while not self.kill_yourself:
                 sleep(0.1)

+ 1 - 0
setup.py

@@ -83,6 +83,7 @@ setup(
         'GitPython>=2.1.3',
         'packaging>=16.8',
         'transitions>=0.4.3',
+        'RPi.GPIO>=0.6.3'
     ],