From 3d1a946bff51b838c28028136f3919340ec68d6b Mon Sep 17 00:00:00 2001 From: oberfritze <139758614+oberfritze@users.noreply.github.com> Date: Sat, 12 Aug 2023 19:19:48 +0200 Subject: [PATCH] Add files via upload NRF24L01+ Radio send channel evaluation and hopping heuristik (helps to reduces TX retransmits in relation to TX count) --- src/hm/hmPayload.h | 929 +++++++++++++++++++++++---------------------- src/hm/hmRadio.h | 849 +++++++++++++++++++++++------------------ 2 files changed, 941 insertions(+), 837 deletions(-) diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h index dfb8464c..b0d6d8ed 100644 --- a/src/hm/hmPayload.h +++ b/src/hm/hmPayload.h @@ -1,458 +1,471 @@ -//----------------------------------------------------------------------------- -// 2023 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __HM_PAYLOAD_H__ -#define __HM_PAYLOAD_H__ - -#include "../utils/dbg.h" -#include "../utils/crc.h" -#include "../config/config.h" -#include - -typedef struct { - uint8_t txCmd; - uint8_t txId; - uint8_t invId; - uint32_t ts; - uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; - uint8_t len[MAX_PAYLOAD_ENTRIES]; - bool complete; - uint8_t maxPackId; - bool lastFound; - uint8_t retransmits; - bool requested; - bool gotFragment; - bool rxTmo; -} invPayload_t; - - -typedef std::function payloadListenerType; -typedef std::function alarmListenerType; - - -template -class HmPayload { - public: - HmPayload() {} - - void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { - mApp = app; - mSys = sys; - mStat = stat; - mMaxRetrans = maxRetransmits; - mTimestamp = timestamp; - for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - reset(i); - } - mSerialDebug = false; - mHighPrioIv = NULL; - mCbAlarm = NULL; - mCbPayload = NULL; - } - - void enableSerialDebug(bool enable) { - mSerialDebug = enable; - } - - void addPayloadListener(payloadListenerType cb) { - mCbPayload = cb; - } - - void addAlarmListener(alarmListenerType cb) { - mCbAlarm = cb; - } - - void loop() { - if (NULL != mHighPrioIv) { - ivSend(mHighPrioIv, true); - mHighPrioIv = NULL; - } - } - - void zeroYieldDay(Inverter<> *iv) { - DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - uint8_t pos; - for(uint8_t ch = 0; ch <= iv->channels; ch++) { - pos = iv->getPosByChFld(ch, FLD_YD, rec); - iv->setValue(pos, rec, 0.0f); - pos = iv->getPosByChFld(ch, FLD_MP, rec); - iv->setValue(pos, rec, 0.0f); - } - } - - void zeroInverterValues(Inverter<> *iv) { - DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - for(uint8_t ch = 0; ch <= iv->channels; ch++) { - uint8_t pos = 0; - for(uint8_t fld = 0; fld < FLD_EVT; fld++) { - switch(fld) { - case FLD_YD: - case FLD_YT: - case FLD_MP: - continue; - } - pos = iv->getPosByChFld(ch, fld, rec); - iv->setValue(pos, rec, 0.0f); - } - } - - notify(RealTimeRunData_Debug); - } - - void ivSendHighPrio(Inverter<> *iv) { - mHighPrioIv = iv; - } - - void ivSend(Inverter<> *iv, bool highPrio = false) { - bool save_rxTmo; - - if(!highPrio) { - if (mPayload[iv->id].requested) { - if (!mPayload[iv->id].complete) - process(false); // no retransmit - - if (!mPayload[iv->id].complete) { - if (mSerialDebug) - DPRINT_IVID(DBG_INFO, iv->id); - if (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId) { - mStat->rxFailNoAnser++; // got nothing - if (mSerialDebug) - DBGPRINTLN(F("enqueued cmd failed/timeout")); - } else { - mStat->rxFail++; // got fragments but not complete response - if (mSerialDebug) { - DBGPRINT(F("no complete Payload received! (retransmits: ")); - DBGPRINT(String(mPayload[iv->id].retransmits)); - DBGPRINTLN(F(")")); - } - } - iv->setQueuedCmdFinished(); // command failed - } - } - } - - save_rxTmo = mPayload[iv->id].rxTmo; - reset(iv->id); - mPayload[iv->id].rxTmo = save_rxTmo; - mPayload[iv->id].requested = true; - - yield(); -#ifdef undef - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("Requesting Inv SN ")); - DBGPRINTLN(String(iv->config->serial.u64, HEX)); - } -#endif - - if (iv->getDevControlRequest()) { - if (mSerialDebug) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("Devcontrol request 0x")); - DBGPRINT(String(iv->devControlCmd, HEX)); - DBGPRINT(F(" power limit ")); - DBGPRINTLN(String(iv->powerLimit[0])); - } - mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); - mPayload[iv->id].txCmd = iv->devControlCmd; - //iv->clearCmdQueue(); - //iv->enqueCommand(SystemConfigPara); // read back power limit - } else { - uint8_t cmd = iv->getQueuedCmd(); - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("prepareDevInformCmd 0x")); - DBGHEXLN(cmd); - mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); - mPayload[iv->id].txCmd = cmd; - } - } - - void add(Inverter<> *iv, packet_t *p) { - if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command - mPayload[iv->id].txId = p->packet[0]; - DPRINTLN(DBG_DEBUG, F("Response from info request received")); - uint8_t *pid = &p->packet[9]; - if (*pid == 0x00) { - DPRINTLN(DBG_DEBUG, F("fragment number zero received and ignored")); - } else { - DPRINT(DBG_DEBUG, F("PID: 0x")); - DPRINTLN(DBG_DEBUG, String(*pid, HEX)); - if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { - memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); - mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; - mPayload[iv->id].gotFragment = true; - } - - if ((*pid & ALL_FRAMES) == ALL_FRAMES) { - // Last packet - if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) { - mPayload[iv->id].maxPackId = (*pid & 0x7f); - if (*pid > 0x81) - mPayload[iv->id].lastFound = true; - } - } - } - } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command - DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); - - mPayload[iv->id].txId = p->packet[0]; - iv->clearDevControlRequest(); - - if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { - bool ok = true; - - if((p->packet[10] == 0x00) && (p->packet[11] == 0x00)) { -#ifdef AHOY_MQTT_SUPPORT - mApp->setMqttPowerLimitAck(iv); -#endif - } else { - ok = false; - } - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("has ")); - if(!ok) DBGPRINT(F("not ")); - DBGPRINT(F("accepted power limit set point ")); - DBGPRINT(String(iv->powerLimit[0])); - DBGPRINT(F(" with PowerLimitControl ")); - DBGPRINTLN(String(iv->powerLimit[1])); - - iv->clearCmdQueue(); - iv->enqueCommand(SystemConfigPara); // read back power limit - if(mHighPrioIv == NULL) // do it immediately if possible - mHighPrioIv = iv; - } - iv->devControlCmd = Init; - } - } - - void process(bool retransmit) { - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - if (IV_MI == iv->ivGen) // only process HM inverters - continue; // skip to next inverter - - if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { - // no processing needed if txId is not 0x95 - mPayload[iv->id].complete = true; - continue; // skip to next inverter - } - - if (!mPayload[iv->id].complete) { - bool crcPass, pyldComplete; - crcPass = build(iv->id, &pyldComplete); - if (!crcPass && !pyldComplete) { // payload not complete - if ((mPayload[iv->id].requested) && (retransmit)) { - if (mPayload[iv->id].retransmits < mMaxRetrans) { - mPayload[iv->id].retransmits++; - if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { - // This is required to prevent retransmissions without answer. - DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); - mPayload[iv->id].retransmits = mMaxRetrans; - } else if(iv->devControlCmd == ActivePowerContr) { - DPRINT_IVID(DBG_INFO, iv->id); - DPRINTLN(DBG_INFO, F("retransmit power limit")); - mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); - } else { - if(false == mPayload[iv->id].gotFragment) { - DPRINT_IVID(DBG_WARN, iv->id); - if (mPayload[iv->id].rxTmo) { - DBGPRINTLN(F("nothing received")); - mPayload[iv->id].retransmits = mMaxRetrans; - } else { - DBGPRINTLN(F("nothing received: complete retransmit")); - mPayload[iv->id].txCmd = iv->getQueuedCmd(); - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); - mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); - } - } else { - for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { - if (mPayload[iv->id].len[i] == 0) { - DPRINT_IVID(DBG_WARN, iv->id); - DBGPRINT(F("Frame ")); - DBGPRINT(String(i + 1)); - DBGPRINTLN(F(" missing: Request Retransmit")); - mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); - break; // only request retransmit one frame per loop - } - yield(); - } - } - } - } else if (false == mPayload[iv->id].gotFragment) { - // only if there is no sign of life - mPayload[iv->id].rxTmo = true; // inv might be down, no complete retransmit anymore - } - } - } else if(!crcPass && pyldComplete) { // crc error on complete Payload - if (mPayload[iv->id].retransmits < mMaxRetrans) { - mPayload[iv->id].retransmits++; - DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); - mPayload[iv->id].txCmd = iv->getQueuedCmd(); - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINT(F("prepareDevInformCmd 0x")); - DBGHEXLN(mPayload[iv->id].txCmd); - mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); - } else if (false == mPayload[iv->id].gotFragment) { - // only if there is no sign of life - mPayload[iv->id].rxTmo = true; // inv might be down, no complete retransmit anymore - } - } else { // payload complete -#ifdef undef - DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); - DBGHEXLN(mPayload[iv->id].txCmd); - DPRINT(DBG_INFO, F("procPyld: txid: 0x")); - DBGHEXLN(mPayload[iv->id].txId); -#endif - DPRINT(DBG_DEBUG, F("procPyld: max: ")); - DPRINTLN(DBG_DEBUG, String(mPayload[iv->id].maxPackId)); - - record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser - mPayload[iv->id].complete = true; - mPayload[iv->id].rxTmo = false; - - uint8_t payload[128]; - uint8_t payloadLen = 0; - - memset(payload, 0, 128); - - for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { - memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); - payloadLen += (mPayload[iv->id].len[i]); - yield(); - } - payloadLen -= 2; - -#ifdef undef - if (mSerialDebug) { - DPRINT(DBG_INFO, F("Payload (")); - DBGPRINT(String(payloadLen)); - DBGPRINT(F("): ")); - mSys->Radio.dumpBuf(payload, payloadLen); - } -#endif - - if (NULL == rec) { - DPRINTLN(DBG_ERROR, F("record is NULL!")); - } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { - if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) - mStat->rxSuccess++; - - rec->ts = mPayload[iv->id].ts; - for (uint8_t i = 0; i < rec->length; i++) { - iv->addValue(i, payload, rec); - yield(); - } - iv->doCalculations(); - uint8_t pos = iv->getPosByChFld(CH0, FLD_PAC, rec); - if (pos != 0xff) { - float ac_power = iv->getValue(pos, rec); - mSys->handle_pac (iv, (uint16_t)(ac_power+0.5f)); - } - - notify(mPayload[iv->id].txCmd); - - if(AlarmData == mPayload[iv->id].txCmd) { - uint8_t i = 0; - uint16_t code; - uint32_t start, end; - while(1) { - code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); - if(0 == code) - break; - if (NULL != mCbAlarm) - (mCbAlarm)(code, start, end); - yield(); - } - } - } else { - DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); - DBGPRINT(String(rec->pyldLen)); - DBGPRINTLN(F(" bytes")); - mStat->rxFail++; - } - - iv->setQueuedCmdFinished(); - } - } - yield(); - } - } - - private: - void notify(uint8_t val) { - if(NULL != mCbPayload) - (mCbPayload)(val); - } - - void notify(uint16_t code, uint32_t start, uint32_t endTime) { - if (NULL != mCbAlarm) - (mCbAlarm)(code, start, endTime); - } - - bool build(uint8_t id, bool *complete) { - DPRINTLN(DBG_VERBOSE, F("build")); - uint16_t crc = 0xffff, crcRcv = 0x0000; - if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) - mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; - - // check if all fragments are there - *complete = true; - for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { - if(mPayload[id].len[i] == 0) - *complete = false; - } - if(!*complete) - return false; - - for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { - if (mPayload[id].len[i] > 0) { - if (i == (mPayload[id].maxPackId - 1)) { - crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); - crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); - } else - crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); - } - yield(); - } - - return (crc == crcRcv) ? true : false; - } - - void reset(uint8_t id) { -#ifdef undef - DPRINT_IVID(DBG_INFO, id); - DBGPRINTLN(F("resetPayload")); -#endif - memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); - mPayload[id].txCmd = 0; - mPayload[id].gotFragment = false; - mPayload[id].retransmits = 0; - mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; - mPayload[id].lastFound = false; - mPayload[id].complete = false; - mPayload[id].requested = false; - mPayload[id].ts = *mTimestamp; - mPayload[id].rxTmo = true; // design: dont start with complete retransmit - } - - IApp *mApp; - HMSYSTEM *mSys; - statistics_t *mStat; - uint8_t mMaxRetrans; - uint32_t *mTimestamp; - invPayload_t mPayload[MAX_NUM_INVERTERS]; - bool mSerialDebug; - Inverter<> *mHighPrioIv; - - alarmListenerType mCbAlarm; - payloadListenerType mCbPayload; -}; - -#endif /*__HM_PAYLOAD_H__*/ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __HM_PAYLOAD_H__ +#define __HM_PAYLOAD_H__ + +#include "../utils/dbg.h" +#include "../utils/crc.h" +#include "../config/config.h" +#include + +typedef struct { + uint8_t txCmd; + uint8_t txId; + uint8_t invId; + uint32_t ts; + uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; + uint8_t len[MAX_PAYLOAD_ENTRIES]; + bool complete; + uint8_t maxPackId; + bool lastFound; + uint8_t retransmits; + bool requested; + bool gotFragment; + bool rxTmo; + uint8_t lastFragments; // for send quality measurement +} invPayload_t; + + +typedef std::function payloadListenerType; +typedef std::function alarmListenerType; + + +template +class HmPayload { + public: + HmPayload() {} + + void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { + mApp = app; + mSys = sys; + mStat = stat; + mMaxRetrans = maxRetransmits; + mTimestamp = timestamp; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + reset(i); + } + mSerialDebug = false; + mHighPrioIv = NULL; + mCbAlarm = NULL; + mCbPayload = NULL; + } + + void enableSerialDebug(bool enable) { + mSerialDebug = enable; + } + + void addPayloadListener(payloadListenerType cb) { + mCbPayload = cb; + } + + void addAlarmListener(alarmListenerType cb) { + mCbAlarm = cb; + } + + void loop() { + if (NULL != mHighPrioIv) { + ivSend(mHighPrioIv, true); + mHighPrioIv = NULL; + } + } + + void zeroYieldDay(Inverter<> *iv) { + DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + uint8_t pos; + for(uint8_t ch = 0; ch <= iv->channels; ch++) { + pos = iv->getPosByChFld(ch, FLD_YD, rec); + iv->setValue(pos, rec, 0.0f); + pos = iv->getPosByChFld(ch, FLD_MP, rec); + iv->setValue(pos, rec, 0.0f); + } + } + + void zeroInverterValues(Inverter<> *iv) { + DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + for(uint8_t ch = 0; ch <= iv->channels; ch++) { + uint8_t pos = 0; + for(uint8_t fld = 0; fld < FLD_EVT; fld++) { + switch(fld) { + case FLD_YD: + case FLD_YT: + case FLD_MP: + continue; + } + pos = iv->getPosByChFld(ch, fld, rec); + iv->setValue(pos, rec, 0.0f); + } + } + + notify(RealTimeRunData_Debug); + } + + void ivSendHighPrio(Inverter<> *iv) { + mHighPrioIv = iv; + } + + void ivSend(Inverter<> *iv, bool highPrio = false) { + bool save_rxTmo; + + if(!highPrio) { + if (mPayload[iv->id].requested) { + if (!mPayload[iv->id].complete) + process(false); // no retransmit + + if (!mPayload[iv->id].complete) { + if (mSerialDebug) + DPRINT_IVID(DBG_INFO, iv->id); + if (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId) { + mStat->rxFailNoAnser++; // got nothing + if (mSerialDebug) + DBGPRINTLN(F("enqueued cmd failed/timeout")); + } else { + mStat->rxFail++; // got fragments but not complete response + if (mSerialDebug) { + DBGPRINT(F("no complete Payload received! (retransmits: ")); + DBGPRINT(String(mPayload[iv->id].retransmits)); + DBGPRINTLN(F(")")); + } + } + iv->setQueuedCmdFinished(); // command failed + } + } + } + + save_rxTmo = mPayload[iv->id].rxTmo; + reset(iv->id); + mPayload[iv->id].rxTmo = save_rxTmo; + mPayload[iv->id].requested = true; + + yield(); +#ifdef undef + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Requesting Inv SN ")); + DBGPRINTLN(String(iv->config->serial.u64, HEX)); + } +#endif + + if (iv->getDevControlRequest()) { + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Devcontrol request 0x")); + DBGPRINT(String(iv->devControlCmd, HEX)); + DBGPRINT(F(" power limit ")); + DBGPRINTLN(String(iv->powerLimit[0])); + } + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); + mPayload[iv->id].txCmd = iv->devControlCmd; + //iv->clearCmdQueue(); + //iv->enqueCommand(SystemConfigPara); // read back power limit + } else { + uint8_t cmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(cmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); + mPayload[iv->id].txCmd = cmd; + } + } + + void add(Inverter<> *iv, packet_t *p) { + if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command + mPayload[iv->id].txId = p->packet[0]; + DPRINTLN(DBG_DEBUG, F("Response from info request received")); + uint8_t *pid = &p->packet[9]; + if (*pid == 0x00) { + DPRINTLN(DBG_DEBUG, F("fragment number zero received and ignored")); + } else { + DPRINT(DBG_DEBUG, F("PID: 0x")); + DPRINTLN(DBG_DEBUG, String(*pid, HEX)); + if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { + memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); + mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; + mPayload[iv->id].gotFragment = true; + } + + if ((*pid & ALL_FRAMES) == ALL_FRAMES) { + // Last packet + if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) { + mPayload[iv->id].maxPackId = (*pid & 0x7f); + if (*pid > 0x81) + mPayload[iv->id].lastFound = true; + } + } + } + } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command + DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); + + mPayload[iv->id].txId = p->packet[0]; + iv->clearDevControlRequest(); + + if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { + bool ok = true; + + if((p->packet[10] == 0x00) && (p->packet[11] == 0x00)) { +#ifdef AHOY_MQTT_SUPPORT + mApp->setMqttPowerLimitAck(iv); +#endif + } else { + ok = false; + } + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("has ")); + if(!ok) DBGPRINT(F("not ")); + DBGPRINT(F("accepted power limit set point ")); + DBGPRINT(String(iv->powerLimit[0])); + DBGPRINT(F(" with PowerLimitControl ")); + DBGPRINTLN(String(iv->powerLimit[1])); + + iv->clearCmdQueue(); + iv->enqueCommand(SystemConfigPara); // read back power limit + if(mHighPrioIv == NULL) // do it immediately if possible + mHighPrioIv = iv; + } + iv->devControlCmd = Init; + } + } + + void process(bool retransmit) { + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + if (IV_MI == iv->ivGen) // only process HM inverters + continue; // skip to next inverter + + if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { + // no processing needed if txId is not 0x95 + mPayload[iv->id].complete = true; + continue; // skip to next inverter + } + + if (!mPayload[iv->id].complete) { + bool crcPass, pyldComplete; + uint8 Fragments; + crcPass = build(iv->id, &pyldComplete, &Fragments); + + // evaluate quality of send channel with rcv params + mSys->Radio.evalSendChannelQuality (crcPass, mPayload[iv->id].retransmits, + Fragments, mPayload[iv->id].lastFragments); + mPayload[iv->id].lastFragments = Fragments; + if (!crcPass && !pyldComplete) { // payload not complete + if ((mPayload[iv->id].requested) && (retransmit)) { + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { + // This is required to prevent retransmissions without answer. + DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else if(iv->devControlCmd == ActivePowerContr) { + DPRINT_IVID(DBG_INFO, iv->id); + DPRINTLN(DBG_INFO, F("retransmit power limit")); + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); + } else { + if(false == mPayload[iv->id].gotFragment) { + DPRINT_IVID(DBG_WARN, iv->id); + if (mPayload[iv->id].rxTmo) { + DBGPRINTLN(F("nothing received")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else { + DBGPRINTLN(F("nothing received: complete retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + } + } else { + for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { + if (mPayload[iv->id].len[i] == 0) { + DPRINT_IVID(DBG_WARN, iv->id); + DBGPRINT(F("Frame ")); + DBGPRINT(String(i + 1)); + DBGPRINTLN(F(" missing: Request Retransmit")); + mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); + break; // only request retransmit one frame per loop + } + yield(); + } + } + } + } else if (false == mPayload[iv->id].gotFragment) { + // only if there is no sign of life + mPayload[iv->id].rxTmo = true; // inv might be down, no complete retransmit anymore + } + } + } else if(!crcPass && pyldComplete) { // crc error on complete Payload + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + } else if (false == mPayload[iv->id].gotFragment) { + // only if there is no sign of life + mPayload[iv->id].rxTmo = true; // inv might be down, no complete retransmit anymore + } + } else { // payload complete +#ifdef undef + DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + DPRINT(DBG_INFO, F("procPyld: txid: 0x")); + DBGHEXLN(mPayload[iv->id].txId); +#endif + DPRINT(DBG_DEBUG, F("procPyld: max: ")); + DPRINTLN(DBG_DEBUG, String(mPayload[iv->id].maxPackId)); + + record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser + mPayload[iv->id].complete = true; + mPayload[iv->id].rxTmo = false; + + uint8_t payload[128]; + uint8_t payloadLen = 0; + + memset(payload, 0, 128); + + for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { + memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + payloadLen += (mPayload[iv->id].len[i]); + yield(); + } + payloadLen -= 2; + +#ifdef undef + if (mSerialDebug) { + DPRINT(DBG_INFO, F("Payload (")); + DBGPRINT(String(payloadLen)); + DBGPRINT(F("): ")); + mSys->Radio.dumpBuf(payload, payloadLen); + } +#endif + + if (NULL == rec) { + DPRINTLN(DBG_ERROR, F("record is NULL!")); + } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { + if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) + mStat->rxSuccess++; + + rec->ts = mPayload[iv->id].ts; + for (uint8_t i = 0; i < rec->length; i++) { + iv->addValue(i, payload, rec); + yield(); + } + iv->doCalculations(); + uint8_t pos = iv->getPosByChFld(CH0, FLD_PAC, rec); + + if (pos != 0xff) { + float ac_power = iv->getValue(pos, rec); + mSys->handle_pac (iv, (uint16_t)(ac_power+0.5f)); + } + + notify(mPayload[iv->id].txCmd); + + if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + } + } else { + DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); + DBGPRINT(String(rec->pyldLen)); + DBGPRINTLN(F(" bytes")); + mStat->rxFail++; + } + + iv->setQueuedCmdFinished(); + } + } + yield(); + } + } + + private: + void notify(uint8_t val) { + if(NULL != mCbPayload) + (mCbPayload)(val); + } + + void notify(uint16_t code, uint32_t start, uint32_t endTime) { + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, endTime); + } + + bool build(uint8_t id, bool *complete, uint8_t *fragments) { + DPRINTLN(DBG_VERBOSE, F("build")); + uint16_t crc = 0xffff, crcRcv = 0x0000; + if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + + // check if all fragments are there + *complete = true; + *fragments = 0; + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if(mPayload[id].len[i] == 0) { + *complete = false; + } else { + (*fragments)++; + } + } + if(!*complete) + return false; + + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if (mPayload[id].len[i] > 0) { + if (i == (mPayload[id].maxPackId - 1)) { + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); + crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); + } else + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); + } + yield(); + } + + return (crc == crcRcv) ? true : false; + } + + void reset(uint8_t id) { +#ifdef undef + DPRINT_IVID(DBG_INFO, id); + DBGPRINTLN(F("resetPayload")); +#endif + memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); + mPayload[id].txCmd = 0; + mPayload[id].gotFragment = false; + mPayload[id].retransmits = 0; + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + mPayload[id].lastFound = false; + mPayload[id].complete = false; + mPayload[id].requested = false; + mPayload[id].ts = *mTimestamp; + mPayload[id].rxTmo = true; // design: dont start with complete retransmit + mPayload[id].lastFragments = 0; // for send channel quality measurement + } + + IApp *mApp; + HMSYSTEM *mSys; + statistics_t *mStat; + uint8_t mMaxRetrans; + uint32_t *mTimestamp; + invPayload_t mPayload[MAX_NUM_INVERTERS]; + bool mSerialDebug; + Inverter<> *mHighPrioIv; + + alarmListenerType mCbAlarm; + payloadListenerType mCbPayload; +}; + +#endif /*__HM_PAYLOAD_H__*/ diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 29d8ae0d..3ace71ce 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -1,379 +1,470 @@ -//----------------------------------------------------------------------------- -// 2023 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __RADIO_H__ -#define __RADIO_H__ - -#include "../utils/dbg.h" -#include -#include "../utils/crc.h" -#include "../config/config.h" -#include "SPI.h" - -#define SPI_SPEED 1000000 - -#define RF_CHANNELS 5 - -#define TX_REQ_INFO 0x15 -#define TX_REQ_DEVCONTROL 0x51 -#define ALL_FRAMES 0x80 -#define SINGLE_FRAME 0x81 - -const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; - - -//----------------------------------------------------------------------------- -// MACROS -//----------------------------------------------------------------------------- -#define CP_U32_LittleEndian(buf, v) ({ \ - uint8_t *b = buf; \ - b[0] = ((v >> 24) & 0xff); \ - b[1] = ((v >> 16) & 0xff); \ - b[2] = ((v >> 8) & 0xff); \ - b[3] = ((v ) & 0xff); \ -}) - -#define CP_U32_BigEndian(buf, v) ({ \ - uint8_t *b = buf; \ - b[3] = ((v >> 24) & 0xff); \ - b[2] = ((v >> 16) & 0xff); \ - b[1] = ((v >> 8) & 0xff); \ - b[0] = ((v ) & 0xff); \ -}) - -#define BIT_CNT(x) ((x)<<3) - -//----------------------------------------------------------------------------- -// HM Radio class -//----------------------------------------------------------------------------- -template -class HmRadio { - public: - HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { - if(mSerialDebug) { - DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); - DBGPRINT(String(CE_PIN)); - DBGPRINT(F(", CS_PIN: ")); - DBGPRINT(String(CS_PIN)); - DBGPRINT(F(", SPI_SPEED: ")); - DBGPRINT(String(SPI_SPEED)); - DBGPRINTLN(F(")")); - } - - // Depending on the program, the module can work on 2403, 2423, 2440, 2461 or 2475MHz. - // Channel List 2403, 2423, 2440, 2461, 2475MHz - mRfChLst[0] = 03; - mRfChLst[1] = 23; - mRfChLst[2] = 40; - mRfChLst[3] = 61; - mRfChLst[4] = 75; - - // default channels - mTxChIdx = 2; // Start TX with 40 - mRxChIdx = 0; // Start RX with 03 - - mSendCnt = 0; - mRetransmits = 0; - - mSerialDebug = false; - mIrqRcvd = false; - } - ~HmRadio() {} - - void setup(uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) { - DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); - pinMode(irq, INPUT_PULLUP); - - uint32_t dtuSn = 0x87654321; - uint32_t chipID = 0; // will be filled with last 3 bytes of MAC - #ifdef ESP32 - uint64_t MAC = ESP.getEfuseMac(); - chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); - #else - chipID = ESP.getChipId(); - #endif - if(chipID) { - dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal - for(int i = 0; i < 7; i++) { - dtuSn |= (chipID % 10) << (i * 4); - chipID /= 10; - } - } - // change the byte order of the DTU serial number and append the required 0x01 at the end - DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; - - #ifdef ESP32 - #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 - mSpi = new SPIClass(FSPI); - #else - mSpi = new SPIClass(VSPI); - #endif - mSpi->begin(sclk, miso, mosi, cs); - #else - //the old ESP82xx cannot freely place their SPI pins - mSpi = new SPIClass(); - mSpi->begin(); - #endif - mNrf24.begin(mSpi, ce, cs); - mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms - - mNrf24.setChannel(mRfChLst[mRxChIdx]); - mNrf24.startListening(); - mNrf24.setDataRate(RF24_250KBPS); - mNrf24.setAutoAck(true); - mNrf24.enableDynamicPayloads(); - mNrf24.setCRCLength(RF24_CRC_16); - mNrf24.setAddressWidth(5); - mNrf24.openReadingPipe(1, reinterpret_cast(&DTU_RADIO_ID)); - - // enable all receiving interrupts - mNrf24.maskIRQ(false, false, false); - - DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); - DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr])); - mNrf24.setPALevel(ampPwr & 0x03); - - if(mNrf24.isChipConnected()) { - DPRINTLN(DBG_INFO, F("Radio Config:")); - mNrf24.printPrettyDetails(); - } - else - DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); - } - - bool loop(void) { - if (!mIrqRcvd) - return false; // nothing to do - mIrqRcvd = false; - bool tx_ok, tx_fail, rx_ready; - mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH - mNrf24.flush_tx(); // empty TX FIFO - - // start listening - mNrf24.setChannel(mRfChLst[mRxChIdx]); - mNrf24.startListening(); - - uint32_t startMicros = micros(); - uint32_t loopMillis = millis(); - while (millis()-loopMillis < 400) { - while (micros()-startMicros < 5110) { // listen (4088us or?) 5110us to each channel - if (mIrqRcvd) { - mIrqRcvd = false; - if (getReceived()) { // everything received - return true; - } - } - yield(); - } - // switch to next RX channel - startMicros = micros(); - if(++mRxChIdx >= RF_CHANNELS) - mRxChIdx = 0; - mNrf24.setChannel(mRfChLst[mRxChIdx]); - yield(); - } - // not finished but time is over - return true; - } - - void handleIntr(void) { - mIrqRcvd = true; - } - - bool isChipConnected(void) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); - return mNrf24.isChipConnected(); - } - void enableDebug() { - mSerialDebug = true; - } - - void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) { - DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); - DBGHEXLN(cmd); - initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); - uint8_t cnt = 10; - if (isNoMI) { - mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor - mTxBuf[cnt++] = 0x00; - if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet - mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit - mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit - mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings - mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling - } - } else { //MI 2nd gen. specific - switch (cmd) { - case TurnOn: - //mTxBuf[0] = 0x50; - mTxBuf[9] = 0x55; - mTxBuf[10] = 0xaa; - break; - case TurnOff: - mTxBuf[9] = 0xaa; - mTxBuf[10] = 0x55; - break; - case ActivePowerContr: - cnt++; - mTxBuf[9] = 0x5a; - mTxBuf[10] = 0x5a; - mTxBuf[11] = data[0]; // power limit - break; - default: - return; - } - cnt++; - } - sendPacket(invId, cnt, isRetransmit, isNoMI); - } - - void prepareDevInformCmd(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId, bool isRetransmit, uint8_t reqfld=TX_REQ_INFO) { // might not be necessary to add additional arg. - if(mSerialDebug) { - DPRINT(DBG_DEBUG, F("prepareDevInformCmd 0x")); - DPRINTLN(DBG_DEBUG,String(cmd, HEX)); - } - initPacket(invId, reqfld, ALL_FRAMES); - mTxBuf[10] = cmd; // cid - mTxBuf[11] = 0x00; - CP_U32_LittleEndian(&mTxBuf[12], ts); - if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { - mTxBuf[18] = (alarmMesId >> 8) & 0xff; - mTxBuf[19] = (alarmMesId ) & 0xff; - } - sendPacket(invId, 24, isRetransmit, true); - } - - void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) { - initPacket(invId, mid, pid); - sendPacket(invId, 10, isRetransmit, appendCrc16); - } - - void dumpBuf(uint8_t buf[], uint8_t len) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf")); - for(uint8_t i = 0; i < len; i++) { - DHEX(buf[i]); - DBGPRINT(" "); - } - DBGPRINTLN(""); - } - - uint8_t getDataRate(void) { - if(!mNrf24.isChipConnected()) - return 3; // unkown - return mNrf24.getDataRate(); - } - - bool isPVariant(void) { - return mNrf24.isPVariant(); - } - - std::queue mBufCtrl; - - uint32_t mSendCnt; - uint32_t mRetransmits; - - bool mSerialDebug; - - private: - bool getReceived(void) { - bool tx_ok, tx_fail, rx_ready; - mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH - - bool isLastPackage = false; - while(mNrf24.available()) { - uint8_t len; - len = mNrf24.getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed - if (len > 0) { - packet_t p; - p.ch = mRfChLst[mRxChIdx]; - p.len = len; - mNrf24.read(p.packet, len); - if (p.packet[0] != 0x00) { - mBufCtrl.push(p); - if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command - isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received - else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command - isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received - else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore fragment number zero and MI status messages //#0 was p.packet[0] != 0x00 && - isLastPackage = true; // response from dev control command - } - } - yield(); - } - return isLastPackage; - } - - void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) { -#ifdef undef - if(mSerialDebug) { - DPRINT(DBG_VERBOSE, F("initPacket, mid: ")); - DHEX(mid); - DBGPRINT(F(" pid: ")); - DBGHEXLN(pid); - } -#endif - memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); - mTxBuf[0] = mid; // message id - CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); - CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); - mTxBuf[9] = pid; - } - - void sendPacket(uint64_t invId, uint8_t len, bool isRetransmit, bool appendCrc16=true) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket")); - //DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt)); - - // append crc's - if (appendCrc16 && (len > 10)) { - // crc control data - uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); - mTxBuf[len++] = (crc >> 8) & 0xff; - mTxBuf[len++] = (crc ) & 0xff; - } - // crc over all - mTxBuf[len] = ah::crc8(mTxBuf, len); - len++; - - // set TX and RX channels - mTxChIdx = (mTxChIdx + 1) % RF_CHANNELS; - mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS; - - if(mSerialDebug) { - DPRINT(DBG_INFO, F("TX ")); - DBGPRINT(String(len)); -#ifdef undef - DBGPRINT("B Ch"); - DBGPRINT(String(mRfChLst[mTxChIdx])); - DBGPRINT(F(" | ")); - dumpBuf(mTxBuf, len); -#else - DBGPRINTLN (" Bytes"); -#endif - } - - mNrf24.stopListening(); - mNrf24.setChannel(mRfChLst[mTxChIdx]); - mNrf24.openWritingPipe(reinterpret_cast(&invId)); - mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response - - if(isRetransmit) - mRetransmits++; - else - mSendCnt++; - } - - volatile bool mIrqRcvd; - uint64_t DTU_RADIO_ID; - - uint8_t mRfChLst[RF_CHANNELS]; - uint8_t mTxChIdx; - uint8_t mRxChIdx; - - SPIClass* mSpi; - RF24 mNrf24; - uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; -}; - -#endif /*__RADIO_H__*/ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __RADIO_H__ +#define __RADIO_H__ + +#include "../utils/dbg.h" +#include +#include "../utils/crc.h" +#include "../config/config.h" +#include "SPI.h" + +#define SPI_SPEED 1000000 + +#define RF_CHANNELS 5 + +#define TX_REQ_INFO 0x15 +#define TX_REQ_DEVCONTROL 0x51 +#define ALL_FRAMES 0x80 +#define SINGLE_FRAME 0x81 + +#define SEND_CHANNEL_QUALITY_INTEGRATOR_SIZE 4 +#define SEND_CHANNEL_MAX_QUALITY 4 +#define SEND_CHANNEL_MIN_QUALITY -6 +#define SEND_CHANNEL_QUALITY_GOOD 2 +#define SEND_CHANNEL_QUALITY_OK 1 +#define SEND_CHANNEL_QUALITY_NEUTRAL 0 +#define SEND_CHANNEL_QUALITY_LOW -1 +#define SEND_CHANNEL_QUALITY_BAD -2 + +const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; + + +//----------------------------------------------------------------------------- +// MACROS +//----------------------------------------------------------------------------- +#define CP_U32_LittleEndian(buf, v) ({ \ + uint8_t *b = buf; \ + b[0] = ((v >> 24) & 0xff); \ + b[1] = ((v >> 16) & 0xff); \ + b[2] = ((v >> 8) & 0xff); \ + b[3] = ((v ) & 0xff); \ +}) + +#define CP_U32_BigEndian(buf, v) ({ \ + uint8_t *b = buf; \ + b[3] = ((v >> 24) & 0xff); \ + b[2] = ((v >> 16) & 0xff); \ + b[1] = ((v >> 8) & 0xff); \ + b[0] = ((v ) & 0xff); \ +}) + +#define BIT_CNT(x) ((x)<<3) + +//----------------------------------------------------------------------------- +// HM Radio class +//----------------------------------------------------------------------------- +template +class HmRadio { + public: + HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { + if(mSerialDebug) { + DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); + DBGPRINT(String(CE_PIN)); + DBGPRINT(F(", CS_PIN: ")); + DBGPRINT(String(CS_PIN)); + DBGPRINT(F(", SPI_SPEED: ")); + DBGPRINT(String(SPI_SPEED)); + DBGPRINTLN(F(")")); + } + + // Depending on the program, the module can work on 2403, 2423, 2440, 2461 or 2475MHz. + // Channel List 2403, 2423, 2440, 2461, 2475MHz + mRfChLst[0] = 03; + mRfChLst[1] = 23; + mRfChLst[2] = 40; + mRfChLst[3] = 61; + mRfChLst[4] = 75; + + // default channels + mTxChIdx = 2; // Start TX with 40 + mRxChIdx = 0; // Start RX with 03 + + mSendCnt = 0; + mRetransmits = 0; + + mSerialDebug = false; + mIrqRcvd = false; + } + ~HmRadio() {} + + void setup(uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) { + DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); + pinMode(irq, INPUT_PULLUP); + + uint32_t dtuSn = 0x87654321; + uint32_t chipID = 0; // will be filled with last 3 bytes of MAC + #ifdef ESP32 + uint64_t MAC = ESP.getEfuseMac(); + chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); + #else + chipID = ESP.getChipId(); + #endif + if(chipID) { + dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal + for(int i = 0; i < 7; i++) { + dtuSn |= (chipID % 10) << (i * 4); + chipID /= 10; + } + } + // change the byte order of the DTU serial number and append the required 0x01 at the end + DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; + + #ifdef ESP32 + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + mSpi = new SPIClass(FSPI); + #else + mSpi = new SPIClass(VSPI); + #endif + mSpi->begin(sclk, miso, mosi, cs); + #else + //the old ESP82xx cannot freely place their SPI pins + mSpi = new SPIClass(); + mSpi->begin(); + #endif + mNrf24.begin(mSpi, ce, cs); + mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms + + mNrf24.setChannel(mRfChLst[mRxChIdx]); + mNrf24.startListening(); + mNrf24.setDataRate(RF24_250KBPS); + mNrf24.setAutoAck(true); + mNrf24.enableDynamicPayloads(); + mNrf24.setCRCLength(RF24_CRC_16); + mNrf24.setAddressWidth(5); + mNrf24.openReadingPipe(1, reinterpret_cast(&DTU_RADIO_ID)); + + // enable all receiving interrupts + mNrf24.maskIRQ(false, false, false); + + DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); + DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr])); + mNrf24.setPALevel(ampPwr & 0x03); + + if(mNrf24.isChipConnected()) { + DPRINTLN(DBG_INFO, F("Radio Config:")); + mNrf24.printPrettyDetails(); + } + else + DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); + } + + bool loop(void) { + if (!mIrqRcvd) + return false; // nothing to do + mIrqRcvd = false; + bool tx_ok, tx_fail, rx_ready; + mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH + mNrf24.flush_tx(); // empty TX FIFO + + // start listening + mNrf24.setChannel(mRfChLst[mRxChIdx]); + mNrf24.startListening(); + + uint32_t startMicros = micros(); + uint32_t loopMillis = millis(); + while (millis()-loopMillis < 400) { + while (micros()-startMicros < 5110) { // listen (4088us or?) 5110us to each channel + if (mIrqRcvd) { + mIrqRcvd = false; + if (getReceived()) { // everything received + return true; + } + } + yield(); + } + // switch to next RX channel + startMicros = micros(); + if(++mRxChIdx >= RF_CHANNELS) + mRxChIdx = 0; + mNrf24.setChannel(mRfChLst[mRxChIdx]); + yield(); + } + // not finished but time is over + return true; + } + + void handleIntr(void) { + mIrqRcvd = true; + } + + bool isChipConnected(void) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); + return mNrf24.isChipConnected(); + } + void enableDebug() { + mSerialDebug = true; + } + + void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) { + DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); + DBGHEXLN(cmd); + initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); + uint8_t cnt = 10; + if (isNoMI) { + mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor + mTxBuf[cnt++] = 0x00; + if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet + mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit + mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit + mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings + mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling + } + } else { //MI 2nd gen. specific + switch (cmd) { + case TurnOn: + //mTxBuf[0] = 0x50; + mTxBuf[9] = 0x55; + mTxBuf[10] = 0xaa; + break; + case TurnOff: + mTxBuf[9] = 0xaa; + mTxBuf[10] = 0x55; + break; + case ActivePowerContr: + cnt++; + mTxBuf[9] = 0x5a; + mTxBuf[10] = 0x5a; + mTxBuf[11] = data[0]; // power limit + break; + default: + return; + } + cnt++; + } + sendPacket(invId, cnt, isRetransmit, isNoMI); + } + + void prepareDevInformCmd(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId, bool isRetransmit, uint8_t reqfld=TX_REQ_INFO) { // might not be necessary to add additional arg. + if(mSerialDebug) { + DPRINT(DBG_DEBUG, F("prepareDevInformCmd 0x")); + DPRINTLN(DBG_DEBUG,String(cmd, HEX)); + } + initPacket(invId, reqfld, ALL_FRAMES); + mTxBuf[10] = cmd; // cid + mTxBuf[11] = 0x00; + CP_U32_LittleEndian(&mTxBuf[12], ts); + if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { + mTxBuf[18] = (alarmMesId >> 8) & 0xff; + mTxBuf[19] = (alarmMesId ) & 0xff; + } + sendPacket(invId, 24, isRetransmit, true); + } + + void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) { + initPacket(invId, mid, pid); + sendPacket(invId, 10, isRetransmit, appendCrc16); + } + + void dumpBuf(uint8_t buf[], uint8_t len) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf")); + for(uint8_t i = 0; i < len; i++) { + DHEX(buf[i]); + DBGPRINT(" "); + } + DBGPRINTLN(""); + } + + uint8_t getDataRate(void) { + if(!mNrf24.isChipConnected()) + return 3; // unkown + return mNrf24.getDataRate(); + } + + bool isPVariant(void) { + return mNrf24.isPVariant(); + } + + bool isNewSendChannel () + { + return mTxChIdx != mTxLastChIdx; + } + + uint8_t getNextSendChannelIndex (void) + { + // start with the next index: round robbin in case of same max bad quality for all channels + uint8_t bestIndex = (mTxChIdx + 1) % RF_CHANNELS; + uint8_t curIndex = (bestIndex + 1) % RF_CHANNELS; + uint16_t i; + + for (i=1; i mChQuality[bestIndex]) { + bestIndex = curIndex; + } + curIndex = (curIndex + 1) % RF_CHANNELS; + } + return bestIndex; + } + + void addSendChannelQuality (int8_t quality) + { + // continous averaging + // assume: mTxChIdx is still the last send channel index used + quality = mChQuality[mTxChIdx] + quality; + if (quality < SEND_CHANNEL_MIN_QUALITY) { + quality = SEND_CHANNEL_MIN_QUALITY; + } else if (quality > SEND_CHANNEL_MAX_QUALITY) { + quality = SEND_CHANNEL_MAX_QUALITY; + } + mChQuality[mTxChIdx] = quality; + } + + void evalSendChannelQuality (bool crcPass, uint8_t Retransmits, uint8_t rxFragments, + uint8_t lastRxFragments) + { + if (lastRxFragments == rxFragments) { + // nothing received: send probably lost + if (!Retransmits || isNewSendChannel()) { + // dont overestimate burst distortion + addSendChannelQuality (SEND_CHANNEL_QUALITY_BAD); + } + } else if (!lastRxFragments && crcPass) { + if (!Retransmits || isNewSendChannel()) { + // every fragment received successfull immediately + addSendChannelQuality (SEND_CHANNEL_QUALITY_GOOD); + } else { + // every fragment received successfully + addSendChannelQuality (SEND_CHANNEL_QUALITY_OK); + } + } else if (crcPass) { + if (isNewSendChannel ()) { + // last Fragment successfully received on new send channel + addSendChannelQuality (SEND_CHANNEL_QUALITY_OK); + } + } else if (!Retransmits || isNewSendChannel()) { + // no complete receive for this send channel + addSendChannelQuality (SEND_CHANNEL_QUALITY_LOW); + } + } + + void resetSendChannelQuality () + { + for(uint8_t i = 0; i < RF_CHANNELS; i++) { + mChQuality[mTxChIdx] = 0; + } + } + + void dumpSendQuality() + { + for(uint8_t i = 0; i < RF_CHANNELS; i++) { + DBGPRINT(" " + String (mChQuality[i])); + } + } + + std::queue mBufCtrl; + + uint32_t mSendCnt; + uint32_t mRetransmits; + + bool mSerialDebug; + + private: + bool getReceived(void) { + bool tx_ok, tx_fail, rx_ready; + mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH + + bool isLastPackage = false; + while(mNrf24.available()) { + uint8_t len; + len = mNrf24.getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed + if (len > 0) { + packet_t p; + p.ch = mRfChLst[mRxChIdx]; + p.len = len; + mNrf24.read(p.packet, len); + if (p.packet[0] != 0x00) { + mBufCtrl.push(p); + if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command + isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received + else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command + isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received + else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore fragment number zero and MI status messages //#0 was p.packet[0] != 0x00 && + isLastPackage = true; // response from dev control command + } + } + yield(); + } + return isLastPackage; + } + + void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) { +#ifdef undef + if(mSerialDebug) { + DPRINT(DBG_VERBOSE, F("initPacket, mid: ")); + DHEX(mid); + DBGPRINT(F(" pid: ")); + DBGHEXLN(pid); + } +#endif + memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); + mTxBuf[0] = mid; // message id + CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); + CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); + mTxBuf[9] = pid; + } + + void sendPacket(uint64_t invId, uint8_t len, bool isRetransmit, bool appendCrc16=true) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket")); + //DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt)); + + // append crc's + if (appendCrc16 && (len > 10)) { + // crc control data + uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); + mTxBuf[len++] = (crc >> 8) & 0xff; + mTxBuf[len++] = (crc ) & 0xff; + } + // crc over all + mTxBuf[len] = ah::crc8(mTxBuf, len); + len++; + + // set TX and RX channels + + mTxLastChIdx = mTxChIdx; + mTxChIdx = getNextSendChannelIndex (); + mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS; + + if(mSerialDebug) { +#ifdef undef + DPRINT(DBG_INFO, F("TX ")); + DBGPRINT(String(len)); + DBGPRINT("B Ch"); + DBGPRINT(String(mRfChLst[mTxChIdx])); + DBGPRINT(F(" | ")); + dumpBuf(mTxBuf, len); +#else + DPRINT(DBG_INFO, F("TX (Ch ") + String (mRfChLst[mTxChIdx]) + "), " + + String (len) + " Bytes, Quality:"); + dumpSendQuality(); + DBGPRINTLN(""); +#endif + } + mNrf24.stopListening(); + mNrf24.setChannel(mRfChLst[mTxChIdx]); + mNrf24.openWritingPipe(reinterpret_cast(&invId)); + mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response + + if(isRetransmit) + mRetransmits++; + else + mSendCnt++; + } + + volatile bool mIrqRcvd; + uint64_t DTU_RADIO_ID; + + uint8_t mRfChLst[RF_CHANNELS]; + int8_t mChQuality[RF_CHANNELS]; + uint8_t mTxChIdx; + uint8_t mTxLastChIdx; + uint8_t mRxChIdx; + + SPIClass* mSpi; + RF24 mNrf24; + uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; +}; + +#endif /*__RADIO_H__*/