#include <src/SDI12.h>
SDI12
class
The main class for SDI 12 instances.
Data Line States
Functions for maintaining the proper data line state.
The Arduino is responsible for managing communication with the sensors. Since all the data transfer happens on the same line, the state of the data line is very important.
Specifications
Per the SDI-12 specification, the voltage ranges for SDI-12 are:
- When the pin is in the SDI12_HOLDING state, it is holding the line LOW so that interference does not unintentionally wake the sensors up. The interrupt is disabled for the dataPin, because we are not expecting any SDI-12 traffic.
- In the SDI12_TRANSMITTING state, we would like exclusive control of the Arduino, so we shut off all interrupts, and vary the voltage of the dataPin in order to wake up and send commands to the sensor.
- In the SDI12_LISTENING state, we are waiting for a sensor to respond, so we drop the voltage level to LOW and relinquish control (INPUT).
- If we would like to disable all SDI-12 functionality, then we set the system to the SDI12_DISABLED state, removing the interrupt associated with the dataPin. For predictability, we set the pin to a LOW level high impedance state (INPUT).
As a Table
Summarized in a table:
State | Interrupts | Pin Mode | Pin Level |
---|---|---|---|
SDI12_DISABLED | Pin Disable | INPUT | — |
SDI12_ENABLED | Pin Disable | INPUT | — |
SDI12_HOLDING | Pin Disable | OUTPUT | LOW |
SDI12_TRANSMITTING | All/Pin Disable | OUTPUT | VARYING |
SDI12_LISTENING | All Enable | INPUT | — |
Sequencing
Generally, this flow of line states is acceptable:
HOLDING --> TRANSMITTING --> LISTENING --> TRANSMITTING --> LISTENING
If you have interference, you should force a hold, using forceHold(). The flow would then be:
HOLDING --> TRANSMITTING --> LISTENING -->
done reading, forceHold() ---> HOLDING
- enum SDI12_STATES { SDI12_DISABLED, SDI12_ENABLED, SDI12_HOLDING, SDI12_TRANSMITTING, SDI12_LISTENING }
- The various SDI-12 line states.
- using SDI12_STATES = enum SDI12::SDI12_STATES
- The various SDI-12 line states.
- void forceHold()
- Set line state to SDI12_HOLDING.
- void forceListen()
- Set line state to SDI12_LISTENING.
Static member variables
These are constants that apply to all SDI-12 instances.
- static SDI12* _activeObject
- static pointer to active SDI12 instance
- static SDI12Timer sdi12timer
- The SDI12Timer instance to use for checking bit reception times.
- static const uint16_t bitWidth_micros
- The size of a bit in microseconds.
- static const uint16_t lineBreak_micros
- The required "break" before sending commands, >= 12ms.
- static const uint16_t marking_micros
- The required mark before a command or response, >= 8.33ms.
-
static const sdi12timer_
t txBitWidth - the width of a single bit in "ticks" of the cpu clock.
- static const uint8_t WAITING_FOR_START_BIT
- A mask for the rxState while waiting for a start bit; 0b11111111.
-
static sdi12timer_
t prevBitTCNT - Stores the time of the previous RX transition in micros.
- static uint8_t rxState
- Tracks how many bits are accounted for on an incoming character.
- static uint8_t rxMask
- a bit mask for building a received character
- static uint8_t rxValue
- the value of the character being built
Buffer Setup
Creating a circular buffer for incoming data.
The buffer is used to store characters from the SDI-12 data line. Characters are read into the buffer when an interrupt is received on the data line. The buffer uses a circular implementation with pointers to both the head and the tail. All SDI-12 instances share the same buffer.
The default buffer size is the maximum length of a response to a normal SDI-12 command, which is 81 characters:
- address is a single (1) character
- values has a maximum value of 75 characters
- CRC is 3 characters
- CR is a single character
- LF is a single character
For more information on circular buffers: http://en.wikipedia.org/wiki/Circular_
- static uint8_t _rxBuffer
- A single incoming character buffer for ALL SDI-12 objects (Rx buffer)
- static volatile uint8_t _rxBufferTail
- Index of buffer head. (unsigned 8-bit integer, can map from 0-255)
- static volatile uint8_t _rxBufferHead
- Index of buffer tail. (unsigned 8-bit integer, can map from 0-255)
- bool _bufferOverflow
- The buffer overflow status.
Constructor, Destructor, Begins, and Setters
These functions set up the SDI-12 object and prepare it for use.
- int8_t _dataPin
- reference to the data pin
- int16_t TIMEOUT
- The value to return if a parse or read times out with no return from the sensor.
- bool _parityFailure
- reference to the data pin
- SDI12()
- Construct a new SDI12 instance with no data pin set.
- SDI12(int8_t dataPin) explicit
- Construct a new SDI12 with the data pin set.
- ~SDI12()
- Destroy the SDI12 object.
- void begin()
- Begin the SDI-12 object.
- void begin(int8_t dataPin)
- Set the SDI12::_datapin and begin the SDI-12 object.
- void end()
- Disable the SDI-12 object (but do not destroy it).
- void setTimeoutValue(int16_t value)
- Set the value to return if a parse int or parse float times out with no return from the sensor.
- int8_t getDataPin() -> int8_t
- Get the data pin for the current SDI-12 instance.
- void setDataPin(int8_t dataPin)
- Set the data pin for the current SDI-12 instance.
Reading from the SDI-12 Buffer
These functions are for reading incoming data stored in the SDI-12 buffer.
- int available() -> int override
- Return the number of bytes available in the Rx buffer.
- int peek() -> int override
- Reveal next byte in the Rx buffer without consuming it.
- void clearBuffer()
- Clear the Rx buffer by setting the head and tail pointers to the same value.
- int read() -> int override
- Return next byte in the Rx buffer, consuming it.
- void flush() override
- Wait for sending to finish - because no TX buffering, does nothing.
-
long parseInt(LookaheadMode lookahead = SKIP_
ALL, char ignore = '\x01') -> long - Return the first valid (long) integer value from the current position.
-
float parseFloat(LookaheadMode lookahead = SKIP_
ALL, char ignore = '\x01') -> float - Return the first valid float value from the current position.
- int peekNextDigit(LookaheadMode lookahead, bool detectDecimal) -> int protected
- Return the next numeric digit in the stream or -1 if timeout.
Using more than one SDI-12 Object
Functions needed for multiple instances of the SDI12 class.
This library is allows for multiple instances of itself running on the same or different pins. SDI-12 can support up to 62 sensors on a single pin/bus, so it is not necessary to use an instance for each sensor.
Because we are using pin change interrupts there can only be one active object at a time (since this is the only reliable way to determine which pin the interrupt occurred on). The active object is the only object that will respond properly to interrupts. However promoting another instance to Active status does not automatically remove the interrupts on the other pin. For proper behavior it is recommended to use this pattern:
Waking Up and Talking To Sensors
These functions are needed to communicate with SDI-12 sensors (slaves) or an SDI-12 datalogger (master).
- size_t write(uint8_t byte) -> size_t virtual
- Write out a byte on the SDI-12 line.
- void sendCommand(String& cmd, int8_t extraWakeTime = 0)
- Send a command out on the data line, acting as a datalogger (master)
- void sendCommand(const char* cmd, int8_t extraWakeTime = 0)
- Send a command out on the data line, acting as a datalogger (master)
- void sendCommand(FlashString cmd, int8_t extraWakeTime = 0)
- Send a command out on the data line, acting as a datalogger (master)
- uint16_t calculateCRC(String& resp) -> uint16_t
- Calculates the 16-bit Cyclic Redundancy Check (CRC) for an SDI-12 message.
- uint16_t calculateCRC(const char* resp) -> uint16_t
- Calculates the 16-bit Cyclic Redundancy Check (CRC) for an SDI-12 message.
- uint16_t calculateCRC(FlashString resp) -> uint16_t
- Calculates the 16-bit Cyclic Redundancy Check (CRC) for an SDI-12 message.
- String crcToString(uint16_t crc) -> String
- Converts a numeric 16-bit CRC to an ASCII String.
- bool verifyCRC(String& respWithCRC) -> bool
- Verifies that the CRC returned at the end of an SDI-12 message matches that of the content of the message.
- void sendResponse(String& resp, bool addCRC = false)
- Send a response out on the data line (for slave use)
- void sendResponse(const char* resp, bool addCRC = false)
- Send a response out on the data line (for slave use)
- void sendResponse(FlashString resp, bool addCRC = false)
- Send a response out on the data line (for slave use)
Interrupt Service Routine
Functions for handling interrupts - responding to changes on the data line and converting them to characters in the Rx buffer.
- static void handleInterrupt()
- Intermediary used by the ISR - passes off responsibility for the interrupt to the active object.
Enum documentation
enum SDI12:: SDI12_STATES
The various SDI-12 line states.
Enumerators | |
---|---|
SDI12_DISABLED |
SDI-12 is disabled, pin mode INPUT, interrupts disabled for the pin |
SDI12_ENABLED |
SDI-12 is enabled, pin mode INPUT, interrupts disabled for the pin |
SDI12_HOLDING |
The line is being held LOW, pin mode OUTPUT, interrupts disabled for the pin |
SDI12_TRANSMITTING |
Data is being transmitted by the SDI-12 master, pin mode OUTPUT, interrupts disabled for the pin |
SDI12_LISTENING |
The SDI-12 master is listening for a response from the slave, pin mode INPUT, interrupts enabled for the pin |
Function documentation
void SDI12:: forceHold()
Set line state to SDI12_HOLDING.
A public function which forces the line into a "holding" state. This is generally unneeded, but for deployments where interference is an issue, it should be used after all expected bytes have been returned from the sensor.
void SDI12:: forceListen()
Set line state to SDI12_LISTENING.
A public function which forces the line into a "listening" state. This may be needed for implementing a slave-side device, which should relinquish control of the data line when not transmitting.
SDI12:: SDI12()
Construct a new SDI12 instance with no data pin set.
Before using the SDI-12 instance, the data pin must be set with SDI12::setDataPin(dataPin) or SDI12::begin(dataPin). This empty constructor is provided for easier integration with other Arduino libraries.
When the constructor is called it resets the buffer overflow status to FALSE.
SDI12:: ~SDI12()
Destroy the SDI12 object.
When the destructor is called, it's main task is to disable any interrupts that had been previously assigned to the pin, so that the pin will behave as expected when used for other purposes. This is achieved by putting the SDI-12 object in the SDI12_DISABLED state. After disabling interrupts, the pointer to the current active SDI-12 instance is set to null if it had pointed to the destroyed object. Finally, for AVR board, the timer prescaler is set back to whatever it had been prior to creating the SDI-12 object.
void SDI12:: begin()
Begin the SDI-12 object.
This is called to begin the functionality of the SDI-12 object. It sets the object as the active object, sets the stream timeout to 150ms to match SDI-12 specs, sets the timeout return value to SDI12::
void SDI12:: begin(int8_t dataPin)
Set the SDI12::_datapin and begin the SDI-12 object.
Parameters | |
---|---|
dataPin | The data pin's digital pin number |
This is called to begin the functionality of the SDI-12 object. It sets the object as the active object, sets the stream timeout to 150ms to match SDI-12 specs, sets the timeout return value to SDI12::
void SDI12:: end()
Disable the SDI-12 object (but do not destroy it).
Set the SDI-12 state to disabled, set the pointer to the current active instance to null, and then, for AVR boards, unset the timer prescaler.
This can be called to temporarily cease all functionality of the SDI-12 object. It is not as harsh as destroying the object with the destructor, as it will maintain the memory buffer.
void SDI12:: setTimeoutValue(int16_t value)
Set the value to return if a parse int or parse float times out with no return from the sensor.
Parameters | |
---|---|
value | the value to return on timeout |
The "standard" timeout return for an Arduino stream object when no character is available in the Rx buffer is "0." For enviromental sensors (the typical SDI-12 users) 0 is a common result value. To better distinguish between a timeout because of no sensor response and a true zero return, the timeout should be set to some value that is NOT a possible return from that sensor. If the timeout is not set, -9999 is used.
int8_t SDI12:: getDataPin()
Get the data pin for the current SDI-12 instance.
Returns | int8_t The data pin number |
---|
void SDI12:: setDataPin(int8_t dataPin)
Set the data pin for the current SDI-12 instance.
Parameters | |
---|---|
dataPin | The data pin's digital pin number |
int SDI12:: available() override
Return the number of bytes available in the Rx buffer.
Returns | int The number of characters in the buffer |
---|
available() is a public function that returns the number of characters available in the Rx buffer.
To understand how: _rxBufferTail + SDI12_BUFFER_SIZE - _rxBufferHead) % SDI12_BUFFER_SIZE;
accomplishes this task, we will use a few examples.
To start take the buffer below that has SDI12_BUFFER_SIZE = 10
. The message "abc" has been wrapped around (circular buffer).
[ c ] [ - ] [ - ] [ - ] [ - ] [ - ] [ - ] [ - ] [ a ] [ b ]
The number of available characters is (1 + 10 - 8) % 10 = 3
The '' or modulo operator finds the remainder of division of one number by another. In integer arithmetic 3 / 10 = 0, but has a remainder of 3. We can only get the remainder by using the the modulo ''. 3 % 10 = 3. This next case demonstrates more clearly why the modulo is used.
[ a ] [ b ] [ c ] [ - ] [ - ] [ - ] [ - ] [ - ] [ - ] [ - ]
The number of available characters is (4 + 10 - 1) % 10 = 3
If we did not use the modulo we would get either ( 4 + 10 - 1 ) = 13 characters or ( 4 + 10 - 1 ) / 10 = 1 character. Obviously neither is correct.
If there has been a buffer overflow, available() will return -1.
int SDI12:: peek() override
Reveal next byte in the Rx buffer without consuming it.
Returns | int The next byte in the character buffer. |
---|
peek() is a public function that allows the user to look at the character that is at the head of the buffer. Unlike read() it does not consume the character (i.e. the index addressed by _rxBufferHead is not changed). peek() returns -1 if there are no characters to show.
void SDI12:: clearBuffer()
Clear the Rx buffer by setting the head and tail pointers to the same value.
clearBuffer() is a public function that clears the buffers contents by setting the index for both head and tail back to zero.
int SDI12:: read() override
Return next byte in the Rx buffer, consuming it.
Returns | int The next byte in the character buffer. |
---|
read() returns the character at the current head in the buffer after incrementing the index of the buffer head. This action 'consumes' the character, meaning it can not be read from the buffer again. If you would rather see the character, but leave the index to head intact, you should use peek();
long SDI12:: parseInt(LookaheadMode lookahead = SKIP_ ALL,
char ignore = '\x01')
Return the first valid (long) integer value from the current position.
Parameters | |
---|---|
lookahead | the mode to use to look ahead in the stream, default is LookaheadMode::SKIP_ALL |
ignore | a character to ignore in the stream, default is '\x01' |
Returns | long The first valid integer in the stream |
The value of lookahead determines how parseInt looks ahead in the stream. See LookaheadMode enumeration at the top of the file. Lookahead is terminated by the first character that is not a valid part of an integer. Once parsing commences, 'ignore' will be skipped in the stream.
float SDI12:: parseFloat(LookaheadMode lookahead = SKIP_ ALL,
char ignore = '\x01')
Return the first valid float value from the current position.
Parameters | |
---|---|
lookahead | the mode to use to look ahead in the stream, default is LookaheadMode::SKIP_ALL |
ignore | a character to ignore in the stream, default is '\x01' |
Returns | float The first valid float in the stream |
The value of lookahead determines how parseInt looks ahead in the stream. See LookaheadMode enumeration at the top of the file. Lookahead is terminated by the first character that is not a valid part of an integer. Once parsing commences, 'ignore' will be skipped in the stream.
int SDI12:: peekNextDigit(LookaheadMode lookahead,
bool detectDecimal) protected
Return the next numeric digit in the stream or -1 if timeout.
Parameters | |
---|---|
lookahead | the mode to use to look ahead in the stream |
detectDecimal | True to accept a decimal point ('.') as part of a number |
Returns | int The next numeric digit in the stream |
bool SDI12:: setActive()
Set this instance as the active SDI-12 instance.
Returns | bool True indicates that the current SDI-12 instance was not formerly the active one and now is. False indicates that the current SDI-12 instance is already the active one and the state was not changed. |
---|
A method for setting the current object as the active object; returns TRUE if the object was not formerly the active object and now is.
- Promoting an inactive to the active instance will start it in the SDI12_HOLDING state and return TRUE.
- Otherwise, if the object is currently the active instance, it will remain unchanged and return FALSE.
bool SDI12:: isActive()
Check if this instance is active.
Returns | bool True indicates that the curren SDI-12 instace is the active one. |
---|
isActive() is a method for checking if the object is the active object. Returns true if the object is currently the active object, false otherwise.
size_t SDI12:: write(uint8_t byte) virtual
Write out a byte on the SDI-12 line.
Parameters | |
---|---|
byte | The character to write |
Returns | size_t The number of characters written |
Sets the state to transmitting, writes a character, and then sets the state back to listening. This function must be implemented as part of the Arduino Stream instance, but is NOT intenteded to be used for SDI-12 objects. Instead, use the SDI12::
void SDI12:: sendCommand(String& cmd,
int8_t extraWakeTime = 0)
Send a command out on the data line, acting as a datalogger (master)
Parameters | |
---|---|
cmd | the command to send |
extraWakeTime | The amount of additional time in milliseconds that the sensor takes to wake before being ready to receive a command. Default is 0ms - meaning the sensor is ready for a command by the end of the 12ms break. Per protocol, the wake time must be less than 100 ms. |
A publicly accessible function that sends a break to wake sensors and sends out a command byte by byte on the data line.
void SDI12:: sendCommand(const char* cmd,
int8_t extraWakeTime = 0)
Send a command out on the data line, acting as a datalogger (master)
Parameters | |
---|---|
cmd | the command to send |
extraWakeTime | The amount of additional time in milliseconds that the sensor takes to wake before being ready to receive a command. Default is 0ms - meaning the sensor is ready for a command by the end of the 12ms break. Per protocol, the wake time must be less than 100 ms. |
A publicly accessible function that sends a break to wake sensors and sends out a command byte by byte on the data line.
void SDI12:: sendCommand(FlashString cmd,
int8_t extraWakeTime = 0)
Send a command out on the data line, acting as a datalogger (master)
Parameters | |
---|---|
cmd | the command to send |
extraWakeTime | The amount of additional time in milliseconds that the sensor takes to wake before being ready to receive a command. Default is 0ms - meaning the sensor is ready for a command by the end of the 12ms break. Per protocol, the wake time must be less than 100 ms. |
A publicly accessible function that sends a break to wake sensors and sends out a command byte by byte on the data line.
uint16_t SDI12:: calculateCRC(String& resp)
Calculates the 16-bit Cyclic Redundancy Check (CRC) for an SDI-12 message.
Parameters | |
---|---|
resp | The message to calculate the CRC for. |
Returns | uint16_t The calculated CRC |
uint16_t SDI12:: calculateCRC(const char* resp)
Calculates the 16-bit Cyclic Redundancy Check (CRC) for an SDI-12 message.
Parameters | |
---|---|
resp | The message to calculate the CRC for. |
Returns | uint16_t The calculated CRC |
uint16_t SDI12:: calculateCRC(FlashString resp)
Calculates the 16-bit Cyclic Redundancy Check (CRC) for an SDI-12 message.
Parameters | |
---|---|
resp | The message to calculate the CRC for. |
Returns | uint16_t The calculated CRC |
String SDI12:: crcToString(uint16_t crc)
Converts a numeric 16-bit CRC to an ASCII String.
Parameters | |
---|---|
crc | The 16-bit CRC |
Returns | String An ASCII string for the CRC |
From the SDI-12 Specifications:
The 16 bit CRC is encoded as three ASCII characters using the following algorithm: 1st character = 0x40 OR (CRC shifted right 12 bits) 2nd character = 0x40 OR ((CRC shifted right 6 bits) AND 0x3F) 3rd character = 0x40 OR (CRC AND 0x3F)
bool SDI12:: verifyCRC(String& respWithCRC)
Verifies that the CRC returned at the end of an SDI-12 message matches that of the content of the message.
Parameters | |
---|---|
respWithCRC | The full SDI-12 message, including the CRC at the end. |
Returns | bool True if the CRC matches and the message is valid, false if the CRC doesn't match and the message could be retried. |
void SDI12:: sendResponse(String& resp,
bool addCRC = false)
Send a response out on the data line (for slave use)
Parameters | |
---|---|
resp | the response to send |
addCRC | True to append a CRC to the outgoing response |
A publicly accessible function that sends out an 8.33 ms marking and a response byte by byte on the data line. This is needed if the Arduino is acting as an SDI-12 device itself, not as a recorder for another SDI-12 device.
void SDI12:: sendResponse(const char* resp,
bool addCRC = false)
Send a response out on the data line (for slave use)
Parameters | |
---|---|
resp | the response to send |
addCRC | True to append a CRC to the outgoing response |
A publicly accessible function that sends out an 8.33 ms marking and a response byte by byte on the data line. This is needed if the Arduino is acting as an SDI-12 device itself, not as a recorder for another SDI-12 device.
void SDI12:: sendResponse(FlashString resp,
bool addCRC = false)
Send a response out on the data line (for slave use)
Parameters | |
---|---|
resp | the response to send |
addCRC | True to append a CRC to the outgoing response |
A publicly accessible function that sends out an 8.33 ms marking and a response byte by byte on the data line. This is needed if the Arduino is acting as an SDI-12 device itself, not as a recorder for another SDI-12 device.
static void SDI12:: handleInterrupt()
Intermediary used by the ISR - passes off responsibility for the interrupt to the active object.
On espressif boards (ESP8266 and ESP32), the ISR must be stored in IRAM
Variable documentation
static const uint16_t SDI12:: bitWidth_micros
The size of a bit in microseconds.
1200 baud = 1200 bits/second ~ 833.333 µs/bit
static uint8_t SDI12:: rxState
Tracks how many bits are accounted for on an incoming character.
- if 0: indicates that we got a start bit
- if >0: indicates the number of bits received
0 - got start bit 1 - got data bit 0 2 - got data bit 1 3 - got data bit 2 4 - got data bit 3 5 - got data bit 4 6 - got data bit 5 7 - got data bit 6 8 - got data bit 7 (parity) 9 - got stop bit 255 - waiting for next start bit
static uint8_t SDI12:: rxMask
a bit mask for building a received character
The mask has a single bit set, in the place of the active bit based on the rxState.
static uint8_t SDI12:: _rxBuffer
A single incoming character buffer for ALL SDI-12 objects (Rx buffer)
Increasing the buffer size will use more RAM. If you exceed 256 characters, be sure to change the data type of the index to support the larger range of addresses. To adjust the size of the buffer, change the value of SDI12_BUFFER_SIZE
in the header file.
int16_t SDI12:: TIMEOUT
The value to return if a parse or read times out with no return from the sensor.
The timeout return for an Arduino stream object when no character is available in the Rx buffer is "0." For enviromental sensors (the typical SDI-12 users) 0 is a common result value. To better distinguish between a timeout because of no sensor response and a true zero return, the timeout should be set to some value that is NOT a possible return from that sensor. If the timeout is not set, -9999 is used.