/*******************************************************************************
 *
 * adusbph : A simple configuration utility for the Ad-Teknik USB Phoenix / 
 *           Smartmouse interface.
 *
 * Ad-Teknik Homepage: http://www.adteknik.se
 *
 * History :
 *   Version 1.0 - Initial release.
 *
 * Copyright 2005 Jeffrey Deans <jeffrey@muspell.plus.com>
 *
 *******************************************************************************
 *
 *  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 St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 ******************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <strings.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/poll.h>
#include <sys/ioctl.h>

#define NDEBUG
#include <assert.h>

#define ASCII_ACK 0x6

/******************************************************************************
 * Program options and options parsing 
 */
typedef enum { READER_PHOENIX, READER_SMARTMOUSE } READER_MODE;

static int verbose             = 0;
static int check_firmware      = 0;
static READER_MODE reader_mode = READER_PHOENIX;
static int clock_freq          = 3580; /* 3.58MHz */
static int reset_delay         = 1;

static char serial_port[FILENAME_MAX] = "";

static struct option long_options[] =
{
  { "fwrev", 0, 0, 0 },
  { "mode",  1, 0, 0 },
  { "clock", 1, 0, 0 },
  { "reset-delay", 1, 0, 0 },
  { "verbose", 0, 0, 0 },
  { 0, 0, 0, 0 }
};


/*******************************************************************************
 * TTY handling and commands for the USB Smartmouse / Phoenix interface
 */
struct pollfd  ttypoll;
struct termios ttytermios;

/* commands sent to the interface */
static char cmd_get_fw[2]          = { '*', 'A' };
static char cmd_set_mode[3]        = { '*', 'B', 0 };
static char cmd_exit_prog[2]       = { '*', 'C' };
static char cmd_set_reset_delay[3] = { '*', 'D', 0 };

/* expected responses to the above commands */
static char rsp_ack[2];               /* NUL, ACK (NUL is due to looped-back BREAK) */
static char rsp_firmware[6];          /* * A <lo> <hi> 0x10 ACK */ 
static char rsp_set_mode[4];          /* * B <mode> ACK */
static char rsp_exit_command_mode[3]; /* * C ACK */
static char rsp_set_reset_delay[4];   /* * D <delay> ACK */


/******************************************************************************/

static void print_usage(void)
{
  fprintf(stderr, "Usage: adusbph [OPTIONS] <serial port>\n\n");
  fprintf(stderr, "  -v, --verbose                       Verbose progress information\n");
  fprintf(stderr, "  -f, --fwrev                         Get firmware revision\n");
  fprintf(stderr, "  -m, --mode  <phoenix | smartmouse>  Select phoenix or smartmouse mode\n");
  fprintf(stderr, "  -k, --clock <3580 | 3680 | 6000>    Set smartcard clock in kHz\n");
  fprintf(stderr, "  -r, --reset-delay <1 | 0>           Enable / Disable reset delay\n");
  fprintf(stderr, "\n");
  fprintf(stderr, "  <serial port> is typically /dev/ttyUSB0\n\n");
}


/*******************************************************************************
 * Functions to set the program flags from the command line arguments.
 */
static void set_check_firmware(void)
{
  check_firmware = 1;
}

static void set_mode(char *optarg)
{
  assert(NULL != optarg);
  
  if (0 == strcasecmp(optarg, "phoenix"))
  {
    reader_mode = READER_PHOENIX;
  }
  else
  {
    if (0 == strcasecmp(optarg, "smartmouse"))
    {
      reader_mode = READER_SMARTMOUSE;
    }
    else
    {
      print_usage();
      exit(EXIT_FAILURE);
    }
  }
}

static void set_clock(char *optarg)
{
  assert(NULL != optarg);

  if (0 == strcmp(optarg, "3580"))
  {
    clock_freq = 3580;
  }
  else
  if (0 == strcmp(optarg, "3680"))
  {
    clock_freq = 3680;
  }
  else
  if (0 == strcmp(optarg, "6000"))
  {
    clock_freq = 6000;
  }
  else
  {
    print_usage();
    exit(EXIT_FAILURE);
  }
}

static void set_reset_delay(char *optarg)
{
  assert(NULL != optarg);
  
  if (0 == strcmp(optarg, "0"))
  {
    reset_delay = 0;
  }
  else
  if (0 == strcmp(optarg, "1"))
  {
    reset_delay = 1;
  }
  else
  {
    print_usage();
    exit(EXIT_FAILURE);
  }
}


/******************************************************************************
 * Open requested serial port and set appropriate tty speed and termios
 * options. A serial BREAK is used to put the interface into command mode, and
 * because Tx is looped back to Rx in a Phoenix/Smartmouse, we receive this 
 * break ourselves. So we must configure the tty to ignore a BREAK, and not 
 * raise a signal - instead the BREAK is received as a single NUL which is
 * expected on entry to command mode. Also see cmd_enter_command_mode() below.
 */
static void open_port(void)
{
  ttypoll.fd = open(serial_port, (O_NOCTTY | O_RDWR));
  
  if (-1 != ttypoll.fd)
  {
    if (verbose) printf("opened \"%s\"\n", serial_port);
   
    /* Get the original tty settings, but don't restore them on
       exit in case another program needs to be run that doesn't
       set up the tty. */
    tcgetattr(ttypoll.fd, &ttytermios);

    /* Set the tty to 9600 baud */
    cfsetospeed(&ttytermios, B9600);
    cfsetispeed(&ttytermios, B9600);
    
    /* Set the tty to raw mode, and turn on modem control
       lines, and set the IGNBRK, ~BRKINT parameters so we
       receive a NUL character on a BREAK */
    cfmakeraw(&ttytermios);
    ttytermios.c_cflag |=  (CREAD | CRTSCTS | IGNBRK );
    ttytermios.c_cflag &= ~(BRKINT);
    tcsetattr(ttypoll.fd, TCSANOW, &ttytermios);
  }
  else
  {
    fprintf(stderr, "Couldn't open \"%s\" : %s\n", serial_port, strerror(errno));
    exit(EXIT_FAILURE);
  }
}


/*******************************************************************************
 * Close the port.
 */
static void close_port(void)
{
  close(ttypoll.fd);
}


/******************************************************************************
 * Read a response from the interface.
 *
 * response      : array of characters into which the response is to be 
 *                 received.
 * response_size : size of the response array.
 * timeout_ms    : time in milliseconds to wait for the requested number of 
 *                 bytes.
 */
static int read_response(char *response, int response_size, int timeout_ms)
{
  int res;
  int count = 0;
  
  ttypoll.events = POLLIN | POLLERR | POLLHUP;
  
  while (count < response_size)
  {
    res = poll(&ttypoll, 1, timeout_ms);

    if (0 == res)
    {
      if (verbose) printf("timed out getting response\n");
      return count;
    }
    if (-1 == res)
    {
      fprintf(stderr, "poll() error\n");
      return -1;
    }
    if (0 < res)
    {
      if ((res & POLLIN) == POLLIN)
      {
        if (1 == read(ttypoll.fd, &response[count], 1))
        {
          if (verbose) printf("0x%02x\n", response[count]);
          count++;
        }
      }
      else
      {
        return count;
      }
    }
  }
  return count;
}


/*******************************************************************************
 * Put the interface into command mode.
 */
static int cmd_enter_command_mode(void)
{
  /* flush (discard) pending IO */
  tcflush(ttypoll.fd, TCIOFLUSH);
  
  /* send break for 1.2s */
  ioctl(ttypoll.fd, TIOCSBRK);
  sleep(2); /* close enough to 1.2s(!) */
  ioctl(ttypoll.fd, TIOCCBRK);

  if ((sizeof(rsp_ack) == read_response(rsp_ack, sizeof(rsp_ack), 1000))
      && (rsp_ack[1] == ASCII_ACK) )
  {
    if (verbose) printf("entered command mode\n");
    return 0;
  }
  else
  {
    fprintf(stderr, "couldn't enter command mode\n");
    return -1;
  }
}

/*******************************************************************************
 * Exit from command mode.
 */
static int cmd_exit_command_mode(void)
{
  write(ttypoll.fd, cmd_exit_prog, sizeof(cmd_exit_prog));
  tcdrain(ttypoll.fd);
  
  if (sizeof(rsp_exit_command_mode) == read_response(rsp_exit_command_mode, sizeof(rsp_exit_command_mode), 1000))
  {
    if (verbose) printf("exit from command mode OK\n");
    return 0;
  }
  else
  {
    fprintf(stderr, "failed to exit from command mode\n");
    return -1;
  }
}


/*******************************************************************************
 * Read firmware version from the interface.
 */
static int cmd_check_firmware(void)
{
  /* flush (discard) pending IO */
  tcflush(ttypoll.fd, TCIOFLUSH);
  
  write(ttypoll.fd, cmd_get_fw, sizeof(cmd_get_fw));
  tcdrain(ttypoll.fd);
  
  if (sizeof(rsp_firmware) == read_response(rsp_firmware, sizeof(rsp_firmware), 1000))
  {
    if (rsp_firmware[0] == '*' && rsp_firmware[1] == 'A' && rsp_firmware[5] == ASCII_ACK)
    {
      printf("Firmware version %d.%d, Product ID 0x%x\n", 
          rsp_firmware[3], rsp_firmware[2], rsp_firmware[4]);

      return 0;
    }
    else
    {
      fprintf(stderr, "error reading firmware versions\n");
      return -1;
    }
  }
  return -1;
}


/*******************************************************************************
 * Set Phoenix / Smartmouse mode.
 */
static int cmd_set_reader_mode(void)
{
  char mode_byte = 0;

  if (reader_mode == READER_SMARTMOUSE)
  {
    mode_byte |= (1<<7);
  }

  switch (clock_freq)
  {
    case 3580:
      mode_byte |= 0x1;
      break;

    case 3560:
      mode_byte |= 0x2;
      break;

    case 6000:
      mode_byte |= 0x3;
      break;

    default:
      break;
  }
  
  cmd_set_mode[2] = mode_byte;

  /* flush (discard) pending IO */
  tcflush(ttypoll.fd, TCIOFLUSH);
  
  write(ttypoll.fd, cmd_set_mode, sizeof(cmd_set_mode));
  tcdrain(ttypoll.fd);
  
  if ((sizeof(rsp_set_mode) == read_response(rsp_set_mode, sizeof(rsp_set_mode), 1000)) &&
      (rsp_set_mode[3] == ASCII_ACK) )
  {
    if (verbose) printf("Set mode 0x%2.2x OK\n", rsp_set_mode[2]);
    return 0;
  }
  else
  {
    fprintf(stderr, "error setting mode\n");
    return -1;
  }
}


/*******************************************************************************
 * Enable or disable the reset delay.
 */
static int cmd_set_reader_reset_delay(void)
{
  cmd_set_reset_delay[2] = reset_delay;

  /* flush (discard) pending IO */
  tcflush(ttypoll.fd, TCIOFLUSH);
  
  write(ttypoll.fd, cmd_set_reset_delay, sizeof(cmd_set_reset_delay));
  tcdrain(ttypoll.fd);
  
  if ((sizeof(rsp_set_reset_delay) == read_response(rsp_set_reset_delay, sizeof(rsp_set_reset_delay), 1000)) &&
      (rsp_set_reset_delay[3] == ASCII_ACK) )
  {
    if (verbose) printf("Set reset delay 0x%2.2x OK\n", rsp_set_reset_delay[2]);
    return 0;
  }
  else
  {
    fprintf(stderr, "Error setting reset delay\n");
    return -1;
  }
}


/******************************************************************************/

int main(int argc, char **argv)
{
  int c;
  int digit_optind = 0;

  if (argc < 2)
  {
    print_usage();
    exit(EXIT_FAILURE);
  }

  while (1)
  {
    int this_option_optind = optind ? optind : 1;
    int option_index = 0;

    c = getopt_long (argc, argv, "fm:k:r:v", long_options, &option_index);

    if (-1 == c)
    {
      break;
    }
 
    switch (c)
    {
      case 0:
      {
        const char *long_name = long_options[option_index].name;
        
        if (0 == strcmp(long_name, "fwrev"))
        {
          set_check_firmware();
        }
        else
        if (0 == strcmp(long_name, "mode"))
        {
          set_mode(optarg);
        }
        else
        if (0 == strcmp(long_name, "clock"))
        {
          set_clock(optarg);
        }
        else
        if (0 == strcmp(long_name, "reset-delay"))
        {
          set_reset_delay(optarg);
        }
        else
        if (0 == strcmp(long_name, "verbose"))
        {
          verbose++;
        }
        else
        {
          printf("option %s\n", long_options[option_index].name);
        }
        break;
      }

      case 'f':
      {
        set_check_firmware();
        break;
      }
      
      case 'm':
      {
        set_mode(optarg);
        break;
      }

      case 'k':
      {
        set_clock(optarg);
        break;
      }
      
      case 'r':
      {
        set_reset_delay(optarg);
        break;
      }

      case 'v':
      {
        verbose++;
        break;
      }

      case '?':
        print_usage();
        exit(EXIT_FAILURE);
        break;

      default:
        printf ("?? getopt returned character code 0%o ??\n", c);
        break;
    }
  }

  if (optind < argc) 
  {
    strncpy(serial_port, argv[optind], FILENAME_MAX-1);
  }
  else
  {
    fprintf(stderr, "No device name specified\n");
    exit(EXIT_FAILURE);
  }
  
  open_port();

  /* Check to see if card is inserted */
  {
    int control;
    
    ioctl(ttypoll.fd, TIOCMGET, &control);

    if (!((control & TIOCM_CAR) == TIOCM_CAR))
    {
      fprintf(stderr, "DCD is high - please remove card from reader\n");
      close_port();
      exit(EXIT_FAILURE);
    }
  }

  if (-1 == cmd_enter_command_mode())
  {
    close_port();
    exit(EXIT_FAILURE);
  }

  if (check_firmware)
  {
    if (-1 == cmd_check_firmware())
    {
      close_port();
      exit(EXIT_FAILURE);
    }
  }

  if (-1 == cmd_set_reader_mode())
  {
    close_port();
    exit(EXIT_FAILURE);
  }
  else
  {
    printf("Reader mode    = %s\nCLK frequency  = %d.%d MHz\n",
        reader_mode == READER_SMARTMOUSE ? "Smartmouse" : 
            reader_mode == READER_PHOENIX ? "Phoenix" : "Unknown",
        clock_freq / 1000, (clock_freq % 1000) / 10);
  }

  if (-1 == cmd_set_reader_reset_delay())
  {
    close_port();
    exit(EXIT_FAILURE);
  }
  else
  {
    printf("Reset delay    = %s\n", reset_delay == 1 ? "Enabled" : "Disabled");
  }
  
  cmd_exit_command_mode();
  close_port();
  exit(EXIT_SUCCESS);
}

