diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dcf8f5..dada644 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,3 +107,46 @@ jobs: - name: Run cargo clippy run: cargo clippy -- -D warnings + + build-gui: + name: Build GUI + runs-on: windows-2022 + steps: + - uses: actions/checkout@v3 + + - name: Download releases to bundle + run: | + mkdir releases + mkdir releases\0.1.9 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_ansi_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_ansi_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_iso_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_iso_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_jis_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_jis_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_numpad_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_numpad_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.9/framework_macropad_default_v0.1.9.uf2 -OutFile releases\0.1.9\framework_macropad_default.uf2 + mkdir releases\0.1.8 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_ansi_default.uf2 -OutFile releases\0.1.8\framework_ansi_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_iso_default.uf2 -OutFile releases\0.1.8\framework_iso_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_jis_default.uf2 -OutFile releases\0.1.8\framework_jis_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_numpad_default.uf2 -OutFile releases\0.1.8\framework_numpad_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.8/framework_gridpad_default.uf2 -OutFile releases\0.1.8\framework_gridpad_default.uf2 + mkdir releases\0.1.7 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_ansi_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_ansi_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_iso_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_iso_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_jis_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_jis_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_numpad_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_numpad_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.7/framework_gridpad_default_v0.1.7.uf2 -OutFile releases\0.1.7\framework_gridpad_default.uf2 + mkdir releases\0.1.6 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_ansi_default.uf2 -OutFile releases\0.1.6\framework_ansi_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_iso_default.uf2 -OutFile releases\0.1.6\framework_iso_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_jis_default.uf2 -OutFile releases\0.1.6\framework_jis_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_numpad_default.uf2 -OutFile releases\0.1.6\framework_numpad_default.uf2 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/qmk_firmware/releases/download/v0.1.6/framework_gridpad_default.uf2 -OutFile releases\0.1.6\framework_gridpad_default.uf2 + + - name: Create Executable + uses: JohnAZoidberg/pyinstaller-action@dont-clean + with: + python_ver: '3.11' + spec: qmk_gui.py + requirements: 'requirements.txt' + upload_exe_with_name: 'qmk_gui.exe' + options: --onefile, --name "qmk_gui", --windowed, --add-data "releases;releases" diff --git a/.gitignore b/.gitignore index 2f7896d..b699590 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ target/ + +venv/ + +# pyinstaller +qmk_gui.spec +build/ +dist/ diff --git a/README.md b/README.md index 59f7e4b..398487c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ It will soon be superceded by QMK XAP, but that isn't ready yet. Tested to work on Windows and Linux, without any drivers or admin privileges. +###### GUI + +There is also an easy to use GUI tool that does not require commandline interaction. +On Linux install Python requirements via `python3 -m install -r requirements.txt` and run `qmk_gui.py`. +On Windows download the `qmk_gui.exe` and run it. + ## Running Download the latest binary from the [releases page](https://github.com/FrameworkComputer/qmk_hid/releases). diff --git a/qmk_gui.py b/qmk_gui.py new file mode 100755 index 0000000..69e7f5e --- /dev/null +++ b/qmk_gui.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python3 +import os +import sys +import time + +import PySimpleGUI as sg +import hid + +import uf2conv + +# TODO: +# - Clear EEPROM +# - Save settings +# - Get current values +# - Set sliders to current values +# - Show connected devices +# - Get firmware version + +FWK_VID = 0x32AC + +QMK_INTERFACE = 0x01 +RAW_HID_BUFFER_SIZE = 32 + +RAW_USAGE_PAGE = 0xFF60 +CONSOLE_USAGE_PAGE = 0xFF31 +# Generic Desktop +G_DESK_USAGE_PAGE = 0x01 +CONSUMER_USAGE_PAGE = 0x0C + +GET_PROTOCOL_VERSION = 0x01 # always 0x01 +GET_KEYBOARD_VALUE = 0x02 +SET_KEYBOARD_VALUE = 0x03 +# DynamicKeymapGetKeycode = 0x04 +# DynamicKeymapSetKeycode = 0x05 +# DynamicKeymapReset = 0x06 +CUSTOM_SET_VALUE = 0x07 +CUSTOM_GET_VALUE = 0x08 +CUSTOM_SAVE = 0x09 +EEPROM_RESET = 0x0A +BOOTLOADER_JUMP = 0x0B + +CHANNEL_CUSTOM = 0 +CHANNEL_BACKLIGHT = 1 +CHANNEL_RGB_LIGHT = 2 +CHANNEL_RGB_MATRIX = 3 +CHANNEL_AUDIO = 4 + +BACKLIGHT_VALUE_BRIGHTNESS = 1 +BACKLIGHT_VALUE_EFFECT = 2 + +RGB_MATRIX_VALUE_BRIGHTNESS = 1 +RGB_MATRIX_VALUE_EFFECT = 2 +RGB_MATRIX_VALUE_EFFECT_SPEED = 3 +RGB_MATRIX_VALUE_COLOR = 4 + +RED_HUE = 0 +YELLOW_HUE = 43 +GREEN_HUE = 85 +CYAN_HUE = 125 +BLUE_HUE = 170 +PURPLE_HUE = 213 + +RGB_EFFECTS = [ + "Off", + "SOLID_COLOR", + "ALPHAS_MODS", + "GRADIENT_UP_DOWN", + "GRADIENT_LEFT_RIGHT", + "BREATHING", + "BAND_SAT", + "BAND_VAL", + "BAND_PINWHEEL_SAT", + "BAND_PINWHEEL_VAL", + "BAND_SPIRAL_SAT", + "BAND_SPIRAL_VAL", + "CYCLE_ALL", + "CYCLE_LEFT_RIGHT", + "CYCLE_UP_DOWN", + "CYCLE_OUT_IN", + "CYCLE_OUT_IN_DUAL", + "RAINBOW_MOVING_CHEVRON", + "CYCLE_PINWHEEL", + "CYCLE_SPIRAL", + "DUAL_BEACON", + "RAINBOW_BEACON", + "RAINBOW_PINWHEELS", + "RAINDROPS", + "JELLYBEAN_RAINDROPS", + "HUE_BREATHING", + "HUE_PENDULUM", + "HUE_WAVE", + "PIXEL_FRACTAL", + "PIXEL_FLOW", + "PIXEL_RAIN", + "TYPING_HEATMAP", + "DIGITAL_RAIN", + "SOLID_REACTIVE_SIMPLE", + "SOLID_REACTIVE", + "SOLID_REACTIVE_WIDE", + "SOLID_REACTIVE_MULTIWIDE", + "SOLID_REACTIVE_CROSS", + "SOLID_REACTIVE_MULTICROSS", + "SOLID_REACTIVE_NEXUS", + "SOLID_REACTIVE_MULTINEXUS", + "SPLASH", + "MULTISPLASH", + "SOLID_SPLASH", + "SOLID_MULTISPLASH", +] + +def format_fw_ver(fw_ver): + fw_ver_major = (fw_ver & 0xFF00) >> 8 + fw_ver_minor = (fw_ver & 0x00F0) >> 4 + fw_ver_patch = (fw_ver & 0x000F) + return f"{fw_ver_major}.{fw_ver_minor}.{fw_ver_patch}" + +def main(devices): + device_checkboxes = [] + for dev in devices: + device_info = "{}\nSerial No: {}\nFW Version: {}\n".format( + dev['product_string'], + dev['serial_number'], + format_fw_ver(dev['release_number']) + ) + checkbox = sg.Checkbox(device_info, default=True, key='-CHECKBOX-{}-'.format(dev['path']), enable_events=True) + device_checkboxes.append([checkbox]) + + releases = find_releases() + versions = sorted(list(releases.keys()), reverse=True) + + # Only in the pyinstaller bundle are the FW update binaries included + if is_pyinstaller(): + bundled_update = [ + [sg.Text("Update Version")], + [sg.Text("Version"), sg.Push(), sg.Combo(versions, k='-VERSION-', enable_events=True, default_value=versions[0])], + [sg.Text("Type"), sg.Push(), sg.Combo(list(releases[versions[0]]), k='-TYPE-', enable_events=True)], + [sg.Text("Make sure the firmware is compatible with\nALL selected devices!")], + [sg.Button("Flash", k='-FLASH-', disabled=True)], + [sg.HorizontalSeparator()], + ] + else: + bundled_update = [] + + + layout = [ + [sg.Text("Detected Devices")], + ] + device_checkboxes + [ + [sg.HorizontalSeparator()], + + [sg.Text("Bootloader")], + [sg.Button("Bootloader", k='-BOOTLOADER-')], + [sg.HorizontalSeparator()], + ] + bundled_update + [ + [sg.Text("Single-Zone Brightness")], + # TODO: Get default from device + [sg.Slider((0, 255), orientation='h', default_value=120, + k='-BRIGHTNESS-', enable_events=True)], + #[sg.Button("Enable Breathing", k='-ENABLE-BREATHING-')], + #[sg.Button("Disable Breathing", k='-DISABLE-BREATHING-')], + + [sg.Text("RGB Brightness")], + # TODO: Get default from device + [sg.Slider((0, 255), orientation='h', default_value=120, + k='-RGB-BRIGHTNESS-', enable_events=True)], + + [sg.Text("RGB Color")], + [ + sg.Button("Red", k='-RED-'), + sg.Button("Green", k='-GREEN-'), + sg.Button("Blue", k='-BLUE-'), + sg.Button("White", k='-WHITE-'), + sg.Button("Off", k='-OFF-'), + ], + + [sg.Text("RGB Effect")], + [sg.Combo(RGB_EFFECTS, k='-RGB-EFFECT-', enable_events=True)], + + [sg.HorizontalSeparator()], + [sg.Text("Save Settings")], + [sg.Button("Save", k='-SAVE-'), sg.Button("Clear EEPROM", k='-CLEAR-EEPROM-')], + ] + window = sg.Window("QMK Keyboard Control", layout) + + selected_devices = [] + + while True: + event, values = window.read() + #print('Event', event) + #print('Values', values) + + selected_devices = [ + dev for dev in devices if + values and values['-CHECKBOX-{}-'.format(dev['path'])] + ] + # print("Selected {} devices".format(len(selected_devices))) + + # Updating firmware + if event == "-FLASH-" and len(selected_devices) != 1: + sg.Popup('To flash select exactly 1 device.') + continue + if event == "-VERSION-": + # After selecting a version, we can list the types of firmware available for this version + types = list(releases[values['-VERSION-']]) + window['-TYPE-'].update(value=types[0], values=types) + if event == "-TYPE-": + # Once the user has selected a type, the exact firmware file is known and can be flashed + window['-FLASH-'].update(disabled=False) + if event == "-FLASH-": + ver = values['-VERSION-'] + t = values['-TYPE-'] + # print("Flashing", releases[ver][t]) + flash_firmware(dev, releases[ver][t]) + restart_hint() + window['-CHECKBOX-{}-'.format(dev['path'])].update(False, disabled=True) + + # Run commands on all selected devices + for dev in selected_devices: + if event == "-BOOTLOADER-": + bootloader_jump(dev) + window['-CHECKBOX-{}-'.format(dev['path'])].update(False, disabled=True) + restart_hint() + + if event == '-BRIGHTNESS-': + brightness(dev, int(values['-BRIGHTNESS-'])) + + if event == '-RGB-BRIGHTNESS-': + rgb_brightness(dev, int(values['-RGB-BRIGHTNESS-'])) + + if event == '-RGB-EFFECT-': + effect = RGB_EFFECTS.index(values['-RGB-EFFECT-']) + set_rgb_u8(dev, RGB_MATRIX_VALUE_EFFECT, effect) + # TODO: Get effect + + if event == '-RED-': + set_rgb_color(dev, RED_HUE, 255) + if event == '-GREEN-': + set_rgb_color(dev, GREEN_HUE, 255) + if event == '-BLUE-': + set_rgb_color(dev, BLUE_HUE, 255) + if event == '-WHITE-': + set_rgb_color(dev, None, 0) + if event == '-OFF-': + window['-RGB-BRIGHTNESS-'].Update(0) + rgb_brightness(dev, 0) + + if event == '-SAVE-': + save(dev) + + if event == '-CLEAR-EEPROM-': + eeprom_reset(dev) + + if event == sg.WIN_CLOSED: + break + + window.close() + + +def is_pyinstaller(): + return getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') + + +def resource_path(): + """ Get absolute path to resource, works for dev and for PyInstaller""" + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return base_path + + +# Example return value +# { +# '0.1.7': { +# 'ansi': 'framework_ansi_default_v0.1.7.uf2', +# 'gridpad': 'framework_gridpad_default_v0.1.7.uf2' +# }, +# '0.1.8': { +# 'ansi': 'framework_ansi_default.uf2', +# 'gridpad': 'framework_gridpad_default.uf2', +# } +# } +def find_releases(): + from os import listdir + from os.path import isfile, join + import re + + res_path = resource_path() + versions = listdir(os.path.join(res_path, "releases")) + releases = {} + for version in versions: + path = join(res_path, "releases", version) + releases[version] = {} + for filename in listdir(path): + if not isfile(join(path, filename)): + continue + type_search = re.search('framework_(.*)_default.*\.uf2', filename) + if not type_search: + print(f"Filename '{filename}' not matching patten!") + sys.exit(1) + continue + fw_type = type_search.group(1) + releases[version][fw_type] = os.path.join(res_path, "releases", version, filename) + return releases + + +def find_devs(show, verbose): + if verbose: + show = True + + devices = [] + for device_dict in hid.enumerate(): + vid = device_dict["vendor_id"] + pid = device_dict["product_id"] + product = device_dict["product_string"] + manufacturer = device_dict["manufacturer_string"] + sn = device_dict['serial_number'] + interface = device_dict['interface_number'] + path = device_dict['path'] + + if vid != FWK_VID: + if verbose: + print("Vendor ID not matching") + continue + + if interface != QMK_INTERFACE: + if verbose: + print("Interface not matching") + continue + # For some reason on Linux it'll always show usage_page==0 + if os.name == 'nt' and device_dict['usage_page'] not in [RAW_USAGE_PAGE, CONSOLE_USAGE_PAGE]: + if verbose: + print("Usage Page not matching") + continue + + fw_ver = device_dict["release_number"] + + if (os.name == 'nt' and device_dict['usage_page'] == RAW_USAGE_PAGE) or verbose: + if show: + print(f"Manufacturer: {manufacturer}") + print(f"Product: {product}") + print("FW Version: {}".format(format_fw_ver(fw_ver))) + print(f"Serial No: {sn}") + + if verbose: + print(f"VID/PID: {vid:02X}:{pid:02X}") + print(f"Interface: {interface}") + # TODO: print Usage Page + print("") + + devices.append(device_dict) + + return devices + + +def send_message(dev, message_id, msg, out_len): + data = [0xFE] * RAW_HID_BUFFER_SIZE + data[0] = 0x00 # NULL report ID + data[1] = message_id + + if msg: + if len(msg) > RAW_HID_BUFFER_SIZE-2: + print("Message too big. BUG. Please report") + sys.exit(1) + for i, x in enumerate(msg): + data[2+i] = x + + try: + # TODO: Do this somewhere outside + h = hid.device() + h.open_path(dev['path']) + #h.set_nonblocking(0) + h.write(data) + + if out_len == 0: + return None + + out_data = h.read(out_len) + return out_data + except IOError as ex: + print(ex) + sys.exit(1) + +def set_keyboard_value(dev, value, number): + msg = [value, number] + send_message(dev, SET_KEYBOARD_VALUE, msg, 0) + +def set_rgb_u8(dev, value, value_data): + msg = [CHANNEL_RGB_MATRIX, value, value_data] + send_message(dev, CUSTOM_SET_VALUE, msg, 0) + +def get_rgb_u8(dev, value): + msg = [CHANNEL_RGB_MATRIX, value] + output = send_message(dev, CUSTOM_SET_VALUE, msg, 3) + print("output", output) + return output[2] + +def get_backlight(dev, value, value_data): + msg = [CHANNEL_BACKLIGHT, value] + output = send_message(dev, CUSTOM_SET_VALUE, msg, 3) + print(output[2]) + return output[2] + +def set_backlight(dev, value, value_data): + msg = [CHANNEL_BACKLIGHT, value, value_data] + send_message(dev, CUSTOM_SET_VALUE, msg, 0) + +def save(dev): + save_rgb(dev) + save_backlight(dev) + +def save_rgb(dev): + msg = [CHANNEL_RGB_MATRIX] + send_message(dev, CUSTOM_SAVE, msg, 0) + +def save_backlight(dev): + msg = [CHANNEL_BACKLIGHT] + send_message(dev, CUSTOM_SAVE, msg, 0) + +def eeprom_reset(dev): + send_message(dev, EEPROM_RESET, None, 0) + + +def bootloader_jump(dev): + send_message(dev, BOOTLOADER_JUMP, None, 0) + + +def rgb_brightness(dev, brightness): + set_rgb_u8(dev, RGB_MATRIX_VALUE_BRIGHTNESS, brightness) + #brightness = get_rgb_u8(dev, RGB_MATRIX_VALUE_BRIGHTNESS) + #print(f"New Brightness: {brightness}") + + +def brightness(dev, brightness): + set_backlight(dev, BACKLIGHT_VALUE_BRIGHTNESS, brightness) + #brightness = get_backlight(dev, BACKLIGHT_VALUE_BRIGHTNESS) + #print(f"New Brightness: {brightness}") + + +def set_rgb_color(dev, hue, saturation): + if not hue: + # TODO: Just choose current hue + hue = 0 + msg = [CHANNEL_RGB_MATRIX, RGB_MATRIX_VALUE_COLOR, hue, saturation] + send_message(dev, CUSTOM_SET_VALUE, msg, 0) + + +def restart_hint(): + sg.Popup('After updating a device, \nrestart the application\nto reload the connections.') + + +def flash_firmware(dev, fw_path): + print(f"Flashing {fw_path}") + + # First jump to bootloader + drives = uf2conv.list_drives() + if not drives: + print("jump to bootloader") + bootloader_jump(dev) + + timeout = 10 # 5s + while not drives: + if timeout == 0: + print("Failed to find device in bootloader") + # TODO: Handle return value + return False + # Wait for it to appear + time.sleep(0.5) + timeout -= 1 + drives = uf2conv.get_drives() + + + if len(drives) == 0: + print("No drive to deploy.") + return False + + # Firmware is pretty small, can just fit it all into memory + with open(fw_path, 'rb') as f: + fw_buf = f.read() + + for d in drives: + print("Flashing {} ({})".format(d, uf2conv.board_id(d))) + uf2conv.write_file(d + "/NEW.UF2", fw_buf) + + print("Flashing finished") + + +if __name__ == "__main__": + devices = find_devs(show=False, verbose=False) + print("Found {} devices".format(len(devices))) + + main(devices) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c8e4248 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +hidapi==0.14.0 +PySimpleGUI==4.60.5 diff --git a/uf2conv.py b/uf2conv.py new file mode 100644 index 0000000..5c8fbc5 --- /dev/null +++ b/uf2conv.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +# yapf: disable +import sys +import struct +import subprocess +import re +import os +import os.path +import argparse +import json + +# Don't even need -b. hex has this embedded +# > ./util/uf2conv.py .build/framework_ansi_default.hex -o ansi.uf2 -b 0x10000000 -f rp2040 --convert --blocks-reserved 1 +# Converted to 222 blocks +# Converted to uf2, output size: 113664, start address: 0x10000000 +# Wrote 113664 bytes to ansi.uf2 +# # 113664 / 512 = 222 +# +# > ./util/uf2conv.py serial.bin -o serial.uf2 -b 0x100ff000 -f rp2040 --convert --blocks-offset 222 +# Converted to 1 blocks +# Converted to uf2, output size: 512, start address: 0x100ff000 +# Wrote 512 bytes to serial.uf2 + + + +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto + +INFO_FILE = "/INFO_UF2.TXT" + +appstartaddr = 0x2000 +familyid = 0x0 + + +def is_uf2(buf): + w = struct.unpack(" 476: + assert False, "Invalid UF2 data size at " + ptr + newaddr = hd[3] + if (hd[2] & 0x2000) and (currfamilyid == None): + currfamilyid = hd[7] + if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid): + currfamilyid = hd[7] + curraddr = newaddr + if familyid == 0x0 or familyid == hd[7]: + appstartaddr = newaddr + print(f" flags: 0x{hd[2]:02x}") + print(f" addr: 0x{hd[3]:02x}") + print(f" len: {hd[4]}") + print(f" block no: {hd[5]}") + print(f" blocks: {hd[6]}") + print(f" size/famid: {hd[7]}") + print() + padding = newaddr - curraddr + if padding < 0: + assert False, "Block out of order at " + ptr + if padding > 10*1024*1024: + assert False, "More than 10M of padding needed at " + ptr + if padding % 4 != 0: + assert False, "Non-word padding size at " + ptr + while padding > 0: + padding -= 4 + outp.append(b"\x00\x00\x00\x00") + if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]): + outp.append(block[32 : 32 + datalen]) + curraddr = newaddr + datalen + if hd[2] & 0x2000: + if hd[7] in families_found.keys(): + if families_found[hd[7]] > newaddr: + families_found[hd[7]] = newaddr + else: + families_found[hd[7]] = newaddr + if prev_flag == None: + prev_flag = hd[2] + if prev_flag != hd[2]: + all_flags_same = False + if blockno == (numblocks - 1): + print("--- UF2 File Header Info ---") + families = load_families() + for family_hex in families_found.keys(): + family_short_name = "" + for name, value in families.items(): + if value == family_hex: + family_short_name = name + print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex)) + print("Target Address is 0x{:08x}".format(families_found[family_hex])) + if all_flags_same: + print("All block flag values consistent, 0x{:04x}".format(hd[2])) + else: + print("Flags were not all the same") + print("----------------------------") + if len(families_found) > 1 and familyid == 0x0: + outp = [] + appstartaddr = 0x0 + return b"".join(outp) + +def convert_to_carray(file_content): + outp = "const unsigned long bindata_len = %d;\n" % len(file_content) + outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {" + for i in range(len(file_content)): + if i % 16 == 0: + outp += "\n" + outp += "0x%02x, " % file_content[i] + outp += "\n};\n" + return bytes(outp, "utf-8") + +def convert_to_uf2(file_content, blocks_reserved=0, blocks_offset=0): + global familyid + datapadding = b"" + while len(datapadding) < 512 - 256 - 32 - 4: + datapadding += b"\x00\x00\x00\x00" + numblocks = (len(file_content) + 255) // 256 + outp = [] + for blockno in range(numblocks): + ptr = 256 * blockno + chunk = file_content[ptr:ptr + 256] + flags = 0x0 + if familyid: + flags |= 0x2000 + hd = struct.pack(b"= 3 and words[1] == "2" and words[2] == "FAT": + drives.append(words[0]) + else: + rootpath = "/media" + if sys.platform == "darwin": + rootpath = "/Volumes" + elif sys.platform == "linux": + tmp = rootpath + "/" + os.environ["USER"] + if os.path.isdir(tmp): + rootpath = tmp + tmp = "/run" + rootpath + "/" + os.environ["USER"] + if os.path.isdir(tmp): + rootpath = tmp + for d in os.listdir(rootpath): + drives.append(os.path.join(rootpath, d)) + + + def has_info(d): + try: + return os.path.isfile(d + INFO_FILE) + except: + return False + + return list(filter(has_info, drives)) + + +def board_id(path): + with open(path + INFO_FILE, mode='r') as file: + file_content = file.read() + return re.search("Board-ID: ([^\r\n]*)", file_content).group(1) + + +def list_drives(): + for d in get_drives(): + print(d, board_id(d)) + + +def write_file(name, buf): + with open(name, "wb") as f: + f.write(buf) + print("Wrote %d bytes to %s" % (len(buf), name)) + + +def load_families(): + # The expectation is that the `uf2families.json` file is in the same + # directory as this script. Make a path that works using `__file__` + # which contains the full path to this script. + filename = "uf2families.json" + pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) + with open(pathname) as f: + raw_families = json.load(f) + + families = {} + for family in raw_families: + families[family["short_name"]] = int(family["id"], 0) + + return families + + +def main(): + global appstartaddr, familyid + def error(msg): + print(msg, file=sys.stderr) + sys.exit(1) + parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.') + parser.add_argument('input', metavar='INPUT', type=str, nargs='?', + help='input file (HEX, BIN or UF2)') + parser.add_argument('-b' , '--base', dest='base', type=str, + default="0x2000", + help='set base address of application for BIN format (default: 0x2000)') + parser.add_argument('-o' , '--output', metavar="FILE", dest='output', type=str, + help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible') + parser.add_argument('-d' , '--device', dest="device_path", + help='select a device path to flash') + parser.add_argument('-l' , '--list', action='store_true', + help='list connected devices') + parser.add_argument('-c' , '--convert', action='store_true', + help='do not flash, just convert') + parser.add_argument('-D' , '--deploy', action='store_true', + help='just flash, do not convert') + parser.add_argument('-f' , '--family', dest='family', type=str, + default="0x0", + help='specify familyID - number or name (default: 0x0)') + parser.add_argument('--blocks-offset', dest='blocks_offset', type=str, + default="0x0", + help='TODO') + parser.add_argument('--blocks-reserved', dest='blocks_reserved', type=str, + default="0x0", + help='TODO') + parser.add_argument('-C' , '--carray', action='store_true', + help='convert binary file to a C array, not UF2') + parser.add_argument('-i', '--info', action='store_true', + help='display header information from UF2, do not convert') + args = parser.parse_args() + appstartaddr = int(args.base, 0) + blocks_offset = int(args.blocks_offset, 0) + blocks_reserved = int(args.blocks_reserved, 0) + + families = load_families() + + if args.family.upper() in families: + familyid = families[args.family.upper()] + else: + try: + familyid = int(args.family, 0) + except ValueError: + error("Family ID needs to be a number or one of: " + ", ".join(families.keys())) + + if args.list: + list_drives() + else: + if not args.input: + error("Need input file") + with open(args.input, mode='rb') as f: + inpbuf = f.read() + from_uf2 = is_uf2(inpbuf) + ext = "uf2" + if args.deploy: + outbuf = inpbuf + elif from_uf2 and not args.info: + outbuf = convert_from_uf2(inpbuf) + ext = "bin" + elif from_uf2 and args.info: + outbuf = "" + convert_from_uf2(inpbuf) + + elif is_hex(inpbuf): + outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"), blocks_reserved, blocks_offset) + elif args.carray: + outbuf = convert_to_carray(inpbuf) + ext = "h" + else: + outbuf = convert_to_uf2(inpbuf, blocks_reserved, blocks_offset) + if not args.deploy and not args.info: + print("Converted to %s, output size: %d, start address: 0x%x" % + (ext, len(outbuf), appstartaddr)) + if args.convert or ext != "uf2": + drives = [] + if args.output == None: + args.output = "flash." + ext + else: + drives = get_drives() + + if args.output: + write_file(args.output, outbuf) + else: + if len(drives) == 0: + error("No drive to deploy.") + if outbuf: + for d in drives: + print("Flashing %s (%s)" % (d, board_id(d))) + write_file(d + "/NEW.UF2", outbuf) + + +if __name__ == "__main__": + main()