Browse Source

add mute neuron mute mute control from api

nico 7 years ago
parent
commit
f703c04687

+ 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
 
 

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

+ 80 - 6
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, json_name="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, json_name="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,75 @@ 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
+        for signal in SignalLauncher.get_launched_signals_list():
+            if isinstance(signal, Order):
+                data = {
+                    "mute": signal.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, json_name="mute")
+
+        # find the order signal and call the mute method
+        for signal in SignalLauncher.get_launched_signals_list():
+            if isinstance(signal, Order):
+                signal.set_mute_status(mute)
+                data = {
+                    "mute": signal.get_mute_status()
+                }
+                return jsonify(data), 200
+
+        data = {
+            "error": "Cannot switch mute status"
+        }
+        return jsonify(error=data), 400
+
+    @requires_auth
+    def unmute(self):
+        # find the order signal and call the mute method
+        for signal in SignalLauncher.get_launched_signals_list():
+            if isinstance(signal, Order):
+                signal.set_mute_status(False)
+                data = {
+                    "mute": "False"
+                }
+                return jsonify(data), 200
+
+        data = {
+            "error": "Cannot unmute"
+        }
+        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,22 +402,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, json_name):
         """
         """
         Get the no_voice flag from the request if exist
         Get the no_voice flag from the request if exist
         :param http_request:
         :param http_request:
+        :param json_name
         :return:
         :return:
         """
         """
 
 
         no_voice = False
         no_voice = 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 json_name in received_json:
+                no_voice = self.str_to_bool(received_json[json_name])
         except TypeError:
         except TypeError:
             # no json received
             # no json received
             pass
             pass
-        logger.debug("[FlaskAPI] no_voice: %s" % no_voice)
+        logger.debug("[FlaskAPI] Boolean %s : %s" % (json_name, no_voice))
         return no_voice
         return no_voice
 
 
     @staticmethod
     @staticmethod

+ 19 - 5
kalliope/core/SignalLauncher.py

@@ -8,21 +8,35 @@ 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

+ 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


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

@@ -0,0 +1,34 @@
+import logging
+
+from kalliope import SignalLauncher
+from kalliope.core.NeuronModule import NeuronModule, MissingParameterException
+from kalliope.signals.order import Order
+
+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():
+            for signal in SignalLauncher.get_launched_signals_list():
+                if isinstance(signal, Order):
+                    signal.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)