//$Id: NativeSerial_linux.cpp,v 1.7 2010-06-29 22:07:42 scipio Exp $

/* Copyright (c) 2000-2003 The Regents of the University of California.  
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the
 *   distribution.
 * - Neither the name of the copyright holder nor the names of
 *   its contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

//@author Cory Sharp <cssharp@eecs.berkeley.edu>

#include <stdexcept>
#include <sstream>
#include <iostream>
#include <fstream>

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termio.h>
#include <termios.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

#include "NativeSerialEnums.h"
using namespace NativeSerialEnums;


class comm_port_error : public std::runtime_error
{
  public:
    comm_port_error( const char* msg ): std::runtime_error(msg) { }
};


class NativeSerial
{
public:
  typedef std::string String;

private:

  std::string m_portname;
  int m_fd;
  int m_events_in;
  int m_events_out;
  bool m_wait_for_events;

protected:

  void note( std::string s )
  {
    //std::cout << "NativeSerial_linux " << m_portname << ": " << s << std::endl;
  }

  String cat( const char* prefix, const String& err )
  {
    return (prefix == NULL ? "" : String(prefix)+": ") + err;
  }

  void errno_wrap( bool error, const char* extra_err = NULL )
  {
    if( error && (errno != 0) )
      throw comm_port_error( cat(extra_err, strerror(errno)).c_str() );
  }

  void block_on_read( bool block )
  {
note( "block_on_read begin" );
    fcntl( m_fd, F_SETFL, (block ? 0 : FNDELAY) );
note( "block_on_read end" );
  }

  struct termios get_comm_state()
  {
note( "get_comm_state begin" );
    struct termios options;
    errno_wrap( tcgetattr( m_fd, &options ) == -1, "get_comm_state" );
note( "get_comm_state end" );
    return options;
  }


  int get_modem_status()
  {
note( "get_modem_status begin" );
    int status = 0;
    errno_wrap( ioctl( m_fd, TIOCMGET, &status ) == -1, "get_modem_status" );
note( "get_modem_status end" );
    return status;
  }

  void set_modem_status( int status )
  {
note( "set_modem_status begin" );
    errno_wrap( ioctl( m_fd, TIOCMSET, &status ) == -1, "set_modem_status" );
note( "set_modem_status end" );
  }

  int baud_to_enum( int baud )
  {
    switch( baud )
    {
      case 0: return B0;
      case 50: return B50;
      case 75: return B75;
      case 110: return B110;
      case 134: return B134;
      case 150: return B150;
      case 200: return B200;
      case 300: return B300;
      case 600: return B600;
      case 1200: return B1200;
      case 1800: return B1800;
      case 2400: return B2400;
      case 4800: return B4800;
      case 9600: return B9600;
      case 19200: return B19200;
      case 38400: return B38400;
      case 57600: return B57600;
      case 115200: return B115200;
      case 230400: return B230400;
    }
    throw comm_port_error("baud_to_enum, bad baud rate");
  }

  int enum_to_baud( int baudenum )
  {
    switch( baudenum )
    {
      case B0: return 0;
      case B50: return 50;
      case B75: return 75;
      case B110: return 110;
      case B134: return 134;
      case B150: return 150;
      case B200: return 200;
      case B300: return 300;
      case B600: return 600;
      case B1200: return 1200;
      case B1800: return 1800;
      case B2400: return 2400;
      case B4800: return 4800;
      case B9600: return 9600;
      case B19200: return 19200;
      case B38400: return 38400;
      case B57600: return 57600;
      case B115200: return 115200;
      case B230400: return 230400;
    }
    throw comm_port_error("enum_to_baud, bad baud rate");
  }

/*
  static DWORD map_events_to_win32( int event )
  {
    DWORD ev = 0;
    if( event & DATA_AVAILABLE ) ev |= EV_RXCHAR;
    if( event & OUTPUT_EMPTY ) ev |= EV_TXEMPTY;
    if( event & CTS ) ev |= EV_CTS;
    if( event & DSR ) ev |= EV_DSR;
    if( event & RING_INDICATOR ) ev |= EV_RING;
    if( event & CARRIER_DETECT ) ev |= EV_RLSD;
    if( event & OVERRUN_ERROR ) ev |= EV_ERR;
    if( event & PARITY_ERROR ) ev |= EV_ERR;
    if( event & FRAMING_ERROR ) ev |= EV_ERR;
    if( event & BREAK_INTERRUPT ) ev |= EV_BREAK;
    return ev;
  }

  static int map_events_from_win32( DWORD ev, DWORD errors )
  {
    int event = 0;
    if( ev & EV_RXCHAR ) event |= DATA_AVAILABLE;
    if( ev & EV_TXEMPTY ) event |= OUTPUT_EMPTY;
    if( ev & EV_CTS ) event |= CTS;
    if( ev & EV_DSR ) event |= DSR;
    if( ev & EV_RING ) event |= RING_INDICATOR;
    if( ev & EV_RLSD ) event |= CARRIER_DETECT;
    if( ev & EV_ERR )
    {
      if( errors & CE_BREAK ) event |= BREAK_INTERRUPT;
      if( errors & CE_FRAME ) event |= FRAMING_ERROR;
      if( errors & CE_IOE ) throw comm_port_error("Win32 Comm IO Error");
      if( errors & CE_MODE ) throw comm_port_error("Win32 Comm Invalid Mode");
      if( errors & CE_OVERRUN ) event |= OVERRUN_ERROR;
      if( errors & CE_RXOVER ) event |= OVERRUN_ERROR; //?? okay
      if( errors & CE_RXPARITY ) event |= PARITY_ERROR;
      if( errors & CE_TXFULL ) event |= OVERRUN_ERROR; //?? okay
    }
    if( ev & EV_BREAK ) event |= BREAK_INTERRUPT;
    return event;
  }
*/

public:

  void setSerialPortParams( int baudrate, int databits, int stopbits, int parity )
  {
note( "setSerialPortParams begin" );
    struct termios state = get_comm_state();

    int baudenum = baud_to_enum(baudrate);
    errno_wrap( cfsetispeed( &state, baudenum ) == -1, "baudrate" );
    errno_wrap( cfsetospeed( &state, baudenum ) == -1, "baudrate" );

    //throw comm_port_error("nuthin");

    state.c_cflag &= ~CSIZE;
    switch( databits )
    {
      case 5: state.c_cflag |= CS5; break;
      case 6: state.c_cflag |= CS6; break;
      case 7: state.c_cflag |= CS7; break;
      case 8: default: state.c_cflag |= CS8;
    }

    if( stopbits == STOPBITS_2 )
      state.c_cflag |= CSTOPB;
    else
      state.c_cflag &= ~CSTOPB;

    state.c_cflag |= PARENB;
    switch( parity )
    {
      case NPARITY_EVEN: state.c_cflag &= ~PARODD; break;
      case NPARITY_ODD: state.c_cflag |= PARODD; break;
      case NPARITY_NONE: default: state.c_cflag &= ~PARENB;
    }

    errno_wrap( tcsetattr( m_fd, TCSANOW, &state ) == -1, "set_comm_state" );
note( "setSerialPortParams end" );
  }

  int getBaudRate()
  {
    struct termios state = get_comm_state();
    return enum_to_baud( cfgetospeed( &state ) );
  }

  int getDataBits()
  {
    switch( get_comm_state().c_cflag & CSIZE )
    {
      case CS5: return 5;
      case CS6: return 6;
      case CS7: return 7;
      case CS8: default: return 8;
    }
  }

  int getStopBits()
  {
    int stop = get_comm_state().c_cflag;
    return (stop & CSTOPB) ? STOPBITS_2 : STOPBITS_1;
  }

  int getParity()
  {
    int parity = get_comm_state().c_cflag;
    if( parity & PARENB )
      return (parity & PARODD) ? NPARITY_ODD : NPARITY_EVEN;
    return NPARITY_NONE;
  }

  int read( signed char* buffer, int off, int len )
  {
note( "read begin" );
    int nread = ::read( m_fd, buffer+off, len );
    errno_wrap( nread == -1, "read" );
#if 0
printf("   ...  read:");
for( int i=0; i<nread; i++ )
  printf(" %02x",buffer[off+i]&255);
printf("\n");
#endif
note( "read end" );
    return nread;
  }

  int write( const signed char* buffer, int off, int len )
  {
note( "write begin" );
    int nwritten = ::write( m_fd, buffer+off, len );
    errno_wrap( nwritten == -1, "write" );
#if 0
printf("   ... wrote:");
for( int i=0; i<nwritten; i++ )
  printf(" %02x",buffer[off+i]&255);
printf("\n");
#endif
note( "write end" );
    return nwritten;
  }

  int read()
  {
    signed char byte;
    return (read(&byte,0,1) > 0) ? ((unsigned char)byte) : -1;
  }

  int write( int b )
  {
    signed char byte = b;
    return write( &byte, 0, 1 );
  }

  int available()
  {
note( "available begin" );
    int navail = 0;
    int rv = 0;
    errno_wrap( rv=ioctl( m_fd, FIONREAD, &navail ) == -1, "available" );
//printf("... fionread=%d, rv=%d\n",navail,rv);
note( "available end" );
    return navail;
  }

  void notifyOn( int event, bool enable )
  {
    if( enable )
      m_events_in |= event;
    else
      m_events_in &= ~event;
  }

  bool isNotifyOn( int event )
  {
    return (m_events_in & event) != 0;
  }

  bool waitForEvent()
  {
note( "waitForEvent begin" );
    fd_set input;
    struct timeval tv;
    m_events_out = 0;
    int fd = m_fd;

    while( m_wait_for_events && (m_fd != -1) && (m_events_out == 0) )
    {
      FD_ZERO( &input );
      FD_SET( fd, &input );
      tv.tv_sec = 0;
      tv.tv_usec = 100*1000; // 1ms is the minimum resolution, at best

      if( select( fd+1, &input, NULL, NULL, &tv ) == -1 )
      {
	if( errno == EINTR )
	  break;
	errno_wrap( true, "waitForEvent.select" );
      }

      if( FD_ISSET( fd, &input ) )
	m_events_out |= DATA_AVAILABLE;
    }

    m_wait_for_events = true;
note( "waitForEvent end" );
    return (m_events_out != 0);
  }

  bool cancelWait()
  {
note( "cancelWait begin" );
    m_wait_for_events = false;
note( "cancelWait end" );
  }

  bool didEventOccur( int event )
  {
    return (m_events_out & event) != 0;
  }

  void setDTR( bool high )
  {
    if( high )
      set_modem_status( get_modem_status() | TIOCM_DTR );
    else
      set_modem_status( get_modem_status() & ~TIOCM_DTR );
  }

  void setRTS( bool high )
  {
    if( high )
      set_modem_status( get_modem_status() | TIOCM_RTS );
    else
      set_modem_status( get_modem_status() & ~TIOCM_RTS );
  }

  bool isDTR()
  {
    return (get_modem_status() & TIOCM_DTR) != 0;
  }

  bool isRTS()
  {
    return (get_modem_status() & TIOCM_RTS) != 0;
  }

  bool isCTS()
  {
    return (get_modem_status() & TIOCM_CTS) != 0;
  }

  bool isDSR()
  {
    return (get_modem_status() & TIOCM_DSR) != 0;
  }

  bool isRI()
  {
    return (get_modem_status() & TIOCM_RI) != 0;
  }

  bool isCD()
  {
    return (get_modem_status() & TIOCM_CD) != 0;
  }

  void sendBreak( int millis )
  {
  }

  NativeSerial( const char* portname ):
    m_fd(-1),
    m_events_in(0), 
    m_events_out(0),
    m_wait_for_events(true)
  {
    m_portname = portname;
note( "constructor begin" );
    m_fd = open( portname, O_RDWR | O_NOCTTY | O_NONBLOCK );
    errno_wrap( m_fd == -1, "open" );

//std::cout << "NativeSerial constructor [1] " << portname << std::endl;

    block_on_read(false);

    // set default port parmeters
    //struct termios options = get_comm_state();
    struct termios options;
    memset( &options, 0, sizeof(options) );

    // disable rts/cts, no parity bits, one stop bit, clear databits mask
    //local mode, enable receiver, 8 databits
    options.c_cflag = CLOCAL | CREAD | CS8;

    //raw mode
    options.c_lflag = 0;

    //disable software flow control, etc
    options.c_iflag = IGNPAR | IGNBRK;

    //raw output mode
    options.c_oflag = 0;

    //set thresholds
    options.c_cc[VMIN] = 0;
    options.c_cc[VTIME] = 0;

    errno_wrap( tcflush( m_fd, TCIOFLUSH ) == -1, "flush" );
    errno_wrap( tcsetattr( m_fd, TCSANOW, &options ) == -1, "setattr" );

    setDTR(false);
    setRTS(false);
note( "constructor end" );
  }

  ~NativeSerial()
  {
note( "destructor begin" );
    close();
note( "destructor end" );
  }

  void close()
  {
note( "close begin" );
//std::cout << "NativeSerial_linux close fd=" << m_fd << std::endl;
    if( m_fd != -1 )
    {
      cancelWait();
      struct timeval tv = { tv_sec:0, tv_usec:1100 };
      select( 0, NULL, NULL, NULL, &tv );
      ::close( m_fd );
      m_fd = -1;
    }
note( "close end" );
  }

  static std::string getTOSCommMap()
  {
    const char* env = getenv( "TOSCOMMMAP" );
    return (env == NULL) ? "com1=/dev/ttyS0:usb1=/dev/ttyUSB0" : env;
  }
};


#include "TOSComm_wrap.cxx"

