A challenge that new engineering students have when developing programs is trying to determine how to plan out their program. Should the student include a loop? Two loops? One after the other? One inside another? When adding sensors and actuators -- as we do in our course -- when should the sensor be read and when should the actuator be turned on or off?
The State Machine, while perhaps a weird or intimidating term for new Engineering students, is a long-standing component of Electrical Engineering programs, but, more recently, is also often taught to Software Engineering and CS students in the context of Design Patterns. Regardless of where more advanced students see them, State Machines provide a practical framework for guiding students in programming projects like the ones we see in early-year programming courses in Engineering.
The most common example of a state machine in the real world is the humble traffic light. It switches from red to green to yellow and back to red, typically based on a clock's signal. Over and over, it just works. Looping, changing, looping, changing.
First comes the Red "state". The clock counts down. Then comes the Green "state". The clock counts down. Then comes the Yellow "state". The clock counts down. And then it starts over in the Red "state". That's it. That's the machine. It's a machine -- a traffic light -- with three states. It senses time and it actuates its three lights. It's a simple State Machine.
In EECS 1011 and 1021 at York University, we extend this concept to the courses' Main Projects. Since 2020 we've been using automated plant watering as the objective for the project and this application lends itself very well to a State Machine implementation.
This blog post is intended to provide guidance to my students as they work on their projects. Knowing that sample code can be helpful, I've decided to include an example here. But I've added a twist: while it's written in MATLAB, it uses a different set of hardware than the students use. It still waters a plant, but it does it using Tinkerforge parts rather than Arduino-compatible parts. That means that both the hardware and the underlying program library are different. So students who wish to follow along will have to adapt this example to suit their own setup.
Above, in the video screenshot you can see a simplified visualization shows how the state machine works. Below is a more detailed version:
The following is an example of a program that implements a procedural state machine for plant watering using TinkerForge electronics components and the MATLAB programming language. It is provided here to give EECS 1011 students a model to examine. It will not solve the plant watering project problem for Arduino-based hardware in MATLAB because they use different underlying support software libraries. You may use this as inspiration and general guidance for how one might attempt to create a state machine with Matlab and Arduino hardware. There may be mistakes in this code. It is not meant to run "out of the box". It is meant simply to provide guidance.
% Tinkerforge Plant Watering Program.
%
% tinkerforgePlantWaterV1.m
%
% Tinkerforge + Matlab instructions:
% https://www.tinkerforge.com/en/doc/Software/API_Bindings_MATLAB.html#api-bindings-matlab
% Java class path: https://www.mathworks.com/help/matlab/matlab_external/java-class-path.html
% cd(prefdir)
% edit javaclasspath.txt
% put this in the new file: /Users/drsmith/Library/Application Support/MathWorks/MATLAB/R2023a/Tinkerforge.jar
% (https://www.mathworks.com/help/matlab/matlab_external/static-path-of-java-class-path.html)
%
% 1. Tinkerforge JAR is copied into /Users/drsmith/Documents/MATLAB/Tinkerforge.jar
% b. followed instructions at https://www.mathworks.com/help/matlab/matlab_external/static-path-of-java-class-path.html
% c. cd(prefdir)
% d. edit javaclasspath.txt
% e. added /Users/drsmith/Documents/MATLAB/Tinkerforge.jar in the
% editor
% f. saved.
% g. restarted matlab
% h. type javaclasspath to ensure that it's listed (yes)
% 2. Example with Poti: https://www.tinkerforge.com/en/doc/Software/Bricklets/RotaryPoti_Bricklet_MATLAB.html#rotary-poti-bricklet-matlab-examples
%
%
% James Smith; June 2023.
%
% ----------------------------------------------
function matlab_example_simple()
import com.tinkerforge.IPConnection; % Library for main brick
import com.tinkerforge.BrickletRotaryPoti; % Lib for rotary potentiometer
import com.tinkerforge.BrickletSegmentDisplay4x7; % Lib. for 7 Seg Display
import com.tinkerforge.BrickletDualRelay; % Lib. for dual relay.
import com.tinkerforge.BrickletMoisture; % Lib. for moisture
% Constants
MAXTIME = 60; % Seconds
SLEEPTIME = 1000;
% MOISTURE values. 0 is dry. 25 is skin. 1400 is wet paper towel.
MOISTURE_DRY = 0; % Driest value possible.
MOISTURE_WET = 1400; % Wettest value measured. (wet paper towel)
MOISTURE_THRESHOLD = MOISTURE_WET / 3; % can hold the moisture sensor to get a response at 25.
MOISTURE_HAND = 20;
POTI_THRESHOLD = 0; % values between -150 and +150
%Tinkerforge-related constants
HOST = "localhost";
PORT = 4223;
MAIN_UID = "5W5fXS"; % Main Brick
POTI_UID = "gby"; % Potentiometer
SEVENSEGMENT_UID = "pS1"; % 7SegmentDisplay
MOISTURE_UID = "s21"; % Moisture sensor
RELAY_UID = "rBa"; % Dual relay
% Seven Segment : Display digits 0-9 and A-F.
DIGITS = [hex2dec('3f') hex2dec('06') hex2dec('5b') ...
hex2dec('4f') hex2dec('66') hex2dec('6d') ...
hex2dec('7d') hex2dec('07') hex2dec('7f') ...
hex2dec('6f') hex2dec('77') hex2dec('7c') ...
hex2dec('39') hex2dec('5e') hex2dec('79') ...
hex2dec('71')]; % 0~9,A,b,C,d,E,F
% Seven Segment: Just the letters
INTERVALDIGITS = [hex2dec('77') hex2dec('7c') ...
hex2dec('39') hex2dec('5e') hex2dec('79') ...
hex2dec('71')]; % 0~9,A,b,C,d,E,F
BLANKDIGIT = hex2dec('00');
% STATEVALUES = %['InitialState', 'SoilDryState', 'SoilWetState', 'FinalState', 'ManualWaterState', 'ErrorState'];
STATEVALUES = ["State_One_Initial",...
"State_Two","State_Three",... % State A, parts i and ii
"State_Four","State_Five",... % State B, parts i and ii
"State_Six","State_Seven",... % State C, parts i and ii
"State_Eight_Final", "State_Nine_Error"];
% Track state using a procedural approach: an array
currentState = STATEVALUES(1);
previousState = currentState;
% Track state using OO & classes in Matlab. (optional)
% Define two variables to track states in the state machine
% Note that these values are defined in SoilMoistureMachineState.m
% currentState = SoilMoistureMachineState.InitialState;
% previousState = SoilMoistureMachineState.InitialState;
moistureValue = 0;
% --- Connect to Brick & bricklets -------
ipcon = IPConnection(); % Create IP connection
sd = handle(BrickletSegmentDisplay4x7(SEVENSEGMENT_UID, ipcon), 'CallbackProperties'); % Create device object
dr = handle(BrickletDualRelay(RELAY_UID, ipcon), 'CallbackProperties'); % Create device object
m = handle(BrickletMoisture(MOISTURE_UID, ipcon), 'CallbackProperties'); % Create device object
rp = handle(BrickletRotaryPoti(POTI_UID, ipcon), 'CallbackProperties'); % Create device object
% connect!
ipcon.connect(HOST, PORT); % Connect to brickd
% Don't use device before ipcon is connected
% ---- Finished initiating the connection ---------
% start measuring time.
tic; % start time at zero... now!
currentTime = toc; % record difference to tic ... now!
% Make sure relay is off
dr.setState(false, false);
% ============ start of main loop =================================
while(currentTime < MAXTIME)
% What is the state of the user button? (Don't have one right now)
buttonValue = 0; % default to off.
% What time interval are we in?
theInterval = whatTimeIntervalIsIt(0,toc);
%disp(theInterval); % display within MATLAB
if(theInterval=="IntervalA")
displayDigit = INTERVALDIGITS(1); % Interval A
elseif(theInterval=="IntervalB")
displayDigit = INTERVALDIGITS(2); % Interval B
elseif(theInterval=="IntervalC")
displayDigit = INTERVALDIGITS(3); % Interval C
else
displayDigit = INTERVALDIGITS(4); % Error condition.
end
% -----------------------------------------------------------------
% State One (Initial state)
% Stay here briefly and then transition to State 2
%
% We're in State Initial -- get ready. ---------------------------
if(currentState == STATEVALUES(1))
if(theInterval == "IntervalA") % State 1 -> State 2
previousState = currentState;
currentState = STATEVALUES(2);
else % Error...
previousState = currentState;
currentState = STATEVALUES(9); % go to State 9 (error)
end
% -----------------------------------------------------------------
% State Two (During Time Interval A, but do nothing)
%
% we're in State A(i) -- do nothing ---------------------------
elseif(currentState == STATEVALUES(2))
if(theInterval == "IntervalB") % State 2 -> State 4
previousState = currentState;
currentState = STATEVALUES(4);
elseif(buttonValue ~= 0) % State 2 -> State 3
previousState = currentState;
currentState = STATEVALUES(3); % Button starts watering
elseif(rp.getPosition() < POTI_THRESHOLD) % Pot turned CCW
previousState = currentState;
currentState = STATEVALUES(8); % go to exit state, State 8
else % Stay in State 2
previousState = currentState;
currentState = STATEVALUES(2);
end
% -----------------------------------------------------------------
% State Three (During Time Interval A, water the plant manually)
%
% we're in State A(ii) -- water plant ---------------------------
elseif(currentState == STATEVALUES(2))
if(theInterval == "IntervalB") % State 3 -> State 4
previousState = currentState;
currentState = STATEVALUES(4);
elseif(buttonValue ~= 0) % Stay in State 3
previousState = currentState;
currentState = STATEVALUES(3);
elseif(rp.getPosition() < POTI_THRESHOLD) % Pot turned CCW
previousState = currentState;
currentState = STATEVALUES(8); % go to exit state, State 8
else % Return to State 2
previousState = currentState;
currentState = STATEVALUES(2);
end
% -----------------------------------------------------------------
% State Four (During Time Interval B, do nothing)
%
% We're in State B(i) -- do nothing ---------------------------
elseif(currentState == STATEVALUES(4))
if(theInterval == "IntervalC") % State 4 -> State 6
previousState = currentState;
currentState = STATEVALUES(6);
elseif(buttonValue ~= 0) % State 4 -> State 5
previousState = currentState;
currentState = STATEVALUES(5); % Button starts watering
elseif(rp.getPosition() < POTI_THRESHOLD)
previousState = currentState;
currentState = STATEVALUES(8); % go to exit state, State 8
else % Stay in State 4
previousState = currentState;
currentState = STATEVALUES(4);
end
% -----------------------------------------------------------------
% State Five (During Time Interval B, but water plant manually)
%
% We're in State B(ii) -- water plant ---------------------------
elseif(currentState == STATEVALUES(5))
if(theInterval == "IntervalC") % State 5 -> State 6
previousState = currentState;
currentState = STATEVALUES(6);
elseif(buttonValue ~= 0) % Remain in State 5
previousState = currentState;
currentState = STATEVALUES(5); % Button starts watering
elseif(rp.getPosition() < POTI_THRESHOLD) % Pot turned CCW
previousState = currentState;
currentState = STATEVALUES(8); % go to exit state, State 8
else % Return to State 4
previousState = currentState;
currentState = STATEVALUES(4);
end
% -----------------------------------------------------------------
% State Six (During Time Interval C, do nothing)
%
% We're in State C(i) -- do nothing ---------------------------
elseif(currentState == STATEVALUES(6))
if(theInterval == "IntervalA") % State 6 -> State 2
previousState = currentState;
currentState = STATEVALUES(2);
elseif(m.getMoistureValue <= MOISTURE_THRESHOLD) % Go to State 7
previousState = currentState;
currentState = STATEVALUES(7); % Go to State 7 to water
elseif(rp.getPosition() < POTI_THRESHOLD) % Pot turned CCW
previousState = currentState;
currentState = STATEVALUES(8); % go to exit state, State 8
else % Stay in State 6
previousState = currentState;
currentState = STATEVALUES(6);
end
% -----------------------------------------------------------------
% State Seven (Time Interval C, water plant if too dry)
%
% We're in State C(ii) -- water plant ---------------------------
elseif(currentState == STATEVALUES(7))
if(theInterval == "IntervalA") % State 7 -> State 2
previousState = currentState;
currentState = STATEVALUES(2);
elseif(m.getMoistureValue <= MOISTURE_THRESHOLD) % Stay in State 7
previousState = currentState;
currentState = STATEVALUES(7); % Go to State 7 to water
elseif(rp.getPosition() < POTI_THRESHOLD) % Pot turned CCW
previousState = currentState;
currentState = STATEVALUES(8); % go to exit state, State 8
else % Return to State 6
previousState = currentState;
currentState = STATEVALUES(6);
end
elseif(currentState == STATEVALUES(8)) % CCW pot.
previousState = currentState;
currentState = STATEVALUES(8);
break;
else % Error state.
previousState = currentState;
currentState = STATEVALUES(9);
break;
end
% -----------------------------------------------------------------
% State Final (Time to exit because potentiometer value reached)
% position = rp.getPosition();
% We're in State Final -- wind down. ---------------------------
% -----------------------------------------------------------------
% State Error
%
% We're in Error State -- exit. ---------------------------
disp(["Interval: " theInterval "\t State: " currentState])
% ---------- Take action based on state -------------------------
% Write "4223" to the display with full brightness without colon.
% Adding 1 with the index because the array was designed for arrays
% that starts with index 0 but MATLAB arrays start with index 1.
segments = [BLANKDIGIT BLANKDIGIT BLANKDIGIT displayDigit];
sd.setSegments(segments, 7, false);
if((currentState == STATEVALUES(3)) || ...
(currentState == STATEVALUES(5)) || ...
(currentState == STATEVALUES(7)))
dr.setState(true, false); % left relay on.
else
dr.setState(false, false); % Both relays off.
end
% ---------------------------------------------------------------
% wait for one second
pause(1);
% query the time.
currentTime = toc;
end
% ========== end of main loop ====================================
% ========== wrap up. ============================================
%input('Press key to exit\n', 's');
% Turn off relays & show "off" on the display
dr.setState(false, false);
segments = [BLANKDIGIT DIGITS(1) DIGITS(16) DIGITS(16)];
sd.setSegments(segments, 7, false);
% disconnect from Tinkerforge board.
ipcon.disconnect();
end
The operation of this state machine can be viewed in this video:
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.