3
\$\begingroup\$

I am writing a serial port communication app. And I thought I could get some feedback about the general structure of the program. If I am doing MVC correct, etc. Since this is a "large" program I came up with a something like a minimum working example, that shows the essence of my app (code is below):

But I guess nobody wants to read this much code, so here is a textual summary:

The Model:

  • The DataModel loads data from a json file. It as a JsonSerializableConfig and a List of DataSources as members. These get filled with data when the json is deserialized. An instance of the DataModel gets injected into Controllers that need access the data. The Controllers then can call for example : dataModel.getConfig().getStartByte();
  • JsonSerializableConfig: A container for configuration data with setters and getters. For example start byte and message length are saved here. Tells you how to decode the data from the serial port (How to detect a message ...)
  • DataSource: An abstract class that represents some generic thing that produces data. It could be a temperature sensor or a bit flag or GPS position.
  • SimpleSensor A realization of the abstract DataSource class. You can add Listeners to it and get informed about new data. And it is possible to insert new data. The insertion of data and subscribing to it can be in different threads.
  • SimpleSensorListener So that Controllers or other classes can subscribe to a SimpleSensor
  • Of course in the real application there are more realizations of the abstract DataSource and the DataModel also contains predefined Telecommands.

The Controller(s):

  • MainWindowControl: It gets a DataModel injected into it. Subscribes to all DataSource that are SimpleSensors and writes the data it receives to a TextArea (from a fxml file).
  • Of course in the real application there are way more Controllers where you can select what DataSources to display and how.

The View(s):

  • In my minimal example this is just fxml with one large TextArea.

Other

  • SerialCommThread: In my minimum working example a fake one. But in the real application it receives data from the serial port and distributes it to the various DataSources. It also gets access to the Model.

Specific Questions:

  1. If the data in the model is made editable isn't that evil global State ? Because some Controller could call DataModel.getConfig().setStartByte(-1);
  2. So should the data in the model be effectively final ? It can't be final because of deserialization by gson. What if I want to change the model data, by not editing the json file directly ?
  3. Is the MVC implemented correctly here ? A model for how to decode data from the serial port. Views to define GUI elements. Controllers that subscribe to DataSources from the Model and update the GUI.

Since I wanted to include a working example, I included the code for a minimum working version, with very little features. The complete Project can be found at GitHub. I hope someone can give his 5ct.

Main.java, MainWindowControl.java, SerialCommThread.java

package main;


import model.DataModel;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.File;

public class Main extends Application {

    private static final String MAIN_WINDOW_FXML = "../gui/main_window.fxml";

    /**
     * The default interpretation file that is used, which defines available sources etc.
     */
    private static final String DEFAULT_INTERPRETATION_FILE = "interpretationFiles/test.txt";

    public static  void main(String[] args){
        launch(args);
    }

    @Override
    public  void  start(Stage primaryStage) throws Exception{
        //Init Model
        DataModel model = new DataModel();
        model.loadConfigData(new File(DEFAULT_INTERPRETATION_FILE));

        //Load fxml
        FXMLLoader loader = new FXMLLoader(getClass().getResource(MAIN_WINDOW_FXML));
        Scene scene = new Scene(loader.load(), 800, 600);

        //Inject model
        MainWindowControl mainWindowControl = loader.getController();
        mainWindowControl.initModel(model);

        //GUI-stuff
        primaryStage.setScene(scene);
        primaryStage.setMinWidth(800);
        primaryStage.setMinHeight(600);
        primaryStage.show();

        //Start a SerialCommThread
        SerialCommThread serialCommThread = new SerialCommThread(model);
        serialCommThread.setDaemon(true);
        serialCommThread.start();
    }
}

package main;

import javafx.fxml.FXML;
import javafx.scene.control.TextArea;
import model.DataModel;
import model.DataSource;
import model.SimpleSensor;
import model.SimpleSensorListener;

public class MainWindowControl implements SimpleSensorListener {

    @FXML
    private TextArea textArea;

    public void initModel(DataModel model) {
        for(DataSource dataSource :model.getDataSources()){
            if(dataSource instanceof SimpleSensor){
                SimpleSensor sensor = (SimpleSensor)dataSource;
                sensor.addListener(this);
            }
        }
    }

    @Override
    public void onUpdateData(SimpleSensor sensor, Number value) {
        textArea.appendText(sensor.getName()+" : " +value.doubleValue()+System.lineSeparator());
    }
}

package main;


import model.DataModel;
import model.DataSource;

import java.util.*;

/**
 * Created by Kai on 25.05.2017.
 * An extra Thread for handling the serial Port Communication.
 * It can receive and send data.
 * The received data is inserted into the dataModel, as specified in the config file.
 * Additionally this Thread makes its "Health" visible to other threads, although there is no Watchdog that would kill it :D (yet !!!)
 */
public class SerialCommThread extends Thread {



    /**
     * Maps message id's to corresponding lists of sources, that can be found in this kind of message.
     * For example messages with the id 1 always contain temp 1 gyro x,y,z and temp2.
     * In order to be able to use byte arrays as keys and still have meaningful comparision I use the Byte Wrapper that implements exactly that.
     */
    private Map<Byte, List<DataSource>> messageMap = new HashMap<Byte, List<DataSource>>();

    private volatile boolean isRunning = true;

    /**
     * Constructor for a SerialCommunicationThread.
     * @param dataModel the thread needs to know stuff how to decode the data it receives from the serial port.
     */
    public SerialCommThread(DataModel dataModel){
        initMessageMap(dataModel);
    }

    /**
     * This method is, in a way the core of the whole GSS program.
     */
    @Override
    public void run() {
        while(isRunning) {
            //Read
            readByte();
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void readByte() {
        //mock real serial communication.
        decodeMessage(new byte[]{1,0,42});
    }

    /**
     * Initializes the MessageMap with all messageId's as keys and lists of corresponding sources as lists.
     */
    private void initMessageMap(DataModel dataModel) {
        for (DataSource datasource:dataModel.getDataSources()) {
            if(messageMap.containsKey(datasource.getMessageId())){
                messageMap.get(datasource.getMessageId()).add(datasource);
            }else {     // if the map does not contain the key yet create a new List of datasources, and add it to the map.
                List<DataSource> dataSourceList = new ArrayList<>();
                dataSourceList.add(datasource);
                messageMap.put(datasource.getMessageId(),dataSourceList);
            }
        }
    }

    /**
     * Decodes the message, without start/stop bytes.
     * @param msgBuffer
     */
    private void decodeMessage(byte[] msgBuffer) {
        byte messageId = msgBuffer[0];

        if(messageMap.get(messageId)!= null){
            for(DataSource source : messageMap.get(messageId)){
                byte[] value = Arrays.copyOfRange(msgBuffer, 1, 3);
                source.insertValue(value);
            }
        }
    }

    public void stopThread(){
        isRunning = false;
    }
}

DataModel.java, DataSource.java, JsonSerializableConfig.java

package model;


import com.google.gson.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import java.io.File;
import java.io.IOException;


/**
 * Holds all data needed for the application. The configuration, etc.
 * My Approach to the MVC-Pattern.
 */
public class DataModel {

    private JsonSerializableConfig config;
    private final ObservableList<DataSource> dataSources = FXCollections.observableArrayList();

    public ObservableList<DataSource> getDataSources() {
        return dataSources;
    }

    //TODO is this really the right way ?
    public JsonSerializableConfig getConfig(){
        return config;
    }

    public void loadConfigData(File file) throws IOException {
        GsonBuilder gsonBuilder = new GsonBuilder();
        Gson gson = gsonBuilder.create();
        config =  gson.fromJson(IOUtility.readFile(file),JsonSerializableConfig.class);
        dataSources.clear();
        dataSources.addAll(config.getDataSources());
    }
}

package model;

/**
 * A DataSource can be a simple temperature sensor, or a GPS sensor, or a bit flag or whatever.
 * Created by Kai on 27.04.2017.
 */
public abstract class DataSource {

    //Fields for identifying this data-source (e.g a temp sensor or whatever) in a data package coming from the serial-port.
    private byte messageId;

    public byte getMessageId(){
        return messageId;
    }

    /**
     * Inserts a new value into this dataSource.
     * @param bytes the value encoded as bytes.
     */
    public abstract void insertValue(byte[] bytes);

    public abstract String getName();
}

package model;


import java.util.ArrayList;
import java.util.List;

/**
 * Created by Kai on 25.05.2017.
 * This class can hold all the data, that is needed to decode incoming data from the serial Port and how to decode it.
 */
public class JsonSerializableConfig {

    private ArrayList<SimpleSensor> simpleSensors;
    /**
     * The start byte of a message send through Serial Communication.
     */
    private byte startByte;
    /**
     * The length of a single complete message, in bytes. Only used for the FIXED_MSG_LENGTH ReadMode.
     */
    private int messageLength;

    public List<DataSource> getDataSources() {
        ArrayList<DataSource> dataSources = new ArrayList<>();
        for (SimpleSensor sensor: simpleSensors) {
            dataSources.add(sensor);
        }
        return dataSources;
    }

    /**
     * @return the start Bytes of the Messages we want to receive.
     */
    public byte getStartByte() {
        return startByte;
    }

    /**
     * All msg must have the same length.
     * @return
     */
    public int getMessageLength() {
        return messageLength;
    }
}

SimpleSensor.java SimpleSensorListener.java

package model;

import javafx.animation.AnimationTimer;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Created by Kai on 27.04.2017.
 * A simple Sensor, represents for example a temperature sensor or gyroscope etc.
 */
public class SimpleSensor extends DataSource {

    private String name;
    /**
     * This allows for the thread safe data exchange between the gui Thread and the SerialCommThread
     */
    private ConcurrentLinkedQueue<Number> dataQueue = new ConcurrentLinkedQueue<>();
    private List<SimpleSensorListener> listeners = new ArrayList<>();

    public SimpleSensor(){
        informListeners();
    }

    public void addListener (SimpleSensorListener listener) {
        listeners.add(listener);
    }

    public void removeListeners(SimpleSensorListener toBeRemoved) {
        listeners.remove(toBeRemoved);
    }

    /**
     * gets called in the JavaFX Main thread.
     * Iterates over all Listeners and informs them.
     */
    private void informListeners() {
        SimpleSensor sensor = this;
        new AnimationTimer() {
            @Override
            public void handle(long now) {
                while (!dataQueue.isEmpty()) {
                    Number value = dataQueue.remove();
                    for(SimpleSensorListener listener : listeners){
                        listener.onUpdateData(sensor, value);
                    }
                }
            }
        }.start();
    }

    /**
     * Inserts a new value into this dataSource.
     * The reason it gets the raw bytes is that the dataSource itself will know best how to decode it.
     * @param bytes the value encoded as bytes.
     */
    @Override
    public void insertValue(byte[] bytes) {
        short rawValue = ByteBuffer.wrap(bytes).getShort();
        dataQueue.add((int)rawValue);
    }

    @Override
    public String getName() {
        return name;
    }
}


package model;

/**
 * A class that implements this Interface promises it can do something with updates from a DataSource, that publishes points from time to time.
 * This way various classes can register to a DataSource an get informed about new data.
 * Created by Kai on 18.05.2017.
 */
public interface SimpleSensorListener {
    void onUpdateData(SimpleSensor dataSource, Number value);
}

main_window.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1" fx:controller="main.MainWindowControl">
   <children>
      <TextArea fx:id="textArea" layoutX="10.0" layoutY="10.0" prefHeight="580.0" prefWidth="780.0" text="Incoming Data:" />
   </children>
</AnchorPane>
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.