Feature: Serial SML power meter: poll asynchronously
This commit is contained in:
parent
7b962f58b0
commit
a1138a2202
@ -12,8 +12,33 @@ public:
|
|||||||
~PowerMeterSerialSml();
|
~PowerMeterSerialSml();
|
||||||
|
|
||||||
bool init() final;
|
bool init() final;
|
||||||
void loop() final;
|
void loop() final { } // polling is performed asynchronously
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// we assume that an SML datagram is complete after no additional
|
||||||
|
// characters were received for this many milliseconds.
|
||||||
|
static uint8_t constexpr _datagramGapMillis = 50;
|
||||||
|
|
||||||
|
static uint32_t constexpr _baud = 9600;
|
||||||
|
|
||||||
|
// size in bytes of the software serial receive buffer. must have the
|
||||||
|
// capacity to hold a full SML datagram, as we are processing the datagrams
|
||||||
|
// only after all data of one datagram was received.
|
||||||
|
static int constexpr _bufCapacity = 1024; // memory usage: 1 byte each
|
||||||
|
|
||||||
|
// amount of bits (RX pin state transitions) the software serial can buffer
|
||||||
|
// without decoding bits to bytes and storing those in the receive buffer.
|
||||||
|
// this value dictates how ofter we need to call a function of the software
|
||||||
|
// serial instance that performs bit decoding (we call available()).
|
||||||
|
static int constexpr _isrCapacity = 256; // memory usage: 8 bytes each (timestamp + pointer)
|
||||||
|
|
||||||
|
static void pollingLoopHelper(void* context);
|
||||||
|
std::atomic<bool> _taskDone;
|
||||||
|
void pollingLoop();
|
||||||
|
|
||||||
|
TaskHandle_t _taskHandle = nullptr;
|
||||||
|
bool _stopPolling;
|
||||||
|
mutable std::mutex _pollingMutex;
|
||||||
|
|
||||||
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
|
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,26 +17,99 @@ bool PowerMeterSerialSml::init()
|
|||||||
|
|
||||||
pinMode(pin.powermeter_rx, INPUT);
|
pinMode(pin.powermeter_rx, INPUT);
|
||||||
_upSmlSerial = std::make_unique<SoftwareSerial>();
|
_upSmlSerial = std::make_unique<SoftwareSerial>();
|
||||||
_upSmlSerial->begin(9600, SWSERIAL_8N1, pin.powermeter_rx, -1, false, 128, 95);
|
_upSmlSerial->begin(_baud, SWSERIAL_8N1, pin.powermeter_rx, -1/*tx pin*/,
|
||||||
|
false/*invert*/, _bufCapacity, _isrCapacity);
|
||||||
_upSmlSerial->enableRx(true);
|
_upSmlSerial->enableRx(true);
|
||||||
_upSmlSerial->enableTx(false);
|
_upSmlSerial->enableTx(false);
|
||||||
_upSmlSerial->flush();
|
_upSmlSerial->flush();
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock(_pollingMutex);
|
||||||
|
_stopPolling = false;
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
uint32_t constexpr stackSize = 3072;
|
||||||
|
xTaskCreate(PowerMeterSerialSml::pollingLoopHelper, "PM:SML",
|
||||||
|
stackSize, this, 1/*prio*/, &_taskHandle);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
PowerMeterSerialSml::~PowerMeterSerialSml()
|
PowerMeterSerialSml::~PowerMeterSerialSml()
|
||||||
{
|
{
|
||||||
if (!_upSmlSerial) { return; }
|
_taskDone = false;
|
||||||
_upSmlSerial->end();
|
|
||||||
|
std::unique_lock<std::mutex> lock(_pollingMutex);
|
||||||
|
_stopPolling = true;
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
if (_taskHandle != nullptr) {
|
||||||
|
while (!_taskDone) { delay(10); }
|
||||||
|
_taskHandle = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PowerMeterSerialSml::loop()
|
if (_upSmlSerial) {
|
||||||
{
|
_upSmlSerial->end();
|
||||||
if (!_upSmlSerial) { return; }
|
_upSmlSerial = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (_upSmlSerial->available()) {
|
void PowerMeterSerialSml::pollingLoopHelper(void* context)
|
||||||
|
{
|
||||||
|
auto pInstance = static_cast<PowerMeterSerialSml*>(context);
|
||||||
|
pInstance->pollingLoop();
|
||||||
|
pInstance->_taskDone = true;
|
||||||
|
vTaskDelete(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerMeterSerialSml::pollingLoop()
|
||||||
|
{
|
||||||
|
int lastAvailable = 0;
|
||||||
|
uint32_t gapStartMillis = 0;
|
||||||
|
std::unique_lock<std::mutex> lock(_pollingMutex);
|
||||||
|
|
||||||
|
while (!_stopPolling) {
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
// calling available() will decode bytes into the receive buffer and
|
||||||
|
// hence free data from the ISR buffer, so we need to call this rather
|
||||||
|
// frequenly.
|
||||||
|
int nowAvailable = _upSmlSerial->available();
|
||||||
|
|
||||||
|
if (nowAvailable <= 0) {
|
||||||
|
// sleep, but at most until the software serial ISR
|
||||||
|
// buffer is potentially half full with transitions.
|
||||||
|
uint32_t constexpr delayMs = _isrCapacity * 1000 / _baud / 2;
|
||||||
|
|
||||||
|
delay(delayMs); // this yields so other tasks are scheduled
|
||||||
|
|
||||||
|
lock.lock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sleep more if new data arrived in the meantime. process data only
|
||||||
|
// once a SML datagram seems to be complete (no new data arrived while
|
||||||
|
// we slept). this seems to be important as using read() while more
|
||||||
|
// data arrives causes trouble (we are missing bytes).
|
||||||
|
if (nowAvailable > lastAvailable) {
|
||||||
|
lastAvailable = nowAvailable;
|
||||||
|
delay(10);
|
||||||
|
gapStartMillis = millis();
|
||||||
|
lock.lock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((millis() - gapStartMillis) < _datagramGapMillis) {
|
||||||
|
delay(10);
|
||||||
|
lock.lock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_upSmlSerial->available() > 0) {
|
||||||
processSmlByte(_upSmlSerial->read());
|
processSmlByte(_upSmlSerial->read());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastAvailable = 0;
|
||||||
|
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user