MainController.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import logging
  2. import random
  3. from time import sleep
  4. from flask import Flask
  5. from transitions import Machine
  6. from kalliope.core import Utils
  7. from kalliope.core.ConfigurationManager import SettingLoader
  8. from kalliope.core.OrderListener import OrderListener
  9. from kalliope.core.Players import Mplayer
  10. from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
  11. from kalliope.core.SynapseLauncher import SynapseLauncher
  12. from kalliope.core.TriggerLauncher import TriggerLauncher
  13. from kalliope.core.Utils.RpiUtils import RpiUtils
  14. from kalliope.neurons.say.say import Say
  15. logging.basicConfig()
  16. logger = logging.getLogger("kalliope")
  17. class MainController:
  18. """
  19. This Class is the global controller of the application.
  20. """
  21. states = ['init',
  22. 'starting_trigger',
  23. 'playing_ready_sound',
  24. 'waiting_for_trigger_callback',
  25. 'stopping_trigger',
  26. 'start_order_listener',
  27. 'playing_wake_up_answer',
  28. 'waiting_for_order_listener_callback',
  29. 'analysing_order']
  30. def __init__(self, brain=None):
  31. self.brain = brain
  32. # get global configuration
  33. sl = SettingLoader()
  34. self.settings = sl.settings
  35. # keep in memory the order to process
  36. self.order_to_process = None
  37. # Starting the rest API
  38. self._start_rest_api()
  39. self.rpi_utils = None
  40. if self.settings.rpi_settings:
  41. # the useer set GPIO pin, we need to instantiate the RpiUtils class in order to setup GPIO
  42. self.rpi_utils = RpiUtils(self.settings.rpi_settings, self.muted_button_pressed)
  43. if self.settings.rpi_settings.pin_mute_button:
  44. # start the listening for button pressed thread only if the user set a pin
  45. self.rpi_utils.daemon = True
  46. self.rpi_utils.start()
  47. # switch high the start led, as kalliope is started. Only if the setting exist
  48. if self.settings.rpi_settings:
  49. if self.settings.rpi_settings.pin_led_started:
  50. logger.debug("[MainController] Switching pin_led_started to ON")
  51. RpiUtils.switch_pin_to_on(self.settings.rpi_settings.pin_led_started)
  52. # save an instance of the trigger
  53. self.trigger_instance = None
  54. self.trigger_callback_called = False
  55. # save the current order listener
  56. self.order_listener = None
  57. self.order_listener_callback_called = False
  58. # boolean used to know id we played the on ready notification at least one time
  59. self.on_ready_notification_played_once = False
  60. # Initialize the state machine
  61. self.machine = Machine(model=self, states=MainController.states, initial='init', queued=True)
  62. # define transitions
  63. self.machine.add_transition('start_trigger', ['init', 'analysing_order'], 'starting_trigger')
  64. self.machine.add_transition('play_ready_sound', 'starting_trigger', 'playing_ready_sound')
  65. self.machine.add_transition('wait_trigger_callback', 'playing_ready_sound', 'waiting_for_trigger_callback')
  66. self.machine.add_transition('stop_trigger', 'waiting_for_trigger_callback', 'stopping_trigger')
  67. self.machine.add_transition('play_wake_up_answer', 'waiting_for_trigger_callback', 'playing_wake_up_answer')
  68. self.machine.add_transition('wait_for_order', 'playing_wake_up_answer', 'waiting_for_order_listener_callback')
  69. self.machine.add_transition('analyse_order', 'waiting_for_order_listener_callback', 'analysing_order')
  70. self.machine.add_ordered_transitions()
  71. # add method which are called when changing state
  72. self.machine.on_enter_starting_trigger('start_trigger_process')
  73. self.machine.on_enter_playing_ready_sound('play_ready_sound_process')
  74. self.machine.on_enter_waiting_for_trigger_callback('waiting_for_trigger_callback_thread')
  75. self.machine.on_enter_playing_wake_up_answer('play_wake_up_answer_thread')
  76. self.machine.on_enter_stopping_trigger('stop_trigger_process')
  77. self.machine.on_enter_start_order_listener('start_order_listener_thread')
  78. self.machine.on_enter_waiting_for_order_listener_callback('waiting_for_order_listener_callback_thread')
  79. self.machine.on_enter_analysing_order('analysing_order_thread')
  80. self.start_trigger()
  81. def start_trigger_process(self):
  82. """
  83. This function will start the trigger thread that listen for the hotword
  84. """
  85. logger.debug("[MainController] Entering state: %s" % self.state)
  86. self.trigger_instance = self._get_default_trigger()
  87. self.trigger_callback_called = False
  88. self.trigger_instance.daemon = True
  89. # Wait that the kalliope trigger is pronounced by the user
  90. self.trigger_instance.start()
  91. self.next_state()
  92. def play_ready_sound_process(self):
  93. """
  94. Play a sound when Kalliope is ready to be awaken at the first start
  95. """
  96. logger.debug("[MainController] Entering state: %s" % self.state)
  97. if (not self.on_ready_notification_played_once and self.settings.play_on_ready_notification == "once") or \
  98. self.settings.play_on_ready_notification == "always":
  99. # we remember that we played the notification one time
  100. self.on_ready_notification_played_once = True
  101. # here we tell the user that we are listening
  102. if self.settings.on_ready_answers is not None:
  103. Say(message=self.settings.on_ready_answers)
  104. elif self.settings.on_ready_sounds is not None:
  105. random_sound_to_play = self._get_random_sound(self.settings.on_ready_sounds)
  106. Mplayer.play(random_sound_to_play)
  107. self.next_state()
  108. def waiting_for_trigger_callback_thread(self):
  109. """
  110. Method to print in debug that the main process is waiting for a trigger detection
  111. """
  112. logger.debug("[MainController] Entering state: %s" % self.state)
  113. Utils.print_info("Waiting for trigger detection")
  114. # this loop is used to keep the main thread alive
  115. while not self.trigger_callback_called:
  116. sleep(0.1)
  117. self.next_state()
  118. def waiting_for_order_listener_callback_thread(self):
  119. """
  120. Method to print in debug that the main process is waiting for an order to analyse
  121. """
  122. logger.debug("[MainController] Entering state: %s" % self.state)
  123. # this loop is used to keep the main thread alive
  124. while not self.order_listener_callback_called:
  125. sleep(0.1)
  126. if self.settings.rpi_settings:
  127. if self.settings.rpi_settings.pin_led_listening:
  128. RpiUtils.switch_pin_to_off(self.settings.rpi_settings.pin_led_listening)
  129. self.next_state()
  130. def trigger_callback(self):
  131. """
  132. we have detected the hotword, we can now pause the Trigger for a while
  133. The user can speak out loud his order during this time.
  134. """
  135. logger.debug("[MainController] Trigger callback called, switching to the next state")
  136. self.trigger_callback_called = True
  137. def stop_trigger_process(self):
  138. """
  139. The trigger has been awaken, we don't needed it anymore
  140. :return:
  141. """
  142. logger.debug("[MainController] Entering state: %s" % self.state)
  143. self.trigger_instance.stop()
  144. self.next_state()
  145. def start_order_listener_thread(self):
  146. """
  147. Start the STT engine thread
  148. """
  149. logger.debug("[MainController] Entering state: %s" % self.state)
  150. # start listening for an order
  151. self.order_listener_callback_called = False
  152. self.order_listener = OrderListener(callback=self.order_listener_callback)
  153. self.order_listener.daemon = True
  154. self.order_listener.start()
  155. self.next_state()
  156. def play_wake_up_answer_thread(self):
  157. """
  158. Play a sound or make Kalliope say something to notify the user that she has been awaken and now
  159. waiting for order
  160. """
  161. logger.debug("[MainController] Entering state: %s" % self.state)
  162. # if random wake answer sentence are present, we play this
  163. if self.settings.random_wake_up_answers is not None:
  164. Say(message=self.settings.random_wake_up_answers)
  165. else:
  166. random_sound_to_play = self._get_random_sound(self.settings.random_wake_up_sounds)
  167. Mplayer.play(random_sound_to_play)
  168. self.next_state()
  169. def order_listener_callback(self, order):
  170. """
  171. Receive an order, try to retrieve it in the brain.yml to launch to attached plugins
  172. :param order: the sentence received
  173. :type order: str
  174. """
  175. logger.debug("[MainController] Order listener callback called. Order to process: %s" % order)
  176. self.order_to_process = order
  177. self.order_listener_callback_called = True
  178. def analysing_order_thread(self):
  179. """
  180. Start the order analyser with the caught order to process
  181. """
  182. logger.debug("[MainController] order in analysing_order_thread %s" % self.order_to_process)
  183. SynapseLauncher.run_matching_synapse_from_order(self.order_to_process,
  184. self.brain,
  185. self.settings,
  186. is_api_call=False)
  187. # return to the state "unpausing_trigger"
  188. self.start_trigger()
  189. def _get_default_trigger(self):
  190. """
  191. Return an instance of the default trigger
  192. :return: Trigger
  193. """
  194. for trigger in self.settings.triggers:
  195. if trigger.name == self.settings.default_trigger_name:
  196. return TriggerLauncher.get_trigger(trigger, callback=self.trigger_callback)
  197. @staticmethod
  198. def _get_random_sound(random_wake_up_sounds):
  199. """
  200. Return a path of a sound to play
  201. If the path is absolute, test if file exist
  202. If the path is relative, we check if the file exist in the sound folder
  203. :param random_wake_up_sounds: List of wake_up sounds
  204. :return: path of a sound to play
  205. """
  206. # take first randomly a path
  207. random_path = random.choice(random_wake_up_sounds)
  208. logger.debug("[MainController] Selected sound: %s" % random_path)
  209. return Utils.get_real_file_path(random_path)
  210. def _start_rest_api(self):
  211. """
  212. Start the Rest API if asked in the user settings
  213. """
  214. # run the api if the user want it
  215. if self.settings.rest_api.active:
  216. Utils.print_info("Starting REST API Listening port: %s" % self.settings.rest_api.port)
  217. app = Flask(__name__)
  218. flask_api = FlaskAPI(app=app,
  219. port=self.settings.rest_api.port,
  220. brain=self.brain,
  221. allowed_cors_origin=self.settings.rest_api.allowed_cors_origin)
  222. flask_api.daemon = True
  223. flask_api.start()
  224. def muted_button_pressed(self, muted=False):
  225. logger.debug("[MainController] Mute button pressed. Switch trigger process to muted: %s" % muted)
  226. if muted:
  227. self.trigger_instance.pause()
  228. Utils.print_info("Kalliope now muted")
  229. else:
  230. self.trigger_instance.unpause()
  231. Utils.print_info("Kalliope now listening for trigger detection")