-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d4ab0f8
commit 185e449
Showing
14 changed files
with
513 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* text=auto |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
state.json | ||
__pycache__/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.