package de.uniluebeck.itm.wsn.drivers.mock; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import de.uniluebeck.itm.util.StringUtils; import de.uniluebeck.itm.util.concurrent.ExecutorUtils; import de.uniluebeck.itm.wsn.drivers.core.ChipType; import de.uniluebeck.itm.wsn.drivers.core.Device; import de.uniluebeck.itm.wsn.drivers.core.MacAddress; import de.uniluebeck.itm.wsn.drivers.core.operation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.*; import java.util.Map; import java.util.concurrent.*; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Singleton public class MockDevice implements Device { private class MessageRunnable implements Runnable { private final OutputStream outputStream; private final byte[] messageBytes; private MessageRunnable(final OutputStream outputStream, final byte[] messageBytes) { this.outputStream = outputStream; this.messageBytes = messageBytes; } @Override public void run() { try { if (log.isTraceEnabled()) { log.trace("Writing message bytes {}", StringUtils.toHexString(messageBytes)); } sleepIfUartLatencyConfigured(); synchronized (outputStream) { outputStream.write(messageBytes); outputStream.flush(); } } catch (IOException e) { log.error("IOException while writing to MockConnection.inputStreamPipedOutputStream: {}", e); throw new RuntimeException(e); } } } private class EchoRunnable implements Runnable { private final InputStream inputStream; private final OutputStream outputStream; private EchoRunnable(final InputStream inputStream, final OutputStream outputStream) { this.inputStream = inputStream; this.outputStream = outputStream; } @Override public void run() { log.trace("MockDevice.echoRunnable started!"); try { byte[] b = new byte[1024]; int read; synchronized (inputStream) { while ((read = inputStream.read(b)) != -1) { if (log.isTraceEnabled()) { log.trace("MockDevice.echoRunnable echoing {} bytes: {}", read, new String(b, 0, read)); } sleepIfUartLatencyConfigured(); try { synchronized (outputStream) { outputStream.write(b, 0, read); outputStream.flush(); } } catch (IOException e) { log.error("IOException while writing to outputStream: {}", e); throw new RuntimeException(e); } } } } catch (Exception e) { if (e instanceof InterruptedIOException) { // expected when shutting down } else { log.error("Exception in MockDevice.echoRunnable: {}", e); throw new RuntimeException(e); } } } } private static final String OPTION_UART_LATENCY = "UART_LATENCY"; private static final String OPTION_BOOT_MESSAGE = "BOOT_MESSAGE"; private static final String OPTION_BOOT_MESSAGE_TYPE = "BOOT_MESSAGE_TYPE"; private static final String OPTION_HEARTBEAT_MESSAGE = "HEARTBEAT_MESSAGE"; private static final String OPTION_HEARTBEAT_MESSAGE_TYPE = "HEARTBEAT_MESSAGE_TYPE"; private static final String OPTION_HEARTBEAT_MESSAGE_RATE = "HEARTBEAT_MESSAGE_RATE"; private static final String OPTION_ECHO = "ECHO"; private static final Logger log = LoggerFactory.getLogger(MockDevice.class); private final PipedInputStream driverInputStream; private final PipedOutputStream driverOutputStream; private final PipedOutputStream pipedOutputStreamToDriverInputStream; private final PipedInputStream pipedInputStreamFromDriverOutputStream; private final OperationFactory operationFactory; private final Map<String, String> configuration; private ScheduledExecutorService scheduler; private ScheduledFuture<?> heartbeatSchedule; private ExecutorService echoExecutor; private Future<?> echoFuture; private volatile boolean connected; @Inject public MockDevice(@Named("driverInputStream") final PipedInputStream driverInputStream, @Named("driverOutputStream") final PipedOutputStream driverOutputStream, @Named("pipedOutputStreamToDriverInputStream") final PipedOutputStream pipedOutputStreamToDriverInputStream, @Named("pipedInputStreamFromDriverOutputStream") final PipedInputStream pipedInputStreamFromDriverOutputStream, final OperationFactory operationFactory, @Named("configuration") final Map<String, String> configuration) { this.driverInputStream = driverInputStream; this.driverOutputStream = driverOutputStream; this.pipedOutputStreamToDriverInputStream = pipedOutputStreamToDriverInputStream; this.pipedInputStreamFromDriverOutputStream = pipedInputStreamFromDriverOutputStream; this.operationFactory = operationFactory; this.configuration = configuration; } @Override public OperationFuture<Void> eraseFlash(long timeoutMillis, @Nullable OperationListener<Void> listener) { log.trace("Erasing flash (timeout: " + timeoutMillis + "ms)"); return executeOperation(operationFactory.createEraseFlashOperation(timeoutMillis, listener)); } @Override public OperationFuture<ChipType> getChipType(long timeoutMillis, @Nullable OperationListener<ChipType> listener) { log.trace("Reading Chip Type (timeout: " + timeoutMillis + "ms)"); return executeOperation(operationFactory.createGetChipTypeOperation(timeoutMillis, listener)); } @Override public OperationFuture<Boolean> isNodeAlive(final long timeoutMillis, @Nullable final OperationListener<Boolean> listener) { log.trace("Checking if node is alive (timeout: {}ms)", timeoutMillis); return executeOperation(operationFactory.createIsNodeAliveOperation(timeoutMillis, listener)); } @Override public OperationFuture<Void> program(byte[] data, long timeoutMillis, @Nullable OperationListener<Void> listener) { log.trace("Programming (timeout: " + timeoutMillis + "ms)"); return executeOperation(operationFactory.createProgramOperation(data, timeoutMillis, listener)); } @Override public OperationFuture<byte[]> readFlash(int address, int length, long timeoutMillis, @Nullable OperationListener<byte[]> listener) { log.trace("Reading flash (address: " + address + ", length: " + length + ", timeout: " + timeoutMillis + "ms)"); checkArgument(address >= 0, "Negative length is not allowed."); checkArgument(length >= 0, "Negative address is not allowed."); return executeOperation(operationFactory.createReadFlashOperation(address, length, timeoutMillis, listener)); } @Override public OperationFuture<MacAddress> readMac(long timeoutMillis, @Nullable OperationListener<MacAddress> listener) { log.trace("Reading MAC address (timeout: " + timeoutMillis + "ms)"); return executeOperation(operationFactory.createReadMacAddressOperation(timeoutMillis, listener)); } @Override public OperationFuture<Void> reset(long timeoutMillis, @Nullable OperationListener<Void> listener) { log.trace("Resetting (timeout: " + timeoutMillis + "ms)"); return executeOperation(operationFactory.createResetOperation(timeoutMillis, listener)); } @Override public OperationFuture<Void> writeFlash(int address, byte[] data, int length, long timeoutMillis, @Nullable OperationListener<Void> listener) { log.trace("Writing flash (address: " + address + ", length: " + length + ", timeout: " + timeoutMillis + "ms)"); checkArgument(address >= 0, "Negative length is not allowed."); checkNotNull(data, "Null data is not allowed."); checkArgument(length >= 0, "Negative address is not allowed."); return executeOperation( operationFactory.createWriteFlashOperation(address, data, length, timeoutMillis, listener) ); } @Override public OperationFuture<Void> writeMac(MacAddress macAddress, long timeoutMillis, @Nullable OperationListener<Void> listener) { log.trace("Writing MAC address (mac address: " + macAddress + ", timeout: " + timeoutMillis + "ms)"); checkNotNull(macAddress, "Null MAC address is not allowed."); return executeOperation(operationFactory.createWriteMacAddressOperation(macAddress, timeoutMillis, listener)); } @Override public InputStream getInputStream() { return driverInputStream; } @Override public OutputStream getOutputStream() { return driverOutputStream; } @Override public void connect(final String uri) throws IOException { try { final ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat("MockDevice-Thread %d") .build(); scheduler = Executors.newScheduledThreadPool(1, threadFactory); echoExecutor = Executors.newSingleThreadExecutor(threadFactory); startHeartBeatIfConfigured(); startEchoIfConfigured(); sendBootMessageIfConfigured(); } finally { connected = true; } } @Override public void close() throws IOException { try { stopHeartBeatIfRunning(); stopEchoIfRunning(); if (scheduler != null) { ExecutorUtils.shutdown(scheduler, 1, TimeUnit.SECONDS); } if (echoExecutor != null) { ExecutorUtils.shutdown(echoExecutor, 1, TimeUnit.SECONDS); } synchronized (driverInputStream) { synchronized (pipedOutputStreamToDriverInputStream) { driverInputStream.close(); pipedOutputStreamToDriverInputStream.close(); } } synchronized (driverOutputStream) { synchronized (pipedInputStreamFromDriverOutputStream) { driverOutputStream.close(); pipedInputStreamFromDriverOutputStream.close(); } } } finally { connected = false; } } @Override public boolean isConnected() { return connected; } @Override public boolean isClosed() { return !isConnected(); } void acquireLockOnDevice() { stopHeartBeatIfRunning(); stopEchoIfRunning(); } void releaseLockOnDevice() { startEchoIfConfigured(); startHeartBeatIfConfigured(); } void reset() { sleep(500); sendBootMessageIfConfigured(); } private void sleepIfUartLatencyConfigured() { final String uartLatencyString = configuration.get(OPTION_UART_LATENCY); final Integer uartLatency = uartLatencyString == null ? null : Integer.parseInt(uartLatencyString); if (uartLatency != null) { sleep(uartLatency); } } private <T> OperationFuture<T> executeOperation(final Operation<T> operation) { final OperationFutureImpl<T> operationFuture = new OperationFutureImpl<T>(operation); operation.addListener( new OperationAdapter<T>() { @Override public void onFailure(final Throwable throwable) { operationFuture.setException(throwable); } @Override public void onSuccess(final T result) { operationFuture.set(result); } } ); scheduler.submit(operation); return operationFuture; } private void sleep(final long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } } private void sendBootMessageIfConfigured() { String bootMessage = configuration.get(OPTION_BOOT_MESSAGE); String bootMessageType = configuration.get(OPTION_BOOT_MESSAGE_TYPE); if (bootMessage != null) { if (bootMessageType == null) { bootMessageType = "ascii"; } final byte[] bootMessageBytes = parseMessageBytes(bootMessage, bootMessageType); new MessageRunnable(pipedOutputStreamToDriverInputStream, bootMessageBytes).run(); } } private void startHeartBeatIfConfigured() { String heartbeatMessage = configuration.get(OPTION_HEARTBEAT_MESSAGE); String heartbeatMessageType = configuration.get(OPTION_HEARTBEAT_MESSAGE_TYPE); String heartbeatMessageRate = configuration.get(OPTION_HEARTBEAT_MESSAGE_RATE); if (heartbeatMessage != null) { log.debug("Starting heartbeat"); if (heartbeatMessageType == null) { heartbeatMessageType = "ascii"; } if (heartbeatMessageRate == null) { heartbeatMessageRate = "10000"; } int heartbeatMessageRateMillis; try { heartbeatMessageRateMillis = Integer.parseInt(heartbeatMessageRate); } catch (NumberFormatException e) { throw new RuntimeException( "HEARTBEAT_MESSAGE_RATE value \"" + heartbeatMessageRate + "\" can't be parsed as an Integer!" ); } byte[] heartbeatMessageBytes = parseMessageBytes(heartbeatMessage, heartbeatMessageType); heartbeatSchedule = scheduler.scheduleAtFixedRate( new MessageRunnable(pipedOutputStreamToDriverInputStream, heartbeatMessageBytes), heartbeatMessageRateMillis, heartbeatMessageRateMillis, TimeUnit.MILLISECONDS ); } } private byte[] parseMessageBytes(final String message, final String messageType) { byte[] heartbeatMessageBytes; if ("ascii".equals(messageType)) { heartbeatMessageBytes = message.getBytes(); } else if ("binary".equals(messageType)) { heartbeatMessageBytes = StringUtils.fromStringToByteArray(message); } else { throw new RuntimeException("Unknown HEARTBEAT_MESSAGE_TYPE value \"" + messageType + "\""); } return heartbeatMessageBytes; } private void stopHeartBeatIfRunning() { if (heartbeatSchedule != null) { log.debug("Stopping heartbeat"); heartbeatSchedule.cancel(true); heartbeatSchedule = null; } } private void startEchoIfConfigured() { final String echoString = configuration.get(OPTION_ECHO); final boolean echo = echoString == null || Boolean.parseBoolean(echoString); if (echo) { log.debug("Starting echo runnable"); echoFuture = echoExecutor.submit(new EchoRunnable( pipedInputStreamFromDriverOutputStream, pipedOutputStreamToDriverInputStream ) ); } } private void stopEchoIfRunning() { if (echoFuture != null) { log.debug("Stopping echo runnable"); echoFuture.cancel(true); echoFuture = null; } } }