219 lines
8.1 KiB
Python
219 lines
8.1 KiB
Python
# 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)
|