diff --git a/.gitignore b/.gitignore index 3c8b0ce..aca0b68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ * -!fusion -!fusion/datatypes.py -!fusion/device.py +!lib +!lib/datatypes.py +!lib/device.py !.gitignore !play.py -!requirements.txt +!pyproject.toml !profiles.py !test.py diff --git a/fusion/datatypes.py b/lib/datatypes.py similarity index 100% rename from fusion/datatypes.py rename to lib/datatypes.py diff --git a/fusion/device.py b/lib/device.py similarity index 100% rename from fusion/device.py rename to lib/device.py diff --git a/play.py b/play.py index 4c0500c..5aa6d6b 100644 --- a/play.py +++ b/play.py @@ -1,130 +1,135 @@ -from fusion.datatypes import * -from fusion.device import get_device +try: + from lib.datatypes import * #local script + from lib.device import get_device +except ImportError: + from .lib.datatypes import * #as a module + from .lib.device import get_device + from cv2.typing import MatLike import sys, cv2, time, numpy, atexit #10 is actually not transient - it is persistent as a secondary profile but for our purposes we can treat it as transient #since nobody should have the need to write to a profile number this high (switching between profiles require os interaction anyways so might as well write a new image) TRANSIENT_ID = 10 if len(sys.argv) < 2: print(f'Usage: {__file__} <video path> [profile id]') exit(-1) if len(sys.argv) >= 3: try: profile = int(sys.argv[2]) if profile not in range(5): raise TypeError() except: print('Invalid profile id given, defaulting to transience (will be reverted after play (or reboot if interrupted))...') profile = TRANSIENT_ID else: profile = TRANSIENT_ID dev = get_device() orig_effect = dev.get_simple_light_effect() #if transient, set it back to the old profile if profile == TRANSIENT_ID: def reset(): if orig_effect.fusion_effect < FusionLightEffect.Custom1: dev.set_simple_light_effect(orig_effect) else: load = PictureMatrix.from_bytes(dev.get_custom_light_effect(orig_effect.fusion_effect - FusionLightEffect.Custom1)) dev.set_custom_light_effect(profile, load) atexit.register(reset) #change to profile for the image stream first (technically only need to change if not transient and current profile is non custom, but we revert afterwards anyway) #to avoid any flashing set to black first (mainly when we are in simple modes which means last seen image will be shown right when set_simple_light_effect is called) dev.set_custom_light_effect(profile, PictureMatrix([RGB(0,0,0) for i in range(105)])) #need to set it to one of the "proper" profiles for it to know to update dev.set_simple_light_effect(FusionLightData(FusionLightEffect.Custom1 + profile if profile != TRANSIENT_ID else FusionLightEffect.Custom1, 1, 255, FusionLightColor.White, FusionLightDirection.Left2Right)) def set_frame(raw: MatLike, fps_data: List[float]) -> None: pre = cv2.resize(raw, (19, 6), interpolation=cv2.INTER_AREA) im = cv2.cvtColor(pre, cv2.COLOR_BGR2RGB) #display what's supposed to be encoded in the keyboard at this frame cv2.imshow('preview', cv2.resize(pre, (190*5, 60*5))) if len(fps_data) > 1: cv2.setWindowTitle('preview', f'Preview (FPS: {1/numpy.average(numpy.diff(fps_data))})') cv2.waitKey(1) #turns out even if i do translate this into a list of RGB instead of just directly send mats it still gives basically around the same fps pixels = im pixels = [] h, w, _ = im.shape for y in range(h): for x in range(w): pixels.append(RGB(*im[y, x])) #apply any adjustment needed to give true colors dev.set_custom_light_effect(profile, PictureMatrix.pixel_matrix_to_keys(pixels), adjust=dev.ADJUST_RGB) #apparently also works with static images vid = cv2.VideoCapture(sys.argv[1]) #skip some frames so it's not too slow #TODO make this less hardcoded - this value is from testing on my laptop with bad apple AVG_FPS = 11 interval = vid.get(cv2.CAP_PROP_FPS) // AVG_FPS actual_fps = [] #this actually doesnt help that much it just gives like 1 more fps and takes a while to buffer :( PRECOMPUTE = False if PRECOMPUTE: buf = [] print('Buffering...') count = 0 while vid.isOpened(): ret, frame = vid.read() if ret: buf.append(frame) count += interval vid.set(cv2.CAP_PROP_POS_FRAMES, count) else: vid.release() break print('Done. Starting player...') for frame in buf: actual_fps.append(time.time()) set_frame(frame, actual_fps) #get an averagable data before popping if len(actual_fps) > 10: actual_fps.pop(0) else: count = 0 while vid.isOpened(): actual_fps.append(time.time()) ret, frame = vid.read() if ret: set_frame(frame, actual_fps) count += interval vid.set(cv2.CAP_PROP_POS_FRAMES, count) #get an averagable data before popping if len(actual_fps) > 10: actual_fps.pop(0) else: vid.release() break #keep last frame until we manually quit cv2.waitKey(0) \ No newline at end of file diff --git a/profiles.py b/profiles.py index 44b7130..57fcf37 100644 --- a/profiles.py +++ b/profiles.py @@ -1,50 +1,55 @@ -from fusion.device import get_device -from fusion.datatypes import * +try: + from lib.datatypes import * #local script + from lib.device import get_device +except ImportError: + from .lib.datatypes import * #as a module + from .lib.device import get_device + from cv2.typing import MatLike import sys, cv2 """ For setting images in / switching between profiles. """ if len(sys.argv) < 2: print(f'Usage: {__file__} <profile id> [image path]') exit(-1) #only allow properly supported profile ids since theres not much point in supporting it especially when it gets reset after reboot #(even though profile ids way higher than this is actually writable and persistent, see set_custom_light_effect comments) try: profile = int(sys.argv[1]) if profile not in range(5): raise TypeError() except: print('Invalid profile given (must be from 0-4).') exit(-1) dev = get_device() if len(sys.argv) > 2: #load a new image def set_image(raw: MatLike) -> None: pre = cv2.resize(raw, (19, 6), interpolation=cv2.INTER_AREA) im = cv2.cvtColor(pre, cv2.COLOR_BGR2RGB) pixels = [] h, w, _ = im.shape for y in range(h): for x in range(w): pixels.append(RGB(*im[y, x])) #adjustment needed to give true colors (maybe it's just my keyboard) dev.set_custom_light_effect(profile, PictureMatrix.pixel_matrix_to_keys(pixels), adjust=dev.ADJUST_RGB) set_image(cv2.imread(sys.argv[2])) else: #at runtime this would not reload if we dont set_custom_light_effect so we need to do it anyways #(unless we are not in any of the custom profiles then the last image will show up right when we do set_simple_light_effect) load = PictureMatrix.from_bytes(dev.get_custom_light_effect(profile)) dev.set_custom_light_effect(profile, load) dev.set_simple_light_effect(FusionLightData(FusionLightEffect.Custom1 + profile, 1, 255, FusionLightColor.White, FusionLightDirection.Left2Right)) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8e7a3bc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "fusion" +version = "0.1.0" +authors = [ + { name = "despawningbone", email = "contact@despawningbone.me" }, +] +description = "GIGABYTE Fusion RGB reimplementation" +dependencies = [ + "hidapi" +] + +[project.optional-dependencies] +player = ["opencv-python"] + +#install both the lib and the scripts onto site packages +[tool.setuptools] +include-package-data = false +packages = ["fusion", "fusion.lib"] +package-dir = { fusion = "." } #don't include fusion.lib here it's unnecessary + +#comment the above section and use this instead to just install the dependencies since this repo can also be run standalone +#[tool.setuptools] +#packages = [] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ece54bd..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -hidapi \ No newline at end of file diff --git a/test.py b/test.py index fb129e8..c7db826 100644 --- a/test.py +++ b/test.py @@ -1,25 +1,29 @@ -from fusion.device import get_device -from fusion.datatypes import * +try: + from lib.datatypes import * #local script + from lib.device import get_device +except ImportError: + from .lib.datatypes import * #as a module + from .lib.device import get_device dev = get_device() print(dev.get_version_string()) #set simple effect #print(dev.set_simple_light_effect(FusionLightData(FusionLightEffect.Wave, 1, 50, FusionLightColor.Random, FusionLightDirection.Right2Left))) #print(dev.set_simple_light_effect(FusionLightData(FusionLightEffect.Static, 1, 50, FusionLightColor.White, FusionLightDirection.Right2Left))) #get and set to verify if get is correctly implemented # load = PictureMatrix.from_bytes(dev.get_custom_light_effect(1)) # print(load) # dev.set_custom_light_effect(1, load) #manually set color mat = PictureMatrix([RGB(70, 255, 170) for i in range(105)]) #seems like the rgb algo in the keyboard firmware really sucks - this value seems more true white than anything (edit: it's actually known in the driver and has special cases) dev.set_custom_light_effect(10, mat) print(dev.set_simple_light_effect(FusionLightData(FusionLightEffect.Custom1+10, 1, 255, FusionLightColor.Random, FusionLightDirection.Left2Right))) #test reset (seems like after reset if we set the light effect to custom profiles without data it would crash? not if we immediately set the profile afterwards tho) #also seems like if we change profiles right(? seems to last longer than 100ms for sure) after reset some data (e.g wave direction and speed) would get reset # print(dev.reset()) # print(dev.set_simple_light_effect(FusionLightData(FusionLightEffect.Static, 2, 255, FusionLightColor.White, FusionLightDirection.Up2Down))) \ No newline at end of file