diff --git a/g203-led.py b/g203-led.py index 3c56991..5963154 100755 --- a/g203-led.py +++ b/g203-led.py @@ -1,6 +1,7 @@ #!env/bin/python # Logitech G203 Prodigy Mouse LED control +# Altered to work with G203 LightSync instead by TheAquaSheep # https://github.com/smasty/g203-led # Author: Smasty, hello@smasty.net # Licensed under the MIT license. @@ -12,10 +13,13 @@ import re import binascii g203_vendor_id = 0x046d -g203_product_id = 0xc084 +g203_prodigy_product_id = 0xc084 +g203_lightsync_product_id = 0xc092 +g203_product_id = g203_prodigy_product_id default_rate = 10000 default_brightness = 100 +default_direction = 'right' dev = None @@ -23,18 +27,33 @@ wIndex = None def help(): - print("""Logitech G203 Prodigy Mouse LED control + print("""Logitech G203 Prodigy / Lightsync Mouse LED control Usage: -\tg203-led solid {color} - Solid color mode -\tg203-led cycle [{rate} [{brightness}]] - Cycle through all colors -\tg203-led breathe {color} [{rate} [{brightness}]] - Single color breathing -\tg203-led intro {on|off} - Enable/disable startup effect +\tg203-led [lightsync] solid {color} - Solid color mode +\tg203-led [lightsync] cycle [{rate} [{brightness}]] - Cycle through all colors +\tg203-led [lightsync] breathe {color} [{rate} [{brightness}]] - Single color breathing +\tg203-led [lightsync] intro {on|off} - Enable/disable startup effect +\tg203-led [lightsync] dpi {dpi} - Set mouse dpi Arguments: \tColor: RRGGBB (RGB hex value) -\tRate: 100-60000 (Number of milliseconds. Default: 10000ms) -\tBrightness: 0-100 (Percentage. Default: 100%)""") +\tRate: 1000-60000 (Number of milliseconds. Default: 10000ms) +\tBrightness: 1-100 (Percentage. Default: 100%) +\tDPI: 200-8000 (Prodigy), 50-8000 (Lightsync) + +Assumes Prodigy by default unless "lightsync" is given as the first command argument. +This ensures backward compatibility. +Lightsync additional features: +\tg203-led lightsync triple {color color color} - Sets all 3 colors from left to right. +\tg203-led lightsync wave {rate} [{brightness} [{direction}]] - Like cycle but appears to move right or left. +\tg203-led lightsync blend [{rate} [{brightness}]] - Like breathe with the side colors changing after some delay. + +\tDirection is either "left" or "right". Default: right). + +Note that the lightsync setting will not persist. +There is onboard memory for persistence but it is not used by this script.""") + def main(): @@ -42,7 +61,7 @@ def main(): help() sys.exit() - args = sys.argv + [None] * (5 - len(sys.argv)) + args = sys.argv + [None] * (6 - len(sys.argv)) mode = args[1] if mode == 'solid': @@ -54,9 +73,45 @@ def main(): process_color(args[2]), process_rate(args[3]), process_brightness(args[4]) - ) + ) elif mode == 'intro': set_intro_effect(args[2]) + elif mode == 'dpi': + set_dpi(process_dpi(args[2])) + elif mode == 'lightsync': + global g203_product_id + g203_product_id = g203_lightsync_product_id + mode = args[2] + if mode == 'solid': + set_ls_solid(process_color(args[3])) + elif mode == 'cycle': + set_ls_cycle(process_rate(args[3]), process_brightness(args[4])) + elif mode == 'breathe': + set_ls_breathe( + process_color(args[3]), + process_rate(args[4]), + process_brightness(args[5]) + ) + elif mode == 'intro': + set_ls_intro(args[3]) + elif mode == 'dpi': + set_dpi(process_dpi(args[3])) + elif mode == 'triple': + set_ls_triple( + process_color(args[3]), + process_color(args[4]), + process_color(args[5]) + ) + elif mode == 'wave': + set_ls_wave( + process_rate(args[3]), + process_brightness(args[4]), + process_direction(args[5]) + ) + elif mode == 'blend': + set_ls_blend(process_rate(args[3]), process_brightness(args[4])) + else: + print_error('Unknown lightsync mode.') else: print_error('Unknown mode.') @@ -68,7 +123,7 @@ def print_error(msg): def process_color(color): if not color: - print_error('No color specifed.') + print_error('No color specified.') if color[0] == '#': color = color[1:] if not re.match('^[0-9a-fA-F]{6}$', color): @@ -79,7 +134,7 @@ def process_rate(rate): if not rate: rate = default_rate try: - return '{:04x}'.format(max(100, min(65535, int(rate)))) + return '{:04x}'.format(max(1000, min(65535, int(rate)))) except ValueError: print_error('Invalid rate specified.') @@ -91,6 +146,26 @@ def process_brightness(brightness): except ValueError: print_error('Invalid brightness specified.') +def process_direction(direction): + if not direction: + direction = default_direction + else: + if not (direction == 'left' or direction == 'right'): + print_error('Invalid direction specified.') + return direction + +def process_dpi(dpi): + if not dpi: + print_error('No DPI specified.') + lower_lim = 200 + if g203_product_id == g203_lightsync_product_id: + lower_lim = 50 + try: + return '{:04x}'.format(max(lower_lim, min(8000, int(dpi)))) + except ValueError: + print_error('Invalid DPI specified.') + return dpi + def set_led_solid(color): return set_led('01', color + '0000000000') @@ -99,7 +174,6 @@ def set_led_breathe(color, rate, brightness): return set_led('03', color + rate + '00' + brightness + '00') def set_led_cycle(rate, brightness): - print(rate, brightness) return set_led('02', '0000000000' + rate + brightness) @@ -122,10 +196,83 @@ def set_intro_effect(arg): send_command('11ff0e5b0001'+toggle+'00000000000000000000000000') +def set_dpi(dpi): + cmd = '10ff0a3b00{}'.format(dpi) + send_command(cmd, disable_ls_onboard_memory=False) -def send_command(data): +def set_ls_solid(color): + cmd = '11ff0e1b0001{}0000000000000001000000'.format(color) + send_command(cmd, disable_ls_onboard_memory=True) + +def set_ls_cycle(rate, brightness): + cmd = '11ff0e1b00020000000000{}{}000001000000'.format(rate, brightness) + send_command(cmd, disable_ls_onboard_memory=True) + +def set_ls_breathe(color, rate, brightness): + cmd = '11ff0e1b0004{}{}00{}00000001000000'.format(color, rate, brightness) + send_command(cmd, disable_ls_onboard_memory=True) + +def set_ls_intro(arg): + if arg == 'on' or arg == '1': + toggle = '01' + elif arg == 'off' or arg == '0': + toggle = '02' + else: + print_error('Invalid value.') + cmd = '11ff0e3b010001{}000000000000000000000000'.format(toggle) + send_command(cmd, disable_ls_onboard_memory=False) + +def set_ls_triple(color_left, color_middle, color_right): + cmd = '11ff121b01{}02{}03{}00000000'.format(color_left, color_middle, color_right) + send_command(cmd, disable_ls_onboard_memory=False) + +def set_ls_wave(rate, brightness, direction): + rate_U8 = rate[0:2] + rate_L8 = rate[2:4] + state = '01' + if direction == 'left': + state = '06' + cmd = '11ff0e1b0003000000000000{}{}{}{}01000000'.format(rate_L8, state, brightness, rate_U8) + send_command(cmd, disable_ls_onboard_memory=True) + +def set_ls_blend(rate, brightness): + rate_U8 = rate[0:2] + rate_L8 = rate[2:4] + cmd = '11ff0e1b0006000000000000{}{}{}0001000000'.format(rate_L8, rate_U8, brightness) + send_command(cmd, disable_ls_onboard_memory=True) + +def clear_ls_buffer(): #tested on lightsync but may also affect prodigy + try: + while True: + dev.read(0x82, 20) + except usb.core.USBError: + return + +def send_command(data, disable_ls_onboard_memory=False, clear_ls_buf=False): attach_mouse() - dev.ctrl_transfer(0x21, 0x09, 0x0211, wIndex, binascii.unhexlify(data)) + + if clear_ls_buf: # if this is ever needed in practise the default can be changed above. + clear_ls_buffer() + + if disable_ls_onboard_memory: + dev.ctrl_transfer(0x21, 0x09, 0x210, wIndex, binascii.unhexlify('10ff0e5b010305')) + dev.read(0x82, 20) + + wValue=0x211 + if len(data) == 14: + wValue = 0x210 + + dev.ctrl_transfer(0x21, 0x09, wValue, wIndex, binascii.unhexlify(data)) + dev.read(0x82, 20) + + if data[0:8] == '11ff121b': + apply_triple_cmd = '11ff127b00000000000000000000000000000000' + dev.ctrl_transfer(0x21, 0x09, 0x211, wIndex, binascii.unhexlify(apply_triple_cmd)) + dev.read(0x82, 20) + + if clear_ls_buf: # done again to ensure the buffer did not fill between the last clear and cmd + clear_ls_buffer() + detach_mouse()