def callback(ctx: click.Context, param: click.Parameter, value: str): import pros.conductor as c project_path = c.Project.find_project(value) if project_path is None: raise click.UsageError( f'{os.path.abspath(value or ".")} is not inside a PROS project. ' f'Execute this command from within a PROS project or specify it ' f'with --project project/path') return c.Project(project_path)
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 upload(path: str, project: Optional[c.Project], port: str, **kwargs): """ Upload a binary to a microcontroller. [PATH] may be a directory or file. If a directory, finds a PROS project root and uploads the binary for the correct target automatically. If a file, then the file is uploaded. Note that --target must be specified in this case. [PORT] may be any valid communication port file, such as COM1 or /dev/ttyACM0. If left blank, then a port is automatically detected based on the target (or as supplied by the PROS project) """ import pros.serial.devices.vex as vex from pros.serial.ports import DirectPort args = [] if path is None or os.path.isdir(path): if project is None: project_path = c.Project.find_project(path or os.getcwd()) if project_path is None: raise click.UsageError( 'Specify a file to upload or set the cwd inside a PROS project' ) project = c.Project(project_path) path = os.path.join(project.location, project.output) if project.target == 'v5' and not kwargs['name']: kwargs['name'] = project.name # apply upload_options as a template options = dict(**project.upload_options) options.update(kwargs) kwargs = options kwargs[ 'target'] = project.target # enforce target because uploading to the wrong uC is VERY bad if 'program-version' in kwargs: kwargs['version'] = kwargs['program-version'] if 'name' not in kwargs: kwargs['name'] = project.name if 'target' not in kwargs: logger(__name__).debug( f'Target not specified. Arguments provided: {kwargs}') raise click.UsageError( 'Target not specified. specify a project (using the file argument) or target manually' ) if kwargs['target'] == 'v5': port = resolve_v5_port(port, 'system') elif kwargs['target'] == 'cortex': port = resolve_cortex_port(port) else: logger(__name__).debug(f"Invalid target provided: {kwargs['target']}") logger(__name__).debug('Target should be one of ("v5" or "cortex").') if not port: return -1 if kwargs['target'] == 'v5': if kwargs['name'] is None: kwargs['name'] = os.path.splitext(os.path.basename(path))[0] args.append(kwargs.pop('name').replace('@', '_')) kwargs['slot'] -= 1 if kwargs['run_after'] and kwargs['run_screen']: kwargs['run_after'] = vex.V5Device.FTCompleteOptions.RUN_SCREEN elif kwargs['run_after'] and not kwargs['run_screen']: kwargs[ 'run_after'] = vex.V5Device.FTCompleteOptions.RUN_IMMEDIATELY else: kwargs['run_after'] = vex.V5Device.FTCompleteOptions.DONT_RUN elif kwargs['target'] == 'cortex': pass # print what was decided ui.echo('Uploading {} to {} device on {}'.format(path, kwargs['target'], port), nl=False) if kwargs['target'] == 'v5': ui.echo(f' as {args[0]} to slot {kwargs["slot"] + 1}', nl=False) ui.echo('') logger(__name__).debug('Arguments: {}'.format(str(kwargs))) if not os.path.isfile(path) and path is not '-': logger(__name__).error( '{} is not a valid file! Make sure it exists (e.g. by building your project)' .format(path)) return -1 # Do the actual uploading! try: ser = DirectPort(port) device = None if kwargs['target'] == 'v5': device = vex.V5Device(ser) elif kwargs['target'] == 'cortex': device = vex.CortexDevice(ser).get_connected_device() with click.open_file(path, mode='rb') as pf: device.write_program(pf, *args, **kwargs) except Exception as e: logger(__name__).exception(e, exc_info=True) exit(1) ui.finalize('upload', f'Finished uploading {path} to {kwargs["target"]} on {port}')
def upload(path: Optional[str], project: Optional[c.Project], port: str, **kwargs): """ Upload a binary to a microcontroller. [PATH] may be a directory or file. If a directory, finds a PROS project root and uploads the binary for the correct target automatically. If a file, then the file is uploaded. Note that --target must be specified in this case. [PORT] may be any valid communication port file, such as COM1 or /dev/ttyACM0. If left blank, then a port is automatically detected based on the target (or as supplied by the PROS project) """ import pros.serial.devices.vex as vex from pros.serial.ports import DirectPort if path is None or os.path.isdir(path): if project is None: project_path = c.Project.find_project(path or os.getcwd()) if project_path is None: raise click.UsageError( 'Specify a file to upload or set the cwd inside a PROS project' ) project = c.Project(project_path) path = os.path.join(project.location, project.output) if project.target == 'v5' and not kwargs['remote_name']: kwargs['remote_name'] = project.name # apply upload_options as a template options = dict(**project.upload_options) if 'slot' in options and kwargs.get('slot', None) is None: kwargs.pop('slot') elif kwargs.get('slot', None) is None: kwargs['slot'] = 1 options.update(kwargs) kwargs = options kwargs[ 'target'] = project.target # enforce target because uploading to the wrong uC is VERY bad if 'program-version' in kwargs: kwargs['version'] = kwargs['program-version'] if 'remote_name' not in kwargs: kwargs['remote_name'] = project.name if 'target' not in kwargs or kwargs['target'] is None: logger(__name__).debug( f'Target not specified. Arguments provided: {kwargs}') raise click.UsageError( 'Target not specified. specify a project (using the file argument) or target manually' ) if kwargs['target'] == 'v5': port = resolve_v5_port(port, 'system')[0] elif kwargs['target'] == 'cortex': port = resolve_cortex_port(port) else: logger(__name__).debug(f"Invalid target provided: {kwargs['target']}") logger(__name__).debug('Target should be one of ("v5" or "cortex").') if not port: raise dont_send( click.UsageError( 'No port provided or located. Make sure to specify --target if needed.' )) if kwargs['target'] == 'v5': if kwargs['remote_name'] is None: kwargs['remote_name'] = os.path.splitext(os.path.basename(path))[0] kwargs['remote_name'] = kwargs['remote_name'].replace('@', '_') kwargs['slot'] -= 1 if kwargs['run_after'] and kwargs['run_screen']: kwargs['run_after'] = vex.V5Device.FTCompleteOptions.RUN_SCREEN elif kwargs['run_after'] and not kwargs['run_screen']: kwargs[ 'run_after'] = vex.V5Device.FTCompleteOptions.RUN_IMMEDIATELY else: kwargs['run_after'] = vex.V5Device.FTCompleteOptions.DONT_RUN kwargs.pop('run_screen') elif kwargs['target'] == 'cortex': pass logger(__name__).debug('Arguments: {}'.format(str(kwargs))) # Do the actual uploading! try: ser = DirectPort(port) device = None if kwargs['target'] == 'v5': device = vex.V5Device(ser) elif kwargs['target'] == 'cortex': device = vex.CortexDevice(ser).get_connected_device() if project is not None: device.upload_project(project, **kwargs) else: with click.open_file(path, mode='rb') as pf: device.write_program(pf, **kwargs) except Exception as e: logger(__name__).exception(e, exc_info=True) exit(1)
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()