SDI-12 for Arduino > Examples > h_SDI-12_slave_implementation.ino

h_SDI-12_slave_implementation.ino example

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 datalogger.

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.

PlatformIO Configuration

; PlatformIO Project Configuration File

[platformio]
description = SDI-12 Library Example H:  Using Arduino as SDI-12 Slave
src_dir = .piolibdeps/Arduino-SDI-12_ID1486/examples/h_SDI-12_slave_implementation

[env:mayfly]
monitor_speed = 115200
board = mayfly
platform = atmelavr
framework = arduino
lib_ldf_mode = deep+
lib_ignore = RTCZero
lib_deps =
    SDI-12

The Complete Example

/**
 * @file h_SDI-12_slave_implementation.ino
 * @copyright (c) 2013-2020 Stroud Water Research Center (SWRC)
 *                          and the EnviroDIY Development Team
 *            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
 * datalogger.
 *
 * 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>

#define DATA_PIN 7   /*!< The pin of the SDI-12 data bus */
#define POWER_PIN 22 /*!< The sensor power pin (or -1 if not switching power) */

char sensorAddress = '5';
int  state         = 0;

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

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

void pollSensor(float* measurementValues) {
  measurementValues[0] = 1.111111;
  measurementValues[1] = -2.222222;
  measurementValues[2] = 3.333333;
  measurementValues[3] = -4.444444;
  measurementValues[4] = 5.555555;
  measurementValues[5] = -6.666666;
  measurementValues[6] = 7.777777;
  measurementValues[7] = -8.888888;
  measurementValues[8] = -9.999999;
}

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 = "13COMPNAME0000011.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 =
          "0219";  // 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 speficied in the switch-case structure above.
  slaveSDI12.sendResponse(String(sensorAddress) + responseStr + "\r\n");
}

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 < 9; i++) {
    // Read float value "i" as a String with 6 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], 6);
    // 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() {
  slaveSDI12.begin();
  delay(500);
  slaveSDI12.forceListen();  // sets SDIPIN as input to prepare for incoming message
}

void loop() {
  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 == '!') {
        // 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
      slaveSDI12.sendResponse(String(sensorAddress) + "\r\n");
      state = WAIT;
      slaveSDI12.forceListen();  // sets SDI-12 pin as input to prepare for incoming
                                 // message AGAIN
      break;
  }
}