Skip to content

Commit 7fb179f

Browse files
committed
python: Implement firmware update
Signed-off-by: Daniel Schaefer <[email protected]>
1 parent d97884d commit 7fb179f

File tree

6 files changed

+552
-9
lines changed

6 files changed

+552
-9
lines changed

.github/workflows/software.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ jobs:
105105
steps:
106106
- uses: actions/checkout@v4
107107

108+
- name: Download releases to bundle
109+
run: |
110+
mkdir releases
111+
mkdir releases\0.2.0
112+
Invoke-WebRequest -Uri https://github.com/FrameworkComputer/inputmodule-rs/releases/download/v0.2.0/ledmatrix.uf2 -OutFile releases\0.2.0\ledmatrix.uf2
113+
108114
- name: Create Executable
109115
uses: Martin005/pyinstaller-action@main
110116
with:

python/inputmodule/cli.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
brightness,
1515
get_brightness,
1616
CommandVals,
17-
bootloader,
17+
bootloader_jump,
1818
GameOfLifeStartParam,
1919
GameControlVal,
2020
)
@@ -272,7 +272,7 @@ def main_cli():
272272
sys.exit(1)
273273

274274
if args.bootloader:
275-
bootloader(dev)
275+
bootloader_jump(dev)
276276
elif args.sleep is not None:
277277
send_command(dev, CommandVals.Sleep, [args.sleep])
278278
elif args.is_sleeping:
@@ -394,6 +394,7 @@ def find_devs():
394394
def print_devs(ports):
395395
for port in ports:
396396
print(f"{port.device}")
397+
print(f" {port.name}")
397398
print(f" VID: 0x{port.vid:04X}")
398399
print(f" PID: 0x{port.pid:04X}")
399400
print(f" SN: {port.serial_number}")

python/inputmodule/firmware_update.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import os
2+
import time
3+
4+
from inputmodule.inputmodule import bootloader_jump
5+
from inputmodule import uf2conv
6+
7+
def dev_to_str(dev):
8+
return dev.name
9+
10+
def flash_firmware(dev, fw_path):
11+
print(f"Flashing {fw_path} onto {dev_to_str(dev)}")
12+
13+
# First jump to bootloader
14+
drives = uf2conv.list_drives()
15+
if not drives:
16+
print("Jump to bootloader")
17+
bootloader_jump(dev)
18+
19+
timeout = 10 # 5s
20+
while not drives:
21+
if timeout == 0:
22+
print("Failed to find device in bootloader")
23+
# TODO: Handle return value
24+
return False
25+
# Wait for it to appear
26+
time.sleep(0.5)
27+
timeout -= 1
28+
drives = uf2conv.get_drives()
29+
30+
31+
if len(drives) == 0:
32+
print("No drive to deploy.")
33+
return False
34+
35+
# Firmware is pretty small, can just fit it all into memory
36+
with open(fw_path, 'rb') as f:
37+
fw_buf = f.read()
38+
39+
for d in drives:
40+
print("Flashing {} ({})".format(d, uf2conv.board_id(d)))
41+
uf2conv.write_file(d + "/NEW.UF2", fw_buf)
42+
43+
print("Flashing finished")
44+
45+
# Example return value
46+
# {
47+
# '0.1.7': {
48+
# 'ansi': 'framework_ansi_default_v0.1.7.uf2',
49+
# 'gridpad': 'framework_gridpad_default_v0.1.7.uf2'
50+
# },
51+
# '0.1.8': {
52+
# 'ansi': 'framework_ansi_default.uf2',
53+
# 'gridpad': 'framework_gridpad_default.uf2',
54+
# }
55+
# }
56+
def find_releases(res_path, filename_format):
57+
from os import listdir
58+
from os.path import isfile, join
59+
import re
60+
61+
releases = {}
62+
try:
63+
versions = listdir(os.path.join(res_path, "releases"))
64+
except FileNotFoundError:
65+
return releases
66+
67+
for version in versions:
68+
path = join(res_path, "releases", version)
69+
releases[version] = {}
70+
for filename in listdir(path):
71+
if not isfile(join(path, filename)):
72+
continue
73+
type_search = re.search(filename_format, filename)
74+
if not type_search:
75+
print(f"Filename '{filename}' not matching patten!")
76+
sys.exit(1)
77+
continue
78+
fw_type = type_search.group(1)
79+
releases[version][fw_type] = os.path.join(res_path, "releases", version, filename)
80+
return releases

python/inputmodule/gui/__init__.py

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
import tkinter as tk
66
from tkinter import ttk, messagebox
77

8+
from inputmodule import firmware_update
89
from inputmodule.inputmodule import (
910
send_command,
1011
get_version,
1112
brightness,
1213
get_brightness,
13-
bootloader,
14+
bootloader_jump,
1415
CommandVals,
1516
Game,
1617
GameControlVal
@@ -54,10 +55,12 @@ def run_gui(devices):
5455
tab1 = ttk.Frame(tabControl)
5556
tab_games = ttk.Frame(tabControl)
5657
tab2 = ttk.Frame(tabControl)
58+
tab_fw = ttk.Frame(tabControl)
5759
tab3 = ttk.Frame(tabControl)
5860
tabControl.add(tab1, text="Home")
5961
tabControl.add(tab_games, text="Games")
6062
tabControl.add(tab2, text="Dynamic Controls")
63+
tabControl.add(tab_fw, text="Firmware Update")
6164
tabControl.add(tab3, text="Advanced")
6265
tabControl.pack(expand=1, fill="both")
6366

@@ -75,7 +78,7 @@ def run_gui(devices):
7578
checkbox_var = tk.BooleanVar(value=True)
7679
checkbox = ttk.Checkbutton(detected_devices_frame, text=device_info, variable=checkbox_var, style="TCheckbutton")
7780
checkbox.pack(anchor="w")
78-
device_checkboxes[dev.name] = checkbox_var
81+
device_checkboxes[dev.name] = (checkbox_var, checkbox)
7982

8083
# Brightness Slider
8184
brightness_frame = ttk.LabelFrame(tab1, text="Brightness", style="TLabelframe")
@@ -160,6 +163,26 @@ def run_gui(devices):
160163
symbols_frame.pack(fill="x", padx=10, pady=5)
161164
ttk.Button(symbols_frame, text="Send '2 5 degC thunder'", command=lambda: send_symbols(devices), style="TButton").pack(side="left", padx=5, pady=5)
162165

166+
# Firmware Update
167+
bootloader_frame = ttk.LabelFrame(tab_fw, text="Bootloader", style="TLabelframe")
168+
bootloader_frame.pack(fill="x", padx=10, pady=5)
169+
ttk.Button(bootloader_frame, text="Enter Bootloader", command=lambda: perform_action(devices, "bootloader"), style="TButton").pack(side="left", padx=5, pady=5)
170+
171+
bundled_fw_frame = ttk.LabelFrame(tab_fw, text="Bundled Updates", style="TLabelframe")
172+
bundled_fw_frame.pack(fill="x", padx=10, pady=5)
173+
releases = firmware_update.find_releases(resource_path(), r'(ledmatrix).uf2')
174+
if not releases:
175+
tk.Label(bundled_fw_frame, text="Cannot find firmware updates").pack(side="top", padx=5, pady=5)
176+
else:
177+
versions = sorted(list(releases.keys()), reverse=True)
178+
179+
#tk.Label(fw_update_frame, text="Ignore user configured keymap").pack(side="top", padx=5, pady=5)
180+
fw_ver_combo = ttk.Combobox(bundled_fw_frame, values=versions, style="TCombobox", state="readonly")
181+
fw_ver_combo.pack(side=tk.LEFT, padx=5, pady=5)
182+
fw_ver_combo.current(0)
183+
flash_btn = ttk.Button(bundled_fw_frame, text="Update", command=lambda: tk_flash_firmware(devices, releases, fw_ver_combo.get(), 'ledmatrix'), style="TButton")
184+
flash_btn.pack(side="left", padx=5, pady=5)
185+
163186
# PWM Frequency Combo Box
164187
pwm_freq_frame = ttk.LabelFrame(tab3, text="PWM Frequency", style="TLabelframe")
165188
pwm_freq_frame.pack(fill="x", padx=10, pady=5)
@@ -177,7 +200,6 @@ def run_gui(devices):
177200
device_control_frame = ttk.LabelFrame(tab1, text="Device Control", style="TLabelframe")
178201
device_control_frame.pack(fill="x", padx=10, pady=5)
179202
control_buttons = {
180-
"Bootloader": "bootloader",
181203
"Sleep": "sleep",
182204
"Wake": "wake"
183205
}
@@ -194,8 +216,12 @@ def perform_action(devices, action):
194216
if action in action_map:
195217
threading.Thread(target=action_map[action], args=(devices,), daemon=True).start(),
196218

219+
if action == "bootloader":
220+
disable_devices(devices)
221+
restart_hint()
222+
197223
action_map = {
198-
"bootloader": bootloader,
224+
"bootloader": bootloader_jump,
199225
"sleep": lambda dev: send_command(dev, CommandVals.Sleep, [True]),
200226
"wake": lambda dev: send_command(dev, CommandVals.Sleep, [False]),
201227
"start_animation": lambda dev: animate(dev, True),
@@ -263,14 +289,47 @@ def set_pwm_freq(devices, freq):
263289
pwm_freq(dev, freq)
264290

265291
def get_selected_devices(devices):
266-
return [dev for dev in devices if dev.name in device_checkboxes and device_checkboxes[dev.name].get()]
292+
return [dev for dev in devices if dev.name in device_checkboxes and device_checkboxes[dev.name][0].get()]
267293

268294
def resource_path():
269295
"""Get absolute path to resource, works for dev and for PyInstaller"""
270296
try:
271297
# PyInstaller creates a temp folder and stores path in _MEIPASS
272298
base_path = sys._MEIPASS
273299
except Exception:
274-
base_path = os.path.abspath("../../")
300+
base_path = os.path.abspath(".")
275301

276302
return base_path
303+
304+
def info_popup(msg):
305+
parent = tk.Tk()
306+
parent.title("Info")
307+
message = tk.Message(parent, text=msg, width=800)
308+
message.pack(padx=20, pady=20)
309+
parent.mainloop()
310+
311+
def tk_flash_firmware(devices, releases, version, fw_type):
312+
selected_devices = get_selected_devices(devices)
313+
if len(selected_devices) != 1:
314+
info_popup('To flash select exactly 1 device.')
315+
return
316+
dev = selected_devices[0]
317+
firmware_update.flash_firmware(dev, releases[version][fw_type])
318+
# Disable device that we just flashed
319+
disable_devices(devices)
320+
restart_hint()
321+
322+
def restart_hint():
323+
parent = tk.Tk()
324+
parent.title("Restart Application")
325+
message = tk.Message(parent, text="After updating a device,\n restart the application to reload the connections.", width=800)
326+
message.pack(padx=20, pady=20)
327+
parent.mainloop()
328+
329+
def disable_devices(devices):
330+
# Disable checkbox of selected devices
331+
for dev in devices:
332+
for name, (checkbox_var, checkbox) in device_checkboxes.items():
333+
if name == dev.name:
334+
checkbox_var.set(False)
335+
checkbox.config(state=tk.DISABLED)

python/inputmodule/inputmodule/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class GameControlVal(IntEnum):
9090
RESPONSE_SIZE = 32
9191

9292

93-
def bootloader(dev):
93+
def bootloader_jump(dev):
9494
"""Reboot into the bootloader to flash new firmware"""
9595
send_command(dev, CommandVals.BootloaderReset, [0x00])
9696

0 commit comments

Comments
 (0)