15 from collections
import namedtuple
16 from threading
import Lock
25 from tornado
import autoreload, ioloop
26 from tornado.websocket
import WebSocketHandler
30 Namespace = namedtuple(
'Namespace', [
'name',
'pages'])
32 namespace_lock = Lock()
34 RESERVED_KEYS = [
'__from',
'__idle']
38 """ Extract page related data from a message. 41 message: messagebus message object 43 tuple (page, namespace, index) 45 ValueError if value is missing. 49 if 'page' not in data:
50 raise ValueError(
"Page missing in data")
55 page = data.get(
"page",
"")
56 namespace = data.get(
"__from",
"")
57 return page, namespace, index
66 Configuration.init(self.
bus)
67 config = Configuration.get()
70 self.
config = config.get(
"enclosure")
109 self.bus.run_forever()
110 except Exception
as e:
111 LOG.error(
"Error: {0}".format(e))
117 def send(self, *args, **kwargs):
118 """ Send to all registered GUIs. """ 119 for gui
in self.GUIs.values():
121 gui.socket.send(*args, **kwargs)
123 LOG.error(
'GUI connection {} has no socket!'.format(gui))
126 """ Send an event to the GUIs. """ 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')}
133 except Exception
as e:
134 LOG.error(
'Could not send event ({})'.format(repr(e)))
138 namespace = data.get(
"__from",
"")
142 if key
not in RESERVED_KEYS:
144 self.
set(namespace, key, data[key])
145 except Exception
as e:
146 LOG.exception(repr(e))
148 def set(self, namespace, name, value):
149 """ Perform the send of the values to the connected GUIs. """ 156 if namespace
in [l.name
for l
in self.
loaded]:
157 msg = {
"type":
"mycroft.session.set",
158 "namespace": namespace,
159 "data": {name: value}}
163 """ Bus handler for removing pages. """ 168 except Exception
as e:
169 LOG.exception(repr(e))
172 """ Bus handler for removing namespace. """ 174 namespace = message.data[
'__from']
177 except Exception
as e:
178 LOG.exception(repr(e))
185 self.
show(namespace, page, index)
186 except Exception
as e:
187 LOG.exception(repr(e))
190 for i, skill
in enumerate(self.
loaded):
191 if skill[0] == namespace:
196 """ Insert pages into the namespace 199 namespace (str): Namespace to add to 200 pages (list): Pages (str) to insert 202 LOG.debug(
"Inserting new pages")
203 if not isinstance(pages, list):
204 raise ValueError(
'Argument must be list of pages')
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]
219 namespace (str): Namespace to remove from 220 pos (int): Page position to remove 222 LOG.debug(
"Deleting {} from {}".format(pos, namespace))
223 self.
send({
"type":
"mycroft.gui.list.remove",
224 "namespace": namespace,
229 self.
loaded[0].pages.pop(pos)
232 """ Insert new namespace and pages. 234 This first sends a message adding a new namespace at the 235 highest priority (position 0 in the namespace stack) 238 namespace (str): The skill namespace to create 239 pages (str): Pages to insert (name matches QML) 241 LOG.debug(
"Inserting new namespace")
242 self.
send({
"type":
"mycroft.session.list.insert",
243 "namespace":
"mycroft.system.active_skills",
245 "data": [{
"skill_id": namespace}]
249 data = self.datastore.get(namespace, {})
251 msg = {
"type":
"mycroft.session.set",
252 "namespace": namespace,
253 "data": {key: data[key]}}
256 LOG.debug(
"Inserting new page")
257 self.
send({
"type":
"mycroft.gui.list.insert",
258 "namespace": namespace,
260 "data": [{
"url": p}
for p
in pages]
263 self.loaded.insert(0,
Namespace(namespace, pages))
266 """ Move an existing namespace to a new position in the stack. 269 from_pos (int): Position in the stack to move from 270 to_pos (int): Position to move to 272 LOG.debug(
"Activating existing namespace")
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,
283 self.loaded.insert(to_pos, self.loaded.pop(from_pos))
286 """ Switch page to an already loaded page. 289 pages (list): pages (str) to switch to 290 namespace (str): skill namespace 293 num = self.
loaded[0].pages.index(pages[0])
294 except Exception
as e:
295 LOG.exception(repr(e))
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}})
305 def show(self, namespace, page, index):
306 """ Show a page and load it as needed. 309 page (str or list): page(s) to show 310 namespace (str): skill namespace 311 index (int): ??? TODO: Unused in code ??? 313 TODO: - Update sync to match. 314 - Separate into multiple functions/methods 317 LOG.debug(
"GUIConnection activating: " + namespace)
318 pages = page
if isinstance(page, list)
else [page]
335 new_pages = [p
for p
in pages
if p
not in self.
loaded[0].pages]
341 except Exception
as e:
342 LOG.exception(repr(e))
345 """ Remove namespace. 348 namespace (str): namespace to remove 354 LOG.debug(
"Removing namespace {} at {}".format(namespace, index))
355 self.
send({
"type":
"mycroft.session.list.remove",
356 "namespace":
"mycroft.system.active_skills",
361 self.loaded.pop(index)
364 """ Remove the listed pages from the provided namespace. 367 namespace (str): The namespace to modify 368 pages (list): List of page names (str) to delete 376 pages = [p
for p
in pages
if p
in self.
loaded[index].pages]
378 indexes = [self.
loaded[index].pages.index(p)
for p
in pages]
379 indexes = sorted(indexes)
381 for page_index
in indexes:
383 except Exception
as e:
384 LOG.exception(repr(e))
399 LOG.debug(
"on_gui_client_connected")
400 gui_id = message.data.get(
"gui_id")
403 if gui_id
in self.
GUIs:
408 LOG.debug(
"Heard announcement from gui_id: {}".format(gui_id))
411 self.bus.emit(
Message(
"mycroft.gui.port",
412 {
"port": self.
GUIs[gui_id].port,
416 LOG.info(
"Disconnecting!")
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]
423 LOG.warning(
'ID doesn\'t exist')
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. 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. 477 For the rough protocol, see: 478 https://cgit.kde.org/scratch/mart/mycroft-gui.git/tree/transportProtocol.txt?h=newapi # nopep8 480 TODO: Implement variable deletion 481 TODO: Implement 'models' support 482 TODO: Implement events 483 TODO: Implement data coming back from Qt to Mycroft 489 def __init__(self, id, config, callback_disconnect, enclosure):
490 LOG.debug(
"Creating GUIConnection")
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")
504 self.
port = base_port + GUIConnection._last_idx
505 GUIConnection._last_idx += 1
509 [(route, GUIWebsocketHandler)], **gui_app_settings
512 self.webapp.gui = self
513 self.webapp.listen(self.
port, host)
514 except Exception
as e:
515 LOG.debug(
'Error: {}'.format(repr(e)))
519 if not GUIConnection.server_thread:
521 ioloop.IOLoop.instance().start)
522 LOG.debug(
'IOLoop started @ ' 523 'ws://{}:{}{}'.format(host, self.
port, route))
526 LOG.debug(
"on_connection_opened")
527 self.
socket = socket_handler
531 """ Upload namespaces, pages and data. """ 533 for namespace, pages
in self.enclosure.loaded:
535 self.socket.send({
"type":
"mycroft.session.list.insert",
536 "namespace":
"mycroft.system.active_skills",
537 "position": namespace_pos,
538 "data": [{
"skill_id": namespace}]
541 self.socket.send({
"type":
"mycroft.gui.list.insert",
542 "namespace": namespace,
544 "data": [{
"url": p}
for p
in pages]
547 data = self.enclosure.datastore.get(namespace, {})
549 self.socket.send({
"type":
"mycroft.session.set",
550 "namespace": namespace,
551 "data": {key: data[key]}
558 LOG.debug(
"on_connection_closed")
560 LOG.debug(
"Server stopped: {}".format(self.
socket))
569 The socket pipeline between Qt and Mycroft 573 self.application.gui.on_connection_opened(self)
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')):
582 msg_type =
'gui.page_interaction' 584 'namespace': msg[
'namespace'],
585 'page_number': msg[
'parameters'].
get(
'number')
587 elif msg.get(
'type') ==
"mycroft.events.triggered":
589 msg_type =
'{}.{}'.format(msg[
'namespace'], msg[
'event_name'])
590 msg_data = msg[
'parameters']
592 elif msg.get(
'type') ==
'mycroft.session.set':
594 msg_type =
'{}.{}'.format(msg[
'namespace'],
'set')
595 msg_data = msg[
'data']
597 message =
Message(msg_type, msg_data)
598 self.application.gui.enclosure.bus.emit(message)
601 """ Wraps WebSocketHandler.write_message() with a lock. """ 606 if isinstance(message, Message):
609 LOG.info(
'message: {}'.format(message))
613 """Send the given data across the socket as JSON 616 data (dict): Data to transmit 622 self.application.gui.on_connection_closed(self)
def create_daemon(target, args=(), kwargs=None)
def on_gui_set_value(self, message)
def __insert_new_namespace(self, namespace, pages)
def show(self, namespace, page, index)
def __move_namespace(self, from_pos, to_pos)
def on_connection_opened(self, socket_handler)
def __insert_pages(self, namespace, pages)
def __switch_page(self, namespace, pages)
def _get_page_data(message)
def on_connection_closed(self, socket)
def send(self, args, kwargs)
GUI client API.
def on_gui_client_connected(self, message)
GUI client socket.
def send_message(self, message)
def on_message(self, message)
def __remove_page(self, namespace, pos)
def write_message(self, arg, kwarg)
def callback_disconnect(self, gui_id)
def __init__(self, id, config, callback_disconnect, enclosure)
def register_gui_handlers(self)
def on_gui_show_page(self, message)
def remove_pages(self, namespace, pages)
def remove_namespace(self, namespace)
def on_gui_delete_namespace(self, message)
def on_gui_delete_page(self, message)
def on_gui_send_event(self, message)
def get(phrase, lang=None, context=None)
def __find_namespace(self, namespace)
def set(self, namespace, name, value)