/* -*-C-*- */
/*
 * Copyright 2001 Free Software Foundation, Inc.
 * 
 * This file is part of GNU Radio
 * 
 * GNU Radio 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, or (at your option)
 * any later version.
 * 
 * GNU Radio 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 GNU Radio; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include "mc4020.h"
#include <getopt.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <math.h>

typedef int	bool;
typedef int	tristate_t;	// TRUE / FALSE / NOT_SET

#define	TRUE	1
#define	FALSE	0
#define	NOT_SET	0xAAAA5555
#define	VOLTS_NOT_SET	(123e6)


char *default_dev = "/dev/mc4020aux0";

static void
perror_and_die (const char *msg)
{
  perror (msg);
  exit (1);
}

static bool
scale_volts (int fd, double volts, int iocmd, int *dac_value)
{
  int		r;
  int		n;
  double	range;

  if (ioctl (fd, iocmd, &r) < 0)
    perror_and_die ("GIOCGETDACxRANGE");

  switch (r){
  case MCDAC_RANGE_5V:
    range = 5.0;
    break;

  case MCDAC_RANGE_10V:
    range = 10.0;
    break;

  default:
    abort ();
  }

  if (fabs (volts) > range){
    fprintf (stderr, "%g volts is out of range [%g,%g]\n",
	     volts, -range, range);
    return FALSE;
  }
    
  n = (int) (((volts + range) * 2048 / range) + 0.5);
  if (n >= 4096)	// clamp
    n = 4095;
  if (n < 0)		// clamp
    n = 0;

  *dac_value = n;

  // printf ("%g volts -> %5d (0x%03x)\n", volts, n, n);

  return TRUE;
}


static void
usage (struct option *option)
{
  int	i;
  int	len = 0;
  
  fprintf (stderr,
   "usage: mc4020-set-dac [-d <device>] [-0 <volts>] [-1 <volts>] [-h] [-v]\n");
  for (i = 0; option[i].name != 0; i++){
    if (len == 0){
      fprintf (stderr, "\t");
      len = 8;
    }
    fprintf (stderr, "[--%s", option[i].name);
    len += strlen (option[i].name) + 1;
    switch (option[i].has_arg){

    default:
    case no_argument:
      fprintf (stderr, "] ");
      len += 2;
      break;

    case required_argument:
      fprintf (stderr, "=<value>] ");
      len += 10;
      break;

    case optional_argument:
      fprintf (stderr, "[=<value>]]");
      len += 12;
      break;
    }
    if (len >= 64){
      fprintf (stderr, "\n");
      len = 0;
    }
  }
  if (len != 0)
    fprintf (stderr, "\n");

  fprintf (stderr, 
"\nThe -0 -1 --dac0 and --dac1 options expect their arguments to be in units\n"
"of volts.  It's a good idea to explicitly specify the range with the\n"
"--5v --10v --dac0-range-5v --dac0-range-10v --dac1-range-5v or \n"
"--dac1-range-10v options.  The --5v and --10v set the ranges on both\n"
"dacs.  The --dac0-unscaled and --dac1-unscaled options allow you to specify\n"
"an integer value which is directly written to the respective dac\n\n"
"Example: to set dac0 to -4.35V and dac1 to 2.95V use:\n\n"
"  $ set-dac --5v --dac0 -4.35 --dac1 2.95\n\n"
	   );
}


struct optvals {
  int		disable;		// --disable
  tristate_t	dac0_range_5v;		// --dac0-range-5v | dac0-range-10v
  tristate_t	dac1_range_5v;		// --dac1-range-5v | dac1-range-10v
  tristate_t	both_range_5v;		// --5v
  int		dac0_unscaled;
  double	dac0_volts;
  int		dac1_unscaled;
  double	dac1_volts;
  int		verbose;
  int		help;
  char		*device;
};

#define	DAC0_UNSCALED	0x10000
#define	DAC1_UNSCALED	0x10001

// return pointer to statically allocated optvals struct iff successful,
// else NULL

struct optvals *
parse_args (int argc, char **argv)
{
  static struct optvals	opt;
  static struct option long_options[] = {
    // these options set a flag
    { "disable",       no_argument,	&opt.disable, TRUE },
    { "dac0-range-5v", no_argument,	&opt.dac0_range_5v, TRUE },
    { "dac0-range-10v",no_argument,	&opt.dac0_range_5v, FALSE },
    { "dac1-range-5v", no_argument,	&opt.dac1_range_5v, TRUE },
    { "dac1-range-10v",no_argument,	&opt.dac1_range_5v, FALSE },
    { "5v",            no_argument,     &opt.both_range_5v, TRUE },
    { "10v",           no_argument,     &opt.both_range_5v, FALSE },
    { "help",	       no_argument,	&opt.help, TRUE },
    { "verbose",       no_argument,	&opt.verbose, TRUE },
    // these options take an arg
    { "device",        required_argument, 0, 'd' },
    { "dac0-unscaled", required_argument, 0, DAC0_UNSCALED },
    { "dac1-unscaled", required_argument, 0, DAC1_UNSCALED },
    { "dac0", 	       required_argument, 0, '0' },
    { "dac1", 	       required_argument, 0, '1' },
    // end marker
    { 0, 0, 0, 0 }
  };

  int		c;

  memset (&opt, 0, sizeof (opt));
  
  opt.disable = FALSE;
  opt.dac0_range_5v = NOT_SET;
  opt.dac1_range_5v = NOT_SET;
  opt.both_range_5v = NOT_SET;
  opt.dac0_unscaled = NOT_SET;
  opt.dac0_volts = VOLTS_NOT_SET;
  opt.dac1_unscaled = NOT_SET;
  opt.dac1_volts = VOLTS_NOT_SET;
  opt.verbose = FALSE;
  opt.help = FALSE;
  opt.device = default_dev;
  
  while (1){
    int    option_index = 0;
    char  *endptr;

    c = getopt_long (argc, argv, "vhd:0:1:",
		     long_options, &option_index);

    if (c == -1)	// end of options
      break;

    switch (c){
    case 0:
      // if this option sets a flag, do nothing else now.
      break;

    case 'v':
      opt.verbose = TRUE;
      break;
      
    case 'h':
      opt.help = TRUE;
      break;
      
    case 'd':
      opt.device = optarg;
      break;

    case '0':
      opt.dac0_volts = strtod (optarg, &endptr);
      if (endptr == optarg || *endptr != 0){
	fprintf (stderr, "invalid voltage: %s\n", optarg);
	return 0;
      }
      break;

    case '1':
      opt.dac1_volts = strtod (optarg, &endptr);
      if (endptr == optarg || *endptr != 0){
	fprintf (stderr, "invalid voltage: %s\n", optarg);
	return 0;
      }
      break;

    case DAC0_UNSCALED:
      opt.dac0_unscaled = strtol (optarg, &endptr, 0);
      if (endptr == optarg || *endptr != 0){
	fprintf (stderr, "invalid integer: %s\n", optarg);
	return 0;
      }
      break;

    case DAC1_UNSCALED:
      opt.dac1_unscaled = strtol (optarg, &endptr, 0);
      if (endptr == optarg || *endptr != 0){
	fprintf (stderr, "invalid integer: %s\n", optarg);
	return 0;
      }
      break;
      
    case '?':
      // getopt_long already printed an error message
      usage (long_options);
      return 0;

    default:
      abort ();
    }
  }

  if (opt.help){
    usage (long_options);
    return 0;
  }

  if (opt.both_range_5v != NOT_SET){
    if (opt.dac0_range_5v != NOT_SET || opt.dac1_range_5v != NOT_SET){
      usage (long_options);
      return 0;
    }
    opt.dac0_range_5v = opt.both_range_5v;
    opt.dac1_range_5v = opt.both_range_5v;
  }

  if (opt.dac0_unscaled != NOT_SET && opt.dac0_volts != VOLTS_NOT_SET){
    usage (long_options);
    return 0;
  }

  if (opt.dac1_unscaled != NOT_SET && opt.dac1_volts != VOLTS_NOT_SET){
    usage (long_options);
    return 0;
  }

  if (optind != argc){
    usage (long_options);
    return 0;
  }
  
  return &opt;
}

int 
main (int argc, char **argv)
{
  int			fd;
  struct optvals	*opt;


  opt = parse_args (argc, argv);
  if (opt == 0)
    return 1;

  fd = open (opt->device, O_RDONLY);
  if (fd < 0)
    perror_and_die (opt->device);

  // if disable is specified, do only that.

  if (opt->disable){
    if (ioctl (fd, GIOCENABLEDACS, 0) < 0)
      perror_and_die ("GIOCENABLEDACS");
    return 0;
  }

  // set ranges if required

  if (opt->dac0_range_5v != NOT_SET){
    if (ioctl (fd, GIOCSETDAC0RANGE,
	       opt->dac0_range_5v ? MCDAC_RANGE_5V : MCDAC_RANGE_10V) < 0)
      perror_and_die ("GIOCSETDAC0RANGE");
  }

  if (opt->dac1_range_5v != NOT_SET){
    if (ioctl (fd, GIOCSETDAC1RANGE,
	       opt->dac1_range_5v ? MCDAC_RANGE_5V : MCDAC_RANGE_10V) < 0)
      perror_and_die ("GIOCSETDAC1RANGE");
  }

  // if unscaled values are specified, just jam them in...

  if (opt->dac0_unscaled != NOT_SET){
    if (ioctl (fd, GIOCWRITEDAC0, opt->dac0_unscaled) < 0)
      perror_and_die ("GIOCWRITEDAC0");
  }

  if (opt->dac1_unscaled != NOT_SET){
    if (ioctl (fd, GIOCWRITEDAC1, opt->dac1_unscaled) < 0)
      perror_and_die ("GIOCWRITEDAC1");
  }

  // if values in volts are specified scale them and set.

  if (opt->dac0_volts != VOLTS_NOT_SET){
    int	n;
    if (!scale_volts (fd, opt->dac0_volts, GIOCGETDAC0RANGE, &n))
      exit (1);
    if (ioctl (fd, GIOCWRITEDAC0, n) < 0)
      perror_and_die ("GIOCWRITEDAC0");
  }

  if (opt->dac1_volts != VOLTS_NOT_SET){
    int	n;
    if (!scale_volts (fd, opt->dac1_volts, GIOCGETDAC1RANGE, &n))
      exit (1);
    if (ioctl (fd, GIOCWRITEDAC1, n) < 0)
      perror_and_die ("GIOCWRITEDAC1");
  }

  // finally, ensure that dacs are enabled

  if (ioctl (fd, GIOCENABLEDACS, 1) < 0)
    perror_and_die ("GIOCENABLEDACS");

  return 0;
}
