Warning, file /plasma-bigscreen/mycroft-bigscreen-setup/__init__.py was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 # Copyright 2021 Aditya Mehra <aix.m@outlook.com> 0002 # Copyright 2017 Mycroft AI Inc. 0003 # 0004 # Licensed under the Apache License, Version 2.0 (the "License"); 0005 # you may not use this file except in compliance with the License. 0006 # You may obtain a copy of the License at 0007 # 0008 # http://www.apache.org/licenses/LICENSE-2.0 0009 # 0010 # Unless required by applicable law or agreed to in writing, software 0011 # distributed under the License is distributed on an "AS IS" BASIS, 0012 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 0013 # See the License for the specific language governing permissions and 0014 # limitations under the License. 0015 # 0016 0017 import time 0018 from requests import HTTPError 0019 from threading import Timer, Lock 0020 from uuid import uuid4 0021 0022 from adapt.intent import IntentBuilder 0023 0024 import mycroft.audio 0025 from mycroft.api import DeviceApi, is_paired, check_remote_pairing 0026 from mycroft.identity import IdentityManager 0027 from mycroft.messagebus.message import Message 0028 from mycroft.skills.core import MycroftSkill, intent_handler 0029 0030 0031 ACTION_BUTTON_PLATFORMS = ('mycroft_mark_1', 'mycroft_mark_2', 'bigscreen') 0032 MAX_PAIRING_CODE_RETRIES = 30 0033 ACTIVATION_POLL_FREQUENCY = 10 # secs between checking server for activation 0034 0035 0036 def _stop_speaking(): 0037 """Stop speaking the pairing code if it is still being spoken.""" 0038 if mycroft.audio.is_speaking(): 0039 mycroft.audio.stop_speaking() 0040 0041 0042 class BigscreenSetupSkill(MycroftSkill): 0043 """Device pairing logic.""" 0044 def __init__(self): 0045 super(BigscreenSetupSkill, self).__init__("BigscreenSetupSkill") 0046 self.api = DeviceApi() 0047 self.pairing_token = None 0048 self.pairing_code = None 0049 self.pairing_code_expiration = None 0050 self.state = str(uuid4()) 0051 self.platform = None 0052 self.nato_alphabet = None 0053 self.mycroft_ready = False 0054 self.pairing_code_retry_cnt = 0 0055 self.account_creation_requested = False 0056 0057 # These attributes track the status of the device activation 0058 self.device_activation_lock = Lock() 0059 self.device_activation_checker = None 0060 self.device_activation_cancelled = False 0061 self.activation_attempt_count = 0 0062 0063 # These attributes are used when tracking the ready state to control 0064 # when the paired dialog is spoken. 0065 self.paired_dialog_lock = Lock() 0066 self.paired_dialog = None 0067 self.pairing_performed = False 0068 0069 # These attributes are used when determining if pairing has started. 0070 self.pairing_status_lock = Lock() 0071 self.pairing_in_progress = False 0072 0073 def initialize(self): 0074 """Register event handlers, setup language and platform dependent info.""" 0075 self.add_event("mycroft.not.paired", self.not_paired) 0076 self.gui.register_handler("mycroft.device.set.first.config", 0077 self.handle_config_selected_event) 0078 self.gui.register_handler("mycroft.device.speak.first.config", 0079 self.speak_setup_menu) 0080 self.nato_alphabet = self.translate_namedvalues('codes') 0081 # TODO replace self.platform logic with call to enclosure capabilities 0082 self.platform = self.config_core['enclosure'].get( 0083 'platform', 'unknown' 0084 ) 0085 self._select_paired_dialog() 0086 0087 # If the device isn't paired catch mycroft.ready to report 0088 # that the device is ready for use. 0089 # This assumes that the pairing skill is loaded as a priority skill 0090 # before the rest of the skills are loaded. 0091 if not is_paired(): 0092 self.add_event("mycroft.ready", self.handle_mycroft_ready) 0093 0094 def _select_paired_dialog(self): 0095 """Select the correct dialog file to communicate pairing complete.""" 0096 if self.platform in ACTION_BUTTON_PLATFORMS: 0097 self.paired_dialog = 'pairing.paired' 0098 else: 0099 self.paired_dialog = 'pairing.paired.no.button' 0100 0101 def handle_mycroft_ready(self, _): 0102 """Catch info that skills are loaded and ready.""" 0103 with self.paired_dialog_lock: 0104 if is_paired() and self.pairing_performed: 0105 self.speak_dialog(self.paired_dialog) 0106 else: 0107 self.mycroft_ready = True 0108 0109 def not_paired(self, message): 0110 """When not paired, tell the user so and start pairing.""" 0111 if not message.data.get('quiet', True): 0112 self.speak_dialog("pairing.not.paired") 0113 self.handle_pairing() 0114 0115 @intent_handler(IntentBuilder("PairingIntent") 0116 .require("PairingKeyword").require("DeviceKeyword")) 0117 def handle_pairing(self, message=None): 0118 """Attempt to pair the device to the Selene database.""" 0119 already_paired = check_remote_pairing(ignore_errors=True) 0120 if already_paired: 0121 self.speak_dialog("already.paired") 0122 self.log.info( 0123 "Pairing skill invoked but device is paired, exiting" 0124 ) 0125 elif self.pairing_code is None: 0126 start_pairing = self._check_pairing_in_progress() 0127 if start_pairing: 0128 self.reload_skill = False # Prevent restart during pairing 0129 self.enclosure.deactivate_mouth_events() 0130 self.handle_setup_menu() 0131 #self._communicate_create_account_url() 0132 #self._execute_pairing_sequence() 0133 0134 # Pairing GUI events 0135 # Backend selection menu 0136 def handle_setup_menu(self): 0137 self.gui['firstConfigDialog'] = 1 0138 self.handle_display_manager("ConfigIntro") 0139 0140 def speak_setup_menu(self, message): 0141 self.speak_dialog("select_mycroft_config") 0142 0143 def handle_config_selected_event(self, message): 0144 self.bus.emit(Message("mycroft.audio.speech.stop")) 0145 time.sleep(2) 0146 self.handle_config_confirmation(message.data["config"]) 0147 0148 def handle_display_manager(self, state): 0149 self.gui["state"] = state 0150 self.gui.show_page( 0151 "ProcessLoader.qml", 0152 override_idle=True, 0153 override_animations=True) 0154 0155 # Config confirmation 0156 def handle_config_confirmation(self, selection): 0157 if selection == "pttwakeword": 0158 self.gui["configurePtt"] = True 0159 0160 elif selection == "wakeword": 0161 self.gui["configurePtt"] = False 0162 0163 self._execute_pairing_sequence() 0164 0165 def _check_pairing_in_progress(self): 0166 """Determine if skill was invoked while pairing is in progress.""" 0167 with self.pairing_status_lock: 0168 if self.pairing_in_progress: 0169 self.log.debug( 0170 "Pairing in progress; ignoring call to handle_pairing" 0171 ) 0172 start_pairing = False 0173 else: 0174 self.pairing_in_progress = True 0175 start_pairing = True 0176 0177 return start_pairing 0178 0179 def _communicate_create_account_url(self): 0180 """Tell the user the URL for creating an account and display it. 0181 This should only happen once per pairing sequence. If pairing is 0182 restarted due to an error, this will be skipped. 0183 """ 0184 if not self.account_creation_requested: 0185 self.log.info("Communicating account URL to user") 0186 self.account_creation_requested = True 0187 if self.gui.connected: 0188 self.gui.show_page("create_account.qml", override_idle=True) 0189 else: 0190 self.enclosure.mouth_text("account.mycroft.ai ") 0191 self.speak_dialog("create.account") 0192 mycroft.audio.wait_while_speaking() 0193 time.sleep(30) 0194 0195 def _execute_pairing_sequence(self): 0196 """Interact with the user to pair the device.""" 0197 self.log.info("Initiating device pairing sequence...") 0198 self._get_pairing_data() 0199 if self.pairing_code is not None: 0200 self._communicate_pairing_url() 0201 self._display_pairing_code() 0202 self._speak_pairing_code() 0203 self._attempt_activation() 0204 0205 def _get_pairing_data(self): 0206 """Obtain a pairing code and access token from the Selene API 0207 A pairing code is good for 24 hours so set an expiration time in case 0208 pairing does not complete. If the call to the API fails, retry for 0209 five minutes. If the API call does not succeed after five minutes 0210 abort the pairing process. 0211 """ 0212 self.log.info('Retrieving pairing code from device API...') 0213 try: 0214 pairing_data = self.api.get_code(self.state) 0215 self.pairing_code = pairing_data['code'] 0216 self.pairing_token = pairing_data['token'] 0217 self.pairing_code_expiration = ( 0218 time.monotonic() 0219 + pairing_data['expiration'] 0220 ) 0221 except Exception: 0222 self.log.exception("API call to retrieve pairing data failed") 0223 self._handle_pairing_data_retrieval_error() 0224 else: 0225 self.log.info('Pairing code obtained: ' + self.pairing_code) 0226 self.pairing_code_retry_cnt = 0 # Reset counter on success 0227 0228 def _handle_pairing_data_retrieval_error(self): 0229 """Retry retrieving pairing code for five minutes, then abort.""" 0230 if self.pairing_code_retry_cnt < MAX_PAIRING_CODE_RETRIES: 0231 time.sleep(10) 0232 self.pairing_code_retry_cnt += 1 0233 self._restart_pairing(quiet=True) 0234 else: 0235 self._end_pairing('connection.error') 0236 self.pairing_code_retry_cnt = 0 0237 0238 def _communicate_pairing_url(self): 0239 """Tell the user the URL for pairing and display it, if possible""" 0240 self.log.info("Communicating pairing URL to user") 0241 if self.gui.connected: 0242 self.gui.show_page("pairing_start.qml", override_idle=True) 0243 else: 0244 self.enclosure.mouth_text("mycroft.ai/pair ") 0245 self.speak_dialog("pairing.intro") 0246 mycroft.audio.wait_while_speaking() 0247 time.sleep(5) 0248 0249 def _display_pairing_code(self): 0250 """Show the pairing code on the display, if one is available""" 0251 if self.gui.connected: 0252 self.gui['code'] = self.pairing_code 0253 self.gui.show_page("pairing.qml", override_idle=True) 0254 else: 0255 self.enclosure.mouth_text(self.pairing_code) 0256 0257 def _attempt_activation(self): 0258 """Speak the pairing code if two """ 0259 with self.device_activation_lock: 0260 if not self.device_activation_cancelled: 0261 self._check_speak_code_interval() 0262 self._start_device_activation_checker() 0263 0264 def _check_speak_code_interval(self): 0265 """Only speak pairing code every two minutes.""" 0266 self.activation_attempt_count += 1 0267 if not self.activation_attempt_count % 12: 0268 self._speak_pairing_code() 0269 0270 def _speak_pairing_code(self): 0271 """Speak pairing code.""" 0272 self.log.debug("Speaking pairing code") 0273 pairing_code_utterance = map(self.nato_alphabet.get, self.pairing_code) 0274 speak_data = dict(code='. '.join(pairing_code_utterance) + '.') 0275 self.speak_dialog("pairing.code", speak_data) 0276 0277 def _start_device_activation_checker(self): 0278 """Set a timer to check the activation status in ten seconds.""" 0279 self.device_activation_checker = Timer( 0280 ACTIVATION_POLL_FREQUENCY, self.check_for_device_activation 0281 ) 0282 self.device_activation_checker.daemon = True 0283 self.device_activation_checker.start() 0284 0285 def check_for_device_activation(self): 0286 """Call the device API to determine if user completed activation. 0287 Called every 10 seconds by a Timer. Checks if user has activated the 0288 device on account.mycroft.ai. Activation is considered successful when 0289 the API call returns without error. When the API call throws an 0290 HTTPError, the assumption is that the uer has not yet completed 0291 activation. 0292 """ 0293 self.log.debug('Checking for device activation') 0294 try: 0295 login = self.api.activate(self.state, self.pairing_token) 0296 except HTTPError: 0297 self._handle_not_yet_activated() 0298 except Exception: 0299 self.log.exception("An unexpected error occurred.") 0300 self._restart_pairing() 0301 else: 0302 self._handle_activation(login) 0303 0304 def _handle_not_yet_activated(self): 0305 """Activation has not been completed, determine what to do next. 0306 The pairing code expires after 24 hours. Restart pairing if expired. 0307 If the pairing code is still valid, speak the pairing code if the 0308 appropriate amount of time has elapsed since last spoken and restart 0309 the device activation checking timer. 0310 """ 0311 if time.monotonic() > self.pairing_code_expiration: 0312 self._reset_pairing_attributes() 0313 self.handle_pairing() 0314 else: 0315 self._attempt_activation() 0316 0317 def _handle_activation(self, login): 0318 """Steps to take after successful device activation.""" 0319 self._save_identity(login) 0320 _stop_speaking() 0321 self._display_pairing_success() 0322 self.bus.emit(Message("mycroft.paired", login)) 0323 self.pairing_performed = True 0324 self._speak_pairing_success() 0325 self.bus.emit(Message("configuration.updated")) 0326 self.reload_skill = True 0327 0328 def _save_identity(self, login): 0329 """Save this device's identifying information to disk. 0330 The user has successfully paired the device on account.mycroft.ai. 0331 The UUID and access token of the device can now be saved to the 0332 local identity file. If saving the identity file fails twice, 0333 something went very wrong and the pairing process will restart. 0334 """ 0335 save_attempts = 1 0336 while save_attempts < 2: 0337 try: 0338 IdentityManager.save(login) 0339 except Exception: 0340 if save_attempts == 1: 0341 save_attempts += 1 0342 log_msg = "First attempt to save identity file failed." 0343 self.log.exception(log_msg) 0344 time.sleep(2) 0345 else: 0346 log_msg = ( 0347 "Second attempt to save identity file failed. " 0348 "Restarting the pairing sequence..." 0349 ) 0350 self.log.exception(log_msg) 0351 self._restart_pairing() 0352 else: 0353 self.log.info('Identity file saved.') 0354 break 0355 0356 def _display_pairing_success(self): 0357 """Display a pairing complete screen on GUI or clear Arduino""" 0358 if self.gui.connected: 0359 self.gui.show_page("pairing_done.qml", override_idle=False) 0360 else: 0361 self.enclosure.activate_mouth_events() # clears the display 0362 0363 def _speak_pairing_success(self): 0364 """Tell the user the device is paired. 0365 If the device is not ready for use, also tell the user to wait until 0366 the device is ready. 0367 """ 0368 with self.paired_dialog_lock: 0369 if self.mycroft_ready: 0370 self.speak_dialog(self.paired_dialog) 0371 mycroft.audio.wait_while_speaking() 0372 else: 0373 self.speak_dialog("wait.for.startup") 0374 mycroft.audio.wait_while_speaking() 0375 0376 def _end_pairing(self, error_dialog): 0377 """Resets the pairing and don't restart it. 0378 Arguments: 0379 error_dialog: Reason for the ending of the pairing process. 0380 """ 0381 self.speak_dialog(error_dialog) 0382 self.bus.emit(Message("mycroft.mic.unmute", None)) 0383 self._reset_pairing_attributes() 0384 0385 def _restart_pairing(self, quiet=False): 0386 """Resets the pairing and don't restart it. 0387 Arguments: 0388 quiet: indicates if an error message should be spoken to the user 0389 """ 0390 self.log.info("Aborting pairing process and restarting...") 0391 self.enclosure.activate_mouth_events() 0392 if not quiet: 0393 self.speak_dialog("unexpected.error.restarting") 0394 self._reset_pairing_attributes() 0395 self.bus.emit(Message("mycroft.not.paired", data=dict(quiet=quiet))) 0396 0397 def _reset_pairing_attributes(self): 0398 """Reset attributes that need to be in a certain state for pairing.""" 0399 with self.pairing_status_lock: 0400 self.pairing_in_progress = False 0401 with self.device_activation_lock: 0402 self.activation_attempt_count = 0 0403 self.device_activation_checker = None 0404 self.pairing_code = None 0405 self.pairing_token = None 0406 0407 def shutdown(self): 0408 """Skill process termination steps.""" 0409 with self.device_activation_lock: 0410 self.device_activation_cancelled = True 0411 if self.device_activation_checker: 0412 self.device_activation_checker.cancel() 0413 if self.device_activation_checker: 0414 self.device_activation_checker.join() 0415 0416 0417 def create_skill(): 0418 """Entrypoint for skill process to load the skill.""" 0419 return BigscreenSetupSkill()