def simple_cli(): global bus global bSimple bSimple = True bus = WebsocketClient() # Mycroft messagebus connection event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() bus.on('speak', handle_speak) try: while True: # Sleep for a while so all the output that results # from the previous command finishes before we print. time.sleep(1.5) print("Input (Ctrl+C to quit):") line = sys.stdin.readline() bus.emit(Message("recognizer_loop:utterance", {'utterances': [line.strip()]})) except KeyboardInterrupt as e: # User hit Ctrl+C to quit print("") except KeyboardInterrupt as e: LOG.exception(e) event_thread.exit() sys.exit()
def main(): global ws ws = WebsocketClient() tts.init(ws) if '--quiet' in sys.argv: ws.on('speak', handle_quiet) else: ws.on('speak', handle_speak) event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() try: while True: # TODO: Change this mechanism # Sleep for a while so all the output that results # from the previous command finishes before we print. time.sleep(1.5) print("Input (Ctrl+C to quit):") line = sys.stdin.readline() ws.emit( Message("recognizer_loop:utterance", {'utterances': [line.strip()]})) except KeyboardInterrupt, e: # User hit Ctrl+C to quit print("")
class WebSocketHandler(tornado.websocket.WebSocketHandler): peer = "unknown" def create_internal_emitter(self): # connect to mycroft internal websocket self.emitter = WebsocketClient() self.register_internal_messages() self.emitter_thread = Thread(target=self.connect_to_internal_emitter) self.emitter_thread.setDaemon(True) self.emitter_thread.start() def register_internal_messages(self): # catch all messages self.emitter.on('speak', self.handle_speak) def connect_to_internal_emitter(self): self.emitter.run_forever() def open(self): LOG.info('Client IP: ' + self.request.remote_ip) self.peer = self.request.remote_ip clients[self.peer] = self self.create_internal_emitter() self.write_message("Welcome to Jarbas Web Client") def on_message(self, message): utterance = message.strip() LOG.info("Utterance : " + utterance) if utterance: if utterance == '"mic_on"': create_signal('startListening') else: data = {"utterances": [utterance], "lang": lang} context = { "source": self.peer, "destinatary": "skills", "client_name": platform, "peer": self.peer } self.emitter.emit( Message("recognizer_loop:utterance", data, context)) def handle_speak(self, event): if event.context.get("client_name", platform) == platform: peer = event.context.get("peer", "") if peer == self.peer: self.write_message(event.data['utterance']) def on_close(self): global clients self.emitter.remove("speak", self.handle_speak) clients.pop(self.peer)
def main(): global client client = WebsocketClient() if not '--quiet' in sys.argv: client.on('speak', handle_speak) event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() try: while True: print("Input:") line = sys.stdin.readline() client.emit(Message("recognizer_loop:utterance", metadata={'utterances': [line.strip()]})) except KeyboardInterrupt, e: event_thread.exit() sys.exit()
def stop_speaking(ws=None): from mycroft.messagebus.client.ws import WebsocketClient from mycroft.messagebus.message import Message if ws is None: ws = WebsocketClient() # TODO: Less hacky approach to this once Audio Manager is implemented # Skills should only be able to stop speech they've initiated create_signal('stoppingTTS') ws.emit(Message('mycroft.audio.speech.stop')) # Block until stopped while check_for_signal("isSpeaking", -1): time.sleep(0.25) # This consumes the signal check_for_signal('stoppingTTS')
class DevicePairingClient(object): def __init__(self, config=_config, pairing_code=None): self.config = config self.paired = False self.ws_client = WebsocketClient(host=config.get("host"), port=config.get("port"), path=config.get("route"), ssl=str2bool(config.get("ssl"))) self.identity_manager = IdentityManager() self.identity = self.identity_manager.identity self.pairing_code = (pairing_code if pairing_code else generate_pairing_code()) def on_registration(self, message): # TODO: actually accept the configuration message and store it in # identity identity = self.identity_manager.get() register_payload = message.metadata if register_payload.get("device_id") == identity.device_id: identity.token = register_payload.get('token') identity.owner = register_payload.get('user') self.identity_manager.update(identity) self.ws_client.close() self.paired = True def send_device_info(self): msg = Message("device_info", metadata={ "pairing_code": self.pairing_code, "device_id": self.identity.device_id }) self.ws_client.emit(msg) @staticmethod def print_error(message): print(repr(message)) def run(self): self.ws_client.on('registration', self.on_registration) self.ws_client.on('open', self.send_device_info) self.ws_client.on('error', self.print_error) self.ws_client.run_forever()
class DevicePairingClient(object): def __init__(self, config=_config, pairing_code=None): self.config = config self.paired = False self.ws_client = WebsocketClient(host=config.get("host"), port=config.get("port"), path=config.get("route"), ssl=str2bool(config.get("ssl"))) self.identity_manager = IdentityManager() self.identity = self.identity_manager.identity self.pairing_code = ( pairing_code if pairing_code else generate_pairing_code()) def on_registration(self, message): # TODO: actually accept the configuration message and store it in # identity identity = self.identity_manager.get() register_payload = message.metadata if register_payload.get("device_id") == identity.device_id: identity.token = register_payload.get('token') identity.owner = register_payload.get('user') self.identity_manager.update(identity) self.ws_client.close() self.paired = True def send_device_info(self): msg = Message("device_info", metadata={ "pairing_code": self.pairing_code, "device_id": self.identity.device_id }) self.ws_client.emit(msg) @staticmethod def print_error(message): print(repr(message)) def run(self): self.ws_client.on('registration', self.on_registration) self.ws_client.on('open', self.send_device_info) self.ws_client.on('error', self.print_error) self.ws_client.run_forever()
def simple_cli(): global ws ws = WebsocketClient() event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() try: while True: # Sleep for a while so all the output that results # from the previous command finishes before we print. time.sleep(1.5) print("Input (Ctrl+C to quit):") line = sys.stdin.readline() ws.emit( Message("recognizer_loop:utterance", {'utterances': [line.strip()]})) except KeyboardInterrupt, e: # User hit Ctrl+C to quit print("")
def main(): global ws ws = WebsocketClient() if '--quiet' not in sys.argv: ws.on('speak', handle_speak) event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() try: while True: print("Input:") line = sys.stdin.readline() ws.emit( Message("recognizer_loop:utterance", {'utterances': [line.strip()]})) except KeyboardInterrupt, e: logger.exception(e) event_thread.exit() sys.exit()
def simple_cli(): global ws ws = WebsocketClient() event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() try: while True: # Sleep for a while so all the output that results # from the previous command finishes before we print. time.sleep(1.5) print("Input (Ctrl+C to quit):") line = sys.stdin.readline() ws.emit( Message("recognizer_loop:utterance", {'utterances': [line.strip()]})) except KeyboardInterrupt, e: # User hit Ctrl+C to quit print("")
def main(): global ws ws = WebsocketClient() ConfigurationManager.init(ws) def echo(message): try: _message = json.loads(message) if _message.get("type") == "registration": # do not log tokens from registration messages _message["data"]["token"] = None message = json.dumps(_message) except: pass logger.debug(message) ws.on('message', echo) ws.once('open', load_skills_callback) time.sleep(5) ws.emit(Message("speak", {'utterance': "Hello!"})) ws.run_forever()
def catchInput(queryInput): bus = dbus.SessionBus() remote_object = bus.get_object("com.mycroftkde.KDEPlasmoid","/ComMycroftkdeKDEPlasmoidInterface") global client client = WebsocketClient() if '--quiet' not in sys.argv: client.on('speak', handle_speak) event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() try: while True: print("DebuggingGui") line = remote_object.sendQuery(guioutputstring,dbus_interface = "com.mycroftkde.KDEPlasmoid") client.emit( Message("recognizer_loop:utterance", metadata={'utterances': [line.strip()]})) line = ("") return except KeyboardInterrupt, e: logger.exception(e) event_thread.exit() sys.exit()
def catchInput(queryInput): bus = dbus.SessionBus() remote_object = bus.get_object("org.gnome.Shell","/com/mycroftaignome/MycroftGnomeResult") getText = remote_object.sendQuery(guioutputstring,dbus_interface = "com.mycroftaignome.MycroftAiGnomeBox") print getText global client client = WebsocketClient() client.on('speak', handle_speak) event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() try: while True: print("Input:") queryInput= object.sendQuery(guioutputstring,dbus_interface = "com.mycroftaignome.MycroftAiGnomeBox") client.emit( Message("recognizer_loop:utterance", metadata={'utterances': [queryInput]})) break except KeyboardInterrupt, e: logger.exception(e) event_thread.exit() sys.exit()
class TestMessagebusMethods(unittest.TestCase): """This class is for testing the messsagebus. It currently only tests send and receive. The tests could include more. """ def setUp(self): """ This sets up for testing the message buss This requires starting the mycroft service and creating two WebsocketClient object to talk with eachother. Not this is threaded and will require cleanup """ # start the mycroft service. and get the pid of the script. self.pid = Popen(["python", "mycroft/messagebus/service/main.py"]).pid # Create the two web clients self.ws1 = WebsocketClient() self.ws2 = WebsocketClient() # init the flags for handler's self.handle1 = False self.handle2 = False # Start threads to handle websockets Thread(target=self.ws1.run_forever).start() Thread(target=self.ws2.run_forever).start() # Setup handlers for each of the messages. self.ws1.on('ws1.message', self.onHandle1) self.ws2.on('ws2.message', self.onHandle2) def onHandle1(self, event): """This is the handler for ws1.message This for now simply sets a flag to true when received. Args: event(Message): this is the message received """ self.handle1 = True def onHandle2(self, event): """This is the handler for ws2.message This for now simply sets a flag to true when received. Args: event(Message): this is the message received """ self.handle2 = True def tearDown(self): """This is the clean up for the tests This will close the websockets ending the threads then kill the mycroft service that was started in setUp. """ self.ws1.close() self.ws2.close() retcode = call(["kill", "-9", str(self.pid)]) def test_ClientServer(self): """This is the test to send a message from each of the websockets to the other. """ # Send the messages self.ws2.emit(Message('ws1.message')) self.ws1.emit(Message('ws2.message')) # allow time for messages to be processed time.sleep(0.2) # Check that both of the handlers were called. self.assertTrue(self.handle1) self.assertTrue(self.handle2)
class Enclosure: 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 # 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 = [] 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) self.bus.on("gui.page.delete", self.on_gui_delete_page) self.bus.on("gui.clear.namespace", self.on_gui_delete_namespace) self.bus.on("gui.event.send", self.on_gui_send_event) def run(self): try: self.bus.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() ###################################################################### # GUI client API 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: LOG.error('GUI connection {} has no socket!'.format(gui)) def on_gui_send_event(self, message): """ Send an event to the GUIs. """ try: data = { 'type': 'mycroft.events.triggered', 'namespace': message.data.get('__from'), 'event_name': message.data.get('event_name'), 'params': message.data.get('params') } self.send(data) except Exception as e: LOG.error('Could not send event ({})'.format(repr(e))) def on_gui_set_value(self, message): data = message.data namespace = data.get("__from", "") # Pass these values on to the GUI renderers 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_delete_page(self, message): """ Bus handler for removing pages. """ page, namespace, _ = _get_page_data(message) try: with namespace_lock: self.remove_pages(namespace, page) except Exception as e: LOG.exception(repr(e)) def on_gui_delete_namespace(self, message): """ Bus handler for removing namespace. """ try: namespace = message.data['__from'] with namespace_lock: self.remove_namespace(namespace) except Exception as e: LOG.exception(repr(e)) def on_gui_show_page(self, message): try: page, namespace, index = _get_page_data(message) # Pass the request to the GUI(s) to pull up a page template with namespace_lock: 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 namespace Args: namespace (str): Namespace to add to pages (list): Pages (str) to insert """ 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] }) # Insert the pages into local reprensentation as well. updated = Namespace(self.loaded[0].name, self.loaded[0].pages + pages) self.loaded[0] = updated def __remove_page(self, namespace, pos): """ Delete page. Args: namespace (str): Namespace to remove from pos (int): Page position to remove """ LOG.debug("Deleting {} from {}".format(pos, namespace)) self.send({ "type": "mycroft.gui.list.remove", "namespace": namespace, "position": pos, "items_number": 1 }) # Remove the page from the local reprensentation as well. self.loaded[0].pages.pop(pos) 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) Args: namespace (str): The skill namespace to create pages (str): Pages to insert (name matches QML) """ 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. Args: from_pos (int): Position in the stack to move from to_pos (int): 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. Args: pages (list): pages (str) to switch to namespace (str): 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. Args: page (str or list): page(s) to show namespace (str): skill namespace index (int): ??? TODO: Unused in code ??? 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)) def remove_namespace(self, namespace): """ Remove namespace. Args: namespace (str): namespace to remove """ index = self.__find_namespace(namespace) if index is None: return else: LOG.debug("Removing namespace {} at {}".format(namespace, index)) self.send({ "type": "mycroft.session.list.remove", "namespace": "mycroft.system.active_skills", "position": index, "items_number": 1 }) # Remove namespace from loaded namespaces self.loaded.pop(index) def remove_pages(self, namespace, pages): """ Remove the listed pages from the provided namespace. Args: namespace (str): The namespace to modify pages (list): List of page names (str) to delete """ try: index = self.__find_namespace(namespace) if index is None: return else: # Remove any pages that doesn't exist in the namespace pages = [p for p in pages if p in self.loaded[index].pages] # Make sure to remove pages from the back indexes = [self.loaded[index].pages.index(p) for p in pages] indexes = sorted(indexes) indexes.reverse() for page_index in indexes: self.__remove_page(namespace, page_index) except Exception as e: LOG.exception(repr(e)) ###################################################################### # GUI client socket # # 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 LOG.debug("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, self) 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", { "port": self.GUIs[gui_id].port, "gui_id": gui_id })) def callback_disconnect(self, gui_id): LOG.info("Disconnecting!") # TODO: Whatever is needed to kill the websocket instance 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 # 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
class Enclosure: 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 # 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 = [] 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) self.bus.on("gui.page.delete", self.on_gui_delete_page) self.bus.on("gui.clear.namespace", self.on_gui_delete_namespace) self.bus.on("gui.event.send", self.on_gui_send_event) def run(self): try: self.bus.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() ###################################################################### # GUI client API 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: LOG.error('GUI connection {} has no socket!'.format(gui)) def on_gui_send_event(self, message): """ Send an event to the GUIs. """ try: data = {'type': 'mycroft.events.triggered', 'namespace': message.data.get('__from'), 'event_name': message.data.get('event_name'), 'params': message.data.get('params')} self.send(data) except Exception as e: LOG.error('Could not send event ({})'.format(repr(e))) def on_gui_set_value(self, message): data = message.data namespace = data.get("__from", "") # Pass these values on to the GUI renderers 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_delete_page(self, message): """ Bus handler for removing pages. """ page, namespace, _ = _get_page_data(message) try: self.remove_pages(namespace, page) except Exception as e: LOG.exception(repr(e)) def on_gui_delete_namespace(self, message): """ Bus handler for removing namespace. """ try: namespace = message.data['__from'] self.remove_namespace(namespace) except Exception as e: LOG.exception(repr(e)) def on_gui_show_page(self, message): try: page, namespace, index = _get_page_data(message) # Pass the request to the GUI(s) to pull up a page template 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 namespace Args: namespace (str): Namespace to add to pages (list): Pages (str) to insert """ 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] }) # Insert the pages into local reprensentation as well. self.loaded[0].pages += pages def __remove_page(self, namespace, pos): """ Delete page. Args: namespace (str): Namespace to remove from pos (int): Page position to remove """ LOG.debug("Deleting {} from {}".format(pos, namespace)) self.send({"type": "mycroft.gui.list.remove", "namespace": namespace, "position": pos, "items_number": 1 }) # Remove the page from the local reprensentation as well. self.loaded[0].pages.pop(pos) 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) Args: namespace (str): The skill namespace to create pages (str): Pages to insert (name matches QML) """ 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. Args: from_pos (int): Position in the stack to move from to_pos (int): 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. Args: pages (list): pages (str) to switch to namespace (str): 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. Args: page (str or list): page(s) to show namespace (str): skill namespace index (int): ??? TODO: Unused in code ??? 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)) def remove_namespace(self, namespace): """ Remove namespace. Args: namespace (str): namespace to remove """ index = self.__find_namespace(namespace) if index is None: return else: LOG.debug("Removing namespace {} at {}".format(namespace, index)) self.send({"type": "mycroft.session.list.remove", "namespace": "mycroft.system.active_skills", "position": index, "items_number": 1 }) # Remove namespace from loaded namespaces self.loaded.pop(index) def remove_pages(self, namespace, pages): """ Remove the listed pages from the provided namespace. Args: namespace (str): The namespace to modify pages (list): List of page names (str) to delete """ try: index = self.__find_namespace(namespace) if index is None: return else: # Remove any pages that doesn't exist in the namespace pages = [p for p in pages if p in self.loaded[index].pages] # Make sure to remove pages from the back indexes = [self.loaded[index].pages.index(p) for p in pages] indexes = sorted(indexes) indexes.reverse() for page_index in indexes: self.__remove_page(namespace, page_index) except Exception as e: LOG.exception(repr(e)) ###################################################################### # GUI client socket # # 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 LOG.debug("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, self) 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", {"port": self.GUIs[gui_id].port, "gui_id": gui_id})) def callback_disconnect(self, gui_id): LOG.info("Disconnecting!") # TODO: Whatever is needed to kill the websocket instance 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 # 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
class WiFi: def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.ws = WebsocketClient() ConfigurationManager.init(self.ws) self.enclosure = EnclosureAPI(self.ws) self.init_events() self.conn_monitor = None self.conn_monitor_stop = threading.Event() def init_events(self): ''' Register handlers for various websocket events used to communicate with outside systems. ''' # This event is generated by an outside mechanism. On a # Holmes unit this comes from the Enclosure's menu item # being selected. self.ws.on('mycroft.wifi.start', self.start) # These events are generated by Javascript in the captive # portal. self.ws.on('mycroft.wifi.stop', self.stop) self.ws.on('mycroft.wifi.scan', self.scan) self.ws.on('mycroft.wifi.connect', self.connect) def start(self, event=None): ''' Fire up the MYCROFT access point for the user to connect to with a phone or computer. ''' LOG.info("Starting access point...") # Fire up our access point self.ap.up() LOG.info("Done putting ap up...") if not self.server: LOG.info("Creating web server...") self.server = WebServer(self.ap.ip, 80) LOG.info("Starting web server...") self.server.start() LOG.info("Created web server.") LOG.info("Access point started!\n%s" % self.ap.__dict__) self._start_connection_monitor() def _connection_prompt(self, prefix): # let the user know to connect to it... passwordSpelled = ", ".join(self.ap.password) self._speak_and_show( prefix + " Use your mobile device or computer to " "connect to the wifi network " "'MYCROFT'; Then enter the uppercase " "password " + passwordSpelled, self.ap.password) def _speak_and_show(self, speak, show): ''' Communicate with the user throughout the process ''' self.ws.emit(Message("speak", {'utterance': speak})) if show is None: return # TODO: This sleep should not be necessary, but without it the # text to be displayed by enclosure.mouth_text() gets # wiped out immediately when the utterance above is # begins processing. # Remove the sleep once this behavior is corrected. sleep(0.25) self.enclosure.mouth_text(show) def _start_connection_monitor(self): LOG.info("Starting monitor thread...\n") if self.conn_monitor is not None: LOG.info("Killing old thread...\n") self.conn_monitor_stop.set() self.conn_monitor_stop.wait() self.conn_monitor = threading.Thread( target=self._do_connection_monitor, args={}) self.conn_monitor.daemon = True self.conn_monitor.start() LOG.info("Monitor thread setup complete.\n") def _stop_connection_monitor(self): ''' Set flag that will let monitoring thread close ''' self.conn_monitor_stop.set() def _do_connection_monitor(self): LOG.info("Invoked monitor thread...\n") mtimeLast = os.path.getmtime('/var/lib/misc/dnsmasq.leases') bHasConnected = False cARPFailures = 0 timeStarted = time.time() timeLastAnnounced = 0 # force first announcement to now self.conn_monitor_stop.clear() while not self.conn_monitor_stop.isSet(): # do our monitoring... mtime = os.path.getmtime('/var/lib/misc/dnsmasq.leases') if mtimeLast != mtime: # Something changed in the dnsmasq lease file - # presumably a (re)new lease bHasConnected = True cARPFailures = 0 mtimeLast = mtime timeStarted = time.time() # reset start time after connection timeLastAnnounced = time.time() - 45 # announce how to connect if time.time() - timeStarted > 60 * 5: # After 5 minutes, shut down the access point LOG.info("Auto-shutdown of access point after 5 minutes") self.stop() continue if time.time() - timeLastAnnounced >= 45: if bHasConnected: self._speak_and_show( "Now you can open your browser and go to start dot " "mycroft dot A I, then follow the instructions given " " there", "start.mycroft.ai") else: self._connection_prompt("Allow me to walk you through the " " wifi setup process; ") timeLastAnnounced = time.time() if bHasConnected: # Flush the ARP entries associated with our access point # This will require all network hardware to re-register # with the ARP tables if still present. if cARPFailures == 0: res = cli_no_output('ip', '-s', '-s', 'neigh', 'flush', self.ap.subnet + '.0/24') # Give ARP system time to re-register hardware sleep(5) # now look at the hardware that has responded, if no entry # shows up on our access point after 2*5=10 seconds, the user # has disconnected if not self._is_ARP_filled(): cARPFailures += 1 if cARPFailures > 2: self._connection_prompt("Connection lost,") bHasConnected = False else: cARPFailures = 0 sleep(5) # wait a bit to prevent thread from hogging CPU LOG.info("Exiting monitor thread...\n") self.conn_monitor_stop.clear() def _is_ARP_filled(self): res = cli_no_output('/usr/sbin/arp', '-n') out = str(res.get("stdout")) if out: # Parse output, skipping header for o in out.split("\n")[1:]: if o[0:len(self.ap.subnet)] == self.ap.subnet: if "(incomplete)" in o: # ping the IP to get the ARP table entry reloaded ip_disconnected = o.split(" ")[0] cli_no_output('/bin/ping', '-c', '1', '-W', '3', ip_disconnected) else: return True # something on subnet is connected! return False def scan(self, event=None): LOG.info("Scanning wifi connections...") networks = {} status = self.get_status() for cell in Cell.all(self.iface): update = True ssid = cell.ssid quality = self.get_quality(cell.quality) # If there are duplicate network IDs (e.g. repeaters) only # report the strongest signal if networks.__contains__(ssid): update = networks.get(ssid).get("quality") < quality if update and ssid: networks[ssid] = { 'quality': quality, 'encrypted': cell.encrypted, 'connected': self.is_connected(ssid, status) } self.ws.emit(Message("mycroft.wifi.scanned", {'networks': networks})) LOG.info("Wifi connections scanned!\n%s" % networks) @staticmethod def get_quality(quality): values = quality.split("/") return float(values[0]) / float(values[1]) def connect(self, event=None): if event and event.data: ssid = event.data.get("ssid") connected = self.is_connected(ssid) if connected: LOG.warn("Mycroft is already connected to %s" % ssid) else: self.disconnect() LOG.info("Connecting to: %s" % ssid) nid = wpa(self.iface, 'add_network') wpa(self.iface, 'set_network', nid, 'ssid', '"' + ssid + '"') if event.data.__contains__("pass"): psk = '"' + event.data.get("pass") + '"' wpa(self.iface, 'set_network', nid, 'psk', psk) else: wpa(self.iface, 'set_network', nid, 'key_mgmt', 'NONE') wpa(self.iface, 'enable', nid) connected = self.get_connected(ssid) if connected: wpa(self.iface, 'save_config') self.ws.emit(Message("mycroft.wifi.connected", {'connected': connected})) LOG.info("Connection status for %s = %s" % (ssid, connected)) if connected: self.ws.emit(Message("speak", { 'utterance': "Thank you, I'm now connected to the " "internet and ready for use"})) # TODO: emit something that triggers a pairing check def disconnect(self): status = self.get_status() nid = status.get("id") if nid: ssid = status.get("ssid") wpa(self.iface, 'disable', nid) LOG.info("Disconnecting %s id: %s" % (ssid, nid)) def get_status(self): res = cli('wpa_cli', '-i', self.iface, 'status') out = str(res.get("stdout")) if out: return dict(o.split("=") for o in out.split("\n")[:-1]) return {} def get_connected(self, ssid, retry=5): connected = self.is_connected(ssid) while not connected and retry > 0: sleep(2) retry -= 1 connected = self.is_connected(ssid) return connected def is_connected(self, ssid, status=None): status = status or self.get_status() state = status.get("wpa_state") return status.get("ssid") == ssid and state == "COMPLETED" def stop(self, event=None): LOG.info("Stopping access point...") self._stop_connection_monitor() self.ap.down() if self.server: self.server.server.shutdown() self.server.server.server_close() self.server.join() self.server = None LOG.info("Access point stopped!") def _do_net_check(self): # give system 5 seconds to resolve network or get plugged in sleep(5) LOG.info("Checking internet connection again") if not connected() and self.conn_monitor is None: # TODO: Enclosure/localization self._speak_and_show( "This device is not connected to the Internet. Either plug " "in a network cable or hold the button on top for two " "seconds, then select wifi from the menu", None) def run(self): try: # When the system first boots up, check for a valid internet # connection. LOG.info("Checking internet connection") if not connected(): LOG.info("No connection initially, waiting 20...") self.net_check = threading.Thread( target=self._do_net_check, args={}) self.net_check.daemon = True self.net_check.start() else: LOG.info("Connection found!") self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop()
class Enclosure(object): """ Serves as a communication interface between Arduino and Mycroft Core. ``Enclosure`` initializes and aggregates all enclosures implementation. E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino`` It also listens to the basis events in order to perform those core actions on the unit. E.g. Start and Stop talk animation """ _last_internet_notification = 0 def __init__(self): self.ws = WebsocketClient() ConfigurationManager.init(self.ws) self.config = ConfigurationManager.get().get("enclosure") self.__init_serial() self.reader = EnclosureReader(self.serial, self.ws) self.writer = EnclosureWriter(self.serial, self.ws) # Send a message to the Arduino across the serial line asking # for a reply with version info. self.writer.write("system.version") # When the Arduino responds, it will generate this message self.ws.on("enclosure.started", self.on_arduino_responded) self.arduino_responded = False # Start a 5 second timer. If the serial port hasn't received # any acknowledgement of the "system.version" within those # 5 seconds, assume there is nothing on the other end (e.g. # we aren't running a Mark 1 with an Arduino) Timer(5, self.check_for_response).start() # Notifications from mycroft-core self.ws.on("enclosure.notify.no_internet", self.on_no_internet) def on_arduino_responded(self, event=None): self.eyes = EnclosureEyes(self.ws, self.writer) self.mouth = EnclosureMouth(self.ws, self.writer) self.system = EnclosureArduino(self.ws, self.writer) self.weather = EnclosureWeather(self.ws, self.writer) self.__register_events() self.__reset() self.arduino_responded = True # verify internet connection and prompt user on bootup if needed if not connected(): # We delay this for several seconds to ensure that the other # clients are up and connected to the messagebus in order to # receive the "speak". This was sometimes happening too # quickly and the user wasn't notified what to do. Timer(5, self.on_no_internet).start() def on_no_internet(self, event=None): if connected(): # One last check to see if connection was established return if time.time()-Enclosure._last_internet_notification < 30: # don't bother the user with multiple notifications with 30 secs return Enclosure._last_internet_notification = time.time() # TODO: This should go into EnclosureMark1 subclass of Enclosure. # Handle the translation within that code. self.ws.emit(Message("speak", { 'utterance': "This device is not connected to the Internet. " "Either plug in a network cable or hold the button " "on top for two seconds, then select wifi from the " "menu"})) def __init_serial(self): try: self.port = self.config.get("port") self.rate = self.config.get("rate") self.timeout = self.config.get("timeout") self.serial = serial.serial_for_url( url=self.port, baudrate=self.rate, timeout=self.timeout) LOG.info("Connected to: %s rate: %s timeout: %s" % (self.port, self.rate, self.timeout)) except: LOG.error("Impossible to connect to serial port: " + self.port) raise def __register_events(self): self.ws.on('enclosure.mouth.events.activate', self.__register_mouth_events) self.ws.on('enclosure.mouth.events.deactivate', self.__remove_mouth_events) self.ws.on('enclosure.reset', self.__reset) self.__register_mouth_events() def __register_mouth_events(self, event=None): self.ws.on('recognizer_loop:record_begin', self.mouth.listen) self.ws.on('recognizer_loop:record_end', self.mouth.reset) self.ws.on('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.on('recognizer_loop:audio_output_end', self.mouth.reset) def __remove_mouth_events(self, event=None): self.ws.remove('recognizer_loop:record_begin', self.mouth.listen) self.ws.remove('recognizer_loop:record_end', self.mouth.reset) self.ws.remove('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.remove('recognizer_loop:audio_output_end', self.mouth.reset) def __reset(self, event=None): # Reset both the mouth and the eye elements to indicate the unit is # ready for input. self.writer.write("eyes.reset") self.writer.write("mouth.reset") def speak(self, text): self.ws.emit(Message("speak", {'utterance': text})) def run(self): try: self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() def check_for_response(self): if not self.arduino_responded: # There is nothing on the other end of the serial port # close these serial-port readers and this process self.writer.stop() self.reader.stop() self.serial.close() self.ws.close()
class Enclosure(object): """ Serves as a communication interface between Arduino and Mycroft Core. ``Enclosure`` initializes and aggregates all enclosures implementation. E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino`` It also listens to the basis events in order to perform those core actions on the unit. E.g. Start and Stop talk animation """ def __init__(self): self.ws = WebsocketClient() ConfigurationManager.init(self.ws) self.config = ConfigurationManager.get().get("enclosure") self.__init_serial() self.reader = EnclosureReader(self.serial, self.ws) self.writer = EnclosureWriter(self.serial, self.ws) self.writer.write("system.version") self.ws.on("enclosure.start", self.start) self.started = False Timer(5, self.stop).start() def start(self, event=None): self.eyes = EnclosureEyes(self.ws, self.writer) self.mouth = EnclosureMouth(self.ws, self.writer) self.system = EnclosureArduino(self.ws, self.writer) self.weather = EnclosureWeather(self.ws, self.writer) self.__register_events() #self.update() self.test() self.started = True def update(self): if self.config.get("update"): try: self.speak("Upgrading enclosure version") subprocess.check_call("/opt/enclosure/upload.sh") self.speak("Enclosure update completed") ConfigurationManager.save({"enclosure": {"update": False}}) except: self.speak("I cannot upgrade right now, I'll try later") def test(self): if self.config.get("test"): self.speak("Beginning hardware test") self.writer.write("test.begin") ConfigurationManager.save({"enclosure": {"test": False}}) def __init_serial(self): try: self.port = self.config.get("port") self.rate = self.config.get("rate") self.timeout = self.config.get("timeout") self.serial = serial.serial_for_url(url=self.port, baudrate=self.rate, timeout=self.timeout) LOG.info("Connected to: %s rate: %s timeout: %s" % (self.port, self.rate, self.timeout)) except: LOG.error("Impossible to connect to serial port: " + self.port) raise def __register_events(self): self.ws.on('enclosure.mouth.events.activate', self.__register_mouth_events) self.ws.on('enclosure.mouth.events.deactivate', self.__remove_mouth_events) self.__register_mouth_events() def __register_mouth_events(self, event=None): self.ws.on('recognizer_loop:record_begin', self.mouth.listen) self.ws.on('recognizer_loop:record_end', self.mouth.reset) self.ws.on('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.on('recognizer_loop:audio_output_end', self.mouth.reset) def __remove_mouth_events(self, event=None): self.ws.remove('recognizer_loop:record_begin', self.mouth.listen) self.ws.remove('recognizer_loop:record_end', self.mouth.reset) self.ws.remove('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.remove('recognizer_loop:audio_output_end', self.mouth.reset) def speak(self, text): self.ws.emit(Message("speak", {'utterance': text})) def run(self): try: self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() def stop(self): if not self.started: self.writer.stop() self.reader.stop() self.serial.close() self.ws.close()
class Enclosure(object): """ Serves as a communication interface between Arduino and Mycroft Core. ``Enclosure`` initializes and aggregates all enclosures implementation. E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino`` It also listens to the basis events in order to perform those core actions on the unit. E.g. Start and Stop talk animation """ def __init__(self): self.ws = WebsocketClient() ConfigurationManager.init(self.ws) self.config = ConfigurationManager.get().get("enclosure") self.__init_serial() self.reader = EnclosureReader(self.serial, self.ws) self.writer = EnclosureWriter(self.serial, self.ws) self.writer.write("system.version") self.ws.on("enclosure.start", self.start) self.started = False Timer(5, self.stop).start() # WHY? This at least needs an explaination, this is non-obvious behavior def start(self, event=None): self.eyes = EnclosureEyes(self.ws, self.writer) self.mouth = EnclosureMouth(self.ws, self.writer) self.system = EnclosureArduino(self.ws, self.writer) self.weather = EnclosureWeather(self.ws, self.writer) self.__register_events() self.__reset() self.started = True def __init_serial(self): try: self.port = self.config.get("port") self.rate = self.config.get("rate") self.timeout = self.config.get("timeout") self.serial = serial.serial_for_url( url=self.port, baudrate=self.rate, timeout=self.timeout) LOG.info("Connected to: %s rate: %s timeout: %s" % (self.port, self.rate, self.timeout)) except: LOG.error("Impossible to connect to serial port: " + self.port) raise def __register_events(self): self.ws.on('enclosure.mouth.events.activate', self.__register_mouth_events) self.ws.on('enclosure.mouth.events.deactivate', self.__remove_mouth_events) self.ws.on('enclosure.reset', self.__reset) self.__register_mouth_events() def __register_mouth_events(self, event=None): self.ws.on('recognizer_loop:record_begin', self.mouth.listen) self.ws.on('recognizer_loop:record_end', self.mouth.reset) self.ws.on('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.on('recognizer_loop:audio_output_end', self.mouth.reset) def __remove_mouth_events(self, event=None): self.ws.remove('recognizer_loop:record_begin', self.mouth.listen) self.ws.remove('recognizer_loop:record_end', self.mouth.reset) self.ws.remove('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.remove('recognizer_loop:audio_output_end', self.mouth.reset) def __reset(self, event=None): # Reset both the mouth and the eye elements to indicate the unit is # ready for input. self.writer.write("eyes.reset") self.writer.write("mouth.reset") def speak(self, text): self.ws.emit(Message("speak", {'utterance': text})) def run(self): try: self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() def stop(self): if not self.started: self.writer.stop() self.reader.stop() self.serial.close() self.ws.close()
class Enclosure(object): """ Serves as a communication interface between Arduino and Mycroft Core. ``Enclosure`` initializes and aggregates all enclosures implementation. E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino`` It also listens to the basis events in order to perform those core actions on the unit. E.g. Start and Stop talk animation """ def __init__(self): self.ws = WebsocketClient() ConfigurationManager.init(self.ws) self.config = ConfigurationManager.get().get("enclosure") self.__init_serial() self.reader = EnclosureReader(self.serial, self.ws) self.writer = EnclosureWriter(self.serial, self.ws) self.writer.write("system.version") self.ws.on("enclosure.start", self.start) self.started = False Timer(5, self.stop).start() # WHY? This at least # needs an explanation, this is non-obvious behavior def start(self, event=None): self.eyes = EnclosureEyes(self.ws, self.writer) self.mouth = EnclosureMouth(self.ws, self.writer) self.system = EnclosureArduino(self.ws, self.writer) self.weather = EnclosureWeather(self.ws, self.writer) self.__register_events() self.__reset() self.started = True def __init_serial(self): try: self.port = self.config.get("port") self.rate = self.config.get("rate") self.timeout = self.config.get("timeout") self.serial = serial.serial_for_url(url=self.port, baudrate=self.rate, timeout=self.timeout) LOG.info("Connected to: %s rate: %s timeout: %s" % (self.port, self.rate, self.timeout)) except: LOG.error("Impossible to connect to serial port: " + self.port) raise def __register_events(self): self.ws.on('enclosure.mouth.events.activate', self.__register_mouth_events) self.ws.on('enclosure.mouth.events.deactivate', self.__remove_mouth_events) self.ws.on('enclosure.reset', self.__reset) self.__register_mouth_events() def __register_mouth_events(self, event=None): self.ws.on('recognizer_loop:record_begin', self.mouth.listen) self.ws.on('recognizer_loop:record_end', self.mouth.reset) self.ws.on('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.on('recognizer_loop:audio_output_end', self.mouth.reset) def __remove_mouth_events(self, event=None): self.ws.remove('recognizer_loop:record_begin', self.mouth.listen) self.ws.remove('recognizer_loop:record_end', self.mouth.reset) self.ws.remove('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.remove('recognizer_loop:audio_output_end', self.mouth.reset) def __reset(self, event=None): # Reset both the mouth and the eye elements to indicate the unit is # ready for input. self.writer.write("eyes.reset") self.writer.write("mouth.reset") def speak(self, text): self.ws.emit(Message("speak", {'utterance': text})) def run(self): try: self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() def stop(self): if not self.started: self.writer.stop() self.reader.stop() self.serial.close() self.ws.close()
def gui_main(stdscr): global scr global ws global line global log_line_lr_scroll global longest_visible_line global find_str global last_key scr = stdscr init_screen() ws = WebsocketClient() ws.on('speak', handle_speak) ws.on('message', handle_message) event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() history = [] hist_idx = -1 # index, from the bottom try: input = "" while True: draw_screen() c = scr.getch() # Convert VT100 ESC codes generated by some terminals if c == 27: c1 = scr.getch() c2 = scr.getch() if c1 == 79 and c2 == 120: c = curses.KEY_UP elif c1 == 79 and c2 == 116: c = curses.KEY_LEFT elif c1 == 79 and c2 == 114: c = curses.KEY_DOWN elif c1 == 79 and c2 == 118: c = curses.KEY_RIGHT elif c1 == 79 and c2 == 121: c = curses.KEY_PPAGE # aka PgUp elif c1 == 79 and c2 == 115: c = curses.KEY_NPAGE # aka PgDn elif c1 == 79 and c2 == 119: c = curses.KEY_HOME elif c1 == 79 and c2 == 113: c = curses.KEY_END else: c = c2 last_key = str(c) + ",ESC+" + str(c1) + "+" + str(c2) else: last_key = str(c) if c == curses.KEY_ENTER or c == 10 or c == 13: # ENTER sends the typed line to be processed by Mycroft if line == "": continue if line[:1] == ":": # Lines typed like ":help" are 'commands' if handle_cmd(line[1:]) == 1: break else: # Treat this as an utterance history.append(line) chat.append(line) ws.emit( Message("recognizer_loop:utterance", { 'utterances': [line.strip()], 'lang': 'en-us' })) hist_idx = -1 line = "" elif c == 16 or c == 545: # Ctrl+P or Ctrl+Left (Previous) # Move up the history stack hist_idx = clamp(hist_idx + 1, -1, len(history) - 1) if hist_idx >= 0: line = history[len(history) - hist_idx - 1] else: line = "" elif c == 14 or c == 560: # Ctrl+N or Ctrl+Right (Next) # Move down the history stack hist_idx = clamp(hist_idx - 1, -1, len(history) - 1) if hist_idx >= 0: line = history[len(history) - hist_idx - 1] else: line = "" elif c == curses.KEY_LEFT: # scroll long log lines left log_line_lr_scroll += curses.COLS // 4 elif c == curses.KEY_RIGHT: # scroll long log lines right log_line_lr_scroll -= curses.COLS // 4 if log_line_lr_scroll < 0: log_line_lr_scroll = 0 elif c == curses.KEY_HOME: # HOME scrolls log lines all the way to the start log_line_lr_scroll = longest_visible_line elif c == curses.KEY_END: # END scrolls log lines all the way to the end log_line_lr_scroll = 0 elif c == curses.KEY_UP: scroll_log(False, 1) elif c == curses.KEY_DOWN: scroll_log(True, 1) elif c == curses.KEY_NPAGE: # aka PgDn # PgDn to go down a page in the logs scroll_log(True) elif c == curses.KEY_PPAGE: # aka PgUp # PgUp to go up a page in the logs scroll_log(False) elif c == curses.KEY_RESIZE: # Generated by Curses when window/screen has been resized y, x = scr.getmaxyx() curses.resizeterm(y, x) # resizeterm() causes another curses.KEY_RESIZE, so # we need to capture that to prevent a loop of resizes c = scr.getch() elif c == curses.KEY_BACKSPACE or c == 127: # Backspace to erase a character in the utterance line = line[:-1] elif c == 6: # Ctrl+F (Find) line = ":find " elif c == 24: # Ctrl+X (Exit) if find_str: # End the find session find_str = None rebuild_filtered_log() elif curses.ascii.isascii(c): # Accept typed character in the utterance line += chr(c) # DEBUG: Uncomment the following code to see what key codes # are generated when an unknown key is pressed. # else: # line += str(c) except KeyboardInterrupt as e: # User hit Ctrl+C to quit pass except KeyboardInterrupt as e: LOG.exception(e) finally: scr.erase() scr.refresh() scr = None pass
class WiFi: def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.ws = WebsocketClient() ConfigurationManager.init(self.ws) self.enclosure = EnclosureAPI(self.ws) self.init_events() self.conn_monitor = None self.conn_monitor_stop = threading.Event() def init_events(self): ''' Register handlers for various websocket events used to communicate with outside systems. ''' # This event is generated by an outside mechanism. On a # Holmes unit this comes from the Enclosure's menu item # being selected. self.ws.on('mycroft.wifi.start', self.start) # These events are generated by Javascript in the captive # portal. self.ws.on('mycroft.wifi.stop', self.stop) self.ws.on('mycroft.wifi.scan', self.scan) self.ws.on('mycroft.wifi.connect', self.connect) def start(self, event=None): ''' Fire up the MYCROFT access point for the user to connect to with a phone or computer. ''' LOG.info("Starting access point...") # Fire up our access point self.ap.up() if not self.server: LOG.info("Creating web server...") self.server = WebServer(self.ap.ip, 80) LOG.info("Starting web server...") self.server.start() LOG.info("Created web server.") LOG.info("Access point started!\n%s" % self.ap.__dict__) self._start_connection_monitor() def _connection_prompt(self, prefix): # let the user know to connect to it... passwordSpelled = ", ".join(self.ap.password) self._speak_and_show( prefix + " Use your mobile device or computer to " "connect to the wifi network " "'MYCROFT'; Then enter the uppercase " "password " + passwordSpelled, self.ap.password) def _speak_and_show(self, speak, show): ''' Communicate with the user throughout the process ''' self.ws.emit(Message("speak", {'utterance': speak})) if show is None: return # TODO: This sleep should not be necessary, but without it the # text to be displayed by enclosure.mouth_text() gets # wiped out immediately when the utterance above is # begins processing. # Remove the sleep once this behavior is corrected. sleep(0.25) self.enclosure.mouth_text(show) def _start_connection_monitor(self): LOG.info("Starting monitor thread...\n") if self.conn_monitor is not None: LOG.info("Killing old thread...\n") self.conn_monitor_stop.set() self.conn_monitor_stop.wait() self.conn_monitor = threading.Thread( target=self._do_connection_monitor, args={}) self.conn_monitor.daemon = True self.conn_monitor.start() LOG.info("Monitor thread setup complete.\n") def _stop_connection_monitor(self): ''' Set flag that will let monitoring thread close ''' self.conn_monitor_stop.set() def _do_connection_monitor(self): LOG.info("Invoked monitor thread...\n") mtimeLast = os.path.getmtime('/var/lib/misc/dnsmasq.leases') bHasConnected = False cARPFailures = 0 timeStarted = time.time() timeLastAnnounced = 0 # force first announcement to now self.conn_monitor_stop.clear() while not self.conn_monitor_stop.isSet(): # do our monitoring... mtime = os.path.getmtime('/var/lib/misc/dnsmasq.leases') if mtimeLast != mtime: # Something changed in the dnsmasq lease file - # presumably a (re)new lease bHasConnected = True cARPFailures = 0 mtimeLast = mtime timeStarted = time.time() # reset start time after connection timeLastAnnounced = time.time() - 45 # announce how to connect if time.time() - timeStarted > 60 * 5: # After 5 minutes, shut down the access point LOG.info("Auto-shutdown of access point after 5 minutes") self.stop() continue if time.time() - timeLastAnnounced >= 45: if bHasConnected: self._speak_and_show( "Now you can open your browser and go to start dot " "mycroft dot A I, then follow the instructions given " " there", "start.mycroft.ai") else: self._connection_prompt("Allow me to walk you through the " " wifi setup process; ") timeLastAnnounced = time.time() if bHasConnected: # Flush the ARP entries associated with our access point # This will require all network hardware to re-register # with the ARP tables if still present. if cARPFailures == 0: res = cli_no_output('ip', '-s', '-s', 'neigh', 'flush', self.ap.subnet + '.0/24') # Give ARP system time to re-register hardware sleep(5) # now look at the hardware that has responded, if no entry # shows up on our access point after 2*5=10 seconds, the user # has disconnected if not self._is_ARP_filled(): cARPFailures += 1 if cARPFailures > 2: self._connection_prompt("Connection lost,") bHasConnected = False else: cARPFailures = 0 sleep(5) # wait a bit to prevent thread from hogging CPU LOG.info("Exiting monitor thread...\n") self.conn_monitor_stop.clear() def _is_ARP_filled(self): res = cli_no_output('/usr/sbin/arp', '-n') out = str(res.get("stdout")) if out: # Parse output, skipping header for o in out.split("\n")[1:]: if o[0:len(self.ap.subnet)] == self.ap.subnet: if "(incomplete)" in o: # ping the IP to get the ARP table entry reloaded ip_disconnected = o.split(" ")[0] cli_no_output('/bin/ping', '-c', '1', '-W', '3', ip_disconnected) else: return True # something on subnet is connected! return False def scan(self, event=None): LOG.info("Scanning wifi connections...") networks = {} status = self.get_status() for cell in Cell.all(self.iface): update = True ssid = cell.ssid quality = self.get_quality(cell.quality) # If there are duplicate network IDs (e.g. repeaters) only # report the strongest signal if networks.__contains__(ssid): update = networks.get(ssid).get("quality") < quality if update and ssid: networks[ssid] = { 'quality': quality, 'encrypted': cell.encrypted, 'connected': self.is_connected(ssid, status) } self.ws.emit(Message("mycroft.wifi.scanned", {'networks': networks})) LOG.info("Wifi connections scanned!\n%s" % networks) @staticmethod def get_quality(quality): values = quality.split("/") return float(values[0]) / float(values[1]) def connect(self, event=None): if event and event.data: ssid = event.data.get("ssid") connected = self.is_connected(ssid) if connected: LOG.warn("Mycroft is already connected to %s" % ssid) else: self.disconnect() LOG.info("Connecting to: %s" % ssid) nid = wpa(self.iface, 'add_network') wpa(self.iface, 'set_network', nid, 'ssid', '"' + ssid + '"') if event.data.__contains__("pass"): psk = '"' + event.data.get("pass") + '"' wpa(self.iface, 'set_network', nid, 'psk', psk) else: wpa(self.iface, 'set_network', nid, 'key_mgmt', 'NONE') wpa(self.iface, 'enable', nid) connected = self.get_connected(ssid) if connected: wpa(self.iface, 'save_config') self.ws.emit(Message("mycroft.wifi.connected", {'connected': connected})) LOG.info("Connection status for %s = %s" % (ssid, connected)) if connected: self.ws.emit(Message("speak", { 'utterance': "Thank you, I'm now connected to the " "internet and ready for use"})) # TODO: emit something that triggers a pairing check def disconnect(self): status = self.get_status() nid = status.get("id") if nid: ssid = status.get("ssid") wpa(self.iface, 'disable', nid) LOG.info("Disconnecting %s id: %s" % (ssid, nid)) def get_status(self): res = cli('wpa_cli', '-i', self.iface, 'status') out = str(res.get("stdout")) if out: return dict(o.split("=") for o in out.split("\n")[:-1]) return {} def get_connected(self, ssid, retry=5): connected = self.is_connected(ssid) while not connected and retry > 0: sleep(2) retry -= 1 connected = self.is_connected(ssid) return connected def is_connected(self, ssid, status=None): status = status or self.get_status() state = status.get("wpa_state") return status.get("ssid") == ssid and state == "COMPLETED" def stop(self, event=None): LOG.info("Stopping access point...") self._stop_connection_monitor() self.ap.down() if self.server: self.server.server.shutdown() self.server.server.server_close() self.server.join() self.server = None LOG.info("Access point stopped!") def _do_net_check(self): # give system 5 seconds to resolve network or get plugged in sleep(5) LOG.info("Checking internet connection again") if not connected() and self.conn_monitor is None: # TODO: Enclosure/localization self._speak_and_show( "This device is not connected to the Internet. Either plug " "in a network cable or hold the button on top for two " "seconds, then select wifi from the menu", None) def run(self): try: # When the system first boots up, check for a valid internet # connection. LOG.info("Checking internet connection") if not connected(): LOG.info("No connection initially, waiting 20...") self.net_check = threading.Thread( target=self._do_net_check, args={}) self.net_check.daemon = True self.net_check.start() else: LOG.info("Connection found!") self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop()
class JarbasClientFactory(WebSocketClientFactory, ReconnectingClientFactory): protocol = JarbasClientProtocol def __init__(self, *args, **kwargs): super(JarbasClientFactory, self).__init__(*args, **kwargs) self.client = None self.status = "disconnected" # mycroft_ws self.emitter = None self.emitter_thread = None self.create_internal_emitter() # initialize methods def connect_to_internal_emitter(self): self.emitter.run_forever() def create_internal_emitter(self): # connect to mycroft internal websocket self.emitter = WebsocketClient() self.register_internal_messages() self.emitter_thread = Thread(target=self.connect_to_internal_emitter) self.emitter_thread.setDaemon(True) self.emitter_thread.start() def register_internal_messages(self): self.emitter.on("server.message.received", self.handle_receive_server_message) self.emitter.on("server.message.send", self.handle_send_server_message) # websocket handlers def clientConnectionFailed(self, connector, reason): logger.info("Client connection failed: " + str(reason) + " .. retrying ..") self.status = "disconnected" self.retry(connector) def clientConnectionLost(self, connector, reason): logger.info("Client connection lost: " + str(reason) + " .. retrying ..") self.status = "disconnected" self.retry(connector) # mycroft handlers def handle_receive_server_message(self, message): server_msg = message.data.get("payload") is_file = message.data.get("isBinary") if is_file: # TODO received file pass else: # forward server message to internal bus message = Message.deserialize(server_msg) self.emitter.emit(message) def handle_send_server_message(self, message): server_msg = message.data.get("payload") is_file = message.data.get("isBinary") if is_file: # TODO send file pass else: # send message to server server_msg = Message.deserialize(server_msg) server_msg.context["platform"] = platform self.sendMessage(server_msg.type, server_msg.data, server_msg.context) def sendRaw(self, data): if self.client is None: logger.error("Client is none") return self.client.sendMessage(data, isBinary=True) def sendMessage(self, type, data, context=None): if self.client is None: logger.error("Client is none") return if context is None: context = {} msg = self.client.Message_to_raw_data(Message(type, data, context)) self.client.sendMessage(msg, isBinary=False) self.emitter.emit( Message("server.message.sent", { "type": type, "data": data, "context": context, "raw": msg }))
class WiFi: def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.ws = WebsocketClient() self.enclosure = EnclosureAPI(self.ws) self.init_events() self.conn_monitor = None self.conn_monitor_stop = threading.Event() self.starting = False def init_events(self): ''' Register handlers for various websocket events used to communicate with outside systems. ''' # This event is generated by an outside mechanism. On a # Mark 1 unit this comes from the Enclosure's WIFI menu # item being selected. self.ws.on('mycroft.wifi.start', self.start) # Similar to the above. Resets to factory defaults self.ws.on('mycroft.wifi.reset', self.reset) # Similar to the above. Enable/disable SSH self.ws.on('mycroft.enable.ssh', self.ssh_enable) self.ws.on('mycroft.disable.ssh', self.ssh_disable) # These events are generated by Javascript in the captive # portal. self.ws.on('mycroft.wifi.stop', self.stop) self.ws.on('mycroft.wifi.scan', self.scan) self.ws.on('mycroft.wifi.connect', self.connect) def start(self, event=None): ''' Fire up the MYCROFT access point for the user to connect to with a phone or computer. ''' if self.starting: return self.starting = True LOG.info("Starting access point...") self.intro_msg = "" if event and event.data.get("msg"): self.intro_msg = event.data.get("msg") self.allow_timeout = True if event and event.data.get("allow_timeout"): self.allow_timeout = event.data.get("allow_timeout") # Fire up our access point self.ap.up() if not self.server: LOG.info("Creating web server...") self.server = WebServer(self.ap.ip, 80) LOG.info("Starting web server...") self.server.start() LOG.info("Created web server.") LOG.info("Access point started!\n%s" % self.ap.__dict__) self._start_connection_monitor() def _connection_prompt(self, intro): while self.ap.password is None or self.ap.password == "": sleep(1) # give it time to load # Speak the connection instructions and show the password on screen passwordSpelled = ", ".join(self.ap.password) self._speak_and_show(intro + " Use your mobile device or computer to connect " "to the wifi network 'MYCROFT'. Then enter the " "password " + passwordSpelled, self.ap.password) def _speak_and_show(self, speak, show): ''' Communicate with the user throughout the process ''' self.ws.emit(Message("speak", {'utterance': speak})) if show is None: return wait_while_speaking() self.enclosure.mouth_text(show) def _start_connection_monitor(self): LOG.info("Starting monitor thread...\n") if self.conn_monitor is not None: LOG.info("Killing old thread...\n") self.conn_monitor_stop.set() self.conn_monitor_stop.wait() self.conn_monitor = threading.Thread( target=self._do_connection_monitor, args={}) self.conn_monitor.daemon = True self.conn_monitor.start() LOG.info("Monitor thread setup complete.\n") def _stop_connection_monitor(self): ''' Set flag that will let monitoring thread close ''' self.conn_monitor_stop.set() def _do_connection_monitor(self): LOG.info("Invoked monitor thread...\n") mtimeLast = os.path.getmtime('/var/lib/misc/dnsmasq.leases') bHasConnected = False cARPFailures = 0 timeStarted = time.time() timeLastAnnounced = timeStarted - 45 # first reminder in 90 secs self.conn_monitor_stop.clear() while not self.conn_monitor_stop.isSet(): # do our monitoring... mtime = os.path.getmtime('/var/lib/misc/dnsmasq.leases') if mtimeLast != mtime: # Something changed in the dnsmasq lease file - # presumably a (re)new lease bHasConnected = True cARPFailures = 0 mtimeLast = mtime timeStarted = time.time() # reset start time after connection timeLastAnnounced = time.time() - 45 # announce how to connect if time.time() - timeStarted > 60 * 5 and self.allow_timeout: # After 5 minutes, shut down the access point (unless the # system has never been setup, in which case we stay up # indefinitely) LOG.info("Auto-shutdown of access point after 5 minutes") self.stop() continue if time.time() - timeLastAnnounced >= 45: if bHasConnected: self._speak_and_show( "Follow the prompt on your mobile device or computer " "and choose a wifi network. If you don't get a " "prompt, open your browser and go to start dot " "mycroft dot A I.", "start.mycroft.ai") else: if self.intro_msg: self._connection_prompt(self.intro_msg) self.intro_msg = None # only speak the intro once else: self._connection_prompt("Allow me to walk you through " "the wifi setup process.") timeLastAnnounced = time.time() if bHasConnected: # Flush the ARP entries associated with our access point # This will require all network hardware to re-register # with the ARP tables if still present. if cARPFailures == 0: res = cli_no_output('ip', '-s', '-s', 'neigh', 'flush', self.ap.subnet + '.0/24') # Give ARP system time to re-register hardware sleep(5) # now look at the hardware that has responded, if no entry # shows up on our access point after 2*5=10 seconds, the user # has disconnected if not self._is_ARP_filled(): cARPFailures += 1 if cARPFailures > 2: self._connection_prompt("Connection lost.") bHasConnected = False else: cARPFailures = 0 sleep(5) # wait a bit to prevent thread from hogging CPU LOG.info("Exiting monitor thread...\n") self.conn_monitor_stop.clear() def _is_ARP_filled(self): res = cli_no_output('/usr/sbin/arp', '-n') out = str(res.get("stdout")) if out: # Parse output, skipping header for o in out.split("\n")[1:]: if o[0:len(self.ap.subnet)] == self.ap.subnet: if "(incomplete)" in o: # ping the IP to get the ARP table entry reloaded ip_disconnected = o.split(" ")[0] cli_no_output('/bin/ping', '-c', '1', '-W', '3', ip_disconnected) else: return True # something on subnet is connected! return False def scan(self, event=None): LOG.info("Scanning wifi connections...") networks = {} status = self.get_status() for cell in Cell.all(self.iface): if "x00" in cell.ssid: continue # ignore hidden networks update = True ssid = cell.ssid quality = self.get_quality(cell.quality) # If there are duplicate network IDs (e.g. repeaters) only # report the strongest signal if networks.__contains__(ssid): update = networks.get(ssid).get("quality") < quality if update and ssid: networks[ssid] = { 'quality': quality, 'encrypted': cell.encrypted, 'connected': self.is_connected(ssid, status) } self.ws.emit(Message("mycroft.wifi.scanned", {'networks': networks})) LOG.info("Wifi connections scanned!\n%s" % networks) @staticmethod def get_quality(quality): values = quality.split("/") return float(values[0]) / float(values[1]) def connect(self, event=None): if event and event.data: ssid = event.data.get("ssid") connected = self.is_connected(ssid) if connected: LOG.warn("Mycroft is already connected to %s" % ssid) else: self.disconnect() LOG.info("Connecting to: %s" % ssid) nid = wpa(self.iface, 'add_network') wpa(self.iface, 'set_network', nid, 'ssid', '"' + ssid + '"') if event.data.__contains__("pass"): psk = '"' + event.data.get("pass") + '"' wpa(self.iface, 'set_network', nid, 'psk', psk) else: wpa(self.iface, 'set_network', nid, 'key_mgmt', 'NONE') wpa(self.iface, 'enable', nid) connected = self.get_connected(ssid) if connected: wpa(self.iface, 'save_config') self.ws.emit(Message("mycroft.wifi.connected", {'connected': connected})) LOG.info("Connection status for %s = %s" % (ssid, connected)) def disconnect(self): status = self.get_status() nid = status.get("id") if nid: ssid = status.get("ssid") wpa(self.iface, 'disable', nid) LOG.info("Disconnecting %s id: %s" % (ssid, nid)) def get_status(self): res = cli('wpa_cli', '-i', self.iface, 'status') out = str(res.get("stdout")) if out: return dict(o.split("=") for o in out.split("\n")[:-1]) return {} def get_connected(self, ssid, retry=5): connected = self.is_connected(ssid) while not connected and retry > 0: sleep(2) retry -= 1 connected = self.is_connected(ssid) return connected def is_connected(self, ssid, status=None): status = status or self.get_status() state = status.get("wpa_state") return status.get("ssid") == ssid and state == "COMPLETED" def stop(self, event=None): LOG.info("Stopping access point...") if is_speaking(): stop_speaking() # stop any assistance being spoken self._stop_connection_monitor() self.ap.down() self.enclosure.mouth_reset() # remove "start.mycroft.ai" self.starting = False if self.server: self.server.server.shutdown() self.server.server.server_close() self.server.join() self.server = None LOG.info("Access point stopped!") def run(self): try: self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() def reset(self, event=None): """Reset the unit to the factory defaults """ LOG.info("Resetting the WPA_SUPPLICANT File") try: call( "echo '" + WPA_SUPPLICANT + "'> /etc/wpa_supplicant/wpa_supplicant.conf", shell=True) # UGLY BUT WORKS call(RM_SKILLS) except Exception as e: LOG.error("Error: {0}".format(e)) def ssh_enable(self, event=None): LOG.info("Enabling SSH") try: call('systemctl enable ssh.service', shell=True) call('systemctl start ssh.service', shell=True) except Exception as e: LOG.error("Error: {0}".format(e)) def ssh_disable(self, event=None): LOG.info("Disabling SSH") try: call('systemctl stop ssh.service', shell=True) call('systemctl disable ssh.service', shell=True) except Exception as e: LOG.error("Error: {0}".format(e))
class JarbasServerFactory(WebSocketServerFactory): def __init__(self, *args, **kwargs): super(JarbasServerFactory, self).__init__(*args, **kwargs) # list of clients self.clients = {} # ip block policy self.ip_list = [] self.blacklist = True # if False, ip_list is a whitelist # mycroft_ws self.emitter = None self.emitter_thread = None self.create_internal_emitter() def emitter_send(self, type, data=None, context=None): data = data or {} context = context or {} self.emitter.emit(Message(type, data, context)) def connect_to_internal_emitter(self): self.emitter.run_forever() def create_internal_emitter(self): # connect to mycroft internal websocket self.emitter = WebsocketClient() self.register_internal_messages() self.emitter_thread = Thread(target=self.connect_to_internal_emitter) self.emitter_thread.setDaemon(True) self.emitter_thread.start() def register_internal_messages(self): # catch all messages self.emitter.on('message', self.handle_message) self.emitter.on('client.broadcast', self.handle_broadcast) self.emitter.on('client.send', self.handle_send) # websocket handlers def register_client(self, client, platform=None): """ Add client to list of managed connections. """ platform = platform or "unknown" logger.info("registering client: " + str(client.peer)) t, ip, sock = " ", "", "" # client.peer.split(":") # see if ip adress is blacklisted if ip in self.ip_list and self.blacklist: logger.warning("Blacklisted ip tried to connect: " + ip) self.unregister_client(client, reason=u"Blacklisted ip") return # see if ip adress is whitelisted elif ip not in self.ip_list and not self.blacklist: logger.warning("Unknown ip tried to connect: " + ip) # if not whitelisted kick self.unregister_client(client, reason=u"Unknown ip") return self.clients[client.peer] = { "object": client, "status": "connected", "platform": platform } def unregister_client(self, client, code=3078, reason=u"unregister client request"): """ Remove client from list of managed connections. """ logger.info("deregistering client: " + str(client.peer)) if client.peer in self.clients.keys(): client_data = self.clients[client.peer] or {} j, ip, sock_num = " ", "", "" # client.peer.split(":") context = { "user": client_data.get("names", ["unknown_user"])[0], "source": client.peer } self.emitter.emit( Message("user.disconnect", { "reason": reason, "ip": ip, "sock": sock_num }, context)) client.sendClose(code, reason) self.clients.pop(client.peer) def process_message(self, client, payload, isBinary): """ Process message from client, decide what to do internally here """ logger.info("processing message from client: " + str(client.peer)) client_data = self.clients[client.peer] # client_protocol, ip, sock_num = client.peer.split(":") # TODO this would be the place to check for blacklisted # messages/skills/intents per user if isBinary: # TODO receive files pass else: # add context for this message message = Message.deserialize(payload) message.context["source"] = client.peer message.context["destinatary"] = "skills" if "platform" not in message.context: message.context["platform"] = client_data.get( "platform", "unknown") # send client message to internal mycroft bus self.emitter.emit(message) # mycroft handlers def handle_send(self, message): # send message to client msg = message.data.get("payload") is_file = message.data.get("isBinary") peer = message.data.get("peer") if is_file: # TODO send file pass elif peer in self.clients: # send message to client client = self.clients[peer] payload = Message.serialize(msg) client.sendMessage(payload, False) else: logger.error("That client is not connected") self.emitter_send("client.send.error", { "error": "That client is not connected", "peer": peer }, message.context) def handle_broadcast(self, message): # send message to all clients msg = message.data.get("payload") is_file = message.data.get("isBinary") if is_file: # TODO send file pass else: # send message to all clients server_msg = Message.serialize(msg) self.broadcast(server_msg) def handle_message(self, message): # forward internal messages to clients if they are the target message = Message.deserialize(message) if message.type == "complete_intent_failure": message.type = "hivemind.complete_intent_failure" message.context = message.context or {} peer = message.context.get("destinatary") if peer and peer in self.clients: client_data = self.clients[peer] or {} client = client_data.get("object") client.sendMessage(message.serialize(), False)
class Enclosure(object): """ Serves as a communication interface between Arduino and Mycroft Core. ``Enclosure`` initializes and aggregates all enclosures implementation. E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino`` It also listens to the basis events in order to perform those core actions on the unit. E.g. Start and Stop talk animation """ _last_internet_notification = 0 def __init__(self): self.ws = WebsocketClient() ConfigurationManager.init(self.ws) self.config = ConfigurationManager.instance().get("enclosure") self.__init_serial() self.reader = EnclosureReader(self.serial, self.ws) self.writer = EnclosureWriter(self.serial, self.ws) # Send a message to the Arduino across the serial line asking # for a reply with version info. self.writer.write("system.version") # When the Arduino responds, it will generate this message self.ws.on("enclosure.started", self.on_arduino_responded) self.arduino_responded = False # Start a 5 second timer. If the serial port hasn't received # any acknowledgement of the "system.version" within those # 5 seconds, assume there is nothing on the other end (e.g. # we aren't running a Mark 1 with an Arduino) Timer(5, self.check_for_response).start() # Notifications from mycroft-core self.ws.on("enclosure.notify.no_internet", self.on_no_internet) def on_arduino_responded(self, event=None): self.eyes = EnclosureEyes(self.ws, self.writer) self.mouth = EnclosureMouth(self.ws, self.writer) self.system = EnclosureArduino(self.ws, self.writer) self.weather = EnclosureWeather(self.ws, self.writer) self.__register_events() self.__reset() self.arduino_responded = True # verify internet connection and prompt user on bootup if needed if not connected(): # We delay this for several seconds to ensure that the other # clients are up and connected to the messagebus in order to # receive the "speak". This was sometimes happening too # quickly and the user wasn't notified what to do. Timer(5, self._do_net_check).start() def on_no_internet(self, event=None): if connected(): # One last check to see if connection was established return if time.time()-Enclosure._last_internet_notification < 30: # don't bother the user with multiple notifications with 30 secs return Enclosure._last_internet_notification = time.time() # TODO: This should go into EnclosureMark1 subclass of Enclosure. if has_been_paired(): # Handle the translation within that code. self.ws.emit(Message("speak", { 'utterance': "This device is not connected to the Internet. " "Either plug in a network cable or hold the " "button on top for two seconds, then select " "wifi from the menu"})) else: # enter wifi-setup mode automatically self.ws.emit(Message("mycroft.wifi.start")) def __init_serial(self): try: self.port = self.config.get("port") self.rate = self.config.get("rate") self.timeout = self.config.get("timeout") self.serial = serial.serial_for_url( url=self.port, baudrate=self.rate, timeout=self.timeout) LOG.info("Connected to: %s rate: %s timeout: %s" % (self.port, self.rate, self.timeout)) except: LOG.error("Impossible to connect to serial port: " + self.port) raise def __register_events(self): self.ws.on('enclosure.mouth.events.activate', self.__register_mouth_events) self.ws.on('enclosure.mouth.events.deactivate', self.__remove_mouth_events) self.ws.on('enclosure.reset', self.__reset) self.__register_mouth_events() def __register_mouth_events(self, event=None): self.ws.on('recognizer_loop:record_begin', self.mouth.listen) self.ws.on('recognizer_loop:record_end', self.mouth.reset) self.ws.on('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.on('recognizer_loop:audio_output_end', self.mouth.reset) def __remove_mouth_events(self, event=None): self.ws.remove('recognizer_loop:record_begin', self.mouth.listen) self.ws.remove('recognizer_loop:record_end', self.mouth.reset) self.ws.remove('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.remove('recognizer_loop:audio_output_end', self.mouth.reset) def __reset(self, event=None): # Reset both the mouth and the eye elements to indicate the unit is # ready for input. self.writer.write("eyes.reset") self.writer.write("mouth.reset") def speak(self, text): self.ws.emit(Message("speak", {'utterance': text})) def run(self): try: self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() def check_for_response(self): if not self.arduino_responded: # There is nothing on the other end of the serial port # close these serial-port readers and this process self.writer.stop() self.reader.stop() self.serial.close() self.ws.close() def _do_net_check(self): # TODO: This should live in the derived Enclosure, e.g. Enclosure_Mark1 LOG.info("Checking internet connection") if not connected(): # and self.conn_monitor is None: if has_been_paired(): # TODO: Enclosure/localization self.ws.emit(Message("speak", { 'utterance': "This unit is not connected to the Internet." " Either plug in a network cable or hold the " "button on top for two seconds, then select " "wifi from the menu" })) else: # Begin the unit startup process, this is the first time it # is being run with factory defaults. # TODO: This logic should be in Enclosure_Mark1 # TODO: Enclosure/localization # Don't listen to mic during this out-of-box experience self.ws.emit(Message("mycroft.mic.mute", None)) # Kick off wifi-setup automatically self.ws.emit(Message("mycroft.wifi.start", {'msg': "Hello I am Mycroft, your new " "assistant. To assist you I need to be " "connected to the internet. You can " "either plug me in with a network cable," " or use wifi. To setup wifi ", 'allow_timeout': False}))
class Enclosure(object): """ Serves as a communication interface between Arduino and Mycroft Core. ``Enclosure`` initializes and aggregates all enclosures implementation. E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino`` It also listens to the basis events in order to perform those core actions on the unit. E.g. Start and Stop talk animation """ _last_internet_notification = 0 def __init__(self): self.ws = WebsocketClient() self.ws.on("open", self.on_ws_open) Configuration.init(self.ws) global_config = Configuration.get() self.lang = global_config['lang'] self.config = global_config.get("enclosure") self.__init_serial() self.reader = EnclosureReader(self.serial, self.ws, self.lang) self.writer = EnclosureWriter(self.serial, self.ws) # initiates the web sockets on display manager # NOTE: this is a temporary place to initiate display manager sockets initiate_display_manager_ws() def on_ws_open(self, event=None): # Mark 1 auto-detection: # # Prepare to receive message when the Arduino responds to the # following "system.version" self.ws.on("enclosure.started", self.on_arduino_responded) self.arduino_responded = False # Send a message to the Arduino across the serial line asking # for a reply with version info. self.writer.write("system.version") # Start a 5 second timer. If the serial port hasn't received # any acknowledgement of the "system.version" within those # 5 seconds, assume there is nothing on the other end (e.g. # we aren't running a Mark 1 with an Arduino) Timer(5, self.check_for_response).start() # Notifications from mycroft-core self.ws.on("enclosure.notify.no_internet", self.on_no_internet) def on_arduino_responded(self, event=None): self.eyes = EnclosureEyes(self.ws, self.writer) self.mouth = EnclosureMouth(self.ws, self.writer) self.system = EnclosureArduino(self.ws, self.writer) self.weather = EnclosureWeather(self.ws, self.writer) self.__register_events() self.__reset() self.arduino_responded = True # verify internet connection and prompt user on bootup if needed if not connected(): # We delay this for several seconds to ensure that the other # clients are up and connected to the messagebus in order to # receive the "speak". This was sometimes happening too # quickly and the user wasn't notified what to do. Timer(5, self._do_net_check).start() Timer(60, self._hack_check_for_duplicates).start() def on_no_internet(self, event=None): if connected(): # One last check to see if connection was established return if time.time() - Enclosure._last_internet_notification < 30: # don't bother the user with multiple notifications with 30 secs return Enclosure._last_internet_notification = time.time() # TODO: This should go into EnclosureMark1 subclass of Enclosure. if has_been_paired(): # Handle the translation within that code. self.ws.emit(Message("speak", { 'utterance': "This device is not connected to the Internet. " "Either plug in a network cable or hold the " "button on top for two seconds, then select " "wifi from the menu"})) else: # enter wifi-setup mode automatically self.ws.emit(Message('system.wifi.setup', {'lang': self.lang})) def __init_serial(self): try: self.port = self.config.get("port") self.rate = self.config.get("rate") self.timeout = self.config.get("timeout") self.serial = serial.serial_for_url( url=self.port, baudrate=self.rate, timeout=self.timeout) LOG.info("Connected to: %s rate: %s timeout: %s" % (self.port, self.rate, self.timeout)) except: LOG.error("Impossible to connect to serial port: "+str(self.port)) raise def __register_events(self): self.ws.on('enclosure.mouth.events.activate', self.__register_mouth_events) self.ws.on('enclosure.mouth.events.deactivate', self.__remove_mouth_events) self.ws.on('enclosure.reset', self.__reset) self.__register_mouth_events() def __register_mouth_events(self, event=None): self.ws.on('recognizer_loop:record_begin', self.mouth.listen) self.ws.on('recognizer_loop:record_end', self.mouth.reset) self.ws.on('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.on('recognizer_loop:audio_output_end', self.mouth.reset) def __remove_mouth_events(self, event=None): self.ws.remove('recognizer_loop:record_begin', self.mouth.listen) self.ws.remove('recognizer_loop:record_end', self.mouth.reset) self.ws.remove('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.remove('recognizer_loop:audio_output_end', self.mouth.reset) def __reset(self, event=None): # Reset both the mouth and the eye elements to indicate the unit is # ready for input. self.writer.write("eyes.reset") self.writer.write("mouth.reset") def speak(self, text): self.ws.emit(Message("speak", {'utterance': text})) def run(self): try: self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() def check_for_response(self): if not self.arduino_responded: # There is nothing on the other end of the serial port # close these serial-port readers and this process self.writer.stop() self.reader.stop() self.serial.close() self.ws.close() def _handle_pairing_complete(self, Message): """ Handler for 'mycroft.paired', unmutes the mic after the pairing is complete. """ self.ws.emit(Message("mycroft.mic.unmute")) def _do_net_check(self): # TODO: This should live in the derived Enclosure, e.g. Enclosure_Mark1 LOG.info("Checking internet connection") if not connected(): # and self.conn_monitor is None: if has_been_paired(): # TODO: Enclosure/localization self.speak("This unit is not connected to the Internet. " "Either plug in a network cable or hold the " "button on top for two seconds, then select " "wifi from the menu") else: # Begin the unit startup process, this is the first time it # is being run with factory defaults. # TODO: This logic should be in Enclosure_Mark1 # TODO: Enclosure/localization # Don't listen to mic during this out-of-box experience self.ws.emit(Message("mycroft.mic.mute")) # Setup handler to unmute mic at the end of on boarding # i.e. after pairing is complete self.ws.once('mycroft.paired', self._handle_pairing_complete) self.speak(mycroft.dialog.get('mycroft.intro')) wait_while_speaking() time.sleep(2) # a pause sounds better than just jumping in # Kick off wifi-setup automatically data = {'allow_timeout': False, 'lang': self.lang} self.ws.emit(Message('system.wifi.setup', data)) def _hack_check_for_duplicates(self): # TEMPORARY HACK: Look for multiple instance of the # mycroft-speech-client and/or mycroft-skills services, which could # happen when upgrading a shipping Mark 1 from release 0.8.17 or # before. When found, force the unit to reboot. import psutil LOG.info("Hack to check for duplicate service instances") count_instances = 0 needs_reboot = False for process in psutil.process_iter(): if process.cmdline() == ['python2.7', '/usr/local/bin/mycroft-speech-client']: count_instances += 1 if (count_instances > 1): LOG.info("Duplicate mycroft-speech-client found") needs_reboot = True count_instances = 0 for process in psutil.process_iter(): if process.cmdline() == ['python2.7', '/usr/local/bin/mycroft-skills']: count_instances += 1 if (count_instances > 1): LOG.info("Duplicate mycroft-skills found") needs_reboot = True if needs_reboot: LOG.info("Hack reboot...") self.reader.process("unit.reboot") self.ws.emit(Message("enclosure.eyes.spin")) self.ws.emit(Message("enclosure.mouth.reset"))
class LocalListener(object): def __init__(self, hmm=None, lm=None, le_dict=None, lang="en-us", emitter=None, debug=False): self.lang = lang self.decoder = None self.listening = False self.emitter = emitter self.event_thread = None self.async_thread = None # get hmm, already in mycroft self.hmm = MYCROFT_ROOT_PATH + '/mycroft/client/speech/recognizer/model/en-us/hmm/' # load wav config self.audioconfig = ConfigurationManager.get() self.reset_decoder() if not debug: ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p) def py_error_handler(filename, line, function, err, fmt): ignores = [0, 2, 16, 77] if err not in ignores: print err, fmt c_error_handler = ERROR_HANDLER_FUNC(py_error_handler) @contextmanager def noalsaerr(): asound = cdll.LoadLibrary('libasound.so') asound.snd_lib_error_set_handler(c_error_handler) yield asound.snd_lib_error_set_handler(None) with noalsaerr(): self.p = pyaudio.PyAudio() else: self.p = pyaudio.PyAudio() self.stream = self.p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=1024) if self.emitter is None: self.emitter = WebsocketClient() def connect(): # Once the websocket has connected, just watch it for events self.emitter.run_forever() self.event_thread = Thread(target=connect) self.event_thread.setDaemon(True) self.event_thread.start() sleep(2) def emit(self, message, data=None, context=None): if self.emitter is not None: data = data or {} context = context or {"source": "LocalListener"} self.emitter.emit(Message(message, data, context)) def handle_record_begin(self): # If enabled, play a wave file with a short sound to audibly # indicate recording has begun. if self.audioconfig.get('confirm_listening'): file = resolve_resource_file( self.audioconfig.get('sounds').get('start_listening')) if file: play_wav(file) LOG.info("deactivating speech recognition") self.emit("recognizer_loop:sleep") self.emit("recognizer_loop:local_listener.start") self.emit('recognizer_loop:record_begin') def handle_record_end(self): print("End Recording...") self.emit('recognizer_loop:record_end') LOG.info("reactivating speech recognition") self.emit("recognizer_loop:local_listener.end") self.emit("recognizer_loop:wake_up") def reset_decoder(self, hmm=None, lm=None, le_dict=None): self.lastutt = None LOG.info("resetting decoder") lang = self.lang le_dict = le_dict or join(dirname(__file__), lang, '9794.dic') if self.lang == 'en-us': hmm = self.hmm else: hmm = hmm or join(dirname(__file__), lang, 'hmm') lm = lm or join(dirname(__file__), lang, '9794.lm') self.config = Decoder.default_config() self.config.set_string('-hmm', hmm) self.config.set_string('-lm', lm) self.config.set_string('-dict', le_dict) self.config.set_string('-logfn', '/dev/null') self.decoder = Decoder(self.config) def listen_once_async(self): LOG.info("starting async local listening") self.async_thread_ = Thread(target=self._async_listen_once) self.async_thread_.setDaemon(True) self.async_thread_.start() def listen_async(self): LOG.info("starting async local listening") self.async_thread = Thread(target=self._async_listen) self.async_thread.setDaemon(True) self.async_thread.start() def _async_listen(self): for ut in self.listen(): if ut is not None: print "emitting to bus:", ut self.emit("recognizer_loop:utterance", { "utterances": [ut.lower()], "lang": self.lang }) def _async_listen_once(self): ut = self.listen_once() if ut is not None: print "emitting to bus:", ut self.emit("recognizer_loop:utterance", { "utterances": [ut.lower()], "lang": self.lang }) def listen(self): self.reset_decoder() self.handle_record_begin() self.stream.start_stream() self.listening = True in_speech_bf = False self.decoder.start_utt() LOG.info("continuous listening") while self.listening: buf = self.stream.read(1024) if buf: self.decoder.process_raw(buf, False, False) if self.decoder.get_in_speech() != in_speech_bf: in_speech_bf = self.decoder.get_in_speech() if not in_speech_bf: self.decoder.end_utt() utt = self.decoder.hyp().hypstr self.decoder.start_utt() if utt.strip() != '': reply = utt.strip() self.lastutt = reply yield reply else: break self.shutdown() def listen_once(self): ut = self._listen_once() return ''.join(ut) def _listen_once(self): self.reset_decoder() self.handle_record_begin() self.stream.start_stream() self.listening = True in_speech_bf = False self.decoder.start_utt() LOG.info("listening once") while self.listening: buf = self.stream.read(1024) if buf: self.decoder.process_raw(buf, False, False) if self.decoder.get_in_speech() != in_speech_bf: in_speech_bf = self.decoder.get_in_speech() if not in_speech_bf: self.decoder.end_utt() utt = self.decoder.hyp().hypstr self.decoder.start_utt() if utt.strip() != '': reply = utt.strip() yield reply self.lastutt = reply self.listening = False else: break self.shutdown() def listen_numbers(self, configpath=None): LOG.info("listening for numbers") for number in self.listen_specialized( config=self.numbers_config(configpath)): yield number def listen_numbers_once(self, configpath=None): LOG.info("listening for numbers once") return self.listen_once_specialized( config=self.numbers_config(configpath)) def listen_specialized(self, dictionary=None, config=None): self.reset_decoder() if config is None: config = self.config else: LOG.info("loading custom decoder config") if dictionary is not None: LOG.info("loading custom dictionary") config.set_string('-dict', self.create_dict(dictionary)) print dictionary.keys() self.decoder = Decoder(config) self.handle_record_begin() self.stream.start_stream() self.listening = True in_speech_bf = False self.decoder.start_utt() LOG.info("continuous listening") while self.listening: buf = self.stream.read(1024) if buf: self.decoder.process_raw(buf, False, False) if self.decoder.get_in_speech() != in_speech_bf: in_speech_bf = self.decoder.get_in_speech() if not in_speech_bf: self.decoder.end_utt() utt = self.decoder.hyp().hypstr self.decoder.start_utt() if utt.strip() != '': reply = utt.strip() self.lastutt = reply yield reply else: break self.shutdown() def listen_once_specialized(self, dictionary=None, config=None): ut = self._listen_once_specialized(dictionary, config) return ''.join(ut) def _listen_once_specialized(self, dictionary=None, config=None): self.reset_decoder() if config is None: config = self.config else: LOG.info("loading custom decoder config") if dictionary is not None: LOG.info("loading custom dictionary") config.set_string('-dict', self.create_dict(dictionary)) print dictionary.keys() self.decoder = Decoder(config) self.handle_record_begin() self.stream.start_stream() self.listening = True in_speech_bf = False self.decoder.start_utt() LOG.info("listening once") while self.listening: buf = self.stream.read(1024) if buf: self.decoder.process_raw(buf, False, False) if self.decoder.get_in_speech() != in_speech_bf: in_speech_bf = self.decoder.get_in_speech() if not in_speech_bf: self.decoder.end_utt() utt = self.decoder.hyp().hypstr self.decoder.start_utt() if utt.strip() != '': reply = utt.strip() yield reply self.lastutt = reply self.listening = False else: break self.shutdown() def stop_listening(self): if self.async_thread: LOG.info('stopping async thread') self.async_thread.join(timeout=1) self.listening = False self.async_thread = None return True return False def numbers_config(self, numbers): print('running number config') numbers = numbers or join(dirname(__file__), self.lang, 'numbers.dic') if not exists(numbers): if self.lang.startswith("en"): numbers = self.create_dict({ "ONE": "W AH N", "TWO": "T UW", "THREE": "TH R IY", "FOUR": "F AO R", "FIVE": "F AY V", "SIX": "S IH K S", "SEVEN": "S EH V AH N", "EIGHT": "EY T", "NINE": "N AY N", "TEN": "T EH N" }) else: raise NotImplementedError config = self.config config.set_string('-dict', numbers) return config def create_dict(self, phonemes_dict): (fd, file_name) = tempfile.mkstemp() with fdopen(fd, 'w') as f: for key_phrase in phonemes_dict: phonemes = phonemes_dict[key_phrase] words = key_phrase.split() phoneme_groups = phonemes.split('.') for word, phoneme in zip(words, phoneme_groups): f.write(word + ' ' + phoneme + '\n') return file_name def shutdown(self): self.decoder.end_utt() self.handle_record_end() self.stream.stop_stream() self.stream.close() if self.event_thread: LOG.info('disconnecting from bus') self.event_thread.join(timeout=1) self.event_thread = None if self.async_thread: LOG.info('stopping async thread') self.async_thread.join(timeout=1) self.listening = False self.async_thread = None LOG.info('stopping local recognition') self.decoder = None self.stream = None if self.p: self.p.terminate()
def gui_main(stdscr): global scr global ws global line global log_line_lr_scroll global longest_visible_line global find_str global last_key scr = stdscr init_screen() ws = WebsocketClient() ws.on('speak', handle_speak) ws.on('message', handle_message) event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() history = [] hist_idx = -1 # index, from the bottom try: input = "" while True: draw_screen() c = scr.getch() # Convert VT100 ESC codes generated by some terminals if c == 27: c1 = scr.getch() c2 = scr.getch() if c1 == 79 and c2 == 120: c = curses.KEY_UP elif c1 == 79 and c2 == 116: c = curses.KEY_LEFT elif c1 == 79 and c2 == 114: c = curses.KEY_DOWN elif c1 == 79 and c2 == 118: c = curses.KEY_RIGHT elif c1 == 79 and c2 == 121: c = curses.KEY_PPAGE # aka PgUp elif c1 == 79 and c2 == 115: c = curses.KEY_NPAGE # aka PgDn elif c1 == 79 and c2 == 119: c = curses.KEY_HOME elif c1 == 79 and c2 == 113: c = curses.KEY_END else: c = c2 last_key = str(c)+",ESC+"+str(c1)+"+"+str(c2) else: last_key = str(c) if c == curses.KEY_ENTER or c == 10 or c == 13: # ENTER sends the typed line to be processed by Mycroft if line == "": continue if line[:1] == ":": # Lines typed like ":help" are 'commands' if handle_cmd(line[1:]) == 1: break else: # Treat this as an utterance history.append(line) chat.append(line) ws.emit(Message("recognizer_loop:utterance", {'utterances': [line.strip()], 'lang': 'en-us'})) hist_idx = -1 line = "" elif c == 16 or c == 545: # Ctrl+P or Ctrl+Left (Previous) # Move up the history stack hist_idx = clamp(hist_idx + 1, -1, len(history) - 1) if hist_idx >= 0: line = history[len(history) - hist_idx - 1] else: line = "" elif c == 14 or c == 560: # Ctrl+N or Ctrl+Right (Next) # Move down the history stack hist_idx = clamp(hist_idx - 1, -1, len(history) - 1) if hist_idx >= 0: line = history[len(history) - hist_idx - 1] else: line = "" elif c == curses.KEY_LEFT: # scroll long log lines left log_line_lr_scroll += curses.COLS // 4 elif c == curses.KEY_RIGHT: # scroll long log lines right log_line_lr_scroll -= curses.COLS // 4 if log_line_lr_scroll < 0: log_line_lr_scroll = 0 elif c == curses.KEY_HOME: # HOME scrolls log lines all the way to the start log_line_lr_scroll = longest_visible_line elif c == curses.KEY_END: # END scrolls log lines all the way to the end log_line_lr_scroll = 0 elif c == curses.KEY_UP: scroll_log(False, 1) elif c == curses.KEY_DOWN: scroll_log(True, 1) elif c == curses.KEY_NPAGE: # aka PgDn # PgDn to go down a page in the logs scroll_log(True) elif c == curses.KEY_PPAGE: # aka PgUp # PgUp to go up a page in the logs scroll_log(False) elif c == 2 or c == 550: # Ctrl+B or Ctrl+PgDn scroll_log(True, max_log_lines) elif c == 20 or c == 555: # Ctrl+T or Ctrl+PgUp scroll_log(False, max_log_lines) elif c == curses.KEY_RESIZE: # Generated by Curses when window/screen has been resized y, x = scr.getmaxyx() curses.resizeterm(y, x) # resizeterm() causes another curses.KEY_RESIZE, so # we need to capture that to prevent a loop of resizes c = scr.getch() elif c == curses.KEY_BACKSPACE or c == 127: # Backspace to erase a character in the utterance line = line[:-1] elif c == 6: # Ctrl+F (Find) line = ":find " elif c == 24: # Ctrl+X (Exit) if find_str: # End the find session find_str = None rebuild_filtered_log() elif curses.ascii.isascii(c): # Accept typed character in the utterance line += chr(c) # DEBUG: Uncomment the following code to see what key codes # are generated when an unknown key is pressed. # else: # line += str(c) except KeyboardInterrupt as e: # User hit Ctrl+C to quit pass except KeyboardInterrupt as e: LOG.exception(e) finally: scr.erase() scr.refresh() scr = None pass
class TestMessagebusMethods(unittest.TestCase): """This class is for testing the messsagebus. It currently only tests send and receive. The tests could include more. """ def setUp(self): """ This sets up for testing the message buss This requires starting the mycroft service and creating two WebsocketClient object to talk with eachother. Not this is threaded and will require cleanup """ # start the mycroft service. and get the pid of the script. self.pid = Popen(["python", "mycroft/messagebus/service/main.py"]).pid # delay to allow the service to start up. time.sleep(10) # Create the two web clients self.ws1 = WebsocketClient() self.ws2 = WebsocketClient() # init the flags for handler's self.handle1 = False self.handle2 = False # Start threads to handle websockets Thread(target=self.ws1.run_forever).start() Thread(target=self.ws2.run_forever).start() # Sleep to give the websockets to startup before adding handlers time.sleep(10) # Setup handlers for each of the messages. self.ws1.on('ws1.message', self.onHandle1) self.ws2.on('ws2.message', self.onHandle2) def onHandle1(self, event): """This is the handler for ws1.message This for now simply sets a flag to true when received. Args: event(Message): this is the message received """ self.handle1 = True def onHandle2(self, event): """This is the handler for ws2.message This for now simply sets a flag to true when received. Args: event(Message): this is the message received """ self.handle2 = True def tearDown(self): """This is the clean up for the tests This will close the websockets ending the threads then kill the mycroft service that was started in setUp. """ self.ws1.close() self.ws2.close() retcode = call(["kill", "-9", str(self.pid)]) def test_ClientServer(self): """This is the test to send a message from each of the websockets to the other. """ # Send the messages self.ws2.emit(Message('ws1.message')) self.ws1.emit(Message('ws2.message')) # allow time for messages to be processed time.sleep(10) # Check that both of the handlers were called. self.assertTrue(self.handle1) self.assertTrue(self.handle2)
class Enclosure(object): """ Serves as a communication interface between Arduino and Mycroft Core. ``Enclosure`` initializes and aggregates all enclosures implementation. E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino`` It also listens to the basis events in order to perform those core actions on the unit. E.g. Start and Stop talk animation """ _last_internet_notification = 0 def __init__(self): self.ws = WebsocketClient() self.ws.on("open", self.on_ws_open) Configuration.init(self.ws) global_config = Configuration.get() self.lang = global_config['lang'] self.config = global_config.get("enclosure") self.__init_serial() self.reader = EnclosureReader(self.serial, self.ws, self.lang) self.writer = EnclosureWriter(self.serial, self.ws) # initiates the web sockets on display manager # NOTE: this is a temporary place to initiate display manager sockets initiate_display_manager_ws() def on_ws_open(self, event=None): # Mark 1 auto-detection: # # Prepare to receive message when the Arduino responds to the # following "system.version" self.ws.on("enclosure.started", self.on_arduino_responded) self.arduino_responded = False # Send a message to the Arduino across the serial line asking # for a reply with version info. self.writer.write("system.version") # Start a 5 second timer. If the serial port hasn't received # any acknowledgement of the "system.version" within those # 5 seconds, assume there is nothing on the other end (e.g. # we aren't running a Mark 1 with an Arduino) Timer(5, self.check_for_response).start() # Notifications from mycroft-core self.ws.on("enclosure.notify.no_internet", self.on_no_internet) def on_arduino_responded(self, event=None): self.eyes = EnclosureEyes(self.ws, self.writer) self.mouth = EnclosureMouth(self.ws, self.writer) self.system = EnclosureArduino(self.ws, self.writer) self.weather = EnclosureWeather(self.ws, self.writer) self.__register_events() self.__reset() self.arduino_responded = True # verify internet connection and prompt user on bootup if needed if not connected(): # We delay this for several seconds to ensure that the other # clients are up and connected to the messagebus in order to # receive the "speak". This was sometimes happening too # quickly and the user wasn't notified what to do. Timer(5, self._do_net_check).start() Timer(60, self._hack_check_for_duplicates).start() def on_no_internet(self, event=None): if connected(): # One last check to see if connection was established return if time.time() - Enclosure._last_internet_notification < 30: # don't bother the user with multiple notifications with 30 secs return Enclosure._last_internet_notification = time.time() # TODO: This should go into EnclosureMark1 subclass of Enclosure. if has_been_paired(): # Handle the translation within that code. self.ws.emit( Message( "speak", { 'utterance': "This device is not connected to the Internet. " "Either plug in a network cable or hold the " "button on top for two seconds, then select " "wifi from the menu" })) else: # enter wifi-setup mode automatically self.ws.emit(Message('system.wifi.setup', {'lang': self.lang})) def __init_serial(self): try: self.port = self.config.get("port") self.rate = self.config.get("rate") self.timeout = self.config.get("timeout") self.serial = serial.serial_for_url(url=self.port, baudrate=self.rate, timeout=self.timeout) LOG.info("Connected to: %s rate: %s timeout: %s" % (self.port, self.rate, self.timeout)) except: LOG.error("Impossible to connect to serial port: " + str(self.port)) raise def __register_events(self): self.ws.on('enclosure.mouth.events.activate', self.__register_mouth_events) self.ws.on('enclosure.mouth.events.deactivate', self.__remove_mouth_events) self.ws.on('enclosure.reset', self.__reset) self.__register_mouth_events() def __register_mouth_events(self, event=None): self.ws.on('recognizer_loop:record_begin', self.mouth.listen) self.ws.on('recognizer_loop:record_end', self.mouth.reset) self.ws.on('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.on('recognizer_loop:audio_output_end', self.mouth.reset) def __remove_mouth_events(self, event=None): self.ws.remove('recognizer_loop:record_begin', self.mouth.listen) self.ws.remove('recognizer_loop:record_end', self.mouth.reset) self.ws.remove('recognizer_loop:audio_output_start', self.mouth.talk) self.ws.remove('recognizer_loop:audio_output_end', self.mouth.reset) def __reset(self, event=None): # Reset both the mouth and the eye elements to indicate the unit is # ready for input. self.writer.write("eyes.reset") self.writer.write("mouth.reset") def speak(self, text): self.ws.emit(Message("speak", {'utterance': text})) def run(self): try: self.ws.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop() def check_for_response(self): if not self.arduino_responded: # There is nothing on the other end of the serial port # close these serial-port readers and this process self.writer.stop() self.reader.stop() self.serial.close() self.ws.close() def _handle_pairing_complete(self, Message): """ Handler for 'mycroft.paired', unmutes the mic after the pairing is complete. """ self.ws.emit(Message("mycroft.mic.unmute")) def _do_net_check(self): # TODO: This should live in the derived Enclosure, e.g. Enclosure_Mark1 LOG.info("Checking internet connection") if not connected(): # and self.conn_monitor is None: if has_been_paired(): # TODO: Enclosure/localization self.speak("This unit is not connected to the Internet. " "Either plug in a network cable or hold the " "button on top for two seconds, then select " "wifi from the menu") else: # Begin the unit startup process, this is the first time it # is being run with factory defaults. # TODO: This logic should be in Enclosure_Mark1 # TODO: Enclosure/localization # Don't listen to mic during this out-of-box experience self.ws.emit(Message("mycroft.mic.mute")) # Setup handler to unmute mic at the end of on boarding # i.e. after pairing is complete self.ws.once('mycroft.paired', self._handle_pairing_complete) self.speak(mycroft.dialog.get('mycroft.intro')) wait_while_speaking() time.sleep(2) # a pause sounds better than just jumping in # Kick off wifi-setup automatically data = {'allow_timeout': False, 'lang': self.lang} self.ws.emit(Message('system.wifi.setup', data)) def _hack_check_for_duplicates(self): # TEMPORARY HACK: Look for multiple instance of the # mycroft-speech-client and/or mycroft-skills services, which could # happen when upgrading a shipping Mark 1 from release 0.8.17 or # before. When found, force the unit to reboot. import psutil LOG.info("Hack to check for duplicate service instances") count_instances = 0 needs_reboot = False for process in psutil.process_iter(): if process.cmdline() == [ 'python2.7', '/usr/local/bin/mycroft-speech-client' ]: count_instances += 1 if (count_instances > 1): LOG.info("Duplicate mycroft-speech-client found") needs_reboot = True count_instances = 0 for process in psutil.process_iter(): if process.cmdline() == [ 'python2.7', '/usr/local/bin/mycroft-skills' ]: count_instances += 1 if (count_instances > 1): LOG.info("Duplicate mycroft-skills found") needs_reboot = True if needs_reboot: LOG.info("Hack reboot...") self.reader.process("unit.reboot") self.ws.emit(Message("enclosure.eyes.spin")) self.ws.emit(Message("enclosure.mouth.reset"))
class Enclosure(): 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.active_namespaces = [] 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 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) 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? 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]) 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 if 'index' in data: index = data['index'] else: index = 0 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) ###################################################################### # GUI client socket # # 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 DEBUG("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, self) 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", { "port": self.GUIs[gui_id].port, "gui_id": gui_id })) def callback_disconnect(self, gui_id): DEBUG("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
class WiFi: NAME = "WiFiClient" def __init__(self): self.iface = pyw.winterfaces()[0] self.ap = AccessPoint(self.iface) self.server = None self.client = WebsocketClient() self.enclosure = EnclosureAPI(self.client) self.config = ConfigurationManager.get().get(self.NAME) self.init_events() self.first_setup() def init_events(self): self.client.on('mycroft.wifi.start', self.start) self.client.on('mycroft.wifi.stop', self.stop) self.client.on('mycroft.wifi.scan', self.scan) self.client.on('mycroft.wifi.connect', self.connect) def first_setup(self): if str2bool(self.config.get('setup')): self.start() def start(self, event=None): LOG.info("Starting access point...") self.client.emit( Message( "speak", metadata={'utterance': "Initializing wireless setup mode."})) self.ap.up() if not self.server: self.server = WebServer(self.ap.ip, 80) self.server.start() self.enclosure.mouth_text(self.ap.password) LOG.info("Access point started!\n%s" % self.ap.__dict__) def scan(self, event=None): LOG.info("Scanning wifi connections...") networks = {} status = self.get_status() for cell in Cell.all(self.iface): update = True ssid = cell.ssid quality = self.get_quality(cell.quality) if networks.__contains__(ssid): update = networks.get(ssid).get("quality") < quality if update and ssid: networks[ssid] = { 'quality': quality, 'encrypted': cell.encrypted, 'connected': self.is_connected(ssid, status) } self.client.emit( Message("mycroft.wifi.scanned", {'networks': networks})) LOG.info("Wifi connections scanned!\n%s" % networks) @staticmethod def get_quality(quality): values = quality.split("/") return float(values[0]) / float(values[1]) def connect(self, event=None): if event and event.metadata: ssid = event.metadata.get("ssid") connected = self.is_connected(ssid) if connected: LOG.warn("Mycroft is already connected to %s" % ssid) else: self.disconnect() LOG.info("Connecting to: %s" % ssid) nid = wpa(self.iface, 'add_network') wpa(self.iface, 'set_network', nid, 'ssid', '"' + ssid + '"') if event.metadata.__contains__("pass"): psk = '"' + event.metadata.get("pass") + '"' wpa(self.iface, 'set_network', nid, 'psk', psk) else: wpa(self.iface, 'set_network', nid, 'key_mgmt', 'NONE') wpa(self.iface, 'enable', nid) connected = self.get_connected(ssid) if connected: wpa(self.iface, 'save_config') ConfigurationManager.set(self.NAME, 'setup', False, True) self.client.emit( Message("mycroft.wifi.connected", {'connected': connected})) LOG.info("Connection status for %s = %s" % (ssid, connected)) def disconnect(self): status = self.get_status() nid = status.get("id") if nid: ssid = status.get("ssid") wpa(self.iface, 'disable', nid) LOG.info("Disconnecting %s id: %s" % (ssid, nid)) def get_status(self): res = cli('wpa_cli', '-i', self.iface, 'status') out = str(res.get("stdout")) if out: return dict(o.split("=") for o in out.split("\n")[:-1]) return {} def get_connected(self, ssid, retry=5): connected = self.is_connected(ssid) while not connected and retry > 0: sleep(2) retry -= 1 connected = self.is_connected(ssid) return connected def is_connected(self, ssid, status=None): status = status or self.get_status() state = status.get("wpa_state") return status.get("ssid") == ssid and state == "COMPLETED" def stop(self, event=None): LOG.info("Stopping access point...") self.ap.down() if self.server: self.server.server.shutdown() self.server.server.server_close() self.server.join() self.server = None LOG.info("Access point stopped!") def run(self): try: self.client.run_forever() except Exception as e: LOG.error("Error: {0}".format(e)) self.stop()
class Enclosure: """ Serves as a communication interface between Arduino and Mycroft Core. This interface is currently designed to shut itself down when run on a Raspberry Pi that is not connected to a Mycroft Mark 1 board on the serial port. So it can safely be started on any Raspberry Pi. ``Enclosure`` initializes and aggregates all enclosures implementation. E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino`` It also listens to the basis events in order to perform those core actions on the unit. E.g. Start and Stop talk animation """ def __init__(self): resetSelf = False try: self.config = ConfigurationManager.get().get("enclosure") platform = self.config.get('platform') if platform is None: # Use the serial port to check if this is a Mycroft # Mark 1 unit. platform = self.detect_platform() if platform == 'unknown': # Since this is a semi-permanent detection, be # certain! Sometimes noise on the serial line # causes a faulty mis-detection on a real # Mycroft Mark 1 platform = self.detect_platform() ConfigurationManager.set('enclosure', 'platform', platform) # After a platform detection, the system is usually # already active, so the message from the loop # has already gone by on the messagebus. So self reset. resetSelf = True except Exception as e: self.disconnect() raise Exception("Exception: Unable to determine platform\n" + str(e)) LOGGER.info("Platform = '" + platform + "'") if platform == "mycroft_mark_1": # We are a mycroft_mark_1 unit, start up the # enclosure client to communicate over the # serial port self.__init_serial() else: self.disconnect() raise Exception("Exception: Not a Mycroft Mark 1, shutting down") self.client = WebsocketClient() self.reader = EnclosureReader(self.serial, self.client) self.writer = EnclosureWriter(self.serial, self.client) # Create helpers to handle the various parts of the enclosure self.eyes = EnclosureEyes(self.client, self.writer) self.mouth = EnclosureMouth(self.client, self.writer) self.system = EnclosureArduino(self.client, self.writer) # TODO: Remove EnclosureWeather once the Skill can send images # directly to the EnclosureAPI. self.weather = EnclosureWeather(self.client, self.writer) self.__register_events() if resetSelf: self.__handle_reset(None) def detect_platform(self): LOGGER.info("Auto-detecting platform") try: self.__init_serial() # Thoroughly flush the serial port. Since this happens # right after boot, sometimes there is junk on the serial # port line from electrical noise on a Pi. time.sleep(3) self.serial.flushInput() self.serial.flushOutput() # Write "system.ping" self.serial.write("system.ping") time.sleep(1) # Now check to see if we got a response from the ping command # command we just sent. Remember, there might not be a Mark 1 # Arduino on the other side of the serial port. data = self.serial.readline() LOGGER.info("Serial response: '" + data + "'") # A Mycroft Arduino echos the output to the serial line # prefixed by "Command: ". This assumes only a Mycroft # Arduino responds to this, which is probably a dubious # assumption, but a decent enough auto-detect for now. if "Command: system.ping" in data: # We have a Mycroft Mark 1! # The Arduino returns a version number in the response # with recent builds, but not with older builds. # Empty the serial queue of all responses to the ping. time.sleep(0.5) self.serial.flushInput() return "mycroft_mark_1" except: pass # Likely running on a generic Raspberry Pi or Linux desktop return "unknown" def setup(self): must_upload = self.config.get('must_upload') if must_upload is not None and str2bool(must_upload): ConfigurationManager.set('enclosure', 'must_upload', False) time.sleep(5) self.client.emit(Message("speak", metadata={ 'utterance': "I am currently uploading to the arduino."})) self.client.emit(Message("speak", metadata={ 'utterance': "I will be finished in just a moment."})) self.upload_hex() self.client.emit(Message("speak", metadata={ 'utterance': "Arduino programing complete."})) must_start_test = self.config.get('must_start_test') if must_start_test is not None and str2bool(must_start_test): ConfigurationManager.set('enclosure', 'must_start_test', False) time.sleep(0.5) # Ensure arduino has booted self.client.emit(Message("speak", metadata={ 'utterance': "Begining hardware self test."})) self.writer.write("test.begin") @staticmethod def upload_hex(): old_path = os.getcwd() try: os.chdir('/opt/enclosure/') subprocess.check_call('./upload.sh') finally: os.chdir(old_path) def __init_serial(self): if getattr(self, 'serial', None) is not None: return # already initialized LOGGER.info("Opening serial port") try: self.port = self.config.get("port") self.rate = int(self.config.get("rate")) self.timeout = int(self.config.get("timeout")) self.serial = serial.serial_for_url( url=self.port, baudrate=self.rate, timeout=self.timeout) LOGGER.info( "Connected to: " + self.port + " rate: " + str(self.rate) + " timeout: " + str(self.timeout)) except: LOGGER.error( "It is not possible to connect to serial port: " + self.port) raise def __register_events(self): self.client.on('mycroft.paired', self.__update_events) self.client.on('enclosure.reset', self.__handle_reset) self.client.on('enclosure.mouth.listeners', self.__mouth_listeners) self.__register_mouth_events() def __mouth_listeners(self, event=None): if event and event.metadata: active = event.metadata['active'] if active: self.__register_mouth_events() else: self.__remove_mouth_events() def __register_mouth_events(self): self.client.on('recognizer_loop:record_begin', self.mouth.listen) self.client.on('recognizer_loop:record_end', self.mouth.reset) self.client.on('recognizer_loop:audio_output_start', self.mouth.talk) self.client.on('recognizer_loop:audio_output_end', self.mouth.reset) def __remove_mouth_events(self): self.client.remove('recognizer_loop:record_begin', self.mouth.listen) self.client.remove('recognizer_loop:record_end', self.mouth.reset) self.client.remove('recognizer_loop:audio_output_start', self.mouth.talk) self.client.remove('recognizer_loop:audio_output_end', self.mouth.reset) def __handle_reset(self, event=None): # Reset both the mouth and the eye elements to indicate the unit is # ready for input. self.writer.write("eyes.reset") self.writer.write("mouth.reset") def __update_events(self, event=None): if event and event.metadata: if event.metadata.get('paired', False): self.__register_mouth_events() else: self.__remove_mouth_events() def run(self): try: self.client.run_forever() except Exception as e: LOGGER.error("Client error: {0}".format(e)) self.stop() def disconnect(self): if getattr(self, 'serial', None) is not None: self.serial.close() self.serial = None def stop(self): self.writer.stop() self.reader.stop() self.disconnect()
def gui_main(stdscr): global scr global bus global line global log_line_lr_scroll global longest_visible_line global find_str global last_key global history global screen_lock scr = stdscr init_screen() scr.keypad(1) scr.notimeout(1) bus = WebsocketClient() # Mycroft messagebus connection bus.on('speak', handle_speak) bus.on('message', handle_message) bus.on('recognizer_loop:utterance', handle_utterance) event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() gui_thread = ScreenDrawThread() gui_thread.setDaemon(True) # this thread won't prevent prog from exiting gui_thread.start() hist_idx = -1 # index, from the bottom c = 0 try: while True: set_screen_dirty() try: c = scr.get_wch() # unicode char or int for special keys except KeyboardInterrupt: # User hit Ctrl+C to quit if find_str: # End the find session find_str = None rebuild_filtered_log() continue # Consumed the Ctrl+C, get next character else: c = 24 # treat as Ctrl+X (Exit) except curses.error: # This happens in odd cases, such as when you Ctrl+Z suspend # the CLI and then resume. Curses fails on get_wch(). continue if isinstance(c, int): code = c else: code = ord(c) # Convert VT100 ESC codes generated by some terminals if code == 27: # NOTE: Not sure exactly why, but the screen can get corrupted # if we draw to the screen while doing a scr.getch(). So # lock screen updates until the VT100 sequence has been # completely read. with screen_lock: scr.timeout(0) c1 = -1 start = time.time() while c1 == -1: c1 = scr.getch() if time.time()-start > 1: break # 1 second timeout waiting for ESC code c2 = -1 while c2 == -1: c2 = scr.getch() if time.time()-start > 1: # 1 second timeout break # 1 second timeout waiting for ESC code scr.timeout(-1) if c1 == 79 and c2 == 120: c = curses.KEY_UP elif c1 == 79 and c2 == 116: c = curses.KEY_LEFT elif c1 == 79 and c2 == 114: c = curses.KEY_DOWN elif c1 == 79 and c2 == 118: c = curses.KEY_RIGHT elif c1 == 79 and c2 == 121: c = curses.KEY_PPAGE # aka PgUp elif c1 == 79 and c2 == 115: c = curses.KEY_NPAGE # aka PgDn elif c1 == 79 and c2 == 119: c = curses.KEY_HOME elif c1 == 79 and c2 == 113: c = curses.KEY_END else: c = c1 if c1 != -1: last_key = str(c) + ",ESC+" + str(c1) + "+" + str(c2) code = c else: last_key = "ESC" else: if code < 33: last_key = str(code) else: last_key = str(code) if code == 27: # Hitting ESC twice clears the entry line hist_idx = -1 line = "" elif c == curses.KEY_RESIZE: # Generated by Curses when window/screen has been resized y, x = scr.getmaxyx() curses.resizeterm(y, x) # resizeterm() causes another curses.KEY_RESIZE, so # we need to capture that to prevent a loop of resizes c = scr.get_wch() elif screen_mode == SCR_HELP: # in Help mode, any key goes to next page show_next_help() continue elif c == '\n' or code == 10 or code == 13 or code == 343: # ENTER sends the typed line to be processed by Mycroft if line == "": continue if line[:1] == ":": # Lines typed like ":help" are 'commands' if handle_cmd(line[1:]) == 1: break else: # Treat this as an utterance bus.emit(Message("recognizer_loop:utterance", {'utterances': [line.strip()], 'lang': 'en-us'})) hist_idx = -1 line = "" elif code == 16 or code == 545: # Ctrl+P or Ctrl+Left (Previous) # Move up the history stack hist_idx = clamp(hist_idx + 1, -1, len(history) - 1) if hist_idx >= 0: line = history[len(history) - hist_idx - 1] else: line = "" elif code == 14 or code == 560: # Ctrl+N or Ctrl+Right (Next) # Move down the history stack hist_idx = clamp(hist_idx - 1, -1, len(history) - 1) if hist_idx >= 0: line = history[len(history) - hist_idx - 1] else: line = "" elif c == curses.KEY_LEFT: # scroll long log lines left log_line_lr_scroll += curses.COLS // 4 elif c == curses.KEY_RIGHT: # scroll long log lines right log_line_lr_scroll -= curses.COLS // 4 if log_line_lr_scroll < 0: log_line_lr_scroll = 0 elif c == curses.KEY_HOME: # HOME scrolls log lines all the way to the start log_line_lr_scroll = longest_visible_line elif c == curses.KEY_END: # END scrolls log lines all the way to the end log_line_lr_scroll = 0 elif c == curses.KEY_UP: scroll_log(False, 1) elif c == curses.KEY_DOWN: scroll_log(True, 1) elif c == curses.KEY_NPAGE: # aka PgDn # PgDn to go down a page in the logs scroll_log(True) elif c == curses.KEY_PPAGE: # aka PgUp # PgUp to go up a page in the logs scroll_log(False) elif code == 2 or code == 550: # Ctrl+B or Ctrl+PgDn scroll_log(True, max_log_lines) elif code == 20 or code == 555: # Ctrl+T or Ctrl+PgUp scroll_log(False, max_log_lines) elif code == curses.KEY_BACKSPACE or code == 127: # Backspace to erase a character in the utterance line = line[:-1] elif code == 6: # Ctrl+F (Find) line = ":find " elif code == 18: # Ctrl+R (Redraw) scr.erase() elif code == 24: # Ctrl+X (Exit) if find_str: # End the find session find_str = None rebuild_filtered_log() else: break elif code > 31 and isinstance(c, str): # Accept typed character in the utterance line += c finally: scr.erase() scr.refresh() scr = None
class Enclosure: """ Serves as a communication interface between Arduino and Mycroft Core. ``Enclosure`` initializes and aggregates all enclosures implementation. E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino`` It also listens to the basis events in order to perform those core actions on the unit. E.g. Start and Stop talk animation """ def __init__(self): self.__init_serial() self.client = WebsocketClient() self.reader = EnclosureReader(self.serial, self.client) self.writer = EnclosureWriter(self.serial, self.client) self.eyes = EnclosureEyes(self.client, self.writer) self.mouth = EnclosureMouth(self.client, self.writer) self.system = EnclosureArduino(self.client, self.writer) self.weather = EnclosureWeather(self.client, self.writer) self.__register_events() def setup(self): must_upload = self.config.get('must_upload') if must_upload is not None and str2bool(must_upload): ConfigurationManager.set('enclosure', 'must_upload', False) time.sleep(5) self.client.emit(Message("speak", metadata={ 'utterance': "I am currently uploading to the arduino."})) self.client.emit(Message("speak", metadata={ 'utterance': "I will be finished in just a moment."})) self.upload_hex() self.client.emit(Message("speak", metadata={ 'utterance': "Arduino programing complete."})) must_start_test = self.config.get('must_start_test') if must_start_test is not None and str2bool(must_start_test): ConfigurationManager.set('enclosure', 'must_start_test', False) time.sleep(0.5) # Ensure arduino has booted self.client.emit(Message("speak", metadata={ 'utterance': "Begining hardware self test."})) self.writer.write("test.begin") @staticmethod def upload_hex(): old_path = os.getcwd() try: os.chdir('/opt/enclosure/') subprocess.check_call('./upload.sh') finally: os.chdir(old_path) def __init_serial(self): try: self.config = ConfigurationManager.get().get("enclosure") self.port = self.config.get("port") self.rate = int(self.config.get("rate")) self.timeout = int(self.config.get("timeout")) self.serial = serial.serial_for_url( url=self.port, baudrate=self.rate, timeout=self.timeout) LOGGER.info( "Connected to: " + self.port + " rate: " + str(self.rate) + " timeout: " + str(self.timeout)) except: LOGGER.error( "It is not possible to connect to serial port: " + self.port) raise def __register_events(self): self.client.on('mycroft.paired', self.__update_events) self.client.on('enclosure.mouth.listeners', self.__mouth_listeners) self.__register_mouth_events() def __mouth_listeners(self, event=None): if event and event.metadata: active = event.metadata['active'] if active: self.__register_mouth_events() else: self.__remove_mouth_events() def __register_mouth_events(self): self.client.on('recognizer_loop:record_begin', self.mouth.listen) self.client.on('recognizer_loop:record_end', self.mouth.reset) self.client.on('recognizer_loop:audio_output_start', self.mouth.talk) self.client.on('recognizer_loop:audio_output_end', self.mouth.reset) def __remove_mouth_events(self): self.client.remove('recognizer_loop:record_begin', self.mouth.listen) self.client.remove('recognizer_loop:record_end', self.mouth.reset) self.client.remove('recognizer_loop:audio_output_start', self.mouth.talk) self.client.remove('recognizer_loop:audio_output_end', self.mouth.reset) def __update_events(self, event=None): if event and event.metadata: if event.metadata.get('paired', False): self.__register_mouth_events() else: self.__remove_mouth_events() def run(self): try: self.client.run_forever() except Exception as e: LOGGER.error("Client error: {0}".format(e)) self.stop() def stop(self): self.writer.stop() self.reader.stop() self.serial.close()
class EnclosureTemplate(object): """ This base class is intended to be used to interface with the hardware that is running Mycroft. It exposes all possible commands which can be sent to a Mycroft enclosure implementation. """ def __init__(self, bus=None, name=""): """ Args: bus: name: """ if bus is None: self.bus = WebsocketClient() self.event_thread = Thread(target=self._connect) self.event_thread.setDaemon(True) self.event_thread.start() else: self.bus = bus self.log = LOG self.name = name self.bus.on("open", self.on_ws_open) self.bus.on("enclosure.reset", self.reset) # enclosure commands for Mycroft's Hardware. self.bus.on("enclosure.system.reset", self.system_reset) self.bus.on("enclosure.system.mute", self.system_mute) self.bus.on("enclosure.system.unmute", self.system_unmute) self.bus.on("enclosure.system.blink", self.system_blink) # enclosure commands for eyes self.bus.on('enclosure.eyes.on', self.eyes_on) self.bus.on('enclosure.eyes.off', self.eyes_off) self.bus.on('enclosure.eyes.blink', self.eyes_blink) self.bus.on('enclosure.eyes.narrow', self.eyes_narrow) self.bus.on('enclosure.eyes.look', self.eyes_look) self.bus.on('enclosure.eyes.color', self.eyes_color) self.bus.on('enclosure.eyes.level', self.eyes_brightness) self.bus.on('enclosure.eyes.volume', self.eyes_volume) self.bus.on('enclosure.eyes.spin', self.eyes_spin) self.bus.on('enclosure.eyes.timedspin', self.eyes_timed_spin) self.bus.on('enclosure.eyes.reset', self.eyes_reset) self.bus.on('enclosure.eyes.setpixel', self.eyes_set_pixel) self.bus.on('enclosure.eyes.fill', self.eyes_fill) # enclosure commands for mouth self.bus.on("enclosure.mouth.reset", self.mouth_reset) self.bus.on("enclosure.mouth.talk", self.mouth_talk) self.bus.on("enclosure.mouth.think", self.mouth_think) self.bus.on("enclosure.mouth.listen", self.mouth_listen) self.bus.on("enclosure.mouth.smile", self.mouth_smile) self.bus.on("enclosure.mouth.viseme", self.mouth_viseme) self.bus.on("enclosure.mouth.text", self.mouth_text) self.bus.on("enclosure.mouth.display", self.mouth_display) self.bus.on("enclosure.mouth.events.activate", self.activate_mouth_events) self.bus.on("enclosure.mouth.events.deactivate", self.deactivate_mouth_events) # enclosure commands for weather display self.bus.on("enclosure.weather.display", self.weather_display) # enclosure commands for events self.bus.on("mycroft.awoken", self.handle_awake) self.bus.on("recognizer_loop:sleep", self.handle_sleep) self.bus.on("speak", self.handle_speak) self.bus.on('recognizer_loop:record_begin', self.record_begin) self.bus.on('recognizer_loop:record_end', self.record_end) self.bus.on('mycroft.audio.speech.start', self.talk_start) self.bus.on("enclosure.notify.no_internet", self.on_no_internet) self.activate_mouth_events() def shutdown(self): """ """ self.bus.remove("enclosure.reset", self.reset) self.bus.remove("enclosure.system.reset", self.system_reset) self.bus.remove("enclosure.system.mute", self.system_mute) self.bus.remove("enclosure.system.unmute", self.system_unmute) self.bus.remove("enclosure.system.blink", self.system_blink) self.bus.remove("enclosure.eyes.on", self.eyes_on) self.bus.remove("enclosure.eyes.off", self.eyes_off) self.bus.remove("enclosure.eyes.blink", self.eyes_blink) self.bus.remove("enclosure.eyes.narrow", self.eyes_narrow) self.bus.remove("enclosure.eyes.look", self.eyes_look) self.bus.remove("enclosure.eyes.color", self.eyes_color) self.bus.remove("enclosure.eyes.brightness", self.eyes_brightness) self.bus.remove("enclosure.eyes.reset", self.eyes_reset) self.bus.remove("enclosure.eyes.timedspin", self.eyes_timed_spin) self.bus.remove("enclosure.eyes.volume", self.eyes_volume) self.bus.remove("enclosure.eyes.spin", self.eyes_spin) self.bus.remove("enclosure.eyes.set_pixel", self.eyes_set_pixel) self.bus.remove("enclosure.mouth.reset", self.mouth_reset) self.bus.remove("enclosure.mouth.talk", self.mouth_talk) self.bus.remove("enclosure.mouth.think", self.mouth_think) self.bus.remove("enclosure.mouth.listen", self.mouth_listen) self.bus.remove("enclosure.mouth.smile", self.mouth_smile) self.bus.remove("enclosure.mouth.viseme", self.mouth_viseme) self.bus.remove("enclosure.mouth.text", self.mouth_text) self.bus.remove("enclosure.mouth.display", self.mouth_display) self.bus.remove("enclosure.mouth.events.activate", self.activate_mouth_events) self.bus.remove("enclosure.mouth.events.deactivate", self.deactivate_mouth_events) self.bus.remove("enclosure.weather.display", self.weather_display) self.bus.remove("mycroft.awoken", self.handle_awake) self.bus.remove("recognizer_loop:sleep", self.handle_sleep) self.bus.remove("speak", self.handle_speak) self.bus.remove('recognizer_loop:record_begin', self.record_begin) self.bus.remove('recognizer_loop:record_end', self.record_end) self.bus.remove('recognizer_loop:audio_output_start', self.talk_start) self.bus.remove("enclosure.notify.no_internet", self.on_no_internet) self.deactivate_mouth_events() def on_no_internet(self, message=None): """ Args: message: """ pass def on_ws_open(self): """ """ pass def speak(self, text): """ Args: text: """ self.bus.emit(Message("speak", {'utterance': text})) def run(self): ''' start enclosure ''' while True: time.sleep(1) def record_begin(self, message=None): ''' listening started ''' pass def record_end(self, message=None): ''' listening ended ''' pass def talk_start(self, message=None): ''' speaking started ''' pass def handle_awake(self, message=None): ''' handle wakeup animation ''' pass def handle_sleep(self, message=None): ''' handle naptime animation ''' # TODO naptime skill animation should be handled here pass def handle_speak(self, message=None): ''' handle speak messages, intended for enclosures that disregard visemes ''' pass def _connect(self): """ """ # Once the websocket has connected, just watch it for speak events self.bus.run_forever() def reset(self, message=None): """The enclosure should restore itself to a started state. Typically this would be represented by the eyes being 'open' and the mouth reset to its default (smile or blank). """ pass def system_reset(self, message=None): """The enclosure hardware should reset any CPUs, etc.""" pass def system_mute(self, message=None): """Mute (turn off) the system speaker.""" pass def system_unmute(self, message=None): """Unmute (turn on) the system speaker.""" pass def system_blink(self, message=None): """The 'eyes' should blink the given number of times. Args: times (int): number of times to blink """ pass def eyes_on(self, message=None): """Illuminate or show the eyes.""" pass def eyes_off(self, message=None): """Turn off or hide the eyes.""" pass def eyes_fill(self, message=None): pass def eyes_blink(self, message=None): """Make the eyes blink Args: side (str): 'r', 'l', or 'b' for 'right', 'left' or 'both' """ pass def eyes_narrow(self, message=None): """Make the eyes look narrow, like a squint""" pass def eyes_look(self, message=None): """Make the eyes look to the given side Args: side (str): 'r' for right 'l' for left 'u' for up 'd' for down 'c' for crossed """ pass def eyes_color(self, message=None): """Change the eye color to the given RGB color Args: r (int): 0-255, red value g (int): 0-255, green value b (int): 0-255, blue value """ pass def eyes_brightness(self, message=None): """Set the brightness of the eyes in the display. Args: level (int): 1-30, bigger numbers being brighter """ pass def eyes_reset(self, message=None): """Restore the eyes to their default (ready) state.""" pass def eyes_timed_spin(self, message=None): """Make the eyes 'roll' for the given time. Args: length (int): duration in milliseconds of roll, None = forever """ pass def eyes_volume(self, message=None): """Indicate the volume using the eyes Args: volume (int): 0 to 11 """ pass def eyes_spin(self, message=None): """ Args: """ pass def eyes_set_pixel(self, message=None): """ Args: """ pass def mouth_reset(self, message=None): """Restore the mouth display to normal (blank)""" pass def mouth_talk(self, message=None): """Show a generic 'talking' animation for non-synched speech""" pass def mouth_think(self, message=None): """Show a 'thinking' image or animation""" pass def mouth_listen(self, message=None): """Show a 'thinking' image or animation""" pass def mouth_smile(self, message=None): """Show a 'smile' image or animation""" pass def mouth_viseme(self, message=None): """Display a viseme mouth shape for synched speech Args: code (int): 0 = shape for sounds like 'y' or 'aa' 1 = shape for sounds like 'aw' 2 = shape for sounds like 'uh' or 'r' 3 = shape for sounds like 'th' or 'sh' 4 = neutral shape for no sound 5 = shape for sounds like 'f' or 'v' 6 = shape for sounds like 'oy' or 'ao' """ pass def mouth_text(self, message=None): """Display text (scrolling as needed) Args: text (str): text string to display """ pass def mouth_display(self, message=None): """Display images on faceplate. Currently supports images up to 16x8, or half the face. You can use the 'x' parameter to cover the other half of the faceplate. Args: img_code (str): text string that encodes a black and white image x (int): x offset for image y (int): y offset for image refresh (bool): specify whether to clear the faceplate before displaying the new image or not. Useful if you'd like to display muliple images on the faceplate at once. """ pass def weather_display(self, message=None): """Show a the temperature and a weather icon Args: img_code (char): one of the following icon codes 0 = sunny 1 = partly cloudy 2 = cloudy 3 = light rain 4 = raining 5 = stormy 6 = snowing 7 = wind/mist temp (int): the temperature (either C or F, not indicated) """ pass def activate_mouth_events(self, message=None): """Enable movement of the mouth with speech""" pass def deactivate_mouth_events(self, message=None): """Disable movement of the mouth with speech""" pass