Example H: Using SDI-12 in Slave Mode.
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
2 * @example{lineno} h_SDI-12_slave_implementation.ino
3 * @copyright Stroud Water Research Center
4 * @license This example is published under the BSD-3 license.
6 * @author D. Wasielewski
8 * @brief Example H: Using SDI-12 in Slave Mode
10 * Example sketch demonstrating how to implement an arduino as a slave on an SDI-12 bus.
11 * This may be used, for example, as a middleman between an I2C sensor and an SDI-12
14 * Note that an SDI-12 slave must respond to M! or C! with the number of values it will
15 * report and the max time until these values will be available. This example uses 9
16 * values available in 21 s, but references to these numbers and the output array size
17 * and datatype should be changed for your specific application.
19 * D. Wasielewski, 2016
20 * Builds upon work started by:
21 * https://github.com/jrzondagh/AgriApps-SDI-12-Arduino-Sensor
22 * https://github.com/Jorge-Mendes/Agro-Shield/tree/master/SDI-12ArduinoSensor
24 * Suggested improvements:
25 * - Get away from memory-hungry arduino String objects in favor of char buffers
26 * - Make an int variable for the "number of values to report" instead of the
27 * hard-coded 9s interspersed throughout the code
33#define SDI12_DATA_PIN 7
35#ifndef SDI12_POWER_PIN
36#define SDI12_POWER_PIN 22
39int8_t dataPin = SDI12_DATA_PIN; /*!< The pin of the SDI-12 data bus */
40int8_t powerPin = SDI12_POWER_PIN; /*!< The sensor power pin (or -1) */
41char sensorAddress = '5'; /*!< The address of the SDI-12 sensor */
45#define INITIATE_CONCURRENT 1
46#define INITIATE_MEASUREMENT 2
47#define PROCESS_COMMAND 3
49// Create object by which to communicate with the SDI-12 bus on SDIPIN
50SDI12 slaveSDI12(dataPin);
52void pollSensor(float* measurementValues) {
53 measurementValues[0] = 1.111111;
54 measurementValues[1] = -2.222222;
55 measurementValues[2] = 3.333333;
56 measurementValues[3] = -4.444444;
57 measurementValues[4] = 5.555555;
58 measurementValues[5] = -6.666666;
59 measurementValues[6] = 7.777777;
60 measurementValues[7] = -8.888888;
61 measurementValues[8] = -9.999999;
64void parseSdi12Cmd(String command, String* dValues) {
65 /* Ingests a command from an SDI-12 master, sends the applicable response, and
66 * (when applicable) sets a flag to initiate a measurement
69 // First char of command is always either (a) the address of the device being
70 // probed OR (b) a '?' for address query.
71 // Do nothing if this command is addressed to a different device
72 if (command.charAt(0) != sensorAddress && command.charAt(0) != '?') { return; }
74 // If execution reaches this point, the slave should respond with something in
75 // the form: <address><responseStr><Carriage Return><Line Feed>
76 // The following if-switch-case block determines what to put into <responseStr>,
77 // and the full response will be constructed afterward. For '?!' (address query)
78 // or 'a!' (acknowledge active) commands, responseStr is blank so section is skipped
79 String responseStr = "";
80 if (command.length() > 1) {
81 switch (command.charAt(1)) {
84 // Slave should respond with ID message: 2-char SDI-12 version + 8-char
85 // company name + 6-char sensor model + 3-char sensor version + 0-13 char S/N
86 responseStr = "13COMPNAME0000011.0001"; // Substitute proper ID String here
89 // Initiate concurrent measurement command
90 // Slave should immediately respond with: "tttnn":
91 // 3-digit (seconds until measurement is available) +
92 // 2-digit (number of values that will be available)
93 // Slave should also start a measurment and relinquish control of the data line
95 "02109"; // 9 values ready in 21 sec; Substitue sensor-specific values here
96 // It is not preferred for the actual measurement to occur in this subfunction,
97 // because doing to would hold the main program hostage until the measurement
98 // is complete. Instead, we'll just set a flag and handle the measurement
100 state = INITIATE_CONCURRENT;
102 // NOTE: "aC1...9!" commands may be added by duplicating this case and adding
103 // additional states to the state flag
105 // Initiate measurement command
106 // Slave should immediately respond with: "tttnn":
107 // 3-digit (seconds until measurement is available) +
108 // 1-digit (number of values that will be available)
109 // Slave should also start a measurment but may keep control of the data line
110 // until advertised time elapsed OR measurement is complete and service request
113 "0219"; // 9 values ready in 21 sec; Substitue sensor-specific values here
114 // It is not preferred for the actual measurement to occur in this subfunction,
115 // because doing to would hold the main program hostage until the measurement is
116 // complete. Instead, we'll just set a flag and handle the measurement
117 // elsewhere. It is preferred though not required that the slave send a service
118 // request upon completion of the measurement. This should be handled in the
120 state = INITIATE_MEASUREMENT;
122 // NOTE: "aM1...9!" commands may be added by duplicating this case and adding
123 // additional states to the state flag
127 // Slave should respond with a String of values
128 // Values to be returned must be split into Strings of 35 characters or fewer
129 // (75 or fewer for concurrent). The number following "D" in the SDI-12 command
130 // specifies which String to send
131 responseStr = dValues[(int)command.charAt(2) - 48];
134 // Change address command
135 // Slave should respond with blank message (just the [new] address + <CR> +
137 sensorAddress = command.charAt(2);
140 // Mostly for debugging; send back UNKN if unexpected command received
141 responseStr = "UNKN";
146 // Issue the response specified in the switch-case structure above.
147 String fullResponse = String(sensorAddress) + responseStr + "\r\n";
148 slaveSDI12.sendResponse(fullResponse);
151void formatOutputSDI(float* measurementValues, String* dValues, unsigned int maxChar) {
152 /* Ingests an array of floats and produces Strings in SDI-12 output format */
157 // upper limit on i should be number of elements in measurementValues
158 for (int i = 0; i < 9; i++) {
159 // Read float value "i" as a String with 6 deceimal digits
160 // (NOTE: SDI-12 specifies max of 7 digits per value; we can only use 6
161 // decimal place precision if integer part is one digit)
162 String valStr = String(measurementValues[i], 6);
163 // Explictly add implied + sign if non-negative
164 if (valStr.charAt(0) != '-') { valStr = '+' + valStr; }
165 // Append dValues[j] if it will not exceed 35 (aM!) or 75 (aC!) characters
166 if (dValues[j].length() + valStr.length() < maxChar) {
167 dValues[j] += valStr;
169 // Start a new dValues "line" if appending would exceed 35/75 characters
171 dValues[++j] = valStr;
175 // Fill rest of dValues with blank strings
176 while (j < 9) { dValues[++j] = ""; }
182 slaveSDI12.forceListen(); // sets SDIPIN as input to prepare for incoming message
186 static float measurementValues[9]; // 9 floats to hold simulated sensor data
188 dValues[10]; // 10 String objects to hold the responses to aD0!-aD9! commands
189 static String commandReceived = ""; // String object to hold the incoming command
192 // If a byte is available, an SDI message is queued up. Read in the entire message
193 // before proceding. It may be more robust to add a single character per loop()
194 // iteration to a static char buffer; however, the SDI-12 spec requires a precise
195 // response time, and this method is invariant to the remaining loop() contents.
196 int avail = slaveSDI12.available();
198 slaveSDI12.clearBuffer();
199 } // Buffer is full; clear
200 else if (avail > 0) {
201 for (int a = 0; a < avail; a++) {
202 char charReceived = slaveSDI12.read();
203 // Character '!' indicates the end of an SDI-12 command; if the current
204 // character is '!', stop listening and respond to the command
205 if (charReceived == '!') {
206 state = PROCESS_COMMAND;
207 // Command string is completed; do something with it
208 parseSdi12Cmd(commandReceived, dValues);
209 // Clear command string to reset for next command
210 commandReceived = "";
211 // '!' should be the last available character anyway, but exit the "for" loop
212 // just in case there are any stray characters
213 slaveSDI12.clearBuffer();
214 // eliminate the chance of getting anything else after the '!'
215 slaveSDI12.forceHold();
218 // If the current character is anything but '!', it is part of the command
219 // string. Append the commandReceived String object.
221 // Append command string with new character
222 commandReceived += String(charReceived);
227 // For aM! and aC! commands, parseSdi12Cmd will modify "state" to indicate that
228 // a measurement should be taken
234 case INITIATE_CONCURRENT:
236 // Do whatever the sensor is supposed to do here
237 // For this example, we will just create arbitrary "simulated" sensor data
238 // NOTE: Your application might have a different data type (e.g. int) and
239 // number of values to report!
240 pollSensor(measurementValues);
241 // Populate the "dValues" String array with the values in SDI-12 format
242 formatOutputSDI(measurementValues, dValues, 75);
244 slaveSDI12.forceListen(); // sets SDI-12 pin as input to prepare for incoming
248 case INITIATE_MEASUREMENT:
250 // Do whatever the sensor is supposed to do here
251 // For this example, we will just create arbitrary "simulated" sensor data
252 // NOTE: Your application might have a different data type (e.g. int) and
253 // number of values to report!
254 pollSensor(measurementValues);
255 // Populate the "dValues" String array with the values in SDI-12 format
256 formatOutputSDI(measurementValues, dValues, 35);
257 // For aM!, Send "service request" (<address><CR><LF>) when data is ready
258 String fullResponse = String(sensorAddress) + "\r\n";
259 slaveSDI12.sendResponse(fullResponse);
261 slaveSDI12.forceListen(); // sets SDI-12 pin as input to prepare for incoming
265 case PROCESS_COMMAND:
268 slaveSDI12.forceListen();