├── 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 |
--------------------------------------------------------------------------------