Lab: Two-Way (Duplex) Serial Communication Using An Arduino and P5.js
In this lab I combined a number of capabilities to create a pointing and selecting device (mouse) that uses two-way (duplex) communication.
Here is the circuit I used along with a Fritzing diagram:
Code with some smoothing to the readings:
#include <Arduino_LSM6DS3.h> // This library reads sensor values from the Intertial Measurement Unit (IMU).
  // digital input
  const int buttonPin = 2;
  // buffer to smooth readings
  int xBuffer[] = {0,0,0};
  int yBuffer[] = {0,0,0};
 
void setup() {
  // configure the serial connection:
  Serial.begin(9600);
  // configure the digital input:
  pinMode(buttonPin, INPUT);
  // Initialize the IMU sensor
  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (true); // halt program
  }
  Serial.println("IMU initialized!");
}
 
void loop() {
  // Local variables for orientation data from IMU
  float aX, aY, aZ;
  float gX, gY, gZ;
  const char * spacer = ", ";
  // Try to read the IMU sensor
  if (
    IMU.accelerationAvailable() 
    && IMU.gyroscopeAvailable()
  ) {      
    IMU.readAcceleration(aX, aY, aZ);
    IMU.readGyroscope(gX, gY, gZ);
    // adjust the sensor values to be closer to 10bit analog reads
    aX = (aX + 1)*512;
    aY = (aY + 1)*512;
    // do some smoothing to reads
    aX = (xBuffer[0] + xBuffer[1] + xBuffer[2] + aX)/4;
    aY = (yBuffer[0] + yBuffer[1] + yBuffer[2] + aY)/4;
    // save new value to buffer
    xBuffer[2] = xBuffer[1];
    xBuffer[1] = xBuffer[0];
    xBuffer[0] = aX;
    yBuffer[2] = yBuffer[1];
    yBuffer[1] = yBuffer[0];
    yBuffer[0] = aY;
    // print the X rotation:
    Serial.print(aX);
    Serial.print(",");
   
    // print the y rotation:
    Serial.print(aY);
    Serial.print(",");
   
    // read the button:
    int sensorValue = digitalRead(buttonPin);
    // print the results:
    Serial.println(sensorValue);
  }
}
Taking a look at the X and Y axis IMU readings in the serial plotter I noticed there was quite a lot of noise and decided to implement a buffer that averages out the last four readings. Example of the difference here:
Now to use this serial output to control a P5 sketch I shifted to the code running on my computer that the Arduino was connected to. You can see the P5 sketch here.
With this function working I next modified the code for the Arduino to only send data after a “handshake” with the computer. The benefit of this approach is that it will only send data when requested and reduces the chance of a buffer overflow.
Here is the modified Arduino code:
#include <Arduino_LSM6DS3.h> // This library reads sensor values from the Intertial Measurement Unit (IMU).
  // digital input
  const int buttonPin = 2;
  // buffer to smooth readings
  int xBuffer[] = {0,0,0};
  int yBuffer[] = {0,0,0};
 
void setup() {
  // configure the serial connection:
  Serial.begin(9600);
  // Initialize the IMU sensor
  if (!IMU.begin()) {
    //Serial.println("Failed to initialize IMU!");
    while (true); // halt program
  }
  //Serial.println("IMU initialized!");
  
  while (Serial.available() <= 0) {
    Serial.println("hello"); // send a starting message
    delay(300);              // wait 1/3 second
  }
  // configure the digital input:
  pinMode(buttonPin, INPUT);
  
}
 
void loop() {
    
  // Local variables for orientation data from IMU
  float aX, aY, aZ;
  float gX, gY, gZ;
  const char * spacer = ", ";
  // Try to read the IMU sensor
  if (
    IMU.accelerationAvailable() 
    && IMU.gyroscopeAvailable()
  ) {      
    IMU.readAcceleration(aX, aY, aZ);
    IMU.readGyroscope(gX, gY, gZ);
    // adjust the sensor values to be closer to 10bit analog reads
    aX = (aX + 1)*512;
    aY = (aY + 1)*512;
    // do some smoothing to reads
    aX = (xBuffer[0] + xBuffer[1] + xBuffer[2] + aX)/4;
    aY = (yBuffer[0] + yBuffer[1] + yBuffer[2] + aY)/4;
    // save new value to buffer
    xBuffer[2] = xBuffer[1];
    xBuffer[1] = xBuffer[0];
    xBuffer[0] = aX;
    yBuffer[2] = yBuffer[1];
    yBuffer[1] = yBuffer[0];
    yBuffer[0] = aY;
    if (Serial.available() > 0) {
      
      // print the X rotation:
      Serial.print(aX);
      Serial.print(",");
     
      // print the y rotation:
      Serial.print(aY);
      Serial.print(",");
     
      // read the button:
      int sensorValue = digitalRead(buttonPin);
      // print the results:
      Serial.println(sensorValue);
      
    }
  } 
}
I did run into an issue with this arrangement where the “handshake” would cause a stream of values rather than a single set:
Finally I took what I learned in this lab and tried it out with my own sketch, this time using my Dodeca enclosure and mapping the movement to a 3D object in a P5.js sketch. Arduino code below.
#include <Arduino_LSM6DS3.h> // This library reads sensor values from the Intertial Measurement Unit (IMU).
#include "MadgwickAHRS.h" // This library generates Euler angle measures from the IMU.
  // digital input
  const int buttonPin = 2;
  // Initialize a Madgwick filter:
  Madgwick filter;
  // Set sensor's sample rate to 104 Hz to improve accuracy of Euler angle interpretations by Madgwick library
  const float sensorRate = 104.00;
  // buffer to smooth readings
  int xBuffer[] = {0,0,0};
  int yBuffer[] = {0,0,0};
  // LED indicator info
  int ledPin = 17;
  int brightness = 1;
  int fadeAmount = 1;
 
void setup() {
  // configure the serial connection:
  Serial.begin(9600);
  // Initialize the IMU sensor
  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (true); // halt program
  }
  
  // Start the filter to run at the sample rate
  filter.begin(sensorRate);
  
  Serial.println("IMU initialized!");
  
  // configure the digital input:
  pinMode(buttonPin, INPUT);
  
}
 
void loop() {
  pulseLED();
    
  // Local variables for orientation data from IMU
  float aX, aY, aZ;
  float gX, gY, gZ;
  const char * spacer = ", ";
  // Local variables for orientation from Madgwick library
  float roll, pitch, heading;
  // Try to read the IMU sensor
  if (
    IMU.accelerationAvailable() 
    && IMU.gyroscopeAvailable()
  ) {      
    IMU.readAcceleration(aX, aY, aZ);
    IMU.readGyroscope(gX, gY, gZ);
    // Update the filter, which computes orientation
    filter.updateIMU(gX, gY, gZ, aX, aY, aZ);
    // Print the heading, pitch and roll
    roll = filter.getRoll() + 180; // 180 to -180 y leaves x unchanged
    pitch = filter.getPitch() + 90; // 90 to -90 x leaves y unchanged
    // adjust the sensor values to be closer to 10bit analog reads
    //aX = (aX + 1)*512;
    //aY = (aY + 1)*512;
    // do some smoothing to reads
    pitch = (xBuffer[0] + xBuffer[1] + xBuffer[2] + pitch)/4;
    roll = (yBuffer[0] + yBuffer[1] + yBuffer[2] + roll)/4;
    // save new value to buffer
    xBuffer[2] = xBuffer[1];
    xBuffer[1] = xBuffer[0];
    xBuffer[0] = pitch;
    yBuffer[2] = yBuffer[1];
    yBuffer[1] = yBuffer[0];
    yBuffer[0] = roll;
      
    // print the X rotation:
    Serial.print(pitch);
    Serial.print(",");
   
    // print the y rotation:
    Serial.print(roll);
    Serial.print(",");
   
    // read the button:
    int sensorValue = digitalRead(buttonPin);
    // print the results:
    Serial.println(sensorValue);
      
  } 
}
void pulseLED(){
  // Write brightness to LED
  analogWrite(ledPin, brightness);
  brightness = brightness + fadeAmount;
  // Reverse the direction of the fading at the ends of the fade
  if(brightness == 1 || brightness == 254)
  {
    fadeAmount = -fadeAmount ;
  }   
  //Serial.print("fading LED to ");
  //Serial.println(brightness);
  delay(2);
}
            
        






 
                                                                     
                                                                    