def firmware_check(s: Session) -> Union[dict, bool]: """ Checks for the latest version of available firmware. Returns a {dict} if new firmware is available. {'version': '<new version>', 'download_url': '<url to download firmware>'} :param s: Requests Session object :type s: Session :return: Returns False if configured firmware is equal latest. :rtype: Union[dict, bool] """ logger.info('CHECKING') r = request(s, get_cfg('SERVICE.SERVER_URL') + '/update/current', 'GET') if not r: logger.error('FAILED') return False rj = r.json() logger.debug(rj) if rj['version'] is not None and not rj['version'] == get_cfg( 'FACTORY_FIRMWARE.FW_VERSION'): logger.info('New Firmware Available: Installed (%s), Available (%s)' % (get_cfg('FACTORY_FIRMWARE.FW_VERSION'), rj['version'])) return rj else: logger.info('LATEST ALREADY INSTALLED.') return False
def _lid_image(self, msg: dict) -> None: logger.info('capturing Lid Image') img = cam.capture(cam.GFCAM_LID) logger.info('uploading Lid Image') img_upload(self._session, img, msg) if get_cfg('LOGGING.SAVE_SENT_IMAGES'): logger.info('saving Lid Image') open('%s/%s.jpeg' % (get_cfg('LOGGING.DIR'), msg['id']), 'wb').write(img)
def _head_image(self, msg: dict, settings: dict = None) -> None: if settings is not None and settings.get('HCil') > 0: img = 'HEAD_LASER_%s.jpg' % get_cfg('EMULATOR.MATERIAL_THICKNESS') else: img = 'HEAD_NO_LASER_%s.jpg' % get_cfg( 'EMULATOR.MATERIAL_THICKNESS') with open('%s/%s' % (get_cfg('EMULATOR.IMAGE_SRC_DIR'), img), 'rb') as f: img_upload(self._session, f.read(), msg) self._last_action = 'head_image'
def _head_image(self, msg: dict, settings: dict = None) -> None: logger.info('capturing Head Image') set_head_led_from_pulse(settings['HCil']) img = cam.capture(cam.GFCAM_HEAD, int(settings['HCex']), int(settings['HCga'])) head_all_led_off() logger.info('uploading Head Image') img_upload(self._session, img, msg) if get_cfg('LOGGING.SAVE_SENT_IMAGES'): logger.info('saving Head Image') open('%s/%s.jpeg' % (get_cfg('LOGGING.DIR'), msg['id']), 'wb').write(img)
def get_session() -> Session: """ Creates web api session object. :return: Requests Session object :rtype: Session """ s = Session() set_cfg('SESSION.USER_AGENT', 'OpenGlow/%s' % get_cfg('FACTORY_FIRMWARE.FW_VERSION')) s.headers.update({'user-agent': get_cfg('SESSION.USER_AGENT')}) logger.debug('Returning object : %s' % s) return s
def _hunt(self, msg: dict) -> None: ZAxis.home() self._motion(msg) home_offset = int(get_cfg('MOTION.Z_HOME_OFFSET')) if home_offset is not None: logger.debug('moving z to home offset %s half steps' % home_offset) offset_dir = Dir.Pos if home_offset > 0 else Dir.Neg while home_offset != 0: ZAxis.step(offset_dir)
def load_motion(s: Session, url: str, out_file: str) -> Union[dict, bool]: """ Downloads motion/print file, parses header :param s: Requests Session object :type s: Session :param url: Target URL :type url: str :param out_file: Where to write the results :type out_file: str :return: Header dict or False on failure :rtype: Union[dict, bool] """ r = request(s, url, 'GET', stream=True) info = { 'header_data': {}, 'header_len': 0, 'size': 0, 'run_time': None, } with open(out_file, 'wb') as f: res = r.iter_content(chunk_size=1024) puls_data = next(res) if puls_data[1:4].decode() != 'GF1': logger.error('received data not a GF puls file') return False info['header_len'] = _byte_to_int(puls_data[4:8]) - 8 s = 8 while s < (info['header_len'] + 8): info['header_data'][puls_data[s:s + 4].decode()] = _byte_to_int( puls_data[s + 4:s + 8]) s += 8 size = len(puls_data[s:]) stat = decode_all_steps(puls_data[s:]) # TODO: REMOVE THIS TEMP RAW WRITE - ONLY FOR QUICK TESTING base_file_name = '%s/%s' % (str(Path( get_cfg('LOGGING.FILE')).parent), time.strftime("%Y-%m-%d_%H%M%S")) raw = open(base_file_name + '.puls', 'wb') raw.write(puls_data) f.write(puls_data[s:]) for chunk in res: if chunk: size = size + len(chunk) stat = decode_all_steps(chunk, stat) f.write(chunk) raw.write(chunk) raw.close() info['size'] = size info['run_time'] = timedelta(seconds=size / info['header_data']['STfr']) info['stats'] = stat raw = open(base_file_name + '.info', 'w') raw.write( json.dumps(info, sort_keys=True, indent=4, default=str) + '\n') raw.close() return info
def __init__(self, q_rx: Queue, q_tx: Queue): """ Class Initializer :param q_rx: WSS RX Message Queue :type q_rx: Queue :param q_tx: WSS TX Message Queue :type q_tx: Queue """ self.msg_q_rx = q_rx self.msg_q_tx = q_tx self.stop = False self.ready = False self.ws = WebSocket(url=get_cfg('SERVICE.STATUS_SERVICE_URL') + '/' + get_cfg('SESSION.WS_TOKEN'), protocols=['glowforge'], agent=get_cfg('SESSION.USER_AGENT')) self.ignore_events = [ 'ping', 'pong', 'poll', 'connecting', 'connected' ] Thread.__init__(self)
def send_report(q: Queue, msg: dict) -> None: """ Send device settings report in the required format. :param q: {WSS message queue :type q: Queue :param msg: WSS Message :type msg: dict :return: """ settings = '' logger.info("START") for item, setting in MACHINE_SETTINGS.items(): value = '' if setting.default is None else str(setting.default) if setting.in_report: if item == 'SAid': value = msg['id'] if item in _CONFIG_PROVIDERS: config_value = get_cfg(_CONFIG_PROVIDERS.get(item)) if config_value is not None: value = str(config_value) if item in _REGISTERED_PROVIDERS: value = str(_REGISTERED_PROVIDERS[item]()) if setting.type is int: value = int(float(value)) elif setting.type is str: value = '"{}"'.format(value) else: value = setting.type(value) settings += '"{}":{},'.format(item, value) # The service appears to bypass the homing process if we send it an empty settings value. # It is assumed that this prevents the machine from re-homing every time it loses and regains # the connection to the Glowforge service (a seemingly frequent occurrence). if not get_cfg('SETTINGS.SET') and not get_cfg('EMULATOR.BYPASS_HOMING'): send_wss_event(q, msg['id'], 'settings:completed', data={'key': 'settings', 'value': '{"values":{%s}}' % settings[:-1]}) set_cfg('SETTINGS.SET', True) else: send_wss_event(q, msg['id'], 'settings:completed', data={'key': 'settings', 'value': '{}'}) logger.info("COMPLETE")
def _safe_to_move(self) -> bool: switches = self._sw_thread.all_switches() if not switches[InputSwitch.SW_DOORS]: logger.info('door open, unsafe to move') return False if cnc.state is not MachineState.IDLE: logger.info('machine is not idle, state: %s' % cnc.state.value) return False if temp_sensor.water_2.C > int(get_cfg('THERMAL.MAX_START_TEMP')): logger.info('machine temp is too high, temp: %s' % temp_sensor.water_2.C) return False return True
def _initialize(self) -> None: logger.debug('initializing machine') self._sw_thread.start() # Setup machine set_lid_led(MACHINE_SETTINGS['LLvl'].default) TEC.off() WaterPump.on() WaterPump.set_heater(int(get_cfg('THERMAL.WATER_HEATER_PERCENT'))) fans.reset() cnc.reset() ZAxis.reset() set_button_color(ButtonColor.OFF) cnc.enable()
def _motion_download(self, msg: dict): logger.info('DOWNLOADING') base_file_name = '%s/%s_%s' % (get_cfg('EMULATOR.MOTION_DL_DIR'), time.strftime("%Y-%m-%d_%H%M%S"), msg['action_type']) info = load_motion(self._session, msg['motion_url'], base_file_name + '.puls') with open(base_file_name + '.info', 'w') as f: f.write( json.dumps(info, sort_keys=True, indent=4, default=str) + '\n') logger.debug('Header: %s' % info) logger.info('COMPLETE') return
def _lid_image(self, msg: dict) -> None: if self._homing_stage == 0 and self._last_action == 'motion': # Looks like were are homing again self._homing_stage = 1 if self._homing_stage != 0: logger.info('HOME Step %s' % self._homing_stage) img = 'HOME_%s.jpg' % self._homing_stage self._homing_stage = self._homing_stage + 1 if self._homing_stage < 4 else 0 else: img = 'LID_IMAGE.jpg' with open('%s/%s' % (get_cfg('EMULATOR.IMAGE_SRC_DIR'), img), 'rb') as f: img_upload(self._session, f.read(), msg) self._last_action = 'lid_image'
def firmware_download(s: Session, fw: dict) -> bool: """ Downloads latest firmware to directory set in config. :param s: Requests Session object :type s: Session :param fw: Dictionary returned by _api_firmware_check() :type fw: dict :return: {bool} True :rtype: bool """ logger.info('DOWNLOADING') Path(get_cfg('FACTORY_FIRMWARE.DOWNLOAD_DIR')).mkdir(parents=True, exist_ok=True) r = request(s, fw['download_url'], 'GET', stream=True) with open( '%s/glowforge-fw-v%s.fw' % (get_cfg('FACTORY_FIRMWARE.DOWNLOAD_DIR'), fw['version']), 'wb') as f: for chunk in r.iter_content(chunk_size=1024): if chunk: f.write(chunk) logger.info('COMPLETE.') return True
def authenticate_machine(s: Session) -> bool: """ Authenticates machine to service. POSTS serial number prefixed with an S and machine password to <server_url>/machines/sign_in. On success, server responds with JSON encoded 'ws_token' for WSS auth, and 'auth_token' that is added as a header to all session web API requests. Auth token header is 'Authorization': 'Bearer <auth_token>' :param s: Emulator session object. :type s: Session :return: Authentication result. :rtype: bool """ logger.info('START') Retry.BACKOFF_MAX = 30 retries = Retry( total=30, backoff_factor=5, method_whitelist=["GET"] ) adapter = HTTPAdapter(max_retries=retries) s.mount("https://", adapter) r = request(s, get_cfg('SERVICE.SERVER_URL') + '/machines/sign_in', 'POST', data={'serial': 'S' + str(get_cfg('MACHINE.SERIAL')), 'password': get_cfg('MACHINE.PASSWORD')}) if r: rj = r.json() logger.debug(rj) set_cfg('SESSION.WS_TOKEN', rj['ws_token']) set_cfg('SESSION.AUTH_TOKEN', rj['auth_token']) update_header(s, 'Authorization', "Bearer %s" % get_cfg('SESSION.AUTH_TOKEN')) logger.info('SUCCESS') return True else: logger.error('FAILED') return False
def __init__(self): """ Initialize object """ BaseMachine.__init__(self) # Track which step in the homing process we are at. # Example script has 5 saved head images, which are uploaded to the service in sequence to # emulate the homing process. Files are located in _RESOURCES/IMG # HOME_1.jpg: Image of the head located at expected center position # This image was taken with the head already in the calibrated position to speed up the process. # HOME_2.jpg: Image after first retreat move # HOME_3.jpg: Image after second retreat move # HOME_4.jpg: Image after third retreat move # LID_IMAGE.jpg: Image of bed with Proofgrade(tm) material # This image is sent for every none-homing lid_image request self._homing_stage = 0 if get_cfg('EMULATOR.BYPASS_HOMING') else 1 # Used to track the action just before a lid_image request. This is used to detect homing operations after # the initial start up (after a print job, for instance) self._last_action = None
def img_upload(s: Session, img: bytes, msg: dict) -> bool: """ Uploads head/lid image :param s: Requests Session object :type s: Session :param img: Path to file to upload, or bytes object containing image :type img: bytes :param msg: WSS message :type msg: dict :return: :rtype: bool """ logger.info('START') url = get_cfg('SERVICE.SERVER_URL') + '/api/machines/%s/%s' % ( msg['action_type'], msg['id']) headers = {'Content-Type': 'image/jpeg'} r = request(s, url, 'POST', data=img, headers=headers) if not r: logger.debug(r) return False else: logger.debug(r.json) logger.info('COMPLETE') return True
def _motion(self, msg: dict) -> None: logger.info('start motion') if self._safe_to_move: cnc.clear_all() # Download puls file from service logger.info('loading motion file from %s' % msg['motion_url']) self._motion_stats = load_motion(self._session, msg['motion_url'], PULS_DEVICE) logger.info('motion stats: %s' % self._motion_stats) if msg['action_type'] == 'print': send_wss_event(self._q_msg_tx, msg['id'], 'print:download:completed') cnc.laser_latch(0) self._button_wait(msg) if not self._running_action_cancelled: send_wss_event(self._q_msg_tx, msg['id'], 'print:warmup:starting') # Configure for print, and wait for warm up if not self._running_action_cancelled: self._config_from_pulse('run', self._motion_stats['header_data']) if msg['action_type'] == 'print': if get_cfg('MOTION.WARM_UP_DELAY'): sleep(int(get_cfg('MOTION.WARM_UP_DELAY'))) # Run motion job if not self._running_action_cancelled: if msg['action_type'] == 'print': logger.info('start temps: %s' % str(temp_sensor.all)) send_wss_event(self._q_msg_tx, msg['id'], 'print:running') self._run_loop() cnc.laser_latch(1) pos = cnc.position logger.info( 'end positions (actual/expected): X (%s/%s), Y (%s/%s), Z (%s/%s)' % ( pos.x.steps, self._motion_stats['stats']['XEND'], pos.y.steps, self._motion_stats['stats']['YEND'], pos.z.steps, self._motion_stats['stats']['ZEND'], )) logger.info('motion bytes actual:%s, expected: %s' % (pos.bytes.processed, self._motion_stats['size'])) if msg['action_type'] == 'print': logger.info('end print temps: %s' % str(temp_sensor.all)) # Cool down for prints if msg['action_type'] == 'print': self._return_home() logger.info('start cool down') self._config_from_pulse('cool_down', self._motion_stats['header_data']) if get_cfg('MOTION.COOL_DOWN_DELAY'): sleep(int(get_cfg('MOTION.COOL_DOWN_DELAY'))) logger.info('end cool-down temps: %s' % str(temp_sensor.all)) # Config for idle logger.info('start idle') self._config_from_pulse('idle', self._motion_stats['header_data']) pos = cnc.position logger.info('end positions (%s, %s, %s)' % (pos.x.steps, pos.y.steps, pos.z.steps)) logger.info('end motion')