IO

This post goes over an application that reads inputs being activated as a pattern using their state mask. However many times inputs are triggered within a certain amount of milliseconds between them, those inputs state masks are added to the pattern already there. This continues until there is an input not within the set amount of milliseconds. Then it prints the pattern and begins creating a new one.

Make sure to have properly setup the project before using this code, to learn how to setup a custom java application on the JNIOR, a link is here.

After setting up the project, it will require two files. This first one we create is called MultiplexSample, which calls the run function we will create in the second file.

//function runs the application
public class MultiplexSample {

    public static void main(String[] args) {
        
        InputMultiplexer inputMultiplex = new InputMultiplexer();
        inputMultiplex.run();
        
    }
    
}

After that we begin creating the second file called InputMultiplexer. The first thing we include in the project is the import statements. The two imports we use are for IoEvents and the Iolog. We will use these to get the input’s information and process them as patterns.

We then declare the global variables. The first one we declare is the _iolog object. The Iolog keeps track of all IoEvents, including inputs. We will use the IoEvents from the Iolog to grab the information of the inputs when they are activated.

After this is _eventCaptureTime which grabs the start of when the IoEvents are grabbed at the start of each loop through the application.

We then need a long value called _startOfPatternTime. This is the current time of each new input that comes in. We need this because the program loops every 500 seconds, and if 1 input happens right after the loop ends we’ll miss it. The _startOfPatternTime is subtracted from the _eventCaptureTime which helps us check if that input should keep the pattern going.

The _refreshTimestamp value helps us only grab new IoEvents from the Iolog by only grabbing events past its value.

Next is the IoEvent array _ioEvents which contains the inputs we are processing from the Iolog for the current pattern.

We then have the pattern value, which is the pattern we are constantly building from the IoEvents state mask.

Lastly is PATTERN_WINDOW_DURATION which is the value in milliseconds we are looking to see pass between inputs to determine if they are close enough to combine into a new pattern or process as separate ones.

import com.integpg.system.IoEvent;
import com.integpg.system.Iolog;

public class InputMultiplexer implements Runnable {

    //grabs the Iolog to get IoEvents
    private Iolog _iolog = new Iolog();
    //time of the first input to find the start of the input pattern
    private long _startOfPatternTime = 0;
    //time where Iolog is refreshed as to not look at old IoEvents
    private long _refreshTimestamp = _startOfPatternTime;
    //array of IoEvents to evaluate from the Iolog
    private IoEvent[] _ioEvents;
    //value for checking pattern has more IoEvents to process
    private long _eventCaptureTime = 0;
    //input pattern to log
    private int _pattern = 0;
    //duration between inputs required to continue a pattern
    private static final int PATTERN_WINDOW_DURATION = 100;

Next is the run function, which sets the _eventCaptureTime value, calls the other functions we are about to create, and permanently loops the program every 500 seconds. This makes it so the application is constantly checking the Iolog for new IoEvents.

    //function calls other functions to log input pattern in a infinite loop
    @Override
    public void run() {

        while (true) {

            _eventCaptureTime = System.currentTimeMillis();
            getIoEvents();

            lookForIoPattern();

            try {
                Thread.sleep(500);
            } catch (InterruptedException ex) {

            }

        }

    }

Next is the lookForIoPattern function. This function starts with a for loop that will go through all recent IoEvents from the Iolog and processes their states to begin creating a new pattern.

Next the IoEvent states and mask values have an & operation done to get which states have turned high. Next the _startOfPatternTime was set if this was the first input in the pattern. After that _startOfPatternTime would be compared with either the current IoEvent’s timestamp or the _eventCaptureTime value to determine if more inputs need to be added to the pattern. _startOfPatternTime is compared to currentIoEvent’s timestamp to check if timestamps of inputs were within PATTERN_WINDOW_DURATION milliseconds of each other, which in this example is 100 milliseconds. If the 500 milliseconds that the program is looping in ends as another input comes in, then its compared to the _eventCaptureTime value to check if another input came in within 100 milliseconds of the last input, even if the loop ended. This would prevent patterns from getting cut of from one another because of the 500 millisecond loop.

//function checks the IoEvents of the Iolog and evaluates an input pattern
    private void lookForIoPattern() {

        //grabs the IoEvents of the Iolog
        for (int index = _ioEvents.length - 1; index >= 0; index--) {

            IoEvent currentIoEvent = _ioEvents[index];

            System.out.println(currentIoEvent.timestamp);

            //This value shows the state total for the IoEvent
            System.out.println("This is the current I/O state: " + currentIoEvent.states);
            //This value shows which input values were activated for the IoEvent
            int statesTurnedHigh = currentIoEvent.states & currentIoEvent.mask;
            System.out.println("This is the value of statesTurnedHigh: " + statesTurnedHigh + "\n");

            //grabs the start of the input pattern if its hasn't been assigned yet and if enough time passes between IoEvents it evaluates the pattern,
            //otherwise the pattern has an or statement performed on it with the current IO event
            if (statesTurnedHigh != 0) {

                if (_startOfPatternTime == 0) {
                    _startOfPatternTime = currentIoEvent.timestamp;
                    System.out.println("Start of pattern found.");
                }
                if (currentIoEvent.timestamp - _startOfPatternTime > PATTERN_WINDOW_DURATION) {
                    evaluatePattern(_pattern);
                    _startOfPatternTime = currentIoEvent.timestamp;
                    _pattern = currentIoEvent.states;

                } else {
                    _pattern = currentIoEvent.states | _pattern;
                }

            }

        }

        //checks if the pattern is done or if the loop needs to grab more IoEvents
        if (_startOfPatternTime != 0) {

            if (_eventCaptureTime - _startOfPatternTime > PATTERN_WINDOW_DURATION) {

                evaluatePattern(_pattern);
                _startOfPatternTime = 0;
                _pattern = 0;

            }
        }
    }

The last two functions are the evaluatePattern function and the getIoEvents function. The evaluatePattern function simply grabs the pattern created from the lookForIoPattern function and prints it. The getIoEvents function is what is called to grab the most recent IoEvents from the IoLog.

 //prints out the Input Pattern
    private void evaluatePattern(int createdPattern) {

        System.out.println("Pattern is " + createdPattern);

    }



    //grabs IoEvents and puts them into a class level array
    private void getIoEvents() {

        _iolog.refresh(_refreshTimestamp);
        _ioEvents = _iolog.getInputEvents();

    }

}

After adding those last functions, you should now have program that grabs all the inputs activating on the JNIOR, and creates a state pattern depending on how close each input’s timestamp is. Below is the code for the full application. Make sure you set the MultiplexSample file as the main function of your project.

//function runs the application
public class MultiplexSample {

    public static void main(String[] args) {
        
        InputMultiplexer inputMultiplex = new InputMultiplexer();
        inputMultiplex.run();
        
    }
    
}
import com.integpg.system.IoEvent;
import com.integpg.system.Iolog;

public class InputMultiplexer implements Runnable {

    //grabs the Iolog to get IoEvents
    private Iolog _iolog = new Iolog();
    //time of the first input to find the start of the input pattern
    private long _startOfPatternTime = 0;
    //time where Iolog is refreshed as to not look at old IoEvents
    private long _refreshTimestamp = _startOfPatternTime;
    //array of IoEvents to evaluate from the Iolog
    private IoEvent[] _ioEvents;
    //value for checking pattern has more IoEvents to process
    private long _eventCaptureTime = 0;
    //input pattern to log
    private int _pattern = 0;
    //duration between inputs required to continue a pattern
    private static final int PATTERN_WINDOW_DURATION = 100;



    //function calls other functions to log input pattern in a infinite loop
    @Override
    public void run() {

        while (true) {

            _eventCaptureTime = System.currentTimeMillis();
            getIoEvents();

            lookForIoPattern();

            try {
                Thread.sleep(500);
            } catch (InterruptedException ex) {

            }

        }

    }



    //function checks the IoEvents of the Iolog and evaluates an input pattern
    private void lookForIoPattern() {

        //grabs the IoEvents of the Iolog
        for (int index = _ioEvents.length - 1; index >= 0; index--) {

            IoEvent currentIoEvent = _ioEvents[index];

            System.out.println(currentIoEvent.timestamp);

            //This value shows the state total for the IoEvent
            System.out.println("This is the current I/O state: " + currentIoEvent.states);
            //This value shows which input values were activated for the IoEvent
            int statesTurnedHigh = currentIoEvent.states & currentIoEvent.mask;
            System.out.println("This is the value of statesTurnedHigh: " + statesTurnedHigh + "\n");

            //grabs the start of the input pattern if its hasn't been assigned yet and if enough time passes between IoEvents it evaluates the pattern,
            //otherwise the pattern has an or statement performed on it with the current IO event
            if (statesTurnedHigh != 0) {

                if (_startOfPatternTime == 0) {
                    _startOfPatternTime = currentIoEvent.timestamp;
                    System.out.println("Start of pattern found.");
                }
                if (currentIoEvent.timestamp - _startOfPatternTime > PATTERN_WINDOW_DURATION) {
                    evaluatePattern(_pattern);
                    _startOfPatternTime = currentIoEvent.timestamp;
                    _pattern = currentIoEvent.states;

                } else {
                    _pattern = currentIoEvent.states | _pattern;
                }

            }

        }

        //checks if the pattern is done or if the loop needs to grab more IoEvents
        if (_startOfPatternTime != 0) {

            if (_eventCaptureTime - _startOfPatternTime > PATTERN_WINDOW_DURATION) {

                evaluatePattern(_pattern);
                _startOfPatternTime = 0;
                _pattern = 0;

            }
        }
    }



    //prints out the Input Pattern
    private void evaluatePattern(int createdPattern) {

        System.out.println("Pattern is " + createdPattern);

    }



    //grabs IoEvents and puts them into a class level array
    private void getIoEvents() {

        _iolog.refresh(_refreshTimestamp);
        _ioEvents = _iolog.getInputEvents();

    }

}

The JNIOR Control Panel is shipped with a ‘click’ sound that plays when a switch is pressed. This gives immediate feedback to the user that the panel has power and that the software is running normally and is ready to accept the press. There are times when the panel is used in a situation where the ‘click’ may not want to be heard. In this situation we may want to turn off that sound. The following application will accomplish this goal.


To run this application you need to download it from the button and load it on your JNIOR. Applications are generally loaded in the flash directory. To run it simply type the command as you see it above.

  ControlPanel Application JAR [ Mar 27 2019, 7 KB, MD5: c573b4c989ea0380729fecae5283e937 ]

In the following code you will notice two classes. The main class of interest here is the ControlPanel class. This class will load the arguments into a ParameterGroup class so that we can process different commands at some point as well as there specific options. For now our command line is pretty simple but we set this application up for the future.

package com.integ;

import com.integpg.sensor.SensorPort;
import com.integpg.system.JANOS;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;

/**
 * This application will allow you to set the volume of the control panel click
 */
public class ControlPanel {

    private static final String USAGE_STRING = "CONTROLPANEL\n\n"
            + "Options:\r\n"
            + "  -VOLUME volume     Sets the volume level to a value between 0% and 100%\n"
            + "\n"
            + "Set options for the control panel";



    public static void main(String[] args) {
        // load the arguments into our parameters class
        ParameterGroups parameters = new ParameterGroups(args);

        // if the paremters are empty or there is a -help parameter then print the usage string and exit
        if (parameters.isEmpty() || parameters.containsParameterGroup("help")) {
            System.out.println(USAGE_STRING);
            return;
        }

        // go through each parameter and process it
        while (parameters.hasMoreParameters()) {
            String parameterGroupName = parameters.getNextParameterGroup();
            ArrayList<String> parameterOptions = parameters.getParameterOptions(parameterGroupName);

            switch (parameterGroupName) {
                case "volume":
                    try {
                        // process set volume
                        processSetVolume(parameterOptions);
                    } catch (Exception ex) {
                        System.out.println(ex.getMessage());
                    }
                    break;

                default:
                    System.out.println("unknown parameter: " + parameterGroupName);
                    return;

            }
        }

    }



    private static void processSetVolume(ArrayList<String> options) {
//        ArrayList<String> volumeArguments = _parameters.get("volume");
        if (0 == options.size())
            throw new RuntimeException("must specify a volume parameter between 0% and 100%.");

        int volume;
        try {
            // get the volume from the arguments.  allow the user to enter a double but 
            // save the integer part
            volume = Double.valueOf((String)options.get(0)).intValue();
            if (0 > volume || 100 < volume) throw new Exception();
        } catch (Exception ex) {
            throw new RuntimeException("invalid volume specified: '" + options.get(0)
                    + "', volume parameter must be between 0% and 100%.");
        }

        // go through the external devices and set the volume for any connected control panels
        for (long externalModuleAddress : getExternalAddresses()) {
            // get a string representation of the address
            String addressString = getAddressString(externalModuleAddress).toUpperCase();

            // determine the type of module and handle accordingly
            if (0xfa == (externalModuleAddress & 0xff)) {

                try {
                    byte[] writeBlock = getSetVolumeWriteBlock((int) (volume * 2.55));
                    SensorPort.writeDeviceBlock(externalModuleAddress, writeBlock);

                    // lets print the result to the screen as well as the syslog
                    String result = String.format("Panel at %s set volume to %d percent", addressString, volume);
                    System.out.println(result);
                    JANOS.syslog(result);
                } catch (IOException ex) {
                    throw new RuntimeException(
                            String.format("unable to write to the control panel: %s.", addressString));
                }
            }
        }

    }



    private static long[] getExternalAddresses() {
        try {
            // query the unit for an array of external module addresses
            return SensorPort.externalDeviceList();
        } catch (IOException ex) {
            throw new RuntimeException("unable to get external device list.");
        }
    }



    private static String getAddressString(long address) {
        String addressString = "0000000000000000" + Long.toHexString(address);
        return addressString.substring(addressString.length() - 16);
    }



    private static byte[] getSetVolumeWriteBlock(int volume) {
        // the write block for the control panel is 20 bytes
        byte[] bytes = new byte[20];

        // to set the volume we only need to set the command byte and the audio volume byte
        bytes[10] = 4; // set audio volume command
        bytes[11] = (byte) volume; // audio volume level

        return bytes;
    }
}


/**
 * A class for interpreting command line arguments into parameter groups
 */
class ParameterGroups {

    private final Hashtable<String, ArrayList<String>> _parameters = new Hashtable<>();
    private Enumeration<String> _enumeration;



    public ParameterGroups(String[] args) {
        // parse the arguments
        ArrayList<String> options = null;
        for (int i = 0; i < args.length; i++) {
            String arg = args[i].toLowerCase();

            if (arg.charAt(0) == '-') {
                while (arg.charAt(0) == '-') {
                    arg = arg.substring(1);
                }
                options = new ArrayList<>();
                _parameters.put(arg, options);

            } else if (options != null) {
                options.add(arg);

            } else {
                System.out.println("Illegal parameter usage");
                return;

            }
        }
        _enumeration = _parameters.keys();
    }



    public boolean isEmpty() {
        return 0 == _parameters.size();
    }



    public boolean containsParameterGroup(String paramterGroup) {
        return _parameters.containsKey(paramterGroup);
    }



    public String getNextParameterGroup() {
        if (null == _enumeration) _enumeration = _parameters.keys();
        if (_enumeration.hasMoreElements()) {
            return _enumeration.nextElement();
        } else {
            return null;
        }
    }



    ArrayList<String> getParameterOptions(String parameterGroupName) {
        return _parameters.get(parameterGroupName);
    }



    boolean hasMoreParameters() {
        return _enumeration.hasMoreElements();
    }

}


Once the ParameterGroups class has been instantiated with our command line arguments we loop through each parameter group. When we find the “volume” parameter group we call processSetVolume() and pass in the parameter options.

        // go through each parameter group and process it
        while (parameters.hasMoreParameters()) {
            String parameterGroupName = parameters.getNextParameterGroup();
            ArrayList<String> parameterOptions = parameters.getParameterOptions(parameterGroupName);

            switch (parameterGroupName) {
                case "volume":
                    try {
                        // process set volume
                        processSetVolume(parameterOptions);
                    } catch (Exception ex) {
                        System.out.println(ex.getMessage());
                    }
                    break;

                default:
                    System.out.println("unknown parameter: " + parameterGroupName);
                    return;

            }
        }

Now that we are inside the processSetVolume() method we need to validate the options provided. We make sure that a volume was provided and that it is a valid number between 0% and 100%.




    private static void processSetVolume(ArrayList<String> options) {
//        ArrayList<String> volumeArguments = _parameters.get("volume");
        if (0 == options.size())
            throw new RuntimeException("must specify a volume parameter between 0% and 100%.");

        int volume;
        try {
            // get the volume from the arguments.  allow the user to enter a double but 
            // save the integer part
            volume = Double.valueOf(options.get(0)).intValue();
            if (0 > volume || 100 < volume) throw new Exception();
        } catch (Exception ex) {
            throw new RuntimeException("invalid volume specified: '" + options.get(0)
                    + "', volume parameter must be between 0% and 100%.");
        }

...


Now that the options have been validated and we processed the assigned volume we are ready to write to the connected control panels. We loop through each connected panel and check to make sure it is a Control Panel which is type 0xFA. If it is a Control Panel we can get the write block and send it to the Sensor Port.


        // go through the external devices and set the volume for any connected control panels
        for (long externalModuleAddress : getExternalAddresses()) {
            // get a string representation of the address
            String addressString = getAddressString(externalModuleAddress).toUpperCase();

            // determine the type of module and handle accordingly
            if (0xfa == (externalModuleAddress & 0xff)) {

                try {
                    byte[] writeBlock = getSetVolumeWriteBlock((int) (volume * 2.55));
                    SensorPort.writeDeviceBlock(externalModuleAddress, writeBlock);

                    // lets print the result to the screen as well as the syslog
                    String result = String.format("Panel at %s set volume to %d percent", addressString, volume);
                    System.out.println(result);
                    JANOS.syslog(result);
                } catch (IOException ex) {
                    throw new RuntimeException(
                            String.format("unable to write to the control panel: %s.", addressString));
                }
            }
        }

This example will monitor the IO Log utilizing the IoLog class. There are many ways to watch IO for changes. You could simply poll the IO and see if it changes. You are likely to set up a polling routing on some interval. If that interval is not fast enough then you have the potential of missing an change. Using the IO Log ensures that you see that a transition occurred. You have the added benefit of getting the exact time of that transition to the millisecond. This helps if you are measuring the time between transitions.

package com.integpg;

import com.integpg.system.IoEvent;
import com.integpg.system.Iolog;
import java.util.Date;

public class IOLogSampleMain implements Runnable {

    private final Iolog _iolog = new Iolog();
    private Thread _thd;



    public static void main(String[] args) throws InterruptedException {
        new IOLogSampleMain().start();

        Thread.sleep(Integer.MAX_VALUE);
    }



    public void start() {
        if (null == _thd) {
            _thd = new Thread((Runnable) this);
            _thd.setName("IoLogMonitor");
            _thd.start();
        }
    }



    @Override
    @Override
    public void run() {
        long lastConsumedTimestamp = System.currentTimeMillis();

        while (true) {
            try {
                // refresh the iolog object so that we only get new events since the last time we queried it
                _iolog.refresh(lastConsumedTimestamp);
                // get the events for the inputs only
                IoEvent[] ioEvents = _iolog.getInputEvents();

                // make sure we were returned events
                if (ioEvents.length > 0) {
                    System.out.println("IoLogMonitor returned " + ioEvents.length + " events");
                    // go through each event and print some meaningful information about it
                    for (int index = ioEvents.length - 1; index >= 0; index--) {
                        IoEvent ioEvent = ioEvents[index];
                        System.out.println("ioEvent[" + index + "]: time: " + ioEvent.timestamp + " changed input mask: " + Long.toHexString(ioEvent.mask) + " input states mask: " + Long.toHexString(ioEvent.states));

                        // update our lastConsumedTimestamp with the date of the most recent event
                        if (ioEvent.timestamp > lastConsumedTimestamp) {
                            lastConsumedTimestamp = ioEvent.timestamp;
                            System.out.println("lastConsumedTimestamp: " + new Date(lastConsumedTimestamp));
                        }
                    }
                }

                Thread.sleep(100);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

This sample shows you how to pulse multiple outputs and a single output. The method must take two binary masks. One describing the desired states durring the pulse and the other describing which channels will be pulsed.

package com.integpg;
import com.integpg.system.JANOS;
public class PulseOutputs {
    public static void main(String[] args) {
        // to get the states of the outputs use the JANOS class and the getOutputStates method
        int outputStates = JANOS.getOutputStates();
        //print the Output States through telnet (console) connection.
        System.out.println("Output States are: " + outputStates);
        //Pulse 8 Relay Outputs On for 5 seconds (5000 milliseconds) after which outputs will return to previous state.
        //All channels (1111 1111b)
        JANOS.setOutputPulsed(255, 255, 5000);
        //Sleep 10 seconds to so that there is a noticable difference between on and off states.
        try {
            Thread.sleep(10000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        int counter = 0;
        while(counter<5){
        //Pulse Channel 5 Relay Output On for 5 seconds (5000 milliseconds) after which output will return to previous state.
        //Channels   8765 4321
        //Channel 5 (0001 0000b)
        JANOS.setOutputPulsed(16, 16, 5000);
            try {
                Thread.sleep(10000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            counter++;
            System.out.println("Counter: " + counter);
        }
    }
}

Sometimes you want to control the outputs. The outputs may be wired to things light lights, sirens, valves and maybe fans. This example will show you how

package com.integpg;
import com.integpg.system.JANOS;
public class WriteOutputs {
    public static void main(String[] args) {
        // to get the states of the outputs use the JANOS class and the getOutputStates method
        int outputStates = JANOS.getOutputStates();
        //print the Output States through telnet (console) connection.
        System.out.println("Output States are: " + outputStates);
        //set output relay for channel 5 (channel n-1) and true top turn the relay on, false to turn relay off.
        JANOS.setOutputRelay(4 , true);
        //Relay Output 5 should now be on.
        //print the Output States through telnet (console) connection.
        System.out.println("Output States are: " + outputStates);
    }
}