6

I surrender. Trying for some weeks now to find out what is blocking received serial data from being updated by the graphic part of my code. First time programming in Java. Have about 15 years of experience programming micros and I'm used to solve my own problems but this goes beyond the point where that tactic is productive. My application consist of two files.

One file stems from RXTX project and catch serial data sent in several packets twice every second. This works like a charm (took some time) and I can see that captured data is correct and stable.

The other file is graphic and consist of around 80 menus where end user can read and sometimes write values. Navigating is done with mouse events on buttons and scroll bar so far. This part also work as it should. Values can be read, changed and saved etc.

The part where I'm stuck is that updated values from serial file never update the graphic screen. Have tried to follow hundreds of examples and tutorials (many from this site) with no luck.

The concept of object related languages is new to me and still pretty confusing. Pretty sure my problem involves inheritance and classes. Threads is another candidate... Have cut down code to smallest size that still would run and present my problem and hope someone can see whats wrong.

package components;

import gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;
import gnu.io.SerialPortEvent;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.swing.SwingUtilities;

public class SerialComm extends ScreenBuilder implements java.util.EventListener {

InputStream in;

public SerialComm() {
    super();
}

public interface SerialPortEventListener
        extends java.util.EventListener {
}

void connect(String portName) throws Exception {
    CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier("COM1");
    if (portIdentifier.isCurrentlyOwned()) {
        System.out.println("Error: Port is currently in use");
    } else {
        CommPortIdentifier.getPortIdentifier("COM1");
        System.out.println("" + portName);
        CommPort commPort = portIdentifier.open("COM1", 2000);
        if (commPort instanceof SerialPort) {
            SerialPort serialPort = (SerialPort) commPort;
            serialPort.setSerialPortParams(115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_2, SerialPort.PARITY_NONE);
            InputStream in = serialPort.getInputStream();
            OutputStream out = serialPort.getOutputStream();
            serialPort.addEventListener(new SerialComm.SerialReader(in));
            serialPort.notifyOnDataAvailable(true);

            (new Thread(new SerialComm.SerialReader(in))).start();
            // TX functionality commented for now
            //               (new Thread(new SerialWriter(out))).start();

        } else {
            System.out.println("Error: Only serial ports are handled by this     example.");
        }
    }
}

public class SerialReader extends SerialComm implements Runnable,
        gnu.io.SerialPortEventListener {

    public SerialReader(InputStream in) {
        this.in = in;
    }

    @Override
    public void run() {
    count=11; // just for test. run is normally empty
    count2=count; // and real code runs within serialEvent()
    System.out.println("SerialReader " + count);
    dspUpdate(); // do some desperate stuff in graphics file
    System.out.println("Post Update " + count);
    }

    @Override
    public void serialEvent(SerialPortEvent event) {
    System.out.println("SerialEvent");
        switch (event.getEventType()) {
            case SerialPortEvent.DATA_AVAILABLE:
                try {
                    synchronized (in) {
                        while (in.available() < 0) {
                            in.wait(1, 800000);
                        } //in real code RX data is captured here twice a sec
                    } //and stored into buffers defined in ScreenBuilder
    //dspUpdate() is called from here to make ScreenBuilder update its screen
    //That never happens despite all my attempts               
                } catch (IOException e) {
                    System.out.println("IO Exception");
                } catch (InterruptedException e) {
                    System.out.println("InterruptedException caught");
                }
        }
    }
}

/* "main" connect PC serial port and start graphic part of application
 * To demonstrate problem with no serial data stream present
 * order of init between serial port and graphics are switched
 */

public static void main(String[] args) {

    SwingUtilities.invokeLater(new Runnable() {

        @Override
        public void run() {
            ScreenBuilder screen = new ScreenBuilder();
            screen.createAndShowGUI();
            System.out.println("Created GUI");
        }
    });
    try {
        (new SerialComm()).connect("COM1");
    } catch (Exception e) {
        System.out.println("Error");
        e.printStackTrace();
    }
  }
}

And the graphics file

package components;

import java.awt.*;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.BorderFactory;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.*;

public class ScreenBuilder extends JPanel implements ActionListener {

public Font smallFont = new Font("Dialog", Font.PLAIN, 12);
Color screenColor;
Color lineColor;
short btn=0;
short count;
short count2;
Button helpButton;

public static void createAndShowGUI() {
    System.out.println("Created GUI on EDT? "
            + SwingUtilities.isEventDispatchThread());
    JFrame f = new JFrame("JUST A TEST");
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.add(new ScreenBuilder());
    f.pack();
    f.setVisible(true);
}

public void dspButton() {
    setLayout(null);// 
    helpButton = new Button("?");
    helpButton.setLocation(217, 8); // set X, Y
    helpButton.setSize(16, 14); //Set Size X, Y //
    helpButton.addActionListener(this);
    add(helpButton);
    setBackground(Color.black);
    helpButton.setBackground(Color.black);
    screenColor = Color.black;
    helpButton.setForeground(Color.white);
    lineColor = Color.white;
}

@Override
public void actionPerformed(ActionEvent e) {
    if (e.getSource() == helpButton) {
        count2++;
        System.out.println("Pressed Button ");
        repaint();
    }
}

public ScreenBuilder() {
    setBorder(BorderFactory.createLineBorder(Color.black));
}

@Override
public Dimension getPreferredSize() {
    return new Dimension(240, 180);
}

public void dspUpdate() {
    /*
     * This function is called from SerialComm
     * Should be called when serial packets have arrived (twice a second)
     * and update screen with values from serial stream
     * For now just a test var to validate that values from SerialComm
     * get to here (they do)
     */
count++;
System.out.println("Update Count " + count);
System.out.println("Update Count2 " + count2);
//    revalidate(); // another futile attempt to update screen
//    repaint();
}

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(lineColor);
    g.setFont(smallFont);
    count++;
    g.drawString("" + count, 130, 20);
    g.drawString("" + count2, 150, 20);
    if (btn == 0) {
      dspButton();
      btn = 1;
    }
  }
}
  • 4
    never hand up, never surrender – mKorbel Oct 10 '12 at 17:15
  • I'm not as that familiar with swing, but can you explain the relationship between method calls? It's a bit tricky to explain what I don't get: In the first place `SerialComm` calls `dspUpdate()`. This method will call `repaint` (good I think), repaint calls `paintComponent` which calls `dspUpdate`? – f4lco Oct 10 '12 at 17:24
  • @phineas s/he has issue with Concurency in Swing, there all updates to the GUI must be done on EDT, – mKorbel Oct 10 '12 at 17:29
  • hi,The dspUpdate() call from inside PaintComponents() was another test in desperation from my side, should have erased it. Sorry... – user1735586 Oct 10 '12 at 17:35
  • mKorbal wrote: s/he has issue with Concurency in Swing, there all updates to the GUI must be done on EDT, Can you please tell me more? /Richard – user1735586 Oct 10 '12 at 17:41
  • I recommend reading [Lesson: Concurrency in Swing](http://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html), especially [Tasks that Have Interim Results](http://docs.oracle.com/javase/tutorial/uiswing/concurrency/interim.html). Does the tutorial give you a useful pattern? – f4lco Oct 10 '12 at 17:47
  • 1
    Could you describe precisely what you would expect and what is actually hapenning? Just for your information: revalidate() must be called after adding some components to a container and if you are using a layout manager. repaint() should be called if, eventually, you want paintComponent to be invoked (repaint()==redraw this component). EDT=Event Dispatching Thread. All GUI-related things must be done on that Thread and it should never be blocked, otherwise the GUI looks frozen (since events (amongst which there is a Repaint-Event) can no longer be dispatched). – Guillaume Polet Oct 10 '12 at 18:03
  • I have removed dspUpdate() call from within PaintComponent() it was just my last try to see some difference and might cause confusion regarding program flow – user1735586 Oct 10 '12 at 18:48
  • @user1735586 not you can't to remove something, you have to explain your goal, maybe paintComponent isn't proper method "How to display progress or partial output from CommPort", – mKorbel Oct 10 '12 at 18:53

1 Answers1

2

The biggest problem you're running into is putting everything into the GUI classes. Try to separate your model (backend serial communication stuff) from your front end (pretty GUI stuff), and you'll save yourself a lot of headache. In the example I've tried to do that for you - it's in one file, but you should probably separate it into 3: Model, View, and Control (control is what communicates between model and view).

If you add your Serial Communication code (which you said was working) to the Model instead of the sample thread, you should be able to communicate between the view and model without too much hassle. I tried to preserve as much of your code as I could.

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class TranslucentWindow {

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                try {
                    View screen = new View();
                    System.out.println("Created GUI");
                    Model model = new Model();

                    Control c = new Control(screen, model);
                } catch (Exception e) {
                    System.out.println("Error");
                    e.printStackTrace();
                }
            }
        });
    }

    //Only cares about the backend.  Simplified because you said all the backend code was working right.
    public static class Model{

        //Data that was updated - you can change this to whatever you want.
        public String count;
        //Listener that notifies anyone interested that data changed
        public ActionListener refreshListener;

        public void run() {
            //As a sample, we're updating info every 1/2 sec.  But you'd have your Serial Listener stuff here
            Thread t = new Thread(new Runnable(){
                @Override
                public void run() {
                    int i = 0;
                    while(true){
                        dspUpdate(i++);
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }});
            t.start();
        }

        //Update data and notify your listeners
        public void dspUpdate(int input) {
            count = String.valueOf(input);
            System.out.println("Update Count " + count);
            refreshListener.actionPerformed(new ActionEvent(this, input, "Update"));
        }

    }


    //Only cares about the display of the screen
    public static class View extends JPanel {

        public Font smallFont = new Font("Dialog", Font.PLAIN, 12);
        Color screenColor;
        Color lineColor;
        short btn=0;
        String modelRefreshInfo;
        int buttonPressCount;
        Button helpButton;

        public View(){
            //Build Panel
            dspButton();

            //Create and show window
            System.out.println("Created GUI on EDT? "+ SwingUtilities.isEventDispatchThread());
            JFrame f = new JFrame("JUST A TEST");
            f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            f.add(this);
            f.pack();
            f.setVisible(true);
        }

        public void dspButton() {
            setLayout(null);// 
            helpButton = new Button("?");
            helpButton.setLocation(217, 8); // set X, Y
            helpButton.setSize(16, 14); //Set Size X, Y //
            add(helpButton);
            setBackground(Color.black);
            helpButton.setBackground(Color.black);
            screenColor = Color.black;
            helpButton.setForeground(Color.white);
            lineColor = Color.white;
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(240, 180);
        }   

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(lineColor);
            g.setFont(smallFont);
            g.drawString("ModelUpdates: " + modelRefreshInfo, 10, 20);
            g.drawString("RefreshCount: " + buttonPressCount, 10, 40);
            if (btn == 0) {
                dspButton();
                btn = 1;
            }
        }
    }

    //Links up the view and the model
    public static class Control{
        View screen;
        Model model;

        public Control(View screen, Model model){
            this.screen = screen;
            //Tells the screen what to do when the button is pressed
            this.screen.helpButton.addActionListener(new ActionListener(){
                @Override
                public void actionPerformed(ActionEvent e) {
                    //Update the screen with the model's info
                    Control.this.screen.buttonPressCount++;
                    System.out.println("Pressed Button ");
                    Control.this.screen.repaint();
                }
            });

            this.model = model;
            //Hands new data in the model to the screen
            this.model.refreshListener = new ActionListener(){
                @Override
                public void actionPerformed(ActionEvent e) {
                    //Update the screen with the model's info
                    Control.this.screen.modelRefreshInfo = Control.this.model.count;
                    System.out.println("Model Refreshed");
                    Control.this.screen.repaint();
                }
            };

            //Starts up the model
            this.model.run();
        }       
    }
}
Nick Rippe
  • 6,465
  • 14
  • 30
  • +1 I'd use `SwingWorker`, but a separate thread for serial IO is essential; see also this [answer](http://stackoverflow.com/a/12731752/230513). – trashgod Oct 10 '12 at 19:31
  • @Nick Rippe Thanks a million! The "real" serial code involves saving ~200 incoming bytes into buffers shared by the graphic part. This happens every 500ms so that event will be my "timer". Need to refresh also "hidden" values in buffers that might be displayed later when scrolling through menus. Will this happen in the code you provided? My original code was ~9000 lines so it will take me some time to put things back according to your suggestions. Probably be back tomorrow with some questions along the way. Need to go to bed now... /Richard – user1735586 Oct 10 '12 at 20:05
  • You should be able to cram those other values into the same listener - just add more lines like this that pass the info from the Model the View. `Control.this.screen.modelRefreshInfo = Control.this.model.count;` – Nick Rippe Oct 10 '12 at 21:28
  • @user1735586 Also a +1 = Thanks on StackOverflow ;) Good luck! – Nick Rippe Oct 10 '12 at 21:34
  • Since several of you asked for a more detailed description, here it goes: I've created a control system for heating. There is a remote control with graphic LCD to set up parameters. LCD also gives information about temperatures when running. I want to duplicate what you can see on the LCD. Eventually Java code will live in a very small webserver and the application will open in a web browser. This way you can use a smartphone connected to browser through webserver to control and study the heating system as it runs. (more to come) – user1735586 Oct 11 '12 at 17:02
  • As a first step I'm sending serial packets over RS232 from "BaseUnit" that do A/D conversions on NTC sensors and control relays and triacs on output side. There is a RS485 bus connecting BaseUnit with Remote. I.e. MainScreen displays 8 temperatures and 2 rectangles that will be filled as temperature rises. Also some graphic symbols display the state of pumps, triacs and GSM connection. Most other screens (menus) only contain a Parameter name and its value. Values can be changed by the user and new values need to be sent to BaseUnit to make it change behavior. but for now I concentrate on RX. – user1735586 Oct 11 '12 at 17:05
  • I need to catch and save ~200 incoming bytes (this works) and pass those values to the graphic part (menu system). This is where I'm stuck. On graphic screen there are 5 buttons and a scrollbar to navigate and change values (this works). The method to choose must include saving also hidden values into buffers, not only updating the current menu. It must also include 2-way serial comms, that is I need to send values changed by user to BaseUnit. I hope this makes it clearer what my intention is. Fell free to ask more. I will now make an effort to put my code into skeleton provided by Nick Rippe. – user1735586 Oct 11 '12 at 17:18