# Copyright 2017 Mycroft AI Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import sys from mycroft.configuration import Configuration from mycroft.messagebus.client.ws import WebsocketClient from mycroft.util import create_daemon from mycroft.util.log import LOG import tornado.web from tornado import autoreload, ioloop from tornado.websocket import WebSocketHandler class Enclosure(object): def __init__(self): # Establish Enclosure's websocket connection to the messagebus self.bus = WebsocketClient() # Load full config Configuration.init(self.bus) config = Configuration.get() self.lang = config['lang'] self.config = config.get("enclosure") self.global_config = config # Listen for new GUI clients to announce themselves on the main bus self.GUIs = {} # GUIs, either local or remote self.bus.on("mycroft.gui.connected", self.on_gui_client_connected) self.register_gui_handlers() # First send any data: self.bus.on("gui.value.set", self.on_gui_set_value) self.bus.on("gui.page.show", self.on_gui_show_page) def run(self): try: self.bus.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() ###################################################################### # GUI client support # # The basic mechanism is: # 1) GUI client announces itself on the main messagebus # 2) Mycroft prepares a port for a socket connection to this GUI # 3) The port is announced over the messagebus # 4) The GUI connects on the socket # 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 print("on_gui_client_connected") gui_id = message.data.get("gui_id") # Spin up a new communication socket for this GUI if gui_id in self.GUIs: # TODO: Close it? pass self.GUIs[gui_id] = GUIConnection(gui_id, self.global_config, self.callback_disconnect) # Announce connection, the GUI should connect on it soon self.bus.emit(Message("mycroft.gui.port", {"port": self.GUIs[gui_id].port, "gui_id": gui_id})) def on_gui_set_value(self, message): data = message.data # Pass these values on to the GUI renderers for id in self.GUIs: for d in data: self.GUIs[id].send('set '+d+' to '+data[d]) # TODO: Actually use the protocol def on_gui_show_page(self, message): data = message.data if not 'page' in data: return # Pass the display request to the GUI to pull up for id in self.GUIs: self.GUIs[id].send('show ' + data['page']) # TODO: Actually use the protocol def callback_disconnect(self, gui_id): print("Disconnecting!") # TODO: Whatever is needed to kill the websocket instance del self.GUIs[gui_id] def register_gui_handlers(self): # TODO: Register handlers for standard (Mark 1) events # self.bus.on('enclosure.eyes.on', self.on) # self.bus.on('enclosure.eyes.off', self.off) # self.bus.on('enclosure.eyes.blink', self.blink) # self.bus.on('enclosure.eyes.narrow', self.narrow) # self.bus.on('enclosure.eyes.look', self.look) # self.bus.on('enclosure.eyes.color', self.color) # self.bus.on('enclosure.eyes.level', self.brightness) # self.bus.on('enclosure.eyes.volume', self.volume) # self.bus.on('enclosure.eyes.spin', self.spin) # self.bus.on('enclosure.eyes.timedspin', self.timed_spin) # self.bus.on('enclosure.eyes.reset', self.reset) # self.bus.on('enclosure.eyes.setpixel', self.set_pixel) # self.bus.on('enclosure.eyes.fill', self.fill) # self.bus.on('enclosure.mouth.reset', self.reset) # self.bus.on('enclosure.mouth.talk', self.talk) # self.bus.on('enclosure.mouth.think', self.think) # self.bus.on('enclosure.mouth.listen', self.listen) # self.bus.on('enclosure.mouth.smile', self.smile) # self.bus.on('enclosure.mouth.viseme', self.viseme) # self.bus.on('enclosure.mouth.text', self.text) # self.bus.on('enclosure.mouth.display', self.display) # self.bus.on('enclosure.mouth.display_image', self.display_image) # self.bus.on('enclosure.weather.display', self.display_weather) # self.bus.on('recognizer_loop:record_begin', self.mouth.listen) # self.bus.on('recognizer_loop:record_end', self.mouth.reset) # self.bus.on('recognizer_loop:audio_output_start', self.mouth.talk) # self.bus.on('recognizer_loop:audio_output_end', self.mouth.reset) pass ########################################################################## # GUIConnection ########################################################################## gui_app_settings = { 'debug': True } class GUIConnection(object): last_used_port = 0 server_thread = None def __init__(self, id, config, callback_disconnect): print("Creating GUIConnection") self.id = id self.server = None self.socket = None self.callback_disconnect = callback_disconnect # 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_used_port GUIConnection.last_used_port += 1 routes = [ (route, MycroftGUIWebsocket) ] self.application = tornado.web.Application(routes, **gui_app_settings) self.application.gui = self self.server = self.application.listen(self.port, host) # TODO: This might need to move up a level # Can't run two IOLoop's in the same process if not GUIConnection.server_thread: GUIConnection.server_thread = create_daemon(ioloop.IOLoop. instance().start) print("IOLoop started on ws://"+str(host)+":"+str(self.port)+str(route)) def on_connection_opened(self, socket): print("on_connection_opened") self.socket = socket def on_connection_closed(self, socket): # Self-destruct (can't reconnect on the same port) print("on_connection_closed") if self.server: self.server.stop() self.server = None self.callback_disconnect(self.id) class MycroftGUIWebsocket(WebSocketHandler): """ Serves as a communication interface between Qt/QML frontend and Mycroft Core. This is bidirectional, e.g. "show me this visual" to the frontend as well as "the user just tapped this button" from the frontend. For the rough protocol, see: https://cgit.kde.org/scratch/mart/mycroft-gui.git/tree/transportProtocol.txt?h=newapi # nopep8 """ def open(self): print("WebSocket opened") self.application.gui.on_connection_opened(self) def on_message(self, message): self.write_message(u"You said: " + message) def send(self, message): self.write_message(message) def on_close(self): print("WebSocket closed") self.application.gui.on_connection_closed(self)