package com.dreikraft.axbo.data; import com.dreikraft.events.ApplicationEventDispatcher; import com.dreikraft.axbo.OS; import com.dreikraft.axbo.events.AxboConnected; import com.dreikraft.axbo.events.AxboDisconnected; import com.dreikraft.axbo.util.ByteUtil; import gnu.io.CommPortIdentifier; import gnu.io.NoSuchPortException; import gnu.io.PortInUseException; import gnu.io.SerialPort; import gnu.io.SerialPortEvent; import gnu.io.UnsupportedCommOperationException; import java.util.List; import java.io.IOException; import java.util.ArrayList; import java.util.Enumeration; import java.util.TooManyListenersException; import org.apache.commons.logging.*; /** * The RXTXSerialDataInterface handles serial interface communication via the * RXTX comm port implementation. The class is implemented as an enum singleton * (thread safe singleton). * * @author jan.illetschko@3kraft.com */ public enum RXTXSerialDataInterface implements DataInterface { /** * Singleton instance. */ INSTANCE; /** * Logger. */ public static final Log log = LogFactory.getLog(RXTXSerialDataInterface.class); /** * Serial port owner name. */ public static final String PORT_OWNER = "axbo"; /** * Serial interface data recipient thread monitor. */ static private class DataMonitor { private boolean dataReceived = false; /** * Retrieves Data received flag. * * @return true if data was received. */ public boolean isDataReceived() { return dataReceived; } /** * Sets data received flag. * * @param dataReceived true if data was received. */ public void setDataReceived(boolean dataReceived) { this.dataReceived = dataReceived; } } private SerialPort serialPort; private final DataMonitor dataMonitor = new DataMonitor(); /** * Starts the serial interface if it has not been already started. * * @param portName the name of the serial interface. * @throws DataInterfaceException if aXbo can not be connected. */ @Override public void start(final String portName) throws DataInterfaceException { CommPortIdentifier portId = getCommPortIdentifier(portName); // the port has been started already and can be reused if (isStarted(portName)) { return; } // configure a new conection try { if (log.isDebugEnabled()) { log.debug("lookup serial port: " + portName); } // get the default serial device data final DeviceType deviceType = DeviceContext.getDeviceType(); // try to open the serial port, wait for x seconds serialPort = (SerialPort) portId.open(PORT_OWNER, deviceType.getTimeout()); // set connection parameters serialPort.setSerialPortParams(deviceType.getBaudRate(), deviceType.getDataBits(), deviceType.getStopBits(), deviceType.getParity()); serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); // add event listener, enable events serialPort.addEventListener(this); serialPort.notifyOnDataAvailable(true); // notify gui about successful connection ApplicationEventDispatcher.getInstance().dispatchGUIEvent(new AxboConnected( this, true)); } catch (PortInUseException | UnsupportedCommOperationException | TooManyListenersException ex) { serialPort.close(); throw new DataInterfaceException(ex.getMessage(), ex); } } /** * Stops and releases the serial port. */ @Override public void stop() { if (serialPort != null) { if (log.isDebugEnabled()) { log.debug("closing serial port " + serialPort.getName()); } serialPort.close(); // send gui notification ApplicationEventDispatcher.getInstance().dispatchGUIEvent(new AxboDisconnected( this, true)); } } /** * Checks if the serial port with the given name has already been started. * * @param portName the name of the serial port * @return true, if the port has been started. * @throws DataInterfaceException if the serial port fails */ @Override public boolean isStarted(final String portName) throws DataInterfaceException { CommPortIdentifier testedPortId = getCommPortIdentifier(portName); return testedPortId != null && testedPortId.isCurrentlyOwned() && PORT_OWNER.equals(testedPortId.getCurrentOwner()); } /** * Writes data to a connected serial port. Waits for confirmation of receipt * from aXbo (which is processed in a different thread). If confirmation * misses within a configured time on the interface, it will try to rewrite * the data. * * @param portName the name of the serial port. * @param data the data bytes to write * @param retries number of retries, if the writing fails. * * @throws DataInterfaceException if data cannot be written successfully onto * the serial interface or no data recipient confirmation was received from * aXbo */ @Override public void writeData(final String portName, final byte[] data, int retries) throws DataInterfaceException { if (log.isDebugEnabled()) { log.debug("write data: " + ByteUtil.dumpByteArray(data)); } // try to start serial port always start(portName); try { // serialize data writes synchronized (dataMonitor) { // reset parser state DeviceContext.getDeviceType().getProtocolHandler().reset(); int retryCount = 0; // reset the data monitor dataMonitor.setDataReceived(false); // try to write the data onto the connected interface while (!dataMonitor.isDataReceived() && retryCount < retries) { serialPort.setOutputBufferSize(data.length); serialPort.getOutputStream().write(data, 0, data.length); // wait for confirmation from aXbo in different thread dataMonitor.wait(DeviceContext.getDeviceType().getTimeout()); retryCount++; } } } catch (InterruptedException | IOException ex) { log.error(ex.getMessage(), ex); } // no confirmation has been received if (!dataMonitor.isDataReceived()) { throw new DataInterfaceException("no data"); } } /** * Process incoming data from aXbo asynchronous (thread). * * @param e a data event from the serial interface. */ @Override public void serialEvent(SerialPortEvent e) { if (e.getEventType() == SerialPortEvent.DATA_AVAILABLE) { // new data is available int newDataLength; try { // read data from serial port into a byte array buffer while ((newDataLength = serialPort.getInputStream().available()) > 0) { byte[] data = new byte[newDataLength]; if (serialPort.getInputStream().read(data, 0, newDataLength) > 0) { // send byte array to singleton parser for further processing DeviceContext.getDeviceType().getProtocolHandler().parse(data); } } } catch (Exception ex) { log.error(ex.getMessage(), ex); } } } /** * Retrieve available COM ports from operating system. * * @return the List of COM ports. */ @Override public List<String> getCommPortNames() { final List<String> commPortNames = new ArrayList<>(); if (OS.Mac.isCurrent()) { // on mac os there is only this device commPortNames.add("/dev/tty.SLAB_USBtoUART"); } else { // on windows and linux search available comm ports for (Enumeration<?> e = CommPortIdentifier.getPortIdentifiers(); e.hasMoreElements();) { CommPortIdentifier port = (CommPortIdentifier) e.nextElement(); if (port.getPortType() == CommPortIdentifier.PORT_SERIAL) { commPortNames.add(port.getName()); } } } return commPortNames; } private CommPortIdentifier getCommPortIdentifier(String portName) throws DataInterfaceException { try { return CommPortIdentifier.getPortIdentifier(portName); } catch (NoSuchPortException ex) { throw new DataInterfaceException(ex.getMessage(), ex); } } /** * Notifies that the interface received data and is ready to write new data. */ @Override public void dataReceived() { try { synchronized (dataMonitor) { if (!dataMonitor.isDataReceived()) { dataMonitor.setDataReceived(true); // notify waiting writer thread dataMonitor.notifyAll(); } } } catch (Throwable t) { log.error(t.getMessage(), t); } } }