scripts/mycroft/client/enclosure/mark1/__init__.py
Go to the documentation of this file.
1 # Copyright 2017 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 import subprocess
16 import time
17 import sys
18 from alsaaudio import Mixer
19 from threading import Thread, Timer
20 
21 import serial
22 
23 import mycroft.dialog
24 from mycroft.client.enclosure.base import Enclosure
25 from mycroft.api import has_been_paired
26 from mycroft.audio import wait_while_speaking
27 from mycroft.client.enclosure.mark1.arduino import EnclosureArduino
28 from mycroft.client.enclosure.mark1.eyes import EnclosureEyes
29 from mycroft.client.enclosure.mark1.mouth import EnclosureMouth
31  init_display_manager_bus_connection
32 from mycroft.configuration import Configuration, LocalConf, USER_CONFIG
33 from mycroft.messagebus.message import Message
34 from mycroft.util import play_wav, create_signal, connected, check_for_signal
35 from mycroft.util.audio_test import record
36 from mycroft.util.log import LOG
37 from queue import Queue
38 
39 # The Mark 1 hardware consists of a Raspberry Pi main CPU which is connected
40 # to an Arduino over the serial port. A custom serial protocol sends
41 # commands to control various visual elements which are controlled by the
42 # Arduino (e.g. two circular rings of RGB LEDs; and four 8x8 white LEDs).
43 #
44 # The Arduino can also send back notifications in response to either
45 # pressing or turning a rotary encoder.
46 
47 
48 class EnclosureReader(Thread):
49  """
50  Reads data from Serial port.
51 
52  Listens to all commands sent by Arduino that must be be performed on
53  Mycroft Core.
54 
55  E.g. Mycroft Stop Feature
56  #. Arduino sends a Stop command after a button press on a Mycroft unit
57  #. ``EnclosureReader`` captures the Stop command
58  #. Notify all Mycroft Core processes (e.g. skills) to be stopped
59 
60  Note: A command is identified by a line break
61  """
62 
63  def __init__(self, serial, bus, lang=None):
64  super(EnclosureReader, self).__init__(target=self.read)
65  self.alive = True
66  self.daemon = True
67  self.serial = serial
68  self.bus = bus
69  self.lang = lang or 'en-us'
70  self.start()
71 
72  # Notifications from mycroft-core
73  self.bus.on("mycroft.stop.handled", self.on_stop_handled)
74 
75  def read(self):
76  while self.alive:
77  try:
78  data = self.serial.readline()[:-2]
79  if data:
80  try:
81  data_str = data.decode()
82  except UnicodeError as e:
83  data_str = data.decode('utf-8', errors='replace')
84  LOG.warning('Invalid characters in response from '
85  ' enclosure: {}'.format(repr(e)))
86  self.process(data_str)
87  except Exception as e:
88  LOG.error("Reading error: {0}".format(e))
89 
90  def on_stop_handled(self, event):
91  # A skill performed a stop
92  check_for_signal('buttonPress')
93 
94  def process(self, data):
95  # TODO: Look into removing this emit altogether.
96  # We need to check if any other serial bus messages
97  # are handled by other parts of the code
98  if "mycroft.stop" not in data:
99  self.bus.emit(Message(data))
100 
101  if "Command: system.version" in data:
102  # This happens in response to the "system.version" message
103  # sent during the construction of Enclosure()
104  self.bus.emit(Message("enclosure.started"))
105 
106  if "mycroft.stop" in data:
107  if has_been_paired():
108  create_signal('buttonPress')
109  self.bus.emit(Message("mycroft.stop"))
110 
111  if "volume.up" in data:
112  self.bus.emit(Message("mycroft.volume.increase",
113  {'play_sound': True}))
114 
115  if "volume.down" in data:
116  self.bus.emit(Message("mycroft.volume.decrease",
117  {'play_sound': True}))
118 
119  if "system.test.begin" in data:
120  self.bus.emit(Message('recognizer_loop:sleep'))
121 
122  if "system.test.end" in data:
123  self.bus.emit(Message('recognizer_loop:wake_up'))
124 
125  if "mic.test" in data:
126  mixer = Mixer()
127  prev_vol = mixer.getvolume()[0]
128  mixer.setvolume(35)
129  self.bus.emit(Message("speak", {
130  'utterance': "I am testing one two three"}))
131 
132  time.sleep(0.5) # Prevents recording the loud button press
133  record("/tmp/test.wav", 3.0)
134  mixer.setvolume(prev_vol)
135  play_wav("/tmp/test.wav").communicate()
136 
137  # Test audio muting on arduino
138  subprocess.call('speaker-test -P 10 -l 0 -s 1', shell=True)
139 
140  if "unit.shutdown" in data:
141  # Eyes to soft gray on shutdown
142  self.bus.emit(Message("enclosure.eyes.color",
143  {'r': 70, 'g': 65, 'b': 69}))
144  self.bus.emit(
145  Message("enclosure.eyes.timedspin",
146  {'length': 12000}))
147  self.bus.emit(Message("enclosure.mouth.reset"))
148  time.sleep(0.5) # give the system time to pass the message
149  self.bus.emit(Message("system.shutdown"))
150 
151  if "unit.reboot" in data:
152  # Eyes to soft gray on reboot
153  self.bus.emit(Message("enclosure.eyes.color",
154  {'r': 70, 'g': 65, 'b': 69}))
155  self.bus.emit(Message("enclosure.eyes.spin"))
156  self.bus.emit(Message("enclosure.mouth.reset"))
157  time.sleep(0.5) # give the system time to pass the message
158  self.bus.emit(Message("system.reboot"))
159 
160  if "unit.setwifi" in data:
161  self.bus.emit(Message("system.wifi.setup", {'lang': self.lang}))
162 
163  if "unit.factory-reset" in data:
164  self.bus.emit(Message("speak", {
165  'utterance': mycroft.dialog.get("reset to factory defaults")}))
166  subprocess.call(
167  'rm ~/.mycroft/identity/identity2.json',
168  shell=True)
169  self.bus.emit(Message("system.wifi.reset"))
170  self.bus.emit(Message("system.ssh.disable"))
172  self.bus.emit(Message("enclosure.mouth.reset"))
173  self.bus.emit(Message("enclosure.eyes.spin"))
174  self.bus.emit(Message("enclosure.mouth.reset"))
175  time.sleep(5) # give the system time to process all messages
176  self.bus.emit(Message("system.reboot"))
177 
178  if "unit.enable-ssh" in data:
179  # This is handled by the wifi client
180  self.bus.emit(Message("system.ssh.enable"))
181  self.bus.emit(Message("speak", {
182  'utterance': mycroft.dialog.get("ssh enabled")}))
183 
184  if "unit.disable-ssh" in data:
185  # This is handled by the wifi client
186  self.bus.emit(Message("system.ssh.disable"))
187  self.bus.emit(Message("speak", {
188  'utterance': mycroft.dialog.get("ssh disabled")}))
189 
190  if "unit.enable-learning" in data or "unit.disable-learning" in data:
191  enable = 'enable' in data
192  word = 'enabled' if enable else 'disabled'
193 
194  LOG.info("Setting opt_in to: " + word)
195  new_config = {'opt_in': enable}
196  user_config = LocalConf(USER_CONFIG)
197  user_config.merge(new_config)
198  user_config.store()
199 
200  self.bus.emit(Message("speak", {
201  'utterance': mycroft.dialog.get("learning " + word)}))
202 
203  def stop(self):
204  self.alive = False
205 
206 
207 class EnclosureWriter(Thread):
208  """
209  Writes data to Serial port.
210  #. Enqueues all commands received from Mycroft enclosures
211  implementation
212  #. Process them on the received order by writing on the Serial port
213 
214  E.g. Displaying a text on Mycroft's Mouth
215  #. ``EnclosureMouth`` sends a text command
216  #. ``EnclosureWriter`` captures and enqueue the command
217  #. ``EnclosureWriter`` removes the next command from the queue
218  #. ``EnclosureWriter`` writes the command to Serial port
219 
220  Note: A command has to end with a line break
221  """
222 
223  def __init__(self, serial, bus, size=16):
224  super(EnclosureWriter, self).__init__(target=self.flush)
225  self.alive = True
226  self.daemon = True
227  self.serial = serial
228  self.bus = bus
229  self.commands = Queue(size)
230  self.start()
231 
232  def flush(self):
233  while self.alive:
234  try:
235  cmd = self.commands.get() + '\n'
236  self.serial.write(cmd.encode())
237  self.commands.task_done()
238  except Exception as e:
239  LOG.error("Writing error: {0}".format(e))
240 
241  def write(self, command):
242  self.commands.put(str(command))
243 
244  def stop(self):
245  self.alive = False
246 
247 
249  """
250  Serves as a communication interface between Arduino and Mycroft Core.
251 
252  ``Enclosure`` initializes and aggregates all enclosures implementation.
253 
254  E.g. ``EnclosureEyes``, ``EnclosureMouth`` and ``EnclosureArduino``
255 
256  It also listens to the basic events in order to perform those core actions
257  on the unit.
258 
259  E.g. Start and Stop talk animation
260  """
261 
262  _last_internet_notification = 0
263 
264  def __init__(self):
265  super().__init__()
266 
267  self.__init_serial()
268  self.reader = EnclosureReader(self.serial, self.bus, self.lang)
269  self.writer = EnclosureWriter(self.serial, self.bus)
270 
271  # Prepare to receive message when the Arduino responds to the
272  # following "system.version"
273  self.bus.on("enclosure.started", self.on_arduino_responded)
274  self.arduino_responded = False
275  # Send a message to the Arduino across the serial line asking
276  # for a reply with version info.
277  self.writer.write("system.version")
278  # Start a 5 second timer. If the serial port hasn't received
279  # any acknowledgement of the "system.version" within those
280  # 5 seconds, assume there is nothing on the other end (e.g.
281  # we aren't running a Mark 1 with an Arduino)
282  Timer(5, self.check_for_response).start()
283 
284  # Notifications from mycroft-core
285  self.bus.on("enclosure.notify.no_internet", self.on_no_internet)
286 
287  # initiates the web sockets on display manager
288  # NOTE: this is a temporary place to connect the display manager
290 
291  def on_arduino_responded(self, event=None):
292  self.eyes = EnclosureEyes(self.bus, self.writer)
293  self.mouth = EnclosureMouth(self.bus, self.writer)
294  self.system = EnclosureArduino(self.bus, self.writer)
295  self.__register_events()
296  self.__reset()
297  self.arduino_responded = True
298 
299  # verify internet connection and prompt user on bootup if needed
300  if not connected():
301  # We delay this for several seconds to ensure that the other
302  # clients are up and connected to the messagebus in order to
303  # receive the "speak". This was sometimes happening too
304  # quickly and the user wasn't notified what to do.
305  Timer(5, self._do_net_check).start()
306 
307  def on_no_internet(self, event=None):
308  if connected():
309  # One last check to see if connection was established
310  return
311 
312  if time.time() - Enclosure._last_internet_notification < 30:
313  # don't bother the user with multiple notifications with 30 secs
314  return
315 
316  Enclosure._last_internet_notification = time.time()
317 
318  # TODO: This should go into EnclosureMark1 subclass of Enclosure.
319  if has_been_paired():
320  # Handle the translation within that code.
321  self.bus.emit(Message("speak", {
322  'utterance': "This device is not connected to the Internet. "
323  "Either plug in a network cable or hold the "
324  "button on top for two seconds, then select "
325  "wifi from the menu"}))
326  else:
327  # enter wifi-setup mode automatically
328  self.bus.emit(Message('system.wifi.setup', {'lang': self.lang}))
329 
330  def __init_serial(self):
331  try:
332  self.port = self.config.get("port")
333  self.rate = self.config.get("rate")
334  self.timeout = self.config.get("timeout")
335  self.serial = serial.serial_for_url(
336  url=self.port, baudrate=self.rate, timeout=self.timeout)
337  LOG.info("Connected to: %s rate: %s timeout: %s" %
338  (self.port, self.rate, self.timeout))
339  except Exception:
340  LOG.error("Impossible to connect to serial port: "+str(self.port))
341  raise
342 
343  def __register_events(self):
344  self.bus.on('enclosure.mouth.events.activate',
346  self.bus.on('enclosure.mouth.events.deactivate',
348  self.bus.on('enclosure.reset',
349  self.__reset)
351 
352  def __register_mouth_events(self, event=None):
353  self.bus.on('recognizer_loop:record_begin', self.mouth.listen)
354  self.bus.on('recognizer_loop:record_end', self.mouth.reset)
355  self.bus.on('recognizer_loop:audio_output_start', self.mouth.talk)
356  self.bus.on('recognizer_loop:audio_output_end', self.mouth.reset)
357 
358  def __remove_mouth_events(self, event=None):
359  self.bus.remove('recognizer_loop:record_begin', self.mouth.listen)
360  self.bus.remove('recognizer_loop:record_end', self.mouth.reset)
361  self.bus.remove('recognizer_loop:audio_output_start',
362  self.mouth.talk)
363  self.bus.remove('recognizer_loop:audio_output_end',
364  self.mouth.reset)
365 
366  def __reset(self, event=None):
367  # Reset both the mouth and the eye elements to indicate the unit is
368  # ready for input.
369  self.writer.write("eyes.reset")
370  self.writer.write("mouth.reset")
371 
372  def speak(self, text):
373  self.bus.emit(Message("speak", {'utterance': text}))
374 
376  if not self.arduino_responded:
377  # There is nothing on the other end of the serial port
378  # close these serial-port readers and this process
379  self.writer.stop()
380  self.reader.stop()
381  self.serial.close()
382  self.bus.close()
383 
384  def _handle_pairing_complete(self, Message):
385  """
386  Handler for 'mycroft.paired', unmutes the mic after the pairing is
387  complete.
388  """
389  self.bus.emit(Message("mycroft.mic.unmute"))
390 
391  def _do_net_check(self):
392  # TODO: This should live in the derived Enclosure, e.g. EnclosureMark1
393  LOG.info("Checking internet connection")
394  if not connected(): # and self.conn_monitor is None:
395  if has_been_paired():
396  # TODO: Enclosure/localization
397  self.speak("This unit is not connected to the Internet. "
398  "Either plug in a network cable or hold the "
399  "button on top for two seconds, then select "
400  "wifi from the menu")
401  else:
402  # Begin the unit startup process, this is the first time it
403  # is being run with factory defaults.
404 
405  # TODO: This logic should be in EnclosureMark1
406  # TODO: Enclosure/localization
407 
408  # Don't listen to mic during this out-of-box experience
409  self.bus.emit(Message("mycroft.mic.mute"))
410  # Setup handler to unmute mic at the end of on boarding
411  # i.e. after pairing is complete
412  self.bus.once('mycroft.paired', self._handle_pairing_complete)
413 
414  self.speak(mycroft.dialog.get('mycroft.intro'))
416  time.sleep(2) # a pause sounds better than just jumping in
417 
418  # Kick off wifi-setup automatically
419  data = {'allow_timeout': False, 'lang': self.lang}
420  self.bus.emit(Message('system.wifi.setup', data))
def check_for_signal(signal_name, sec_lifetime=0)
Definition: signal.py:105
def record(file_path, duration, rate, channels)
def create_signal(signal_name)
Definition: signal.py:90
def get(phrase, lang=None, context=None)


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40