From e185a29e864bdda952b336940b047b5f97419d46 Mon Sep 17 00:00:00 2001 From: Robin Lange Date: Mon, 28 Oct 2019 18:51:54 +1100 Subject: [PATCH] Reverted the effects protocol https://github.com/Askannz/msi-perkeyrgb/issues/24 There is a critical issue that LOCKS the RGB controller in an unusable state, which persists after reboot. At least 2 machines are affected, including mine. Still trying to figure out a solution to reset the controller and make it usable again, bit in the meantime I'm reverting all the new features that may have caused this. --- README.md | 39 ++--- examples/breathe.cfg | 7 - examples/multi_effects.cfg | 22 --- msi_perkeyrgb/config.py | 267 ++-------------------------------- msi_perkeyrgb/main.py | 38 +---- msi_perkeyrgb/msi_keyboard.py | 41 ++---- msi_perkeyrgb/msiprotocol.py | 139 +----------------- setup.py | 2 +- 8 files changed, 50 insertions(+), 505 deletions(-) delete mode 100644 examples/breathe.cfg delete mode 100644 examples/multi_effects.cfg diff --git a/README.md b/README.md index a1cec31..8f3e496 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,9 @@ After installation, you must reboot your computer (necessary for the udev rule t Features ---------- -Keyboard wide setup from the command line supports `steady` and `breathe` modes. +Keys can be assigned a fixed color ("steady" mode), either through a configuration file for each individual key, or via a command-line argument for the whole keyboard, -Per-key configuration supports most if not all supported custom effects, including colorshift effects and `reactive` keys through a configuration file. - -Presets are available for supported models, which emulate vendor-provided SteelSeries configurations. +A few select presets are also available for supported models, which emulate vendor-provided SteelSeries configurations. Compatibility @@ -37,17 +35,17 @@ Compatibility This tool should probably work on any recent MSI laptop with a per-key RGB keyboard. It was tested with the following models : -| Model | Basic color support | Effects support -| ---- | ------------------- | -------------- -| GE63 | Yes | Yes -| GE73 | Yes | Untested -| GE75 | Yes | Untested -| GL63 | Yes | Untested -| GS63 | Yes | Untested -| GS65 | Yes | Yes -| GS75 | Yes | Untested -| GT63 | Yes | Untested -| GT75 | Yes | Untested +| Model | Basic color support +| ---- | ------------------- +| GE63 | Yes +| GE73 | Yes +| GE75 | Yes +| GL63 | Yes +| GS63 | Yes +| GS65 | Yes +| GS75 | Yes +| GT63 | Yes +| GT75 | Yes If you have some additional test results, feel free to open a GitHub issue to help expand this list ! @@ -83,11 +81,6 @@ Steady color : msi-perkeyrgb --model -s ``` -"Breathe" color : -``` -msi-perkeyrgb --model -b -``` - Built-in preset (see `--list-presets` for available options) : ``` msi-perkeyrgb --model -p @@ -99,9 +92,7 @@ Set from configuration file : ``` msi-perkeyrgb --model -c ``` -This is by far the most flexible and powerful way to use this tool, since the configuration file allows you to set individual key configurations and supports a wide range of lighting effects. - -The configuration file can have any extension. See the [dedicated wiki page](https://github.com/Askannz/msi-perkeyrgb/wiki/Configuration-file-guide) for its syntax and examples. +The configuration file allows you to set individual key configurations. It can have any extension. See the [dedicated wiki page](https://github.com/Askannz/msi-perkeyrgb/wiki/Configuration-file-guide) for its syntax and examples. How does it work, and credits @@ -111,7 +102,7 @@ The SteelSeries keyboard is connected to the MSI laptop by two independent inter * A PS/2 interface to send keypresses * a USB HID-compliant interface to receive RGB commands -Talking to the RGB controller from Linux is a matter of sending the correct binary packets on the USB HID interface. I used Wireshark to capture the traffic between the SteelSeries Engine on Windows and the keyboard, and then analyzed the captured data to figure out the protocol used. I was only able to reverse-engineer the simple "steady color" commands, but that work was massively improved upon by [TauAkiou](https://github.com/TauAkiou), who figured out the rest of the protocol and implemented the remaining effects. His work include an amazingly detailed write-up of the protocol which you can read [here](documentation/0b_packet_information/msi-kb-effectdoc). +Talking to the RGB controller from Linux is a matter of sending the correct binary packets on the USB HID interface. I used Wireshark to capture the traffic between the SteelSeries Engine on Windows and the keyboard, and then analyzed the captured data to figure out the protocol used. I was only able to reverse-engineer the simple "steady color" commands, but that work was massively improved upon by [TauAkiou](https://github.com/TauAkiou), who figured out the rest of the protocol and implemented the remaining effects (UPDATE: effects support been disabled for now, for security reasons. See https://github.com/Askannz/msi-perkeyrgb/issues/24 ). His work include an amazingly detailed write-up of the protocol which you can read [here](documentation/0b_packet_information/msi-kb-effectdoc). Also thanks to [tbh1](https://github.com/tbh1) for providing packet dumps of presets effects. diff --git a/examples/breathe.cfg b/examples/breathe.cfg deleted file mode 100644 index 7862ac4..0000000 --- a/examples/breathe.cfg +++ /dev/null @@ -1,7 +0,0 @@ -all effect breathe - -effect breathe - start ff0000 - trans 000000 500 - trans ff0000 750 -end diff --git a/examples/multi_effects.cfg b/examples/multi_effects.cfg deleted file mode 100644 index 5129de9..0000000 --- a/examples/multi_effects.cfg +++ /dev/null @@ -1,22 +0,0 @@ -all steady 000000 -all effect shift1 -23-35 effect shift2 - - -effect shift1 - start 0000ff - trans 000000 200 - trans 00ff00 150 - trans 000000 200 - trans ff0000 150 - trans 000000 250 - wave 40 60 xy 5 out -end - -effect shift2 - start 0000ff - trans 00ff00 500 - trans ff0000 500 - trans 0000ff 750 -end - diff --git a/msi_perkeyrgb/config.py b/msi_perkeyrgb/config.py index 1e9d02e..7cba72b 100644 --- a/msi_perkeyrgb/config.py +++ b/msi_perkeyrgb/config.py @@ -1,7 +1,6 @@ import re import sys -PRESET_COLORS = {"off": [0x00, 0x00, 0x00]} ALIAS_ALL = "all" ALIASES = {ALIAS_ALL: "9-133,fn", "f_row": "67-76,95,96", @@ -11,37 +10,6 @@ "characters": "24-35,38-48,52-61,65"} -class MsiEffect: - - def __init__(self, ident): - self.start_color = [0x00, 0x00, 0x00] - self.transition_list = [] - self.period = 0 - self.identifier = ident - self.wave_mode = False - self.wave_origin_x = None - self.wave_origin_y = None - self.wave_rad_control = None - self.wave_wavelength = None - self.wave_direction = None - - -class Transition: - - def __init__(self, color, duration): - self.color = color - self.duration = duration - - -class KeyBlock: - def __init__(self, color=PRESET_COLORS["off"], effect="", mode=-1): - self.mode = mode - self.effect_name = effect - self.color = color - self.react_color = PRESET_COLORS["off"] - self.react_duration = 0 - - class ConfigError(Exception): pass @@ -64,7 +32,7 @@ def load_config(config_path, msi_keymap): f = sys.stdin else: f = open(config_path, "r") - config_map, effect_map, warnings = parse_config(f, msi_keymap) + config_map = parse_config(f, msi_keymap) f.close() except FileNotFoundError as e: raise ConfigError("File %s does not exist" % config_path) from e @@ -77,7 +45,7 @@ def load_config(config_path, msi_keymap): except Exception as e: raise ConfigError("Unknown error : %s" % str(e)) from e else: - return config_map, effect_map, warnings + return config_map def load_steady(color, msi_keymap): @@ -92,189 +60,46 @@ def load_steady(color, msi_keymap): except LineParseError as e: raise ConfigParseError("Color parse error %s" % str(e)) from e else: - colors_map = update_colors_map(colors_map, keycodes, "steady", color, '') + colors_map = update_colors_map(colors_map, keycodes, color) return colors_map, warnings -def load_breathe(color, msi_keymap): - """Set up the keymap to use a breathing animation. - """ - colors_map = {} - effect_map = {} - warnings = [] - - try: - keycodes = parse_keycodes(msi_keymap, ALIAS_ALL) - color = parse_color(color) - except LineParseError as e: - raise ConfigParseError("Color parse error %s" % str(e)) from e - else: - colors_map = update_colors_map(colors_map, keycodes, "effect", PRESET_COLORS["off"], "breathe_effect") - - # Set up the effect - effect_map["breathe_effect"] = MsiEffect(0) - effect_map["breathe_effect"].start_color = color - effect_map["breathe_effect"].transition_list.append(Transition(PRESET_COLORS["off"], 500)) - effect_map["breathe_effect"].transition_list.append(Transition(color, 750)) - - return colors_map, effect_map, warnings - - def parse_config(f, msi_keymap): colors_map = {} warnings = [] - effect_map = {} - - in_event_block = 0 - selected_event_name = "" for i, line in enumerate(f): line = line.replace("\n", "") - line = line.replace("\t", "") - - if line == "": - continue if line.replace(" ", "")[0] == "#": continue parameters = list(filter(None, line.split(' '))) - # Effects must be declared multi-line; therefore special parsing considerations must be made. - if in_event_block == 1: - if parameters[0] == "trans": - if len(parameters) != 3: - raise ConfigParseError("line %d: Effect trans statement invalid (requires 3 parameters, got %d)" % (i+1, len(parameters))) - - try: - transition_color = parse_color(parameters[1]) - transition_duration = int(parameters[2]) - - effect_map[selected_event_name].period = parse_time_period(transition_duration, effect_map[selected_event_name].period) - except LineParseError as e: - raise ConfigParseError("line %d : %s" % (i + 1, str(e))) from e - else: - effect_add_transition(effect_map, selected_event_name, transition_color, transition_duration) - continue - elif parameters[0] == "start": - if len(parameters) != 2: - raise ConfigParseError("line %d: Effect start statement invalid (requires 2 parameters, got %d)" % i+1, len(parameters)) - try: - startcolor = parse_color(parameters[1]) - except LineParseError as e: - raise ConfigParseError("line %d : %s" % (i + 1, str(e))) from e - else: - effect_map[selected_event_name].start_color = startcolor - continue - elif parameters[0] == "wave": - if len(parameters) != 6: - raise ConfigParseError("line %d: Effect wave statement invalid (requires 6 parameters)") - try: - effect_map[selected_event_name].wave_mode = True - - origin_x = verify_percentage(int(parameters[1])) - effect_map[selected_event_name].wave_origin_x = origin_x - - origin_y = verify_percentage(int(parameters[2])) - effect_map[selected_event_name].wave_origin_y = origin_y - - rad_control = parameters[3].lower() - if rad_control == "xy": - effect_map[selected_event_name].wave_rad_control = "xy" - elif rad_control == "y": - effect_map[selected_event_name].wave_rad_control = "y" - elif rad_control == "x": - effect_map[selected_event_name].wave_rad_control = "x" - else: - raise ConfigParseError("line %d: Parameter 'axis' is not valid. (expected: \"x\", \"y\", \"xy\", got %s" % i+1, parameters[3].lower) - - wavelength = verify_percentage(int(parameters[4])) - effect_map[selected_event_name].wave_wavelength = wavelength - - direction = parameters[5].lower() - if direction == "in": - effect_map[selected_event_name].wave_direction = direction - elif direction == "out": - effect_map[selected_event_name].wave_direction = direction - else: - raise ConfigParseError("line %d: parameter \"direction\" is not valid." - " (expected: \"in\", \"out\", got %s)" % i+1, direction) - except LineParseError as e: - raise ConfigParseError("line %d: %s" % (i + 1, str(e))) from e - continue - - elif parameters[0] == "end": - in_event_block = 0 - selected_event_name = "" - continue - else: - raise ConfigParseError("line %d: Invalid statement in Effect block (found %s}" % i+1, line) - if i == 0 and parameters[0] == "model": warnings += ["Passing the laptop model in the configuration file is deprecated, use the --model option instead."] continue - if parameters[0] == "effect": - if len(parameters) != 2: - raise ConfigParseError("line %d: Effect declaration invalid. (expected 2 parameters, got %d)" % (i+1, len(parameters))) - selected_event_name = parameters[1] - effect_map[selected_event_name] = MsiEffect(len(effect_map)) - in_event_block = 1 - continue - - if i == 0 and parameters[0] == "end": - warnings += ["Found an 'end' that doesn't correspond to an effect block."] - continue - # Parsing a keys/color line if len(parameters) == 0: continue - elif len(parameters) < 3 | len(parameters) > 4: - raise ConfigParseError("line %d : Invalid number of parameters (expected 3 or 4) got %d)" % - (i+1, len(parameters))) + elif len(parameters) > 3: + raise ConfigParseError("line %d : Invalid number of parameters (expected 3, got %d)" % (i+1, len(parameters))) else: try: - effectname = None - color_react = None - color_react_duration = None - color = None - keycodes = parse_keycodes(msi_keymap, parameters[0]) - parsemode = parse_mode(parameters[1]) - - if parsemode == "effect": - if len(parameters) == 3: - effectname = parameters[2] - else: - raise ConfigParseError("line %d : Invalid number of parameters (expected 3 or 5) got %d)" % - (i + 1, len(parameters))) - - elif parsemode == "steady": - if len(parameters) != 3: - raise ConfigParseError("line %d : Invalid number of parameters (expected 3) got %d)" % - (i + 1, len(parameters))) - color = parse_color(parameters[2]) - elif parsemode == "reactive": - if len(parameters) != 5: - raise ConfigParseError("line %d : Invalid number of parameters for parameter 'reactive (expected 5) got %d)" % - (i + 1, len(parameters))) - color = parse_color(parameters[2]) - color_react = parse_color(parameters[3]) - color_react_duration = int(parameters[4]) + parse_mode(parameters[1]) + color = parse_color(parameters[2]) except LineParseError as e: raise ConfigParseError("line %d : %s" % (i+1, str(e))) from e else: - colors_map = update_colors_map(colors_map, keycodes, parsemode, color, effectname, color_react, - color_react_duration) - - if in_event_block == 1: - raise ConfigParseError(": An effect block was not closed properly.") + colors_map = update_colors_map(colors_map, keycodes, color) - return colors_map, effect_map, warnings + return colors_map, warnings def parse_keycodes(msi_keymap, keys_parameter): @@ -310,89 +135,25 @@ def parse_keycodes(msi_keymap, keys_parameter): return keycodes -# Added entry for effects; still in progress. +# This is a stub because there is only one mode for now. Will be modified in future versions. def parse_mode(mode_parameter): - if mode_parameter == "steady": - return "steady" - elif mode_parameter == "effect": - return "effect" - elif mode_parameter == "reactive": - return "reactive" - else: + if mode_parameter != "steady": raise LineParseError("Unknown mode %s" % mode_parameter) def parse_color(color_parameter): - if re.fullmatch("^[0-9a-f]{6}$", color_parameter.lower()): # Color in HTML notation + if re.fullmatch("^[0-9a-f]{6}$", color_parameter): # Color in HTML notation color = [int(color_parameter[i:i+2], 16) for i in [0, 2, 4]] return color else: raise LineParseError("%s is not a valid color" % color_parameter) -def parse_time_period(duration_parameter, current_period): - new_period = current_period + duration_parameter - - if new_period > 0xFFFF: - raise ConfigError("Time period %d is too large." % new_period) - - return new_period - - -def update_colors_map(colors_map, keycodes, keymode, color=None, effect_name=None, react_color=None, react_duration=None): - - # Modes: - # 0: Effect, Manual Refresh - # 1: Static - # 2: Effect, Automatic Refresh - # 8: Reactive +def update_colors_map(colors_map, keycodes, color): for k in keycodes: - colors_map[k] = KeyBlock() - if keymode == "effect": - colors_map[k].mode = 0 - colors_map[k].effect_name = effect_name - elif keymode == "steady": - colors_map[k].mode = 1 - colors_map[k].color = color - elif keymode == "reactive": - colors_map[k].mode = 8 - colors_map[k].color = color - colors_map[k].react_color = react_color - colors_map[k].react_duration = react_duration - else: - raise ConfigParseError("Invalid key mode detected for keycode %s" % k) + colors_map[k] = color return colors_map - - -def effect_add_transition(effect_map, name, color, duration): - if len(effect_map[name].transition_list) > 16: - raise ConfigParseError("There are too many transitions attached to effect %s. (read %d transitions)" % name, len(effect_map.transition_list)) - effect_map[name].transition_list.append(Transition(color, duration)) - - return effect_map - - -def verify_effect(effect_map): - # Currently, only check if the period is larger then FF. - # TODO: Determine what else and how else effects can be verified. - - for key in effect_map: - period = 0 - for trans in effect_map[key].transition_list: - period += trans.duration - if period > 0xFFFF: - raise ConfigParseError("Total transition duration is too long. (length: %d ms)" % period) - effect_map[key].period = period - - return effect_map - - -def verify_percentage(percent): - if percent <= 100 | percent >= 100: - return percent / 100 - else: - raise ConfigParseError("Percentage value is invalid. (length: %d ms)" % percent) diff --git a/msi_perkeyrgb/main.py b/msi_perkeyrgb/main.py index 1b8d561..9020b2c 100755 --- a/msi_perkeyrgb/main.py +++ b/msi_perkeyrgb/main.py @@ -2,13 +2,13 @@ import sys import argparse -from .config import load_config, load_steady, load_breathe, ConfigError +from .protocol_data.msi_keymaps import AVAILABLE_MSI_KEYMAPS +from .config import load_config, load_steady, ConfigError from .parsing import parse_model, parse_usb_id, parse_preset, UnknownModelError, UnknownIdError, UnknownPresetError from .msi_keyboard import MSI_Keyboard -from .protocol_data.msi_keymaps import AVAILABLE_MSI_KEYMAPS from .hidapi_wrapping import HIDLibraryError, HIDNotFoundError, HIDOpenError -VERSION = "1.4_effects_alpha" +VERSION = "2.1" DEFAULT_ID = "1038:1122" DEFAULT_MODEL = "GE63" # Default laptop model if nothing specified @@ -25,9 +25,6 @@ def main(): parser.add_argument('-m', '--model', action='store', help='Set laptop model (see --list-models). If not specified, will use %s as default.' % DEFAULT_MODEL) parser.add_argument('--list-models', action='store_true', help='List available laptop models.') parser.add_argument('-s', '--steady', action='store', metavar='HEXCOLOR', help='Set all of the keyboard to a steady html color. ex. 00ff00 for green') - parser.add_argument('-b', '--breathe', action='store', metavar='HEXCOLOR', help='Set all of the keyboard to breathing effect of defined color. ex. 00ff00 for green') - parser.add_argument('-V', '--verify', action='store', metavar='FILEPATH', help='Verifies the configuration file located at FILEPATH. Refer to the readme for syntax. If set to "-", ' - 'the configuration file is read from the standard output (stdin) instead.') args = parser.parse_args() @@ -93,7 +90,7 @@ def main(): print("No USB device with ID %s found." % args.id) sys.exit(1) except HIDOpenError: - print("Cannot open keyboard. Possible causes :\n- You don't have permissions to open the HID device. Run this program as root, or give yourself read/write permissions to the corresponding /dev/hidraw*. If you have just installed this tool, reboot your computer for the udev rule to take effect.\n- USB device with id is not a HID device.") + print("Cannot open keyboard. Possible causes :\n- You don't have permissions to open the HID device. Run this program as root, or give yourself read/write permissions to the corresponding /dev/hidraw*. If you have just installed this tool, reboot your computer for the udev rule to take effect.\n- The USB device is not a HID device.") sys.exit(1) # If user has requested disabling @@ -112,22 +109,10 @@ def main(): kb.set_preset(preset) kb.refresh() - elif args.verify: - try: - colors_map, effects_map, warnings = load_config(args.verify, msi_keymap) - except ConfigError as e: - print("Error reading config file : %s" % str(e)) - sys.exit(1) - - print("Config file '%s' parsed successfully." % args.verify) - - for w in warnings: - print("Warning :", w) - # If user has requested to load a config file elif args.config: try: - colors_map, effects_map, warnings = load_config(args.config, msi_keymap) + colors_map, warnings = load_config(args.config, msi_keymap) except ConfigError as e: print("Error reading config file : %s" % str(e)) sys.exit(1) @@ -135,7 +120,7 @@ def main(): for w in warnings: print("Warning :", w) - kb.set_colors(colors_map, effects_map) + kb.set_colors(colors_map) kb.refresh() # If user has requested to display a steady color @@ -145,16 +130,7 @@ def main(): except ConfigError as e: print("Error preparing steady color : %s" % str(e)) sys.exit(1) - kb.set_colors(colors_map, None) - kb.refresh() - - elif args.breathe: - try: - colors_map, effects_map, warnings = load_breathe(args.breathe, msi_keymap) - except ConfigError as e: - print(("Error preparing breathing effect : %s" % str(e))) - sys.exit(1) - kb.set_colors(colors_map, effects_map) + kb.set_colors(colors_map) kb.refresh() # If user has not requested anything diff --git a/msi_perkeyrgb/msi_keyboard.py b/msi_perkeyrgb/msi_keyboard.py index de90608..5aef9b9 100644 --- a/msi_perkeyrgb/msi_keyboard.py +++ b/msi_perkeyrgb/msi_keyboard.py @@ -1,12 +1,11 @@ import os import random import json -from .config import KeyBlock from .protocol_data.msi_keymaps import AVAILABLE_MSI_KEYMAPS from .protocol_data.keycodes import REGION_KEYCODES from .protocol_data.presets_index import PRESETS_FILES from .hidapi_wrapping import HID_Keyboard -from .msiprotocol import make_key_colors_packet, make_refresh_packet, make_effect_packet +from .msiprotocol import make_key_colors_packet, make_refresh_packet class MSI_Keyboard: @@ -45,13 +44,13 @@ def set_color_all(self, color): keycodes = REGION_KEYCODES[region] n = len(keycodes) - colors_values = [KeyBlock(color, "", 1)] * n + colors_values = [color] * n colors_map = dict(zip(keycodes, colors_values)) - key_colors_packet = make_key_colors_packet(region, colors_map, None) + key_colors_packet = make_key_colors_packet(region, colors_map) self._hid_keyboard.send_feature_report(key_colors_packet) - def set_random_color_all(self, color): + def set_random_color_all(self): for region in REGION_KEYCODES.keys(): @@ -62,33 +61,19 @@ def set_random_color_all(self, color): r = random.randint(0, 255) g = random.randint(0, 255) b = random.randint(0, 255) - colors_values.append(KeyBlock([r, g, b], "", 1)) + colors_values.append([r, g, b]) colors_map = dict(zip(keycodes, colors_values)) - key_colors_packet = make_key_colors_packet(region, colors_map, None) + key_colors_packet = make_key_colors_packet(region, colors_map) self._hid_keyboard.send_feature_report(key_colors_packet) - def set_effect_all(self, effect_map, effect_name): - - for region in REGION_KEYCODES.keys(): - - keycodes = REGION_KEYCODES[region] - n = len(keycodes) - colors_values = [] - - colors_values = [KeyBlock([0x00, 0x00, 0x00], effect_name, 0)] * n - colors_map = dict(zip(keycodes, colors_values)) - - key_colors_packet = make_key_colors_packet(region, colors_map, effect_map) - self._hid_keyboard.send_feature_report(key_colors_packet) - - def set_colors(self, linux_colors_map, effect_map): + def set_colors(self, linux_colors_map): # Translating from Linux keycodes to MSI's own encoding linux_keycodes = linux_colors_map.keys() - keyblocks = linux_colors_map.values() + colors = linux_colors_map.values() msi_keycodes = [self._msi_keymap[k] for k in linux_keycodes] - msi_colors_map = dict(zip(msi_keycodes, keyblocks)) + msi_colors_map = dict(zip(msi_keycodes, colors)) # Sorting requested keycodes by keyboard region maps_sorted_by_region = {} @@ -99,15 +84,9 @@ def set_colors(self, linux_colors_map, effect_map): maps_sorted_by_region[region] = {} maps_sorted_by_region[region][keycode] = msi_colors_map[keycode] - # Sending effect commands - if effect_map is not None: - for _, effect_objects in effect_map.items(): - effect_packet = make_effect_packet(effect_objects) - self._hid_keyboard.send_feature_report(effect_packet) - # Sending color commands by region for region, region_colors_map in maps_sorted_by_region.items(): - key_colors_packet = make_key_colors_packet(region, region_colors_map, effect_map) + key_colors_packet = make_key_colors_packet(region, region_colors_map) self._hid_keyboard.send_feature_report(key_colors_packet) def set_preset(self, preset): diff --git a/msi_perkeyrgb/msiprotocol.py b/msi_perkeyrgb/msiprotocol.py index 67df415..e636c62 100644 --- a/msi_perkeyrgb/msiprotocol.py +++ b/msi_perkeyrgb/msiprotocol.py @@ -1,10 +1,8 @@ -import math - NB_KEYS = 42 REGION_ID_CODES = {"alphanum": 0x2a, "enter": 0x0b, "modifiers": 0x18, "numpad": 0x24} -def make_key_colors_packet(region, colors_map, effect_map): +def make_key_colors_packet(region, colors_map): packet = [] @@ -13,26 +11,9 @@ def make_key_colors_packet(region, colors_map, effect_map): packet += header k = 0 - for keycode, blkobj in colors_map.items(): - - if blkobj.mode == 1: - effect_id = 0 - blkobj.react_color = [0x00, 0x00, 0x00] - elif blkobj.mode == 8: - effect_id = 255 - else: - blkobj.color = [0x00, 0x00, 0x00] - blkobj.react_color = [0x00, 0x00, 0x00] - effect_id = effect_map[blkobj.effect_name].identifier - - # key_fragment was split as otherwise, adding the int directly will add a single byte to the array. - # The duration must reside in two bytes, or else the packet will end up out of alignment. - key_fragment = blkobj.color + blkobj.react_color - - key_fragment += blkobj.react_duration.to_bytes(2, 'little') - - key_fragment += [effect_id] + [blkobj.mode] + [0x00] + [keycode] + for keycode, rgb in colors_map.items(): + key_fragment = rgb + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00] + [keycode] packet += key_fragment k += 1 @@ -46,120 +27,6 @@ def make_key_colors_packet(region, colors_map, effect_map): return packet -def make_effect_packet(effect_entry): - - # see '0b_packet_information/msi_kb_effectdoc' for documentation on the 0b effect packet. - - packet = [] - header = [0x0b, 0x00] - packet += header - period = 0 - - current_color = [0x00, 0x00, 0x00] - - # begin transition blocks - x = 0 - for transition in effect_entry.transition_list: - if x == 0: - # First transition will always start with the slot number. - packet += effect_entry.identifier.to_bytes(1, 'little') - current_color = effect_entry.start_color - else: - packet += x.to_bytes(1, 'little') - - packet += [0x00] - - # The packet does not utilize colors directly, but instead uses the difference in colors - # to calculate how many 'stages' the lighting should dim. - color_delta = calculate_color_delta(current_color, transition.color) - packet += color_delta - - packet += [0x00] - - packet += transition.duration.to_bytes(2, 'little') - period += transition.duration - - current_color = transition.color - x += 1 - - if x < 16: - packet += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] * (16 - x) - - packet += [0x00, 0x00] - - packet += get_color_starting_bytes(effect_entry.start_color) - - # Separator. - packet += [0xff, 0x00] - - if effect_entry.wave_mode: - packet += get_range_val_from_percent(effect_entry.wave_origin_x, 0, 0x105C).to_bytes(2, 'little') - packet += get_range_val_from_percent(effect_entry.wave_origin_y, 0, 0x040D).to_bytes(2, 'little') - if effect_entry.wave_rad_control == "xy": - packet += [0x01, 0x00, 0x01, 0x00] - elif effect_entry.wave_rad_control == "y": - packet += [0x00, 0x00, 0x01, 0x00] - elif effect_entry.wave_rad_control == "x": - packet += [0x01, 0x00, 0x00, 0x00] - else: - packet += [0x00, 0x00, 0x00, 0x00] - - packet += get_range_val_from_percent(effect_entry.wave_wavelength, 0x001F, 0x03E9).to_bytes(2, 'little') - else: - packet += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - - packet += [len(effect_entry.transition_list)] - - packet += [0x00] - - # Total duration - packet += period.to_bytes(2, 'little') - - if effect_entry.wave_mode: - if effect_entry.wave_direction == "in": - packet += [0x00] - elif effect_entry.wave_direction == "out": - packet += [0x01] - - # Final packet filler bytes - packet += [0x00] * 365 - - return packet - - -def calculate_color_delta(start, target): - delta_red = math.floor((target[0] - start[0]) / 16) - delta_green = math.floor((target[1] - start[1]) / 16) - delta_blue = math.floor((target[2] - start[2]) / 16) - - if delta_red < 0: - delta_red = 256 + delta_red - if delta_green < 0: - delta_green = 256 + delta_green - if delta_blue < 0: - delta_blue = 256 + delta_blue - - return [delta_red, delta_green, delta_blue] - - -def get_color_starting_bytes(color): - color_bytes = [] - - red = color[0] - green = color[1] - blue = color[2] - - color_bytes += [((red & 0b00001111) << 4), ((red & 0b11110000) >> 4)] - color_bytes += [((green & 0b00001111) << 4), ((green & 0b11110000) >> 4)] - color_bytes += [((blue & 0b00001111) << 4), ((blue & 0b11110000) >> 4)] - - return color_bytes - - -def get_range_val_from_percent(percent, minimum, maximum): - return math.floor(((maximum - minimum) * percent) + minimum) - - def make_refresh_packet(): packet = [0x09] + [0x00] * 63 diff --git a/setup.py b/setup.py index 55c982e..17c95ab 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='msi-perkeyrgb', - version='2.0', + version='2.1', description='Configuration tool for per-key RGB keyboards on MSI laptops.', long_description=open( join(dirname(__file__), 'README.md')).read(),