From 16877a8ea37e9741fdcdb9919f88918c03e802e8 Mon Sep 17 00:00:00 2001 From: Marc-Philip Date: Thu, 13 Apr 2023 09:18:07 +0200 Subject: [PATCH 01/66] fix typo --- webapp/src/locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 527bcaff..ee60ecf2 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -418,7 +418,7 @@ "Status": "Status", "Send": "Send", "Receive": "Receive", - "StatusHint": "Hint: The inverter is power by it's DC input. If there is no sun, the inverter is off. Requests can still be sent.", + "StatusHint": "Hint: The inverter is powered by it's DC input. If there is no sun, the inverter is off. Requests can still be sent.", "Type": "Type", "Action": "Action", "DeleteInverter": "Delete inverter", @@ -530,4 +530,4 @@ "ValueSelected": "Selected", "ValueActive": "Active" } -} \ No newline at end of file +} From 5d289dac47b2d474a14e24dd8f246a4896942077 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 13 Apr 2023 19:21:29 +0200 Subject: [PATCH 02/66] vscode: Recommend additional extensions for development --- .vscode/extensions.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 080e70d0..0b8b3cc0 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,7 +2,10 @@ // See http://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ - "platformio.platformio-ide" + "platformio.platformio-ide", + "DavidAnson.vscode-markdownlint", + "Vue.volar", + "Vue.vscode-typescript-vue-plugin" ], "unwantedRecommendations": [ "ms-vscode.cpptools-extension-pack" From a554423d393eb63e1346a4119ab170dca20ad523 Mon Sep 17 00:00:00 2001 From: Marc-Philip Date: Fri, 14 Apr 2023 08:14:23 +0200 Subject: [PATCH 03/66] one more typo --- webapp/src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index ee60ecf2..11748f0f 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -418,7 +418,7 @@ "Status": "Status", "Send": "Send", "Receive": "Receive", - "StatusHint": "Hint: The inverter is powered by it's DC input. If there is no sun, the inverter is off. Requests can still be sent.", + "StatusHint": "Hint: The inverter is powered by its DC input. If there is no sun, the inverter is off. Requests can still be sent.", "Type": "Type", "Action": "Action", "DeleteInverter": "Delete inverter", From a252d2ac3a14b054db7c635ff98f56472616c39c Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sun, 5 Mar 2023 18:26:19 +0100 Subject: [PATCH 04/66] Added CMT2300 driver --- lib/CMT2300a/cmt2300a.c | 778 +++++++++++++++++++++++++++++++++ lib/CMT2300a/cmt2300a.h | 96 ++++ lib/CMT2300a/cmt2300a_defs.h | 606 +++++++++++++++++++++++++ lib/CMT2300a/cmt2300a_hal.c | 76 ++++ lib/CMT2300a/cmt2300a_hal.h | 51 +++ lib/CMT2300a/cmt2300a_params.h | 215 +++++++++ lib/CMT2300a/cmt_spi3.c | 120 +++++ lib/CMT2300a/cmt_spi3.h | 14 + platformio.ini | 5 + 9 files changed, 1961 insertions(+) create mode 100644 lib/CMT2300a/cmt2300a.c create mode 100644 lib/CMT2300a/cmt2300a.h create mode 100644 lib/CMT2300a/cmt2300a_defs.h create mode 100644 lib/CMT2300a/cmt2300a_hal.c create mode 100644 lib/CMT2300a/cmt2300a_hal.h create mode 100644 lib/CMT2300a/cmt2300a_params.h create mode 100644 lib/CMT2300a/cmt_spi3.c create mode 100644 lib/CMT2300a/cmt_spi3.h diff --git a/lib/CMT2300a/cmt2300a.c b/lib/CMT2300a/cmt2300a.c new file mode 100644 index 00000000..45b09f5e --- /dev/null +++ b/lib/CMT2300a/cmt2300a.c @@ -0,0 +1,778 @@ +/* + * THE FOLLOWING FIRMWARE IS PROVIDED: (1) "AS IS" WITH NO WARRANTY; AND + * (2)TO ENABLE ACCESS TO CODING INFORMATION TO GUIDE AND FACILITATE CUSTOMER. + * CONSEQUENTLY, CMOSTEK SHALL NOT BE HELD LIABLE FOR ANY DIRECT, INDIRECT OR + * CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING FROM THE CONTENT + * OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE CODING INFORMATION + * CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS. + * + * Copyright (C) CMOSTEK SZ. + */ + +/*! + * @file cmt2300a.c + * @brief CMT2300A transceiver RF chip driver + * + * @version 1.3 + * @date Jul 17 2017 + * @author CMOSTEK R@D + */ + +#include "cmt2300a.h" + +/*! ******************************************************** + * @name CMT2300A_SoftReset + * @desc Soft reset. + * *********************************************************/ +void CMT2300A_SoftReset(void) +{ + CMT2300A_WriteReg(0x7F, 0xFF); +} + +/*! ******************************************************** + * @name CMT2300A_GetChipStatus + * @desc Get the chip status. + * @return + * CMT2300A_STA_PUP + * CMT2300A_STA_SLEEP + * CMT2300A_STA_STBY + * CMT2300A_STA_RFS + * CMT2300A_STA_TFS + * CMT2300A_STA_RX + * CMT2300A_STA_TX + * CMT2300A_STA_EEPROM + * CMT2300A_STA_ERROR + * CMT2300A_STA_CAL + * *********************************************************/ +uint8_t CMT2300A_GetChipStatus(void) +{ + return CMT2300A_ReadReg(CMT2300A_CUS_MODE_STA) & CMT2300A_MASK_CHIP_MODE_STA; +} + +/*! ******************************************************** + * @name CMT2300A_AutoSwitchStatus + * @desc Auto switch the chip status, and 10 ms as timeout. + * @param nGoCmd: the chip next status + * @return TRUE or FALSE + * *********************************************************/ +bool CMT2300A_AutoSwitchStatus(uint8_t nGoCmd) +{ +#ifdef ENABLE_AUTO_SWITCH_CHIP_STATUS + uint32_t nBegTick = CMT2300A_GetTickCount(); + uint8_t nWaitStatus = 0; + + switch (nGoCmd) { + case CMT2300A_GO_SLEEP: + nWaitStatus = CMT2300A_STA_SLEEP; + break; + case CMT2300A_GO_STBY: + nWaitStatus = CMT2300A_STA_STBY; + break; + case CMT2300A_GO_TFS: + nWaitStatus = CMT2300A_STA_TFS; + break; + case CMT2300A_GO_TX: + nWaitStatus = CMT2300A_STA_TX; + break; + case CMT2300A_GO_RFS: + nWaitStatus = CMT2300A_STA_RFS; + break; + case CMT2300A_GO_RX: + nWaitStatus = CMT2300A_STA_RX; + break; + } + + CMT2300A_WriteReg(CMT2300A_CUS_MODE_CTL, nGoCmd); + + while (CMT2300A_GetTickCount() - nBegTick < 10) { + CMT2300A_DelayUs(100); + + if (nWaitStatus == CMT2300A_GetChipStatus()) + return true; + + if (CMT2300A_GO_TX == nGoCmd) { + CMT2300A_DelayUs(100); + + if (CMT2300A_MASK_TX_DONE_FLG & CMT2300A_ReadReg(CMT2300A_CUS_INT_CLR1)) + return true; + } + + if (CMT2300A_GO_RX == nGoCmd) { + CMT2300A_DelayUs(100); + + if (CMT2300A_MASK_PKT_OK_FLG & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG)) + return true; + } + } + + return false; + +#else + CMT2300A_WriteReg(CMT2300A_CUS_MODE_CTL, nGoCmd); + return true; +#endif +} + +/*! ******************************************************** + * @name CMT2300A_GoSleep + * @desc Entry SLEEP mode. + * @return TRUE or FALSE + * *********************************************************/ +bool CMT2300A_GoSleep(void) +{ + return CMT2300A_AutoSwitchStatus(CMT2300A_GO_SLEEP); +} + +/*! ******************************************************** + * @name CMT2300A_GoStby + * @desc Entry Sleep mode. + * @return TRUE or FALSE + * *********************************************************/ +bool CMT2300A_GoStby(void) +{ + return CMT2300A_AutoSwitchStatus(CMT2300A_GO_STBY); +} + +/*! ******************************************************** + * @name CMT2300A_GoTFS + * @desc Entry TFS mode. + * @return TRUE or FALSE + * *********************************************************/ +bool CMT2300A_GoTFS(void) +{ + return CMT2300A_AutoSwitchStatus(CMT2300A_GO_TFS); +} + +/*! ******************************************************** + * @name CMT2300A_GoRFS + * @desc Entry RFS mode. + * @return TRUE or FALSE + * *********************************************************/ +bool CMT2300A_GoRFS(void) +{ + return CMT2300A_AutoSwitchStatus(CMT2300A_GO_RFS); +} + +/*! ******************************************************** + * @name CMT2300A_GoTx + * @desc Entry Tx mode. + * @return TRUE or FALSE + * *********************************************************/ +bool CMT2300A_GoTx(void) +{ + return CMT2300A_AutoSwitchStatus(CMT2300A_GO_TX); +} + +/*! ******************************************************** + * @name CMT2300A_GoRx + * @desc Entry Rx mode. + * @return TRUE or FALSE + * *********************************************************/ +bool CMT2300A_GoRx(void) +{ + return CMT2300A_AutoSwitchStatus(CMT2300A_GO_RX); +} + +/*! ******************************************************** + * @name CMT2300A_ConfigGpio + * @desc Config GPIO pins mode. + * @param nGpioSel: GPIO1_SEL | GPIO2_SEL | GPIO3_SEL | GPIO4_SEL + * GPIO1_SEL: + * CMT2300A_GPIO1_SEL_DOUT/DIN + * CMT2300A_GPIO1_SEL_INT1 + * CMT2300A_GPIO1_SEL_INT2 + * CMT2300A_GPIO1_SEL_DCLK + * + * GPIO2_SEL: + * CMT2300A_GPIO2_SEL_INT1 + * CMT2300A_GPIO2_SEL_INT2 + * CMT2300A_GPIO2_SEL_DOUT/DIN + * CMT2300A_GPIO2_SEL_DCLK + * + * GPIO3_SEL: + * CMT2300A_GPIO3_SEL_CLKO + * CMT2300A_GPIO3_SEL_DOUT/DIN + * CMT2300A_GPIO3_SEL_INT2 + * CMT2300A_GPIO3_SEL_DCLK + * + * GPIO4_SEL: + * CMT2300A_GPIO4_SEL_RSTIN + * CMT2300A_GPIO4_SEL_INT1 + * CMT2300A_GPIO4_SEL_DOUT + * CMT2300A_GPIO4_SEL_DCLK + * *********************************************************/ +void CMT2300A_ConfigGpio(uint8_t nGpioSel) +{ + CMT2300A_WriteReg(CMT2300A_CUS_IO_SEL, nGpioSel); +} + +/*! ******************************************************** + * @name CMT2300A_ConfigInterrupt + * @desc Config interrupt on INT1 and INT2. + * @param nInt1Sel, nInt2Sel + * CMT2300A_INT_SEL_RX_ACTIVE + * CMT2300A_INT_SEL_TX_ACTIVE + * CMT2300A_INT_SEL_RSSI_VLD + * CMT2300A_INT_SEL_PREAM_OK + * CMT2300A_INT_SEL_SYNC_OK + * CMT2300A_INT_SEL_NODE_OK + * CMT2300A_INT_SEL_CRC_OK + * CMT2300A_INT_SEL_PKT_OK + * CMT2300A_INT_SEL_SL_TMO + * CMT2300A_INT_SEL_RX_TMO + * CMT2300A_INT_SEL_TX_DONE + * CMT2300A_INT_SEL_RX_FIFO_NMTY + * CMT2300A_INT_SEL_RX_FIFO_TH + * CMT2300A_INT_SEL_RX_FIFO_FULL + * CMT2300A_INT_SEL_RX_FIFO_WBYTE + * CMT2300A_INT_SEL_RX_FIFO_OVF + * CMT2300A_INT_SEL_TX_FIFO_NMTY + * CMT2300A_INT_SEL_TX_FIFO_TH + * CMT2300A_INT_SEL_TX_FIFO_FULL + * CMT2300A_INT_SEL_STATE_IS_STBY + * CMT2300A_INT_SEL_STATE_IS_FS + * CMT2300A_INT_SEL_STATE_IS_RX + * CMT2300A_INT_SEL_STATE_IS_TX + * CMT2300A_INT_SEL_LED + * CMT2300A_INT_SEL_TRX_ACTIVE + * CMT2300A_INT_SEL_PKT_DONE + * *********************************************************/ +void CMT2300A_ConfigInterrupt(uint8_t nInt1Sel, uint8_t nInt2Sel) +{ + nInt1Sel &= CMT2300A_MASK_INT1_SEL; + nInt1Sel |= (~CMT2300A_MASK_INT1_SEL) & CMT2300A_ReadReg(CMT2300A_CUS_INT1_CTL); + CMT2300A_WriteReg(CMT2300A_CUS_INT1_CTL, nInt1Sel); + + nInt2Sel &= CMT2300A_MASK_INT2_SEL; + nInt2Sel |= (~CMT2300A_MASK_INT2_SEL) & CMT2300A_ReadReg(CMT2300A_CUS_INT2_CTL); + CMT2300A_WriteReg(CMT2300A_CUS_INT2_CTL, nInt2Sel); +} + +/*! ******************************************************** + * @name CMT2300A_SetInterruptPolar + * @desc Set the polarity of the interrupt. + * @param bEnable(TRUE): active-high (default) + * bEnable(FALSE): active-low + * *********************************************************/ +void CMT2300A_SetInterruptPolar(bool bActiveHigh) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_INT1_CTL); + + if (bActiveHigh) + tmp &= ~CMT2300A_MASK_INT_POLAR; + else + tmp |= CMT2300A_MASK_INT_POLAR; + + CMT2300A_WriteReg(CMT2300A_CUS_INT1_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_SetFifoThreshold + * @desc Set FIFO threshold. + * @param nFifoThreshold + * *********************************************************/ +void CMT2300A_SetFifoThreshold(uint8_t nFifoThreshold) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_PKT29); + + tmp &= ~CMT2300A_MASK_FIFO_TH; + tmp |= nFifoThreshold & CMT2300A_MASK_FIFO_TH; + + CMT2300A_WriteReg(CMT2300A_CUS_PKT29, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableAntennaSwitch + * @desc Enable antenna switch, output TX_ACTIVE/RX_ACTIVE + * via GPIO1/GPIO2. + * @param nMode + * 0: RF_SWT1_EN=1, RF_SWT2_EN=0 + * GPIO1: RX_ACTIVE, GPIO2: TX_ACTIVE + * 1: RF_SWT1_EN=0, RF_SWT2_EN=1 + * GPIO1: RX_ACTIVE, GPIO2: ~RX_ACTIVE + * *********************************************************/ +void CMT2300A_EnableAntennaSwitch(uint8_t nMode) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_INT1_CTL); + + if (0 == nMode) { + tmp |= CMT2300A_MASK_RF_SWT1_EN; + tmp &= ~CMT2300A_MASK_RF_SWT2_EN; + } else if (1 == nMode) { + tmp &= ~CMT2300A_MASK_RF_SWT1_EN; + tmp |= CMT2300A_MASK_RF_SWT2_EN; + } + + CMT2300A_WriteReg(CMT2300A_CUS_INT1_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableInterrupt + * @desc Enable interrupt. + * @param nEnable + * CMT2300A_MASK_SL_TMO_EN | + * CMT2300A_MASK_RX_TMO_EN | + * CMT2300A_MASK_TX_DONE_EN | + * CMT2300A_MASK_PREAM_OK_EN | + * CMT2300A_MASK_SYNC_OK_EN | + * CMT2300A_MASK_NODE_OK_EN | + * CMT2300A_MASK_CRC_OK_EN | + * CMT2300A_MASK_PKT_DONE_EN + * *********************************************************/ +void CMT2300A_EnableInterrupt(uint8_t nEnable) +{ + CMT2300A_WriteReg(CMT2300A_CUS_INT_EN, nEnable); +} + +/*! ******************************************************** + * @name CMT2300A_EnableRxFifoAutoClear + * @desc Auto clear Rx FIFO before entry Rx mode. + * @param bEnable(TRUE): Enable it(default) + * bEnable(FALSE): Disable it + * *********************************************************/ +void CMT2300A_EnableRxFifoAutoClear(bool bEnable) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_FIFO_CTL); + + if (bEnable) + tmp &= ~CMT2300A_MASK_FIFO_AUTO_CLR_DIS; + else + tmp |= CMT2300A_MASK_FIFO_AUTO_CLR_DIS; + + CMT2300A_WriteReg(CMT2300A_CUS_FIFO_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableFifoMerge + * @desc Enable FIFO merge. + * @param bEnable(TRUE): use a single 64-byte FIFO for either Tx or Rx + * bEnable(FALSE): use a 32-byte FIFO for Tx and another 32-byte FIFO for Rx(default) + * *********************************************************/ +void CMT2300A_EnableFifoMerge(bool bEnable) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_FIFO_CTL); + + if (bEnable) + tmp |= CMT2300A_MASK_FIFO_MERGE_EN; + else + tmp &= ~CMT2300A_MASK_FIFO_MERGE_EN; + + CMT2300A_WriteReg(CMT2300A_CUS_FIFO_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableReadFifo + * @desc Enable SPI to read the FIFO. + * *********************************************************/ +void CMT2300A_EnableReadFifo(void) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_FIFO_CTL); + tmp &= ~CMT2300A_MASK_SPI_FIFO_RD_WR_SEL; + tmp &= ~CMT2300A_MASK_FIFO_RX_TX_SEL; + CMT2300A_WriteReg(CMT2300A_CUS_FIFO_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableWriteFifo + * @desc Enable SPI to write the FIFO. + * *********************************************************/ +void CMT2300A_EnableWriteFifo(void) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_FIFO_CTL); + tmp |= CMT2300A_MASK_SPI_FIFO_RD_WR_SEL; + tmp |= CMT2300A_MASK_FIFO_RX_TX_SEL; + CMT2300A_WriteReg(CMT2300A_CUS_FIFO_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_RestoreFifo + * @desc Restore the FIFO. + * *********************************************************/ +void CMT2300A_RestoreFifo(void) +{ + CMT2300A_WriteReg(CMT2300A_CUS_FIFO_CLR, CMT2300A_MASK_FIFO_RESTORE); +} + +/*! ******************************************************** + * @name CMT2300A_ClearFifo + * @desc Clear the Tx FIFO. + * @return FIFO flags + * CMT2300A_MASK_RX_FIFO_FULL_FLG | + * CMT2300A_MASK_RX_FIFO_NMTY_FLG | + * CMT2300A_MASK_RX_FIFO_TH_FLG | + * CMT2300A_MASK_RX_FIFO_OVF_FLG | + * CMT2300A_MASK_TX_FIFO_FULL_FLG | + * CMT2300A_MASK_TX_FIFO_NMTY_FLG | + * CMT2300A_MASK_TX_FIFO_TH_FLG + * *********************************************************/ +uint8_t CMT2300A_ClearTxFifo(void) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_FIFO_FLAG); + CMT2300A_WriteReg(CMT2300A_CUS_FIFO_CLR, CMT2300A_MASK_FIFO_CLR_TX); + return tmp; +} + +/*! ******************************************************** + * @name CMT2300A_ClearFifo + * @desc Clear the Rx FIFO. + * @return FIFO flags + * CMT2300A_MASK_RX_FIFO_FULL_FLG | + * CMT2300A_MASK_RX_FIFO_NMTY_FLG | + * CMT2300A_MASK_RX_FIFO_TH_FLG | + * CMT2300A_MASK_RX_FIFO_OVF_FLG | + * CMT2300A_MASK_TX_FIFO_FULL_FLG | + * CMT2300A_MASK_TX_FIFO_NMTY_FLG | + * CMT2300A_MASK_TX_FIFO_TH_FLG + * *********************************************************/ +uint8_t CMT2300A_ClearRxFifo(void) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_FIFO_FLAG); + CMT2300A_WriteReg(CMT2300A_CUS_FIFO_CLR, CMT2300A_MASK_FIFO_CLR_RX); + return tmp; +} + +/*! ******************************************************** + * @name CMT2300A_ClearInterruptFlags + * @desc Clear all interrupt flags. + * @return Some interrupt flags + * CMT2300A_MASK_SL_TMO_EN | + * CMT2300A_MASK_RX_TMO_EN | + * CMT2300A_MASK_TX_DONE_EN | + * CMT2300A_MASK_PREAM_OK_FLG | + * CMT2300A_MASK_SYNC_OK_FLG | + * CMT2300A_MASK_NODE_OK_FLG | + * CMT2300A_MASK_CRC_OK_FLG | + * CMT2300A_MASK_PKT_OK_FLG + * *********************************************************/ +uint8_t CMT2300A_ClearInterruptFlags(void) +{ + uint8_t nFlag1, nFlag2; + uint8_t nClr1 = 0; + uint8_t nClr2 = 0; + uint8_t nRet = 0; + uint8_t nIntPolar; + + nIntPolar = CMT2300A_ReadReg(CMT2300A_CUS_INT1_CTL); + nIntPolar = (nIntPolar & CMT2300A_MASK_INT_POLAR) ? 1 : 0; + + nFlag1 = CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG); + nFlag2 = CMT2300A_ReadReg(CMT2300A_CUS_INT_CLR1); + + if (nIntPolar) { + /* Interrupt flag active-low */ + nFlag1 = ~nFlag1; + nFlag2 = ~nFlag2; + } + + if (CMT2300A_MASK_LBD_FLG & nFlag1) { + nClr2 |= CMT2300A_MASK_LBD_CLR; /* Clear LBD_FLG */ + } + + if (CMT2300A_MASK_COL_ERR_FLG & nFlag1) { + nClr2 |= CMT2300A_MASK_PKT_DONE_CLR; /* Clear COL_ERR_FLG by PKT_DONE_CLR */ + } + + if (CMT2300A_MASK_PKT_ERR_FLG & nFlag1) { + nClr2 |= CMT2300A_MASK_PKT_DONE_CLR; /* Clear PKT_ERR_FLG by PKT_DONE_CLR */ + } + + if (CMT2300A_MASK_PREAM_OK_FLG & nFlag1) { + nClr2 |= CMT2300A_MASK_PREAM_OK_CLR; /* Clear PREAM_OK_FLG */ + nRet |= CMT2300A_MASK_PREAM_OK_FLG; /* Return PREAM_OK_FLG */ + } + + if (CMT2300A_MASK_SYNC_OK_FLG & nFlag1) { + nClr2 |= CMT2300A_MASK_SYNC_OK_CLR; /* Clear SYNC_OK_FLG */ + nRet |= CMT2300A_MASK_SYNC_OK_FLG; /* Return SYNC_OK_FLG */ + } + + if (CMT2300A_MASK_NODE_OK_FLG & nFlag1) { + nClr2 |= CMT2300A_MASK_NODE_OK_CLR; /* Clear NODE_OK_FLG */ + nRet |= CMT2300A_MASK_NODE_OK_FLG; /* Return NODE_OK_FLG */ + } + + if (CMT2300A_MASK_CRC_OK_FLG & nFlag1) { + nClr2 |= CMT2300A_MASK_CRC_OK_CLR; /* Clear CRC_OK_FLG */ + nRet |= CMT2300A_MASK_CRC_OK_FLG; /* Return CRC_OK_FLG */ + } + + if (CMT2300A_MASK_PKT_OK_FLG & nFlag1) { + nClr2 |= CMT2300A_MASK_PKT_DONE_CLR; /* Clear PKT_OK_FLG */ + nRet |= CMT2300A_MASK_PKT_OK_FLG; /* Return PKT_OK_FLG */ + } + + if (CMT2300A_MASK_SL_TMO_FLG & nFlag2) { + nClr1 |= CMT2300A_MASK_SL_TMO_CLR; /* Clear SL_TMO_FLG */ + nRet |= CMT2300A_MASK_SL_TMO_EN; /* Return SL_TMO_FLG by SL_TMO_EN */ + } + + if (CMT2300A_MASK_RX_TMO_FLG & nFlag2) { + nClr1 |= CMT2300A_MASK_RX_TMO_CLR; /* Clear RX_TMO_FLG */ + nRet |= CMT2300A_MASK_RX_TMO_EN; /* Return RX_TMO_FLG by RX_TMO_EN */ + } + + if (CMT2300A_MASK_TX_DONE_FLG & nFlag2) { + nClr1 |= CMT2300A_MASK_TX_DONE_CLR; /* Clear TX_DONE_FLG */ + nRet |= CMT2300A_MASK_TX_DONE_EN; /* Return TX_DONE_FLG by TX_DONE_EN */ + } + + CMT2300A_WriteReg(CMT2300A_CUS_INT_CLR1, nClr1); + CMT2300A_WriteReg(CMT2300A_CUS_INT_CLR2, nClr2); + + if (nIntPolar) { + /* Interrupt flag active-low */ + nRet = ~nRet; + } + + return nRet; +} + +/*! ******************************************************** + * @name CMT2300A_ConfigTxDin + * @desc Used to select whether to use GPIO1 or GPIO2 or GPIO3 + * as DIN in the direct mode. It only takes effect when + * call CMT2300A_EnableTxDin(TRUE) in the direct mode. + * @param nDinSel + * CMT2300A_TX_DIN_SEL_GPIO1 + * CMT2300A_TX_DIN_SEL_GPIO2 + * CMT2300A_TX_DIN_SEL_GPIO3 + * *********************************************************/ +void CMT2300A_ConfigTxDin(uint8_t nDinSel) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_FIFO_CTL); + tmp &= ~CMT2300A_MASK_TX_DIN_SEL; + tmp |= nDinSel; + CMT2300A_WriteReg(CMT2300A_CUS_FIFO_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableTxDin + * @desc Used to change GPIO1/GPIO2/GPIO3 between DOUT and DIN. + * @param bEnable(TRUE): used as DIN + * bEnable(FALSE): used as DOUT(default) + * *********************************************************/ +void CMT2300A_EnableTxDin(bool bEnable) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_FIFO_CTL); + + if (bEnable) + tmp |= CMT2300A_MASK_TX_DIN_EN; + else + tmp &= ~CMT2300A_MASK_TX_DIN_EN; + + CMT2300A_WriteReg(CMT2300A_CUS_FIFO_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableTxDinInvert + * @desc Used to invert DIN data in direct mode. + * @param bEnable(TRUE): invert DIN + * bEnable(FALSE): not invert DIN(default) + * *********************************************************/ +void CMT2300A_EnableTxDinInvert(bool bEnable) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_INT2_CTL); + + if (bEnable) + tmp |= CMT2300A_MASK_TX_DIN_INV; + else + tmp &= ~CMT2300A_MASK_TX_DIN_INV; + + CMT2300A_WriteReg(CMT2300A_CUS_INT2_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_IsExist + * @desc Chip indentify. + * @return TRUE: chip is exist, FALSE: chip not found + * *********************************************************/ +bool CMT2300A_IsExist(void) +{ + uint8_t back, dat; + + back = CMT2300A_ReadReg(CMT2300A_CUS_PKT17); + CMT2300A_WriteReg(CMT2300A_CUS_PKT17, 0xAA); + + dat = CMT2300A_ReadReg(CMT2300A_CUS_PKT17); + CMT2300A_WriteReg(CMT2300A_CUS_PKT17, back); + + if (0xAA == dat) + return true; + else + return false; +} + +/*! ******************************************************** + * @name CMT2300A_GetRssiCode + * @desc Get RSSI code. + * @return RSSI code + * *********************************************************/ +uint8_t CMT2300A_GetRssiCode(void) +{ + return CMT2300A_ReadReg(CMT2300A_CUS_RSSI_CODE); +} + +/*! ******************************************************** + * @name CMT2300A_GetRssiDBm + * @desc Get RSSI dBm. + * @return dBm + * *********************************************************/ +int CMT2300A_GetRssiDBm(void) +{ + return (int)CMT2300A_ReadReg(CMT2300A_CUS_RSSI_DBM) - 128; +} + +/*! ******************************************************** + * @name CMT2300A_SetFrequencyChannel + * @desc This defines up to 255 frequency channel + * for fast frequency hopping operation. + * @param nChann: the frequency channel + * *********************************************************/ +void CMT2300A_SetFrequencyChannel(uint8_t nChann) +{ + CMT2300A_WriteReg(CMT2300A_CUS_FREQ_CHNL, nChann); +} + +/*! ******************************************************** + * @name CMT2300A_SetFrequencyStep + * @desc This defines the frequency channel step size + * for fast frequency hopping operation. + * One step size is 2.5 kHz. + * @param nOffset: the frequency step + * *********************************************************/ +void CMT2300A_SetFrequencyStep(uint8_t nOffset) +{ + CMT2300A_WriteReg(CMT2300A_CUS_FREQ_OFS, nOffset); +} + +/*! ******************************************************** + * @name CMT2300A_SetPayloadLength + * @desc Set payload length. + * @param nLength + * *********************************************************/ +void CMT2300A_SetPayloadLength(uint16_t nLength) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_PKT14); + + tmp &= ~CMT2300A_MASK_PAYLOAD_LENG_10_8; + tmp |= (nLength >> 4) & CMT2300A_MASK_PAYLOAD_LENG_10_8; + CMT2300A_WriteReg(CMT2300A_CUS_PKT14, tmp); + + tmp = nLength & CMT2300A_MASK_PAYLOAD_LENG_7_0; + CMT2300A_WriteReg(CMT2300A_CUS_PKT15, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableLfosc + * @desc If you need use sleep timer, you should enable LFOSC. + * @param bEnable(TRUE): Enable it(default) + * bEnable(FALSE): Disable it + * *********************************************************/ +void CMT2300A_EnableLfosc(bool bEnable) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_SYS2); + + if (bEnable) { + tmp |= CMT2300A_MASK_LFOSC_RECAL_EN; + tmp |= CMT2300A_MASK_LFOSC_CAL1_EN; + tmp |= CMT2300A_MASK_LFOSC_CAL2_EN; + } else { + tmp &= ~CMT2300A_MASK_LFOSC_RECAL_EN; + tmp &= ~CMT2300A_MASK_LFOSC_CAL1_EN; + tmp &= ~CMT2300A_MASK_LFOSC_CAL2_EN; + } + + CMT2300A_WriteReg(CMT2300A_CUS_SYS2, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableLfoscOutput + * @desc LFOSC clock is output via GPIO3. + * @param bEnable(TRUE): Enable it + * bEnable(FALSE): Disable it(default) + * *********************************************************/ +void CMT2300A_EnableLfoscOutput(bool bEnable) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_INT2_CTL); + + if (bEnable) + tmp |= CMT2300A_MASK_LFOSC_OUT_EN; + else + tmp &= ~CMT2300A_MASK_LFOSC_OUT_EN; + + CMT2300A_WriteReg(CMT2300A_CUS_INT2_CTL, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_EnableAfc + * @desc AFC enable or disanble. + * @param bEnable(TRUE): Enable it + * bEnable(FALSE): Disable it(default) + * *********************************************************/ +void CMT2300A_EnableAfc(bool bEnable) +{ + uint8_t tmp = CMT2300A_ReadReg(CMT2300A_CUS_FSK5); + + if (bEnable) + tmp |= 0x10; + else + tmp &= ~0x10; + + CMT2300A_WriteReg(CMT2300A_CUS_FSK5, tmp); +} + +/*! ******************************************************** + * @name CMT2300A_SetAfcOvfTh + * @desc This is optional, only needed when using Rx fast frequency hopping. + * @param afcOvfTh: AFC_OVF_TH see AN142 and AN197 for details. + * *********************************************************/ +void CMT2300A_SetAfcOvfTh(uint8_t afcOvfTh) +{ + CMT2300A_WriteReg(CMT2300A_CUS_FSK4, afcOvfTh); +} + +/*! ******************************************************** + * @name CMT2300A_Init + * @desc Initialize chip status. + * *********************************************************/ +bool CMT2300A_Init(void) +{ + uint8_t tmp; + + CMT2300A_SoftReset(); + CMT2300A_DelayMs(20); + + if (!CMT2300A_GoStby()) + return false; // CMT2300A not switched to standby mode! + + if (!CMT2300A_IsExist()) + return false; // CMT2300A not found! + + tmp = CMT2300A_ReadReg(CMT2300A_CUS_MODE_STA); + tmp |= CMT2300A_MASK_CFG_RETAIN; /* Enable CFG_RETAIN */ + tmp &= ~CMT2300A_MASK_RSTN_IN_EN; /* Disable RSTN_IN */ + CMT2300A_WriteReg(CMT2300A_CUS_MODE_STA, tmp); + + tmp = CMT2300A_ReadReg(CMT2300A_CUS_EN_CTL); + tmp |= CMT2300A_MASK_LOCKING_EN; /* Enable LOCKING_EN */ + CMT2300A_WriteReg(CMT2300A_CUS_EN_CTL, tmp); + + CMT2300A_EnableLfosc(false); /* Disable LFOSC */ + + CMT2300A_ClearInterruptFlags(); + + return true; +} + +/*! ******************************************************** + * @name CMT2300A_ConfigRegBank + * @desc Config one register bank. + * *********************************************************/ +bool CMT2300A_ConfigRegBank(uint8_t base_addr, const uint8_t bank[], uint8_t len) +{ + uint8_t i; + for (i = 0; i < len; i++) + CMT2300A_WriteReg(i + base_addr, bank[i]); + + return true; +} diff --git a/lib/CMT2300a/cmt2300a.h b/lib/CMT2300a/cmt2300a.h new file mode 100644 index 00000000..e6d484d8 --- /dev/null +++ b/lib/CMT2300a/cmt2300a.h @@ -0,0 +1,96 @@ +/* + * THE FOLLOWING FIRMWARE IS PROVIDED: (1) "AS IS" WITH NO WARRANTY; AND + * (2)TO ENABLE ACCESS TO CODING INFORMATION TO GUIDE AND FACILITATE CUSTOMER. + * CONSEQUENTLY, CMOSTEK SHALL NOT BE HELD LIABLE FOR ANY DIRECT, INDIRECT OR + * CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING FROM THE CONTENT + * OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE CODING INFORMATION + * CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS. + * + * Copyright (C) CMOSTEK SZ. + */ + +/*! + * @file cmt2300a.h + * @brief CMT2300A transceiver RF chip driver + * + * @version 1.3 + * @date Jul 17 2017 + * @author CMOSTEK R@D + */ + +#ifndef __CMT2300A_H +#define __CMT2300A_H + +#include "cmt2300a_defs.h" +#include "cmt2300a_hal.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define ENABLE_AUTO_SWITCH_CHIP_STATUS /* Enable the auto switch chip status */ + +/* ************************************************************************ + The following are for chip status controls. +* ************************************************************************ */ +void CMT2300A_SoftReset(void); +uint8_t CMT2300A_GetChipStatus(void); +bool CMT2300A_AutoSwitchStatus(uint8_t nGoCmd); +bool CMT2300A_GoSleep(void); +bool CMT2300A_GoStby(void); +bool CMT2300A_GoTFS(void); +bool CMT2300A_GoRFS(void); +bool CMT2300A_GoTx(void); +bool CMT2300A_GoRx(void); + +/* ************************************************************************ + * The following are for chip interrupts, GPIO, FIFO operations. + * ************************************************************************ */ +void CMT2300A_ConfigGpio(uint8_t nGpioSel); +void CMT2300A_ConfigInterrupt(uint8_t nInt1Sel, uint8_t nInt2Sel); +void CMT2300A_SetInterruptPolar(bool bActiveHigh); +void CMT2300A_SetFifoThreshold(uint8_t nFifoThreshold); +void CMT2300A_EnableAntennaSwitch(uint8_t nMode); +void CMT2300A_EnableInterrupt(uint8_t nEnable); +void CMT2300A_EnableRxFifoAutoClear(bool bEnable); +void CMT2300A_EnableFifoMerge(bool bEnable); +void CMT2300A_EnableReadFifo(void); +void CMT2300A_EnableWriteFifo(void); +void CMT2300A_RestoreFifo(void); +uint8_t CMT2300A_ClearTxFifo(void); +uint8_t CMT2300A_ClearRxFifo(void); +uint8_t CMT2300A_ClearInterruptFlags(void); + +/* ************************************************************************ + * The following are for Tx DIN operations in direct mode. + * ************************************************************************ */ +void CMT2300A_ConfigTxDin(uint8_t nDinSel); +void CMT2300A_EnableTxDin(bool bEnable); +void CMT2300A_EnableTxDinInvert(bool bEnable); + +/* ************************************************************************ + * The following are general operations. + * ************************************************************************ */ +bool CMT2300A_IsExist(void); +uint8_t CMT2300A_GetRssiCode(void); +int CMT2300A_GetRssiDBm(void); +void CMT2300A_SetFrequencyChannel(uint8_t nChann); +void CMT2300A_SetFrequencyStep(uint8_t nOffset); +void CMT2300A_SetPayloadLength(uint16_t nLength); +void CMT2300A_EnableLfosc(bool bEnable); +void CMT2300A_EnableLfoscOutput(bool bEnable); +void CMT2300A_EnableAfc(bool bEnable); +void CMT2300A_SetAfcOvfTh(uint8_t afcOvfTh); + +/* ************************************************************************ + * The following are for chip initializes. + * ************************************************************************ */ +bool CMT2300A_Init(void); +bool CMT2300A_ConfigRegBank(uint8_t base_addr, const uint8_t bank[], uint8_t len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/lib/CMT2300a/cmt2300a_defs.h b/lib/CMT2300a/cmt2300a_defs.h new file mode 100644 index 00000000..c2c6ef10 --- /dev/null +++ b/lib/CMT2300a/cmt2300a_defs.h @@ -0,0 +1,606 @@ +/* + * THE FOLLOWING FIRMWARE IS PROVIDED: (1) "AS IS" WITH NO WARRANTY; AND + * (2)TO ENABLE ACCESS TO CODING INFORMATION TO GUIDE AND FACILITATE CUSTOMER. + * CONSEQUENTLY, CMOSTEK SHALL NOT BE HELD LIABLE FOR ANY DIRECT, INDIRECT OR + * CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING FROM THE CONTENT + * OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE CODING INFORMATION + * CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS. + * + * Copyright (C) CMOSTEK SZ. + */ + +/*! + * @file cmt2300a_defs.h + * @brief CMT2300A registers defines + * + * @version 1.2 + * @date Jul 17 2017 + * @author CMOSTEK R@D + */ + +#ifndef __CMT2300A_DEFS_H +#define __CMT2300A_DEFS_H + +/* ---------- CMT bank defines ---------- */ +#define CMT2300A_CMT_BANK_ADDR 0x00 +#define CMT2300A_CMT_BANK_SIZE 12 +#define CMT2300A_CUS_CMT1 0x00 +#define CMT2300A_CUS_CMT2 0x01 +#define CMT2300A_CUS_CMT3 0x02 +#define CMT2300A_CUS_CMT4 0x03 +#define CMT2300A_CUS_CMT5 0x04 +#define CMT2300A_CUS_CMT6 0x05 +#define CMT2300A_CUS_CMT7 0x06 +#define CMT2300A_CUS_CMT8 0x07 +#define CMT2300A_CUS_CMT9 0x08 +#define CMT2300A_CUS_CMT10 0x09 +#define CMT2300A_CUS_CMT11 0x0A +#define CMT2300A_CUS_RSSI 0x0B + +/* ---------- System bank defines ---------- */ +#define CMT2300A_SYSTEM_BANK_ADDR 0x0C +#define CMT2300A_SYSTEM_BANK_SIZE 12 +#define CMT2300A_CUS_SYS1 0x0C +#define CMT2300A_CUS_SYS2 0x0D +#define CMT2300A_CUS_SYS3 0x0E +#define CMT2300A_CUS_SYS4 0x0F +#define CMT2300A_CUS_SYS5 0x10 +#define CMT2300A_CUS_SYS6 0x11 +#define CMT2300A_CUS_SYS7 0x12 +#define CMT2300A_CUS_SYS8 0x13 +#define CMT2300A_CUS_SYS9 0x14 +#define CMT2300A_CUS_SYS10 0x15 +#define CMT2300A_CUS_SYS11 0x16 +#define CMT2300A_CUS_SYS12 0x17 + +/* ---------- Frequency bank defines ---------- */ +#define CMT2300A_FREQUENCY_BANK_ADDR 0x18 +#define CMT2300A_FREQUENCY_BANK_SIZE 8 +#define CMT2300A_CUS_RF1 0x18 +#define CMT2300A_CUS_RF2 0x19 +#define CMT2300A_CUS_RF3 0x1A +#define CMT2300A_CUS_RF4 0x1B +#define CMT2300A_CUS_RF5 0x1C +#define CMT2300A_CUS_RF6 0x1D +#define CMT2300A_CUS_RF7 0x1E +#define CMT2300A_CUS_RF8 0x1F + +/* ---------- Data rate bank defines ---------- */ +#define CMT2300A_DATA_RATE_BANK_ADDR 0x20 +#define CMT2300A_DATA_RATE_BANK_SIZE 24 +#define CMT2300A_CUS_RF9 0x20 +#define CMT2300A_CUS_RF10 0x21 +#define CMT2300A_CUS_RF11 0x22 +#define CMT2300A_CUS_RF12 0x23 +#define CMT2300A_CUS_FSK1 0x24 +#define CMT2300A_CUS_FSK2 0x25 +#define CMT2300A_CUS_FSK3 0x26 +#define CMT2300A_CUS_FSK4 0x27 +#define CMT2300A_CUS_FSK5 0x28 +#define CMT2300A_CUS_FSK6 0x29 +#define CMT2300A_CUS_FSK7 0x2A +#define CMT2300A_CUS_CDR1 0x2B +#define CMT2300A_CUS_CDR2 0x2C +#define CMT2300A_CUS_CDR3 0x2D +#define CMT2300A_CUS_CDR4 0x2E +#define CMT2300A_CUS_AGC1 0x2F +#define CMT2300A_CUS_AGC2 0x30 +#define CMT2300A_CUS_AGC3 0x31 +#define CMT2300A_CUS_AGC4 0x32 +#define CMT2300A_CUS_OOK1 0x33 +#define CMT2300A_CUS_OOK2 0x34 +#define CMT2300A_CUS_OOK3 0x35 +#define CMT2300A_CUS_OOK4 0x36 +#define CMT2300A_CUS_OOK5 0x37 + +/* ---------- Baseband bank defines ---------- */ +#define CMT2300A_BASEBAND_BANK_ADDR 0x38 +#define CMT2300A_BASEBAND_BANK_SIZE 29 +#define CMT2300A_CUS_PKT1 0x38 +#define CMT2300A_CUS_PKT2 0x39 +#define CMT2300A_CUS_PKT3 0x3A +#define CMT2300A_CUS_PKT4 0x3B +#define CMT2300A_CUS_PKT5 0x3C +#define CMT2300A_CUS_PKT6 0x3D +#define CMT2300A_CUS_PKT7 0x3E +#define CMT2300A_CUS_PKT8 0x3F +#define CMT2300A_CUS_PKT9 0x40 +#define CMT2300A_CUS_PKT10 0x41 +#define CMT2300A_CUS_PKT11 0x42 +#define CMT2300A_CUS_PKT12 0x43 +#define CMT2300A_CUS_PKT13 0x44 +#define CMT2300A_CUS_PKT14 0x45 +#define CMT2300A_CUS_PKT15 0x46 +#define CMT2300A_CUS_PKT16 0x47 +#define CMT2300A_CUS_PKT17 0x48 +#define CMT2300A_CUS_PKT18 0x49 +#define CMT2300A_CUS_PKT19 0x4A +#define CMT2300A_CUS_PKT20 0x4B +#define CMT2300A_CUS_PKT21 0x4C +#define CMT2300A_CUS_PKT22 0x4D +#define CMT2300A_CUS_PKT23 0x4E +#define CMT2300A_CUS_PKT24 0x4F +#define CMT2300A_CUS_PKT25 0x50 +#define CMT2300A_CUS_PKT26 0x51 +#define CMT2300A_CUS_PKT27 0x52 +#define CMT2300A_CUS_PKT28 0x53 +#define CMT2300A_CUS_PKT29 0x54 + +/* ---------- Tx bank defines ---------- */ +#define CMT2300A_TX_BANK_ADDR 0x55 +#define CMT2300A_TX_BANK_SIZE 11 +#define CMT2300A_CUS_TX1 0x55 +#define CMT2300A_CUS_TX2 0x56 +#define CMT2300A_CUS_TX3 0x57 +#define CMT2300A_CUS_TX4 0x58 +#define CMT2300A_CUS_TX5 0x59 +#define CMT2300A_CUS_TX6 0x5A +#define CMT2300A_CUS_TX7 0x5B +#define CMT2300A_CUS_TX8 0x5C +#define CMT2300A_CUS_TX9 0x5D +#define CMT2300A_CUS_TX10 0x5E +#define CMT2300A_CUS_LBD 0x5F + +/* ---------- Control1 bank defines ---------- */ +#define CMT2300A_CONTROL1_BANK_ADDR 0x60 +#define CMT2300A_CONTROL1_BANK_SIZE 11 +#define CMT2300A_CUS_MODE_CTL 0x60 +#define CMT2300A_CUS_MODE_STA 0x61 +#define CMT2300A_CUS_EN_CTL 0x62 +#define CMT2300A_CUS_FREQ_CHNL 0x63 +#define CMT2300A_CUS_FREQ_OFS 0x64 +#define CMT2300A_CUS_IO_SEL 0x65 +#define CMT2300A_CUS_INT1_CTL 0x66 +#define CMT2300A_CUS_INT2_CTL 0x67 +#define CMT2300A_CUS_INT_EN 0x68 +#define CMT2300A_CUS_FIFO_CTL 0x69 +#define CMT2300A_CUS_INT_CLR1 0x6A + +/* ---------- Control2 bank defines ---------- */ +#define CMT2300A_CONTROL2_BANK_ADDR 0x6B +#define CMT2300A_CONTROL2_BANK_SIZE 7 +#define CMT2300A_CUS_INT_CLR2 0x6B +#define CMT2300A_CUS_FIFO_CLR 0x6C +#define CMT2300A_CUS_INT_FLAG 0x6D +#define CMT2300A_CUS_FIFO_FLAG 0x6E +#define CMT2300A_CUS_RSSI_CODE 0x6F +#define CMT2300A_CUS_RSSI_DBM 0x70 +#define CMT2300A_CUS_LBD_RESULT 0x71 + +/* ********** CMT2300A_CUS_CMT2 registers ********** */ +#define CMT2300A_MASK_PRODUCT_ID 0xFF + +/* ********** CMT2300A_CUS_CMT5 registers ********** */ +#define CMT2300A_MASK_LMT_CODE 0xC0 + +/* ********** CMT2300A_CUS_CMT9 registers ********** */ +#define CMT2300A_MASK_RSSI_OFFSET_SIGN 0x80 +#define CMT2300A_MASK_DIG_CLKDIV 0x1F + +/* ********** CMT2300A_CUS_RSSI registers ********** */ +#define CMT2300A_MASK_RSSI_OFFSET 0xF8 +#define CMT2300A_MASK_RSSI_SLOPE 0x07 + +/* ********** CMT2300A_CUS_SYS1 registers ********** */ +#define CMT2300A_MASK_LMT_VTR 0xC0 +#define CMT2300A_MASK_MIXER_BIAS 0x30 +#define CMT2300A_MASK_LNA_MODE 0x0C +#define CMT2300A_MASK_LNA_BIAS 0x03 + +/* ********** CMT2300A_CUS_SYS2 registers ********** */ +#define CMT2300A_MASK_LFOSC_RECAL_EN 0x80 +#define CMT2300A_MASK_LFOSC_CAL1_EN 0x40 +#define CMT2300A_MASK_LFOSC_CAL2_EN 0x20 +#define CMT2300A_MASK_RX_TIMER_EN 0x10 +#define CMT2300A_MASK_SLEEP_TIMER_EN 0x08 +#define CMT2300A_MASK_TX_DC_EN 0x04 +#define CMT2300A_MASK_RX_DC_EN 0x02 +#define CMT2300A_MASK_DC_PAUSE 0x01 + +/* ********** CMT2300A_CUS_SYS3 registers ********** */ +#define CMT2300A_MASK_SLEEP_BYPASS_EN 0x80 +#define CMT2300A_MASK_XTAL_STB_TIME 0x70 +#define CMT2300A_MASK_TX_EXIT_STATE 0x0C +#define CMT2300A_MASK_RX_EXIT_STATE 0x03 + +/* ********** CMT2300A_CUS_SYS4 registers ********** */ +#define CMT2300A_MASK_SLEEP_TIMER_M_7_0 0xFF + +/* ********** CMT2300A_CUS_SYS5 registers ********** */ +#define CMT2300A_MASK_SLEEP_TIMER_M_10_8 0x70 +#define CMT2300A_MASK_SLEEP_TIMER_R 0x0F + +/* ********** CMT2300A_CUS_SYS6 registers ********** */ +#define CMT2300A_MASK_RX_TIMER_T1_M_7_0 0xFF + +/* ********** CMT2300A_CUS_SYS7 registers ********** */ +#define CMT2300A_MASK_RX_TIMER_T1_M_10_8 0x70 +#define CMT2300A_MASK_RX_TIMER_T1_R 0x0F + +/* ********** CMT2300A_CUS_SYS8 registers ********** */ +#define CMT2300A_MASK_RX_TIMER_T2_M_7_0 0xFF + +/* ********** CMT2300A_CUS_SYS9 registers ********** */ +#define CMT2300A_MASK_RX_TIMER_T2_M_10_8 0x70 +#define CMT2300A_MASK_RX_TIMER_T2_R 0x0F + +/* ********** CMT2300A_CUS_SYS10 registers ********** */ +#define CMT2300A_MASK_COL_DET_EN 0x80 +#define CMT2300A_MASK_COL_OFS_SEL 0x40 +#define CMT2300A_MASK_RX_AUTO_EXIT_DIS 0x20 +#define CMT2300A_MASK_DOUT_MUTE 0x10 +#define CMT2300A_MASK_RX_EXTEND_MODE 0x0F + +/* ********** CMT2300A_CUS_SYS11 registers ********** */ +#define CMT2300A_MASK_PJD_TH_SEL 0x80 +#define CMT2300A_MASK_CCA_INT_SEL 0x60 +#define CMT2300A_MASK_RSSI_DET_SEL 0x18 +#define CMT2300A_MASK_RSSI_AVG_MODE 0x07 + +/* ********** CMT2300A_CUS_SYS12 registers ********** */ +#define CMT2300A_MASK_PJD_WIN_SEL 0xC0 +#define CMT2300A_MASK_CLKOUT_EN 0x20 +#define CMT2300A_MASK_CLKOUT_DIV 0x1F + +/* ********** CMT2300A_CUS_RF1 registers ********** */ +#define CMT2300A_MASK_FREQ_RX_N 0xFF + +/* ********** CMT2300A_CUS_RF2 registers ********** */ +#define CMT2300A_MASK_FREQ_RX_K_7_0 0xFF + +/* ********** CMT2300A_CUS_RF3 registers ********** */ +#define CMT2300A_MASK_FREQ_RX_K_15_8 0xFF + +/* ********** CMT2300A_CUS_RF4 registers ********** */ +#define CMT2300A_MASK_FREQ_PALDO_SEL 0x80 +#define CMT2300A_MASK_FREQ_DIVX_CODE 0x70 +#define CMT2300A_MASK_FREQ_RX_K_19_16 0x0F + +/* ********** CMT2300A_CUS_RF5 registers ********** */ +#define CMT2300A_MASK_FREQ_TX_N 0xFF + +/* ********** CMT2300A_CUS_RF6 registers ********** */ +#define CMT2300A_MASK_FREQ_TX_K_7_0 0xFF + +/* ********** CMT2300A_CUS_RF7 registers ********** */ +#define CMT2300A_MASK_FREQ_TX_K_15_8 0xFF + +/* ********** CMT2300A_CUS_RF8 registers ********** */ +#define CMT2300A_MASK_FSK_SWT 0x80 +#define CMT2300A_MASK_FREQ_VCO_BANK 0x70 +#define CMT2300A_MASK_FREQ_TX_K_19_16 0x0F + +/* ********** CMT2300A_CUS_PKT1 registers ********** */ +#define CMT2300A_MASK_RX_PREAM_SIZE 0xF8 +#define CMT2300A_MASK_PREAM_LENG_UNIT 0x04 +#define CMT2300A_MASK_DATA_MODE 0x03 +/* CMT2300A_MASK_PREAM_LENG_UNIT options */ +#define CMT2300A_PREAM_LENG_UNIT_8_BITS 0x00 +#define CMT2300A_PREAM_LENG_UNIT_4_BITS 0x04 +/* CMT2300A_MASK_DATA_MODE options */ +#define CMT2300A_DATA_MODE_DIRECT 0x00 +#define CMT2300A_DATA_MODE_PACKET 0x02 + +/* ********** CMT2300A_CUS_PKT2 registers ********** */ +#define CMT2300A_MASK_TX_PREAM_SIZE_7_0 0xFF + +/* ********** CMT2300A_CUS_PKT3 registers ********** */ +#define CMT2300A_MASK_TX_PREAM_SIZE_15_8 0xFF + +/* ********** CMT2300A_CUS_PKT4 registers ********** */ +#define CMT2300A_MASK_PREAM_VALUE 0xFF + +/* ********** CMT2300A_CUS_PKT5 registers ********** */ +#define CMT2300A_MASK_SYNC_TOL 0x70 +#define CMT2300A_MASK_SYNC_SIZE 0x0E +#define CMT2300A_MASK_SYNC_MAN_EN 0x01 + +/* ********** CMT2300A_CUS_PKT6 registers ********** */ +#define CMT2300A_MASK_SYNC_VALUE_7_0 0xFF + +/* ********** CMT2300A_CUS_PKT7 registers ********** */ +#define CMT2300A_MASK_SYNC_VALUE_15_8 0xFF + +/* ********** CMT2300A_CUS_PKT8 registers ********** */ +#define CMT2300A_MASK_SYNC_VALUE_23_16 0xFF + +/* ********** CMT2300A_CUS_PKT9 registers ********** */ +#define CMT2300A_MASK_SYNC_VALUE_31_24 0xFF + +/* ********** CMT2300A_CUS_PKT10 registers ********** */ +#define CMT2300A_MASK_SYNC_VALUE_39_32 0xFF + +/* ********** CMT2300A_CUS_PKT11 registers ********** */ +#define CMT2300A_MASK_SYNC_VALUE_47_40 0xFF + +/* ********** CMT2300A_CUS_PKT12 registers ********** */ +#define CMT2300A_MASK_SYNC_VALUE_55_48 0xFF + +/* ********** CMT2300A_CUS_PKT13 registers ********** */ +#define CMT2300A_MASK_SYNC_VALUE_63_56 0xFF + +/* ********** CMT2300A_CUS_PKT14 registers ********** */ +#define CMT2300A_MASK_PAYLOAD_LENG_10_8 0x70 +#define CMT2300A_MASK_AUTO_ACK_EN 0x08 +#define CMT2300A_MASK_NODE_LENG_POS_SEL 0x04 +#define CMT2300A_MASK_PAYLOAD_BIT_ORDER 0x02 +#define CMT2300A_MASK_PKT_TYPE 0x01 +/* CMT2300A_MASK_NODE_LENG_POS_SEL options */ +#define CMT2300A_NODE_LENG_FIRST_NODE 0x00 +#define CMT2300A_NODE_LENG_FIRST_LENGTH 0x04 +/* CMT2300A_MASK_PAYLOAD_BIT_ORDER options */ +#define CMT2300A_PAYLOAD_BIT_ORDER_MSB 0x00 +#define CMT2300A_PAYLOAD_BIT_ORDER_LSB 0x02 +/* CMT2300A_MASK_PKT_TYPE options */ +#define CMT2300A_PKT_TYPE_FIXED 0x00 +#define CMT2300A_PKT_TYPE_VARIABLE 0x01 + +/* ********** CMT2300A_CUS_PKT15 registers ********** */ +#define CMT2300A_MASK_PAYLOAD_LENG_7_0 0xFF + +/* ********** CMT2300A_CUS_PKT16 registers ********** */ +#define CMT2300A_MASK_NODE_FREE_EN 0x20 +#define CMT2300A_MASK_NODE_ERR_MASK 0x10 +#define CMT2300A_MASK_NODE_SIZE 0x0C +#define CMT2300A_MASK_NODE_DET_MODE 0x03 +/* CMT2300A_MASK_NODE_DET_MODE options */ +#define CMT2300A_NODE_DET_NODE 0x00 +#define CMT2300A_NODE_DET_VALUE 0x01 +#define CMT2300A_NODE_DET_VALUE_0 0x02 +#define CMT2300A_NODE_DET_VALUE_0_1 0x03 + +/* ********** CMT2300A_CUS_PKT17 registers ********** */ +#define CMT2300A_MASK_NODE_VALUE_7_0 0xFF + +/* ********** CMT2300A_CUS_PKT18 registers ********** */ +#define CMT2300A_MASK_NODE_VALUE_15_8 0xFF + +/* ********** CMT2300A_CUS_PKT19 registers ********** */ +#define CMT2300A_MASK_NODE_VALUE_23_16 0xFF + +/* ********** CMT2300A_CUS_PKT20 registers ********** */ +#define CMT2300A_MASK_NODE_VALUE_31_24 0xFF + +/* ********** CMT2300A_CUS_PKT21 registers ********** */ +#define CMT2300A_MASK_FEC_TYPE 0x80 +#define CMT2300A_MASK_FEC_EN 0x40 +#define CMT2300A_MASK_CRC_BYTE_SWAP 0x20 +#define CMT2300A_MASK_CRC_BIT_INV 0x10 +#define CMT2300A_MASK_CRC_RANGE 0x08 +#define CMT2300A_MASK_CRC_TYPE 0x06 +#define CMT2300A_MASK_CRC_EN 0x01 +/* CMT2300A_MASK_CRC_BYTE_SWAP options */ +#define CMT2300A_CRC_ORDER_HBYTE 0x00 +#define CMT2300A_CRC_ORDER_LBYTE 0x20 +/* CMT2300A_MASK_CRC_RANGE options */ +#define CMT2300A_CRC_RANGE_PAYLOAD 0x00 +#define CMT2300A_CRC_RANGE_DATA 0x08 +/* CMT2300A_MASK_CRC_TYPE options */ +#define CMT2300A_CRC_TYPE_CCITT16 0x00 +#define CMT2300A_CRC_TYPE_IBM16 0x02 +#define CMT2300A_CRC_TYPE_ITuint16_t 0x04 + +/* ********** CMT2300A_CUS_PKT22 registers ********** */ +#define CMT2300A_MASK_CRC_SEED_7_0 0xFF + +/* ********** CMT2300A_CUS_PKT23 registers ********** */ +#define CMT2300A_MASK_CRC_SEED_15_8 0xFF + +/* ********** CMT2300A_CUS_PKT24 registers ********** */ +#define CMT2300A_MASK_CRC_BIT_ORDER 0x80 +#define CMT2300A_MASK_WHITEN_SEED_8_8 0x40 +#define CMT2300A_MASK_WHITEN_SEED_TYPE 0x20 +#define CMT2300A_MASK_WHITEN_TYPE 0x18 +#define CMT2300A_MASK_WHITEN_EN 0x04 +#define CMT2300A_MASK_MANCH_TYPE 0x02 +#define CMT2300A_MASK_MANCH_EN 0x01 +/* CMT2300A_MASK_CRC_BIT_ORDER options */ +#define CMT2300A_CRC_BIT_ORDER_MSB 0x00 +#define CMT2300A_CRC_BIT_ORDER_LSB 0x80 +/* CMT2300A_MASK_WHITEN_SEED_TYPE options */ +#define CMT2300A_WHITEN_SEED_TYPE_1 0x00 +#define CMT2300A_WHITEN_SEED_TYPE_2 0x20 +/* CMT2300A_MASK_WHITEN_TYPE options */ +#define CMT2300A_WHITEN_TYPE_PN9_CCITT 0x00 +#define CMT2300A_WHITEN_TYPE_PN9_IBM 0x08 +#define CMT2300A_WHITEN_TYPE_PN7 0x10 +/* CMT2300A_MASK_MANCH_TYPE options */ +#define CMT2300A_MANCH_TYPE_ONE_01 0x00 +#define CMT2300A_MANCH_TYPE_ONE_10 0x02 + +/* ********** CMT2300A_CUS_PKT25 registers ********** */ +#define CMT2300A_MASK_WHITEN_SEED_7_0 0xFF + +/* ********** CMT2300A_CUS_PKT26 registers ********** */ +#define CMT2300A_MASK_TX_PREFIX_TYPE 0x03 + +/* ********** CMT2300A_CUS_PKT27 registers ********** */ +#define CMT2300A_MASK_TX_PKT_NUM 0xFF + +/* ********** CMT2300A_CUS_PKT28 registers ********** */ +#define CMT2300A_MASK_TX_PKT_GAP 0xFF + +/* ********** CMT2300A_CUS_PKT29 registers ********** */ +#define CMT2300A_MASK_FIFO_AUTO_RES_EN 0x80 +#define CMT2300A_MASK_FIFO_TH 0x7F + +/* ********** CMT2300A_CUS_MODE_CTL registers ********** */ +#define CMT2300A_MASK_CHIP_MODE_SWT 0xFF +/* CMT2300A_MASK_CHIP_MODE_SWT options */ +#define CMT2300A_GO_EEPROM 0x01 +#define CMT2300A_GO_STBY 0x02 +#define CMT2300A_GO_RFS 0x04 +#define CMT2300A_GO_RX 0x08 +#define CMT2300A_GO_SLEEP 0x10 +#define CMT2300A_GO_TFS 0x20 +#define CMT2300A_GO_TX 0x40 +#define CMT2300A_GO_SWITCH 0x80 + +/* ********** CMT2300A_CUS_MODE_STA registers ********** */ +#define CMT2300A_MASK_RSTN_IN_EN 0x20 +#define CMT2300A_MASK_CFG_RETAIN 0x10 +#define CMT2300A_MASK_CHIP_MODE_STA 0x0F +/* CMT2300A_MASK_CHIP_MODE_STA options */ +#define CMT2300A_STA_IDLE 0x00 +#define CMT2300A_STA_SLEEP 0x01 +#define CMT2300A_STA_STBY 0x02 +#define CMT2300A_STA_RFS 0x03 +#define CMT2300A_STA_TFS 0x04 +#define CMT2300A_STA_RX 0x05 +#define CMT2300A_STA_TX 0x06 +#define CMT2300A_STA_EEPROM 0x07 +#define CMT2300A_STA_ERROR 0x08 +#define CMT2300A_STA_CAL 0x09 + +/* ********** CMT2300A_CUS_EN_CTL registers ********** */ +#define CMT2300A_MASK_LOCKING_EN 0x20 + +/* ********** CMT2300A_CUS_FREQ_CHNL registers ********** */ +#define CMT2300A_MASK_FH_CHANNEL 0xFF + +/* ********** CMT2300A_CUS_FREQ_OFS registers ********** */ +#define CMT2300A_MASK_FH_OFFSET 0xFF + +/* ********** CMT2300A_CUS_IO_SEL registers ********** */ +#define CMT2300A_MASK_GPIO4_SEL 0xC0 +#define CMT2300A_MASK_GPIO3_SEL 0x30 +#define CMT2300A_MASK_GPIO2_SEL 0x0C +#define CMT2300A_MASK_GPIO1_SEL 0x03 +/* CMT2300A_MASK_GPIO4_SEL options */ +#define CMT2300A_GPIO4_SEL_RSTIN 0x00 +#define CMT2300A_GPIO4_SEL_INT1 0x40 +#define CMT2300A_GPIO4_SEL_DOUT 0x80 +#define CMT2300A_GPIO4_SEL_DCLK 0xC0 +/* CMT2300A_MASK_GPIO3_SEL options */ +#define CMT2300A_GPIO3_SEL_CLKO 0x00 +#define CMT2300A_GPIO3_SEL_DOUT 0x10 +#define CMT2300A_GPIO3_SEL_DIN 0x10 +#define CMT2300A_GPIO3_SEL_INT2 0x20 +#define CMT2300A_GPIO3_SEL_DCLK 0x30 +/* CMT2300A_MASK_GPIO2_SEL options */ +#define CMT2300A_GPIO2_SEL_INT1 0x00 +#define CMT2300A_GPIO2_SEL_INT2 0x04 +#define CMT2300A_GPIO2_SEL_DOUT 0x08 +#define CMT2300A_GPIO2_SEL_DIN 0x08 +#define CMT2300A_GPIO2_SEL_DCLK 0x0C +/* CMT2300A_MASK_GPIO1_SEL options */ +#define CMT2300A_GPIO1_SEL_DOUT 0x00 +#define CMT2300A_GPIO1_SEL_DIN 0x00 +#define CMT2300A_GPIO1_SEL_INT1 0x01 +#define CMT2300A_GPIO1_SEL_INT2 0x02 +#define CMT2300A_GPIO1_SEL_DCLK 0x03 + +/* ********** CMT2300A_CUS_INT1_CTL registers ********** */ +#define CMT2300A_MASK_RF_SWT1_EN 0x80 +#define CMT2300A_MASK_RF_SWT2_EN 0x40 +#define CMT2300A_MASK_INT_POLAR 0x20 +#define CMT2300A_MASK_INT1_SEL 0x1F +/* CMT2300A_MASK_INT_POLAR options */ +#define CMT2300A_INT_POLAR_SEL_0 0x00 +#define CMT2300A_INT_POLAR_SEL_1 0x20 +/* CMT2300A_MASK_INT1_SEL options */ +#define CMT2300A_INT_SEL_RX_ACTIVE 0x00 +#define CMT2300A_INT_SEL_TX_ACTIVE 0x01 +#define CMT2300A_INT_SEL_RSSI_VLD 0x02 +#define CMT2300A_INT_SEL_PREAM_OK 0x03 +#define CMT2300A_INT_SEL_SYNC_OK 0x04 +#define CMT2300A_INT_SEL_NODE_OK 0x05 +#define CMT2300A_INT_SEL_CRC_OK 0x06 +#define CMT2300A_INT_SEL_PKT_OK 0x07 +#define CMT2300A_INT_SEL_SL_TMO 0x08 +#define CMT2300A_INT_SEL_RX_TMO 0x09 +#define CMT2300A_INT_SEL_TX_DONE 0x0A +#define CMT2300A_INT_SEL_RX_FIFO_NMTY 0x0B +#define CMT2300A_INT_SEL_RX_FIFO_TH 0x0C +#define CMT2300A_INT_SEL_RX_FIFO_FULL 0x0D +#define CMT2300A_INT_SEL_RX_FIFO_WBYTE 0x0E +#define CMT2300A_INT_SEL_RX_FIFO_OVF 0x0F +#define CMT2300A_INT_SEL_TX_FIFO_NMTY 0x10 +#define CMT2300A_INT_SEL_TX_FIFO_TH 0x11 +#define CMT2300A_INT_SEL_TX_FIFO_FULL 0x12 +#define CMT2300A_INT_SEL_STATE_IS_STBY 0x13 +#define CMT2300A_INT_SEL_STATE_IS_FS 0x14 +#define CMT2300A_INT_SEL_STATE_IS_RX 0x15 +#define CMT2300A_INT_SEL_STATE_IS_TX 0x16 +#define CMT2300A_INT_SEL_LED 0x17 +#define CMT2300A_INT_SEL_TRX_ACTIVE 0x18 +#define CMT2300A_INT_SEL_PKT_DONE 0x19 + +/* ********** CMT2300A_CUS_INT2_CTL registers ********** */ +#define CMT2300A_MASK_LFOSC_OUT_EN 0x40 +#define CMT2300A_MASK_TX_DIN_INV 0x20 +#define CMT2300A_MASK_INT2_SEL 0x1F + +/* ********** CMT2300A_CUS_INT_EN registers ********** */ +#define CMT2300A_MASK_SL_TMO_EN 0x80 +#define CMT2300A_MASK_RX_TMO_EN 0x40 +#define CMT2300A_MASK_TX_DONE_EN 0x20 +#define CMT2300A_MASK_PREAM_OK_EN 0x10 +#define CMT2300A_MASK_SYNC_OK_EN 0x08 +#define CMT2300A_MASK_NODE_OK_EN 0x04 +#define CMT2300A_MASK_CRC_OK_EN 0x02 +#define CMT2300A_MASK_PKT_DONE_EN 0x01 + +/* ********** CMT2300A_CUS_FIFO_CTL registers ********** */ +#define CMT2300A_MASK_TX_DIN_EN 0x80 +#define CMT2300A_MASK_TX_DIN_SEL 0x60 +#define CMT2300A_MASK_FIFO_AUTO_CLR_DIS 0x10 +#define CMT2300A_MASK_FIFO_TX_RD_EN 0x08 +#define CMT2300A_MASK_FIFO_RX_TX_SEL 0x04 +#define CMT2300A_MASK_FIFO_MERGE_EN 0x02 +#define CMT2300A_MASK_SPI_FIFO_RD_WR_SEL 0x01 +/* CMT2300A_MASK_TX_DIN_SEL options */ +#define CMT2300A_TX_DIN_SEL_GPIO1 0x00 +#define CMT2300A_TX_DIN_SEL_GPIO2 0x20 +#define CMT2300A_TX_DIN_SEL_GPIO3 0x40 + +/* ********** CMT2300A_CUS_INT_CLR1 registers ********** */ +#define CMT2300A_MASK_SL_TMO_FLG 0x20 +#define CMT2300A_MASK_RX_TMO_FLG 0x10 +#define CMT2300A_MASK_TX_DONE_FLG 0x08 +#define CMT2300A_MASK_TX_DONE_CLR 0x04 +#define CMT2300A_MASK_SL_TMO_CLR 0x02 +#define CMT2300A_MASK_RX_TMO_CLR 0x01 + +/* ********** CMT2300A_CUS_INT_CLR2 registers ********** */ +#define CMT2300A_MASK_LBD_CLR 0x20 +#define CMT2300A_MASK_PREAM_OK_CLR 0x10 +#define CMT2300A_MASK_SYNC_OK_CLR 0x08 +#define CMT2300A_MASK_NODE_OK_CLR 0x04 +#define CMT2300A_MASK_CRC_OK_CLR 0x02 +#define CMT2300A_MASK_PKT_DONE_CLR 0x01 + +/* ********** CMT2300A_CUS_FIFO_CLR registers ********** */ +#define CMT2300A_MASK_FIFO_RESTORE 0x04 +#define CMT2300A_MASK_FIFO_CLR_RX 0x02 +#define CMT2300A_MASK_FIFO_CLR_TX 0x01 + +/* ********** CMT2300A_CUS_INT_FLAG registers ********** */ +#define CMT2300A_MASK_LBD_FLG 0x80 +#define CMT2300A_MASK_COL_ERR_FLG 0x40 +#define CMT2300A_MASK_PKT_ERR_FLG 0x20 +#define CMT2300A_MASK_PREAM_OK_FLG 0x10 +#define CMT2300A_MASK_SYNC_OK_FLG 0x08 +#define CMT2300A_MASK_NODE_OK_FLG 0x04 +#define CMT2300A_MASK_CRC_OK_FLG 0x02 +#define CMT2300A_MASK_PKT_OK_FLG 0x01 + +/* ********** CMT2300A_CUS_FIFO_FLAG registers ********** */ +#define CMT2300A_MASK_RX_FIFO_FULL_FLG 0x40 +#define CMT2300A_MASK_RX_FIFO_NMTY_FLG 0x20 +#define CMT2300A_MASK_RX_FIFO_TH_FLG 0x10 +#define CMT2300A_MASK_RX_FIFO_OVF_FLG 0x08 +#define CMT2300A_MASK_TX_FIFO_FULL_FLG 0x04 +#define CMT2300A_MASK_TX_FIFO_NMTY_FLG 0x02 +#define CMT2300A_MASK_TX_FIFO_TH_FLG 0x01 + +/* ********** CMT2300A_CUS_RSSI_CODE registers ********** */ +#define CMT2300A_MASK_RSSI_CODE 0xFF + +/* ********** CMT2300A_CUS_RSSI_DBM registers ********** */ +#define CMT2300A_MASK_RSSI_DBM 0xFF + +/* ********** CMT2300A_CUS_LBD_RESULT registers ********** */ +#define CMT2300A_MASK_LBD_RESULT 0xFF + +#endif diff --git a/lib/CMT2300a/cmt2300a_hal.c b/lib/CMT2300a/cmt2300a_hal.c new file mode 100644 index 00000000..4fe01668 --- /dev/null +++ b/lib/CMT2300a/cmt2300a_hal.c @@ -0,0 +1,76 @@ +/* + * THE FOLLOWING FIRMWARE IS PROVIDED: (1) "AS IS" WITH NO WARRANTY; AND + * (2)TO ENABLE ACCESS TO CODING INFORMATION TO GUIDE AND FACILITATE CUSTOMER. + * CONSEQUENTLY, CMOSTEK SHALL NOT BE HELD LIABLE FOR ANY DIRECT, INDIRECT OR + * CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING FROM THE CONTENT + * OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE CODING INFORMATION + * CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS. + * + * Copyright (C) CMOSTEK SZ. + */ + +/*! + * @file cmt2300a_hal.c + * @brief CMT2300A hardware abstraction layer + * + * @version 1.2 + * @date Jul 17 2017 + * @author CMOSTEK R@D + */ + +#include "cmt2300a_hal.h" +#include "cmt_spi3.h" +#include + +/*! ******************************************************** + * @name CMT2300A_InitSpi + * @desc Initializes the CMT2300A SPI interface. + * *********************************************************/ +void CMT2300A_InitSpi(void) +{ + cmt_spi3_init(); +} + +/*! ******************************************************** + * @name CMT2300A_ReadReg + * @desc Read the CMT2300A register at the specified address. + * @param addr: register address + * @return Register value + * *********************************************************/ +uint8_t CMT2300A_ReadReg(uint8_t addr) +{ + return cmt_spi3_read(addr); +} + +/*! ******************************************************** + * @name CMT2300A_WriteReg + * @desc Write the CMT2300A register at the specified address. + * @param addr: register address + * dat: register value + * *********************************************************/ +void CMT2300A_WriteReg(uint8_t addr, uint8_t dat) +{ + cmt_spi3_write(addr, dat); +} + +/*! ******************************************************** + * @name CMT2300A_ReadFifo + * @desc Reads the contents of the CMT2300A FIFO. + * @param buf: buffer where to copy the FIFO read data + * len: number of bytes to be read from the FIFO + * *********************************************************/ +void CMT2300A_ReadFifo(uint8_t buf[], uint16_t len) +{ + cmt_spi3_read_fifo(buf, len); +} + +/*! ******************************************************** + * @name CMT2300A_WriteFifo + * @desc Writes the buffer contents to the CMT2300A FIFO. + * @param buf: buffer containing data to be put on the FIFO + * len: number of bytes to be written to the FIFO + * *********************************************************/ +void CMT2300A_WriteFifo(const uint8_t buf[], uint16_t len) +{ + cmt_spi3_write_fifo(buf, len); +} diff --git a/lib/CMT2300a/cmt2300a_hal.h b/lib/CMT2300a/cmt2300a_hal.h new file mode 100644 index 00000000..eb4937b3 --- /dev/null +++ b/lib/CMT2300a/cmt2300a_hal.h @@ -0,0 +1,51 @@ +/* + * THE FOLLOWING FIRMWARE IS PROVIDED: (1) "AS IS" WITH NO WARRANTY; AND + * (2)TO ENABLE ACCESS TO CODING INFORMATION TO GUIDE AND FACILITATE CUSTOMER. + * CONSEQUENTLY, CMOSTEK SHALL NOT BE HELD LIABLE FOR ANY DIRECT, INDIRECT OR + * CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING FROM THE CONTENT + * OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE CODING INFORMATION + * CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS. + * + * Copyright (C) CMOSTEK SZ. + */ + +/*! + * @file cmt2300a_hal.h + * @brief CMT2300A hardware abstraction layer + * + * @version 1.2 + * @date Jul 17 2017 + * @author CMOSTEK R@D + */ + +#ifndef __CMT2300A_HAL_H +#define __CMT2300A_HAL_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ************************************************************************ + * The following need to be modified by user + * ************************************************************************ */ +#define CMT2300A_DelayMs(ms) delay(ms) +#define CMT2300A_DelayUs(us) delayMicroseconds(us) +#define CMT2300A_GetTickCount() millis() +/* ************************************************************************ */ + +void CMT2300A_InitSpi(void); + +uint8_t CMT2300A_ReadReg(uint8_t addr); +void CMT2300A_WriteReg(uint8_t addr, uint8_t dat); + +void CMT2300A_ReadFifo(uint8_t buf[], uint16_t len); +void CMT2300A_WriteFifo(const uint8_t buf[], uint16_t len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/lib/CMT2300a/cmt2300a_params.h b/lib/CMT2300a/cmt2300a_params.h new file mode 100644 index 00000000..5dc80311 --- /dev/null +++ b/lib/CMT2300a/cmt2300a_params.h @@ -0,0 +1,215 @@ +#ifndef __CMT2300A_PARAMS_H +#define __CMT2300A_PARAMS_H + +#include "cmt2300a_defs.h" +#include + +/* [CMT Bank] */ +static uint8_t g_cmt2300aCmtBank_default[CMT2300A_CMT_BANK_SIZE] = { + 0x00, + 0x66, + 0xEC, + 0x1D, + 0x70, + 0x80, + 0x14, + 0x08, + 0x91, + 0x02, + 0x02, + 0xD0, +}; + +/* [CMT Bank] with RSSI offset of +- 0 and 13dBm*/ +static uint8_t g_cmt2300aCmtBank_RSSI_0[CMT2300A_CMT_BANK_SIZE] = { + 0x00, + 0x66, + 0xEC, + 0x1C, + 0x70, + 0x80, + 0x14, + 0x08, + 0x11, + 0x02, + 0x02, + 0x00, +}; + +/* [System Bank] */ +static uint8_t g_cmt2300aSystemBank[CMT2300A_SYSTEM_BANK_SIZE] = { + 0xAE, + 0xE0, + 0x35, + 0x00, + 0x00, + 0xF4, + 0x10, + 0xE2, + 0x42, + 0x20, + 0x0C, + 0x81, +}; + +/* [Frequency Bank] 868 MHz (default) */ +static uint8_t g_cmt2300aFrequencyBank_868[CMT2300A_FREQUENCY_BANK_SIZE] = { + 0x42, + 0xCF, + 0xA7, + 0x8C, + 0x42, + 0xC4, + 0x4E, + 0x1C, +}; + +/* [Frequency Bank] 863 MHz (EU) */ +static uint8_t g_cmt2300aFrequencyBank_863[CMT2300A_FREQUENCY_BANK_SIZE] = { + 0x42, + 0x6D, + 0x80, + 0x86, + 0x42, + 0x62, + 0x27, + 0x16, +}; + +/* [Frequency Bank] 860 MHz */ +static uint8_t g_cmt2300aFrequencyBank_860[CMT2300A_FREQUENCY_BANK_SIZE] = { + 0x42, + 0x32, + 0xCF, + 0x82, + 0x42, + 0x27, + 0x76, + 0x12, +}; + +/* [Data Rate Bank] */ +static uint8_t g_cmt2300aDataRateBank[CMT2300A_DATA_RATE_BANK_SIZE] = { + 0xA6, + 0xC9, + 0x20, + 0x20, + 0xD2, + 0x35, + 0x0C, + 0x0A, + 0x9F, + 0x4B, + 0x29, + 0x29, + 0xC0, + 0x14, + 0x05, + 0x53, + 0x10, + 0x00, + 0xB4, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, +}; + +/* [Baseband Bank] - default */ +static uint8_t g_cmt2300aBasebandBank_default[CMT2300A_BASEBAND_BANK_SIZE] = { + 0x12, + 0x1E, + 0x00, + 0xAA, + 0x06, + 0x00, + 0x00, + 0x00, + 0x00, + 0xD6, + 0xD5, + 0xD4, + 0x2D, + 0x01, + 0x1F, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xC3, + 0x00, + 0x00, + 0x60, + 0xFF, + 0x00, + 0x00, + 0x1F, + 0x10, +}; + +/* [Baseband Bank] - EU */ +static uint8_t g_cmt2300aBasebandBank_EU[CMT2300A_BASEBAND_BANK_SIZE] = { + 0x12, + 0x1E, + 0x00, + 0xAA, + 0x06, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x5A, + 0x48, + 0x4D, + 0x01, + 0x1D, // default should be 0x1F (32 bytes length), but 0x45 is 0x01 -> variable length, so this value is ignored + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xC3, + 0x00, + 0x00, + 0x60, + 0xFF, + 0x00, + 0x00, + 0x1F, + 0x10, +}; + +/* [Tx Bank] 20 dBm */ +static uint8_t g_cmt2300aTxBank_20dBm[CMT2300A_TX_BANK_SIZE] = { + 0x70, + 0x4D, + 0x06, + 0x00, + 0x07, + 0x50, + 0x00, + 0x8A, + 0x18, + 0x3F, + 0x7F, +}; + +/* [Tx Bank] 13 dBm */ +static uint8_t g_cmt2300aTxBank_13dBm[CMT2300A_TX_BANK_SIZE] = { + 0x70, + 0x4D, + 0x06, + 0x00, + 0x07, + 0x50, + 0x00, + 0x42, + 0x0C, + 0x3F, + 0x7F, +}; + +#endif diff --git a/lib/CMT2300a/cmt_spi3.c b/lib/CMT2300a/cmt_spi3.c new file mode 100644 index 00000000..79805ed2 --- /dev/null +++ b/lib/CMT2300a/cmt_spi3.c @@ -0,0 +1,120 @@ +#include "cmt_spi3.h" +#include +#include +#include // for esp_rom_gpio_connect_out_signal + +#define CMT_SPI_CLK 1000000 // 1 MHz + +spi_device_handle_t spi_reg, spi_fifo; + +void cmt_spi3_init(void) +{ + spi_bus_config_t buscfg = { + .mosi_io_num = CMT_PIN_SDIO, + .miso_io_num = -1, // single wire MOSI/MISO + .sclk_io_num = CMT_PIN_CLK, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = 32, + }; + spi_device_interface_config_t devcfg = { + .command_bits = 0, + .address_bits = 0, + .dummy_bits = 0, + .mode = 0, // SPI mode 0 + .clock_speed_hz = CMT_SPI_CLK, + .spics_io_num = CMT_PIN_CS, + .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, + .queue_size = 1, + .pre_cb = NULL, + .post_cb = NULL, + }; + + ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, 0)); + ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &devcfg, &spi_reg)); + + // FiFo + spi_device_interface_config_t devcfg2 = { + .command_bits = 0, + .address_bits = 0, + .dummy_bits = 0, + .mode = 0, // SPI mode 0 + .cs_ena_pretrans = 2, + .cs_ena_posttrans = (uint8_t)(1 / (CMT_SPI_CLK * 10e6 * 2) + 2), // >2 us + .clock_speed_hz = CMT_SPI_CLK, + .spics_io_num = CMT_PIN_FCS, + .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, + .queue_size = 1, + .pre_cb = NULL, + .post_cb = NULL, + }; + ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &devcfg2, &spi_fifo)); + + esp_rom_gpio_connect_out_signal(CMT_PIN_SDIO, spi_periph_signal[SPI2_HOST].spid_out, true, false); + delay(100); +} + +void cmt_spi3_write(uint8_t addr, uint8_t dat) +{ + uint8_t tx_data[2]; + tx_data[0] = ~addr; + tx_data[1] = ~dat; + spi_transaction_t t = { + .length = 2 * 8, + .tx_buffer = &tx_data, + .rx_buffer = NULL + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); + delayMicroseconds(100); +} + +uint8_t cmt_spi3_read(uint8_t addr) +{ + uint8_t tx_data, rx_data; + tx_data = ~(addr | 0x80); // negation and MSB high (read command) + spi_transaction_t t = { + .length = 8, + .rxlength = 8, + .tx_buffer = &tx_data, + .rx_buffer = &rx_data + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); + delayMicroseconds(100); + return rx_data; +} + +void cmt_spi3_write_fifo(const uint8_t* buf, uint16_t len) +{ + uint8_t tx_data; + + spi_transaction_t t = { + .flags = SPI_TRANS_MODE_OCT, + .length = 8, + .tx_buffer = &tx_data, // reference to write data + .rx_buffer = NULL + }; + + for (uint8_t i = 0; i < len; i++) { + tx_data = ~buf[i]; // negate buffer contents + ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); + delayMicroseconds(4); // > 4 us + } +} + +void cmt_spi3_read_fifo(uint8_t* buf, uint16_t len) +{ + uint8_t rx_data; + + spi_transaction_t t = { + .length = 8, + .rxlength = 8, + .tx_buffer = NULL, + .rx_buffer = &rx_data + }; + + for (uint8_t i = 0; i < len; i++) { + ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); + delayMicroseconds(4); // > 4 us + buf[i] = rx_data; + } +} diff --git a/lib/CMT2300a/cmt_spi3.h b/lib/CMT2300a/cmt_spi3.h new file mode 100644 index 00000000..db11f952 --- /dev/null +++ b/lib/CMT2300a/cmt_spi3.h @@ -0,0 +1,14 @@ +#ifndef __CMT_SPI3_H +#define __CMT_SPI3_H + +#include + +void cmt_spi3_init(void); + +void cmt_spi3_write(uint8_t addr, uint8_t dat); +uint8_t cmt_spi3_read(uint8_t addr); + +void cmt_spi3_write_fifo(const uint8_t* p_buf, uint16_t len); +void cmt_spi3_read_fifo(uint8_t* p_buf, uint16_t len); + +#endif diff --git a/platformio.ini b/platformio.ini index ecf1632d..841da184 100644 --- a/platformio.ini +++ b/platformio.ini @@ -56,6 +56,11 @@ build_flags = ${env.build_flags} -DHOYMILES_PIN_IRQ=16 -DHOYMILES_PIN_CE=4 -DHOYMILES_PIN_CS=5 + -DCMT_PIN_CLK=18 + -DCMT_PIN_SDIO=23 + -DCMT_PIN_CS=5 + -DCMT_PIN_FCS=4 + -DCMT_PIN_GPIO3=15 [env:olimex_esp32_poe] From c2e4c5d43e6b71c76f00764b310843ffcb0764d2 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 6 Mar 2023 19:02:10 +0100 Subject: [PATCH 05/66] Added first implementation of HMS inverter classes --- lib/Hoymiles/src/Hoymiles.cpp | 11 +++- lib/Hoymiles/src/HoymilesRadio.h | 1 + lib/Hoymiles/src/inverters/HMS_1CH.cpp | 25 +++++++++ lib/Hoymiles/src/inverters/HMS_1CH.h | 38 ++++++++++++++ lib/Hoymiles/src/inverters/HMS_2CH.cpp | 25 +++++++++ lib/Hoymiles/src/inverters/HMS_2CH.h | 45 ++++++++++++++++ lib/Hoymiles/src/inverters/HMS_4CH.cpp | 25 +++++++++ lib/Hoymiles/src/inverters/HMS_4CH.h | 58 +++++++++++++++++++++ lib/Hoymiles/src/inverters/HMS_Abstract.cpp | 8 +++ lib/Hoymiles/src/inverters/HMS_Abstract.h | 9 ++++ 10 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 lib/Hoymiles/src/inverters/HMS_1CH.cpp create mode 100644 lib/Hoymiles/src/inverters/HMS_1CH.h create mode 100644 lib/Hoymiles/src/inverters/HMS_2CH.cpp create mode 100644 lib/Hoymiles/src/inverters/HMS_2CH.h create mode 100644 lib/Hoymiles/src/inverters/HMS_4CH.cpp create mode 100644 lib/Hoymiles/src/inverters/HMS_4CH.h create mode 100644 lib/Hoymiles/src/inverters/HMS_Abstract.cpp create mode 100644 lib/Hoymiles/src/inverters/HMS_Abstract.h diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 32074e8a..58675278 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -6,6 +6,9 @@ #include "inverters/HM_1CH.h" #include "inverters/HM_2CH.h" #include "inverters/HM_4CH.h" +#include "inverters/HMS_1CH.h" +#include "inverters/HMS_2CH.h" +#include "inverters/HMS_4CH.h" #include #define HOY_SEMAPHORE_TAKE() xSemaphoreTake(_xSemaphore, portMAX_DELAY) @@ -85,7 +88,13 @@ void HoymilesClass::loop() std::shared_ptr HoymilesClass::addInverter(const char* name, uint64_t serial) { std::shared_ptr i = nullptr; - if (HM_4CH::isValidSerial(serial)) { + if (HMS_4CH::isValidSerial(serial)) { + i = std::make_shared(serial); + } else if (HMS_2CH::isValidSerial(serial)) { + i = std::make_shared(serial); + } else if (HMS_1CH::isValidSerial(serial)) { + i = std::make_shared(serial); + } else if (HM_4CH::isValidSerial(serial)) { i = std::make_shared(serial); } else if (HM_2CH::isValidSerial(serial)) { i = std::make_shared(serial); diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 6963361c..e80975cb 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -8,6 +8,7 @@ #include #include #include +#include // number of fragments hold in buffer #define FRAGMENT_BUFFER_SIZE 30 diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.cpp b/lib/Hoymiles/src/inverters/HMS_1CH.cpp new file mode 100644 index 00000000..a4e05c92 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMS_1CH.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "HMS_1CH.h" + +HMS_1CH::HMS_1CH(uint64_t serial) + : HMS_Abstract(serial) {}; + +bool HMS_1CH::isValidSerial(uint64_t serial) +{ + // serial >= 0x112400000000 && serial <= 0x112499999999 + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x1124; +} + +String HMS_1CH::typeName() +{ + return "HMS-300, HMS-350, HMS-400, HMS-450, HMS-500"; +} + +const std::list* HMS_1CH::getByteAssignment() +{ + return &byteAssignment; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.h b/lib/Hoymiles/src/inverters/HMS_1CH.h new file mode 100644 index 00000000..8db300c5 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMS_1CH.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HMS_Abstract.h" +#include + +class HMS_1CH : public HMS_Abstract { +public: + explicit HMS_1CH(uint64_t serial); + static bool isValidSerial(uint64_t serial); + String typeName(); + const std::list* getByteAssignment(); + +private: + const std::list byteAssignment = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 12, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 8, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 14, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 22, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 18, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_PRA, UNIT_VA, 20, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 16, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 24, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 26, 2, 10, true, 1 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 28, 2, 1, false, 0 }, + + { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + }; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp new file mode 100644 index 00000000..a3ee7ce0 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "HMS_2CH.h" + +HMS_2CH::HMS_2CH(uint64_t serial) + : HMS_Abstract(serial) {}; + +bool HMS_2CH::isValidSerial(uint64_t serial) +{ + // serial >= 0x114400000000 && serial <= 0x114499999999 + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x1144; +} + +String HMS_2CH::typeName() +{ + return "HMS-600, HMS-700, HMS-800, HMS-900, HMS-1000"; +} + +const std::list* HMS_2CH::getByteAssignment() +{ + return &byteAssignment; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.h b/lib/Hoymiles/src/inverters/HMS_2CH.h new file mode 100644 index 00000000..1815a685 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMS_2CH.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HMS_Abstract.h" +#include + +class HMS_2CH : public HMS_Abstract { +public: + explicit HMS_2CH(uint64_t serial); + static bool isValidSerial(uint64_t serial); + String typeName(); + const std::list* getByteAssignment(); + +private: + const std::list byteAssignment = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + + { TYPE_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 }, + { TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, + { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_PRA, UNIT_VA, 32, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, + + { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + }; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.cpp b/lib/Hoymiles/src/inverters/HMS_4CH.cpp new file mode 100644 index 00000000..c9f01f25 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMS_4CH.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "HMS_4CH.h" + +HMS_4CH::HMS_4CH(uint64_t serial) + : HMS_Abstract(serial) {}; + +bool HMS_4CH::isValidSerial(uint64_t serial) +{ + // serial >= 0x114400000000 && serial <= 0x114499999999 + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x1164; +} + +String HMS_4CH::typeName() +{ + return "HMS-1600, HMS-1800, HMS-2000"; +} + +const std::list* HMS_4CH::getByteAssignment() +{ + return &byteAssignment; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.h b/lib/Hoymiles/src/inverters/HMS_4CH.h new file mode 100644 index 00000000..f7e9bced --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMS_4CH.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HMS_Abstract.h" + +class HMS_4CH : public HMS_Abstract { +public: + explicit HMS_4CH(uint64_t serial); + static bool isValidSerial(uint64_t serial); + String typeName(); + const std::list* getByteAssignment(); + +private: + const std::list byteAssignment = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + + { TYPE_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 }, + { TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, + { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + + { TYPE_DC, CH2, FLD_UDC, UNIT_V, 26, 2, 10, false, 1 }, + { TYPE_DC, CH2, FLD_IDC, UNIT_A, 30, 2, 100, false, 2 }, + { TYPE_DC, CH2, FLD_PDC, UNIT_W, 34, 2, 10, false, 1 }, + { TYPE_DC, CH2, FLD_YD, UNIT_WH, 46, 2, 1, false, 0 }, + { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + + { TYPE_DC, CH3, FLD_UDC, UNIT_V, 28, 2, 10, false, 1 }, + { TYPE_DC, CH3, FLD_IDC, UNIT_A, 32, 2, 100, false, 2 }, + { TYPE_DC, CH3, FLD_PDC, UNIT_W, 36, 2, 10, false, 1 }, + { TYPE_DC, CH3, FLD_YD, UNIT_WH, 48, 2, 1, false, 0 }, + { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 42, 4, 1000, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 50, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 58, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 54, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_PRA, UNIT_VA, 56, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 52, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 60, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 62, 2, 10, true, 1 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 64, 2, 1, false, 0 }, + + { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + }; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp new file mode 100644 index 00000000..c0070a91 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "HMS_Abstract.h" + +HMS_Abstract::HMS_Abstract(uint64_t serial) + : HM_Abstract(serial) {}; diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.h b/lib/Hoymiles/src/inverters/HMS_Abstract.h new file mode 100644 index 00000000..20bfa5f2 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_Abstract.h" + +class HMS_Abstract : public HM_Abstract { +public: + explicit HMS_Abstract(uint64_t serial); +}; \ No newline at end of file From a7e9aaa8622924263a07a28664b0ceba2e98ab6d Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 6 Mar 2023 20:20:16 +0100 Subject: [PATCH 06/66] Move reference to the radio instance into the inverter instance This is required to support different radios for different inverters --- lib/Hoymiles/src/Hoymiles.cpp | 24 +++++----- lib/Hoymiles/src/inverters/HMS_1CH.cpp | 4 +- lib/Hoymiles/src/inverters/HMS_1CH.h | 2 +- lib/Hoymiles/src/inverters/HMS_2CH.cpp | 4 +- lib/Hoymiles/src/inverters/HMS_2CH.h | 2 +- lib/Hoymiles/src/inverters/HMS_4CH.cpp | 4 +- lib/Hoymiles/src/inverters/HMS_4CH.h | 2 +- lib/Hoymiles/src/inverters/HMS_Abstract.cpp | 4 +- lib/Hoymiles/src/inverters/HMS_Abstract.h | 2 +- lib/Hoymiles/src/inverters/HM_1CH.cpp | 4 +- lib/Hoymiles/src/inverters/HM_1CH.h | 2 +- lib/Hoymiles/src/inverters/HM_2CH.cpp | 4 +- lib/Hoymiles/src/inverters/HM_2CH.h | 2 +- lib/Hoymiles/src/inverters/HM_4CH.cpp | 4 +- lib/Hoymiles/src/inverters/HM_4CH.h | 2 +- lib/Hoymiles/src/inverters/HM_Abstract.cpp | 46 +++++++++---------- lib/Hoymiles/src/inverters/HM_Abstract.h | 20 ++++---- .../src/inverters/InverterAbstract.cpp | 3 +- lib/Hoymiles/src/inverters/InverterAbstract.h | 23 ++++++---- src/MqttHandleInverter.cpp | 12 ++--- src/WebApi_limit.cpp | 2 +- src/WebApi_power.cpp | 4 +- 22 files changed, 90 insertions(+), 86 deletions(-) diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 58675278..2392af7c 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -41,36 +41,36 @@ void HoymilesClass::loop() _messageOutput->print("Fetch inverter: "); _messageOutput->println(iv->serial(), HEX); - iv->sendStatsRequest(_radio.get()); + iv->sendStatsRequest(); // Fetch event log bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; - iv->sendAlarmLogRequest(_radio.get(), force); + iv->sendAlarmLogRequest(force); // Fetch limit if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK) || ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { _messageOutput->println("Request SystemConfigPara"); - iv->sendSystemConfigParaRequest(_radio.get()); + iv->sendSystemConfigParaRequest(); } // Set limit if required if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { _messageOutput->println("Resend ActivePowerControl"); - iv->resendActivePowerControlRequest(_radio.get()); + iv->resendActivePowerControlRequest(); } // Set power status if required if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { _messageOutput->println("Resend PowerCommand"); - iv->resendPowerControlRequest(_radio.get()); + iv->resendPowerControlRequest(); } // Fetch dev info (but first fetch stats) if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) { _messageOutput->println("Request device info"); - iv->sendDevInfoRequest(_radio.get()); + iv->sendDevInfoRequest(); } } if (++inverterPos >= getNumInverters()) { @@ -89,17 +89,17 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, u { std::shared_ptr i = nullptr; if (HMS_4CH::isValidSerial(serial)) { - i = std::make_shared(serial); + i = std::make_shared(_radio.get(), serial); } else if (HMS_2CH::isValidSerial(serial)) { - i = std::make_shared(serial); + i = std::make_shared(_radio.get(), serial); } else if (HMS_1CH::isValidSerial(serial)) { - i = std::make_shared(serial); + i = std::make_shared(_radio.get(), serial); } else if (HM_4CH::isValidSerial(serial)) { - i = std::make_shared(serial); + i = std::make_shared(_radio.get(), serial); } else if (HM_2CH::isValidSerial(serial)) { - i = std::make_shared(serial); + i = std::make_shared(_radio.get(), serial); } else if (HM_1CH::isValidSerial(serial)) { - i = std::make_shared(serial); + i = std::make_shared(_radio.get(), serial); } if (i) { diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.cpp b/lib/Hoymiles/src/inverters/HMS_1CH.cpp index a4e05c92..9fe5406c 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CH.cpp @@ -4,8 +4,8 @@ */ #include "HMS_1CH.h" -HMS_1CH::HMS_1CH(uint64_t serial) - : HMS_Abstract(serial) {}; +HMS_1CH::HMS_1CH(HoymilesRadio* radio, uint64_t serial) + : HMS_Abstract(radio, serial) {}; bool HMS_1CH::isValidSerial(uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.h b/lib/Hoymiles/src/inverters/HMS_1CH.h index 8db300c5..5902e7c1 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CH.h +++ b/lib/Hoymiles/src/inverters/HMS_1CH.h @@ -6,7 +6,7 @@ class HMS_1CH : public HMS_Abstract { public: - explicit HMS_1CH(uint64_t serial); + explicit HMS_1CH(HoymilesRadio* radio, uint64_t serial); static bool isValidSerial(uint64_t serial); String typeName(); const std::list* getByteAssignment(); diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index a3ee7ce0..2b1ea490 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -4,8 +4,8 @@ */ #include "HMS_2CH.h" -HMS_2CH::HMS_2CH(uint64_t serial) - : HMS_Abstract(serial) {}; +HMS_2CH::HMS_2CH(HoymilesRadio* radio, uint64_t serial) + : HMS_Abstract(radio, serial) {}; bool HMS_2CH::isValidSerial(uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.h b/lib/Hoymiles/src/inverters/HMS_2CH.h index 1815a685..d5e76c51 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.h +++ b/lib/Hoymiles/src/inverters/HMS_2CH.h @@ -6,7 +6,7 @@ class HMS_2CH : public HMS_Abstract { public: - explicit HMS_2CH(uint64_t serial); + explicit HMS_2CH(HoymilesRadio* radio, uint64_t serial); static bool isValidSerial(uint64_t serial); String typeName(); const std::list* getByteAssignment(); diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.cpp b/lib/Hoymiles/src/inverters/HMS_4CH.cpp index c9f01f25..db2df148 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_4CH.cpp @@ -4,8 +4,8 @@ */ #include "HMS_4CH.h" -HMS_4CH::HMS_4CH(uint64_t serial) - : HMS_Abstract(serial) {}; +HMS_4CH::HMS_4CH(HoymilesRadio* radio, uint64_t serial) + : HMS_Abstract(radio, serial) {}; bool HMS_4CH::isValidSerial(uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.h b/lib/Hoymiles/src/inverters/HMS_4CH.h index f7e9bced..090bea59 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.h +++ b/lib/Hoymiles/src/inverters/HMS_4CH.h @@ -5,7 +5,7 @@ class HMS_4CH : public HMS_Abstract { public: - explicit HMS_4CH(uint64_t serial); + explicit HMS_4CH(HoymilesRadio* radio, uint64_t serial); static bool isValidSerial(uint64_t serial); String typeName(); const std::list* getByteAssignment(); diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp index c0070a91..7c0ea34c 100644 --- a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp @@ -4,5 +4,5 @@ */ #include "HMS_Abstract.h" -HMS_Abstract::HMS_Abstract(uint64_t serial) - : HM_Abstract(serial) {}; +HMS_Abstract::HMS_Abstract(HoymilesRadio* radio, uint64_t serial) + : HM_Abstract(radio, serial) {}; diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.h b/lib/Hoymiles/src/inverters/HMS_Abstract.h index 20bfa5f2..5ec60a01 100644 --- a/lib/Hoymiles/src/inverters/HMS_Abstract.h +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.h @@ -5,5 +5,5 @@ class HMS_Abstract : public HM_Abstract { public: - explicit HMS_Abstract(uint64_t serial); + explicit HMS_Abstract(HoymilesRadio* radio, uint64_t serial); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index 031f87d6..df3a8442 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -4,8 +4,8 @@ */ #include "HM_1CH.h" -HM_1CH::HM_1CH(uint64_t serial) - : HM_Abstract(serial) {}; +HM_1CH::HM_1CH(HoymilesRadio* radio, uint64_t serial) + : HM_Abstract(radio, serial) {}; bool HM_1CH::isValidSerial(uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HM_1CH.h b/lib/Hoymiles/src/inverters/HM_1CH.h index 94186467..11dcbe24 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.h +++ b/lib/Hoymiles/src/inverters/HM_1CH.h @@ -6,7 +6,7 @@ class HM_1CH : public HM_Abstract { public: - explicit HM_1CH(uint64_t serial); + explicit HM_1CH(HoymilesRadio* radio, uint64_t serial); static bool isValidSerial(uint64_t serial); String typeName(); const std::list* getByteAssignment(); diff --git a/lib/Hoymiles/src/inverters/HM_2CH.cpp b/lib/Hoymiles/src/inverters/HM_2CH.cpp index bdd18cc3..87c5f394 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -5,8 +5,8 @@ */ #include "HM_2CH.h" -HM_2CH::HM_2CH(uint64_t serial) - : HM_Abstract(serial) {}; +HM_2CH::HM_2CH(HoymilesRadio* radio, uint64_t serial) + : HM_Abstract(radio, serial) {}; bool HM_2CH::isValidSerial(uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HM_2CH.h b/lib/Hoymiles/src/inverters/HM_2CH.h index 6e8672ca..a79a854b 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.h +++ b/lib/Hoymiles/src/inverters/HM_2CH.h @@ -5,7 +5,7 @@ class HM_2CH : public HM_Abstract { public: - explicit HM_2CH(uint64_t serial); + explicit HM_2CH(HoymilesRadio* radio, uint64_t serial); static bool isValidSerial(uint64_t serial); String typeName(); const std::list* getByteAssignment(); diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp index 6994d533..cc52b482 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -4,8 +4,8 @@ */ #include "HM_4CH.h" -HM_4CH::HM_4CH(uint64_t serial) - : HM_Abstract(serial) {}; +HM_4CH::HM_4CH(HoymilesRadio* radio, uint64_t serial) + : HM_Abstract(radio, serial) {}; bool HM_4CH::isValidSerial(uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HM_4CH.h b/lib/Hoymiles/src/inverters/HM_4CH.h index eccc60f2..dc5ea550 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.h +++ b/lib/Hoymiles/src/inverters/HM_4CH.h @@ -5,7 +5,7 @@ class HM_4CH : public HM_Abstract { public: - explicit HM_4CH(uint64_t serial); + explicit HM_4CH(HoymilesRadio* radio, uint64_t serial); static bool isValidSerial(uint64_t serial); String typeName(); const std::list* getByteAssignment(); diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index a07dec93..097fffeb 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -12,10 +12,10 @@ #include "commands/RealTimeRunDataCommand.h" #include "commands/SystemConfigParaCommand.h" -HM_Abstract::HM_Abstract(uint64_t serial) - : InverterAbstract(serial) {}; +HM_Abstract::HM_Abstract(HoymilesRadio* radio, uint64_t serial) + : InverterAbstract(radio, serial) {}; -bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio) +bool HM_Abstract::sendStatsRequest() { if (!getEnablePolling()) { return false; @@ -29,14 +29,14 @@ bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio) time_t now; time(&now); - RealTimeRunDataCommand* cmd = radio->enqueCommand(); + RealTimeRunDataCommand* cmd = _radio->enqueCommand(); cmd->setTime(now); cmd->setTargetAddress(serial()); return true; } -bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) +bool HM_Abstract::sendAlarmLogRequest(bool force) { if (!getEnablePolling()) { return false; @@ -60,7 +60,7 @@ bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) time_t now; time(&now); - AlarmDataCommand* cmd = radio->enqueCommand(); + AlarmDataCommand* cmd = _radio->enqueCommand(); cmd->setTime(now); cmd->setTargetAddress(serial()); EventLog()->setLastAlarmRequestSuccess(CMD_PENDING); @@ -68,7 +68,7 @@ bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) return true; } -bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) +bool HM_Abstract::sendDevInfoRequest() { if (!getEnablePolling()) { return false; @@ -82,18 +82,18 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) time_t now; time(&now); - DevInfoAllCommand* cmdAll = radio->enqueCommand(); + DevInfoAllCommand* cmdAll = _radio->enqueCommand(); cmdAll->setTime(now); cmdAll->setTargetAddress(serial()); - DevInfoSimpleCommand* cmdSimple = radio->enqueCommand(); + DevInfoSimpleCommand* cmdSimple = _radio->enqueCommand(); cmdSimple->setTime(now); cmdSimple->setTargetAddress(serial()); return true; } -bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) +bool HM_Abstract::sendSystemConfigParaRequest() { if (!getEnablePolling()) { return false; @@ -107,7 +107,7 @@ bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) time_t now; time(&now); - SystemConfigParaCommand* cmd = radio->enqueCommand(); + SystemConfigParaCommand* cmd = _radio->enqueCommand(); cmd->setTime(now); cmd->setTargetAddress(serial()); SystemConfigPara()->setLastLimitRequestSuccess(CMD_PENDING); @@ -115,7 +115,7 @@ bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) return true; } -bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) +bool HM_Abstract::sendActivePowerControlRequest(float limit, PowerLimitControlType type) { if (!getEnableCommands()) { return false; @@ -128,7 +128,7 @@ bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limi _activePowerControlLimit = limit; _activePowerControlType = type; - ActivePowerControlCommand* cmd = radio->enqueCommand(); + ActivePowerControlCommand* cmd = _radio->enqueCommand(); cmd->setActivePowerLimit(limit, type); cmd->setTargetAddress(serial()); SystemConfigPara()->setLastLimitCommandSuccess(CMD_PENDING); @@ -136,12 +136,12 @@ bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limi return true; } -bool HM_Abstract::resendActivePowerControlRequest(HoymilesRadio* radio) +bool HM_Abstract::resendActivePowerControlRequest() { - return sendActivePowerControlRequest(radio, _activePowerControlLimit, _activePowerControlType); + return sendActivePowerControlRequest(_activePowerControlLimit, _activePowerControlType); } -bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) +bool HM_Abstract::sendPowerControlRequest(bool turnOn) { if (!getEnableCommands()) { return false; @@ -153,7 +153,7 @@ bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) _powerState = 0; } - PowerControlCommand* cmd = radio->enqueCommand(); + PowerControlCommand* cmd = _radio->enqueCommand(); cmd->setPowerOn(turnOn); cmd->setTargetAddress(serial()); PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING); @@ -161,7 +161,7 @@ bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) return true; } -bool HM_Abstract::sendRestartControlRequest(HoymilesRadio* radio) +bool HM_Abstract::sendRestartControlRequest() { if (!getEnableCommands()) { return false; @@ -169,7 +169,7 @@ bool HM_Abstract::sendRestartControlRequest(HoymilesRadio* radio) _powerState = 2; - PowerControlCommand* cmd = radio->enqueCommand(); + PowerControlCommand* cmd = _radio->enqueCommand(); cmd->setRestart(); cmd->setTargetAddress(serial()); PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING); @@ -177,17 +177,17 @@ bool HM_Abstract::sendRestartControlRequest(HoymilesRadio* radio) return true; } -bool HM_Abstract::resendPowerControlRequest(HoymilesRadio* radio) +bool HM_Abstract::resendPowerControlRequest() { switch (_powerState) { case 0: - return sendPowerControlRequest(radio, false); + return sendPowerControlRequest(false); break; case 1: - return sendPowerControlRequest(radio, true); + return sendPowerControlRequest(true); break; case 2: - return sendRestartControlRequest(radio); + return sendRestartControlRequest(); break; default: diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.h b/lib/Hoymiles/src/inverters/HM_Abstract.h index fe89dd24..ea44c242 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.h +++ b/lib/Hoymiles/src/inverters/HM_Abstract.h @@ -5,16 +5,16 @@ class HM_Abstract : public InverterAbstract { public: - explicit HM_Abstract(uint64_t serial); - bool sendStatsRequest(HoymilesRadio* radio); - bool sendAlarmLogRequest(HoymilesRadio* radio, bool force = false); - bool sendDevInfoRequest(HoymilesRadio* radio); - bool sendSystemConfigParaRequest(HoymilesRadio* radio); - bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type); - bool resendActivePowerControlRequest(HoymilesRadio* radio); - bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn); - bool sendRestartControlRequest(HoymilesRadio* radio); - bool resendPowerControlRequest(HoymilesRadio* radio); + explicit HM_Abstract(HoymilesRadio* radio, uint64_t serial); + bool sendStatsRequest(); + bool sendAlarmLogRequest(bool force = false); + bool sendDevInfoRequest(); + bool sendSystemConfigParaRequest(); + bool sendActivePowerControlRequest(float limit, PowerLimitControlType type); + bool resendActivePowerControlRequest(); + bool sendPowerControlRequest(bool turnOn); + bool sendRestartControlRequest(); + bool resendPowerControlRequest(); private: uint8_t _lastAlarmLogCnt = 0; diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index d537f3c1..4951cdd8 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -7,9 +7,10 @@ #include "crc.h" #include -InverterAbstract::InverterAbstract(uint64_t serial) +InverterAbstract::InverterAbstract(HoymilesRadio *radio, uint64_t serial) { _serial.u64 = serial; + _radio = radio; char serial_buff[sizeof(uint64_t) * 8 + 1]; snprintf(serial_buff, sizeof(serial_buff), "%0x%08x", diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 079bf7f4..c111fc12 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -32,7 +32,7 @@ class CommandAbstract; class InverterAbstract { public: - explicit InverterAbstract(uint64_t serial); + explicit InverterAbstract(HoymilesRadio* radio, uint64_t serial); void init(); uint64_t serial(); const String& serialString(); @@ -54,15 +54,15 @@ public: void addRxFragment(uint8_t fragment[], uint8_t len); uint8_t verifyAllFragments(CommandAbstract* cmd); - virtual bool sendStatsRequest(HoymilesRadio* radio) = 0; - virtual bool sendAlarmLogRequest(HoymilesRadio* radio, bool force = false) = 0; - virtual bool sendDevInfoRequest(HoymilesRadio* radio) = 0; - virtual bool sendSystemConfigParaRequest(HoymilesRadio* radio) = 0; - virtual bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) = 0; - virtual bool resendActivePowerControlRequest(HoymilesRadio* radio) = 0; - virtual bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) = 0; - virtual bool sendRestartControlRequest(HoymilesRadio* radio) = 0; - virtual bool resendPowerControlRequest(HoymilesRadio* radio) = 0; + virtual bool sendStatsRequest() = 0; + virtual bool sendAlarmLogRequest(bool force = false) = 0; + virtual bool sendDevInfoRequest() = 0; + virtual bool sendSystemConfigParaRequest() = 0; + virtual bool sendActivePowerControlRequest(float limit, PowerLimitControlType type) = 0; + virtual bool resendActivePowerControlRequest() = 0; + virtual bool sendPowerControlRequest(bool turnOn) = 0; + virtual bool sendRestartControlRequest() = 0; + virtual bool resendPowerControlRequest() = 0; AlarmLogParser* EventLog(); DevInfoParser* DevInfo(); @@ -70,6 +70,9 @@ public: StatisticsParser* Statistics(); SystemConfigParaParser* SystemConfigPara(); +protected: + HoymilesRadio* _radio; + private: serial_u _serial; String _serialString; diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index 449cc0e4..ba73767d 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -208,18 +208,18 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) { // Set inverter limit relative persistent MessageOutput.printf("Limit Persistent: %d %%\r\n", payload_val); - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativPersistent); + inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativPersistent); } else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) { // Set inverter limit absolute persistent MessageOutput.printf("Limit Persistent: %d W\r\n", payload_val); - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutPersistent); + inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutPersistent); } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { // Set inverter limit relative non persistent MessageOutput.printf("Limit Non-Persistent: %d %%\r\n", payload_val); if (!properties.retain) { - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativNonPersistent); + inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativNonPersistent); } else { MessageOutput.println("Ignored because retained"); } @@ -228,7 +228,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro // Set inverter limit absolute non persistent MessageOutput.printf("Limit Non-Persistent: %d W\r\n", payload_val); if (!properties.retain) { - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutNonPersistent); + inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutNonPersistent); } else { MessageOutput.println("Ignored because retained"); } @@ -236,13 +236,13 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else if (!strcmp(setting, TOPIC_SUB_POWER)) { // Turn inverter on or off MessageOutput.printf("Set inverter power to: %d\r\n", payload_val); - inv->sendPowerControlRequest(Hoymiles.getRadio(), payload_val > 0); + inv->sendPowerControlRequest(payload_val > 0); } else if (!strcmp(setting, TOPIC_SUB_RESTART)) { // Restart inverter MessageOutput.printf("Restart inverter\r\n"); if (!properties.retain && payload_val == 1) { - inv->sendRestartControlRequest(Hoymiles.getRadio()); + inv->sendRestartControlRequest(); } else { MessageOutput.println("Ignored because retained"); } diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index d69dba1a..e20c49a2 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -146,7 +146,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, type); + inv->sendActivePowerControlRequest(limit, type); retMsg["type"] = "success"; retMsg["message"] = "Settings saved!"; diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index d545ffec..6be68a88 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -118,10 +118,10 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (root.containsKey("power")) { uint16_t power = root["power"].as(); - inv->sendPowerControlRequest(Hoymiles.getRadio(), power); + inv->sendPowerControlRequest(power); } else { if (root["restart"].as()) { - inv->sendRestartControlRequest(Hoymiles.getRadio()); + inv->sendRestartControlRequest(); } } From 8404dd57a76bed05c0d0845b971d00911a6a41c4 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 6 Mar 2023 22:16:34 +0100 Subject: [PATCH 07/66] Add a HoymilesRadio base class This enables to have multiple radio implementations while the inverter classes just refere to the base class --- lib/Hoymiles/src/Hoymiles.cpp | 24 +-- lib/Hoymiles/src/Hoymiles.h | 6 +- lib/Hoymiles/src/HoymilesRadio.cpp | 260 +----------------------- lib/Hoymiles/src/HoymilesRadio.h | 46 +---- lib/Hoymiles/src/HoymilesRadio_NRF.cpp | 270 +++++++++++++++++++++++++ lib/Hoymiles/src/HoymilesRadio_NRF.h | 54 +++++ src/InverterSettings.cpp | 4 +- src/MqttHandleDtu.cpp | 2 +- src/MqttHandleHass.cpp | 2 +- src/MqttHandleInverter.cpp | 2 +- src/WebApi_dtu.cpp | 4 +- src/WebApi_sysstatus.cpp | 4 +- src/WebApi_ws_live.cpp | 2 +- 13 files changed, 352 insertions(+), 328 deletions(-) create mode 100644 lib/Hoymiles/src/HoymilesRadio_NRF.cpp create mode 100644 lib/Hoymiles/src/HoymilesRadio_NRF.h diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 2392af7c..64c1eb1c 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -22,20 +22,20 @@ void HoymilesClass::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pin HOY_SEMAPHORE_GIVE(); // release before first use _pollInterval = 0; - _radio.reset(new HoymilesRadio()); - _radio->init(initialisedSpiBus, pinCE, pinIRQ); + _radioNrf.reset(new HoymilesRadio_NRF()); + _radioNrf->init(initialisedSpiBus, pinCE, pinIRQ); } void HoymilesClass::loop() { HOY_SEMAPHORE_TAKE(); - _radio->loop(); + _radioNrf->loop(); if (getNumInverters() > 0) { if (millis() - _lastPoll > (_pollInterval * 1000)) { static uint8_t inverterPos = 0; - if (_radio->isIdle()) { + if (_radioNrf->isIdle()) { std::shared_ptr iv = getInverterByPos(inverterPos); if (iv != nullptr) { _messageOutput->print("Fetch inverter: "); @@ -89,17 +89,17 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, u { std::shared_ptr i = nullptr; if (HMS_4CH::isValidSerial(serial)) { - i = std::make_shared(_radio.get(), serial); + i = std::make_shared(_radioNrf.get(), serial); } else if (HMS_2CH::isValidSerial(serial)) { - i = std::make_shared(_radio.get(), serial); + i = std::make_shared(_radioNrf.get(), serial); } else if (HMS_1CH::isValidSerial(serial)) { - i = std::make_shared(_radio.get(), serial); + i = std::make_shared(_radioNrf.get(), serial); } else if (HM_4CH::isValidSerial(serial)) { - i = std::make_shared(_radio.get(), serial); + i = std::make_shared(_radioNrf.get(), serial); } else if (HM_2CH::isValidSerial(serial)) { - i = std::make_shared(_radio.get(), serial); + i = std::make_shared(_radioNrf.get(), serial); } else if (HM_1CH::isValidSerial(serial)) { - i = std::make_shared(_radio.get(), serial); + i = std::make_shared(_radioNrf.get(), serial); } if (i) { @@ -171,9 +171,9 @@ size_t HoymilesClass::getNumInverters() return _inverters.size(); } -HoymilesRadio* HoymilesClass::getRadio() +HoymilesRadio_NRF* HoymilesClass::getRadioNrf() { - return _radio.get(); + return _radioNrf.get(); } uint32_t HoymilesClass::PollInterval() diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index 27e3d033..de12ce23 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "HoymilesRadio.h" +#include "HoymilesRadio_NRF.h" #include "inverters/InverterAbstract.h" #include "types.h" #include @@ -27,14 +27,14 @@ public: void removeInverterBySerial(uint64_t serial); size_t getNumInverters(); - HoymilesRadio* getRadio(); + HoymilesRadio_NRF* getRadioNrf(); uint32_t PollInterval(); void setPollInterval(uint32_t interval); private: std::vector> _inverters; - std::unique_ptr _radio; + std::unique_ptr _radioNrf; SemaphoreHandle_t _xSemaphore; diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index eb25d558..08ef1c3a 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -1,158 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2023 Thomas Basler and others */ #include "HoymilesRadio.h" #include "Hoymiles.h" -#include "commands/RequestFrameCommand.h" -#include "crc.h" -#include -#include - -void HoymilesRadio::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ) -{ - _dtuSerial.u64 = 0; - - _spiPtr.reset(initialisedSpiBus); - _radio.reset(new RF24(pinCE, initialisedSpiBus->pinSS())); - - _radio->begin(_spiPtr.get()); - - _radio->setDataRate(RF24_250KBPS); - _radio->enableDynamicPayloads(); - _radio->setCRCLength(RF24_CRC_16); - _radio->setAddressWidth(5); - _radio->setRetries(0, 0); - _radio->maskIRQ(true, true, false); // enable only receiving interrupts - if (_radio->isChipConnected()) { - Hoymiles.getMessageOutput()->println("Connection successful"); - } else { - Hoymiles.getMessageOutput()->println("Connection error!!"); - } - - attachInterrupt(digitalPinToInterrupt(pinIRQ), std::bind(&HoymilesRadio::handleIntr, this), FALLING); - - openReadingPipe(); - _radio->startListening(); -} - -void HoymilesRadio::loop() -{ - EVERY_N_MILLIS(4) - { - switchRxCh(); - } - - if (_packetReceived) { - Hoymiles.getMessageOutput()->println("Interrupt received"); - while (_radio->available()) { - if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) { - fragment_t f; - memset(f.fragment, 0xcc, MAX_RF_PAYLOAD_SIZE); - f.len = _radio->getDynamicPayloadSize(); - f.channel = _radio->getChannel(); - if (f.len > MAX_RF_PAYLOAD_SIZE) - f.len = MAX_RF_PAYLOAD_SIZE; - _radio->read(f.fragment, f.len); - _rxBuffer.push(f); - } else { - Hoymiles.getMessageOutput()->println("Buffer full"); - _radio->flush_rx(); - } - } - _packetReceived = false; - - } else { - // Perform package parsing only if no packages are received - if (!_rxBuffer.empty()) { - fragment_t f = _rxBuffer.back(); - if (checkFragmentCrc(&f)) { - std::shared_ptr inv = Hoymiles.getInverterByFragment(&f); - - if (nullptr != inv) { - // Save packet in inverter rx buffer - char buf[30]; - snprintf(buf, sizeof(buf), "RX Channel: %d --> ", f.channel); - dumpBuf(buf, f.fragment, f.len); - inv->addRxFragment(f.fragment, f.len); - } else { - Hoymiles.getMessageOutput()->println("Inverter Not found!"); - } - - } else { - Hoymiles.getMessageOutput()->println("Frame kaputt"); - } - - // Remove paket from buffer even it was corrupted - _rxBuffer.pop(); - } - } - - if (_busyFlag && _rxTimeout.occured()) { - Hoymiles.getMessageOutput()->println("RX Period End"); - std::shared_ptr inv = Hoymiles.getInverterBySerial(_commandQueue.front().get()->getTargetAddress()); - - if (nullptr != inv) { - CommandAbstract* cmd = _commandQueue.front().get(); - uint8_t verifyResult = inv->verifyAllFragments(cmd); - if (verifyResult == FRAGMENT_ALL_MISSING_RESEND) { - Hoymiles.getMessageOutput()->println("Nothing received, resend whole request"); - sendLastPacketAgain(); - - } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { - Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded"); - _commandQueue.pop(); - _busyFlag = false; - - } else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) { - Hoymiles.getMessageOutput()->println("Retransmit timeout"); - _commandQueue.pop(); - _busyFlag = false; - - } else if (verifyResult == FRAGMENT_HANDLE_ERROR) { - Hoymiles.getMessageOutput()->println("Packet handling error"); - _commandQueue.pop(); - _busyFlag = false; - - } else if (verifyResult > 0) { - // Perform Retransmit - Hoymiles.getMessageOutput()->print("Request retransmit: "); - Hoymiles.getMessageOutput()->println(verifyResult); - sendRetransmitPacket(verifyResult); - - } else { - // Successful received all packages - Hoymiles.getMessageOutput()->println("Success"); - _commandQueue.pop(); - _busyFlag = false; - } - } else { - // If inverter was not found, assume the command is invalid - Hoymiles.getMessageOutput()->println("RX: Invalid inverter found"); - _commandQueue.pop(); - _busyFlag = false; - } - } else if (!_busyFlag) { - // Currently in idle mode --> send packet if one is in the queue - if (!_commandQueue.empty()) { - CommandAbstract* cmd = _commandQueue.front().get(); - - auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); - if (nullptr != inv) { - inv->clearRxFragmentBuffer(); - sendEsbPacket(cmd); - } else { - Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); - _commandQueue.pop(); - } - } - } -} - -void HoymilesRadio::setPALevel(rf24_pa_dbm_e paLevel) -{ - _radio->setPALevel(paLevel); -} serial_u HoymilesRadio::DtuSerial() { @@ -162,62 +13,6 @@ serial_u HoymilesRadio::DtuSerial() void HoymilesRadio::setDtuSerial(uint64_t serial) { _dtuSerial.u64 = serial; - openReadingPipe(); -} - -bool HoymilesRadio::isIdle() -{ - return !_busyFlag; -} - -bool HoymilesRadio::isConnected() -{ - return _radio->isChipConnected(); -} - -bool HoymilesRadio::isPVariant() -{ - return _radio->isPVariant(); -} - -void HoymilesRadio::openReadingPipe() -{ - serial_u s; - s = convertSerialToRadioId(_dtuSerial); - _radio->openReadingPipe(1, s.u64); -} - -void HoymilesRadio::openWritingPipe(serial_u serial) -{ - serial_u s; - s = convertSerialToRadioId(serial); - _radio->openWritingPipe(s.u64); -} - -void ARDUINO_ISR_ATTR HoymilesRadio::handleIntr() -{ - _packetReceived = true; -} - -uint8_t HoymilesRadio::getRxNxtChannel() -{ - if (++_rxChIdx >= sizeof(_rxChLst)) - _rxChIdx = 0; - return _rxChLst[_rxChIdx]; -} - -uint8_t HoymilesRadio::getTxNxtChannel() -{ - if (++_txChIdx >= sizeof(_txChLst)) - _txChIdx = 0; - return _txChLst[_txChIdx]; -} - -void HoymilesRadio::switchRxCh() -{ - _radio->stopListening(); - _radio->setChannel(getRxNxtChannel()); - _radio->startListening(); } serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial) @@ -232,59 +27,6 @@ serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial) return radioId; } -bool HoymilesRadio::checkFragmentCrc(fragment_t* fragment) -{ - uint8_t crc = crc8(fragment->fragment, fragment->len - 1); - return (crc == fragment->fragment[fragment->len - 1]); -} - -void HoymilesRadio::sendEsbPacket(CommandAbstract* cmd) -{ - cmd->incrementSendCount(); - - cmd->setRouterAddress(DtuSerial().u64); - - _radio->stopListening(); - _radio->setChannel(getTxNxtChannel()); - - serial_u s; - s.u64 = cmd->getTargetAddress(); - openWritingPipe(s); - _radio->setRetries(3, 15); - - Hoymiles.getMessageOutput()->print("TX "); - Hoymiles.getMessageOutput()->print(cmd->getCommandName()); - Hoymiles.getMessageOutput()->print(" Channel: "); - Hoymiles.getMessageOutput()->print(_radio->getChannel()); - Hoymiles.getMessageOutput()->print(" --> "); - cmd->dumpDataPayload(Hoymiles.getMessageOutput()); - _radio->write(cmd->getDataPayload(), cmd->getDataSize()); - - _radio->setRetries(0, 0); - openReadingPipe(); - _radio->setChannel(getRxNxtChannel()); - _radio->startListening(); - _busyFlag = true; - _rxTimeout.set(cmd->getTimeout()); -} - -void HoymilesRadio::sendRetransmitPacket(uint8_t fragment_id) -{ - CommandAbstract* cmd = _commandQueue.front().get(); - - CommandAbstract* requestCmd = cmd->getRequestFrameCommand(fragment_id); - - if (requestCmd != nullptr) { - sendEsbPacket(requestCmd); - } -} - -void HoymilesRadio::sendLastPacketAgain() -{ - CommandAbstract* cmd = _commandQueue.front().get(); - sendEsbPacket(cmd); -} - void HoymilesRadio::dumpBuf(const char* info, uint8_t buf[], uint8_t len) { diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index e80975cb..557bfaf9 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -1,30 +1,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "TimeoutHelper.h" #include "commands/CommandAbstract.h" #include "types.h" -#include #include -#include #include -#include - -// number of fragments hold in buffer -#define FRAGMENT_BUFFER_SIZE 30 class HoymilesRadio { public: - void init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ); - void loop(); - void setPALevel(rf24_pa_dbm_e paLevel); - serial_u DtuSerial(); - void setDtuSerial(uint64_t serial); - - bool isIdle(); - bool isConnected(); - bool isPVariant(); + virtual void setDtuSerial(uint64_t serial); template T* enqueCommand() @@ -33,37 +18,10 @@ public: return static_cast(_commandQueue.back().get()); } -private: - void ARDUINO_ISR_ATTR handleIntr(); +protected: static serial_u convertSerialToRadioId(serial_u serial); - uint8_t getRxNxtChannel(); - uint8_t getTxNxtChannel(); - void switchRxCh(); - void openReadingPipe(); - void openWritingPipe(serial_u serial); - bool checkFragmentCrc(fragment_t* fragment); void dumpBuf(const char* info, uint8_t buf[], uint8_t len); - void sendEsbPacket(CommandAbstract* cmd); - void sendRetransmitPacket(uint8_t fragment_id); - void sendLastPacketAgain(); - - std::unique_ptr _spiPtr; - std::unique_ptr _radio; - uint8_t _rxChLst[5] = { 3, 23, 40, 61, 75 }; - uint8_t _rxChIdx = 0; - - uint8_t _txChLst[5] = { 3, 23, 40, 61, 75 }; - uint8_t _txChIdx = 0; - - volatile bool _packetReceived = false; - - std::queue _rxBuffer; - TimeoutHelper _rxTimeout; - serial_u _dtuSerial; - - bool _busyFlag = false; - std::queue> _commandQueue; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp new file mode 100644 index 00000000..120bde53 --- /dev/null +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "HoymilesRadio_NRF.h" +#include "Hoymiles.h" +#include "commands/RequestFrameCommand.h" +#include "crc.h" +#include +#include + +void HoymilesRadio_NRF::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ) +{ + _dtuSerial.u64 = 0; + + _spiPtr.reset(initialisedSpiBus); + _radio.reset(new RF24(pinCE, initialisedSpiBus->pinSS())); + + _radio->begin(_spiPtr.get()); + + _radio->setDataRate(RF24_250KBPS); + _radio->enableDynamicPayloads(); + _radio->setCRCLength(RF24_CRC_16); + _radio->setAddressWidth(5); + _radio->setRetries(0, 0); + _radio->maskIRQ(true, true, false); // enable only receiving interrupts + if (_radio->isChipConnected()) { + Hoymiles.getMessageOutput()->println("Connection successful"); + } else { + Hoymiles.getMessageOutput()->println("Connection error!!"); + } + + attachInterrupt(digitalPinToInterrupt(pinIRQ), std::bind(&HoymilesRadio_NRF::handleIntr, this), FALLING); + + openReadingPipe(); + _radio->startListening(); +} + +void HoymilesRadio_NRF::loop() +{ + EVERY_N_MILLIS(4) + { + switchRxCh(); + } + + if (_packetReceived) { + Hoymiles.getMessageOutput()->println("Interrupt received"); + while (_radio->available()) { + if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) { + fragment_t f; + memset(f.fragment, 0xcc, MAX_RF_PAYLOAD_SIZE); + f.len = _radio->getDynamicPayloadSize(); + f.channel = _radio->getChannel(); + if (f.len > MAX_RF_PAYLOAD_SIZE) + f.len = MAX_RF_PAYLOAD_SIZE; + _radio->read(f.fragment, f.len); + _rxBuffer.push(f); + } else { + Hoymiles.getMessageOutput()->println("Buffer full"); + _radio->flush_rx(); + } + } + _packetReceived = false; + + } else { + // Perform package parsing only if no packages are received + if (!_rxBuffer.empty()) { + fragment_t f = _rxBuffer.back(); + if (checkFragmentCrc(&f)) { + std::shared_ptr inv = Hoymiles.getInverterByFragment(&f); + + if (nullptr != inv) { + // Save packet in inverter rx buffer + char buf[30]; + snprintf(buf, sizeof(buf), "RX Channel: %d --> ", f.channel); + dumpBuf(buf, f.fragment, f.len); + inv->addRxFragment(f.fragment, f.len); + } else { + Hoymiles.getMessageOutput()->println("Inverter Not found!"); + } + + } else { + Hoymiles.getMessageOutput()->println("Frame kaputt"); + } + + // Remove paket from buffer even it was corrupted + _rxBuffer.pop(); + } + } + + if (_busyFlag && _rxTimeout.occured()) { + Hoymiles.getMessageOutput()->println("RX Period End"); + std::shared_ptr inv = Hoymiles.getInverterBySerial(_commandQueue.front().get()->getTargetAddress()); + + if (nullptr != inv) { + CommandAbstract* cmd = _commandQueue.front().get(); + uint8_t verifyResult = inv->verifyAllFragments(cmd); + if (verifyResult == FRAGMENT_ALL_MISSING_RESEND) { + Hoymiles.getMessageOutput()->println("Nothing received, resend whole request"); + sendLastPacketAgain(); + + } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { + Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded"); + _commandQueue.pop(); + _busyFlag = false; + + } else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) { + Hoymiles.getMessageOutput()->println("Retransmit timeout"); + _commandQueue.pop(); + _busyFlag = false; + + } else if (verifyResult == FRAGMENT_HANDLE_ERROR) { + Hoymiles.getMessageOutput()->println("Packet handling error"); + _commandQueue.pop(); + _busyFlag = false; + + } else if (verifyResult > 0) { + // Perform Retransmit + Hoymiles.getMessageOutput()->print("Request retransmit: "); + Hoymiles.getMessageOutput()->println(verifyResult); + sendRetransmitPacket(verifyResult); + + } else { + // Successful received all packages + Hoymiles.getMessageOutput()->println("Success"); + _commandQueue.pop(); + _busyFlag = false; + } + } else { + // If inverter was not found, assume the command is invalid + Hoymiles.getMessageOutput()->println("RX: Invalid inverter found"); + _commandQueue.pop(); + _busyFlag = false; + } + } else if (!_busyFlag) { + // Currently in idle mode --> send packet if one is in the queue + if (!_commandQueue.empty()) { + CommandAbstract* cmd = _commandQueue.front().get(); + + auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); + if (nullptr != inv) { + inv->clearRxFragmentBuffer(); + sendEsbPacket(cmd); + } else { + Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); + _commandQueue.pop(); + } + } + } +} + +void HoymilesRadio_NRF::setPALevel(rf24_pa_dbm_e paLevel) +{ + _radio->setPALevel(paLevel); +} + +void HoymilesRadio_NRF::setDtuSerial(uint64_t serial) +{ + HoymilesRadio::setDtuSerial(serial); + openReadingPipe(); +} + +bool HoymilesRadio_NRF::isIdle() +{ + return !_busyFlag; +} + +bool HoymilesRadio_NRF::isConnected() +{ + return _radio->isChipConnected(); +} + +bool HoymilesRadio_NRF::isPVariant() +{ + return _radio->isPVariant(); +} + +void HoymilesRadio_NRF::openReadingPipe() +{ + serial_u s; + s = convertSerialToRadioId(_dtuSerial); + _radio->openReadingPipe(1, s.u64); +} + +void HoymilesRadio_NRF::openWritingPipe(serial_u serial) +{ + serial_u s; + s = convertSerialToRadioId(serial); + _radio->openWritingPipe(s.u64); +} + +void ARDUINO_ISR_ATTR HoymilesRadio_NRF::handleIntr() +{ + _packetReceived = true; +} + +uint8_t HoymilesRadio_NRF::getRxNxtChannel() +{ + if (++_rxChIdx >= sizeof(_rxChLst)) + _rxChIdx = 0; + return _rxChLst[_rxChIdx]; +} + +uint8_t HoymilesRadio_NRF::getTxNxtChannel() +{ + if (++_txChIdx >= sizeof(_txChLst)) + _txChIdx = 0; + return _txChLst[_txChIdx]; +} + +void HoymilesRadio_NRF::switchRxCh() +{ + _radio->stopListening(); + _radio->setChannel(getRxNxtChannel()); + _radio->startListening(); +} + +bool HoymilesRadio_NRF::checkFragmentCrc(fragment_t* fragment) +{ + uint8_t crc = crc8(fragment->fragment, fragment->len - 1); + return (crc == fragment->fragment[fragment->len - 1]); +} + +void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract* cmd) +{ + cmd->incrementSendCount(); + + cmd->setRouterAddress(DtuSerial().u64); + + _radio->stopListening(); + _radio->setChannel(getTxNxtChannel()); + + serial_u s; + s.u64 = cmd->getTargetAddress(); + openWritingPipe(s); + _radio->setRetries(3, 15); + + Hoymiles.getMessageOutput()->print("TX "); + Hoymiles.getMessageOutput()->print(cmd->getCommandName()); + Hoymiles.getMessageOutput()->print(" Channel: "); + Hoymiles.getMessageOutput()->print(_radio->getChannel()); + Hoymiles.getMessageOutput()->print(" --> "); + cmd->dumpDataPayload(Hoymiles.getMessageOutput()); + _radio->write(cmd->getDataPayload(), cmd->getDataSize()); + + _radio->setRetries(0, 0); + openReadingPipe(); + _radio->setChannel(getRxNxtChannel()); + _radio->startListening(); + _busyFlag = true; + _rxTimeout.set(cmd->getTimeout()); +} + +void HoymilesRadio_NRF::sendRetransmitPacket(uint8_t fragment_id) +{ + CommandAbstract* cmd = _commandQueue.front().get(); + + CommandAbstract* requestCmd = cmd->getRequestFrameCommand(fragment_id); + + if (requestCmd != nullptr) { + sendEsbPacket(requestCmd); + } +} + +void HoymilesRadio_NRF::sendLastPacketAgain() +{ + CommandAbstract* cmd = _commandQueue.front().get(); + sendEsbPacket(cmd); +} + diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.h b/lib/Hoymiles/src/HoymilesRadio_NRF.h new file mode 100644 index 00000000..5ed2e364 --- /dev/null +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HoymilesRadio.h" +#include "TimeoutHelper.h" +#include "commands/CommandAbstract.h" +#include +#include +#include +#include + +// number of fragments hold in buffer +#define FRAGMENT_BUFFER_SIZE 30 + +class HoymilesRadio_NRF : public HoymilesRadio { +public: + void init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ); + void loop(); + void setPALevel(rf24_pa_dbm_e paLevel); + + virtual void setDtuSerial(uint64_t serial); + + bool isIdle(); + bool isConnected(); + bool isPVariant(); + +private: + void ARDUINO_ISR_ATTR handleIntr(); + uint8_t getRxNxtChannel(); + uint8_t getTxNxtChannel(); + void switchRxCh(); + void openReadingPipe(); + void openWritingPipe(serial_u serial); + bool checkFragmentCrc(fragment_t* fragment); + + void sendEsbPacket(CommandAbstract* cmd); + void sendRetransmitPacket(uint8_t fragment_id); + void sendLastPacketAgain(); + + std::unique_ptr _spiPtr; + std::unique_ptr _radio; + uint8_t _rxChLst[5] = { 3, 23, 40, 61, 75 }; + uint8_t _rxChIdx = 0; + + uint8_t _txChLst[5] = { 3, 23, 40, 61, 75 }; + uint8_t _txChIdx = 0; + + volatile bool _packetReceived = false; + + std::queue _rxBuffer; + TimeoutHelper _rxTimeout; + + bool _busyFlag = false; +}; \ No newline at end of file diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 39719d23..1562b135 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -29,10 +29,10 @@ void InverterSettingsClass::init() Hoymiles.init(spiClass, pin.nrf24_en, pin.nrf24_irq); MessageOutput.println(" Setting radio PA level... "); - Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); + Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); MessageOutput.println(" Setting DTU serial... "); - Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); + Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); MessageOutput.println(" Setting poll interval... "); Hoymiles.setPollInterval(config.Dtu_PollInterval); diff --git a/src/MqttHandleDtu.cpp b/src/MqttHandleDtu.cpp index 818a1b40..f9dfcc50 100644 --- a/src/MqttHandleDtu.cpp +++ b/src/MqttHandleDtu.cpp @@ -16,7 +16,7 @@ void MqttHandleDtuClass::init() void MqttHandleDtuClass::loop() { - if (!MqttSettings.getConnected() || !Hoymiles.getRadio()->isIdle()) { + if (!MqttSettings.getConnected() || !Hoymiles.getRadioNrf()->isIdle()) { return; } diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 9aa3e134..3f72feb1 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -41,7 +41,7 @@ void MqttHandleHassClass::publishConfig() return; } - if (!MqttSettings.getConnected() && Hoymiles.getRadio()->isIdle()) { + if (!MqttSettings.getConnected() && Hoymiles.getRadioNrf()->isIdle()) { return; } diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index ba73767d..af18ac57 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -36,7 +36,7 @@ void MqttHandleInverterClass::init() void MqttHandleInverterClass::loop() { - if (!MqttSettings.getConnected() || !Hoymiles.getRadio()->isIdle()) { + if (!MqttSettings.getConnected() || !Hoymiles.getRadioNrf()->isIdle()) { return; } diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 93365a41..81311772 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -132,7 +132,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) response->setLength(); request->send(response); - Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); - Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); + Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); + Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); Hoymiles.setPollInterval(config.Dtu_PollInterval); } \ No newline at end of file diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index f70cdd87..c392d6cb 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -69,8 +69,8 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["uptime"] = esp_timer_get_time() / 1000000; - root["radio_connected"] = Hoymiles.getRadio()->isConnected(); - root["radio_pvariant"] = Hoymiles.getRadio()->isPVariant(); + root["radio_connected"] = Hoymiles.getRadioNrf()->isConnected(); + root["radio_pvariant"] = Hoymiles.getRadioNrf()->isPVariant(); response->setLength(); request->send(response); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 3507b50a..1b4b4ec7 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -175,7 +175,7 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) JsonObject hintObj = root.createNestedObject("hints"); struct tm timeinfo; hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); - hintObj["radio_problem"] = (!Hoymiles.getRadio()->isConnected() || !Hoymiles.getRadio()->isPVariant()); + hintObj["radio_problem"] = (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant()); if (!strcmp(Configuration.get().Security_Password, ACCESS_POINT_PASSWORD)) { hintObj["default_password"] = true; } else { From 41e2ba7fcf1d41f95cb60a9bb7ae8e1891e42e4d Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 6 Mar 2023 23:42:05 +0100 Subject: [PATCH 08/66] Move serveral methods from the HoymilesRadio_NRF class to the HoymilesRadio base class --- lib/Hoymiles/src/HoymilesRadio.cpp | 24 ++++++++++++++++++++++++ lib/Hoymiles/src/HoymilesRadio.h | 5 +++++ lib/Hoymiles/src/HoymilesRadio_NRF.cpp | 25 +------------------------ lib/Hoymiles/src/HoymilesRadio_NRF.h | 3 --- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 08ef1c3a..c68def6e 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -4,6 +4,7 @@ */ #include "HoymilesRadio.h" #include "Hoymiles.h" +#include "crc.h" serial_u HoymilesRadio::DtuSerial() { @@ -27,6 +28,29 @@ serial_u HoymilesRadio::convertSerialToRadioId(serial_u serial) return radioId; } +bool HoymilesRadio::checkFragmentCrc(fragment_t* fragment) +{ + uint8_t crc = crc8(fragment->fragment, fragment->len - 1); + return (crc == fragment->fragment[fragment->len - 1]); +} + +void HoymilesRadio::sendRetransmitPacket(uint8_t fragment_id) +{ + CommandAbstract* cmd = _commandQueue.front().get(); + + CommandAbstract* requestCmd = cmd->getRequestFrameCommand(fragment_id); + + if (requestCmd != nullptr) { + sendEsbPacket(requestCmd); + } +} + +void HoymilesRadio::sendLastPacketAgain() +{ + CommandAbstract* cmd = _commandQueue.front().get(); + sendEsbPacket(cmd); +} + void HoymilesRadio::dumpBuf(const char* info, uint8_t buf[], uint8_t len) { diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 557bfaf9..93bc457a 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -22,6 +22,11 @@ protected: static serial_u convertSerialToRadioId(serial_u serial); void dumpBuf(const char* info, uint8_t buf[], uint8_t len); + bool checkFragmentCrc(fragment_t* fragment); + virtual void sendEsbPacket(CommandAbstract* cmd) = 0; + void sendRetransmitPacket(uint8_t fragment_id); + void sendLastPacketAgain(); + serial_u _dtuSerial; std::queue> _commandQueue; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp index 120bde53..ded07f6e 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -5,7 +5,6 @@ #include "HoymilesRadio_NRF.h" #include "Hoymiles.h" #include "commands/RequestFrameCommand.h" -#include "crc.h" #include #include @@ -215,11 +214,7 @@ void HoymilesRadio_NRF::switchRxCh() _radio->startListening(); } -bool HoymilesRadio_NRF::checkFragmentCrc(fragment_t* fragment) -{ - uint8_t crc = crc8(fragment->fragment, fragment->len - 1); - return (crc == fragment->fragment[fragment->len - 1]); -} + void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract* cmd) { @@ -250,21 +245,3 @@ void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract* cmd) _busyFlag = true; _rxTimeout.set(cmd->getTimeout()); } - -void HoymilesRadio_NRF::sendRetransmitPacket(uint8_t fragment_id) -{ - CommandAbstract* cmd = _commandQueue.front().get(); - - CommandAbstract* requestCmd = cmd->getRequestFrameCommand(fragment_id); - - if (requestCmd != nullptr) { - sendEsbPacket(requestCmd); - } -} - -void HoymilesRadio_NRF::sendLastPacketAgain() -{ - CommandAbstract* cmd = _commandQueue.front().get(); - sendEsbPacket(cmd); -} - diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.h b/lib/Hoymiles/src/HoymilesRadio_NRF.h index 5ed2e364..63e85f5a 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.h +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.h @@ -31,11 +31,8 @@ private: void switchRxCh(); void openReadingPipe(); void openWritingPipe(serial_u serial); - bool checkFragmentCrc(fragment_t* fragment); void sendEsbPacket(CommandAbstract* cmd); - void sendRetransmitPacket(uint8_t fragment_id); - void sendLastPacketAgain(); std::unique_ptr _spiPtr; std::unique_ptr _radio; From 8927b8374a3a1997e1a73471607434fd3f96e867 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 7 Mar 2023 18:38:55 +0100 Subject: [PATCH 09/66] Added HMS/HMT devices to the DevInfoParser --- lib/Hoymiles/src/parser/DevInfoParser.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 343cdd0e..2e89df2c 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -27,6 +27,17 @@ const devInfo_t devInfo[] = { { { 0x10, 0x02, 0x30, ALL }, 1500, "MI-1500 Gen3" }, { { 0x10, 0x12, 0x30, ALL }, 1500, "HM-1500" }, { { 0x10, 0x10, 0x10, 0x15 }, static_cast(300 * 0.7), "HM-300" }, // HM-300 factory limitted to 70% + + { { 0x10, 0x20, 0x21, ALL }, 350, "HMS-350" }, // 00 + { { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500" }, // 02 + { { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800" }, // 00 + { { 0x10, 0x21, 0x71, ALL }, 1000, "HMS-1000" }, // 05 + { { 0x10, 0x12, 0x51, ALL }, 1800, "HMS-1800" }, // 01 + { { 0x10, 0x22, 0x51, ALL }, 1800, "HMS-1800" }, // 16 + { { 0x10, 0x12, 0x71, ALL }, 2000, "HMS-2000" }, // 01 + + { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800" }, // 01 + { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250" } // 01 }; void DevInfoParser::clearBufferAll() From 90c689a41ad87bfc4921b75536d81e13cb2be8fa Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 7 Mar 2023 18:48:24 +0100 Subject: [PATCH 10/66] Implement HoymilesRadio_CMT --- lib/CMT2300a/cmt2300a_params.h | 290 +++++---------- lib/Hoymiles/src/Hoymiles.cpp | 12 +- lib/Hoymiles/src/Hoymiles.h | 2 + lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 495 +++++++++++++++++++++++++ lib/Hoymiles/src/HoymilesRadio_CMT.h | 85 +++++ lib/Hoymiles/src/types.h | 1 + platformio.ini | 1 + 7 files changed, 693 insertions(+), 193 deletions(-) create mode 100644 lib/Hoymiles/src/HoymilesRadio_CMT.cpp create mode 100644 lib/Hoymiles/src/HoymilesRadio_CMT.h diff --git a/lib/CMT2300a/cmt2300a_params.h b/lib/CMT2300a/cmt2300a_params.h index 5dc80311..39ae7b12 100644 --- a/lib/CMT2300a/cmt2300a_params.h +++ b/lib/CMT2300a/cmt2300a_params.h @@ -4,212 +4,124 @@ #include "cmt2300a_defs.h" #include -/* [CMT Bank] */ -static uint8_t g_cmt2300aCmtBank_default[CMT2300A_CMT_BANK_SIZE] = { - 0x00, - 0x66, - 0xEC, - 0x1D, - 0x70, - 0x80, - 0x14, - 0x08, - 0x91, - 0x02, - 0x02, - 0xD0, -}; - -/* [CMT Bank] with RSSI offset of +- 0 and 13dBm*/ -static uint8_t g_cmt2300aCmtBank_RSSI_0[CMT2300A_CMT_BANK_SIZE] = { - 0x00, - 0x66, - 0xEC, - 0x1C, - 0x70, - 0x80, - 0x14, - 0x08, - 0x11, - 0x02, - 0x02, - 0x00, +/* [CMT Bank] with RSSI offset of +- 0 (and 13dBm) */ +static uint8_t g_cmt2300aCmtBank[CMT2300A_CMT_BANK_SIZE] = { +0x00, +0x66, +0xEC, +0x1C, +0x70, +0x80, +0x14, +0x08, +0x11, +0x02, +0x02, +0x00, }; /* [System Bank] */ static uint8_t g_cmt2300aSystemBank[CMT2300A_SYSTEM_BANK_SIZE] = { - 0xAE, - 0xE0, - 0x35, - 0x00, - 0x00, - 0xF4, - 0x10, - 0xE2, - 0x42, - 0x20, - 0x0C, - 0x81, -}; - -/* [Frequency Bank] 868 MHz (default) */ -static uint8_t g_cmt2300aFrequencyBank_868[CMT2300A_FREQUENCY_BANK_SIZE] = { - 0x42, - 0xCF, - 0xA7, - 0x8C, - 0x42, - 0xC4, - 0x4E, - 0x1C, -}; - -/* [Frequency Bank] 863 MHz (EU) */ -static uint8_t g_cmt2300aFrequencyBank_863[CMT2300A_FREQUENCY_BANK_SIZE] = { - 0x42, - 0x6D, - 0x80, - 0x86, - 0x42, - 0x62, - 0x27, - 0x16, +0xAE, +0xE0, +0x35, +0x00, +0x00, +0xF4, +0x10, +0xE2, +0x42, +0x20, +0x0C, +0x81, }; /* [Frequency Bank] 860 MHz */ -static uint8_t g_cmt2300aFrequencyBank_860[CMT2300A_FREQUENCY_BANK_SIZE] = { - 0x42, - 0x32, - 0xCF, - 0x82, - 0x42, - 0x27, - 0x76, - 0x12, +static uint8_t g_cmt2300aFrequencyBank[CMT2300A_FREQUENCY_BANK_SIZE] = { +0x42, +0x32, +0xCF, +0x82, +0x42, +0x27, +0x76, +0x12, }; /* [Data Rate Bank] */ static uint8_t g_cmt2300aDataRateBank[CMT2300A_DATA_RATE_BANK_SIZE] = { - 0xA6, - 0xC9, - 0x20, - 0x20, - 0xD2, - 0x35, - 0x0C, - 0x0A, - 0x9F, - 0x4B, - 0x29, - 0x29, - 0xC0, - 0x14, - 0x05, - 0x53, - 0x10, - 0x00, - 0xB4, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, -}; - -/* [Baseband Bank] - default */ -static uint8_t g_cmt2300aBasebandBank_default[CMT2300A_BASEBAND_BANK_SIZE] = { - 0x12, - 0x1E, - 0x00, - 0xAA, - 0x06, - 0x00, - 0x00, - 0x00, - 0x00, - 0xD6, - 0xD5, - 0xD4, - 0x2D, - 0x01, - 0x1F, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xC3, - 0x00, - 0x00, - 0x60, - 0xFF, - 0x00, - 0x00, - 0x1F, - 0x10, +0xA6, +0xC9, +0x20, +0x20, +0xD2, +0x35, +0x0C, +0x0A, +0x9F, +0x4B, +0x29, +0x29, +0xC0, +0x14, +0x05, +0x53, +0x10, +0x00, +0xB4, +0x00, +0x00, +0x01, +0x00, +0x00, }; /* [Baseband Bank] - EU */ -static uint8_t g_cmt2300aBasebandBank_EU[CMT2300A_BASEBAND_BANK_SIZE] = { - 0x12, - 0x1E, - 0x00, - 0xAA, - 0x06, - 0x00, - 0x00, - 0x00, - 0x00, - 0x48, - 0x5A, - 0x48, - 0x4D, - 0x01, - 0x1D, // default should be 0x1F (32 bytes length), but 0x45 is 0x01 -> variable length, so this value is ignored - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0xC3, - 0x00, - 0x00, - 0x60, - 0xFF, - 0x00, - 0x00, - 0x1F, - 0x10, -}; - -/* [Tx Bank] 20 dBm */ -static uint8_t g_cmt2300aTxBank_20dBm[CMT2300A_TX_BANK_SIZE] = { - 0x70, - 0x4D, - 0x06, - 0x00, - 0x07, - 0x50, - 0x00, - 0x8A, - 0x18, - 0x3F, - 0x7F, +static uint8_t g_cmt2300aBasebandBank[CMT2300A_BASEBAND_BANK_SIZE] = { +0x12, +0x1E, +0x00, +0xAA, +0x06, +0x00, +0x00, +0x00, +0x00, +0x48, +0x5A, +0x48, +0x4D, +0x01, +0x1F, +0x00, +0x00, +0x00, +0x00, +0x00, +0xC3, +0x00, +0x00, +0x60, +0xFF, +0x00, +0x00, +0x1F, +0x10, }; /* [Tx Bank] 13 dBm */ -static uint8_t g_cmt2300aTxBank_13dBm[CMT2300A_TX_BANK_SIZE] = { - 0x70, - 0x4D, - 0x06, - 0x00, - 0x07, - 0x50, - 0x00, - 0x42, - 0x0C, - 0x3F, - 0x7F, +static uint8_t g_cmt2300aTxBank[CMT2300A_TX_BANK_SIZE] = { +0x70, +0x4D, +0x06, +0x00, +0x07, +0x50, +0x00, +0x42, +0x0C, +0x3F, +0x7F, }; #endif diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 64c1eb1c..0f3f14ff 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -24,18 +24,22 @@ void HoymilesClass::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pin _pollInterval = 0; _radioNrf.reset(new HoymilesRadio_NRF()); _radioNrf->init(initialisedSpiBus, pinCE, pinIRQ); + + _radioCmt.reset(new HoymilesRadio_CMT()); + _radioCmt->init(); } void HoymilesClass::loop() { HOY_SEMAPHORE_TAKE(); _radioNrf->loop(); + _radioCmt->loop(); if (getNumInverters() > 0) { if (millis() - _lastPoll > (_pollInterval * 1000)) { static uint8_t inverterPos = 0; - if (_radioNrf->isIdle()) { + if (_radioNrf->isIdle() && _radioCmt->isIdle()) { std::shared_ptr iv = getInverterByPos(inverterPos); if (iv != nullptr) { _messageOutput->print("Fetch inverter: "); @@ -89,11 +93,11 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, u { std::shared_ptr i = nullptr; if (HMS_4CH::isValidSerial(serial)) { - i = std::make_shared(_radioNrf.get(), serial); + i = std::make_shared(_radioCmt.get(), serial); } else if (HMS_2CH::isValidSerial(serial)) { - i = std::make_shared(_radioNrf.get(), serial); + i = std::make_shared(_radioCmt.get(), serial); } else if (HMS_1CH::isValidSerial(serial)) { - i = std::make_shared(_radioNrf.get(), serial); + i = std::make_shared(_radioCmt.get(), serial); } else if (HM_4CH::isValidSerial(serial)) { i = std::make_shared(_radioNrf.get(), serial); } else if (HM_2CH::isValidSerial(serial)) { diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index de12ce23..0442fbd0 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -2,6 +2,7 @@ #pragma once #include "HoymilesRadio_NRF.h" +#include "HoymilesRadio_CMT.h" #include "inverters/InverterAbstract.h" #include "types.h" #include @@ -35,6 +36,7 @@ public: private: std::vector> _inverters; std::unique_ptr _radioNrf; + std::unique_ptr _radioCmt; SemaphoreHandle_t _xSemaphore; diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp new file mode 100644 index 00000000..28d35443 --- /dev/null +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "HoymilesRadio_CMT.h" +#include "Hoymiles.h" +#include "crc.h" +#include +#include +#include + +#define CMT2300A_ONE_STEP_SIZE 2500 // frequency channel step size for fast frequency hopping operation: One step size is 2.5 kHz. +#define FH_OFFSET 100 // value * CMT2300A_ONE_STEP_SIZE = channel frequency offset +#define HOY_BASE_FREQ 860000000 // Hoymiles base frequency for CMD56 channels is 860.00 MHz +#define HOY_BOOT_FREQ 868000000 // Hoymiles boot/init frequency after power up inverter + +String HoymilesRadio_CMT::cmtChToFreq(const uint8_t channel) +{ + return String((HOY_BASE_FREQ + (cmtBaseChOff860 + channel) * FH_OFFSET * CMT2300A_ONE_STEP_SIZE) / 1000000.0, 2) + " MHz"; +} + +void HoymilesRadio_CMT::cmtSwitchChannel(const uint8_t channel) +{ + yield(); + CMT2300A_SetFrequencyChannel(channel); + yield(); + cmtActualCh = channel; + // Hoymiles.getMessageOutput()->println("[cmtSwitchChannel] switched channel to " + cmtGetActFreq()); +} + +uint8_t HoymilesRadio_CMT::cmtFreqToChan(const String func_name, const String var_name, const uint32_t freq_kHz) +{ + if ((freq_kHz % 250) != 0) { + Hoymiles.getMessageOutput()->println(func_name + " " + var_name + " " + String(freq_kHz / 1000.0, 3) + " MHz is not divisible by 250 kHz!"); + return 0xFF; // ERROR + } + const uint32_t min_Freq_kHz = (HOY_BASE_FREQ + (cmtBaseChOff860 >= 1 ? cmtBaseChOff860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000; // frequency can not be lower than actual initailized base freq + const uint32_t max_Freq_kHz = (HOY_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000; // =923500, 0xFF does not work + if (freq_kHz < min_Freq_kHz || freq_kHz > max_Freq_kHz) { + Hoymiles.getMessageOutput()->println(func_name + " " + var_name + " " + String(freq_kHz / 1000.0, 2) + " MHz is out of Hoymiles/CMT range! (" + String(min_Freq_kHz / 1000.0, 2) + " MHz - " + String(max_Freq_kHz / 1000.0, 2) + " MHz)"); + return 0xFF; // ERROR + } + if (freq_kHz < 863000 || freq_kHz > 870000) + Hoymiles.getMessageOutput()->println(func_name + " !!! caution: " + var_name + " " + String(freq_kHz / 1000.0, 2) + " MHz is out of EU legal range! (863 - 870 MHz)"); + return (freq_kHz * 1000 - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET - cmtBaseChOff860; // frequency to channel +} + +bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) +{ + const uint8_t toChannel = cmtFreqToChan("[cmtSwitchDtuFreq]", "to_freq_kHz", to_freq_kHz); + if (toChannel == 0xFF) + return false; + + cmtSwitchChannel(toChannel); + + return true; +} + +bool HoymilesRadio_CMT::cmtConfig(void) +{ +#ifdef ENABLE_ANTENNA_SWITCH + /* If you enable antenna switch, GPIO1/GPIO2 will output RX_ACTIVE/TX_ACTIVE, + and it can't output INT1/INT2 via GPIO1/GPIO2 */ + CMT2300A_EnableAntennaSwitch(0); + +#else + /* Config GPIOs */ + CMT2300A_ConfigGpio( + CMT2300A_GPIO3_SEL_INT2); + + /* Config interrupt */ + CMT2300A_ConfigInterrupt( + CMT2300A_INT_SEL_TX_DONE, /* Config INT1 */ + CMT2300A_INT_SEL_PKT_OK /* Config INT2 */ + ); +#endif + + /* Enable interrupt */ + CMT2300A_EnableInterrupt( + CMT2300A_MASK_TX_DONE_EN | CMT2300A_MASK_PREAM_OK_EN | CMT2300A_MASK_SYNC_OK_EN | CMT2300A_MASK_CRC_OK_EN | CMT2300A_MASK_PKT_DONE_EN); + + /* Disable low frequency OSC calibration */ + // CMT2300A_EnableLfosc(FALSE); + + CMT2300A_SetFrequencyStep(100); // set FH_OFFSET to 100 (frequency = base freq + 2.5kHz*FH_OFFSET*FH_CHANNEL) + + /* Use a single 64-byte FIFO for either Tx or Rx */ + CMT2300A_EnableFifoMerge(true); + + /* Go to sleep for configuration to take effect */ + if (!CMT2300A_GoSleep()) + return false; // CMT2300A not switched to sleep mode! + + return true; +} + +bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const uint32_t from_freq_kHz, const uint32_t to_freq_kHz) +{ + const uint8_t fromChannel = cmtFreqToChan("[cmtSwitchInvAndDtuFreq]", "from_freq_kHz", from_freq_kHz); + const uint8_t toChannel = cmtFreqToChan("[cmtSwitchInvAndDtuFreq]", "to_freq_kHz", to_freq_kHz); + if (fromChannel == 0xFF || toChannel == 0xFF) + return false; + + cmtSwitchChannel(fromChannel); + cmtTx56toCh = toChannel; + + // CMD56 for inverter frequency/channel switch + cmtTxBuffer[0] = 0x56; + // cmtTxBuffer[1-4] = last inverter serial + // cmtTxBuffer[5-8] = dtu serial + cmtTxBuffer[9] = 0x02; + cmtTxBuffer[10] = 0x15; + cmtTxBuffer[11] = 0x21; + cmtTxBuffer[12] = (uint8_t)(cmtBaseChOff860 + toChannel); + cmtTxBuffer[13] = 0x14; + cmtTxBuffer[14] = crc8(cmtTxBuffer, 14); + + Hoymiles.getMessageOutput()->print("TX CMD56 "); + Hoymiles.getMessageOutput()->print(cmtChToFreq(cmtActualCh)); + Hoymiles.getMessageOutput()->print(" --> "); + dumpBuf("", cmtTxBuffer, 15); + + cmtTxLength = 15; + cmtTxTimeout = 100; + + cmtNextState = CMT_STATE_TX_START; + + //_busyFlag = true; + + return true; +} + +enumCMTresult HoymilesRadio_CMT::cmtProcess(void) +{ + enumCMTresult nRes = CMT_BUSY; + + switch (cmtNextState) { + case CMT_STATE_IDLE: { + nRes = CMT_IDLE; + break; + } + case CMT_STATE_RX_START: { + CMT2300A_GoStby(); + CMT2300A_ClearInterruptFlags(); + + /* Must clear FIFO after enable SPI to read or write the FIFO */ + CMT2300A_EnableReadFifo(); + CMT2300A_ClearRxFifo(); + + if (!CMT2300A_GoRx()) + cmtNextState = CMT_STATE_ERROR; + else + cmtNextState = CMT_STATE_RX_WAIT; + + cmtRxTimeCount = CMT2300A_GetTickCount(); + cmtRxTimeout = 200; + + break; + } + case CMT_STATE_RX_WAIT: { +#ifdef ENABLE_ANTENNA_SWITCH + if (CMT2300A_MASK_PKT_OK_FLG & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG)) /* Read PKT_OK flag */ +#else + if (_packetReceived) /* Read INT2, PKT_OK */ +#endif + { + Hoymiles.getMessageOutput()->println("Interrupt received"); + _packetReceived = false; // reset interrupt + cmtNextState = CMT_STATE_RX_DONE; + } + + if ((CMT2300A_GetTickCount() - cmtRxTimeCount) > cmtRxTimeout) + cmtNextState = CMT_STATE_RX_TIMEOUT; + + break; + } + case CMT_STATE_RX_DONE: { + CMT2300A_GoStby(); + + bool isLastFrame = false; + + uint8_t state = CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG); + if ((state & 0x1b) == 0x1b) { + cmtRxTimeoutCnt = 0; + + if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) { + fragment_t f; + memset(f.fragment, 0xcc, MAX_RF_PAYLOAD_SIZE); + CMT2300A_ReadFifo(&f.len, 1); // first byte in FiFo is length + f.channel = cmtActualCh; + f.rssi = CMT2300A_GetRssiDBm(); + if (f.len > MAX_RF_PAYLOAD_SIZE) + f.len = MAX_RF_PAYLOAD_SIZE; + CMT2300A_ReadFifo(f.fragment, f.len); + if (f.fragment[9] & 0x80) // last frame detection for end Rx + isLastFrame = true; + _rxBuffer.push(f); + } else { + Hoymiles.getMessageOutput()->println("Buffer full"); + } + } else if ((state & 0x19) == 0x19) + Hoymiles.getMessageOutput()->println("[CMT_STATE_RX_DONE] state: " + String(state, HEX) + " (CRC_ERROR)"); + else + Hoymiles.getMessageOutput()->println("[CMT_STATE_RX_DONE] wrong state: " + String(state, HEX)); + + CMT2300A_ClearInterruptFlags(); + + CMT2300A_GoSleep(); + + if (isLastFrame) // last frame received + cmtNextState = CMT_STATE_IDLE; + else + cmtNextState = CMT_STATE_RX_START; // receive next frame(s) + + nRes = CMT_RX_DONE; + break; + } + case CMT_STATE_RX_TIMEOUT: { + CMT2300A_GoSleep(); + + Hoymiles.getMessageOutput()->println("RX timeout!"); + + cmtNextState = CMT_STATE_IDLE; + + // send CMD56 after 3 Rx timeouts + if (cmtRxTimeoutCnt < 2) + cmtRxTimeoutCnt++; + else { + uint32_t invSerial = cmtTxBuffer[1] << 24 | cmtTxBuffer[2] << 16 | cmtTxBuffer[3] << 8 | cmtTxBuffer[4]; // read inverter serial from last Tx buffer + cmtSwitchInvAndDtuFreq(invSerial, HOY_BOOT_FREQ / 1000, CMT_WORK_FREQ); + } + + nRes = CMT_RX_TIMEOUT; + break; + } + case CMT_STATE_TX_START: { + CMT2300A_GoStby(); + CMT2300A_ClearInterruptFlags(); + + /* Must clear FIFO after enable SPI to read or write the FIFO */ + CMT2300A_EnableWriteFifo(); + CMT2300A_ClearTxFifo(); + + CMT2300A_WriteReg(CMT2300A_CUS_PKT15, cmtTxLength); // set Tx length + /* The length need be smaller than 32 */ + CMT2300A_WriteFifo(cmtTxBuffer, cmtTxLength); + + if (!(CMT2300A_ReadReg(CMT2300A_CUS_FIFO_FLAG) & CMT2300A_MASK_TX_FIFO_NMTY_FLG)) + cmtNextState = CMT_STATE_ERROR; + + if (!CMT2300A_GoTx()) + cmtNextState = CMT_STATE_ERROR; + else + cmtNextState = CMT_STATE_TX_WAIT; + + cmtTxTimeCount = CMT2300A_GetTickCount(); + + break; + } + case CMT_STATE_TX_WAIT: { + // #ifdef ENABLE_ANTENNA_SWITCH + if (CMT2300A_MASK_TX_DONE_FLG & CMT2300A_ReadReg(CMT2300A_CUS_INT_CLR1)) /* Read TX_DONE flag */ + // #else + // if(CMT2300A_ReadGpio1()) /* Read INT1, TX_DONE */ + // #endif + { + cmtNextState = CMT_STATE_TX_DONE; + } + + if ((CMT2300A_GetTickCount() - cmtTxTimeCount) > cmtTxTimeout) + cmtNextState = CMT_STATE_TX_TIMEOUT; + + break; + } + case CMT_STATE_TX_DONE: { + CMT2300A_ClearInterruptFlags(); + CMT2300A_GoSleep(); + + if (cmtTx56toCh != 0xFF) { + cmtSwitchChannel(cmtTx56toCh); + cmtTx56toCh = 0xFF; + cmtNextState = CMT_STATE_IDLE; + } else + cmtNextState = CMT_STATE_RX_START; // receive answer + + nRes = CMT_TX_DONE; + break; + } + case CMT_STATE_TX_TIMEOUT: { + CMT2300A_GoSleep(); + + Hoymiles.getMessageOutput()->println("TC timeout!"); + + if (cmtTx56toCh != 0xFF) { + cmtTx56toCh = 0xFF; + cmtNextState = CMT_STATE_IDLE; + } + + cmtNextState = CMT_STATE_IDLE; + + nRes = CMT_TX_TIMEOUT; + break; + } + case CMT_STATE_ERROR: { + CMT2300A_SoftReset(); + CMT2300A_DelayMs(20); + + CMT2300A_GoStby(); + cmtConfig(); + + cmtNextState = CMT_STATE_IDLE; + + nRes = CMT_ERROR; + break; + } + default: + break; + } + + return nRes; +} + +void HoymilesRadio_CMT::init() +{ + _dtuSerial.u64 = 0; + uint8_t tmp; + + CMT2300A_InitSpi(); + if (!CMT2300A_Init()) { + Hoymiles.getMessageOutput()->println("CMT2300A_Init() failed!"); + return; + } + + /* config registers */ + CMT2300A_ConfigRegBank(CMT2300A_CMT_BANK_ADDR, g_cmt2300aCmtBank, CMT2300A_CMT_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_SYSTEM_BANK_ADDR, g_cmt2300aSystemBank, CMT2300A_SYSTEM_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_FREQUENCY_BANK_ADDR, g_cmt2300aFrequencyBank, CMT2300A_FREQUENCY_BANK_SIZE); // cmtBaseChOff860 need to be changed to the same frequency for channel calculation + CMT2300A_ConfigRegBank(CMT2300A_DATA_RATE_BANK_ADDR, g_cmt2300aDataRateBank, CMT2300A_DATA_RATE_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_BASEBAND_BANK_ADDR, g_cmt2300aBasebandBank, CMT2300A_BASEBAND_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_TX_BANK_ADDR, g_cmt2300aTxBank, CMT2300A_TX_BANK_SIZE); + + cmtBaseChOff860 = (860000000 - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET; + + // xosc_aac_code[2:0] = 2 + tmp = (~0x07) & CMT2300A_ReadReg(CMT2300A_CUS_CMT10); + CMT2300A_WriteReg(CMT2300A_CUS_CMT10, tmp | 0x02); + + if (!cmtConfig()) { + Hoymiles.getMessageOutput()->println("cmtConfig() failed!"); + return; + } + + attachInterrupt(digitalPinToInterrupt(CMT_PIN_GPIO3), std::bind(&HoymilesRadio_CMT::handleIntr, this), RISING); + + cmtSwitchDtuFreq(CMT_WORK_FREQ); // start dtu at work freqency, for fast Rx if inverter is already on and frequency switched + + _ChipConnected = true; + + Hoymiles.getMessageOutput()->println("CMT init successful"); +} + +void HoymilesRadio_CMT::loop() +{ + enumCMTresult mCMTstate = cmtProcess(); + + if (mCMTstate != CMT_RX_DONE) { // Perform package parsing only if no packages are received + if (!_rxBuffer.empty()) { + fragment_t f = _rxBuffer.back(); + if (checkFragmentCrc(&f)) { + std::shared_ptr inv = Hoymiles.getInverterByFragment(&f); + + if (nullptr != inv) { + // Save packet in inverter rx buffer + Hoymiles.getMessageOutput()->print("RX "); + Hoymiles.getMessageOutput()->print(cmtChToFreq(f.channel)); + Hoymiles.getMessageOutput()->print(" --> "); + dumpBuf("", f.fragment, f.len); + Hoymiles.getMessageOutput()->print("| "); + Hoymiles.getMessageOutput()->print(f.rssi); + Hoymiles.getMessageOutput()->println(" dBm"); + + inv->addRxFragment(f.fragment, f.len); + } else { + Hoymiles.getMessageOutput()->println("Inverter Not found!"); + } + + } else { + Hoymiles.getMessageOutput()->println("Frame kaputt"); // ;-) + } + + // Remove paket from buffer even it was corrupted + _rxBuffer.pop(); + } + } + + if (_busyFlag && _rxTimeout.occured()) { + Hoymiles.getMessageOutput()->println("RX Period End"); + std::shared_ptr inv = Hoymiles.getInverterBySerial(_commandQueue.front().get()->getTargetAddress()); + + if (nullptr != inv) { + CommandAbstract* cmd = _commandQueue.front().get(); + uint8_t verifyResult = inv->verifyAllFragments(cmd); + if (verifyResult == FRAGMENT_ALL_MISSING_RESEND) { + Hoymiles.getMessageOutput()->println("Nothing received"); + // sendLastPacketAgain(); + _commandQueue.pop(); + _busyFlag = false; + + } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { + Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded"); + _commandQueue.pop(); + _busyFlag = false; + + } else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) { + Hoymiles.getMessageOutput()->println("Retransmit timeout"); + _commandQueue.pop(); + _busyFlag = false; + + } else if (verifyResult == FRAGMENT_HANDLE_ERROR) { + Hoymiles.getMessageOutput()->println("Packet handling error"); + _commandQueue.pop(); + _busyFlag = false; + + } else if (verifyResult > 0) { + // Perform Retransmit + Hoymiles.getMessageOutput()->print("Request retransmit: "); + Hoymiles.getMessageOutput()->println(verifyResult); + sendRetransmitPacket(verifyResult); + + } else { + // Successful received all packages + Hoymiles.getMessageOutput()->println("Success"); + _commandQueue.pop(); + _busyFlag = false; + } + } else { + // If inverter was not found, assume the command is invalid + Hoymiles.getMessageOutput()->println("RX: Invalid inverter found"); + _commandQueue.pop(); + _busyFlag = false; + } + } else if (!_busyFlag) { + // Currently in idle mode --> send packet if one is in the queue + if (!_commandQueue.empty()) { + CommandAbstract* cmd = _commandQueue.front().get(); + + auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); + if (nullptr != inv) { + inv->clearRxFragmentBuffer(); + sendEsbPacket(cmd); + } else { + Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); + _commandQueue.pop(); + } + } + } +} + +bool HoymilesRadio_CMT::isIdle() +{ + return !_busyFlag; +} + +bool HoymilesRadio_CMT::isConnected() +{ + return _ChipConnected; +} + +void ARDUINO_ISR_ATTR HoymilesRadio_CMT::handleIntr() +{ + _packetReceived = true; +} + +void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) +{ + cmd->incrementSendCount(); + + cmd->setRouterAddress(DtuSerial().u64); + + Hoymiles.getMessageOutput()->print("TX "); + Hoymiles.getMessageOutput()->print(cmd->getCommandName()); + Hoymiles.getMessageOutput()->print(" "); + Hoymiles.getMessageOutput()->print(cmtChToFreq(cmtActualCh)); + Hoymiles.getMessageOutput()->print(" --> "); + cmd->dumpDataPayload(Hoymiles.getMessageOutput()); + + memcpy(cmtTxBuffer, cmd->getDataPayload(), cmd->getDataSize()); + cmtTxLength = cmd->getDataSize(); + cmtTxTimeout = 100; + + cmtNextState = CMT_STATE_TX_START; + + _busyFlag = true; + _rxTimeout.set(cmd->getTimeout()); +} diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h new file mode 100644 index 00000000..be066537 --- /dev/null +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HoymilesRadio.h" +#include "TimeoutHelper.h" +#include "commands/CommandAbstract.h" +#include "types.h" +#include +#include +#include + +// number of fragments hold in buffer +#define FRAGMENT_BUFFER_SIZE 30 + +/* CMT states */ +typedef enum { + CMT_STATE_IDLE = 0, + CMT_STATE_RX_START, + CMT_STATE_RX_WAIT, + CMT_STATE_RX_DONE, + CMT_STATE_RX_TIMEOUT, + CMT_STATE_TX_START, + CMT_STATE_TX_WAIT, + CMT_STATE_TX_DONE, + CMT_STATE_TX_TIMEOUT, + CMT_STATE_ERROR, +} enumCMTstate; + +/* CMT process function results */ +typedef enum { + CMT_IDLE = 0, + CMT_BUSY, + CMT_RX_DONE, + CMT_RX_TIMEOUT, + CMT_TX_DONE, + CMT_TX_TIMEOUT, + CMT_ERROR, +} enumCMTresult; + +class HoymilesRadio_CMT : public HoymilesRadio { +public: + void init(); + void loop(); + + bool isIdle(); + bool isConnected(); + +private: + void ARDUINO_ISR_ATTR handleIntr(); + + void sendEsbPacket(CommandAbstract* cmd); + + volatile bool _packetReceived = false; + + std::queue _rxBuffer; + TimeoutHelper _rxTimeout; + + bool _busyFlag = false; + + bool _ChipConnected = false; + + String cmtChToFreq(const uint8_t channel); + void cmtSwitchChannel(const uint8_t channel); + uint8_t cmtFreqToChan(const String func_name, const String var_name, const uint32_t freq_kHz); + bool cmtSwitchDtuFreq(const uint32_t to_freq_kHz); + bool cmtConfig(void); + bool cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const uint32_t from_freq_kHz, const uint32_t to_freq_kHz); + enumCMTresult cmtProcess(void); + + enumCMTstate cmtNextState = CMT_STATE_IDLE; + uint8_t cmtTxBuffer[32]; + uint8_t cmtTxLength = 0; + + uint32_t cmtRxTimeout = 200; + uint32_t cmtTxTimeout = 200; + uint32_t cmtRxTimeCount = 0; + uint32_t cmtTxTimeCount = 0; + + uint8_t cmtBaseChOff860; // offset from initalized CMT base frequency to Hoy base frequency in channels + uint8_t cmtActualCh; // actual used channel, should be stored per inverter und set before next Tx, if hopping is used + + uint8_t cmtTx56toCh = 0xFF; // send CMD56 active to Channel xx, inactive = 0xFF + + uint8_t cmtRxTimeoutCnt = 0; // Rx timeout counter !!! should be stored per inverter !!! +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/types.h b/lib/Hoymiles/src/types.h index 9a43d2f1..9ab3f6c8 100644 --- a/lib/Hoymiles/src/types.h +++ b/lib/Hoymiles/src/types.h @@ -16,5 +16,6 @@ typedef struct { uint8_t fragment[MAX_RF_PAYLOAD_SIZE]; uint8_t len; uint8_t channel; + int8_t rssi; bool wasReceived; } fragment_t; diff --git a/platformio.ini b/platformio.ini index 841da184..b2b0d710 100644 --- a/platformio.ini +++ b/platformio.ini @@ -61,6 +61,7 @@ build_flags = ${env.build_flags} -DCMT_PIN_CS=5 -DCMT_PIN_FCS=4 -DCMT_PIN_GPIO3=15 + -DCMT_WORK_FREQ=865000 [env:olimex_esp32_poe] From a585ffe199f030240b0f0873cd7b87715fae7681 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 7 Mar 2023 20:30:00 +0100 Subject: [PATCH 11/66] Add variable for max channel count (to extend serveral arrays) --- lib/Hoymiles/src/parser/StatisticsParser.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index c52514ad..c16c35db 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -60,7 +60,7 @@ enum ChannelNum_t { CH1, CH2, CH3, - CH4 + CH_CNT }; enum ChannelType_t { @@ -122,7 +122,7 @@ public: private: uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE] = {}; uint8_t _statisticLength = 0; - uint16_t _stringMaxPower[CH4]; + uint16_t _stringMaxPower[CH_CNT]; const std::list* _byteAssignment; std::list _fieldSettings; From 3c0d89f599937520d981b3d041bb442b7e4cdddc Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 7 Mar 2023 21:37:23 +0100 Subject: [PATCH 12/66] Replaced println by printf and code style changes --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 152 ++++++++++++------------- lib/Hoymiles/src/HoymilesRadio_CMT.h | 4 +- 2 files changed, 72 insertions(+), 84 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 28d35443..d91e0ffb 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -24,32 +24,36 @@ void HoymilesRadio_CMT::cmtSwitchChannel(const uint8_t channel) yield(); CMT2300A_SetFrequencyChannel(channel); yield(); - cmtActualCh = channel; - // Hoymiles.getMessageOutput()->println("[cmtSwitchChannel] switched channel to " + cmtGetActFreq()); + cmtCurrentCh = channel; } -uint8_t HoymilesRadio_CMT::cmtFreqToChan(const String func_name, const String var_name, const uint32_t freq_kHz) +uint8_t HoymilesRadio_CMT::cmtFreqToChan(const String& func_name, const String& var_name, const uint32_t freq_kHz) { if ((freq_kHz % 250) != 0) { - Hoymiles.getMessageOutput()->println(func_name + " " + var_name + " " + String(freq_kHz / 1000.0, 3) + " MHz is not divisible by 250 kHz!"); + Hoymiles.getMessageOutput()->printf("%s %s %.3f MHz is not divisible by 250 kHz!\r\n", + func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0); return 0xFF; // ERROR } const uint32_t min_Freq_kHz = (HOY_BASE_FREQ + (cmtBaseChOff860 >= 1 ? cmtBaseChOff860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000; // frequency can not be lower than actual initailized base freq const uint32_t max_Freq_kHz = (HOY_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000; // =923500, 0xFF does not work if (freq_kHz < min_Freq_kHz || freq_kHz > max_Freq_kHz) { - Hoymiles.getMessageOutput()->println(func_name + " " + var_name + " " + String(freq_kHz / 1000.0, 2) + " MHz is out of Hoymiles/CMT range! (" + String(min_Freq_kHz / 1000.0, 2) + " MHz - " + String(max_Freq_kHz / 1000.0, 2) + " MHz)"); + Hoymiles.getMessageOutput()->printf("%s %s %.2f MHz is out of Hoymiles/CMT range! (%.2f MHz - %.2f MHz)\r\n", + func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0, min_Freq_kHz / 1000.0, max_Freq_kHz / 1000.0); return 0xFF; // ERROR } - if (freq_kHz < 863000 || freq_kHz > 870000) - Hoymiles.getMessageOutput()->println(func_name + " !!! caution: " + var_name + " " + String(freq_kHz / 1000.0, 2) + " MHz is out of EU legal range! (863 - 870 MHz)"); + if (freq_kHz < 863000 || freq_kHz > 870000) { + Hoymiles.getMessageOutput()->printf("%s !!! caution: %s %.2f MHz is out of EU legal range! (863 - 870 MHz)\r\n", + func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0); + } return (freq_kHz * 1000 - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET - cmtBaseChOff860; // frequency to channel } bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) { const uint8_t toChannel = cmtFreqToChan("[cmtSwitchDtuFreq]", "to_freq_kHz", to_freq_kHz); - if (toChannel == 0xFF) + if (toChannel == 0xFF) { return false; + } cmtSwitchChannel(toChannel); @@ -58,12 +62,6 @@ bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) bool HoymilesRadio_CMT::cmtConfig(void) { -#ifdef ENABLE_ANTENNA_SWITCH - /* If you enable antenna switch, GPIO1/GPIO2 will output RX_ACTIVE/TX_ACTIVE, - and it can't output INT1/INT2 via GPIO1/GPIO2 */ - CMT2300A_EnableAntennaSwitch(0); - -#else /* Config GPIOs */ CMT2300A_ConfigGpio( CMT2300A_GPIO3_SEL_INT2); @@ -73,23 +71,20 @@ bool HoymilesRadio_CMT::cmtConfig(void) CMT2300A_INT_SEL_TX_DONE, /* Config INT1 */ CMT2300A_INT_SEL_PKT_OK /* Config INT2 */ ); -#endif /* Enable interrupt */ CMT2300A_EnableInterrupt( CMT2300A_MASK_TX_DONE_EN | CMT2300A_MASK_PREAM_OK_EN | CMT2300A_MASK_SYNC_OK_EN | CMT2300A_MASK_CRC_OK_EN | CMT2300A_MASK_PKT_DONE_EN); - /* Disable low frequency OSC calibration */ - // CMT2300A_EnableLfosc(FALSE); - CMT2300A_SetFrequencyStep(100); // set FH_OFFSET to 100 (frequency = base freq + 2.5kHz*FH_OFFSET*FH_CHANNEL) /* Use a single 64-byte FIFO for either Tx or Rx */ CMT2300A_EnableFifoMerge(true); /* Go to sleep for configuration to take effect */ - if (!CMT2300A_GoSleep()) + if (!CMT2300A_GoSleep()) { return false; // CMT2300A not switched to sleep mode! + } return true; } @@ -98,8 +93,9 @@ bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const { const uint8_t fromChannel = cmtFreqToChan("[cmtSwitchInvAndDtuFreq]", "from_freq_kHz", from_freq_kHz); const uint8_t toChannel = cmtFreqToChan("[cmtSwitchInvAndDtuFreq]", "to_freq_kHz", to_freq_kHz); - if (fromChannel == 0xFF || toChannel == 0xFF) + if (fromChannel == 0xFF || toChannel == 0xFF) { return false; + } cmtSwitchChannel(fromChannel); cmtTx56toCh = toChannel; @@ -115,9 +111,7 @@ bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const cmtTxBuffer[13] = 0x14; cmtTxBuffer[14] = crc8(cmtTxBuffer, 14); - Hoymiles.getMessageOutput()->print("TX CMD56 "); - Hoymiles.getMessageOutput()->print(cmtChToFreq(cmtActualCh)); - Hoymiles.getMessageOutput()->print(" --> "); + Hoymiles.getMessageOutput()->printf("TX CMD56 %s --> ", cmtChToFreq(cmtCurrentCh).c_str()); dumpBuf("", cmtTxBuffer, 15); cmtTxLength = 15; @@ -125,8 +119,6 @@ bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const cmtNextState = CMT_STATE_TX_START; - //_busyFlag = true; - return true; } @@ -135,11 +127,11 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) enumCMTresult nRes = CMT_BUSY; switch (cmtNextState) { - case CMT_STATE_IDLE: { + case CMT_STATE_IDLE: nRes = CMT_IDLE; break; - } - case CMT_STATE_RX_START: { + + case CMT_STATE_RX_START: CMT2300A_GoStby(); CMT2300A_ClearInterruptFlags(); @@ -147,33 +139,31 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) CMT2300A_EnableReadFifo(); CMT2300A_ClearRxFifo(); - if (!CMT2300A_GoRx()) + if (!CMT2300A_GoRx()) { cmtNextState = CMT_STATE_ERROR; - else + } else { cmtNextState = CMT_STATE_RX_WAIT; + } cmtRxTimeCount = CMT2300A_GetTickCount(); cmtRxTimeout = 200; break; - } - case CMT_STATE_RX_WAIT: { -#ifdef ENABLE_ANTENNA_SWITCH - if (CMT2300A_MASK_PKT_OK_FLG & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG)) /* Read PKT_OK flag */ -#else + + case CMT_STATE_RX_WAIT: if (_packetReceived) /* Read INT2, PKT_OK */ -#endif { Hoymiles.getMessageOutput()->println("Interrupt received"); _packetReceived = false; // reset interrupt cmtNextState = CMT_STATE_RX_DONE; } - if ((CMT2300A_GetTickCount() - cmtRxTimeCount) > cmtRxTimeout) + if ((CMT2300A_GetTickCount() - cmtRxTimeCount) > cmtRxTimeout) { cmtNextState = CMT_STATE_RX_TIMEOUT; + } break; - } + case CMT_STATE_RX_DONE: { CMT2300A_GoStby(); @@ -187,35 +177,40 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) fragment_t f; memset(f.fragment, 0xcc, MAX_RF_PAYLOAD_SIZE); CMT2300A_ReadFifo(&f.len, 1); // first byte in FiFo is length - f.channel = cmtActualCh; + f.channel = cmtCurrentCh; f.rssi = CMT2300A_GetRssiDBm(); - if (f.len > MAX_RF_PAYLOAD_SIZE) + if (f.len > MAX_RF_PAYLOAD_SIZE) { f.len = MAX_RF_PAYLOAD_SIZE; + } CMT2300A_ReadFifo(f.fragment, f.len); - if (f.fragment[9] & 0x80) // last frame detection for end Rx + if (f.fragment[9] & 0x80) { // last frame detection for end Rx isLastFrame = true; + } _rxBuffer.push(f); } else { Hoymiles.getMessageOutput()->println("Buffer full"); } - } else if ((state & 0x19) == 0x19) - Hoymiles.getMessageOutput()->println("[CMT_STATE_RX_DONE] state: " + String(state, HEX) + " (CRC_ERROR)"); - else - Hoymiles.getMessageOutput()->println("[CMT_STATE_RX_DONE] wrong state: " + String(state, HEX)); + } else if ((state & 0x19) == 0x19) { + Hoymiles.getMessageOutput()->printf("[CMT_STATE_RX_DONE] state: %x (CRC_ERROR)\r\n", state); + } else { + Hoymiles.getMessageOutput()->printf("[CMT_STATE_RX_DONE] wrong state: %x\r\n", state); + } CMT2300A_ClearInterruptFlags(); CMT2300A_GoSleep(); - if (isLastFrame) // last frame received + if (isLastFrame) { // last frame received cmtNextState = CMT_STATE_IDLE; - else + } else { cmtNextState = CMT_STATE_RX_START; // receive next frame(s) + } nRes = CMT_RX_DONE; break; } - case CMT_STATE_RX_TIMEOUT: { + + case CMT_STATE_RX_TIMEOUT: CMT2300A_GoSleep(); Hoymiles.getMessageOutput()->println("RX timeout!"); @@ -223,17 +218,17 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) cmtNextState = CMT_STATE_IDLE; // send CMD56 after 3 Rx timeouts - if (cmtRxTimeoutCnt < 2) + if (cmtRxTimeoutCnt < 2) { cmtRxTimeoutCnt++; - else { + } else { uint32_t invSerial = cmtTxBuffer[1] << 24 | cmtTxBuffer[2] << 16 | cmtTxBuffer[3] << 8 | cmtTxBuffer[4]; // read inverter serial from last Tx buffer cmtSwitchInvAndDtuFreq(invSerial, HOY_BOOT_FREQ / 1000, CMT_WORK_FREQ); } nRes = CMT_RX_TIMEOUT; break; - } - case CMT_STATE_TX_START: { + + case CMT_STATE_TX_START: CMT2300A_GoStby(); CMT2300A_ClearInterruptFlags(); @@ -245,34 +240,33 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) /* The length need be smaller than 32 */ CMT2300A_WriteFifo(cmtTxBuffer, cmtTxLength); - if (!(CMT2300A_ReadReg(CMT2300A_CUS_FIFO_FLAG) & CMT2300A_MASK_TX_FIFO_NMTY_FLG)) + if (!(CMT2300A_ReadReg(CMT2300A_CUS_FIFO_FLAG) & CMT2300A_MASK_TX_FIFO_NMTY_FLG)) { cmtNextState = CMT_STATE_ERROR; + } - if (!CMT2300A_GoTx()) + if (!CMT2300A_GoTx()) { cmtNextState = CMT_STATE_ERROR; - else + } else { cmtNextState = CMT_STATE_TX_WAIT; + } cmtTxTimeCount = CMT2300A_GetTickCount(); break; - } - case CMT_STATE_TX_WAIT: { - // #ifdef ENABLE_ANTENNA_SWITCH + + case CMT_STATE_TX_WAIT: if (CMT2300A_MASK_TX_DONE_FLG & CMT2300A_ReadReg(CMT2300A_CUS_INT_CLR1)) /* Read TX_DONE flag */ - // #else - // if(CMT2300A_ReadGpio1()) /* Read INT1, TX_DONE */ - // #endif { cmtNextState = CMT_STATE_TX_DONE; } - if ((CMT2300A_GetTickCount() - cmtTxTimeCount) > cmtTxTimeout) + if ((CMT2300A_GetTickCount() - cmtTxTimeCount) > cmtTxTimeout) { cmtNextState = CMT_STATE_TX_TIMEOUT; + } break; - } - case CMT_STATE_TX_DONE: { + + case CMT_STATE_TX_DONE: CMT2300A_ClearInterruptFlags(); CMT2300A_GoSleep(); @@ -280,16 +274,17 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) cmtSwitchChannel(cmtTx56toCh); cmtTx56toCh = 0xFF; cmtNextState = CMT_STATE_IDLE; - } else + } else { cmtNextState = CMT_STATE_RX_START; // receive answer + } nRes = CMT_TX_DONE; break; - } - case CMT_STATE_TX_TIMEOUT: { + + case CMT_STATE_TX_TIMEOUT: CMT2300A_GoSleep(); - Hoymiles.getMessageOutput()->println("TC timeout!"); + Hoymiles.getMessageOutput()->println("TX timeout!"); if (cmtTx56toCh != 0xFF) { cmtTx56toCh = 0xFF; @@ -300,8 +295,8 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) nRes = CMT_TX_TIMEOUT; break; - } - case CMT_STATE_ERROR: { + + case CMT_STATE_ERROR: CMT2300A_SoftReset(); CMT2300A_DelayMs(20); @@ -312,7 +307,7 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) nRes = CMT_ERROR; break; - } + default: break; } @@ -371,13 +366,9 @@ void HoymilesRadio_CMT::loop() if (nullptr != inv) { // Save packet in inverter rx buffer - Hoymiles.getMessageOutput()->print("RX "); - Hoymiles.getMessageOutput()->print(cmtChToFreq(f.channel)); - Hoymiles.getMessageOutput()->print(" --> "); + Hoymiles.getMessageOutput()->printf("RX %s --> ", cmtChToFreq(f.channel).c_str()); dumpBuf("", f.fragment, f.len); - Hoymiles.getMessageOutput()->print("| "); - Hoymiles.getMessageOutput()->print(f.rssi); - Hoymiles.getMessageOutput()->println(" dBm"); + Hoymiles.getMessageOutput()->printf("| %d dBm", f.rssi); inv->addRxFragment(f.fragment, f.len); } else { @@ -477,11 +468,8 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) cmd->setRouterAddress(DtuSerial().u64); - Hoymiles.getMessageOutput()->print("TX "); - Hoymiles.getMessageOutput()->print(cmd->getCommandName()); - Hoymiles.getMessageOutput()->print(" "); - Hoymiles.getMessageOutput()->print(cmtChToFreq(cmtActualCh)); - Hoymiles.getMessageOutput()->print(" --> "); + Hoymiles.getMessageOutput()->printf("TX %s %s --> ", + cmd->getCommandName().c_str(), cmtChToFreq(cmtCurrentCh).c_str()); cmd->dumpDataPayload(Hoymiles.getMessageOutput()); memcpy(cmtTxBuffer, cmd->getDataPayload(), cmd->getDataSize()); diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index be066537..5c0ff969 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -61,7 +61,7 @@ private: String cmtChToFreq(const uint8_t channel); void cmtSwitchChannel(const uint8_t channel); - uint8_t cmtFreqToChan(const String func_name, const String var_name, const uint32_t freq_kHz); + uint8_t cmtFreqToChan(const String& func_name, const String& var_name, const uint32_t freq_kHz); bool cmtSwitchDtuFreq(const uint32_t to_freq_kHz); bool cmtConfig(void); bool cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const uint32_t from_freq_kHz, const uint32_t to_freq_kHz); @@ -77,7 +77,7 @@ private: uint32_t cmtTxTimeCount = 0; uint8_t cmtBaseChOff860; // offset from initalized CMT base frequency to Hoy base frequency in channels - uint8_t cmtActualCh; // actual used channel, should be stored per inverter und set before next Tx, if hopping is used + uint8_t cmtCurrentCh; // current used channel, should be stored per inverter und set before next Tx, if hopping is used uint8_t cmtTx56toCh = 0xFF; // send CMD56 active to Channel xx, inactive = 0xFF From 45882543b683d17536fb712d23de99fda4a862f6 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 8 Mar 2023 20:12:22 +0100 Subject: [PATCH 13/66] Allow dynamic pin mapping for CMT module --- docs/DeviceProfiles.md | 5 + include/PinMapping.h | 8 ++ lib/CMT2300a/cmt2300a_hal.c | 4 +- lib/CMT2300a/cmt2300a_hal.h | 2 +- lib/CMT2300a/cmt_spi3.c | 12 +- lib/CMT2300a/cmt_spi3.h | 2 +- lib/Hoymiles/src/Hoymiles.cpp | 115 ++++++++++-------- lib/Hoymiles/src/Hoymiles.h | 4 +- lib/Hoymiles/src/HoymilesRadio.cpp | 10 ++ lib/Hoymiles/src/HoymilesRadio.h | 5 + lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 16 +-- lib/Hoymiles/src/HoymilesRadio_CMT.h | 5 +- lib/Hoymiles/src/HoymilesRadio_NRF.cpp | 6 +- lib/Hoymiles/src/HoymilesRadio_NRF.h | 3 - .../src/inverters/InverterAbstract.cpp | 5 + lib/Hoymiles/src/inverters/InverterAbstract.h | 2 + platformio.ini | 7 +- src/InverterSettings.cpp | 12 +- src/PinMapping.cpp | 41 +++++++ 19 files changed, 174 insertions(+), 90 deletions(-) diff --git a/docs/DeviceProfiles.md b/docs/DeviceProfiles.md index 099e6096..7d676927 100644 --- a/docs/DeviceProfiles.md +++ b/docs/DeviceProfiles.md @@ -91,6 +91,11 @@ The json file can contain multiple profiles. Each profile requires a name and di | nrf24.irq | number | Interrupt Pin | | nrf24.en | number | Enable Pin | | nrf24.cs | number | Chip Select Pin | +| cmt.sdio | number | SDIO Pin | +| cmt.clk | number | CLK Pin | +| cmt.cs | number | CS Pin | +| cmt.fcs | number | FCS Pin | +| cmt.gpio3 | number | GPIO3 Pin | | eth.enabled | boolean | Enable/Disable the ethernet stack | | eth.phy_addr | number | Unique PHY addr | | eth.power | number | Power Pin (if available). Use -1 for not assigned pins. | diff --git a/include/PinMapping.h b/include/PinMapping.h index b80b3efc..df8bb781 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -18,6 +18,13 @@ struct PinMapping_t { int8_t nrf24_irq; int8_t nrf24_en; int8_t nrf24_cs; + + int8_t cmt_clk; + int8_t cmt_cs; + int8_t cmt_fcs; + int8_t cmt_gpio3; + int8_t cmt_sdio; + int8_t eth_phy_addr; bool eth_enabled; int eth_power; @@ -40,6 +47,7 @@ public: PinMapping_t& get(); bool isValidNrf24Config(); + bool isValidCmt2300Config(); bool isValidEthConfig(); private: diff --git a/lib/CMT2300a/cmt2300a_hal.c b/lib/CMT2300a/cmt2300a_hal.c index 4fe01668..3f7111ca 100644 --- a/lib/CMT2300a/cmt2300a_hal.c +++ b/lib/CMT2300a/cmt2300a_hal.c @@ -26,9 +26,9 @@ * @name CMT2300A_InitSpi * @desc Initializes the CMT2300A SPI interface. * *********************************************************/ -void CMT2300A_InitSpi(void) +void CMT2300A_InitSpi(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs) { - cmt_spi3_init(); + cmt_spi3_init(pin_sdio, pin_clk, pin_cs, pin_fcs); } /*! ******************************************************** diff --git a/lib/CMT2300a/cmt2300a_hal.h b/lib/CMT2300a/cmt2300a_hal.h index eb4937b3..8a27251f 100644 --- a/lib/CMT2300a/cmt2300a_hal.h +++ b/lib/CMT2300a/cmt2300a_hal.h @@ -36,7 +36,7 @@ extern "C" { #define CMT2300A_GetTickCount() millis() /* ************************************************************************ */ -void CMT2300A_InitSpi(void); +void CMT2300A_InitSpi(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs); uint8_t CMT2300A_ReadReg(uint8_t addr); void CMT2300A_WriteReg(uint8_t addr, uint8_t dat); diff --git a/lib/CMT2300a/cmt_spi3.c b/lib/CMT2300a/cmt_spi3.c index 79805ed2..464701f6 100644 --- a/lib/CMT2300a/cmt_spi3.c +++ b/lib/CMT2300a/cmt_spi3.c @@ -7,12 +7,12 @@ spi_device_handle_t spi_reg, spi_fifo; -void cmt_spi3_init(void) +void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs) { spi_bus_config_t buscfg = { - .mosi_io_num = CMT_PIN_SDIO, + .mosi_io_num = pin_sdio, .miso_io_num = -1, // single wire MOSI/MISO - .sclk_io_num = CMT_PIN_CLK, + .sclk_io_num = pin_clk, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 32, @@ -23,7 +23,7 @@ void cmt_spi3_init(void) .dummy_bits = 0, .mode = 0, // SPI mode 0 .clock_speed_hz = CMT_SPI_CLK, - .spics_io_num = CMT_PIN_CS, + .spics_io_num = pin_cs, .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, .queue_size = 1, .pre_cb = NULL, @@ -42,7 +42,7 @@ void cmt_spi3_init(void) .cs_ena_pretrans = 2, .cs_ena_posttrans = (uint8_t)(1 / (CMT_SPI_CLK * 10e6 * 2) + 2), // >2 us .clock_speed_hz = CMT_SPI_CLK, - .spics_io_num = CMT_PIN_FCS, + .spics_io_num = pin_fcs, .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, .queue_size = 1, .pre_cb = NULL, @@ -50,7 +50,7 @@ void cmt_spi3_init(void) }; ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &devcfg2, &spi_fifo)); - esp_rom_gpio_connect_out_signal(CMT_PIN_SDIO, spi_periph_signal[SPI2_HOST].spid_out, true, false); + esp_rom_gpio_connect_out_signal(pin_sdio, spi_periph_signal[SPI2_HOST].spid_out, true, false); delay(100); } diff --git a/lib/CMT2300a/cmt_spi3.h b/lib/CMT2300a/cmt_spi3.h index db11f952..a29a03da 100644 --- a/lib/CMT2300a/cmt_spi3.h +++ b/lib/CMT2300a/cmt_spi3.h @@ -3,7 +3,7 @@ #include -void cmt_spi3_init(void); +void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs); void cmt_spi3_write(uint8_t addr, uint8_t dat); uint8_t cmt_spi3_read(uint8_t addr); diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 0f3f14ff..59876049 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -3,12 +3,12 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "Hoymiles.h" -#include "inverters/HM_1CH.h" -#include "inverters/HM_2CH.h" -#include "inverters/HM_4CH.h" #include "inverters/HMS_1CH.h" #include "inverters/HMS_2CH.h" #include "inverters/HMS_4CH.h" +#include "inverters/HM_1CH.h" +#include "inverters/HM_2CH.h" +#include "inverters/HM_4CH.h" #include #define HOY_SEMAPHORE_TAKE() xSemaphoreTake(_xSemaphore, portMAX_DELAY) @@ -16,73 +16,90 @@ HoymilesClass Hoymiles; -void HoymilesClass::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ) +void HoymilesClass::init() { _xSemaphore = xSemaphoreCreateMutex(); - HOY_SEMAPHORE_GIVE(); // release before first use + HOY_SEMAPHORE_GIVE(); // release before first use _pollInterval = 0; _radioNrf.reset(new HoymilesRadio_NRF()); - _radioNrf->init(initialisedSpiBus, pinCE, pinIRQ); - _radioCmt.reset(new HoymilesRadio_CMT()); - _radioCmt->init(); +} + +void HoymilesClass::initNRF(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ) +{ + _radioNrf->init(initialisedSpiBus, pinCE, pinIRQ); +} + +void HoymilesClass::initCMT(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio3) +{ + _radioCmt->init(pin_sdio, pin_clk, pin_cs, pin_fcs, pin_gpio3); } void HoymilesClass::loop() { HOY_SEMAPHORE_TAKE(); - _radioNrf->loop(); - _radioCmt->loop(); + if (_radioNrf->isInitialized()) { + _radioNrf->loop(); + } + + if (_radioCmt->isInitialized()) { + _radioCmt->loop(); + } if (getNumInverters() > 0) { if (millis() - _lastPoll > (_pollInterval * 1000)) { static uint8_t inverterPos = 0; - if (_radioNrf->isIdle() && _radioCmt->isIdle()) { - std::shared_ptr iv = getInverterByPos(inverterPos); - if (iv != nullptr) { - _messageOutput->print("Fetch inverter: "); - _messageOutput->println(iv->serial(), HEX); - - iv->sendStatsRequest(); - - // Fetch event log - bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; - iv->sendAlarmLogRequest(force); - - // Fetch limit - if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK) - || ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) - && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { - _messageOutput->println("Request SystemConfigPara"); - iv->sendSystemConfigParaRequest(); - } - - // Set limit if required - if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { - _messageOutput->println("Resend ActivePowerControl"); - iv->resendActivePowerControlRequest(); - } - - // Set power status if required - if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { - _messageOutput->println("Resend PowerCommand"); - iv->resendPowerControlRequest(); - } - - // Fetch dev info (but first fetch stats) - if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) { - _messageOutput->println("Request device info"); - iv->sendDevInfoRequest(); - } - } + std::shared_ptr iv = getInverterByPos(inverterPos); + if ((iv == nullptr) || ((iv != nullptr) && (!iv->getRadio()->isInitialized()))) { if (++inverterPos >= getNumInverters()) { inverterPos = 0; } } - _lastPoll = millis(); + if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isIdle()) { + _messageOutput->print("Fetch inverter: "); + _messageOutput->println(iv->serial(), HEX); + + iv->sendStatsRequest(); + + // Fetch event log + bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; + iv->sendAlarmLogRequest(force); + + // Fetch limit + if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK) + || ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) + && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { + _messageOutput->println("Request SystemConfigPara"); + iv->sendSystemConfigParaRequest(); + } + + // Set limit if required + if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { + _messageOutput->println("Resend ActivePowerControl"); + iv->resendActivePowerControlRequest(); + } + + // Set power status if required + if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { + _messageOutput->println("Resend PowerCommand"); + iv->resendPowerControlRequest(); + } + + // Fetch dev info (but first fetch stats) + if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) { + _messageOutput->println("Request device info"); + iv->sendDevInfoRequest(); + } + + if (++inverterPos >= getNumInverters()) { + inverterPos = 0; + } + + _lastPoll = millis(); + } } } diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index 0442fbd0..0d2d1ec5 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -15,7 +15,9 @@ class HoymilesClass { public: - void init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ); + void init(); + void initNRF(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pinIRQ); + void initCMT(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio3); void loop(); void setMessageOutput(Print* output); diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index c68def6e..3ceb408c 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -61,4 +61,14 @@ void HoymilesRadio::dumpBuf(const char* info, uint8_t buf[], uint8_t len) Hoymiles.getMessageOutput()->printf("%02X ", buf[i]); } Hoymiles.getMessageOutput()->println(""); +} + +bool HoymilesRadio::isInitialized() +{ + return _isInitialized; +} + +bool HoymilesRadio::isIdle() +{ + return !_busyFlag; } \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 93bc457a..9e3aad31 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -11,6 +11,9 @@ public: serial_u DtuSerial(); virtual void setDtuSerial(uint64_t serial); + bool isIdle(); + bool isInitialized(); + template T* enqueCommand() { @@ -29,4 +32,6 @@ protected: serial_u _dtuSerial; std::queue> _commandQueue; + bool _isInitialized = false; + bool _busyFlag = false; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index d91e0ffb..8db734de 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -222,7 +222,7 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) cmtRxTimeoutCnt++; } else { uint32_t invSerial = cmtTxBuffer[1] << 24 | cmtTxBuffer[2] << 16 | cmtTxBuffer[3] << 8 | cmtTxBuffer[4]; // read inverter serial from last Tx buffer - cmtSwitchInvAndDtuFreq(invSerial, HOY_BOOT_FREQ / 1000, CMT_WORK_FREQ); + cmtSwitchInvAndDtuFreq(invSerial, HOY_BOOT_FREQ / 1000, HOYMILES_CMT_WORK_FREQ); } nRes = CMT_RX_TIMEOUT; @@ -315,12 +315,12 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) return nRes; } -void HoymilesRadio_CMT::init() +void HoymilesRadio_CMT::init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio3) { _dtuSerial.u64 = 0; uint8_t tmp; - CMT2300A_InitSpi(); + CMT2300A_InitSpi(pin_sdio, pin_clk, pin_cs, pin_fcs); if (!CMT2300A_Init()) { Hoymiles.getMessageOutput()->println("CMT2300A_Init() failed!"); return; @@ -345,11 +345,12 @@ void HoymilesRadio_CMT::init() return; } - attachInterrupt(digitalPinToInterrupt(CMT_PIN_GPIO3), std::bind(&HoymilesRadio_CMT::handleIntr, this), RISING); + attachInterrupt(digitalPinToInterrupt(pin_gpio3), std::bind(&HoymilesRadio_CMT::handleIntr, this), RISING); - cmtSwitchDtuFreq(CMT_WORK_FREQ); // start dtu at work freqency, for fast Rx if inverter is already on and frequency switched + cmtSwitchDtuFreq(HOYMILES_CMT_WORK_FREQ); // start dtu at work freqency, for fast Rx if inverter is already on and frequency switched _ChipConnected = true; + _isInitialized = true; Hoymiles.getMessageOutput()->println("CMT init successful"); } @@ -447,11 +448,6 @@ void HoymilesRadio_CMT::loop() } } -bool HoymilesRadio_CMT::isIdle() -{ - return !_busyFlag; -} - bool HoymilesRadio_CMT::isConnected() { return _ChipConnected; diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index 5c0ff969..91bec78a 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -39,10 +39,9 @@ typedef enum { class HoymilesRadio_CMT : public HoymilesRadio { public: - void init(); + void init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio3); void loop(); - bool isIdle(); bool isConnected(); private: @@ -55,8 +54,6 @@ private: std::queue _rxBuffer; TimeoutHelper _rxTimeout; - bool _busyFlag = false; - bool _ChipConnected = false; String cmtChToFreq(const uint8_t channel); diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp index ded07f6e..088acbeb 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -33,6 +33,7 @@ void HoymilesRadio_NRF::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t openReadingPipe(); _radio->startListening(); + _isInitialized = true; } void HoymilesRadio_NRF::loop() @@ -159,11 +160,6 @@ void HoymilesRadio_NRF::setDtuSerial(uint64_t serial) openReadingPipe(); } -bool HoymilesRadio_NRF::isIdle() -{ - return !_busyFlag; -} - bool HoymilesRadio_NRF::isConnected() { return _radio->isChipConnected(); diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.h b/lib/Hoymiles/src/HoymilesRadio_NRF.h index 63e85f5a..88c0d2f9 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.h +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.h @@ -20,7 +20,6 @@ public: virtual void setDtuSerial(uint64_t serial); - bool isIdle(); bool isConnected(); bool isPVariant(); @@ -46,6 +45,4 @@ private: std::queue _rxBuffer; TimeoutHelper _rxTimeout; - - bool _busyFlag = false; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 4951cdd8..372772f0 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -96,6 +96,11 @@ bool InverterAbstract::getEnableCommands() return _enableCommands; } +HoymilesRadio* InverterAbstract::getRadio() +{ + return _radio; +} + AlarmLogParser* InverterAbstract::EventLog() { return _alarmLogParser.get(); diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index c111fc12..e2a35862 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -64,6 +64,8 @@ public: virtual bool sendRestartControlRequest() = 0; virtual bool resendPowerControlRequest() = 0; + HoymilesRadio* getRadio(); + AlarmLogParser* EventLog(); DevInfoParser* DevInfo(); PowerCommandParser* PowerCommand(); diff --git a/platformio.ini b/platformio.ini index b2b0d710..87fe8151 100644 --- a/platformio.ini +++ b/platformio.ini @@ -56,12 +56,7 @@ build_flags = ${env.build_flags} -DHOYMILES_PIN_IRQ=16 -DHOYMILES_PIN_CE=4 -DHOYMILES_PIN_CS=5 - -DCMT_PIN_CLK=18 - -DCMT_PIN_SDIO=23 - -DCMT_PIN_CS=5 - -DCMT_PIN_FCS=4 - -DCMT_PIN_GPIO3=15 - -DCMT_WORK_FREQ=865000 + -DHOYMILES_CMT_WORK_FREQ=865000 [env:olimex_esp32_poe] diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 1562b135..92c5eadb 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -22,11 +22,19 @@ void InverterSettingsClass::init() // Initialize inverter communication MessageOutput.print("Initialize Hoymiles interface... "); - if (PinMapping.isValidNrf24Config()) { + if (PinMapping.isValidNrf24Config() || PinMapping.isValidCmt2300Config()) { SPIClass* spiClass = new SPIClass(VSPI); spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); Hoymiles.setMessageOutput(&MessageOutput); - Hoymiles.init(spiClass, pin.nrf24_en, pin.nrf24_irq); + Hoymiles.init(); + + if (PinMapping.isValidNrf24Config()) { + Hoymiles.initNRF(spiClass, pin.nrf24_en, pin.nrf24_irq); + } + + if (PinMapping.isValidCmt2300Config()) { + Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio3); + } MessageOutput.println(" Setting radio PA level... "); Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index a8e0bc43..c62dc032 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -38,6 +38,26 @@ #define LED1 -1 #endif +#ifndef CMT_CLK +#define CMT_CLK -1 +#endif + +#ifndef CMT_CS +#define CMT_CS -1 +#endif + +#ifndef CMT_FCS +#define CMT_FCS -1 +#endif + +#ifndef CMT_GPIO3 +#define CMT_GPIO3 -1 +#endif + +#ifndef CMT_SDIO +#define CMT_SDIO -1 +#endif + PinMappingClass PinMapping; PinMappingClass::PinMappingClass() @@ -50,6 +70,12 @@ PinMappingClass::PinMappingClass() _pinMapping.nrf24_miso = HOYMILES_PIN_MISO; _pinMapping.nrf24_mosi = HOYMILES_PIN_MOSI; + _pinMapping.cmt_clk = CMT_CLK; + _pinMapping.cmt_cs = CMT_CS; + _pinMapping.cmt_fcs = CMT_FCS; + _pinMapping.cmt_gpio3 = CMT_GPIO3; + _pinMapping.cmt_sdio = CMT_SDIO; + #ifdef OPENDTU_ETHERNET _pinMapping.eth_enabled = true; #else @@ -104,6 +130,12 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.nrf24_miso = doc[i]["nrf24"]["miso"] | HOYMILES_PIN_MISO; _pinMapping.nrf24_mosi = doc[i]["nrf24"]["mosi"] | HOYMILES_PIN_MOSI; + _pinMapping.cmt_clk = doc[i]["cmt"]["clk"] | CMT_CLK; + _pinMapping.cmt_cs = doc[i]["cmt"]["cs"] | CMT_CS; + _pinMapping.cmt_fcs = doc[i]["cmt"]["fcs"] | CMT_FCS; + _pinMapping.cmt_gpio3 = doc[i]["cmt"]["gpio3"] | CMT_GPIO3; + _pinMapping.cmt_sdio = doc[i]["cmt"]["sdio"] | CMT_SDIO; + #ifdef OPENDTU_ETHERNET _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true; #else @@ -143,6 +175,15 @@ bool PinMappingClass::isValidNrf24Config() && _pinMapping.nrf24_mosi >= 0; } +bool PinMappingClass::isValidCmt2300Config() +{ + return _pinMapping.cmt_clk >= 0 + && _pinMapping.cmt_cs >= 0 + && _pinMapping.cmt_fcs >= 0 + && _pinMapping.cmt_gpio3 >= 0 + && _pinMapping.cmt_sdio >= 0; +} + bool PinMappingClass::isValidEthConfig() { return _pinMapping.eth_enabled; From ef614751b1c9e4b44eca65aaf12142a035d70505 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 8 Mar 2023 22:46:05 +0100 Subject: [PATCH 14/66] webapp: Show CMT pins in device manager --- src/WebApi_device.cpp | 7 +++++++ webapp/src/types/PinMapping.ts | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 53f79d38..e1494554 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -47,6 +47,13 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) nrfPinObj["miso"] = pin.nrf24_miso; nrfPinObj["mosi"] = pin.nrf24_mosi; + JsonObject cmtPinObj = curPin.createNestedObject("cmt"); + cmtPinObj["clk"] = pin.cmt_clk; + cmtPinObj["cs"] = pin.cmt_cs; + cmtPinObj["fcs"] = pin.cmt_fcs; + cmtPinObj["sdio"] = pin.cmt_sdio; + cmtPinObj["gpio3"] = pin.cmt_gpio3; + JsonObject ethPinObj = curPin.createNestedObject("eth"); ethPinObj["enabled"] = pin.eth_enabled; ethPinObj["phy_addr"] = pin.eth_phy_addr; diff --git a/webapp/src/types/PinMapping.ts b/webapp/src/types/PinMapping.ts index 0445c1f8..1170872e 100644 --- a/webapp/src/types/PinMapping.ts +++ b/webapp/src/types/PinMapping.ts @@ -7,6 +7,14 @@ export interface Nrf24 { cs: number; } +export interface Cmt2300 { + clk: number; + cs: number; + fcs: number; + sdio: number; + gpio3: number; + } + export interface Ethernet { enabled: boolean; phy_addr: number; @@ -28,6 +36,7 @@ export interface Display { export interface Device { name: string; nrf24: Nrf24; + cmt: Cmt2300; eth: Ethernet; display: Display; } From dc91929d6e2e7686041ec3bc4426f1d0a633c30d Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 8 Mar 2023 22:59:14 +0100 Subject: [PATCH 15/66] First rough implementation of HMT inverters --- include/Configuration.h | 2 +- lib/Hoymiles/src/Hoymiles.cpp | 5 +- lib/Hoymiles/src/inverters/HMT_6CH.cpp | 25 +++++++ lib/Hoymiles/src/inverters/HMT_6CH.h | 81 ++++++++++++++++++++++ lib/Hoymiles/src/parser/StatisticsParser.h | 6 +- 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 lib/Hoymiles/src/inverters/HMT_6CH.cpp create mode 100644 lib/Hoymiles/src/inverters/HMT_6CH.h diff --git a/include/Configuration.h b/include/Configuration.h index 8f0a645d..49545466 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -23,7 +23,7 @@ #define INV_MAX_NAME_STRLEN 31 #define INV_MAX_COUNT 10 -#define INV_MAX_CHAN_COUNT 4 +#define INV_MAX_CHAN_COUNT 6 #define CHAN_MAX_NAME_STRLEN 31 diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 59876049..91817003 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -6,6 +6,7 @@ #include "inverters/HMS_1CH.h" #include "inverters/HMS_2CH.h" #include "inverters/HMS_4CH.h" +#include "inverters/HMT_6CH.h" #include "inverters/HM_1CH.h" #include "inverters/HM_2CH.h" #include "inverters/HM_4CH.h" @@ -109,7 +110,9 @@ void HoymilesClass::loop() std::shared_ptr HoymilesClass::addInverter(const char* name, uint64_t serial) { std::shared_ptr i = nullptr; - if (HMS_4CH::isValidSerial(serial)) { + if (HMT_6CH::isValidSerial(serial)) { + i = std::make_shared(_radioCmt.get(), serial); + } else if (HMS_4CH::isValidSerial(serial)) { i = std::make_shared(_radioCmt.get(), serial); } else if (HMS_2CH::isValidSerial(serial)) { i = std::make_shared(_radioCmt.get(), serial); diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.cpp b/lib/Hoymiles/src/inverters/HMT_6CH.cpp new file mode 100644 index 00000000..48cdb75c --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMT_6CH.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "HMT_6CH.h" + +HMT_6CH::HMT_6CH(HoymilesRadio* radio, uint64_t serial) + : HMS_Abstract(radio, serial) {}; + +bool HMT_6CH::isValidSerial(uint64_t serial) +{ + // serial >= 0x138200000000 && serial <= 0x138299999999 + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x1382; +} + +String HMT_6CH::typeName() +{ + return F("HMT-1800, HMT-2250"); +} + +const std::list* HMT_6CH::getByteAssignment() +{ + return &byteAssignment; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.h b/lib/Hoymiles/src/inverters/HMT_6CH.h new file mode 100644 index 00000000..7d19724f --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMT_6CH.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HMS_Abstract.h" + +class HMT_6CH : public HMS_Abstract { +public: + explicit HMT_6CH(HoymilesRadio* radio, uint64_t serial); + static bool isValidSerial(uint64_t serial); + String typeName(); + const std::list* getByteAssignment(); + +private: + const std::list byteAssignment = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + + { TYPE_DC, CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { TYPE_DC, CH1, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, + { TYPE_DC, CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + + { TYPE_DC, CH2, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, + { TYPE_DC, CH2, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, + { TYPE_DC, CH2, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, + { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, + { TYPE_DC, CH2, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + + { TYPE_DC, CH3, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, + { TYPE_DC, CH3, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, + { TYPE_DC, CH3, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, + { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, + { TYPE_DC, CH3, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + + { TYPE_DC, CH4, FLD_UDC, UNIT_V, 46, 2, 10, false, 1 }, + { TYPE_DC, CH4, FLD_IDC, UNIT_A, 48, 2, 100, false, 2 }, + { TYPE_DC, CH4, FLD_PDC, UNIT_W, 52, 2, 10, false, 1 }, + { TYPE_DC, CH4, FLD_YT, UNIT_KWH, 56, 4, 1000, false, 3 }, + { TYPE_DC, CH4, FLD_YD, UNIT_WH, 64, 2, 1, false, 0 }, + { TYPE_DC, CH4, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH4, CMD_CALC, false, 3 }, + + { TYPE_DC, CH5, FLD_UDC, UNIT_V, 46, 2, 10, false, 1 }, + { TYPE_DC, CH5, FLD_IDC, UNIT_A, 50, 2, 100, false, 2 }, + { TYPE_DC, CH5, FLD_PDC, UNIT_W, 54, 2, 10, false, 1 }, + { TYPE_DC, CH5, FLD_YT, UNIT_KWH, 60, 4, 1000, false, 3 }, + { TYPE_DC, CH5, FLD_YD, UNIT_WH, 66, 2, 1, false, 0 }, + { TYPE_DC, CH5, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH5, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 68, 2, 10, false, 1 }, // dummy + //{ TYPE_AC, CH0, FLD_UAC_1N, UNIT_V, 68, 2, 10, false, 1 }, + //{ TYPE_AC, CH0, FLD_UAC_2N, UNIT_V, 70, 2, 10, false, 1 }, + //{ TYPE_AC, CH0, FLD_UAC_3N, UNIT_V, 72, 2, 10, false, 1 }, + //{ TYPE_AC, CH0, FLD_UAC_12, UNIT_V, 74, 2, 10, false, 1 }, + //{ TYPE_AC, CH0, FLD_UAC_23, UNIT_V, 76, 2, 10, false, 1 }, + //{ TYPE_AC, CH0, FLD_UAC_31, UNIT_V, 78, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 80, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 82, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_PRA, UNIT_VA, 84, 2, 10, true, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 86, 2, 100, false, 2 }, // dummy + //{ TYPE_AC, CH0, FLD_IAC_1, UNIT_A, 86, 2, 100, false, 2 }, + //{ TYPE_AC, CH0, FLD_IAC_2, UNIT_A, 88, 2, 100, false, 2 }, + //{ TYPE_AC, CH0, FLD_IAC_3, UNIT_A, 90, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 92, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 94, 2, 10, true, 1 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 96, 2, 1, false, 0 }, + + { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + }; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index c16c35db..f4493f10 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -60,6 +60,8 @@ enum ChannelNum_t { CH1, CH2, CH3, + CH4, + CH5, CH_CNT }; @@ -72,7 +74,7 @@ const char* const channelsTypes[] = { "AC", "DC", "INV" }; typedef struct { ChannelType_t type; - ChannelNum_t ch; // channel 0 - 4 + ChannelNum_t ch; // channel 0 - 5 FieldId_t fieldId; // field id UnitId_t unitId; // uint id uint8_t start; // pos of first byte in buffer @@ -84,7 +86,7 @@ typedef struct { typedef struct { ChannelType_t type; - ChannelNum_t ch; // channel 0 - 4 + ChannelNum_t ch; // channel 0 - 5 FieldId_t fieldId; // field id float offset; // offset (positive/negative) to be applied on the fetched value } fieldSettings_t; From b7fb294368d7a17a69dd3fe258c99ec452d6bac5 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 8 Mar 2023 23:03:35 +0100 Subject: [PATCH 16/66] Set DTU serial for CMT modules --- lib/Hoymiles/src/Hoymiles.cpp | 5 +++++ lib/Hoymiles/src/Hoymiles.h | 1 + src/InverterSettings.cpp | 1 + src/WebApi_dtu.cpp | 1 + 4 files changed, 8 insertions(+) diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 91817003..9504a503 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -200,6 +200,11 @@ HoymilesRadio_NRF* HoymilesClass::getRadioNrf() return _radioNrf.get(); } +HoymilesRadio_CMT* HoymilesClass::getRadioCmt() +{ + return _radioCmt.get(); +} + uint32_t HoymilesClass::PollInterval() { return _pollInterval; diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index 0d2d1ec5..663fbf11 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -31,6 +31,7 @@ public: size_t getNumInverters(); HoymilesRadio_NRF* getRadioNrf(); + HoymilesRadio_CMT* getRadioCmt(); uint32_t PollInterval(); void setPollInterval(uint32_t interval); diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 92c5eadb..7be4dbfe 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -41,6 +41,7 @@ void InverterSettingsClass::init() MessageOutput.println(" Setting DTU serial... "); Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); + Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu_Serial); MessageOutput.println(" Setting poll interval... "); Hoymiles.setPollInterval(config.Dtu_PollInterval); diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 81311772..c0ad977e 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -134,5 +134,6 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); + Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu_Serial); Hoymiles.setPollInterval(config.Dtu_PollInterval); } \ No newline at end of file From de2b7ab2d28200d960216e8b1909f4d43132ff53 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 8 Mar 2023 23:08:28 +0100 Subject: [PATCH 17/66] Check that all RF modules as in idle mode before sending mqtt packages --- lib/Hoymiles/src/Hoymiles.cpp | 5 +++++ lib/Hoymiles/src/Hoymiles.h | 2 ++ src/MqttHandleDtu.cpp | 2 +- src/MqttHandleHass.cpp | 2 +- src/MqttHandleInverter.cpp | 2 +- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 9504a503..c4641ecc 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -205,6 +205,11 @@ HoymilesRadio_CMT* HoymilesClass::getRadioCmt() return _radioCmt.get(); } +bool HoymilesClass::isAllRadioIdle() +{ + return _radioNrf.get()->isIdle() && _radioCmt.get()->isIdle(); +} + uint32_t HoymilesClass::PollInterval() { return _pollInterval; diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index 663fbf11..78631836 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -36,6 +36,8 @@ public: uint32_t PollInterval(); void setPollInterval(uint32_t interval); + bool isAllRadioIdle(); + private: std::vector> _inverters; std::unique_ptr _radioNrf; diff --git a/src/MqttHandleDtu.cpp b/src/MqttHandleDtu.cpp index f9dfcc50..dfca2340 100644 --- a/src/MqttHandleDtu.cpp +++ b/src/MqttHandleDtu.cpp @@ -16,7 +16,7 @@ void MqttHandleDtuClass::init() void MqttHandleDtuClass::loop() { - if (!MqttSettings.getConnected() || !Hoymiles.getRadioNrf()->isIdle()) { + if (!MqttSettings.getConnected() || !Hoymiles.isAllRadioIdle()) { return; } diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 3f72feb1..3f2c2143 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -41,7 +41,7 @@ void MqttHandleHassClass::publishConfig() return; } - if (!MqttSettings.getConnected() && Hoymiles.getRadioNrf()->isIdle()) { + if (!MqttSettings.getConnected() && Hoymiles.isAllRadioIdle()) { return; } diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index af18ac57..ed620dc8 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -36,7 +36,7 @@ void MqttHandleInverterClass::init() void MqttHandleInverterClass::loop() { - if (!MqttSettings.getConnected() || !Hoymiles.getRadioNrf()->isIdle()) { + if (!MqttSettings.getConnected() || !Hoymiles.isAllRadioIdle()) { return; } From 83c623708f246475fb0a2c8b0555dd53ea49b83e Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 9 Mar 2023 18:57:22 +0100 Subject: [PATCH 18/66] Fix crash if radio settings where changed while the radio was not initialized --- lib/Hoymiles/src/Hoymiles.cpp | 9 ++------- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 6 ++++++ lib/Hoymiles/src/HoymilesRadio_NRF.cpp | 19 +++++++++++++++++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index c4641ecc..68ce7970 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -40,13 +40,8 @@ void HoymilesClass::initCMT(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8 void HoymilesClass::loop() { HOY_SEMAPHORE_TAKE(); - if (_radioNrf->isInitialized()) { - _radioNrf->loop(); - } - - if (_radioCmt->isInitialized()) { - _radioCmt->loop(); - } + _radioNrf->loop(); + _radioCmt->loop(); if (getNumInverters() > 0) { if (millis() - _lastPoll > (_pollInterval * 1000)) { diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 8db734de..e7b245cf 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -357,6 +357,9 @@ void HoymilesRadio_CMT::init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int void HoymilesRadio_CMT::loop() { + if (!_isInitialized) { + return; + } enumCMTresult mCMTstate = cmtProcess(); if (mCMTstate != CMT_RX_DONE) { // Perform package parsing only if no packages are received @@ -450,6 +453,9 @@ void HoymilesRadio_CMT::loop() bool HoymilesRadio_CMT::isConnected() { + if (!_isInitialized) { + return false; + } return _ChipConnected; } diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp index 088acbeb..03a1d6cd 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -38,6 +38,10 @@ void HoymilesRadio_NRF::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t void HoymilesRadio_NRF::loop() { + if (!_isInitialized) { + return; + } + EVERY_N_MILLIS(4) { switchRxCh(); @@ -151,22 +155,35 @@ void HoymilesRadio_NRF::loop() void HoymilesRadio_NRF::setPALevel(rf24_pa_dbm_e paLevel) { + if (!_isInitialized) { + return; + } _radio->setPALevel(paLevel); } void HoymilesRadio_NRF::setDtuSerial(uint64_t serial) { HoymilesRadio::setDtuSerial(serial); + + if (!_isInitialized) { + return; + } openReadingPipe(); } bool HoymilesRadio_NRF::isConnected() { + if (!_isInitialized) { + return false; + } return _radio->isChipConnected(); } bool HoymilesRadio_NRF::isPVariant() { + if (!_isInitialized) { + return false; + } return _radio->isPVariant(); } @@ -210,8 +227,6 @@ void HoymilesRadio_NRF::switchRxCh() _radio->startListening(); } - - void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract* cmd) { cmd->incrementSendCount(); From 46036eb958d9786c3921d9a16a97f13bca161178 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 9 Mar 2023 21:51:31 +0100 Subject: [PATCH 19/66] Simplify dumpBuf method in HoymilesRadio --- lib/Hoymiles/src/HoymilesRadio.cpp | 10 ++++------ lib/Hoymiles/src/HoymilesRadio.h | 2 +- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 4 ++-- lib/Hoymiles/src/HoymilesRadio_NRF.cpp | 5 ++--- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 3ceb408c..117d45e9 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -51,16 +51,14 @@ void HoymilesRadio::sendLastPacketAgain() sendEsbPacket(cmd); } -void HoymilesRadio::dumpBuf(const char* info, uint8_t buf[], uint8_t len) +void HoymilesRadio::dumpBuf(const uint8_t buf[], uint8_t len, bool appendNewline) { - - if (NULL != info) - Hoymiles.getMessageOutput()->print(String(info)); - for (uint8_t i = 0; i < len; i++) { Hoymiles.getMessageOutput()->printf("%02X ", buf[i]); } - Hoymiles.getMessageOutput()->println(""); + if (appendNewline) { + Hoymiles.getMessageOutput()->println(""); + } } bool HoymilesRadio::isInitialized() diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 9e3aad31..056b61c3 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -23,7 +23,7 @@ public: protected: static serial_u convertSerialToRadioId(serial_u serial); - void dumpBuf(const char* info, uint8_t buf[], uint8_t len); + void dumpBuf(const uint8_t buf[], uint8_t len, bool appendNewline = true); bool checkFragmentCrc(fragment_t* fragment); virtual void sendEsbPacket(CommandAbstract* cmd) = 0; diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index e7b245cf..b6c055b3 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -112,7 +112,7 @@ bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const cmtTxBuffer[14] = crc8(cmtTxBuffer, 14); Hoymiles.getMessageOutput()->printf("TX CMD56 %s --> ", cmtChToFreq(cmtCurrentCh).c_str()); - dumpBuf("", cmtTxBuffer, 15); + dumpBuf(cmtTxBuffer, 15); cmtTxLength = 15; cmtTxTimeout = 100; @@ -371,7 +371,7 @@ void HoymilesRadio_CMT::loop() if (nullptr != inv) { // Save packet in inverter rx buffer Hoymiles.getMessageOutput()->printf("RX %s --> ", cmtChToFreq(f.channel).c_str()); - dumpBuf("", f.fragment, f.len); + dumpBuf(f.fragment, f.len, false); Hoymiles.getMessageOutput()->printf("| %d dBm", f.rssi); inv->addRxFragment(f.fragment, f.len); diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp index 03a1d6cd..d41085f5 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -75,9 +75,8 @@ void HoymilesRadio_NRF::loop() if (nullptr != inv) { // Save packet in inverter rx buffer - char buf[30]; - snprintf(buf, sizeof(buf), "RX Channel: %d --> ", f.channel); - dumpBuf(buf, f.fragment, f.len); + Hoymiles.getMessageOutput()->printf("RX Channel: %d --> ", f.channel); + dumpBuf(f.fragment, f.len); inv->addRxFragment(f.fragment, f.len); } else { Hoymiles.getMessageOutput()->println("Inverter Not found!"); From c19d2007bdabfc5b7304bbbce1a386c1687b589e Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 9 Mar 2023 22:22:44 +0100 Subject: [PATCH 20/66] webapp: Added cmt radio status to system overview --- src/WebApi_sysstatus.cpp | 8 +++-- webapp/src/components/RadioInfo.vue | 50 ++++++++++++++++++++++++----- webapp/src/locales/de.json | 7 ++-- webapp/src/locales/en.json | 7 ++-- webapp/src/locales/fr.json | 7 ++-- webapp/src/types/SystemStatus.ts | 7 ++-- 6 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index c392d6cb..cca3c296 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -69,8 +69,12 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["uptime"] = esp_timer_get_time() / 1000000; - root["radio_connected"] = Hoymiles.getRadioNrf()->isConnected(); - root["radio_pvariant"] = Hoymiles.getRadioNrf()->isPVariant(); + root["nrf_configured"] = Hoymiles.getRadioNrf()->isInitialized(); + root["nrf_connected"] = Hoymiles.getRadioNrf()->isConnected(); + root["nrf_pvariant"] = Hoymiles.getRadioNrf()->isPVariant(); + + root["cmt_configured"] = Hoymiles.getRadioCmt()->isInitialized(); + root["cmt_connected"] = Hoymiles.getRadioCmt()->isConnected(); response->setLength(); request->send(response); diff --git a/webapp/src/components/RadioInfo.vue b/webapp/src/components/RadioInfo.vue index 8f16a212..a59cee45 100644 --- a/webapp/src/components/RadioInfo.vue +++ b/webapp/src/components/RadioInfo.vue @@ -4,27 +4,61 @@ - + - + + + + + + + + + + + + +
{{ $t('radioinfo.ChipStatus') }}{{ $t('radioinfo.Status', { module: "nRF24" }) }} - +
{{ $t('radioinfo.ChipType') }}{{ $t('radioinfo.ChipStatus', { module: "nRF24" }) }} + v-if="systemStatus.nrf_configured && systemStatus.nrf_connected">{{ $t('radioinfo.Connected') }} + v-else-if="systemStatus.nrf_configured && !systemStatus.nrf_connected">{{ $t('radioinfo.NotConnected') }} + +
{{ $t('radioinfo.ChipType', { module: "nRF24" }) }} + + +
{{ $t('radioinfo.Status', { module: "CMT2300a" }) }} + +
{{ $t('radioinfo.ChipStatus', { module: "CMT2300a" }) }} + + + + +
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 699f7747..b322e736 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -187,10 +187,13 @@ }, "radioinfo": { "RadioInformation": "Funkmodulinformationen", - "ChipStatus": "Chip-Status", - "ChipType": "Chip-Typ", + "Status": "{module} Status", + "ChipStatus": "{module} Chip-Status", + "ChipType": "{module} Chip-Type", "Connected": "verbunden", "NotConnected": "nicht verbunden", + "Configured": "konfiguriert", + "NotConfigured": "nicht konfiguriert", "Unknown": "unbekannt" }, "networkinfo": { diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 11748f0f..f930ae13 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -187,10 +187,13 @@ }, "radioinfo": { "RadioInformation": "Radio Information", - "ChipStatus": "Chip Status", - "ChipType": "Chip Type", + "Status": "{module} Status", + "ChipStatus": "{module} Chip Status", + "ChipType": "{module} Chip Type", "Connected": "connected", "NotConnected": "not connected", + "Configured": "configured", + "NotConfigured": "not configured", "Unknown": "Unknown" }, "networkinfo": { diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index d45ecaae..5f86d1fd 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -187,10 +187,13 @@ }, "radioinfo": { "RadioInformation": "Informations sur la radio", - "ChipStatus": "État de la puce", - "ChipType": "Type de puce", + "Status": "{module} Status", + "ChipStatus": "{module} sÉtat de la puce", + "ChipType": "{module} Type de puce", "Connected": "connectée", "NotConnected": "non connectée", + "Configured": "configured", + "NotConfigured": "not configured", "Unknown": "Inconnue" }, "networkinfo": { diff --git a/webapp/src/types/SystemStatus.ts b/webapp/src/types/SystemStatus.ts index 6be634cd..80b86c0d 100644 --- a/webapp/src/types/SystemStatus.ts +++ b/webapp/src/types/SystemStatus.ts @@ -25,6 +25,9 @@ export interface SystemStatus { sketch_total: number; sketch_used: number; // RadioInfo - radio_connected: boolean; - radio_pvariant: boolean; + nrf_configured: boolean; + nrf_connected: boolean; + nrf_pvariant: boolean; + cmt_configured: boolean; + cmt_connected: boolean; } \ No newline at end of file From 0ec90e00009793860605fb5bf2b06de34c08db49 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 9 Mar 2023 23:19:37 +0100 Subject: [PATCH 21/66] webapp: Adjusted radio problem hint in home view to detect problems of nrf and cmt radios --- src/WebApi_ws_live.cpp | 4 +++- webapp/src/locales/de.json | 2 +- webapp/src/locales/en.json | 2 +- webapp/src/locales/fr.json | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 1b4b4ec7..9d8c22d5 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -175,7 +175,9 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) JsonObject hintObj = root.createNestedObject("hints"); struct tm timeinfo; hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); - hintObj["radio_problem"] = (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant()); + hintObj["radio_problem"] = + (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || + (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); if (!strcmp(Configuration.get().Security_Password, ACCESS_POINT_PASSWORD)) { hintObj["default_password"] = true; } else { diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index b322e736..f84caf1d 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -500,7 +500,7 @@ "DiscussionBody": "Diskutieren Sie mit uns auf Discord oder Github" }, "hints": { - "RadioProblem": "Es konnte keine Verbindung zu einem NRF24L01+ Funkmodul hergestellt werden. Bitte überprüfen Sie die Verdrahtung.", + "RadioProblem": "Es konnte keine Verbindung zu einem der konfigurierten Funkmodule hergestellt werden. Bitte überprüfen Sie die Verdrahtung.", "TimeSync": "Die Uhr wurde noch nicht synchronisiert. Ohne eine korrekt eingestellte Uhr werden keine Anfragen an den Wechselrichter gesendet. Dies ist kurz nach dem Start normal. Nach einer längeren Laufzeit (>1 Minute) bedeutet es jedoch, dass der NTP-Server nicht erreichbar ist.", "TimeSyncLink": "Bitte überprüfen Sie Ihre Zeiteinstellungen.", "DefaultPassword": "Sie verwenden das Standardpasswort für die Weboberfläche und den Notfall Access Point. Dies ist potenziell unsicher.", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index f930ae13..bb398bcb 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -500,7 +500,7 @@ "DiscussionBody": "Discuss with us on Discord or Github" }, "hints": { - "RadioProblem": "Could not connect to a correct NRF24L01+ radio module. Please check the wiring.", + "RadioProblem": "Could not connect to a configured radio module. Please check the wiring.", "TimeSync": "The clock has not yet been synchronised. Without a correctly set clock, no requests are made to the inverter. This is normal shortly after the start. However, after a longer runtime (>1 minute), it indicates that the NTP server is not accessible.", "TimeSyncLink": "Please check your time settings.", "DefaultPassword": "You are using the default password for the web interface and the emergency access point. This is potentially insecure.", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 5f86d1fd..135d18bb 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -500,7 +500,7 @@ "DiscussionBody": "Discutez avec nous sur Discord ou sur Github" }, "hints": { - "RadioProblem": "Impossible de se connecter à un module radio NRF24L01+ correct. Veuillez vérifier le câblage.", + "RadioProblem": "Impossible de se connecter à un module radio configuré.. Veuillez vérifier le câblage.", "TimeSync": "L'horloge n'a pas encore été synchronisée. Sans une horloge correctement réglée, aucune demande n'est adressée à l'onduleur. Ceci est normal peu de temps après le démarrage. Cependant, après un temps de fonctionnement plus long (>1 minute), cela indique que le serveur NTP n'est pas accessible.", "TimeSyncLink": "Veuillez vérifier vos paramètres horaires.", "DefaultPassword": "Vous utilisez le mot de passe par défaut pour l'interface Web et le point d'accès d'urgence. Ceci est potentiellement non sécurisé.", From 3e1b778565f86da1812b25e53d77875522b587b1 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 16 Mar 2023 21:14:04 +0100 Subject: [PATCH 22/66] Change max power limit from 1500W to 2250W to support HMS/HMT inverters --- src/MqttHandleHass.cpp | 4 ++-- src/WebApi_limit.cpp | 6 +++--- webapp/src/views/HomeView.vue | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 3f2c2143..a2f38f0e 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -58,8 +58,8 @@ void MqttHandleHassClass::publishConfig() publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%"); publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%"); - publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 10, 1500); - publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 10, 1500); + publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 10, 2250); + publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 10, 2250); publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0"); publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0"); diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index e20c49a2..9470e4ca 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -112,10 +112,10 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - if (root["limit_value"].as() == 0 || root["limit_value"].as() > 1500) { - retMsg["message"] = "Limit must between 1 and 1500!"; + if (root["limit_value"].as() == 0 || root["limit_value"].as() > 2250) { + retMsg["message"] = "Limit must between 1 and 2250!"; retMsg["code"] = WebApiError::LimitInvalidLimit; - retMsg["param"]["max"] = 1500; + retMsg["param"]["max"] = 2250; response->setLength(); request->send(response); return; diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index ec583ef9..1d55a158 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -612,7 +612,7 @@ export default defineComponent({ } else { this.targetLimitTypeText = this.$t('home.Absolute'); this.targetLimitMin = 10; - this.targetLimitMax = (this.currentLimitList.max_power > 0 ? this.currentLimitList.max_power : 1500); + this.targetLimitMax = (this.currentLimitList.max_power > 0 ? this.currentLimitList.max_power : 2250); } this.targetLimitType = type; }, From 06cc19fc7075417154d0dbfd82345671bb121dc5 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 17 Mar 2023 18:09:28 +0100 Subject: [PATCH 23/66] Use TimeoutHelper for TX timeout --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 8 ++++---- lib/Hoymiles/src/HoymilesRadio_CMT.h | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index b6c055b3..5d4efc10 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -115,7 +115,7 @@ bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const dumpBuf(cmtTxBuffer, 15); cmtTxLength = 15; - cmtTxTimeout = 100; + _txTimeout.set(100); cmtNextState = CMT_STATE_TX_START; @@ -250,7 +250,7 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) cmtNextState = CMT_STATE_TX_WAIT; } - cmtTxTimeCount = CMT2300A_GetTickCount(); + _txTimeout.reset(); break; @@ -260,7 +260,7 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) cmtNextState = CMT_STATE_TX_DONE; } - if ((CMT2300A_GetTickCount() - cmtTxTimeCount) > cmtTxTimeout) { + if (_txTimeout.occured()) { cmtNextState = CMT_STATE_TX_TIMEOUT; } @@ -476,7 +476,7 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) memcpy(cmtTxBuffer, cmd->getDataPayload(), cmd->getDataSize()); cmtTxLength = cmd->getDataSize(); - cmtTxTimeout = 100; + _txTimeout.set(100); cmtNextState = CMT_STATE_TX_START; diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index 91bec78a..77bbb2ab 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -53,6 +53,7 @@ private: std::queue _rxBuffer; TimeoutHelper _rxTimeout; + TimeoutHelper _txTimeout; bool _ChipConnected = false; @@ -69,9 +70,7 @@ private: uint8_t cmtTxLength = 0; uint32_t cmtRxTimeout = 200; - uint32_t cmtTxTimeout = 200; uint32_t cmtRxTimeCount = 0; - uint32_t cmtTxTimeCount = 0; uint8_t cmtBaseChOff860; // offset from initalized CMT base frequency to Hoy base frequency in channels uint8_t cmtCurrentCh; // current used channel, should be stored per inverter und set before next Tx, if hopping is used From 67055276cae4899493b81dc68962e7bf4ec49b62 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sat, 18 Mar 2023 11:59:02 +0100 Subject: [PATCH 24/66] Implement different Eventlog messages for HMT inverters Also make message list much more readable --- lib/Hoymiles/src/inverters/HMT_6CH.cpp | 2 +- lib/Hoymiles/src/inverters/HMT_6CH.h | 4 +- lib/Hoymiles/src/inverters/HMT_Abstract.cpp | 12 + lib/Hoymiles/src/inverters/HMT_Abstract.h | 9 + lib/Hoymiles/src/parser/AlarmLogParser.cpp | 307 ++++++-------------- lib/Hoymiles/src/parser/AlarmLogParser.h | 20 +- 6 files changed, 138 insertions(+), 216 deletions(-) create mode 100644 lib/Hoymiles/src/inverters/HMT_Abstract.cpp create mode 100644 lib/Hoymiles/src/inverters/HMT_Abstract.h diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.cpp b/lib/Hoymiles/src/inverters/HMT_6CH.cpp index 48cdb75c..d50963b9 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_6CH.cpp @@ -5,7 +5,7 @@ #include "HMT_6CH.h" HMT_6CH::HMT_6CH(HoymilesRadio* radio, uint64_t serial) - : HMS_Abstract(radio, serial) {}; + : HMT_Abstract(radio, serial) {}; bool HMT_6CH::isValidSerial(uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.h b/lib/Hoymiles/src/inverters/HMT_6CH.h index 7d19724f..8c26c493 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.h +++ b/lib/Hoymiles/src/inverters/HMT_6CH.h @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "HMS_Abstract.h" +#include "HMT_Abstract.h" -class HMT_6CH : public HMS_Abstract { +class HMT_6CH : public HMT_Abstract { public: explicit HMT_6CH(HoymilesRadio* radio, uint64_t serial); static bool isValidSerial(uint64_t serial); diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp new file mode 100644 index 00000000..d2fc7a4f --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "HMT_Abstract.h" +#include "parser/AlarmLogParser.h" + +HMT_Abstract::HMT_Abstract(HoymilesRadio* radio, uint64_t serial) + : HM_Abstract(radio, serial) +{ + EventLog()->setMessageType(AlarmMessageType_t::HMT); +}; diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.h b/lib/Hoymiles/src/inverters/HMT_Abstract.h new file mode 100644 index 00000000..17116a95 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_Abstract.h" + +class HMT_Abstract : public HM_Abstract { +public: + explicit HMT_Abstract(HoymilesRadio* radio, uint64_t serial); +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index 33075a04..d7f54252 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -1,11 +1,90 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2023 Thomas Basler and others */ #include "AlarmLogParser.h" #include "../Hoymiles.h" #include +const std::list AlarmLogParser::_alarmMessages = { + { AlarmMessageType_t::ALL, 1, "Inverter start" }, + { AlarmMessageType_t::ALL, 2, "DTU command failed" }, + { AlarmMessageType_t::ALL, 121, "Over temperature protection" }, + { AlarmMessageType_t::ALL, 124, "Shut down by remote control" }, + { AlarmMessageType_t::ALL, 125, "Grid configuration parameter error" }, + { AlarmMessageType_t::ALL, 126, "Software error code 126" }, + { AlarmMessageType_t::ALL, 127, "Firmware error" }, + { AlarmMessageType_t::ALL, 128, "Software error code 128" }, + { AlarmMessageType_t::ALL, 129, "Abnormal bias" }, + { AlarmMessageType_t::ALL, 130, "Offline" }, + { AlarmMessageType_t::ALL, 141, "Grid: Grid overvoltage" }, + { AlarmMessageType_t::ALL, 142, "Grid: 10 min value grid overvoltage" }, + { AlarmMessageType_t::ALL, 143, "Grid: Grid undervoltage" }, + { AlarmMessageType_t::ALL, 144, "Grid: Grid overfrequency" }, + { AlarmMessageType_t::ALL, 145, "Grid: Grid underfrequency" }, + { AlarmMessageType_t::ALL, 146, "Grid: Rapid grid frequency change rate" }, + { AlarmMessageType_t::ALL, 147, "Grid: Power grid outage" }, + { AlarmMessageType_t::ALL, 148, "Grid: Grid disconnection" }, + { AlarmMessageType_t::ALL, 149, "Grid: Island detected" }, + { AlarmMessageType_t::HMT, 171, "Grid: Abnormal phase difference between phase to phase" }, + { AlarmMessageType_t::ALL, 205, "MPPT-A: Input overvoltage" }, + { AlarmMessageType_t::ALL, 206, "MPPT-B: Input overvoltage" }, + { AlarmMessageType_t::ALL, 207, "MPPT-A: Input undervoltage" }, + { AlarmMessageType_t::ALL, 208, "MPPT-B: Input undervoltage" }, + { AlarmMessageType_t::ALL, 209, "PV-1: No input" }, + { AlarmMessageType_t::ALL, 210, "PV-2: No input" }, + { AlarmMessageType_t::ALL, 211, "PV-3: No input" }, + { AlarmMessageType_t::ALL, 212, "PV-4: No input" }, + { AlarmMessageType_t::ALL, 213, "MPPT-A: PV-1 & PV-2 abnormal wiring" }, + { AlarmMessageType_t::ALL, 214, "MPPT-B: PV-3 & PV-4 abnormal wiring" }, + { AlarmMessageType_t::ALL, 215, "PV-1: Input overvoltage" }, + { AlarmMessageType_t::HMT, 215, "MPPT-C: Input overvoltage" }, + { AlarmMessageType_t::ALL, 216, "PV-1: Input undervoltage" }, + { AlarmMessageType_t::HMT, 216, "MPPT-C: Input undervoltage" }, + { AlarmMessageType_t::ALL, 217, "PV-2: Input overvoltage" }, + { AlarmMessageType_t::HMT, 217, "PV-5: No input" }, + { AlarmMessageType_t::ALL, 218, "PV-2: Input undervoltage" }, + { AlarmMessageType_t::HMT, 218, "PV-6: No input" }, + { AlarmMessageType_t::ALL, 219, "PV-3: Input overvoltage" }, + { AlarmMessageType_t::HMT, 219, "MPPT-C: PV-5 & PV-6 abnormal wiring" }, + { AlarmMessageType_t::ALL, 220, "PV-3: Input undervoltage" }, + { AlarmMessageType_t::ALL, 221, "PV-4: Input overvoltage" }, + { AlarmMessageType_t::HMT, 221, "Abnormal wiring of grid neutral line" }, + { AlarmMessageType_t::ALL, 222, "PV-4: Input undervoltage" }, + { AlarmMessageType_t::ALL, 301, "Hardware error code 301" }, + { AlarmMessageType_t::ALL, 302, "Hardware error code 302" }, + { AlarmMessageType_t::ALL, 303, "Hardware error code 303" }, + { AlarmMessageType_t::ALL, 304, "Hardware error code 304" }, + { AlarmMessageType_t::ALL, 305, "Hardware error code 305" }, + { AlarmMessageType_t::ALL, 306, "Hardware error code 306" }, + { AlarmMessageType_t::ALL, 307, "Hardware error code 307" }, + { AlarmMessageType_t::ALL, 308, "Hardware error code 308" }, + { AlarmMessageType_t::ALL, 309, "Hardware error code 309" }, + { AlarmMessageType_t::ALL, 310, "Hardware error code 310" }, + { AlarmMessageType_t::ALL, 311, "Hardware error code 311" }, + { AlarmMessageType_t::ALL, 312, "Hardware error code 312" }, + { AlarmMessageType_t::ALL, 313, "Hardware error code 313" }, + { AlarmMessageType_t::ALL, 314, "Hardware error code 314" }, + { AlarmMessageType_t::ALL, 5041, "Error code-04 Port 1" }, + { AlarmMessageType_t::ALL, 5042, "Error code-04 Port 2" }, + { AlarmMessageType_t::ALL, 5043, "Error code-04 Port 3" }, + { AlarmMessageType_t::ALL, 5044, "Error code-04 Port 4" }, + { AlarmMessageType_t::ALL, 5051, "PV Input 1 Overvoltage/Undervoltage" }, + { AlarmMessageType_t::ALL, 5052, "PV Input 2 Overvoltage/Undervoltage" }, + { AlarmMessageType_t::ALL, 5053, "PV Input 3 Overvoltage/Undervoltage" }, + { AlarmMessageType_t::ALL, 5054, "PV Input 4 Overvoltage/Undervoltage" }, + { AlarmMessageType_t::ALL, 5060, "Abnormal bias" }, + { AlarmMessageType_t::ALL, 5070, "Over temperature protection" }, + { AlarmMessageType_t::ALL, 5080, "Grid Overvoltage/Undervoltage" }, + { AlarmMessageType_t::ALL, 5090, "Grid Overfrequency/Underfrequency" }, + { AlarmMessageType_t::ALL, 5100, "Island detected" }, + { AlarmMessageType_t::ALL, 5120, "EEPROM reading and writing error" }, + { AlarmMessageType_t::ALL, 5150, "10 min value grid overvoltage" }, + { AlarmMessageType_t::ALL, 5200, "Firmware error" }, + { AlarmMessageType_t::ALL, 8310, "Shut down" }, + { AlarmMessageType_t::ALL, 9000, "Microinverter is suspected of being stolen" }, +}; + void AlarmLogParser::clearBuffer() { memset(_payloadAlarmLog, 0, ALARM_LOG_PAYLOAD_SIZE); @@ -37,6 +116,11 @@ LastCommandSuccess AlarmLogParser::getLastAlarmRequestSuccess() return _lastAlarmRequestSuccess; } +void AlarmLogParser::setMessageType(AlarmMessageType_t type) +{ + _messageType = type; +} + void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry) { uint8_t entryStartOffset = 2 + entryId * ALARM_LOG_ENTRY_SIZE; @@ -62,217 +146,16 @@ void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry) entry->EndTime += (endTimeOffset + timezoneOffset); } - switch (entry->MessageId) { - case 1: - entry->Message = "Inverter start"; - break; - case 2: - entry->Message = "DTU command failed"; - break; - case 121: - entry->Message = "Over temperature protection"; - break; - case 124: - entry->Message = "Shut down by remote control"; - break; - case 125: - entry->Message = "Grid configuration parameter error"; - break; - case 126: - entry->Message = "Software error code 126"; - break; - case 127: - entry->Message = "Firmware error"; - break; - case 128: - entry->Message = "Software error code 128"; - break; - case 129: - entry->Message = "Abnormal bias"; - break; - case 130: - entry->Message = "Offline"; - break; - case 141: - entry->Message = "Grid: Grid overvoltage"; - break; - case 142: - entry->Message = "Grid: 10 min value grid overvoltage"; - break; - case 143: - entry->Message = "Grid: Grid undervoltage"; - break; - case 144: - entry->Message = "Grid: Grid overfrequency"; - break; - case 145: - entry->Message = "Grid: Grid underfrequency"; - break; - case 146: - entry->Message = "Grid: Rapid grid frequency change rate"; - break; - case 147: - entry->Message = "Grid: Power grid outage"; - break; - case 148: - entry->Message = "Grid: Grid disconnection"; - break; - case 149: - entry->Message = "Grid: Island detected"; - break; - case 205: - entry->Message = "MPPT-A: Input overvoltage"; - break; - case 206: - entry->Message = "MPPT-B: Input overvoltage"; - break; - case 207: - entry->Message = "MPPT-A: Input undervoltage"; - break; - case 208: - entry->Message = "MPPT-B: Input undervoltage"; - break; - case 209: - entry->Message = "PV-1: No input"; - break; - case 210: - entry->Message = "PV-2: No input"; - break; - case 211: - entry->Message = "PV-3: No input"; - break; - case 212: - entry->Message = "PV-4: No input"; - break; - case 213: - entry->Message = "MPPT-A: PV-1 & PV-2 abnormal wiring"; - break; - case 214: - entry->Message = "MPPT-B: PV-3 & PV-4 abnormal wiring"; - break; - case 215: - entry->Message = "PV-1: Input overvoltage"; - break; - case 216: - entry->Message = "PV-1: Input undervoltage"; - break; - case 217: - entry->Message = "PV-2: Input overvoltage"; - break; - case 218: - entry->Message = "PV-2: Input undervoltage"; - break; - case 219: - entry->Message = "PV-3: Input overvoltage"; - break; - case 220: - entry->Message = "PV-3: Input undervoltage"; - break; - case 221: - entry->Message = "PV-4: Input overvoltage"; - break; - case 222: - entry->Message = "PV-4: Input undervoltage"; - break; - case 301: - entry->Message = "Hardware error code 301"; - break; - case 302: - entry->Message = "Hardware error code 302"; - break; - case 303: - entry->Message = "Hardware error code 303"; - break; - case 304: - entry->Message = "Hardware error code 304"; - break; - case 305: - entry->Message = "Hardware error code 305"; - break; - case 306: - entry->Message = "Hardware error code 306"; - break; - case 307: - entry->Message = "Hardware error code 307"; - break; - case 308: - entry->Message = "Hardware error code 308"; - break; - case 309: - entry->Message = "Hardware error code 309"; - break; - case 310: - entry->Message = "Hardware error code 310"; - break; - case 311: - entry->Message = "Hardware error code 311"; - break; - case 312: - entry->Message = "Hardware error code 312"; - break; - case 313: - entry->Message = "Hardware error code 313"; - break; - case 314: - entry->Message = "Hardware error code 314"; - break; - case 5041: - entry->Message = "Error code-04 Port 1"; - break; - case 5042: - entry->Message = "Error code-04 Port 2"; - break; - case 5043: - entry->Message = "Error code-04 Port 3"; - break; - case 5044: - entry->Message = "Error code-04 Port 4"; - break; - case 5051: - entry->Message = "PV Input 1 Overvoltage/Undervoltage"; - break; - case 5052: - entry->Message = "PV Input 2 Overvoltage/Undervoltage"; - break; - case 5053: - entry->Message = "PV Input 3 Overvoltage/Undervoltage"; - break; - case 5054: - entry->Message = "PV Input 4 Overvoltage/Undervoltage"; - break; - case 5060: - entry->Message = "Abnormal bias"; - break; - case 5070: - entry->Message = "Over temperature protection"; - break; - case 5080: - entry->Message = "Grid Overvoltage/Undervoltage"; - break; - case 5090: - entry->Message = "Grid Overfrequency/Underfrequency"; - break; - case 5100: - entry->Message = "Island detected"; - break; - case 5120: - entry->Message = "EEPROM reading and writing error"; - break; - case 5150: - entry->Message = "10 min value grid overvoltage"; - break; - case 5200: - entry->Message = "Firmware error"; - break; - case 8310: - entry->Message = "Shut down"; - break; - case 9000: - entry->Message = "Microinverter is suspected of being stolen"; - break; - default: - entry->Message = "Unknown"; - break; + entry->Message = "Unknown"; + for (auto& msg : _alarmMessages) { + if (msg.MessageId == entry->MessageId) { + if (msg.InverterType == _messageType) { + entry->Message = msg.Message; + break; + } else if (msg.InverterType == AlarmMessageType_t::ALL) { + entry->Message = msg.Message; + } + } } } diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.h b/lib/Hoymiles/src/parser/AlarmLogParser.h index 4910abc0..5a9c9e2f 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.h +++ b/lib/Hoymiles/src/parser/AlarmLogParser.h @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" -#include #include +#include +#include #define ALARM_LOG_ENTRY_COUNT 15 #define ALARM_LOG_ENTRY_SIZE 12 @@ -15,6 +16,17 @@ struct AlarmLogEntry_t { time_t EndTime; }; +enum class AlarmMessageType_t { + ALL = 0, + HMT +}; + +typedef struct { + AlarmMessageType_t InverterType; + uint16_t MessageId; + String Message; +} AlarmMessage_t; + class AlarmLogParser : public Parser { public: void clearBuffer(); @@ -26,6 +38,8 @@ public: void setLastAlarmRequestSuccess(LastCommandSuccess status); LastCommandSuccess getLastAlarmRequestSuccess(); + void setMessageType(AlarmMessageType_t type); + private: static int getTimezoneOffset(); @@ -33,4 +47,8 @@ private: uint8_t _alarmLogLength; LastCommandSuccess _lastAlarmRequestSuccess = CMD_NOK; // Set to NOK to fetch at startup + + AlarmMessageType_t _messageType = AlarmMessageType_t::ALL; + + static const std::list _alarmMessages; }; \ No newline at end of file From defcc02204642512378d041be0b5fbdc2e3eca61 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sat, 18 Mar 2023 12:09:24 +0100 Subject: [PATCH 25/66] Set CMT to 13dBm and added parameter values in plain text --- lib/CMT2300a/cmt2300a_params.h | 93 ++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/lib/CMT2300a/cmt2300a_params.h b/lib/CMT2300a/cmt2300a_params.h index 39ae7b12..4e10f6a2 100644 --- a/lib/CMT2300a/cmt2300a_params.h +++ b/lib/CMT2300a/cmt2300a_params.h @@ -1,10 +1,97 @@ +/* +;--------------------------------------- +; CMT2300A Configuration File +; Generated by CMOSTEK RFPDK 1.46 +; 2023.03.17 23:16 +;--------------------------------------- +; Mode = Advanced +; Part Number = CMT2300A +; Frequency = 860.000 MHz +; Xtal Frequency = 26.0000 MHz +; Demodulation = GFSK +; AGC = On +; Data Rate = 20.0 kbps +; Deviation = 20.0 kHz +; Tx Xtal Tol. = 20 ppm +; Rx Xtal Tol. = 20 ppm +; TRx Matching Network Type = 20 dBm +; Tx Power = +13 dBm +; Gaussian BT = 0.5 +; Bandwidth = Auto-Select kHz +; CDR Type = Counting +; CDR DR Range = NA +; AFC = On +; AFC Method = Auto-Select +; Data Representation = 0:F-low 1:F-high +; Rx Duty-Cycle = Off +; Tx Duty-Cycle = Off +; Sleep Timer = Off +; Sleep Time = NA +; Rx Timer = Off +; Rx Time T1 = NA +; Rx Time T2 = NA +; Rx Exit State = STBY +; Tx Exit State = STBY +; SLP Mode = Disable +; RSSI Valid Source = PJD +; PJD Window = 8 Jumps +; LFOSC Calibration = On +; Xtal Stable Time = 155 us +; RSSI Compare TH = NA +; Data Mode = Packet +; Whitening = Disable +; Whiten Type = NA +; Whiten Seed Type = NA +; Whiten Seed = NA +; Manchester = Disable +; Manchester Type = NA +; FEC = Enable +; FEC Type = x^3+x^2+1 +; Tx Prefix Type = 0 +; Tx Packet Number = 1 +; Tx Packet Gap = 32 +; Packet Type = Variable Length +; Node-Length Position = First Node, then Length +; Payload Bit Order = Start from msb +; Preamble Rx Size = 2 +; Preamble Tx Size = 30 +; Preamble Value = 170 +; Preamble Unit = 8-bit +; Sync Size = 4-byte +; Sync Value = 1296587336 +; Sync Tolerance = None +; Sync Manchester = Disable +; Node ID Size = NA +; Node ID Value = NA +; Node ID Mode = None +; Node ID Err Mask = Disable +; Node ID Free = Disable +; Payload Length = 32 +; CRC Options = IBM-16 +; CRC Seed = 0 crc_seed +; CRC Range = Entire Payload +; CRC Swap = Start from MSB +; CRC Bit Invert = Normal +; CRC Bit Order = Start from bit 15 +; Dout Mute = Off +; Dout Adjust Mode = Disable +; Dout Adjust Percentage = NA +; Collision Detect = Off +; Collision Detect Offset = NA +; RSSI Detect Mode = At PREAM_OK +; RSSI Filter Setting = 32-tap +; RF Performance = High +; LBD Threshold = 2.4 V +; RSSI Offset = 0 +; RSSI Offset Sign = 0 +*/ #ifndef __CMT2300A_PARAMS_H #define __CMT2300A_PARAMS_H #include "cmt2300a_defs.h" #include -/* [CMT Bank] with RSSI offset of +- 0 (and 13dBm) */ +/* [CMT Bank] with RSSI offset of +- 0 (and Tx power double bit not set) */ static uint8_t g_cmt2300aCmtBank[CMT2300A_CMT_BANK_SIZE] = { 0x00, 0x66, @@ -118,8 +205,8 @@ static uint8_t g_cmt2300aTxBank[CMT2300A_TX_BANK_SIZE] = { 0x07, 0x50, 0x00, -0x42, -0x0C, +0x53, +0x09, 0x3F, 0x7F, }; From 035fdbc54aec22326da9fe73fceefb457285ccda Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 21 Mar 2023 20:55:28 +0100 Subject: [PATCH 26/66] Increase CMT SPI speed to 4 MHz --- lib/CMT2300a/cmt_spi3.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CMT2300a/cmt_spi3.c b/lib/CMT2300a/cmt_spi3.c index 464701f6..409782a1 100644 --- a/lib/CMT2300a/cmt_spi3.c +++ b/lib/CMT2300a/cmt_spi3.c @@ -3,7 +3,7 @@ #include #include // for esp_rom_gpio_connect_out_signal -#define CMT_SPI_CLK 1000000 // 1 MHz +#define CMT_SPI_CLK 4000000 // 4 MHz spi_device_handle_t spi_reg, spi_fifo; From 098691af9dcea43e40eb82ff624653acf2c82b88 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 22 Mar 2023 19:59:03 +0100 Subject: [PATCH 27/66] First step towards a modular CMT2300 driver similar to the NRF24 one --- lib/CMT2300a/cmt2300a_hal.c | 4 +- lib/CMT2300a/cmt2300a_hal.h | 2 +- lib/CMT2300a/cmt2300wrapper.cpp | 192 +++++++++++++++++++++++++ lib/CMT2300a/cmt2300wrapper.h | 42 ++++++ lib/CMT2300a/cmt_spi3.c | 10 +- lib/CMT2300a/cmt_spi3.h | 2 +- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 80 +++-------- lib/Hoymiles/src/HoymilesRadio_CMT.h | 7 +- 8 files changed, 268 insertions(+), 71 deletions(-) create mode 100644 lib/CMT2300a/cmt2300wrapper.cpp create mode 100644 lib/CMT2300a/cmt2300wrapper.h diff --git a/lib/CMT2300a/cmt2300a_hal.c b/lib/CMT2300a/cmt2300a_hal.c index 3f7111ca..7bf1b60f 100644 --- a/lib/CMT2300a/cmt2300a_hal.c +++ b/lib/CMT2300a/cmt2300a_hal.c @@ -26,9 +26,9 @@ * @name CMT2300A_InitSpi * @desc Initializes the CMT2300A SPI interface. * *********************************************************/ -void CMT2300A_InitSpi(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs) +void CMT2300A_InitSpi(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, uint32_t spi_speed) { - cmt_spi3_init(pin_sdio, pin_clk, pin_cs, pin_fcs); + cmt_spi3_init(pin_sdio, pin_clk, pin_cs, pin_fcs, spi_speed); } /*! ******************************************************** diff --git a/lib/CMT2300a/cmt2300a_hal.h b/lib/CMT2300a/cmt2300a_hal.h index 8a27251f..1d6e1f4f 100644 --- a/lib/CMT2300a/cmt2300a_hal.h +++ b/lib/CMT2300a/cmt2300a_hal.h @@ -36,7 +36,7 @@ extern "C" { #define CMT2300A_GetTickCount() millis() /* ************************************************************************ */ -void CMT2300A_InitSpi(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs); +void CMT2300A_InitSpi(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, uint32_t spi_speed); uint8_t CMT2300A_ReadReg(uint8_t addr); void CMT2300A_WriteReg(uint8_t addr, uint8_t dat); diff --git a/lib/CMT2300a/cmt2300wrapper.cpp b/lib/CMT2300a/cmt2300wrapper.cpp new file mode 100644 index 00000000..603d1b04 --- /dev/null +++ b/lib/CMT2300a/cmt2300wrapper.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "cmt2300wrapper.h" +#include "cmt2300a.h" +#include "cmt2300a_params.h" + +CMT2300a::CMT2300a(uint8_t pin_sdio, uint8_t pin_clk, uint8_t pin_cs, uint8_t pin_fcs, uint32_t spi_speed) +{ + _pin_sdio = pin_sdio; + _pin_clk = pin_clk; + _pin_cs = pin_cs; + _pin_fcs = pin_fcs; + _spi_speed = spi_speed; +} + +bool CMT2300a::begin(void) +{ + return _init_pins() && _init_radio(); +} + +bool CMT2300a::isChipConnected() +{ + return CMT2300A_IsExist(); +} + +bool CMT2300a::setPALevel(int8_t level) +{ + uint16_t Tx_dBm_word; + switch (level) { + // for TRx Matching Network Type: 20 dBm + case -10: + Tx_dBm_word = 0x0501; + break; + case -9: + Tx_dBm_word = 0x0601; + break; + case -8: + Tx_dBm_word = 0x0701; + break; + case -7: + Tx_dBm_word = 0x0801; + break; + case -6: + Tx_dBm_word = 0x0901; + break; + case -5: + Tx_dBm_word = 0x0A01; + break; + case -4: + Tx_dBm_word = 0x0B01; + break; + case -3: + Tx_dBm_word = 0x0C01; + break; + case -2: + Tx_dBm_word = 0x0D01; + break; + case -1: + Tx_dBm_word = 0x0E01; + break; + case 0: + Tx_dBm_word = 0x1002; + break; + case 1: + Tx_dBm_word = 0x1302; + break; + case 2: + Tx_dBm_word = 0x1602; + break; + case 3: + Tx_dBm_word = 0x1902; + break; + case 4: + Tx_dBm_word = 0x1C02; + break; + case 5: + Tx_dBm_word = 0x1F03; + break; + case 6: + Tx_dBm_word = 0x2403; + break; + case 7: + Tx_dBm_word = 0x2804; + break; + case 8: + Tx_dBm_word = 0x2D04; + break; + case 9: + Tx_dBm_word = 0x3305; + break; + case 10: + Tx_dBm_word = 0x3906; + break; + case 11: + Tx_dBm_word = 0x4107; + break; + case 12: + Tx_dBm_word = 0x4908; + break; + case 13: + Tx_dBm_word = 0x5309; + break; + case 14: + Tx_dBm_word = 0x5E0B; + break; + case 15: + Tx_dBm_word = 0x6C0C; + break; + case 16: + Tx_dBm_word = 0x7D0C; + break; + // the following values require the double bit: + case 17: + Tx_dBm_word = 0x4A0C; + break; + case 18: + Tx_dBm_word = 0x580F; + break; + case 19: + Tx_dBm_word = 0x6B12; + break; + case 20: + Tx_dBm_word = 0x8A18; + break; + default: + return false; + } + if (level > 16) { // set bit for double Tx value + CMT2300A_WriteReg(CMT2300A_CUS_CMT4, CMT2300A_ReadReg(CMT2300A_CUS_CMT4) | 0x01); // set bit0 + } else { + CMT2300A_WriteReg(CMT2300A_CUS_CMT4, CMT2300A_ReadReg(CMT2300A_CUS_CMT4) & 0xFE); // reset bit0 + } + CMT2300A_WriteReg(CMT2300A_CUS_TX8, Tx_dBm_word >> 8); + CMT2300A_WriteReg(CMT2300A_CUS_TX9, Tx_dBm_word & 0xFF); + + return true; +} + +bool CMT2300a::_init_pins() +{ + CMT2300A_InitSpi(_pin_sdio, _pin_clk, _pin_cs, _pin_fcs, _spi_speed); + + return true; // assuming pins are connected properly +} + +bool CMT2300a::_init_radio() +{ + if (!CMT2300A_Init()) { + return false; + } + + /* config registers */ + CMT2300A_ConfigRegBank(CMT2300A_CMT_BANK_ADDR, g_cmt2300aCmtBank, CMT2300A_CMT_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_SYSTEM_BANK_ADDR, g_cmt2300aSystemBank, CMT2300A_SYSTEM_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_FREQUENCY_BANK_ADDR, g_cmt2300aFrequencyBank, CMT2300A_FREQUENCY_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_DATA_RATE_BANK_ADDR, g_cmt2300aDataRateBank, CMT2300A_DATA_RATE_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_BASEBAND_BANK_ADDR, g_cmt2300aBasebandBank, CMT2300A_BASEBAND_BANK_SIZE); + CMT2300A_ConfigRegBank(CMT2300A_TX_BANK_ADDR, g_cmt2300aTxBank, CMT2300A_TX_BANK_SIZE); + + // xosc_aac_code[2:0] = 2 + uint8_t tmp; + tmp = (~0x07) & CMT2300A_ReadReg(CMT2300A_CUS_CMT10); + CMT2300A_WriteReg(CMT2300A_CUS_CMT10, tmp | 0x02); + + /* Config GPIOs */ + CMT2300A_ConfigGpio( + CMT2300A_GPIO3_SEL_INT2); + + /* Config interrupt */ + CMT2300A_ConfigInterrupt( + CMT2300A_INT_SEL_TX_DONE, /* Config INT1 */ + CMT2300A_INT_SEL_PKT_OK /* Config INT2 */ + ); + + /* Enable interrupt */ + CMT2300A_EnableInterrupt( + CMT2300A_MASK_TX_DONE_EN | CMT2300A_MASK_PREAM_OK_EN | CMT2300A_MASK_SYNC_OK_EN | CMT2300A_MASK_CRC_OK_EN | CMT2300A_MASK_PKT_DONE_EN); + + CMT2300A_SetFrequencyStep(FH_OFFSET); // set FH_OFFSET (frequency = base freq + 2.5kHz*FH_OFFSET*FH_CHANNEL) + + /* Use a single 64-byte FIFO for either Tx or Rx */ + CMT2300A_EnableFifoMerge(true); + + /* Go to sleep for configuration to take effect */ + if (!CMT2300A_GoSleep()) { + return false; // CMT2300A not switched to sleep mode! + } + + return true; +} diff --git a/lib/CMT2300a/cmt2300wrapper.h b/lib/CMT2300a/cmt2300wrapper.h new file mode 100644 index 00000000..143817b3 --- /dev/null +++ b/lib/CMT2300a/cmt2300wrapper.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +#define CMT2300A_ONE_STEP_SIZE 2500 // frequency channel step size for fast frequency hopping operation: One step size is 2.5 kHz. +#define CMT_BASE_FREQ 860000000 // from Frequency Bank in cmt2300a_params.h +#define FH_OFFSET 100 // value * CMT2300A_ONE_STEP_SIZE = channel frequency offset +#define CMT_SPI_SPEED 4000000 // 4 MHz + +class CMT2300a { +public: + CMT2300a(uint8_t pin_sdio, uint8_t pin_clk, uint8_t pin_cs, uint8_t pin_fcs, uint32_t _spi_speed = CMT_SPI_SPEED); + + bool begin(void); + + /** + * Checks if the chip is connected to the SPI bus + */ + bool isChipConnected(); + + bool setPALevel(int8_t level); + +private: + /** + * initialize the GPIO pins + */ + bool _init_pins(); + + /** + * initialize radio. + * @warning This function assumes the SPI bus object's begin() method has been + * previously called. + */ + bool _init_radio(); + + int8_t _pin_sdio; + int8_t _pin_clk; + int8_t _pin_cs; + int8_t _pin_fcs; + uint32_t _spi_speed; +}; \ No newline at end of file diff --git a/lib/CMT2300a/cmt_spi3.c b/lib/CMT2300a/cmt_spi3.c index 409782a1..7092b528 100644 --- a/lib/CMT2300a/cmt_spi3.c +++ b/lib/CMT2300a/cmt_spi3.c @@ -3,11 +3,9 @@ #include #include // for esp_rom_gpio_connect_out_signal -#define CMT_SPI_CLK 4000000 // 4 MHz - spi_device_handle_t spi_reg, spi_fifo; -void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs) +void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, uint32_t spi_speed) { spi_bus_config_t buscfg = { .mosi_io_num = pin_sdio, @@ -22,7 +20,7 @@ void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fc .address_bits = 0, .dummy_bits = 0, .mode = 0, // SPI mode 0 - .clock_speed_hz = CMT_SPI_CLK, + .clock_speed_hz = spi_speed, .spics_io_num = pin_cs, .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, .queue_size = 1, @@ -40,8 +38,8 @@ void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fc .dummy_bits = 0, .mode = 0, // SPI mode 0 .cs_ena_pretrans = 2, - .cs_ena_posttrans = (uint8_t)(1 / (CMT_SPI_CLK * 10e6 * 2) + 2), // >2 us - .clock_speed_hz = CMT_SPI_CLK, + .cs_ena_posttrans = (uint8_t)(1 / (spi_speed * 10e6 * 2) + 2), // >2 us + .clock_speed_hz = spi_speed, .spics_io_num = pin_fcs, .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, .queue_size = 1, diff --git a/lib/CMT2300a/cmt_spi3.h b/lib/CMT2300a/cmt_spi3.h index a29a03da..0e77e311 100644 --- a/lib/CMT2300a/cmt_spi3.h +++ b/lib/CMT2300a/cmt_spi3.h @@ -3,7 +3,7 @@ #include -void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs); +void cmt_spi3_init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, uint32_t spi_speed); void cmt_spi3_write(uint8_t addr, uint8_t dat); uint8_t cmt_spi3_read(uint8_t addr); diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 5d4efc10..e10950c1 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -7,10 +7,7 @@ #include "crc.h" #include #include -#include -#define CMT2300A_ONE_STEP_SIZE 2500 // frequency channel step size for fast frequency hopping operation: One step size is 2.5 kHz. -#define FH_OFFSET 100 // value * CMT2300A_ONE_STEP_SIZE = channel frequency offset #define HOY_BASE_FREQ 860000000 // Hoymiles base frequency for CMD56 channels is 860.00 MHz #define HOY_BOOT_FREQ 868000000 // Hoymiles boot/init frequency after power up inverter @@ -60,35 +57,6 @@ bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) return true; } -bool HoymilesRadio_CMT::cmtConfig(void) -{ - /* Config GPIOs */ - CMT2300A_ConfigGpio( - CMT2300A_GPIO3_SEL_INT2); - - /* Config interrupt */ - CMT2300A_ConfigInterrupt( - CMT2300A_INT_SEL_TX_DONE, /* Config INT1 */ - CMT2300A_INT_SEL_PKT_OK /* Config INT2 */ - ); - - /* Enable interrupt */ - CMT2300A_EnableInterrupt( - CMT2300A_MASK_TX_DONE_EN | CMT2300A_MASK_PREAM_OK_EN | CMT2300A_MASK_SYNC_OK_EN | CMT2300A_MASK_CRC_OK_EN | CMT2300A_MASK_PKT_DONE_EN); - - CMT2300A_SetFrequencyStep(100); // set FH_OFFSET to 100 (frequency = base freq + 2.5kHz*FH_OFFSET*FH_CHANNEL) - - /* Use a single 64-byte FIFO for either Tx or Rx */ - CMT2300A_EnableFifoMerge(true); - - /* Go to sleep for configuration to take effect */ - if (!CMT2300A_GoSleep()) { - return false; // CMT2300A not switched to sleep mode! - } - - return true; -} - bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const uint32_t from_freq_kHz, const uint32_t to_freq_kHz) { const uint8_t fromChannel = cmtFreqToChan("[cmtSwitchInvAndDtuFreq]", "from_freq_kHz", from_freq_kHz); @@ -301,7 +269,7 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) CMT2300A_DelayMs(20); CMT2300A_GoStby(); - cmtConfig(); + _radio->begin(); cmtNextState = CMT_STATE_IDLE; @@ -318,41 +286,24 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) void HoymilesRadio_CMT::init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio3) { _dtuSerial.u64 = 0; - uint8_t tmp; - CMT2300A_InitSpi(pin_sdio, pin_clk, pin_cs, pin_fcs); - if (!CMT2300A_Init()) { - Hoymiles.getMessageOutput()->println("CMT2300A_Init() failed!"); - return; - } + _radio.reset(new CMT2300a(pin_sdio, pin_clk, pin_cs, pin_fcs)); - /* config registers */ - CMT2300A_ConfigRegBank(CMT2300A_CMT_BANK_ADDR, g_cmt2300aCmtBank, CMT2300A_CMT_BANK_SIZE); - CMT2300A_ConfigRegBank(CMT2300A_SYSTEM_BANK_ADDR, g_cmt2300aSystemBank, CMT2300A_SYSTEM_BANK_SIZE); - CMT2300A_ConfigRegBank(CMT2300A_FREQUENCY_BANK_ADDR, g_cmt2300aFrequencyBank, CMT2300A_FREQUENCY_BANK_SIZE); // cmtBaseChOff860 need to be changed to the same frequency for channel calculation - CMT2300A_ConfigRegBank(CMT2300A_DATA_RATE_BANK_ADDR, g_cmt2300aDataRateBank, CMT2300A_DATA_RATE_BANK_SIZE); - CMT2300A_ConfigRegBank(CMT2300A_BASEBAND_BANK_ADDR, g_cmt2300aBasebandBank, CMT2300A_BASEBAND_BANK_SIZE); - CMT2300A_ConfigRegBank(CMT2300A_TX_BANK_ADDR, g_cmt2300aTxBank, CMT2300A_TX_BANK_SIZE); + _radio->begin(); cmtBaseChOff860 = (860000000 - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET; - // xosc_aac_code[2:0] = 2 - tmp = (~0x07) & CMT2300A_ReadReg(CMT2300A_CUS_CMT10); - CMT2300A_WriteReg(CMT2300A_CUS_CMT10, tmp | 0x02); + cmtSwitchDtuFreq(HOYMILES_CMT_WORK_FREQ); // start dtu at work freqency, for fast Rx if inverter is already on and frequency switched - if (!cmtConfig()) { - Hoymiles.getMessageOutput()->println("cmtConfig() failed!"); - return; + if (_radio->isChipConnected()) { + Hoymiles.getMessageOutput()->println("Connection successful"); + } else { + Hoymiles.getMessageOutput()->println("Connection error!!"); } attachInterrupt(digitalPinToInterrupt(pin_gpio3), std::bind(&HoymilesRadio_CMT::handleIntr, this), RISING); - cmtSwitchDtuFreq(HOYMILES_CMT_WORK_FREQ); // start dtu at work freqency, for fast Rx if inverter is already on and frequency switched - - _ChipConnected = true; _isInitialized = true; - - Hoymiles.getMessageOutput()->println("CMT init successful"); } void HoymilesRadio_CMT::loop() @@ -451,12 +402,25 @@ void HoymilesRadio_CMT::loop() } } +void HoymilesRadio_CMT::setPALevel(int8_t paLevel) +{ + if (!_isInitialized) { + return; + } + + if (_radio->setPALevel(paLevel)) { + Hoymiles.getMessageOutput()->printf("CMT TX power set to %d dBm\r\n", paLevel); + } else { + Hoymiles.getMessageOutput()->printf("CMT TX power %d dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel); + } +} + bool HoymilesRadio_CMT::isConnected() { if (!_isInitialized) { return false; } - return _ChipConnected; + return _radio->isChipConnected(); } void ARDUINO_ISR_ATTR HoymilesRadio_CMT::handleIntr() diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index 77bbb2ab..fdcad943 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -8,6 +8,7 @@ #include #include #include +#include // number of fragments hold in buffer #define FRAGMENT_BUFFER_SIZE 30 @@ -41,6 +42,7 @@ class HoymilesRadio_CMT : public HoymilesRadio { public: void init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio3); void loop(); + void setPALevel(int8_t paLevel); bool isConnected(); @@ -49,19 +51,18 @@ private: void sendEsbPacket(CommandAbstract* cmd); + std::unique_ptr _radio; + volatile bool _packetReceived = false; std::queue _rxBuffer; TimeoutHelper _rxTimeout; TimeoutHelper _txTimeout; - bool _ChipConnected = false; - String cmtChToFreq(const uint8_t channel); void cmtSwitchChannel(const uint8_t channel); uint8_t cmtFreqToChan(const String& func_name, const String& var_name, const uint32_t freq_kHz); bool cmtSwitchDtuFreq(const uint32_t to_freq_kHz); - bool cmtConfig(void); bool cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const uint32_t from_freq_kHz, const uint32_t to_freq_kHz); enumCMTresult cmtProcess(void); From fc5f6887cb7edbfe7a34634e04c00b22da504322 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 27 Mar 2023 22:42:52 +0200 Subject: [PATCH 28/66] Adjust name from CMT2300a to CMT2300A --- lib/CMT2300a/cmt2300wrapper.cpp | 12 ++++++------ lib/CMT2300a/cmt2300wrapper.h | 4 ++-- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 2 +- lib/Hoymiles/src/HoymilesRadio_CMT.h | 2 +- webapp/src/components/RadioInfo.vue | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/CMT2300a/cmt2300wrapper.cpp b/lib/CMT2300a/cmt2300wrapper.cpp index 603d1b04..99c70ebb 100644 --- a/lib/CMT2300a/cmt2300wrapper.cpp +++ b/lib/CMT2300a/cmt2300wrapper.cpp @@ -6,7 +6,7 @@ #include "cmt2300a.h" #include "cmt2300a_params.h" -CMT2300a::CMT2300a(uint8_t pin_sdio, uint8_t pin_clk, uint8_t pin_cs, uint8_t pin_fcs, uint32_t spi_speed) +CMT2300A::CMT2300A(uint8_t pin_sdio, uint8_t pin_clk, uint8_t pin_cs, uint8_t pin_fcs, uint32_t spi_speed) { _pin_sdio = pin_sdio; _pin_clk = pin_clk; @@ -15,17 +15,17 @@ CMT2300a::CMT2300a(uint8_t pin_sdio, uint8_t pin_clk, uint8_t pin_cs, uint8_t pi _spi_speed = spi_speed; } -bool CMT2300a::begin(void) +bool CMT2300A::begin(void) { return _init_pins() && _init_radio(); } -bool CMT2300a::isChipConnected() +bool CMT2300A::isChipConnected() { return CMT2300A_IsExist(); } -bool CMT2300a::setPALevel(int8_t level) +bool CMT2300A::setPALevel(int8_t level) { uint16_t Tx_dBm_word; switch (level) { @@ -138,14 +138,14 @@ bool CMT2300a::setPALevel(int8_t level) return true; } -bool CMT2300a::_init_pins() +bool CMT2300A::_init_pins() { CMT2300A_InitSpi(_pin_sdio, _pin_clk, _pin_cs, _pin_fcs, _spi_speed); return true; // assuming pins are connected properly } -bool CMT2300a::_init_radio() +bool CMT2300A::_init_radio() { if (!CMT2300A_Init()) { return false; diff --git a/lib/CMT2300a/cmt2300wrapper.h b/lib/CMT2300a/cmt2300wrapper.h index 143817b3..ac1c9d89 100644 --- a/lib/CMT2300a/cmt2300wrapper.h +++ b/lib/CMT2300a/cmt2300wrapper.h @@ -8,9 +8,9 @@ #define FH_OFFSET 100 // value * CMT2300A_ONE_STEP_SIZE = channel frequency offset #define CMT_SPI_SPEED 4000000 // 4 MHz -class CMT2300a { +class CMT2300A { public: - CMT2300a(uint8_t pin_sdio, uint8_t pin_clk, uint8_t pin_cs, uint8_t pin_fcs, uint32_t _spi_speed = CMT_SPI_SPEED); + CMT2300A(uint8_t pin_sdio, uint8_t pin_clk, uint8_t pin_cs, uint8_t pin_fcs, uint32_t _spi_speed = CMT_SPI_SPEED); bool begin(void); diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index e10950c1..54d13d90 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -287,7 +287,7 @@ void HoymilesRadio_CMT::init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int { _dtuSerial.u64 = 0; - _radio.reset(new CMT2300a(pin_sdio, pin_clk, pin_cs, pin_fcs)); + _radio.reset(new CMT2300A(pin_sdio, pin_clk, pin_cs, pin_fcs)); _radio->begin(); diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index fdcad943..b78a5605 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -51,7 +51,7 @@ private: void sendEsbPacket(CommandAbstract* cmd); - std::unique_ptr _radio; + std::unique_ptr _radio; volatile bool _packetReceived = false; diff --git a/webapp/src/components/RadioInfo.vue b/webapp/src/components/RadioInfo.vue index a59cee45..2ebf7219 100644 --- a/webapp/src/components/RadioInfo.vue +++ b/webapp/src/components/RadioInfo.vue @@ -40,13 +40,13 @@ - {{ $t('radioinfo.Status', { module: "CMT2300a" }) }} + {{ $t('radioinfo.Status', { module: "CMT2300A" }) }} - {{ $t('radioinfo.ChipStatus', { module: "CMT2300a" }) }} + {{ $t('radioinfo.ChipStatus', { module: "CMT2300A" }) }} -const std::list AlarmLogParser::_alarmMessages = { +const std::array AlarmLogParser::_alarmMessages = {{ { AlarmMessageType_t::ALL, 1, "Inverter start" }, { AlarmMessageType_t::ALL, 2, "DTU command failed" }, { AlarmMessageType_t::ALL, 121, "Over temperature protection" }, @@ -83,7 +83,7 @@ const std::list AlarmLogParser::_alarmMessages = { { AlarmMessageType_t::ALL, 5200, "Firmware error" }, { AlarmMessageType_t::ALL, 8310, "Shut down" }, { AlarmMessageType_t::ALL, 9000, "Microinverter is suspected of being stolen" }, -}; +}}; void AlarmLogParser::clearBuffer() { diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.h b/lib/Hoymiles/src/parser/AlarmLogParser.h index 5a9c9e2f..b57948be 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.h +++ b/lib/Hoymiles/src/parser/AlarmLogParser.h @@ -3,7 +3,7 @@ #include "Parser.h" #include #include -#include +#include #define ALARM_LOG_ENTRY_COUNT 15 #define ALARM_LOG_ENTRY_SIZE 12 @@ -50,5 +50,5 @@ private: AlarmMessageType_t _messageType = AlarmMessageType_t::ALL; - static const std::list _alarmMessages; + static const std::array _alarmMessages; }; \ No newline at end of file From 854fcdaeaecbdf0a747486f31a45707a633057e1 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 28 Mar 2023 21:48:57 +0200 Subject: [PATCH 30/66] Allow configuration of the TX PA Level of the CMT2300A module --- include/Configuration.h | 5 ++-- include/defaults.h | 3 +- src/Configuration.cpp | 11 +++++-- src/InverterSettings.cpp | 3 +- src/WebApi_dtu.cpp | 35 +++++++++++++++------- webapp/src/locales/de.json | 14 +++++---- webapp/src/locales/en.json | 14 +++++---- webapp/src/locales/fr.json | 14 +++++---- webapp/src/types/DtuConfig.ts | 9 ++++-- webapp/src/views/DtuAdminView.vue | 48 ++++++++++++++++++++++--------- 10 files changed, 104 insertions(+), 52 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 49545466..3b431981 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -4,7 +4,7 @@ #include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011800 // 0.1.24 // make sure to clean all after change +#define CONFIG_VERSION 0x00011900 // 0.1.24 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -82,7 +82,8 @@ struct CONFIG_T { uint64_t Dtu_Serial; uint32_t Dtu_PollInterval; - uint8_t Dtu_PaLevel; + uint8_t Dtu_NrfPaLevel; + int8_t Dtu_CmtPaLevel; bool Mqtt_Hass_Enabled; bool Mqtt_Hass_Retain; diff --git a/include/defaults.h b/include/defaults.h index 8c371661..c325081f 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -76,7 +76,8 @@ #define DTU_SERIAL 0x99978563412 #define DTU_POLL_INTERVAL 5 -#define DTU_PA_LEVEL 0 +#define DTU_NRF_PA_LEVEL 0 +#define DTU_CMT_PA_LEVEL 0 #define MQTT_HASS_ENABLED false #define MQTT_HASS_EXPIRE true diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 593d6dfe..1427d2e9 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -79,7 +79,8 @@ bool ConfigurationClass::write() JsonObject dtu = doc.createNestedObject("dtu"); dtu["serial"] = config.Dtu_Serial; dtu["poll_interval"] = config.Dtu_PollInterval; - dtu["pa_level"] = config.Dtu_PaLevel; + dtu["nrf_pa_level"] = config.Dtu_NrfPaLevel; + dtu["cmt_pa_level"] = config.Dtu_CmtPaLevel; JsonObject security = doc.createNestedObject("security"); security["password"] = config.Security_Password; @@ -219,7 +220,8 @@ bool ConfigurationClass::read() JsonObject dtu = doc["dtu"]; config.Dtu_Serial = dtu["serial"] | DTU_SERIAL; config.Dtu_PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL; - config.Dtu_PaLevel = dtu["pa_level"] | DTU_PA_LEVEL; + config.Dtu_NrfPaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL; + config.Dtu_CmtPaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL; JsonObject security = doc["security"]; strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); @@ -290,6 +292,11 @@ void ConfigurationClass::migrate() config.Mqtt_PublishInterval = mqtt["publish_invterval"]; } + if (config.Cfg_Version < 0x00011900) { + JsonObject dtu = doc["dtu"]; + config.Dtu_NrfPaLevel = dtu["pa_level"]; + } + f.close(); config.Cfg_Version = CONFIG_VERSION; diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 7be4dbfe..d4a8e57b 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -37,7 +37,8 @@ void InverterSettingsClass::init() } MessageOutput.println(" Setting radio PA level... "); - Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); + Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_NrfPaLevel); + Hoymiles.getRadioCmt()->setPALevel(config.Dtu_CmtPaLevel); MessageOutput.println(" Setting DTU serial... "); Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index c0ad977e..05cede7d 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -38,9 +38,12 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) snprintf(buffer, sizeof(buffer), "%0x%08x", ((uint32_t)((config.Dtu_Serial >> 32) & 0xFFFFFFFF)), ((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF))); - root["dtu_serial"] = buffer; - root["dtu_pollinterval"] = config.Dtu_PollInterval; - root["dtu_palevel"] = config.Dtu_PaLevel; + root["serial"] = buffer; + root["pollinterval"] = config.Dtu_PollInterval; + root["nrf_enabled"] = Hoymiles.getRadioNrf()->isInitialized(); + root["nrf_palevel"] = config.Dtu_NrfPaLevel; + root["cmt_enabled"] = Hoymiles.getRadioCmt()->isInitialized(); + root["cmt_palevel"] = config.Dtu_CmtPaLevel; response->setLength(); request->send(response); @@ -85,7 +88,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } - if (!(root.containsKey("dtu_serial") && root.containsKey("dtu_pollinterval") && root.containsKey("dtu_palevel"))) { + if (!(root.containsKey("serial") && root.containsKey("pollinterval") && root.containsKey("nrf_palevel") && root.containsKey("cmt_palevel"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; response->setLength(); @@ -93,7 +96,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } - if (root["dtu_serial"].as() == 0) { + if (root["serial"].as() == 0) { retMsg["message"] = "Serial cannot be zero!"; retMsg["code"] = WebApiError::DtuSerialZero; response->setLength(); @@ -101,7 +104,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } - if (root["dtu_pollinterval"].as() == 0) { + if (root["pollinterval"].as() == 0) { retMsg["message"] = "Poll interval must be greater zero!"; retMsg["code"] = WebApiError::DtuPollZero; response->setLength(); @@ -109,7 +112,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } - if (root["dtu_palevel"].as() > 3) { + if (root["nrf_palevel"].as() > 3) { retMsg["message"] = "Invalid power level setting!"; retMsg["code"] = WebApiError::DtuInvalidPowerLevel; response->setLength(); @@ -117,12 +120,21 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } + if (root["cmt_palevel"].as() < -10 || root["cmt_palevel"].as() > 20) { + retMsg["message"] = F("Invalid power level setting!"); + retMsg["code"] = WebApiError::DtuInvalidPowerLevel; + response->setLength(); + request->send(response); + return; + } + CONFIG_T& config = Configuration.get(); // Interpret the string as a hex value and convert it to uint64_t - config.Dtu_Serial = strtoll(root["dtu_serial"].as().c_str(), NULL, 16); - config.Dtu_PollInterval = root["dtu_pollinterval"].as(); - config.Dtu_PaLevel = root["dtu_palevel"].as(); + config.Dtu_Serial = strtoll(root["serial"].as().c_str(), NULL, 16); + config.Dtu_PollInterval = root["pollinterval"].as(); + config.Dtu_NrfPaLevel = root["nrf_palevel"].as(); + config.Dtu_CmtPaLevel = root["cmt_palevel"].as(); Configuration.write(); retMsg["type"] = "success"; @@ -132,7 +144,8 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) response->setLength(); request->send(response); - Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); + Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu_NrfPaLevel); + Hoymiles.getRadioCmt()->setPALevel(config.Dtu_CmtPaLevel); Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu_Serial); Hoymiles.setPollInterval(config.Dtu_PollInterval); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index f84caf1d..d5412f60 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -322,13 +322,15 @@ "SerialHint": "Sowohl der Wechselrichter als auch die DTU haben eine Seriennummer. Die DTU-Seriennummer wird beim ersten Start zufällig generiert und muss normalerweise nicht geändert werden.", "PollInterval": "Abfrageintervall:", "Seconds": "Sekunden", - "PaLevel": "Sendeleistung:", - "PaLevelHint": "Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.", + "NrfPaLevel": "NRF24 Sendeleistung:", + "CmtPaLevel": "CMT2300A Sendeleistung:", + "NrfPaLevelHint": "Verwendet für HM-Wechselrichter. Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.", + "CmtPaLevelHint": "Verwendet für HMS/HMT-Wechselrichter. Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.", "Save": "Speichern", - "Min": "Minimum (-18 dBm)", - "Low": "Niedrig (-12 dBm)", - "High": "Hoch (-6 dBm)", - "Max": "Maximum (0 dBm)" + "Min": "Minimum ({db} dBm)", + "Low": "Niedrig ({db} dBm)", + "High": "Hoch ({db} dBm)", + "Max": "Maximum ({db} dBm)" }, "securityadmin": { "SecuritySettings": "Sicherheitseinstellungen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index bb398bcb..a6c6c9d2 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -322,13 +322,15 @@ "SerialHint": "Both the inverter and the DTU have a serial number. The DTU serial number is randomly generated at the first start and does not normally need to be changed.", "PollInterval": "Poll Interval:", "Seconds": "Seconds", - "PaLevel": "PA Level:", - "PaLevelHint": "Make sure your power supply is stable enough before increasing the transmit power.", + "NrfPaLevel": "NRF24 Transmitting power:", + "CmtPaLevel": "CMT2300A Transmitting power:", + "NrfPaLevelHint": "Used for HM-Inverters. Make sure your power supply is stable enough before increasing the transmit power.", + "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Make sure your power supply is stable enough before increasing the transmit power.", "Save": "Save", - "Min": "Minimum (-18 dBm)", - "Low": "Low (-12 dBm)", - "High": "High (-6 dBm)", - "Max": "Maximum (0 dBm)" + "Min": "Minimum ({db} dBm)", + "Low": "Low ({db} dBm)", + "High": "High ({db} dBm)", + "Max": "Maximum ({db} dBm)" }, "securityadmin": { "SecuritySettings": "Security Settings", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 135d18bb..42d6cf1a 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -322,13 +322,15 @@ "SerialHint": "L'onduleur et le DTU ont tous deux un numéro de série. Le numéro de série du DTU est généré de manière aléatoire lors du premier démarrage et ne doit normalement pas être modifié.", "PollInterval": "Intervalle de sondage", "Seconds": "Secondes", - "PaLevel": "Niveau de puissance d'émission", - "PaLevelHint": "Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", + "NrfPaLevel": "NRF24 Niveau de puissance d'émission", + "CmtPaLevel": "CMT2300A Niveau de puissance d'émission", + "NrfPaLevelHint": "Used for HM-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", + "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", "Save": "Sauvegarder", - "Min": "Minimum (-18 dBm)", - "Low": "Bas (-12 dBm)", - "High": "Haut (-6 dBm)", - "Max": "Maximum (0 dBm)" + "Min": "Minimum ({db} dBm)", + "Low": "Bas ({db} dBm)", + "High": "Haut ({db} dBm)", + "Max": "Maximum ({db} dBm)" }, "securityadmin": { "SecuritySettings": "Paramètres de sécurité", diff --git a/webapp/src/types/DtuConfig.ts b/webapp/src/types/DtuConfig.ts index 51e45fa3..e6cc6b48 100644 --- a/webapp/src/types/DtuConfig.ts +++ b/webapp/src/types/DtuConfig.ts @@ -1,5 +1,8 @@ export interface DtuConfig { - dtu_serial: number; - dtu_pollinterval: number; - dtu_palevel: number; + serial: number; + pollinterval: number; + nrf_enabled: boolean; + nrf_palevel: number; + cmt_enabled: boolean; + cmt_palevel: number; } \ No newline at end of file diff --git a/webapp/src/views/DtuAdminView.vue b/webapp/src/views/DtuAdminView.vue index 2ab7f966..07c2e1f2 100644 --- a/webapp/src/views/DtuAdminView.vue +++ b/webapp/src/views/DtuAdminView.vue @@ -7,24 +7,38 @@
-
-
+ +
From 1614b2ad2df2b09a340a41979ee70e7716bc357a Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 30 Mar 2023 23:31:35 +0200 Subject: [PATCH 37/66] Add newline after log output --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 4fd88357..e8e193b8 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -344,7 +344,7 @@ void HoymilesRadio_CMT::loop() // Save packet in inverter rx buffer Hoymiles.getMessageOutput()->printf("RX %s --> ", cmtChToFreq(f.channel).c_str()); dumpBuf(f.fragment, f.len, false); - Hoymiles.getMessageOutput()->printf("| %d dBm", f.rssi); + Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); inv->addRxFragment(f.fragment, f.len); } else { From 6ea34b331d14b59ef1be8b93040eb5e16ac99c5f Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 30 Mar 2023 23:35:05 +0200 Subject: [PATCH 38/66] Increase command timeouts to support inverters with 6 channels and more phases --- lib/Hoymiles/src/commands/AlarmDataCommand.cpp | 2 +- lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp index d1158597..46b62e7c 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp @@ -10,7 +10,7 @@ AlarmDataCommand::AlarmDataCommand(uint64_t target_address, uint64_t router_addr { setTime(time); setDataType(0x11); - setTimeout(600); + setTimeout(750); } String AlarmDataCommand::getCommandName() diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp index 97d0cebb..6a7db92a 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp @@ -10,7 +10,7 @@ RealTimeRunDataCommand::RealTimeRunDataCommand(uint64_t target_address, uint64_t { setTime(time); setDataType(0x0b); - setTimeout(200); + setTimeout(500); } String RealTimeRunDataCommand::getCommandName() From 50ce7f014d807b13b06c3572cdd51f605a799ed5 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 31 Mar 2023 18:11:28 +0200 Subject: [PATCH 39/66] Expose min and max frequency in HoymilesRadio_CMT --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 22 ++++++++++++++++++---- lib/Hoymiles/src/HoymilesRadio_CMT.h | 3 +++ src/WebApi_dtu.cpp | 9 ++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index e8e193b8..71b8d702 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -14,6 +14,12 @@ // offset from initalized CMT base frequency to Hoy base frequency in channels #define CMT_BASE_CH_OFFSET860 ((860000000 - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET) +// frequency can not be lower than actual initailized base freq +#define CMT_MIN_FREQ_KHZ ((HOY_BASE_FREQ + (CMT_BASE_CH_OFFSET860 >= 1 ? CMT_BASE_CH_OFFSET860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) + +// =923500, 0xFF does not work +#define CMT_MAX_FREQ_KHZ ((HOY_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) + String HoymilesRadio_CMT::cmtChToFreq(const uint8_t channel) { return String((HOY_BASE_FREQ + (CMT_BASE_CH_OFFSET860 + channel) * FH_OFFSET * CMT2300A_ONE_STEP_SIZE) / 1000000.0, 2) + " MHz"; @@ -34,11 +40,9 @@ uint8_t HoymilesRadio_CMT::cmtFreqToChan(const String& func_name, const String& func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0); return 0xFF; // ERROR } - const uint32_t min_Freq_kHz = (HOY_BASE_FREQ + (CMT_BASE_CH_OFFSET860 >= 1 ? CMT_BASE_CH_OFFSET860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000; // frequency can not be lower than actual initailized base freq - const uint32_t max_Freq_kHz = (HOY_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000; // =923500, 0xFF does not work - if (freq_kHz < min_Freq_kHz || freq_kHz > max_Freq_kHz) { + if (freq_kHz < CMT_MIN_FREQ_KHZ || freq_kHz > CMT_MAX_FREQ_KHZ) { Hoymiles.getMessageOutput()->printf("%s %s %.2f MHz is out of Hoymiles/CMT range! (%.2f MHz - %.2f MHz)\r\n", - func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0, min_Freq_kHz / 1000.0, max_Freq_kHz / 1000.0); + func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0, CMT_MIN_FREQ_KHZ / 1000.0, CMT_MAX_FREQ_KHZ / 1000.0); return 0xFF; // ERROR } if (freq_kHz < 863000 || freq_kHz > 870000) { @@ -453,6 +457,16 @@ bool HoymilesRadio_CMT::isConnected() return _radio->isChipConnected(); } +uint32_t HoymilesRadio_CMT::getMinFrequency() +{ + return CMT_MIN_FREQ_KHZ; +} + +uint32_t HoymilesRadio_CMT::getMaxFrequency() +{ + return CMT_MAX_FREQ_KHZ; +} + void ARDUINO_ISR_ATTR HoymilesRadio_CMT::handleInt1() { _packetSent = true; diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index f4218c14..a2fe1377 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -51,6 +51,9 @@ public: bool isConnected(); + static uint32_t getMinFrequency(); + static uint32_t getMaxFrequency(); + private: void ARDUINO_ISR_ATTR handleInt1(); void ARDUINO_ISR_ATTR handleInt2(); diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index c89fb781..fc4dadef 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -129,11 +129,14 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } - if (root["cmt_frequency"].as() < 860000 || root["cmt_frequency"].as() > 923000 || root["cmt_frequency"].as() % 250 > 0) { + if (root["cmt_frequency"].as() < Hoymiles.getRadioCmt()->getMinFrequency() + || root["cmt_frequency"].as() > Hoymiles.getRadioCmt()->getMaxFrequency() + || root["cmt_frequency"].as() % 250 > 0) { + retMsg["message"] = "Invalid CMT frequency setting!"; retMsg["code"] = WebApiError::DtuInvalidCmtFrequency; - retMsg["param"]["min"] = 860000; - retMsg["param"]["max"] = 923000; + retMsg["param"]["min"] = Hoymiles.getRadioCmt()->getMinFrequency(); + retMsg["param"]["max"] = Hoymiles.getRadioCmt()->getMaxFrequency(); response->setLength(); request->send(response); return; From 1e7b16adb91e8e77b084754abc2c0f08a0f541ad Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 31 Mar 2023 19:33:43 +0200 Subject: [PATCH 40/66] webapp: Nicer cmt frequency input --- webapp/src/locales/de.json | 1 + webapp/src/locales/en.json | 1 + webapp/src/locales/fr.json | 1 + webapp/src/views/DtuAdminView.vue | 30 +++++++++++++++++++++++++----- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index b23e874e..5a761ed4 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -329,6 +329,7 @@ "CmtPaLevelHint": "Verwendet für HMS/HMT-Wechselrichter. Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.", "CmtFrequency": "CMT2300A Frequenz:", "CmtFrequencyHint": "Stelle sicher, dass du nur Frequenzen verwendet werden welche im entsprechenden Land erlaubt sind!", + "CmtFrequencyWarning": "Die ausgewählte Frequenz befindet außerhalb des in der EU zugelassenen Bereiches. Vergewissere dich, dass mit dieser Auswahl keine lokalen Regularien verletzt werden.", "khz": "kHz", "Save": "Speichern", "Min": "Minimum ({db} dBm)", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 08212696..c4f927ba 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -329,6 +329,7 @@ "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Make sure your power supply is stable enough before increasing the transmit power.", "CmtFrequency": "CMT2300A Frequency:", "CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country!", + "CmtFrequencyWarning": "The selected frequency is outside the range allowed in the EU. Make sure that this selection does not violate any local regulations.", "khz": "kHz", "Save": "Save", "Min": "Minimum ({db} dBm)", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index c03fbd2e..ca6b56a1 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -329,6 +329,7 @@ "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", "CmtFrequency": "CMT2300A Frequency:", "CmtFrequencyHint": "Stelle sicher, dass du nur Frequenzen verwendet werden welche im entsprechenden Land erlaubt sind!", + "CmtFrequencyWarning": "The selected frequency is outside the range allowed in the EU. Make sure that this selection does not violate any local regulations.", "khz": "kHz", "Save": "Sauvegarder", "Min": "Minimum ({db} dBm)", diff --git a/webapp/src/views/DtuAdminView.vue b/webapp/src/views/DtuAdminView.vue index 0e098d54..9d4c3c51 100644 --- a/webapp/src/views/DtuAdminView.vue +++ b/webapp/src/views/DtuAdminView.vue @@ -44,12 +44,24 @@ - + +
+
+ + min="860250" max="923500" step="250" + id="cmtFrequency" aria-describedby="basic-addon2" + style="height: unset;" /> + {{ cmtFrequencyText }} +
+ +
+ + @@ -98,6 +110,14 @@ export default defineComponent({ created() { this.getDtuConfig(); }, + computed: { + cmtFrequencyText() { + return this.$n(this.dtuConfigList.cmt_frequency / 1000, "decimalTwoDigits") + " MHz"; + }, + cmtIsOutOfEu() { + return this.dtuConfigList.cmt_frequency < 863000 || this.dtuConfigList.cmt_frequency > 870000; + } + }, methods: { getDtuConfig() { this.dataLoading = true; From c3368450f6b368afbd8047c68d5733a42140f95f Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 31 Mar 2023 21:12:12 +0200 Subject: [PATCH 41/66] Initialize spiClass only if valid pin config was found --- src/InverterSettings.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index a11fb7ee..0c1a1fea 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -23,12 +23,12 @@ void InverterSettingsClass::init() // Initialize inverter communication MessageOutput.print("Initialize Hoymiles interface... "); if (PinMapping.isValidNrf24Config() || PinMapping.isValidCmt2300Config()) { - SPIClass* spiClass = new SPIClass(VSPI); - spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); Hoymiles.setMessageOutput(&MessageOutput); Hoymiles.init(); if (PinMapping.isValidNrf24Config()) { + SPIClass* spiClass = new SPIClass(VSPI); + spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); Hoymiles.initNRF(spiClass, pin.nrf24_en, pin.nrf24_irq); } From 15156b4b8787be9d698c5855fc2bc710a2afa7cb Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 31 Mar 2023 21:15:57 +0200 Subject: [PATCH 42/66] Set CMT frequency only if a valid pin config was found --- src/InverterSettings.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 0c1a1fea..13862f15 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -34,6 +34,8 @@ void InverterSettingsClass::init() if (PinMapping.isValidCmt2300Config()) { Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3); + MessageOutput.println(F(" Setting CMT target frequency... ")); + Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu_CmtFrequency); } MessageOutput.println(" Setting radio PA level... "); @@ -44,9 +46,6 @@ void InverterSettingsClass::init() Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu_Serial); Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu_Serial); - MessageOutput.println(" Setting CMT target frequency... "); - Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu_CmtFrequency); - MessageOutput.println(" Setting poll interval... "); Hoymiles.setPollInterval(config.Dtu_PollInterval); From ac5df9a91d1253e358d8af01c2e60c3a9401572f Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 31 Mar 2023 21:35:28 +0200 Subject: [PATCH 43/66] webapp: Implement CMT pa level as range control --- webapp/src/views/DtuAdminView.vue | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/webapp/src/views/DtuAdminView.vue b/webapp/src/views/DtuAdminView.vue index 9d4c3c51..dadce8c6 100644 --- a/webapp/src/views/DtuAdminView.vue +++ b/webapp/src/views/DtuAdminView.vue @@ -36,11 +36,14 @@
- +
+ + {{ cmtPaLevelText }} +
@@ -96,12 +99,6 @@ export default defineComponent({ { key: 2, value: 'High', db: "-6" }, { key: 3, value: 'Max', db: "0" }, ], - cmtpalevelList: [ - { key: 0, value: 'Min', db: "0" }, - { key: 13, value: 'Low', db: "13" }, - { key: 17, value: 'High', db: "17" }, - { key: 20, value: 'Max', db: "20" }, - ], alertMessage: "", alertType: "info", showAlert: false, @@ -114,6 +111,9 @@ export default defineComponent({ cmtFrequencyText() { return this.$n(this.dtuConfigList.cmt_frequency / 1000, "decimalTwoDigits") + " MHz"; }, + cmtPaLevelText() { + return this.$n(this.dtuConfigList.cmt_palevel * 1) + " dBm"; + }, cmtIsOutOfEu() { return this.dtuConfigList.cmt_frequency < 863000 || this.dtuConfigList.cmt_frequency > 870000; } From d6c2a4ba1c547f55f3f02e72e536dc54d5109cc6 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 31 Mar 2023 21:43:11 +0200 Subject: [PATCH 44/66] webapp: Remove hard coded texts in dtuadmin view --- webapp/src/locales/de.json | 3 ++- webapp/src/locales/en.json | 3 ++- webapp/src/locales/fr.json | 3 ++- webapp/src/views/DtuAdminView.vue | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 5a761ed4..1e48a33d 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -330,7 +330,8 @@ "CmtFrequency": "CMT2300A Frequenz:", "CmtFrequencyHint": "Stelle sicher, dass du nur Frequenzen verwendet werden welche im entsprechenden Land erlaubt sind!", "CmtFrequencyWarning": "Die ausgewählte Frequenz befindet außerhalb des in der EU zugelassenen Bereiches. Vergewissere dich, dass mit dieser Auswahl keine lokalen Regularien verletzt werden.", - "khz": "kHz", + "MHz": "{mhz} MHz", + "dBm": "{dbm} dBm", "Save": "Speichern", "Min": "Minimum ({db} dBm)", "Low": "Niedrig ({db} dBm)", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index c4f927ba..95940b4a 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -330,7 +330,8 @@ "CmtFrequency": "CMT2300A Frequency:", "CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country!", "CmtFrequencyWarning": "The selected frequency is outside the range allowed in the EU. Make sure that this selection does not violate any local regulations.", - "khz": "kHz", + "MHz": "{mhz} MHz", + "dBm": "{dbm} dBm", "Save": "Save", "Min": "Minimum ({db} dBm)", "Low": "Low ({db} dBm)", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index ca6b56a1..480089d2 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -330,7 +330,8 @@ "CmtFrequency": "CMT2300A Frequency:", "CmtFrequencyHint": "Stelle sicher, dass du nur Frequenzen verwendet werden welche im entsprechenden Land erlaubt sind!", "CmtFrequencyWarning": "The selected frequency is outside the range allowed in the EU. Make sure that this selection does not violate any local regulations.", - "khz": "kHz", + "MHz": "{mhz} MHz", + "dBm": "{dbm} dBm", "Save": "Sauvegarder", "Min": "Minimum ({db} dBm)", "Low": "Bas ({db} dBm)", diff --git a/webapp/src/views/DtuAdminView.vue b/webapp/src/views/DtuAdminView.vue index dadce8c6..1a5fc079 100644 --- a/webapp/src/views/DtuAdminView.vue +++ b/webapp/src/views/DtuAdminView.vue @@ -109,10 +109,10 @@ export default defineComponent({ }, computed: { cmtFrequencyText() { - return this.$n(this.dtuConfigList.cmt_frequency / 1000, "decimalTwoDigits") + " MHz"; + return this.$t("dtuadmin.MHz", { mhz: this.$n(this.dtuConfigList.cmt_frequency / 1000, "decimalTwoDigits") }); }, cmtPaLevelText() { - return this.$n(this.dtuConfigList.cmt_palevel * 1) + " dBm"; + return this.$t("dtuadmin.dBm", { dbm: this.$n(this.dtuConfigList.cmt_palevel * 1) }); }, cmtIsOutOfEu() { return this.dtuConfigList.cmt_frequency < 863000 || this.dtuConfigList.cmt_frequency > 870000; From 85070ffda0531661878c26e9a4de26d7b22c01bb Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 31 Mar 2023 21:49:16 +0200 Subject: [PATCH 45/66] webapp: Add hint to cmt frequency that it can take up to 15min until a connection is established --- webapp/src/locales/de.json | 2 +- webapp/src/locales/en.json | 2 +- webapp/src/locales/fr.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 1e48a33d..20a26f67 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -328,7 +328,7 @@ "NrfPaLevelHint": "Verwendet für HM-Wechselrichter. Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.", "CmtPaLevelHint": "Verwendet für HMS/HMT-Wechselrichter. Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.", "CmtFrequency": "CMT2300A Frequenz:", - "CmtFrequencyHint": "Stelle sicher, dass du nur Frequenzen verwendet werden welche im entsprechenden Land erlaubt sind!", + "CmtFrequencyHint": "Stelle sicher, dass du nur Frequenzen verwendet werden welche im entsprechenden Land erlaubt sind! Nach einer Frequenzänderung kann es bis zu 15min dauern bis eine Verbindung hergestellt wird.", "CmtFrequencyWarning": "Die ausgewählte Frequenz befindet außerhalb des in der EU zugelassenen Bereiches. Vergewissere dich, dass mit dieser Auswahl keine lokalen Regularien verletzt werden.", "MHz": "{mhz} MHz", "dBm": "{dbm} dBm", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 95940b4a..9b2d8d72 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -328,7 +328,7 @@ "NrfPaLevelHint": "Used for HM-Inverters. Make sure your power supply is stable enough before increasing the transmit power.", "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Make sure your power supply is stable enough before increasing the transmit power.", "CmtFrequency": "CMT2300A Frequency:", - "CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country!", + "CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country! After a frequency change, it can take up to 15min until a connection is established.", "CmtFrequencyWarning": "The selected frequency is outside the range allowed in the EU. Make sure that this selection does not violate any local regulations.", "MHz": "{mhz} MHz", "dBm": "{dbm} dBm", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 480089d2..dc1bb6d6 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -328,7 +328,7 @@ "NrfPaLevelHint": "Used for HM-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", "CmtFrequency": "CMT2300A Frequency:", - "CmtFrequencyHint": "Stelle sicher, dass du nur Frequenzen verwendet werden welche im entsprechenden Land erlaubt sind!", + "CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country! After a frequency change, it can take up to 15min until a connection is established.", "CmtFrequencyWarning": "The selected frequency is outside the range allowed in the EU. Make sure that this selection does not violate any local regulations.", "MHz": "{mhz} MHz", "dBm": "{dbm} dBm", From 25722f60551898b3b4ca2a2fed2c9e9b67af7b6b Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sun, 9 Apr 2023 19:40:41 +0200 Subject: [PATCH 46/66] Adjust buffer size in StatisticsParser for inverters with more inputs --- lib/Hoymiles/src/parser/StatisticsParser.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index f4493f10..6cea199b 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -5,7 +5,7 @@ #include #include -#define STATISTIC_PACKET_SIZE (4 * 16) +#define STATISTIC_PACKET_SIZE (7 * 16) // units enum UnitId_t { From 6331210b94ce5d7cc28b3ff43ba68d6edd632739 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sun, 9 Apr 2023 20:43:02 +0200 Subject: [PATCH 47/66] IsReachable of the inverter was never reached --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 71b8d702..d920fd06 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -372,10 +372,8 @@ void HoymilesRadio_CMT::loop() CommandAbstract* cmd = _commandQueue.front().get(); uint8_t verifyResult = inv->verifyAllFragments(cmd); if (verifyResult == FRAGMENT_ALL_MISSING_RESEND) { - Hoymiles.getMessageOutput()->println("Nothing received"); - // sendLastPacketAgain(); - _commandQueue.pop(); - _busyFlag = false; + Hoymiles.getMessageOutput()->println("Nothing received, resend whole request"); + sendLastPacketAgain(); } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded"); From 5b648b63acfe2c158f7909bfce8246637bec1c97 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sun, 9 Apr 2023 23:13:58 +0200 Subject: [PATCH 48/66] Implemented blocking write method in CMT2300 driver and use it in sendEsbPacket. --- lib/CMT2300a/cmt2300wrapper.cpp | 35 ++++++++++++++++++++++++++ lib/CMT2300a/cmt2300wrapper.h | 2 ++ lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 10 +++++--- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/CMT2300a/cmt2300wrapper.cpp b/lib/CMT2300a/cmt2300wrapper.cpp index 539630e0..6bfe7b90 100644 --- a/lib/CMT2300a/cmt2300wrapper.cpp +++ b/lib/CMT2300a/cmt2300wrapper.cpp @@ -25,6 +25,41 @@ bool CMT2300A::isChipConnected() return CMT2300A_IsExist(); } +bool CMT2300A::write(const uint8_t* buf, uint8_t len) +{ + CMT2300A_GoStby(); + CMT2300A_ClearInterruptFlags(); + + /* Must clear FIFO after enable SPI to read or write the FIFO */ + CMT2300A_EnableWriteFifo(); + CMT2300A_ClearTxFifo(); + + CMT2300A_WriteReg(CMT2300A_CUS_PKT15, len); // set Tx length + /* The length need be smaller than 32 */ + CMT2300A_WriteFifo(buf, len); + + if (!(CMT2300A_ReadReg(CMT2300A_CUS_FIFO_FLAG) & CMT2300A_MASK_TX_FIFO_NMTY_FLG)) { + return false; + } + + if (!CMT2300A_GoTx()) { + return false; + } + + uint32_t timer = millis(); + + while (!(CMT2300A_MASK_TX_DONE_FLG & CMT2300A_ReadReg(CMT2300A_CUS_INT_CLR1))) { + if (millis() - timer > 95) { + return false; + } + } + + CMT2300A_ClearInterruptFlags(); + CMT2300A_GoSleep(); + + return true; +} + bool CMT2300A::setPALevel(int8_t level) { uint16_t Tx_dBm_word; diff --git a/lib/CMT2300a/cmt2300wrapper.h b/lib/CMT2300a/cmt2300wrapper.h index ac1c9d89..977f2269 100644 --- a/lib/CMT2300a/cmt2300wrapper.h +++ b/lib/CMT2300a/cmt2300wrapper.h @@ -19,6 +19,8 @@ public: */ bool isChipConnected(); + bool write(const uint8_t* buf, uint8_t len); + bool setPALevel(int8_t level); private: diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index d920fd06..8cf565d9 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -485,11 +485,15 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) cmd->getCommandName().c_str(), cmtChToFreq(cmtCurrentCh).c_str()); cmd->dumpDataPayload(Hoymiles.getMessageOutput()); + // Still here for to handle CMD56 correctly (inverter serial etc.) memcpy(cmtTxBuffer, cmd->getDataPayload(), cmd->getDataSize()); - cmtTxLength = cmd->getDataSize(); - _txTimeout.set(100); - cmtNextState = CMT_STATE_TX_START; + if (_radio->write(cmd->getDataPayload(), cmd->getDataSize())) { + _packetSent = false; // still bad hack, to be removed + cmtNextState = CMT_STATE_RX_START; + } else { + Hoymiles.getMessageOutput()->println("TX SPI Timeout"); + } _busyFlag = true; _rxTimeout.set(cmd->getTimeout()); From fffd872b20d190b156c38d87b48628729fc99087 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 10 Apr 2023 00:06:39 +0200 Subject: [PATCH 49/66] Replace HOY_BASE_FREQ by CMT_BASE_FREQ --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 8cf565d9..f3870a5a 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -8,21 +8,20 @@ #include #include -#define HOY_BASE_FREQ 860000000 // Hoymiles base frequency for CMD56 channels is 860.00 MHz #define HOY_BOOT_FREQ 868000000 // Hoymiles boot/init frequency after power up inverter or connection lost for 15 min // offset from initalized CMT base frequency to Hoy base frequency in channels -#define CMT_BASE_CH_OFFSET860 ((860000000 - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET) +#define CMT_BASE_CH_OFFSET860 ((860000000 - CMT_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET) // frequency can not be lower than actual initailized base freq -#define CMT_MIN_FREQ_KHZ ((HOY_BASE_FREQ + (CMT_BASE_CH_OFFSET860 >= 1 ? CMT_BASE_CH_OFFSET860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) +#define CMT_MIN_FREQ_KHZ ((CMT_BASE_FREQ + (CMT_BASE_CH_OFFSET860 >= 1 ? CMT_BASE_CH_OFFSET860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) // =923500, 0xFF does not work -#define CMT_MAX_FREQ_KHZ ((HOY_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) +#define CMT_MAX_FREQ_KHZ ((CMT_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) String HoymilesRadio_CMT::cmtChToFreq(const uint8_t channel) { - return String((HOY_BASE_FREQ + (CMT_BASE_CH_OFFSET860 + channel) * FH_OFFSET * CMT2300A_ONE_STEP_SIZE) / 1000000.0, 2) + " MHz"; + return String((CMT_BASE_FREQ + (CMT_BASE_CH_OFFSET860 + channel) * FH_OFFSET * CMT2300A_ONE_STEP_SIZE) / 1000000.0, 2) + " MHz"; } void HoymilesRadio_CMT::cmtSwitchChannel(const uint8_t channel) @@ -49,7 +48,7 @@ uint8_t HoymilesRadio_CMT::cmtFreqToChan(const String& func_name, const String& Hoymiles.getMessageOutput()->printf("%s !!! caution: %s %.2f MHz is out of EU legal range! (863 - 870 MHz)\r\n", func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0); } - return (freq_kHz * 1000 - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET - CMT_BASE_CH_OFFSET860; // frequency to channel + return (freq_kHz * 1000 - CMT_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET - CMT_BASE_CH_OFFSET860; // frequency to channel } bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) From 2a92f67a9a360f5ad79d887471c7a8f50b82a8c0 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 10 Apr 2023 00:29:21 +0200 Subject: [PATCH 50/66] Implement get and set channel in cmt2300 wrapper class --- lib/CMT2300a/cmt2300wrapper.cpp | 10 ++++++++++ lib/CMT2300a/cmt2300wrapper.h | 12 ++++++++++++ lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 4 +--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/CMT2300a/cmt2300wrapper.cpp b/lib/CMT2300a/cmt2300wrapper.cpp index 6bfe7b90..df3fdfbf 100644 --- a/lib/CMT2300a/cmt2300wrapper.cpp +++ b/lib/CMT2300a/cmt2300wrapper.cpp @@ -60,6 +60,16 @@ bool CMT2300A::write(const uint8_t* buf, uint8_t len) return true; } +void CMT2300A::setChannel(uint8_t channel) +{ + CMT2300A_SetFrequencyChannel(channel); +} + +uint8_t CMT2300A::getChannel(void) +{ + return CMT2300A_ReadReg(CMT2300A_CUS_FREQ_CHNL); +} + bool CMT2300A::setPALevel(int8_t level) { uint16_t Tx_dBm_word; diff --git a/lib/CMT2300a/cmt2300wrapper.h b/lib/CMT2300a/cmt2300wrapper.h index 977f2269..6ae68fdc 100644 --- a/lib/CMT2300a/cmt2300wrapper.h +++ b/lib/CMT2300a/cmt2300wrapper.h @@ -21,6 +21,18 @@ public: bool write(const uint8_t* buf, uint8_t len); + /** + * Set RF communication channel. The frequency used by a channel is + * @param channel Which RF channel to communicate on, 0-254 + */ + void setChannel(uint8_t channel); + + /** + * Get RF communication channel + * @return The currently configured RF Channel + */ + uint8_t getChannel(void); + bool setPALevel(int8_t level); private: diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index f3870a5a..db377e33 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -26,9 +26,7 @@ String HoymilesRadio_CMT::cmtChToFreq(const uint8_t channel) void HoymilesRadio_CMT::cmtSwitchChannel(const uint8_t channel) { - yield(); - CMT2300A_SetFrequencyChannel(channel); - yield(); + _radio->setChannel(channel); cmtCurrentCh = channel; } From a11ee472c6fd09487bb893bd2e9e55377c3d27d0 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 10 Apr 2023 00:52:27 +0200 Subject: [PATCH 51/66] Optimize cmtChToFreq method to return float instead of string. Renamed also to getFrequencyFromChannel --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 16 ++++++++-------- lib/Hoymiles/src/HoymilesRadio_CMT.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index db377e33..31c06b38 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -9,9 +9,9 @@ #include #define HOY_BOOT_FREQ 868000000 // Hoymiles boot/init frequency after power up inverter or connection lost for 15 min - +#define HOY_BASE_FREQ 860000000 // offset from initalized CMT base frequency to Hoy base frequency in channels -#define CMT_BASE_CH_OFFSET860 ((860000000 - CMT_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET) +#define CMT_BASE_CH_OFFSET860 ((HOY_BASE_FREQ - CMT_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET) // frequency can not be lower than actual initailized base freq #define CMT_MIN_FREQ_KHZ ((CMT_BASE_FREQ + (CMT_BASE_CH_OFFSET860 >= 1 ? CMT_BASE_CH_OFFSET860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) @@ -19,9 +19,9 @@ // =923500, 0xFF does not work #define CMT_MAX_FREQ_KHZ ((CMT_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) -String HoymilesRadio_CMT::cmtChToFreq(const uint8_t channel) +float HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) { - return String((CMT_BASE_FREQ + (CMT_BASE_CH_OFFSET860 + channel) * FH_OFFSET * CMT2300A_ONE_STEP_SIZE) / 1000000.0, 2) + " MHz"; + return (CMT_BASE_FREQ + (CMT_BASE_CH_OFFSET860 + channel) * FH_OFFSET * CMT2300A_ONE_STEP_SIZE) / 1000000.0; } void HoymilesRadio_CMT::cmtSwitchChannel(const uint8_t channel) @@ -83,7 +83,7 @@ bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const cmtTxBuffer[13] = 0x14; cmtTxBuffer[14] = crc8(cmtTxBuffer, 14); - Hoymiles.getMessageOutput()->printf("TX CMD56 %s --> ", cmtChToFreq(cmtCurrentCh).c_str()); + Hoymiles.getMessageOutput()->printf("TX CMD56 %.2f MHz --> ", getFrequencyFromChannel(cmtCurrentCh)); dumpBuf(cmtTxBuffer, 15); cmtTxLength = 15; @@ -343,7 +343,7 @@ void HoymilesRadio_CMT::loop() if (nullptr != inv) { // Save packet in inverter rx buffer - Hoymiles.getMessageOutput()->printf("RX %s --> ", cmtChToFreq(f.channel).c_str()); + Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel)); dumpBuf(f.fragment, f.len, false); Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); @@ -478,8 +478,8 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) cmd->setRouterAddress(DtuSerial().u64); - Hoymiles.getMessageOutput()->printf("TX %s %s --> ", - cmd->getCommandName().c_str(), cmtChToFreq(cmtCurrentCh).c_str()); + Hoymiles.getMessageOutput()->printf("TX %s %.2f MHz --> ", + cmd->getCommandName().c_str(), getFrequencyFromChannel(cmtCurrentCh)); cmd->dumpDataPayload(Hoymiles.getMessageOutput()); // Still here for to handle CMD56 correctly (inverter serial etc.) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index a2fe1377..a124f949 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -74,7 +74,7 @@ private: uint32_t _inverterTargetFrequency = HOYMILES_CMT_WORK_FREQ; - String cmtChToFreq(const uint8_t channel); + static float getFrequencyFromChannel(const uint8_t channel); void cmtSwitchChannel(const uint8_t channel); uint8_t cmtFreqToChan(const String& func_name, const String& var_name, const uint32_t freq_kHz); bool cmtSwitchDtuFreq(const uint32_t to_freq_kHz); From 1259f0950316d5ff415a849b679d512c87278a16 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 10 Apr 2023 01:00:08 +0200 Subject: [PATCH 52/66] Replace multiple print calls by a single printf in HoymilesRadio_NRF --- lib/Hoymiles/src/HoymilesRadio_NRF.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp index d41085f5..76c78c27 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -240,11 +240,8 @@ void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract* cmd) openWritingPipe(s); _radio->setRetries(3, 15); - Hoymiles.getMessageOutput()->print("TX "); - Hoymiles.getMessageOutput()->print(cmd->getCommandName()); - Hoymiles.getMessageOutput()->print(" Channel: "); - Hoymiles.getMessageOutput()->print(_radio->getChannel()); - Hoymiles.getMessageOutput()->print(" --> "); + Hoymiles.getMessageOutput()->printf("TX %s Channel: %d --> ", + cmd->getCommandName().c_str(), _radio->getChannel()); cmd->dumpDataPayload(Hoymiles.getMessageOutput()); _radio->write(cmd->getDataPayload(), cmd->getDataSize()); From cfb37906cabff86a5066f57a3524544a06cf2038 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 10 Apr 2023 01:13:08 +0200 Subject: [PATCH 53/66] Rename cmtFreqToChan to getChannelFromFrequency and simplify handling of current channel --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 37 +++++++++++--------------- lib/Hoymiles/src/HoymilesRadio_CMT.h | 6 ++--- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 31c06b38..4262e24c 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -24,52 +24,45 @@ float HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) return (CMT_BASE_FREQ + (CMT_BASE_CH_OFFSET860 + channel) * FH_OFFSET * CMT2300A_ONE_STEP_SIZE) / 1000000.0; } -void HoymilesRadio_CMT::cmtSwitchChannel(const uint8_t channel) -{ - _radio->setChannel(channel); - cmtCurrentCh = channel; -} - -uint8_t HoymilesRadio_CMT::cmtFreqToChan(const String& func_name, const String& var_name, const uint32_t freq_kHz) +uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t freq_kHz) { if ((freq_kHz % 250) != 0) { - Hoymiles.getMessageOutput()->printf("%s %s %.3f MHz is not divisible by 250 kHz!\r\n", - func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0); + Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by 250 kHz!\r\n", freq_kHz / 1000.0); return 0xFF; // ERROR } if (freq_kHz < CMT_MIN_FREQ_KHZ || freq_kHz > CMT_MAX_FREQ_KHZ) { - Hoymiles.getMessageOutput()->printf("%s %s %.2f MHz is out of Hoymiles/CMT range! (%.2f MHz - %.2f MHz)\r\n", - func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0, CMT_MIN_FREQ_KHZ / 1000.0, CMT_MAX_FREQ_KHZ / 1000.0); + Hoymiles.getMessageOutput()->printf("%.2f MHz is out of Hoymiles/CMT range! (%.2f MHz - %.2f MHz)\r\n", + freq_kHz / 1000.0, CMT_MIN_FREQ_KHZ / 1000.0, CMT_MAX_FREQ_KHZ / 1000.0); return 0xFF; // ERROR } if (freq_kHz < 863000 || freq_kHz > 870000) { - Hoymiles.getMessageOutput()->printf("%s !!! caution: %s %.2f MHz is out of EU legal range! (863 - 870 MHz)\r\n", - func_name.c_str(), var_name.c_str(), freq_kHz / 1000.0); + Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of EU legal range! (863 - 870 MHz)\r\n", + freq_kHz / 1000.0); } return (freq_kHz * 1000 - CMT_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET - CMT_BASE_CH_OFFSET860; // frequency to channel } bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) { - const uint8_t toChannel = cmtFreqToChan("[cmtSwitchDtuFreq]", "to_freq_kHz", to_freq_kHz); + const uint8_t toChannel = getChannelFromFrequency(to_freq_kHz); if (toChannel == 0xFF) { return false; } - cmtSwitchChannel(toChannel); + _radio->setChannel(toChannel); return true; } bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const uint32_t from_freq_kHz, const uint32_t to_freq_kHz) { - const uint8_t fromChannel = cmtFreqToChan("[cmtSwitchInvAndDtuFreq]", "from_freq_kHz", from_freq_kHz); - const uint8_t toChannel = cmtFreqToChan("[cmtSwitchInvAndDtuFreq]", "to_freq_kHz", to_freq_kHz); + const uint8_t fromChannel = getChannelFromFrequency(from_freq_kHz); + const uint8_t toChannel = getChannelFromFrequency(to_freq_kHz); if (fromChannel == 0xFF || toChannel == 0xFF) { return false; } - cmtSwitchChannel(fromChannel); + _radio->setChannel(fromChannel); cmtTx56toCh = toChannel; // CMD56 for inverter frequency/channel switch @@ -83,7 +76,7 @@ bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const cmtTxBuffer[13] = 0x14; cmtTxBuffer[14] = crc8(cmtTxBuffer, 14); - Hoymiles.getMessageOutput()->printf("TX CMD56 %.2f MHz --> ", getFrequencyFromChannel(cmtCurrentCh)); + Hoymiles.getMessageOutput()->printf("TX CMD56 %.2f MHz --> ", getFrequencyFromChannel(_radio->getChannel())); dumpBuf(cmtTxBuffer, 15); cmtTxLength = 15; @@ -155,7 +148,7 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) fragment_t f; memset(f.fragment, 0xcc, MAX_RF_PAYLOAD_SIZE); CMT2300A_ReadFifo(&f.len, 1); // first byte in FiFo is length - f.channel = cmtCurrentCh; + f.channel = _radio->getChannel(); f.rssi = CMT2300A_GetRssiDBm(); if (f.len > MAX_RF_PAYLOAD_SIZE) { f.len = MAX_RF_PAYLOAD_SIZE; @@ -255,7 +248,7 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) CMT2300A_GoSleep(); if (cmtTx56toCh != 0xFF) { - cmtSwitchChannel(cmtTx56toCh); + _radio->setChannel(cmtTx56toCh); cmtTx56toCh = 0xFF; cmtNextState = CMT_STATE_IDLE; } else { @@ -479,7 +472,7 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) cmd->setRouterAddress(DtuSerial().u64); Hoymiles.getMessageOutput()->printf("TX %s %.2f MHz --> ", - cmd->getCommandName().c_str(), getFrequencyFromChannel(cmtCurrentCh)); + cmd->getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel())); cmd->dumpDataPayload(Hoymiles.getMessageOutput()); // Still here for to handle CMD56 correctly (inverter serial etc.) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index a124f949..a615507f 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -75,8 +75,8 @@ private: uint32_t _inverterTargetFrequency = HOYMILES_CMT_WORK_FREQ; static float getFrequencyFromChannel(const uint8_t channel); - void cmtSwitchChannel(const uint8_t channel); - uint8_t cmtFreqToChan(const String& func_name, const String& var_name, const uint32_t freq_kHz); + static uint8_t getChannelFromFrequency(const uint32_t freq_kHz); + bool cmtSwitchDtuFreq(const uint32_t to_freq_kHz); bool cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const uint32_t from_freq_kHz, const uint32_t to_freq_kHz); enumCMTresult cmtProcess(void); @@ -88,8 +88,6 @@ private: uint32_t cmtRxTimeout = 200; uint32_t cmtRxTimeCount = 0; - uint8_t cmtCurrentCh; // current used channel, should be stored per inverter und set before next Tx, if hopping is used - uint8_t cmtTx56toCh = 0xFF; // send CMD56 active to Channel xx, inactive = 0xFF uint8_t cmtRxTimeoutCnt = 0; // Rx timeout counter !!! should be stored per inverter !!! From f5767e61ef12ccb0ac91884a4d7bee0dd26a34f7 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 6 Apr 2023 22:18:05 +0200 Subject: [PATCH 54/66] Implement CMD56 as own command. By doing so, it's possible to send all packets via the sendEsbPacket method. A lot of stuff could be removed which is no more used. --- lib/Hoymiles/src/Hoymiles.cpp | 4 + lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 132 ++---------------- lib/Hoymiles/src/HoymilesRadio_CMT.h | 20 +-- .../src/commands/ChannelChangeCommand.cpp | 39 ++++++ .../src/commands/ChannelChangeCommand.h | 16 +++ lib/Hoymiles/src/commands/README.md | 1 + lib/Hoymiles/src/inverters/HMS_Abstract.cpp | 20 ++- lib/Hoymiles/src/inverters/HMS_Abstract.h | 2 + lib/Hoymiles/src/inverters/HMT_Abstract.cpp | 16 +++ lib/Hoymiles/src/inverters/HMT_Abstract.h | 2 + .../src/inverters/InverterAbstract.cpp | 5 + lib/Hoymiles/src/inverters/InverterAbstract.h | 1 + 12 files changed, 121 insertions(+), 137 deletions(-) create mode 100644 lib/Hoymiles/src/commands/ChannelChangeCommand.cpp create mode 100644 lib/Hoymiles/src/commands/ChannelChangeCommand.h diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 7bb26b65..4ae3223d 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -58,6 +58,10 @@ void HoymilesClass::loop() _messageOutput->print("Fetch inverter: "); _messageOutput->println(iv->serial(), HEX); + if (!iv->isReachable()) { + iv->sendChangeChannelRequest(); + } + iv->sendStatsRequest(); // Fetch event log diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 4262e24c..771895d6 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -54,39 +54,6 @@ bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) return true; } -bool HoymilesRadio_CMT::cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const uint32_t from_freq_kHz, const uint32_t to_freq_kHz) -{ - const uint8_t fromChannel = getChannelFromFrequency(from_freq_kHz); - const uint8_t toChannel = getChannelFromFrequency(to_freq_kHz); - if (fromChannel == 0xFF || toChannel == 0xFF) { - return false; - } - - _radio->setChannel(fromChannel); - cmtTx56toCh = toChannel; - - // CMD56 for inverter frequency/channel switch - cmtTxBuffer[0] = 0x56; - // cmtTxBuffer[1-4] = last inverter serial - // cmtTxBuffer[5-8] = dtu serial - cmtTxBuffer[9] = 0x02; - cmtTxBuffer[10] = 0x15; - cmtTxBuffer[11] = 0x21; - cmtTxBuffer[12] = (uint8_t)(CMT_BASE_CH_OFFSET860 + toChannel); - cmtTxBuffer[13] = 0x14; - cmtTxBuffer[14] = crc8(cmtTxBuffer, 14); - - Hoymiles.getMessageOutput()->printf("TX CMD56 %.2f MHz --> ", getFrequencyFromChannel(_radio->getChannel())); - dumpBuf(cmtTxBuffer, 15); - - cmtTxLength = 15; - _txTimeout.set(100); - - cmtNextState = CMT_STATE_TX_START; - - return true; -} - enumCMTresult HoymilesRadio_CMT::cmtProcess(void) { enumCMTresult nRes = CMT_BUSY; @@ -142,7 +109,6 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) uint8_t state = CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG); if ((state & 0x1b) == 0x1b) { - cmtRxTimeoutCnt = 0; if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) { fragment_t f; @@ -188,91 +154,9 @@ enumCMTresult HoymilesRadio_CMT::cmtProcess(void) cmtNextState = CMT_STATE_IDLE; - // send CMD56 after 3 Rx timeouts - if (cmtRxTimeoutCnt < 2) { - cmtRxTimeoutCnt++; - } else { - uint32_t invSerial = cmtTxBuffer[1] << 24 | cmtTxBuffer[2] << 16 | cmtTxBuffer[3] << 8 | cmtTxBuffer[4]; // read inverter serial from last Tx buffer - cmtSwitchInvAndDtuFreq(invSerial, HOY_BOOT_FREQ / 1000, _inverterTargetFrequency); - } - nRes = CMT_RX_TIMEOUT; break; - case CMT_STATE_TX_START: - CMT2300A_GoStby(); - CMT2300A_ClearInterruptFlags(); - - /* Must clear FIFO after enable SPI to read or write the FIFO */ - CMT2300A_EnableWriteFifo(); - CMT2300A_ClearTxFifo(); - - CMT2300A_WriteReg(CMT2300A_CUS_PKT15, cmtTxLength); // set Tx length - /* The length need be smaller than 32 */ - CMT2300A_WriteFifo(cmtTxBuffer, cmtTxLength); - - if (!(CMT2300A_ReadReg(CMT2300A_CUS_FIFO_FLAG) & CMT2300A_MASK_TX_FIFO_NMTY_FLG)) { - cmtNextState = CMT_STATE_ERROR; - } - - if (!CMT2300A_GoTx()) { - cmtNextState = CMT_STATE_ERROR; - } else { - cmtNextState = CMT_STATE_TX_WAIT; - } - - _txTimeout.reset(); - - break; - - case CMT_STATE_TX_WAIT: - if (!_gpio2_configured) { - if (CMT2300A_MASK_TX_DONE_FLG & CMT2300A_ReadReg(CMT2300A_CUS_INT_CLR1)) { // read INT1, TX_DONE flag - _packetSent = true; - } - } - if (_packetSent) { - Hoymiles.getMessageOutput()->println(F("Interrupt 1 received")); - _packetSent = false; // reset interrupt 1 - cmtNextState = CMT_STATE_TX_DONE; - } - - if (_txTimeout.occured()) { - cmtNextState = CMT_STATE_TX_TIMEOUT; - } - - break; - - case CMT_STATE_TX_DONE: - CMT2300A_ClearInterruptFlags(); - CMT2300A_GoSleep(); - - if (cmtTx56toCh != 0xFF) { - _radio->setChannel(cmtTx56toCh); - cmtTx56toCh = 0xFF; - cmtNextState = CMT_STATE_IDLE; - } else { - cmtNextState = CMT_STATE_RX_START; // receive answer - } - - nRes = CMT_TX_DONE; - break; - - case CMT_STATE_TX_TIMEOUT: - CMT2300A_GoSleep(); - - Hoymiles.getMessageOutput()->println("TX timeout!"); - - if (cmtTx56toCh != 0xFF) { - cmtTx56toCh = 0xFF; - cmtNextState = CMT_STATE_IDLE; - } - - cmtNextState = CMT_STATE_IDLE; - - nRes = CMT_TX_TIMEOUT; - break; - case CMT_STATE_ERROR: CMT2300A_SoftReset(); CMT2300A_DelayMs(20); @@ -437,6 +321,11 @@ void HoymilesRadio_CMT::setInverterTargetFrequency(uint32_t frequency) cmtSwitchDtuFreq(_inverterTargetFrequency); } +uint32_t HoymilesRadio_CMT::getInverterTargetFrequency() +{ + return _inverterTargetFrequency; +} + bool HoymilesRadio_CMT::isConnected() { if (!_isInitialized) { @@ -471,19 +360,22 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) cmd->setRouterAddress(DtuSerial().u64); + uint8_t oldChannel; + oldChannel = _radio->getChannel(); + if (cmd->getDataPayload()[0] == 0x56) { // @todo(tbnobody) Bad hack to identify ChannelChange Command + cmtSwitchDtuFreq(HOY_BOOT_FREQ / 1000); + } + Hoymiles.getMessageOutput()->printf("TX %s %.2f MHz --> ", cmd->getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel())); cmd->dumpDataPayload(Hoymiles.getMessageOutput()); - // Still here for to handle CMD56 correctly (inverter serial etc.) - memcpy(cmtTxBuffer, cmd->getDataPayload(), cmd->getDataSize()); - if (_radio->write(cmd->getDataPayload(), cmd->getDataSize())) { - _packetSent = false; // still bad hack, to be removed cmtNextState = CMT_STATE_RX_START; } else { Hoymiles.getMessageOutput()->println("TX SPI Timeout"); } + _radio->setChannel(oldChannel); _busyFlag = true; _rxTimeout.set(cmd->getTimeout()); diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index a615507f..0ba8f97c 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -24,10 +24,6 @@ typedef enum { CMT_STATE_RX_WAIT, CMT_STATE_RX_DONE, CMT_STATE_RX_TIMEOUT, - CMT_STATE_TX_START, - CMT_STATE_TX_WAIT, - CMT_STATE_TX_DONE, - CMT_STATE_TX_TIMEOUT, CMT_STATE_ERROR, } enumCMTstate; @@ -37,8 +33,6 @@ typedef enum { CMT_BUSY, CMT_RX_DONE, CMT_RX_TIMEOUT, - CMT_TX_DONE, - CMT_TX_TIMEOUT, CMT_ERROR, } enumCMTresult; @@ -48,12 +42,16 @@ public: void loop(); void setPALevel(int8_t paLevel); void setInverterTargetFrequency(uint32_t frequency); + uint32_t getInverterTargetFrequency(); bool isConnected(); static uint32_t getMinFrequency(); static uint32_t getMaxFrequency(); + static float getFrequencyFromChannel(const uint8_t channel); + static uint8_t getChannelFromFrequency(const uint32_t freq_kHz); + private: void ARDUINO_ISR_ATTR handleInt1(); void ARDUINO_ISR_ATTR handleInt2(); @@ -74,21 +72,11 @@ private: uint32_t _inverterTargetFrequency = HOYMILES_CMT_WORK_FREQ; - static float getFrequencyFromChannel(const uint8_t channel); - static uint8_t getChannelFromFrequency(const uint32_t freq_kHz); - bool cmtSwitchDtuFreq(const uint32_t to_freq_kHz); - bool cmtSwitchInvAndDtuFreq(const uint64_t inv_serial, const uint32_t from_freq_kHz, const uint32_t to_freq_kHz); enumCMTresult cmtProcess(void); enumCMTstate cmtNextState = CMT_STATE_IDLE; - uint8_t cmtTxBuffer[32]; - uint8_t cmtTxLength = 0; uint32_t cmtRxTimeout = 200; uint32_t cmtRxTimeCount = 0; - - uint8_t cmtTx56toCh = 0xFF; // send CMD56 active to Channel xx, inactive = 0xFF - - uint8_t cmtRxTimeoutCnt = 0; // Rx timeout counter !!! should be stored per inverter !!! }; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp new file mode 100644 index 00000000..c6f8dde7 --- /dev/null +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "ChannelChangeCommand.h" + +ChannelChangeCommand::ChannelChangeCommand(uint64_t target_address, uint64_t router_address, uint8_t channel) + : CommandAbstract(target_address, router_address) +{ + _payload[0] = 0x56; + _payload[9] = 0x02; + _payload[10] = 0x15; + _payload[11] = 0x21; + _payload[13] = 0x14; + _payload_size = 14; + + setChannel(channel); + setTimeout(10); +} + +String ChannelChangeCommand::getCommandName() +{ + return "ChannelChangeCommand"; +} + +void ChannelChangeCommand::setChannel(uint8_t channel) +{ + _payload[12] = channel; +} + +uint8_t ChannelChangeCommand::getChannel() +{ + return _payload[12]; +} + +bool ChannelChangeCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +{ + return true; +} diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.h b/lib/Hoymiles/src/commands/ChannelChangeCommand.h new file mode 100644 index 00000000..f8f0eabb --- /dev/null +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "CommandAbstract.h" + +class ChannelChangeCommand : public CommandAbstract { +public: + explicit ChannelChangeCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t channel = 0); + + virtual String getCommandName(); + + void setChannel(uint8_t channel); + uint8_t getChannel(); + + virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/README.md b/lib/Hoymiles/src/commands/README.md index 7e34a210..90ca62d3 100644 --- a/lib/Hoymiles/src/commands/README.md +++ b/lib/Hoymiles/src/commands/README.md @@ -13,3 +13,4 @@ * ParaSetCommand * SingleDataCommand * RequestFrameCommand + * ChannelChangeCommand diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp index 7c0ea34c..30de0038 100644 --- a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp @@ -3,6 +3,24 @@ * Copyright (C) 2023 Thomas Basler and others */ #include "HMS_Abstract.h" +#include "Hoymiles.h" +#include "HoymilesRadio_CMT.h" +#include "commands/ChannelChangeCommand.h" HMS_Abstract::HMS_Abstract(HoymilesRadio* radio, uint64_t serial) - : HM_Abstract(radio, serial) {}; + : HM_Abstract(radio, serial) +{ +} + +bool HMS_Abstract::sendChangeChannelRequest() +{ + if (!(getEnableCommands() && getEnablePolling())) { + return false; + } + + ChannelChangeCommand* cmdChannel = _radio->enqueCommand(); + cmdChannel->setChannel(HoymilesRadio_CMT::getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency())); + cmdChannel->setTargetAddress(serial()); + + return true; +}; diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.h b/lib/Hoymiles/src/inverters/HMS_Abstract.h index 5ec60a01..6d363f6e 100644 --- a/lib/Hoymiles/src/inverters/HMS_Abstract.h +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.h @@ -6,4 +6,6 @@ class HMS_Abstract : public HM_Abstract { public: explicit HMS_Abstract(HoymilesRadio* radio, uint64_t serial); + + virtual bool sendChangeChannelRequest(); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp index d2fc7a4f..9aa2d093 100644 --- a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp @@ -3,6 +3,9 @@ * Copyright (C) 2023 Thomas Basler and others */ #include "HMT_Abstract.h" +#include "Hoymiles.h" +#include "HoymilesRadio_CMT.h" +#include "commands/ChannelChangeCommand.h" #include "parser/AlarmLogParser.h" HMT_Abstract::HMT_Abstract(HoymilesRadio* radio, uint64_t serial) @@ -10,3 +13,16 @@ HMT_Abstract::HMT_Abstract(HoymilesRadio* radio, uint64_t serial) { EventLog()->setMessageType(AlarmMessageType_t::HMT); }; + +bool HMT_Abstract::sendChangeChannelRequest() +{ + if (!(getEnableCommands() && getEnablePolling())) { + return false; + } + + ChannelChangeCommand* cmdChannel = _radio->enqueCommand(); + cmdChannel->setChannel(HoymilesRadio_CMT::getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency())); + cmdChannel->setTargetAddress(serial()); + + return true; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.h b/lib/Hoymiles/src/inverters/HMT_Abstract.h index 17116a95..9e10a2c3 100644 --- a/lib/Hoymiles/src/inverters/HMT_Abstract.h +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.h @@ -6,4 +6,6 @@ class HMT_Abstract : public HM_Abstract { public: explicit HMT_Abstract(HoymilesRadio* radio, uint64_t serial); + + virtual bool sendChangeChannelRequest(); }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 372772f0..25b35e5d 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -96,6 +96,11 @@ bool InverterAbstract::getEnableCommands() return _enableCommands; } +bool InverterAbstract::sendChangeChannelRequest() +{ + return false; +} + HoymilesRadio* InverterAbstract::getRadio() { return _radio; diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index e2a35862..7663bbbe 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -63,6 +63,7 @@ public: virtual bool sendPowerControlRequest(bool turnOn) = 0; virtual bool sendRestartControlRequest() = 0; virtual bool resendPowerControlRequest() = 0; + virtual bool sendChangeChannelRequest(); HoymilesRadio* getRadio(); From e2aa29f117aff3605449dc46632d931bd890d41c Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 11 Apr 2023 21:58:14 +0200 Subject: [PATCH 55/66] Remove cmtProocess method and move RF logic into the cmt2300wrapper class --- lib/CMT2300a/cmt2300wrapper.cpp | 64 ++++++++++ lib/CMT2300a/cmt2300wrapper.h | 56 +++++++++ lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 163 ++++++------------------- lib/Hoymiles/src/HoymilesRadio_CMT.h | 27 +--- 4 files changed, 155 insertions(+), 155 deletions(-) diff --git a/lib/CMT2300a/cmt2300wrapper.cpp b/lib/CMT2300a/cmt2300wrapper.cpp index df3fdfbf..21265f50 100644 --- a/lib/CMT2300a/cmt2300wrapper.cpp +++ b/lib/CMT2300a/cmt2300wrapper.cpp @@ -25,6 +25,46 @@ bool CMT2300A::isChipConnected() return CMT2300A_IsExist(); } +bool CMT2300A::startListening(void) +{ + CMT2300A_GoStby(); + CMT2300A_ClearInterruptFlags(); + + /* Must clear FIFO after enable SPI to read or write the FIFO */ + CMT2300A_EnableReadFifo(); + CMT2300A_ClearRxFifo(); + + if (!CMT2300A_GoRx()) { + return false; + } else { + return true; + } +} + +bool CMT2300A::stopListening(void) +{ + CMT2300A_ClearInterruptFlags(); + return CMT2300A_GoSleep(); +} + +bool CMT2300A::available(void) +{ + return ( + CMT2300A_MASK_PREAM_OK_FLG | + CMT2300A_MASK_SYNC_OK_FLG | + CMT2300A_MASK_CRC_OK_FLG | + CMT2300A_MASK_PKT_OK_FLG + ) & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG); +} + +void CMT2300A::read(void* buf, uint8_t len) +{ + // Fetch the payload + CMT2300A_ReadFifo(static_cast(buf), len); + + CMT2300A_ClearInterruptFlags(); +} + bool CMT2300A::write(const uint8_t* buf, uint8_t len) { CMT2300A_GoStby(); @@ -70,6 +110,18 @@ uint8_t CMT2300A::getChannel(void) return CMT2300A_ReadReg(CMT2300A_CUS_FREQ_CHNL); } +uint8_t CMT2300A::getDynamicPayloadSize(void) +{ + uint8_t result; + CMT2300A_ReadFifo(&result, 1); // first byte in FiFo is length + return result; +} + +int CMT2300A::getRssiDBm() +{ + return CMT2300A_GetRssiDBm(); +} + bool CMT2300A::setPALevel(int8_t level) { uint16_t Tx_dBm_word; @@ -183,6 +235,18 @@ bool CMT2300A::setPALevel(int8_t level) return true; } +bool CMT2300A::rxFifoAvailable() +{ + return ( + CMT2300A_MASK_PKT_OK_FLG + ) & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG); +} + +void CMT2300A::flush_rx(void) +{ + CMT2300A_ClearRxFifo(); +} + bool CMT2300A::_init_pins() { CMT2300A_InitSpi(_pin_sdio, _pin_clk, _pin_cs, _pin_fcs, _spi_speed); diff --git a/lib/CMT2300a/cmt2300wrapper.h b/lib/CMT2300a/cmt2300wrapper.h index 6ae68fdc..5f76b06f 100644 --- a/lib/CMT2300a/cmt2300wrapper.h +++ b/lib/CMT2300a/cmt2300wrapper.h @@ -19,6 +19,43 @@ public: */ bool isChipConnected(); + bool startListening(void); + + bool stopListening(void); + + /** + * Check whether there are bytes available to be read + * @code + * if(radio.available()){ + * radio.read(&data,sizeof(data)); + * } + * @endcode + * + * @see available(uint8_t*) + * + * @return True if there is a payload available, false if none is + */ + bool available(void); + + /** + * Read payload data from the RX FIFO buffer(s). + * + * The length of data read is usually the next available payload's length + * @see + * - getDynamicPayloadSize() + * + * @note I specifically chose `void*` as a data type to make it easier + * for beginners to use. No casting needed. + * + * @param buf Pointer to a buffer where the data should be written + * @param len Maximum number of bytes to read into the buffer. This + * value should match the length of the object referenced using the + * `buf` parameter. The absolute maximum number of bytes that can be read + * in one call is 32 (for dynamic payload lengths) or whatever number was + * previously passed to setPayloadSize() (for static payload lengths). + */ + void read(void* buf, uint8_t len); + bool write(const uint8_t* buf, uint8_t len); /** @@ -33,8 +70,27 @@ public: */ uint8_t getChannel(void); + /** + * Get Dynamic Payload Size + * + * For dynamic payloads, this pulls the size of the payload off + * the chip + * + * @return Payload length of last-received dynamic payload + */ + uint8_t getDynamicPayloadSize(void); + + int getRssiDBm(); + bool setPALevel(int8_t level); + bool rxFifoAvailable(); + + /** + * Empty the RX (receive) FIFO buffers. + */ + void flush_rx(void); + private: /** * initialize the GPIO pins diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 771895d6..fe32afc2 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -6,7 +6,6 @@ #include "Hoymiles.h" #include "crc.h" #include -#include #define HOY_BOOT_FREQ 868000000 // Hoymiles boot/init frequency after power up inverter or connection lost for 15 min #define HOY_BASE_FREQ 860000000 @@ -54,128 +53,6 @@ bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz) return true; } -enumCMTresult HoymilesRadio_CMT::cmtProcess(void) -{ - enumCMTresult nRes = CMT_BUSY; - - switch (cmtNextState) { - case CMT_STATE_IDLE: - nRes = CMT_IDLE; - break; - - case CMT_STATE_RX_START: - CMT2300A_GoStby(); - CMT2300A_ClearInterruptFlags(); - - /* Must clear FIFO after enable SPI to read or write the FIFO */ - CMT2300A_EnableReadFifo(); - CMT2300A_ClearRxFifo(); - - if (!CMT2300A_GoRx()) { - cmtNextState = CMT_STATE_ERROR; - } else { - cmtNextState = CMT_STATE_RX_WAIT; - } - - cmtRxTimeCount = CMT2300A_GetTickCount(); - cmtRxTimeout = 200; - - break; - - case CMT_STATE_RX_WAIT: - if (!_gpio3_configured) { - if (CMT2300A_MASK_PKT_OK_FLG & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG)) { // read INT2, PKT_OK flag - _packetReceived = true; - } - } - - if (_packetReceived) - { - Hoymiles.getMessageOutput()->println("Interrupt 2 received"); - _packetReceived = false; // reset interrupt 2 - cmtNextState = CMT_STATE_RX_DONE; - } - - if ((CMT2300A_GetTickCount() - cmtRxTimeCount) > cmtRxTimeout) { - cmtNextState = CMT_STATE_RX_TIMEOUT; - } - - break; - - case CMT_STATE_RX_DONE: { - CMT2300A_GoStby(); - - bool isLastFrame = false; - - uint8_t state = CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG); - if ((state & 0x1b) == 0x1b) { - - if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) { - fragment_t f; - memset(f.fragment, 0xcc, MAX_RF_PAYLOAD_SIZE); - CMT2300A_ReadFifo(&f.len, 1); // first byte in FiFo is length - f.channel = _radio->getChannel(); - f.rssi = CMT2300A_GetRssiDBm(); - if (f.len > MAX_RF_PAYLOAD_SIZE) { - f.len = MAX_RF_PAYLOAD_SIZE; - } - CMT2300A_ReadFifo(f.fragment, f.len); - if (f.fragment[9] & 0x80) { // last frame detection for end Rx - isLastFrame = true; - } - _rxBuffer.push(f); - } else { - Hoymiles.getMessageOutput()->println("Buffer full"); - } - } else if ((state & 0x19) == 0x19) { - Hoymiles.getMessageOutput()->printf("[CMT_STATE_RX_DONE] state: %x (CRC_ERROR)\r\n", state); - } else { - Hoymiles.getMessageOutput()->printf("[CMT_STATE_RX_DONE] wrong state: %x\r\n", state); - } - - CMT2300A_ClearInterruptFlags(); - - CMT2300A_GoSleep(); - - if (isLastFrame) { // last frame received - cmtNextState = CMT_STATE_IDLE; - } else { - cmtNextState = CMT_STATE_RX_START; // receive next frame(s) - } - - nRes = CMT_RX_DONE; - break; - } - - case CMT_STATE_RX_TIMEOUT: - CMT2300A_GoSleep(); - - Hoymiles.getMessageOutput()->println("RX timeout!"); - - cmtNextState = CMT_STATE_IDLE; - - nRes = CMT_RX_TIMEOUT; - break; - - case CMT_STATE_ERROR: - CMT2300A_SoftReset(); - CMT2300A_DelayMs(20); - - CMT2300A_GoStby(); - _radio->begin(); - - cmtNextState = CMT_STATE_IDLE; - - nRes = CMT_ERROR; - break; - - default: - break; - } - - return nRes; -} - void HoymilesRadio_CMT::init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3) { _dtuSerial.u64 = 0; @@ -210,9 +87,37 @@ void HoymilesRadio_CMT::loop() if (!_isInitialized) { return; } - enumCMTresult mCMTstate = cmtProcess(); - if (mCMTstate != CMT_RX_DONE) { // Perform package parsing only if no packages are received + if (!_gpio3_configured) { + if (_radio->rxFifoAvailable()) { // read INT2, PKT_OK flag + _packetReceived = true; + } + } + + if (_packetReceived) { + Hoymiles.getMessageOutput()->println("Interrupt received"); + while (_radio->available()) { + if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) { + fragment_t f; + memset(f.fragment, 0xcc, MAX_RF_PAYLOAD_SIZE); + f.len = _radio->getDynamicPayloadSize(); + f.channel = _radio->getChannel(); + f.rssi = _radio->getRssiDBm(); + if (f.len > MAX_RF_PAYLOAD_SIZE) { + f.len = MAX_RF_PAYLOAD_SIZE; + } + _radio->read(f.fragment, f.len); + _rxBuffer.push(f); + } else { + Hoymiles.getMessageOutput()->println("Buffer full"); + _radio->flush_rx(); + } + } + _radio->flush_rx(); + _packetReceived = false; + + } else { + // Perform package parsing only if no packages are received if (!_rxBuffer.empty()) { fragment_t f = _rxBuffer.back(); if (checkFragmentCrc(&f)) { @@ -360,6 +265,8 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) cmd->setRouterAddress(DtuSerial().u64); + _radio->stopListening(); + uint8_t oldChannel; oldChannel = _radio->getChannel(); if (cmd->getDataPayload()[0] == 0x56) { // @todo(tbnobody) Bad hack to identify ChannelChange Command @@ -370,13 +277,11 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) cmd->getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel())); cmd->dumpDataPayload(Hoymiles.getMessageOutput()); - if (_radio->write(cmd->getDataPayload(), cmd->getDataSize())) { - cmtNextState = CMT_STATE_RX_START; - } else { + if (!_radio->write(cmd->getDataPayload(), cmd->getDataSize())) { Hoymiles.getMessageOutput()->println("TX SPI Timeout"); } _radio->setChannel(oldChannel); - + _radio->startListening(); _busyFlag = true; _rxTimeout.set(cmd->getTimeout()); } diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index 0ba8f97c..b1cfa7c4 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -6,9 +6,9 @@ #include "commands/CommandAbstract.h" #include "types.h" #include +#include #include #include -#include // number of fragments hold in buffer #define FRAGMENT_BUFFER_SIZE 30 @@ -17,25 +17,6 @@ #define HOYMILES_CMT_WORK_FREQ 865000 #endif -/* CMT states */ -typedef enum { - CMT_STATE_IDLE = 0, - CMT_STATE_RX_START, - CMT_STATE_RX_WAIT, - CMT_STATE_RX_DONE, - CMT_STATE_RX_TIMEOUT, - CMT_STATE_ERROR, -} enumCMTstate; - -/* CMT process function results */ -typedef enum { - CMT_IDLE = 0, - CMT_BUSY, - CMT_RX_DONE, - CMT_RX_TIMEOUT, - CMT_ERROR, -} enumCMTresult; - class HoymilesRadio_CMT : public HoymilesRadio { public: void init(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8_t pin_fcs, int8_t pin_gpio2, int8_t pin_gpio3); @@ -73,10 +54,4 @@ private: uint32_t _inverterTargetFrequency = HOYMILES_CMT_WORK_FREQ; bool cmtSwitchDtuFreq(const uint32_t to_freq_kHz); - enumCMTresult cmtProcess(void); - - enumCMTstate cmtNextState = CMT_STATE_IDLE; - - uint32_t cmtRxTimeout = 200; - uint32_t cmtRxTimeCount = 0; }; \ No newline at end of file From ac7b5dba117a853aac2ce1111cbe8aede848f3fb Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 11 Apr 2023 22:11:10 +0200 Subject: [PATCH 56/66] Resend and Retransmit count is now implementable per command --- lib/Hoymiles/src/commands/CommandAbstract.cpp | 12 +++++++++++- lib/Hoymiles/src/commands/CommandAbstract.h | 8 ++++++++ lib/Hoymiles/src/inverters/InverterAbstract.cpp | 6 +++--- lib/Hoymiles/src/inverters/InverterAbstract.h | 2 -- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/Hoymiles/src/commands/CommandAbstract.cpp b/lib/Hoymiles/src/commands/CommandAbstract.cpp index e7fb4ab5..78d8d07d 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.cpp +++ b/lib/Hoymiles/src/commands/CommandAbstract.cpp @@ -100,4 +100,14 @@ void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], uint64_t serial) void CommandAbstract::gotTimeout(InverterAbstract* inverter) { -} \ No newline at end of file +} + +uint8_t CommandAbstract::getMaxResendCount() +{ + return MAX_RESEND_COUNT; +} + +uint8_t CommandAbstract::getMaxRetransmitCount() +{ + return MAX_RETRANSMIT_COUNT; +} diff --git a/lib/Hoymiles/src/commands/CommandAbstract.h b/lib/Hoymiles/src/commands/CommandAbstract.h index 3baa348e..8029ac87 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.h +++ b/lib/Hoymiles/src/commands/CommandAbstract.h @@ -6,6 +6,8 @@ #include #define RF_LEN 32 +#define MAX_RESEND_COUNT 4 // Used if all packages are missing +#define MAX_RETRANSMIT_COUNT 5 // Used to send the retransmit package class InverterAbstract; @@ -39,6 +41,12 @@ public: virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) = 0; virtual void gotTimeout(InverterAbstract* inverter); + // Sets the amount how often the specific command is resent if all fragments where missing + virtual uint8_t getMaxResendCount(); + + // Sets the amount how often a missing fragment is re-requested if it was not available + virtual uint8_t getMaxRetransmitCount(); + protected: uint8_t _payload[RF_LEN]; uint8_t _payload_size; diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 25b35e5d..3833aac6 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -181,7 +181,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) // All missing if (_rxFragmentLastPacketId == 0) { Hoymiles.getMessageOutput()->println("All missing"); - if (cmd->getSendCount() <= MAX_RESEND_COUNT) { + if (cmd->getSendCount() <= cmd->getMaxResendCount()) { return FRAGMENT_ALL_MISSING_RESEND; } else { cmd->gotTimeout(this); @@ -192,7 +192,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) // Last fragment is missing (the one with 0x80) if (_rxFragmentMaxPacketId == 0) { Hoymiles.getMessageOutput()->println("Last missing"); - if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) { + if (_rxFragmentRetransmitCnt++ < cmd->getMaxRetransmitCount()) { return _rxFragmentLastPacketId + 1; } else { cmd->gotTimeout(this); @@ -204,7 +204,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd) for (uint8_t i = 0; i < _rxFragmentMaxPacketId - 1; i++) { if (!_rxFragmentBuffer[i].wasReceived) { Hoymiles.getMessageOutput()->println("Middle missing"); - if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) { + if (_rxFragmentRetransmitCnt++ < cmd->getMaxRetransmitCount()) { return i + 1; } else { cmd->gotTimeout(this); diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 7663bbbe..755414f2 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -24,8 +24,6 @@ enum { }; #define MAX_RF_FRAGMENT_COUNT 13 -#define MAX_RETRANSMIT_COUNT 5 // Used to send the retransmit package -#define MAX_RESEND_COUNT 4 // Used if all packages are missing #define MAX_ONLINE_FAILURE_COUNT 2 class CommandAbstract; From 8356db94b9ca6a42759de8424b713f2b7b4f8268 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 11 Apr 2023 22:20:13 +0200 Subject: [PATCH 57/66] Send ChannelChangeCommand only once per cycle as the inverter will not response at all --- lib/Hoymiles/src/commands/ChannelChangeCommand.cpp | 6 ++++++ lib/Hoymiles/src/commands/ChannelChangeCommand.h | 2 ++ 2 files changed, 8 insertions(+) diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp index c6f8dde7..139bbea3 100644 --- a/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp @@ -37,3 +37,9 @@ bool ChannelChangeCommand::handleResponse(InverterAbstract* inverter, fragment_t { return true; } + +uint8_t ChannelChangeCommand::getMaxResendCount() +{ + // This command will never retrieve an answer. Therefor it's not required to repeat it + return 0; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.h b/lib/Hoymiles/src/commands/ChannelChangeCommand.h index f8f0eabb..b646217c 100644 --- a/lib/Hoymiles/src/commands/ChannelChangeCommand.h +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.h @@ -13,4 +13,6 @@ public: uint8_t getChannel(); virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); + + virtual uint8_t getMaxResendCount(); }; \ No newline at end of file From 5448a6d0bac29a49a0b9b956a95d0a9cf6873534 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 11 Apr 2023 22:45:26 +0200 Subject: [PATCH 58/66] Doc: Added new supported inverters --- README.md | 57 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f47572fd..d02dd497 100644 --- a/README.md +++ b/README.md @@ -24,28 +24,41 @@ Like to show your own build? Just send me a Pull Request. ## Currently supported Inverters -* Hoymiles HM-300 -* Hoymiles HM-350 -* Hoymiles HM-400 -* Hoymiles HM-600 -* Hoymiles HM-700 -* Hoymiles HM-800 -* Hoymiles HM-1000 -* Hoymiles HM-1200 -* Hoymiles HM-1500 -* Solenso SOL-H350 -* Solenso SOL-H400 -* Solenso SOL-H800 -* TSUN TSOL-M350 (Maybe depending on firmware/serial number on the inverter) -* TSUN TSOL-M800 (Maybe depending on firmware/serial number on the inverter) -* TSUN TSOL-M1600 (Maybe depending on firmware/serial number on the inverter) +| Model | Required RF Module | DC Inputs | AC Phases | +| --------------------| ------------------ | --------- | --------- | +| Hoymiles HM-300 | NRF24L01+ | 1 | 1 | +| Hoymiles HM-350 | NRF24L01+ | 1 | 1 | +| Hoymiles HM-400 | NRF24L01+ | 1 | 1 | +| Hoymiles HM-600 | NRF24L01+ | 2 | 1 | +| Hoymiles HM-700 | NRF24L01+ | 2 | 1 | +| Hoymiles HM-800 | NRF24L01+ | 2 | 1 | +| Hoymiles HM-1000 | NRF24L01+ | 4 | 1 | +| Hoymiles HM-1200 | NRF24L01+ | 4 | 1 | +| Hoymiles HM-1500 | NRF24L01+ | 4 | 1 | +| Hoymiles HMS-300 | CMT2300A | 1 | 1 | +| Hoymiles HMS-350 | CMT2300A | 1 | 1 | +| Hoymiles HMS-400 | CMT2300A | 1 | 1 | +| Hoymiles HMS-450 | CMT2300A | 1 | 1 | +| Hoymiles HMS-500 | CMT2300A | 1 | 1 | +| Hoymiles HMS-600 | CMT2300A | 2 | 1 | +| Hoymiles HMS-700 | CMT2300A | 2 | 1 | +| Hoymiles HMS-800 | CMT2300A | 2 | 1 | +| Hoymiles HMS-900 | CMT2300A | 2 | 1 | +| Hoymiles HMS-1000 | CMT2300A | 2 | 1 | +| Hoymiles HMS-1600 | CMT2300A | 4 | 1 | +| Hoymiles HMS-1800 | CMT2300A | 4 | 1 | +| Hoymiles HMS-2000 | CMT2300A | 4 | 1 | +| Hoymiles HMT-1800 | CMT2300A | 6 | 3 | +| Hoymiles HMT-2250 | CMT2300A | 6 | 3 | +| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | +| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | +| Solenso SOL-H800 | NRF24L01+ | 2 | 1 | +| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | +| TSUN TSOL-M800 | NRF24L01+ | 2 | 1 | +| TSUN TSOL-M1600 | NRF24L01+ | 4 | 1 | **TSUN compatibility remark:** -Compatibility with OpenDTU seems to be related to serial numbers. Current findings indicate that TSUN inverters with a serial number starting with "11" are supported, whereby inverters with a serial number starting with "10" are not. -Firmware version seems to play not a significant role and cannot be read from the stickers. For completeness, the following firmware version have been reported to work with OpenDTU: - -* v1.0.8, v1.0.10 TSOL-M800 (DE) -* v1.0.12 TSOL-M1600 +Compatibility with OpenDTU is most likly related to the serial number of the inverter. Current findings indicate that TSUN inverters with a serial number starting with "11" are supported, whereby inverters with a serial number starting with "10" are not. ## Features for end users @@ -66,6 +79,8 @@ Firmware version seems to play not a significant role and cannot be read from th * Prometheus API endpoint (/api/prometheus/metrics) * English, german and french web interface * Displays (SSD1306, SH1106, PCD8544) +* Status LEDs +* Konfiguration management (export / import configurations) * Dark Theme ## Features for developers @@ -304,7 +319,7 @@ A documentation of the Web API can be found here: [Web-API Documentation](docs/W * OpenDTU needs access to a working NTP server to get the current date & time. * If your problem persists, check the [Issues on Github](https://github.com/tbnobody/OpenDTU/issues). Please inspect not only the open issues, also the closed issues contain useful information. * Another source of information are the [Discussions](https://github.com/tbnobody/OpenDTU/discussions/) -* When flashing with VSCode Plattform.IO fails and also with ESPRESSIF tool a demo bin file cannot be flashed to the ESP32 with error message "A fatal error occurred: MD5 of file does not match data in flash!" than un-wire/unconnect ESP32 from the NRF24L01+ board. Try to flash again and rewire afterwards. +* When flashing with VSCode Plattform.IO fails and also with ESPRESSIF tool a demo bin file cannot be flashed to the ESP32 with error message "A fatal error occurred: MD5 of file does not match data in flash!" than un-wire/unconnect ESP32 from the NRF24L01+ board. Try to flash again and rewire afterwards. ## Related Projects From 71e88e6f732e2afcc0c8b0a585b687b4a4711c99 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 11 Apr 2023 23:05:35 +0200 Subject: [PATCH 59/66] Doc: Added some device profiles containing the CMT2300A pin assignment --- docs/DeviceProfiles/blinkyparts_esp32.json | 102 ++++++++++++++++++++- docs/DeviceProfiles/nodemcu_esp32.json | 28 ++++++ 2 files changed, 126 insertions(+), 4 deletions(-) diff --git a/docs/DeviceProfiles/blinkyparts_esp32.json b/docs/DeviceProfiles/blinkyparts_esp32.json index 8a7da533..0ee922bf 100644 --- a/docs/DeviceProfiles/blinkyparts_esp32.json +++ b/docs/DeviceProfiles/blinkyparts_esp32.json @@ -1,6 +1,6 @@ [ { - "name": "LEDs, Display", + "name": "NRF, LEDs, Display", "nrf24": { "miso": 19, "mosi": 23, @@ -20,7 +20,35 @@ } }, { - "name": "Only Display", + "name": "CMT, LEDs, Display", + "nrf24": { + "miso": -1, + "mosi": -1, + "clk": -1, + "irq": -1, + "en": -1, + "cs": -1 + }, + "cmt": { + "clk": 18, + "cs": 4, + "fcs": 5, + "sdio": 23, + "gpio2": 19, + "gpio3": 16 + }, + "display": { + "type": 3, + "data": 21, + "clk": 22 + }, + "led": { + "led0": 25, + "led1": 26 + } + }, + { + "name": "NRF, Display", "nrf24": { "miso": 19, "mosi": 23, @@ -36,7 +64,31 @@ } }, { - "name": "Only LEDs", + "name": "CMT, Display", + "nrf24": { + "miso": -1, + "mosi": -1, + "clk": -1, + "irq": -1, + "en": -1, + "cs": -1 + }, + "cmt": { + "clk": 18, + "cs": 4, + "fcs": 5, + "sdio": 23, + "gpio2": 19, + "gpio3": 16 + }, + "display": { + "type": 3, + "data": 21, + "clk": 22 + } + }, + { + "name": "NRF, LEDs", "nrf24": { "miso": 19, "mosi": 23, @@ -51,7 +103,30 @@ } }, { - "name": "No Output", + "name": "CMT, LEDs", + "nrf24": { + "miso": -1, + "mosi": -1, + "clk": -1, + "irq": -1, + "en": -1, + "cs": -1 + }, + "cmt": { + "clk": 18, + "cs": 4, + "fcs": 5, + "sdio": 23, + "gpio2": 19, + "gpio3": 16 + }, + "led": { + "led0": 25, + "led1": 26 + } + }, + { + "name": "NRF", "nrf24": { "miso": 19, "mosi": 23, @@ -60,5 +135,24 @@ "en": 4, "cs": 5 } + }, + { + "name": "CMT", + "nrf24": { + "miso": -1, + "mosi": -1, + "clk": -1, + "irq": -1, + "en": -1, + "cs": -1 + }, + "cmt": { + "clk": 18, + "cs": 4, + "fcs": 5, + "sdio": 23, + "gpio2": 19, + "gpio3": 16 + } } ] \ No newline at end of file diff --git a/docs/DeviceProfiles/nodemcu_esp32.json b/docs/DeviceProfiles/nodemcu_esp32.json index e3aa6c57..a42af371 100644 --- a/docs/DeviceProfiles/nodemcu_esp32.json +++ b/docs/DeviceProfiles/nodemcu_esp32.json @@ -19,6 +19,34 @@ "clk_mode": 0 } }, + { + "name": "Generic NodeMCU 32 with CMT2300A", + "nrf24": { + "miso": -1, + "mosi": -1, + "clk": -1, + "irq": -1, + "en": -1, + "cs": -1 + }, + "cmt": { + "clk": 18, + "cs": 4, + "fcs": 5, + "sdio": 23, + "gpio2": 19, + "gpio3": 16 + }, + "eth": { + "enabled": false, + "phy_addr": -1, + "power": -1, + "mdc": -1, + "mdio": -1, + "type": 0, + "clk_mode": 0 + } + }, { "name": "Generic NodeMCU 32 with SSD1306", "nrf24": { From d15b6ffe676cc52cd4ef30c96f0555ba5f280931 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 11 Apr 2023 23:12:20 +0200 Subject: [PATCH 60/66] Doc: Added some remarks regarding the CMT2300A module --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d02dd497..4cb715ab 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Sample Picture: Also supported: Board with Ethernet-Connector and Power-over-Ethernet [Olimex ESP32-POE](https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware) -### NRF24L01+ radio board +### NRF24L01+ radio board (See inverter table above for supported inverters) The PLUS sign is IMPORTANT! There are different variants available, with antenna on the printed circuit board or external antenna. @@ -137,11 +137,19 @@ A heavily incomplete list of trusted hardware shops in germany is: This list is for your convenience only, the project is not related to any of these shops. +### CMT2300A radio board (See inverter table above for supported inverters) + +It is important to get a module which supports SPI communicatiton. The following modules are currently supported: + +* EBYTE E49-900M20S + +The CMT2300A uses 3-Wire half duplex SPI communication. Due to this fact it currently requires a separate SPI bus. If you want to run the CMT2300A module on the same ESP32 as a NRF24L01+ module or a PCD8544 display make sure you get a ESP which supports 2 SPI busses. + ### Power supply Use a power suppy with 5 V and 1 A. The USB cable connected to your PC/Notebook may be powerful enough or may be not. -## Wiring up +## Wiring up the NRF24L01+ module ### Schematic From f1f4322db5edc88266e5103cfdddafc6300a3639 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 13 Apr 2023 19:08:52 +0200 Subject: [PATCH 61/66] Doc: Added MPP-Tracker count --- README.md | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 4cb715ab..4f02667d 100644 --- a/README.md +++ b/README.md @@ -24,38 +24,38 @@ Like to show your own build? Just send me a Pull Request. ## Currently supported Inverters -| Model | Required RF Module | DC Inputs | AC Phases | -| --------------------| ------------------ | --------- | --------- | -| Hoymiles HM-300 | NRF24L01+ | 1 | 1 | -| Hoymiles HM-350 | NRF24L01+ | 1 | 1 | -| Hoymiles HM-400 | NRF24L01+ | 1 | 1 | -| Hoymiles HM-600 | NRF24L01+ | 2 | 1 | -| Hoymiles HM-700 | NRF24L01+ | 2 | 1 | -| Hoymiles HM-800 | NRF24L01+ | 2 | 1 | -| Hoymiles HM-1000 | NRF24L01+ | 4 | 1 | -| Hoymiles HM-1200 | NRF24L01+ | 4 | 1 | -| Hoymiles HM-1500 | NRF24L01+ | 4 | 1 | -| Hoymiles HMS-300 | CMT2300A | 1 | 1 | -| Hoymiles HMS-350 | CMT2300A | 1 | 1 | -| Hoymiles HMS-400 | CMT2300A | 1 | 1 | -| Hoymiles HMS-450 | CMT2300A | 1 | 1 | -| Hoymiles HMS-500 | CMT2300A | 1 | 1 | -| Hoymiles HMS-600 | CMT2300A | 2 | 1 | -| Hoymiles HMS-700 | CMT2300A | 2 | 1 | -| Hoymiles HMS-800 | CMT2300A | 2 | 1 | -| Hoymiles HMS-900 | CMT2300A | 2 | 1 | -| Hoymiles HMS-1000 | CMT2300A | 2 | 1 | -| Hoymiles HMS-1600 | CMT2300A | 4 | 1 | -| Hoymiles HMS-1800 | CMT2300A | 4 | 1 | -| Hoymiles HMS-2000 | CMT2300A | 4 | 1 | -| Hoymiles HMT-1800 | CMT2300A | 6 | 3 | -| Hoymiles HMT-2250 | CMT2300A | 6 | 3 | -| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | -| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | -| Solenso SOL-H800 | NRF24L01+ | 2 | 1 | -| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | -| TSUN TSOL-M800 | NRF24L01+ | 2 | 1 | -| TSUN TSOL-M1600 | NRF24L01+ | 4 | 1 | +| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases | +| --------------------| ------------------ | --------- | ----------- | --------- | +| Hoymiles HM-300 | NRF24L01+ | 1 | 1 | 1 | +| Hoymiles HM-350 | NRF24L01+ | 1 | 1 | 1 | +| Hoymiles HM-400 | NRF24L01+ | 1 | 1 | 1 | +| Hoymiles HM-600 | NRF24L01+ | 2 | 2 | 1 | +| Hoymiles HM-700 | NRF24L01+ | 2 | 2 | 1 | +| Hoymiles HM-800 | NRF24L01+ | 2 | 2 | 1 | +| Hoymiles HM-1000 | NRF24L01+ | 4 | 2 | 1 | +| Hoymiles HM-1200 | NRF24L01+ | 4 | 2 | 1 | +| Hoymiles HM-1500 | NRF24L01+ | 4 | 2 | 1 | +| Hoymiles HMS-300 | CMT2300A | 1 | 1 | 1 | +| Hoymiles HMS-350 | CMT2300A | 1 | 1 | 1 | +| Hoymiles HMS-400 | CMT2300A | 1 | 1 | 1 | +| Hoymiles HMS-450 | CMT2300A | 1 | 1 | 1 | +| Hoymiles HMS-500 | CMT2300A | 1 | 1 | 1 | +| Hoymiles HMS-600 | CMT2300A | 2 | 2 | 1 | +| Hoymiles HMS-700 | CMT2300A | 2 | 2 | 1 | +| Hoymiles HMS-800 | CMT2300A | 2 | 2 | 1 | +| Hoymiles HMS-900 | CMT2300A | 2 | 2 | 1 | +| Hoymiles HMS-1000 | CMT2300A | 2 | 2 | 1 | +| Hoymiles HMS-1600 | CMT2300A | 4 | 4 | 1 | +| Hoymiles HMS-1800 | CMT2300A | 4 | 4 | 1 | +| Hoymiles HMS-2000 | CMT2300A | 4 | 4 | 1 | +| Hoymiles HMT-1800 | CMT2300A | 6 | 3 | 3 | +| Hoymiles HMT-2250 | CMT2300A | 6 | 3 | 3 | +| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | 1 | +| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | 1 | +| Solenso SOL-H800 | NRF24L01+ | 2 | 2 | 1 | +| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 | +| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 | +| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 | **TSUN compatibility remark:** Compatibility with OpenDTU is most likly related to the serial number of the inverter. Current findings indicate that TSUN inverters with a serial number starting with "11" are supported, whereby inverters with a serial number starting with "10" are not. @@ -143,7 +143,7 @@ It is important to get a module which supports SPI communicatiton. The following * EBYTE E49-900M20S -The CMT2300A uses 3-Wire half duplex SPI communication. Due to this fact it currently requires a separate SPI bus. If you want to run the CMT2300A module on the same ESP32 as a NRF24L01+ module or a PCD8544 display make sure you get a ESP which supports 2 SPI busses. +The CMT2300A uses 3-Wire half duplex SPI communication. Due to this fact it currently requires a separate SPI bus. If you want to run the CMT2300A module on the same ESP32 as a NRF24L01+ module or a PCD8544 display make sure you get a ESP which supports 2 SPI busses. Currently the SPI bus host is hardcoded to number 2. This may change in future. ### Power supply From f3942bb647785ab6c757e8054aa68241dda29f6f Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 13 Apr 2023 19:11:18 +0200 Subject: [PATCH 62/66] Fix: Set correct frequency when changing it via web ui Previously it could happen that the frequency was changed between saving old and recovering new frequency. Therefor an invalid frequency was saved in the CMT module --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index fe32afc2..4ca70cc2 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -267,8 +267,6 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) _radio->stopListening(); - uint8_t oldChannel; - oldChannel = _radio->getChannel(); if (cmd->getDataPayload()[0] == 0x56) { // @todo(tbnobody) Bad hack to identify ChannelChange Command cmtSwitchDtuFreq(HOY_BOOT_FREQ / 1000); } @@ -280,7 +278,7 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract* cmd) if (!_radio->write(cmd->getDataPayload(), cmd->getDataSize())) { Hoymiles.getMessageOutput()->println("TX SPI Timeout"); } - _radio->setChannel(oldChannel); + cmtSwitchDtuFreq(_inverterTargetFrequency); _radio->startListening(); _busyFlag = true; _rxTimeout.set(cmd->getTimeout()); From 6856ba99727c75223d9a6d1e6d4eb87725ac8314 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 13 Apr 2023 19:20:08 +0200 Subject: [PATCH 63/66] Fix: Change defines to get a correct calculation if base frequency of CMT module is different compared to Hoymiles base frequency --- lib/Hoymiles/src/HoymilesRadio_CMT.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 4ca70cc2..c69a4ac8 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -10,13 +10,13 @@ #define HOY_BOOT_FREQ 868000000 // Hoymiles boot/init frequency after power up inverter or connection lost for 15 min #define HOY_BASE_FREQ 860000000 // offset from initalized CMT base frequency to Hoy base frequency in channels -#define CMT_BASE_CH_OFFSET860 ((HOY_BASE_FREQ - CMT_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET) +#define CMT_BASE_CH_OFFSET860 ((CMT_BASE_FREQ - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET) // frequency can not be lower than actual initailized base freq -#define CMT_MIN_FREQ_KHZ ((CMT_BASE_FREQ + (CMT_BASE_CH_OFFSET860 >= 1 ? CMT_BASE_CH_OFFSET860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) +#define MIN_FREQ_KHZ ((HOY_BASE_FREQ + (CMT_BASE_CH_OFFSET860 >= 1 ? CMT_BASE_CH_OFFSET860 : 1) * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) // =923500, 0xFF does not work -#define CMT_MAX_FREQ_KHZ ((CMT_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) +#define MAX_FREQ_KHZ ((HOY_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000) float HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) { @@ -29,9 +29,9 @@ uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t freq_kHz) Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by 250 kHz!\r\n", freq_kHz / 1000.0); return 0xFF; // ERROR } - if (freq_kHz < CMT_MIN_FREQ_KHZ || freq_kHz > CMT_MAX_FREQ_KHZ) { + if (freq_kHz < MIN_FREQ_KHZ || freq_kHz > MAX_FREQ_KHZ) { Hoymiles.getMessageOutput()->printf("%.2f MHz is out of Hoymiles/CMT range! (%.2f MHz - %.2f MHz)\r\n", - freq_kHz / 1000.0, CMT_MIN_FREQ_KHZ / 1000.0, CMT_MAX_FREQ_KHZ / 1000.0); + freq_kHz / 1000.0, MIN_FREQ_KHZ / 1000.0, MAX_FREQ_KHZ / 1000.0); return 0xFF; // ERROR } if (freq_kHz < 863000 || freq_kHz > 870000) { @@ -241,12 +241,12 @@ bool HoymilesRadio_CMT::isConnected() uint32_t HoymilesRadio_CMT::getMinFrequency() { - return CMT_MIN_FREQ_KHZ; + return MIN_FREQ_KHZ; } uint32_t HoymilesRadio_CMT::getMaxFrequency() { - return CMT_MAX_FREQ_KHZ; + return MAX_FREQ_KHZ; } void ARDUINO_ISR_ATTR HoymilesRadio_CMT::handleInt1() From 7c37d289c00aa3c284ee0bfa775b3a0305faf94d Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 14 Apr 2023 23:56:18 +0200 Subject: [PATCH 64/66] Enabled additional statistics data for HMT inverters Not yet shown in web ui and mqtt --- lib/Hoymiles/src/inverters/HMT_6CH.h | 20 ++++++++++---------- lib/Hoymiles/src/parser/StatisticsParser.h | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.h b/lib/Hoymiles/src/inverters/HMT_6CH.h index 8c26c493..119d2ec1 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.h +++ b/lib/Hoymiles/src/inverters/HMT_6CH.h @@ -54,20 +54,20 @@ private: { TYPE_DC, CH5, FLD_YD, UNIT_WH, 66, 2, 1, false, 0 }, { TYPE_DC, CH5, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH5, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_UAC, UNIT_V, 68, 2, 10, false, 1 }, // dummy - //{ TYPE_AC, CH0, FLD_UAC_1N, UNIT_V, 68, 2, 10, false, 1 }, - //{ TYPE_AC, CH0, FLD_UAC_2N, UNIT_V, 70, 2, 10, false, 1 }, - //{ TYPE_AC, CH0, FLD_UAC_3N, UNIT_V, 72, 2, 10, false, 1 }, - //{ TYPE_AC, CH0, FLD_UAC_12, UNIT_V, 74, 2, 10, false, 1 }, - //{ TYPE_AC, CH0, FLD_UAC_23, UNIT_V, 76, 2, 10, false, 1 }, - //{ TYPE_AC, CH0, FLD_UAC_31, UNIT_V, 78, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 74, 2, 10, false, 1 }, // dummy + { TYPE_AC, CH0, FLD_UAC_1N, UNIT_V, 68, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_UAC_2N, UNIT_V, 70, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_UAC_3N, UNIT_V, 72, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_UAC_12, UNIT_V, 74, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_UAC_23, UNIT_V, 76, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_UAC_31, UNIT_V, 78, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_F, UNIT_HZ, 80, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_PAC, UNIT_W, 82, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_PRA, UNIT_VA, 84, 2, 10, true, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 86, 2, 100, false, 2 }, // dummy - //{ TYPE_AC, CH0, FLD_IAC_1, UNIT_A, 86, 2, 100, false, 2 }, - //{ TYPE_AC, CH0, FLD_IAC_2, UNIT_A, 88, 2, 100, false, 2 }, - //{ TYPE_AC, CH0, FLD_IAC_3, UNIT_A, 90, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_IAC_1, UNIT_A, 86, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_IAC_2, UNIT_A, 88, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_IAC_3, UNIT_A, 90, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 92, 2, 1000, false, 3 }, { TYPE_INV, CH0, FLD_T, UNIT_C, 94, 2, 10, true, 1 }, diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index 6cea199b..1b09395e 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -38,10 +38,21 @@ enum FieldId_t { FLD_EFF, FLD_IRR, FLD_PRA, - FLD_EVT_LOG + FLD_EVT_LOG, + // HMT only + FLD_UAC_1N, + FLD_UAC_2N, + FLD_UAC_3N, + FLD_UAC_12, + FLD_UAC_23, + FLD_UAC_31, + FLD_IAC_1, + FLD_IAC_2, + FLD_IAC_3 }; const char* const fields[] = { "Voltage", "Current", "Power", "YieldDay", "YieldTotal", - "Voltage", "Current", "Power", "Frequency", "Temperature", "PowerFactor", "Efficiency", "Irradiation", "ReactivePower", "EventLogCount" }; + "Voltage", "Current", "Power", "Frequency", "Temperature", "PowerFactor", "Efficiency", "Irradiation", "ReactivePower", "EventLogCount", + "Voltage Ph1-N", "Voltage Ph2-N", "Voltage Ph3-N", "Voltage Ph1-Ph2", "Voltage Ph2-Ph3", "Voltage Ph3-Ph1", "Current Ph1", "Current Ph2", "Current Ph3" }; // indices to calculation functions, defined in hmInverter.h enum { From 34ac6faefc4a3d68ea1eb8087cd997e8227aab41 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sat, 15 Apr 2023 11:01:40 +0200 Subject: [PATCH 65/66] webapp: Update dependencies --- webapp/package.json | 2 +- webapp/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index 0b82c7ea..a06f3ba4 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -30,7 +30,7 @@ "@vue/eslint-config-typescript": "^11.0.2", "@vue/tsconfig": "^0.1.3", "eslint": "^8.38.0", - "eslint-plugin-vue": "^9.10.0", + "eslint-plugin-vue": "^9.11.0", "npm-run-all": "^4.1.5", "sass": "^1.62.0", "terser": "^5.16.9", diff --git a/webapp/yarn.lock b/webapp/yarn.lock index c2f25136..99dc090e 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -1006,10 +1006,10 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-plugin-vue@^9.10.0: - version "9.10.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.10.0.tgz#bb6423166e6eab800344245b6eef6ce9480c78a7" - integrity sha512-2MgP31OBf8YilUvtakdVMc8xVbcMp7z7/iQj8LHVpXrSXHPXSJRUIGSPFI6b6pyCx/buKaFJ45ycqfHvQRiW2g== +eslint-plugin-vue@^9.11.0: + version "9.11.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.11.0.tgz#99a247455c02181f24d9240d422380fd16dd630c" + integrity sha512-bBCJAZnkBV7ATH4Z1E7CvN3nmtS4H7QUU3UBxPdo8WohRU+yHjnQRALpTbxMVcz0e4Mx3IyxIdP5HYODMxK9cQ== dependencies: "@eslint-community/eslint-utils" "^4.3.0" natural-compare "^1.4.0" From 481bc00f289c94a02c065229af7c2f9f0166524a Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sat, 15 Apr 2023 11:05:57 +0200 Subject: [PATCH 66/66] webapp: add app.js.gz --- webapp_dist/js/app.js.gz | Bin 152261 -> 153817 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index 3eea55befda607d820285f042b76d30cf0c5d01a..bceba0cc78bfadcb2cc77134fd74373dc7f2aead 100644 GIT binary patch delta 88010 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 O%l`)>EZee{ehL5zaRmJU