From a1138a22027280a79554692dff79be0a96ce904d Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 14 Jun 2024 21:02:02 +0200 Subject: [PATCH] Feature: Serial SML power meter: poll asynchronously --- include/PowerMeterSerialSml.h | 27 ++++++++++- src/PowerMeterSerialSml.cpp | 91 +++++++++++++++++++++++++++++++---- 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/include/PowerMeterSerialSml.h b/include/PowerMeterSerialSml.h index 634f5994..613dace5 100644 --- a/include/PowerMeterSerialSml.h +++ b/include/PowerMeterSerialSml.h @@ -12,8 +12,33 @@ public: ~PowerMeterSerialSml(); bool init() final; - void loop() final; + void loop() final { } // polling is performed asynchronously 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 _taskDone; + void pollingLoop(); + + TaskHandle_t _taskHandle = nullptr; + bool _stopPolling; + mutable std::mutex _pollingMutex; + std::unique_ptr _upSmlSerial = nullptr; }; diff --git a/src/PowerMeterSerialSml.cpp b/src/PowerMeterSerialSml.cpp index e19b4067..41f845a0 100644 --- a/src/PowerMeterSerialSml.cpp +++ b/src/PowerMeterSerialSml.cpp @@ -17,26 +17,99 @@ bool PowerMeterSerialSml::init() pinMode(pin.powermeter_rx, INPUT); _upSmlSerial = std::make_unique(); - _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->enableTx(false); _upSmlSerial->flush(); + std::unique_lock lock(_pollingMutex); + _stopPolling = false; + lock.unlock(); + + uint32_t constexpr stackSize = 3072; + xTaskCreate(PowerMeterSerialSml::pollingLoopHelper, "PM:SML", + stackSize, this, 1/*prio*/, &_taskHandle); + return true; } PowerMeterSerialSml::~PowerMeterSerialSml() { - if (!_upSmlSerial) { return; } - _upSmlSerial->end(); -} + _taskDone = false; -void PowerMeterSerialSml::loop() -{ - if (!_upSmlSerial) { return; } + std::unique_lock lock(_pollingMutex); + _stopPolling = true; + lock.unlock(); - while (_upSmlSerial->available()) { - processSmlByte(_upSmlSerial->read()); + if (_taskHandle != nullptr) { + while (!_taskDone) { delay(10); } + _taskHandle = nullptr; } + if (_upSmlSerial) { + _upSmlSerial->end(); + _upSmlSerial = nullptr; + } +} + +void PowerMeterSerialSml::pollingLoopHelper(void* context) +{ + auto pInstance = static_cast(context); + pInstance->pollingLoop(); + pInstance->_taskDone = true; + vTaskDelete(nullptr); +} + +void PowerMeterSerialSml::pollingLoop() +{ + int lastAvailable = 0; + uint32_t gapStartMillis = 0; + std::unique_lock 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()); + } + + lastAvailable = 0; + + lock.lock(); + } }