Skip to content

Instantly share code, notes, and snippets.

@cmetz
Created December 14, 2020 11:02
Show Gist options
  • Save cmetz/0cd6a79ed39db793e4d5a4f5c0f254a9 to your computer and use it in GitHub Desktop.
Save cmetz/0cd6a79ed39db793e4d5a4f5c0f254a9 to your computer and use it in GitHub Desktop.
Python Durgod K320 Nebula Hacks
import usb.core
import usb.util
import array
import time
import random
import math
import webcolors
import random
from collections import OrderedDict, namedtuple
from sys import exit
Key = namedtuple('Key',['index', 'row', 'x', 'y'])
MODE_RADAR = 0x00
MODE_WAVE = 0x01
MODE_COLOR_SHIFT = 0x02
MODE_BREATH = 0x03
MODE_TWINKLE = 0x04
MODE_REACTIVE = 0x05
MODE_LASER = 0x06
MODE_RIPPLE = 0x07
MODE_SNAKE = 0x08
MODE_TYPING_SPEED = 0x09
MODE_MANUAL = 0x0a
BREATH_MODE_FIXED = 0
BREATH_MODE_RANDOM = 1
K320_KEYS = OrderedDict({
'ESC': Key( 0, 0, 0, 0),
'F1': Key( 2, 0, 24, 0),
'F2': Key( 3, 0, 36, 0),
'F3': Key( 4, 0, 48, 0),
'F4': Key( 5, 0, 60, 0),
'F5': Key( 6, 0, 78, 0),
'F6': Key( 7, 0, 90, 0),
'F7': Key( 8, 0, 102, 0),
'F8': Key( 9, 0, 114, 0),
'F9': Key( 10, 0, 132, 0),
'F10': Key( 11, 0, 144, 0),
'F11': Key( 12, 0, 156, 0),
'F12': Key( 13, 0, 168, 0),
'PRTSC': Key( 14, 0, 0, 13),
'SCRLK': Key( 15, 0, 0, 13),
'PAUSE': Key( 16, 0, 0, 13),
'GRAVE': Key( 21, 1, 0, 13),
'1': Key( 22, 1, 12, 13),
'2': Key( 23, 1, 24, 13),
'3': Key( 24, 1, 36, 13),
'4': Key( 25, 1, 48, 13),
'5': Key( 26, 1, 60, 13),
'6': Key( 27, 1, 72, 13),
'7': Key( 28, 1, 84, 13),
'8': Key( 29, 1, 96, 13),
'9': Key( 30, 1, 108, 13),
'0': Key( 31, 1, 120, 13),
'-': Key( 32, 1, 132, 13),
'+': Key( 33, 1, 144, 13),
'BACKSPACE':Key( 34, 1, 156, 13),
'INS': Key( 35, 1, 0, 13),
'HOME': Key( 36, 1, 0, 13),
'PGUP': Key( 37, 1, 0, 13),
'TAB': Key( 42, 2, 0, 13),
'q': Key( 43, 2, 0, 13),
'w': Key( 44, 2, 0, 13),
'e': Key( 45, 2, 0, 13),
'r': Key( 46, 2, 0, 13),
't': Key( 47, 2, 0, 13),
'y': Key( 48, 2, 0, 13),
'u': Key( 49, 2, 0, 13),
'i': Key( 50, 2, 0, 13),
'o': Key( 51, 2, 0, 13),
'p': Key( 52, 2, 0, 13),
'[': Key( 53, 2, 0, 13),
']': Key( 54, 2, 0, 13),
'\\': Key( 55, 2, 0, 13),
'DEL': Key( 56, 2, 0, 13),
'END': Key( 57, 2, 0, 13),
'PGDN': Key( 58, 2, 0, 13),
'CAPSLOCK': Key( 63, 3, 0, 13),
'a': Key( 64, 3, 0, 13),
's': Key( 65, 3, 0, 13),
'd': Key( 66, 3, 0, 13),
'f': Key( 67, 3, 0, 13),
'g': Key( 68, 3, 0, 13),
'h': Key( 69, 3, 0, 13),
'j': Key( 70, 3, 0, 13),
'k': Key( 71, 3, 0, 13),
'l': Key( 72, 3, 0, 13),
';': Key( 73, 3, 0, 13),
"'": Key( 74, 3, 0, 13),
'ENTER': Key( 76, 3, 0, 13),
'L_SHIFT': Key( 84, 4, 0, 13),
'z': Key( 86, 4, 0, 13),
'x': Key( 87, 4, 0, 13),
'c': Key( 88, 4, 0, 13),
'v': Key( 89, 4, 0, 13),
'b': Key( 90, 4, 0, 13),
'n': Key( 91, 4, 0, 13),
'm': Key( 92, 4, 0, 13),
',': Key( 93, 4, 0, 13),
'.': Key( 94, 4, 0, 13),
'/': Key( 95, 4, 0, 13),
'R_SHIFT': Key( 97, 4, 0, 13),
'UP': Key( 99, 4, 0, 13),
'L_CTRL': Key(105, 5, 0, 13),
'WIN': Key(106, 5, 0, 13),
'L_ALT': Key(107, 5, 0, 13),
'SPACE': Key(111, 5, 0, 13),
'R_ALT': Key(115, 5, 0, 13),
'FN': Key(116, 5, 0, 13),
'MENU': Key(117, 5, 0, 13),
'R_CTRL': Key(118, 5, 0, 13),
'LEFT': Key(119, 5, 0, 13),
'DOWN': Key(120, 5, 0, 13),
'RIGHT': Key(121, 5, 0, 13),
})
def get_nebula_device():
return usb.core.find(idVendor=0x2f68, idProduct=0x0081)
def get_nebula_endpoint(device):
for config in device:
if config.bNumInterfaces > 2:
intf = config[(2,0)]
ep = usb.util.find_descriptor(
intf,
# match the first OUT endpoint
custom_match = lambda e: e.bEndpointAddress == 0x04)
return ep
return None
class ManualColors(object):
def __init__(self):
self.data = [0x00] * 3 * 14 * 9 # RGB * 14 keys * 9 rows
def set_background_color(self, color):
self.data = color * 14 * 9
def set_key_color(self, key_name, color):
self.data[K320_KEYS[key_name].index * 3] = color[0]
self.data[K320_KEYS[key_name].index * 3 + 1] = color[1]
self.data[K320_KEYS[key_name].index * 3 + 2] = color[2]
def get_key_color(self, key_name):
self.data[K320_KEYS[key_name].index * 3:K320_KEYS[key_name].index * 3 + 3]
class Nebula(object):
def __init__(self):
self._device = get_nebula_device()
self._ep = get_nebula_endpoint(self._device)
self._on = True
self._light_level = 9
self._speed = 1
self._sample_rate = 1
self._direction = 0
self._breath_mode = 0
self._breath_color1 = [255, 0, 0]
self._breath_color2 = [0, 255, 0]
self._mode = MODE_MANUAL
def write(self, data):
return self._ep.write(data) == len(data)
def on(self):
return self.write([0x03, 0x06, 0x86, 0x00])
def off(self):
return self.write([0x03, 0x06, 0x86, 0x01])
def set_light_level(self, level):
self._light_level = max(min(level, 9), 0)
return self.write([0x03, 0x06, 0x82, self._light_level])
def get_light_level(self):
return self._light_level
def set_speed(self, speed):
self._speed = max(min(speed, 3), 1)
return self.write([0x03, 0x06, 0x83, self._speed])
def get_speed(self):
return self._speed
def set_sample_rate(self, sample_rate):
self._sample_rate = max(min(sample_rate, 5), 1)
return self.write([0x03, 0x06, 0x85, self._sample_rate])
def get_sample_rate(self):
return self._sample_rate
def set_breath_mode(self, breath_mode, breath_color1=None, breath_color2=None):
self._breath_mode = breath_mode
if breath_color1:
self._breath_color1 = breath_color1
if breath_color2:
self._breath_color2 = breath_color2
if self.get_mode() == MODE_BREATH:
self.set_mode(MODE_BREATH)
def get_breath_mode(self):
return self._breath_mode
def set_effect_mode(self):
self.write([0x03, 0x19, 0x88])
def set_nebula_mode(self):
self.write([0x03, 0x19, 0x66])
def set_mode(self, mode):
self._mode = mode
self.write([
0x03, 0x06, 0x80,
self._mode,
self._direction,
self._breath_mode,
self._breath_color1[0], self._breath_color1[1], self._breath_color1[2],
self._speed,
self._light_level,
self._sample_rate,
self._breath_color2[0], self._breath_color2[1], self._breath_color2[2]])
time.sleep(0.1) #give the mode switch some time
def get_mode(self):
return self._mode
def set_manual_colors(self, manual_colors):
for i in range(0, 9):
begin = 14 * 3 * i
end = 14 * 3 * (i + 1)
data = [0x03, 0x08, 0x00, 0x00, 0x08, i, 0x00, 0x00] + manual_colors.data[begin:end]
if not(self.write( data + [0x00] * (64-len(data)))):
return False
return True
def set_nebula_colors(self, manual_colors):
for i in range(0, 9):
begin = 14 * 3 * i
end = 14 * 3 * (i + 1)
data = [0x03, 0x18, 0x08, i] + manual_colors.data[begin:end]
if not(self.write( data + [0x00] * (64-len(data)))):
return False
return True
def __enter__(self):
if self._device.is_kernel_driver_active(2):
self._device.detach_kernel_driver(2)
return self
def __exit__(self, type, value, traceback):
usb.util.dispose_resources(self._device)
self._device.attach_kernel_driver(2)
def zickzack(x):
return abs(round(2 / math.pi * math.asin(math.cos(math.pi * (x / (128 / 1) + 3) / 2)) * 255))
if __name__ == '__main__':
with Nebula() as nb:
nb.set_nebula_mode()
#time.sleep(1)
mc = ManualColors()
mc.set_background_color([5, 10, 100])
for i in range(1000):
key = random.choice(list(K320_KEYS.keys()))
color = list(webcolors.html5_parse_legacy_color(random.choice(list(webcolors.CSS3_NAMES_TO_HEX.keys()))))
mc.set_key_color(key, color)
nb.set_nebula_colors(mc)
time.sleep(0.001)
time.sleep(1)
mc.set_background_color([5, 10, 100])
for c in 'wasd':
mc.set_key_color(c, [200, 80, 0])
nb.set_nebula_colors(mc)
time.sleep(0.05)
#time.sleep(3)
#exit(0)
nb.set_effect_mode()
#for i in range(0, 9):
# data = [0x03, 0x18, 0x08, i, 0xFF,0xFF,0xFF,0xFF]
# nb.write( data + [0x00] * (64-len(data)))
nb.off()
time.sleep(1)
nb.on()
# #light level
# for i in range(0, 10):
# nb.set_light_level(i)
# time.sleep(0.2)
# nb.write([0x03, 0x06, 0x80, 0x08, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x09, 0x01, 0x00, 0xFF, 0x00])
# #speed
# for i in range(3, 0, -1):
# nb.set_speed(i)
# time.sleep(5)
# # sample
# for i in range(5, 0, -1):
# nb.set_sample_rate(i)
# time.sleep(5)
# nb.set_mode(MODE_BREATH)
# nb.set_breath_mode(BREATH_MODE_RANDOM)
# manual colors
COLOR_CYCLE = [
# [255, 0, 0],
# [0, 255, 0],
# [0, 0, 255],
# [255, 255, 0],
# [255, 0, 255],
# [255, 255, 255],
# [0, 25, 5],
[5, 10, 100],
]
# for _ in range(100):
# COLOR_CYCLE += [[random.randrange(256), random.randrange(256), random.randrange(256)]]
nb.set_mode(MODE_MANUAL)
mc = ManualColors()
for color in COLOR_CYCLE:
mc.set_background_color(color)
for c in 'wasd':
mc.set_key_color(c, [200, 80, 0])
# for c in ['F1', 'F2', 'F3' ,'F4']:
# mc.set_key_color(c, [0, 255, 0])
# for c in ['F5', 'F6', 'F7' ,'F8']:
# mc.set_key_color(c, [0, 255, 255])
# for c in ['F9', 'F10', 'F11' ,'F12']:
# mc.set_key_color(c, [255, 0, 255])
# for n, k in K320_KEYS.items():
# if k.row == 1:
# mc.set_key_color(n, [100, 100, 100])
# for c in 'i7yujmko9':
# mc.set_key_color(c, [255, 0, 0])
nb.set_manual_colors(mc)
time.sleep(0.5)
# #wave
# nb.write([0x03, 0x06, 0x80, 0x01, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x09, 0x01, 0x00, 0xFF, 0x00])
# colors = []
# for i in range(256):
# colors += [0xFF, zickzack(i), 0]
# for i in range(24):
# begin = 32 * i
# end = 32 * (i + 1)
# data = [0x03, 0x09, 0x00, 0x00, 0x17, i, 0x00, 0x00] + colors[begin:end]
# nb.write(data + [0x00] * (64-len(data)))
# nb.write([0x03, 0x0a, 0x00, 0x03, 0x05, 0x00, 0x00, 0x00, 0xd5, 0x00, 0xc0, 0xb6, 0xac, 0xa2, 0x93, 0x88, 0x7e, 0x74, 0x65, 0x5b, 0x51, 0x46, 0x3a, 0x30, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
# nb.write([0x03, 0x0a, 0x00, 0x03, 0x05, 0x01, 0x00, 0x00, 0xd5, 0xcb, 0xc0, 0xb6, 0xac, 0xa2, 0x98, 0x8e, 0x83, 0x79, 0x6f, 0x65, 0x5b, 0x4c, 0x3a, 0x30, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
# nb.write([0x03, 0x0a, 0x00, 0x03, 0x05, 0x02, 0x00, 0x00, 0xd2, 0xc5, 0xbb, 0xb1, 0xa7, 0x9d, 0x93, 0x88, 0x7e, 0x74, 0x6a, 0x60, 0x56, 0x49, 0x3a, 0x30, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
# nb.write([0x03, 0x0a, 0x00, 0x03, 0x05, 0x03, 0x00, 0x00, 0xd1, 0xc3, 0xb9, 0xaf, 0xa5, 0x9a, 0x90, 0x86, 0x7c, 0x72, 0x68, 0x5d, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
# nb.write([0x03, 0x0a, 0x00, 0x03, 0x05, 0x04, 0x00, 0x00, 0xcf, 0x00, 0xbe, 0xb4, 0xa9, 0x9f, 0x95, 0x8b, 0x81, 0x77, 0x6c, 0x62, 0x00, 0x4f, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
# nb.write([0x03, 0x0a, 0x00, 0x03, 0x05, 0x05, 0x00, 0x00, 0xd4, 0xc7, 0xba, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x6e, 0x61, 0x54, 0x48, 0x3a, 0x30, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
import hid
import time
K320_VID = 0x2f68
K320_PID = 0x0081
K320_USAGE_PAGE = 0xffc2
K320_INTERFACE = 2
def get_nebule_device():
for h in hid.enumerate(vid=K320_VID, pid=K320_PID):
if h['usage_page'] == K320_USAGE_PAGE and h['interface_number'] == K320_INTERFACE:
return hid.Device(path=h['path'])
return None
def write(nebula_device: hid.Device, data, wait=False):
nebula_device.write(bytes([0x00] + data))
if wait:
ret = nebula_device.read(65, 1000)
d = get_nebule_device()
# effect mode
write(d, [0x03, 0x19, 0x88])
# off
write(d, [0x03, 0x06, 0x86, 0x01])
time.sleep(1)
#on
write(d, [0x03, 0x06, 0x86, 0x00])
time.sleep(1)
# nebula mode (streamed colors)
write(d, [0x03, 0x19, 0x66])
for i in range(0, 9):
data = [0x03, 0x18, 0x08, i, 0xFF, 0x00, 0x00]
write(d, data, True)
time.sleep(1)
# back to effects mode
write(d, [0x03, 0x19, 0x88])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment