// -------------------------------------------------------------------------------------
// PortComm.m
// serial port communication handler
// Author: Steve Herrick, Martin D. Flynn, NeXT Computer, Inc.
// -------------------------------------------------------------------------------------
// THIS CODE FRAGMENT IS FOR DEMONSTRATION PURPOSES ONLY.
// Permission is granted to freely redistribute this source code, and to use fragments
// of this code in your own applications if you find them to be useful.  This class,
// along with the source code, come with no warranty of any kind, and the user assumes
// all responsibility for its use.
// -------------------------------------------------------------------------------------

// -------------------------------------------------------------------------------------
#import <stdio.h>
#import <fcntl.h>
#import <string.h>
#import <libc.h>
#import <ctype.h>
#import <mach/cthreads.h>
#import <sys/types.h>
#import <sys/time.h>
#import <sys/uio.h>
#import <sys/ioctl.h>
#import <sys/socket.h>
#import <netinet/in.h>
#import <netdb.h>
#import <objc/hashtable.h>
#import "PortComm.h"

// -------------------------------------------------------------------------------------
#define         EOL(C)          (((C) == '\n') || ((C) == '\r'))
#define			mutexAlloc		mutex_alloc
#define			mutexFree(X)	mutex_free(X)
#define			mutexLock(X)	mutex_lock(X)
#define			mutexUnlock(X)	mutex_unlock(X)

// -------------------------------------------------------------------------------------
@implementation PortComm

// -------------------------------------------------------------------------------------
// forward/external declarations
extern int              close(int fd);

// -------------------------------------------------------------------------------------
static BOOL				debug = NO;						// debug mode
static int              openError = noERROR;            // global error number

// -------------------------------------------------------------------------------------
#define                 validPORT(N)            (((N) >= 0) && ((N) <= 1))
static char             *portDeviceTable[] = { portA, portB, 0 };
                          
// -------------------------------------------------------------------------------------
// new instance

/* new serial port instance */
+ newPort:(int)portNum baud:(u_char)baudRate parity:(u_short)parity charMask:(u_char)mask
{
  
  char				*portDev;
  BOOL				eolCheck = YES;
  int				ioSet, fd = -1;
  struct sgttyb		sg;
  struct ltchars	lc;
  
  /* single pass loop */
  for (openError = noERROR;;) {
  
    /* open serial port device */
    portDev = (char *)[PortComm getPortDevice:portNum];
    if (!portDev) { openError = errorBADPORTNUM; break; }
	if (debug) printf("PortComm: openning serial port #%d (%s)\n", portNum, portDev);
    fd = open(portDev, O_RDWR);
    if (fd < 0) { openError = errorOPEN; break; }
    
    /* prevent any other unauthorized port accesses */
    if (ioctl(fd, TIOCEXCL, (void*)nil) < 0) { openError = errorTIOCEXCL; break; }

    /* force line discipline */
	ioSet = OTTYDISC;
	if (ioctl(fd, TIOCSETD, &ioSet) < 0) { openError = errorTIOCSETD; break; }

    /* set local word mode */
	ioSet = LNOHANG /* | LPASS8OUT | LPASS8 */;
	if (ioctl(fd, TIOCLSET, &ioSet) < 0) { openError = errorTIOCLSET; break; }
	
    /* force everything else we can think of */
	if (ioctl(fd, TIOCCBRK , (void*)nil) < 0) { openError = errorTIOCCBRK ; break; }
	if (ioctl(fd, TIOCSDTR , (void*)nil) < 0) { openError = errorTIOCSDTR ; break; }
	if (ioctl(fd, TIOCNOTTY, (void*)nil) < 0) { openError = errorTIOCNOTTY; break; }

    /* set special chars */
	memset(&lc, 0xFF, sizeof(lc));
    if (ioctl(fd, TIOCSLTC, &lc) < 0) { openError = errorTIOCSLTC; break; }

    /* get current port configuration */
    if (ioctl(fd, TIOCGETP, &sg) < 0) { openError = errorTIOCGETP; break; }

    /* turn off flags */
	sg.sg_flags &= ~(ODDP | EVENP | ECHO | CBREAK | RAW | TANDEM);

	/* check parity */
	eolCheck = (parity == parityRAW)? NO : YES;
	if (parity == parityRAW_eol) parity = parityRAW;
	
    /* update port configuration */
	memset(&sg, 0, sizeof(sg));
    sg.sg_ispeed = baudRate;
    sg.sg_ospeed = baudRate;
	sg.sg_erase	 = 0xFF;
	sg.sg_kill	 = 0xFF;
    sg.sg_flags |= parity & (ODDP | EVENP | ANYP | RAW);	// set parity

    /* set new port configuration */
    if (ioctl(fd, TIOCSETP, &sg) < 0) { openError = errorTIOCSETP; break; }
    
    /* break from single pass loop */
    break;
    
  }

  /* check for error */
  if (openError != noERROR) {
    if (fd >= 0) close(fd);
	if (debug) printf("PortComm: Unable to open serial port #%d\n", portNum);
    return (id)nil;
  }
    
  /* create instance */
  self        = [[self alloc] init];
  portFd      = fd;
  portNumber  = portNum;
  portBaud    = baudRate;
  portParity  = parity;
  checkEol	  = eolCheck;
  charMask	  = (mask)? mask : 0xFF;
  if (debug) printf("PortComm: Successfully opened serial port #%d\n", portNum);
  
  return self;
}
  
+ newPort:(int)portNum baud:(u_char)baudRate parity:(u_short)parity
{
  return [self newPort:portNum baud:baudRate parity:parity charMask:0];
}

/* new instance of object */
- init
{
  [super init];
  isListening = NO;
  rcdBuff     = (char*)nil;
  dataHandler = (id)nil;
  portFd	  = -1;
  portNumber  = -1;
  portBaud    = B0;
  portParity  = parityUNKNOWN;
  portError   = noERROR;
  forkMutex   = mutexAlloc();
  writeMutex  = mutexAlloc();
  maxBytes	  = 1024;
  charMask    = 0xFF;
  checkEol    = YES;
  return self;
}
  
/* close/free port object */
- free
{
  if (isListening) {
  	printf("WARNING: 'free' issued while serial port handler thread is active\n");
	printf("         Application may terminate abnormally...\n");
  }
  close(portFd);
  if (rcdBuff) free(rcdBuff);
  mutexFree(forkMutex);
  mutexFree(writeMutex);
  return [super free];
}

/* return port file descriptor */
- (int)portFd
{
  return portFd;
}
  
// -------------------------------------------------------------------------------------
// convert baud rate to actual defined value
+ (u_char)baudRateConstant:(int)baudRate
{
  if (baudRate ==     0) return B0;
  if (baudRate ==    50) return B50;
  if (baudRate ==    75) return B75;
  if (baudRate ==   110) return B110;
  if (baudRate ==   134) return B134;
  if (baudRate ==   150) return B150;
  if (baudRate ==   200) return B200;
  if (baudRate ==   300) return B300;
  if (baudRate ==   600) return B600;
  if (baudRate ==  1200) return B1200;
  if (baudRate ==  1800) return B1800;
  if (baudRate ==  2400) return B2400;
  if (baudRate ==  4800) return B4800;
  if (baudRate ==  9600) return B9600;
  if (baudRate == 19200) return EXTA;
  if (baudRate == 38400) return EXTB;
  return B0;
}
  
// -------------------------------------------------------------------------------------
// return port device name for port number
+ (const char *)getPortDevice:(int)portNum
{
  return (validPORT(portNum))? portDeviceTable[portNum]: (char*)nil;
}

// -------------------------------------------------------------------------------------
// check portnumber validity

/* validate port number */
+ (BOOL)isValidPort:(int)portNum
{
  return (portNum >= 0) && (portNum <= 1);
}

/* return port number */
- (int)portNumber
{
  return portNumber;
}
  
// -------------------------------------------------------------------------------------
// set run flags

/* set data handler id */
- setDataHandler:handler
{
  dataHandler = handler;
  return self;
}

/* set maximum read buffer size */
- setMaxReadSize:(int)size
{
  maxBytes = size;
  return self;
}

/* set debug mode */
- setDebugMode:(BOOL)debugMode
{
  debug = debugMode;
  return self;
}

/* set character mask */
- setCharMask:(u_char)mask
{
  charMask = mask;
  return self;
}

// -------------------------------------------------------------------------------------

/* return port open error */
+ (int)openError
{
  return openError;
}

/* return port error */
- (int)portError
{
  return portError;
}

// -------------------------------------------------------------------------------------
// write data to port

- (int)writeToPort:(char *)buff
{
  return [self writeToPort:(char *)buff length:strlen(buff)];
}

- (int)writeToPort:(char *)buff length:(int)buffLen
{
  int           writeLen;
  mutexLock(writeMutex);
  if (debug) printf("PortComm: writing '%s' (len=%d)\n", buff, buffLen);
  writeLen = write(portFd, buff, buffLen);
  mutexUnlock(writeMutex);
  if (debug && (writeLen != buffLen)) printf("PortComm: write error! (%d)\n", writeLen);
  return (writeLen == buffLen)? 0: -1;  
}

// -------------------------------------------------------------------------------------
// do timeout
- (int)readTimeout:(float)timeout
{
  int				flag, charsReady;
  u_long			uSec;
  struct timeval	tm;
  fd_set			portList;
    
  /* get number of character currently ready */
  flag = ioctl(portFd, FIONREAD, &charsReady);
  if (flag < 0) return flag;					// if error
  if (charsReady) return charsReady;			// if characters ready

  /* wait for character to be present */
  if (debug) printf("PortComm: waiting for characters to be ready (%.1f)\n", timeout);
  FD_ZERO(&portList);
  FD_SET(portFd, &portList);
  uSec = (u_long)(timeout * 1000000.0);
  tm.tv_sec  = uSec / 1000000L;
  tm.tv_usec = uSec % 1000000L;
  flag = select(portFd + 1, &portList, 0, 0, &tm);
  if (debug && (flag <  0)) printf("PortComm: character wait error! (%d)\n", flag); else
  if (debug && (flag == 0)) printf("PortComm: character wait timeout!\n");

  /* return select results */
  return flag;
      
}

// -------------------------------------------------------------------------------------
// read data from port and return it in buff (null terminated)
- (int)read:(char *)buff maxLen:(int)maxLen timeout:(float)timeout
{
  int	i, cnt;

  /* read from port */
  for (i = cnt = 0; cnt < maxLen;) {
  
    /* timeout for characters ready */
    if (timeout && ([self readTimeout:timeout] <= 0)) return -1;

	/* read data from port */
	if (debug) printf("PortComm: waiting on read\n");
    cnt += read(portFd, &buff[cnt], 1);	// maxLen - cnt - 1
	if (charMask != 0xFF) for (;i < cnt; i++) buff[i] &= charMask;
    if (checkEol && (cnt > 0) && EOL(buff[cnt - 1])) break;
	
  }
  
  /* remove trailing CR/LF (if not in raw mode) */
  if (checkEol) {
	for (;(cnt > 0) && EOL(buff[cnt - 1]); buff[--cnt] = 0);
	buff[cnt] = 0;
  }
  if (debug) printf("PortComm: read '%s'\n", buff);

  return cnt;
}

// -------------------------------------------------------------------------------------
// read data from port and send it to 'record' method

/* thread function */
static void readLoop(id self)
{
  [self readLoop:0.0];
  cthread_exit(0);
  }

- readLoop:(float)timeout
{
  int           cnt;
  
  /* allocate record buffer */
  mutexLock(forkMutex);
  if (rcdBuff) free(rcdBuff);
  rcdBuff = (char*)malloc(maxBytes + 1);
  mutexUnlock(forkMutex);
  
  /* read loop */
  for (;;) {
    cnt = [self read:rcdBuff maxLen:maxBytes timeout:timeout];
    mutexLock(forkMutex);
    [self dataRecord:rcdBuff len:cnt fromPort:portNumber];
    mutexUnlock(forkMutex);
  }
    
  /* return (never executed) */
  isListening = NO;
  return self;
  
}

// -------------------------------------------------------------------------------------
// process data record (overridden by subclass)
- (BOOL)dataRecord:(char*)buff len:(int)len fromPort:(int)portNum
{
  if (dataHandler                                                       &&
      [dataHandler respondsTo:@selector(dataRecord:len:fromPort:)]      &&
      (dataHandler != self)                                               ) {
    [dataHandler dataRecord:buff len:len fromPort:portNum];
    return YES;
  }
  return NO;
}

// -------------------------------------------------------------------------------------
// start read loop thread (from main thread)
- forkPortListener
{
  mutexLock(forkMutex);
  if (!isListening) {
    portThread = cthread_fork((cthread_fn_t)readLoop, (any_t)self);
    cthread_detach(portThread);
    isListening = YES;
  }
  mutexUnlock(forkMutex);
  return self;
}
  
@end
