From db5ea4bd769444e97431eef5024a7a7125545c79 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 26 Nov 2024 14:21:30 +0800 Subject: [PATCH] python: Split out Make firmware_update.py same as in FrameworkComputer/ledmatrix-rs Signed-off-by: Daniel Schaefer --- python/qmk_hid/firmware_update.py | 81 +++++++ python/qmk_hid/gui.py | 347 +----------------------------- python/qmk_hid/protocol.py | 265 +++++++++++++++++++++++ 3 files changed, 353 insertions(+), 340 deletions(-) create mode 100644 python/qmk_hid/firmware_update.py create mode 100644 python/qmk_hid/protocol.py diff --git a/python/qmk_hid/firmware_update.py b/python/qmk_hid/firmware_update.py new file mode 100644 index 0000000..92d0bd5 --- /dev/null +++ b/python/qmk_hid/firmware_update.py @@ -0,0 +1,81 @@ +import os +import time + +from qmk_hid.protocol import bootloader_jump +from qmk_hid import uf2conv + +def dev_to_str(dev): + return dev['path'] + +def flash_firmware(dev, fw_path): + print(f"Flashing {fw_path} onto {dev_to_str(dev)}") + + # 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") + + +# 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(res_path, filename_format): + from os import listdir + from os.path import isfile, join + import re + + releases = {} + try: + versions = listdir(os.path.join(res_path, "releases")) + except FileNotFoundError: + return 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(filename_format, 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 diff --git a/python/qmk_hid/gui.py b/python/qmk_hid/gui.py index 082a737..46aa1aa 100644 --- a/python/qmk_hid/gui.py +++ b/python/qmk_hid/gui.py @@ -7,7 +7,6 @@ import tkinter as tk from tkinter import ttk, messagebox -import hid if os.name == 'nt': from win32api import GetKeyState, keybd_event from win32con import VK_NUMLOCK, VK_CAPITAL @@ -15,107 +14,17 @@ import webbrowser -from qmk_hid import uf2conv +from qmk_hid.protocol import * +from qmk_hid import firmware_update # TODO: # - Get current values # - Set sliders to current values PROGRAM_VERSION = "0.2.0" -FWK_VID = 0x32AC DEBUG_PRINT = False -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 debug_print(*args): if DEBUG_PRINT: print(args) @@ -287,7 +196,7 @@ def main(): toggle_btn = ttk.Button(registry_frame, text="Disable Selective Suspend", command=lambda dev: selective_suspend_wrapper(dev, False), style="TButton", state=tk.DISABLED).pack(side="left", padx=5, pady=5) # Only in the pyinstaller bundle are the FW update binaries included - releases = find_releases() + releases = firmware_update.find_releases(resource_path(), r'framework_(.*)_default.*\.uf2') if not releases: tk.Label(tab_fw_update, text="Cannot find firmware updates").pack(side="top", padx=5, pady=5) else: @@ -309,7 +218,7 @@ def main(): flash_btn = ttk.Button(fw_update_frame, text="Update", command=lambda: tk_flash_firmware(devices, releases, fw_ver_combo.get(), fw_type_combo.get()), state=tk.DISABLED, style="TButton") flash_btn.pack(side="left", padx=5, pady=5) - program_ver_label = tk.Label(tab1, text="Program Version: 0.2.0") + program_ver_label = tk.Label(tab1, text=f"Program Version: {PROGRAM_VERSION}") program_ver_label.pack(side=tk.LEFT, padx=5, pady=5) root.mainloop() @@ -391,214 +300,6 @@ def backlight_watcher(window, devs): time.sleep(1) -# 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 - - releases = {} - res_path = resource_path() - try: - versions = listdir(os.path.join(res_path, "releases")) - except FileNotFoundError: - return 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(r'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 - # Lots of false positives, so at least skip Framework false positives - if vid == FWK_VID and pid not in [0x12, 0x13, 0x14, 0x18, 0x19]: - if verbose: - print("False positive, device is not allowed") - 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"Path: {path}") - 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+3) - return out_data - except (IOError, OSError) as ex: - disable_devices([dev]) - debug_print("Error ({}): ".format(dev['path']), ex) - -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) - -# Returns brightness level: x/255 -def get_rgb_u8(dev, value): - msg = [CHANNEL_RGB_MATRIX, value] - output = send_message(dev, CUSTOM_GET_VALUE, msg, 1) - if output[0] == 255: # Not RGB - return None - return output[3] - -# Returns (hue, saturation) -def get_rgb_color(dev): - msg = [CHANNEL_RGB_MATRIX, RGB_MATRIX_VALUE_COLOR] - output = send_message(dev, CUSTOM_GET_VALUE, msg, 2) - return (output[3], output[4]) - -# Returns brightness level: x/255 -def get_backlight(dev, value): - msg = [CHANNEL_BACKLIGHT, value] - output = send_message(dev, CUSTOM_GET_VALUE, msg, 1) - return output[3] - -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 bios_mode(dev, enable): - param = 0x01 if enable else 0x00 - send_message(dev, BOOTLOADER_JUMP, [0x05, param], 0) - - -def factory_mode(dev, enable): - param = 0x01 if enable else 0x00 - send_message(dev, BOOTLOADER_JUMP, [0x06, param], 0) - - -def set_rgb_brightness(dev, brightness): - set_rgb_u8(dev, RGB_MATRIX_VALUE_BRIGHTNESS, brightness) - - -def set_brightness(dev, brightness): - set_backlight(dev, BACKLIGHT_VALUE_BRIGHTNESS, brightness) - -def set_white_effect(dev, breathing_on): - set_backlight(dev, BACKLIGHT_VALUE_EFFECT, breathing_on) - -# Set both -def set_white_rgb_brightness(dev, brightness): - set_brightness(dev, brightness) - set_rgb_brightness(dev, brightness) - - -def set_rgb_color(dev, hue, saturation): - (cur_hue, cur_sat) = get_rgb_color(dev) - if hue is None: - hue = cur_hue - msg = [CHANNEL_RGB_MATRIX, RGB_MATRIX_VALUE_COLOR, hue, saturation] - send_message(dev, CUSTOM_SET_VALUE, msg, 0) - - def restart_hint(): parent = tk.Tk() parent.title("Restart Application") @@ -609,7 +310,7 @@ def restart_hint(): def info_popup(msg): parent = tk.Tk() parent.title("Info") - message = tk.Message(parent, text="msg", width=800) + message = tk.Message(parent, text=msg, width=800) message.pack(padx=20, pady=20) parent.mainloop() @@ -622,40 +323,6 @@ def replug_hint(): parent.mainloop() -def flash_firmware(dev, fw_path): - print(f"Flashing {fw_path} onto {dev['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") def selective_suspend_wrapper(dev, enable): if enable: @@ -740,8 +407,8 @@ def disable_devices(devices): def perform_action(devices, action, value=None): if action == "bootloader": disable_devices(devices) - restart_hint() + if action == "off": brightness_scale.set(0) @@ -790,7 +457,7 @@ def tk_flash_firmware(devices, releases, version, fw_type): info_popup('To flash select exactly 1 device.') return dev = selected_devices[0] - flash_firmware(dev, releases[version][fw_type]) + firmware_update.flash_firmware(dev, releases[version][fw_type]) # Disable device that we just flashed disable_devices(devices) restart_hint() diff --git a/python/qmk_hid/protocol.py b/python/qmk_hid/protocol.py new file mode 100644 index 0000000..3aedb8b --- /dev/null +++ b/python/qmk_hid/protocol.py @@ -0,0 +1,265 @@ +import os +import sys + +import hid + +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 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 + # Lots of false positives, so at least skip Framework false positives + if vid == FWK_VID and pid not in [0x12, 0x13, 0x14, 0x18, 0x19]: + if verbose: + print("False positive, device is not allowed") + 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"Path: {path}") + 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+3) + return out_data + except (IOError, OSError) as ex: + disable_devices([dev]) + debug_print("Error ({}): ".format(dev['path']), ex) + +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) + +# Returns brightness level: x/255 +def get_rgb_u8(dev, value): + msg = [CHANNEL_RGB_MATRIX, value] + output = send_message(dev, CUSTOM_GET_VALUE, msg, 1) + if output[0] == 255: # Not RGB + return None + return output[3] + +# Returns (hue, saturation) +def get_rgb_color(dev): + msg = [CHANNEL_RGB_MATRIX, RGB_MATRIX_VALUE_COLOR] + output = send_message(dev, CUSTOM_GET_VALUE, msg, 2) + return (output[3], output[4]) + +# Returns brightness level: x/255 +def get_backlight(dev, value): + msg = [CHANNEL_BACKLIGHT, value] + output = send_message(dev, CUSTOM_GET_VALUE, msg, 1) + return output[3] + +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 bios_mode(dev, enable): + param = 0x01 if enable else 0x00 + send_message(dev, BOOTLOADER_JUMP, [0x05, param], 0) + + +def factory_mode(dev, enable): + param = 0x01 if enable else 0x00 + send_message(dev, BOOTLOADER_JUMP, [0x06, param], 0) + + +def set_rgb_brightness(dev, brightness): + set_rgb_u8(dev, RGB_MATRIX_VALUE_BRIGHTNESS, brightness) + + +def set_brightness(dev, brightness): + set_backlight(dev, BACKLIGHT_VALUE_BRIGHTNESS, brightness) + +def set_white_effect(dev, breathing_on): + set_backlight(dev, BACKLIGHT_VALUE_EFFECT, breathing_on) + +# Set both +def set_white_rgb_brightness(dev, brightness): + set_brightness(dev, brightness) + set_rgb_brightness(dev, brightness) + + +def set_rgb_color(dev, hue, saturation): + (cur_hue, cur_sat) = get_rgb_color(dev) + if hue is None: + hue = cur_hue + msg = [CHANNEL_RGB_MATRIX, RGB_MATRIX_VALUE_COLOR, hue, saturation] + send_message(dev, CUSTOM_SET_VALUE, msg, 0) +