#!/usr/bin/python
# -*- coding: utf-8 -*-
#
#Pure python code to control logitech G3,G5,G7 and G9's 
#hardware dpi buttons on linux
#
#This code is based on ActiveState python recipe-576834
#G-series mouse commands tooks from http://piie.net/temp/g5_hiddev.c
#
#License: New BSD license
#Author: timchen119.at.gmail.com
#

import sys
import struct
import array
import fcntl
from optparse import OptionParser

class LOGIMOUSE:
    MOUSE_VENDOR=1133
    MOUSE_G5_FIRST=49217
    MOUSE_G5_SECOND=49225
    MOUSE_G7=50458
    MOUSE_G3=49218
    MOUSE_G9=49224

    #DISABLE BUTTONS
    DISABLE_SPEED_BUTTONS = [0x00, 0x80, 0x01, 0x00, 0x00, 0x00]

    #SET MOUSE DPI
    SET_DPI = {
    "400" : [0x00, 0x80, 0x63, 0x80, 0x00, 0x00],
    "800" : [0x00, 0x80, 0x63, 0x81, 0x00, 0x00],
    "1600" : [0x00, 0x80, 0x63, 0x82, 0x00, 0x00],
    "2000" : [0x00, 0x80, 0x63, 0x83, 0x00, 0x00],
    }

    #LED CONTROL
    SET_LED = {
    "1" : [0x00, 0x80, 0x51, 0x11, 0x02, 0x00],
    "2" : [0x00, 0x80, 0x51, 0x21, 0x01, 0x00],
    "3" : [0x00, 0x80, 0x51, 0x12, 0x01, 0x00],
    "NONE" : [0x00, 0x80, 0x51, 0x11, 0x01, 0x00],
    "ALL" : [0x00, 0x80, 0x51, 0x22, 0x02, 0x00],
    }

    #####################CHANGE THIS DEFAULT############################
    MOUSE_SETTINGS=["/dev/usb/hiddev0"]
    ####################################################################

class struxx:
    _fields = None
    _format = None
    _buffer = None
    def __init__(self):
        self.reset()

    def __len__(self):
        """binary represntation length, for fields, use __dict__ or something"""
        return struct.calcsize(self._format)

    def __iter__(self):
        return [getattr(self, field) for field in self._fields.split(";")].__iter__()

    def reset(self):
        for field in self._fields.split(";"):
            setattr(self, field, 0)
        self._buffer = array.array('B', [0]*len(self))

    def pack(self):
        self._buffer = array.array('B', struct.pack(self._format, *self))

    def unpack(self):
        rv = struct.unpack(self._format, self._buffer)
        for i in range(len(rv)):
            setattr(self, self._fields.split(";")[i], rv[i])

    def ioctl(self, fd, ioctlno):
        self.pack()
        rv = fcntl.ioctl(fd, ioctlno, self._buffer, True)
        self.unpack()
        return rv

class uint(struxx):
    _fields = "uint"
    _format = "I"
    def get_version(self, fd): return self.ioctl(fd, HIDIOCGVERSION)
    def get_flags(self, fd): return self.ioctl(fd, HIDIOCGFLAG)
    def set_flags(self, fd): return self.ioctl(fd, HIDIOCSFLAG)

class hiddev_devinfo(struxx):
    _fields = "bustype;busnum;devnum;ifnum;vendor;product;version;num_applications"
    _format = "IIIIHHHI"
    def get(self, fd): return self.ioctl(fd, HIDIOCGDEVINFO)

class hiddev_string_descriptor(struxx):
    _fields = "index;value"
    _format = "i256c"

    def reset(self):
        self.index = 0
        self.value = '\0'*256

    def pack(self):
        tmp = struct.pack("i", self.index) + self.value[:256].ljust(256, '\0')
        self._buffer = array.array('B', tmp)

    def unpack(self):
        self.index = struct.unpack("i", self._buffer[:4])
        self.value = self._buffer[4:].tostring()

    def get_string(self, fd, idx):
        self.index = idx
        return self.ioctl(fd, HIDIOCGSTRING)

class hiddev_report_info(struxx):
    _fields = "report_type;report_id;num_fields"
    _format = "III"
    def get_info(self, fd): return self.ioctl(fd, HIDIOCGREPORTINFO)
    def set_info(self, fd): return self.ioctl(fd, HIDIOCSREPORT)

class hiddev_field_info(struxx):
    _fields = "report_type;report_id;field_index;maxusage;flags;physical;logical;application;logical_minimum;logical_maximum;physical_minimum;physical_maximum;unit_exponent;unit"
    _format = "I"*8+"i"*4+"II"
    def get_info(self, fd): return self.ioctl(fd, HIDIOCGFIELDINFO)

class hiddev_usage_ref(struxx):
    _fields = "report_type;report_id;field_index;usage_index;usage_code;value"
    _format = "I"*5+"i"
    def set_info(self, fd): return self.ioctl(fd, HIDIOCSUSAGE)

class hiddev_collection_info(struxx):
    _fields = "index;type;usage;level"
    _format = "I"*4
    def get_info(self, fd, index):
        self.index = index
        return self.ioctl(fd, HIDIOCGCOLLECTIONINFO)

class hiddev_event(struxx):
    _fields = "hid;value"
    _format = "Hi"

IOCPARM_MASK = 0x7f
IOC_NONE = 0x20000000
IOC_WRITE = 0x40000000
IOC_READ = 0x80000000

def FIX(x): return struct.unpack("i", struct.pack("I", x))[0]
def _IO(x,y): return FIX(IOC_NONE|(ord(x)<<8)|y)
def _IOR(x,y,t): return FIX(IOC_READ|((t&IOCPARM_MASK)<<16)|(ord(x)<<8)|y)
def _IOW(x,y,t): return FIX(IOC_WRITE|((t&IOCPARM_MASK)<<16)|(ord(x)<<8)|y)
def _IOWR(x,y,t): return FIX(IOC_READ|IOC_WRITE|((t&IOCPARM_MASK)<<16)|(ord(x)<<8)|y)

HIDIOCGVERSION         =_IOR('H', 0x01, struct.calcsize("I"))
HIDIOCAPPLICATION      =_IO('H', 0x02)
HIDIOCGDEVINFO         =_IOR('H', 0x03, len(hiddev_devinfo()))
HIDIOCGSTRING          =_IOR('H', 0x04, len(hiddev_string_descriptor()))
HIDIOCINITREPORT       =_IO('H', 0x05)
def HIDIOCGNAME(buflen): return _IOR('H', 0x06, buflen)
HIDIOCGREPORT          =_IOW('H', 0x07, len(hiddev_report_info()))
HIDIOCSREPORT          =_IOW('H', 0x08, len(hiddev_report_info()))
HIDIOCGREPORTINFO      =_IOWR('H', 0x09, len(hiddev_report_info()))
HIDIOCGFIELDINFO       =_IOWR('H', 0x0A, len(hiddev_field_info()))
HIDIOCGUSAGE           =_IOWR('H', 0x0B, len(hiddev_usage_ref()))
HIDIOCSUSAGE           =_IOW('H', 0x0C, len(hiddev_usage_ref()))
HIDIOCGUCODE           =_IOWR('H', 0x0D, len(hiddev_usage_ref()))
HIDIOCGFLAG            =_IOR('H', 0x0E, struct.calcsize("I"))
HIDIOCSFLAG            =_IOW('H', 0x0F, struct.calcsize("I"))
HIDIOCGCOLLECTIONINDEX =_IOW('H', 0x10, len(hiddev_usage_ref()))
HIDIOCGCOLLECTIONINFO  =_IOWR('H', 0x11, len(hiddev_collection_info()))
def HIDIOCGPHYS(buflen): return _IOR('H', 0x12, buflen)

HID_REPORT_TYPE_INPUT   =1
HID_REPORT_TYPE_OUTPUT  =2
HID_REPORT_TYPE_FEATURE =3
HID_REPORT_TYPE_MIN     =1
HID_REPORT_TYPE_MAX     =3
HID_REPORT_ID_UNKNOWN =0xffffffff
HID_REPORT_ID_FIRST   =0x00000100
HID_REPORT_ID_NEXT    =0x00000200
HID_REPORT_ID_MASK    =0x000000ff
HID_REPORT_ID_MAX     =0x000000ff

def send_command(fd,codes):
    ri = hiddev_report_info()
    ri.report_type = HID_REPORT_TYPE_OUTPUT
    ri.report_id = 0x10
    ri.num_fields = 1
    for i in range(6):
        uref = hiddev_usage_ref()
        uref.report_type = HID_REPORT_TYPE_OUTPUT;
        uref.report_id   = 0x10;
        uref.field_index = 0;
        uref.usage_index = i;
        uref.usage_code  = 0xff000001;
        uref.value       = codes[i];
        uref.set_info(fd)
    ri.set_info(fd)

def check_valid_mouse(fd):
    devinfo = hiddev_devinfo()
    devinfo.get(fd)

    if devinfo.vendor == LOGIMOUSE.MOUSE_VENDOR:
        print "LOGITECH MOUSE FOUND!"
    else:
        print "ERROR: NO LOGITECH MOUSE FOUND! EXIT NOW!"
        sys.exit(1)

    if devinfo.product == LOGIMOUSE.MOUSE_G3:
        print ">>  G3 Gaming Mouse detected!\n"
    elif devinfo.product == LOGIMOUSE.MOUSE_G5_FIRST:
        print ">>  G5 1ST Generation Gaming Mouse detected!\n"
    elif devinfo.product == LOGIMOUSE.MOUSE_G5_SECOND:
        print ">>  G5 2ND Generation Gaming Mouse detected!\n"
    elif devinfo.product == LOGIMOUSE.MOUSE_G7:
        print ">>  G7 Gaming Mouse detected!\n"
    elif devinfo.product == LOGIMOUSE.MOUSE_G9:
        print ">>  G9 Gaming Mouse detected!\n"
    else:
        print "ERROR: NO LOGITECH G-SERIES MOUSE FOUND! EXIT NOW!"
        sys.exit(1)

def parse_arguments():
    usage = "usage: %prog [options] /dev/usb/hiddev0\n"
    usage += "\n%prog control logitech G3,G5,G7 and G9's hardware dpi buttons on linux\n"
    usage += "\nExample: g5mouse.py -d 1600 -l 1 /dev/usb/hiddev0\n"
    usage += "\nAuthor: timchen119.at.gmail.com"
    
    parser = OptionParser(usage)

    parser.add_option("-d", "--dpi", dest="dpi",
                      help="set dpi: 400,800,1600,2000")
    parser.add_option("-l", "--led", dest="led",
                      help="set led: NONE,1,2,3,ALL")
    parser.add_option("-n", "--nodpibuttons", action="store_true", dest="nodpibuttons",
                      help="disable + and - DPI speed buttons")
    parser.add_option("-e", "--dpibuttons", action="store_true", dest="dpibuttons",
                      help="enable + and - DPI speed buttons")

    (options, args) = parser.parse_args()

    if len(args) == 0 and (options.dpi or options.led or options.nodpibuttons or options.dpibuttons):
        print "\n\n################ USE DEFAULT CONFIG !!! ################"
        print "\n\nINFO: use defualt device %s" % LOGIMOUSE.MOUSE_SETTINGS[0]
    elif len(args) == 1:
        LOGIMOUSE.MOUSE_SETTINGS[0] = args[0]
    else:
        parser.print_help()
        sys.exit(1)

    if options.dpi and options.dpi in ['400','800','1600','2000']:
        LOGIMOUSE.MOUSE_SETTINGS.append(LOGIMOUSE.SET_DPI[options.dpi])
    else:
        print "use default DPI: 1600DPI"
        LOGIMOUSE.MOUSE_SETTINGS.append(LOGIMOUSE.SET_DPI['1600'])
        
    if options.led and options.led in ['NONE','1','2','3','ALL']:
        LOGIMOUSE.MOUSE_SETTINGS.append(LOGIMOUSE.SET_LED[options.led])
    else:
        print "use default LED settings: NO LEDS"
        LOGIMOUSE.MOUSE_SETTINGS.append(LOGIMOUSE.SET_LED['NONE'])
        
    if options.nodpibuttons and options.nodpibuttons == True:
        LOGIMOUSE.MOUSE_SETTINGS.append(LOGIMOUSE.DISABLE_SPEED_BUTTONS)
    else:
        print "Default: enable hardware speed buttons."

def main():
    parse_arguments()

    try:
        f = open(LOGIMOUSE.MOUSE_SETTINGS[0], "r")
    except:
        print "\n\nERROR: no such device %s\n" % LOGIMOUSE.MOUSE_SETTINGS[0]
        sys.exit(1)
        
    check_valid_mouse(f)

    for cmd in LOGIMOUSE.MOUSE_SETTINGS[1:]:
        send_command(f,cmd)

if __name__=="__main__":
    main()
