Skip to content

Instantly share code, notes, and snippets.

@Jacajack
Last active June 22, 2023 23:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jacajack/dc40a4a7883149937a1da51c07a9c6c4 to your computer and use it in GitHub Desktop.
Save Jacajack/dc40a4a7883149937a1da51c07a9c6c4 to your computer and use it in GitHub Desktop.
A simple script for Razer BlackWidow V4 command dial support with OpenRazer and evdev
#!/usr/bin/env python3
import asyncio
import evdev
import evdev.ecodes as ecodes
import openrazer.client
import colorsys
import time
import math
import random
# Distributes dial events to sub-functions. When pressed, the next function is activated
class CommandDialFunctionMux:
def __init__(self, on_change = None):
self.mode_id = 0
self.modes = []
self.last_change_time = time.time()
self.on_change = on_change
def add_mode(self, name, color, function):
self.modes.append({
'name': name,
'color': color,
'function': function,
'counter': 0
})
def current_mode(self):
return self.modes[self.mode_id]
def on_turn(self, delta):
mode = self.current_mode()
mode['counter'] += delta
mode['function'](self, mode['counter'], delta, False)
self.last_change_time = time.time()
if self.on_change:
self.on_change(self)
def on_press(self):
self.mode_id = (self.mode_id + 1) % len(self.modes)
self.last_change_time = time.time()
print(f"Command dial function {self.mode_id}")
if self.on_change:
self.on_change(self)
def get_color(self):
return self.current_mode()['color']
def time_since_change(self):
return time.time() - self.last_change_time
# A nice glimmering effect, very suboptimal, but
# works well as an example
class GlimmerEffect:
def __init__(self, kbd, hue = 1):
self.kbd = kbd
self.hue = 1
self.intensity = 0.85
self.speed = 1.5
self.phases = {}
def on_dial(self, mux, counter, delta, press):
self.hue = counter % 128 / 128
def update(self):
t = time.time()
for y in range(kbd.fx.advanced.rows):
for x in range(kbd.fx.advanced.cols):
phase = self.phases.get((x, y))
if phase is None:
self.phases[(x, y)] = random.random()
phase = self.phases[(x, y)]
s = 1
v_offset = math.sin(t * self.speed + phase * 2 * math.pi) ** 8
v = (1 - self.intensity) + self.intensity * v_offset
h = (self.hue + v_offset * self.intensity * 0.01) % 1
kbd.fx.advanced.matrix[y, x] = tuple(x * 255 for x in colorsys.hsv_to_rgb(h, s, v))
# Emits scroll events to the provided mouse device
class Scroller:
def __init__(self, dev, code):
self.dev = dev
self.code = code
def on_dial(self, mux, counter, delta, press):
if delta:
self.dev.write(ecodes.EV_REL, self.code, delta)
self.dev.syn()
# Handles events coming from the original mouse device
async def handle_mouse(dial_handler):
async for event in razer_mouse.async_read_loop():
# Handle and filter out any horizontal scroll events
# We grabbed the mouse device, so we re-emit other events like volume control
if event.type == ecodes.EV_REL and event.code == ecodes.REL_HWHEEL:
dial_handler.on_turn(event.value)
elif event.type == ecodes.EV_REL and event.code == ecodes.REL_HWHEEL_HI_RES:
pass
else:
virtual_mouse.write_event(event)
# Handles events coming from the original kbd device
async def handle_keyboard(dial_handler):
async for event in razer_kbd.async_read_loop():
# We're only interested in F24 (command dial press)
# No need to re-emit, we don't own the kbd here
if event.type == ecodes.EV_KEY and event.code == ecodes.KEY_F24 and event.value == 1:
dial_handler.on_press()
# Returns numbers in specified intervals
async def effect_updater(dt):
i = 0
while True:
yield i
await asyncio.sleep(dt)
i += 1
# Performs a single effect update
# Called periodically and whenever the dial is turned
def update_effect(dial_mux, effect):
effect.update()
if dial_mux.time_since_change() < 3:
effect.kbd.fx.advanced.matrix[0, 1] = dial_mux.get_color()
effect.kbd.fx.advanced.draw()
# Responsible for updating the keyboard effect in specified intervals
async def effect_loop(dial_mux, effect):
async for i in effect_updater(0.05):
update_effect(dial_mux, effect)
if __name__ == '__main__':
devman = openrazer.client.DeviceManager()
kbd = devman.devices[0] # TODO pick BlackWidow V4 Pro
# FIXME hardcoded paths
razer_mouse = evdev.InputDevice("/dev/input/by-id/usb-Razer_Razer_BlackWidow_V4_Pro-if02-event-mouse")
razer_kbd = evdev.InputDevice("/dev/input/by-id/usb-Razer_Razer_BlackWidow_V4_Pro-if01-event-kbd")
# Extend capabilities defined by the original mouse device
razer_mouse_cap = razer_mouse.capabilities(absinfo = False, verbose = False)
mouse_cap = {
ecodes.EV_REL: razer_mouse_cap[ecodes.EV_REL] + [ecodes.REL_WHEEL, ecodes.REL_HWHEEL],
ecodes.EV_KEY: razer_mouse_cap[ecodes.EV_KEY] + [ecodes.KEY_VOLUMEUP, ecodes.KEY_VOLUMEDOWN],
}
# Virtual devices where we send our events
virtual_mouse = evdev.UInput(mouse_cap, name = "command_dial_mouse", version = 0x3)
virtual_kbd = evdev.UInput.from_device(razer_kbd, name = "command_dial_kbd")
# Keyboard matrix effect
glimmer_effect = GlimmerEffect(kbd)
effect = glimmer_effect
# These guys are used for scrolling with the command dial
scroller_v = Scroller(dev = virtual_mouse, code = ecodes.REL_WHEEL)
scroller_h = Scroller(dev = virtual_mouse, code = ecodes.REL_HWHEEL)
dial_mux = CommandDialFunctionMux(on_change = lambda mux: update_effect(mux, effect))
dial_mux.add_mode(
name = "Vertical scroll",
color = (255, 255, 0),
function = scroller_v.on_dial
)
dial_mux.add_mode(
name = "Horizontal scroll",
color = (255, 0, 255),
function = scroller_h.on_dial
)
dial_mux.add_mode(
name = "Glimmer color",
color = (255, 255, 255),
function = glimmer_effect.on_dial
)
# Grab mouse and start our main loop
with razer_mouse.grab_context():
asyncio.ensure_future(handle_mouse(dial_mux))
asyncio.ensure_future(handle_keyboard(dial_mux))
asyncio.ensure_future(effect_loop(dial_mux, effect))
event_loop = asyncio.get_event_loop()
event_loop.run_forever()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment