GUI update number 6 (#1964)
* Add communication from GUI to skills - "set" events from Qt will set/update a variable in the skills .gui member - It's possible to add general event handlers using self.gui.register_handler() - Moved registration of skill_id to just after skill init * Ensure that simultaneously writes doesn't occur Wrap WebSocketHandler.write_message() with a lock in an attempt to handle "buffererror: existing exports of data: object cannot be re-sized." * Add better logging to help debug disconnect issue * Allow overriding the idle page SkillGUI.show_page() and SkillGUI.show_pages() now takes an optional override_idle parameter. This is used as a hint by the mark-2 skill and if possible the idle screen will not be shown. * Improve debugging using Logger * Raise exception when sending a non-existing gui page * Restore running state to new connections When a GUI is connected data and running namespaces are synchronised and shown. This refactors the code quite a bit moving the GUI state from the GUIConnection object to the Enclosure. The GUIConnection object does the handles the sync in the on_connection_open() method. * Add gui.page_interaction message Currently triggered on page change on the display. * Handle message when gui changes sessionData * Check if socket exists on gui before sending data * Increase port on each failure and retrypull/1970/head
parent
1b0ff61609
commit
9ef95506d0
|
@ -13,6 +13,7 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
from collections import namedtuple
|
||||
from threading import Lock
|
||||
|
||||
from mycroft.configuration import Configuration
|
||||
from mycroft.messagebus.client.ws import WebsocketClient
|
||||
|
@ -27,15 +28,13 @@ from mycroft.messagebus.message import Message
|
|||
|
||||
|
||||
Namespace = namedtuple('Namespace', ['name', 'pages'])
|
||||
write_lock = Lock()
|
||||
|
||||
|
||||
def DEBUG(str):
|
||||
print(str)
|
||||
# pass # disable by default
|
||||
RESERVED_KEYS = ['__from', '__idle']
|
||||
|
||||
|
||||
class Enclosure:
|
||||
|
||||
def __init__(self):
|
||||
# Establish Enclosure's websocket connection to the messagebus
|
||||
self.bus = WebsocketClient()
|
||||
|
@ -48,6 +47,27 @@ class Enclosure:
|
|||
self.config = config.get("enclosure")
|
||||
self.global_config = config
|
||||
|
||||
# This datastore holds the data associated with the GUI provider. Data
|
||||
# is stored in Namespaces, so you can have:
|
||||
# self.datastore["namespace"]["name"] = value
|
||||
# Typically the namespace is a meaningless identifier, but there is a
|
||||
# special "SYSTEM" namespace.
|
||||
self.datastore = {}
|
||||
|
||||
# self.loaded is a list, each element consists of a namespace named
|
||||
# tuple.
|
||||
# The namespace namedtuple has the properties "name" and "pages"
|
||||
# The name contains the namespace name as a string and pages is a
|
||||
# mutable list of loaded pages.
|
||||
#
|
||||
# [Namespace name, [List of loaded qml pages]]
|
||||
# [
|
||||
# ["SKILL_NAME", ["page1.qml, "page2.qml", ... , "pageN.qml"]
|
||||
# [...]
|
||||
# ]
|
||||
self.loaded = [] # list of lists in order.
|
||||
self.explicit_move = True # Set to true to send reorder commands
|
||||
|
||||
# Listen for new GUI clients to announce themselves on the main bus
|
||||
self.GUIs = {} # GUIs, either local or remote
|
||||
self.active_namespaces = []
|
||||
|
@ -68,35 +88,42 @@ class Enclosure:
|
|||
######################################################################
|
||||
# GUI client API
|
||||
|
||||
def _gui_activate(self, namespace, move_to_top=False):
|
||||
if not namespace:
|
||||
return
|
||||
|
||||
if namespace not in self.active_namespaces:
|
||||
if move_to_top:
|
||||
self.active_namespaces.insert(0, namespace)
|
||||
def send(self, *args, **kwargs):
|
||||
""" Send to all registered GUIs. """
|
||||
for gui in self.GUIs.values():
|
||||
if gui.socket:
|
||||
gui.socket.send(*args, **kwargs)
|
||||
else:
|
||||
self.active_namespaces.append(namespace)
|
||||
elif move_to_top:
|
||||
self.active_namespaces.remove(namespace)
|
||||
self.active_namespaces.insert(0, namespace)
|
||||
# TODO: Keep a timestamp and auto-cull?
|
||||
LOG.error('GUI connection {} has no socket!'.format(gui))
|
||||
|
||||
def on_gui_set_value(self, message):
|
||||
data = message.data
|
||||
namespace = data.get("__from", "")
|
||||
|
||||
self._gui_activate(namespace)
|
||||
|
||||
# Pass these values on to the GUI renderers
|
||||
for id in self.GUIs:
|
||||
for key in data:
|
||||
if key != "__from":
|
||||
self.GUIs[id].set(namespace, key, data[key])
|
||||
for key in data:
|
||||
if key not in RESERVED_KEYS:
|
||||
try:
|
||||
self.set(namespace, key, data[key])
|
||||
except Exception as e:
|
||||
LOG.exception(repr(e))
|
||||
|
||||
def set(self, namespace, name, value):
|
||||
""" Perform the send of the values to the connected GUIs. """
|
||||
if namespace not in self.datastore:
|
||||
self.datastore[namespace] = {}
|
||||
if self.datastore[namespace].get(name) != value:
|
||||
self.datastore[namespace][name] = value
|
||||
|
||||
# If the namespace is loaded send data to gui
|
||||
if namespace in [l.name for l in self.loaded]:
|
||||
msg = {"type": "mycroft.session.set",
|
||||
"namespace": namespace,
|
||||
"data": {name: value}}
|
||||
self.send(msg)
|
||||
|
||||
def on_gui_show_page(self, message):
|
||||
data = message.data
|
||||
|
||||
# Note: 'page' can be either a string or a list of strings
|
||||
if 'page' not in data:
|
||||
return
|
||||
|
@ -104,12 +131,143 @@ class Enclosure:
|
|||
index = data['index']
|
||||
else:
|
||||
index = 0
|
||||
page = data.get("page", "")
|
||||
namespace = data.get("__from", "")
|
||||
self._gui_activate(namespace, move_to_top=True)
|
||||
|
||||
# Pass the request to the GUI(s) to pull up a page template
|
||||
for id in self.GUIs:
|
||||
self.GUIs[id].show(namespace, data['page'], index)
|
||||
try:
|
||||
self.show(namespace, page, index)
|
||||
except Exception as e:
|
||||
LOG.exception(repr(e))
|
||||
|
||||
def __find_namespace(self, namespace):
|
||||
for i, skill in enumerate(self.loaded):
|
||||
if skill[0] == namespace:
|
||||
return i
|
||||
return None
|
||||
|
||||
def __insert_pages(self, namespace, pages):
|
||||
""" Insert pages into the """
|
||||
LOG.debug("Inserting new pages")
|
||||
if not isinstance(pages, list):
|
||||
raise ValueError('Argument must be list of pages')
|
||||
|
||||
self.send({"type": "mycroft.gui.list.insert",
|
||||
"namespace": namespace,
|
||||
"position": len(self.loaded[0].pages),
|
||||
"data": [{"url": p} for p in pages]
|
||||
})
|
||||
|
||||
# append pages to local representation
|
||||
for p in pages:
|
||||
self.loaded[0].pages.append(p)
|
||||
|
||||
def __insert_new_namespace(self, namespace, pages):
|
||||
""" Insert new namespace and pages.
|
||||
|
||||
This first sends a message adding a new namespace at the
|
||||
highest priority (position 0 in the namespace stack)
|
||||
|
||||
Arguments:
|
||||
namespace: The skill namespace to create
|
||||
pages: Pages to insert
|
||||
"""
|
||||
LOG.debug("Inserting new namespace")
|
||||
self.send({"type": "mycroft.session.list.insert",
|
||||
"namespace": "mycroft.system.active_skills",
|
||||
"position": 0,
|
||||
"data": [{"skill_id": namespace}]
|
||||
})
|
||||
|
||||
# Load any already stored Data
|
||||
data = self.datastore.get(namespace, {})
|
||||
for key in data:
|
||||
msg = {"type": "mycroft.session.set",
|
||||
"namespace": namespace,
|
||||
"data": {key: data[key]}}
|
||||
self.send(msg)
|
||||
|
||||
LOG.debug("Inserting new page")
|
||||
self.send({"type": "mycroft.gui.list.insert",
|
||||
"namespace": namespace,
|
||||
"position": 0,
|
||||
"data": [{"url": p} for p in pages]
|
||||
})
|
||||
# Make sure the local copy is updated
|
||||
self.loaded.insert(0, Namespace(namespace, pages))
|
||||
|
||||
def __move_namespace(self, from_pos, to_pos):
|
||||
""" Move an existing namespace to a new position in the stack.
|
||||
|
||||
Arguments:
|
||||
from_pos: Position in the stack to move from
|
||||
to_pos: Position to move to
|
||||
"""
|
||||
LOG.debug("Activating existing namespace")
|
||||
# Seems like the namespace is moved to the top automatically when
|
||||
# a page change is done. Deactivating this for now.
|
||||
if self.explicit_move:
|
||||
LOG.debug("move {} to {}".format(from_pos, to_pos))
|
||||
self.send({"type": "mycroft.session.list.move",
|
||||
"namespace": "mycroft.system.active_skills",
|
||||
"from": from_pos, "to": to_pos,
|
||||
"items_number": 1})
|
||||
# Move the local representation of the skill from current
|
||||
# position to position 0.
|
||||
self.loaded.insert(to_pos, self.loaded.pop(from_pos))
|
||||
|
||||
def __switch_page(self, namespace, pages):
|
||||
""" Switch page to an already loaded page.
|
||||
|
||||
Arguments:
|
||||
pages: pages to switch to
|
||||
namespace: skill namespace
|
||||
"""
|
||||
try:
|
||||
num = self.loaded[0].pages.index(pages[0])
|
||||
except Exception as e:
|
||||
LOG.exception(repr(e))
|
||||
num = 0
|
||||
|
||||
LOG.debug('Switching to already loaded page at '
|
||||
'index {} in namespace {}'.format(num, namespace))
|
||||
self.send({"type": "mycroft.events.triggered",
|
||||
"namespace": namespace,
|
||||
"event_name": "page_gained_focus",
|
||||
"data": {"number": num}})
|
||||
|
||||
def show(self, namespace, page, index):
|
||||
""" Show a page and load it as needed.
|
||||
|
||||
TODO: - Update sync to match.
|
||||
- Separate into multiple functions/methods
|
||||
"""
|
||||
|
||||
LOG.debug("GUIConnection activating: " + namespace)
|
||||
pages = page if isinstance(page, list) else [page]
|
||||
|
||||
# find namespace among loaded namespaces
|
||||
try:
|
||||
index = self.__find_namespace(namespace)
|
||||
if index is None:
|
||||
# This namespace doesn't exist, insert them first so they're
|
||||
# shown.
|
||||
self.__insert_new_namespace(namespace, pages)
|
||||
return
|
||||
else: # Namespace exists
|
||||
if index > 0:
|
||||
# Namespace is inactive, activate it by moving it to
|
||||
# position 0
|
||||
self.__move_namespace(index, 0)
|
||||
|
||||
# Find if any new pages needs to be inserted
|
||||
new_pages = [p for p in pages if p not in self.loaded[0].pages]
|
||||
if new_pages:
|
||||
self.__insert_pages(namespace, new_pages)
|
||||
else:
|
||||
# No new pages, just switch
|
||||
self.__switch_page(namespace, pages)
|
||||
except Exception as e:
|
||||
LOG.exception(repr(e))
|
||||
|
||||
######################################################################
|
||||
# GUI client socket
|
||||
|
@ -122,10 +280,9 @@ class Enclosure:
|
|||
# 5) Connection persists for graphical interaction indefinitely
|
||||
#
|
||||
# If the connection is lost, it must be renegotiated and restarted.
|
||||
|
||||
def on_gui_client_connected(self, message):
|
||||
# GUI has announced presence
|
||||
DEBUG("on_gui_client_connected")
|
||||
LOG.debug("on_gui_client_connected")
|
||||
gui_id = message.data.get("gui_id")
|
||||
|
||||
# Spin up a new communication socket for this GUI
|
||||
|
@ -134,7 +291,7 @@ class Enclosure:
|
|||
pass
|
||||
self.GUIs[gui_id] = GUIConnection(gui_id, self.global_config,
|
||||
self.callback_disconnect, self)
|
||||
DEBUG("Heard announcement from gui_id: {}".format(gui_id))
|
||||
LOG.debug("Heard announcement from gui_id: {}".format(gui_id))
|
||||
|
||||
# Announce connection, the GUI should connect on it soon
|
||||
self.bus.emit(Message("mycroft.gui.port",
|
||||
|
@ -142,9 +299,14 @@ class Enclosure:
|
|||
"gui_id": gui_id}))
|
||||
|
||||
def callback_disconnect(self, gui_id):
|
||||
DEBUG("Disconnecting!")
|
||||
LOG.info("Disconnecting!")
|
||||
# TODO: Whatever is needed to kill the websocket instance
|
||||
del self.GUIs[gui_id]
|
||||
LOG.info(self.GUIs.keys())
|
||||
LOG.info('deleting: {}'.format(gui_id))
|
||||
if gui_id in self.GUIs:
|
||||
del self.GUIs[gui_id]
|
||||
else:
|
||||
LOG.warning('ID doesn\'t exist')
|
||||
|
||||
def register_gui_handlers(self):
|
||||
# TODO: Register handlers for standard (Mark 1) events
|
||||
|
@ -211,233 +373,82 @@ class GUIConnection:
|
|||
server_thread = None
|
||||
|
||||
def __init__(self, id, config, callback_disconnect, enclosure):
|
||||
DEBUG("Creating GUIConnection")
|
||||
LOG.debug("Creating GUIConnection")
|
||||
self.id = id
|
||||
self.socket = None
|
||||
self.callback_disconnect = callback_disconnect
|
||||
self.enclosure = enclosure
|
||||
self._active_namespaces = None
|
||||
|
||||
# This datastore holds the data associated with the GUI provider. Data
|
||||
# is stored in Namespaces, so you can have:
|
||||
# self.datastore["namespace"]["name"] = value
|
||||
# Typically the namespace is a meaningless identifier, but there is a
|
||||
# special "SYSTEM" namespace.
|
||||
self.datastore = {}
|
||||
|
||||
self.current_namespace = None
|
||||
self.current_pages = []
|
||||
self.current_index = None
|
||||
|
||||
# self.loaded is a list, each element consists of a namespace named
|
||||
# tuple.
|
||||
# The namespace namedtuple has the properties "name" and "pages"
|
||||
# The name contains the namespace name as a string and pages is a
|
||||
# mutable list of loaded pages.
|
||||
#
|
||||
# [Namespace name, [List of loaded qml pages]]
|
||||
# [
|
||||
# ["SKILL_NAME", ["page1.qml, "page2.qml", ... , "pageN.qml"]
|
||||
# [...]
|
||||
# ]
|
||||
self.loaded = [] # list of lists in order.
|
||||
self.explicit_move = True # Set to true to send reorder commands
|
||||
|
||||
# Each connection will run its own Tornado server. If the
|
||||
# connection drops, the server is killed.
|
||||
websocket_config = config.get("gui_websocket")
|
||||
host = websocket_config.get("host")
|
||||
route = websocket_config.get("route")
|
||||
self.port = websocket_config.get("base_port") + GUIConnection._last_idx
|
||||
GUIConnection._last_idx += 1
|
||||
base_port = websocket_config.get("base_port")
|
||||
|
||||
try:
|
||||
self.webapp = tornado.web.Application([
|
||||
(route, GUIWebsocketHandler)
|
||||
], **gui_app_settings)
|
||||
self.webapp.gui = self # Hacky way to associate socket with this
|
||||
self.webapp.listen(self.port, host)
|
||||
except Exception as e:
|
||||
DEBUG('Error: {}'.format(repr(e)))
|
||||
while True:
|
||||
self.port = base_port + GUIConnection._last_idx
|
||||
GUIConnection._last_idx += 1
|
||||
|
||||
try:
|
||||
self.webapp = tornado.web.Application(
|
||||
[(route, GUIWebsocketHandler)], **gui_app_settings
|
||||
)
|
||||
# Hacky way to associate socket with this object:
|
||||
self.webapp.gui = self
|
||||
self.webapp.listen(self.port, host)
|
||||
except Exception as e:
|
||||
LOG.debug('Error: {}'.format(repr(e)))
|
||||
continue
|
||||
break
|
||||
# Can't run two IOLoop's in the same process
|
||||
if not GUIConnection.server_thread:
|
||||
GUIConnection.server_thread = create_daemon(
|
||||
ioloop.IOLoop.instance().start)
|
||||
DEBUG("IOLoop started @ ws://{}:{}{}".format(host, self.port, route))
|
||||
LOG.debug('IOLoop started @ '
|
||||
'ws://{}:{}{}'.format(host, self.port, route))
|
||||
|
||||
def on_connection_opened(self, socket_handler):
|
||||
DEBUG("on_connection_opened")
|
||||
LOG.debug("on_connection_opened")
|
||||
self.socket = socket_handler
|
||||
self.synchronize()
|
||||
|
||||
# Synchronize existing datastore
|
||||
for namespace in self.datastore:
|
||||
msg = {"type": "mycroft.session.set",
|
||||
"namespace": namespace,
|
||||
"data": self.datastore[namespace]}
|
||||
self.socket.send_message(msg)
|
||||
# TODO REPLACE WITH CODE USING self.loaded
|
||||
# if self.current_pages:
|
||||
# self.show(self.current_namespace, self.current_pages,
|
||||
# self.current_index)
|
||||
def synchronize(self):
|
||||
""" Upload namespaces, pages and data. """
|
||||
namespace_pos = 0
|
||||
for namespace, pages in self.enclosure.loaded:
|
||||
# Insert namespace
|
||||
self.socket.send({"type": "mycroft.session.list.insert",
|
||||
"namespace": "mycroft.system.active_skills",
|
||||
"position": namespace_pos,
|
||||
"data": [{"skill_id": namespace}]
|
||||
})
|
||||
# Insert pages
|
||||
self.socket.send({"type": "mycroft.gui.list.insert",
|
||||
"namespace": namespace,
|
||||
"position": 0,
|
||||
"data": [{"url": p} for p in pages]
|
||||
})
|
||||
# Insert data
|
||||
data = self.enclosure.datastore.get(namespace, {})
|
||||
for key in data:
|
||||
self.socket.send({"type": "mycroft.session.set",
|
||||
"namespace": namespace,
|
||||
"data": {key: data[key]}
|
||||
})
|
||||
|
||||
namespace_pos += 1
|
||||
|
||||
def on_connection_closed(self, socket):
|
||||
# Self-destruct (can't reconnect on the same port)
|
||||
DEBUG("on_connection_closed")
|
||||
LOG.debug("on_connection_closed")
|
||||
if self.socket:
|
||||
DEBUG("Server stopped: {}".format(self.socket))
|
||||
LOG.debug("Server stopped: {}".format(self.socket))
|
||||
# TODO: How to stop the webapp for this socket?
|
||||
# self.socket.stop()
|
||||
self.socket = None
|
||||
self.callback_disconnect(self.id)
|
||||
|
||||
def set(self, namespace, name, value):
|
||||
if namespace not in self.datastore:
|
||||
self.datastore[namespace] = {}
|
||||
if self.datastore[namespace].get(name) != value:
|
||||
self.datastore[namespace][name] = value
|
||||
|
||||
# If the namespace is loaded send data to gui
|
||||
if namespace in [l.name for l in self.loaded]:
|
||||
msg = {"type": "mycroft.session.set",
|
||||
"namespace": namespace,
|
||||
"data": {name: value}}
|
||||
self.socket.send(msg)
|
||||
|
||||
def __find_namespace(self, namespace):
|
||||
for i, skill in enumerate(self.loaded):
|
||||
if skill[0] == namespace:
|
||||
return i
|
||||
return None
|
||||
|
||||
def __insert_pages(self, namespace, pages):
|
||||
""" Insert pages into the """
|
||||
DEBUG("Inserting new pages")
|
||||
if not isinstance(pages, list):
|
||||
raise ValueError('Argument must be list of pages')
|
||||
|
||||
self.socket.send({"type": "mycroft.gui.list.insert",
|
||||
"namespace": namespace,
|
||||
"position": len(self.loaded[0].pages),
|
||||
"data": [{"url": p} for p in pages]
|
||||
})
|
||||
|
||||
# append pages to local representation
|
||||
for p in pages:
|
||||
self.loaded[0].pages.append(p)
|
||||
|
||||
def __insert_new_namespace(self, namespace, pages):
|
||||
""" Insert new namespace and pages.
|
||||
|
||||
This first sends a message adding a new namespace at the
|
||||
highest priority (position 0 in the namespace stack)
|
||||
|
||||
Arguments:
|
||||
namespace: The skill namespace to create
|
||||
pages: Pages to insert
|
||||
"""
|
||||
DEBUG("Inserting new namespace")
|
||||
self.socket.send({"type": "mycroft.session.list.insert",
|
||||
"namespace": "mycroft.system.active_skills",
|
||||
"position": 0,
|
||||
"data": [{"skill_id": namespace}]
|
||||
})
|
||||
|
||||
# Load any already stored Data
|
||||
data = self.datastore.get(namespace, {})
|
||||
for key in data:
|
||||
msg = {"type": "mycroft.session.set",
|
||||
"namespace": namespace,
|
||||
"data": {key: data[key]}}
|
||||
self.socket.send(msg)
|
||||
|
||||
DEBUG("Inserting new page")
|
||||
self.socket.send({"type": "mycroft.gui.list.insert",
|
||||
"namespace": namespace,
|
||||
"position": 0,
|
||||
"data": [{"url": p} for p in pages]
|
||||
})
|
||||
# Make sure the local copy is updated
|
||||
self.loaded.insert(0, Namespace(namespace, pages))
|
||||
|
||||
def __move_namespace(self, from_pos, to_pos):
|
||||
""" Move an existing namespace to a new position in the stack.
|
||||
|
||||
Arguments:
|
||||
from_pos: Position in the stack to move from
|
||||
to_pos: Position to move to
|
||||
"""
|
||||
DEBUG("Activating existing namespace")
|
||||
# Seems like the namespace is moved to the top automatically when
|
||||
# a page change is done. Deactivating this for now.
|
||||
if self.explicit_move:
|
||||
DEBUG("move {} to {}".format(from_pos, to_pos))
|
||||
self.socket.send({"type": "mycroft.session.list.move",
|
||||
"namespace": "mycroft.system.active_skills",
|
||||
"from": from_pos, "to": to_pos,
|
||||
"items_number": 1})
|
||||
# Move the local representation of the skill from current
|
||||
# position to position 0.
|
||||
self.loaded.insert(to_pos, self.loaded.pop(from_pos))
|
||||
|
||||
def __switch_page(self, namespace, pages):
|
||||
""" Switch page to an already loaded page.
|
||||
|
||||
Arguments:
|
||||
pages: pages to switch to
|
||||
namespace: skill namespace
|
||||
"""
|
||||
try:
|
||||
num = self.loaded[0].pages.index(pages[0])
|
||||
except Exception as e:
|
||||
DEBUG(e)
|
||||
num = 0
|
||||
|
||||
DEBUG("Switching to already loaded page at "
|
||||
"index {} in namespace {}".format(num, namespace))
|
||||
self.socket.send({"type": "mycroft.events.triggered",
|
||||
"namespace": namespace,
|
||||
"event_name": "page_gained_focus",
|
||||
"data": {"number": num}})
|
||||
|
||||
def show(self, namespace, page, index):
|
||||
""" Show a page and load it as needed.
|
||||
|
||||
TODO: - Update sync to match.
|
||||
- Separate into multiple functions/methods
|
||||
"""
|
||||
|
||||
DEBUG("GUIConnection activating: " + namespace)
|
||||
pages = page if isinstance(page, list) else [page]
|
||||
|
||||
# find namespace among loaded namespaces
|
||||
try:
|
||||
index = self.__find_namespace(namespace)
|
||||
if index is None:
|
||||
# This namespace doesn't exist, insert them first so they're
|
||||
# shown.
|
||||
self.__insert_new_namespace(namespace, pages)
|
||||
return
|
||||
else: # Namespace exists
|
||||
if index > 0:
|
||||
# Namespace is inactive, activate it by moving it to
|
||||
# position 0
|
||||
self.__move_namespace(index, 0)
|
||||
|
||||
# Find if any new pages needs to be inserted
|
||||
new_pages = [p for p in pages if p not in self.loaded[0].pages]
|
||||
if new_pages:
|
||||
self.__insert_pages(namespace, new_pages)
|
||||
else:
|
||||
# No new pages, just switch
|
||||
self.__switch_page(namespace, pages)
|
||||
except Exception as e:
|
||||
DEBUG(repr(e))
|
||||
|
||||
# TODO: Not quite sure this is needed or if self.loaded can be used
|
||||
self.current_namespace = namespace
|
||||
self.current_pages = pages
|
||||
self.current_index = index
|
||||
|
||||
|
||||
class GUIWebsocketHandler(WebSocketHandler):
|
||||
"""
|
||||
|
@ -448,10 +459,40 @@ class GUIWebsocketHandler(WebSocketHandler):
|
|||
self.application.gui.on_connection_opened(self)
|
||||
|
||||
def on_message(self, message):
|
||||
DEBUG("Received: {}".format(message))
|
||||
LOG.debug("Received: {}".format(message))
|
||||
msg = json.loads(message)
|
||||
if (msg.get('type') == "mycroft.events.triggered" and
|
||||
msg.get('event_name') == 'page_gained_focus'):
|
||||
# System event, a page was changed
|
||||
msg_type = 'gui.page_interaction'
|
||||
msg_data = {
|
||||
'namespace': msg['namespace'],
|
||||
'page_number': msg['parameters']['number']
|
||||
}
|
||||
elif msg.get('type') == "mycroft.events.triggered":
|
||||
# A normal event was triggered
|
||||
msg_type = '{}.{}'.format(msg['namespace'], msg['event_name'])
|
||||
msg_data = msg['parameters']
|
||||
|
||||
elif msg.get('type') == 'mycroft.session.set':
|
||||
# A value was changed send it back to the skill
|
||||
msg_type = '{}.{}'.format(msg['namespace'], 'set')
|
||||
msg_data = msg['data']
|
||||
|
||||
message = Message(msg_type, msg_data)
|
||||
self.application.gui.enclosure.bus.emit(message)
|
||||
|
||||
def write_message(self, *arg, **kwarg):
|
||||
""" Wraps WebSocketHandler.write_message() with a lock. """
|
||||
with write_lock:
|
||||
super().write_message(*arg, **kwarg)
|
||||
|
||||
def send_message(self, message):
|
||||
self.write_message(message.serialize())
|
||||
if isinstance(message, Message):
|
||||
self.write_message(message.serialize())
|
||||
else:
|
||||
LOG.info('message: {}'.format(message))
|
||||
self.write_message(str(message))
|
||||
|
||||
def send(self, data):
|
||||
"""Send the given data across the socket as JSON
|
||||
|
|
|
@ -128,11 +128,11 @@ def load_skill(skill_descriptor, bus, skill_id, BLACKLISTED_SKILLS=None):
|
|||
callable(skill_module.create_skill)):
|
||||
# v2 skills framework
|
||||
skill = skill_module.create_skill()
|
||||
skill.skill_id = skill_id
|
||||
skill.settings.allow_overwrite = True
|
||||
skill.settings.load_skill_settings_from_file()
|
||||
skill.bind(bus)
|
||||
try:
|
||||
skill.skill_id = skill_id
|
||||
skill.load_data_files(path)
|
||||
# Set up intent handlers
|
||||
skill._register_decorated()
|
||||
|
@ -228,6 +228,46 @@ class SkillGUI:
|
|||
self.__session_data = {} # synced to GUI for use by this skill's pages
|
||||
self.page = None # the active GUI page (e.g. QML template) to show
|
||||
self.skill = skill
|
||||
self.on_gui_changed_callback = None
|
||||
|
||||
def build_message_type(self, event):
|
||||
""" Builds a message matching the output from the enclosure. """
|
||||
return '{}.{}'.format(self.skill.skill_id, event)
|
||||
|
||||
def setup_default_handlers(self):
|
||||
""" Sets the handlers for the default messages. """
|
||||
msg_type = self.build_message_type('set')
|
||||
print("LISTENING FOR {}".format(msg_type))
|
||||
self.skill.add_event(msg_type, self.gui_set)
|
||||
|
||||
def register_handler(self, event, handler):
|
||||
""" Register a handler for gui events.
|
||||
|
||||
when using the triggerEvent method from Qt
|
||||
triggerEvent("event", {"data": "cool"})
|
||||
|
||||
Arguments:
|
||||
event (str): event to catch
|
||||
handler: function to handle the event
|
||||
"""
|
||||
msg_type = self.build_message_type(event)
|
||||
self.skill.add_event(msg_type, handler)
|
||||
|
||||
def set_on_gui_changed(self, callback):
|
||||
""" Registers a callback function to run when a value is
|
||||
changed from the GUI.
|
||||
|
||||
Arguments:
|
||||
callback: Function to call when a value is changed
|
||||
"""
|
||||
self.on_gui_changed_callback = callback
|
||||
|
||||
def gui_set(self, message):
|
||||
for key in message.data:
|
||||
print("SETTING {} TO {}".format(key, message.data[key]))
|
||||
self[key] = message.data[key]
|
||||
if self.on_gui_changed_callback:
|
||||
self.on_gui_changed_callback()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.__session_data[key] = value
|
||||
|
@ -249,16 +289,17 @@ class SkillGUI:
|
|||
self.__session_data = {}
|
||||
self.page = None
|
||||
|
||||
def show_page(self, name):
|
||||
def show_page(self, name, override_idle=None):
|
||||
"""
|
||||
Begin showing the page in the GUI
|
||||
|
||||
Args:
|
||||
name (str): Name of page (e.g "mypage.qml") to display
|
||||
override_idle: If set will override the idle screen
|
||||
"""
|
||||
self.show_pages([name])
|
||||
self.show_pages([name], 0, override_idle)
|
||||
|
||||
def show_pages(self, page_names, index=0):
|
||||
def show_pages(self, page_names, index=0, override_idle=None):
|
||||
"""
|
||||
Begin showing the list of pages in the GUI
|
||||
|
||||
|
@ -267,6 +308,7 @@ class SkillGUI:
|
|||
["Weather.qml", "Forecast.qml", "Details.qml"]
|
||||
index (int): Page number (0-based) to show initially. For the
|
||||
above list a value of 1 would start on "Forecast.qml"
|
||||
override_idle: If set will override the idle screen
|
||||
"""
|
||||
if not isinstance(page_names, list):
|
||||
raise ValueError('page_names must be a list')
|
||||
|
@ -288,13 +330,13 @@ class SkillGUI:
|
|||
if page:
|
||||
page_urls.append("file://" + page)
|
||||
else:
|
||||
self.skill.log.debug("Unable to find page: " + str(name))
|
||||
return
|
||||
raise FileNotFoundError("Unable to find page: {}".format(name))
|
||||
|
||||
self.skill.bus.emit(Message("gui.page.show",
|
||||
{"page": page_urls,
|
||||
"index": index,
|
||||
"__from": self.skill.skill_id}))
|
||||
"__from": self.skill.skill_id,
|
||||
"__idle": override_idle}))
|
||||
|
||||
def show_text(self, text, title=None):
|
||||
""" Display a GUI page for viewing simple text
|
||||
|
@ -458,6 +500,9 @@ class MycroftSkill:
|
|||
bus.on(name, func)
|
||||
self.events.append((name, func))
|
||||
|
||||
# Intialize the SkillGui
|
||||
self.gui.setup_default_handlers()
|
||||
|
||||
def detach(self):
|
||||
for (name, intent) in self.registered_intents:
|
||||
name = str(self.skill_id) + ':' + name
|
||||
|
|
Loading…
Reference in New Issue