def get_matching_files(globs: List[str]) -> Set[str]: matching_files: List[str] = [] _path = os.path.normpath(path) + os.path.sep for g in [g for g in globs if glob.has_magic(g)]: files = glob.glob(f'{path}/{g}', recursive=True) files = filter(lambda f: os.path.isfile(f), files) files = [ os.path.normpath(os.path.normpath(f).split(_path)[-1]) for f in files ] matching_files.extend(files) # matches things like src/opcontrol.{c,cpp} so that we can expand to src/opcontrol.c and src/opcontrol.cpp pattern = re.compile(r'^([\w{}]+.){{((?:\w+,)*\w+)}}$'.format( os.path.sep.replace('\\', '\\\\'))) for f in [os.path.normpath(f) for f in globs if not glob.has_magic(f)]: if re.match(pattern, f): matches = re.split(pattern, f) logger(__name__).debug(f'Matches on {f}: {matches}') matching_files.extend( [f'{matches[1]}{ext}' for ext in matches[2].split(',')]) else: matching_files.append(f) matching_files: Set[str] = set(matching_files) return matching_files
def get_connected_device(self) -> SystemDevice: logger(__name__).info('Interrogating Cortex...') stm32 = STM32Device(self.port, do_negoitate=False) try: stm32.get(n_retries=1) return stm32 except VEXCommError: return self
def expose_bootloader(self): logger(__name__).info('Exposing bootloader') for _ in itertools.repeat(None, 5): self._tx_packet(0x25) time.sleep(0.1) self.port.read_all() time.sleep(0.3) return STM32Device(self.port, must_initialize=True)
def write_program(self, file: typing.BinaryIO, **kwargs): logger(__name__).info('Writing program to Cortex') status = self.query_system() logger(__name__).info(status) if not status.flags | self.SystemStatusFlags.TETH_USB and not status.flags | self.SystemStatusFlags.DL_MODE: self.send_to_download_channel() bootloader = self.expose_bootloader() return bootloader.write_program(file, **kwargs)
def stop(self, *args): if not self.alive.is_set(): logger(__name__).warning('Stopping terminal') self.alive.set() self.device.destroy() if threading.current_thread() != self.transmitter_thread and self.transmitter_thread.is_alive(): self.console.cleanup() self.console.cancel() logger(__name__).info('All done!')
def terminal(port: str, backend: str, **kwargs): """ Open a terminal to a serial port There are two possible backends for the terminal: "share" or "solo". In "share" mode, a server/bridge is created so that multiple PROS processes (such as another terminal or flash command) may communicate with the device. In the simpler solo mode, only one PROS process may communicate with the device. The default mode is "share", but "solo" may be preferred when "share" doesn't perform adequately. Note: share backend is not yet implemented. """ from pros.serial.devices.vex.v5_user_device import V5UserDevice from pros.serial.terminal import Terminal if port == 'default': project_path = c.Project.find_project(os.getcwd()) if project_path is None: v5_port = resolve_v5_port(None, 'user', quiet=True) cortex_port = resolve_cortex_port(None, quiet=True) if ((v5_port is None) ^ (cortex_port is None)) or (v5_port is not None and v5_port == cortex_port): port = v5_port or cortex_port else: raise click.UsageError( 'You must be in a PROS project directory to enable default port selecting' ) else: project = c.Project(project_path) port = project.target if port == 'v5': port = None port = resolve_v5_port(port, 'user') elif port == 'cortex': port = None port = resolve_cortex_port(port) kwargs['raw'] = True if not port: return -1 if backend == 'share': ser = ports.SerialSharePort(port) else: ser = ports.DirectPort(port) if kwargs.get('raw', False): device = devices.RawStreamDevice(ser) else: device = V5UserDevice(ser) term = Terminal(device, request_banner=kwargs.pop('request_banner', True)) signal.signal(signal.SIGINT, term.stop) term.start() while not term.alive.is_set(): time.sleep(0.005) term.join() logger(__name__).info('CLI Main Thread Dying')
def _txrx_ack_packet(self, command: int, timeout=0.1): """ Goes through a send/receive cycle with a VEX device. Transmits the command with the optional additional payload, then reads and parses the outer layer of the response :param command: Command to send the device :param retries: Number of retries to attempt to parse the output before giving up and raising an error :return: Returns a dictionary containing the received command field and the payload. Correctly computes the payload length even if the extended command (0x56) is used (only applies to the V5). """ tx = self._tx_packet(command) self._rx_ack(timeout=timeout) logger(__name__).debug('TX: {}'.format(bytes_to_str(tx)))
def main(): try: ctx_obj = {} click_handler = pros.common.ui.log.PROSLogHandler(ctx_obj=ctx_obj) ctx_obj['click_handler'] = click_handler formatter = pros.common.ui.log.PROSLogFormatter( '%(levelname)s - %(name)s:%(funcName)s - %(message)s', ctx_obj) click_handler.setFormatter(formatter) logging.basicConfig(level=logging.WARNING, handlers=[click_handler]) cli.main(prog_name='pros', obj=ctx_obj) except KeyboardInterrupt: click.echo('Aborted!') except Exception as e: logger(__name__).exception(e)
def fetch_template(self, template: BaseTemplate, destination: str, **kwargs) -> Template: if 'location' not in kwargs: logger(__name__).debug( f"Template not specified. Provided arguments: {kwargs}") raise KeyError('Location of local template must be specified.') location = kwargs['location'] if os.path.isdir(location): location_dir = location if not os.path.isfile(os.path.join(location_dir, 'template.pros')): raise ConfigNotFoundException( f'A template.pros file was not found in {location_dir}.') template_file = os.path.join(location_dir, 'template.pros') elif zipfile.is_zipfile(location): with zipfile.ZipFile(location) as zf: with click.progressbar( length=len(zf.namelist()), label=f"Extracting {location}") as progress_bar: for file in zf.namelist(): zf.extract(file, path=destination) progress_bar.update(1) template_file = os.path.join(destination, 'template.pros') location_dir = destination elif os.path.isfile(location): location_dir = os.path.dirname(location) template_file = location elif isinstance(template, ExternalTemplate): location_dir = template.directory template_file = template.save_file else: raise ValueError( f"The specified location was not a file or directory ({location})." ) if location_dir != destination: n_files = len([ os.path.join(dp, f) for dp, dn, fn in os.walk(location_dir) for f in fn ]) with click.progressbar(length=n_files, label='Copying to local cache') as pb: def my_copy(*args): pb.update(1) shutil.copy2(*args) shutil.copytree(location_dir, destination, copy_function=my_copy) return ExternalTemplate(file=template_file)
def read(self) -> Tuple[bytes, bytes]: msg = None, None while msg[0] is None or (msg[0] not in self.topics and not self._accept_all): while b'\0' not in self.buffer: self.buffer.extend(self.port.read(1)) self.buffer.extend(self.port.read(-1)) assert b'\0' in self.buffer msg, self.buffer = self.buffer.split(b'\0', 1) try: msg = cobs.decode(msg) except cobs.DecodeError: logger(__name__).warning(f'Could not decode bytes: {msg.hex()}') assert len(msg) >= 4 msg = bytes(msg[:4]), bytes(msg[4:]) return msg
def write_program(self, file: typing.BinaryIO, **kwargs): action_string = '' if hasattr(file, 'name'): action_string += f' {Path(file.name).name}' action_string += f' to Cortex on {self.port}' ui.echo(f'Uploading {action_string}') logger(__name__).info('Writing program to Cortex') status = self.query_system() logger(__name__).info(status) if not status.flags | self.SystemStatusFlags.TETH_USB and not status.flags | self.SystemStatusFlags.DL_MODE: self.send_to_download_channel() bootloader = self.expose_bootloader() rv = bootloader.write_program(file, **kwargs) ui.finalize('upload', f'Finished uploading {action_string}') return rv
def transmitter(self): try: while not self.alive.is_set() and self._transmitter_alive: try: c = self.console.getkey() except KeyboardInterrupt: c = '\x03' if self.alive.is_set(): break if c == '\x03' or not self.no_sigint: self.stop() break else: self.device.write(c.encode(encoding='utf-8')) self.console.write(c) except Exception as e: if not self.alive.is_set(): logger(__name__).exception(e) else: logger(__name__).debug(e) self.stop() logger(__name__).info('Terminal transmitter dying')
def create_template(ctx, path: str, destination: str, do_zip: bool, **kwargs): """ Create a template to be used in other projects Templates primarily consist of the following fields: name, version, and files to install. Templates have two types of files: system files and user files. User files are files in a template intended to be modified by users - they are not replaced during upgrades or removed by default when a library is uninstalled. System files are files that are for the "system." They get replaced every time the template is upgraded. The default PROS project is a template. The user files are files like src/opcontrol.c and src/initialize.c, and the system files are files like firmware/libpros.a and include/api.h. You should specify the --system and --user options multiple times to include more than one file. Both flags also accept glob patterns. When a glob pattern is provided and inside a PROS project, then all files that match the pattern that are NOT supplied by another template are included. Example usage: pros conduct create-template . libblrs 2.0.1 --system "firmware/*.a" --system "include/*.h" """ project = c.Project.find_project(path, recurse_times=1) if project: project = c.Project(project) path = project.location if not kwargs['supported_kernels'] and kwargs['name'] != 'kernel': kwargs['supported_kernels'] = f'^{project.kernel}' kwargs['target'] = project.target if not destination: if os.path.isdir(path): destination = path else: destination = os.path.dirname(path) kwargs['system_files'] = list(kwargs['system_files']) kwargs['user_files'] = list(kwargs['user_files']) kwargs['metadata'] = { ctx.args[i][2:]: ctx.args[i + 1] for i in range(0, int(len(ctx.args) / 2) * 2, 2) } def get_matching_files(globs: List[str]) -> Set[str]: matching_files: List[str] = [] _path = os.path.normpath(path) + os.path.sep for g in [g for g in globs if glob.has_magic(g)]: files = glob.glob(f'{path}/{g}', recursive=True) files = filter(lambda f: os.path.isfile(f), files) files = [ os.path.normpath(os.path.normpath(f).split(_path)[-1]) for f in files ] matching_files.extend(files) # matches things like src/opcontrol.{c,cpp} so that we can expand to src/opcontrol.c and src/opcontrol.cpp pattern = re.compile(r'^([\w{}]+.){{((?:\w+,)*\w+)}}$'.format( os.path.sep.replace('\\', '\\\\'))) for f in [os.path.normpath(f) for f in globs if not glob.has_magic(f)]: if re.match(pattern, f): matches = re.split(pattern, f) logger(__name__).debug(f'Matches on {f}: {matches}') matching_files.extend( [f'{matches[1]}{ext}' for ext in matches[2].split(',')]) else: matching_files.append(f) matching_files: Set[str] = set(matching_files) return matching_files matching_system_files: Set[str] = get_matching_files( kwargs['system_files']) matching_user_files: Set[str] = get_matching_files(kwargs['user_files']) matching_system_files: Set[ str] = matching_system_files - matching_user_files # exclude existing project.pros and template.pros from the template, # and name@*.zip so that we don't redundantly include ZIPs exclude_files = { 'project.pros', 'template.pros', *get_matching_files([f"{kwargs['name']}@*.zip"]) } if project: exclude_files = exclude_files.union(project.list_template_files()) matching_system_files = matching_system_files - exclude_files matching_user_files = matching_user_files - exclude_files def filename_remap(file_path: str) -> str: if os.path.dirname(file_path) == 'bin': return file_path.replace('bin', 'firmware', 1) return file_path kwargs['system_files'] = list(map(filename_remap, matching_system_files)) kwargs['user_files'] = list(map(filename_remap, matching_user_files)) if do_zip: if not os.path.isdir( destination) and os.path.splitext(destination)[-1] != '.zip': logger(__name__).error( f'{destination} must be a zip file or an existing directory.') return -1 with tempfile.TemporaryDirectory() as td: template = ExternalTemplate(file=os.path.join(td, 'template.pros'), **kwargs) template.save() if os.path.isdir(destination): destination = os.path.join(destination, f'{template.identifier}.zip') with zipfile.ZipFile(destination, mode='w') as z: z.write(template.save_file, arcname='template.pros') for file in matching_user_files: source_path = os.path.join(path, file) dest_file = filename_remap(file) if os.path.exists(source_path): ui.echo(f'U: {file}' + ( f' -> {dest_file}' if file != dest_file else '')) z.write(f'{path}/{file}', arcname=dest_file) for file in matching_system_files: source_path = os.path.join(path, file) dest_file = filename_remap(file) if os.path.exists(source_path): ui.echo(f'S: {file}' + ( f' -> {dest_file}' if file != dest_file else '')) z.write(f'{path}/{file}', arcname=dest_file) else: if os.path.isdir(destination): destination = os.path.join(destination, 'template.pros') template = ExternalTemplate(file=destination, **kwargs) template.save()
def reader(self): if self.request_banner: try: self.device.write(b'pRb') except Exception as e: logger(__name__).exception(e) try: while not self.alive.is_set() and self._reader_alive: data = self.device.read() if not data: continue if data[0] == b'sout': text = decode_bytes_to_str(data[1]) elif data[0] == b'serr': text = '{}{}{}'.format(colorama.Fore.RED, decode_bytes_to_str(data[1]), colorama.Style.RESET_ALL) elif data[0] == b'kdbg': text = '{}\n\nKERNEL DEBUG:\t{}{}\n'.format(colorama.Back.GREEN + colorama.Style.BRIGHT, decode_bytes_to_str(data[1]), colorama.Style.RESET_ALL) elif data[0] != b'': text = '{}{}'.format(decode_bytes_to_str(data[0]), decode_bytes_to_str(data[1])) else: text = "{}".format(decode_bytes_to_str(data[1])) self.console.write(text) except UnicodeError as e: logger(__name__).exception(e) except PortConnectionException: logger(__name__).warning(f'Connection to {self.device.name} broken') if not self.alive.is_set(): self.stop() except Exception as e: if not self.alive.is_set(): logger(__name__).exception(e) else: logger(__name__).debug(e) self.stop() logger(__name__).info('Terminal receiver dying')
def query_system(self) -> SystemStatus: logger(__name__).info('Querying system information') rx = self._txrx_simple_struct(0x21, "<8B2x") status = CortexDevice.SystemStatus(rx) ui.finalize('cortex-status', status) return status
def send_to_download_channel(self): logger(__name__).info('Sending to download channel') self._txrx_ack_packet(0x35, timeout=1.0)
def perform_upgrade(self) -> UpgradeResult: logger(__name__).debug(self.__dict__) from click import launch return UpgradeResult(launch(self.info_url) == 0)