mirror of
https://github.com/Architeuthis-Flux/JumperlessV5.git
synced 2025-12-26 03:56:48 +00:00
351 lines
13 KiB
Python
Executable File
351 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Simple Auto Upload for Jumperless
|
|
Watches for a specific USB port, uploads firmware once, then waits for disconnect/reconnect.
|
|
|
|
Supports two upload methods:
|
|
1. PlatformIO upload (default) - for development builds
|
|
2. UF2 file copy - for pre-built firmware files with automatic bootloader triggering
|
|
|
|
Features:
|
|
- Automatic bootloader mode triggering via 1200 baud connection
|
|
- Cross-platform drive detection for UF2 uploads
|
|
- Backward compatible with existing workflows
|
|
|
|
Usage:
|
|
python3 simple_auto_upload.py [PORT] [--uf2 UF2_FILE]
|
|
|
|
Options:
|
|
PORT USB port (e.g., /dev/cu.usbmodem101, COM3)
|
|
--uf2 FILE Use specific UF2 file for upload
|
|
--uf2=FILE Alternative syntax for UF2 file
|
|
|
|
Examples:
|
|
python3 simple_auto_upload.py /dev/cu.usbmodem101
|
|
python3 simple_auto_upload.py COM3
|
|
python3 simple_auto_upload.py --uf2 firmware.uf2
|
|
python3 simple_auto_upload.py /dev/cu.usbmodem101 --uf2 custom.uf2
|
|
"""
|
|
|
|
import subprocess
|
|
import time
|
|
import sys
|
|
import os
|
|
import shutil
|
|
import serial.tools.list_ports
|
|
import glob
|
|
|
|
|
|
|
|
#python autoUploader/simple_auto_upload.py
|
|
|
|
# Configuration
|
|
USB_PORT = "/dev/cu.usbmodem101" # Change this to your Jumperless port
|
|
ENVIRONMENT = "jumperless_v5"
|
|
CHECK_INTERVAL = 1 # seconds
|
|
|
|
def find_uf2_files(directory="."):
|
|
"""Find all UF2 files in the given directory"""
|
|
pattern = os.path.join(directory, "*.uf2")
|
|
return glob.glob(pattern)
|
|
|
|
def find_jumperless_mount():
|
|
"""Find mounted Jumperless drive by looking for INFO_UF2.TXT"""
|
|
if sys.platform == "darwin": # macOS
|
|
import subprocess
|
|
try:
|
|
result = subprocess.run(['mount'], capture_output=True, text=True)
|
|
mount_lines = result.stdout.split('\n')
|
|
for line in mount_lines:
|
|
# Extract mount point from the line (format: "/dev/diskXsY on /Volumes/NAME (type, options)")
|
|
if ' on /Volumes/' in line:
|
|
mount_point = line.split(' on ')[1].split(' (')[0]
|
|
|
|
# Check if this is a potential RP2040/RP2350 drive
|
|
if any(pattern in mount_point.upper() for pattern in ['RP2350', 'RP2040', 'RPI-RP2']):
|
|
info_file = os.path.join(mount_point, "INFO_UF2.TXT")
|
|
if os.path.exists(info_file):
|
|
return mount_point
|
|
|
|
# Also check any volume that has INFO_UF2.TXT (fallback for custom names)
|
|
info_file = os.path.join(mount_point, "INFO_UF2.TXT")
|
|
if os.path.exists(info_file):
|
|
try:
|
|
with open(info_file, 'r') as f:
|
|
content = f.read()
|
|
# Accept any valid UF2 bootloader (RP2040, RP2350, etc.)
|
|
if any(pattern in content.upper() for pattern in ['RASPBERRY PI', 'UF2 BOOTLOADER']):
|
|
return mount_point
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
elif sys.platform == "linux":
|
|
# Linux mount detection
|
|
try:
|
|
with open('/proc/mounts', 'r') as f:
|
|
for line in f:
|
|
parts = line.split()
|
|
if len(parts) >= 2:
|
|
mount_point = parts[1]
|
|
|
|
# Check if this is a potential RP2040/RP2350 drive
|
|
if any(pattern in mount_point.upper() for pattern in ['RP2350', 'RP2040', 'RPI-RP2']):
|
|
info_file = os.path.join(mount_point, "INFO_UF2.TXT")
|
|
if os.path.exists(info_file):
|
|
return mount_point
|
|
|
|
# Also check any mount point that has INFO_UF2.TXT (fallback)
|
|
info_file = os.path.join(mount_point, "INFO_UF2.TXT")
|
|
if os.path.exists(info_file):
|
|
try:
|
|
with open(info_file, 'r') as f:
|
|
content = f.read()
|
|
# Accept any valid UF2 bootloader (RP2040, RP2350, etc.)
|
|
if any(pattern in content.upper() for pattern in ['RASPBERRY PI', 'UF2 BOOTLOADER']):
|
|
return mount_point
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
elif sys.platform == "win32":
|
|
# Windows drive detection
|
|
import string
|
|
for drive in string.ascii_uppercase:
|
|
drive_path = f"{drive}:\\"
|
|
|
|
# Check if this is a potential RP2040/RP2350 drive
|
|
if any(pattern in drive_path.upper() for pattern in ['RP2350', 'RP2040', 'RPI-RP2']):
|
|
info_file = os.path.join(drive_path, "INFO_UF2.TXT")
|
|
if os.path.exists(info_file):
|
|
return drive_path
|
|
|
|
# Also check any drive that has INFO_UF2.TXT (fallback)
|
|
info_file = os.path.join(drive_path, "INFO_UF2.TXT")
|
|
if os.path.exists(info_file):
|
|
try:
|
|
with open(info_file, 'r') as f:
|
|
content = f.read()
|
|
# Accept any valid UF2 bootloader (RP2040, RP2350, etc.)
|
|
if any(pattern in content.upper() for pattern in ['RASPBERRY PI', 'UF2 BOOTLOADER']):
|
|
return drive_path
|
|
except:
|
|
pass
|
|
|
|
return None
|
|
|
|
class SimpleJumperlessUploader:
|
|
def __init__(self):
|
|
self.uploaded_to_current_device = False
|
|
self.device_connected = False
|
|
self.ignore_next_reconnect = False
|
|
self.last_upload_time = 0
|
|
|
|
def is_device_connected(self):
|
|
"""Check if the target USB port exists"""
|
|
ports = [port.device for port in serial.tools.list_ports.comports()]
|
|
return USB_PORT in ports
|
|
|
|
def run_upload(self):
|
|
"""Run PlatformIO upload command"""
|
|
print(f"↑ Uploading to {USB_PORT}...")
|
|
print("=" * 50)
|
|
|
|
cmd = [
|
|
"platformio", "run",
|
|
"--target", "upload",
|
|
"--environment", ENVIRONMENT,
|
|
"--upload-port", USB_PORT
|
|
]
|
|
|
|
try:
|
|
# Run command and show output in real-time
|
|
result = subprocess.run(cmd, text=True)
|
|
|
|
print("=" * 50)
|
|
if result.returncode == 0:
|
|
print("☺ Upload successful!")
|
|
self.last_upload_time = time.time()
|
|
self.ignore_next_reconnect = True
|
|
return True
|
|
else:
|
|
print("☹ Upload failed!")
|
|
return False
|
|
|
|
except FileNotFoundError:
|
|
print("☹ PlatformIO not found! Install with: pip install platformio")
|
|
return False
|
|
except Exception as e:
|
|
print(f"☹ Upload error: {e}")
|
|
return False
|
|
|
|
def trigger_bootloader(self):
|
|
"""Trigger RP2350B bootloader by connecting at 1200 baud"""
|
|
print(f"🔧 Triggering bootloader on {USB_PORT}...")
|
|
try:
|
|
# Connect at 1200 baud for ~100ms to trigger bootloader
|
|
with serial.Serial(USB_PORT, 1200, timeout=0.1) as ser:
|
|
time.sleep(0.1) # Hold connection briefly
|
|
print("✅ Bootloader trigger sent")
|
|
return True
|
|
except Exception as e:
|
|
print(f"⚠️ Could not trigger bootloader: {e}")
|
|
return False
|
|
|
|
def run_uf2_upload(self, uf2_file=None):
|
|
"""Upload UF2 file by copying to mounted Jumperless drive"""
|
|
print(f"🔄 UF2 Upload mode - looking for mounted Jumperless drive...")
|
|
|
|
# Find UF2 file if not provided
|
|
if not uf2_file:
|
|
uf2_files = find_uf2_files()
|
|
if not uf2_files:
|
|
print("☹ No UF2 files found in current directory!")
|
|
return False
|
|
uf2_file = uf2_files[0]
|
|
print(f"📁 Using UF2 file: {uf2_file}")
|
|
|
|
if not os.path.exists(uf2_file):
|
|
print(f"☹ UF2 file not found: {uf2_file}")
|
|
return False
|
|
|
|
print("=" * 50)
|
|
|
|
# Trigger bootloader mode
|
|
print("🔧 Attempting to trigger bootloader mode...")
|
|
if not self.trigger_bootloader():
|
|
print("💡 Manual bootloader entry may be required")
|
|
|
|
# Give device time to enter bootloader mode
|
|
print("⏳ Waiting for device to enter bootloader mode...")
|
|
time.sleep(2)
|
|
|
|
# Wait for Jumperless drive to appear
|
|
max_wait = 30 # seconds
|
|
for i in range(max_wait):
|
|
mount_point = find_jumperless_mount()
|
|
if mount_point:
|
|
break
|
|
if i == 0:
|
|
print(f"🔍 Looking for Jumperless drive...")
|
|
time.sleep(1)
|
|
|
|
if not mount_point:
|
|
print("☹ Could not find mounted Jumperless drive!")
|
|
print("💡 Device may not have entered bootloader mode. Try double-tap reset if needed.")
|
|
return False
|
|
|
|
print(f"🎯 Found Jumperless drive at: {mount_point}")
|
|
|
|
try:
|
|
# Copy UF2 file to the drive
|
|
destination = os.path.join(mount_point, "firmware.uf2")
|
|
print(f"📋 Copying {uf2_file} to {destination}...")
|
|
shutil.copy2(uf2_file, destination)
|
|
|
|
print("=" * 50)
|
|
print("☺ UF2 upload successful!")
|
|
print("✨ Device should restart automatically.")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"☹ UF2 copy error: {e}")
|
|
return False
|
|
|
|
def run(self):
|
|
"""Main loop"""
|
|
print(f"👀 Watching for Jumperless on {USB_PORT}")
|
|
print("Press Ctrl+C to stop")
|
|
|
|
try:
|
|
while True:
|
|
currently_connected = self.is_device_connected()
|
|
|
|
# Device just connected
|
|
if currently_connected and not self.device_connected:
|
|
# Check if we should ignore this reconnect (happens after upload)
|
|
time_since_upload = time.time() - self.last_upload_time
|
|
if self.ignore_next_reconnect and time_since_upload < 10: # 30 second window
|
|
print(f"🔄 Device reconnected after upload - ignoring...")
|
|
|
|
self.device_connected = True
|
|
self.ignore_next_reconnect = False
|
|
else:
|
|
print(f"◎ Device connected on {USB_PORT}")
|
|
self.device_connected = True
|
|
self.uploaded_to_current_device = False
|
|
self.ignore_next_reconnect = False
|
|
|
|
# Wait a moment for device to settle
|
|
time.sleep(2)
|
|
|
|
# Upload if we haven't already
|
|
if not self.uploaded_to_current_device:
|
|
# Check if we should use UF2 upload method
|
|
if self.uf2_file or find_uf2_files():
|
|
print("🔄 Using UF2 upload method...")
|
|
uf2_file_to_use = self.uf2_file if self.uf2_file else None
|
|
success = self.run_uf2_upload(uf2_file_to_use)
|
|
else:
|
|
print("🔧 Using PlatformIO upload method...")
|
|
success = self.run_upload()
|
|
|
|
if success:
|
|
self.uploaded_to_current_device = True
|
|
print("✨ Ready! Disconnect and reconnect to upload again.")
|
|
|
|
# Device disconnected
|
|
elif not currently_connected and self.device_connected:
|
|
if self.ignore_next_reconnect:
|
|
print(f"🔄 Device disconnected during upload process...")
|
|
else:
|
|
print(f"◯ Device disconnected from {USB_PORT}")
|
|
self.uploaded_to_current_device = False
|
|
self.device_connected = False
|
|
|
|
time.sleep(CHECK_INTERVAL)
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n🛑 Stopping...")
|
|
|
|
def main():
|
|
global USB_PORT
|
|
usb_port = USB_PORT
|
|
uf2_file = None
|
|
|
|
# Parse command line arguments
|
|
i = 1
|
|
while i < len(sys.argv):
|
|
if sys.argv[i] == "--uf2" and i + 1 < len(sys.argv):
|
|
uf2_file = sys.argv[i + 1]
|
|
i += 2
|
|
elif sys.argv[i].startswith("--uf2="):
|
|
uf2_file = sys.argv[i][6:] # Remove --uf2=
|
|
i += 1
|
|
elif not sys.argv[i].startswith("--"):
|
|
# Assume it's a port specification
|
|
usb_port = sys.argv[i]
|
|
i += 1
|
|
else:
|
|
i += 1
|
|
|
|
USB_PORT = usb_port
|
|
|
|
if uf2_file:
|
|
print(f"Using custom UF2 file: {uf2_file}")
|
|
|
|
print("○ Simple Jumperless Auto-Upload")
|
|
print("=" * 40)
|
|
print(f"Target port: {USB_PORT}")
|
|
print(f"Environment: {ENVIRONMENT}")
|
|
if uf2_file:
|
|
print(f"UF2 file: {uf2_file}")
|
|
print()
|
|
|
|
uploader = SimpleJumperlessUploader()
|
|
# Store the UF2 file for use in upload method
|
|
uploader.uf2_file = uf2_file
|
|
uploader.run()
|
|
|
|
if __name__ == "__main__":
|
|
main() |