//
//  DIGI.CPP
//
//  Source code from:
//
//  Serial Communications: A C++ Developer's Guide, 2nd Edition
//  by Mark Nelson, IDG Books, 1999
//
//  Please see the book for information on usage.
//
// This file contains all of the code used by the DigiBoard class.
// All access of the DigiBoard is done via the INT 14H interface
// described in the DOC file available from DigiBoard.
//

#include <stdio.h>
#include <ctype.h>
#include "pc8250.h"
#include "rs232.h"
#include "digi.h"

// The DigiBoard constructor is nice and simple.  It has to read
// in the old settings to save them, then set the new ones according
// to the parameters passed in the constructor.  The only private
// member exclusive to the derived class is the line-status flag,
// which is initialized to 0.  The call to function 0x20 is used
// to disable BIOS timing emulation.

DigiBoard::DigiBoard( RS232PortName port,
                      long baud_rate,
                      char parity,
                      int word_length,
                      int stop_bits,
                      int dtr,
                      int rts,
                      int xon_xoff,
                      int rts_cts,
                      int dtr_dsr )
{
    union REGS r;

    port_name = port;
    error_status = RS232_SUCCESS;

    first_debug_output_line = RS232::FormatDebugOutput();
    debug_line_count = FormatDebugOutput();
    if ( !valid_port() ) {
        error_status = (RS232Error) DIGIBOARD_DRIVER_NOT_FOUND;
        return;
    }
    read_settings();
    saved_settings = settings;
    settings.Adjust( baud_rate,
                     parity,
                     word_length,
                     stop_bits,
                     dtr,
                     rts,
                     xon_xoff,
                     rts_cts,
                     dtr_dsr );
    write_settings();
    r.h.ah = 0x20;
    r.h.al = 0;
    r.x.dx = port_name;
    int86( 0x14, &r, &r );
    line_status = 0;
}

// The destructor just restores the old state, nothing more.

DigiBoard::~DigiBoard( void )
{
    settings = saved_settings;
    write_settings();
}

//
// A call to function 0x0c and 0x05 are needed to read all the
// parameters found in the Settings class.  All that is
// needed after that is a bunch of switch statements to convert
// the enumerated results that come back from the driver to
// settings usable by programmers.
//

void DigiBoard::read_settings( void )
{
    union REGS r;

    settings.BaudRate = -1L;
    settings.Parity = '?';
    settings.WordLength = -1;
    settings.StopBits = -1;
    settings.Dtr = -1;
    settings.Rts = -1;
    settings.XonXoff = -1;
    settings.RtsCts = -1;
    settings.DtrDsr = -1;
    r.h.ah = 0xc;
    r.x.dx = port_name;
    int86( 0x14, &r, &r );
    if ( r.h.ah == 0xff )
        return;
    switch ( r.h.cl ) {
        case 0x00 : settings.BaudRate = 110L;    break;
        case 0x01 : settings.BaudRate = 150L;    break;
        case 0x02 : settings.BaudRate = 300L;    break;
        case 0x03 : settings.BaudRate = 600L;    break;
        case 0x04 : settings.BaudRate = 1200L;   break;
        case 0x05 : settings.BaudRate = 2400L;   break;
        case 0x06 : settings.BaudRate = 4800L;   break;
        case 0x07 : settings.BaudRate = 9600L;   break;
        case 0x08 : settings.BaudRate = 19200L;  break;
        case 0x09 : settings.BaudRate = 38400L;  break;
        case 0x0a : settings.BaudRate = 57600L;  break;
        case 0x0b : settings.BaudRate = 75600L;  break;
        case 0x0c : settings.BaudRate = 115200L; break;
        case 0x0d : settings.BaudRate = 50L;     break;
        case 0x0e : settings.BaudRate = 75L;     break;
        case 0x0f : settings.BaudRate = 134L;    break;
        case 0x10 : settings.BaudRate = 200L;    break;
        case 0x11 : settings.BaudRate = 1800L;   break;
    }
    switch ( r.h.bh ) {
        case 0 : settings.Parity = 'N'; break;
        case 1 : settings.Parity = 'O'; break;
        case 2 : settings.Parity = 'E'; break;
    }
    switch ( r.h.ch ) {
        case 0 : settings.WordLength = 5; break;
        case 1 : settings.WordLength = 6; break;
        case 2 : settings.WordLength = 7; break;
        case 3 : settings.WordLength = 8; break;
    }
    switch ( r.h.bl ) {
        case 0 : settings.StopBits = 1; break;
        case 1 : settings.StopBits = 2; break;
    }
    settings.XonXoff = ( r.h.ah & 3 ) ? 1: 0;
    settings.DtrDsr = ( r.h.al & 0x21 ) ? 1 : 0;
    settings.RtsCts = ( r.h.al & 0x12 ) ? 1 : 0;
    r.x.dx = port_name;
    r.h.ah = 5;
    r.h.al = 0;
    int86( 0x14, &r, &r );
    settings.Dtr = ( r.h.bl & MCR_DTR ) ? 1 : 0;
    settings.Rts = ( r.h.bl & MCR_RTS ) ? 1 : 0;
}

// Setting the digiboard up with all the parameters found in the
// Settings class requires three INT 14H calls.  Function 4
// sets the standard communications parameters, Function 5 sets
// the modem control lines, and function 0x1e sets up handshaking.
//

RS232Error DigiBoard::write_settings( void )
{
    union REGS r;
    RS232Error status = RS232_SUCCESS;

    r.x.dx = port_name;
    r.h.ah = 4;
    r.h.al = 0;
    switch ( toupper( settings.Parity ) ) {
        case 'E' : r.h.bh = 1; break;
        case 'O' : r.h.bh = 2; break;
        default  : settings.Parity = 'N';
                   status = RS232_ILLEGAL_PARITY_SETTING;
        case 'N' : r.h.bh = 0; break;
    }
    switch ( settings.StopBits ) {
        default : settings.StopBits = 1;
                  status = RS232_ILLEGAL_STOP_BITS;
        case 1  : r.h.bl = 0; break;
        case 2  : r.h.bl = 1; break;
    }
    switch ( settings.WordLength ) {
        case 5   : r.h.ch = 0; break;
        case 6   : r.h.ch = 1; break;
        case 7   : r.h.ch = 2; break;
        default  : settings.WordLength = 8;
                   status = RS232_ILLEGAL_WORD_LENGTH;
        case 8   : r.h.ch = 3; break;
    }
    switch ( settings.BaudRate ) {
        case 110L    : r.h.cl = 0x00; break;
        case 150L    : r.h.cl = 0x01; break;
        case 300L    : r.h.cl = 0x02; break;
        case 600L    : r.h.cl = 0x03; break;
        case 1200L   : r.h.cl = 0x04; break;
        case 2400L   : r.h.cl = 0x05; break;
        case 4800L   : r.h.cl = 0x06; break;
        default      : settings.BaudRate = 9600L;
                       status = RS232_ILLEGAL_BAUD_RATE;
        case 9600L   : r.h.cl = 0x07; break;
        case 19200L  : r.h.cl = 0x08; break;
        case 38400L  : r.h.cl = 0x09; break;
        case 57600L  : r.h.cl = 0x0a; break;
        case 76800L  : r.h.cl = 0x0b; break;
        case 115200L : r.h.cl = 0x0c; break;
        case 50L     : r.h.cl = 0x0d; break;
        case 75L     : r.h.cl = 0x0e; break;
        case 134L    : r.h.cl = 0x0f; break;
        case 200L    : r.h.cl = 0x10; break;
        case 1800L   : r.h.cl = 0x11; break;
    }
    int86( 0x14, &r, &r );
    r.x.dx = port_name;
    r.h.ah = 0x1e;
    r.h.bh = (unsigned char) ( ( settings.XonXoff ) ? 3: 0 );
    r.h.bl = (unsigned char) ( ( settings.RtsCts ) ? 0x12 : 0 );
    r.h.bl |= ( settings.DtrDsr ) ? 0x21 : 0;
    int86( 0x14, &r, &r );
    r.x.dx = port_name;
    r.h.ah = 5;
    r.h.al = 1;
    r.h.bl = (unsigned char) ( ( settings.Dtr ) ? 1 : 0 );
    r.h.bl |= ( settings.Rts ) ? 2 : 0;
    int86( 0x14, &r, &r );
    return status;
}

// Function 6 is called to return the name of the port, but
// it also functions effectively as a check to see if the
// DigiBoard considers the port to be a valid one.

int DigiBoard::valid_port( void )
{
    union REGS r;

    r.x.dx = port_name;
    r.h.ah = 6;
    r.h.al = 0;
    int86( 0x14, &r, &r );
    return ( r.h.ah != 0xff );
}

// Reading a byte uses the two BIOS emulation functions, one to
// read in the modem status, and the other to read the character.
// This function, like many of the other ones, updates the
// line status flags with the result read back from the board.

int DigiBoard::read_byte( void )
{
    union REGS r;

    if ( error_status < 0 )
        return error_status;
    r.h.ah = 3;
    r.x.dx = port_name;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    if ( r.h.ah & LSR_DATA_READY ) {
        r.h.ah = 2;
        r.x.dx = port_name;
        int86( 0x14, &r, &r );
        line_status |= r.h.ah;
        return( r.h.al );
    }
    return( RS232_TIMEOUT );
}

// This function also uses a standard BIOS function call.

int DigiBoard::write_byte( int c )
{
    union REGS r;

    if ( error_status < 0 )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 0x01;
    r.h.al = (char) c;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    if ( r.h.ah & 0x80 )
        return RS232_TIMEOUT;
    return RS232_SUCCESS;
}

// DigiBoard has two private functions, 14 and 15, which are
// used to read or write blocks of data.  They both transfer
// as much data as possible, then return a count.

int DigiBoard::read_buffer( char *buffer, unsigned int count )
{
    union REGS r;
    struct SREGS s;

    if ( error_status < 0 )
        return error_status;
    r.x.dx = port_name;
    r.x.cx = count;
    r.h.ah = 0x0f;
    s.es = (unsigned int) ( (long) (void __far *) buffer >> 16 );
    r.x.bx = (unsigned int) (long) (void __far *) buffer;
    int86x( 0x14, &r, &r, &s );
    ByteCount = r.x.ax;
    buffer[ ByteCount ] = '\0';
    if ( ByteCount != count )
        return RS232_TIMEOUT;
    return( RS232_SUCCESS );
}

int DigiBoard::write_buffer( char *buffer, unsigned int count )
{
    union REGS r;
    struct SREGS s;

    if ( error_status < RS232_SUCCESS )
        return error_status;

    r.x.dx = port_name;
    r.x.cx = count;
    r.h.ah = 0x0e;
    s.es = (unsigned int) ( (long) (void __far *) buffer >> 16 );
    r.x.bx = (unsigned int) (long) (void __far *) buffer;
    int86x( 0x14, &r, &r, &s );
    ByteCount = r.x.ax;
    if ( ByteCount != count )
        return RS232_TIMEOUT;
    return RS232_SUCCESS;
}

// This function does no work on its own.  Instead, it uses
// the adjust function to change the settings member, then
// calls the write_settings() function to do the job.

RS232Error DigiBoard::Set( long baud_rate,
                           int parity,
                           int word_length,
                           int stop_bits )
{
    settings.Adjust( baud_rate,
                     parity,
                     word_length,
                     stop_bits,
                     UNCHANGED,
                     UNCHANGED,
                     UNCHANGED,
                     UNCHANGED,
                     UNCHANGED );
    return write_settings();
}

// DigiBoard function 7 sets a break of a variable time in 10
// millisecond ticks.

int DigiBoard::Break( long milliseconds )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 7;
    r.h.al = 1;
    r.x.bx = (int) ( milliseconds / 10 );
    int86( 0x14, &r, &r );
    return RS232_SUCCESS;
}

// All four of the modem status functions just use BIOS function
// 3 to read in the MSR of the UART.  They then just mask off
// the appropriate bit from the MSR and return a boolean value
// to the calling program.

int DigiBoard::Cd( void )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 3;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    return ( r.h.al & MSR_CD ) != 0;
}

int DigiBoard::Ri( void )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 3;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    return ( r.h.al & MSR_RI ) != 0;
}

int DigiBoard::Cts( void )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 3;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    return ( r.h.al & MSR_CTS ) != 0;
}

int DigiBoard::Dsr( void )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 3;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    return ( r.h.al & MSR_DSR ) != 0;
}

// Like the modem status functions, the four line status functions
// use BIOS function 3 to read the LSR from the UART, then mask
// off the appropriate bits and return a logical true or false to
// the calling program.

int DigiBoard::ParityError( int reset )
{
    union REGS r;
    int status;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 3;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    status = ( line_status & LSR_PARITY_ERROR ) != 0;
    if ( reset != UNCHANGED && reset != 0 )
        line_status &= ~LSR_PARITY_ERROR;
    return status;
}

int DigiBoard::BreakDetect( int reset )
{
    union REGS r;
    int status;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 3;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    status = ( line_status & LSR_BREAK_DETECT ) != 0;
    if ( reset != UNCHANGED && reset != 0 )
        line_status &= ~LSR_BREAK_DETECT;
    return status;
}

int DigiBoard::FramingError( int reset )
{
    union REGS r;
    int status;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 3;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    status = ( line_status & LSR_FRAMING_ERROR ) != 0;
    if ( reset != UNCHANGED && reset != 0 )
        line_status &= ~LSR_FRAMING_ERROR;
    return status;
}

int DigiBoard::HardwareOverrunError( int reset )
{
    union REGS r;
    int status;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 3;
    int86( 0x14, &r, &r );
    line_status |= r.h.ah;
    status = ( line_status & LSR_OVERRUN_ERROR ) != 0;
    if ( reset != UNCHANGED && reset != 0 )
        line_status &= ~LSR_OVERRUN_ERROR;
    return status;
}

// The five handshaking and modem control functions are all
// basically lazy. They modify a the appropriate value in
// the settings member, then call write_settings() to do the
// real work.

int DigiBoard::XonXoffHandshaking( int setting )
{
    if ( setting != UNCHANGED ) {
        settings.XonXoff = ( setting != 0 );
        write_settings();
    }
    return settings.XonXoff;
}

int DigiBoard::RtsCtsHandshaking( int setting )
{
    if ( setting != UNCHANGED ) {
        settings.RtsCts = ( setting != 0 );
        write_settings();
    }
    return settings.RtsCts;
}

int DigiBoard::DtrDsrHandshaking( int setting )
{
    if ( setting != UNCHANGED ) {
        settings.DtrDsr = ( setting != 0 );
        write_settings();
    }
    return settings.DtrDsr;
}

int DigiBoard::Dtr( int setting )
{
    if ( setting != UNCHANGED ) {
        settings.Dtr = ( setting != 0 );
        write_settings();
    }
    return ( settings.Dtr != 0 );
}

int DigiBoard::Rts( int setting )
{
    if ( setting != UNCHANGED ) {
        settings.Rts = ( setting != 0 );
        write_settings();
    }
    return ( settings.Rts != 0 );
}

// DigiBoard only lets us peek ahead by one byte into the
// input buffer.  If there is a character there, this function
// reads it in and stores it in the appropriate place.

int DigiBoard::PeekBuffer( void *buffer, unsigned int count )
{
    union REGS r;

    if ( count ) {
        r.h.ah = 3;
        r.x.dx = port_name;
        int86( 0x14, &r, &r );
        line_status |= r.h.ah;
        if ( r.h.ah & 0x80 )
             return RS232_ERROR;
        if ( r.h.ah & 1 ) {
            r.h.ah = 0x14;
            r.x.dx = port_name;
            int86( 0x14, &r, &r );
            line_status |= r.h.ah;
            *( (char *) buffer ) = r.h.al;
            ByteCount = 1;
        } else
            ByteCount = 0;
    }
    return RS232_SUCCESS;
}

// These functions all use private digiboard INT 14 calls to
// return buffer counts.  The only complication is that there
// is no direct way to determine the RX space free.  It has to
// be calculated indirectly as the RX buffer size - the buffer
// space used.

int DigiBoard::TXSpaceUsed( void )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 0xfd;
    r.h.al = 1;
    int86( 0x14, &r, &r );
    return( r.x.cx );
}

int DigiBoard::TXSpaceFree( void )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 0x12;
    int86( 0x14, &r, &r );
    return r.x.ax;
}

int DigiBoard::RXSpaceUsed( void )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 0x0a;
    int86( 0x14, &r, &r );
    return r.x.ax;
}

int DigiBoard::RXSpaceFree( void )
{
    union REGS r;
    int buffer_size;

    if ( error_status < RS232_SUCCESS )
        return error_status;

    r.x.dx = port_name;
    r.h.ah = 0x1b;
    r.h.al = 1;
    int86( 0x14, &r, &r );
    buffer_size = r.x.bx;
    r.h.ah = 10;
    r.x.dx = port_name;
    int86( 0x14, &r, &r );
    return( buffer_size - r.x.ax );
}

// DigiBoard provides two private INT 14 calls to handle flushing
// the TX and RX buffers.

int DigiBoard::FlushRXBuffer( void )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 0x10;
    int86( 0x14, &r, &r );
    return RS232_SUCCESS;
}

int DigiBoard::FlushTXBuffer( void )
{
    union REGS r;

    if ( error_status < RS232_SUCCESS )
        return error_status;
    r.x.dx = port_name;
    r.h.ah = 0x11;
    int86( 0x14, &r, &r );
    return RS232_SUCCESS;
}

int DigiBoard::FormatDebugOutput( char *buffer, int line_number )
{
    union REGS r;

    if ( buffer == 0 )
        return( first_debug_output_line +  4 );
    if ( line_number < first_debug_output_line )
        return RS232::FormatDebugOutput( buffer, line_number );
    switch( line_number - first_debug_output_line ) {
        case 0 :
            r.x.dx = port_name;
            r.h.ah = 6;
            r.h.al = 1;
            int86( 0x14, &r, &r );
            sprintf( buffer,
                     "Derived class: DigiBoard  Boards/Channels: %2d/%2d  "
                     "Ver: %2x.%02x  1st Port: COM%-2d",
                     r.x.cx,
                     r.x.ax,
                     r.h.bh, r.h.bl,
                     r.x.dx + 1 );
            break;
        case 1 :
           ParityError( UNCHANGED );
           sprintf( buffer,
                     "Parity Err: %d  "
                     "Break Det: %d  "
                     "Overrun Err: %d  "
                     "Framing Err: %d  ",
                     ( line_status & LSR_PARITY_ERROR ) ? 1 : 0,
                     ( line_status & LSR_BREAK_DETECT ) ? 1 : 0,
                     ( line_status & LSR_OVERRUN_ERROR ) ? 1 : 0,
                     ( line_status & LSR_FRAMING_ERROR ) ? 1 : 0 );
           break;
        case 2 :
            sprintf( buffer,
                     "Buffer Counts: RX Used/Free: %5u/%5u  "
                     "TX Used/Free: %5u/%5u",
                     RXSpaceUsed(),
                     RXSpaceFree(),
                     TXSpaceUsed(),
                     TXSpaceFree() );
             break;
        case 3 :
            sprintf( buffer,
                     "RI: %2d  CD: %2d  CTS: %2d  DSR: %2d",
                     Ri(), Cd(), Cts(), Dsr() );
            break;
        default :
            return RS232_ILLEGAL_LINE_NUMBER;
    }
    return RS232_SUCCESS;
}

char * DigiBoard::ErrorName( int error )
{
    if ( error < RS232_NEXT_FREE_ERROR && error >= RS232_ERROR )
        return RS232::ErrorName( error );
    if ( error < RS232_NEXT_FREE_WARNING && error >= RS232_WARNING )
        return RS232::ErrorName( error );
    if ( error >= RS232_SUCCESS )
        return RS232::ErrorName( error );
    switch ( error ) {
        case DIGIBOARD_DRIVER_NOT_FOUND   : return( "Driver not found" );
        default                           : return( "Undefined error" );
    }
}
