Skip to main content Skip to local navigation

Main Project Example for EECS 1021

Main Project Example for EECS 1021

The following is an example of a main project for the EECS 1021 course. While I think that it's important to provide concrete examples it's also important not to provide examples that are trivial to copy and, thus, reduce the effectiveness of the example as a learning tool. As such, I'm providing this example using Tinkerforge hardware rather than the Arduino hardware that students use. This requires the use of two fundamentally different sets of libraries:

Because of these differences, simply copying the code below isn't going to result in a working project. But if you're looking for structure, some further guidance or some key hints, this page should do the trick.

The following is a video report example for the main project.

Video report example for the main project.

Note, again, that the example is based on Tinkerforge hardware and its Java API. You'll notice the explicit use of learning outcomes in the video. These learning outcomes are provided to the students in course documentation. It's also incorporated into a grade guide (rubric) for students. Here it is, for convenience:

Marking rubrics for the learning outcomes
We will be evaluating your project using the five course learning outcomes and their rubrics.  Both the Report and the Video will be assessed using these rubrics. Each learning outcome has a course-specific version (“CLO”) and a generic program one (GAI).  Students should focus on the CLO but can refer to the GAI for context.

Learning Outcome 1
(CLO 1)	Demonstrate the ability to test and debug a given program and reason about its correctness.
General form of the learning outcome (GAI 2b): Formulate a strategy for solving an engineering problem	Note 1: JUnit testing is one way to do this.
Note 2: You can also describe how you verified that the moisture-to-pumping process worked, how you determined something was broken & fixed it, etc.	Grade:


Report      /3
Video        /3

	1.	Does not formulate an adequate strategy
2.	Formulates a partial strategy for solving a problem, but it is not the best strategy for the context
3.	Formulates a strategy for solving an engineering problem
4.	Formulates multiple potential/possible strategies that could be used to solve the problem	Recommended approach: It’s best to cover Unit Testing here (see: https://tinyurl.com/52wcnvxt) 

Learning Outcome 2
(CLO 2)	Course-specific learning outcome 2 (CLO2):  Given a problem specification and a suitable API, build an application that meets the given requirement. 
General form of the learning outcome: (GAI 4b):  Conceive design solutions to solve the defined problem)	Note 1. You are to provide the specification in your report
Note 2. Princeton StdLib, Firmata4j, jFreeChart, jSerialComm, JavaFX and Open CSV all have suitable APIs.  Feel free to use others.	Grade:


Report      /3
Video        /3

	1.	Does not design solutions to solve defined problem.
2.	Designs incomplete solutions.
3.	Solutions complete, but lacking in elegance/innovation/creativity/professionalism.
4.	Conceives elegant/innovative/creative/professional standard solutions to solve the defined problem	Recommended approach: 
Describe why StdLib and Firmata4j are appropriate for this project and explain how you used them (e.g. which classes, methods, etc.)	
Learning Outcome 3
(CLO 3)	Course-specific learning outcome 3 (CLO3):  Use ready-made collections to solve problems involving aggregations of typed data. 
General form of the learning outcome (GAI 5b): Adapt appropriate techniques, resources, and modern engineering tools to complex engineering problems	Note : look over class videos on ArrayList and HashMap.  These are both part of Java Collections.	Grade:


Report      /3
Video        /3

	1.	Does not adapt modelling methods, tools or software; can only use existing ones 
2.	Minor or ineffective adaptation of modelling methods, tools or software
3.	Effective adaptation of modelling methods, tools or software 
4.	Effective adaptation, creation or extension of modelling methods, tools or software	Recommended approach: your sensor data should be stored in an ArrayList or HashMap prior to graphing as both are part of Java’s Collections.	
Learning Outcome 4
(CLO 4)	Course-specific learning outcome 4 (CLO4):  Build an event-driven application that controls sensors and actuators in order to connect events to physical actions.
General form of the learning outcome: (GAI 4b):  Conceive design solutions to solve the defined problem)	Note 1 : Java Listeners and the Firmata Change Value method are both applicable.  Note 2: State-machines are key here.  Structure your code as an event-driven state machine.	Grade:


Report      /3
Video        /3

	1.	Does not design solutions to solve defined problem.
2.	Designs incomplete solutions.
3.	Solutions complete, but lacking in elegance/innovation/creativity/professionalism. 
4.	Conceives elegant/innovative/creative/professional standard solutions to solve the defined problem	Recommended approach: Describe the state machine and the conditional statements that underlie it when the Arduino hardware communicates with Java.	
Learning Outcome 5
(CLO 5)	Course-specific learning outcome 5 (CLO5):  Program common applications from a variety of engineering disciplines using an object-oriented language and solve them on the computer.
General form of the learning outcome: (GAI 4c):  Apply an iterative process to refine or assign solutions for a given engineering design problem	There are two things to describe:
1. Object-oriented elements in your program. This includes: abstraction, encapsulation, data hiding, inheritance and polymorphism.
2. How your solution improved through refinement.
	Grade:


Report      /3
Video        /3

	1.	Does not produce a solution.	
2.	Applies an incomplete iterative process: Solutions need further refinement
3.	Applies an appropriate number of iterations to refine or assign solutions for a given engineering design problem	
4.	Applies an iterative process to arrive at an elegant/innovative/creative/professional standard solution for a given engineering design problem	Recommended approach: Name at least one OO concept and how it was used in a concrete manner. Also, tell me how your implementation changed from when you started to when you finished it.
Marking rubric for the main project

Here is the source code for this project, using the Tinkerforge hardware. Note the structure of the project:

TinkerForgeWatering_March2024_v5 v
Project v
:一
TinkerForgeWatering_March2024_v5 ~/Ideal
>
1.idea
> D out
く
口 src
• ca.yorku.eecs.eecs1021
© Board
© GraphPeriodicUpdate
© MainClass
© RelayPeriodicUpdate
© SensorPeriodicSample
© SevenSegPeriodicUpdate
© StateMachinePlantWatering
© theUnitTest
Project structure. Files are in the ca.yorku.eecs.eecs1012 package. There are eight java files within it.

The main file, MainClass.java

/* ==============================================================
 * James Andrew Smith
 * March 17, 2024
 * Maven: TinkerForge import: com.tinkerforge:tinkerforge:2.1.33
 *
 ============================================================== */

import com.tinkerforge.*;

import java.io.IOException;
import java.util.Timer;

public class MainClass {

    /* Make Tinkerforge objects available within the Main Class */
    static IPConnection ipcon = new IPConnection();  // Create IP connection
    static BrickletMoisture sensorMoisture = new BrickletMoisture(Board.UID_MOISTURE, ipcon);
    static BrickletSegmentDisplay4x7 displaySevenSeg = new BrickletSegmentDisplay4x7(Board.UID_7SEGMENT, ipcon);
    static BrickletDualRelay doubleRelay = new BrickletDualRelay(Board.UID_DUALRELAY,ipcon);
    static BrickletDualButton doubleButton = new BrickletDualButton(Board.UID_DUALBUTTON,ipcon);

    static BrickMaster mainBrick = new BrickMaster(Board.UID_MAINBRICK, ipcon); // Create device object

    /* sensor values */
    static int sensorValueMainVoltage = 0;  // [Millivolts]
    static int sensorValueMainTemperature = 0;  // [?]
    static int sensorValueMoisture = 0;
    private static boolean buttonWateringPressedValue = false;


    public static void main(String[] args) throws TinkerforgeException {

        /* 0. Initialize hardware */
        initHardware();

        /* 1. Launch button listener.  Contains an emergency Exit if button pushed. */
        launchButtonListener();

        /* 2. Launch Timed Task for State Machine updates*/
        launchStateMachineUpdates();

        /* 3. Launch sensor sampling Task */
        launchPeriodicSensorSampling();


    /*    for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println("Moisture value (periodic) : " + SensorSample.getMoisturePeriodic());
        }
        */

        /* 4. Launch relay action */
        launchPeriodicActuation();

        /* 5. Launch graph updater */
        launchPeriodicGraph();

        /* 6. Launch the 7 segment display updater */
        launchSevenSegmentDisplayUpdater();

        /* 7. Exit */
        promptUserToExit();
        exitProgram();


    }

    private static void launchSevenSegmentDisplayUpdater() throws TinkerforgeException {


        // set it up to display the seven segments display periodically..
        var periodicSevenSegment = new SevenSegPeriodicUpdate(displaySevenSeg);
        new Timer().schedule(periodicSevenSegment,
                0,
                Board.PERIOD_STATEMACHINE_UPDATE);

    }

    private static void launchPeriodicGraph() throws TinkerforgeException {

        // create the window once.
        GraphPeriodicUpdate.createGraph();

        // set it up to get data periodically.
        var periodicGraph = new GraphPeriodicUpdate();
        new Timer().schedule(periodicGraph,
                0,
                Board.PERIOD_GRAPHUPDATE);

    }

    private static void launchPeriodicActuation() throws TinkerforgeException {

        /* what is the main board temperature? */
        sensorValueMainTemperature = mainBrick.getChipTemperature();
        // Get current stack voltage
        sensorValueMainVoltage = mainBrick.getStackVoltage(); // Can throw com.tinkerforge.TimeoutException


        //System.out.println("Stack Voltage: " + stackVoltage/1000.0 + " V");
        /* what is the time on the central PC? */
        var currentTime = System.currentTimeMillis();

        /* what is the moisture value? */
        sensorValueMoisture = sensorMoisture.getMoistureValue();

        /* what is the button state */

        var periodRelay = new RelayPeriodicUpdate(doubleRelay);
        new Timer().schedule(periodRelay,
                0,
                Board.PERIOD_RELAYACTION);

    }

    private static void launchPeriodicSensorSampling() throws TinkerforgeException {

        /* what is the main board temperature? */
        sensorValueMainTemperature = mainBrick.getChipTemperature();
        // Get current stack voltage
        sensorValueMainVoltage = mainBrick.getStackVoltage(); // Can throw com.tinkerforge.TimeoutException


        //System.out.println("Stack Voltage: " + stackVoltage/1000.0 + " V");
        /* what is the time on the central PC? */
        var currentTime = System.currentTimeMillis();

        /* what is the moisture value? */
        sensorValueMoisture = sensorMoisture.getMoistureValue();

        /* what is the button state */

        var periodSensorSample = new SensorPeriodicSample(sensorMoisture,mainBrick);
        new Timer().schedule(periodSensorSample,
                0,
                Board.PERIOD_SAMPLESENSORS);

    }

    private static void launchStateMachineUpdates() {
        /* Launch a timer task with a specific period */
        var repeatingTask = new StateMachinePlantWatering();
        new Timer().schedule(repeatingTask,0,Board.PERIOD_STATEMACHINE_UPDATE);
    }

    /**
     * This method requests that user press [enter]
     * It effectively doesn't allow the program to continue until [enter] is pressed.
     *
     * @ param: none
     * @ returns: none
     * @ throws: none        */
    private static void promptUserToExit() {
        System.out.println("Press [enter] to exit");
        try {
            System.in.read();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * This method exits the program in a graceful manner.
     * Turn off: (1) the 7-segment display and (2) the relays.
     * @ param: none
     * @ returns: none
     * @throws TinkerforgeException if there's an issue with Tinkerforge communi\cation.
     */
    static void exitProgram() throws TinkerforgeException {
        short[] segments = {Board.SEGMENT_OFF, Board.SEGMENT_OFF, Board.SEGMENT_OFF, Board.SEGMENT_OFF};

        /* turn off the relay and display */
        displaySevenSeg.setSegments(segments, (short)7, false);     // Display off
        doubleRelay.setState(Board.RELAY_OFF,Board.RELAY_OFF);          // Relays off

        /* messages to screen */
        System.out.println("Turning off display and relays...");
        System.out.println("exiting... ");

        /* Force exit */
        System.exit(0);
    }

    public static void initHardware() throws TinkerforgeException {

        /* Apparently there is a "rugged" version for ipcon.  But
         * not clear beyond C# and python how to do it.  Not sure about Java.
         * To be examined later.
         * Source: https://www.tinkerforge.com/en/doc/Tutorials/Tutorial_Rugged/Tutorial.html
         */

        /* ---------------------------------------------
         * Connect to the TinkerForge board
         *
         * -------------------------------------------- */

        /* Connect to the board */
        ipcon.connect(Board.HOST,Board.PORT);

        // Write "0000" to the display with full brightness without colon
        short[] segments = {Board.DIGITS[0], Board.DIGITS[0], Board.DIGITS[0], Board.DIGITS[0]};
        displaySevenSeg.setSegments(segments, (short)7, false);
    }


    public static void launchButtonListener(){


        // Add state changed listener
        doubleButton.addStateChangedListener((buttonL, buttonR, ledL, ledR) -> {

            /* Check for emergency stop request */
            if(buttonL == BrickletDualButton.BUTTON_STATE_PRESSED) {
                System.out.println("emergency button: Pressed");

                /* activate exit.
                *  This is an emergency exit.  Bypasses everything else. */
                try {
                    exitProgram();
                } catch (TinkerforgeException e) {
                    throw new RuntimeException(e);
                }

            }
            /* otherwise, look for request to manually water plant.
            *  And let the state machine class know that the button has been pressed. */
            else if(buttonR == BrickletDualButton.BUTTON_STATE_PRESSED) {
                System.out.println("watering button: Pressed");
                buttonWateringPressedValue = true;
                //StateMachinePlantWatering.buttonWateringPressed = true;
            } else if(buttonR == BrickletDualButton.BUTTON_STATE_RELEASED) {
                System.out.println("watering button: Released");
                buttonWateringPressedValue = false;
                //StateMachinePlantWatering.buttonWateringPressed = false;
            }

            /*
            if(buttonR == BrickletDualButton.BUTTON_STATE_PRESSED) {
                System.out.println("Right Button: Pressed");
            } else if(buttonR == BrickletDualButton.BUTTON_STATE_RELEASED) {
                System.out.println("Right Button: Released");
            }
            */
            //System.out.println("event happened");


        });
    }


    // Provide the value of the watering button value to other classes, like the State Machine.
    public static boolean getButtonWateringPressed(){
        return buttonWateringPressedValue;
    }
}

A State Machine class, StateMachinePlantWatering.java

import java.util.TimerTask;


public class StateMachinePlantWatering extends TimerTask {
    public static final long INTERVALPERIOD = 5000;     // 5000 ms intervals.
    enum StateValue {
        STATE0,     // startup
        STATE1,     // init     (pump off)
        STATE2,     // 1st stage regular operation, default  (pump off)
        STATE3,     // 1st stage regular operation, button pressed   (pump on)
        STATE4,     // 2nd stage default  (pump off)
        STATE5,     // 2nd stage sensor dry   (pump on)
        STATE6,      // Exit stage (regular timeout)     (pump off)
        STATE7,     // Exit stage (e-stop button) (pump off)
        STATE8,     // exit stage (error) (pump off)
        STATE9      // don't have a use for this yet.
    }
    static StateValue wateringState = StateValue.STATE0;

    static public Boolean buttonWateringPressed = false;

    @Override
    public void run() {

        // check on the periodically-measured soil moisture
        int currentMoisture = SensorPeriodicSample.getMoisturePeriodic();

        // pass along the most recently recorded soil moisture value.
        updateState(currentMoisture);

    }

    public static StateValue getWateringState(){
        return wateringState;
    }

    public static void updateState(int theMoisture) {

        Boolean buttonWatering = MainClass.getButtonWateringPressed();
        /* update the state.
         *  If it's just timing-related and no events have happened...
         *  1 -> 2 -> 4 -> 1 */
        if (wateringState == StateValue.STATE0){
            wateringState = StateValue.STATE1;
            System.out.println("state 1: turn off pump");
        }
        else if (wateringState == StateValue.STATE1){
            wateringState = StateValue.STATE2;
            System.out.println("state 2: turn off pump");

        }
        else if (wateringState == StateValue.STATE2){

            // Turn on the pump if the button was pressed.
            if(buttonWatering == true){
                wateringState = StateValue.STATE3;
                System.out.println("state 3: turn ON pump");
            }else{
                wateringState = StateValue.STATE4;
                System.out.println("state 4: turn off pump");
            }


        }
        // Button was pressed and we're watering manually...
        else if (wateringState == StateValue.STATE3){

            // Turn on the pump if the button was pressed.
            if(buttonWatering == true){
                wateringState = StateValue.STATE3;
                System.out.println("state 3: turn ON pump");
            }else{
                // return to state 2 if the button is released.
                wateringState = StateValue.STATE2;
                System.out.println("state 2: turn OFF pump");
            }


        }
        // If we're on State 4 & soil is wet, then move on to the next step (State 1)
        else if ((wateringState == StateValue.STATE4) && (theMoisture >= Board.MOISTURE_WETSOIL_THRESHOLD)){
            wateringState = StateValue.STATE1;
            System.out.println("Soil is wet.");
            System.out.println("state 1: turn off pump");
        }
        // Otherwise, if we're on State 4 & soil is dry, then go  to State 5 to deal with dry soil
        else if ( (wateringState == StateValue.STATE4) && (theMoisture < Board.MOISTURE_WETSOIL_THRESHOLD) ){
            wateringState = StateValue.STATE5;
            System.out.println("Soil is dry. ( " + theMoisture + " ).");
            System.out.println("state 5: turn ON pump");
        }
        // Otherwise, if we're on State 5 & soil is dry, then remain in State 5 to deal with dry soil
        else if ( (wateringState == StateValue.STATE5) && (theMoisture < Board.MOISTURE_WETSOIL_THRESHOLD) ){
            wateringState = StateValue.STATE5;  // stay on state5
            System.out.println("Soil is still dry. ( " + theMoisture + " ).");
            System.out.println("state 5: turn ON pump");
        }
        // Otherwise, if we're on State 5 & soil is WET, then go to State 1 to deal with wet soil
        else if ( (wateringState == StateValue.STATE5) && (theMoisture >= Board.MOISTURE_WETSOIL_THRESHOLD) ){
            wateringState = StateValue.STATE1;  // go to state 1
            System.out.println("Soil is now wet. ( " + theMoisture + " ).");
            System.out.println("state 5: turn OFF pump");
        }
        else{
            wateringState = StateValue.STATE6;
            System.out.println("error: states not updating right...");
            // need to exit program...
        }
        //System.out.println("-------------------------------------");
        //System.out.println("-------------------------------------");
        //System.out.println("DEBUG: Button state: " +       MainClass.getButtonWateringPressed());
        //System.out.println("-------------------------------------");
        //System.out.println("-------------------------------------");


    }
}

A class for constants, Board.java

public class Board {
     public static final boolean RELAY_OFF = false;
     public static final boolean RELAY_ON = true;

     static final String HOST = "localhost";
     static final int PORT = 4223;
     static final String UID_MAINBRICK = "5W5fXS";
     static final String UID_MOISTURE = "s21";
     static final String UID_DUALBUTTON = "j1T";
     static final String UID_7SEGMENT = "pS1";
     static final String UID_DUALRELAY = "rBa";

      static final byte[] DIGITS = {0x3f,0x06,0x5b,0x4f,
             0x66,0x6d,0x7d,0x07,
             0x7f,0x6f,0x77,0x7c,
             0x39,0x5e,0x79,0x71}; // 0~9,A,b,C,d,E,F
     static final byte SEGMENT_OFF = 0x0;

     static final int MOISTURE_WETSOIL_THRESHOLD = 70;  // higher means wetter.
     static final int PERIOD_RELAYACTION = 500;   // [500 ms]
     static final int PERIOD_SAMPLESENSORS = 200; //  [200 ms]
     static final int PERIOD_STATEMACHINE_UPDATE = 1000; // [1000 ms = 1 second]
     static final int PERIOD_GRAPHUPDATE = 1000;  // 1 seconds.

}

A class for graphing: GraphPeriodicUpdate

This file uses jFreeChart but students are likely to use Princeton StdLib's StdDraw instead:

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

import javax.swing.*;
import java.awt.*;
import java.util.TimerTask;

public class GraphPeriodicUpdate extends TimerTask {

    static         JFrame window = new JFrame();
    static XYSeries series = new XYSeries("Moisture Readings Over Time");
    private static int graph_xvalue = 0;
    private static int graph_yvalue = 0;
    public static void createGraph(){
        window.setTitle("Graph of Moisture Measurement over Time (0 = dry; 100 = very wet)");
        window.setSize(800, 600);
        window.setLayout(new BorderLayout());
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JLabel label = new JLabel();
        label.setLocation(550, 250);
        label.setFont(new Font("Sans Serif", Font.ITALIC,15));
        window.add(label, BorderLayout.EAST);


        //XYSeries series = new XYSeries("Moisture Readings Over Time");

        XYSeriesCollection dataset = new XYSeriesCollection(series);
        JFreeChart chart = ChartFactory.createXYLineChart(
                "Moisture Readings over Time",
                "Time [sec]",
                " Moisture Values [0 = dry; 100 = very wet]", dataset,
                PlotOrientation.VERTICAL,true,true,false);

        window.add(new ChartPanel(chart), BorderLayout.CENTER);

        window.setVisible(true);

    }

    @Override
    public void run() {

        // get the sensor value.
        graph_yvalue = normalizeYValue(SensorPeriodicSample.getMoisturePeriodic());

        // update x and y.
        series.add(graph_xvalue++, graph_yvalue);

        // update the graph, live.
        window.repaint();


    }

    // Conversion of sensor values from 0 to 1023 into 0 to 100;
    static public int normalizeYValue(int originalValue){

        final float MAXVALUE = 4095.0F;//1023.0F;
        final float MINVALUE = 0.0F;
        final int ERRORVALUE = +1000;


        int theReturnValue;

        System.out.println("DEBUG:          the original value: " + originalValue        );

        if((originalValue > MAXVALUE)){
            theReturnValue = ERRORVALUE;
            //System.out.println("ERROR: sensor value is too high (over " + MAXVALUE + ")");
        }
        else if ( (originalValue < MINVALUE) ){
            theReturnValue = ERRORVALUE;
            //System.out.println("ERROR: sensor value is too low (under " + MINVALUE + ")");
        }
        else{
            theReturnValue = (int)(100.0*((float)(originalValue)*((1.0)/(MAXVALUE-MINVALUE))));
        }
        return theReturnValue;
    }
}

A class for actuation / pumping: RelayPeriodicUpdate.java

This class activates the relay on the Tinkerforge system. Students have access to either a MOSFET switch or a Relay in their kits. Either will do the trick with the pump.

import com.tinkerforge.BrickletDualRelay;
import com.tinkerforge.TinkerforgeException;

import java.util.TimerTask;

public class RelayPeriodicUpdate extends TimerTask {
    private final BrickletDualRelay doubleRelay;

    public RelayPeriodicUpdate(BrickletDualRelay doubleRelay) {
        this.doubleRelay = doubleRelay;
    }

    @Override
    public void run() {

        // what is the current state?
        StateMachinePlantWatering.StateValue theState = StateMachinePlantWatering.getWateringState();

        // If state 3 or 5 is detected, then water plant.  Otherwise, turn off the pump.
        if ((theState == StateMachinePlantWatering.StateValue.STATE5)           // moist soil.
            || (theState == StateMachinePlantWatering.StateValue.STATE3)        // button pressed.
        ){
            // Turn on the pump via the relay.
            try {
                doubleRelay.setState(true,false);
            } catch (TinkerforgeException e) {
                throw new RuntimeException(e);
            }
        }
            else{  // all other states.
                // Turn off the pump
            try {
                doubleRelay.setState(false,false);
            } catch (TinkerforgeException e) {
                throw new RuntimeException(e);
            }
        }

    }


}

A class for checking the moisture sensor, SensorPeriodicUpdate.java

import com.tinkerforge.BrickMaster;
import com.tinkerforge.BrickletMoisture;
import com.tinkerforge.TinkerforgeException;

import java.time.Clock;
import java.util.HashMap;
import java.util.TimerTask;

public class SensorPeriodicSample extends TimerTask {

    BrickletMoisture sensorMoisture;
    BrickMaster mainBrick;
    private static int moistureValuePeriodic;
    private static long milliSeconds;
    private static HashMap<Long,Integer> MoistureTimeStamped = new HashMap<>();
    public SensorPeriodicSample(BrickletMoisture sensorMoisture, BrickMaster mainBrick) {
        this.sensorMoisture = sensorMoisture;
        this.mainBrick = mainBrick;
    }
    // constructor


    @Override
    public void run() {
        // record time
        Clock clock = Clock.systemDefaultZone();
        milliSeconds=clock.millis();
        // poll sensors
        try {
            moistureValuePeriodic = sensorMoisture.getMoistureValue();
        } catch (TinkerforgeException e) {
            throw new RuntimeException(e);
        }

        // store time-stamped value in a HashMap (CLO3)
        MoistureTimeStamped.put(milliSeconds,moistureValuePeriodic);

        int sum = MoistureTimeStamped.values().stream().mapToInt(Integer::intValue).sum();
        System.out.println("Moisture average = " + sum/MoistureTimeStamped.size());
    }

    // getters for sensor data.
    public static int getMoisturePeriodic(){
        // the moisture value is captured while running periodically.
        // store the value in this static variable and make it available
        // upon request to another class/method.
        return moistureValuePeriodic;
    }

    //
}

A class for the onboard display, SevenSegPeriodicUpdate

The students have an OLED in their kit. But with the TInkerforge system I was using a simpler 7-segment display.

import com.tinkerforge.BrickletSegmentDisplay4x7;
import com.tinkerforge.TinkerforgeException;

import java.util.TimerTask;



public class SevenSegPeriodicUpdate extends TimerTask {
    private final BrickletSegmentDisplay4x7 sevenSegDisplay;
    private static short[] theSegments = new short[]{0,0,0,0};

    private static final byte[] DIGITS = {0x3f,0x06,0x5b,0x4f,
            0x66,0x6d,0x7d,0x07,
            0x7f,0x6f,0x77,0x7c,
            0x39,0x5e,0x79,0x71}; // 0~9,A,b,C,d,E,F

    public SevenSegPeriodicUpdate(BrickletSegmentDisplay4x7 display) {
        this.sevenSegDisplay = display;
    }



    @Override
    public void run() {

        // what is the current state?
        StateMachinePlantWatering.StateValue theState = StateMachinePlantWatering.getWateringState();


        if (theState == StateMachinePlantWatering.StateValue.STATE0){
            theSegments[3] = DIGITS[0];
        }
        else if (theState == StateMachinePlantWatering.StateValue.STATE1){
            theSegments[3] = DIGITS[1];
        }
        else if (theState == StateMachinePlantWatering.StateValue.STATE2){
            theSegments[3] = DIGITS[2];
        }
        else if (theState == StateMachinePlantWatering.StateValue.STATE3){
            theSegments[3] = DIGITS[3];
        }
        else if (theState == StateMachinePlantWatering.StateValue.STATE4){
            theSegments[3] = DIGITS[4];
        }
        else if (theState == StateMachinePlantWatering.StateValue.STATE5){
            theSegments[3] = DIGITS[5];
        }
        else if (theState == StateMachinePlantWatering.StateValue.STATE6){
            theSegments[3] = DIGITS[6];
        }
        else{
            theSegments[3] = DIGITS[15];  // 'F'
        }
        // set the other digits to blank.
        theSegments[0] = Board.SEGMENT_OFF; theSegments[1] = Board.SEGMENT_OFF; theSegments[2] = Board.SEGMENT_OFF;

        /* Update the display*/
        try {
            sevenSegDisplay.setSegments(theSegments, (short)7, false);     // Display off
        } catch (TinkerforgeException e) {
            throw new RuntimeException(e);
        }

    }


}

The testing class, theUnitTest

This is a file for undertaking unit testing in the project. It only examines one method. But more could have been examined.

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Random;

public class theUnitTest {
    final int MAXINPUT = 4095;
    final int MININPUT = 0;
    final int ERRORVALUE = -1000;

    final int MINRESULT = 0;
    final int MAXRESULT = 100;

    @Test
    public void testTheGraphNormalizingMethod(){

        // test to make sure that -1000 is returned for values above 4095 ...
        System.out.println("Testing against too positive input...that it returns an error value of " + ERRORVALUE);
        String theErrorMessage0 = "Error: wrong value returned for an input above 4095.";
        assertEquals(theErrorMessage0, ERRORVALUE,GraphPeriodicUpdate.normalizeYValue(MAXINPUT+1));
        assertEquals(theErrorMessage0, ERRORVALUE,GraphPeriodicUpdate.normalizeYValue(MAXINPUT*1000));

        // ... or below 0.
        System.out.println("Testing against too negative input... that it returns an error value of " + ERRORVALUE);
        String theErrorMessage1 = "Error: wrong value returned for an input below 0.";
        assertEquals(theErrorMessage1, ERRORVALUE,GraphPeriodicUpdate.normalizeYValue(MININPUT-1));
        assertEquals(theErrorMessage1, ERRORVALUE,GraphPeriodicUpdate.normalizeYValue((MININPUT-1)*1000));

        // TESt to see how it behaves for values from 0 to 4095 (2^12-1)
        // if there's a problem,
        System.out.println("Testing against values from " + MININPUT + " to " + MAXINPUT);
        int theResult;
        theResult = GraphPeriodicUpdate.normalizeYValue(MININPUT);
        String theErrorMessage2 = "Error: The result shouldn't be below 0";  // show this if the test fails.
        assertTrue(theErrorMessage2, theResult >= MINRESULT);

        theResult = GraphPeriodicUpdate.normalizeYValue(MAXINPUT+1);
        String theErrorMessage3 = "Error: The result shouldn't be above 100"; // show this if the test fails.
        assertTrue(theErrorMessage3, theResult <= MAXRESULT);

        // Throw at it a hundred random numbers, bounded from min to max values, to see if it fails...
        for (int i = 0; i < 100; i++) {
                System.out.print(".");
                Random randomObject = new Random();
                int randValue = randomObject.nextInt(MAXINPUT+1);
                theResult = GraphPeriodicUpdate.normalizeYValue(randValue);
                String theErrorMessage4 ="Error: The result is _not_ between 0 and 100";
                assertTrue(theErrorMessage4, ((theResult <= MAXRESULT)&&(theResult>=MINRESULT)));
        }


    }
}

And that's it. It's not meant to be perfect code. It has errors and could be refined.


a pen

James Andrew Smith is a Professional Engineer and Associate Professor in the Electrical Engineering and Computer Science Department of York University's Lassonde School, with degrees in Electrical and Mechanical Engineering from the University of Alberta and McGill University.  Previously a program director in biomedical engineering, his research background spans robotics, locomotion, human birth and engineering education. While on sabbatical in 2018-19 with his wife and kids he lived in Strasbourg, France and he taught at the INSA Strasbourg and Hochschule Karlsruhe and wrote about his personal and professional perspectives.  James is a proponent of using social media to advocate for justice, equity, diversity and inclusion as well as evidence-based applications of research in the public sphere. You can find him on Twitter. Originally from Québec City, he now lives in Toronto, Canada.