/*
 * Simple hack to control the wheel of Logitech's MX-Revolution mouse.
 *
 * Requires hiddev.
 *
 * Written November 2006 by E. Toernig's bonobo - no copyrights.
 *
 * Contact: Edgar Toernig <froese@gmx.de>
 *
 * Discovered commands:
 * (all numbers in hex, FS=free-spinning mode, CC=click-to-click mode):
 *   6 byte commands send with report ID 10:
 *   01 80 56 z1 00 00	immediate FS
 *   01 80 56 z2 00 00	immediate CC
 *   01 80 56 03 00 00	FS when wheel is moved
 *   01 80 56 04 00 00	CC when wheel is moved
 *   01 80 56 z5 xx yy	CC and switch to FS when wheel is rotated at given
 *			speed; xx = up-speed, yy = down-speed
 *			(speed in something like clicks per second, 1-50,
 *			 0 = previously set speed)
 *   01 80 56 06 00 00	?
 *   01 80 56 z7 xy 00	FS with button x, CC with button y.
 *   01 80 56 z8 0x 00	toggle FS/CC with button x; same result as 07 xx 00.
 *
 * If z=0 switch temporary, if z=8 set as default after powerup.
 *
 * Button numbers:
 *   0 previously set button
 *   1 left button	(can't be used for mode changes)
 *   2 right button	(can't be used for mode changes)
 *   3 middle (wheel) button
 *   4 rear thumb button
 *   5 front thumb button
 *   6 find button
 *   7 wheel left tilt
 *   8 wheel right tilt
 *   9 side wheel forward
 *  11 side wheel backward
 *  13 side wheel pressed
 */

#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define streq(a,b)	(strcmp((a), (b)) == 0)
#define strneq(a,b,c)	(strncmp((a), (b), (c)) == 0)

#define LOGITECH	0x046d
#define MX_REVOLUTION	0xc51a

/*** extracted from hiddev.h ***/

typedef signed short s16;
typedef signed int s32;
typedef unsigned int u32;

struct hiddev_devinfo {
	u32 bustype;
	u32 busnum;
	u32 devnum;
	u32 ifnum;
	s16 vendor;
	s16 product;
	s16 version;
	u32 num_applications;
};

struct hiddev_report_info {
	u32 report_type;
	u32 report_id;
	u32 num_fields;
};

#define HID_REPORT_TYPE_INPUT	1
#define HID_REPORT_TYPE_OUTPUT	2
#define HID_REPORT_TYPE_FEATURE	3

struct hiddev_usage_ref {
	u32 report_type;
	u32 report_id;
	u32 field_index;
	u32 usage_index;
	u32 usage_code;
	s32 value;
};

struct hiddev_usage_ref_multi {
	struct hiddev_usage_ref uref;
	u32 num_values;
	s32 values[1024];
};

#define HIDIOCGDEVINFO		_IOR('H', 0x03, struct hiddev_devinfo)
#define HIDIOCGREPORT		_IOW('H', 0x07, struct hiddev_report_info)
#define HIDIOCSREPORT		_IOW('H', 0x08, struct hiddev_report_info)
#define HIDIOCGUSAGES		_IOWR('H', 0x13, struct hiddev_usage_ref_multi)
#define HIDIOCSUSAGES		_IOW('H', 0x14, struct hiddev_usage_ref_multi)

/*** end hiddev.h ***/


static void
fatal(const char *fmt, ...)
{
    va_list args;

    va_start(args, fmt);
    fprintf(stderr, "revoco: ");
    vfprintf(stderr, fmt, args);
    fprintf(stderr, "\n");
    va_end(args);
    exit(1);
}

static int
open_dev(char *path)
{
    char buf[128];
    int i, fd;
    struct hiddev_devinfo dinfo;

    for (i = 0; i < 16; ++i)
    {
	sprintf(buf, path, i);
	fd = open(buf, O_RDWR);
	if (fd >= 0)
	{
	    if (ioctl(fd, HIDIOCGDEVINFO, &dinfo) == 0)
		if (dinfo.vendor == (short)LOGITECH)
		    if (dinfo.product == (short)MX_REVOLUTION)
			return fd;
	    close(fd);
	}
    }
    return -1;
}

static void
close_dev(int fd)
{
    close(fd);
}

static void
send_report(int fd, int id, int *buf, int n)
{
    struct hiddev_usage_ref_multi uref;
    struct hiddev_report_info rinfo;
    int i;

    uref.uref.report_type = HID_REPORT_TYPE_OUTPUT;
    uref.uref.report_id = id;
    uref.uref.field_index = 0;
    uref.uref.usage_index = 0;
    uref.num_values = n;
    for (i = 0; i < n; ++i)
	uref.values[i] = buf[i];
    if (ioctl(fd, HIDIOCSUSAGES, &uref) == -1)
	fatal("send report %02x/%d, HIDIOCSUSAGES: %s", id, n, strerror(errno));

    rinfo.report_type = HID_REPORT_TYPE_OUTPUT;
    rinfo.report_id = id;
    rinfo.num_fields = 1;
    if (ioctl(fd, HIDIOCSREPORT, &rinfo) == -1)
	fatal("send report %02x/%d, HIDIOCSREPORT: %s", id, n, strerror(errno));
}

static void
query_report(int fd, int id, int *buf, int n)
{
    struct hiddev_usage_ref_multi uref;
    struct hiddev_report_info rinfo;
    int i;

    rinfo.report_type = HID_REPORT_TYPE_INPUT;
    rinfo.report_id = id;
    rinfo.num_fields = 1;
    if (ioctl(fd, HIDIOCGREPORT, &rinfo) == -1)
	fatal("query report %02x/%d, HIDIOCGREPORT: %s", id, n, strerror(errno));

    uref.uref.report_type = HID_REPORT_TYPE_INPUT;
    uref.uref.report_id = id;
    uref.uref.field_index = 0;
    uref.uref.usage_index = 0;
    uref.num_values = n;
    if (ioctl(fd, HIDIOCGUSAGES, &uref) == -1)
	fatal("query report %02x/%d, HIDIOCGUSAGES: %s", id, n, strerror(errno));
    for (i = 0; i < n; ++i)
	buf[i] = uref.values[i];
}

static void
mx_cmd(int fd, int b1, int b2, int b3)
{
    int buf[6] = { 0x01, 0x80, 0x56, b1, b2, b3 };

    send_report(fd, 0x10, buf, 6);
}

static char *
onearg(char *str, char prefix, int *arg, int def, int min, int max)
{
    char *end;
    long n;

    *arg = def;

    if (*str == '\0')
	return str;

    if (*str != prefix)
	fatal("bad argument `%s': `%c' expected", str, prefix);

    n = strtol(++str, &end, 0);
    if (str != end)
    {
	*arg = n;
	if (n < min || n > max)
	    fatal("argument `%.*s' out of range (%d-%d)", end - str, str,
								    min, max);
    }
    return end;
}

static void
twoargs(char *str, int *arg1, int *arg2, int def, int min, int max)
{
    char *p = str;

    p = onearg(p, '=', arg1, def, min, max);
    p = onearg(p, ',', arg2, *arg1, min, max);
    if (*p)
	fatal("malformed argument `%s'", str);
}

static int
nargs(char *str, int *buf, int n, int def, int min, int max)
{
    char *p = str;
    int i = 0, del = '=';

    while (n--)
    {
	if (*p)
	    i++;
	p = onearg(p, del, buf++, def, min, max);
	del = ',';
    }
    if (*p)
	fatal("malformed argument `%s'", str);
    return i;
}

static void
configure(int handle, int argc, char **argv)
{
    int i, arg1, arg2;

    for (i = 1; i < argc; ++i) {
	int perm = 0x80;
	char *cmd = argv[i];

	if (strneq(cmd, "temp-", 5))
	    perm = 0, cmd += 5;

	if (streq(cmd, "free"))
	{
	    mx_cmd(handle, perm + 1, 0, 0);
	}
	else if (streq(cmd, "click"))
	{
	    mx_cmd(handle, perm + 2, 0, 0);
	}
	else if (strneq(cmd, "manual", 6))
	{
	    twoargs(cmd + 6, &arg1, &arg2, 0, 0, 15);
	    mx_cmd(handle, perm + 7, arg1 * 16 + arg2, 0);
	}
	else if (strneq(cmd, "auto", 4))
	{
	    twoargs(cmd + 4, &arg1, &arg2, 0, 0, 50);
	    mx_cmd(handle, perm + 5, arg1, arg2);
	}
	else if (strneq(argv[i], "soft-free", 9))
	{
	    twoargs(argv[i] + 9, &arg1, &arg2, 0, 0, 255);
	    mx_cmd(handle, 3, arg1, arg2);
	}
	else if (strneq(argv[i], "soft-click", 10))
	{
	    twoargs(argv[i] + 10, &arg1, &arg2, 0, 0, 255);
	    mx_cmd(handle, 4, arg1, arg2);
	}
    /*** debug commands ***/
	else if (strneq(argv[i], "raw", 3))
	{
	    int buf[256], n;

	    n = nargs(argv[i] + 3, buf, 256, 0, 0, 255);
	    send_report(handle, buf[0], buf+1, n-1);
	}
	else if (strneq(argv[i], "query", 5))
	{
	    int buf[256], j;

	    twoargs(argv[i] + 5, &arg1, &arg2, -1, 0, 255);
	    if (arg1 == -1)
		arg1 = 0x10, arg2 = 6;
	    query_report(handle, arg1, buf, arg2);

	    printf("report %02x:", arg1);
	    for (j = 0; j < arg2; ++j)
		printf(" %02x", buf[j]);
	    printf("\n");
	}
	else if (strneq(argv[i], "sleep", 5))
	{
	    twoargs(argv[i] + 5, &arg1, &arg2, 1, 0, 255);
	    sleep(arg1);
	}
	else
	    fatal("unknown option `%s'", argv[i]);
    }
}

static void
usage(void)
{
    printf("Revoco v"VERSION" - Change the wheel behaviour of "
				    "Logitech's MX-Revolution mouse.\n\n");
    printf("Usage:\n");
    printf("  revoco free                      free spinning mode\n");
    printf("  revoco click                     click-to-click mode\n");
    printf("  revoco manual[=button[,button]]  manual mode change via button\n");
    printf("  revoco auto[=speed[,speed]]      automatic mode change (up, down)\n");
    printf("\n");
    printf("Prefixing the mode with 'temp-' (i.e. temp-free) switches the mode\n");
    printf("temporarily, otherwise it becomes the default mode after power up.\n");
    printf("\n");
    printf("Button numbers:\n");
    printf("  0 previously set button   7 wheel left tilt\n");
    printf("  3 middle (wheel button)   8 wheel right tilt\n");
    printf("  4 rear thumb button       9 thumb wheel forward\n");
    printf("  5 front thumb button     11 thumb wheel backward\n");
    printf("  6 find button            13 thumb wheel pressed\n");
    printf("\n");
    exit(0);
}

static void
trouble_shooting(void)
{
    char *path;
    int fd;

    fd = open(path = "/dev/hiddev0", O_RDWR);
    if (fd == -1 && errno == ENOENT)
	fd = open(path = "/dev/usb/hiddev0", O_RDWR);

    if (fd != -1)
	fatal("No Logitech MX-Revolution (%04x:%04x) found.",
						    LOGITECH, MX_REVOLUTION);

    if (errno == EPERM || errno == EACCES)
	fatal("No permission to access hiddev (%s-15)\n"
	      "Try 'sudo revoco ...'", path);

    fatal("Hiddev kernel driver not found.  Check with 'dmesg | grep hiddev'\n"
  "whether it is present in the kernel.  If it is, make sure that the device\n"
  "nodes (either /dev/usb/hiddev0-15 or /dev/hiddev0-15) are present.  You\n"
  "can create them with\n"
  "\n"
  "\tmkdir /dev/usb\n"
  "\tmknod /dev/usb/hiddev0 c 180 96\n"
  "\tmknod /dev/usb/hiddev1 c 180 97\n\t...\n"
  "\n"
  "or better by adding a rule to the udev database in\n"
  "/etc/udev/rules.d/10-local.rules\n"
  "\n"
  "\tBUS=\"usb\", KERNEL=\"hiddev[0-9]*\", NAME=\"usb/%k\", MODE=\"660\"\n");
}

int
main(int argc, char **argv)
{
    int handle;

    if (argc < 2)
	usage();
    if (argc > 1 && (streq(argv[1], "-h") || streq(argv[1], "--help")))
	usage();

    handle = open_dev("/dev/usb/hiddev%d");
    if (handle == -1)
	handle = open_dev("/dev/hiddev%d");
    if (handle == -1)
	trouble_shooting();

    configure(handle, argc, argv);

    close_dev(handle);
    exit(0);
}
