├── version ├── pip_version ├── win_version ├── pros.icns ├── scripts ├── clean.sh ├── clean.ps1 ├── install_mac_build_dependencies.sh ├── build_cli_mac.sh ├── install_build_dependencies.ps1 ├── build.sh ├── install_build_dependencies.sh ├── build.bat ├── build.ps1 └── build_pkg_mac.sh ├── requirements.txt ├── prosconductor ├── __init__.py └── providers │ ├── utils.py │ ├── __init__.py │ ├── githubreleases.py │ └── local.py ├── .gitignore ├── proscli ├── state.py ├── __init__.py ├── main.py ├── terminal.py ├── build.py ├── upgrade.py ├── conductor_management.py ├── utils.py ├── flasher.py ├── serial_terminal.py └── conductor.py ├── .github ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── .circleci └── config.yml ├── prosflasher ├── __init__.py ├── ports.py ├── bootloader.py └── upload.py ├── setup.py ├── prosconfig ├── cliconfig.py └── __init__.py ├── Jenkinsfile ├── version.py ├── README.md └── LICENSE /version: -------------------------------------------------------------------------------- 1 | 2.6.1 -------------------------------------------------------------------------------- /pip_version: -------------------------------------------------------------------------------- 1 | 2.6.1 -------------------------------------------------------------------------------- /win_version: -------------------------------------------------------------------------------- 1 | 2.6.1.0 -------------------------------------------------------------------------------- /pros.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purduesigbots/pros-cli2/HEAD/pros.icns -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ROOT=$(dirname $(dirname "$0")) 4 | echo $ROOT 5 | rm -rf $ROOT/out -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | pyserial 3 | cachetools 4 | requests 5 | tabulate 6 | jsonpickle 7 | semantic_version 8 | -------------------------------------------------------------------------------- /prosconductor/__init__.py: -------------------------------------------------------------------------------- 1 | from prosconductor.providers import DepotConfig 2 | from prosconfig import Config 3 | # from typing import List 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .pyc 3 | __pycache__/ 4 | 5 | build/ 6 | .cache/ 7 | dist/ 8 | 9 | pros_cli.egg-info/ 10 | 11 | out/ 12 | *.zip 13 | -------------------------------------------------------------------------------- /scripts/clean.ps1: -------------------------------------------------------------------------------- 1 | $root = $MyInvocation.MyCommand.Definition | Split-Path -Parent | Split-Path -Parent 2 | if(Test-Path -Path $root\out) { 3 | Remove-Item -Recurse -Force -Path $root\out 4 | } -------------------------------------------------------------------------------- /proscli/state.py: -------------------------------------------------------------------------------- 1 | import prosconfig.cliconfig 2 | 3 | 4 | class State(object): 5 | def __init__(self): 6 | self.verbosity = 0 7 | self.debug = False 8 | self.machine_output = False 9 | self.log_key = 'pros-logging' 10 | self.pros_cfg = prosconfig.cliconfig.CliConfig(ctx=self) 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Expected Behavior: 2 | 3 | #### Actual Behavior: 4 | 5 | #### Steps to reproduce: 6 | 7 | #### System information: 8 | Operating System: (Windows, OS X, Ubuntu) 9 | 10 | PROS Version: (run `pros --version`) 11 | 12 | #### Additional Information 13 | 14 | #### Screenshots/Output Dumps/Stack Traces 15 | -------------------------------------------------------------------------------- /proscli/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from proscli.terminal import terminal_cli 4 | 5 | from proscli.conductor import conductor_cli 6 | from proscli.build import build_cli 7 | from proscli.flasher import flasher_cli 8 | from proscli.upgrade import upgrade_cli 9 | 10 | from proscli.utils import pass_state, verbosity_option 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /scripts/install_mac_build_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo =============== INSTALLING PYTHON3 =============== 4 | 5 | brew upgrade python 6 | 7 | if [ $? -ne 0 ] 8 | then 9 | echo Failed to install python 10 | exit 1 11 | fi 12 | 13 | echo =============== INSTALLING PIP DEPENDENCIES =============== 14 | 15 | pip3 install --upgrade pip 16 | pip3 install wheel cx_Freeze 17 | -------------------------------------------------------------------------------- /scripts/build_cli_mac.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo =============== UPDATING VERSION =============== 4 | python3 version.py 5 | 6 | echo =============== INSTALL DEPENDENCIES =============== 7 | pip3 install --upgrade -r requirements.txt 8 | 9 | echo =============== BUILD CLI =============== 10 | python3 build.py bdist_mac 11 | 12 | echo =============== COMPRESS ARTIFACTS =============== 13 | cd build 14 | tar -cvf ../proscli.tar.gz PROS\ CLI.app 15 | cd - 16 | -------------------------------------------------------------------------------- /scripts/install_build_dependencies.ps1: -------------------------------------------------------------------------------- 1 | try { 2 | if(get-command pip3) { echo "found pip3" } 3 | else { 4 | echo "Couldn't find pip3. Make sure it's installed and available from PATH" 5 | exit 1 6 | } 7 | } catch { 8 | echo "Couldn't find pip3. Make sure it's installed and available from PATH" 9 | exit 1 10 | } 11 | 12 | pip3 install vex 13 | 14 | echo "Done installing build dependencies" 15 | echo "You should now be able to build the cli using 'vex -mr foo ./scripts/build.bat'" 16 | 17 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | macos: 5 | xcode: "9.0" 6 | steps: 7 | - checkout 8 | - run: chmod +x scripts/install_mac_build_dependencies.sh scripts/build_cli_mac.sh scripts/build_pkg_mac.sh 9 | - run: scripts/install_mac_build_dependencies.sh 10 | - run: scripts/build_cli_mac.sh 11 | - store_artifacts: 12 | path: proscli.tar.gz 13 | - run: scripts/build_pkg_mac.sh 14 | - store_artifacts: 15 | path: pros-cli.pkg 16 | -------------------------------------------------------------------------------- /prosflasher/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | def adr_to_str(address): 3 | """ 4 | Converts a well-formed bytearray address (i.e. one with a checksum) to a string 5 | :param address: 6 | :return: 7 | """ 8 | return '0x{}({:02X})'.format(''.join('{:02X}'.format(x) for x in address[:-1]), address[-1]) 9 | 10 | 11 | def bytes_to_str(arr): 12 | if isinstance(arr, str): 13 | arr = bytes(arr) 14 | if hasattr(arr, '__iter__'): 15 | return ''.join('{:02X} '.format(x) for x in arr) 16 | else: # actually just a single byte 17 | return '{:02X}'.format(arr) 18 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PROS 2 | 3 | :+1: :steam_locomotive: Thanks for taking the time to contribute :steam_locomotive: :+1: 4 | 5 | **Did you find a bug?** 6 | - **Ensure the bug wasn't already reported** by searching GitHub [issues](https://github.com/purduesigbots/pros-cli/issues) 7 | - If you're unable to find an issue, [open](https://github.com/purduesigbots/pros-cli/issues/new) one. 8 | 9 | **Did you patch a bug or add a new feature?** 10 | - Open a pull request 11 | - Ensure the pull request description clearly describes the problem and solution. If there's an issue number, include it so it can be referenced. 12 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python=python 4 | echo Testing python executable version 5 | python -c "import sys; exit(0 if sys.version_info > (3,5) else 1)" 6 | if [ $? -ne 0 ] 7 | then 8 | python=python3 9 | fi 10 | 11 | echo Installing wheel and cx_Freeze 12 | pip3 install wheel cx_Freeze 13 | 14 | echo Updating version 15 | $python version.py 16 | 17 | echo Installing pros-cli requirements 18 | pip3 install --upgrade -r requirements.txt 19 | 20 | echo Building Wheel 21 | $python setup.py bdist_wheel 22 | 23 | echo Building Binary 24 | $python build.py build_exe 25 | 26 | echo Moving artifacts to ./out 27 | mkdir -p ./out 28 | rm -rf ./out/* 29 | cp dist/pros_cli*.whl out 30 | cp pros_cli*.zip out 31 | -------------------------------------------------------------------------------- /scripts/install_build_dependencies.sh: -------------------------------------------------------------------------------- 1 | echo =============== INSTALLING PYTHON3 =============== 2 | 3 | sudo apt-get install -y python3 python3-dev python3-pip libssl-dev 4 | 5 | if [ $? -ne 0 ] 6 | then 7 | echo apt install failed to run, maybe need root privileges? 8 | exit 1 9 | fi 10 | 11 | echo =============== DONE INSTALLING PYTHON3 =============== 12 | 13 | echo =============== INSTALLING pip-vex =============== 14 | sudo pip3 install vex 15 | 16 | if [ $? -ne 0 ] 17 | then 18 | echo failed to install vex 19 | exit 1 20 | fi 21 | echo =============== DONE INSTALLING vex =============== 22 | 23 | 24 | echo BUILD DEPENDENCIES FINISHED INSTALLING 25 | echo To build, you should now be able to use 'vex -mr foo ./scripts/build.sh' 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | # setup.py for non-frozen builds 4 | from pip.req import parse_requirements 5 | install_reqs = [str(r.req) for r in parse_requirements('requirements.txt', session=False)] 6 | 7 | setup( 8 | name='pros-cli', 9 | version=open('pip_version').read().strip(), 10 | packages=['prosflasher', 'proscli', 'prosconfig', 'prosconductor', 'prosconductor.providers'], 11 | url='https://github.com/purduesigbots/pros-cli', 12 | license='MPL-2.0', 13 | author='Purdue ACM SIGBots', 14 | author_email='pros_development@cs.purdue.edu', 15 | description='Command Line Interface for managing PROS projects', 16 | install_requires=install_reqs, 17 | entry_points=""" 18 | [console_scripts] 19 | pros=proscli.main:main 20 | """ 21 | ) 22 | -------------------------------------------------------------------------------- /proscli/main.py: -------------------------------------------------------------------------------- 1 | import click 2 | import proscli 3 | from proscli.utils import default_options, get_version 4 | 5 | 6 | def main(): 7 | # the program name should always be pros. don't care if it's not... 8 | try: 9 | cli.main(prog_name='pros') 10 | except KeyboardInterrupt: 11 | click.echo('Aborted!') 12 | 13 | 14 | @click.command('pros', 15 | cls=click.CommandCollection, 16 | context_settings=dict(help_option_names=['-h', '--help']), 17 | sources=[proscli.terminal_cli, proscli.build_cli, proscli.flasher_cli, 18 | proscli.conductor_cli, proscli.upgrade_cli]) 19 | @click.version_option(version=get_version(), prog_name='pros') 20 | @default_options 21 | def cli(): 22 | pass 23 | 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /prosconfig/cliconfig.py: -------------------------------------------------------------------------------- 1 | import click 2 | import os.path 3 | import prosconductor.providers.githubreleases 4 | import sys 5 | from prosconfig import Config 6 | 7 | 8 | class CliConfig(Config): 9 | def __init__(self, file=None, ctx=None): 10 | if not file: 11 | file = os.path.join(click.get_app_dir('PROS'), 'cli.pros') 12 | self.default_libraries = [] # type: list(str) 13 | self.providers = [] 14 | self.applyDefaultProviders() 15 | super(CliConfig, self).__init__(file, ctx=ctx) 16 | 17 | def applyDefaultProviders(self): 18 | if os.path.isfile(prosconductor.providers.githubreleases.__file__): 19 | self.providers.append(prosconductor.providers.githubreleases.__file__) 20 | elif hasattr(sys, 'frozen'): 21 | self.providers.append(os.path.join(os.path.dirname(sys.executable), 'githubreleases.pyc')) 22 | -------------------------------------------------------------------------------- /scripts/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set root=%~dp0.. 4 | 5 | set python=python 6 | echo Testing python executable version 7 | python -c "import sys; exit(0 if sys.version_info > (3,5) else 1)" 8 | if errorlevel 1 set python=python3 9 | 10 | echo Installing wheel and cx_Freeze 11 | git clone --branch 5.0.2 https://github.com/anthony-tuininga/cx_Freeze.git 12 | pip3 install --upgrade cx_Freeze\. 13 | pip3 install --upgrade wheel 14 | 15 | echo Updating version 16 | %python% %root%\version.py 17 | 18 | echo Installing pros-cli requirements 19 | pip3 install --upgrade -r %root%\requirements.txt 20 | 21 | echo Building Wheel 22 | %python% %root%\setup.py bdist_wheel 23 | 24 | echo Building Binary 25 | %python% %root%\build.py build_exe 26 | 27 | echo Moving artifacts to .\out 28 | if not exist %root%\out mkdir %root%\out 29 | del /Q %root%\out\*.* 30 | copy %root%\dist\pros_cli*.whl %root%\out\ 31 | copy %root%\pros_cli*.zip %root%\out\ 32 | 33 | cd out 34 | %python% %root%\version.py 35 | cd .. 36 | 37 | -------------------------------------------------------------------------------- /scripts/build.ps1: -------------------------------------------------------------------------------- 1 | $root = $MyInvocation.MyCommand.Definition | Split-Path -Parent | Split-Path -Parent 2 | 3 | $python = python 4 | Write-Output Testing python executable version 5 | python -c "import sys; exit(0 if sys.version_info > (3,5) else 1)" 6 | if ( -not $? ) { 7 | $python=python3 8 | } 9 | 10 | Set-Location $root 11 | Write-Output "Installing wheel and cx_Freeze" 12 | pip3 install wheel cx_Freeze 13 | 14 | Write-Output "Updating version" 15 | & $python version.py 16 | 17 | Write-Output "Installing pros-cli requirements" 18 | pip3 install --upgrade -r requirements.txt 19 | 20 | Write-Output "Building wheel" 21 | & $python setup.py bdist_wheel 22 | 23 | Write-Output "Bulding binary" 24 | & $python build.py build_exe 25 | 26 | Write-Output "Moving artifacts to ./out" 27 | Remove-Item -Recurse -Force -Path .\out 28 | New-Item ./out -ItemType directory | Out-Null 29 | Remove-Item .\out\* -Recurse 30 | Copy-Item dist\pros_cli*.whl .\out 31 | Copy-Item .\pros_cli*.zip .\out 32 | 33 | Set-Location $root\out 34 | & $python ../version.py 35 | Set-Location $root 36 | -------------------------------------------------------------------------------- /scripts/build_pkg_mac.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | root=pkg/ROOT 4 | scripts=pkg/scripts 5 | identifier=edu.purdue.cs.pros.pros-cli 6 | 7 | echo =============== SETUP ENVIRONMENT =============== 8 | mkdir -p $root $scripts 9 | 10 | echo =============== CREATE SCRIPTS =============== 11 | cat << EOF > $scripts/preinstall 12 | #!/bin/sh 13 | if [ ! -d /usr/local/bin ]; then 14 | mkdir -p /usr/local/bin 15 | fi 16 | # silently uninstall previous version 17 | if [ -e /usr/local/bin/pros ]; then 18 | rm -rf /Applications/PROS\ CLI.app 19 | fi 20 | EOF 21 | 22 | cat << EOF > $scripts/postinstall 23 | #!/bin/sh 24 | # link PROS CLI binary to /usr/local/bin 25 | [ -d /usr/local/bin ] || mkdir -p /usr/local/bin 26 | ln -s /Applications/PROS\ CLI.app/Contents/MacOS/pros /usr/local/bin/pros 27 | EOF 28 | 29 | chmod +x $scripts/* 30 | 31 | echo =============== CREATE DSTRIBUTION =============== 32 | version=`cat version` 33 | cp -r build/PROS\ CLI.app $root 34 | 35 | pkgbuild \ 36 | --root $root/ \ 37 | --scripts $scripts/ \ 38 | --identifier $identifier \ 39 | --version $version \ 40 | --install-location /Applications \ 41 | pros-cli.pkg 42 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | stage('Build') { 2 | parallel ( 3 | "linux64": { 4 | node("lin64") { 5 | checkout scm 6 | sh './scripts/install_build_dependencies.sh' 7 | sh 'vex -mr jenkins ./scripts/build.sh' 8 | archiveArtifacts artifacts: 'out/*', onlyIfSuccessful: true 9 | } 10 | }, 11 | "linux86": { 12 | node("lin86") { 13 | checkout scm 14 | sh './scripts/install_build_dependencies.sh' 15 | sh 'vex -mr jenkins ./scripts/build.sh' 16 | archiveArtifacts artifacts: 'out/*', onlyIfSuccessful: true 17 | } 18 | }, 19 | "windows64": { 20 | node("win64") { 21 | checkout scm 22 | bat 'powershell -file .\\scripts\\install_build_dependencies.ps1' 23 | bat '.\\scripts\\build.bat' 24 | archiveArtifacts artifacts: 'out/*', onlyIfSuccessful: true 25 | } 26 | }, 27 | "windows86": { 28 | node("win86") { 29 | checkout scm 30 | bat 'powershell -file .\\scripts\\install_build_dependencies.ps1' 31 | bat '.\\scripts\\build.bat' 32 | archiveArtifacts artifacts: 'out/*', onlyIfSuccessful: true 33 | } 34 | } 35 | ) 36 | } -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | try: 4 | v = subprocess.check_output(['git', 'describe', '--dirty', '--abbrev'], stderr=subprocess.DEVNULL).decode().strip() 5 | if '-' in v: 6 | bv = v[:v.index('-')] 7 | bv = bv[:bv.rindex('.') + 1] + str(int(bv[bv.rindex('.') + 1:]) + 1) 8 | sempre = 'dirty' if v.endswith('-dirty') else 'commit' 9 | pippre = 'alpha' if v.endswith('-dirty') else 'pre' 10 | build = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode().strip() 11 | number_since = subprocess.check_output(['git', 'rev-list', v[:v.index('-')] + '..HEAD', '--count']).decode().strip() 12 | semver = bv + '-' + sempre + '+' + build 13 | pipver = bv + pippre + number_since 14 | winver = v[:v.index('-')] + '.' + number_since 15 | else: 16 | semver = v 17 | pipver = v 18 | winver = v + '.0' 19 | 20 | with open('version', 'w') as f: 21 | print('Semantic version is ' + semver) 22 | f.write(semver) 23 | with open('pip_version', 'w') as f: 24 | print('PIP version is ' + pipver) 25 | f.write(pipver) 26 | with open('win_version', 'w') as f: 27 | print('Windows version is ' + winver) 28 | f.write(winver) 29 | except subprocess.CalledProcessError as e: 30 | print('Error calling git') 31 | 32 | -------------------------------------------------------------------------------- /proscli/terminal.py: -------------------------------------------------------------------------------- 1 | import click 2 | import proscli.serial_terminal 3 | import prosflasher.ports 4 | import serial 5 | import signal 6 | import sys 7 | import time 8 | 9 | 10 | @click.group() 11 | def terminal_cli(): 12 | pass 13 | 14 | 15 | @terminal_cli.command(short_help='Open terminal with the microcontroller') 16 | @click.argument('port', default='default') 17 | def terminal(port): 18 | click.echo(click.style('NOTE: This is an early prototype of the terminal.' 19 | ' Nothing is guaranteed to work.', bold=True)) 20 | if port == 'default': 21 | if len(prosflasher.ports.list_com_ports()) == 1: 22 | port = prosflasher.ports.list_com_ports()[0].device 23 | elif len(prosflasher.ports.list_com_ports()) > 1: 24 | click.echo('Multiple ports were found:') 25 | click.echo(prosflasher.ports.create_port_list()) 26 | port = click.prompt('Select a port to open', 27 | type=click.Choice([p.device for p in prosflasher.ports.list_com_ports()])) 28 | else: 29 | click.echo('No ports were found.') 30 | click.get_current_context().abort() 31 | sys.exit() 32 | ser = prosflasher.ports.create_serial(port, serial.PARITY_NONE) 33 | term = proscli.serial_terminal.Terminal(ser) 34 | signal.signal(signal.SIGINT, term.stop) 35 | term.start() 36 | while term.alive: 37 | time.sleep(0.005) 38 | term.join() 39 | ser.close() 40 | print('Exited successfully') 41 | sys.exit(0) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This project has been archived. 2 | Development is currently being done purduesigbots/pros-cli (PROS CLI 3). This repository is now maintained as an archive and no further development will be done. 3 | 4 | # PROS CLI 5 | 6 | PROS is the only open source development environment for the VEX EDR Platform. 7 | 8 | This project provides all of the project management related tasks for PROS. It is currently responsible for: 9 | - Downloading kernel templates 10 | - Creating, upgrading projects 11 | - Flashing binaries to the cortex 12 | 13 | This project is built in Python 3.5, and executables are built on a modified version of cx_Freeze. 14 | 15 | ## Installing for development 16 | PROS CLI can be installed directly from source with the following prerequisites: 17 | - Python 3.5 18 | - PIP (default in Python 3.5) 19 | - Setuptools (default in PYthon 3.5) 20 | 21 | Clone this repository, then run `pip install -e `. Pip will install all the dependencies necessary. 22 | 23 | ## About this project 24 | This python project contains 4 modules: proscli, prosconductor, prosconfig, and prosflasher 25 | 26 | ### proscli 27 | proscli contains the interaction logic for the actual end user experience using the Click framework and 28 | describes all of the commands available. 29 | 30 | ### prosconductor 31 | prosconductor contains the backend logic for managing projects. It is responsible for downloading projects through a 32 | provider (GitHub provider is currently the only provider, but can be extended) 33 | 34 | 35 | ### prosconfig 36 | prosconfig contains classes which represent configuration files, such as template.pros, project.pros, and cli.pros. 37 | These files are serialized by jsonpickle. 38 | 39 | ### prosflasher 40 | prosflasher contains the logic necessary to upload binaries to the VEX Cortex Microcontroller. In the future, we'd like 41 | to reinclude the ability to manipulate the file system. 42 | -------------------------------------------------------------------------------- /proscli/build.py: -------------------------------------------------------------------------------- 1 | import click 2 | import subprocess 3 | import sys 4 | import os 5 | # import prosconfig 6 | import proscli.flasher 7 | import proscli.terminal 8 | import prosconfig 9 | 10 | 11 | @click.group() 12 | def build_cli(): 13 | pass 14 | 15 | 16 | @build_cli.command() 17 | @click.argument('build-args', nargs=-1) 18 | @click.pass_context 19 | def make(ctx, build_args): 20 | """Invokes make. 21 | 22 | If on Windows, will invoke make located in on the PROS_TOOLCHAIN. 23 | 24 | Also has the added benefit of looking for the config.pros file""" 25 | try: 26 | cfg = prosconfig.ProjectConfig(prosconfig.ProjectConfig.find_project('.')) 27 | cwd = cfg.directory 28 | except prosconfig.ConfigNotFoundException: 29 | cwd = '.' 30 | env = os.environ.copy() 31 | if os.name == 'nt': 32 | cmd = os.path.join(os.environ.get('PROS_TOOLCHAIN'), 'bin', 'make.exe') 33 | else: 34 | cmd = 'make' 35 | if os.environ.get('PROS_TOOLCHAIN'): 36 | env['PATH'] += os.pathsep + os.path.join(os.environ.get('PROS_TOOLCHAIN'), 'bin') 37 | build_args = ['make'] + list(build_args) # prepend 'make' because of magic 38 | click.echo('Invoking {} in {}...'.format(' '.join(build_args), cwd)) 39 | p = subprocess.Popen(executable=cmd, args=build_args, cwd=cwd, env=env, 40 | stdout=sys.stdout, stderr=sys.stderr) 41 | p.wait() 42 | if p.returncode != 0: 43 | ctx.exit(1) 44 | 45 | 46 | @build_cli.command(name='mu', help='Combines \'make\' and \'flash\'') 47 | @click.argument('build-args', nargs=-1) 48 | @click.pass_context 49 | def make_flash(ctx, build_args): 50 | ctx.invoke(make, build_args=build_args) 51 | ctx.invoke(proscli.flasher.flash) 52 | 53 | 54 | @build_cli.command(name='mut', help='Combines \'make\', \'flash\', and \'terminal\'') 55 | @click.argument('build-args', nargs=-1) 56 | @click.pass_context 57 | def make_flash_terminal(ctx, build_args): 58 | ctx.invoke(make, build_args=build_args) 59 | ctx.invoke(proscli.flasher.flash) 60 | ctx.invoke(proscli.terminal.terminal) 61 | -------------------------------------------------------------------------------- /proscli/upgrade.py: -------------------------------------------------------------------------------- 1 | import click 2 | from proscli.utils import default_cfg 3 | import os 4 | import os.path 5 | import subprocess 6 | import sys 7 | 8 | import json 9 | 10 | @click.group() 11 | def upgrade_cli(): 12 | pass 13 | 14 | 15 | def get_upgrade_command(): 16 | if getattr(sys, 'frozen', False): 17 | if sys.platform == 'win32': 18 | cmd = os.path.abspath(os.path.join(sys.executable, '..', '..', 'updater.exe')) 19 | if os.path.exists(cmd): 20 | return [cmd, '/reducedgui', '/checknow'] 21 | else: 22 | return False 23 | else: 24 | return False 25 | else: 26 | try: 27 | from pip._vendor import pkg_resources 28 | results = [p for p in pkg_resources.working_set if p.project_name == 'pros-cli'] 29 | if os.path.exists(os.path.join(results[0].location, '.git')): 30 | click.echo('Development environment detected.') 31 | with open(os.devnull) as devnull: 32 | if subprocess.run('where git', stdout=devnull).returncode == 0: 33 | click.echo('Using git.exe') 34 | return ['git', '-C', results[0].location, 'pull'] 35 | else: 36 | click.echo('No suitable Git executable found.') 37 | return False 38 | if len(results) == 0 or not hasattr(results[0], 'location'): 39 | return False 40 | else: 41 | return ['pip3', 'install', '-U', '-t', results[0].location, 'pros-cli'] 42 | except Exception: 43 | return False 44 | 45 | 46 | @upgrade_cli.command('upgrade', help='Provides a facility to run upgrade the PROS CLI') 47 | @default_cfg 48 | def upgrade(cfg): 49 | cmd = get_upgrade_command() 50 | if cmd is False: 51 | click.echo('Could not determine installation type.') 52 | sys.exit(1) 53 | return 54 | elif not cfg.machine_output: 55 | try: 56 | for line in execute(cmd): 57 | click.echo(line) 58 | except subprocess.CalledProcessError: 59 | click.echo('An error occurred. Aborting...') 60 | sys.exit(1) 61 | sys.exit() 62 | else: 63 | for piece in cmd: 64 | click.echo(piece) 65 | 66 | 67 | def execute(cmd): 68 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) 69 | for stdout_line in iter(p.stdout.readline, ""): 70 | yield stdout_line 71 | 72 | p.stdout.close() 73 | r = p.wait() 74 | if r: 75 | raise subprocess.CalledProcessError(r, cmd) 76 | -------------------------------------------------------------------------------- /prosflasher/ports.py: -------------------------------------------------------------------------------- 1 | import click 2 | import serial 3 | import serial.tools.list_ports 4 | 5 | USB_VID = [0x4d8, 0x67b] 6 | BAUD_RATE = 115200 7 | PARITY = serial.PARITY_NONE 8 | BYTE_SIZE = serial.EIGHTBITS 9 | STOP_BITS = serial.STOPBITS_ONE 10 | 11 | 12 | def list_com_ports(): 13 | """ 14 | :return: Returns a list of valid serial ports that we believe are VEX Cortex Microcontrollers 15 | """ 16 | def is_valid_port(p): 17 | """ 18 | Returns true if the port is has a VEX product on it by the following conditions: 19 | The Vendor ID matches the expected VEX Vendor ID (which is a default one) 20 | or VEX occurs in the product name (if it exists) 21 | """ 22 | return p.vid is not None and (p.vid in USB_VID or (isinstance(p.product, str) and 'vex' in p.product.lower())) 23 | return [p for p in serial.tools.list_ports.comports() if is_valid_port(p)] 24 | 25 | 26 | def create_serial(port, parity): 27 | """ 28 | Creates and/or configures a serial port to communicate with the Cortex Microcontroller 29 | :param port: A serial.Serial object, a device string identifier will create a corresponding serial port. 30 | Anything else will create a default serial port with no device assigned. 31 | :return: Returns a correctly configured instance of a serial.Serial object, potentially with a correctly configured 32 | device iff a correct port value was passed in 33 | """ 34 | # port_str = '' 35 | if isinstance(port, str): 36 | try: 37 | # port_str = port 38 | port = serial.Serial(port, baudrate=BAUD_RATE, bytesize=serial.EIGHTBITS, parity=parity, stopbits=serial.STOPBITS_ONE) 39 | except serial.SerialException as e: 40 | click.echo('WARNING: {}'.format(e)) 41 | port = serial.Serial(baudrate=BAUD_RATE, bytesize=serial.EIGHTBITS, parity=parity, stopbits=serial.STOPBITS_ONE) 42 | elif not isinstance(port, serial.Serial): 43 | click.echo('port was not string, send help') 44 | port = serial.Serial(baudrate=BAUD_RATE, bytesize=serial.EIGHTBITS, parity=parity, stopbits=serial.STOPBITS_ONE) 45 | 46 | assert isinstance(port, serial.Serial) 47 | 48 | # port.port = port_str if port_str != '' else None 49 | port.timeout = 0.5 50 | # port.write_timeout = 5.0 51 | port.inter_byte_timeout = 0.2 52 | return port 53 | 54 | 55 | def create_port_list(verbose=False): 56 | """ 57 | Returns a formatted string of all COM ports we believe are valid Cortex ports, delimited by \n 58 | :param verbose: If True, then the hwid will be added to the end of each device 59 | :return: A formatted string for printing describing the COM ports 60 | """ 61 | out = '' 62 | if verbose: 63 | for p in list_com_ports(): 64 | out += '{} : {} ({})\n'.format(p.device, p.description, p.hwid) 65 | else: 66 | for p in list_com_ports(): 67 | out += '{} : {}\n'.format(p.device, p.description) 68 | return out 69 | -------------------------------------------------------------------------------- /prosconductor/providers/utils.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | import importlib.util 3 | import importlib.abc 4 | import os 5 | import re 6 | from prosconductor.providers import DepotProvider, DepotConfig, TemplateTypes, Identifier, TemplateDescriptor 7 | from prosconfig.cliconfig import CliConfig 8 | # from typing import Dict, List 9 | 10 | 11 | @lru_cache() 12 | def get_all_provider_types(pros_cfg=None): 13 | if pros_cfg is None: 14 | pros_cfg = CliConfig() 15 | 16 | for provider_file in pros_cfg.providers: 17 | if os.path.isfile(provider_file): 18 | spec = importlib.util.spec_from_file_location('prosconductor.providers.{}'.format(os.path.basename(provider_file).split('.')[0]), provider_file) 19 | spec.loader.load_module() 20 | 21 | return {x.registrar: x for x in DepotProvider.__subclasses__()} 22 | 23 | 24 | @lru_cache() 25 | def get_depot(depot_cfg, pros_cfg=None): 26 | providers = get_all_provider_types(pros_cfg) 27 | if depot_cfg.registrar in providers: 28 | return providers[depot_cfg.registrar](depot_cfg) 29 | else: 30 | return None 31 | 32 | 33 | @lru_cache() 34 | def get_depot_config(name, pros_cfg=None): 35 | if pros_cfg is None: 36 | pros_cfg = CliConfig() 37 | 38 | return DepotConfig(os.path.join(pros_cfg.directory, name, 'depot.pros')) 39 | 40 | 41 | def get_depot_configs(pros_cfg=None, filters=None): 42 | if pros_cfg is None: 43 | pros_cfg = CliConfig() 44 | if filters is None or not filters: 45 | filters = ['.*'] 46 | return [depot for depot in [get_depot_config(d, pros_cfg=pros_cfg) for d in os.listdir(pros_cfg.directory) 47 | if os.path.isdir(os.path.join(pros_cfg.directory, d))] 48 | if depot.name and not all(m is None for m in [re.match(string=depot.name, pattern=f) for f in filters])] 49 | 50 | 51 | def get_depots(pros_cfg=None, filters=None): 52 | return [get_depot(depot, pros_cfg) for depot in get_depot_configs(pros_cfg, filters) 53 | if get_depot(depot, pros_cfg) is not None] 54 | 55 | 56 | def get_available_templates(pros_cfg=None, template_types=None, 57 | filters=[], offline_only=False): 58 | if pros_cfg is None: 59 | pros_cfg = CliConfig() 60 | if template_types is None: 61 | template_types = [TemplateTypes.kernel, TemplateTypes.library] 62 | 63 | result = dict() # type: Dict[TemplateTypes, Dict[Identifier, List[TemplateDescriptor]]] 64 | for template_type in template_types: 65 | result[template_type] = dict() 66 | 67 | for depot in [depot for depot in get_depots(pros_cfg, filters)]: 68 | if bool(depot.config.types) and not bool([t for t in template_types if t in depot.config.types]): 69 | continue # No intersection between the types declared by the depot and requested types 70 | templates = dict() 71 | offline = depot.list_local(template_types) 72 | if not offline_only: 73 | online = depot.list_online(template_types) 74 | else: 75 | online = {t: set() for t in template_types} 76 | for key in [k for k in online.keys() if k in offline.keys()]: 77 | templates[key] = offline[key] | online[key] 78 | for template_type, identifiers in templates.items(): 79 | for identifier in identifiers: 80 | if identifier not in result[template_type]: 81 | result[template_type][identifier] = list() 82 | result[template_type][identifier].append( 83 | TemplateDescriptor(depot=depot, 84 | online=identifier in online[template_type], 85 | offline=identifier in offline[template_type])) 86 | return result 87 | -------------------------------------------------------------------------------- /proscli/conductor_management.py: -------------------------------------------------------------------------------- 1 | from proscli.conductor import conduct, first_run 2 | from prosconductor.providers import TemplateTypes, Identifier, TemplateConfig 3 | import click 4 | import json 5 | from proscli.utils import default_cfg 6 | import prosconductor.providers.local as local 7 | import prosconductor.providers.utils as utils 8 | import prosconfig 9 | import semantic_version as semver 10 | import jsonpickle 11 | 12 | # Commands in this module are typically for automation/IDE purposes and probably won't be used by front-end users 13 | 14 | 15 | @conduct.command('create-template', short_help='Creates a template with the specified name and version') 16 | @click.argument('name') 17 | @click.argument('version') 18 | @click.argument('location') 19 | @click.option('--ignore', '-i', multiple=True) 20 | @click.option('--upgrade-files', '-u', multiple=True) 21 | @default_cfg 22 | def create_template(cfg, name, version, location, ignore, upgrade_files): 23 | first_run(cfg) 24 | template = local.create_template(utils.Identifier(name, version, None), location=location) 25 | template.template_ignore = list(ignore) 26 | template.upgrade_paths = list(upgrade_files) 27 | template.save() 28 | click.echo(jsonpickle.encode(template)) 29 | click.echo('Created template at {}'.format(template.save_file)) 30 | 31 | 32 | @conduct.command('info-project', help='Provides information about a project. Especially useful for IDEs') 33 | @click.argument('location') 34 | @default_cfg 35 | def info_project(cfg, location): 36 | project = prosconfig.ProjectConfig(path=location) 37 | details = dict() 38 | details['kernel'] = project.kernel 39 | templates = local.get_local_templates(pros_cfg=cfg.pros_cfg, template_types=[TemplateTypes.kernel]) 40 | details['kernelUpToDate'] = semver.compare(project.kernel, 41 | sorted(templates, key=lambda t: semver.Version(t.version))[-1].version) \ 42 | >= 0 43 | templates = local.get_local_templates(pros_cfg=cfg.pros_cfg, template_types=[TemplateTypes.library]) 44 | details['libraries'] = dict() 45 | if project.libraries.__class__ is dict: 46 | for (lib, ver) in project.libraries.items(): 47 | details['libraries'][lib] = dict() 48 | details['libraries'][lib]['version'] = ver 49 | sorted_versions = sorted([t.version for t in templates if t.name == lib], key=lambda v: semver.Version(v)) 50 | if len(sorted_versions) > 0: 51 | latest = semver.compare(ver, sorted_versions[-1]) >= 0 52 | else: 53 | latest = True 54 | details['libraries'][lib]['latest'] = latest 55 | click.echo(json.dumps(details)) 56 | 57 | 58 | @conduct.command('ls-registrars', help='List available registrars') 59 | @default_cfg 60 | def ls_registrars(cfg): 61 | table = { 62 | key: {'location_desc': value.location_desc, 'config': value.config} for key, value in utils.get_all_provider_types().items() 63 | } 64 | click.echo(json.dumps(table)) 65 | 66 | 67 | @conduct.command('info-depot', help='Get config for a depot') 68 | @click.argument('depot') 69 | @default_cfg 70 | def info_depot(cfg, depot): 71 | dpt = utils.get_depot_config(depot) 72 | if dpt is None: 73 | click.echo(json.dumps(dict())) 74 | else: 75 | click.echo(json.dumps(dpt.registrar_options)) 76 | 77 | 78 | @conduct.command('set-depot-key', help='Set a config key/value pair for a depot') 79 | @click.argument('depot') 80 | @click.argument('key') 81 | @click.argument('value') 82 | @default_cfg 83 | def set_depot_key(cfg, depot, key, value): 84 | dpt = utils.get_depot_config(depot) 85 | config = utils.get_all_provider_types(cfg.pros_cfg)[dpt.registrar] 86 | click.echo(config) 87 | config = config.config 88 | click.echo(config) 89 | if dpt is None: 90 | pass 91 | if config.get(key, dict()).get('method', 'str') == 'bool': 92 | value = value in ['true', 'True', 'TRUE', '1', 't', 'y', 'yes'] 93 | dpt.registrar_options[key] = value 94 | dpt.save() 95 | -------------------------------------------------------------------------------- /prosconfig/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | import json.decoder 3 | import jsonpickle 4 | import os.path 5 | import proscli 6 | import proscli.utils 7 | # from typing import List 8 | 9 | 10 | class ConfigNotFoundException(Exception): 11 | def __init__(self, message, *args, **kwargs): 12 | super(ConfigNotFoundException, self).__init__(args, kwargs) 13 | self.message = message 14 | 15 | 16 | class Config(object): 17 | def __init__(self, file, error_on_decode=False, ctx=None): 18 | proscli.utils.debug('Opening {} ({})'.format(file, self.__class__.__name__), ctx=ctx) 19 | self.save_file = file # type: str 20 | self.__ignored = ['save_file', '_Config__ignored'] # type: list(str) 21 | if file: 22 | if os.path.isfile(file): 23 | with open(file, 'r') as f: 24 | try: 25 | self.__dict__.update(jsonpickle.decode(f.read()).__dict__) 26 | except (json.decoder.JSONDecodeError, AttributeError): 27 | if error_on_decode: 28 | raise 29 | else: 30 | pass 31 | elif os.path.isdir(file): 32 | raise ValueError('{} must be a file, not a directory'.format(file)) 33 | else: 34 | try: 35 | self.save() 36 | except Exception: 37 | pass 38 | 39 | def __getstate__(self): 40 | state = self.__dict__.copy() 41 | if '_Config__ignored' in self.__dict__: 42 | for key in [k for k in self.__ignored if k in state]: 43 | del state[key] 44 | return state 45 | 46 | def __setstate__(self, state): 47 | self.__dict__.update(state) 48 | 49 | def delete(self): 50 | if os.path.isfile(self.save_file): 51 | os.remove(self.save_file) 52 | 53 | def save(self, file=None): 54 | if file is None: 55 | file = self.save_file 56 | if isinstance(click.get_current_context().obj, proscli.utils.State) and click.get_current_context().obj.debug: 57 | proscli.utils.debug('Pretty Formatting {} File'.format(self.__class__.__name__)) 58 | jsonpickle.set_encoder_options('json', sort_keys=True, indent=4) 59 | else: 60 | jsonpickle.set_encoder_options('json', sort_keys=True) 61 | if os.path.dirname(file): 62 | os.makedirs(os.path.dirname(file), exist_ok=True) 63 | with open(file, 'w') as f: 64 | f.write(jsonpickle.encode(self)) 65 | 66 | @property 67 | def directory(self) -> str: 68 | return os.path.dirname(os.path.abspath(self.save_file)) 69 | 70 | 71 | class ProjectConfig(Config): 72 | def __init__(self, path: str='.', create: bool=False, raise_on_error: bool=True): 73 | file = ProjectConfig.find_project(path or '.') 74 | if file is None and create: 75 | file = os.path.join(path, 'project.pros') 76 | elif file is None and raise_on_error: 77 | raise ConfigNotFoundException('A project config was not found for {}'.format(path)) 78 | 79 | self.kernel = None # type: str 80 | self.libraries = {} # type: List[str] 81 | self.output = 'bin/output.bin' # type: str 82 | super(ProjectConfig, self).__init__(file, error_on_decode=raise_on_error) 83 | 84 | @staticmethod 85 | def find_project(path): 86 | path = os.path.abspath(path) 87 | if os.path.isfile(path): 88 | return path 89 | elif os.path.isdir(path): 90 | for n in range(10): 91 | if path is not None and os.path.isdir(path): 92 | files = [f for f in os.listdir(path) 93 | if os.path.isfile(os.path.join(path, f)) and f.lower() == 'project.pros'] 94 | if len(files) == 1: # found a project.pros file! 95 | return os.path.join(path, files[0]) 96 | path = os.path.dirname(path) 97 | else: 98 | return None 99 | return None 100 | -------------------------------------------------------------------------------- /proscli/utils.py: -------------------------------------------------------------------------------- 1 | import click 2 | import sys 3 | import os.path 4 | 5 | from proscli.state import State 6 | 7 | pass_state = click.make_pass_decorator(State) 8 | 9 | def get_version(): 10 | try: 11 | return open(os.path.join(os.path.dirname(__file__), '..', 'version')).read().strip() 12 | except Exception: 13 | try: 14 | if getattr(sys, 'frozen', False): 15 | import BUILD_CONSTANTS 16 | return BUILD_CONSTANTS.CLI_VERSION 17 | except Exception: 18 | pass 19 | return None # Let click figure it out 20 | 21 | def verbosity_option(f): 22 | """ 23 | provides a wrapper for creating the verbosity option (so don't have to create callback, parameters, etc.) 24 | """ 25 | def callback(ctx, param, value): 26 | state = ctx.ensure_object(State) 27 | state.verbosity += value 28 | return value 29 | return click.option('-v', '--verbose', count=True, expose_value=False, help='Enable verbosity level.', 30 | is_eager=True, callback=callback)(f) 31 | 32 | 33 | def debug_option(f): 34 | """ 35 | provides a wrapper for creating the debug option (so don't have to create callback, parameters, etc.) 36 | """ 37 | def callback(ctx, param, value): 38 | if not value: 39 | return 40 | state = ctx.ensure_object(State) 41 | state.debug = value 42 | return value 43 | return click.option('-d', '--debug', expose_value=False, is_flag=True, default=False, is_eager=True, 44 | help='Enable debugging output.', callback=callback)(f) 45 | 46 | 47 | def machine_output_option(f): 48 | """ 49 | provides a wrapper for creating the machine output option (so don't have to create callback, parameters, etc.) 50 | """ 51 | def callback(ctx, param, value): 52 | if not value: 53 | return 54 | state = ctx.ensure_object(State) 55 | state.machine_output = value 56 | return value 57 | decorator = click.option('--machine-output', expose_value=False, is_flag=True, default=False, is_eager=True, 58 | help='Enable machine friendly output.', callback=callback)(f) 59 | decorator.__name__ = f.__name__ 60 | return decorator 61 | 62 | 63 | def default_options(f): 64 | """ 65 | combines verbosity, debug, machine output options (most commonly used) 66 | """ 67 | decorator = verbosity_option(debug_option(machine_output_option(f))) 68 | decorator.__name__ = f.__name__ 69 | return decorator 70 | 71 | 72 | def default_cfg(f): 73 | """ 74 | combines default options and passes the state object 75 | :param f: 76 | :return: 77 | """ 78 | return pass_state(default_options(f)) 79 | 80 | 81 | def debug(content, ctx=None, debug_flag=None): 82 | if debug_flag is None: 83 | if ctx is None: 84 | try: 85 | ctx = click.get_current_context() 86 | except Exception: 87 | ctx = State() 88 | if ctx is not None and isinstance(ctx, click.Context): 89 | ctx = ctx.obj 90 | else: 91 | ctx = State() 92 | debug_flag = ctx.debug 93 | 94 | if debug_flag: 95 | click.echo('\tDEBUG: {}'.format(content)) 96 | 97 | 98 | def verbose(content, level: int = 1, ctx=None): 99 | if ctx is None: 100 | try: 101 | ctx = click.get_current_context() 102 | except Exception: 103 | ctx = State() 104 | if ctx is not None and isinstance(ctx, click.Context): 105 | ctx = ctx.obj 106 | elif not isinstance(ctx, State): 107 | ctx = State() 108 | 109 | if ctx.verbosity >= level: 110 | click.echo(content) 111 | 112 | 113 | class AliasGroup(click.Group): 114 | def __init__(self, *args, **kwargs): 115 | super(AliasGroup, self).__init__(*args, **kwargs) 116 | self.cmd_dict = dict() 117 | 118 | def command(self, *args, aliases=[], **kwargs): 119 | def decorator(f): 120 | for alias in aliases: 121 | self.cmd_dict[alias] = f.__name__ if len(args) == 0 else args[0] 122 | cmd = super(AliasGroup, self).command(*args, **kwargs)(f) 123 | self.add_command(cmd) 124 | return cmd 125 | return decorator 126 | 127 | def group(self, aliases=None, *args, **kwargs): 128 | def decorator(f): 129 | for alias in aliases: 130 | self.cmd_dict[alias] = f.__name__ 131 | cmd = super(AliasGroup, self).group(*args, **kwargs)(f) 132 | self.add_command(cmd) 133 | return cmd 134 | return decorator 135 | 136 | def get_command(self, ctx, cmd_name): 137 | # return super(AliasGroup, self).get_command(ctx, cmd_name) 138 | suggestion = super(AliasGroup, self).get_command(ctx, cmd_name) 139 | if suggestion is not None: 140 | return suggestion 141 | if cmd_name in self.cmd_dict: 142 | return super(AliasGroup, self).get_command(ctx, self.cmd_dict[cmd_name]) 143 | return None 144 | 145 | -------------------------------------------------------------------------------- /prosconductor/providers/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | import collections 3 | import enum 4 | import os.path 5 | from prosconfig import Config 6 | import shutil 7 | # from typing import List, Dict, Set, Union 8 | 9 | 10 | class InvalidIdentifierException(Exception): 11 | def __init__(self, message, *args, **kwargs): 12 | self.message = message 13 | super(InvalidIdentifierException, self).__init__(args, kwargs) 14 | 15 | 16 | class Identifier(collections.namedtuple('Identifier', ['name', 'version', 'depot'])): 17 | def __hash__(self): 18 | return (self.name + self.version + self.depot).__hash__() 19 | 20 | # Identifier = collections.namedtuple('Identifier', ['name', 'version', 'depot_registrar']) 21 | 22 | 23 | class TemplateTypes(enum.Enum): 24 | kernel = 1 << 0 25 | library = 1 << 1 26 | 27 | 28 | TemplateDescriptor = collections.namedtuple('TemplateDescriptor', ['depot', 'offline', 'online']) 29 | 30 | 31 | class DepotConfig(Config): 32 | def __init__(self, 33 | file=None, name=None, registrar=None, location=None, 34 | registrar_options=None, 35 | types=None, 36 | root_dir=None): 37 | self.name = name # type: str 38 | self.registrar = registrar # type: str 39 | self.location = location # type: str 40 | self.types = types if types is not None else [] # type: List[TemplateTypes] 41 | self.registrar_options = registrar_options if registrar_options is not None else dict() # type: Dict[str, str] 42 | if not file: 43 | file = os.path.join((root_dir if root_dir is not None else click.get_app_dir('PROS')), name, 'depot.pros') 44 | super(DepotConfig, self).__init__(file) 45 | 46 | def delete(self): 47 | super(DepotConfig, self).delete() 48 | shutil.rmtree(self.directory) 49 | 50 | 51 | class TemplateConfig(Config): 52 | def __init__(self, file): 53 | self.name = None # type: str 54 | self.version = None # type: str 55 | self.depot = None # type: str 56 | self.template_ignore = [] # type: List[str] 57 | self.remove_paths = [] # type List[str] 58 | self.upgrade_paths = [] # type: List[str] 59 | super(TemplateConfig, self).__init__(file) 60 | 61 | @property 62 | def identifier(self): 63 | return Identifier(name=self.name, version=self.version, depot=self.depot) 64 | 65 | 66 | class DepotProvider(object): 67 | registrar = 'default-provider' 68 | location_desc = 'A URL or identifier for a specific depot' 69 | config = {} 70 | 71 | def __init__(self, config): 72 | self.config = config 73 | 74 | def list_online(self, template_types=None): 75 | pass 76 | 77 | def list_latest(self, name): 78 | """ 79 | 80 | :param name: 81 | :return: 82 | """ 83 | pass 84 | 85 | def download(self, identifier): 86 | """ 87 | Downloads the specified template with the given name and version 88 | :return: True if successful, False if not 89 | """ 90 | pass 91 | 92 | def list_local(self, template_types=None): 93 | if template_types is None: 94 | template_types = [TemplateTypes.kernel, TemplateTypes.library] 95 | 96 | result = dict() # type: Dict[TemplateTypes, Set[Identifier]] 97 | for template_type in template_types: 98 | result[template_type] = set() 99 | 100 | for item in [os.path.join(self.config.directory, x) for x in os.listdir(self.config.directory) 101 | if os.path.isdir(os.path.join(self.config.directory, x))]: 102 | if TemplateTypes.kernel in template_types and 'template.pros' in os.listdir(item) and os.path.basename(item).startswith('kernel'): 103 | template_config = TemplateConfig(os.path.join(item, 'template.pros')) 104 | template_config.depot = self.config.name 105 | result[TemplateTypes.kernel].add(template_config.identifier) 106 | elif TemplateTypes.library in template_types and 'template.pros' in os.listdir(item) and not os.path.basename(item).startswith('kernel'): 107 | template_config = TemplateConfig(os.path.join(item, 'template.pros')) 108 | template_config.depot = self.config.name 109 | result[TemplateTypes.library].add(template_config.identifier) 110 | return result 111 | 112 | def verify_configuration(self): 113 | """ 114 | Verifies the current configuration (i.e. is the location valid) 115 | :return: Something falsey if valid, an exception (to be raised or displayed) 116 | """ 117 | pass 118 | 119 | 120 | def get_template_dir(depot, identifier): 121 | if isinstance(depot, DepotConfig): 122 | depot = depot.name 123 | elif isinstance(depot, DepotProvider): 124 | depot = depot.config.name 125 | elif not isinstance(depot, str): 126 | raise ValueError('Depot must a str, DepotConfig, or DepotProvider') 127 | assert isinstance(depot, str) 128 | return os.path.join(click.get_app_dir('PROS'), depot, '{}-{}'.format(identifier.name, identifier.version)) 129 | -------------------------------------------------------------------------------- /prosflasher/bootloader.py: -------------------------------------------------------------------------------- 1 | import click 2 | import serial 3 | import prosflasher.upload 4 | import time 5 | from proscli.utils import debug 6 | from prosflasher import adr_to_str, bytes_to_str 7 | 8 | ACK = 0x79 9 | MAX_WRITE_SIZE = 256 10 | 11 | 12 | def debug_response(command, response, fmt='STM BL RESPONSE TO 0x{}: {}'): 13 | if not isinstance(command, str): 14 | command = bytes_to_str(command) 15 | if not isinstance(response, str): 16 | response = bytes_to_str(response) 17 | debug(fmt.format(command, response)) 18 | 19 | 20 | def send_bootloader_command(port, command, response_size=1): 21 | port.write([command, 0xff - command]) 22 | time.sleep(0.01) 23 | response = port.read(response_size) 24 | debug_response(command, response) 25 | return response 26 | 27 | 28 | def compute_address_commandable(address): 29 | """ 30 | Creates a commandable address, with the checksum appended 31 | :param address: A list of bytes corresponding to the address or the actual address in question 32 | :return: A list of bytes corresponding to the address with the checksum appended 33 | """ 34 | if not isinstance(address, bytearray): 35 | if isinstance(address, bytes) or isinstance(address, list): 36 | address = bytearray(address) 37 | else: 38 | address = [(address >> 24) & 0xff, (address >> 16) & 0xff, (address >> 8) & 0xff, address & 0xff] 39 | checksum = 0x00 40 | for x in address: 41 | checksum ^= x 42 | address.append(checksum) 43 | return address 44 | 45 | 46 | def prepare_bootloader(port): 47 | click.echo('Preparing bootloader... ', nl=False) 48 | # for _ in range(0, 3): 49 | # response = send_bootloader_command(port, 0x00, 15) 50 | # if response is not None and len(response) == 15 and response[0] == ACK and response[-1] == ACK: 51 | # click.echo('complete') 52 | # return True 53 | time.sleep(0.01) 54 | port.rts = 0 55 | time.sleep(0.01) 56 | for _ in range(0, 3): 57 | port.write([0x7f]) 58 | response = port.read(1) 59 | debug_response(0x7f, response) 60 | if not (response is None or len(response) != 1 or response[0] != ACK): 61 | time.sleep(0.01) 62 | response = send_bootloader_command(port, 0x00, 15) 63 | debug_response(0x00, response) 64 | if response is None or len(response) != 15 or response[0] != ACK or response[-1] != ACK: 65 | click.echo('failed (couldn\'t verify commands)') 66 | return False 67 | # send_bootloader_command(port, 0x01, 5) 68 | # send_bootloader_command(port, 0x02, 5) 69 | click.echo('complete') 70 | return True 71 | click.echo('failed') 72 | return False 73 | 74 | 75 | def read_memory(port, start_address, size=0x100): 76 | size -= 1 77 | start_address = compute_address_commandable(start_address) 78 | click.echo('Reading {} bytes from 0x{}...'.format(size, ''.join('{:02X}'.format(x) for x in start_address[:-1]))) 79 | response = send_bootloader_command(port, 0x11) 80 | if response is None or response[0] != 0x79: 81 | click.echo('failed (could not begin read)') 82 | return False 83 | port.write(start_address) 84 | port.flush() 85 | response = port.read(1) 86 | debug_response('address', response) 87 | if response is None or response[0] != 0x79: 88 | click.echo('failed (address not accepted)') 89 | return False 90 | click.echo(''.join('0x{:02X} '.format(x) for x in [size, 0xff - size])) 91 | port.write([size, 0xff - size]) 92 | port.flush() 93 | response = port.read(1) 94 | debug_response('size', response) 95 | if response is None or response[0] != 0x79: 96 | click.echo('failed (size not accepted)') 97 | return False 98 | data = port.read(size + 1) 99 | if data is not None: 100 | click.echo('DATA: ' + ''.join('0x{:02X} '.format(x) for x in data)) 101 | data = port.read_all() 102 | if data is not None: 103 | click.echo('EXTRA DATA: ' + ''.join('0x{:02X} '.format(x) for x in data)) 104 | return True 105 | 106 | 107 | def erase_flash(port): 108 | click.echo('Erasing user flash... ', nl=False) 109 | port.flush() 110 | response = send_bootloader_command(port, 0x43, 1) 111 | if response is None or len(response) < 1 or response[0] != 0x79: 112 | click.echo('failed') 113 | return False 114 | time.sleep(0.01) 115 | response = send_bootloader_command(port, 0xff, 1) 116 | debug_response(0xff, response) 117 | if response is None or len(response) < 1 or response[0] != 0x79: 118 | click.echo('failed (address unacceptable)') 119 | return False 120 | click.echo('complete') 121 | return True 122 | 123 | 124 | def write_flash(port, start_address, data, retry=2, is_wireless=False): 125 | data = bytearray(data) 126 | if len(data) > 256: 127 | click.echo('Tried writing too much data at once! ({} bytes)'.format(len(data))) 128 | return False 129 | port.read_all() 130 | c_addr = compute_address_commandable(start_address) 131 | debug('Writing {} bytes to {}'.format(len(data), adr_to_str(c_addr))) 132 | response = send_bootloader_command(port, 0x31) 133 | if response is None or len(response) < 1 or response[0] != ACK: 134 | if retry > 0: 135 | debug('RETRYING PACKET AT COMMAND') 136 | return write_flash(port, start_address, data, retry=retry - 1) 137 | else: 138 | click.echo('failed (write command not accepted)') 139 | return False 140 | port.write(c_addr) 141 | port.flush() 142 | time.sleep(0.005 if is_wireless else 0.002) 143 | response = port.read(1) 144 | debug_response(adr_to_str(c_addr), response) 145 | if response is None or len(response) < 1 or response[0] != ACK: 146 | if retry > 0: 147 | debug('RETRYING PACKET AT ADDRESS') 148 | return write_flash(port, start_address, data, retry=retry - 1) 149 | else: 150 | click.echo('failed (address not accepted)') 151 | return False 152 | checksum = len(data) - 1 153 | for x in data: 154 | checksum ^= x 155 | send_data = data[:] 156 | send_data.insert(0, len(send_data) - 1) 157 | send_data.append(checksum) 158 | port.write(send_data) 159 | port.flush() 160 | time.sleep(0.007 if is_wireless else 0.002) 161 | response = port.read(1) 162 | debug('STM BL RESPONSE TO WRITE: {}'.format(response)) 163 | if response is None or len(response) < 1 or response[0] != ACK: 164 | if retry > 0: 165 | debug('RETRYING PACKET AT WRITE') 166 | return write_flash(port, start_address, data, retry=retry - 1) 167 | else: 168 | click.echo('failed (could not complete upload)') 169 | return False 170 | port.flush() 171 | port.reset_input_buffer() 172 | return True 173 | 174 | 175 | def chunks(l, n): 176 | """Yield successive n-sized chunks from l.""" 177 | for i in range(0, len(l), n): 178 | yield l[i:i + n] 179 | 180 | 181 | def upload_binary(port, file, is_wireless=False): 182 | address = 0x08000000 183 | with open(file, 'rb') as f: 184 | data = bytes(f.read()) 185 | data_segments = [data[x:x + MAX_WRITE_SIZE] for x in range(0, len(data), MAX_WRITE_SIZE)] 186 | with click.progressbar(data_segments, label='Uploading binary to Cortex...') as segments: 187 | for segment in segments: 188 | if not write_flash(port, address, segment, is_wireless=is_wireless): 189 | return False 190 | address += 0x100 191 | return True 192 | 193 | 194 | def send_go_command(port, address, retry=3): 195 | click.echo('Executing user code... ', nl=False) 196 | c_addr = compute_address_commandable(address) 197 | debug('Executing binary at {}'.format(adr_to_str(c_addr))) 198 | 199 | response = send_bootloader_command(port, 0x21, 1) 200 | debug_response(0x21, response) 201 | if response is None or len(response) < 1 or response[0] != ACK: 202 | click.echo('failed (execute command not accepted)') 203 | return False 204 | time.sleep(0.01) 205 | port.write(c_addr) 206 | time.sleep(0.01) 207 | response = port.read(1) 208 | debug_response(adr_to_str(c_addr), response) 209 | if response is None or len(response) < 1 or response[0] != ACK: 210 | click.echo('user code might not have started properly. May need to restart Cortex') 211 | else: 212 | click.echo('complete') 213 | return True 214 | -------------------------------------------------------------------------------- /proscli/flasher.py: -------------------------------------------------------------------------------- 1 | import click 2 | import os 3 | import os.path 4 | import ntpath 5 | import serial 6 | import sys 7 | import prosflasher.ports 8 | import prosflasher.upload 9 | import prosconfig 10 | from proscli.utils import default_cfg, AliasGroup 11 | from proscli.utils import get_version 12 | 13 | 14 | @click.group(cls=AliasGroup) 15 | def flasher_cli(): 16 | pass 17 | 18 | 19 | @flasher_cli.command(short_help='Upload binaries to the microcontroller.', aliases=['upload']) 20 | @click.option('-sfs/-dfs', '--save-file-system/--delete-file-system', is_flag=True, default=False, 21 | help='Specify whether or not to save the file system when writing to the Cortex. Saving the ' 22 | 'file system takes more time.') 23 | @click.option('-y', is_flag=True, default=False, 24 | help='Automatically say yes to all confirmations.') 25 | @click.option('-f', '-b', '--file', '--binary', default='default', metavar='FILE', 26 | help='Specifies a binary file, project directory, or project config file.') 27 | @click.option('-p', '--port', default='auto', metavar='PORT', help='Specifies the serial port.') 28 | @click.option('--no-poll', is_flag=True, default=False) 29 | @click.option('-r', '--retry', default=2, 30 | help='Specify the number of times the flasher should retry the flash when it detects a failure' 31 | ' (default two times).') 32 | @default_cfg 33 | # @click.option('-m', '--strategy', default='cortex', metavar='STRATEGY', 34 | # help='Specify the microcontroller upload strategy. Not currently used.') 35 | def flash(ctx, save_file_system, y, port, binary, no_poll, retry): 36 | """Upload binaries to the microcontroller. A serial port and binary file need to be specified. 37 | 38 | By default, the port is automatically selected (if you want to be pedantic, 'auto'). 39 | Otherwise, a system COM port descriptor needs to be used. In Windows/NT, this takes the form of COM1. 40 | In *nx systems, this takes the form of /dev/tty1 or /dev/acm1 or similar. 41 | \b 42 | Specifying 'all' as the COM port will automatically upload to all available microcontrollers. 43 | 44 | By default, the CLI will look around for a proper binary to upload to the microcontroller. If one was not found, or 45 | if you want to change the default binary, you can specify it. 46 | """ 47 | click.echo(' ====:: PROS Flasher v{} ::===='.format(get_version())) 48 | if port == 'auto': 49 | ports = prosflasher.ports.list_com_ports() 50 | if len(ports) == 0: 51 | click.echo('No microcontrollers were found. Please plug in a cortex or manually specify a serial port.\n', 52 | err=True) 53 | click.get_current_context().abort() 54 | sys.exit(1) 55 | port = ports[0].device 56 | if len(ports) > 1 and port is not None and y is False: 57 | port = None 58 | for p in ports: 59 | if click.confirm('Download to ' + p.device, default=True): 60 | port = p.device 61 | break 62 | if port is None: 63 | click.echo('No additional ports found.') 64 | click.get_current_context().abort() 65 | sys.exit(1) 66 | if port == 'all': 67 | port = [p.device for p in prosflasher.ports.list_com_ports()] 68 | if len(port) == 0: 69 | click.echo('No microcontrollers were found. Please plug in a cortex or manually specify a serial port.\n', 70 | err=True) 71 | click.get_current_context().abort() 72 | sys.exit(1) 73 | if y is False: 74 | click.confirm('Download to ' + ', '.join(port), default=True, abort=True, prompt_suffix='?') 75 | else: 76 | port = [port] 77 | 78 | if binary == 'default': 79 | binary = os.getcwd() 80 | if ctx.verbosity > 3: 81 | click.echo('Default binary selected, new directory is {}'.format(binary)) 82 | 83 | binary = find_binary(binary) 84 | 85 | if binary is None: 86 | click.echo('No binary was found! Ensure you are in a built PROS project (run make) ' 87 | 'or specify the file with the -f flag', 88 | err=True) 89 | click.get_current_context().exit() 90 | 91 | if ctx.verbosity > 3: 92 | click.echo('Final binary is {}'.format(binary)) 93 | 94 | click.echo('Flashing ' + binary + ' to ' + ', '.join(port)) 95 | for p in port: 96 | tries = 1 97 | code = prosflasher.upload.upload(p, y, binary, no_poll, ctx) 98 | while tries <= retry and (not code or code == -1000): 99 | click.echo('Retrying...') 100 | code = prosflasher.upload.upload(p, y, binary, no_poll, ctx) 101 | tries += 1 102 | 103 | 104 | def find_binary(path): 105 | """ 106 | Helper function for finding the binary associated with a project 107 | 108 | The algorithm is as follows: 109 | - if it is a file, then check if the name of the file is 'pros.config': 110 | - if it is 'pros.config', then find the binary based off the pros.config value (or default 'bin/output.bin') 111 | - otherwise, can only assume it is the binary file to upload 112 | - if it is a directory, start recursively searching up until 'pros.config' is found. max 10 times 113 | - if the pros.config file was found, find binary based off of the pros.config value 114 | - if no pros.config file was found, start recursively searching up (from starting path) until a directory 115 | named bin is found 116 | - if 'bin' was found, return 'bin/output.bin' 117 | :param path: starting path to start the search 118 | :param ctx: 119 | :return: 120 | """ 121 | # logger = logging.getLogger(ctx.log_key) 122 | # logger.debug('Finding binary for {}'.format(path)) 123 | if os.path.isfile(path): 124 | if ntpath.basename(path) == 'pros.config': 125 | pros_cfg = prosconfig.ProjectConfig(path) 126 | return os.path.join(path, pros_cfg.output) 127 | return path 128 | elif os.path.isdir(path): 129 | try: 130 | cfg = prosconfig.ProjectConfig(path, raise_on_error=True) 131 | if cfg is not None and os.path.isfile(os.path.join(cfg.directory, cfg.output)): 132 | return os.path.join(cfg.directory, cfg.output) 133 | except prosconfig.ConfigNotFoundException: 134 | search_dir = path 135 | for n in range(10): 136 | dirs = [d for d in os.listdir(search_dir) 137 | if os.path.isdir(os.path.join(path, search_dir, d)) and d == 'bin'] 138 | if len(dirs) == 1: # found a bin directory 139 | if os.path.isfile(os.path.join(path, search_dir, 'bin', 'output.bin')): 140 | return os.path.join(path, search_dir, 'bin', 'output.bin') 141 | search_dir = ntpath.split(search_dir)[:-1][0] # move to parent dir 142 | return None 143 | 144 | 145 | @flasher_cli.command('poll', short_help='Polls a microcontroller for its system info') 146 | @click.option('-y', '--yes', is_flag=True, default=False, 147 | help='Automatically say yes to all confirmations.') 148 | @click.argument('port', default='all') 149 | @default_cfg 150 | def get_sys_info(cfg, yes, port): 151 | if port == 'auto': 152 | ports = prosflasher.ports.list_com_ports() 153 | if len(ports) == 0: 154 | click.echo('No microcontrollers were found. Please plug in a cortex or manually specify a serial port.\n', 155 | err=True) 156 | sys.exit(1) 157 | port = prosflasher.ports.list_com_ports()[0].device 158 | if port is not None and yes is False: 159 | click.confirm('Poll ' + port, default=True, abort=True, prompt_suffix='?') 160 | if port == 'all': 161 | port = [p.device for p in prosflasher.ports.list_com_ports()] 162 | if len(port) == 0: 163 | click.echo('No microcontrollers were found. Please plug in a cortex or manually specify a serial port.\n', 164 | err=True) 165 | sys.exit(1) 166 | else: 167 | port = [port] 168 | 169 | for p in port: 170 | sys_info = prosflasher.upload.ask_sys_info(prosflasher.ports.create_serial(p, serial.PARITY_EVEN), cfg) 171 | click.echo(repr(sys_info)) 172 | 173 | pass 174 | 175 | 176 | @flasher_cli.command(short_help='List connected microcontrollers') 177 | @default_cfg 178 | def lsusb(cfg): 179 | if len(prosflasher.ports.list_com_ports()) == 0 or prosflasher.ports.list_com_ports() is None: 180 | click.echo('No serial ports found.') 181 | else: 182 | click.echo('Available Ports:') 183 | click.echo(prosflasher.ports.create_port_list(cfg.verbosity > 0)) 184 | 185 | 186 | # @flasher_cli.command(name='dump-cortex', short_help='Dumps user flash contents to a specified file') 187 | # @click.option('-v', '--verbose', is_flag=True) 188 | # @click.argument('file', default=sys.stdout, type=click.File()) 189 | # def dump_cortex(file, verbose): 190 | # pass 191 | -------------------------------------------------------------------------------- /proscli/serial_terminal.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | from proscli.utils import debug 4 | import serial 5 | import signal 6 | import sys 7 | import time 8 | import threading 9 | 10 | # This file is a modification of the miniterm implementation on pyserial 11 | 12 | 13 | class ConsoleBase(object): 14 | """OS abstraction for console (input/output codec, no echo)""" 15 | 16 | def __init__(self): 17 | if sys.version_info >= (3, 0): 18 | self.byte_output = sys.stdout.buffer 19 | else: 20 | self.byte_output = sys.stdout 21 | self.output = sys.stdout 22 | 23 | def setup(self): 24 | """Set console to read single characters, no echo""" 25 | 26 | def cleanup(self): 27 | """Restore default console settings""" 28 | 29 | def getkey(self): 30 | """Read a single key from the console""" 31 | return None 32 | 33 | def write_bytes(self, byte_string): 34 | """Write bytes (already encoded)""" 35 | self.byte_output.write(byte_string) 36 | self.byte_output.flush() 37 | 38 | def write(self, text): 39 | """Write string""" 40 | self.output.write(text) 41 | self.output.flush() 42 | 43 | def cancel(self): 44 | """Cancel getkey operation""" 45 | 46 | # - - - - - - - - - - - - - - - - - - - - - - - - 47 | # context manager: 48 | # switch terminal temporary to normal mode (e.g. to get user input) 49 | 50 | def __enter__(self): 51 | self.cleanup() 52 | return self 53 | 54 | def __exit__(self, *args, **kwargs): 55 | self.setup() 56 | 57 | 58 | if os.name == 'nt': # noqa 59 | import msvcrt 60 | import ctypes 61 | 62 | class Out(object): 63 | """file-like wrapper that uses os.write""" 64 | 65 | def __init__(self, fd): 66 | self.fd = fd 67 | 68 | def flush(self): 69 | pass 70 | 71 | def write(self, s): 72 | os.write(self.fd, s) 73 | 74 | class Console(ConsoleBase): 75 | def __init__(self): 76 | super(Console, self).__init__() 77 | self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP() 78 | self._saved_icp = ctypes.windll.kernel32.GetConsoleCP() 79 | ctypes.windll.kernel32.SetConsoleOutputCP(65001) 80 | ctypes.windll.kernel32.SetConsoleCP(65001) 81 | self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 82 | 'replace') 83 | # the change of the code page is not propagated to Python, 84 | # manually fix it 85 | sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 86 | 'replace') 87 | sys.stdout = self.output 88 | self.output.encoding = 'UTF-8' # needed for input 89 | 90 | def __del__(self): 91 | ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp) 92 | ctypes.windll.kernel32.SetConsoleCP(self._saved_icp) 93 | 94 | def getkey(self): 95 | while True: 96 | z = msvcrt.getwch() 97 | if z == chr(13): 98 | return chr(10) 99 | elif z in (chr(0), chr(0x0e)): # functions keys, ignore 100 | msvcrt.getwch() 101 | else: 102 | return z 103 | 104 | def cancel(self): 105 | # CancelIo, CancelSynchronousIo do not seem to work when using 106 | # getwch, so instead, send a key to the window with the console 107 | hwnd = ctypes.windll.kernel32.GetConsoleWindow() 108 | ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0) 109 | 110 | elif os.name == 'posix': 111 | import atexit 112 | import termios 113 | import select 114 | 115 | class Console(ConsoleBase): 116 | def __init__(self): 117 | super(Console, self).__init__() 118 | self.fd = sys.stdin.fileno() 119 | # an additional pipe is used in getkey, so that the cancel method 120 | # can abort the waiting getkey method 121 | self.pipe_r, self.pipe_w = os.pipe() 122 | self.old = termios.tcgetattr(self.fd) 123 | atexit.register(self.cleanup) 124 | if sys.version_info < (3, 0): 125 | self.enc_stdin = codecs.\ 126 | getreader(sys.stdin.encoding)(sys.stdin) 127 | else: 128 | self.enc_stdin = sys.stdin 129 | 130 | def setup(self): 131 | new = termios.tcgetattr(self.fd) 132 | new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG 133 | new[6][termios.VMIN] = 1 134 | new[6][termios.VTIME] = 0 135 | termios.tcsetattr(self.fd, termios.TCSANOW, new) 136 | 137 | def getkey(self): 138 | ready, _, _ = select.select([self.enc_stdin, self.pipe_r], [], 139 | [], None) 140 | if self.pipe_r in ready: 141 | os.read(self.pipe_r, 1) 142 | return 143 | c = self.enc_stdin.read(1) 144 | if c == chr(0x7f): 145 | c = chr(8) # map the BS key (which yields DEL) to backspace 146 | return c 147 | 148 | def cancel(self): 149 | os.write(self.pipe_w, b"x") 150 | 151 | def cleanup(self): 152 | termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old) 153 | 154 | else: 155 | raise NotImplementedError( 156 | 'Sorry no implementation for your platform ({})' 157 | ' available.'.format(sys.platform)) 158 | 159 | 160 | class Terminal(object): 161 | """This class is loosely based off of the pyserial miniterm""" 162 | def __init__(self, serial_instance: serial.Serial, transformations=(), 163 | output_raw=False): 164 | self.serial = serial_instance 165 | self.transformations = transformations 166 | self._reader_alive = None 167 | self.receiver_thread = None 168 | self._transmitter_alive = None 169 | self.transmitter_thread = None 170 | self.alive = None 171 | self.output_raw = output_raw 172 | self.no_sigint = True # SIGINT flag 173 | signal.signal(signal.SIGINT, self.catch_sigint) # SIGINT handler 174 | self.console = Console() 175 | 176 | def _start_rx(self): 177 | self._reader_alive = True 178 | self.receiver_thread = threading.Thread(target=self.reader, 179 | name='serial-rx-term') 180 | self.receiver_thread.daemon = True 181 | self.receiver_thread.start() 182 | 183 | def _stop_rx(self): 184 | self._reader_alive = False 185 | if hasattr(self.serial, 'cancel_read'): 186 | self.serial.cancel_read() 187 | self.receiver_thread.join() 188 | 189 | def _start_tx(self): 190 | self._transmitter_alive = True 191 | self.transmitter_thread = threading.Thread(target=self.transmitter, 192 | name='serial-tx-term') 193 | self.transmitter_thread.daemon = True 194 | self.transmitter_thread.start() 195 | 196 | def _stop_tx(self): 197 | self._transmitter_alive = False 198 | if hasattr(self.serial, 'cancel_write'): 199 | self.serial.cancel_write() 200 | self.transmitter_thread.join() 201 | 202 | def reader(self): 203 | start_time = time.clock 204 | try: 205 | while self.alive and self._reader_alive: 206 | data = self.serial.read(self.serial.in_waiting or 1) 207 | if data: 208 | if self.output_raw: 209 | self.console.write_bytes(data) 210 | else: 211 | text = data.decode('utf-8', 'ignore') 212 | for transformation in self.transformations: 213 | text = transformation(text) 214 | self.console.write(text) 215 | except serial.SerialException as e: 216 | debug(e) 217 | self.alive = False 218 | 219 | def transmitter(self): 220 | try: 221 | while self.alive and self._transmitter_alive: 222 | try: 223 | c = self.console.getkey() 224 | except KeyboardInterrupt: 225 | c = '\x03' 226 | if not self.alive: 227 | break 228 | if c == '\x03' or not self.no_sigint: 229 | self.stop() 230 | break 231 | else: 232 | self.serial.write(c.encode('utf-8')) 233 | self.console.write(c) 234 | except Exception as e: 235 | debug(e) 236 | self.alive = False 237 | 238 | def catch_sigint(self): 239 | self.no_sigint = False 240 | 241 | def start(self): 242 | self.alive = True 243 | self._start_rx() 244 | self._start_tx() 245 | 246 | def join(self): 247 | if self.transmitter_thread.is_alive(): 248 | self.transmitter_thread.join() 249 | if self.receiver_thread.is_alive(): 250 | self.receiver_thread.join() 251 | 252 | def stop(self): 253 | print('Stopping terminal!') 254 | self.alive = False 255 | self._stop_rx() 256 | self._stop_tx() 257 | -------------------------------------------------------------------------------- /prosconductor/providers/githubreleases.py: -------------------------------------------------------------------------------- 1 | import click 2 | from functools import lru_cache 3 | import jsonpickle 4 | import tempfile 5 | import os 6 | import os.path 7 | import proscli.utils 8 | from prosconductor.providers import TemplateTypes, DepotProvider, InvalidIdentifierException, DepotConfig, Identifier, \ 9 | get_template_dir, TemplateConfig 10 | import re 11 | import requests 12 | import requests.exceptions 13 | import shutil 14 | import sys 15 | # from typing import List, Dict, Set 16 | import zipfile 17 | 18 | 19 | @lru_cache() 20 | def get_cert_attr(): 21 | if getattr(sys, 'frozen', False): 22 | return os.path.join(os.path.dirname(sys.executable), 'cacert.pem') 23 | else: 24 | return True 25 | 26 | 27 | class GithubReleasesDepotProvider(DepotProvider): 28 | registrar = 'github-releases' 29 | location_desc = 'username/repository' 30 | config = { 31 | 'include_prereleases': { 32 | 'method': 'bool', 33 | 'prompt': 'Include pre-releases?', 34 | 'default': False 35 | }, 36 | 'include_draft': { 37 | 'method': 'bool', 38 | 'prompt': 'Include drafts? (requires authentication)', 39 | 'default': False 40 | }, 41 | 'oauth_token': { 42 | 'method': 'str', 43 | 'prompt': 'GitHub OAuth Token', 44 | 'default': '' 45 | } 46 | } 47 | 48 | def __init__(self, config): 49 | super().__init__(config) 50 | 51 | def create_headers(self, accept='application/vnd.github.v3+json'): 52 | headers = {'user-agent': 'pros-cli', 'Accept': accept} 53 | if 'oauth_token' in self.config.registrar_options and self.config.registrar_options['oauth_token']: 54 | headers['Authorization'] = 'token {}'.format(self.config.registrar_options['oauth_token']) 55 | return headers 56 | 57 | def verify_configuration(self): 58 | if not re.fullmatch(pattern='[A-z0-9](?:-?[A-z0-9]){0,38}\/[0-9A-z_\.-]{1,93}', string=self.config.location): 59 | raise InvalidIdentifierException('{} is an invalid GitHub resository'.format(self.config.location)) 60 | 61 | def list_online(self, template_types=None): 62 | self.verify_configuration() 63 | if template_types is None: 64 | template_types = [TemplateTypes.kernel, TemplateTypes.library] 65 | config = self.config 66 | proscli.utils.debug('Fetching listing for {} at {} using {}'.format(config.name, config.location, self.registrar)) 67 | proscli.utils.debug('HEADERS {}'.format(self.create_headers())) 68 | try: 69 | r = requests.get('https://api.github.com/repos/{}/releases'.format(config.location), 70 | headers=self.create_headers(), 71 | verify=get_cert_attr()) 72 | except requests.exceptions.RequestException as ex: 73 | proscli.utils.debug('Error fetching templates {}'.format(ex)) 74 | return {t: set() for t in template_types} 75 | response = {t: set() for t in template_types} # type: Dict[TemplateTypes, Set[Identifier]] 76 | if r.status_code == 200: 77 | # response = dict() # type: Dict[TemplateTypes, Set[Identifier]] 78 | json = r.json() 79 | proscli.utils.debug('Result: {}'.format(r.text)) 80 | # filter out pre-releases according to registar_options (include_prerelease implies prerelease) and 81 | # by if the release has a kernel-template.zip or library-template.zip file 82 | for release in [rel for rel in json if 83 | (not rel['prerelease'] or config.registrar_options.get('include_prereleases', False)) and 84 | (not rel['draft'] or config.registrar_options.get('include_draft', False))]: 85 | for asset in [a for a in release['assets'] if 86 | re.fullmatch(string=a['name'].lower(), pattern='.*-template.zip')]: 87 | if asset['name'].lower() == 'kernel-template.zip' and TemplateTypes.kernel in template_types: 88 | proscli.utils.debug('Found a kernel: {}'.format(release)) 89 | if TemplateTypes.kernel not in response: 90 | response[TemplateTypes.kernel] = set() 91 | response[TemplateTypes.kernel].add(Identifier(name='kernel', version=release['tag_name'], 92 | depot=self.config.name)) 93 | elif asset['name'].lower() != 'kernel-template.zip' and TemplateTypes.library in template_types: 94 | # if the name isn't kernel-template.zip, then it's a library 95 | proscli.utils.debug('Found a library: {}'.format(release)) 96 | if TemplateTypes.library not in response: 97 | response[TemplateTypes.library] = set() 98 | ident = Identifier(name=asset['name'][:-len('-template.zip')], version=release['tag_name'], 99 | depot=self.config.name) 100 | proscli.utils.debug('Found: {}'.format(ident)) 101 | response[TemplateTypes.library].add(ident) 102 | else: 103 | click.echo('Unable to get listing for {} at {}'.format(config.name, config.location)) 104 | proscli.utils.debug(r.__dict__) 105 | proscli.utils.debug(jsonpickle.encode(response)) 106 | return response 107 | 108 | def download(self, identifier): 109 | self.verify_configuration() 110 | template_dir = get_template_dir(self, identifier) 111 | if os.path.isdir(template_dir): 112 | shutil.rmtree(template_dir) 113 | elif os.path.isfile(template_dir): 114 | os.remove(template_dir) 115 | # verify release exists: 116 | click.echo('Fetching release on {} with tag {}'.format(self.config.location, identifier.version)) 117 | r = requests.get('https://api.github.com/repos/{}/releases/tags/{}'.format(self.config.location, 118 | identifier.version), 119 | headers=self.create_headers(), verify=get_cert_attr()) 120 | if r.status_code == 200: 121 | for asset in [a for a in r.json()['assets'] if a['name'] == '{}-template.zip'.format(identifier.name)]: 122 | # Time to download the file 123 | proscli.utils.debug('Found {}'.format(asset['url'])) 124 | dr = requests.get(asset['url'], headers=self.create_headers('application/octet-stream'), stream=True, 125 | verify=get_cert_attr()) 126 | if dr.status_code == 200 or dr.status_code == 302: 127 | with tempfile.NamedTemporaryFile(delete=False) as tf: 128 | # todo: no temp file necessary - go straight from download to zipfile extraction 129 | with click.progressbar(length=asset['size'], 130 | label='Downloading {} (v: {})'.format(asset['name'], 131 | identifier.version)) \ 132 | as progress_bar: 133 | for chunk in dr.iter_content(128): 134 | tf.write(chunk) 135 | progress_bar.update(128) 136 | tf.close() # need to close since opening again as ZipFile 137 | with zipfile.ZipFile(tf.name) as zf: 138 | with click.progressbar(length=len(zf.namelist()), 139 | label='Extracting {}'.format(asset['name'])) as progress_bar: 140 | for file in zf.namelist(): 141 | zf.extract(file, path=template_dir) 142 | progress_bar.update(1) 143 | os.remove(tf.name) 144 | template_config = TemplateConfig(os.path.join(template_dir, 'template.pros')) 145 | if template_config.identifier.version != identifier.version: 146 | click.echo('WARNING: Version fetched does not have the same version downloaded {0} != {1}.' 147 | .format(template_config.identifier.version, identifier.version)) 148 | os.rename(template_dir, get_template_dir(self, template_config.identifier)) 149 | template_dir = get_template_dir(self, template_config.identifier) 150 | click.echo('Template downloaded to {}'.format(template_dir)) 151 | return template_config.identifier 152 | else: 153 | click.echo('Unable to download {} from {} (Status code: {})'.format(asset['name'], 154 | self.config.location, 155 | dr.status_code)) 156 | proscli.utils.debug(dr.__dict__) 157 | return False 158 | else: 159 | click.echo('Unable to find {} on {} (Status code: {})'.format(identifier.version, 160 | self.config.name, 161 | r.status_code)) 162 | proscli.utils.debug(r.__dict__) 163 | return False 164 | -------------------------------------------------------------------------------- /prosconductor/providers/local.py: -------------------------------------------------------------------------------- 1 | import click 2 | # import distutils.dir_util 3 | import fnmatch 4 | import os.path 5 | from proscli.utils import debug, verbose 6 | import prosconfig 7 | from prosconfig.cliconfig import CliConfig 8 | from prosconductor.providers import Identifier, TemplateTypes, TemplateConfig 9 | from prosconductor.providers.utils import get_depots 10 | import shutil 11 | import sys 12 | # from typing import Set, List 13 | 14 | def copytree(src, dst, symlinks=False, ignore=None, overwrite=False, copy_function=shutil.copy2, 15 | ignore_dangling_symlinks=False): 16 | """Modified shutil.copytree, but with exist_ok=True 17 | """ 18 | names = os.listdir(src) 19 | if ignore is not None: 20 | ignored_names = ignore(src, names) 21 | else: 22 | ignored_names = set() 23 | 24 | os.makedirs(dst, exist_ok=True) 25 | errors = [] 26 | for name in names: 27 | if name in ignored_names: 28 | continue 29 | srcname = os.path.join(src, name) 30 | dstname = os.path.join(dst, name) 31 | try: 32 | if os.path.islink(srcname): 33 | linkto = os.readlink(srcname) 34 | if symlinks: 35 | # We can't just leave it to `copy_function` because legacy 36 | # code with a custom `copy_function` may rely on copytree 37 | # doing the right thing. 38 | os.symlink(linkto, dstname) 39 | shutil.copystat(srcname, dstname, follow_symlinks=not symlinks) 40 | else: 41 | # ignore dangling symlink if the flag is on 42 | if not os.path.exists(linkto) and ignore_dangling_symlinks: 43 | continue 44 | # otherwise let the copy occurs. copy2 will raise an error 45 | if os.path.isdir(srcname): 46 | copytree(srcname, dstname, symlinks, ignore, overwrite=overwrite, 47 | copy_function=copy_function) 48 | else: 49 | if os.path.exists(dstname): 50 | if overwrite: 51 | click.echo('Overwriting {}'.format(dstname)) 52 | copy_function(srcname, dstname) 53 | else: 54 | click.echo('Skipping {} because it already exists'.format(dstname)) 55 | else: 56 | copy_function(srcname, dstname) 57 | elif os.path.isdir(srcname): 58 | copytree(srcname, dstname, symlinks, ignore, overwrite=overwrite, 59 | copy_function=copy_function) 60 | else: 61 | # Will raise a SpecialFileError for unsupported file types 62 | if os.path.exists(dstname): 63 | if overwrite: 64 | click.echo('Overwriting {}'.format(dstname)) 65 | copy_function(srcname, dstname) 66 | else: 67 | click.echo('Skipping {} because it already exists'.format(dstname)) 68 | else: 69 | copy_function(srcname, dstname) 70 | # catch the Error from the recursive copytree so that we can 71 | # continue with other files 72 | except shutil.Error as err: 73 | errors.extend(err.args[0]) 74 | except OSError as why: 75 | errors.append((srcname, dstname, str(why))) 76 | try: 77 | shutil.copystat(src, dst) 78 | except OSError as why: 79 | # Copying file access times may fail on Windows 80 | if getattr(why, 'winerror', None) is None: 81 | errors.append((src, dst, str(why))) 82 | if errors: 83 | raise Error(errors) 84 | return dst 85 | 86 | 87 | def get_local_templates(pros_cfg=None, filters=[], 88 | template_types=None): 89 | if template_types is None or not template_types: 90 | template_types = [TemplateTypes.kernel, TemplateTypes.library] 91 | if filters is None or not filters: 92 | filters = ['.*'] 93 | result = [] 94 | for depot in get_depots(pros_cfg, filters): 95 | for k, v in depot.list_local(template_types).items(): 96 | result += v 97 | return result 98 | # return [Identifier(name=i.name, version=i.version, depot=depot.registrar) for i in [d.values() for d in 99 | # [depot.list_local(template_types) for depot in get_depots(pros_cfg, filters)]]] 100 | 101 | 102 | def create_template(identifier, location=None, pros_cli=None): 103 | if pros_cli is None or not pros_cli: 104 | pros_cli = CliConfig() 105 | if location is None or not location: 106 | location = os.path.join(location, identifier.depot, 107 | '{}-{}'.format(identifier.name, identifier.version)) 108 | filename = os.path.join(location, 'template.pros') 109 | config = TemplateConfig(file=filename) 110 | config.name = identifier.name 111 | config.version = identifier.version 112 | config.depot = identifier.depot 113 | config.save() 114 | return config 115 | 116 | 117 | def create_project(identifier, dest, pros_cli=None, require_empty=False, overwrite=False): 118 | if pros_cli is None or not pros_cli: 119 | pros_cli = CliConfig() 120 | filename = os.path.join(pros_cli.directory, identifier.depot, 121 | '{}-{}'.format(identifier.name, identifier.version), 122 | 'template.pros') 123 | if not os.path.isfile(filename): 124 | click.echo('Error: template.pros not found for {}-{}'.format(identifier.name, identifier.version)) 125 | click.get_current_context().abort() 126 | sys.exit() 127 | if require_empty: 128 | if os.path.isfile(dest) or (os.path.isdir(dest) and len(os.listdir(dest)) > 0): 129 | click.echo('Error! Destination is a file or a nonempty directory! Delete the file(s) and try again.') 130 | click.get_current_context().abort() 131 | sys.exit() 132 | config = TemplateConfig(file=filename) 133 | copytree(config.directory, dest, overwrite=overwrite) 134 | for root, dirs, files in os.walk(dest): 135 | for d in dirs: 136 | d = os.path.relpath(os.path.join(root, d), dest) 137 | if any([fnmatch.fnmatch(d, p) for p in config.template_ignore]): 138 | verbose('Removing {}'.format(d)) 139 | os.rmdir(os.path.join(root, d)) 140 | for f in files: 141 | f = os.path.relpath(os.path.join(root, f), dest) 142 | if any([fnmatch.fnmatch(f, p) for p in config.template_ignore]): 143 | verbose('Removing {}'.format(f)) 144 | os.remove(os.path.join(root, f)) 145 | proj_config = prosconfig.ProjectConfig(dest, create=True) 146 | proj_config.kernel = identifier.version 147 | proj_config.save() 148 | 149 | 150 | def upgrade_project(identifier, dest, pros_cli=None): 151 | if pros_cli is None or not pros_cli: 152 | pros_cli = CliConfig() 153 | filename = os.path.join(pros_cli.directory, identifier.depot, 154 | '{}-{}'.format(identifier.name, identifier.version), 155 | 'template.pros') 156 | 157 | if not os.path.isfile(filename): 158 | click.echo('Error: template.pros not found for {}-{}'.format(identifier.name, identifier.version)) 159 | click.get_current_context().abort() 160 | sys.exit() 161 | proj_config = prosconfig.ProjectConfig(dest, raise_on_error=True) 162 | config = TemplateConfig(file=filename) 163 | for root, dirs, files in os.walk(config.directory): 164 | for d in dirs: 165 | f = os.path.relpath(os.path.join(root, d), config.directory) 166 | if any([fnmatch.fnmatch(d, p) for p in config.upgrade_paths]): 167 | verbose('Upgrading {}'.format(d)) 168 | shutil.copytree(os.path.join(config.directory, f), os.path.join(proj_config.directory, f)) 169 | for f in files: 170 | f = os.path.relpath(os.path.join(root, f), config.directory) 171 | if any([fnmatch.fnmatch(f, p) for p in config.upgrade_paths]): 172 | verbose('Upgrading {}'.format(f)) 173 | shutil.copyfile(os.path.join(config.directory, f), os.path.join(proj_config.directory, f)) 174 | for root, dirs, files in os.walk(proj_config.directory): 175 | for d in dirs: 176 | d = os.path.relpath(os.path.join(root, d), proj_config.directory) 177 | if any([fnmatch.fnmatch(d, p) for p in config.remove_paths]): 178 | verbose('Removing {}'.format(d)) 179 | shutil.rmtree(os.path.join(proj_config.directory, d)) 180 | 181 | for f in files: 182 | f = os.path.relpath(os.path.join(root, f), proj_config.directory) 183 | if any([fnmatch.fnmatch(f, p) for p in config.remove_paths]): 184 | verbose('Removing {}'.format(f)) 185 | os.remove(os.path.join(proj_config.directory, f)) 186 | proj_config.kernel = config.identifier.version 187 | proj_config.save() 188 | 189 | 190 | def install_lib(identifier, dest, pros_cli, overwrite=False): 191 | if pros_cli is None or not pros_cli: 192 | pros_cli = CliConfig() 193 | filename = os.path.join(pros_cli.directory, identifier.depot, 194 | '{}-{}'.format(identifier.name, identifier.version), 195 | 'template.pros') 196 | if not os.path.isfile(filename): 197 | click.echo('Error: template.pros not found for {}-{}'.format(identifier.name, identifier.version)) 198 | click.get_current_context().abort() 199 | sys.exit() 200 | proj_config = prosconfig.ProjectConfig(dest) 201 | config = TemplateConfig(file=filename) 202 | copytree(config.directory, dest, overwrite=overwrite) 203 | for root, dirs, files in os.walk(dest): 204 | for d in dirs: 205 | if any([fnmatch.fnmatch(d, p) for p in config.template_ignore]): 206 | verbose('Removing {}'.format(d)) 207 | os.rmdir(os.path.join(root, d)) 208 | for f in files: 209 | if any([fnmatch.fnmatch(f, p) for p in config.template_ignore]): 210 | verbose('Removing {}'.format(f)) 211 | os.remove(os.path.join(root, f)) 212 | if type(proj_config.libraries) is list: 213 | proj_config.libraries = dict() 214 | proj_config.libraries[identifier.name] = identifier.version 215 | proj_config.save() 216 | -------------------------------------------------------------------------------- /prosflasher/upload.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import serial.serialutil 3 | import time 4 | import itertools 5 | import click 6 | import os.path 7 | from enum import Enum 8 | import prosflasher.ports 9 | import prosflasher.bootloader 10 | import proscli.utils 11 | from proscli.utils import debug 12 | from prosflasher import bytes_to_str 13 | import sys 14 | 15 | ACK = 0x79 16 | 17 | 18 | class ConnectionType(Enum): 19 | unknown = -1 20 | serial_vexnet1 = 0x00 21 | serial_vexnet1_turbo = 0x01 # no known affecting difference between turbo and non-turbo 22 | serial_vexnet2 = 0x04 23 | serial_vexnet2_dl = 0x05 24 | serial_usb = 0x10 25 | direct_usb = 0x20 26 | 27 | 28 | class SystemInfo: 29 | device = '' 30 | joystick_firmware = '' 31 | cortex_firmware = '' 32 | joystick_battery = 0.0 33 | cortex_battery = 0.0 34 | backup_battery = 0.0 35 | connection_type = ConnectionType.unknown 36 | previous_polls = 0 37 | byte_representation = [0x00] 38 | 39 | def __repr__(self): 40 | if self.connection_type == ConnectionType.serial_vexnet1: 41 | connection = ' Serial w/ VEXnet 1.0 Keys' 42 | elif self.connection_type == ConnectionType.serial_vexnet2: 43 | connection = ' Serial w/ VEXnet 2.0 Keys' 44 | elif self.connection_type == ConnectionType.serial_vexnet2_dl: 45 | connection = ' Serial w/ VEXnet 2.0 Keys (download mode)' 46 | elif self.connection_type == ConnectionType.serial_usb: 47 | connection = ' Serial w/ a USB cable' 48 | elif self.connection_type == ConnectionType.direct_usb: 49 | connection = 'Directly w/ a USB cable' 50 | else: 51 | connection = 'Unknown tether connection ({})'.format(self.connection_type) 52 | 53 | return \ 54 | '''Cortex Microcontroller connected on {} 55 | Tether: {} 56 | Joystick: F/W {} w/ {:1.2f}V 57 | Cortex: F/W {} w/ {:1.2f}V (Backup: {:1.2f}V)''' \ 58 | .format(self.device, 59 | connection, 60 | self.joystick_firmware, self.joystick_battery, 61 | self.cortex_firmware, self.cortex_battery, self.backup_battery) 62 | 63 | @property 64 | def is_wireless(self): 65 | return self.connection_type == ConnectionType.serial_vexnet2 or \ 66 | self.connection_type == ConnectionType.serial_vexnet2_dl or \ 67 | self.connection_type == ConnectionType.serial_vexnet1 or \ 68 | self.connection_type == ConnectionType.serial_vexnet1_turbo 69 | 70 | 71 | # THIS MANAGES THE UPLOAD PROCESS FOR A GIVEN PORT/BINARY PAIR 72 | def upload(port, y, binary, no_poll=False, ctx=proscli.utils.State()): 73 | if not os.path.isfile(binary): 74 | click.echo('Failed to download... file does not exist') 75 | return False 76 | port = prosflasher.ports.create_serial(port, serial.PARITY_EVEN) 77 | if not port: 78 | click.echo('Failed to download: port not found') 79 | return 80 | try: 81 | # reset_cortex(port, ctx) 82 | stop_user_code(port, ctx) 83 | if not no_poll: 84 | sys_info = ask_sys_info(port, ctx) 85 | if sys_info is None: 86 | time.sleep(1.5) 87 | sys_info = ask_sys_info(port) 88 | if sys_info is None: 89 | sys_info = SystemInfo() 90 | sys_info.connection_type = ConnectionType.unknown 91 | click.echo('Failed to get system info... Prompting to continue...', err=True) 92 | click.echo(repr(sys_info)) 93 | else: 94 | sys_info = SystemInfo() 95 | sys_info.connection_type = ConnectionType.serial_usb # assume this 96 | if sys_info.connection_type == ConnectionType.unknown and not y: 97 | click.confirm('Unable to determine system type. It may be necessary to press the ' 98 | 'programming button on the programming kit. Continuing is usually safe.' 99 | ' Continue?', abort=True, default=True) 100 | if sys_info.connection_type == ConnectionType.serial_vexnet2: 101 | # need to send to download channel 102 | if not send_to_download_channel(port): 103 | return False 104 | if sys_info.is_wireless: # increase read timeout for wireless connections 105 | port.timeout = 1.0 106 | if not expose_bootloader(port): 107 | return False 108 | port.read_all() 109 | if sys_info.connection_type == ConnectionType.serial_usb: 110 | time.sleep(0.25) 111 | if not prosflasher.bootloader.prepare_bootloader(port): 112 | return False 113 | if not prosflasher.bootloader.erase_flash(port): 114 | return False 115 | if not prosflasher.bootloader.upload_binary(port, binary, is_wireless=sys_info.is_wireless): 116 | if sys_info.is_wireless: 117 | click.echo('Binary failed to upload. You may now need to upload via a USB connection because too many ' 118 | 'packets were dropped. Move the joystick closer to the microcontroller for a more reliable ' 119 | 'connection.') 120 | return False 121 | if not prosflasher.bootloader.send_go_command(port, 0x08000000): 122 | return False 123 | 124 | reset_cortex(port) 125 | click.echo("Download complete!") 126 | return True 127 | except serial.serialutil.SerialException as e: 128 | click.echo('Failed to download code! ' + str(e)) 129 | click.echo('Try unplugging and plugging the USB cable back in,' 130 | ' as well as power-cycling the microcontroller.') 131 | return -1000 # stop retries in this case, because there's a problem with the port 132 | finally: 133 | port.close() 134 | 135 | 136 | def stop_user_code(port, ctx=proscli.utils.State()): 137 | click.echo('Stopping user code... ', nl=False) 138 | stopbits = [0x0f, 0x0f, 0x21, 0xde, 0x08, 0x00, 0x00, 0x00, 0x08, 0xf1, 0x04] 139 | debug(bytes_to_str(stopbits), ctx) 140 | if not port.is_open: 141 | port.open() 142 | port.flush() 143 | port.read_all() 144 | time.sleep(0.1) 145 | for stopbit in stopbits: 146 | port.write([stopbit]) 147 | port.flush() 148 | response = port.read_all() 149 | debug(bytes_to_str(response), ctx) 150 | click.echo('complete') 151 | 152 | 153 | def ask_sys_info(port, ctx=proscli.utils.State(), silent=False): 154 | if not silent: 155 | click.echo('Asking for system information... ', nl=False) 156 | sys_info_bits = [0xc9, 0x36, 0xb8, 0x47, 0x21] 157 | if not port.is_open: 158 | port.open() 159 | debug('SYS INFO BITS: {} PORT CFG: {}'.format(bytes_to_str(sys_info_bits), repr(port)), ctx) 160 | for _ in itertools.repeat(None, 10): 161 | port.read_all() 162 | port.write(sys_info_bits) 163 | port.flush() 164 | time.sleep(0.1) 165 | response = port.read_all() 166 | debug('SYS INFO RESPONSE: {}'.format(bytes_to_str(response)), ctx) 167 | if len(response) > 14: 168 | response = response[:14] 169 | if len(response) == 14 and response[0] == 0xaa and response[1] == 0x55\ 170 | and response[2] == 0x21 and response[3] == 0xa: # synchronization matched 171 | sys_info = SystemInfo() 172 | sys_info.device = port.name 173 | sys_info.joystick_firmware = '{}.{}'.format(response[4], response[5]) 174 | sys_info.cortex_firmware = '{}.{}'.format(response[6], response[7]) 175 | if response[8] > 5: # anything smaller than 5 is probably garbage from ADC 176 | sys_info.joystick_battery = response[8] * 0.059 177 | if response[9] > 5: # anything smaller than 5 is probably garbage from ADC 178 | sys_info.cortex_battery = response[9] * 0.059 179 | if response[10] > 5: # anything smaller than 5 is probably garbage from ADC 180 | sys_info.backup_battery = response[10] * 0.059 181 | try: 182 | # Mask FCS bits out of response[11] 183 | sys_info.connection_type = ConnectionType(response[11] & 0b00110111) 184 | except ValueError: 185 | sys_info.connection_type = ConnectionType.unknown 186 | sys_info.previous_polls = response[13] 187 | sys_info.byte_representation = response 188 | if not silent: 189 | click.echo('complete') 190 | return sys_info 191 | time.sleep(0.15) 192 | return None 193 | 194 | 195 | def send_to_download_channel(port, ctx=proscli.utils.State()): 196 | click.echo('Sending to download channel (this may take a while)... ', nl=False) 197 | download_ch_bits = [0xc9, 0x36, 0xb8, 0x47, 0x35] 198 | debug('DL CH BITS: {} PORT CFG: {}'.format(bytes_to_str(download_ch_bits), repr(port)), ctx) 199 | for _ in itertools.repeat(None, 5): 200 | port.read_all() 201 | time.sleep(0.1) 202 | port.write(download_ch_bits) 203 | port.flush() 204 | time.sleep(3) 205 | response = port.read_all() 206 | debug('DB CH RESPONSE: {}'.format(bytes_to_str(response)), ctx) 207 | response = response[-1:] 208 | sys_info = ask_sys_info(port, ctx, silent=True) 209 | if (sys_info is not None and sys_info.connection_type == ConnectionType.serial_vexnet2_dl) or (response is not None and len(response) > 0 and response[0] == ACK): 210 | click.echo('complete') 211 | return True 212 | click.echo('failed') 213 | return False 214 | 215 | 216 | def expose_bootloader(port, ctx=proscli.utils.State()): 217 | click.echo('Exposing bootloader... ', nl=False) 218 | bootloader_bits = [0xc9, 0x36, 0xb8, 0x47, 0x25] 219 | port.flush() 220 | debug('EXPOSE BL BITS: {} PORT CFG: {}'.format(bytes_to_str(bootloader_bits), repr(port)), ctx) 221 | port.read_all() 222 | time.sleep(0.1) 223 | for _ in itertools.repeat(None, 5): 224 | port.write(bootloader_bits) 225 | time.sleep(0.1) 226 | time.sleep(0.3) # time delay to allow shift to download mode 227 | click.echo('complete') 228 | return True 229 | 230 | 231 | def reset_cortex(port, ctx=proscli.utils.State()): 232 | click.echo('Resetting cortex... ', nl=False) 233 | debug('RESET CORTEX. PORT CFG: {}'.format(repr(port)), ctx) 234 | port.parity = serial.PARITY_NONE 235 | port.flush() 236 | port.read_all() 237 | time.sleep(0.1) 238 | port.write([0xc9, 0x36, 0xb8, 0x47, 0x20]) 239 | port.flush() 240 | port.write([0x14]) 241 | click.echo('complete') 242 | time.sleep(0.01) 243 | 244 | 245 | def verify_file(file): 246 | if not os.path.isfile(file): 247 | return False 248 | 249 | 250 | def dump_cortex(port, file, verbose=False): 251 | if not os.path.isfile(file): 252 | click.echo('Failed to download... file does not exist') 253 | return False 254 | port = prosflasher.ports.create_serial(port) 255 | if not port: 256 | click.echo('Failed to download: port not found') 257 | return 258 | try: 259 | reset_cortex(port) 260 | sys_info = ask_sys_info(port) 261 | if sys_info is None: 262 | click.echo('Failed to get system info... Try again', err=True) 263 | click.get_current_context().abort() 264 | sys.exit(1) 265 | click.echo(repr(sys_info)) 266 | stop_user_code(port) 267 | if sys_info.connection_type == ConnectionType.serial_vexnet2: 268 | # need to send to download channel 269 | if not send_to_download_channel(port): 270 | return False 271 | if not expose_bootloader(port): 272 | return False 273 | if not prosflasher.bootloader.prepare_bootloader(port): 274 | return False 275 | if not prosflasher.bootloader.erase_flash(port): 276 | return False 277 | 278 | with open(file, 'wb') as f: 279 | address = 0x08000000 280 | data = prosflasher.bootloader.read_memory(port, address, 256) 281 | while len(data) > 0: 282 | f.write(data) 283 | address += 0x100 284 | 285 | except serial.serialutil.SerialException as e: 286 | click.echo('Failed to download code! ' + str(e)) 287 | finally: 288 | port.close() 289 | click.echo("Download complete!") 290 | pass 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Mozilla Public License Version 2.0 3 | ================================== 4 | 5 | 1. Definitions 6 | -------------- 7 | 8 | 1.1. "Contributor" 9 | means each individual or legal entity that creates, contributes to 10 | the creation of, or owns Covered Software. 11 | 12 | 1.2. "Contributor Version" 13 | means the combination of the Contributions of others (if any) used 14 | by a Contributor and that particular Contributor's Contribution. 15 | 16 | 1.3. "Contribution" 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | means Source Code Form to which the initial Contributor has attached 21 | the notice in Exhibit A, the Executable Form of such Source Code 22 | Form, and Modifications of such Source Code Form, in each case 23 | including portions thereof. 24 | 25 | 1.5. "Incompatible With Secondary Licenses" 26 | means 27 | 28 | (a) that the initial Contributor has attached the notice described 29 | in Exhibit B to the Covered Software; or 30 | 31 | (b) that the Covered Software was made available under the terms of 32 | version 1.1 or earlier of the License, but not also under the 33 | terms of a Secondary License. 34 | 35 | 1.6. "Executable Form" 36 | means any form of the work other than Source Code Form. 37 | 38 | 1.7. "Larger Work" 39 | means a work that combines Covered Software with other material, in 40 | a separate file or files, that is not Covered Software. 41 | 42 | 1.8. "License" 43 | means this document. 44 | 45 | 1.9. "Licensable" 46 | means having the right to grant, to the maximum extent possible, 47 | whether at the time of the initial grant or subsequently, any and 48 | all of the rights conveyed by this License. 49 | 50 | 1.10. "Modifications" 51 | means any of the following: 52 | 53 | (a) any file in Source Code Form that results from an addition to, 54 | deletion from, or modification of the contents of Covered 55 | Software; or 56 | 57 | (b) any new file in Source Code Form that contains any Covered 58 | Software. 59 | 60 | 1.11. "Patent Claims" of a Contributor 61 | means any patent claim(s), including without limitation, method, 62 | process, and apparatus claims, in any patent Licensable by such 63 | Contributor that would be infringed, but for the grant of the 64 | License, by the making, using, selling, offering for sale, having 65 | made, import, or transfer of either its Contributions or its 66 | Contributor Version. 67 | 68 | 1.12. "Secondary License" 69 | means either the GNU General Public License, Version 2.0, the GNU 70 | Lesser General Public License, Version 2.1, the GNU Affero General 71 | Public License, Version 3.0, or any later versions of those 72 | licenses. 73 | 74 | 1.13. "Source Code Form" 75 | means the form of the work preferred for making modifications. 76 | 77 | 1.14. "You" (or "Your") 78 | means an individual or a legal entity exercising rights under this 79 | License. For legal entities, "You" includes any entity that 80 | controls, is controlled by, or is under common control with You. For 81 | purposes of this definition, "control" means (a) the power, direct 82 | or indirect, to cause the direction or management of such entity, 83 | whether by contract or otherwise, or (b) ownership of more than 84 | fifty percent (50%) of the outstanding shares or beneficial 85 | ownership of such entity. 86 | 87 | 2. License Grants and Conditions 88 | -------------------------------- 89 | 90 | 2.1. Grants 91 | 92 | Each Contributor hereby grants You a world-wide, royalty-free, 93 | non-exclusive license: 94 | 95 | (a) under intellectual property rights (other than patent or trademark) 96 | Licensable by such Contributor to use, reproduce, make available, 97 | modify, display, perform, distribute, and otherwise exploit its 98 | Contributions, either on an unmodified basis, with Modifications, or 99 | as part of a Larger Work; and 100 | 101 | (b) under Patent Claims of such Contributor to make, use, sell, offer 102 | for sale, have made, import, and otherwise transfer either its 103 | Contributions or its Contributor Version. 104 | 105 | 2.2. Effective Date 106 | 107 | The licenses granted in Section 2.1 with respect to any Contribution 108 | become effective for each Contribution on the date the Contributor first 109 | distributes such Contribution. 110 | 111 | 2.3. Limitations on Grant Scope 112 | 113 | The licenses granted in this Section 2 are the only rights granted under 114 | this License. No additional rights or licenses will be implied from the 115 | distribution or licensing of Covered Software under this License. 116 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 117 | Contributor: 118 | 119 | (a) for any code that a Contributor has removed from Covered Software; 120 | or 121 | 122 | (b) for infringements caused by: (i) Your and any other third party's 123 | modifications of Covered Software, or (ii) the combination of its 124 | Contributions with other software (except as part of its Contributor 125 | Version); or 126 | 127 | (c) under Patent Claims infringed by Covered Software in the absence of 128 | its Contributions. 129 | 130 | This License does not grant any rights in the trademarks, service marks, 131 | or logos of any Contributor (except as may be necessary to comply with 132 | the notice requirements in Section 3.4). 133 | 134 | 2.4. Subsequent Licenses 135 | 136 | No Contributor makes additional grants as a result of Your choice to 137 | distribute the Covered Software under a subsequent version of this 138 | License (see Section 10.2) or under the terms of a Secondary License (if 139 | permitted under the terms of Section 3.3). 140 | 141 | 2.5. Representation 142 | 143 | Each Contributor represents that the Contributor believes its 144 | Contributions are its original creation(s) or it has sufficient rights 145 | to grant the rights to its Contributions conveyed by this License. 146 | 147 | 2.6. Fair Use 148 | 149 | This License is not intended to limit any rights You have under 150 | applicable copyright doctrines of fair use, fair dealing, or other 151 | equivalents. 152 | 153 | 2.7. Conditions 154 | 155 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 156 | in Section 2.1. 157 | 158 | 3. Responsibilities 159 | ------------------- 160 | 161 | 3.1. Distribution of Source Form 162 | 163 | All distribution of Covered Software in Source Code Form, including any 164 | Modifications that You create or to which You contribute, must be under 165 | the terms of this License. You must inform recipients that the Source 166 | Code Form of the Covered Software is governed by the terms of this 167 | License, and how they can obtain a copy of this License. You may not 168 | attempt to alter or restrict the recipients' rights in the Source Code 169 | Form. 170 | 171 | 3.2. Distribution of Executable Form 172 | 173 | If You distribute Covered Software in Executable Form then: 174 | 175 | (a) such Covered Software must also be made available in Source Code 176 | Form, as described in Section 3.1, and You must inform recipients of 177 | the Executable Form how they can obtain a copy of such Source Code 178 | Form by reasonable means in a timely manner, at a charge no more 179 | than the cost of distribution to the recipient; and 180 | 181 | (b) You may distribute such Executable Form under the terms of this 182 | License, or sublicense it under different terms, provided that the 183 | license for the Executable Form does not attempt to limit or alter 184 | the recipients' rights in the Source Code Form under this License. 185 | 186 | 3.3. Distribution of a Larger Work 187 | 188 | You may create and distribute a Larger Work under terms of Your choice, 189 | provided that You also comply with the requirements of this License for 190 | the Covered Software. If the Larger Work is a combination of Covered 191 | Software with a work governed by one or more Secondary Licenses, and the 192 | Covered Software is not Incompatible With Secondary Licenses, this 193 | License permits You to additionally distribute such Covered Software 194 | under the terms of such Secondary License(s), so that the recipient of 195 | the Larger Work may, at their option, further distribute the Covered 196 | Software under the terms of either this License or such Secondary 197 | License(s). 198 | 199 | 3.4. Notices 200 | 201 | You may not remove or alter the substance of any license notices 202 | (including copyright notices, patent notices, disclaimers of warranty, 203 | or limitations of liability) contained within the Source Code Form of 204 | the Covered Software, except that You may alter any license notices to 205 | the extent required to remedy known factual inaccuracies. 206 | 207 | 3.5. Application of Additional Terms 208 | 209 | You may choose to offer, and to charge a fee for, warranty, support, 210 | indemnity or liability obligations to one or more recipients of Covered 211 | Software. However, You may do so only on Your own behalf, and not on 212 | behalf of any Contributor. You must make it absolutely clear that any 213 | such warranty, support, indemnity, or liability obligation is offered by 214 | You alone, and You hereby agree to indemnify every Contributor for any 215 | liability incurred by such Contributor as a result of warranty, support, 216 | indemnity or liability terms You offer. You may include additional 217 | disclaimers of warranty and limitations of liability specific to any 218 | jurisdiction. 219 | 220 | 4. Inability to Comply Due to Statute or Regulation 221 | --------------------------------------------------- 222 | 223 | If it is impossible for You to comply with any of the terms of this 224 | License with respect to some or all of the Covered Software due to 225 | statute, judicial order, or regulation then You must: (a) comply with 226 | the terms of this License to the maximum extent possible; and (b) 227 | describe the limitations and the code they affect. Such description must 228 | be placed in a text file included with all distributions of the Covered 229 | Software under this License. Except to the extent prohibited by statute 230 | or regulation, such description must be sufficiently detailed for a 231 | recipient of ordinary skill to be able to understand it. 232 | 233 | 5. Termination 234 | -------------- 235 | 236 | 5.1. The rights granted under this License will terminate automatically 237 | if You fail to comply with any of its terms. However, if You become 238 | compliant, then the rights granted under this License from a particular 239 | Contributor are reinstated (a) provisionally, unless and until such 240 | Contributor explicitly and finally terminates Your grants, and (b) on an 241 | ongoing basis, if such Contributor fails to notify You of the 242 | non-compliance by some reasonable means prior to 60 days after You have 243 | come back into compliance. Moreover, Your grants from a particular 244 | Contributor are reinstated on an ongoing basis if such Contributor 245 | notifies You of the non-compliance by some reasonable means, this is the 246 | first time You have received notice of non-compliance with this License 247 | from such Contributor, and You become compliant prior to 30 days after 248 | Your receipt of the notice. 249 | 250 | 5.2. If You initiate litigation against any entity by asserting a patent 251 | infringement claim (excluding declaratory judgment actions, 252 | counter-claims, and cross-claims) alleging that a Contributor Version 253 | directly or indirectly infringes any patent, then the rights granted to 254 | You by any and all Contributors for the Covered Software under Section 255 | 2.1 of this License shall terminate. 256 | 257 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 258 | end user license agreements (excluding distributors and resellers) which 259 | have been validly granted by You or Your distributors under this License 260 | prior to termination shall survive termination. 261 | 262 | ************************************************************************ 263 | * * 264 | * 6. Disclaimer of Warranty * 265 | * ------------------------- * 266 | * * 267 | * Covered Software is provided under this License on an "as is" * 268 | * basis, without warranty of any kind, either expressed, implied, or * 269 | * statutory, including, without limitation, warranties that the * 270 | * Covered Software is free of defects, merchantable, fit for a * 271 | * particular purpose or non-infringing. The entire risk as to the * 272 | * quality and performance of the Covered Software is with You. * 273 | * Should any Covered Software prove defective in any respect, You * 274 | * (not any Contributor) assume the cost of any necessary servicing, * 275 | * repair, or correction. This disclaimer of warranty constitutes an * 276 | * essential part of this License. No use of any Covered Software is * 277 | * authorized under this License except under this disclaimer. * 278 | * * 279 | ************************************************************************ 280 | 281 | ************************************************************************ 282 | * * 283 | * 7. Limitation of Liability * 284 | * -------------------------- * 285 | * * 286 | * Under no circumstances and under no legal theory, whether tort * 287 | * (including negligence), contract, or otherwise, shall any * 288 | * Contributor, or anyone who distributes Covered Software as * 289 | * permitted above, be liable to You for any direct, indirect, * 290 | * special, incidental, or consequential damages of any character * 291 | * including, without limitation, damages for lost profits, loss of * 292 | * goodwill, work stoppage, computer failure or malfunction, or any * 293 | * and all other commercial damages or losses, even if such party * 294 | * shall have been informed of the possibility of such damages. This * 295 | * limitation of liability shall not apply to liability for death or * 296 | * personal injury resulting from such party's negligence to the * 297 | * extent applicable law prohibits such limitation. Some * 298 | * jurisdictions do not allow the exclusion or limitation of * 299 | * incidental or consequential damages, so this exclusion and * 300 | * limitation may not apply to You. * 301 | * * 302 | ************************************************************************ 303 | 304 | 8. Litigation 305 | ------------- 306 | 307 | Any litigation relating to this License may be brought only in the 308 | courts of a jurisdiction where the defendant maintains its principal 309 | place of business and such litigation shall be governed by laws of that 310 | jurisdiction, without reference to its conflict-of-law provisions. 311 | Nothing in this Section shall prevent a party's ability to bring 312 | cross-claims or counter-claims. 313 | 314 | 9. Miscellaneous 315 | ---------------- 316 | 317 | This License represents the complete agreement concerning the subject 318 | matter hereof. If any provision of this License is held to be 319 | unenforceable, such provision shall be reformed only to the extent 320 | necessary to make it enforceable. Any law or regulation which provides 321 | that the language of a contract shall be construed against the drafter 322 | shall not be used to construe this License against a Contributor. 323 | 324 | 10. Versions of the License 325 | --------------------------- 326 | 327 | 10.1. New Versions 328 | 329 | Mozilla Foundation is the license steward. Except as provided in Section 330 | 10.3, no one other than the license steward has the right to modify or 331 | publish new versions of this License. Each version will be given a 332 | distinguishing version number. 333 | 334 | 10.2. Effect of New Versions 335 | 336 | You may distribute the Covered Software under the terms of the version 337 | of the License under which You originally received the Covered Software, 338 | or under the terms of any subsequent version published by the license 339 | steward. 340 | 341 | 10.3. Modified Versions 342 | 343 | If you create software not governed by this License, and you want to 344 | create a new license for such software, you may create and use a 345 | modified version of this License if you rename the license and remove 346 | any references to the name of the license steward (except to note that 347 | such modified license differs from this License). 348 | 349 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 350 | Licenses 351 | 352 | If You choose to distribute Source Code Form that is Incompatible With 353 | Secondary Licenses under the terms of this version of the License, the 354 | notice described in Exhibit B of this License must be attached. 355 | 356 | Exhibit A - Source Code Form License Notice 357 | ------------------------------------------- 358 | 359 | This Source Code Form is subject to the terms of the Mozilla Public 360 | License, v. 2.0. If a copy of the MPL was not distributed with this 361 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 362 | 363 | If it is not possible or desirable to put the notice in a particular 364 | file, then You may include the notice in a location (such as a LICENSE 365 | file in a relevant directory) where a recipient would be likely to look 366 | for such a notice. 367 | 368 | You may add additional accurate notices of copyright ownership. 369 | 370 | Exhibit B - "Incompatible With Secondary Licenses" Notice 371 | --------------------------------------------------------- 372 | 373 | This Source Code Form is "Incompatible With Secondary Licenses", as 374 | defined by the Mozilla Public License, v. 2.0. 375 | 376 | -------------------------------------------------------------------------------- /proscli/conductor.py: -------------------------------------------------------------------------------- 1 | from prosconductor.providers import TemplateTypes, Identifier, TemplateConfig 2 | import click 3 | from collections import OrderedDict 4 | import json 5 | import os.path 6 | import proscli.utils 7 | from proscli.utils import default_cfg, default_options, AliasGroup 8 | import prosconductor.providers as providers 9 | import prosconductor.providers.local as local 10 | import prosconductor.providers.utils as utils 11 | import prosconfig 12 | import semantic_version as semver 13 | import sys 14 | import tabulate 15 | 16 | 17 | # from typing import List 18 | 19 | 20 | def first_run(ctx: proscli.utils.State, force=False, defaults=False, doDownload=True, reapplyProviders=False): 21 | if len(utils.get_depot_configs(ctx.pros_cfg)) == 0: 22 | click.echo('You don\'t currently have any depots configured.') 23 | if len(utils.get_depot_configs(ctx.pros_cfg)) == 0 or force: 24 | if defaults or click.confirm('Add the official PROS kernel depot, pros-mainline?', default=True): 25 | click.get_current_context().invoke(add_depot, name='pros-mainline', 26 | registrar='github-releases', 27 | location='purduesigbots/pros', 28 | configure=False) 29 | click.echo('Added pros-mainline to available depots. ' 30 | 'Add more depots in the future by running `pros conduct add-depot`\n') 31 | if (defaults or click.confirm('Download the latest kernel?', default=True)) and doDownload: 32 | click.get_current_context().invoke(download, name='kernel', depot='pros-mainline') 33 | if reapplyProviders: 34 | click.echo('Applying default providers') 35 | ctx.pros_cfg.applyDefaultProviders() 36 | 37 | @click.group(cls=AliasGroup) 38 | @default_options 39 | def conductor_cli(): 40 | pass 41 | 42 | 43 | @conductor_cli.group(cls=AliasGroup, short_help='Perform project management tasks for PROS', aliases=['cond', 'c']) 44 | @default_options 45 | def conduct(): 46 | pass 47 | 48 | 49 | # region Depot Management 50 | @conduct.command('ls-depot', short_help='List registered depots', 51 | aliases=['lsd', 'list-depots', 'lsdpt', 'depots', 'lsdepot']) 52 | @default_cfg 53 | def list_depots(cfg): 54 | if not cfg.machine_output: 55 | first_run(cfg) 56 | depots = utils.get_depot_configs() 57 | if cfg.machine_output: 58 | table = [{ 59 | 'name': d.name, 60 | 'registrar': d.registrar, 61 | 'location': d.location 62 | } for d in depots] 63 | click.echo(json.dumps(table)) 64 | else: 65 | if not bool(depots): 66 | click.echo('No depots currently registered! Use `pros conduct add-depot` to add a new depot') 67 | else: 68 | click.echo(tabulate.tabulate([(d.name, d.registrar, d.location) for d in depots], 69 | ['Name', 'Registrar', 'Location'], tablefmt='simple')) 70 | 71 | 72 | def validate_name(ctx, param, value): 73 | if os.path.isdir(os.path.join(ctx.obj.pros_cfg.directory, value)): 74 | if value == 'pros-mainline': 75 | raise click.BadParameter('Cannot override pros-mainline!') 76 | 77 | click.confirm('A depot with the name {} already exists. Do you want to overwrite it?'.format(value), 78 | prompt_suffix=' ', abort=True, default=True) 79 | return value 80 | 81 | 82 | def available_providers(): 83 | return utils.get_all_provider_types().keys() 84 | 85 | 86 | def prompt_config(config, options=dict()): 87 | for key, value in config.items(): 88 | if value['method'] == 'bool': 89 | options[key] = click.confirm(value['prompt'], 90 | default=options.get(key, value['default']), 91 | prompt_suffix=' ') 92 | else: # elif value['method'] = 'str': 93 | options[key] = click.prompt(value['prompt'], 94 | default=options.get(key, value['default']), 95 | prompt_suffix=' ') 96 | return options 97 | 98 | @conduct.command('add-depot', short_help='Add a depot to PROS', aliases=['new-depot', 'add-provider', 'new-provider']) 99 | @click.option('--name', metavar='NAME', prompt=True, callback=validate_name, 100 | help='Unique name of the new depot') 101 | @click.option('--registrar', metavar='REGISTRAR', prompt=True, type=click.Choice(available_providers()), 102 | help='Registrar of the new depot') 103 | @click.option('--location', metavar='LOCATION', prompt=True, 104 | help='Online location of the new depot') 105 | @click.option('--configure/--no-configure', default=True) 106 | @click.option('--options', metavar='OPTIONS', default=dict(), 107 | help='Provide the registar\'s options through a JSON string.') 108 | @default_cfg 109 | def add_depot(cfg, name, registrar, location, configure, options): 110 | if isinstance(options, str): 111 | options = json.loads(options) 112 | if configure: 113 | config = utils.get_all_provider_types(cfg.pros_cfg)[registrar].config 114 | options = prompt_config(config, options) 115 | # options = utils.get_all_provider_types(cfg.pros_cfg)[registrar](None) \ 116 | # .configure_registrar_options() 117 | providers.DepotConfig(name=name, registrar=registrar, location=location, registrar_options=options, 118 | root_dir=cfg.pros_cfg.directory) 119 | pass 120 | 121 | 122 | @conduct.command('rm-depot', short_help='Remove a depot from PROS') 123 | @click.option('--name', metavar='NAME', prompt=True, help='Name of the depot') 124 | @default_cfg 125 | def remove_depot(cfg, name): 126 | if name == 'pros-mainline': 127 | raise click.BadParameter('Cannot delete pros-mainline!') 128 | 129 | for depot in [d for d in utils.get_depot_configs(cfg.pros_cfg) if d.name == name]: 130 | click.echo('Removing {} ({})'.format(depot.name, depot.location)) 131 | depot.delete() 132 | 133 | 134 | @conduct.command('config-depot', short_help='Configure a depot') 135 | @click.option('--name', metavar='NAME', prompt=True, help='Name of the depot') 136 | @default_cfg 137 | def config_depot(cfg, name): 138 | if name not in [d.name for d in utils.get_depot_configs(cfg.pros_cfg)]: 139 | click.echo('{} isn\'t a registered depot! Have you added it using `pros conduct add-depot`?') 140 | click.get_current_context().abort() 141 | sys.exit() 142 | depot = [d for d in utils.get_depot_configs(cfg.pros_cfg) if d.name == name][0] 143 | config = utils.get_all_provider_types(cfg.pros_cfg)[depot.registrar].config 144 | depot.registrar_options = prompt_config(config, depot.registrar_options) 145 | depot.save() 146 | # endregion 147 | 148 | 149 | # region Template Management 150 | @conduct.command('ls-template', short_help='List all available templates', 151 | aliases=['lst', 'templates', 'list-templates', 'lstmpl', 'lstemplates', 'lstemplate']) 152 | @click.option('--kernels', 'template_types', flag_value=[TemplateTypes.kernel]) 153 | @click.option('--libraries', 'template_types', flag_value=[TemplateTypes.library]) 154 | @click.option('--all', 'template_types', default=True, 155 | flag_value=[TemplateTypes.library, TemplateTypes.kernel]) 156 | @click.option('--offline-only', is_flag=True, default=False, help='List only only templates available locally') 157 | @click.argument('filters', metavar='REGEX', nargs=-1) 158 | @default_cfg 159 | def list_templates(cfg, template_types, filters, offline_only): 160 | """ 161 | List templates with the applied filters. The first item is guaranteed to be the latest overall template 162 | """ 163 | first_run(cfg) 164 | filters = [f for f in filters if f is not None] 165 | if not filters: 166 | filters = ['.*'] 167 | if filters != ['.*']: 168 | click.echo('Providers matching any of {}: {}' 169 | .format(filters, 170 | [d.name for d in utils.get_depot_configs(cfg.pros_cfg, filters)])) 171 | result = utils.get_available_templates(cfg.pros_cfg, 172 | template_types=template_types, 173 | filters=filters, 174 | offline_only=offline_only) 175 | if TemplateTypes.kernel in template_types: 176 | table = sum( 177 | [[(i.version, d.depot.config.name, 'online' if d.online else '', 'offline' if d.offline else '') for d in 178 | ds] 179 | for i, ds in result[TemplateTypes.kernel].items()], []) 180 | table = sorted(table, key=lambda v: semver.Version(v[0]), reverse=True) 181 | if not cfg.machine_output: 182 | click.echo('Available kernels:') 183 | click.echo(tabulate.tabulate(table, headers=['Version', 'Depot', 'Online', 'Offline'])) 184 | else: 185 | table = [{ 186 | 'version': e[0], 187 | 'depot': e[1], 188 | 'online': e[2] == 'online', 189 | 'offline': e[3] == 'offline' 190 | } 191 | for e in table] 192 | click.echo(json.dumps(table)) 193 | if TemplateTypes.library in template_types: 194 | table = sum( 195 | [[(i.name, i.version, d.depot.config.name, 'online' if d.online else '', 'offline' if d.offline else '') for 196 | d in ds] 197 | for i, ds in result[TemplateTypes.library].items()], []) 198 | if not cfg.machine_output: 199 | click.echo('Available libraries:') 200 | click.echo(tabulate.tabulate(table, headers=['Library', 'Version', 'Depot', 'Online', 'Offline'])) 201 | else: 202 | table = [{ 203 | 'library': e[0], 204 | 'version': e[1], 205 | 'depot': e[2], 206 | 'online': e[3] == 'online', 207 | 'offline': e[4] == 'offline' 208 | } 209 | for e in table] 210 | click.echo(json.dumps(table)) 211 | 212 | 213 | @conduct.command(short_help='Download a template', aliases=['dl', 'd']) 214 | @click.argument('name', default='kernel') 215 | @click.argument('version', default='latest') 216 | @click.argument('depot', default='auto') 217 | @click.option('--no-check', '-nc', is_flag=True, default=False, 218 | help='If all arguments are given, then checks if the template exists won\'t be performed ' 219 | 'before attempting to download.') 220 | @default_cfg 221 | def download(cfg, name, version, depot, no_check): 222 | """ 223 | Download a template with the specified parameters. 224 | 225 | If the arguments are `download latest` or `download latest kernel`, the latest kernel will be downloaded 226 | """ 227 | first_run(cfg) 228 | if name.lower() == 'kernel': 229 | name = 'kernel' 230 | elif name == 'latest': 231 | name = 'kernel' 232 | if version == 'kernel': 233 | version = 'latest' 234 | 235 | if version == 'latest' or depot == 'auto' or not no_check: 236 | click.echo('Fetching online listing to verify available templates.') 237 | listing = utils.get_available_templates(pros_cfg=cfg.pros_cfg, 238 | template_types=[utils.TemplateTypes.kernel if name == 'kernel' 239 | else utils.TemplateTypes.library]) 240 | listing = listing.get(utils.TemplateTypes.kernel if name == 'kernel' else utils.TemplateTypes.library) 241 | listing = {i: d for (i, d) in listing.items() if i.name == name} 242 | if len(listing) == 0: 243 | click.echo('No templates were found with the name {}'.format(name)) 244 | click.get_current_context().abort() 245 | sys.exit() 246 | 247 | if not depot == 'auto': 248 | if depot not in [d.depot.config.name for ds in listing.values() for d in ds]: 249 | click.echo('No templates for {} were found on {}'.format(name, depot)) 250 | click.get_current_context().abort() 251 | sys.exit() 252 | listing = {i: [d for d in ds if d.depot.config.name == depot] for i, ds in listing.items() 253 | if depot in [d.depot.config.name for d in ds]} 254 | 255 | # listing now filtered for depots, if applicable 256 | 257 | if version == 'latest': 258 | identifier, descriptors = OrderedDict( 259 | sorted(listing.items(), key=lambda kvp: semver.Version(kvp[0].version))).popitem() 260 | click.echo('Resolved {} {} to {} {}'.format(name, version, identifier.name, identifier.version)) 261 | else: 262 | if version not in [i.version for (i, d) in listing.items()]: 263 | click.echo('No templates for {} were found with the version {}'.format(name, version)) 264 | click.get_current_context().abort() 265 | sys.exit() 266 | identifier, descriptors = [(i, d) for (i, d) in listing.items() if i.version == version][0] 267 | 268 | # identifier is now selected... 269 | if len(descriptors) == 0: 270 | click.echo('No templates for {} were found with the version {}'.format(name, version)) 271 | click.get_current_context().abort() 272 | sys.exit() 273 | 274 | if len(descriptors) > 1: 275 | if name == 'kernel' and depot == 'auto' and 'pros-mainline' in [desc.depot.config.name for desc in 276 | descriptors]: 277 | descriptor = [desc for desc in descriptors if desc.depot.config.name == 'pros-mainline'] 278 | else: 279 | click.echo('Multiple depots for {}-{} were found. Please specify a depot: '. 280 | format(identifier.name, identifier.version)) 281 | options_table = sorted([(descriptors.index(desc), desc.depot.config.name) for desc in descriptors], 282 | key=lambda l: l[1]) 283 | click.echo(tabulate.tabulate(options_table, headers=['', 'Depot'])) 284 | result = click.prompt('Which depot?', default=options_table[0][1], 285 | type=click.Choice( 286 | [str(i) for (i, n) in options_table] + [n for (i, n) in options_table])) 287 | if result in [str(i) for (i, n) in options_table]: 288 | descriptor = [d for d in descriptors if d.depot.config.name == options_table[int(result)][1]][0] 289 | else: 290 | descriptor = [d for d in descriptors if d.depot.config.name == result][0] 291 | elif depot == 'auto' or descriptors[0].depot.config.name == depot: 292 | descriptor = descriptors[0] 293 | else: 294 | click.echo('Could not find a depot to download {} {}'.format(name, version)) 295 | click.get_current_context().abort() 296 | sys.exit() 297 | else: 298 | identifier = providers.Identifier(name=name, version=version) 299 | descriptor = utils.TemplateDescriptor(depot=utils.get_depot(utils.get_depot_config(name=depot, 300 | pros_cfg=cfg.pros_cfg)), 301 | offline=False, 302 | online=True) 303 | 304 | click.echo('Downloading {} {} from {} using {}'.format(identifier.name, 305 | identifier.version, 306 | descriptor.depot.config.name, 307 | descriptor.depot.registrar)) 308 | new_identifier = descriptor.depot.download(identifier) 309 | if new_identifier == False: 310 | click.echo('Failed to download {0} {1} from {2}'.format(identifier.version, identifier.version, identifier.depot)) 311 | else: 312 | if new_identifier.name == 'kernel': 313 | click.echo('''To create a new PROS project with this template, run `pros conduct new {0} {1}`, 314 | or to upgrade an existing project, run `pros conduct upgrade {0} {1}''' 315 | .format(new_identifier.version, new_identifier.depot)) 316 | else: 317 | click.echo('''To add this library to a PROS project, run `pros conduct add-lib {0} {1} {2}, 318 | or to upgrade an existing project with this library to the new version, run `pros conduct upgrade-lib {0} {1} {2}''' 319 | .format(new_identifier.name, new_identifier.version, new_identifier.depot)) 320 | 321 | # endregion 322 | 323 | 324 | # region Project Management 325 | @conduct.command('new', aliases=['new-proj', 'new-project', 'create', 'create-proj', 'create-project'], 326 | short_help='Creates a new PROS project') 327 | @click.argument('location') 328 | @click.argument('kernel', default='latest') 329 | @click.argument('depot', default='auto') 330 | @click.option('--force', 'mode', flag_value='force') 331 | @click.option('--safe', 'mode', flag_value='safe') 332 | @click.option('--default', 'mode', flag_value='default', default=True) 333 | @default_cfg 334 | def new(cfg, kernel, location, depot, mode): 335 | first_run(cfg) 336 | templates = local.get_local_templates(pros_cfg=cfg.pros_cfg, 337 | template_types=[TemplateTypes.kernel]) # type: Set[Identifier] 338 | if not templates or len(templates) == 0: 339 | click.echo('No templates have been downloaded! Use `pros conduct download` to download the latest kernel.') 340 | click.get_current_context().abort() 341 | sys.exit() 342 | kernel_version = kernel 343 | if kernel == 'latest': 344 | kernel_version = sorted(templates, key=lambda t: semver.Version(t.version))[-1].version 345 | proscli.utils.debug('Resolved version {} to {}'.format(kernel, kernel_version)) 346 | templates = [t for t in templates if t.version == kernel_version] # type: List[Identifier] 347 | depot_registrar = depot 348 | if depot == 'auto': 349 | templates = [t for t in templates if t.version == kernel_version] 350 | if not templates or len(templates) == 0: 351 | click.echo('No templates exist for {}'.format(kernel_version)) 352 | click.get_current_context().abort() 353 | sys.exit() 354 | if 'pros-mainline' in [t.depot for t in templates]: 355 | depot_registrar = 'pros-mainline' 356 | else: 357 | depot_registrar = [t.depot for t in templates][0] 358 | proscli.utils.debug('Resolved depot {} to {}'.format(depot, depot_registrar)) 359 | templates = [t for t in templates if t.depot == depot_registrar] 360 | if not templates or len(templates) == 0: 361 | click.echo('No templates were found for kernel version {} on {}'.format(kernel_version, depot_registrar)) 362 | click.get_current_context().abort() 363 | sys.exit() 364 | template = templates[0] 365 | if not os.path.isabs(location): 366 | location = os.path.abspath(location) 367 | click.echo('Creating new project from {} on {} at {}'.format(template.version, template.depot, location)) 368 | local.create_project(identifier=template, dest=location, pros_cli=cfg.pros_cfg, 369 | require_empty=(mode == 'safe'), overwrite=(mode == 'force')) 370 | 371 | 372 | @conduct.command('upgrade', aliases=['update'], help='Upgrades a PROS project') 373 | @click.argument('location') 374 | @click.argument('kernel', default='latest') 375 | @click.argument('depot', default='auto') 376 | @default_cfg 377 | def upgrade(cfg, kernel, location, depot): 378 | first_run(cfg) 379 | templates = local.get_local_templates(pros_cfg=cfg.pros_cfg, 380 | template_types=[TemplateTypes.kernel]) # type: List[Identifier] 381 | if not templates or len(templates) == 0: 382 | click.echo('No templates have been downloaded! Use `pros conduct download` to download the latest kernel.') 383 | click.get_current_context().abort() 384 | sys.exit() 385 | kernel_version = kernel 386 | if kernel == 'latest': 387 | kernel_version = sorted(templates, key=lambda t: semver.Version(t.version))[-1].version 388 | proscli.utils.debug('Resolved version {} to {}'.format(kernel, kernel_version)) 389 | templates = [t for t in templates if t.version == kernel_version] 390 | depot_registrar = depot 391 | if depot == 'auto': 392 | templates = [t for t in templates if t.version == kernel_version] 393 | if not templates or len(templates) == 0: 394 | click.echo('No templates exist for {}'.format(kernel_version)) 395 | click.get_current_context().abort() 396 | sys.exit() 397 | if 'pros-mainline' in [t.depot for t in templates]: 398 | depot_registrar = 'pros-mainline' 399 | else: 400 | depot_registrar = [t.depot for t in templates][0] 401 | proscli.utils.debug('Resolved depot {} to {}'.format(depot, depot_registrar)) 402 | templates = [t for t in templates if t.depot == depot_registrar] 403 | if not templates or len(templates) == 0: 404 | click.echo('No templates were found for kernel version {} on {}'.format(kernel_version, depot_registrar)) 405 | template = templates[0] 406 | if not os.path.isabs(location): 407 | location = os.path.abspath(location) 408 | click.echo('Upgrading existing project to {} on {} at {}'.format(template.version, template.depot, location)) 409 | local.upgrade_project(identifier=template, dest=location, pros_cli=cfg.pros_cfg) 410 | 411 | 412 | @conduct.command('register', aliases=[], help='Manifest project.pros file') 413 | @click.argument('location') 414 | @click.argument('kernel', default='latest') 415 | @default_cfg 416 | def register(cfg, location, kernel): 417 | first_run(cfg) 418 | kernel_version = kernel 419 | if kernel_version == 'latest': 420 | templates = local.get_local_templates(pros_cfg=cfg.pros_cfg, 421 | template_types=[TemplateTypes.kernel]) # type: List[Identifier] 422 | if not templates or len(templates) == 0: 423 | click.echo('No templates have been downloaded! Use `pros conduct download` to download the latest kernel or' 424 | ' specify a kernel manually.') 425 | click.get_current_context().abort() 426 | sys.exit() 427 | kernel_version = sorted(templates, key=lambda t: semver.Version(t.version))[-1].version 428 | proscli.utils.debug('Resolved version {} to {}'.format(kernel, kernel_version)) 429 | 430 | cfg = prosconfig.ProjectConfig(location, create=True, raise_on_error=True) 431 | cfg.kernel = kernel_version 432 | if not location: 433 | click.echo('Location not specified, registering current directory.') 434 | click.echo('Registering {} with kernel {}'.format(location or os.path.abspath('.'), kernel_version)) 435 | cfg.save() 436 | 437 | 438 | # endregion 439 | 440 | 441 | @conduct.command('add-lib', aliases=['install-lib', 'new-lib', 'new-library', 'install-library', 'add-library'], 442 | help='Installs a new library') 443 | @click.argument('location') 444 | @click.argument('library') 445 | @click.argument('version', default='latest') 446 | @click.argument('depot', default='auto') 447 | @click.option('--force', is_flag=True, default=False) 448 | @default_cfg 449 | def newlib(cfg, location, library, version, depot, force): 450 | if not (version == 'latest') and len(version.split('.')) < 3: 451 | depot = version 452 | version = 'latest' 453 | first_run(cfg) 454 | templates = local.get_local_templates(pros_cfg=cfg.pros_cfg, 455 | template_types=[TemplateTypes.library]) # type: List[Identifier] 456 | selected = None 457 | to_remove = [] 458 | if not templates or len(templates) == 0: 459 | click.echo('No templates have been downloaded! Use `pros conduct download` to download the latest kernel.') 460 | click.get_current_context().abort() 461 | sys.exit() 462 | templates = [t for t in templates if t.name == library] 463 | to_remove = [] 464 | if version == 'latest': 465 | lib_version = sorted(templates, key=lambda t: semver.Version(t.version))[-1].version 466 | highest = lib_version.split('.') 467 | for template in templates: 468 | curr = template.version.split('.') 469 | if len(highest) > len(curr): 470 | to_remove.append(template) 471 | for i in range(len(highest)): 472 | if curr[i] < highest[i]: 473 | to_remove.append(template) 474 | break 475 | 476 | else: 477 | for template in templates: 478 | if template.version != version: 479 | to_remove.append(template) 480 | for template in to_remove: 481 | templates.remove(template) 482 | to_remove = [] 483 | if depot == 'auto': 484 | for template in templates: 485 | if template.depot == 'pros-mainline': 486 | selected = template 487 | break 488 | if selected is None: 489 | selected = templates[0] 490 | else: 491 | for template in templates: 492 | if template.depot != depot: 493 | to_remove.append(template) 494 | for template in to_remove: 495 | templates.remove(template) 496 | to_remove = [] 497 | if len(templates) > 0: 498 | selected = templates[0] 499 | else: 500 | click.echo( 501 | 'No local libraries match the specified name, version, and depot. Check your arguments and make sure the appropriate libraries are downloaded') 502 | click.get_current_context().abort() 503 | sys.exit() 504 | local.install_lib(selected, location, cfg.pros_cfg, overwrite=force) 505 | print('Installed library {} v. {} in {} from {}'.format(selected.name, selected.version, location, selected.depot)) 506 | 507 | 508 | @conduct.command('upgrade-lib', aliases=['update-lib', 'upgrade-library', 'update-library'], 509 | help='Installs a new library') 510 | @click.argument('location') 511 | @click.argument('library') 512 | @click.argument('version', default='latest') 513 | @click.argument('depot', default='auto') 514 | @default_cfg 515 | def upgradelib(cfg, location, library, version, depot): 516 | if not (version == 'latest') and len(version.split('.')) < 3: 517 | depot = version 518 | version = 'latest' 519 | first_run(cfg) 520 | templates = local.get_local_templates(pros_cfg=cfg.pros_cfg, 521 | template_types=[TemplateTypes.library]) # type: List[Identifier] 522 | selected = None 523 | to_remove = [] 524 | if not templates or len(templates) == 0: 525 | click.echo('No templates have been downloaded! Use `pros conduct download` to download the latest kernel.') 526 | click.get_current_context().abort() 527 | sys.exit() 528 | for template in templates: 529 | if template.name != library: 530 | to_remove.append(template) 531 | for template in to_remove: 532 | templates.remove(template) 533 | to_remove = [] 534 | if version == 'latest': 535 | lib_version = sorted(templates, key=lambda t: semver.Version(t.version))[-1].version 536 | highest = lib_version.split('.') 537 | for template in templates: 538 | curr = template.version.split('.') 539 | if len(highest) > len(curr): 540 | to_remove.append(template) 541 | for i in range(len(highest)): 542 | if curr[i] < highest[i]: 543 | to_remove.append(template) 544 | break 545 | 546 | else: 547 | for template in templates: 548 | if template.version != version: 549 | to_remove.append(template) 550 | for template in to_remove: 551 | templates.remove(template) 552 | to_remove = [] 553 | if depot == 'auto': 554 | for template in templates: 555 | if template.depot == 'pros-mainline': 556 | selected = template 557 | break 558 | if selected == None: 559 | selected = templates[0] 560 | else: 561 | for template in templates: 562 | if template.depot != depot: 563 | to_remove.append(template) 564 | for template in to_remove: 565 | templates.remove(template) 566 | to_remove = [] 567 | if len(templates) > 0: 568 | selected = templates[0] 569 | else: 570 | click.echo( 571 | 'No local libraries match the specified name, version, and depot. Check your arguments and make sure the appropriate libraries are downloaded') 572 | click.get_current_context().abort() 573 | sys.exit() 574 | local.upgrade_project(selected, location, cfg.pros_cfg) 575 | proj_config = prosconfig.ProjectConfig(location) 576 | if type(proj_config.libraries) is list: 577 | proj_config.libraries = dict() 578 | proj_config.libraries[selected.name] = selected.version 579 | proj_config.save() 580 | print('Updated library {} v. {} in {} from {}'.format(selected.name, selected.version, location, selected.depot)) 581 | 582 | 583 | @conduct.command('first-run', help='Runs the first-run configuration') 584 | @click.option('--no-force', is_flag=True, default=True) 585 | @click.option('--use-defaults', is_flag=True, default=False) 586 | @click.option('--no-download', is_flag=True, default=True) 587 | @click.option('--apply-providers', is_flag=True, default=False) 588 | @default_cfg 589 | def first_run_cmd(cfg, no_force, use_defaults, no_download, apply_providers): 590 | first_run(cfg, force=no_force, defaults=use_defaults, 591 | doDownload=no_download, reapplyProviders=apply_providers) 592 | 593 | 594 | import proscli.conductor_management 595 | --------------------------------------------------------------------------------