package net.sourceforge.gjtapi.raw.modem;
// NAME
// $RCSfile$
// DESCRIPTION
// [given below in javadoc format]
// DELTA
// $Revision$
// CREATED
// $Date$
// COPYRIGHT
// Westhawk Ltd
// TO DO
//
import java.io.*;
import java.util.TooManyListenersException;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.comm.*;
/**
* A class which handles all of the IO for a Modem.
*
* This class works with modems connected to serial ports. Perhaps it should
* be an interface and implementation.
*
* @author <a href="mailto:ray@westhawk.co.uk">Ray Tran</a>
* @version $Revision$ $Date$
*/
public class ModemIO implements SerialPortEventListener{
public static final int DLE = 0x10;
public static final int ETX = 0x03;
public static final int CR = 0x0c;
public static final int LF = 0x0a;
private static final int BUF_SIZE = 65536;
private static final int MARK_INVALID = -1;
public static final int TIMEOUT = -1;
public static final int GOOD_MATCH = 0;
public static final int BAD_MATCH = 1;
private SerialPort port = null;
private InputStream in;
private PrintStream out;
private byte[] buf;
private int writePos, readPos;
private int markPos = MARK_INVALID;
private int limit = MARK_INVALID;
private int limitCount;
private String lastMatch ="";
private ShieldHandler handler;
private Modem modem;
/**
* Constructor.
*
* @param portname - Name of SerialPort used for talking to the modem.
* @param modem - Reference back to the modem which we are the IO for
*/
public ModemIO(String portname, Modem modem) {
buf = new byte[BUF_SIZE];
writePos = readPos = 0;
this.modem = modem;
handler = new ShieldHandler();
Thread t = new Thread(handler);
t.start();
//First try to open a port to the modem
try {
CommPortIdentifier portID = CommPortIdentifier.getPortIdentifier(portname);
port = (SerialPort) portID.open("ModemProvider", 5000);
port.setSerialPortParams(
115200,
SerialPort.DATABITS_8,
SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE
);
//add ourself as a listener for certain events
port.notifyOnRingIndicator(true);
/*
port.notifyOnDataAvailable(true);
port.notifyOnBreakInterrupt(false);
port.notifyOnCarrierDetect(false);
port.notifyOnCTS(false);
port.notifyOnDSR(false);
port.notifyOnFramingError(false);
port.notifyOnOutputEmpty(false);
port.notifyOnOverrunError(false);
port.notifyOnParityError(false);
*/
port.addEventListener(this);
}catch (TooManyListenersException ex) {
System.err.println("Adding event listener to port (" +
portname + ") failed");
}catch (Exception ex) {
System.err.println("Can't open serial port (" + portname);
ex.printStackTrace();
}
if (port != null){
try{
port.enableReceiveTimeout(1000);
}catch (UnsupportedCommOperationException ex){
System.err.println("Can't set timeout on port(" + portname + ")");
}
try{
in = port.getInputStream();
out = new PrintStream(port.getOutputStream());
}catch (IOException ex){
System.err.println("IOException in ModemIO(" + port.getName() + ")");
ex.printStackTrace();
}
}
}
/**
* Try to read bytes into the circular buffer until the buffer is full, or
* no more bytes are available from the underlying device.
* If there is space in the buffer then we always try to read
* at least one byte, the read will timeout if nothing is available;
*
* @throws IOException
*/
private void fill() throws IOException{
//Don't bother if the buffer is nearly full
if (this.free() > 32 && in.available() > 0){
do{
int read = in.read();
if (read > -1){
//If we get a <DLE> we need to deal with it
if (read == DLE){
read = in.read();
//If the next char is <DLE> pass it thru otherwise
//handle it (quickly)
if (read != DLE){
if (read == -1){
//this really should NEVER happen!
break;
}
handleDLEShield((char)read);
}
}
buf[writePos++] = (byte)read;
if (writePos >= BUF_SIZE){
writePos = 0;
}
}
}while (in.available() > 0 && this.free() > 0);
}
}
/**
* Deals with <DLE> shielded characters.
* As a character may require time consuming processing a seperate
* thread does it to avoid slowing down the fill() loop.
*
* @param shielded - character read after the <DLE> character
*/
private void handleDLEShield(char shielded){
//System.err.println("<DLE>0x" + Integer.toHexString(shielded));
//Expect <DLE>s when the modem detects silence, <DLE>d when dialtone
handler.setShield(shielded);
}
/**
* Returns the number of bytes available to be read.
*
* @return int - the number of bytes available to be read.
* @see #free()
*/
public int available(){
int result;
if (readPos <= writePos){
result = writePos - readPos;
}else{
result = BUF_SIZE - readPos + writePos;
}
return result;
}
/**
* Returns the number of spaces available to be written in.
*
* @return int - the number of spaces available to be written in.
* @see #available()
*/
private int free(){
int result;
int comparePos = ((markPos == MARK_INVALID)?readPos:markPos);
if (comparePos <= writePos){
result = BUF_SIZE - writePos + comparePos - 1;
}else{
result = comparePos - writePos - 1;
}
return result;
}
/**
* Read the next byte from the buffer, re-filling if required.
*
* @return int - the next byte read from the buffer
* @throws IOException
*/
public int read() throws IOException{
int result = -1;
if (in != null && buf != null){
//if the buffer is nearly empty try to fill it
if (this.available() < 16){
fill();
}
if (readPos != writePos){
result = buf[readPos++] & 0xff;
//If we have read more bytes than readlimit since marking then
//clear the mark;
if (markPos != MARK_INVALID){
if (--limitCount == -1){
markPos = MARK_INVALID;
}
}
if (readPos >= BUF_SIZE){
readPos = 0;
}
}
}
return result;
}
public int read(byte[] data) throws IOException{
int result = 0;
int read;
for (int i=0, len = data.length; i<len; i++){
if ((read = read()) > -1){
data[i] = (byte)read;
}else{
result = i;
break;
}
}
return result;
}
public int read(byte[] data, int offs, int len) throws IOException{
int result = 0;
int read;
int limit = offs + len;
if (limit <= data.length){
for (int i=offs; i<len; i++){
if ((read = read()) > -1){
data[i] = (byte)read;
}else{
result = i - offs;
break;
}
}
}
return result;
}
/**
* Read a line from the underlying input stream (via the buffer).
* A line is delimited by a '\r' character.
*
* @return String - An entire line from the input stream.
* @throws IOException
*/
public String readLine() throws IOException{
StringBuffer strBuf = new StringBuffer();
int read;
while ((read=read()) != -1 && read != '\r'){
strBuf.append((char)read);
}
return strBuf.toString();
}
/**
* Mark the current position in the buffer. No more than half of
* the total buffer can be remembered; if readlimit is more than this
* it is silently reduced.
*
* @param readlimit - The maximum number of bytes which can be read without losing the mark
* @see #match()
*/
public void mark(int readlimit){
markPos = readPos;
limit = limitCount = (Math.min(readlimit, BUF_SIZE/2));
}
/**
* @see #mark()
* @see #match()
*
* @return false because although mark (etc.) is implemented, several
* private methods may move the mark. In other words use mark() with caution.
*/
public boolean markSupported(){
return false;
}
public void reset() throws IOException{
if (markPos != MARK_INVALID){
readPos = markPos;
limitCount = limit;
}
}
/**
* Try to skip n bytes of data. Amount skipped is limited by
* available data, but we will attempt to fill the buffer if
* neccesary.
*
* @todo This hasn't been tested, need to check skip works whether wrapping
* or not. Also need to check mark is dealt with correctly.
* @param n - the number of bytes to try to skip over.
* @return long - the number of bytes actually skipped
* @throws IOException
*/
public long skip(long n) throws IOException{
if (available() < n){
fill();
}
n = Math.min(n, available()); //side effect: n must now be in int range!
if (readPos + n < BUF_SIZE){
readPos += n;
}else{
readPos = (int) n - (BUF_SIZE - readPos);
}
if (markPos != MARK_INVALID){
if ((limitCount -= n) < 0){
markPos = MARK_INVALID;
}
}
return n;
}
/**
* Close down and tidy up.
*
* @throws IOException
*/
public void close() throws IOException{
handler.stop();
in.close();
out.close();
port.close();
in = null;
out = null;
port = null;
buf = null;
}
/**
* Send a line of text to the modem. A return charcter ('\r') is
* appended to the line.
*
* @param data - The data to be written to the modem.
*/
public void writeLine(String data){
out.print(data);
out.print('\r');
out.flush();
}
public void write(byte[] wrtBuf){
write(wrtBuf, 0, wrtBuf.length);
}
public void write(byte[] wrtBuf, int off, int len){
out.write(wrtBuf, off, len);
}
public void write(int b){
out.write(b);
}
/**
* Try to match the String goodMatch in the input buffer.
*
* The read position in the input buffer is moved to the first character
* after the matched string.
*
* @param timeout - how long we can look for a match (in milliseconds)
* @param goodMatch - the definition of success. May be a regular expression
* @param badMatch - the definition of failure. May be a regular expression
* @return - TIMEOUT, GOOD_MATCH or BAD_MATCH as appropriate. Use getMatch()
* to get the String which caused either of the matches.
* @throws IOException
* @see #getMatch()
*/
public int match(int timeout, String goodMatch, String badMatch)
throws IOException{
int result = TIMEOUT;
long startTime = System.currentTimeMillis();
Pattern p = Pattern.compile(goodMatch + '|' + badMatch);
Matcher m = p.matcher("");
//Loop until we match one of the Strings or we timeout
boolean matched = false;
while(matched == false){
m.reset(readLine());
matched = m.find();
//Throw a TimeoutException if we have taken too long
if (matched == false && (System.currentTimeMillis() - startTime > timeout)){
//result = TIMEOUT; //default
break;
}
}
if (matched){
lastMatch = m.group(0);
p = Pattern.compile(goodMatch);
m = p.matcher(lastMatch);
if (m.find()){
result = GOOD_MATCH;
}else{
result = BAD_MATCH;
}
}else{
lastMatch = "";
}
return result;
}
/**
* Returns the last string that was matched by the match(int, String, String) method.
*
* @return - String that was matched by match(int, String, String) if no match
* then empty string is returned.
*
* @see #match()
*/
public String getMatch(){
return lastMatch;
}
//SerialPortEventListener implementation
public void serialEvent(SerialPortEvent evt){
switch (evt.getEventType()) {
case SerialPortEvent.RI:
modem.ringing();
break;
case SerialPortEvent.DATA_AVAILABLE:
System.err.println("Data available");
break;
default:
System.err.println("other event: " + evt.getEventType());
}
}
///////////////////////////////////////////////////////////////////////////////
/**
* A Runnable to deal with <DLE> shielded characters. the run() method
* waits on <code>lock</code>, calling either stop() or setShield(char) will
* call notifyAll() on <code>lock</code> which will wake up the run thread
* to deal with the cause.
*
* This may be an architectural mistake - maybe events would have worked
* better?
*/
private class ShieldHandler implements Runnable{
private Vector shields = new Vector();
private boolean stop = false;
private final Object lock = new Object();
public void run(){
do{
synchronized(lock){
try{
lock.wait();
}catch(InterruptedException ex){}
}
if ((stop == false) && (shields.size() > 0)){
char shield = ((Character)shields.remove(0)).charValue();
modem.dleReceived(shield);
}
}while (stop == false);
}
public void stop(){
stop = true;
synchronized (lock) {
lock.notifyAll();
}
}
public void setShield(char shield){
shields.add(new Character(shield));
synchronized (lock) {
lock.notifyAll();
}
}
}
}