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:
- for student version using Arduinos: Firmata4j, JSSC and SLF4J-JCL.
- for the professor version: TinkerForge.
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.
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:
Here is the source code for this project, using the Tinkerforge hardware. Note the structure of the project:
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.
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.