/* usbhotkey: Turn USB keyboard events into X11 keyboard events.                   */   
/* Copyright (C) 2007  Lars Krueger                                                */   
/*                                                                                 */
/* This program is free software; you can redistribute it and/or modify            */   
/* it under the terms of the GNU General Public License as published by            */   
/* the Free Software Foundation; either version 2 of the License, or               */   
/* (at your option) any later version.                                             */   
/*                                                                                 */
/* This program is distributed in the hope that it will be useful,                 */   
/* but WITHOUT ANY WARRANTY; without even the implied warranty of                  */   
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                   */   
/* GNU General Public License for more details.                                    */   
/*                                                                                 */
/* You should have received a copy of the GNU General Public License               */   
/* along with this program; if not, write to the Free Software                     */   
/* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA  */   

/* Portions of this code (overlay image) are ported from xteddy-2.0.1. 
 *
 * The ruby specific code is taken from various tutorials and forum entries
 * from all over the net.  */

#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>

#include <config.h>
/* Undef the symbols that may collide with ruby. */
#undef PACKAGE_NAME
#undef PACKAGE_VERSION
#undef PACKAGE_STRING
#undef PACKAGE_TARNAME

#include <hid.h>

#include <ruby.h>

#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>

#include "constants.h"

/* Uncomment this to enable packet debugging. */
/*  #define DEBUG_PACKET */

/*################################# variables ################################*/
/*=========================== HID device interface ===========================*/
/** @name HID device interface
 *
 */
/*@{*/
/** Handle for opened Human Interface Device */
static HIDInterface * theHid;

/** Set non-zero if ruby script called the "open device" function  */
static unsigned theHidIsOpen=0;

/** Set by signal handler to quit the program. */
static unsigned thePrgShouldQuit=0;

/** Number of the device endpoint.  */
static unsigned theEndPoint;
/*@}*/

/*=========================== USB keyboard packets ===========================*/
/** @name USB keyboard packets
 * We hardcode the packet length here, although it is theoretically possible
 * that a USB keyboard sends other packets.
 *
 * Each USB packet consists of a byte indicating the state of the shift, ctrl,
 * alt, and windows key. The next byte is reserved.
 * Then 6 bytes follow that hold the the ids of the currently pressed keys.
 * Such a packet is sent every time a key is pressed or released.
 */
/*@{*/

/** Number of usable bytes for keycode */
#define  thePacketLen 6

/** Storage space for 2 packets: the last one seen and that of the current
 * event */
static char thePktBuf[2][2+thePacketLen];

/** Points to current packet */
static char * thePacket=thePktBuf[0];
/** Points to packet of last event. */
static char * theLastPacket=thePktBuf[1];

/** The time of the last event. */
static struct timeval theLastTime;
/*@}*/

/*====================== ruby variables and callback ids =====================*/
/** @name ruby access
 *
 * In order to access ruby from C, we need to store function ids and pointer
 * to variables.
 */
/*@{*/

/** Id of the ruby function "UHK_keyup"  */
static ID theKeyUpFcn;

/** Id of the ruby function "UHK_keydn"  */
static ID theKeyDnFcn;

/** Id of the ruby function "UHK_mark" */
static ID theMarkFcn=0;
/*@}*/

/*==================================== X11 ===================================*/
/** @name X11 
 *
 */
/*@{*/

/** X11 server connection */
static Display * the_display=NULL;

/** Number of milliseconds to wait after a keypress was sent. */
#define INTERKEY_DELAY  0

/*@}*/

/*############################## infrastructure ##############################*/
/** @name Infrastructure
 *
 */
/*@{*/

/** Displays error message and help. */
static void 
errorHelp( const char * msg)
{
  fprintf( stderr, "usbhotkey: %s\n", msg);
  fputs( 
      "usbhotkey: Intercept USB keyboard events and send them to X.\n"
      "USAGE: usbhotkey [OPTIONS] <script>\n"
      "\n"
      "Options:\n"
      "  -h\n"
      "  --help -- Display help.\n"
      "\n"
      "Parameters:\n"
      "  <script> -- Ruby script containing the code to run on keypress.\n"
      , stderr);
      /*  "    -- %%%\n" */

  exit( EXIT_FAILURE);
}

/** Signal handler, set a flag to quit. */
static void 
quitMe( 
    int sigNr)
{
  thePrgShouldQuit=1;
}

/** Check for a ruby exception and print some help. */
static void 
handle_ruby_error(
    int status)
{
  if( status)
  {
    unsigned c;
    VALUE lasterr = rb_gv_get("$!");
    VALUE message = rb_obj_as_string(lasterr);
    VALUE ary = rb_funcall( ruby_errinfo, rb_intern("backtrace"), 0);

    if( ! strcmp(RSTRING(message)->ptr, "exit"))
    {
      quitMe(1);
      return;
    }

    printf( "usbhotkey: %s\n", RSTRING(message)->ptr);
    printf( "usbhotkey: Backtrace:\n");
    for( c=0; c<RARRAY(ary)->len; c++) 
      printf( "usbhotkey: \tfrom %s\n", RSTRING(RARRAY(ary)->ptr[c])->ptr);
    errorHelp( "Error executing start script\n");
  }
}

/*@}*/

/*############################ HID device handling ###########################*/
/** @name HID device interface
 *
 */
/*@{*/

/** Ruby callback to open a device given its vendor and product id.  */
static VALUE 
connectHid( 
    VALUE self, 
    VALUE vendorIdVal, 
    VALUE productIdVal,
    VALUE endPointVal)
{
  if( theHidIsOpen)
    rb_raise( rb_eRuntimeError, "HID already connected.");

  {
    unsigned vendorId=NUM2UINT(vendorIdVal);
    unsigned productId=NUM2UINT( productIdVal);
    theEndPoint=NUM2UINT( endPointVal);

    HIDInterfaceMatcher matcher = { vendorId, productId, NULL, NULL, 0 };

    printf( "usbhotkey: Connecting to %04x:%04x\n", vendorId, productId);

    hid_return ret=hid_force_open( theHid, 0, &matcher, 3);
    if( ret!=HID_RET_SUCCESS)
      rb_raise( rb_eRuntimeError, "Can't connect HID.");

    /*    hid_dump_tree( stdout, theHid);
    */

    theHidIsOpen=1;
  }

  return self;
}

/** Closes HID device and X11 connection. */
static void 
closeAll()
{
  if( !theHid)
    return;
  if( theHidIsOpen)
  {
    hid_close( theHid);
    theHidIsOpen=0;
  }
  hid_delete_HIDInterface( &theHid);
  hid_cleanup();

  if( the_display)
  {
    XCloseDisplay( the_display);
    the_display=NULL;
  }
}
/*@}*/

/*########################### USB packet processing ##########################*/

/** Check all keys in test that are not in set and call ruby function for
 * them. */
static void 
process_USB_keys( 
    char * test,
    char * set,
    ID fcn)
{
  unsigned i, j;

  test+=2;
  set+=2;

  for( i=0; i<thePacketLen; ++i)
  {
    if( test[i])
    {
      unsigned found=0;
      for( j=0; j<thePacketLen; ++j)
      {
        if( test[i]==set[j])
        {
          found=1;
          break;
        }
      }
      if( !found)
        rb_funcall( rb_mKernel, fcn, 1, INT2FIX( test[i]));
    }
  }
}

/** Check all shift key bits in test that are not in set and call ruby
 * function for them. */
static void
process_USB_shift_keys(
  unsigned char test,
  unsigned char set,
  ID fcn)
{
  unsigned char mask;
  int i;
  for( i=0; i<8; ++i)
  {
    mask = 1 << i;
    if( test & mask)
    {
      if( (set & mask) == 0)
        rb_funcall( rb_mKernel, fcn, 1, INT2FIX( -i-1));
    }
  }
}

/** Process the current packet. This function calls ruby functions which might
 * raise an exception. In this case the remainder of the function is not
 * executed. */
static VALUE
handlePacket(
    VALUE args)
{
  /* Get the time since the last event. */
  struct timeval thisTime;
  int timeElapsed;
  gettimeofday( &thisTime, NULL);
  timeElapsed=1000*(thisTime.tv_sec-theLastTime.tv_sec)+(thisTime.tv_usec-theLastTime.tv_usec)/1000;

  /* Call the mark function indicating the start of the release sequence. */
  rb_funcall( rb_mKernel, theMarkFcn, 2, INT2FIX( MARK_START_RELEASE), INT2FIX( timeElapsed));

  /* Call the key-up function for those keys that are down, but not in
   * packet anymore. */
  process_USB_keys( theLastPacket, thePacket, theKeyUpFcn);

  /* Check the release of shift keys. */
  process_USB_shift_keys( theLastPacket[0], thePacket[0], theKeyUpFcn);

  /* Call the mark function indicating the end of the release sequence. */
  rb_funcall( rb_mKernel, theMarkFcn, 2, INT2FIX( MARK_END_RELEASE_START_PUSH), INT2FIX( timeElapsed));

  /* Check the press of shift keys. */
  process_USB_shift_keys( thePacket[0], theLastPacket[0], theKeyDnFcn);

  /* Call the key-down function for the keys that are down. */
  process_USB_keys( thePacket, theLastPacket, theKeyDnFcn);

  /* Call the mark function indicating the end of the push sequence. */
  rb_funcall( rb_mKernel, theMarkFcn, 2, INT2FIX( MARK_END_PUSH), INT2FIX( timeElapsed));

  theLastTime=thisTime;

  return 0;
}

/*############################ X11 key generation ############################*/
/** Send X11 key press event. */
static VALUE
x11_keydn( 
    VALUE self,
    VALUE keycode)
{
  unsigned kc=FIX2UINT( keycode);
#ifdef DEBUG_RUBY_IF
  printf( "usbhotkey: x11_keydn_norep( %d)\n", kc);
#endif
  XTestFakeKeyEvent( the_display, XKeysymToKeycode( the_display, kc), True, INTERKEY_DELAY);
  return 0;
}

/** Send X11 key release event. */
static VALUE
x11_keyup(
    VALUE self,
    VALUE keycode)
{
  unsigned kc=FIX2UINT( keycode);
#ifdef DEBUG_RUBY_IF
  printf( "usbhotkey: x11_keyup( %d)\n", kc);
#endif
  XTestFakeKeyEvent( the_display, XKeysymToKeycode( the_display, kc), False, INTERKEY_DELAY);
  return 0;
}

/*################################### Main ###################################*/
int 
main( 
    int argc, 
    char * * argv)
{
  int status=0;
  char * script;

  hid_return ret;

  /* Process commandline */
  int opti=1;
  while( opti<argc)
  {
    if( !strcmp( argv[opti], "-h") || !strcmp( argv[opti], "--help"))
      errorHelp( "Display help text.");
    else
      break;
  }

  if( opti>=argc)
    errorHelp( "No script given.");
  script=argv[opti];

  /* Init libhid */
  hid_init();
  hid_set_debug(HID_DEBUG_NONE);
  hid_set_debug_stream(stderr);
  hid_set_usb_debug(1000);

  theHid=hid_new_HIDInterface();
  atexit( closeAll);
  signal( SIGINT, quitMe);
  signal( SIGTERM, quitMe);

  memset( theLastPacket, 0, thePacketLen+2);

  /* Init X11 connection and Test extension. */
  the_display=XOpenDisplay( NULL);
  if( !the_display)
    errorHelp( "Can't open X11 connection.");

  /* Init ruby */
  ruby_init();
  ruby_init_loadpath();

  /* Provide additional methods. */
  rb_define_global_function( "UHK_connect", connectHid, 3);
  rb_define_global_function( "UHK_keydn", x11_keydn, 1);
  rb_define_global_function( "UHK_keyup", x11_keyup, 1);

  theKeyUpFcn=rb_intern( "UHK_USB_keyup");
  theKeyDnFcn=rb_intern( "UHK_USB_keydn");
  theMarkFcn= rb_intern( "UHK_USB_mark");

  create_mark_constants();
  create_usb_constants();
  create_win_constants();

  /* Load ruby script */
  printf( "usbhotkey: Loading script %s\n", script);
  rb_load_protect(rb_str_new2(script), 0, &status);
  handle_ruby_error( status);

  /* Perform USB and X11 event processing. */
  if( theHidIsOpen)
  {
    printf( "usbhotkey: Starting event loop\n");

    gettimeofday( &theLastTime, NULL);
    theLastTime.tv_sec-=10;

    /* Event loop. */
    while( !thePrgShouldQuit)
    {
      memset( thePacket, 0, thePacketLen+2);

      /* Read a packet. */
      ret = hid_interrupt_read( theHid, theEndPoint, thePacket, thePacketLen+2, 200);
      
      if( ret == HID_RET_SUCCESS )
      {
        XTestGrabControl( the_display,True);
#ifdef DEBUG_PACKET
        unsigned i;
        printf( "usbhotkey: Packet received: ");
        for( i=0; i<thePacketLen+2; ++i)
          printf( "%02x ", thePacket[i]);
        printf( "\n");
#endif

        /* Find set differences and call ruby functions. */
        rb_protect( handlePacket, 0, &status);
        handle_ruby_error( status);

        /* Switch roles of packet and last packet. */
        {
          char * h=thePacket;
          thePacket=theLastPacket;
          theLastPacket=h;
        }
        XTestGrabControl( the_display, False);
        XSync( the_display, False);
      }
      else
#ifdef LIBHID_HAS_TIMEOUT
        if( ret==HID_RET_TIMEOUT)
#endif
      {
        /* Call the mark function indicating an idle event. */
        {
          /* Get the time since the last event. */
          struct timeval thisTime;
          int timeElapsed;
          gettimeofday( &thisTime, NULL);
          timeElapsed=1000*(thisTime.tv_sec-theLastTime.tv_sec)+(thisTime.tv_usec-theLastTime.tv_usec)/1000;
          XTestGrabControl( the_display,True);
          rb_funcall( rb_mKernel, theMarkFcn, 2, INT2FIX( MARK_IDLE), INT2FIX( timeElapsed));
          XTestGrabControl( the_display, False);
          XSync( the_display, False);
          theLastTime=thisTime;
        }
      }
#ifdef LIBHID_HAS_TIMEOUT
        else
          break;
#endif
    }
  }
  else
  {
    printf( "usbhotkey: No device opened by script. Exiting.\n");
  }

  closeAll();

  return EXIT_SUCCESS;
}

  /* else if( !strcmp( argv[opti], "-%%%"))               */
  /* {                                                    */
  /*   ++opti;                                            */
  /*   %%%                                                */
  /* }                                                    */

  /* else if( !strcmp( argv[opti], "-%%%"))               */
  /* {                                                    */
  /*   ++opti;                                            */
  /*   if( opti>=argc)                                    */
  /*     errorHelp( "Missing argument after -%%%");       */
  /*                                                      */
  /*   %%% = %%% argv[opti++] %%% ;                       */
  /* }                                                    */



syntax highlighted by Code2HTML, v. 0.9.1