Explorar el Código

add mute neuron mute mute control from api

nico hace 7 años
padre
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/order             | Run a synapse from a text order    |
 | 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
 
@@ -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"
 ```
 
+### 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
 

+ 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"]
             # 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)
+
             # start each class name
             try:
                 for signal_class_name in list_signals_class_to_load:
                     signal_instance = SignalLauncher.launch_signal_class_by_name(signal_name=signal_class_name,
-                                                                                 brain=brain,
                                                                                  settings=settings)
                     if signal_instance is not None:
                         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 werkzeug.utils import secure_filename
 
+from kalliope import SignalLauncher
 from kalliope._version import version_str
 from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
 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.SynapseLauncher import SynapseLauncher
 from kalliope.core.Utils.FileManager import FileManager
+from kalliope.signals.order import Order
 
 logging.basicConfig()
 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/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('/mute/', view_func=self.get_mute, methods=['GET'])
+        self.app.add_url_rule('/mute/', view_func=self.set_mute, methods=['POST'])
 
     def run(self):
         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)
 
         # 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
         parameters = self.get_parameters_from_request(request)
@@ -204,7 +208,7 @@ class FlaskAPI(threading.Thread):
 
         order = request.get_json('order')
         # 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:
             # get the order
             order_to_run = order["order"]
@@ -307,6 +311,75 @@ class FlaskAPI(threading.Thread):
         func()
         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):
         """
         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
         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
         :param http_request:
+        :param json_name
         :return:
         """
 
         no_voice = False
         try:
             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:
             # no json received
             pass
-        logger.debug("[FlaskAPI] no_voice: %s" % no_voice)
+        logger.debug("[FlaskAPI] Boolean %s : %s" % (json_name, no_voice))
         return no_voice
 
     @staticmethod

+ 19 - 5
kalliope/core/SignalLauncher.py

@@ -8,21 +8,35 @@ logger = logging.getLogger("kalliope")
 
 class SignalLauncher:
 
+    # keep a list of instantiated signals
+    list_launched_signals = list()
+
     def __init__(self):
         pass
 
     @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
         :param signal_name: name of the signal class to load
-        :param brain: Brain Object
         :param settings: Settings Object
         """
         signal_folder = None
         if settings.resources:
             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
         self.trigger_instance = None
         self.trigger_callback_called = False
+        self.is_trigger_muted = False
 
         # save the current order listener
         self.order_listener = None
@@ -60,19 +61,7 @@ class Order(Thread):
         self.on_ready_notification_played_once = False
 
         # 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
         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
         """
         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
         while not self.trigger_callback_called:
             sleep(0.1)
@@ -234,11 +227,41 @@ class Order(Thread):
         logger.debug("[MainController] Selected sound: %s" % 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)
         if muted:
             self.trigger_instance.pause()
+            self.is_trigger_muted = True
             Utils.print_info("Kalliope now muted")
         else:
             self.trigger_instance.unpause()
+            self.is_trigger_muted = False
             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)