h_SDI-12_slave_implementation.ino example

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
1/**
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.
5 * @date 2016
6 * @author D. Wasielewski
7 *
8 * @brief Example H: Using SDI-12 in Slave Mode
9 *
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
12 * data logger.
13 *
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.
18 *
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
23 *
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
28 */
29
30#include <SDI12.h>
31
32#ifndef SDI12_DATA_PIN
33#define SDI12_DATA_PIN 7
34#endif
35#ifndef SDI12_POWER_PIN
36#define SDI12_POWER_PIN 22
37#endif
38
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 */
42int state = 0;
43
44#define WAIT 0
45#define INITIATE_CONCURRENT 1
46#define INITIATE_MEASUREMENT 2
47#define PROCESS_COMMAND 3
48
49// Create object by which to communicate with the SDI-12 bus on SDIPIN
50SDI12 slaveSDI12(dataPin);
51
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;
62}
63
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
67 */
68
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; }
73
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)) {
82 case 'I':
83 // Identify command
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
87 break;
88 case 'C':
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
94 responseStr =
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
99 // elsewhere.
100 state = INITIATE_CONCURRENT;
101 break;
102 // NOTE: "aC1...9!" commands may be added by duplicating this case and adding
103 // additional states to the state flag
104 case 'M':
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
111 // sent
112 responseStr =
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
119 // main loop().
120 state = INITIATE_MEASUREMENT;
121 break;
122 // NOTE: "aM1...9!" commands may be added by duplicating this case and adding
123 // additional states to the state flag
124
125 case 'D':
126 // Send data command
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];
132 break;
133 case 'A':
134 // Change address command
135 // Slave should respond with blank message (just the [new] address + <CR> +
136 // <LF>)
137 sensorAddress = command.charAt(2);
138 break;
139 default:
140 // Mostly for debugging; send back UNKN if unexpected command received
141 responseStr = "UNKN";
142 break;
143 }
144 }
145
146 // Issue the response specified in the switch-case structure above.
147 String fullResponse = String(sensorAddress) + responseStr + "\r\n";
148 slaveSDI12.sendResponse(fullResponse);
149}
150
151void formatOutputSDI(float* measurementValues, String* dValues, unsigned int maxChar) {
152 /* Ingests an array of floats and produces Strings in SDI-12 output format */
153
154 dValues[0] = "";
155 int j = 0;
156
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;
168 }
169 // Start a new dValues "line" if appending would exceed 35/75 characters
170 else {
171 dValues[++j] = valStr;
172 }
173 }
174
175 // Fill rest of dValues with blank strings
176 while (j < 9) { dValues[++j] = ""; }
177}
178
179void setup() {
180 slaveSDI12.begin();
181 delay(500);
182 slaveSDI12.forceListen(); // sets SDIPIN as input to prepare for incoming message
183}
184
185void loop() {
186 static float measurementValues[9]; // 9 floats to hold simulated sensor data
187 static String
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
190
191
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();
197 if (avail < 0) {
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();
216 break;
217 }
218 // If the current character is anything but '!', it is part of the command
219 // string. Append the commandReceived String object.
220 else {
221 // Append command string with new character
222 commandReceived += String(charReceived);
223 }
224 }
225 }
226
227 // For aM! and aC! commands, parseSdi12Cmd will modify "state" to indicate that
228 // a measurement should be taken
229 switch (state) {
230 case WAIT:
231 {
232 break;
233 }
234 case INITIATE_CONCURRENT:
235 {
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);
243 state = WAIT;
244 slaveSDI12.forceListen(); // sets SDI-12 pin as input to prepare for incoming
245 // message AGAIN
246 break;
247 }
248 case INITIATE_MEASUREMENT:
249 {
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);
260 state = WAIT;
261 slaveSDI12.forceListen(); // sets SDI-12 pin as input to prepare for incoming
262 // message AGAIN
263 break;
264 }
265 case PROCESS_COMMAND:
266 {
267 state = WAIT;
268 slaveSDI12.forceListen();
269 break;
270 }
271 }
272}