MainController.py 11 KB

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