Merge branch 'development'

This commit is contained in:
helgeerbe 2024-01-16 17:48:46 +01:00
commit 7c66965ced
52 changed files with 2514 additions and 1674 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
# http://editorconfig.org
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

View File

@ -3,6 +3,7 @@
// for the documentation about the extensions.json format
"recommendations": [
"DavidAnson.vscode-markdownlint",
"EditorConfig.EditorConfig",
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"platformio.platformio-ide"

View File

@ -5,7 +5,7 @@
#include <cstdint>
#define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011a00 // 0.1.26 // make sure to clean all after change
#define CONFIG_VERSION 0x00011b00 // 0.1.27 // make sure to clean all after change
#define WIFI_MAX_SSID_STRLEN 32
#define WIFI_MAX_PASSWORD_STRLEN 64
@ -152,6 +152,7 @@ struct CONFIG_T {
struct {
int8_t PaLevel;
uint32_t Frequency;
uint8_t CountryMode;
} Cmt;
bool VerboseLogging;
} Dtu;
@ -167,7 +168,10 @@ struct CONFIG_T {
uint8_t Rotation;
uint8_t Contrast;
uint8_t Language;
uint32_t DiagramDuration;
struct {
uint32_t Duration;
uint8_t Mode;
} Diagram;
} Display;
struct {
@ -256,4 +260,4 @@ public:
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
};
extern ConfigurationClass Configuration;
extern ConfigurationClass Configuration;

View File

@ -6,6 +6,14 @@
#include <TaskSchedulerDeclarations.h>
#include <U8g2lib.h>
#define CHART_HEIGHT 20 // chart area hight in pixels
#define CHART_WIDTH 47 // chart area width in pixels
// Left-Upper position of diagram is drawn
// (text of Y-axis is display left of that pos)
#define CHART_POSX 80
#define CHART_POSY 0
enum DisplayType_t {
None,
PCD8544,
@ -15,6 +23,13 @@ enum DisplayType_t {
DisplayType_Max,
};
enum DiagramMode_t {
Off,
Small,
Fullscreen,
DisplayMode_Max,
};
class DisplayGraphicClass {
public:
DisplayGraphicClass();
@ -25,6 +40,7 @@ public:
void setStatus(const bool turnOn);
void setOrientation(const uint8_t rotation = DISPLAY_ROTATION);
void setLanguage(const uint8_t language);
void setDiagramMode(DiagramMode_t mode);
void setStartupDisplay();
DisplayGraphicDiagramClass& Diagram();
@ -47,14 +63,15 @@ private:
bool _displayTurnedOn;
DisplayType_t _display_type = DisplayType_t::None;
DiagramMode_t _diagram_mode = DiagramMode_t::Off;
uint8_t _display_language = DISPLAY_LANGUAGE;
uint8_t _mExtra;
uint16_t _period = 1000;
uint16_t _interval = 60000; // interval at which to power save (milliseconds)
const uint16_t _period = 1000;
const uint16_t _interval = 60000; // interval at which to power save (milliseconds)
uint32_t _previousMillis = 0;
char _fmtText[32];
bool _isLarge = false;
uint8_t _lineOffsets[5];
};
extern DisplayGraphicClass Display;
extern DisplayGraphicClass Display;

View File

@ -5,20 +5,14 @@
#include <U8g2lib.h>
#include <array>
#define CHART_HEIGHT 20 // chart area hight in pixels
#define CHART_WIDTH 47 // chart area width in pixels
// Left-Upper position of diagram is drawn
// (text of Y-axis is display left of that pos)
#define DIAG_POSX 80
#define DIAG_POSY 0
#define MAX_DATAPOINTS 128
class DisplayGraphicDiagramClass {
public:
DisplayGraphicDiagramClass();
void init(Scheduler& scheduler, U8G2* display);
void redraw(uint8_t screenSaverOffsetX);
void redraw(uint8_t screenSaverOffsetX, uint8_t xPos, uint8_t yPos, uint8_t width, uint8_t height, bool isFullscreen);
void updatePeriod();
@ -26,15 +20,17 @@ private:
void averageLoop();
void dataPointLoop();
static uint32_t getSecondsPerDot();
uint32_t getSecondsPerDot();
Task _averageTask;
Task _dataPointTask;
U8G2* _display = nullptr;
std::array<float, CHART_WIDTH> _graphValues = {};
std::array<float, MAX_DATAPOINTS> _graphValues = {};
uint8_t _graphValuesCount = 0;
uint8_t _chartWidth = MAX_DATAPOINTS;
float _iRunningAverage = 0;
uint16_t _iRunningAverageCnt = 0;
};

View File

@ -10,17 +10,24 @@ public:
void init();
bool updateValues();
float getPower(int8_t phase);
bool httpRequest(const char* url, Auth authType, const char* username, const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout,
char* response, size_t responseSize, char* error, size_t errorSize);
float getFloatValueByJsonPath(const char* jsonString, const char* jsonPath, float &value);
char httpPowerMeterError[256];
bool queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password,
const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath);
private:
void extractUrlComponents(const String& url, String& protocol, String& hostname, String& uri);
void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
HTTPClient httpClient;
float power[POWERMETER_MAX_PHASES];
String sha256(const String& data);
private:
float power[POWERMETER_MAX_PHASES];
HTTPClient httpClient;
String httpResponse;
bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, const String& uri, bool https, Auth authType, const char* username,
const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath);
void extractUrlComponents(const String& url, String& protocol, String& host, String& uri);
String extractParam(String& authReq, const String& param, const char delimit);
String getcNonce(const int len);
String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter);
bool tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath);
void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
String sha256(const String& data);
};
extern HttpPowerMeterClass HttpPowerMeter;

View File

@ -13,4 +13,5 @@ private:
void onDtuAdminPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};
bool _performReload = false;
};

View File

@ -15,6 +15,7 @@ enum WebApiError {
DtuPollZero,
DtuInvalidPowerLevel,
DtuInvalidCmtFrequency,
DtuInvalidCmtCountry,
ConfigBase = 3000,
ConfigNotDeleted,
@ -89,4 +90,4 @@ enum WebApiError {
HardwareBase = 12000,
HardwarePinMappingLength,
};
};

View File

@ -83,7 +83,8 @@
#define DTU_POLL_INTERVAL 5U
#define DTU_NRF_PA_LEVEL 0U
#define DTU_CMT_PA_LEVEL 0
#define DTU_CMT_FREQUENCY 865000U
#define DTU_CMT_FREQUENCY 865000000U
#define DTU_CMT_COUNTRY_MODE 0U
#define MQTT_HASS_ENABLED false
#define MQTT_HASS_EXPIRE true
@ -99,9 +100,58 @@
#define DISPLAY_CONTRAST 60U
#define DISPLAY_LANGUAGE 0U
#define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL)
#define DISPLAY_DIAGRAM_MODE 1U
#define REACHABLE_THRESHOLD 2U
#define MAX_INVERTER_LIMIT 2250
#define VEDIRECT_ENABLED false
#define VEDIRECT_VERBOSE_LOGGING false
#define VEDIRECT_UPDATESONLY true
#define POWERMETER_ENABLED false
#define POWERMETER_INTERVAL 10
#define POWERMETER_SOURCE 2
#define POWERMETER_SDMBAUDRATE 9600
#define POWERMETER_SDMADDRESS 1
#define POWERLIMITER_ENABLED false
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0
#define POWERLIMITER_INTERVAL 10
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
#define POWERLIMITER_INVERTER_ID 0
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
#define POWERLIMITER_LOWER_POWER_LIMIT 10
#define POWERLIMITER_UPPER_POWER_LIMIT 800
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0
#define POWERLIMITER_VOLTAGE_STOP_THRESHOLD 49.0
#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001
#define POWERLIMITER_RESTART_HOUR -1
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC 100
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 100.0
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 100.0
#define BATTERY_ENABLED false
#define BATTERY_PROVIDER 0 // Pylontech CAN receiver
#define BATTERY_JKBMS_INTERFACE 0
#define BATTERY_JKBMS_POLLING_INTERVAL 5
#define HUAWEI_ENABLED false
#define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL
#define HUAWEI_AUTO_POWER_VOLTAGE_LIMIT 42.0
#define HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT 42.0
#define HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT 150
#define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000
#define VERBOSE_LOGGING true
#define LED_BRIGHTNESS 100U
#define MAX_INVERTER_LIMIT 2250
@ -154,4 +204,4 @@
#define LED_BRIGHTNESS 100U
#define MAX_INVERTER_LIMIT 2250
#define MAX_INVERTER_LIMIT 2250

File diff suppressed because it is too large Load Diff

View File

@ -1,96 +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 <stdint.h>
#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
/*
* 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 <stdint.h>
#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(const 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

View File

@ -1,76 +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 <Arduino.h>
/*! ********************************************************
* @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, uint32_t spi_speed)
{
cmt_spi3_init(pin_sdio, pin_clk, pin_cs, pin_fcs, spi_speed);
}
/*! ********************************************************
* @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);
}
/*
* 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 <Arduino.h>
/*! ********************************************************
* @name CMT2300A_InitSpi
* @desc Initializes the CMT2300A SPI interface.
* *********************************************************/
void CMT2300A_InitSpi(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed)
{
cmt_spi3_init(pin_sdio, pin_clk, pin_cs, pin_fcs, spi_speed);
}
/*! ********************************************************
* @name CMT2300A_ReadReg
* @desc Read the CMT2300A register at the specified address.
* @param addr: register address
* @return Register value
* *********************************************************/
uint8_t CMT2300A_ReadReg(const 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(const uint8_t addr, const 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[], const 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[], const uint16_t len)
{
cmt_spi3_write_fifo(buf, len);
}

View File

@ -1,51 +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 <stdint.h>
#include <Arduino.h>
#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(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);
void CMT2300A_ReadFifo(uint8_t buf[], uint16_t len);
void CMT2300A_WriteFifo(const uint8_t buf[], uint16_t len);
#ifdef __cplusplus
}
#endif
#endif
/*
* 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 <stdint.h>
#include <Arduino.h>
#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(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed);
uint8_t CMT2300A_ReadReg(const uint8_t addr);
void CMT2300A_WriteReg(const uint8_t addr, const uint8_t dat);
void CMT2300A_ReadFifo(uint8_t buf[], const uint16_t len);
void CMT2300A_WriteFifo(const uint8_t buf[], const uint16_t len);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -85,14 +85,14 @@
; RSSI Offset = 0
; RSSI Offset Sign = 0
*/
#ifndef __CMT2300A_PARAMS_H
#define __CMT2300A_PARAMS_H
#ifndef __CMT2300A_PARAMS_860_H
#define __CMT2300A_PARAMS_860_H
#include "cmt2300a_defs.h"
#include <stdint.h>
/* [CMT Bank] with RSSI offset of +- 0 (and Tx power double bit not set) */
static uint8_t g_cmt2300aCmtBank[CMT2300A_CMT_BANK_SIZE] = {
static uint8_t g_cmt2300aCmtBank_860[CMT2300A_CMT_BANK_SIZE] = {
0x00,
0x66,
0xEC,
@ -108,7 +108,7 @@ static uint8_t g_cmt2300aCmtBank[CMT2300A_CMT_BANK_SIZE] = {
};
/* [System Bank] */
static uint8_t g_cmt2300aSystemBank[CMT2300A_SYSTEM_BANK_SIZE] = {
static uint8_t g_cmt2300aSystemBank_860[CMT2300A_SYSTEM_BANK_SIZE] = {
0xAE,
0xE0,
0x35,
@ -124,7 +124,7 @@ static uint8_t g_cmt2300aSystemBank[CMT2300A_SYSTEM_BANK_SIZE] = {
};
/* [Frequency Bank] 860 MHz */
static uint8_t g_cmt2300aFrequencyBank[CMT2300A_FREQUENCY_BANK_SIZE] = {
static uint8_t g_cmt2300aFrequencyBank_860[CMT2300A_FREQUENCY_BANK_SIZE] = {
0x42,
0x32,
0xCF,
@ -136,7 +136,7 @@ static uint8_t g_cmt2300aFrequencyBank[CMT2300A_FREQUENCY_BANK_SIZE] = {
};
/* [Data Rate Bank] */
static uint8_t g_cmt2300aDataRateBank[CMT2300A_DATA_RATE_BANK_SIZE] = {
static uint8_t g_cmt2300aDataRateBank_860[CMT2300A_DATA_RATE_BANK_SIZE] = {
0xA6,
0xC9,
0x20,
@ -164,7 +164,7 @@ static uint8_t g_cmt2300aDataRateBank[CMT2300A_DATA_RATE_BANK_SIZE] = {
};
/* [Baseband Bank] - EU */
static uint8_t g_cmt2300aBasebandBank[CMT2300A_BASEBAND_BANK_SIZE] = {
static uint8_t g_cmt2300aBasebandBank_860[CMT2300A_BASEBAND_BANK_SIZE] = {
0x12,
0x1E,
0x00,
@ -197,7 +197,7 @@ static uint8_t g_cmt2300aBasebandBank[CMT2300A_BASEBAND_BANK_SIZE] = {
};
/* [Tx Bank] 13 dBm */
static uint8_t g_cmt2300aTxBank[CMT2300A_TX_BANK_SIZE] = {
static uint8_t g_cmt2300aTxBank_860[CMT2300A_TX_BANK_SIZE] = {
0x70,
0x4D,
0x06,

View File

@ -0,0 +1,214 @@
/*
;---------------------------------------
; CMT2300A Configuration File
; Generated by CMOSTEK RFPDK 1.46
; 2023.03.17 23:16
;---------------------------------------
; Mode = Advanced
; Part Number = CMT2300A
; Frequency = 900.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_900_H
#define __CMT2300A_PARAMS_900_H
#include "cmt2300a_defs.h"
#include <stdint.h>
/* [CMT Bank] with RSSI offset of +- 0 (and Tx power double bit not set) */
static uint8_t g_cmt2300aCmtBank_900[CMT2300A_CMT_BANK_SIZE] = {
0x00,
0x66,
0xEC,
0x1C,
0x70,
0x80,
0x14,
0x08,
0x11,
0x02,
0x02,
0x00,
};
/* [System Bank] */
static uint8_t g_cmt2300aSystemBank_900[CMT2300A_SYSTEM_BANK_SIZE] = {
0xAE,
0xE0,
0x35,
0x00,
0x00,
0xF4,
0x10,
0xE2,
0x42,
0x20,
0x0C,
0x81,
};
/* [Frequency Bank] 900 MHz */
static uint8_t g_cmt2300aFrequencyBank_900[CMT2300A_FREQUENCY_BANK_SIZE] = {
0x45,
0x46,
0x0A,
0x84,
0x45,
0x3B,
0xB1,
0x13,
};
/* [Data Rate Bank] */
static uint8_t g_cmt2300aDataRateBank_900[CMT2300A_DATA_RATE_BANK_SIZE] = {
0xA6,
0xC9,
0x20,
0x20,
0xD2,
0x35,
0x0C,
0x0B,
0x9F,
0x4B,
0x29,
0x29,
0xC0,
0x14,
0x05,
0x53,
0x10,
0x00,
0xB4,
0x00,
0x00,
0x01,
0x00,
0x00,
};
/* [Baseband Bank] - EU */
static uint8_t g_cmt2300aBasebandBank_900[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_900[CMT2300A_TX_BANK_SIZE] = {
0x70,
0x4D,
0x06,
0x00,
0x07,
0x50,
0x00,
0x53,
0x09,
0x3F,
0x7F,
};
#endif

View File

@ -1,12 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Thomas Basler and others
* Copyright (C) 2023-2024 Thomas Basler and others
*/
#include "cmt2300wrapper.h"
#include "cmt2300a.h"
#include "cmt2300a_params.h"
#include "cmt2300a_params_860.h"
#include "cmt2300a_params_900.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(const uint8_t pin_sdio, const uint8_t pin_clk, const uint8_t pin_cs, const uint8_t pin_fcs, const uint32_t spi_speed)
{
_pin_sdio = pin_sdio;
_pin_clk = pin_clk;
@ -57,7 +58,7 @@ bool CMT2300A::available(void)
) & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG);
}
void CMT2300A::read(void* buf, uint8_t len)
void CMT2300A::read(void* buf, const uint8_t len)
{
// Fetch the payload
CMT2300A_ReadFifo(static_cast<uint8_t*>(buf), len);
@ -65,7 +66,7 @@ void CMT2300A::read(void* buf, uint8_t len)
CMT2300A_ClearInterruptFlags();
}
bool CMT2300A::write(const uint8_t* buf, uint8_t len)
bool CMT2300A::write(const uint8_t* buf, const uint8_t len)
{
CMT2300A_GoStby();
CMT2300A_ClearInterruptFlags();
@ -100,7 +101,7 @@ bool CMT2300A::write(const uint8_t* buf, uint8_t len)
return true;
}
void CMT2300A::setChannel(uint8_t channel)
void CMT2300A::setChannel(const uint8_t channel)
{
CMT2300A_SetFrequencyChannel(channel);
}
@ -122,7 +123,7 @@ int CMT2300A::getRssiDBm()
return CMT2300A_GetRssiDBm();
}
bool CMT2300A::setPALevel(int8_t level)
bool CMT2300A::setPALevel(const int8_t level)
{
uint16_t Tx_dBm_word;
switch (level) {
@ -242,6 +243,22 @@ bool CMT2300A::rxFifoAvailable()
) & CMT2300A_ReadReg(CMT2300A_CUS_INT_FLAG);
}
uint32_t CMT2300A::getBaseFrequency() const
{
return getBaseFrequency(_frequencyBand);
}
FrequencyBand_t CMT2300A::getFrequencyBand() const
{
return _frequencyBand;
}
void CMT2300A::setFrequencyBand(const FrequencyBand_t mode)
{
_frequencyBand = mode;
_init_radio();
}
void CMT2300A::flush_rx(void)
{
CMT2300A_ClearRxFifo();
@ -261,12 +278,24 @@ bool CMT2300A::_init_radio()
}
/* 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);
switch (_frequencyBand) {
case FrequencyBand_t::BAND_900:
CMT2300A_ConfigRegBank(CMT2300A_CMT_BANK_ADDR, g_cmt2300aCmtBank_900, CMT2300A_CMT_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_SYSTEM_BANK_ADDR, g_cmt2300aSystemBank_900, CMT2300A_SYSTEM_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_FREQUENCY_BANK_ADDR, g_cmt2300aFrequencyBank_900, CMT2300A_FREQUENCY_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_DATA_RATE_BANK_ADDR, g_cmt2300aDataRateBank_900, CMT2300A_DATA_RATE_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_BASEBAND_BANK_ADDR, g_cmt2300aBasebandBank_900, CMT2300A_BASEBAND_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_TX_BANK_ADDR, g_cmt2300aTxBank_900, CMT2300A_TX_BANK_SIZE);
break;
default:
CMT2300A_ConfigRegBank(CMT2300A_CMT_BANK_ADDR, g_cmt2300aCmtBank_860, CMT2300A_CMT_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_SYSTEM_BANK_ADDR, g_cmt2300aSystemBank_860, CMT2300A_SYSTEM_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_FREQUENCY_BANK_ADDR, g_cmt2300aFrequencyBank_860, CMT2300A_FREQUENCY_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_DATA_RATE_BANK_ADDR, g_cmt2300aDataRateBank_860, CMT2300A_DATA_RATE_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_BASEBAND_BANK_ADDR, g_cmt2300aBasebandBank_860, CMT2300A_BASEBAND_BANK_SIZE);
CMT2300A_ConfigRegBank(CMT2300A_TX_BANK_ADDR, g_cmt2300aTxBank_860, CMT2300A_TX_BANK_SIZE);
break;
}
// xosc_aac_code[2:0] = 2
uint8_t tmp;

View File

@ -4,13 +4,21 @@
#include <stdint.h>
#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
#define CMT_BASE_FREQ_900 900000000
#define CMT_BASE_FREQ_860 860000000
enum FrequencyBand_t {
BAND_860,
BAND_900,
FrequencyBand_Max,
};
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(const uint8_t pin_sdio, const uint8_t pin_clk, const uint8_t pin_cs, const uint8_t pin_fcs, const uint32_t _spi_speed = CMT_SPI_SPEED);
bool begin(void);
@ -54,15 +62,15 @@ public:
* 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);
void read(void* buf, const uint8_t len);
bool write(const uint8_t* buf, uint8_t len);
bool write(const uint8_t* buf, const 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);
void setChannel(const uint8_t channel);
/**
* Get RF communication channel
@ -82,10 +90,26 @@ public:
int getRssiDBm();
bool setPALevel(int8_t level);
bool setPALevel(const int8_t level);
bool rxFifoAvailable();
uint32_t getBaseFrequency() const;
static constexpr uint32_t getBaseFrequency(FrequencyBand_t band)
{
switch (band) {
case FrequencyBand_t::BAND_900:
return CMT_BASE_FREQ_900;
break;
default:
return CMT_BASE_FREQ_860;
break;
}
}
FrequencyBand_t getFrequencyBand() const;
void setFrequencyBand(const FrequencyBand_t mode);
/**
* Empty the RX (receive) FIFO buffers.
*/
@ -109,4 +133,6 @@ private:
int8_t _pin_cs;
int8_t _pin_fcs;
uint32_t _spi_speed;
};
FrequencyBand_t _frequencyBand = FrequencyBand_t::BAND_860;
};

View File

@ -1,142 +1,142 @@
#include "cmt_spi3.h"
#include <Arduino.h>
#include <driver/spi_master.h>
#include <esp_rom_gpio.h> // for esp_rom_gpio_connect_out_signal
SemaphoreHandle_t paramLock = NULL;
#define SPI_PARAM_LOCK() \
do { \
} while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS)
#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock)
// for ESP32 this is the so-called HSPI
// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore,
// it is simply the first externally usable hardware SPI master controller
#define SPI_CMT SPI2_HOST
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, uint32_t spi_speed)
{
paramLock = xSemaphoreCreateMutex();
spi_bus_config_t buscfg = {
.mosi_io_num = pin_sdio,
.miso_io_num = -1, // single wire MOSI/MISO
.sclk_io_num = pin_clk,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 32,
};
spi_device_interface_config_t devcfg = {
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
.mode = 0, // SPI mode 0
.cs_ena_pretrans = 1,
.cs_ena_posttrans = 1,
.clock_speed_hz = spi_speed,
.spics_io_num = pin_cs,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED));
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &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 / (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,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo));
esp_rom_gpio_connect_out_signal(pin_sdio, spi_periph_signal[SPI_CMT].spid_out, true, false);
delay(100);
}
void cmt_spi3_write(uint8_t addr, uint8_t dat)
{
uint8_t tx_data;
tx_data = ~dat;
spi_transaction_t t = {
.cmd = 1,
.addr = ~addr,
.length = 8,
.tx_buffer = &tx_data,
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
delayMicroseconds(100);
}
uint8_t cmt_spi3_read(uint8_t addr)
{
uint8_t rx_data;
spi_transaction_t t = {
.cmd = 0,
.addr = ~addr,
.length = 8,
.rxlength = 8,
.tx_buffer = NULL,
.rx_buffer = &rx_data
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
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 = {
.length = 8,
.tx_buffer = &tx_data, // reference to write data
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
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
}
SPI_PARAM_UNLOCK();
}
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
};
SPI_PARAM_LOCK();
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;
}
SPI_PARAM_UNLOCK();
}
#include "cmt_spi3.h"
#include <Arduino.h>
#include <driver/spi_master.h>
#include <esp_rom_gpio.h> // for esp_rom_gpio_connect_out_signal
SemaphoreHandle_t paramLock = NULL;
#define SPI_PARAM_LOCK() \
do { \
} while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS)
#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock)
// for ESP32 this is the so-called HSPI
// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore,
// it is simply the first externally usable hardware SPI master controller
#define SPI_CMT SPI2_HOST
spi_device_handle_t spi_reg, spi_fifo;
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed)
{
paramLock = xSemaphoreCreateMutex();
spi_bus_config_t buscfg = {
.mosi_io_num = pin_sdio,
.miso_io_num = -1, // single wire MOSI/MISO
.sclk_io_num = pin_clk,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 32,
};
spi_device_interface_config_t devcfg = {
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
.mode = 0, // SPI mode 0
.cs_ena_pretrans = 1,
.cs_ena_posttrans = 1,
.clock_speed_hz = spi_speed,
.spics_io_num = pin_cs,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED));
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &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 / (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,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo));
esp_rom_gpio_connect_out_signal(pin_sdio, spi_periph_signal[SPI_CMT].spid_out, true, false);
delay(100);
}
void cmt_spi3_write(const uint8_t addr, const uint8_t dat)
{
uint8_t tx_data;
tx_data = ~dat;
spi_transaction_t t = {
.cmd = 1,
.addr = ~addr,
.length = 8,
.tx_buffer = &tx_data,
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
delayMicroseconds(100);
}
uint8_t cmt_spi3_read(const uint8_t addr)
{
uint8_t rx_data;
spi_transaction_t t = {
.cmd = 0,
.addr = ~addr,
.length = 8,
.rxlength = 8,
.tx_buffer = NULL,
.rx_buffer = &rx_data
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
delayMicroseconds(100);
return rx_data;
}
void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len)
{
uint8_t tx_data;
spi_transaction_t t = {
.length = 8,
.tx_buffer = &tx_data, // reference to write data
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
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
}
SPI_PARAM_UNLOCK();
}
void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len)
{
uint8_t rx_data;
spi_transaction_t t = {
.length = 8,
.rxlength = 8,
.tx_buffer = NULL,
.rx_buffer = &rx_data
};
SPI_PARAM_LOCK();
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;
}
SPI_PARAM_UNLOCK();
}

View File

@ -1,14 +1,14 @@
#ifndef __CMT_SPI3_H
#define __CMT_SPI3_H
#include <stdint.h>
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);
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
#ifndef __CMT_SPI3_H
#define __CMT_SPI3_H
#include <stdint.h>
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed);
void cmt_spi3_write(const uint8_t addr, const uint8_t dat);
uint8_t cmt_spi3_read(const uint8_t addr);
void cmt_spi3_write_fifo(const uint8_t* p_buf, const uint16_t len);
void cmt_spi3_read_fifo(uint8_t* p_buf, const uint16_t len);
#endif

View File

@ -1,49 +1,79 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Thomas Basler and others
* Copyright (C) 2023-2024 Thomas Basler and others
*/
#include "HoymilesRadio_CMT.h"
#include "Hoymiles.h"
#include "crc.h"
#include <FunctionalInterrupt.h>
#include <frozen/map.h>
#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 ((CMT_BASE_FREQ - HOY_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET)
// frequency can not be lower than actual initailized base freq
#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 MAX_FREQ_KHZ ((HOY_BASE_FREQ + 0xFE * CMT2300A_ONE_STEP_SIZE * FH_OFFSET) / 1000)
float HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel)
constexpr CountryFrequencyDefinition_t make_value(FrequencyBand_t Band, uint32_t Freq_Legal_Min, uint32_t Freq_Legal_Max, uint32_t Freq_Default, uint32_t Freq_StartUp)
{
return (CMT_BASE_FREQ + (CMT_BASE_CH_OFFSET860 + channel) * FH_OFFSET * CMT2300A_ONE_STEP_SIZE) / 1000000.0;
// frequency can not be lower than actual initailized base freq + 250000
uint32_t minFrequency = CMT2300A::getBaseFrequency(Band) + HoymilesRadio_CMT::getChannelWidth();
// =923500, 0xFF does not work
uint32_t maxFrequency = CMT2300A::getBaseFrequency(Band) + 0xFE * HoymilesRadio_CMT::getChannelWidth();
CountryFrequencyDefinition_t v = { Band, minFrequency, maxFrequency, Freq_Legal_Min, Freq_Legal_Max, Freq_Default, Freq_StartUp };
return v;
}
uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t freq_kHz)
constexpr frozen::map<CountryModeId_t, CountryFrequencyDefinition_t, 3> countryDefinition = {
{ CountryModeId_t::MODE_EU, make_value(FrequencyBand_t::BAND_860, 863e6, 870e6, 865e6, 868e6) },
{ CountryModeId_t::MODE_US, make_value(FrequencyBand_t::BAND_900, 905e6, 925e6, 918e6, 915e6) },
{ CountryModeId_t::MODE_BR, make_value(FrequencyBand_t::BAND_900, 915e6, 928e6, 918e6, 915e6) },
};
uint32_t HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) const
{
if ((freq_kHz % 250) != 0) {
Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by 250 kHz!\r\n", freq_kHz / 1000.0);
return (_radio->getBaseFrequency() + channel * getChannelWidth());
}
uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) const
{
if ((frequency % getChannelWidth()) != 0) {
Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %d kHz!\r\n", frequency / 1000000.0, getChannelWidth());
return 0xFF; // ERROR
}
if (freq_kHz < MIN_FREQ_KHZ || freq_kHz > MAX_FREQ_KHZ) {
if (frequency < getMinFrequency() || frequency > getMaxFrequency()) {
Hoymiles.getMessageOutput()->printf("%.2f MHz is out of Hoymiles/CMT range! (%.2f MHz - %.2f MHz)\r\n",
freq_kHz / 1000.0, MIN_FREQ_KHZ / 1000.0, MAX_FREQ_KHZ / 1000.0);
frequency / 1000000.0, getMinFrequency() / 1000000.0, getMaxFrequency() / 1000000.0);
return 0xFF; // ERROR
}
if (freq_kHz < 863000 || freq_kHz > 870000) {
Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of EU legal range! (863 - 870 MHz)\r\n",
freq_kHz / 1000.0);
if (frequency < countryDefinition.at(_countryMode).Freq_Legal_Min || frequency > countryDefinition.at(_countryMode).Freq_Legal_Max) {
Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%d - %d MHz)\r\n",
frequency / 1000000.0,
static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Min / 1e6),
static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Max / 1e6));
}
return (freq_kHz * 1000 - CMT_BASE_FREQ) / CMT2300A_ONE_STEP_SIZE / FH_OFFSET - CMT_BASE_CH_OFFSET860; // frequency to channel
return (frequency - _radio->getBaseFrequency()) / getChannelWidth(); // frequency to channel
}
bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_freq_kHz)
std::vector<CountryFrequencyList_t> HoymilesRadio_CMT::getCountryFrequencyList() const
{
const uint8_t toChannel = getChannelFromFrequency(to_freq_kHz);
std::vector<CountryFrequencyList_t> v;
for (const auto& [key, value] : countryDefinition) {
CountryFrequencyList_t s;
s.mode = key;
s.definition.Band = value.Band;
s.definition.Freq_Default = value.Freq_Default;
s.definition.Freq_StartUp = value.Freq_StartUp;
s.definition.Freq_Min = value.Freq_Min;
s.definition.Freq_Max = value.Freq_Max;
s.definition.Freq_Legal_Max = value.Freq_Legal_Max;
s.definition.Freq_Legal_Min = value.Freq_Legal_Min;
v.push_back(s);
}
return v;
}
bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_frequency)
{
const uint8_t toChannel = getChannelFromFrequency(to_frequency);
if (toChannel == 0xFF) {
return false;
}
@ -61,6 +91,7 @@ void HoymilesRadio_CMT::init(const int8_t pin_sdio, const int8_t pin_clk, const
_radio->begin();
setCountryMode(CountryModeId_t::MODE_EU);
cmtSwitchDtuFreq(_inverterTargetFrequency); // start dtu at work freqency, for fast Rx if inverter is already on and frequency switched
if (!_radio->isChipConnected()) {
@ -134,7 +165,7 @@ void HoymilesRadio_CMT::loop()
if (nullptr != inv) {
// Save packet in inverter rx buffer
Hoymiles.getVerboseMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel));
Hoymiles.getVerboseMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0);
dumpBuf(f.fragment, f.len, false);
Hoymiles.getVerboseMessageOutput()->printf("| %d dBm\r\n", f.rssi);
@ -191,14 +222,31 @@ bool HoymilesRadio_CMT::isConnected() const
return _radio->isChipConnected();
}
uint32_t HoymilesRadio_CMT::getMinFrequency()
uint32_t HoymilesRadio_CMT::getMinFrequency() const
{
return MIN_FREQ_KHZ;
return countryDefinition.at(_countryMode).Freq_Min;
}
uint32_t HoymilesRadio_CMT::getMaxFrequency()
uint32_t HoymilesRadio_CMT::getMaxFrequency() const
{
return MAX_FREQ_KHZ;
return countryDefinition.at(_countryMode).Freq_Max;
}
CountryModeId_t HoymilesRadio_CMT::getCountryMode() const
{
return _countryMode;
}
void HoymilesRadio_CMT::setCountryMode(const CountryModeId_t mode)
{
_radio->setFrequencyBand(countryDefinition.at(mode).Band);
_countryMode = mode;
}
uint32_t HoymilesRadio_CMT::getInvBootFrequency() const
{
// Hoymiles boot/init frequency after power up inverter or connection lost for 15 min
return countryDefinition.at(_countryMode).Freq_StartUp;
}
void ARDUINO_ISR_ATTR HoymilesRadio_CMT::handleInt1()
@ -220,11 +268,11 @@ void HoymilesRadio_CMT::sendEsbPacket(CommandAbstract& cmd)
_radio->stopListening();
if (cmd.getDataPayload()[0] == 0x56) { // @todo(tbnobody) Bad hack to identify ChannelChange Command
cmtSwitchDtuFreq(HOY_BOOT_FREQ / 1000);
cmtSwitchDtuFreq(getInvBootFrequency());
}
Hoymiles.getVerboseMessageOutput()->printf("TX %s %.2f MHz --> ",
cmd.getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel()));
cmd.getCommandName().c_str(), getFrequencyFromChannel(_radio->getChannel()) / 1000000.0);
cmd.dumpDataPayload(Hoymiles.getVerboseMessageOutput());
if (!_radio->write(cmd.getDataPayload(), cmd.getDataSize())) {

View File

@ -8,14 +8,37 @@
#include <cmt2300wrapper.h>
#include <memory>
#include <queue>
#include <vector>
// number of fragments hold in buffer
#define FRAGMENT_BUFFER_SIZE 30
#ifndef HOYMILES_CMT_WORK_FREQ
#define HOYMILES_CMT_WORK_FREQ 865000
#define HOYMILES_CMT_WORK_FREQ 865000000
#endif
enum CountryModeId_t {
MODE_EU,
MODE_US,
MODE_BR,
CountryModeId_Max
};
struct CountryFrequencyDefinition_t {
FrequencyBand_t Band;
uint32_t Freq_Min;
uint32_t Freq_Max;
uint32_t Freq_Legal_Min;
uint32_t Freq_Legal_Max;
uint32_t Freq_Default;
uint32_t Freq_StartUp;
};
struct CountryFrequencyList_t {
CountryModeId_t mode;
CountryFrequencyDefinition_t definition;
};
class HoymilesRadio_CMT : public HoymilesRadio {
public:
void init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3);
@ -26,11 +49,22 @@ public:
bool isConnected() const;
static uint32_t getMinFrequency();
static uint32_t getMaxFrequency();
uint32_t getMinFrequency() const;
uint32_t getMaxFrequency() const;
static constexpr uint32_t getChannelWidth()
{
return FH_OFFSET * CMT2300A_ONE_STEP_SIZE;
}
static float getFrequencyFromChannel(const uint8_t channel);
static uint8_t getChannelFromFrequency(const uint32_t freq_kHz);
CountryModeId_t getCountryMode() const;
void setCountryMode(const CountryModeId_t mode);
uint32_t getInvBootFrequency() const;
uint32_t getFrequencyFromChannel(const uint8_t channel) const;
uint8_t getChannelFromFrequency(const uint32_t frequency) const;
std::vector<CountryFrequencyList_t> getCountryFrequencyList() const;
private:
void ARDUINO_ISR_ATTR handleInt1();
@ -51,5 +85,7 @@ private:
uint32_t _inverterTargetFrequency = HOYMILES_CMT_WORK_FREQ;
bool cmtSwitchDtuFreq(const uint32_t to_freq_kHz);
};
bool cmtSwitchDtuFreq(const uint32_t to_frequency);
CountryModeId_t _countryMode;
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Thomas Basler and others
* Copyright (C) 2023-2024 Thomas Basler and others
*/
/*
@ -12,7 +12,8 @@ Command structure:
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
-----------------------------------------------------------------------------------------------------------------
56 71 60 35 46 80 12 23 04 02 15 21 00 14 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
56 71 60 35 46 80 12 23 04 02 15 21 00 14 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- (860 MHz band)
56 71 60 35 46 80 12 23 04 03 17 3c 00 14 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- (900 MHz band)
^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^ ^^ ^^ ^^ ^^
ID Target Addr Source Addr ? ? ? CH ? CRC8
*/
@ -22,12 +23,10 @@ ChannelChangeCommand::ChannelChangeCommand(const uint64_t target_address, const
: CommandAbstract(target_address, router_address)
{
_payload[0] = 0x56;
_payload[9] = 0x02;
_payload[10] = 0x15;
_payload[11] = 0x21;
_payload[13] = 0x14;
_payload_size = 14;
setCountryMode(CountryModeId_t::MODE_EU);
setChannel(channel);
setTimeout(10);
}
@ -47,6 +46,27 @@ uint8_t ChannelChangeCommand::getChannel() const
return _payload[12];
}
void ChannelChangeCommand::setCountryMode(const CountryModeId_t mode)
{
switch (mode) {
case CountryModeId_t::MODE_US:
_payload[9] = 0x03;
_payload[10] = 0x17;
_payload[11] = 0x3c;
break;
case CountryModeId_t::MODE_BR:
_payload[9] = 0x03;
_payload[10] = 0x17;
_payload[11] = 0x3c;
break;
default:
_payload[9] = 0x02;
_payload[10] = 0x15;
_payload[11] = 0x21;
break;
}
}
bool ChannelChangeCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id)
{
return true;
@ -56,4 +76,4 @@ uint8_t ChannelChangeCommand::getMaxResendCount()
{
// This command will never retrieve an answer. Therefor it's not required to repeat it
return 0;
}
}

View File

@ -2,6 +2,7 @@
#pragma once
#include "CommandAbstract.h"
#include "../HoymilesRadio_CMT.h"
class ChannelChangeCommand : public CommandAbstract {
public:
@ -12,7 +13,9 @@ public:
void setChannel(const uint8_t channel);
uint8_t getChannel() const;
void setCountryMode(const CountryModeId_t mode);
virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id);
virtual uint8_t getMaxResendCount();
};
};

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Thomas Basler and others
* Copyright (C) 2023-2024 Thomas Basler and others
*/
#include "HMS_Abstract.h"
#include "Hoymiles.h"
@ -19,7 +19,8 @@ bool HMS_Abstract::sendChangeChannelRequest()
}
auto cmdChannel = _radio->prepareCommand<ChannelChangeCommand>();
cmdChannel->setChannel(HoymilesRadio_CMT::getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency()));
cmdChannel->setCountryMode(Hoymiles.getRadioCmt()->getCountryMode());
cmdChannel->setChannel(Hoymiles.getRadioCmt()->getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency()));
cmdChannel->setTargetAddress(serial());
_radio->enqueCommand(cmdChannel);

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Thomas Basler and others
* Copyright (C) 2023-2024 Thomas Basler and others
*/
#include "HMT_Abstract.h"
#include "Hoymiles.h"
@ -21,9 +21,10 @@ bool HMT_Abstract::sendChangeChannelRequest()
}
auto cmdChannel = _radio->prepareCommand<ChannelChangeCommand>();
cmdChannel->setChannel(HoymilesRadio_CMT::getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency()));
cmdChannel->setCountryMode(Hoymiles.getRadioCmt()->getCountryMode());
cmdChannel->setChannel(Hoymiles.getRadioCmt()->getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency()));
cmdChannel->setTargetAddress(serial());
_radio->enqueCommand(cmdChannel);
return true;
};
};

View File

@ -9,7 +9,10 @@ import re
Import("env")
def getPatchPath(env):
return os.path.join(env["PROJECT_DIR"], "patches", env.GetProjectOption('custom_patches'))
patchList = []
for patch in env.GetProjectOption('custom_patches').split(","):
patchList.append(os.path.join(env["PROJECT_DIR"], "patches", patch))
return patchList
def is_tool(name):
"""Check whether `name` is on PATH and marked as executable."""
@ -44,35 +47,36 @@ def main():
print('Git not found. Will not apply custom patches!')
return
directory = getPatchPath(env)
if (not os.path.isdir(directory)):
print('Patch directory not found: ' + directory)
return
directories = getPatchPath(env)
for directory in directories:
if (not os.path.isdir(directory)):
print('Patch directory not found: ' + directory)
return
for file in os.listdir(directory):
if (not file.endswith('.patch')):
continue
for file in os.listdir(directory):
if (not file.endswith('.patch')):
continue
fullPath = os.path.join(directory, file)
preparePath = fullPath + '.prepare'
replaceInFile(fullPath, preparePath, '$$$env$$$', env['PIOENV'])
print('Working on patch: ' + fullPath + '... ', end='')
fullPath = os.path.join(directory, file)
preparePath = fullPath + '.prepare'
replaceInFile(fullPath, preparePath, '$$$env$$$', env['PIOENV'])
print('Working on patch: ' + fullPath + '... ', end='')
# Check if patch was already applied
process = subprocess.run(['git', 'apply', '--reverse', '--check', preparePath], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if (process.returncode == 0):
print('already applied')
os.remove(preparePath)
continue
# Apply patch
process = subprocess.run(['git', 'apply', preparePath])
if (process.returncode == 0):
print('applied')
else:
print('failed')
# Check if patch was already applied
process = subprocess.run(['git', 'apply', '--reverse', '--check', preparePath], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if (process.returncode == 0):
print('already applied')
os.remove(preparePath)
continue
# Apply patch
process = subprocess.run(['git', 'apply', preparePath])
if (process.returncode == 0):
print('applied')
else:
print('failed')
os.remove(preparePath)
main()
main()

View File

@ -45,8 +45,8 @@ lib_deps =
https://github.com/arkhipenko/TaskScheduler#testing
https://github.com/arkhipenko/TaskScheduler#testing
https://github.com/coryjfowler/MCP_CAN_lib
plerup/EspSoftwareSerial @ 8.0.1
mobizt/FirebaseJson @ ^3.0.6
plerup/EspSoftwareSerial @ ^8.0.1
https://github.com/dok-net/ghostl @ ^1.0.1
rweather/Crypto@^0.4.0
extra_scripts =
@ -64,6 +64,8 @@ board_build.embed_files =
webapp_dist/js/app.js.gz
webapp_dist/site.webmanifest
custom_patches =
monitor_filters = esp32_exception_decoder, time, log2file, colorize
monitor_speed = 115200
upload_protocol = esptool
@ -80,13 +82,13 @@ build_flags = ${env.build_flags}
[env:generic_esp32c3]
board = esp32-c3-devkitc-02
custom_patches = esp32c3
custom_patches = ${env.custom_patches},esp32c3
build_flags = ${env.build_flags}
[env:generic_esp32c3_usb]
board = esp32-c3-devkitc-02
custom_patches = esp32c3
custom_patches = ${env.custom_patches},esp32c3
build_flags = ${env.build_flags}
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
@ -248,4 +250,4 @@ build_flags = ${env.build_flags}
-DCMT_GPIO3=8
-DCMT_SDIO=5
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DARDUINO_USB_CDC_ON_BOOT=1

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "Configuration.h"
#include "MessageOutput.h"
@ -97,6 +97,7 @@ bool ConfigurationClass::write()
dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel;
dtu["cmt_pa_level"] = config.Dtu.Cmt.PaLevel;
dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency;
dtu["cmt_country_mode"] = config.Dtu.Cmt.CountryMode;
JsonObject security = doc.createNestedObject("security");
security["password"] = config.Security.Password;
@ -111,7 +112,8 @@ bool ConfigurationClass::write()
display["rotation"] = config.Display.Rotation;
display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language;
display["diagram_duration"] = config.Display.DiagramDuration;
display["diagram_duration"] = config.Display.Diagram.Duration;
display["diagram_mode"] = config.Display.Diagram.Mode;
JsonArray leds = device.createNestedArray("led");
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
@ -339,6 +341,7 @@ bool ConfigurationClass::read()
config.Dtu.Nrf.PaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL;
config.Dtu.Cmt.PaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL;
config.Dtu.Cmt.Frequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY;
config.Dtu.Cmt.CountryMode = dtu["cmt_country_mode"] | DTU_CMT_COUNTRY_MODE;
JsonObject security = doc["security"];
strlcpy(config.Security.Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security.Password));
@ -353,7 +356,8 @@ bool ConfigurationClass::read()
config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION;
config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST;
config.Display.Language = display["language"] | DISPLAY_LANGUAGE;
config.Display.DiagramDuration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION;
config.Display.Diagram.Duration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION;
config.Display.Diagram.Mode = display["diagram_mode"] | DISPLAY_DIAGRAM_MODE;
JsonArray leds = device["led"];
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
@ -513,6 +517,11 @@ void ConfigurationClass::migrate()
nvs_flash_init();
}
if (config.Cfg.Version < 0x00011b00) {
// Convert from kHz to Hz
config.Dtu.Cmt.Frequency *= 1000;
}
f.close();
config.Cfg.Version = CONFIG_VERSION;

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Thomas Basler and others
* Copyright (C) 2023-2024 Thomas Basler and others
*/
#include "Display_Graphic.h"
#include "Datastore.h"
@ -28,8 +28,8 @@ const uint8_t languages[] = {
};
static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" };
static const char* const i18n_current_power_w[] = { "%3.0f W", "%3.0f W", "%3.0f W" };
static const char* const i18n_current_power_kw[] = { "%2.1f kW", "%2.1f kW", "%2.1f kW" };
static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" };
static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" };
static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" };
static const char* const i18n_yield_total_kwh[] = { "total: %.1f kWh", "Ges.: %.1f kWh", "total: %.1f kWh" };
static const char* const i18n_date_format[] = { "%m/%d/%Y %H:%M", "%d.%m.%Y %H:%M", "%d/%m/%Y %H:%M" };
@ -94,13 +94,31 @@ bool DisplayGraphicClass::isValidDisplay()
void DisplayGraphicClass::printText(const char* text, const uint8_t line)
{
setFont(line);
uint8_t dispX;
if (!_isLarge) {
dispX = (line == 0) ? 5 : 0;
} else {
dispX = (line == 0) ? 10 : 5;
switch (line) {
case 0:
if (_diagram_mode == DiagramMode_t::Small) {
// Center between left border and diagram
dispX = (CHART_POSX - _display->getStrWidth(text)) / 2;
} else {
// Center on screen
dispX = (_display->getDisplayWidth() - _display->getStrWidth(text)) / 2;
}
break;
case 3:
// Center on screen
dispX = (_display->getDisplayWidth() - _display->getStrWidth(text)) / 2;
break;
default:
dispX = 5;
break;
}
}
setFont(line);
dispX += enableScreensaver ? (_mExtra % 7) : 0;
_display->drawStr(dispX, _lineOffsets[line], text);
@ -136,6 +154,13 @@ void DisplayGraphicClass::setLanguage(const uint8_t language)
_display_language = language < sizeof(languages) / sizeof(languages[0]) ? language : DISPLAY_LANGUAGE;
}
void DisplayGraphicClass::setDiagramMode(DiagramMode_t mode)
{
if (mode < DiagramMode_t::DisplayMode_Max) {
_diagram_mode = mode;
}
}
void DisplayGraphicClass::setStartupDisplay()
{
if (!isValidDisplay()) {
@ -158,21 +183,37 @@ void DisplayGraphicClass::loop()
_display->clearBuffer();
bool displayPowerSave = false;
bool showText = true;
//=====> Actual Production ==========
if (Datastore.getIsAtLeastOneReachable()) {
displayPowerSave = false;
if (_isLarge) {
uint8_t screenSaverOffsetX = enableScreensaver ? (_mExtra % 7) : 0;
_diagram.redraw(screenSaverOffsetX);
switch (_diagram_mode) {
case DiagramMode_t::Small:
_diagram.redraw(screenSaverOffsetX, CHART_POSX, CHART_POSY, CHART_WIDTH, CHART_HEIGHT, false);
break;
case DiagramMode_t::Fullscreen:
// Every 10 seconds
if (_mExtra % (10 * 2) < 10) {
_diagram.redraw(screenSaverOffsetX, 10, 0, _display->getDisplayWidth() - 12, _display->getDisplayHeight() - 3, true);
showText = false;
}
break;
default:
break;
}
}
const float watts = Datastore.getTotalAcPowerEnabled();
if (watts > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], watts / 1000);
} else {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], watts);
if (showText) {
const float watts = Datastore.getTotalAcPowerEnabled();
if (watts > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], watts / 1000);
} else {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], watts);
}
printText(_fmtText, 0);
}
printText(_fmtText, 0);
_previousMillis = millis();
}
//<=======================
@ -187,23 +228,27 @@ void DisplayGraphicClass::loop()
}
//<=======================
//=====> Today & Total Production =======
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled());
printText(_fmtText, 1);
if (showText) {
//=====> Today & Total Production =======
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled());
printText(_fmtText, 1);
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_total_kwh[_display_language], Datastore.getTotalAcYieldTotalEnabled());
printText(_fmtText, 2);
//<=======================
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_total_kwh[_display_language], Datastore.getTotalAcYieldTotalEnabled());
printText(_fmtText, 2);
//<=======================
//=====> IP or Date-Time ========
if (!(_mExtra % 10) && NetworkSettings.localIP()) {
printText(NetworkSettings.localIP().toString().c_str(), 3);
} else {
// Get current time
time_t now = time(nullptr);
strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now));
printText(_fmtText, 3);
//=====> IP or Date-Time ========
// Change every 3 seconds
if (!(_mExtra % (3 * 2) < 3) && NetworkSettings.localIP()) {
printText(NetworkSettings.localIP().toString().c_str(), 3);
} else {
// Get current time
time_t now = time(nullptr);
strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now));
printText(_fmtText, 3);
}
}
_display->sendBuffer();
_mExtra++;

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Thomas Basler and others
* Copyright (C) 2023-2024 Thomas Basler and others
*/
#include "Display_Graphic_Diagram.h"
#include "Configuration.h"
@ -52,38 +52,39 @@ void DisplayGraphicDiagramClass::dataPointLoop()
uint32_t DisplayGraphicDiagramClass::getSecondsPerDot()
{
return Configuration.get().Display.DiagramDuration / CHART_WIDTH;
return Configuration.get().Display.Diagram.Duration / _chartWidth;
}
void DisplayGraphicDiagramClass::updatePeriod()
{
_dataPointTask.setInterval(getSecondsPerDot() * TASK_SECOND);
// Calculate seconds per datapoint
_dataPointTask.setInterval(Configuration.get().Display.Diagram.Duration * TASK_SECOND / MAX_DATAPOINTS );
}
void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX)
void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX, uint8_t xPos, uint8_t yPos, uint8_t width, uint8_t height, bool isFullscreen)
{
// screenSaverOffsetX expected to be in range 0..6
const uint8_t graphPosX = DIAG_POSX + ((screenSaverOffsetX > 3) ? 1 : 0);
const uint8_t graphPosY = DIAG_POSY + ((screenSaverOffsetX > 3) ? 1 : 0);
_chartWidth = width;
const uint8_t horizontal_line_y = graphPosY + CHART_HEIGHT - 1;
// screenSaverOffsetX expected to be in range 0..6
const uint8_t graphPosX = xPos + ((screenSaverOffsetX > 3) ? 1 : 0);
const uint8_t graphPosY = yPos + ((screenSaverOffsetX > 3) ? 1 : 0);
const uint8_t horizontal_line_y = graphPosY + height - 1;
const uint8_t arrow_size = 2;
// draw diagram axis
_display->drawVLine(graphPosX, graphPosY, CHART_HEIGHT);
_display->drawHLine(graphPosX, horizontal_line_y, CHART_WIDTH);
_display->drawVLine(graphPosX, graphPosY, height);
_display->drawHLine(graphPosX, horizontal_line_y, width);
// UP-arrow
_display->drawLine(graphPosX, graphPosY, graphPosX + arrow_size, graphPosY + arrow_size);
_display->drawLine(graphPosX, graphPosY, graphPosX - arrow_size, graphPosY + arrow_size);
// LEFT-arrow
_display->drawLine(graphPosX + CHART_WIDTH - 1, horizontal_line_y, graphPosX + CHART_WIDTH - 1 - arrow_size, horizontal_line_y - arrow_size);
_display->drawLine(graphPosX + CHART_WIDTH - 1, horizontal_line_y, graphPosX + CHART_WIDTH - 1 - arrow_size, horizontal_line_y + arrow_size);
_display->drawLine(graphPosX + width - 1, horizontal_line_y, graphPosX + width - 1 - arrow_size, horizontal_line_y - arrow_size);
_display->drawLine(graphPosX + width - 1, horizontal_line_y, graphPosX + width - 1 - arrow_size, horizontal_line_y + arrow_size);
// draw AC value
// 4 pixels per char
_display->setFont(u8g2_font_tom_thumb_4x6_mr);
char fmtText[7];
const float maxWatts = *std::max_element(_graphValues.begin(), _graphValues.end());
if (maxWatts > 999) {
@ -91,25 +92,46 @@ void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX)
} else {
snprintf(fmtText, sizeof(fmtText), "%dW", static_cast<uint16_t>(maxWatts));
}
const uint8_t textLength = strlen(fmtText);
_display->drawStr(graphPosX - arrow_size - textLength * 4, graphPosY + 5, fmtText);
if (isFullscreen) {
_display->setFont(u8g2_font_5x8_tr);
_display->setFontDirection(3);
_display->drawStr(graphPosX - arrow_size, graphPosY + _display->getStrWidth(fmtText), fmtText);
_display->setFontDirection(0);
} else {
// 4 pixels per char
_display->setFont(u8g2_font_tom_thumb_4x6_mr);
_display->drawStr(graphPosX - arrow_size - _display->getStrWidth(fmtText), graphPosY + 5, fmtText);
}
// draw chart
const float scaleFactor = maxWatts / CHART_HEIGHT;
uint8_t axisTick = 1;
const float scaleFactorY = maxWatts / static_cast<float>(height);
const float scaleFactorX = static_cast<float>(MAX_DATAPOINTS) / static_cast<float>(_chartWidth);
if (maxWatts > 0 && isFullscreen) {
// draw y axis ticks
const uint16_t yAxisWattPerTick = maxWatts <= 100 ? 10 : maxWatts <= 1000 ? 100 : maxWatts < 5000 ? 500 : 1000;
const uint8_t yAxisTickSizePixel = height / (maxWatts / yAxisWattPerTick);
for (int16_t tickYPos = graphPosY + height; tickYPos > graphPosY - arrow_size; tickYPos -= yAxisTickSizePixel) {
_display->drawPixel(graphPosX - 1, tickYPos);
}
}
uint8_t xAxisTicks = 1;
for (uint8_t i = 1; i < _graphValuesCount; i++) {
// draw one tick per hour to the x-axis
if (i * getSecondsPerDot() > (3600u * axisTick)) {
_display->drawPixel(graphPosX + 1 + i, graphPosY + CHART_HEIGHT);
axisTick++;
if (i * getSecondsPerDot() > (3600u * xAxisTicks)) {
_display->drawPixel((graphPosX + 1 + i) * scaleFactorX, graphPosY + height);
xAxisTicks++;
}
if (scaleFactor == 0) {
if (scaleFactorY == 0 || scaleFactorX == 0) {
continue;
}
_display->drawLine(
graphPosX + i - 1, horizontal_line_y - std::max<int16_t>(0, _graphValues[i - 1] / scaleFactor - 0.5),
graphPosX + i, horizontal_line_y - std::max<int16_t>(0, _graphValues[i] / scaleFactor - 0.5));
graphPosX + (i - 1) / scaleFactorX, horizontal_line_y - std::max<int16_t>(0, _graphValues[i - 1] / scaleFactorY - 0.5),
graphPosX + i / scaleFactorX, horizontal_line_y - std::max<int16_t>(0, _graphValues[i] / scaleFactorY - 0.5));
}
}

View File

@ -3,11 +3,12 @@
#include "HttpPowerMeter.h"
#include "MessageOutput.h"
#include <WiFiClientSecure.h>
#include <FirebaseJson.h>
#include <ArduinoJson.h>
#include <Crypto.h>
#include <SHA256.h>
#include <base64.h>
#include <memory>
#include <ESPmDNS.h>
void HttpPowerMeterClass::init()
{
@ -20,10 +21,7 @@ float HttpPowerMeterClass::getPower(int8_t phase)
bool HttpPowerMeterClass::updateValues()
{
const CONFIG_T& config = Configuration.get();
char response[2000],
errorMessage[256];
const CONFIG_T& config = Configuration.get();
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.PowerMeter.Http_Phase[i];
@ -34,53 +32,83 @@ bool HttpPowerMeterClass::updateValues()
}
if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
if (httpRequest(phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout,
response, sizeof(response), errorMessage, sizeof(errorMessage))) {
if (!getFloatValueByJsonPath(response, phaseConfig.JsonPath, power[i])) {
MessageOutput.printf("[HttpPowerMeter] Couldn't find a value with Json query \"%s\"\r\n", phaseConfig.JsonPath);
return false;
}
} else {
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed. Error: %s\r\n",
i + 1, errorMessage);
if (!queryPhase(i, phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout,
phaseConfig.JsonPath)) {
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1);
MessageOutput.printf("%s\r\n", httpPowerMeterError);
return false;
}
}
}
return true;
}
bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char* username, const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout,
char* response, size_t responseSize, char* error, size_t errorSize)
bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password,
const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath)
{
String urlProtocol;
String urlHostname;
String urlUri;
extractUrlComponents(url, urlProtocol, urlHostname, urlUri);
//hostByName in WiFiGeneric fails to resolve local names. issue described in
//https://github.com/espressif/arduino-esp32/issues/3822
//and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300
//in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses.
//have to do it manually here. Feels Hacky...
String protocol;
String host;
String uri;
extractUrlComponents(url, protocol, host, uri);
response[0] = '\0';
error[0] = '\0';
IPAddress ipaddr((uint32_t)0);
//first check if "host" is already an IP adress
if (!ipaddr.fromString(host))
{
//"host"" is not an IP address so try to resolve the IP adress
//first try locally via mDNS, then via DNS. WiFiGeneric::hostByName() will spam the console if done the otherway around.
const bool mdnsEnabled = Configuration.get().Mdns.Enabled;
if (!mdnsEnabled) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS, try to enable mDNS in Network Settings"), host.c_str());
//ensure we try resolving via DNS even if mDNS is disabled
if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str());
}
}
else
{
ipaddr = MDNS.queryHost(host);
if (ipaddr == INADDR_NONE){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via mDNS"), host.c_str());
//when we cannot find local server via mDNS, try resolving via DNS
if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str());
}
}
}
}
// secureWifiClient MUST be created before HTTPClient
// see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381
std::unique_ptr<WiFiClient> wifiClient;
if (urlProtocol == "https") {
bool https = protocol == "https";
if (https) {
auto secureWifiClient = std::make_unique<WiFiClientSecure>();
secureWifiClient->setInsecure();
wifiClient = std::move(secureWifiClient);
} else {
wifiClient = std::make_unique<WiFiClient>();
}
if (!httpClient.begin(*wifiClient, url)) {
snprintf_P(error, errorSize, "httpClient.begin(%s) failed", url);
return false;
}
prepareRequest(timeout, httpHeader, httpValue);
return httpRequest(phase, *wifiClient, ipaddr.toString(), uri, https, authType, username, password, httpHeader, httpValue, timeout, jsonPath);
}
bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, const String& uri, bool https, Auth authType, const char* username,
const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath)
{
int port = (https ? 443 : 80);
if(!httpClient.begin(wifiClient, host, port, uri, https)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str());
return false;
}
prepareRequest(timeout, httpHeader, httpValue);
if (authType == Auth::digest) {
const char *headers[1] = {"WWW-Authenticate"};
httpClient.collectHeaders(headers, 1);
@ -92,111 +120,108 @@ bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char
auth.concat(base64::encode(authString));
httpClient.addHeader("Authorization", auth);
}
int httpCode = httpClient.GET();
if (httpCode == HTTP_CODE_UNAUTHORIZED && authType == Auth::digest) {
// Handle authentication challenge
char realm[256]; // Buffer to store the realm received from the server
char nonce[256]; // Buffer to store the nonce received from the server
if (httpClient.hasHeader("WWW-Authenticate")) {
String authHeader = httpClient.header("WWW-Authenticate");
if (authHeader.indexOf("Digest") != -1) {
int realmIndex = authHeader.indexOf("realm=\"");
int nonceIndex = authHeader.indexOf("nonce=\"");
if (realmIndex != -1 && nonceIndex != -1) {
int realmEndIndex = authHeader.indexOf("\"", realmIndex + 7);
int nonceEndIndex = authHeader.indexOf("\"", nonceIndex + 7);
if (realmEndIndex != -1 && nonceEndIndex != -1) {
authHeader.substring(realmIndex + 7, realmEndIndex).toCharArray(realm, sizeof(realm));
authHeader.substring(nonceIndex + 7, nonceEndIndex).toCharArray(nonce, sizeof(nonce));
}
}
String cnonce = String(random(1000)); // Generate client nonce
String str = username;
str += ":";
str += realm;
str += ":";
str += password;
String ha1 = sha256(str);
str = "GET:";
str += urlUri;
String ha2 = sha256(str);
str = ha1;
str += ":";
str += nonce;
str += ":00000001:";
str += cnonce;
str += ":auth:";
str += ha2;
String response = sha256(str);
String authorization = "Digest username=\"";
authorization += username;
authorization += "\", realm=\"";
authorization += realm;
authorization += "\", nonce=\"";
authorization += nonce;
authorization += "\", uri=\"";
authorization += urlUri;
authorization += "\", cnonce=\"";
authorization += cnonce;
authorization += "\", nc=00000001, qop=auth, response=\"";
authorization += response;
authorization += "\", algorithm=SHA-256";
httpClient.end();
if (!httpClient.begin(*wifiClient, url)) {
snprintf_P(error, errorSize, "httpClient.begin(%s) for digest auth failed", url);
return false;
}
prepareRequest(timeout, httpHeader, httpValue);
httpClient.addHeader("Authorization", authorization);
httpCode = httpClient.GET();
String authReq = httpClient.header("WWW-Authenticate");
String authorization = getDigestAuth(authReq, String(username), String(password), "GET", String(uri), 1);
httpClient.end();
if(!httpClient.begin(wifiClient, host, port, uri, https)){
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str());
return false;
}
}
prepareRequest(timeout, httpHeader, httpValue);
httpClient.addHeader("Authorization", authorization);
httpCode = httpClient.GET();
}
}
bool result = tryGetFloatValueForPhase(phase, httpCode, jsonPath);
httpClient.end();
return result;
}
String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) {
int _begin = authReq.indexOf(param);
if (_begin == -1) { return ""; }
return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
}
String HttpPowerMeterClass::getcNonce(const int len) {
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
String s = "";
for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; }
return s;
}
String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) {
// extracting required parameters for RFC 2617 Digest
String realm = extractParam(authReq, "realm=\"", '"');
String nonce = extractParam(authReq, "nonce=\"", '"');
String cNonce = getcNonce(8);
char nc[9];
snprintf(nc, sizeof(nc), "%08x", counter);
//sha256 of the user:realm:password
String ha1 = sha256(username + ":" + realm + ":" + password);
//sha256 of method:uri
String ha2 = sha256(method + ":" + uri);
//sha256 of h1:nonce:nc:cNonce:auth:h2
String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + ha2);
//Final authorization String;
String authorization = "Digest username=\"";
authorization += username;
authorization += "\", realm=\"";
authorization += realm;
authorization += "\", nonce=\"";
authorization += nonce;
authorization += "\", uri=\"";
authorization += uri;
authorization += "\", cnonce=\"";
authorization += cNonce;
authorization += "\", nc=";
authorization += String(nc);
authorization += ", qop=auth, response=\"";
authorization += response;
authorization += "\", algorithm=SHA-256";
return authorization;
}
bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath)
{
bool success = false;
if (httpCode == HTTP_CODE_OK) {
String responseBody = httpClient.getString();
if (responseBody.length() > (responseSize - 1)) {
snprintf_P(error, errorSize, "Response too large! Response length: %d Body start: %s",
httpClient.getSize(), responseBody.c_str());
} else {
snprintf(response, responseSize, responseBody.c_str());
httpResponse = httpClient.getString(); //very unfortunate that we cannot parse WifiClient stream directly
StaticJsonDocument<2048> json; //however creating these allocations on stack should be fine to avoid heap fragmentation
deserializeJson(json, httpResponse);
if(!json.containsKey(jsonPath))
{
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("[HttpPowerMeter] Couldn't find a value for phase %i with Json query \"%s\""), phase, jsonPath);
}else {
power[phase] = json[jsonPath].as<float>();
//MessageOutput.printf("Power for Phase %i: %5.2fW\r\n", phase, power[phase]);
success = true;
}
} else if (httpCode <= 0) {
snprintf_P(error, errorSize, "Error(%s): %s", url, httpClient.errorToString(httpCode).c_str());
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str());
} else if (httpCode != HTTP_CODE_OK) {
snprintf_P(error, errorSize, "Bad HTTP code: %d", httpCode);
}
httpClient.end();
if (error[0] != '\0') {
return false;
}
return true;
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode);
}
return success;
}
float HttpPowerMeterClass::getFloatValueByJsonPath(const char* jsonString, const char* jsonPath, float& value)
{
FirebaseJson firebaseJson;
firebaseJson.setJsonData(jsonString);
FirebaseJsonData firebaseJsonResult;
if (!firebaseJson.get(firebaseJsonResult, jsonPath)) {
return false;
}
value = firebaseJsonResult.to<float>();
firebaseJson.clear();
return true;
}
void HttpPowerMeterClass::extractUrlComponents(const String& url, String& protocol, String& hostname, String& uri) {
void HttpPowerMeterClass::extractUrlComponents(const String& url, String& protocol, String& hostname, String& uri) {
// Find protocol delimiter
int protocolEndIndex = url.indexOf(":");
if (protocolEndIndex != -1) {
@ -229,25 +254,24 @@ float HttpPowerMeterClass::getFloatValueByJsonPath(const char* jsonString, const
#define HASH_SIZE 32
String HttpPowerMeterClass::sha256(const String& data) {
SHA256 sha256;
uint8_t hash[HASH_SIZE];
SHA256 sha256;
uint8_t hash[HASH_SIZE];
sha256.reset();
sha256.update(data.c_str(), data.length());
sha256.finalize(hash, HASH_SIZE);
sha256.reset();
sha256.update(data.c_str(), data.length());
sha256.finalize(hash, HASH_SIZE);
String hashStr = "";
for (int i = 0; i < HASH_SIZE; i++) {
String hex = String(hash[i], HEX);
if (hex.length() == 1) {
hashStr += "0";
String hashStr = "";
for (int i = 0; i < HASH_SIZE; i++) {
String hex = String(hash[i], HEX);
if (hex.length() == 1) {
hashStr += "0";
}
hashStr += hex;
}
hashStr += hex;
}
return hashStr;
return hashStr;
}
void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) {
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setUserAgent("OpenDTU-OnBattery");

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Thomas Basler and others
* Copyright (C) 2023-2024 Thomas Basler and others
*/
#include "InverterSettings.h"
#include "Configuration.h"
@ -45,6 +45,8 @@ void InverterSettingsClass::init(Scheduler& scheduler)
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 country mode... "));
Hoymiles.getRadioCmt()->setCountryMode(static_cast<CountryModeId_t>(config.Dtu.Cmt.CountryMode));
MessageOutput.println(F(" Setting CMT target frequency... "));
Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency);
}

View File

@ -83,7 +83,8 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
display["screensaver"] = config.Display.ScreenSaver;
display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language;
display["diagramduration"] = config.Display.DiagramDuration;
display["diagramduration"] = config.Display.Diagram.Duration;
display["diagrammode"] = config.Display.Diagram.Mode;
auto leds = root.createNestedArray("led");
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
@ -179,7 +180,8 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
config.Display.ScreenSaver = root["display"]["screensaver"].as<bool>();
config.Display.Contrast = root["display"]["contrast"].as<uint8_t>();
config.Display.Language = root["display"]["language"].as<uint8_t>();
config.Display.DiagramDuration = root["display"]["diagramduration"].as<uint32_t>();
config.Display.Diagram.Duration = root["display"]["diagramduration"].as<uint32_t>();
config.Display.Diagram.Mode = root["display"]["diagrammode"].as<DiagramMode_t>();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
config.Led_Single[i].Brightness = root["led"][i]["brightness"].as<uint8_t>();
@ -191,6 +193,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
Display.enableScreensaver = config.Display.ScreenSaver;
Display.setContrast(config.Display.Contrast);
Display.setLanguage(config.Display.Language);
Display.setDiagramMode(static_cast<DiagramMode_t>(config.Display.Diagram.Mode));
Display.Diagram().updatePeriod();
WebApi.writeConfig(retMsg);
@ -201,4 +204,4 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
if (performRestart) {
Utils::restartDtu();
}
}
}

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "WebApi_dtu.h"
#include "Configuration.h"
@ -21,6 +21,18 @@ void WebApiDtuClass::init(AsyncWebServer& server)
void WebApiDtuClass::loop()
{
if (_performReload) {
// Execute stuff in main thread to avoid busy SPI bus
CONFIG_T& config = Configuration.get();
Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel);
Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel);
Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial);
Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu.Serial);
Hoymiles.getRadioCmt()->setCountryMode(static_cast<CountryModeId_t>(config.Dtu.Cmt.CountryMode));
Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency);
Hoymiles.setPollInterval(config.Dtu.PollInterval);
_performReload = false;
}
}
void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
@ -46,6 +58,19 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
root["cmt_enabled"] = Hoymiles.getRadioCmt()->isInitialized();
root["cmt_palevel"] = config.Dtu.Cmt.PaLevel;
root["cmt_frequency"] = config.Dtu.Cmt.Frequency;
root["cmt_country"] = config.Dtu.Cmt.CountryMode;
root["cmt_chan_width"] = Hoymiles.getRadioCmt()->getChannelWidth();
auto data = root.createNestedArray("country_def");
auto countryDefs = Hoymiles.getRadioCmt()->getCountryFrequencyList();
for (const auto& definition : countryDefs) {
auto obj = data.createNestedObject();
obj["freq_default"] = definition.definition.Freq_Default;
obj["freq_min"] = definition.definition.Freq_Min;
obj["freq_max"] = definition.definition.Freq_Max;
obj["freq_legal_min"] = definition.definition.Freq_Legal_Min;
obj["freq_legal_max"] = definition.definition.Freq_Legal_Max;
}
response->setLength();
request->send(response);
@ -95,7 +120,8 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("verbose_logging")
&& root.containsKey("nrf_palevel")
&& root.containsKey("cmt_palevel")
&& root.containsKey("cmt_frequency"))) {
&& root.containsKey("cmt_frequency")
&& root.containsKey("cmt_country"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
@ -135,14 +161,23 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
return;
}
if (root["cmt_frequency"].as<uint32_t>() < Hoymiles.getRadioCmt()->getMinFrequency()
|| root["cmt_frequency"].as<uint32_t>() > Hoymiles.getRadioCmt()->getMaxFrequency()
|| root["cmt_frequency"].as<uint32_t>() % 250 > 0) {
if (root["cmt_country"].as<uint8_t>() >= CountryModeId_t::CountryModeId_Max) {
retMsg["message"] = "Invalid country setting!";
retMsg["code"] = WebApiError::DtuInvalidCmtCountry;
response->setLength();
request->send(response);
return;
}
auto FrequencyDefinition = Hoymiles.getRadioCmt()->getCountryFrequencyList()[root["cmt_country"].as<CountryModeId_t>()].definition;
if (root["cmt_frequency"].as<uint32_t>() < FrequencyDefinition.Freq_Min
|| root["cmt_frequency"].as<uint32_t>() > FrequencyDefinition.Freq_Max
|| root["cmt_frequency"].as<uint32_t>() % Hoymiles.getRadioCmt()->getChannelWidth() > 0) {
retMsg["message"] = "Invalid CMT frequency setting!";
retMsg["code"] = WebApiError::DtuInvalidCmtFrequency;
retMsg["param"]["min"] = Hoymiles.getRadioCmt()->getMinFrequency();
retMsg["param"]["max"] = Hoymiles.getRadioCmt()->getMaxFrequency();
retMsg["param"]["min"] = FrequencyDefinition.Freq_Min;
retMsg["param"]["max"] = FrequencyDefinition.Freq_Max;
response->setLength();
request->send(response);
return;
@ -157,17 +192,12 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>();
config.Dtu.Cmt.Frequency = root["cmt_frequency"].as<uint32_t>();
config.Dtu.Cmt.CountryMode = root["cmt_country"].as<CountryModeId_t>();
WebApi.writeConfig(retMsg);
response->setLength();
request->send(response);
Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel);
Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel);
Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial);
Hoymiles.getRadioCmt()->setDtuSerial(config.Dtu.Serial);
Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency);
Hoymiles.setPollInterval(config.Dtu.PollInterval);
Hoymiles.setVerboseLogging(config.Dtu.VerboseLogging);
_performReload = true;
}

View File

@ -200,6 +200,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
response->setLength();
request->send(response);
// reboot requiered as per https://github.com/helgeerbe/OpenDTU-OnBattery/issues/565#issuecomment-1872552559
yield();
delay(1000);
yield();
@ -251,25 +252,18 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
return;
}
char powerMeterResponse[2000],
errorMessage[256];
char response[200];
if (HttpPowerMeter.httpRequest(root["url"].as<String>().c_str(),
root["auth_type"].as<Auth>(), root["username"].as<String>().c_str(), root["password"].as<String>().c_str(),
root["header_key"].as<String>().c_str(), root["header_value"].as<String>().c_str(), root["timeout"].as<uint16_t>(),
powerMeterResponse, sizeof(powerMeterResponse), errorMessage, sizeof(errorMessage))) {
float power;
char response[256];
if (HttpPowerMeter.getFloatValueByJsonPath(powerMeterResponse,
root["json_path"].as<String>().c_str(), power)) {
retMsg["type"] = "success";
snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", power);
} else {
snprintf_P(response, sizeof(response), "Error: Could not find value for JSON path!");
}
int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result
if (HttpPowerMeter.queryPhase(phase, root[F("url")].as<String>().c_str(),
root[F("auth_type")].as<Auth>(), root[F("username")].as<String>().c_str(), root[F("password")].as<String>().c_str(),
root[F("header_key")].as<String>().c_str(), root[F("header_value")].as<String>().c_str(), root[F("timeout")].as<uint16_t>(),
root[F("json_path")].as<String>().c_str())) {
retMsg[F("type")] = F("success");
snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", HttpPowerMeter.getPower(phase + 1));
} else {
snprintf_P(response, sizeof(response), errorMessage);
snprintf_P(response, sizeof(response), "%s", HttpPowerMeter.httpPowerMeterError);
}
retMsg["message"] = response;

View File

@ -82,6 +82,22 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\n",
serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000);
if (i == 0) {
stream->print("# HELP opendtu_inverter_limit_relative current relative limit of the inverter\n");
stream->print("# TYPE opendtu_inverter_limit_relative gauge\n");
}
stream->printf("opendtu_inverter_limit_relative{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n",
serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() / 100.0);
if (inv->DevInfo()->getMaxPower() > 0) {
if (i == 0) {
stream->print("# HELP opendtu_inverter_limit_absolute current relative limit of the inverter\n");
stream->print("# TYPE opendtu_inverter_limit_absolute gauge\n");
}
stream->printf("opendtu_inverter_limit_absolute{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n",
serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0);
}
// Loop all channels if Statistics have been updated at least once since DTU boot
if (inv->Statistics()->getLastUpdate() > 0) {
for (auto& t : inv->Statistics()->getChannelTypes()) {

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2023 Thomas Basler and others
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "Configuration.h"
#include "Datastore.h"
@ -139,6 +139,7 @@ void setup()
Display.enableScreensaver = config.Display.ScreenSaver;
Display.setContrast(config.Display.Contrast);
Display.setLanguage(config.Display.Language);
Display.setDiagramMode(static_cast<DiagramMode_t>(config.Display.Diagram.Mode));
Display.setStartupDisplay();
MessageOutput.println("done");

View File

@ -18,8 +18,8 @@
"mitt": "^3.0.1",
"sortablejs": "^1.15.1",
"spark-md5": "^3.0.2",
"vue": "^3.4.5",
"vue-i18n": "^9.8.0",
"vue": "^3.4.13",
"vue-i18n": "^9.9.0",
"vue-router": "^4.2.5"
},
"devDependencies": {
@ -27,19 +27,21 @@
"@rushstack/eslint-patch": "^1.6.1",
"@tsconfig/node18": "^18.2.2",
"@types/bootstrap": "^5.2.10",
"@types/node": "^20.10.6",
"@types/node": "^20.11.0",
"@types/pulltorefreshjs": "^0.1.7",
"@types/sortablejs": "^1.15.7",
"@types/spark-md5": "^3.0.4",
"@vitejs/plugin-vue": "^5.0.2",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.2",
"eslint-plugin-vue": "^9.20.1",
"npm-run-all": "^4.1.5",
"pulltorefreshjs": "^0.1.22",
"sass": "^1.69.7",
"terser": "^5.26.0",
"typescript": "^5.3.3",
"vite": "^5.0.10",
"vite": "^5.0.11",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.3.1",
"vue-tsc": "^1.8.27"

View File

@ -4,7 +4,12 @@
<div class="page-header">
<div class="row">
<div class="col-sm-11">
<h1>{{ title }}</h1>
<h1>{{ title }}
<span v-if="showWebSocket" :class="{
'onlineMarker': isWebsocketConnected,
'offlineMarker': !isWebsocketConnected,
}"></span>
</h1>
</div>
<div class="col-sm-1" v-if="showReload">
<button type="button" class="float-end btn btn-outline-primary"
@ -28,6 +33,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { BIconArrowClockwise } from 'bootstrap-icons-vue';
import PullToRefresh from 'pulltorefreshjs';
export default defineComponent({
components: {
@ -37,7 +43,81 @@ export default defineComponent({
title: { type: String, required: true },
isLoading: { type: Boolean, required: false, default: false },
isWideScreen: { type: Boolean, required: false, default: false },
isWebsocketConnected: { type: Boolean, required: false, default: false },
showWebSocket: { type: Boolean, required: false, default: false },
showReload: { type: Boolean, required: false, default: false },
},
mounted() {
var self = this;
console.log("init");
PullToRefresh.init({
mainElement: 'main', // above which element?
instructionsPullToRefresh: this.$t('base.Pull'),
instructionsReleaseToRefresh: this.$t('base.Release'),
instructionsRefreshing: this.$t('base.Refreshing'),
onRefresh: function() {
self.$emit('reload');
}
});
},
unmounted() {
console.log("destroy");
PullToRefresh.destroyAll();
},
});
</script>
</script>
<style>
.ptr--text {
color: var(--bs-primary-text-emphasis) !important;
}
.ptr--icon {
color: var(--bs-primary-text-emphasis) !important;
}
.offlineMarker:before {
content: '';
position: absolute;
width: 8px;
height: 8px;
background: #ff0000;
border-color: #ff0000;
border-radius: 50%;
}
.onlineMarker:before {
content: '';
position: absolute;
width: 8px;
height: 8px;
background: #00bb00;
border-color: #00bb00;
border-radius: 50%;
}
.onlineMarker:after {
content: '';
position: absolute;
width: 32px;
height: 32px;
margin: -12px 0 0 -12px;
border: 1px solid #00bb00;
border-radius: 50%;
box-shadow: 0 0 4px #00bb00, inset 0 0 4px rgb(56, 111, 169);
transform: scale(0);
animation: online 2.5s ease-in-out infinite;
}
@keyframes online {
0% {
transform: scale(.1);
opacity: 1;
}
70% {
transform: scale(2.5);
opacity: 0;
}
100% {
opacity: 0;
}
}
</style>

View File

@ -35,7 +35,10 @@
"Loading": "Lade...",
"Reload": "Aktualisieren",
"Cancel": "Abbrechen",
"Save": "Speichern"
"Save": "Speichern",
"Refreshing": "Aktualisieren",
"Pull": "Zum Aktualisieren nach unten ziehen",
"Release": "Loslassen zum Aktualisieren"
},
"localeswitcher": {
"Dark": "Dunkel",
@ -53,6 +56,7 @@
"2002": "Das Abfraginterval muss größer als 0 sein!",
"2003": "Ungültige Sendeleistung angegeben!",
"2004": "Die Frequenz muss zwischen {min} und {max} kHz liegen und ein vielfaches von 250kHz betragen!",
"2005": "Ungültige Landesauswahl!",
"3001": "Nichts gelöscht!",
"3002": "Konfiguration zurückgesetzt. Starte jetzt neu...",
"4001": "@:apiresponse.2001",
@ -418,9 +422,14 @@
"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.",
"CmtCountry": "Region/Land:",
"CmtCountryHint": "Jedes Land hat unterschiedliche Frequenzzuteilungen.",
"country_0": "Europa ({min}MHz - {max}MHz)",
"country_1": "Nordamerika ({min}MHz - {max}MHz)",
"country_2": "Brasilien ({min}MHz - {max}MHz)",
"CmtFrequency": "CMT2300A Frequenz:",
"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.",
"CmtFrequencyWarning": "Die gewählte Frequenz liegt außerhalb des zulässigen Bereichs in der gewählten Region/dem Land. Vergewissere dich, dass mit dieser Auswahl keine lokalen Regularien verletzt werden.",
"MHz": "{mhz} MHz",
"dBm": "{dbm} dBm",
"Min": "Minimum ({db} dBm)",
@ -744,6 +753,10 @@
"PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt",
"Screensaver": "Bildschirmschoner aktivieren:",
"ScreensaverHint": "Bewegt die Ausgabe bei jeder Aktualisierung um ein Einbrennen zu verhindern (v. a. für OLED-Displays nützlich)",
"DiagramMode": "Diagramm Modus:",
"off": "Deaktiviert",
"small": "Klein",
"fullscreen": "Vollbild",
"DiagramDuration": "Diagramm Periode:",
"DiagramDurationHint": "Die Zeitperiode welche im Diagramm dargestellt wird.",
"Seconds": "Sekunden",

View File

@ -35,7 +35,10 @@
"Loading": "Loading...",
"Reload": "Reload",
"Cancel": "Cancel",
"Save": "Save"
"Save": "Save",
"Refreshing": "Refreshing",
"Pull": "Pull down to refresh",
"Release": "Release to refresh"
},
"localeswitcher": {
"Dark": "Dark",
@ -53,6 +56,7 @@
"2002": "Poll interval must be greater zero!",
"2003": "Invalid power level setting!",
"2004": "The frequency must be set between {min} and {max} kHz and must be a multiple of 250kHz!",
"2005": "Invalid country selection!",
"3001": "Not deleted anything!",
"3002": "Configuration resettet. Rebooting now...",
"4001": "@:apiresponse.2001",
@ -420,9 +424,14 @@
"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.",
"CmtCountry": "Region/Country:",
"CmtCountryHint": "Each country has different frequency allocations.",
"country_0": "Europe ({min}MHz - {max}MHz)",
"country_1": "North America ({min}MHz - {max}MHz)",
"country_2": "Brazil ({min}MHz - {max}MHz)",
"CmtFrequency": "CMT2300A Frequency:",
"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.",
"CmtFrequencyWarning": "The selected frequency is outside the allowed range in your selected region/country. Make sure that this selection does not violate any local regulations.",
"MHz": "{mhz} MHz",
"dBm": "{dbm} dBm",
"Min": "Minimum ({db} dBm)",
@ -753,6 +762,10 @@
"PowerSafeHint": "Turn off the display if no inverter is producing.",
"Screensaver": "Enable Screensaver:",
"ScreensaverHint": "Move the display a little bit on each update to prevent burn-in. (Useful especially for OLED displays)",
"DiagramMode": "Diagram mode:",
"off": "Off",
"small": "Small",
"fullscreen": "Fullscreen",
"DiagramDuration": "Diagram duration:",
"DiagramDurationHint": "The time period which is shown in the diagram.",
"Seconds": "Seconds",

View File

@ -35,7 +35,10 @@
"Loading": "Chargement...",
"Reload": "Reload",
"Cancel": "Annuler",
"Save": "Sauvegarder"
"Save": "Sauvegarder",
"Refreshing": "Refreshing",
"Pull": "Pull down to refresh",
"Release": "Release to refresh"
},
"localeswitcher": {
"Dark": "Sombre",
@ -53,6 +56,7 @@
"2002": "L'intervalle de sondage doit être supérieur à zéro !",
"2003": "Réglage du niveau de puissance invalide !",
"2004": "The frequency must be set between {min} and {max} kHz and must be a multiple of 250kHz!",
"2005": "Invalid country selection !",
"3001": "Rien n'a été supprimé !",
"3002": "Configuration réinitialisée. Redémarrage maintenant...",
"4001": "@:apiresponse.2001",
@ -418,9 +422,14 @@
"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.",
"CmtCountry": "Region/Country:",
"CmtCountryHint": "Each country has different frequency allocations.",
"country_0": "Europe ({min}MHz - {max}MHz)",
"country_1": "North America ({min}MHz - {max}MHz)",
"country_2": "Brazil ({min}MHz - {max}MHz)",
"CmtFrequency": "CMT2300A Frequency:",
"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.",
"CmtFrequencyWarning": "The selected frequency is outside the allowed range in your selected region/country. Make sure that this selection does not violate any local regulations.",
"MHz": "{mhz} MHz",
"dBm": "{dbm} dBm",
"Min": "Minimum ({db} dBm)",
@ -711,6 +720,10 @@
"PowerSafeHint": "Eteindre l'écran si aucun onduleur n'est en production.",
"Screensaver": "Activer l'écran de veille",
"ScreensaverHint": "Déplacez un peu l'écran à chaque mise à jour pour éviter le phénomène de brûlure. (Utile surtout pour les écrans OLED)",
"DiagramMode": "Diagram mode:",
"off": "Off",
"small": "Small",
"fullscreen": "Fullscreen",
"DiagramDuration": "Diagram duration:",
"DiagramDurationHint": "The time period which is shown in the diagram.",
"Seconds": "Seconds",

View File

@ -7,6 +7,7 @@ export interface Display {
contrast: number;
language: number;
diagramduration: number;
diagrammode: number;
}
export interface Led {
@ -17,4 +18,4 @@ export interface DeviceConfig {
curPin: Device;
display: Display;
led: Array<Led>;
}
}

View File

@ -1,3 +1,11 @@
export interface CountryDef {
freq_default: number;
freq_min: number;
freq_max: number;
freq_legal_min: number;
freq_legal_max: number;
}
export interface DtuConfig {
serial: number;
pollinterval: number;
@ -7,4 +15,7 @@ export interface DtuConfig {
cmt_enabled: boolean;
cmt_palevel: number;
cmt_frequency: number;
}
cmt_country: number;
country_def: Array<CountryDef>;
cmt_chan_width: number;
}

View File

@ -1,17 +1,8 @@
export const timestampToString = (value: number, includeDays = false): string => {
const days = Math.floor(value / (24 * 60 * 60));
const secAfterDays = value - days * (24 * 60 * 60);
const hours = Math.floor(secAfterDays / (60 * 60));
const secAfterHours = secAfterDays - hours * (60 * 60);
const minutes = Math.floor(secAfterHours / 60);
const seconds = secAfterHours - minutes * 60;
export const timestampToString = (timestampSeconds: number, includeDays = false): string => {
const timeString = new Date(timestampSeconds * 1000).toLocaleTimeString([], { timeZone: "UTC" });
if (!includeDays) return timeString;
const dHours = hours > 9 ? hours : "0" + hours;
const dMins = minutes > 9 ? minutes : "0" + minutes;
const dSecs = seconds > 9 ? seconds : "0" + seconds;
if (includeDays) {
return days + " days " + dHours + ":" + dMins + ":" + dSecs;
}
return dHours + ":" + dMins + ":" + dSecs;
const secondsPerDay = 60 * 60 * 24;
const days = Math.floor(timestampSeconds / secondsPerDay);
return new Intl.RelativeTimeFormat().format(-days, "day") + " " + timeString;
}

View File

@ -39,7 +39,7 @@
<div class="row mb-3">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<div class="btn-group" v-for="(doc, index) in pinMappingList.find(i => i.name === deviceConfigList.curPin.name)?.links" :key="index">
<div class="btn-group mb-2 me-2" v-for="(doc, index) in pinMappingList.find(i => i.name === deviceConfigList.curPin.name)?.links" :key="index">
<a :href="doc.url" class="btn btn-primary" target="_blank">{{ doc.name }}</a>
</div>
</div>
@ -67,6 +67,19 @@
v-model="deviceConfigList.display.screensaver" type="checkbox"
:tooltip="$t('deviceadmin.ScreensaverHint')" />
<div class="row mb-3">
<label class="col-sm-2 col-form-label">
{{ $t('deviceadmin.DiagramMode') }}
</label>
<div class="col-sm-10">
<select class="form-select" v-model="deviceConfigList.display.diagrammode">
<option v-for="mode in diagramModeList" :key="mode.key" :value="mode.key">
{{ $t(`deviceadmin.` + mode.value) }}
</option>
</select>
</div>
</div>
<InputElement :label="$t('deviceadmin.DiagramDuration')"
v-model="deviceConfigList.display.diagramduration" type="number"
min=600 max=86400
@ -183,6 +196,11 @@ export default defineComponent({
{ key: 1, value: "de" },
{ key: 2, value: "fr" },
],
diagramModeList: [
{ key: 0, value: "off" },
{ key: 1, value: "small" },
{ key: 2, value: "fullscreen" },
]
}
},
created() {
@ -274,4 +292,4 @@ export default defineComponent({
}
},
});
</script>
</script>

View File

@ -51,6 +51,20 @@
</div>
</div>
<div class="row mb-3" v-if="dtuConfigList.cmt_enabled">
<label for="inputCmtCountry" class="col-sm-2 col-form-label">
{{ $t('dtuadmin.CmtCountry') }}
<BIconInfoCircle v-tooltip :title="$t('dtuadmin.CmtCountryHint')" />
</label>
<div class="col-sm-10">
<select id="inputCmtCountry" class="form-select" v-model="dtuConfigList.cmt_country">
<option v-for="(country, index) in dtuConfigList.country_def" :key="index" :value="index">
{{ $t(`dtuadmin.country_` + index, {min: country.freq_min / 1e6, max: country.freq_max / 1e6}) }}
</option>
</select>
</div>
</div>
<div class="row mb-3" v-if="dtuConfigList.cmt_enabled">
<label for="cmtFrequency" class="col-sm-2 col-form-label">
{{ $t('dtuadmin.CmtFrequency') }}
@ -60,12 +74,12 @@
<div class="input-group mb-3">
<input type="range" class="form-control form-range"
v-model="dtuConfigList.cmt_frequency"
min="860250" max="923500" step="250"
:min="cmtMinFrequency" :max="cmtMaxFrequency" :step="dtuConfigList.cmt_chan_width"
id="cmtFrequency" aria-describedby="basic-addon2"
style="height: unset;" />
<span class="input-group-text" id="basic-addon2">{{ cmtFrequencyText }}</span>
</div>
<div class="alert alert-danger" role="alert" v-html="$t('dtuadmin.CmtFrequencyWarning')" v-if="cmtIsOutOfEu"></div>
<div class="alert alert-danger" role="alert" v-html="$t('dtuadmin.CmtFrequencyWarning')" v-if="cmtIsOutOfLegalRange"></div>
</div>
</div>
@ -115,13 +129,30 @@ export default defineComponent({
},
computed: {
cmtFrequencyText() {
return this.$t("dtuadmin.MHz", { mhz: this.$n(this.dtuConfigList.cmt_frequency / 1000, "decimalTwoDigits") });
return this.$t("dtuadmin.MHz", { mhz: this.$n(this.dtuConfigList.cmt_frequency / 1000000, "decimalTwoDigits") });
},
cmtPaLevelText() {
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;
cmtMinFrequency() {
return this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_min;
},
cmtMaxFrequency() {
return this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_max;
},
cmtIsOutOfLegalRange() {
return this.dtuConfigList.cmt_frequency < this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_legal_min
|| this.dtuConfigList.cmt_frequency > this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_legal_max;
}
},
watch: {
'dtuConfigList.cmt_country'(newValue, oldValue) {
// Don't do anything on initial load (then oldValue equals undefined)
if (oldValue != undefined) {
this.$nextTick(() => {
this.dtuConfigList.cmt_frequency = this.dtuConfigList.country_def[newValue].freq_default;
});
}
}
},
methods: {
@ -158,4 +189,4 @@ export default defineComponent({
},
},
});
</script>
</script>

View File

@ -1,5 +1,5 @@
<template>
<BasePage :title="$t('home.LiveData')" :isLoading="dataLoading" :isWideScreen="true">
<BasePage :title="$t('home.LiveData')" :isLoading="dataLoading" :isWideScreen="true" :showWebSocket="true" :isWebsocketConnected="isWebsocketConnected" @reload="reloadData">
<HintView :hints="liveData.hints" />
<InverterTotalInfo :totalData="liveData.total" :totalVeData="liveData.vedirect" :totalBattData="liveData.battery" :powerMeterData="liveData.power_meter" :huaweiData="liveData.huawei"/><br />
<div class="row gy-3">
@ -457,6 +457,8 @@ export default defineComponent({
alertTypePower: "info",
showAlertPower: false,
successCommandPower: "",
isWebsocketConnected: false,
};
},
created() {
@ -484,17 +486,23 @@ export default defineComponent({
this.closeSocket();
},
updated() {
console.log("Updated");
// Select first tab
if (this.isFirstFetchAfterConnect) {
this.isFirstFetchAfterConnect = false;
console.log("isFirstFetchAfterConnect");
const firstTabEl = document.querySelector(
"#v-pills-tab:first-child button"
);
if (firstTabEl != null) {
const firstTab = new bootstrap.Tab(firstTabEl);
firstTab.show();
}
this.$nextTick(() => {
console.log("nextTick");
const firstTabEl = document.querySelector(
"#v-pills-tab:first-child button"
);
if (firstTabEl != null) {
this.isFirstFetchAfterConnect = false;
console.log("Show");
const firstTab = new bootstrap.Tab(firstTabEl);
firstTab.show();
}
});
}
},
computed: {
@ -517,15 +525,27 @@ export default defineComponent({
},
methods: {
isLoggedIn,
getInitialData() {
this.dataLoading = true;
getInitialData(triggerLoading : boolean = true) {
if (triggerLoading) {
this.dataLoading = true;
}
fetch("/api/livedata/status", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.liveData = data;
this.dataLoading = false;
if (triggerLoading) {
this.dataLoading = false;
}
});
},
reloadData() {
this.closeSocket();
setTimeout(() => {
this.getInitialData(false);
this.initSocket();
}, 1000);
},
initSocket() {
console.log("Starting connection to WebSocket Server");
@ -549,11 +569,19 @@ export default defineComponent({
}
};
var self = this;
this.socket.onopen = function (event) {
console.log(event);
console.log("Successfully connected to the echo websocket server...");
self.isWebsocketConnected = true;
};
this.socket.onclose = function() {
console.log("Connection to websocket closed...")
self.isWebsocketConnected = false;
}
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
window.onbeforeunload = () => {
this.closeSocket();
@ -772,4 +800,4 @@ export default defineComponent({
border-radius: var(--bs-border-radius);
margin-top: 0.25rem;
}
</style>
</style>

View File

@ -211,20 +211,20 @@
source-map-js "^1.0.1"
yaml-eslint-parser "^1.2.2"
"@intlify/core-base@9.8.0":
version "9.8.0"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.8.0.tgz#969ca59f55084e23e968ec0bfe71678774e568ec"
integrity sha512-UxaSZVZ1DwqC/CltUZrWZNaWNhfmKtfyV4BJSt/Zt4Or/fZs1iFj0B+OekYk1+MRHfIOe3+x00uXGQI4PbO/9g==
"@intlify/core-base@9.9.0":
version "9.9.0"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.9.0.tgz#edc55a5e3dbbf8dbbbf656529ed27832c4c4f522"
integrity sha512-C7UXPymDIOlMGSNjAhNLtKgzITc/8BjINK5gNKXg8GiWCTwL6n3MWr55czksxn8RM5wTMz0qcLOFT+adtaVQaA==
dependencies:
"@intlify/message-compiler" "9.8.0"
"@intlify/shared" "9.8.0"
"@intlify/message-compiler" "9.9.0"
"@intlify/shared" "9.9.0"
"@intlify/message-compiler@9.8.0":
version "9.8.0"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.8.0.tgz#587d69b302f9b8130a4a949b0ab4add519761787"
integrity sha512-McnYWhcoYmDJvssVu6QGR0shqlkJuL1HHdi5lK7fNqvQqRYaQ4lSLjYmZxwc8tRNMdIe9/KUKfyPxU9M6yCtNQ==
"@intlify/message-compiler@9.9.0":
version "9.9.0"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.9.0.tgz#7952759329e7af0388afbce7a984820bbeff82eb"
integrity sha512-yDU/jdUm9KuhEzYfS+wuyja209yXgdl1XFhMlKtXEgSFTxz4COZQCRXXbbH8JrAjMsaJ7bdoPSLsKlY6mXG2iA==
dependencies:
"@intlify/shared" "9.8.0"
"@intlify/shared" "9.9.0"
source-map-js "^1.0.2"
"@intlify/message-compiler@^9.4.0":
@ -240,10 +240,10 @@
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.4.0.tgz#4a78d462fc82433db900981e12eb5b1aae3d6085"
integrity sha512-AFqymip2kToqA0B6KZPg5jSrdcVHoli9t/VhGKE2iiMq9utFuMoGdDC/JOCIZgwxo6aXAk86QyU2XtzEoMuZ6A==
"@intlify/shared@9.8.0":
version "9.8.0"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.8.0.tgz#62adf8f6ef67c8eba6cf8d521e248f3503f237d3"
integrity sha512-TmgR0RCLjzrSo+W3wT0ALf9851iFMlVI9EYNGeWvZFUQTAJx0bvfsMlPdgVtV1tDNRiAfhkFsMKu6jtUY1ZLKQ==
"@intlify/shared@9.9.0":
version "9.9.0"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.9.0.tgz#56907633c0f7b2d50f53269d31e88e7b24d39187"
integrity sha512-1ECUyAHRrzOJbOizyGufYP2yukqGrWXtkmTu4PcswVnWbkcjzk3YQGmJ0bLkM7JZ0ZYAaohLGdYvBYnTOGYJ9g==
"@intlify/unplugin-vue-i18n@^2.0.0":
version "2.0.0"
@ -435,13 +435,18 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
"@types/node@^20.10.6":
version "20.10.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5"
integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==
"@types/node@^20.11.0":
version "20.11.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f"
integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==
dependencies:
undici-types "~5.26.4"
"@types/pulltorefreshjs@^0.1.7":
version "0.1.7"
resolved "https://registry.yarnpkg.com/@types/pulltorefreshjs/-/pulltorefreshjs-0.1.7.tgz#1948638b0c7071282e47bd236d2ccb88bdf66753"
integrity sha512-Y0g/yfuycIvpvUmP97n5NE2+HDAOwfREGVERjhMWw2Y0ODh5wvbflcQ5gXPZ+ihgoq+BQZjA1DL8apw2wAsJXA==
"@types/semver@^7.5.0":
version "7.5.1"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.1.tgz#0480eeb7221eb9bc398ad7432c9d7e14b1a5a367"
@ -547,10 +552,10 @@
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
"@vitejs/plugin-vue@^5.0.2":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.2.tgz#8428ec3f446b9c2f7a7ec950f34e3d6f3c665444"
integrity sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==
"@vitejs/plugin-vue@^5.0.3":
version "5.0.3"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz#164b36653910d27c130cf6c945b4bd9bde5bcbee"
integrity sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==
"@volar/language-core@1.11.1", "@volar/language-core@~1.11.1":
version "1.11.1"
@ -594,13 +599,13 @@
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-core@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.5.tgz#9565aebaadef8649eb7c8e150a5f4f4e2542667d"
integrity sha512-Daka7P1z2AgKjzuueWXhwzIsKu0NkLB6vGbNVEV2iJ8GJTrzraZo/Sk4GWCMRtd/qVi3zwnk+Owbd/xSZbwHtQ==
"@vue/compiler-core@3.4.13":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.13.tgz#49f499034c25b0832845028ea3cd701fe5a17367"
integrity sha512-zGUdmB3j3Irn9z51GXLJ5s0EAHxmsm5/eXl0y6MBaajMeOAaiT4+zaDoxui4Ets98dwIRr8BBaqXXHtHSfm+KA==
dependencies:
"@babel/parser" "^7.23.6"
"@vue/shared" "3.4.5"
"@vue/shared" "3.4.13"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.0.2"
@ -613,13 +618,13 @@
"@vue/compiler-core" "3.2.47"
"@vue/shared" "3.2.47"
"@vue/compiler-dom@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.5.tgz#c53c9d7715b777b1d6d2adcbc491bfd4f9510edd"
integrity sha512-J8YlxknJVd90SXFJ4HwGANSAXsx5I0lK30sO/zvYV7s5gXf7gZR7r/1BmZ2ju7RGH1lnc6bpBc6nL61yW+PsAQ==
"@vue/compiler-dom@3.4.13":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.13.tgz#66a80a6ee412a3d32b7175a146b75d9ec3d1c50c"
integrity sha512-XSNbpr5Rs3kCfVAmBqMu/HDwOS+RL6y28ZZjDlnDUuf146pRWt2sQkwhsOYc9uu2lxjjJy2NcyOkK7MBLVEc7w==
dependencies:
"@vue/compiler-core" "3.4.5"
"@vue/shared" "3.4.5"
"@vue/compiler-core" "3.4.13"
"@vue/shared" "3.4.13"
"@vue/compiler-dom@^3.3.0":
version "3.3.2"
@ -629,16 +634,16 @@
"@vue/compiler-core" "3.3.2"
"@vue/shared" "3.3.2"
"@vue/compiler-sfc@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.5.tgz#f93f986dfc5c7f72b9a5e00b48be75d9116cc948"
integrity sha512-jauvkDuSSUbP0ebhfNqljhShA90YEfX/0wZ+w40oZF43IjGyWYjqYaJbvMJwGOd+9+vODW6eSvnk28f0SGV7OQ==
"@vue/compiler-sfc@3.4.13":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.13.tgz#0f5f6db0e64f522c09995585453ae5f13ba54c60"
integrity sha512-SkpmQN8xIFBd5onT413DFSDdjxULJf6jmJg/t3w/DZ9I8ZzyNlLIBLO0qFLewVHyHCiAgpPZlWqSRZXYrawk3Q==
dependencies:
"@babel/parser" "^7.23.6"
"@vue/compiler-core" "3.4.5"
"@vue/compiler-dom" "3.4.5"
"@vue/compiler-ssr" "3.4.5"
"@vue/shared" "3.4.5"
"@vue/compiler-core" "3.4.13"
"@vue/compiler-dom" "3.4.13"
"@vue/compiler-ssr" "3.4.13"
"@vue/shared" "3.4.13"
estree-walker "^2.0.2"
magic-string "^0.30.5"
postcss "^8.4.32"
@ -668,13 +673,13 @@
"@vue/compiler-dom" "3.2.47"
"@vue/shared" "3.2.47"
"@vue/compiler-ssr@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.5.tgz#d412a4c9b10d69172a5ce0ec78de98dad441a58d"
integrity sha512-DDdEcDzj2lWTMfUMMtEpLDhURai9LhM0zSZ219jCt7b2Vyl0/jy3keFgCPMitG0V1S1YG4Cmws3lWHWdxHQOpg==
"@vue/compiler-ssr@3.4.13":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.13.tgz#90fa9a4116f7974d7a4e43a8a67f3fc162e8720f"
integrity sha512-rwnw9SVBgD6eGKh8UucnwztieQo/R3RQrEGpE0b0cxb2xxvJeLs/fe7DoYlhEfaSyzM/qD5odkK87hl3G3oW+A==
dependencies:
"@vue/compiler-dom" "3.4.5"
"@vue/shared" "3.4.5"
"@vue/compiler-dom" "3.4.13"
"@vue/shared" "3.4.13"
"@vue/devtools-api@^6.5.0":
version "6.5.0"
@ -716,37 +721,37 @@
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/reactivity@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.5.tgz#68bc91cd356eed95dc5e9e0570e3f7becaee578b"
integrity sha512-BcWkKvjdvqJwb7BhhFkXPLDCecX4d4a6GATvCduJQDLv21PkPowAE5GKuIE5p6RC07/Lp9FMkkq4AYCTVF5KlQ==
"@vue/reactivity@3.4.13":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.13.tgz#7eeeb9d598512f66e06a6438fd53464014b5ae59"
integrity sha512-/ZdUOrGKkGVONzVJkfDqNcn2fLMvaa5VlYx2KwTbnRbX06YZ4GJE0PVTmWzIxtBYdpSTLLXgw3pDggO+96KXzg==
dependencies:
"@vue/shared" "3.4.5"
"@vue/shared" "3.4.13"
"@vue/runtime-core@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.5.tgz#2bf253a6f6b0430af1aacf0fdfd8f5782feefce9"
integrity sha512-wh9ELIOQKeWT9SaUPdLrsxRkZv14jp+SJm9aiQGWio+/MWNM3Lib0wE6CoKEqQ9+SCYyGjDBhTOTtO47kCgbkg==
"@vue/runtime-core@3.4.13":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.13.tgz#776cad7c1d56ec5e92a48e040c8483b89f779542"
integrity sha512-Ov4d4At7z3goxqzSqQxdfVYEcN5HY4dM1uDYL6Hu/Es9Za9BEN602zyjWhhi2+BEki5F9NizRSvn02k/tqNWlg==
dependencies:
"@vue/reactivity" "3.4.5"
"@vue/shared" "3.4.5"
"@vue/reactivity" "3.4.13"
"@vue/shared" "3.4.13"
"@vue/runtime-dom@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.5.tgz#b43736d66c32f6038778024587592cb9d68495de"
integrity sha512-n5ewvOjyG3IEpqGBahdPXODFSpVlSz3H4LF76Sx0XAqpIOqyJ5bIb2PrdYuH2ogBMAQPh+o5tnoH4nJpBr8U0Q==
"@vue/runtime-dom@3.4.13":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.13.tgz#74aff1494bee49c037b9c5355d8998c793ac0977"
integrity sha512-ynde9p16eEV3u1VCxUre2e0nKzD0l3NzH0r599+bXeLT1Yhac8Atcot3iL9XNqwolxYCI89KBII+2MSVzfrz6w==
dependencies:
"@vue/runtime-core" "3.4.5"
"@vue/shared" "3.4.5"
"@vue/runtime-core" "3.4.13"
"@vue/shared" "3.4.13"
csstype "^3.1.3"
"@vue/server-renderer@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.5.tgz#4bfa7aa763217d8b2d4767d2c8d968a9d40352c1"
integrity sha512-jOFc/VE87yvifQpNju12VcqimH8pBLxdcT+t3xMeiED1K6DfH9SORyhFEoZlW5TG2Vwfn3Ul5KE+1aC99xnSBg==
"@vue/server-renderer@3.4.13":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.13.tgz#b8c9cfb2147c0a01feba7f136d3a432848dafcab"
integrity sha512-hkw+UQyDZZtSn1q30nObMfc8beVEQv2pG08nghigxGw+iOWodR+tWSuJak0mzWAHlP/xt/qLc//dG6igfgvGEA==
dependencies:
"@vue/compiler-ssr" "3.4.5"
"@vue/shared" "3.4.5"
"@vue/compiler-ssr" "3.4.13"
"@vue/shared" "3.4.13"
"@vue/shared@3.2.47":
version "3.2.47"
@ -758,10 +763,10 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.2.tgz#774cd9b4635ce801b70a3fc3713779a5ef5d77c3"
integrity sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ==
"@vue/shared@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.5.tgz#c8b4eb6399a7fc986565ea736d938b3a1579256d"
integrity sha512-6XptuzlMvN4l4cDnDw36pdGEV+9njYkQ1ZE0Q6iZLwrKefKaOJyiFmcP3/KBDHbt72cJZGtllAc1GaHe6XGAyg==
"@vue/shared@3.4.13":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.13.tgz#a1eefce5ddffe207d53eafbc07f4ebbea0a4768a"
integrity sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==
"@vue/tsconfig@^0.5.1":
version "0.5.1"
@ -1141,17 +1146,17 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
eslint-plugin-vue@^9.19.2:
version "9.19.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.19.2.tgz#7ab83a001a1ac8bccae013c5b9cb5d2c644fb376"
integrity sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA==
eslint-plugin-vue@^9.20.1:
version "9.20.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.20.1.tgz#7ed78846898574b2cd26939f28b0b87798a7b528"
integrity sha512-GyCs8K3lkEvoyC1VV97GJhP1SvqsKCiWGHnbn0gVUYiUhaH2+nB+Dv1uekv1THFMPbBfYxukrzQdltw950k+LQ==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
natural-compare "^1.4.0"
nth-check "^2.1.1"
postcss-selector-parser "^6.0.13"
semver "^7.5.4"
vue-eslint-parser "^9.3.1"
vue-eslint-parser "^9.4.0"
xml-name-validator "^4.0.0"
eslint-scope@^7.1.1:
@ -2142,6 +2147,11 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
pulltorefreshjs@^0.1.22:
version "0.1.22"
resolved "https://registry.yarnpkg.com/pulltorefreshjs/-/pulltorefreshjs-0.1.22.tgz#ddb5e3feee0b2a49fd46e1b18e84fffef2c47ac0"
integrity sha512-haxNVEHnS4NCQA7NeG7TSV69z4uqy/N7nfPRuc4dPWe8H6ygUrMjdNeohE+6v0lVVX/ukSjbLYwPUGUYtFKfvQ==
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@ -2551,10 +2561,10 @@ vite-plugin-css-injected-by-js@^3.3.1:
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.3.1.tgz#26b41f108c5554ee728359bdec01c68c93a48547"
integrity sha512-PjM/X45DR3/V1K1fTRs8HtZHEQ55kIfdrn+dzaqNBFrOYO073SeSNCxp4j7gSYhV9NffVHaEnOL4myoko0ePAg==
vite@^5.0.10:
version "5.0.10"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.10.tgz#1e13ef5c3cf5aa4eed81f5df6d107b3c3f1f6356"
integrity sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==
vite@^5.0.11:
version "5.0.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.11.tgz#31562e41e004cb68e1d51f5d2c641ab313b289e4"
integrity sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==
dependencies:
esbuild "^0.19.3"
postcss "^8.4.32"
@ -2575,13 +2585,26 @@ vue-eslint-parser@^9.3.1:
lodash "^4.17.21"
semver "^7.3.6"
vue-i18n@^9.8.0:
version "9.8.0"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.8.0.tgz#54339daf377a31b234b027c5158e774728b6bc24"
integrity sha512-Izho+6PYjejsTq2mzjcRdBZ5VLRQoSuuexvR8029h5CpN03FYqiqBrShMyf2I1DKkN6kw/xmujcbvC+4QybpsQ==
vue-eslint-parser@^9.4.0:
version "9.4.0"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.0.tgz#dfd22302e2992fe45748a76553cef7afa5bdde27"
integrity sha512-7KsNBb6gHFA75BtneJsoK/dbZ281whUIwFYdQxA68QrCrGMXYzUMbPDHGcOQ0OocIVKrWSKWXZ4mL7tonCXoUw==
dependencies:
"@intlify/core-base" "9.8.0"
"@intlify/shared" "9.8.0"
debug "^4.3.4"
eslint-scope "^7.1.1"
eslint-visitor-keys "^3.3.0"
espree "^9.3.1"
esquery "^1.4.0"
lodash "^4.17.21"
semver "^7.3.6"
vue-i18n@^9.9.0:
version "9.9.0"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.9.0.tgz#20d348fa7e37fc88e4c84f69781b2f1215c7769f"
integrity sha512-xQ5SxszUAqK5n84N+uUyHH/PiQl9xZ24FOxyAaNonmOQgXeN+rD9z/6DStOpOxNFQn4Cgcquot05gZc+CdOujA==
dependencies:
"@intlify/core-base" "9.9.0"
"@intlify/shared" "9.9.0"
"@vue/devtools-api" "^6.5.0"
vue-router@^4.2.5:
@ -2608,16 +2631,16 @@ vue-tsc@^1.8.27:
"@vue/language-core" "1.8.27"
semver "^7.5.4"
vue@^3.4.5:
version "3.4.5"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.5.tgz#c08b9d903a20faaf4df7270bf2fa7487741b2294"
integrity sha512-VH6nHFhLPjgu2oh5vEBXoNZxsGHuZNr3qf4PHClwJWw6IDqw6B3x+4J+ABdoZ0aJuT8Zi0zf3GpGlLQCrGWHrw==
vue@^3.4.13:
version "3.4.13"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.13.tgz#aa522baf2344d1c4c54c769f66c0151f1872f1ff"
integrity sha512-FE3UZ0p+oUZTwz+SzlH/hDFg+XsVRFvwmx0LXjdD1pRK/cO4fu5v6ltAZji4za4IBih3dV78elUK3di8v3pWIg==
dependencies:
"@vue/compiler-dom" "3.4.5"
"@vue/compiler-sfc" "3.4.5"
"@vue/runtime-dom" "3.4.5"
"@vue/server-renderer" "3.4.5"
"@vue/shared" "3.4.5"
"@vue/compiler-dom" "3.4.13"
"@vue/compiler-sfc" "3.4.13"
"@vue/runtime-dom" "3.4.13"
"@vue/server-renderer" "3.4.13"
"@vue/shared" "3.4.13"
webpack-sources@^3.2.3:
version "3.2.3"

Binary file not shown.

Binary file not shown.

Binary file not shown.