Skip to content

Commit

Permalink
It works!
Browse files Browse the repository at this point in the history
  • Loading branch information
SavageCore committed Jun 16, 2023
1 parent d4ab0f8 commit 185e449
Show file tree
Hide file tree
Showing 14 changed files with 513 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
state.json
__pycache__/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Oliver Sayers (savagecore.uk)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
# gp2040-flasher-py
# GP2040-Flasher

> A simple tool to flash [GP2040-CE](https://github.com/OpenStickCommunity/GP2040-CE) boards/controllers with the latest firmware
Quick screencast of it in action:

https://github.com/SavageCore/GP2040-Flasher-Pi/assets/171312/424dc74c-3bee-4f4f-bb7d-0d4fcac6b63b

![PXL_20230616_003118005](https://github.com/SavageCore/GP2040-Flasher-Pi/assets/171312/c4d1ca1c-7698-4c9f-a170-003628e8d6bc)



## Installation

This guide assumes you're running a Raspberry Pi (< 4) with PiTFT and have followed the [Easy Install](https://learn.adafruit.com/adafruit-pitft-28-inch-resistive-touchscreen-display-raspberry-pi/easy-install-2) guide making sure to setup as a [Raw Framebuffer Device](https://learn.adafruit.com/adafruit-pitft-28-inch-resistive-touchscreen-display-raspberry-pi/easy-install-2#pitft-as-raw-framebuffer-device-2982165).

1. Follow the instructions to install [picotool](https://github.com/raspberrypi/picotool).
2. Install SDL2 Dev libraries for your operating system.
2. Clone this repository.
3. Run `pip install -r requirements.txt` to install the required Python packages.

## Usage

1. cd into the directory where you cloned this repository.
2. Run `sudo python3 main.py` to start the program. (You must run as root to access the GPIO pins and the framebuffer.)
3. Select the firmware you want to flash with the top 2 buttons.
4. Hold the BOOTSEL button on the device you wish to flash and plug it into the Pi.
5. The firmware will be flashed to the device and it will reboot as a controller. (The firmware is nuked if needed)

Also included is a script to create a systemd service to start the flasher on boot. To use it, run `./create-systemd-service.sh` and it will start on next boot. To stop it from starting on boot, run `sudo systemctl disable gp2040-flasher`. If you want to start it manually, run `sudo systemctl start gp2040-flasher`.

You can double press the bottom button to exit the program. Note if you installed the systemd service, it will restart the program.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import RPi.GPIO as GPIO
import time


class Button:
def __init__(self, pin, double_press_interval=0.5):
self.pin = pin
self.prev_state = False
self.double_press_interval = double_press_interval
self.last_press_time = 0

# Setup GPIO pin
GPIO.setmode(GPIO.BCM)
GPIO.setup(self.pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

def is_button_pressed(self):
state = GPIO.input(self.pin)
if state == GPIO.LOW and not self.prev_state:
current_time = time.time()
time_since_last_press = current_time - self.last_press_time

if time_since_last_press <= self.double_press_interval:
self.last_press_time = 0 # Reset the last press time
return "double_press"
else:
self.last_press_time = current_time

self.prev_state = True
return True
elif state == GPIO.HIGH and self.prev_state:
self.prev_state = False

return False

def cleanup(self):
GPIO.cleanup(self.pin)
21 changes: 21 additions & 0 deletions create-systemd-service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
# Bash file to create a systemd service for the application
sudo touch /etc/systemd/system/gp2040-flasher.service

echo "[Unit]
Description=GP2040-Flasher
After=network.target
[Service]
ExecStart=sudo /usr/bin/python3 /home/pi/GP2040-Flasher-Pi/main.py
WorkingDirectory=/home/pi/GP2040-Flasher-Pi
StandardOutput=inherit
StandardError=inherit
Restart=always
User=pi
[Install]
WantedBy=multi-user.target" | sudo tee /etc/systemd/system/gp2040-flasher.service

sudo systemctl daemon-reload
sudo systemctl enable gp2040-flasher.service
Binary file added firmware/flash_nuke.uf2
Binary file not shown.
52 changes: 52 additions & 0 deletions github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import requests
import urllib.request
import tempfile
import os


class Github:
def __init__(self):
self.owner = "OpenStickCommunity"
self.repo = "GP2040-CE"
self.firmware_dir = "firmware"

def get_latest_release_info(self):
url = f"https://api.github.com/repos/{self.owner}/{self.repo}/releases"
response = requests.get(url)
if response.status_code == 200:
releases = response.json()
for release in releases:
if not release.get("prerelease"):
version = release["tag_name"]
release_date = release["published_at"]
assets = release["assets"]
# Remove asset with name "flash_nuke.uf2"
assets = [
asset for asset in assets if asset["name"] != "flash_nuke.uf2"]

return version, release_date, assets
else:
# Handle API request error
print("Error:", response.status_code)

return None, None, None

def download_file(self, url):
# Extract the filename from the URL
filename = url.split("/")[-1]

# Set the path to save the file in the temporary directory
save_path = os.path.join(self.firmware_dir, filename)

# Check if the file already exists
if os.path.exists(save_path):
return save_path

try:
# Download the file and save it to the temporary directory
urllib.request.urlretrieve(url, save_path)
print("File downloaded successfully to:", save_path)
return save_path
except urllib.error.URLError as e:
print("Failed to download the file:", e)
return None
194 changes: 194 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import pygame
import pyudev
import os
from splash_screen import SplashScreen
from button import Button
from picotool import Picotool
from github import Github
from state_manager import StateManager

# Hide pygame message
os.putenv('PYGAME_HIDE_SUPPORT_PROMPT', '1')
# Output to PiTFT screen
os.putenv('SDL_FBDEV', '/dev/fb1')

# Load selected firmware from state file or default to 0
state_manager = StateManager()
selected_firmware = state_manager.get_value("selected_firmware") or 0

github = Github()
version, release_date, firmware_files = github.get_latest_release_info()

# Initialise the pygame library
pygame.init()
pygame.mouse.set_visible(False)
screen = pygame.display.set_mode((320, 240), 0, 16)
screen.fill((0, 0, 0))
pygame.display.update()

# If no firmware files are found, exit
if firmware_files is None: # None is returned if the API request fails
print("Error looking up GitHub releases")
exit()


def clear_screen(screen):
screen.fill((0, 0, 0))
pygame.display.update()


def render_text(screen, text, font, color, padding=5):
# Calculate the maximum font size that fits within the screen dimensions
max_font_size = 100
text_width, text_height = font.size(text)
while text_width + (2 * padding) > screen.get_width() or text_height + (2 * padding) > screen.get_height():
max_font_size -= 1
font = pygame.font.Font(None, max_font_size)
text_width, text_height = font.size(text)

text_surface = font.render(text, True, color)

# Calculate the position with padding
x = (screen.get_width() - text_width - (2 * padding)) // 2
y = (screen.get_height() - text_height - (2 * padding)) // 2

# Apply padding to the text position
text_rect = text_surface.get_rect(x=x + padding, y=y + padding)

screen.blit(text_surface, text_rect)
pygame.display.update()


def flash_drive_handler(action, device):
global selected_firmware
global splash

device_name = device.sys_name.split('/')[-1]
if action == 'add' and device_name == "sda" and device.get('ID_VENDOR') == 'RPI' and device.get('ID_MODEL') == 'RP2':
picotool = Picotool()
splash.set_text("Pico detected")

download_url = firmware_files[selected_firmware]["browser_download_url"]
firmware_file = github.download_file(download_url)

# If the firmware file is downloaded successfully, flash it to the Pico
if firmware_file is not None:
if picotool.get_program_name() is None:
splash.set_text("Flashing...")
result = picotool.flash_firmware(firmware_file)
if result:
screen.fill((0, 255, 0))
render_text(screen, "Firmware flashed successfully",
pygame.font.Font(None, 100), (255, 255, 255))

pygame.time.wait(3000)
splash.set_text("Waiting for Pico...")
splash.show()
else:
screen.fill((255, 0, 0))
pygame.display.update()

render_text(screen, "Error flashing firmware",
pygame.font.Font(None, 100), (255, 255, 255))
print("Error flashing firmware")
else:
splash.set_text("Nuking...")
result = picotool.nuke_firmware()
if result:
splash.set_text("Nuked. Waiting for Pico...")
else:
print("Error flashing firmware")
render_text(screen, "Error flashing firmware",
pygame.font.Font(None, 100), (255, 255, 255))
else:
print("Error downloading firmware file")

def get_info_from_firmware_file(firmware_file):
# Example names:
# GP2040-CE_0.7.2_Stress.uf2
# GP2040-CE_0.7.2_RP2040AdvancedBreakoutBoard.uf2
# I want to return:
# 0.7.2
# Stress

firmware_file = firmware_file.replace(".uf2", "")
firmware_file = firmware_file.split("_")

version = firmware_file[1].split(".")
version = ".".join(version)

name = firmware_file[2].split(".")
name = ".".join(name)

return version, name

def main():
global selected_firmware
global splash
global screen

splash.show()

button1 = Button(17)
button2 = Button(22)
button3 = Button(23)
button4 = Button(27)

context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by('block')
observer = pyudev.MonitorObserver(monitor, flash_drive_handler)
observer.start()

# Main loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Check for button presses
if button1.is_button_pressed():
selected_firmware = (
selected_firmware + 1) % len(firmware_files)

state_manager.set_value("selected_firmware", selected_firmware)

version, name = get_info_from_firmware_file(firmware_files[selected_firmware]["name"])

splash.set_text(name + " (" + version + ")")
# Small delay to prevent button bounce
pygame.time.delay(200)
if button2.is_button_pressed():
selected_firmware = (
selected_firmware - 1) % len(firmware_files)

state_manager.set_value("selected_firmware", selected_firmware)

version, name = get_info_from_firmware_file(firmware_files[selected_firmware]["name"])
splash.set_text(name + " (" + version + ")")
# Small delay to prevent button bounce
pygame.time.delay(200)
if button3.is_button_pressed():
splash.set_text("Button 3")
b4result = button4.is_button_pressed()
if b4result == True:
splash.set_text("Button 4")
elif b4result == "double_press":
running = False

# Clean up
observer.stop()
pygame.quit()


if __name__ == '__main__':
initial_text = "Select firmware to flash"
if selected_firmware is not 0:
version, name = get_info_from_firmware_file(firmware_files[selected_firmware]["name"])
initial_text = "Firmware {} selected".format(name + " (" + version + ")")

splash = SplashScreen(
screen, 'assets/images/gp2040-ce-logo-inside-no-background.png', (0, 0, 0), initial_text)

main()
Loading

0 comments on commit 185e449

Please sign in to comment.