This Community site is new! Please help us build a community around the JNIOR.
Sign up, and help share your knowledge. Please sign-up even if you do not plan to post as a sign of support.
If there is evidence of a demand we will continue to develop the content here.

Cinema Controller UI

You got ideas? Let's hear 'em. Here we can talk about your experiences programming on the JNIOR or items that you may wish to have INTEG assist you with.
kmcloutier
Posts: 31
Joined: Tue Sep 12, 2017 7:26 am

Re: Cinema Controller UI

Post by kmcloutier » Thu Nov 30, 2017 1:03 pm

So no more RunMacroResponse? We will just have MacroUpdate. Maybe with an Error field?
{
  "Message":"MacroUpdate",
  "Macro":"Flat Start",  // bad macro name
  "Update":"Could not execute macro",
  "Error":"Macro not found"
}
I am a Senior Software Programmer at INTEG. You have questions and I have answers.

bscloutier
Posts: 401
Joined: Thu Sep 14, 2017 12:55 pm

Re: Cinema Controller UI

Post by bscloutier » Thu Nov 30, 2017 1:10 pm

So I am at the point where I could execute a macro by selecting it from the menu. I think though we want to just show its status when it is selected and have actually running it be another key press. Got to think about it. I can also do the page up and page down now. Then there is the thought about indicating position in the list. Hmm...


kmcloutier
Posts: 31
Joined: Tue Sep 12, 2017 7:26 am

Re: Cinema Controller UI

Post by kmcloutier » Thu Nov 30, 2017 2:07 pm

The new MacroStatus message:
{
  "Message":"MacroStatus ",
  "Macro":"STRING"
  "Status":"STRING",
  "Running",true / false,
  "Error":true / false
}
I am a Senior Software Programmer at INTEG. You have questions and I have answers.

bscloutier
Posts: 401
Joined: Thu Sep 14, 2017 12:55 pm

Re: Cinema Controller UI

Post by bscloutier » Thu Nov 30, 2017 4:09 pm

So I have it working at least to the point that we have so far envisioned. This plays off the code structure developed in the Display and Keyboard thread.

Since in addition to managing the HMI device this program needs to communicate with one or more JNIORs running the Cinema application, I added a class to handle the connections. At this point just one and it is not yet fault tolerant (it won't reconnect).

Here it executes a macro. The status messages are pretty cryptic at this point. That is not the responsibility of the HMI interface.


bscloutier
Posts: 401
Joined: Thu Sep 14, 2017 12:55 pm

Re: Cinema Controller UI

Post by bscloutier » Thu Nov 30, 2017 4:31 pm

So the CineConnect class defines a Runnable thread that is merely instantiated and started from the HMI program. The target host could be the same JNIOR on which the HMI is connected. In that case you would specify 'localhost'. For my tests I have been connecting to a JNIOR over in Kevin's office that has been running the Cinema application. Kevin has been modifying that to support the JSON based protocol we need for this. So in instantiating the connection I pass that JNIOR's IP address and the port that we have been using for this connection.

The approach is to process messages from the Cinema application asynchronously rather than to try and maintain a master-slave exchange. This makes some sense because the Cinema application will spontaneously send us updates for any macros that are running. That would probably be the case no matter if the macro were started by the HMI or through other means. So since we might not know what messages are coming and when, we will just loop and process them as they come.

Here is the code. It kicks off by requesting some general information. Then the loop sorts incoming data into useful arrays which the HMI will access to format its displays as needed. You'll see here how nice the built-in JSON class in the JANOS runtime is for this kind of thing.

package ckeypad;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Json;

public class CineConnect implements Runnable {
    
    // Remote connection point
    private int port = 0;
    private String host = "";
    
    Socket dataSocket = null;
    PrintWriter sockout = null;
    BufferedReader sockin = null;    
    
    private String info = "";
    private String[] macros = null;
    private String[] updates = null;
    private boolean[] states = null;
    private boolean[] errors = null;
    
    public CineConnect(String hoststr, int portnum) {
        host = hoststr;
        port = portnum;
    }

    @Override
    public void run() {
        
        String msg;
        String str;
        Json jdata;
        Object obj;
        
        try {
            dataSocket = new Socket(host, port);
            sockout = new PrintWriter(dataSocket.getOutputStream(), true);
            sockin = new BufferedReader(new InputStreamReader(dataSocket.getInputStream()));

            // Obtain Info
            sendRequest("{\"Message\":\"GetInfo\"}");

            // Obtain Macro List
            sendRequest("{\"Message\":\"GetMacroList\"}");
            
            // Process messages
            for (;;) {
                msg = getMessage();
                jdata = new Json(msg);
                str = jdata.getString("Message");
                
                //System.out.println("<" + msg);

                if (str.equals("GetInfoResponse")) {
                    info = jdata.getString("Information");
                }
                else if (str.equals("GetMacroListResponse")) {
                    macros = (String[]) jdata.get("MacroList");
                    if (macros != null) {
                         updates = new String[macros.length];
                         states = new boolean[macros.length];
                         errors = new boolean[macros.length];
                    }
                }
                else if (str.equals("MacroStatus") && macros != null) {
                    if (macros != null) {
                        String name = jdata.getString("Macro");
                        for (int n = 0; n < macros.length; n++) {
                            if (macros[n].equals(name)) {
                                obj = jdata.getString("Status");
                                updates[n] = (obj != null ? (String)obj : "");
                                obj = jdata.get("Running");
                                states[n]  = (obj != null ? (boolean)obj : false);
                                obj = jdata.get("Errors");
                                errors[n]  = (obj != null ? (boolean)obj : false);
                                break;
                            }
                        }
                    }
                }
            }
        }
        catch (Throwable t) {
            t.printStackTrace();
        }
        finally {
            try {
                sockout.close();
                sockin.close();
                dataSocket.close();
            }
            catch (Throwable t) {
                
            }
        }
    }
    
    synchronized public void sendRequest(String msg) throws Throwable {
        sockout.write(msg.length()/256);
        sockout.write(msg.length() & 0xff);
        sockout.print(msg);
    }
    
    public String getMessage() throws Throwable {
        int len = 256 * sockin.read() + sockin.read();
        char[] data = new char[len];
        sockin.read(data);
        return (new String(data));
    }
    
    public String getInfo() {
        return (info);
    }
    
    public String[] getMacros() {
        return (macros);
    }
    
    public void runMacro(String name) {
        try {
            String msg = "{\"Message\":\"RunMacro\",\"MacroName\":\"" + name + "\"}";
            sendRequest(msg);
            //System.out.println(">" + msg);
        }
        catch (Throwable t) {
            
        }
    }
    
    public String[] getUpdates() {
        return (updates);
    }
    
    public boolean[] getStates() {
        return (states);
    }
    
    public boolean[] getErrors() {
        return (errors);
    }
    
}

So tomorrow I need to add an outer loop to cause a reconnect if we get disconnect from the target. I'll also likely implement a Watchdog to restart the program if it should stop unexpectedly.

bscloutier
Posts: 401
Joined: Thu Sep 14, 2017 12:55 pm

Re: Cinema Controller UI

Post by bscloutier » Thu Nov 30, 2017 4:42 pm

If you refer to the Display and Keyboard thread you can compare the structure of the main program as the code for the Cinema HMI follows. Basically the CineConnect class is instantiated at around Line 47. That is one difference. Naturally the Splash Screen and Menu contexts are a bit different but not complicated. There is a new context for displaying ongoing macro status and starting them. Some of the contexts developed in the other thread aren't here now. I have also added just a little additional checking for null pointers and bounds since what we are displaying now comes from an external source (and who knows what Kevin is doing over there?).

package ckeypad;

import com.integpg.comm.AUXSerialPort;
import com.integpg.system.ArrayUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;

public class Ckeypad {
    
    // Communications streams
    static PrintStream auxout = null;
    static BufferedReader auxin = null;
    
    // HMI display commands
    static final String CLRSCN = "\u00FE\u0058";
    
    static final String LED1OFF = "\u00FE\u0056\u0001\u00FE\u0056\u0002";
    static final String LED1RED = "\u00FE\u0057\u0001\u00FE\u0056\u0002";
    static final String LED1GRN = "\u00FE\u0056\u0001\u00FE\u0057\u0002";
    static final String LED1YEL = "\u00FE\u0057\u0001\u00FE\u0057\u0002";
    static final String LED2OFF = "\u00FE\u0056\u0003\u00FE\u0056\u0004";
    static final String LED2RED = "\u00FE\u0057\u0003\u00FE\u0056\u0004";
    static final String LED2GRN = "\u00FE\u0056\u0003\u00FE\u0057\u0004";
    static final String LED2YEL = "\u00FE\u0057\u0003\u00FE\u0057\u0004";
    static final String LED3OFF = "\u00FE\u0056\u0005\u00FE\u0056\u0006";
    static final String LED3RED = "\u00FE\u0057\u0005\u00FE\u0056\u0006";
    static final String LED3GRN = "\u00FE\u0056\u0005\u00FE\u0057\u0006";
    static final String LED3YEL = "\u00FE\u0057\u0005\u00FE\u0057\u0006";
    
    static final String SCROLLOFF = "\u00FE\u0052";
    static final String CURSORPOS = "\u00FE\u0047";
    
    // HMI Back Light Commands
    static final String DLIGHTON = "\u00FE\u0042\u0001";
    static final String AUTOLIGHT = "\u00FE\u009D\u0013\u00FE\u009C\u0040";
    
    // State
    static int context = 0;
    
    // Connections
    static CineConnect conn = null;
    
    public static void main(String[] args) throws Throwable {
        
        // Establish connections
        conn = new CineConnect("10.0.0.64", 9610);
        new Thread(conn).start();
    
        // Access and configure AUX port
        AUXSerialPort auxport = new AUXSerialPort();
        auxport.open();
        auxport.setSerialPortParams(19200, 8, 1, 0);
        auxport.enableReceiveTimeout(200);
        
        // Estable AUX input and output streams
        auxout = new PrintStream(auxport.getOutputStream());
        auxin = new BufferedReader(new InputStreamReader(auxport.getInputStream()));
        
        // turn off LEDs and clear the screen
        auxout.print(LED1OFF + LED2OFF + LED3OFF + CLRSCN + SCROLLOFF + AUTOLIGHT + DLIGHTON);
        
        // flush any prior serial data
        while (auxin.ready())
            auxin.read();
        
        // loop to process keypad input and periodically update the splash screen
        while (context >= 0) {
            int key;
            
            // Update display according to context.
            switch (context) {
                case 0:
                    splash();
                    break;
                case 1:
                    mainmenu();
                    break;
                case 2:
                    status();
                    break;
            }
            
            // check for keypad entry and perform action
            try {
                key = auxin.read();
                
                // keypad is context dependant
                switch (context) {
                    case 0:
                        splash_key(key);
                        break;
                    case 1:
                        mainmenu_key(key);
                        break;                        
                    case 2:
                        status_key(key);
                        break;
                    default:
                        System.out.println(key);
                }
                
            } catch (IOException ioe) {
                if (!ioe.getMessage().equals("timeout"))
                    throw ioe;
            }
                
        }
        
    }
    
    // ---------- Splash Screen -----------------------------------------------
    static void splash() throws Throwable {

        // Format splash screen
        screen_clear();
        if (conn != null) 
            screen_write(0, 0, conn.getInfo());
        screen_update();
    }
    
    static void splash_key(int key) {
        context = 1;
    }
    
    // ---------- Main Menu ---------------------------------------------------
    static int sel = 0;
    static int top = 0;
    static int selected = 0;
    static String[] macros;
    
    static void mainmenu() {
        int n, line;
        macros = conn.getMacros();
        
        // Display Main Menu
        screen_clear();
        
        // proper scrolling
        if (sel > top + 3)
            top = sel - 3;
        else if (sel < top)
            top = sel;
        
        for (line = 0, n = top; n < macros.length && line < 4; n++, line++) {
            screen_write(line, 0, n == sel ? "\u007e" : " ");
            screen_write(line, 1, macros[n]);
        }
        
        screen_update();
    }
    
    static void mainmenu_key(int key) {

        // main menu
        switch (key) {
            case 'D':   // return from menu
                context = 0;
                break;
            case 'B':   // select prior (or wrap to bottom)
                if (--sel < 0)
                    sel = macros.length - 1;
                break;
            case 'H':   // select next (or wrap to top)
                if (++sel >= macros.length)
                    sel = 0;
                break;
            case 'G':   // page down
                top += 4;
                if (top >= macros.length - 4)
                    top = macros.length - 4;
                if (sel < top)
                    sel = top;
                break;
            case 'A':   // page up
                top -= 4;
                if (top < 0)
                    top = 0;
                if (sel > top + 3)
                    sel = top + 3;
                break;
            case 'E':
            case 'C':
                selected = sel;
                context = 2;
                break;
        }
        
        // Refresh if we are going to display the Splash Screen
        if (context == 0)
            screen_refresh();
    }
    
    // ---------- Status Screen -----------------------------------------------
    static boolean[] running = null;
    
    static void status() throws Throwable {

        // Format macro status screen
        screen_clear();
        
        String name = macros[selected].trim();
        if (name.length() > 20)
            screen_write(0, 0, name.substring(0, 20));
        else {
            screen_write(0, 0, "--------------------");
            screen_write(0, (20 - name.length()) / 2, name);
        }
        
        running = conn.getStates();
        if (running != null) {
            if (!running[selected])
                screen_write(1, 2, "\u007eExecute");
        }
        
        String[] updates = conn.getUpdates();
        if (updates != null) 
            screen_write(2, 0, updates[selected]);
        
        screen_update();
    }
    
    static void status_key(int key) {
        if (key == 'E') {
            if (running != null && !running[selected])
                conn.runMacro(macros[selected]);
        }
        else
            context = 1;
    }

    // ---------- Screen Buffering --------------------------------------------
    static byte[] screen = new byte[80];
    static byte[] displayed = new byte[80];

    static void screen_clear() {
        ArrayUtils.arrayFill(screen, 0, screen.length, (byte)' ');
    }
    
    static void screen_refresh() {
        ArrayUtils.arrayFill(displayed, 0, displayed.length, (byte)0);
    }
    
    // Positions character data on the screen. This is tolerant of null pointers
    //  and does not allow writing beyond the end of the screen area (out of bounds).
    static void screen_write(int row, int col, String data) {
        if (data == null)
            return;
        
        byte[] bufr = data.getBytes();
        int pos = 20 * row + col;
        ArrayUtils.arraycopy(bufr, 0, screen, pos, pos + bufr.length > 80 ? 80 - pos : bufr.length);
    }
    
    // Updates the screen with new data. Uses cursor positioning when that would
    //  save bytes (compress stream).
    static void screen_update() {
        int n, begin = -1, end = -1, cnt = 0;
        
        // locate changed screen data
        for (n = 0; n < 80; n++) {
            if (screen[n] != displayed[n]) {
                if (begin == -1)
                    begin = n;
                end = n;
                cnt = 0;
            } 
            else {
                cnt++;
                if (begin != -1 && cnt > 4) {
                    screen_write(begin, end);
                    begin = -1;
                }
            }
        }

        if (begin != -1) 
            screen_write(begin, end);
        if (end != -1)
            auxout.print(DLIGHTON);

        ArrayUtils.arraycopy(screen, 0, displayed, 0, screen.length);
    }
    
    // Write bytes from starting index to ending index inclusive to proper place
    //  on the screen.
    static void screen_write(int begin, int end) {
        auxout.write(0xFE);
        auxout.write(0x47);
        auxout.write((begin % 20) + 1);
        auxout.write(begin / 20 + 1);
        auxout.write(screen, begin, end - begin + 1);
    }
}

Did you see where I implemented the Page Up and Page Down movement through the list of macros? :D

bscloutier
Posts: 401
Joined: Thu Sep 14, 2017 12:55 pm

Re: Cinema Controller UI

Post by bscloutier » Fri Dec 01, 2017 8:22 am

I notice that the Json class doesn't have a get method for boolean. It is not a big deal as you can cast it as you see that I have. But still there are methods for the other types. The other thing we usually have is an optional default. Again you see how I have handled it but to be consistent say with our getRegistry methods I could enhance the library.

Code: Select all

                                obj = jdata.getString("Status");
                                updates[n] = (obj != null ? (String)obj : "");
                                obj = jdata.get("Running");
                                states[n]  = (obj != null ? (boolean)obj : false);
                                obj = jdata.get("Errors");
                                errors[n]  = (obj != null ? (boolean)obj : false);
These changes will be in JANOS v1.6.3 and later. We can leave the above approach in place just to be compatible with the current and older OS versions if we want.

Post Reply