Serial Control Application Source Code November 16, 2019

This post will outline and provide the source code to our Serial Control application.

The application should really be called ASCII Control instead of Serial Control. The application got it’s name because communication was originally only available via the serial ports. Ethernet communication was added in a later release.

The code was rewritten to clean it up and make it available as a sample. We will talk about the features of the application that deal with serial communication, Ethernet communication, I/O control and I/O monitoring. Features such as versioning and logging are parts of this application that are beyond the scope of this post.

import com.integ.common.system.AssemblyBase;
import com.integ.common.system.ReleaseInfo;

class AssemblyInfo extends AssemblyBase {

    AssemblyInfo() {
        NAME = "SerialControl";
        VERSION = "6.0./****/19325./*//*/29";
        BUILD_TIME = "build-time:11/21/19 14:33 -05:00";

        RELEASE_NOTES = new ReleaseInfo[]{
            new ReleaseInfo("6.0", "15 nov 2019")
            .addNote("rewrite")
        };
    }

    public static final int APP_ID = 1800;
}
import com.integ.common.system.Application;
import com.integ.common.utils.RegistryUtils;

public class Config {

    private static final String APPDATA_ROOT = "AppData/" + Application.getAppName();



    static void init() {
        getSerialPortName();
        getTcpServerPortNumber();

        getIncomingTerminationString();
        getOutgoingTerminationString();
        getSendUnsolicitedIoAlerts();
        getSendDateStamp();
        getSendCounts();
    }



    public static String getSerialPortName() {
        return RegistryUtils.getRegistryKey(String.format("%s/SerialPort", APPDATA_ROOT), "none");
    }



    public static String getIncomingTerminationString() {
        String incomingTerminationString = RegistryUtils.getRegistryKey(
                String.format("%s/IncomingTerminationString", APPDATA_ROOT), "\\n");
        return new String(getTerminationBytes(incomingTerminationString));
    }



    public static String getOutgoingTerminationString() {
        String outgoingTerminationString = RegistryUtils.getRegistryKey(
                String.format("%s/OutgoingTerminationString", APPDATA_ROOT), "\\n");
        return new String(getTerminationBytes(outgoingTerminationString));
    }



    public static int getTcpServerPortNumber() {
        return RegistryUtils.getRegistryKey(
                String.format("%s/TcpServerPortNumber", APPDATA_ROOT), -1);
    }



    private static byte[] getTerminationBytes(String terminationString) {
        String newTermString = "";
        boolean backslashFound = false;
        for (int i = 0; i < terminationString.length(); i++) {
            if (terminationString.charAt(i) == '\\' && !backslashFound) {
                backslashFound = true;
            } else {

                if (backslashFound) {
                    switch (terminationString.charAt(i)) {
                        case 'r':
                            newTermString += '\r';
                            break;
                        case 'n':
                            newTermString += '\n';
                            break;
                        case 't':
                            newTermString += '\r';
                            break;
                        case 'f':
                            newTermString += '\f';
                            break;
                        case 'b':
                            newTermString += '\b';
                            break;
                        case '0':
                            newTermString += '\0';
                            break;
                    }
                } else {
                    newTermString += terminationString.charAt(i);
                }
                backslashFound = false;
            }
        }
        return newTermString.getBytes();
    }



    public static boolean getSendUnsolicitedIoAlerts() {
        return RegistryUtils.getRegistryKey(
                String.format("%s/SendUnsolicitedIoAlerts", APPDATA_ROOT), true);
    }



    public static boolean getSendDateStamp() {
        return RegistryUtils.getRegistryKey(
                String.format("%s/SendDateStamp", APPDATA_ROOT), true);
    }



    public static boolean getSendCounts() {
        return RegistryUtils.getRegistryKey(
                String.format("%s/SendCounts", APPDATA_ROOT), false);
    }

}
import com.integpg.system.JANOS;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;

public class Jrmon {

    public static byte[] parseCommand(String lowerCommand) throws IOException {
        if (0 <= lowerCommand.indexOf(",")) {
            int begin = 0;
            while (begin >= 0) {
                int end = lowerCommand.indexOf(",", begin + 1);
                parseCommand(lowerCommand, begin, end);
                if (end >= 0) {
                    end++;
                }
                begin = end;
            }
        } else {
            return parseCommand(lowerCommand, 0, lowerCommand.length());
        }

        return null;
    }



    public static byte[] parseCommand(String lowerCommand, int beginIndex, int endIndex) throws IOException {
        if (endIndex == -1) {
            endIndex = lowerCommand.length();
        }
        lowerCommand = lowerCommand.substring(beginIndex, endIndex);

        // used for caching the action
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);

        int i, n;
        int states = 0;
        int mask = 0;
        int parameter = 1000;
        boolean close = false;
        boolean open = false;
        boolean toggle = false;
        boolean reset = false;
        boolean setcounters = false;
        boolean dosetcounters = false;
        boolean pulse = false;
        boolean value = false;
        boolean stop = false;
        int shift = 0;

        int len = lowerCommand.length();
        char[] chars = new char[len];
        lowerCommand.getChars(0, len, chars, 0);

        int currentStates = (int) JANOS.getOutputStates();
//        System.out.println("Current States: " + Integer.toBinaryString(currentStates));
        states = currentStates;

        for (i = 0; i < len && !stop; i++) {
            /* each character in the comand */
            switch (chars[i]) {
                case 'c':   // [C]lose sets relay state to 1
                    close = true;
                    open = false;
                    toggle = false;
                    reset = false;
                    setcounters = false;
                    break;  // next character

                case 'o':   // [O]pen sets relay state to 0
                    open = true;
                    close = false;
                    toggle = false;
                    reset = false;
                    setcounters = false;
                    break;  // next character

                case 't':
                    open = false;
                    close = false;
                    toggle = true;
                    reset = false;
                    setcounters = false;
                    break;  // next character

                case 'p':   // [P]ulse indicates that changes are pulsed
                    pulse = true;
                    break;  // next character

                case '=':   // acquire parameter
                    // extract just the content after the '='

                    StringBuffer sb = new StringBuffer();
                    for (n = i + 1; n < lowerCommand.length(); n++) {
                        char c = lowerCommand.charAt(n);
                        if (!Character.isDigit(c)) {
                            break;
                        }
                        sb.append(c);
                        i++;
                    }

                    String cmd = sb.toString(); //command.toString().substring(i+1).trim();
                    // check that only digits are here

                    for (n = 0; n < cmd.length(); n++) {
                        if (!Character.isDigit(cmd.charAt(n))) {
                            return null;
                            // obtain the integer value
                        }
                    }
                    parameter = new Integer(cmd).intValue();
                    value = true;
//                    stop = true;   // ends command interpretation
                    break;

                case 's':   // sets selected counters
                    dosetcounters = true;
                    setcounters = true;
                    open = false;
                    close = false;
                    toggle = false;
                    reset = false;
                    break;

                case '1':   // digit sets up state and mask

                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                    if (!open && !close && !toggle && !reset && !setcounters) {
                        return null;
                    }
                    n = (int) (lowerCommand.charAt(i) - '1');
                    if (shift == 0) {
                        mask |= (1 << n);
                        if (close) {
                            states |= (1 << n);
                        } else if (open) {
                            states &= ~(1 << n);
                        } else if (toggle) {
                            states ^= (1 << n);
                        }

                    } else {
                        mask |= (1 << (n + 8 * shift));
                        if (close) {
                            states |= (1 << n << (8 * shift));
                        } else if (open) {
                            states &= ~(1 << n << (8 * shift));
                        } else if (toggle) {
                            states ^= (1 << n << (8 * shift));
//                            System.out.println("Toggle States: " + Integer.toBinaryString(states));
                        }
                    }
                    shift = 0;
                    break;  // next character

                case '+':
                    shift++;
                    break;

                case '*':    // means all

                    if (!open && !close && !reset && !setcounters) {
                        return null;
                    }
                    mask |= 0xffffffffL;
                    if (close) {
                        states = 0xffffffff;
                    } else if (open) {
                        states = 0;
                    }

                    break;  // next character

                case ' ':   // white space ignored

                    break;  // next character

                default:    // error in command
            }
        }
        /* each character in the command */

        // extra parameter is an error
        if (value && !pulse && !dosetcounters) {
            return null;
            // parameter conflict - can't use both [S]et and [P]ulse in the same command line.
        }
        if (value && pulse && dosetcounters) {
            return null;
            // [S]et Counters command requires a parameter
        }
        if (dosetcounters && !value) {
            return null;
            // if mask is nonzero then we have changes to make on the way out
        }
        if (mask != 0) {
            if (pulse) {
                /* this action is pulsed */
                if (dos != null) {
                    dos.writeByte(1);
                    dos.writeShort((short) mask);
                    dos.writeShort((short) states);
                    dos.writeInt(parameter);
                    JANOS.setOutputPulsed(states, mask, parameter);
                }

            } /* this action is pulsed */ else if (!toggle) {
                if (dos != null) {
                    dos.writeByte(2);
                    dos.writeShort((short) mask);
                    dos.writeShort((short) states);
                }
                JANOS.setOutputStates(states, mask);
            } else if (toggle) {
                baos = new ByteArrayOutputStream();
                dos = new DataOutputStream(baos);
                JANOS.setOutputStates(states, mask);
            }
        }

        return baos.toByteArray();
    }
}

import com.integ.common.logging.AppLog;
import com.integ.common.logging.Logger;
import com.integ.common.logging.SystemOutLog;
import com.integ.common.net.AsciiCommandClient;
import com.integ.common.net.BytesReceivedEvent;
import com.integ.common.net.ClientListener;
import com.integpg.system.JANOS;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.QuickDateFormat;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SerialControlClient
        implements ClientListener {

    private static final QuickDateFormat QUICK_DATE_FORMAT = new QuickDateFormat("MM/dd/yy HH:mm:ss.fff");

    private static final Pattern IO_CONTROL_PATTERN = Pattern.compile("([copt\\+\\d\\*]+(=\\d+)?)$", Pattern.CASE_INSENSITIVE);
    private static final Pattern DIN_QUERY_PATTERN = Pattern.compile("din(\\d+)\\?$", Pattern.CASE_INSENSITIVE);
    private static final Pattern ROUT_QUERY_PATTERN = Pattern.compile("rout(\\d+)\\?$", Pattern.CASE_INSENSITIVE);

    private static final ArrayList<SerialControlClient> CLIENTS = new ArrayList<>();

    // initially we will log to the system.out stream unless setLog is called
    private Logger _log = SystemOutLog.getLogger();

    private final String _clientNameString;
    private final OutputStream _outputStream;
    private final ByteArrayOutputStream _byteArrayOutputStream = new ByteArrayOutputStream();

    private final AsciiCommandClient _asciiCommandClient;



    public SerialControlClient(String clientNameString, InputStream inputStream, OutputStream outputStream) {
        _clientNameString = clientNameString;
        _outputStream = outputStream;

        //
        // create an ascii command client four our serial port
        _asciiCommandClient = new AsciiCommandClient(clientNameString, inputStream);
        _asciiCommandClient.setTerminationBytes(Config.getIncomingTerminationString());
        _asciiCommandClient.setClientListener(this);
        _asciiCommandClient.start();
    }



    public void setLog(Logger log) {
        _log = log;
        _asciiCommandClient.setLog(_log);
    }



    public static void broadcast(String s, long timestamp) {
        synchronized (CLIENTS) {
            for (SerialControlClient serialControlClient : CLIENTS) {
                try {
                    // send the output string with the acutal transition time from the iolog
                    serialControlClient.send(s, timestamp);
                } catch (Exception ex) {
                    // do nothing
                }
            }
        }
    }



    public synchronized void send(String s) {
        send(s, System.currentTimeMillis());
    }



    public synchronized void send(String s, long timestamp) {
        try {
            // reset the byte output array
            _byteArrayOutputStream.reset();

            // if client should respond with datestamp
            if (Config.getSendDateStamp()) {
                // first send the datestamp
                _byteArrayOutputStream.write(QUICK_DATE_FORMAT.format(timestamp).getBytes());
                _byteArrayOutputStream.write(' ');
            }

            // append text
            _byteArrayOutputStream.write(s.getBytes());

            // append the termination characters
            _byteArrayOutputStream.write(Config.getOutgoingTerminationString().getBytes());

            if (_outputStream != null) {
                _outputStream.write(_byteArrayOutputStream.toByteArray());
                // flush the buffer to make sure it went
                _outputStream.flush();
                // log what was sent. since we are using the timestamp obtained at
                // the beginning of this method we could potentially log out of order
                // with the recieve client.  we can synchronize on a log lock to make
                // sure this doesnt happen or we can let it happen and let it be known
                // that it can happen
                _log.info(_clientNameString + " sent: " + s);
            }
        } catch (IOException ex) {
            _log.error(ex);
            AppLog.error(ex);
        }

    }



    @Override
    public void bytesReceived(BytesReceivedEvent evt) {
        byte[] bytes = evt.getBytes();
        String message = new String(bytes);

        _log.info(String.format("%s received: %s", _clientNameString, message));

        try {
            processMessage(message);
        } catch (IOException ex) {
            _log.error(ex);
            AppLog.error(ex);
        }
    }



    @Override
    public void clientStarted(EventObject evt) {
        _log.info(_clientNameString + " client started");

        //
        // add this serial client to the clients list
        synchronized (CLIENTS) {
            CLIENTS.add(this);
        }
    }



    @Override
    public void clientFinished(EventObject evt) {
        _log.info(_clientNameString + " client finished");

        synchronized (CLIENTS) {
            if (CLIENTS.contains(this)) {
                CLIENTS.remove(this);
            }
        }
    }



    private void processMessage(String message) throws IOException {
        Matcher matcher;
        if ((matcher = IO_CONTROL_PATTERN.matcher(message)).find()) {
            System.out.println("IO_CONTROL_PATTERN.groupCount(): " + matcher.groupCount());
            for (int i = 0; i < matcher.groupCount(); i++) {
                System.out.println("  IO_CONTROL_PATTERN.group[" + i + "]: " + matcher.group(i));
            }

            if (1 < matcher.groupCount()) {
                Jrmon.parseCommand(matcher.group(1));
            }

        } //
        // is it a query for input status?
        else if ((matcher = DIN_QUERY_PATTERN.matcher(message)).find()) {
            System.out.println("DIN_QUERY_PATTERN.groupCount(): " + matcher.groupCount());
            for (int i = 0; i < matcher.groupCount(); i++) {
                System.out.println("  DIN_QUERY_PATTERN.group[" + i + "]: " + matcher.group(i));
            }

            int channelNumber = Integer.parseInt(matcher.group(1));
            int state = ((JANOS.getInputStates() >> (channelNumber - 1)) & 1);
            String response = String.format("din%d=%d", channelNumber, state);
            if (Config.getSendCounts()) {
                int counter = JANOS.getInputCounter(channelNumber - 1);
                response = String.format("%s,%d", response, counter);
            }
            send(response);

        } //
        // is it a query for output status?
        else if ((matcher = ROUT_QUERY_PATTERN.matcher(message)).find()) {
            System.out.println("ROUT_QUERY_PATTERN.groupCount(): " + matcher.groupCount());
            for (int i = 0; i < matcher.groupCount(); i++) {
                System.out.println("  ROUT_QUERY_PATTERN.group[" + i + "]: " + matcher.group(i));
            }

            int channelNumber = Integer.parseInt(matcher.group(1));
            int state = ((JANOS.getOutputStates() >> (channelNumber - 1)) & 1);
            String response = String.format("rout%d=%d", channelNumber, state);
            send(response);


        } else {
            send("unknown command: '" + message + "'");

        }
    }

}
import com.integ.common.iolog.*;
import com.integ.common.logging.AppLog;
import com.integ.common.logging.Logger;
import com.integ.common.logging.RollingFileLog;
import com.integ.common.net.*;
import com.integ.common.system.Application;
import com.integpg.comm.AUXSerialPort;
import com.integpg.comm.COMSerialPort;
import com.integpg.comm.SerialPort;
import com.integpg.system.JANOS;
import java.io.IOException;
import java.net.Socket;

/**
 * The Serial Control application allows people to connect to the JNIOR via Serial or Ethernet.
 * I/O monitoring and Relay control is available with simple ASCII commands.
 */
public class SerialControlMain
        implements IoChannelLogListener {

    /* a flag indicating whether there was a valid configuration.  If there is not a 
       valid configuration then we will let the application exit */
    private boolean _validConfiguration = false;

    /* IO log monitors for both inputs and outputs.  we use these to send unsolicited io alerts */
    private DigitalInputsIoLogMonitor _digitalInputsIoLogMonitor;
    private RelayOutputsIoLogMonitor _relayOutputsIoLogMonitor;



    public static void main(String[] args) throws Exception {
        Application.init(new AssemblyInfo());

        SerialControlMain serialControlMain = new SerialControlMain();
        serialControlMain.init(args);
        serialControlMain.run();

    }



    public void init(String[] args) throws Exception {
        //
        // Read the configuration.  Configuration is only consumed on application start-up 
        // since we are bringing up tcp listeners and taking over serial ports
        Config.init();

        //
        // set up various features
        setUpSerialPort();
        setTcpServer();
        
        setIoLogMonitors();
    }



    private void setUpSerialPort() {
        try {
            SerialPort serialPort = null;

            //
            // get the serial port configuration and open the port if the name is valid.  we will 
            // use whatever serial settings are in the registry in AUXSerial or COMSerial
            String serialPortName = Config.getSerialPortName();

            if ("aux".equalsIgnoreCase(serialPortName)) serialPort = new AUXSerialPort();
            else if ("rs232".equalsIgnoreCase(serialPortName)
                    || "com".equalsIgnoreCase(serialPortName)) serialPort = new COMSerialPort();

            //
            // if a valid serial port was assigned then open it and start the client
            if (null != serialPort) {
                AppLog.info(String.format("opening %s serial port", serialPortName));
                serialPort.open();

                //
                // get a logger for the serial connection
                Logger serialLog = RollingFileLog.getLogger(
                        String.format("%s_Serial.log", Application.getAppName()));

                //
                // create an serial control client four our serial port
                SerialControlClient serialControlClient
                        = new SerialControlClient(serialPortName + " port",
                                serialPort.getInputStream(), serialPort.getOutputStream());
                serialControlClient.setLog(serialLog);

                //
                /// mark that there is a valid configuration
                _validConfiguration = true;
            } else {
                AppLog.info("serial port not configured to be in use");
            }
        } catch (Exception ex) {
            AppLog.error("error opening serial port", ex);
        }
    }



    private void setTcpServer() {
        try {
            //
            // get the serial port configuration and open the port if the name is valid.  we will 
            // use whatever serial settings are in the registry in AUXSerial or COMSerial
            int tcpServerPortNumber = Config.getTcpServerPortNumber();

            if (-1 != tcpServerPortNumber) {
                TcpServer tcpServer = new TcpServer("SerialControl-TcpServer", tcpServerPortNumber);
                tcpServer.setLog(AppLog.getLog());
                tcpServer.start();

                //
                // get a logger for the serial connection
                Logger tcpServerLog = RollingFileLog.getLogger(
                        String.format("%s_TcpServer.log", Application.getAppName()));

                tcpServer.setTcpServerListener(new TcpServerListener() {
                    @Override
                    public void clientConnected(TcpServerEvent evt) {
                        Socket socket = evt.getSocket();

                        String clientInfo = String.format("%s:%d",
                                socket.getInetAddress().getHostAddress(), socket.getPort());
                        tcpServerLog.info(String.format("%s is connected", clientInfo));

                        try {
                            //
                            // create an serial control client four our serial port
                            SerialControlClient serialControlClient
                                    = new SerialControlClient(clientInfo,
                                            socket.getInputStream(), socket.getOutputStream());
                            serialControlClient.setLog(tcpServerLog);
                        } catch (IOException ex) {
                            tcpServerLog.error(String.format("error setting up ascii command client for %s", clientInfo), ex);
                        }
                    }
                });

                //
                /// mark that there is a valid configuration
                _validConfiguration = true;
            } else {
                AppLog.info("tcp server not configured to be in use");
            }
        } catch (Exception ex) {
            AppLog.error("error setting up tcp server", ex);
        }
    }



    private void setIoLogMonitors() {
        _digitalInputsIoLogMonitor = new DigitalInputsIoLogMonitor();
        _digitalInputsIoLogMonitor.addIoChannelLogEventListener(this);
        _digitalInputsIoLogMonitor.start();

        _relayOutputsIoLogMonitor = new RelayOutputsIoLogMonitor();
        _relayOutputsIoLogMonitor.addIoChannelLogEventListener(this);
        _relayOutputsIoLogMonitor.start();
    }



    @Override
    public void onIoChannelEvent(IoChannelEvent ioEvent) {
        //
        // see if unsolicited io alerts is enabled in the configuration 
        boolean sendUnsolicitedIoAlerts = Config.getSendUnsolicitedIoAlerts();
        if (sendUnsolicitedIoAlerts) {
            //
            // build the string to send
            String outputString = String.format("%s%d=%d",
                    ioEvent.AbbrTypeString, ioEvent.Channel, (ioEvent.State ? 1 : 0));

             if (ioEvent instanceof DigitalInputChannelEvent 
                     && Config.getSendCounts()) {
                int counter = JANOS.getInputCounter(ioEvent.Channel - 1);
                outputString = String.format("%s,%d", outputString, counter);
            }
            
            //
            // broadcast to all connected clients
            SerialControlClient.broadcast(outputString, ioEvent.TransitionTime);
        }
    }



    public void run() throws Exception {
        //
        // see if there is any valid configuration.  if there isnt, then we can let this 
        // application exit as a reboot is needed
        if (_validConfiguration) {

            //
            // nothing to do here since the client handlers are being performed in a separate thread.
            Thread.sleep(Integer.MAX_VALUE);
        } else {
            AppLog.warn("there was either not valid configuration or errors occured.  nothing to do, exiting.");
        }
    }

}
By | On November 16, 2019 3:16 pm | No Comments | Categorized in: