diff --git a/libs/jssc-0.9-briar.jar b/libs/jssc-0.9-briar.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd1b56b9ba69727bd197f1f1f6678e2c05b88e15 Binary files /dev/null and b/libs/jssc-0.9-briar.jar differ diff --git a/src/net/sf/briar/plugins/modem/Modem.java b/src/net/sf/briar/plugins/modem/Modem.java new file mode 100644 index 0000000000000000000000000000000000000000..d85a7bfacd4ba257d113aa250a9dcf26cb1ace8c --- /dev/null +++ b/src/net/sf/briar/plugins/modem/Modem.java @@ -0,0 +1,35 @@ +package net.sf.briar.plugins.modem; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +interface Modem { + + /** + * Call this method once after creating the modem. If an exception is + * thrown, the modem cannot be used. + */ + void init() throws IOException; + + /** + * Initiates an outgoing call and returns true if the call connects. If the + * call does not connect the modem is hung up. + */ + boolean dial(String number) throws IOException; + + /** Returns a stream for reading from the currently connected call. */ + InputStream getInputStream(); + + /** Returns a stream for writing to the currently connected call. */ + OutputStream getOutputStream(); + + /** Hangs up the modem, ending the currently connected call. */ + void hangUp() throws IOException; + + interface Callback { + + /** Called when an incoming call connects. */ + void incomingCallConnected(); + } +} diff --git a/src/net/sf/briar/plugins/modem/ModemImpl.java b/src/net/sf/briar/plugins/modem/ModemImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..be4515fa57adf1f24d0bb6d27bca7e333ce0a7c7 --- /dev/null +++ b/src/net/sf/briar/plugins/modem/ModemImpl.java @@ -0,0 +1,304 @@ +package net.sf.briar.plugins.modem; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static jssc.SerialPort.PURGE_RXCLEAR; +import static jssc.SerialPort.PURGE_TXCLEAR; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +import jssc.SerialPort; +import jssc.SerialPortEvent; +import jssc.SerialPortEventListener; +import jssc.SerialPortException; + +class ModemImpl implements Modem, SerialPortEventListener { + + private static final Logger LOG = + Logger.getLogger(ModemImpl.class.getName()); + private static final int MAX_LINE_LENGTH = 256; + private static final int[] BAUD_RATES = { + 256000, 128000, 115200, 57600, 38400, 19200, 14400, 9600, 4800, 1200 + }; + private static final int OK_TIMEOUT = 5 * 1000; // Milliseconds + private static final int CONNECT_TIMEOUT = 60 * 1000; // Milliseconds + + private final Callback callback; + private final SerialPort port; + private final AtomicBoolean initialised, offHook, connected; + private final byte[] line; + + private int lineLen = 0; + + // A fresh queue is used for each connection + private volatile BlockingQueue<byte[]> received; + + ModemImpl(Callback callback, String portName) { + this.callback = callback; + port = new SerialPort(portName); + initialised = new AtomicBoolean(false); + offHook = new AtomicBoolean(false); + connected = new AtomicBoolean(false); + line = new byte[MAX_LINE_LENGTH]; + received = new LinkedBlockingQueue<byte[]>(); + } + + public void init() throws IOException { + if(LOG.isLoggable(INFO)) LOG.info("Initialising"); + try { + // Open the serial port + if(!port.openPort()) + throw new IOException("Failed to open serial port"); + // Find a suitable baud rate + boolean foundBaudRate = false; + for(int baudRate : BAUD_RATES) { + if(port.setParams(baudRate, 8, 1, 0)) { + foundBaudRate = true; + break; + } + } + if(!foundBaudRate) + throw new IOException("Could not find a suitable baud rate"); + // Listen for incoming data and hangup events + port.addEventListener(this); + // Initialise the modem + port.purgePort(PURGE_RXCLEAR | PURGE_TXCLEAR); + port.writeBytes("ATZ\r\n".getBytes("US-ASCII")); // Reset + port.writeBytes("ATE0\r\n".getBytes("US-ASCII")); // Echo off + } catch(SerialPortException e) { + throw new IOException(e.toString()); + } + try { + // Wait for the modem to respond "OK" + synchronized(initialised) { + if(!initialised.get()) initialised.wait(OK_TIMEOUT); + if(!initialised.get()) + throw new IOException("Modem did not respond"); + } + } catch(InterruptedException e) { + if(LOG.isLoggable(WARNING)) + LOG.warning("Interrupted while initialising modem"); + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while initialising modem"); + } + } + + public boolean dial(String number) throws IOException { + if(offHook.getAndSet(true)) { + if(LOG.isLoggable(INFO)) + LOG.info("Not dialling - call in progress"); + return false; + } + if(LOG.isLoggable(INFO)) LOG.info("Dialling"); + try { + port.writeBytes(("ATDT" + number + "\r\n").getBytes("US-ASCII")); + } catch(SerialPortException e) { + throw new IOException(e.toString()); + } + try { + synchronized(connected) { + if(!connected.get()) connected.wait(CONNECT_TIMEOUT); + } + } catch(InterruptedException e) { + if(LOG.isLoggable(WARNING)) + LOG.warning("Interrupted while connecting outgoing call"); + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while connecting outgoing call"); + } + if(connected.get()) return true; + hangUp(); + return false; + } + + public InputStream getInputStream() { + return new ModemInputStream(received); + } + + public OutputStream getOutputStream() { + return new ModemOutputStream(); + } + + public void hangUp() throws IOException { + if(LOG.isLoggable(INFO)) LOG.info("Hanging up"); + try { + port.setDTR(false); + } catch(SerialPortException e) { + throw new IOException(e.toString()); + } + received.add(new byte[0]); // Empty buffer indicates EOF + received = new LinkedBlockingQueue<byte[]>(); + connected.set(false); + offHook.set(false); + } + + public void serialEvent(SerialPortEvent ev) { + try { + if(ev.isRXCHAR()) { + byte[] b = port.readBytes(); + if(connected.get()) received.add(b); + else handleText(b); + } else if(ev.isDSR() && ev.getEventValue() == 0) { + if(LOG.isLoggable(INFO)) LOG.info("Remote end hung up"); + hangUp(); + } + } catch(IOException e) { + if(LOG.isLoggable(WARNING)) LOG.warning(e.toString()); + } catch(SerialPortException e) { + if(LOG.isLoggable(WARNING)) LOG.warning(e.toString()); + } + } + + private void handleText(byte[] b) throws IOException { + if(lineLen + b.length > MAX_LINE_LENGTH) + throw new IOException("Line too long"); + for(int i = 0; i < b.length; i++) { + line[lineLen] = b[i]; + if(b[i] == '\n') { + String s = new String(line, 0, lineLen, "US-ASCII").trim(); + lineLen = 0; + if(LOG.isLoggable(INFO)) LOG.info("Modem status: " + s); + if(s.startsWith("CONNECT")) { + // There might be data in the buffer as well as text + int off = i + 1; + if(off < b.length) { + byte[] data = new byte[b.length - off]; + System.arraycopy(b, off, data, 0, data.length); + received.add(data); + } + synchronized(connected) { + if(!connected.getAndSet(true)) + connected.notifyAll(); + } + return; + } else if(s.equals("OK")) { + synchronized(initialised) { + if(!initialised.getAndSet(true)) + initialised.notifyAll(); + } + } else if(s.equals("RING")) { + // FIXME: Don't do this on the event thread + answer(); + } + } else { + lineLen++; + } + } + } + + private void answer() throws IOException { + if(offHook.getAndSet(true)) { + if(LOG.isLoggable(INFO)) + LOG.info("Not answering - call in progress"); + return; + } + if(LOG.isLoggable(INFO)) LOG.info("Answering"); + try { + port.writeBytes("ATA\r\n".getBytes("US-ASCII")); + } catch(SerialPortException e) { + throw new IOException(e.toString()); + } + try { + synchronized(connected) { + if(!connected.get()) connected.wait(CONNECT_TIMEOUT); + } + } catch(InterruptedException e) { + if(LOG.isLoggable(WARNING)) + LOG.warning("Interrupted while connecting incoming call"); + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while connecting incoming call"); + } + if(connected.get()) callback.incomingCallConnected(); + else hangUp(); + } + + private static class ModemInputStream extends InputStream { + + private final BlockingQueue<byte[]> received; + + private byte[] buf = null; + private int offset = 0; + + private ModemInputStream(BlockingQueue<byte[]> received) { + this.received = received; + } + + @Override + public int read() throws IOException { + getBufferIfNecessary(); + if(buf.length == 0) return -1; + return buf[offset++]; + } + + @Override + public int read(byte[] b) throws IOException { + getBufferIfNecessary(); + if(buf.length == 0) return -1; + int len = Math.min(b.length, buf.length - offset); + System.arraycopy(buf, offset, b, 0, len); + offset += len; + return len; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + getBufferIfNecessary(); + if(buf.length == 0) return -1; + len = Math.min(len, buf.length - offset); + System.arraycopy(buf, offset, b, off, len); + offset += len; + return len; + } + + private void getBufferIfNecessary() throws IOException { + if(buf == null || offset == buf.length) { + try { + buf = received.take(); + } catch(InterruptedException e) { + if(LOG.isLoggable(WARNING)) + LOG.warning("Interrupted while reading"); + Thread.currentThread().interrupt(); + throw new IOException(e.toString()); + } + offset = 0; + } + } + } + + private class ModemOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + try { + port.writeByte((byte) b); + } catch(SerialPortException e) { + throw new IOException(e.toString()); + } + } + + @Override + public void write(byte[] b) throws IOException { + try { + port.writeBytes(b); + } catch(SerialPortException e) { + throw new IOException(e.toString()); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if(len < b.length) { + byte[] copy = new byte[len]; + System.arraycopy(b, off, copy, 0, len); + write(copy); + } else { + write(b); + } + } + } +}