Skip to content

Commit

Permalink
Merge pull request #31 from ramonaoptics/provide_a_cli_to_program
Browse files Browse the repository at this point in the history
Provide a static method and cli to program
  • Loading branch information
hmaarrfk authored Apr 5, 2024
2 parents dddd771 + 5600fc7 commit 262da01
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ jobs:
run: pylint teensytoany
- name: isort
run: isort --check-only .
- name: teensytoany_programmer
run: teensytoany_programmer --help
- name: Test with tox
run: tox
6 changes: 6 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# History

## 0.4.0 (2024-04-05)

* Provide functions to fetch local and remote firmware versions.
* Provide a static method to program the latest firmware version and a CLI to
do so from the terminal.

## 0.3.0 (2024-04-02)

* Add a `timeout` property to the functions that deal with updating the firmware.
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@
[![pypi](https://img.shields.io/pypi/v/teensytoany.svg)](https://pypi.python.org/pypi/teensytoany)
[![Travis](https://travis-ci.com/ramonaoptics/python-teensytoany.svg?branch=master)](https://travis-ci.com/ramonaoptics/python-teensytoany)
[![Docs](https://readthedocs.org/projects/python-teensytoany/badge/?version=latest)](https://python-teensytoany.readthedocs.io/en/latest/?badge=latest)


A pythonic way to access the teensytoany board

* Documentation: https://python-teensytoany.readthedocs.io.
* See: https://github.com/ramonaoptics/teensy-to-any

To program the teensy to any devices you will need the following additional dependencies:

* `click`
* `appdirs`
* `requests`

You can install them with:

```bash
conda install click appdirs requests
# or
pip install click appdirs requests
```

Features
--------

Expand Down
5 changes: 4 additions & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ flake8
tox
pytest
pylint
requests
isort
# Optional dependencies below
requests
click
appdirs
5 changes: 5 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ def get_version_and_cmdclass(pkg_path):
include_package_data=True,
keywords='teensytoany',
name='teensytoany',
entry_points={
'console_scripts': [
'teensytoany_programmer=teensytoany.programmer:teensytoany_programmer',
],
},
packages=find_packages(include=['teensytoany']),
tests_require=test_requirements,
url='https://github.com/ramonaoptics/python-teensytoany',
Expand Down
62 changes: 62 additions & 0 deletions teensytoany/programmer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import click

import teensytoany


@click.command(epilog=f"Version {teensytoany.__version__}")
@click.option(
'--serial-number',
default=None,
help=(
'Serial number of the Teensy device to program. '
'If not provided and only one Teensy device found, '
'it will be programmed.'
)
)
# Create an option with 2 valid inputs TEENSY40 and TEENSY32
@click.option(
'--mcu',
type=click.Choice(['TEENSY40', 'TEENSY32']),
default='TEENSY40',
help='Microcontroller to program.'
)
@click.option(
'--firmware-version',
default=None,
type=str,
help='Firmware version to program. If not provided, the latest version will be programmed.'
)
@click.option(
'--download-only',
is_flag=True,
default=False,
help='Download the firmware only, do not program the device.'
)
# Make the epligue print the version
@click.version_option(teensytoany.__version__)
def teensytoany_programmer(
serial_number=None,
mcu='TEENSY40',
firmware_version=None,
download_only=False
):
"""Program a Teensy device with a given firmware version"""
if download_only:
for mcu_to_download in ['TEENSY40', 'TEENSY32']:
if firmware_version is None:
firmware_version = teensytoany.TeensyToAny.get_latest_available_firmware_version(
mcu=mcu_to_download, online=True, local=False
)
print(f"Downloading firmware version {firmware_version} for {mcu}.")
teensytoany.TeensyToAny.download_firmware(mcu=mcu, version=firmware_version)
return

print(f'Programming irmware version {mcu} {firmware_version}', end='')
teensytoany.TeensyToAny.program_firmware(serial_number, mcu=mcu, version=firmware_version)
teensy = teensytoany.TeensyToAny(serial_number)
print(f"TeensyToAny version: {teensy.version}")
print(f"TeensyToAny serial_number: {teensy.serial_number}")
teensy.close()

if __name__ == '__main__':
teensytoany_programmer()
128 changes: 110 additions & 18 deletions teensytoany/teensytoany.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import subprocess
import tempfile
from time import sleep
from typing import Sequence

Expand Down Expand Up @@ -134,7 +133,30 @@ def list_all_serial_numbers(serial_numbers=None, *, device_name=None):
return serial_numbers

@staticmethod
def _get_latest_available_firmware(*, timeout=2):
def get_latest_available_firmware_version(
*, mcu='TEENSY40', online=True, local=True, timeout=2
):
if local:
local_versions = TeensyToAny._find_local_versions(mcu=mcu)
if len(local_versions) > 0:
latest = local_versions[-1]
try:
if online:
latest = TeensyToAny._get_latest_available_firmware_online(
timeout=timeout)
except Exception: # pylint: disable=broad-except
pass

if latest is None:
raise RuntimeError(
"Failed to fetch the latest release information. "
"Please check your internet connection and try again."
)

return latest

@staticmethod
def _get_latest_available_firmware_online(*, timeout=2):
import requests # pylint: disable=import-outside-toplevel

repo_url = "https://api.github.com/repos/ramonaoptics/teensy-to-any"
Expand Down Expand Up @@ -174,9 +196,8 @@ def mcu(self):
return self._ask('mcu')

def _update_firmware(self, *, mcu=None, force=False, timeout=2):
import requests # pylint: disable=import-outside-toplevel

current_version = self.version
serial_number = self.serial_number
if mcu is None:
mcu = self.mcu

Expand All @@ -185,45 +206,116 @@ def _update_firmware(self, *, mcu=None, force=False, timeout=2):
"The current microcontroller is unknown, please specify it "
"before attempting to update the firmware.")

if os.name == 'nt':
# We do supporting updating, but it is "scary" to do so since
# there is no serial number specificity
raise RuntimeError("We do not supporting updating MCUs on windows")

latest_version = self._get_latest_available_firmware(timeout=timeout)
latest_version = self.get_latest_available_firmware_version(mcu=mcu, timeout=timeout)
if not force:
if Version(current_version) >= Version(latest_version):
return

file_url = f"https://github.com/ramonaoptics/teensy-to-any/releases/download/{latest_version}/firmware_{mcu.lower()}.hex" # noqa # pylint: disable=line-too-long
self.close()
self._requested_serial_number = serial_number
try:
self.program_firmware(
serial_number,
mcu=mcu,
version=latest_version,
timeout=timeout
)
# Reraise any exceptions that were caught
finally:
self.open()

firmware_filename = tempfile.mktemp(suffix='.hex')
@staticmethod
def _find_local_versions(*, mcu=None):
firmware_dir = TeensyToAny._generate_firmware_directory(mcu=mcu)
if not firmware_dir.is_dir():
return []

versions = [
d.name
for d in firmware_dir.iterdir()
if d.is_dir() and (d / 'firmware.hex').is_file()
]

versions.sort(key=Version)
return versions

@staticmethod
def _generate_firmware_filename(*, mcu, version):
firmware_dir = TeensyToAny._generate_firmware_directory(mcu=mcu)
firmware_filename = firmware_dir / f"{version}" / "firmware.hex"
return firmware_filename

@staticmethod
def _generate_firmware_directory(*, mcu):
from pathlib import Path # pylint: disable=import-outside-toplevel

from appdirs import AppDirs # pylint: disable=import-outside-toplevel
app = AppDirs('teensytoany', 'ramonaoptics')
cache_dir = Path(app.user_cache_dir)
cache_dir.mkdir(parents=True, exist_ok=True)
firmware_dir = cache_dir / f"{mcu.lower()}"
firmware_dir.mkdir(parents=True, exist_ok=True)
return firmware_dir

@staticmethod
def download_firmware(*, mcu, version, timeout=2):
firmware_filename = TeensyToAny._generate_firmware_filename(mcu=mcu, version=version)

import requests # pylint: disable=import-outside-toplevel
file_url = f"https://github.com/ramonaoptics/teensy-to-any/releases/download/{version}/firmware_{mcu.lower()}.hex" # noqa # pylint: disable=line-too-long
response = requests.get(file_url, timeout=timeout)
if response.status_code != 200:
raise RuntimeError("Failed to download firmware")

firmware_filename.parent.mkdir(parents=True, exist_ok=True)
# Open the file for binary writing
with open(firmware_filename, 'wb') as file:
# Write the content to the file in chunks
for chunk in response.iter_content(chunk_size=4096):
file.write(chunk)

requested_serial_number = self.serial_number
return firmware_filename

@staticmethod
def program_firmware(serial_number=None, *, mcu=None, version=None, timeout=2):
if serial_number is None:
available_serial_numbers = TeensyToAny.list_all_serial_numbers()
if len(available_serial_numbers) == 0:
raise RuntimeError("No TeensyToAny devices found.")
if len(available_serial_numbers) > 1:
raise RuntimeError(
"Multiple TeensyToAny devices found. Please specify the "
"serial number of the device you would like to program."
)
serial_number = available_serial_numbers[0]

if mcu is None:
raise RuntimeError("mcu must be provided and cannot be left as None.")

if version is None:
version = TeensyToAny.get_latest_available_firmware_version(timeout=timeout)

if os.name == 'nt':
# We do supporting updating, but it is "scary" to do so since
# there is no serial number specificity
raise RuntimeError("We do not supporting programing TeensyToAny devices on Windows")

firmware_filename = TeensyToAny._generate_firmware_filename(mcu=mcu, version=version)

if not firmware_filename.is_file():
TeensyToAny.download_firmware(mcu=mcu, version=version, timeout=timeout)

cmd_list = [
'teensy_loader_cli',
'-s',
f'--mcu={mcu}',
f'--serial-number={requested_serial_number}',
firmware_filename,
f'--serial-number={serial_number}',
str(firmware_filename),
]

self.close()
subprocess.check_call(cmd_list)
# Wait for the device to reboot
sleep(1)
self._requested_serial_number = requested_serial_number
self.open()

def __init__(
self,
Expand Down

0 comments on commit 262da01

Please sign in to comment.