diff --git a/lib/device.py b/lib/device.py index 75c2dcb..6d1f63b 100644 --- a/lib/device.py +++ b/lib/device.py @@ -1,183 +1,184 @@ import hid -import winreg from .datatypes import * class FusionDevice(): VID = 0x1044 PID = [0x7a39, 0x7a3a] ADJUST_RGB = None #unsure what these devices are for yet DEVICES = [RawInputDevice(1, 6, 768), RawInputDevice(1, 2, 256), RawInputDevice(65281, 8713, 256), RawInputDevice(65282, 1, 256), RawInputDevice(65280, 65280, 256)] def __init__(self, path: bytes) -> None: self.h = hid.device() self.h.open_path(path) self.path = path def compute_simple_checksum(self, buf): acc = 0 for i in range(8): acc += buf[i] acc %= 0xFF buf[8] = 0xFF - acc def send_simple_command_and_recv(self, opcode: int, report_num = 0): buf = bytearray(9) buf[1] = opcode self.compute_simple_checksum(buf) sent = self.h.send_feature_report(buf) if sent == -1: raise IOError(self.h.error()) buf = self.h.get_feature_report(0, 9) return buf def get_firmware_version(self) -> bytes: return self.send_simple_command_and_recv(0x80) def get_version_string(self) -> str: buf = self.get_firmware_version() minor = list(str(buf[3])) return f'{buf[2]}.{"0" if len(minor) == 1 else minor.pop(0)}.{minor.pop(0)}' def get_simple_light_effect(self) -> FusionLightData: buf = self.send_simple_command_and_recv(0x88) return FusionLightData(FusionLightEffect(buf[3]), buf[4], buf[5], FusionLightColor(buf[6]), FusionLightDirection(buf[7])) #this is not strictly needed when setting custom profiles - #sending the payload via set_custom_light_effect will update the profile and set the current keyboard lighting to it #but for the profile selection to persist across reboots etc explicitly calling this to set the custom profile is needed #(note that just calling this will also not change the current keyboard lighting, only on reboots, # unless we are not in any of the custom profiles then regardless of the actual profile chosen # the last image will show up right when we do set_simple_light_effect) def set_simple_light_effect(self, effect: FusionLightData) -> None: #technically the UI only allows setting 1-10 it looks like, but 255 still works #but if speed is 0 the firmware would crash when switching to a profile that utilizes speed if effect.fusion_speed not in range(1, 256): raise TypeError('Invalid speed!') buf = bytearray(9) buf[1] = 0x8 buf[3] = int(effect.fusion_effect) buf[4] = effect.fusion_speed #also technically this should be a range from 0-50 but up to 255 works (even though it behaves the same as 50 afaict?) buf[5] = effect.fusion_brightness buf[6] = int(effect.fusion_color) buf[7] = int(effect.fusion_direction) self.compute_simple_checksum(buf) sent = self.h.send_feature_report(buf) if sent == -1: raise IOError(self.h.error()) #aka SetPictureMatrix2Device #requires some cooperation from set_simple_light_effect to properly "persist", see above def set_custom_light_effect(self, profile: int, pixels: PictureMatrix, adjust: RGB = None): #seems like profile can be anything (just that outside of 0-4 it doesnt persist?) #edit: so apparently profiles outside of 0-4 (at least up until 255) actually persists and is accessible, just that on a reboot it will assume the data doesnt exist #switching to another profile and then switching back would still work #aka the profile restriction only exists in set_simple_light_effect # if profile not in range(5): # raise TypeError('Invalid profile number!') buf = bytearray(9) buf[1] = 0x12 buf[3] = profile buf[4] = 8 #why? its also always returned by get_custom_light_effect self.compute_simple_checksum(buf) sent = self.h.send_feature_report(buf) if sent == -1: raise IOError(self.h.error()) data = pixels.to_bytes(adjust) for split in [data[i:i+64] for i in range(0, len(data), 64)]: self.h.write(b'\0' + split) #writing all at once would just freeze the keyboard #self.h.write(b'\0' + b'\0'.join([data[i:i+64] for i in range(0, len(data), 64)])) #aka LoadPictureMatrixValue #use PictureMatrix.from_bytes to convert it back into a usable PictureMatrix def get_custom_light_effect(self, profile: int) -> bytes: # if profile not in range(5): # raise TypeError('Invalid profile number!') buf = bytearray(9) buf[1] = 0x92 buf[3] = profile self.compute_simple_checksum(buf) sent = self.h.send_feature_report(buf) if sent == -1: raise IOError(self.h.error()) buf = self.h.get_feature_report(0, 9) data = b'' for i in range(8): data += bytes(self.h.read(65)) return data #clears all data in firmware memory (custom profiles, macros, etc) #in FusionKeyboard.dll it's usually immediately succeeded by a call to reset light profile to static white def reset(self) -> None: buf = bytearray(9) buf[1] = 0x13 buf[2] = 0xFF self.compute_simple_checksum(buf) sent = self.h.send_feature_report(buf) if sent == -1: raise IOError(self.h.error()) #TODO untested classes below class FusionDeviceIone(FusionDevice): PID = [0x7a3c, 0x7a3d, 0x7a3e] #normal, UK, JP def get_version_string(self) -> str: buf = bytearray(264) buf[0] = 0x7 buf[1] = 0x17 report_num = self.h.send_feature_report(buf) print(report_num) if report_num == -1: raise IOError(self.h.error()) - with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "Software\\GigabyteFusion") as key: - return winreg.QueryValue(key, 'Version') + buf = self.h.read(9) + return buf def get_firmware_version(self) -> bytes: - #theres actually no raw response directly so just fake it with the version string - return self.get_version_string().encode() + buf = self.get_version_string() + if len(buf) != 9 or buf[3] != 0x17: + raise IOError('Invalid response received from IONE device!') + return f'{buf[6]}.{buf[7] // 16}.{buf[7] % 16}' #TODO changeIoneAnimationListUI, changeIoneAnimationTrigger, changeIoneColorGroupUI #TODO seems like the way to do complex profile is changeLightEffect_Details for ione class FusionDeviceIte(FusionDevice): PID = [0x7a38, 0x7a3b, 0x7a3f] #normal, UK, JP #Ite requires RGB color adjustment (it's actually a special case even in FusionKeyboard.dll for Ite) #this value is guessed by eyeballing though ADJUST_RGB = RGB(70, 255, 130) class FusionDeviceX9(FusionDevice): VID = 0x4D9 PID = [0x8008] def get_version_string(self) -> str: buf = self.get_firmware_version() return f'{buf[2]}.{"0" if len(str(buf[3])) == 1 else buf[3] // 16}.{buf[3] % 16}' def get_device() -> FusionDevice: for dev in hid.enumerate(): for type in [FusionDevice, FusionDeviceX9, FusionDeviceIone, FusionDeviceIte]: #print(type, type.VID, type.PID, dev['vendor_id'], dev['product_id']) if dev['vendor_id'] == type.VID and dev['product_id'] in type.PID: try: obj = type(dev['path']) #there should only be one that we can get the firmware version successfully from obj.get_firmware_version() return obj except IOError: continue \ No newline at end of file