base.py
Go to the documentation of this file.
1 # Copyright 2017 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 from collections import namedtuple
16 from threading import Lock
17 
18 from mycroft.configuration import Configuration
19 from mycroft.messagebus.client.ws import WebsocketClient
20 from mycroft.util import create_daemon
21 from mycroft.util.log import LOG
22 
23 import tornado.web
24 import json
25 from tornado import autoreload, ioloop
26 from tornado.websocket import WebSocketHandler
27 from mycroft.messagebus.message import Message
28 
29 
30 Namespace = namedtuple('Namespace', ['name', 'pages'])
31 write_lock = Lock()
32 namespace_lock = Lock()
33 
34 RESERVED_KEYS = ['__from', '__idle']
35 
36 
37 def _get_page_data(message):
38  """ Extract page related data from a message.
39 
40  Args:
41  message: messagebus message object
42  Returns:
43  tuple (page, namespace, index)
44  Raises:
45  ValueError if value is missing.
46  """
47  data = message.data
48  # Note: 'page' can be either a string or a list of strings
49  if 'page' not in data:
50  raise ValueError("Page missing in data")
51  if 'index' in data:
52  index = data['index']
53  else:
54  index = 0
55  page = data.get("page", "")
56  namespace = data.get("__from", "")
57  return page, namespace, index
58 
59 
60 class Enclosure:
61  def __init__(self):
62  # Establish Enclosure's websocket connection to the messagebus
64 
65  # Load full config
66  Configuration.init(self.bus)
67  config = Configuration.get()
68 
69  self.lang = config['lang']
70  self.config = config.get("enclosure")
71  self.global_config = config
72 
73  # This datastore holds the data associated with the GUI provider. Data
74  # is stored in Namespaces, so you can have:
75  # self.datastore["namespace"]["name"] = value
76  # Typically the namespace is a meaningless identifier, but there is a
77  # special "SYSTEM" namespace.
78  self.datastore = {}
79 
80  # self.loaded is a list, each element consists of a namespace named
81  # tuple.
82  # The namespace namedtuple has the properties "name" and "pages"
83  # The name contains the namespace name as a string and pages is a
84  # mutable list of loaded pages.
85  #
86  # [Namespace name, [List of loaded qml pages]]
87  # [
88  # ["SKILL_NAME", ["page1.qml, "page2.qml", ... , "pageN.qml"]
89  # [...]
90  # ]
91  self.loaded = [] # list of lists in order.
92  self.explicit_move = True # Set to true to send reorder commands
93 
94  # Listen for new GUI clients to announce themselves on the main bus
95  self.GUIs = {} # GUIs, either local or remote
97  self.bus.on("mycroft.gui.connected", self.on_gui_client_connected)
99 
100  # First send any data:
101  self.bus.on("gui.value.set", self.on_gui_set_value)
102  self.bus.on("gui.page.show", self.on_gui_show_page)
103  self.bus.on("gui.page.delete", self.on_gui_delete_page)
104  self.bus.on("gui.clear.namespace", self.on_gui_delete_namespace)
105  self.bus.on("gui.event.send", self.on_gui_send_event)
106 
107  def run(self):
108  try:
109  self.bus.run_forever()
110  except Exception as e:
111  LOG.error("Error: {0}".format(e))
112  self.stop()
113 
114  ######################################################################
115  # GUI client API
116 
117  def send(self, *args, **kwargs):
118  """ Send to all registered GUIs. """
119  for gui in self.GUIs.values():
120  if gui.socket:
121  gui.socket.send(*args, **kwargs)
122  else:
123  LOG.error('GUI connection {} has no socket!'.format(gui))
124 
125  def on_gui_send_event(self, message):
126  """ Send an event to the GUIs. """
127  try:
128  data = {'type': 'mycroft.events.triggered',
129  'namespace': message.data.get('__from'),
130  'event_name': message.data.get('event_name'),
131  'params': message.data.get('params')}
132  self.send(data)
133  except Exception as e:
134  LOG.error('Could not send event ({})'.format(repr(e)))
135 
136  def on_gui_set_value(self, message):
137  data = message.data
138  namespace = data.get("__from", "")
139 
140  # Pass these values on to the GUI renderers
141  for key in data:
142  if key not in RESERVED_KEYS:
143  try:
144  self.set(namespace, key, data[key])
145  except Exception as e:
146  LOG.exception(repr(e))
147 
148  def set(self, namespace, name, value):
149  """ Perform the send of the values to the connected GUIs. """
150  if namespace not in self.datastore:
151  self.datastore[namespace] = {}
152  if self.datastore[namespace].get(name) != value:
153  self.datastore[namespace][name] = value
154 
155  # If the namespace is loaded send data to GUI
156  if namespace in [l.name for l in self.loaded]:
157  msg = {"type": "mycroft.session.set",
158  "namespace": namespace,
159  "data": {name: value}}
160  self.send(msg)
161 
162  def on_gui_delete_page(self, message):
163  """ Bus handler for removing pages. """
164  page, namespace, _ = _get_page_data(message)
165  try:
166  with namespace_lock:
167  self.remove_pages(namespace, page)
168  except Exception as e:
169  LOG.exception(repr(e))
170 
171  def on_gui_delete_namespace(self, message):
172  """ Bus handler for removing namespace. """
173  try:
174  namespace = message.data['__from']
175  with namespace_lock:
176  self.remove_namespace(namespace)
177  except Exception as e:
178  LOG.exception(repr(e))
179 
180  def on_gui_show_page(self, message):
181  try:
182  page, namespace, index = _get_page_data(message)
183  # Pass the request to the GUI(s) to pull up a page template
184  with namespace_lock:
185  self.show(namespace, page, index)
186  except Exception as e:
187  LOG.exception(repr(e))
188 
189  def __find_namespace(self, namespace):
190  for i, skill in enumerate(self.loaded):
191  if skill[0] == namespace:
192  return i
193  return None
194 
195  def __insert_pages(self, namespace, pages):
196  """ Insert pages into the namespace
197 
198  Args:
199  namespace (str): Namespace to add to
200  pages (list): Pages (str) to insert
201  """
202  LOG.debug("Inserting new pages")
203  if not isinstance(pages, list):
204  raise ValueError('Argument must be list of pages')
205 
206  self.send({"type": "mycroft.gui.list.insert",
207  "namespace": namespace,
208  "position": len(self.loaded[0].pages),
209  "data": [{"url": p} for p in pages]
210  })
211  # Insert the pages into local reprensentation as well.
212  updated = Namespace(self.loaded[0].name, self.loaded[0].pages + pages)
213  self.loaded[0] = updated
214 
215  def __remove_page(self, namespace, pos):
216  """ Delete page.
217 
218  Args:
219  namespace (str): Namespace to remove from
220  pos (int): Page position to remove
221  """
222  LOG.debug("Deleting {} from {}".format(pos, namespace))
223  self.send({"type": "mycroft.gui.list.remove",
224  "namespace": namespace,
225  "position": pos,
226  "items_number": 1
227  })
228  # Remove the page from the local reprensentation as well.
229  self.loaded[0].pages.pop(pos)
230 
231  def __insert_new_namespace(self, namespace, pages):
232  """ Insert new namespace and pages.
233 
234  This first sends a message adding a new namespace at the
235  highest priority (position 0 in the namespace stack)
236 
237  Args:
238  namespace (str): The skill namespace to create
239  pages (str): Pages to insert (name matches QML)
240  """
241  LOG.debug("Inserting new namespace")
242  self.send({"type": "mycroft.session.list.insert",
243  "namespace": "mycroft.system.active_skills",
244  "position": 0,
245  "data": [{"skill_id": namespace}]
246  })
247 
248  # Load any already stored Data
249  data = self.datastore.get(namespace, {})
250  for key in data:
251  msg = {"type": "mycroft.session.set",
252  "namespace": namespace,
253  "data": {key: data[key]}}
254  self.send(msg)
255 
256  LOG.debug("Inserting new page")
257  self.send({"type": "mycroft.gui.list.insert",
258  "namespace": namespace,
259  "position": 0,
260  "data": [{"url": p} for p in pages]
261  })
262  # Make sure the local copy is updated
263  self.loaded.insert(0, Namespace(namespace, pages))
264 
265  def __move_namespace(self, from_pos, to_pos):
266  """ Move an existing namespace to a new position in the stack.
267 
268  Args:
269  from_pos (int): Position in the stack to move from
270  to_pos (int): Position to move to
271  """
272  LOG.debug("Activating existing namespace")
273  # Seems like the namespace is moved to the top automatically when
274  # a page change is done. Deactivating this for now.
275  if self.explicit_move:
276  LOG.debug("move {} to {}".format(from_pos, to_pos))
277  self.send({"type": "mycroft.session.list.move",
278  "namespace": "mycroft.system.active_skills",
279  "from": from_pos, "to": to_pos,
280  "items_number": 1})
281  # Move the local representation of the skill from current
282  # position to position 0.
283  self.loaded.insert(to_pos, self.loaded.pop(from_pos))
284 
285  def __switch_page(self, namespace, pages):
286  """ Switch page to an already loaded page.
287 
288  Args:
289  pages (list): pages (str) to switch to
290  namespace (str): skill namespace
291  """
292  try:
293  num = self.loaded[0].pages.index(pages[0])
294  except Exception as e:
295  LOG.exception(repr(e))
296  num = 0
297 
298  LOG.debug('Switching to already loaded page at '
299  'index {} in namespace {}'.format(num, namespace))
300  self.send({"type": "mycroft.events.triggered",
301  "namespace": namespace,
302  "event_name": "page_gained_focus",
303  "data": {"number": num}})
304 
305  def show(self, namespace, page, index):
306  """ Show a page and load it as needed.
307 
308  Args:
309  page (str or list): page(s) to show
310  namespace (str): skill namespace
311  index (int): ??? TODO: Unused in code ???
312 
313  TODO: - Update sync to match.
314  - Separate into multiple functions/methods
315  """
316 
317  LOG.debug("GUIConnection activating: " + namespace)
318  pages = page if isinstance(page, list) else [page]
319 
320  # find namespace among loaded namespaces
321  try:
322  index = self.__find_namespace(namespace)
323  if index is None:
324  # This namespace doesn't exist, insert them first so they're
325  # shown.
326  self.__insert_new_namespace(namespace, pages)
327  return
328  else: # Namespace exists
329  if index > 0:
330  # Namespace is inactive, activate it by moving it to
331  # position 0
332  self.__move_namespace(index, 0)
333 
334  # Find if any new pages needs to be inserted
335  new_pages = [p for p in pages if p not in self.loaded[0].pages]
336  if new_pages:
337  self.__insert_pages(namespace, new_pages)
338  else:
339  # No new pages, just switch
340  self.__switch_page(namespace, pages)
341  except Exception as e:
342  LOG.exception(repr(e))
343 
344  def remove_namespace(self, namespace):
345  """ Remove namespace.
346 
347  Args:
348  namespace (str): namespace to remove
349  """
350  index = self.__find_namespace(namespace)
351  if index is None:
352  return
353  else:
354  LOG.debug("Removing namespace {} at {}".format(namespace, index))
355  self.send({"type": "mycroft.session.list.remove",
356  "namespace": "mycroft.system.active_skills",
357  "position": index,
358  "items_number": 1
359  })
360  # Remove namespace from loaded namespaces
361  self.loaded.pop(index)
362 
363  def remove_pages(self, namespace, pages):
364  """ Remove the listed pages from the provided namespace.
365 
366  Args:
367  namespace (str): The namespace to modify
368  pages (list): List of page names (str) to delete
369  """
370  try:
371  index = self.__find_namespace(namespace)
372  if index is None:
373  return
374  else:
375  # Remove any pages that doesn't exist in the namespace
376  pages = [p for p in pages if p in self.loaded[index].pages]
377  # Make sure to remove pages from the back
378  indexes = [self.loaded[index].pages.index(p) for p in pages]
379  indexes = sorted(indexes)
380  indexes.reverse()
381  for page_index in indexes:
382  self.__remove_page(namespace, page_index)
383  except Exception as e:
384  LOG.exception(repr(e))
385 
386  ######################################################################
387  # GUI client socket
388  #
389  # The basic mechanism is:
390  # 1) GUI client announces itself on the main messagebus
391  # 2) Mycroft prepares a port for a socket connection to this GUI
392  # 3) The port is announced over the messagebus
393  # 4) The GUI connects on the socket
394  # 5) Connection persists for graphical interaction indefinitely
395  #
396  # If the connection is lost, it must be renegotiated and restarted.
397  def on_gui_client_connected(self, message):
398  # GUI has announced presence
399  LOG.debug("on_gui_client_connected")
400  gui_id = message.data.get("gui_id")
401 
402  # Spin up a new communication socket for this GUI
403  if gui_id in self.GUIs:
404  # TODO: Close it?
405  pass
406  self.GUIs[gui_id] = GUIConnection(gui_id, self.global_config,
407  self.callback_disconnect, self)
408  LOG.debug("Heard announcement from gui_id: {}".format(gui_id))
409 
410  # Announce connection, the GUI should connect on it soon
411  self.bus.emit(Message("mycroft.gui.port",
412  {"port": self.GUIs[gui_id].port,
413  "gui_id": gui_id}))
414 
415  def callback_disconnect(self, gui_id):
416  LOG.info("Disconnecting!")
417  # TODO: Whatever is needed to kill the websocket instance
418  LOG.info(self.GUIs.keys())
419  LOG.info('deleting: {}'.format(gui_id))
420  if gui_id in self.GUIs:
421  del self.GUIs[gui_id]
422  else:
423  LOG.warning('ID doesn\'t exist')
424 
426  # TODO: Register handlers for standard (Mark 1) events
427  # self.bus.on('enclosure.eyes.on', self.on)
428  # self.bus.on('enclosure.eyes.off', self.off)
429  # self.bus.on('enclosure.eyes.blink', self.blink)
430  # self.bus.on('enclosure.eyes.narrow', self.narrow)
431  # self.bus.on('enclosure.eyes.look', self.look)
432  # self.bus.on('enclosure.eyes.color', self.color)
433  # self.bus.on('enclosure.eyes.level', self.brightness)
434  # self.bus.on('enclosure.eyes.volume', self.volume)
435  # self.bus.on('enclosure.eyes.spin', self.spin)
436  # self.bus.on('enclosure.eyes.timedspin', self.timed_spin)
437  # self.bus.on('enclosure.eyes.reset', self.reset)
438  # self.bus.on('enclosure.eyes.setpixel', self.set_pixel)
439  # self.bus.on('enclosure.eyes.fill', self.fill)
440 
441  # self.bus.on('enclosure.mouth.reset', self.reset)
442  # self.bus.on('enclosure.mouth.talk', self.talk)
443  # self.bus.on('enclosure.mouth.think', self.think)
444  # self.bus.on('enclosure.mouth.listen', self.listen)
445  # self.bus.on('enclosure.mouth.smile', self.smile)
446  # self.bus.on('enclosure.mouth.viseme', self.viseme)
447  # self.bus.on('enclosure.mouth.text', self.text)
448  # self.bus.on('enclosure.mouth.display', self.display)
449  # self.bus.on('enclosure.mouth.display_image', self.display_image)
450  # self.bus.on('enclosure.weather.display', self.display_weather)
451 
452  # self.bus.on('recognizer_loop:record_begin', self.mouth.listen)
453  # self.bus.on('recognizer_loop:record_end', self.mouth.reset)
454  # self.bus.on('recognizer_loop:audio_output_start', self.mouth.talk)
455  # self.bus.on('recognizer_loop:audio_output_end', self.mouth.reset)
456  pass
457 
458 
459 ##########################################################################
460 # GUIConnection
461 ##########################################################################
462 
463 gui_app_settings = {
464  'debug': True
465 }
466 
467 
469  """ A single GUIConnection exists per graphic interface. This object
470  maintains the socket used for communication and keeps the state of the
471  Mycroft data in sync with the GUIs data.
472 
473  Serves as a communication interface between Qt/QML frontend and Mycroft
474  Core. This is bidirectional, e.g. "show me this visual" to the frontend as
475  well as "the user just tapped this button" from the frontend.
476 
477  For the rough protocol, see:
478  https://cgit.kde.org/scratch/mart/mycroft-gui.git/tree/transportProtocol.txt?h=newapi # nopep8
479 
480  TODO: Implement variable deletion
481  TODO: Implement 'models' support
482  TODO: Implement events
483  TODO: Implement data coming back from Qt to Mycroft
484  """
485 
486  _last_idx = 0 # this is incremented by 1 for each open GUIConnection
487  server_thread = None
488 
489  def __init__(self, id, config, callback_disconnect, enclosure):
490  LOG.debug("Creating GUIConnection")
491  self.id = id
492  self.socket = None
493  self.callback_disconnect = callback_disconnect
494  self.enclosure = enclosure
495 
496  # Each connection will run its own Tornado server. If the
497  # connection drops, the server is killed.
498  websocket_config = config.get("gui_websocket")
499  host = websocket_config.get("host")
500  route = websocket_config.get("route")
501  base_port = websocket_config.get("base_port")
502 
503  while True:
504  self.port = base_port + GUIConnection._last_idx
505  GUIConnection._last_idx += 1
506 
507  try:
508  self.webapp = tornado.web.Application(
509  [(route, GUIWebsocketHandler)], **gui_app_settings
510  )
511  # Hacky way to associate socket with this object:
512  self.webapp.gui = self
513  self.webapp.listen(self.port, host)
514  except Exception as e:
515  LOG.debug('Error: {}'.format(repr(e)))
516  continue
517  break
518  # Can't run two IOLoop's in the same process
519  if not GUIConnection.server_thread:
520  GUIConnection.server_thread = create_daemon(
521  ioloop.IOLoop.instance().start)
522  LOG.debug('IOLoop started @ '
523  'ws://{}:{}{}'.format(host, self.port, route))
524 
525  def on_connection_opened(self, socket_handler):
526  LOG.debug("on_connection_opened")
527  self.socket = socket_handler
528  self.synchronize()
529 
530  def synchronize(self):
531  """ Upload namespaces, pages and data. """
532  namespace_pos = 0
533  for namespace, pages in self.enclosure.loaded:
534  # Insert namespace
535  self.socket.send({"type": "mycroft.session.list.insert",
536  "namespace": "mycroft.system.active_skills",
537  "position": namespace_pos,
538  "data": [{"skill_id": namespace}]
539  })
540  # Insert pages
541  self.socket.send({"type": "mycroft.gui.list.insert",
542  "namespace": namespace,
543  "position": 0,
544  "data": [{"url": p} for p in pages]
545  })
546  # Insert data
547  data = self.enclosure.datastore.get(namespace, {})
548  for key in data:
549  self.socket.send({"type": "mycroft.session.set",
550  "namespace": namespace,
551  "data": {key: data[key]}
552  })
553 
554  namespace_pos += 1
555 
556  def on_connection_closed(self, socket):
557  # Self-destruct (can't reconnect on the same port)
558  LOG.debug("on_connection_closed")
559  if self.socket:
560  LOG.debug("Server stopped: {}".format(self.socket))
561  # TODO: How to stop the webapp for this socket?
562  # self.socket.stop()
563  self.socket = None
564  self.callback_disconnect(self.id)
565 
566 
567 class GUIWebsocketHandler(WebSocketHandler):
568  """
569  The socket pipeline between Qt and Mycroft
570  """
571 
572  def open(self):
573  self.application.gui.on_connection_opened(self)
574 
575  def on_message(self, message):
576  LOG.debug("Received: {}".format(message))
577  msg = json.loads(message)
578  if (msg.get('type') == "mycroft.events.triggered" and
579  (msg.get('event_name') == 'page_gained_focus' or
580  msg.get('event_name') == 'system.gui.user.interaction')):
581  # System event, a page was changed
582  msg_type = 'gui.page_interaction'
583  msg_data = {
584  'namespace': msg['namespace'],
585  'page_number': msg['parameters'].get('number')
586  }
587  elif msg.get('type') == "mycroft.events.triggered":
588  # A normal event was triggered
589  msg_type = '{}.{}'.format(msg['namespace'], msg['event_name'])
590  msg_data = msg['parameters']
591 
592  elif msg.get('type') == 'mycroft.session.set':
593  # A value was changed send it back to the skill
594  msg_type = '{}.{}'.format(msg['namespace'], 'set')
595  msg_data = msg['data']
596 
597  message = Message(msg_type, msg_data)
598  self.application.gui.enclosure.bus.emit(message)
599 
600  def write_message(self, *arg, **kwarg):
601  """ Wraps WebSocketHandler.write_message() with a lock. """
602  with write_lock:
603  super().write_message(*arg, **kwarg)
604 
605  def send_message(self, message):
606  if isinstance(message, Message):
607  self.write_message(message.serialize())
608  else:
609  LOG.info('message: {}'.format(message))
610  self.write_message(str(message))
611 
612  def send(self, data):
613  """Send the given data across the socket as JSON
614 
615  Args:
616  data (dict): Data to transmit
617  """
618  s = json.dumps(data)
619  self.write_message(s)
620 
621  def on_close(self):
622  self.application.gui.on_connection_closed(self)
def create_daemon(target, args=(), kwargs=None)
def on_gui_set_value(self, message)
Definition: base.py:136
def __insert_new_namespace(self, namespace, pages)
Definition: base.py:231
def show(self, namespace, page, index)
Definition: base.py:305
def __move_namespace(self, from_pos, to_pos)
Definition: base.py:265
def on_connection_opened(self, socket_handler)
Definition: base.py:525
def __insert_pages(self, namespace, pages)
Definition: base.py:195
def __switch_page(self, namespace, pages)
Definition: base.py:285
def _get_page_data(message)
Definition: base.py:37
def on_connection_closed(self, socket)
Definition: base.py:556
def send(self, args, kwargs)
GUI client API.
Definition: base.py:117
def on_gui_client_connected(self, message)
GUI client socket.
Definition: base.py:397
def __remove_page(self, namespace, pos)
Definition: base.py:215
def callback_disconnect(self, gui_id)
Definition: base.py:415
def __init__(self, id, config, callback_disconnect, enclosure)
Definition: base.py:489
def on_gui_show_page(self, message)
Definition: base.py:180
def remove_pages(self, namespace, pages)
Definition: base.py:363
def remove_namespace(self, namespace)
Definition: base.py:344
def on_gui_delete_namespace(self, message)
Definition: base.py:171
def on_gui_delete_page(self, message)
Definition: base.py:162
def on_gui_send_event(self, message)
Definition: base.py:125
def get(phrase, lang=None, context=None)
def __find_namespace(self, namespace)
Definition: base.py:189
def set(self, namespace, name, value)
Definition: base.py:148


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40