Skip to content

Commit

Permalink
Implement binary data type, ability to read/write 8N1 byte data
Browse files Browse the repository at this point in the history
Update buffer to byte (8N1) configuration
Read and peek commands will remove parity bit convert (8N1 to 7E1) to ascii char.
Added readByte(), readBytes(), timedReadByte()
Added writeByte(), writeBytes()
  • Loading branch information
ltan10 committed Apr 19, 2024
1 parent 52b9b98 commit e7cfc22
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 7 deletions.
144 changes: 141 additions & 3 deletions src/SDI12.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ int SDI12::available() {
// reveals the next character in the buffer without consuming
int SDI12::peek() {
if (_rxBufferHead == _rxBufferTail) return -1; // Empty buffer? If yes, -1
return _rxBuffer[_rxBufferHead]; // Otherwise, read from "head"
return _rxBuffer[_rxBufferHead]; // Otherwise, read from "head", excluding parity bit
}

// a public function that clears the buffer contents and resets the status of the buffer
Expand All @@ -115,14 +115,74 @@ void SDI12::clearBuffer() {

// reads in the next character from the buffer (and moves the index ahead)
int SDI12::read() {
_bufferOverflow = false; // Reading makes room in the buffer
if (_rxBufferHead == _rxBufferTail) return -1; // Empty buffer? If yes, -1
uint8_t nextChar = _rxBuffer[_rxBufferHead] & 0x7F; // Otherwise, grab char at head excluding parity bit
_rxBufferHead = (_rxBufferHead + 1) % SDI12_BUFFER_SIZE; // increment head
return nextChar; // return the char
}

/**
* @brief Return next byte in the Rx buffer including parity bit, consuming it
*
* @return @m_span{m-type} int @m_endspan The next byte in the character buffer.
*
* readByte() 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();
*/
int SDI12::readByte() {
_bufferOverflow = false; // Reading makes room in the buffer
if (_rxBufferHead == _rxBufferTail) return -1; // Empty buffer? If yes, -1
uint8_t nextChar = _rxBuffer[_rxBufferHead]; // Otherwise, grab char at head
_rxBufferHead = (_rxBufferHead + 1) % SDI12_BUFFER_SIZE; // increment head
return nextChar; // return the char
}

// these functions HIDE the stream equivalents to return a custom timeout value
/**
* @brief Reads the next byte from the buffer (and moves the index ahead) with timeout
*
* @return int Byte data from buffer or -1 if buffer is empty or timeout during read
*/
int SDI12::timedReadByte(void) {
int c;
_startMillis = millis();
do {
c = readByte();
if (c >= 0) return c;
} while(millis() - _startMillis < _timeout);
return -1; // -1 indicates timeout
}

/**
* @brief Return the number of bytes given by @p length or until timeout,
* and store it at reference pointed to by @p buffer in little-endian format.
*
* @param buffer Reference to location in memory to store the bytes read from buffer
* @param length Max number of bytes to read from buffer
* @return size_t Number of bytes read from buffer
*
* readBytes() attempts to return the number of bytes up to the given @p length
* after incrementing the index of the buffer head using @see timedReadByte().
* This action "consumes" the number of bytes requested by @p length, meaning
* it can not be used to read from the buffer again. If readBytes is unable to
* return to return the number of bytes before timeout, the number of bytes
* returned is less than the required @p length . The byte chunks are then
* stored at the location pointed to by @p buffer in little-endian format.
*/
size_t SDI12::readBytes(char *buffer, size_t length) {
size_t count = 0;
while (count < length) {
int c = timedReadByte();
if (c < 0) break;
*buffer++ = (char)c;
count++;
}
return count;
}

// these functions hide the stream equivalents to return a custom timeout value
// This peekNextDigit function is identical to the Stream version
int SDI12::peekNextDigit(LookaheadMode lookahead, bool detectDecimal) {
int c;
Expand Down Expand Up @@ -471,6 +531,82 @@ void SDI12::writeChar(uint8_t outChar) {
while ((uint8_t)(READTIME - t0) < bitTimeRemaining) {}
}

/**
* @brief Used to send a byte (8N1) out on the data line, use writeBytes(T value) instead
*
* @param byte **uint8_t (char)** the byte to write
*
* This function writes a character out to the data line. SDI-12 specifies the
* general transmission format of a single character as:
* - 10 bits per data frame (8N1)
* - 1 start bit
* - 8 data bits (least significant bit first)
* - 1 stop bit
*
* Recall that we are using inverse logic, so HIGH represents 0, and LOW represents
* a 1.
*
* This function must be implemented as part of the Arduino Stream
* instance, but is *NOT* intenteded to be used for SDI-12 objects.
*
* @return size_t 1
*/
size_t SDI12::writeByte(uint8_t outByte) {
uint8_t currentTxBitNum = 0; // first bit is start bit
uint8_t bitValue = 1; // start bit is HIGH (inverse parity...)

noInterrupts(); // _ALL_ interrupts disabled so timing can't be shifted

sdi12timer_t t0 = READTIME; // start time

digitalWrite(
_dataPin,
HIGH); // immediately get going on the start bit
// this gives us 833µs to calculate parity and position of last high bit
currentTxBitNum++;

// Calculate the position of the last bit that is a 0/HIGH (ie, HIGH, not marking)
// That bit will be the last time-critical bit. All bits after that can be
// sent with interrupts enabled.

uint8_t lastHighBit = 9; // The position of the last bit that is a 0 (ie, HIGH, not marking)
uint8_t msbMask = 0x80; // A mask with all bits at 1
while (msbMask & outByte) {
lastHighBit--;
msbMask >>= 1;
}

// Hold the line for the rest of the start bit duration

while ((uint8_t)(READTIME - t0) < txBitWidth) {}
t0 = READTIME; // advance start time

// repeat for all data bits until the last bit different from marking
while (currentTxBitNum++ < lastHighBit) {
bitValue = outByte & 0x01; // get next bit in the character to send
if (bitValue) {
digitalWrite(_dataPin, LOW); // set the pin state to LOW for 1's
} else {
digitalWrite(_dataPin, HIGH); // set the pin state to HIGH for 0's
}
// Hold the line for this bit duration
while ((uint8_t)(READTIME - t0) < txBitWidth) {}
t0 = READTIME; // start time

outByte = outByte >> 1; // shift character to expose the following bit
}

// Set the line low for the all remaining 1's and the stop bit
digitalWrite(_dataPin, LOW);

interrupts(); // Re-enable universal interrupts as soon as critical timing is past

// Hold the line low until the end of the 10th bit
uint8_t bitTimeRemaining = txBitWidth * (10 - lastHighBit);
while ((uint8_t)(READTIME - t0) < bitTimeRemaining) {}
return 1;
}

// The typical write functionality for a stream object
// This allows you to use the stream print functions to send commands out on
// the SDI-12, line, but it will not wake the sensors in advance of the command.
Expand Down Expand Up @@ -660,7 +796,9 @@ void SDI12::receiveISR() {

// If this was the 8th or more bit then the character and parity are complete.
if (rxState > 7) {
rxValue &= 0x7F; // Throw away the parity bit (and with 0b01111111)
// rxValue &= 0x7F; // Throw away the parity bit (and with 0b01111111)
// Mask out all but the least significant byte (parity handling will be taken care by respective read function)
rxValue &= 0xFF;
charToBuffer(rxValue); // Put the finished character into the buffer


Expand Down
61 changes: 57 additions & 4 deletions src/SDI12.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,26 @@ enum LookaheadMode {
#define READTIME TCNTX
#endif // defined(ESP32) || defined(ESP8266)


/**
* @brief Enumerated type to reference the type of supported binary data types
* for binary measurements
*/
typedef enum SDI12BinaryDataType_e : uint8_t {
kInvalidDataType = 0, // Invalid data or Empty data type
kInt8DataType = 1, // Signed 8-bit integer
kUint8DataType = 2, // Unsigned 8-bit integer
kInt16DataType = 3, // Signed 16-bit integer
kUint16DataType = 4, // Unsigned 16-bit integer
kInt32DataType = 5, // Signed 32-bit integer
kUint32DataType = 6, // Unsigned 32-bit integer
kInt64DataType = 7, // Signed 64-bit integer
kUint64DataType = 8, // Unsigned 64-bit integer
kFloatDataType = 9, // IEEE 32-bit floating point single precision
kDoubleDataType = 10 // IEEE 64-bit floating point double precision
} SDI12BinaryDataType_e;


/**
* @brief The main class for SDI 12 instances
*/
Expand Down Expand Up @@ -437,7 +457,7 @@ class SDI12 : public Stream {
*/
void clearBuffer();
/**
* @brief Return next byte in the Rx buffer, consuming it
* @brief Return next character in the Rx buffer without parity bit, consuming it
*
* @return @m_span{m-type} int @m_endspan The next byte in the character buffer.
*
Expand All @@ -447,6 +467,11 @@ class SDI12 : public Stream {
* the index to head intact, you should use peek();
*/
int read() override;

int readByte(void); // Read a byte data (includes parity) from buffer and move index ahead
int timedReadByte(void); // Read a byte data (includes parity) before timeout from buffer and move index ahead
size_t readBytes(char *buffer, size_t length); // Read up to given number of bytes (iincludes parity) from buffer before timout and move index ahead


/**
* @brief Wait for sending to finish - because no TX buffering, does nothing
Expand Down Expand Up @@ -861,13 +886,13 @@ class SDI12 : public Stream {
*/
void wakeSensors(int8_t extraWakeTime = 0);
/**
* @brief Used to send a character out on the data line
* @brief Used to send a character (7E1) out on the data line
*
* @param out **uint8_t (char)** the character to write
*
* This function writes a character out to the data line. SDI-12 specifies the
* general transmission format of a single character as:
* - 10 bits per data frame
* - 10 bits per data frame (7E1)
* - 1 start bit
* - 7 data bits (least significant bit first)
* - 1 even parity bit
Expand All @@ -878,9 +903,11 @@ class SDI12 : public Stream {
*/
void writeChar(uint8_t out);

size_t writeByte(uint8_t byte); // this function writes a single byte (8N1) out on the data line

public:
/**
* @brief Write out a byte on the SDI-12 line
* @brief Write out a byte (7E1) on the SDI-12 line
*
* @param byte The character to write
* @return @m_span{m-type} size_t @m_endspan The number of characters written
Expand All @@ -892,6 +919,8 @@ class SDI12 : public Stream {
*/
virtual size_t write(uint8_t byte);

template <typename T> size_t writeBytes(T value); // Writes out number of bytes little-endian

/**
* @brief Send a command out on the data line, acting as a datalogger (master)
*
Expand Down Expand Up @@ -979,4 +1008,28 @@ class SDI12 : public Stream {
/**@}*/
};


/**
* @brief Write out number of bytes on the SDI-12 line, least significant byte first (little-endian transmission)
*
* @tparam T @p value type
* @param value Data to be converted to byte chunks
* @return size_t sizeof( @p T ), number of bytes written out
*
* Sets the state to transmitting, starts writing byte chunks of @p value from
* least significant byte to most significant byte, and then sets the state back
* to listening.
*/
template <typename T>
size_t SDI12::writeBytes(T value) {
setState(SDI12_TRANSMITTING);
size_t count = sizeof(T);
for (size_t i = 0; i < count; i++) {
writeByte(value & 0xFF);
value >>= 8;
}
setState(SDI12_LISTENING);
return count;
}

#endif // SRC_SDI12_H_

0 comments on commit e7cfc22

Please sign in to comment.