243 lines
7.5 KiB
C++
243 lines
7.5 KiB
C++
/**
|
|
* Arduino DSMR parser.
|
|
*
|
|
* This software is licensed under the MIT License.
|
|
*
|
|
* Copyright (c) 2015 Matthijs Kooijman <matthijs@stdin.nl>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining
|
|
* a copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*
|
|
* P1 reader, that takes care of toggling a request pin, reading data
|
|
* from a serial port and parsing it.
|
|
*/
|
|
|
|
#ifndef DSMR_INCLUDE_READER_H
|
|
#define DSMR_INCLUDE_READER_H
|
|
|
|
#include <Arduino.h>
|
|
#include "crc16.h"
|
|
|
|
#include "parser.h"
|
|
|
|
namespace dsmr {
|
|
|
|
/**
|
|
* Controls the request pin on the P1 port to enable (periodic)
|
|
* transmission of messages and reads those messages.
|
|
*
|
|
* To enable the request pin, call enable(). This lets the Smart Meter
|
|
* start periodically sending messages. While the request pin is
|
|
* enabled, loop() should be regularly called to read pending bytes.
|
|
*
|
|
* Once a full and correct message is received, loop() (and available())
|
|
* start returning true, until the message is cleared. You can then
|
|
* either read the raw message using raw(), or parse it using parse().
|
|
*
|
|
* The message is cleared when:
|
|
* - clear() is called
|
|
* - parse() is called
|
|
* - loop() is called and the start of a new message is available
|
|
*
|
|
* When disable is called, the request pin is disabled again and any
|
|
* partial message is discarded. Any bytes received while disabled are
|
|
* dropped.
|
|
*/
|
|
class P1Reader {
|
|
public:
|
|
/**
|
|
* Create a new P1Reader. The stream passed should be the serial
|
|
* port to which the P1 TX pin is connected. The req_pin is the
|
|
* pin connected to the request pin. The pin is configured as an
|
|
* output, the Stream is assumed to be already set up (e.g. baud
|
|
* rate configured).
|
|
*/
|
|
P1Reader(Stream *stream, uint8_t req_pin)
|
|
: stream(stream), req_pin(req_pin), once(false), state(State::DISABLED_STATE) {
|
|
pinMode(req_pin, OUTPUT);
|
|
digitalWrite(req_pin, LOW);
|
|
}
|
|
|
|
/**
|
|
* Enable the request pin, to request data on the P1 port.
|
|
* @param once When true, the request pin is automatically
|
|
* disabled once a complete and correct message was
|
|
* receivedc. When false, the request pin stays
|
|
* enabled, so messages will continue to be sent
|
|
* periodically.
|
|
*/
|
|
void enable(bool once) {
|
|
digitalWrite(this->req_pin, HIGH);
|
|
this->state = State::WAITING_STATE;
|
|
this->once = once;
|
|
}
|
|
|
|
/* Disable the request pin again, to stop data from being sent on
|
|
* the P1 port. This will also clear any incomplete data that was
|
|
* previously received, but a complete message will be kept until
|
|
* clear() is called.
|
|
*/
|
|
void disable() {
|
|
digitalWrite(this->req_pin, LOW);
|
|
this->state = State::DISABLED_STATE;
|
|
if (!this->_available)
|
|
this->buffer = "";
|
|
// Clear any pending bytes
|
|
while(this->stream->read() >= 0) /* nothing */;
|
|
}
|
|
|
|
/**
|
|
* Returns true when a complete and correct message was received,
|
|
* until it is cleared.
|
|
*/
|
|
bool available() {
|
|
return this->_available;
|
|
}
|
|
|
|
/**
|
|
* Check for new data to read. Should be called regularly, such as
|
|
* once every loop. Returns true if a complete message is available
|
|
* (just like available).
|
|
*/
|
|
bool loop() {
|
|
while(true) {
|
|
if (state == State::CHECKSUM_STATE) {
|
|
// Let the Stream buffer the CRC bytes. Convert to size_t to
|
|
// prevent unsigned vs signed comparison
|
|
if ((size_t)this->stream->available() < CrcParser::CRC_LEN)
|
|
return false;
|
|
|
|
char buf[CrcParser::CRC_LEN];
|
|
for (uint8_t i = 0; i < CrcParser::CRC_LEN; ++i)
|
|
buf[i] = this->stream->read();
|
|
|
|
ParseResult<uint16_t> crc = CrcParser::parse(buf, buf + lengthof(buf));
|
|
|
|
// Prepare for next message
|
|
state = State::WAITING_STATE;
|
|
|
|
if (!crc.err && crc.result == this->crc) {
|
|
// Message complete, checksum correct
|
|
this->_available = true;
|
|
|
|
if (once)
|
|
this->disable();
|
|
|
|
return true;
|
|
}
|
|
} else {
|
|
// For other states, read bytes one by one
|
|
int c = this->stream->read();
|
|
if (c < 0)
|
|
return false;
|
|
|
|
switch (this->state) {
|
|
case State::DISABLED_STATE:
|
|
// Where did this byte come from? Just toss it
|
|
break;
|
|
case State::WAITING_STATE:
|
|
if (c == '/') {
|
|
this->state = State::READING_STATE;
|
|
// Include the / in the CRC
|
|
this->crc = _crc16_update(0, c);
|
|
this->clear();
|
|
}
|
|
break;
|
|
case State::READING_STATE:
|
|
// Include the ! in the CRC
|
|
this->crc = _crc16_update(this->crc, c);
|
|
if (c == '!')
|
|
this->state = State::CHECKSUM_STATE;
|
|
else
|
|
buffer.concat((char)c);
|
|
|
|
break;
|
|
case State::CHECKSUM_STATE:
|
|
// This cannot happen (given the surrounding if), but the
|
|
// compiler is not smart enough to see this, so list this
|
|
// case to prevent a warning.
|
|
abort();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the data read so far.
|
|
*/
|
|
const String &raw() {
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* If a complete message has been received, parse it and store the
|
|
* result into the ParsedData object passed.
|
|
*
|
|
* After parsing, the message is cleared.
|
|
*
|
|
* If parsing fails, false is returned. If err is passed, the error
|
|
* message is appended to that string.
|
|
*/
|
|
template<typename... Ts>
|
|
bool parse(ParsedData<Ts...> *data, String *err) {
|
|
const char *str = buffer.c_str(), *end = buffer.c_str() + buffer.length();
|
|
ParseResult<void> res = P1Parser::parse_data(data, str, end);
|
|
|
|
if (res.err && err)
|
|
*err = res.fullError(str, end);
|
|
|
|
// Clear the message
|
|
this->clear();
|
|
|
|
return res.err == NULL;
|
|
}
|
|
|
|
/**
|
|
* Clear any complete message from the buffer.
|
|
*/
|
|
void clear() {
|
|
if (_available) {
|
|
buffer = "";
|
|
_available = false;
|
|
}
|
|
}
|
|
|
|
protected:
|
|
Stream *stream;
|
|
uint8_t req_pin;
|
|
enum class State : uint8_t {
|
|
DISABLED_STATE,
|
|
WAITING_STATE,
|
|
READING_STATE,
|
|
CHECKSUM_STATE,
|
|
};
|
|
bool _available;
|
|
bool once;
|
|
State state;
|
|
String buffer;
|
|
uint16_t crc;
|
|
};
|
|
|
|
} // namespace dsmr
|
|
|
|
#endif // DSMR_INCLUDE_READER_H
|