Skip to content

Week 11 - Embedded Networking and Communications

Group Assignment:

  • Send a message between two projects.
  • Document your work to the group work page and reflect on your individual page what you learned

Please refer to the group page to read about our group assignment.

Individual Assignment:

  • Design, build and connect wired or wireless node(s) with network or bus addresses and a local input and/or output devices.

Learnings from this week's group assignment

This week I learned, that setting up wireless communication can be quite difficult and debugging is more challenging then for wired communication. I also realized, that I don't fully understand how the http request works and therefore wasn't able to get everything up and running in time. This topic needs definitly more attention in the future, but first of all I'm more aware of the challenges I can expect when attempting wireless communication.

Approach for this week

This week the goal for me is to communicate with a microcontroller using the SDI-12 protocol.

SDI-12

A Serial-Digital Interface Standard for microprocessor-based Sensors.

Asynchronous, ASCII, 1200 baud, 7 data bits, parity even.

In week 6 I designed a board with the SAMD11C14 microprocesser and a interface for SDI-12 to UART and the plan was to use that. Later I discovered, that there is also an Arduino SDI-12 library, that seems do work directly with a GPIO.

Finishing the interface

First I finished soldering the components that were missing in week 8 and had arrived by now on the PCB. I also made a fixture for the PCB with the M12-connector.

Development Board

Programming a client

Because I realized, that I wasn't sure how to use the interface I decided to test the library first.

I opened the slave example and tried to compile the code for the SAMD11C, but it failed to do so. I got a lot of error messages similar to this:

SDI12_boards.cpp:301:5: note: suggested alternative: 'GCLK_CLKCTRL_ID_TC1_TC2'
     GCLK_CLKCTRL_ID_TCC2_TC3;  // Select the peripheral multiplexer for TCC2 and TC3 to
     ^~~~~~~~~~~~~~~~~~~~~~~~
     GCLK_CLKCTRL_ID_TC1_TC2
exit status 1
Error compiling for board Generic D11C14A.

I figured out, that the library might not support this processor type, so my next idea was to try the XIAO SAMD21 board I had from week 9.

This time the compiling finished without errors and I wrote the program to the board.

Then I connected the D1 pin of the board to the C1 pin of a CR310 datalogger (master device).

Note

Because both devices are connected to the same power supply, I didn't need to connect the ground line, as they both have common ground.

SDI-12 client setup

Next I connected to the CR310 datalogger and opened the terminal to communicate on the SDI-12 bus.

Here is the screenshot of the terminal in the datalogger showing the commands and response from the "sensor".

SDI-12 terminal

First, I ask the sensor with bus address 0 to identify.

Second, I send a measurement command.

Third, I request the nine different readings (that are hardcoded in the example).

==================

After this success, I decided to send real measurments from an I2C sensor, so I connected the GY-521 breakout board I had used in week 9.

There are some lines in the code that need to be adopted to match the number of digits and number of values I'm using.

Here is the code for the gyro-client:

/**
 * @example{lineno} h_SDI-12_slave_implementation.ino
 * @copyright Stroud Water Research Center
 * @license This example is published under the BSD-3 license.
 * @date 2016
 * @author D. Wasielewski
 *
 * @brief Example H:  Using SDI-12 in Slave Mode
 *
 * Example sketch demonstrating how to implement an arduino as a slave on an SDI-12 bus.
 * This may be used, for example, as a middleman between an I2C sensor and an SDI-12
 * data logger.
 *
 * Note that an SDI-12 slave must respond to M! or C! with the number of values it will
 * report and the max time until these values will be available.  This example uses 9
 * values available in 21 s, but references to these numbers and the output array size
 * and datatype should be changed for your specific application.
 *
 * D. Wasielewski, 2016
 * Builds upon work started by:
 * https://github.com/jrzondagh/AgriApps-SDI-12-Arduino-Sensor
 * https://github.com/Jorge-Mendes/Agro-Shield/tree/master/SDI-12ArduinoSensor
 *
 * Suggested improvements:
 *  - Get away from memory-hungry arduino String objects in favor of char buffers
 *  - Make an int variable for the "number of values to report" instead of the
 *    hard-coded 9s interspersed throughout the code
 */

#include <SDI12.h>

#ifndef SDI12_DATA_PIN
#define SDI12_DATA_PIN D1
#endif
#ifndef SDI12_POWER_PIN
#define SDI12_POWER_PIN 22
#endif

int8_t dataPin       = SDI12_DATA_PIN;  /*!< The pin of the SDI-12 data bus */
int8_t powerPin      = SDI12_POWER_PIN; /*!< The sensor power pin (or -1) */
char   sensorAddress = '0'; /*!< The address of the SDI-12 sensor */
int    state         = 0;

#define WAIT 0
#define INITIATE_CONCURRENT 1
#define INITIATE_MEASUREMENT 2
#define PROCESS_COMMAND 3

// Create object by which to communicate with the SDI-12 bus on SDIPIN
SDI12 slaveSDI12(dataPin);

#include<Wire.h>
const int MPU=0x68; 
int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ;

void pollSensor(float* measurementValues) {
  measurementValues[0] = AcX;
  measurementValues[1] = AcY;
  measurementValues[2] = AcZ;
  measurementValues[3] = GyX;
  measurementValues[4] = GyY;
  measurementValues[5] = GyZ;
}

void parseSdi12Cmd(String command, String* dValues) {
  /* Ingests a command from an SDI-12 master, sends the applicable response, and
   * (when applicable) sets a flag to initiate a measurement
   */

  // First char of command is always either (a) the address of the device being
  // probed OR (b) a '?' for address query.
  // Do nothing if this command is addressed to a different device
  if (command.charAt(0) != sensorAddress && command.charAt(0) != '?') { return; }

  // If execution reaches this point, the slave should respond with something in
  // the form:   <address><responseStr><Carriage Return><Line Feed>
  // The following if-switch-case block determines what to put into <responseStr>,
  // and the full response will be constructed afterward. For '?!' (address query)
  // or 'a!' (acknowledge active) commands, responseStr is blank so section is skipped
  String responseStr = "";
  if (command.length() > 1) {
    switch (command.charAt(1)) {
      case 'I':
        // Identify command
        // Slave should respond with ID message: 2-char SDI-12 version + 8-char
        // company name + 6-char sensor model + 3-char sensor version + 0-13 char S/N
        responseStr = "13FABACA250405.0001";  // Substitute proper ID String here
        break;
      case 'C':
        // Initiate concurrent measurement command
        // Slave should immediately respond with: "tttnn":
        //    3-digit (seconds until measurement is available) +
        //    2-digit (number of values that will be available)
        // Slave should also start a measurment and relinquish control of the data line
        responseStr =
          "02109";  // 9 values ready in 21 sec; Substitue sensor-specific values here
        // It is not preferred for the actual measurement to occur in this subfunction,
        // because doing to would hold the main program hostage until the measurement
        // is complete.  Instead, we'll just set a flag and handle the measurement
        // elsewhere.
        state = INITIATE_CONCURRENT;
        break;
        // NOTE: "aC1...9!" commands may be added by duplicating this case and adding
        //       additional states to the state flag
      case 'M':
        // Initiate measurement command
        // Slave should immediately respond with: "tttnn":
        //    3-digit (seconds until measurement is available) +
        //    1-digit (number of values that will be available)
        // Slave should also start a measurment but may keep control of the data line
        // until advertised time elapsed OR measurement is complete and service request
        // sent
        responseStr =
          "0146";  // 9 values ready in 21 sec; Substitue sensor-specific values here
        // It is not preferred for the actual measurement to occur in this subfunction,
        // because doing to would hold the main program hostage until the measurement is
        // complete.  Instead, we'll just set a flag and handle the measurement
        // elsewhere. It is preferred though not required that the slave send a service
        // request upon completion of the measurement.  This should be handled in the
        // main loop().
        state = INITIATE_MEASUREMENT;
        break;
        // NOTE: "aM1...9!" commands may be added by duplicating this case and adding
        //       additional states to the state flag

      case 'D':
        // Send data command
        // Slave should respond with a String of values
        // Values to be returned must be split into Strings of 35 characters or fewer
        // (75 or fewer for concurrent).  The number following "D" in the SDI-12 command
        // specifies which String to send
        responseStr = dValues[(int)command.charAt(2) - 48];
        break;
      case 'A':
        // Change address command
        // Slave should respond with blank message (just the [new] address + <CR> +
        // <LF>)
        sensorAddress = command.charAt(2);
        break;
      default:
        // Mostly for debugging; send back UNKN if unexpected command received
        responseStr = "UNKN";
        break;
    }
  }

  // Issue the response specified in the switch-case structure above.
  String fullResponse = String(sensorAddress) + responseStr + "\r\n";
  slaveSDI12.sendResponse(fullResponse);
}

void formatOutputSDI(float* measurementValues, String* dValues, unsigned int maxChar) {
  /* Ingests an array of floats and produces Strings in SDI-12 output format */

  dValues[0] = "";
  int j      = 0;

  // upper limit on i should be number of elements in measurementValues
  for (int i = 0; i < 6; i++) {
    // Read float value "i" as a String with 0 deceimal digits
    // (NOTE: SDI-12 specifies max of 7 digits per value; we can only use 6
    //  decimal place precision if integer part is one digit)
    String valStr = String(measurementValues[i], 0);
    // Explictly add implied + sign if non-negative
    if (valStr.charAt(0) != '-') { valStr = '+' + valStr; }
    // Append dValues[j] if it will not exceed 35 (aM!) or 75 (aC!) characters
    if (dValues[j].length() + valStr.length() < maxChar) {
      dValues[j] += valStr;
    }
    // Start a new dValues "line" if appending would exceed 35/75 characters
    else {
      dValues[++j] = valStr;
    }
  }

  // Fill rest of dValues with blank strings
  while (j < 9) { dValues[++j] = ""; }
}

void setup() {
  Wire.begin();
  Wire.beginTransmission(MPU);
  Wire.write(0x6B);  
  Wire.write(0);    
  Wire.endTransmission(true);

  slaveSDI12.begin();
  delay(500);
  slaveSDI12.forceListen();  // sets SDIPIN as input to prepare for incoming message
}

void loop() {


  Wire.beginTransmission(MPU);
  Wire.write(0x3B);  
  Wire.endTransmission(false);
  Wire.requestFrom(MPU,12,true);  
  AcX=Wire.read()<<8|Wire.read();    
  AcY=Wire.read()<<8|Wire.read();  
  AcZ=Wire.read()<<8|Wire.read();  
  GyX=Wire.read()<<8|Wire.read();  
  GyY=Wire.read()<<8|Wire.read();  
  GyZ=Wire.read()<<8|Wire.read();  


  static float measurementValues[9];  // 9 floats to hold simulated sensor data
  static String
    dValues[10];  // 10 String objects to hold the responses to aD0!-aD9! commands
  static String commandReceived = "";  // String object to hold the incoming command


  // If a byte is available, an SDI message is queued up. Read in the entire message
  // before proceding.  It may be more robust to add a single character per loop()
  // iteration to a static char buffer; however, the SDI-12 spec requires a precise
  // response time, and this method is invariant to the remaining loop() contents.
  int avail = slaveSDI12.available();
  if (avail < 0) {
    slaveSDI12.clearBuffer();
  }  // Buffer is full; clear
  else if (avail > 0) {
    for (int a = 0; a < avail; a++) {
      char charReceived = slaveSDI12.read();
      // Character '!' indicates the end of an SDI-12 command; if the current
      // character is '!', stop listening and respond to the command
      if (charReceived == '!') {
        state = PROCESS_COMMAND;
        // Command string is completed; do something with it
        parseSdi12Cmd(commandReceived, dValues);
        // Clear command string to reset for next command
        commandReceived = "";
        // '!' should be the last available character anyway, but exit the "for" loop
        // just in case there are any stray characters
        slaveSDI12.clearBuffer();
        // eliminate the chance of getting anything else after the '!'
        slaveSDI12.forceHold();
        break;
      }
      // If the current character is anything but '!', it is part of the command
      // string.  Append the commandReceived String object.
      else {
        // Append command string with new character
        commandReceived += String(charReceived);
      }
    }
  }

  // For aM! and aC! commands, parseSdi12Cmd will modify "state" to indicate that
  // a measurement should be taken
  switch (state) {
    case WAIT:
      {
        break;
      }
    case INITIATE_CONCURRENT:
      {
        // Do whatever the sensor is supposed to do here
        // For this example, we will just create arbitrary "simulated" sensor data
        // NOTE: Your application might have a different data type (e.g. int) and
        //       number of values to report!
        pollSensor(measurementValues);
        // Populate the "dValues" String array with the values in SDI-12 format
        formatOutputSDI(measurementValues, dValues, 75);
        state = WAIT;
        slaveSDI12.forceListen();  // sets SDI-12 pin as input to prepare for incoming
                                   // message AGAIN
        break;
      }
    case INITIATE_MEASUREMENT:
      {
        // Do whatever the sensor is supposed to do here
        // For this example, we will just create arbitrary "simulated" sensor data
        // NOTE: Your application might have a different data type (e.g. int) and
        //       number of values to report!
        pollSensor(measurementValues);
        // Populate the "dValues" String array with the values in SDI-12 format
        formatOutputSDI(measurementValues, dValues, 35);
        // For aM!, Send "service request" (<address><CR><LF>) when data is ready
        String fullResponse = String(sensorAddress) + "\r\n";
        slaveSDI12.sendResponse(fullResponse);
        state = WAIT;
        slaveSDI12.forceListen();  // sets SDI-12 pin as input to prepare for incoming
                                   // message AGAIN
        break;
      }
    case PROCESS_COMMAND:
      {
        state = WAIT;
        slaveSDI12.forceListen();
        break;
      }
  }
}

This video shows the six sensor values in the datalogger being updated every one second.

Programming a host

Next step is to accomplish the opposite and create a host, that can talk to an industrial sensor via the SDI-12 bus.

I'm going to use the XIAO SAMD21 development board again and connect the OLED screen to display the readings.

The sensor I'm using is a humidity and temperature sensor.

I started with the simple logger example.

Bug

Suddenly, while uploading a new version of the program, something went wrong and the XIAO stopped responding. I tried resetting and searched on the internet for a solution. I had to realized, that the board most likely went into a strange state where the bootloader got corrupted. The XIAO is no longer recognized as a device, when connecting to the computer and it seems completly stuck. Because several hours passed away, with no easy way to solve this issue in sight, I decided to get another XIAO board from the lab, instead of spending too much time on resolving this issue.

Because of the above mentioned trouble with the board, I hooked up an improvised setup to continue with the assignment.

I used the basic data request example from the library and adopted it to my needs.

The most complicated part is to split the string that is read by the 0D0! command. The delimiters are either + or - and don't have the same location in the string.

Example: 0+20.0+48.9+8.9+8.4

For that I needed to make some new code to extract the right values and if they are positive or negative.

Here is the final code I came up with:

/**
 * @example{lineno} f_basic_data_request.ino
 * @copyright Stroud Water Research Center
 * @license This example is published under the BSD-3 license.
 * @author Ruben Kertesz <github@emnet.net> or \@rinnamon on twitter
 * @date 2/10/2016
 *
 * @brief Example F: Basic Data Request to a Single Sensor
 *
 * This is a very basic (stripped down) example where the user initiates a measurement
 * and receives the results to a terminal window without typing numerous commands into
 * the terminal.
 *
 * Edited by Ruben Kertesz for ISCO Nile 502 2/10/2016
 */

#include <SDI12.h>

#ifndef SDI12_DATA_PIN
#define SDI12_DATA_PIN D10
#endif
#ifndef SDI12_POWER_PIN
#define SDI12_POWER_PIN -1
#endif

/* connection information */
uint32_t serialBaud    = 115200; /*!< The baud rate for the output serial port */
int8_t   dataPin       = SDI12_DATA_PIN;  /*!< The pin of the SDI-12 data bus */
int8_t   powerPin      = SDI12_POWER_PIN; /*!< The sensor power pin (or -1) */
char     sensorAddress = '0'; /*!< The address of the SDI-12 sensor */

/** Define the SDI-12 bus */
SDI12 mySDI12(dataPin);

String sdiResponse = "";
String myCommand   = "";

float sdimeas[4];
int ind[5];

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0xBC ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {
  Serial.begin(serialBaud);
  while (!Serial && millis() < 10000L);

  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  display.display();
  delay(2000); // Pause for 2 seconds

  // Clear the buffer
  display.clearDisplay();

  Serial.println("Opening SDI-12 bus...");
  mySDI12.begin();
  delay(500);  // allow things to settle

  // Power the sensors;
  if (powerPin >= 0) {
    Serial.println("Powering up sensors...");
    pinMode(powerPin, OUTPUT);
    digitalWrite(powerPin, HIGH);
    delay(200);
  }
}


void loop() {

  // first command to take a measurement
  myCommand = String(sensorAddress) + "M!";
  //Serial.println(myCommand);  // echo command to terminal

  mySDI12.sendCommand(myCommand);
  delay(30);  // wait a while for a response

  while (mySDI12.available()) {  // build response string
    char c = mySDI12.read();
    if ((c != '\n') && (c != '\r')) {
      sdiResponse += c;
      delay(10);  // 1 character ~ 7.5ms
    }
  }
  if (sdiResponse.length() > 1)
  //  Serial.println(sdiResponse);  // write the response to the screen
  mySDI12.clearBuffer();


  delay(1000);       // delay between taking reading and requesting data
  sdiResponse = "";  // clear the response string


  // next command to request data from last measurement
  myCommand = String(sensorAddress) + "D0!";
  //Serial.println(myCommand);  // echo command to terminal

  mySDI12.sendCommand(myCommand);
  delay(30);  // wait a while for a response

  while (mySDI12.available()) {  // build string from response
    char c = mySDI12.read();
    if ((c != '\n') && (c != '\r')) {
      sdiResponse += c;
      delay(10);  // 1 character ~ 7.5ms
    }
  }
  if (sdiResponse.length() > 1)
    Serial.println(sdiResponse);  // write the response to the screen

    // check for + or - character in string and write to index array
    for(int i = 1; i < 5; i++){
      int a = sdiResponse.indexOf('+', ind[i-1]+1);
      int b = sdiResponse.indexOf('-', ind[i-1]+1);

      // check if the next delimiter is + or -
      if(a > 0 && b > 0){
        if(a < b)
          ind[i] = a;
        else
          ind[i] = b;
          sdimeas[i-1] = -1;
      }else if(a != -1){
        ind[i] = a;
      }else{
        ind[i] = b;
        sdimeas[i-1] = -1;
      }
    }

    //split string by index array and write to sdimeas array

    for(int i = 1; i < 5; i++){
      if(ind[i+1] > ind[i]){
        String temp = sdiResponse.substring(ind[i],ind[i+1]);         
        if(sdimeas[i-1] != 0){
          sdimeas[i-1] = sdimeas[i-1] * temp.toFloat();
        }else{
          sdimeas[i-1] = temp.toFloat();
        }
      }
    }

    Serial.print(sdimeas[0]);
    Serial.print(" ");
    Serial.print(sdimeas[1]);
    Serial.print(" ");
    Serial.print(sdimeas[2]);
    Serial.print(" ");
    Serial.println(sdimeas[3]);

    mySDI12.clearBuffer();
    sdiResponse = "";  // clear the response string


    display.clearDisplay();
    display.setTextSize(2);                     // Normal 1:1 pixel scale
    display.setTextColor(SSD1306_WHITE);        // Draw white text

    // Display Temp
    display.setCursor(0,10);                    // Start at top-left corner
    display.print(" T: ");
    display.print(sdimeas[0],1);
    display.print((char)247);
    display.println("C");


    // Display RH
    display.setCursor(0,40);                    // Start at top-left corner
    display.print("RH: ");
    display.print((int)(sdimeas[1]+0.5));
    display.println("%");

    display.display();

    // empty arrays
    for(int i = 1; i < 5; i++){
      sdimeas[i-1] = 0;
      ind[i] = 0;
    }

}

On the picture below you can see the result, where the XIAO SAMD21 acts as a host for the connected SDI-12 sensor and displays the reading for temperature and relative humidity on the OLED screen with a 1 Hz refresh rate.

SDI-12 Host