#!/usr/bin/python
#
#  pymouse -- PyMouse ( ExplorerPS/2 emulator from an USB pointer device )
#
#  Copyright (C) 2002  Frederic Jolliton -- <pymouse@tuxee.net>
#
#  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., 675 Mass Ave, Cambridge, MA 02139, USA.
#

#-----------------------------------------------------------------------------
#
# WARNING: This is a big HACK and doesn't emulate all the PS/2 feature,
# BUT this work very well for me for my Logitech Cordless Trackman Optical
# trackball.
#
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
#
# Most of the information needed to build this program come from
# this page:
#
# http://panda.cs.ndsu.nodak.edu/~achapwes/PICmicro/mouse/mouse.html
#
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
#
# TODO: If possible use the three small buttons arround the wheel
#       as real buttons (numbered 8 9 10)
#
#-----------------------------------------------------------------------------

# Config module
# This is the file that will be symlinked to the real pseudo-device
# at runtime.
aliasDevice = '/tmp/mouse'

# The event source
usbMouse = '/dev/input/event%s'


#-----------------------------------------------------------------------------
#
# PyMouse must be started *BEFORE* X Window.
#
#
# Configure XWindow by editing /etc/X11/XF86Config with:
#
# + - - - - - - - - - - - - - - - - - - - - - -
# : Section "InputDevice"
# :    Identifier  "Mouse1"
# :    Driver      "mouse"
# :    Option      "Device"       "/tmp/mouse"
# :    Option      "Protocol"     "ExplorerPS/2"
# :    Option      "Buttons"      "7"
# :    Option      "ZAxisMapping" "6 7"
# :  [...]
# : EndSection
# + - - - - - - - - - - - - - - - - - - - - - -
#
# And modify your xmodmap file or add the following line to your .xinitrc (or
# any other file executed when X Window start) to reorder buttons number
# (otherwise you can not use correctly the new buttons or lose the wheel
# feature):
#
# xmodmap -e "pointer = 1 2 3 6 7 4 5"
#
# Start X Window, and check with the 'xev' command that your mouse is
# now sending correct button event.
#
#-----------------------------------------------------------------------------

import struct
import pty
import os
import sys
import select
import signal
import time

#
# Open a new PTY master
#
def openPty() :

  fdPty , slave = pty.openpty()
  # Retrieve the filename from the file descriptor
  slave = os.readlink( '/proc/self/fd/%s' % slave )
  os.system( 'stty cs8 -icanon min 1 time 0 -isig -xcase -inpck -echo < %s' % slave )
  try :
    os.unlink( pymouseconf.aliasDevice )
  except OSError :
    pass
  os.symlink( slave , pymouseconf.aliasDevice )
  os.chmod( slave , 0666 )
  return fdPty

#
# Read an USB event
#
# Return ( unixTime , unixTimeMsec , eventType , eventCode , value )
#
def readEvent( fdInput ) :

  return struct.unpack( "LLHHl" , fdInput.read( 16 ) )

#
# Output an PS/2 event
#
# If wheel == None then output a 3 bytes data packet,
# otherwise output a 4 bytes data packet.
#
def produceEvent( fdPty , button , dx , dy , wheel ) :

  if not dataReporting : return
  while dx < 0 : dx += 512
  while dy < 0 : dy += 512
  if wheel == None :
    msg = struct.pack( '@BBB' ,
      ( ( dy >> 3 ) & 32 ) + ( ( dx >> 4 ) & 16 ) + 8 + ( button & 7 ) ,
      dx & 255 ,
      dy & 255 )
  else :
    while wheel < 0 : wheel += 256
    msg = struct.pack( '@BBBB' ,
      ( ( dy >> 3 ) & 32 ) + ( ( dx >> 4 ) & 16 ) + 8 + ( button & 7 ) ,
      dx & 255 ,
      dy & 255 ,
      ( ( button << 1 ) & 48 ) + ( wheel & 15 ) )
  os.write( fdPty , msg )

EVENT_CLICK = 1
EVENT_MOVE = 2

X_AXIS = 0
Y_AXIS = 1
WHEEL_AXIS = 8

# Map code to button number
button = {
  272 : 0 ,
  273 : 1 ,
  274 : 2 ,
  275 : 3 ,
  276 : 4
}

#
# We use a small automate to check if client is trying to
# activate the ExplorerPS/2 protocol. So, the amRate function
# is called with the rate value, and if 3 consecutive setting
# for this value are [ 200 , 200 , 80 ] then we activate
# the ExplorerPS/2 protocol.
#
amRateState = 0

def amRate( rate = None ) :

  global amRateState
  if rate == None : return amRateState

  if amRateState == 0 :
    if rate == 200 : amRateState = 1
  elif amRateState == 1 :
    if rate == 200 : amRateState = 2
    else : amRateState = 0
  elif amRateState == 2 :
    if rate == 80 : amRateState = 3
    elif rate == 200 : amRateState = 1
    else : amRateState = 0
  elif amRateState == 3 :
    if rate == 80 : amRateState = 0
    elif rate == 200 : amRateState = 1
    else : amRateState = 0
  return amRateState

def signalUsr1( signalNumber , stackFrame ) :

  global reopenInput
  reopenInput = 1

def openMouse() :

  fdInput = None
  devFilename = None
  while 1 :
    for n in range( 15 ) :
      try :
        devFilename = pymouseconf.usbMouse % n
        fdInput = open( devFilename )
      except :
        pass
      if fdInput != None : break
    if fdInput != None : break
    print 'Unable to open mouse. Retrying in 1 second.'
    sys.stdout.flush()
    time.sleep( 1 )
  return fdInput , devFilename

signal.signal( signal.SIGUSR1 , signalUsr1 )

# Indicate if data must be generated
dataReporting = None

# Indicate if input must be reopened
reopenInput = None

fdInput = None
def main() :

  global dataReporting , fdInput , reopenInput
  imps2 = 0
  imps2Misc = None
  nextAuto = time.time() + 10

  fdInput , devFilename = openMouse()
  fdPty = openPty()
  print 'PyMouse 0.0 started'
  print 'USB device is %s' % devFilename
  print '(SIGUSR1 reopen the input)'
  sys.stdout.flush()
  buttonPressed = 0
  while 1 :
    try :
      r , w , x = select.select( [ fdInput , fdPty ] , [] , [] , 1 )
    except select.error , e :
      if e[ 0 ] != 4 : raise
      r , w , x = [] , [] , []

    if nextAuto <= time.time() :
      print 'AutoReopen after 10 seconds of inactivity'
      sys.stdout.flush()
      reopenInput = 1
      nextAuto = time.time() + 10

    if fdPty in r :
      cmd = os.read( fdPty , 1 )
      if cmd == '\xff' : # Reset
        os.write( fdPty , '\xfa' )
      elif cmd == '\xfe' : # Resend
        # FIXME: Must resend the last packet
        pass
      elif cmd == '\xf6' : # Set Defaults
        os.write( fdPty , '\xfa' )
      elif cmd == '\xf5' : # Disable Data Reporting
        os.write( fdPty , '\xfa' )
        dataReporting = 0
      elif cmd == '\xf4' : # Enable Data Reporting
        os.write( fdPty , '\xfa' )
        dataReporting = 1
      elif cmd == '\xf3' : # Set Sample Rate
        os.write( fdPty , '\xfa' )
        rate = os.read( fdPty , 1 )
        rate = ord( rate )
        if amRate( rate ) == 3 :
          imps2 = 1
          imps2Misc = 0
        os.write( fdPty , '\xfa' )
      elif cmd == '\xf2' : # Get Device ID
        if imps2 :
          os.write( fdPty , '\xfa\x04' ) # device id
        else :
          os.write( fdPty , '\xfa\x00' ) # device id
      elif cmd == '\xf0' : # Set Remote Mode
        os.write( fdPty , '\xfa' )
      elif cmd == '\xee' : # Set Wrap Mode
        os.write( fdPty , '\xfa' )
      elif cmd == '\xec' : # Reset Wrap Mode (back to remote or stream mode)
        os.write( fdPty , '\xfa' )
      elif cmd == '\xeb' : # Read Data
        os.write( fdPty , '\xfa' )
        # FIXME: Send movement packet
      elif cmd == '\xea' : # Set Stream Mode
        os.write( fdPty , '\xfa' )
      elif cmd == '\xe9' : # Status Request
        os.write( fdPty , '\xfa' )
        # FIXME: Send status packet
      elif cmd == '\xe8' : # Set Resolution
        os.write( fdPty , '\xfa' )
        os.read( fdPty , 1 ) # discard resolution value
        os.write( fdPty , '\xfa' )
      elif cmd == '\xe7' : # Set Scaling 2:1
        os.write( fdPty , '\xfa' )
      elif cmd == '\xe6' : # Set Scaling 1:1
        os.write( fdPty , '\xfa' )

    if fdInput in r :
      nextReopen = time.time() + 10
      unixTime , unixTimeMsec , eventType , eventCode , data = readEvent( fdInput )
      if eventType == EVENT_CLICK :
        buttonNumber = button.get( eventCode )
        if buttonNumber != None :
          if data == 0 : buttonPressed &= ~( 1 << buttonNumber )
          elif data == 1 : buttonPressed |= ( 1 << buttonNumber )
        produceEvent( fdPty , buttonPressed , 0 , 0 , imps2Misc )
      elif eventType == EVENT_MOVE :
        if eventCode == X_AXIS :
          produceEvent( fdPty , buttonPressed , data , 0 , imps2Misc )
        elif eventCode == Y_AXIS :
          produceEvent( fdPty , buttonPressed , 0 , -data , imps2Misc )
        elif eventCode == WHEEL_AXIS and imps2 :
          produceEvent( fdPty , buttonPressed , 0 , 0 , -data )

    if reopenInput :

      reopenInput = None

      fdInput.close()
      fdInput = None

      print 'Input closed (%s)' % devFilename
      sys.stdout.flush()

      reload( pymouseconf )

      fdInput , dev = openMouse()
      print 'Input opened (%s)' % devFilename
      sys.stdout.flush()

try :
  main()
except KeyboardInterrupt :
  print 'Bye'

os.unlink( pymouseconf.aliasDevice )