Browse Source

Merge pull request #337 from kalliope-project/mute_neuron

add mute neuron mute mute control rom api
Monf 7 years ago
parent
commit
ab92745625

+ 35 - 0
Docs/rest_api.md

@@ -12,6 +12,8 @@ Kalliope provides the REST API to manage the synapses. For configuring the API r
 | POST   | /synapses/start/id/<synapse_name> | Run a synapse by its name          |
 | POST   | /synapses/start/id/<synapse_name> | Run a synapse by its name          |
 | POST   | /synapses/start/order             | Run a synapse from a text order    |
 | POST   | /synapses/start/order             | Run a synapse from a text order    |
 | POST   | /synapses/start/audio             | Run a synapse from an audio sample |
 | POST   | /synapses/start/audio             | Run a synapse from an audio sample |
+| GET    | /mute                             | Get the current mute status        |
+| POST   | /mute                             | Switch the mute status             |
 
 
 ## Curl examples
 ## Curl examples
 
 
@@ -306,6 +308,39 @@ Curl command:
 curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@path/to/file.wav" -F no_voice="true"
 curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@path/to/file.wav" -F no_voice="true"
 ```
 ```
 
 
+### Get mute status
+
+Normal response codes: 200
+Error response codes: unauthorized(401), Bad request(400)
+
+Curl command:
+```bash
+curl -i --user admin:secret  -X GET  http://127.0.0.1:5000/mute
+```
+
+Output example:
+```JSON
+{
+  "mute": true
+}
+```
+
+### Switch mute status
+
+Normal response codes: 200
+Error response codes: unauthorized(401), Bad request(400)
+
+Curl command:
+```bash
+curl -i -H "Content-Type: application/json" --user admin:secret  -X POST -d '{"mute": "True"}' http://127.0.0.1:5000/mute
+```
+
+Output example:
+```JSON
+{
+  "mute": true
+}
+```
 
 
 ## No voice flag
 ## No voice flag
 
 

+ 13 - 1
Tests/test_cortex.py

@@ -131,7 +131,7 @@ class TestCortex(unittest.TestCase):
         self.assertDictEqual(dict(), Cortex.memory)
         self.assertDictEqual(dict(), Cortex.memory)
 
 
     def test_save_parameter_from_order_in_memory(self):
     def test_save_parameter_from_order_in_memory(self):
-        # define temp saved order parameter
+        # Test with a value that exist in the temp memory
         order_parameters = {
         order_parameters = {
             "key1": "value1",
             "key1": "value1",
             "key2": "value2"
             "key2": "value2"
@@ -147,5 +147,17 @@ class TestCortex(unittest.TestCase):
 
 
         self.assertDictEqual(expected_dict, Cortex.memory)
         self.assertDictEqual(expected_dict, Cortex.memory)
 
 
+        # test with a value that does not exsit
+        order_parameters = {
+            "key1": "value1",
+            "key2": "value2"
+        }
+
+        Cortex.temp = order_parameters
+        dict_val_to_save = {"my_key_in_memory": "{{key3}}"}
+
+        self.assertFalse(Cortex.save_parameter_from_order_in_memory(dict_val_to_save))
+
+
 if __name__ == '__main__':
 if __name__ == '__main__':
     unittest.main()
     unittest.main()

+ 10 - 0
brain_examples/mute.yml

@@ -0,0 +1,10 @@
+
+- name: "mute-synapse"
+  signals:
+    - order: "stop listening"
+  neurons:
+    - say:
+        message:
+          - "I stop hearing you, sir"
+    - mute:
+        status: True

+ 1 - 1
kalliope/__init__.py

@@ -156,11 +156,11 @@ def main():
             # For example, if the brain contains multiple time the signal type "order", the list will be ["order"]
             # For example, if the brain contains multiple time the signal type "order", the list will be ["order"]
             # If the brain contains some synapse with "order" and "event", the list will be ["order", "event"]
             # If the brain contains some synapse with "order" and "event", the list will be ["order", "event"]
             list_signals_class_to_load = get_list_signal_class_to_load(brain)
             list_signals_class_to_load = get_list_signal_class_to_load(brain)
+
             # start each class name
             # start each class name
             try:
             try:
                 for signal_class_name in list_signals_class_to_load:
                 for signal_class_name in list_signals_class_to_load:
                     signal_instance = SignalLauncher.launch_signal_class_by_name(signal_name=signal_class_name,
                     signal_instance = SignalLauncher.launch_signal_class_by_name(signal_name=signal_class_name,
-                                                                                 brain=brain,
                                                                                  settings=settings)
                                                                                  settings=settings)
                     if signal_instance is not None:
                     if signal_instance is not None:
                         signal_instance.start()
                         signal_instance.start()

+ 5 - 2
kalliope/core/Cortex.py

@@ -100,6 +100,7 @@ class Cortex(with_metaclass(Singleton, object)):
         Save key from the temp dict (where parameters loaded from the voice order where placed temporary)
         Save key from the temp dict (where parameters loaded from the voice order where placed temporary)
         into the memory dict
         into the memory dict
         :param order_parameters: dict of key to save.  {'key_name_in_memory': 'key_name_in_temp_dict'}
         :param order_parameters: dict of key to save.  {'key_name_in_memory': 'key_name_in_temp_dict'}
+        :return True if a value has been saved in the kalliope memory
         """
         """
 
 
         if order_parameters is not None:
         if order_parameters is not None:
@@ -111,6 +112,8 @@ class Cortex(with_metaclass(Singleton, object)):
                 # ask the cortex to save in memory the target "key" if it was in the order
                 # ask the cortex to save in memory the target "key" if it was in the order
                 if Utils.is_containing_bracket(value):
                 if Utils.is_containing_bracket(value):
                     # if the key exist in the temp dict we can load it with jinja
                     # if the key exist in the temp dict we can load it with jinja
-                    if value in Cortex.temp:
-                        value = jinja2.Template(value).render(Cortex.temp)
+                    value = jinja2.Template(value).render(Cortex.temp)
+                    if value:
                         Cortex.save(key, value)
                         Cortex.save(key, value)
+                        return True
+        return False

+ 68 - 11
kalliope/core/RestAPI/FlaskAPI.py

@@ -9,6 +9,7 @@ from flask_cors import CORS
 from flask_restful import abort
 from flask_restful import abort
 from werkzeug.utils import secure_filename
 from werkzeug.utils import secure_filename
 
 
+from kalliope import SignalLauncher
 from kalliope._version import version_str
 from kalliope._version import version_str
 from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
 from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
 from kalliope.core.LIFOBuffer import LIFOBuffer
 from kalliope.core.LIFOBuffer import LIFOBuffer
@@ -17,6 +18,7 @@ from kalliope.core.OrderListener import OrderListener
 from kalliope.core.RestAPI.utils import requires_auth
 from kalliope.core.RestAPI.utils import requires_auth
 from kalliope.core.SynapseLauncher import SynapseLauncher
 from kalliope.core.SynapseLauncher import SynapseLauncher
 from kalliope.core.Utils.FileManager import FileManager
 from kalliope.core.Utils.FileManager import FileManager
+from kalliope.signals.order import Order
 
 
 logging.basicConfig()
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
 logger = logging.getLogger("kalliope")
@@ -72,6 +74,8 @@ class FlaskAPI(threading.Thread):
         self.app.add_url_rule('/synapses/start/order', view_func=self.run_synapse_by_order, methods=['POST'])
         self.app.add_url_rule('/synapses/start/order', view_func=self.run_synapse_by_order, methods=['POST'])
         self.app.add_url_rule('/synapses/start/audio', view_func=self.run_synapse_by_audio, methods=['POST'])
         self.app.add_url_rule('/synapses/start/audio', view_func=self.run_synapse_by_audio, methods=['POST'])
         self.app.add_url_rule('/shutdown/', view_func=self.shutdown_server, methods=['POST'])
         self.app.add_url_rule('/shutdown/', view_func=self.shutdown_server, methods=['POST'])
+        self.app.add_url_rule('/mute/', view_func=self.get_mute, methods=['GET'])
+        self.app.add_url_rule('/mute/', view_func=self.set_mute, methods=['POST'])
 
 
     def run(self):
     def run(self):
         self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False)
         self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False)
@@ -157,7 +161,7 @@ class FlaskAPI(threading.Thread):
         synapse_target = BrainLoader().get_brain().get_synapse_by_name(synapse_name=synapse_name)
         synapse_target = BrainLoader().get_brain().get_synapse_by_name(synapse_name=synapse_name)
 
 
         # get no_voice_flag if present
         # get no_voice_flag if present
-        no_voice = self.get_no_voice_flag_from_request(request)
+        no_voice = self.get_boolean_flag_from_request(request, boolean_flag_to_find="no_voice")
 
 
         # get parameters
         # get parameters
         parameters = self.get_parameters_from_request(request)
         parameters = self.get_parameters_from_request(request)
@@ -204,7 +208,7 @@ class FlaskAPI(threading.Thread):
 
 
         order = request.get_json('order')
         order = request.get_json('order')
         # get no_voice_flag if present
         # get no_voice_flag if present
-        no_voice = self.get_no_voice_flag_from_request(request)
+        no_voice = self.get_boolean_flag_from_request(request, boolean_flag_to_find="no_voice")
         if order is not None:
         if order is not None:
             # get the order
             # get the order
             order_to_run = order["order"]
             order_to_run = order["order"]
@@ -307,6 +311,59 @@ class FlaskAPI(threading.Thread):
         func()
         func()
         return "Shutting down..."
         return "Shutting down..."
 
 
+    @requires_auth
+    def get_mute(self):
+        """
+        Return the current trigger status
+
+        Curl test
+        curl -i --user admin:secret  -X GET  http://127.0.0.1:5000/mute
+        """
+
+        # find the order signal and call the mute method
+        signal_order = SignalLauncher.get_order_instance()
+        if signal_order is not None:
+            data = {
+                "mute": signal_order.get_mute_status()
+            }
+            return jsonify(data), 200
+
+        # if no Order instance
+        data = {
+            "error": "Mute status unknow"
+        }
+        return jsonify(error=data), 400
+
+    @requires_auth
+    def set_mute(self):
+        """
+        Set the trigger status (muted or not)
+
+        Curl test:
+        curl -i -H "Content-Type: application/json" --user admin:secret  -X POST \
+        -d '{"mute": "True"}' http://127.0.0.1:5000/mute
+        """
+
+        if not request.get_json() or 'mute' not in request.get_json():
+            abort(400)
+
+        # get mute if present
+        mute = self.get_boolean_flag_from_request(request, boolean_flag_to_find="mute")
+
+        # find the order signal and call the mute method
+        signal_order = SignalLauncher.get_order_instance()
+        if signal_order is not None:
+            signal_order.set_mute_status(mute)
+            data = {
+                "mute": signal_order.get_mute_status()
+            }
+            return jsonify(data), 200
+
+        data = {
+            "error": "Cannot switch mute status"
+        }
+        return jsonify(error=data), 400
+
     def audio_analyser_callback(self, order):
     def audio_analyser_callback(self, order):
         """
         """
         Callback of the OrderListener. Called after the processing of the audio file
         Callback of the OrderListener. Called after the processing of the audio file
@@ -329,23 +386,23 @@ class FlaskAPI(threading.Thread):
         # this boolean will notify the main process that the order have been processed
         # this boolean will notify the main process that the order have been processed
         self.order_analyser_return = True
         self.order_analyser_return = True
 
 
-    def get_no_voice_flag_from_request(self, http_request):
+    def get_boolean_flag_from_request(self, http_request, boolean_flag_to_find):
         """
         """
-        Get the no_voice flag from the request if exist
+        Get the boolean flag from the request if exist
         :param http_request:
         :param http_request:
-        :return:
+        :param boolean_flag_to_find: json flag to find in the http_request
+        :return: True or False if the boolean flag has been found in the request
         """
         """
-
-        no_voice = False
+        boolean_flag = False
         try:
         try:
             received_json = http_request.get_json(force=True, silent=True, cache=True)
             received_json = http_request.get_json(force=True, silent=True, cache=True)
-            if 'no_voice' in received_json:
-                no_voice = self.str_to_bool(received_json['no_voice'])
+            if boolean_flag_to_find in received_json:
+                boolean_flag = self.str_to_bool(received_json[boolean_flag_to_find])
         except TypeError:
         except TypeError:
             # no json received
             # no json received
             pass
             pass
-        logger.debug("[FlaskAPI] no_voice: %s" % no_voice)
-        return no_voice
+        logger.debug("[FlaskAPI] Boolean %s : %s" % (boolean_flag_to_find, boolean_flag))
+        return boolean_flag
 
 
     @staticmethod
     @staticmethod
     def str_to_bool(s):
     def str_to_bool(s):

+ 31 - 5
kalliope/core/SignalLauncher.py

@@ -1,6 +1,7 @@
 import logging
 import logging
 
 
 from kalliope import Utils
 from kalliope import Utils
+from kalliope.signals.order import Order
 
 
 logging.basicConfig()
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
 logger = logging.getLogger("kalliope")
@@ -8,21 +9,46 @@ logger = logging.getLogger("kalliope")
 
 
 class SignalLauncher:
 class SignalLauncher:
 
 
+    # keep a list of instantiated signals
+    list_launched_signals = list()
+
     def __init__(self):
     def __init__(self):
         pass
         pass
 
 
     @classmethod
     @classmethod
-    def launch_signal_class_by_name(cls, signal_name, brain=None, settings=None):
+    def launch_signal_class_by_name(cls, signal_name, settings=None):
         """
         """
         load the signal class from the given name, pass the brain and settings to the signal
         load the signal class from the given name, pass the brain and settings to the signal
         :param signal_name: name of the signal class to load
         :param signal_name: name of the signal class to load
-        :param brain: Brain Object
         :param settings: Settings Object
         :param settings: Settings Object
         """
         """
         signal_folder = None
         signal_folder = None
         if settings.resources:
         if settings.resources:
             signal_folder = settings.resources.signal_folder
             signal_folder = settings.resources.signal_folder
 
 
-        return Utils.get_dynamic_class_instantiation(package_name="signals",
-                                                     module_name=signal_name,
-                                                     resources_dir=signal_folder)
+        launched_signal = Utils.get_dynamic_class_instantiation(package_name="signals",
+                                                                module_name=signal_name,
+                                                                resources_dir=signal_folder)
+
+        cls.add_launched_signals_to_list(launched_signal)
+
+        return launched_signal
+
+    @classmethod
+    def add_launched_signals_to_list(cls, signal):
+        cls.list_launched_signals.append(signal)
+
+    @classmethod
+    def get_launched_signals_list(cls):
+        return cls.list_launched_signals
+
+    @classmethod
+    def get_order_instance(cls):
+        """
+        Return the Order instance from the list of launched signals if exist
+        :return:
+        """
+        for signal in cls.list_launched_signals:
+            if isinstance(signal, Order):
+                return signal
+        return None

+ 52 - 0
kalliope/neurons/mute/README.md

@@ -0,0 +1,52 @@
+# Mute
+
+## Synopsis
+
+Mute control of kalliope. If set to True the trigger process will be stopped.
+
+Once this neuron is used, and Kalliope muted, the hotword is deactivated. Only ways to unmute are:
+- by calling the API (see [mute section](../../../Docs/rest_api.md#switch-mute-status))
+- If running on Raspberry, by using the unmute button. (See the section [Raspberry LED and mute button](../../../Docs/settings.md#raspberry-led-and-mute-button))
+- by using another signals than a "vocal order" that call back this neuron with a status set to "False"
+- Restarting Kalliope
+
+## Options
+
+| parameter | required | type    | default | choices     | comment                                           |
+|-----------|----------|---------|---------|-------------|---------------------------------------------------|
+| status    | YES      | Boolean |         | True, False | If "True" Kalliope will stop the hotword process  |
+
+
+## Return Values
+
+Not returned values
+
+## Synapses example
+
+Mute Kalliope from a vocal order
+```yml
+- name: "mute-synapse"
+  signals:
+    - order: "stop listening"
+  neurons:
+    - say:
+        message:
+          - "I stop hearing you, sir"
+    - mute:
+        status: True
+```
+
+Unmute Kalliope from another signals. In the following example, a MQTT message is received
+```yml
+- name: "unmute-synapse"
+  signals:
+    - mqtt_subscriber:
+        broker_ip: "127.0.0.1"
+        topic: "/my/sensor"
+  neurons:
+    - mute:
+        status: False
+    - say:
+        message:
+          - "Waiting for orders, sir"
+```

+ 0 - 0
kalliope/neurons/mute/__init__.py


+ 33 - 0
kalliope/neurons/mute/mute.py

@@ -0,0 +1,33 @@
+import logging
+
+from kalliope import SignalLauncher
+from kalliope.core.NeuronModule import NeuronModule
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+
+class Mute(NeuronModule):
+
+    def __init__(self, **kwargs):
+        super(Mute, self).__init__(**kwargs)
+
+        self.status = kwargs.get('status', None)
+
+        # check if parameters have been provided
+        if self._is_parameters_ok():
+            signal_order = SignalLauncher.get_order_instance()
+            if signal_order is not None:
+                signal_order.set_mute_status(self.status)
+
+    def _is_parameters_ok(self):
+        """
+        Check if received parameters are ok to perform operations in the neuron
+        :return: true if parameters are ok, raise an exception otherwise
+
+        .. raises:: MissingParameterException
+        """
+        if self.status is None:
+            logger.debug("[Mute] You must specify a status with a boolean")
+            return False
+        return True

+ 38 - 15
kalliope/signals/order/order.py

@@ -51,6 +51,7 @@ class Order(Thread):
         # save an instance of the trigger
         # save an instance of the trigger
         self.trigger_instance = None
         self.trigger_instance = None
         self.trigger_callback_called = False
         self.trigger_callback_called = False
+        self.is_trigger_muted = False
 
 
         # save the current order listener
         # save the current order listener
         self.order_listener = None
         self.order_listener = None
@@ -60,19 +61,7 @@ class Order(Thread):
         self.on_ready_notification_played_once = False
         self.on_ready_notification_played_once = False
 
 
         # rpi setting for led and mute button
         # rpi setting for led and mute button
-        self.rpi_utils = None
-        if self.settings.rpi_settings:
-            # the useer set GPIO pin, we need to instantiate the RpiUtils class in order to setup GPIO
-            self.rpi_utils = RpiUtils(self.settings.rpi_settings, self.muted_button_pressed)
-            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.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)
+        self.init_rpi_utils()
 
 
         # Initialize the state machine
         # Initialize the state machine
         self.machine = Machine(model=self, states=Order.states, initial='init', queued=True)
         self.machine = Machine(model=self, states=Order.states, initial='init', queued=True)
@@ -135,7 +124,11 @@ class Order(Thread):
         Method to print in debug that the main process is waiting for a trigger detection
         Method to print in debug that the main process is waiting for a trigger detection
         """
         """
         logger.debug("[MainController] Entering state: %s" % self.state)
         logger.debug("[MainController] Entering state: %s" % self.state)
-        Utils.print_info("Waiting for trigger detection")
+        if self.is_trigger_muted:  # the user asked to mute inside the mute neuron
+            Utils.print_info("Kalliope is muted")
+            self.trigger_instance.pause()
+        else:
+            Utils.print_info("Waiting for trigger detection")
         # this loop is used to keep the main thread alive
         # this loop is used to keep the main thread alive
         while not self.trigger_callback_called:
         while not self.trigger_callback_called:
             sleep(0.1)
             sleep(0.1)
@@ -234,11 +227,41 @@ class Order(Thread):
         logger.debug("[MainController] Selected sound: %s" % random_path)
         logger.debug("[MainController] Selected sound: %s" % random_path)
         return Utils.get_real_file_path(random_path)
         return Utils.get_real_file_path(random_path)
 
 
-    def muted_button_pressed(self, muted=False):
+    def set_mute_status(self, muted=False):
+        """
+        Define is the trigger is listening or not
+        :param muted: Boolean. If true, kalliope is muted
+        """
         logger.debug("[MainController] Mute button pressed. Switch trigger process to muted: %s" % muted)
         logger.debug("[MainController] Mute button pressed. Switch trigger process to muted: %s" % muted)
         if muted:
         if muted:
             self.trigger_instance.pause()
             self.trigger_instance.pause()
+            self.is_trigger_muted = True
             Utils.print_info("Kalliope now muted")
             Utils.print_info("Kalliope now muted")
         else:
         else:
             self.trigger_instance.unpause()
             self.trigger_instance.unpause()
+            self.is_trigger_muted = False
             Utils.print_info("Kalliope now listening for trigger detection")
             Utils.print_info("Kalliope now listening for trigger detection")
+
+    def get_mute_status(self):
+        """
+        return the current state of the trigger (muted or not)
+        :return: Boolean
+        """
+        return self.is_trigger_muted
+
+    def init_rpi_utils(self):
+        """
+        Start listening on GPIO if defined in settings
+        """
+        if self.settings.rpi_settings:
+            # the user set GPIO pin, we need to instantiate the RpiUtils class in order to setup GPIO
+            rpi_utils = RpiUtils(self.settings.rpi_settings, self.set_mute_status)
+            if self.settings.rpi_settings.pin_mute_button:
+                # start the listening for button pressed thread only if the user set a pin
+                rpi_utils.daemon = True
+                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)