mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
465 lines
12 KiB
Java
465 lines
12 KiB
Java
package org.briarproject.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.Executor;
|
|
import java.util.concurrent.Semaphore;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.locks.Condition;
|
|
import java.util.concurrent.locks.Lock;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
import java.util.logging.Logger;
|
|
|
|
import jssc.SerialPortEvent;
|
|
import jssc.SerialPortEventListener;
|
|
|
|
import org.briarproject.api.reliability.ReliabilityLayer;
|
|
import org.briarproject.api.reliability.ReliabilityLayerFactory;
|
|
import org.briarproject.api.reliability.WriteHandler;
|
|
import org.briarproject.api.system.Clock;
|
|
|
|
class ModemImpl implements Modem, WriteHandler, 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 = 2 * 60 * 1000; // Milliseconds
|
|
private static final int ESCAPE_SEQUENCE_GUARD_TIME = 1000; // Milliseconds
|
|
|
|
private final Executor executor;
|
|
private final ReliabilityLayerFactory reliabilityFactory;
|
|
private final Clock clock;
|
|
private final Callback callback;
|
|
private final SerialPort port;
|
|
private final Semaphore stateChange;
|
|
private final byte[] line;
|
|
|
|
private int lineLen = 0;
|
|
|
|
private ReliabilityLayer reliability = null;
|
|
private boolean initialised = false, connected = false;
|
|
|
|
private final Lock synchLock = new ReentrantLock();
|
|
private final Condition connectedStateChanged = synchLock.newCondition();
|
|
private final Condition initialisedStateChanged = synchLock.newCondition();
|
|
|
|
|
|
ModemImpl(Executor executor, ReliabilityLayerFactory reliabilityFactory,
|
|
Clock clock, Callback callback, SerialPort port) {
|
|
this.executor = executor;
|
|
this.reliabilityFactory = reliabilityFactory;
|
|
this.clock = clock;
|
|
this.callback = callback;
|
|
this.port = port;
|
|
stateChange = new Semaphore(1);
|
|
line = new byte[MAX_LINE_LENGTH];
|
|
}
|
|
|
|
public boolean start() throws IOException {
|
|
LOG.info("Starting");
|
|
try {
|
|
stateChange.acquire();
|
|
} catch(InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
throw new IOException("Interrupted while waiting to start");
|
|
}
|
|
try {
|
|
// Open the serial port
|
|
port.openPort();
|
|
// Find a suitable baud rate and initialise the modem
|
|
try {
|
|
boolean foundBaudRate = false;
|
|
for(int baudRate : BAUD_RATES) {
|
|
if(port.setParams(baudRate, 8, 1, 0)) {
|
|
foundBaudRate = true;
|
|
break;
|
|
}
|
|
}
|
|
if(!foundBaudRate) {
|
|
tryToClose(port);
|
|
throw new IOException("No suitable baud rate");
|
|
}
|
|
port.purgePort(PURGE_RXCLEAR | PURGE_TXCLEAR);
|
|
port.addEventListener(this);
|
|
port.writeBytes("ATZ\r\n".getBytes("US-ASCII")); // Reset
|
|
port.writeBytes("ATE0\r\n".getBytes("US-ASCII")); // Echo off
|
|
} catch(IOException e) {
|
|
tryToClose(port);
|
|
throw e;
|
|
}
|
|
// Wait for the event thread to receive "OK"
|
|
boolean success = false;
|
|
try {
|
|
synchLock.lock();
|
|
try {
|
|
long now = clock.currentTimeMillis();
|
|
long end = now + OK_TIMEOUT;
|
|
while(now < end && !initialised) {
|
|
initialisedStateChanged.await(end - now, TimeUnit.MILLISECONDS);
|
|
now = clock.currentTimeMillis();
|
|
}
|
|
success = initialised;
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
} catch(InterruptedException e) {
|
|
tryToClose(port);
|
|
Thread.currentThread().interrupt();
|
|
throw new IOException("Interrupted while initialising");
|
|
}
|
|
if(success) return true;
|
|
tryToClose(port);
|
|
return false;
|
|
} finally {
|
|
stateChange.release();
|
|
}
|
|
}
|
|
|
|
private void tryToClose(SerialPort port) {
|
|
try {
|
|
if(port != null) port.closePort();
|
|
} catch(IOException e) {
|
|
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
|
}
|
|
}
|
|
|
|
public void stop() throws IOException {
|
|
LOG.info("Stopping");
|
|
synchLock.lock();
|
|
try {
|
|
// Wake any threads that are waiting to connect
|
|
initialised = false;
|
|
connected = false;
|
|
initialisedStateChanged.signalAll();
|
|
connectedStateChanged.signalAll();
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
// Hang up if necessary and close the port
|
|
try {
|
|
stateChange.acquire();
|
|
} catch(InterruptedException e) {
|
|
tryToClose(port);
|
|
Thread.currentThread().interrupt();
|
|
throw new IOException("Interrupted while waiting to stop");
|
|
}
|
|
try {
|
|
hangUpInner();
|
|
port.closePort();
|
|
} finally {
|
|
stateChange.release();
|
|
}
|
|
}
|
|
|
|
private void hangUpInner() throws IOException {
|
|
ReliabilityLayer reliability;
|
|
synchLock.lock();
|
|
try {
|
|
if(this.reliability == null) {
|
|
LOG.info("Not hanging up - already on the hook");
|
|
return;
|
|
}
|
|
reliability = this.reliability;
|
|
this.reliability = null;
|
|
connected = false;
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
reliability.stop();
|
|
LOG.info("Hanging up");
|
|
try {
|
|
clock.sleep(ESCAPE_SEQUENCE_GUARD_TIME);
|
|
port.writeBytes("+++".getBytes("US-ASCII"));
|
|
clock.sleep(ESCAPE_SEQUENCE_GUARD_TIME);
|
|
port.writeBytes("ATH\r\n".getBytes("US-ASCII"));
|
|
} catch(InterruptedException e) {
|
|
tryToClose(port);
|
|
Thread.currentThread().interrupt();
|
|
throw new IOException("Interrupted while hanging up");
|
|
} catch(IOException e) {
|
|
tryToClose(port);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public boolean dial(String number) throws IOException {
|
|
if(!stateChange.tryAcquire()) {
|
|
LOG.info("Not dialling - state change in progress");
|
|
return false;
|
|
}
|
|
try {
|
|
ReliabilityLayer reliability =
|
|
reliabilityFactory.createReliabilityLayer(this);
|
|
synchLock.lock();
|
|
try {
|
|
if(!initialised) {
|
|
LOG.info("Not dialling - modem not initialised");
|
|
return false;
|
|
}
|
|
if(this.reliability != null) {
|
|
LOG.info("Not dialling - call in progress");
|
|
return false;
|
|
}
|
|
this.reliability = reliability;
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
reliability.start();
|
|
LOG.info("Dialling");
|
|
try {
|
|
String dial = "ATDT" + number + "\r\n";
|
|
port.writeBytes(dial.getBytes("US-ASCII"));
|
|
} catch(IOException e) {
|
|
tryToClose(port);
|
|
throw e;
|
|
}
|
|
// Wait for the event thread to receive "CONNECT"
|
|
try {
|
|
synchLock.lock();
|
|
try {
|
|
long now = clock.currentTimeMillis();
|
|
long end = now + CONNECT_TIMEOUT;
|
|
while(now < end && initialised && !connected) {
|
|
connectedStateChanged.await(end - now, TimeUnit.MILLISECONDS);
|
|
now = clock.currentTimeMillis();
|
|
}
|
|
if(connected) return true;
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
} catch(InterruptedException e) {
|
|
tryToClose(port);
|
|
Thread.currentThread().interrupt();
|
|
throw new IOException("Interrupted while dialling");
|
|
}
|
|
hangUpInner();
|
|
return false;
|
|
} finally {
|
|
stateChange.release();
|
|
}
|
|
}
|
|
|
|
public InputStream getInputStream() throws IOException {
|
|
ReliabilityLayer reliability;
|
|
synchLock.lock();
|
|
try {
|
|
reliability = this.reliability;
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
if(reliability == null) throw new IOException("Not connected");
|
|
return reliability.getInputStream();
|
|
}
|
|
|
|
public OutputStream getOutputStream() throws IOException {
|
|
ReliabilityLayer reliability;
|
|
synchLock.lock();
|
|
try {
|
|
reliability = this.reliability;
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
if(reliability == null) throw new IOException("Not connected");
|
|
return reliability.getOutputStream();
|
|
}
|
|
|
|
public void hangUp() throws IOException {
|
|
try {
|
|
stateChange.acquire();
|
|
} catch(InterruptedException e) {
|
|
tryToClose(port);
|
|
Thread.currentThread().interrupt();
|
|
throw new IOException("Interrupted while waiting to hang up");
|
|
}
|
|
try {
|
|
hangUpInner();
|
|
} finally {
|
|
stateChange.release();
|
|
}
|
|
}
|
|
|
|
public void handleWrite(byte[] b) throws IOException {
|
|
try {
|
|
port.writeBytes(b);
|
|
} catch(IOException e) {
|
|
tryToClose(port);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public void serialEvent(SerialPortEvent ev) {
|
|
try {
|
|
if(ev.isRXCHAR()) {
|
|
byte[] b = port.readBytes();
|
|
if(!handleData(b)) handleText(b);
|
|
} else if(ev.isDSR() && ev.getEventValue() == 0) {
|
|
LOG.info("Remote end hung up");
|
|
hangUp();
|
|
} else {
|
|
if(LOG.isLoggable(INFO)) {
|
|
LOG.info("Serial event " + ev.getEventType() + " " +
|
|
ev.getEventValue());
|
|
}
|
|
}
|
|
} catch(IOException e) {
|
|
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
|
}
|
|
}
|
|
|
|
private boolean handleData(byte[] b) throws IOException {
|
|
ReliabilityLayer reliability;
|
|
synchLock.lock();
|
|
try {
|
|
reliability = this.reliability;
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
if(reliability == null) return false;
|
|
reliability.handleRead(b);
|
|
return true;
|
|
}
|
|
|
|
private void handleText(byte[] b) throws IOException {
|
|
if(lineLen + b.length > MAX_LINE_LENGTH) {
|
|
tryToClose(port);
|
|
throw new IOException("Line too long");
|
|
}
|
|
for(int i = 0; i < b.length; i++) {
|
|
line[lineLen] = b[i];
|
|
if(b[i] == '\n') {
|
|
// FIXME: Use CharsetDecoder to catch invalid ASCII
|
|
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")) {
|
|
synchLock.lock();
|
|
try {
|
|
connected = true;
|
|
connectedStateChanged.signalAll();
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
// 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);
|
|
handleData(data);
|
|
}
|
|
return;
|
|
} else if(s.equals("BUSY") || s.equals("NO DIALTONE")
|
|
|| s.equals("NO CARRIER")) {
|
|
synchLock.lock();
|
|
try {
|
|
connected = false;
|
|
connectedStateChanged.signalAll();
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
} else if(s.equals("OK")) {
|
|
synchLock.lock();
|
|
try {
|
|
initialised = true;
|
|
initialisedStateChanged.signalAll();
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
} else if(s.equals("RING")) {
|
|
executor.execute(new Runnable() {
|
|
public void run() {
|
|
try {
|
|
answer();
|
|
} catch(IOException e) {
|
|
if(LOG.isLoggable(WARNING))
|
|
LOG.log(WARNING, e.toString(), e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
lineLen++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void answer() throws IOException {
|
|
if(!stateChange.tryAcquire()) {
|
|
LOG.info("Not answering - state change in progress");
|
|
return;
|
|
}
|
|
try {
|
|
ReliabilityLayer reliability =
|
|
reliabilityFactory.createReliabilityLayer(this);
|
|
synchLock.lock();
|
|
try {
|
|
if(!initialised) {
|
|
LOG.info("Not answering - modem not initialised");
|
|
return;
|
|
}
|
|
if(this.reliability != null) {
|
|
LOG.info("Not answering - call in progress");
|
|
return;
|
|
}
|
|
this.reliability = reliability;
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
reliability.start();
|
|
LOG.info("Answering");
|
|
try {
|
|
port.writeBytes("ATA\r\n".getBytes("US-ASCII"));
|
|
} catch(IOException e) {
|
|
tryToClose(port);
|
|
throw e;
|
|
}
|
|
// Wait for the event thread to receive "CONNECT"
|
|
boolean success = false;
|
|
try {
|
|
synchLock.lock();
|
|
try {
|
|
long now = clock.currentTimeMillis();
|
|
long end = now + CONNECT_TIMEOUT;
|
|
while(now < end && initialised && !connected) {
|
|
connectedStateChanged.await(end - now, TimeUnit.MILLISECONDS);
|
|
now = clock.currentTimeMillis();
|
|
}
|
|
success = connected;
|
|
}
|
|
finally{
|
|
synchLock.unlock();
|
|
}
|
|
} catch(InterruptedException e) {
|
|
tryToClose(port);
|
|
Thread.currentThread().interrupt();
|
|
throw new IOException("Interrupted while answering");
|
|
}
|
|
if(success) callback.incomingCallConnected();
|
|
else hangUpInner();
|
|
} finally {
|
|
stateChange.release();
|
|
}
|
|
}
|
|
}
|