Tasmota/tasmota/tasmota_xnrg_energy/xnrg_26_mk_sky_blu.ino
meMorizE 54519dccbf
MakeSkyBlu (xnrg_26): Bugfix telemetry data (verified by MQTT) (#24313)
* Change unit relation of local sensor to setoption8, add unit conversion at ThermostatShow
Bugfix for https://github.com/arendst/Tasmota/issues/24165

* Bugfix: Wrong data for FUNC_JSON_APPEND at MkSkyBluShow (Efficiency purged at FloatArrays)
2026-01-07 10:54:58 +01:00

857 lines
38 KiB
C++

/*
xnrg_26_mk_sky_blu.ino - MakeSkyBlue Solar charger support for Tasmota
Copyright (C) 2025 meMorizE
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef USE_ENERGY_SENSOR
#ifdef USE_MAKE_SKY_BLUE
/*********************************************************************************************\
* This implementation communicates with solar charge controller of MakeSkyBlue(Shenzen) Co.LTD
* http://www.makeskyblue.com
* Model: S3-30A/40A/50A/60A MPPT 12V/24V/36V/48V with firmwware V118 (or V119 including a Wifi Box)
* Tested with Model S3-60A
* https://makeskyblue.com/en-de/products/60a-mppt-solar-charge-controller-w-wifi
*
* Precondition to get communication working:
* The charge controller should have a Mini-USB socket at bottom right beside the screw terminals.
* It has a Vcc supply output of +5V and GND but the D+ and D- signals are NOT according USB standard !
* They are TTL serial signals with 9600bps 8N1, D+=Tx_Out D-=Rx_In
* When using a DIY ESP hardware be aware of the 5V TTL levels by using e.g. level shifter or isolator
*
* This implementation supports multiple charge controllers at one tasmota-ESP (by 2025-11):
* ESP82xx up to 3
* ESP32xx up to 8 (theoretically, not tested if so many serial interfaces will work)
* Every serial receiver channel is related to one energy phase.
* It allows to have only one transmitter and multiple receivers, but this does
* not allow individual addressing for on/off and register read/write.
*
* The orignal V119 Wifi Box hardware includes an ESP8285 with 1MB Flash (as module 2AL3B ESP-M)
* This specific hardware uses
* GPIO1: TX0, ESP => Charge controller
* GPIO3: RXD0, ESP <= Charge controller
* GPIO5 red LED, active low, for e.g. LedLink_i
* free (or bootstrap): GPIO4, GPIO12, GPIO13, GPIO14, GPIO15, GPIO16, GPIO17(ADC)
* {"NAME":"MakeSkyBlue Wifi-Adapter (ESP8285)","GPIO":[1,10528,1,10560,1,1,0,0,1,1,1,1,1,1],"FLAG":0,"BASE":18}
*
* Useful runtime commands and options:
* VoltRes 1 - select voltage resolution to 0.1 V (native resolution of solar charger)
* AmpRes 1 - select current resolution to 0.1 A (native resolution of solar charger)
* SetOption72 - read and use total energy from the memory within the charge controller
* SetOption129 1 - Display energy for each phase instead of single sum (only if multiple channels configured)
* SetOption150 1 - Display no common voltage/frequency
*
* This implementation is based on information from
* https://github.com/lasitha-sparrow/Makeskyblue_wifi_Controller
* https://www.genetrysolar.com/community/forum-134/topic-1219/
*
* and logic analyzer recording of the original Wifi-Box firmware together with the Android app:
* Status: - request periodical every 1s
* - response typical within 30ms
* Measurements: - request periodical every 1s, about 380ms after Status request
* - response varies within 30...370ms
* ReadRegister: - requested on demand with an interval of min. 170ms, valid registers 1...9
* - response typical within 30ms
\*********************************************************************************************/
#include <TasmotaSerial.h>
#define XNRG_26 26
/* available compile options, bit encoded */
#if MAKE_SKY_BLUE_OPTION & 0x1
#define MKSB_WITH_SERIAL_DEBUGGING // provide counters for error detection and statistics
// >Practical use: a real device shows frequent timeouts and / or CRC errors, typically if the solar power is more than 350W.
#endif
//
#if MAKE_SKY_BLUE_OPTION & 0x2
#define MKSB_WITH_ON_OFF_SUPPORT // allow to switch charging OFF and ON
// >Use Console command 'EnergyConfig 0 -' to switch OFF
// >Use Console command 'EnergyConfig 0 +' to switch ON
#endif
//
#if MAKE_SKY_BLUE_OPTION & 0x4
#define MKSB_WITH_REGISTER_SUPPORT // read and write access to configuration registers
// >Use Console command 'EnergyConfig' to read and write configuration registers R1...R9
// no parameters: show help
// 1st parameter: i = transmit interface, 0 for all
// 2nd parameter: n = address value of specific register number n
// 3rd parameters: v = value to write to specified register number n, none=read
#endif
#define MKSB_BAUDRATE 9600
// TxRx first byte of every valid frame
#define MKSB_START_FRAME 0xAA
// Tx second byte: Request to the charge controller
#define MKSB_CMD_READ_MEASUREMENTS 0x55 // response: 0xBB
#define MKSB_CMD_READ_REGISTER 0xCB // response: 0xDA
#define MKSB_CMD_WRITE_REGISTER 0xCA // response: 0xDA
#define MKSB_CMD_AUXILARY 0xCC // response: 0xDC or 0xDD
// Rx second byte: Response from the charge controller
#define MKSB_RSP_READ_MEASURES 0xBB // request: 0xAA
#define MKSB_RSP_RW_CONFIG 0xDA // request: 0xCB or 0xCA
#define MKSB_RSP_CLR_WIFI_PASSWORD 0xDC // D05 Clear Wifi-Password (not implemented)
#define MKSB_RSP_POWER 0xDD // request 0xCC
//
#define MKSB_RSP_SZ_READ_MEASURES 20 // bytes // minimum size of response frame
#define MKSB_RSP_SZ_RW_CONFIG 9 // bytes // size of response frame
// TxRx third byte: config registers (request 0xCB or 0xCA, response 0xDA)
#define MKSB_REG_FIRST 1 // vvv = related local parameter at display UI
#define MKSB_REG_VOLTAGE_BULK 1 // D02 MPPT Voltage limit BULK, >= stops charging [mV], e.g. 55000mV
#define MKSB_REG_VOLTAGE_FLOAT 2 // D01 MPPT voltage limit FLOAT, <= restarts charging [mV], SLA battery only
#define MKSB_REG_OUT_TIMER 3 // D00 Time duration the load gets connected [mh], default: 24h = 24000mh
#define MKSB_REG_CURRENT 4 // - MPPT current limit [mA] e.g. 1000...60000mA, CAUTION: respect limit of hardware
#define MKSB_REG_BATT_UVP_CUT 5 // D03 Battery UnderVoltageProtection, <=limit cuts load [mV], e.g. 42400mV
#define MKSB_REG_BATT_UVP_CONN 6 // ~ Battery UnderVoltageProtection, >=limit reconnects load [mV], typical: D03 + 200mV, e.g. 44400mV
#define MKSB_REG_COMM_ADDR 7 // - Communication Address [mAddress]
#define MKSB_REG_BATT_TYPE 8 // D04 Battery Type: value 0=SLA, 1=LiPo (2=LiLo, 3=LiFE, 4=LiTo) [mSelect], e.g. 1000=LiPo
#define MKSB_REG_BATT_CELLS 9 // - Battery System: 1...4 * 12V (Read-Only, set at Batt.connection) [m12V], e.g. 4000m12V
#define MKSB_REG_LAST 9 // ^^^ = related local parameter at display UI
#define MKSB_REG_TOTAL (1+(MKSB_REG_LAST-MKSB_REG_FIRST))
#define MKSB_RX_BUFFER_SIZE 24 // bytes, 20 minimum
#define MKSB_TX_BUFFER_SIZE 8 // bytes, 7 minimum
#define MKSB_STATUS_MPPT_IDLE 0 // Idle, HMI=3.0 Night Mode (PV < XYZ V)
#define MKSB_STATUS_MPPT_OCP_OUTPUT 2 // Overcurrent Protection, ??? E73
#define MKSB_STATUS_MPPT_OVP_OUTPUT 3 // MPPT Bulk Voltage Limit reached
#define MKSB_STATUS_MPPT_CHARGING 4 // Charging, HMI=4.0 MPPT Mode
#define MKSB_STATUS_MPPT_FULL 6 // Battery full
//
#define MKSB_STATUS_BATT_UVP 0x100 // Battery Undervoltage Protection, load cut, Fault E65
#define MKSB_STATUS_BATT_OVP 0x200 // Battery Overvoltage, load still connected, Fault E63
// module type definition
typedef struct MKSB_MODULE_T_
{
uint8_t idx; // index of receiver interface
uint8_t phase_id; // phase id
uint8_t idx_tx; // index of transmitter interface
TasmotaSerial *Serial = nullptr;
char *pRxBuffer = nullptr;
uint8_t txBuffer[MKSB_TX_BUFFER_SIZE];
uint16_t energy_total; // totalizer at charge controller (non-volatile there)
// uint16_t status; // combines mode (LSB) and status / error (MSB)
uint8_t rxIdx; // bytecounter at the Rx data buffer
uint8_t rxChecksum; // for checksum calculation at reception
uint8_t ev250ms_state; // for scheduled serial requesting, snyced with everysecond
#ifdef MKSB_WITH_SERIAL_DEBUGGING
uint32_t cntTx; // count requests transmitted to the charge controller
uint32_t cntRxGood; // count valid responses received from the charge controller
uint32_t cntRxBadCRC; // count CRC-invalid responses
uint32_t tsTx; // timestamp in ms of last byte transmitted
uint32_t tsRx; // timestamp in ms of last byte received
#endif
#ifdef MKSB_WITH_REGISTER_SUPPORT
uint16_t regs_to_read; // bit_n = flags register n to read
uint16_t regs_to_write; // bit_n = flags register n to write
uint16_t regs_to_report; // bit_n = flags register n to report (after valid response received)
uint16_t regs_value[MKSB_REG_TOTAL]; // value storage of all known configuration registers
#endif
#ifdef MKSB_WITH_ON_OFF_SUPPORT
bool actual_state; // true = active, false = stop
bool target_state; // true = active, false = stop
#endif
} MKSB_MODULE_T;
MKSB_MODULE_T *pMskbInstance = nullptr;
float *pMksbInstance_FloatArrays = nullptr; // single allocation reference for all channel float arrays
enum _E_MKSB_FLOATS
{
MKSB_INSTANCE_FLOATARRAY_TEMPERATURE = 0,
MKSB_INSTANCE_FLOATARRAY_BATTVOLTAGE,
MKSB_INSTANCE_FLOATARRAY_BATTCURRENT,
MKSB_INSTANCE_FLOATARRAY_SIZE // number of different float arrays
} E_MKSB_FLOATS;
// Note: each float array has Energy->phase_count elements
// module const data
const char mksb_HTTP_SNS_TEMPERATURE[] PROGMEM = "{s}" D_TEMPERATURE "{m}%s °%c{e}";
const char mksb_HTTP_SNS_BATT_VOLTAGE[] PROGMEM = "{s}" D_BATTERY " " D_VOLTAGE "{m}%s " D_UNIT_VOLT "{e}";
const char mksb_HTTP_SNS_BATT_CURRENT[] PROGMEM = "{s}" D_BATTERY " " D_CURRENT "{m}%s " D_UNIT_AMPERE "{e}";
//const char mksb_HTTP_SNS_STATUS_INFO[] PROGMEM = "{s}" D_STATUS "{m}%s {e}";
//
#ifdef MKSB_WITH_REGISTER_SUPPORT
// config register format strings: requires one float as register value
static const char mksb_fsrreg1[] PROGMEM = "NRG: CH%u R1 = %1_fV [D02] MPPT Bulk Charging Voltage";
static const char mksb_fsrreg2[] PROGMEM = "NRG: CH%u R2 = %1_fV [D01] MPPT Floating Charging Voltage (SLA only)";
static const char mksb_fsrreg3[] PROGMEM = "NRG: CH%u R3 = %0_fh [D00] Load-Output ON duration";
static const char mksb_fsrreg4[] PROGMEM = "NRG: CH%u R4 = %1_fA MPPT Current Limit (CAUTION: change on your own risk!)";
static const char mksb_fsrreg5[] PROGMEM = "NRG: CH%u R5 = %1_fV [D03] Battery Undervoltage Protection (Load-Output OFF)";
static const char mksb_fsrreg6[] PROGMEM = "NRG: CH%u R6 = %1_fV Battery Undervoltage Recovery (Load-Output ON)";
static const char mksb_fsrreg7[] PROGMEM = "NRG: CH%u R7 = %0_f Com Address (not used)";
static const char mksb_fsrreg8[] PROGMEM = "NRG: CH%u R8 = %0_f [D04] Battery Type [0=SLA, 1=Li]";
static const char mksb_fsrreg9[] PROGMEM = "NRG: CH%u R9 = %0_f Battery System [1...4 * 12V] (read-only)";
static const char * mksb_register_fstrings[] PROGMEM = { mksb_fsrreg1, mksb_fsrreg2, mksb_fsrreg3,
mksb_fsrreg4, mksb_fsrreg5, mksb_fsrreg6,
mksb_fsrreg7, mksb_fsrreg8, mksb_fsrreg9 };
#endif
/********************************************************************************************/
/* EXTRACT UNSIGNED INT FROM SERIAL RECEIVED DATA */
uint32_t mExtractUint32(const char *data, uint8_t offset_lsb, uint8_t offset_msb)
{
uint32_t result = 0;
for ( ; offset_msb >= offset_lsb; offset_msb-- ) {
result = (result << 8) | (uint8_t)data[offset_msb];
}
return result;
}
/********************************************************************************************/
/* FINALIZE SERIAL RECEIVE */
static void mFinishReceive(void * me)
{
MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
#ifdef MKSB_WITH_SERIAL_DEBUGGING
if ( mksb->rxIdx ) { // bytes received with timediff since last request
if ( mksb->idx_tx ) { // channel is tranceiver, show time since last send
mksb->tsRx = millis();
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NRG: Rx%u [Tx+ %ums] %*_H"),
mksb->idx, (mksb->tsRx - mksb->tsTx), mksb->rxIdx, mksb->pRxBuffer);
} else { // channel is receiver only, show time since last receive-finish
mksb->tsTx = millis(); // use it for now
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NRG: Rx%u [Rx+ %ums] %*_H"),
mksb->idx, (mksb->tsTx - mksb->tsRx), mksb->rxIdx, mksb->pRxBuffer);
mksb->tsRx = mksb->tsTx;
}
}
#endif
// finalize reception
mksb->Serial->flush(); // ensure receive buffer is empty
mksb->rxIdx = 0; // reset receiver state
}
/********************************************************************************************/
/* SEND SERIAL DATA */
static void mSendSerial(void * me, uint8_t len)
{
uint32_t i;
uint8_t crc;
MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
if (mksb->idx_tx == 0) {
return; // the channel has no transmitter, receive only
}
mksb->Serial->flush(); // ensure receive buffer is empty
mksb->rxIdx = 0; // reset receiver state
// request frame
mksb->Serial->write( MKSB_START_FRAME );
crc = 0;
for ( i = 0; i < len; i++ ) { // variable data
mksb->Serial->write(mksb->txBuffer[i]);
crc += mksb->txBuffer[i];
}
mksb->Serial->write(crc); // checksum
mksb->Serial->flush(); // ensure transmission complete
#ifdef MKSB_WITH_SERIAL_DEBUGGING
mksb->tsTx = millis();
mksb->cntTx++;
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NRG: CH%u Tx: %02X%*_H%02X"),
mksb->idx, MKSB_START_FRAME, len, mksb->txBuffer, crc );
#endif
}
/********************************************************************************************/
/* PARSE SERIAL DATA RECEIVED */
static void mParseMeasurements(void * me)
{
uint8_t phase;
uint32_t voltage, current, power;
MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
phase = mksb->phase_id;
// solar
voltage = mExtractUint32(mksb->pRxBuffer, 6, 7); // voltage [0.1V]
power = mExtractUint32(mksb->pRxBuffer, 8, 9); // power [1.0W]
Energy->voltage[phase] = (float)voltage / 10.0f;
Energy->active_power[phase] = (float)power;
if ( voltage ) { // prevent division by 0
// calculate: solar current I = P / U
Energy->current[phase] = Energy->active_power[phase] / Energy->voltage[phase];
} else {
Energy->current[phase] = 0.0f;
}
Energy->data_valid[phase] = 0;
// battery
current = mExtractUint32(mksb->pRxBuffer, 4, 5); // charge current [0.1A]
voltage = mExtractUint32(mksb->pRxBuffer, 2, 3); // voltage [0.1V]
// temperature of the charge controller electronics, integer resolution is 0.1degC
pMksbInstance_FloatArrays[phase] = ConvertTempToFahrenheit( (float)mExtractUint32(mksb->pRxBuffer, 10, 11) / 10.0f );
phase += Energy->phase_count;
// battery voltage, integer resolution is 0.1V
pMksbInstance_FloatArrays[phase] = (float)voltage / 10.0f;
phase += Energy->phase_count;
// battery charge current, integer resolution is 0.1A
pMksbInstance_FloatArrays[phase] = (float)current / 10.0f;
mksb->energy_total = mExtractUint32(mksb->pRxBuffer, 12, 13); // solar energy total [1.0kWh]
// mksb->status = mExtractUint32(mksb->pRxBuffer, 16, 17); // mode and status
// unused response data: unknown encoding
// mExtractUint32(mksb->pRxBuffer, 14, 15); // ?dummy [14:15]
// mExtractUint32(mksb->pRxBuffer, 18, 18); // ?dummy [18]
}
/********************************************************************************************/
/* RECEIVE SERIAL DATA */
static void MkSkyBluSerialReceive(void * me)
{
int i;
MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
if (mksb->Serial == nullptr) {
return; // serial interface not available
}
while ( mksb->Serial->available() ) {
yield();
if (mksb->rxIdx < MKSB_RX_BUFFER_SIZE) { // buffer available
mksb->pRxBuffer[mksb->rxIdx] = mksb->Serial->read();
if (MKSB_START_FRAME != mksb->pRxBuffer[0] ) { // no start of frame yet
mksb->rxIdx = 0; // reset receiver
} else { // [0] start valid
if (0 == mksb->rxIdx) { // start of frame present
mksb->rxChecksum = 0; // reset checksum calc
} else { // [1+] cmd or later
if ((MKSB_RSP_READ_MEASURES == mksb->pRxBuffer[1]) && (19 == mksb->rxIdx)) {
if ( mksb->rxChecksum == mksb->pRxBuffer[mksb->rxIdx] ) {
mParseMeasurements(mksb);
#ifdef MKSB_WITH_SERIAL_DEBUGGING
mksb->cntRxGood++;
} else {
mksb->cntRxBadCRC++;
#endif
}
mFinishReceive(me);
} else
#ifdef MKSB_WITH_REGISTER_SUPPORT
if ((MKSB_RSP_RW_CONFIG == mksb->pRxBuffer[1]) && (8 == mksb->rxIdx))
{
uint8_t reg;
if ( mksb->rxChecksum == mksb->pRxBuffer[mksb->rxIdx] ) {
reg = mksb->pRxBuffer[2] - MKSB_REG_FIRST;
if ( reg < MKSB_REG_TOTAL ) {
mksb->regs_value[reg] = mExtractUint32(mksb->pRxBuffer, 3, 4);
mksb->regs_to_report |= 1 << reg;
}
#ifdef MKSB_WITH_SERIAL_DEBUGGING
mksb->cntRxGood++;
} else {
mksb->cntRxBadCRC++;
#endif
}
mFinishReceive(me);
} else
#endif
#ifdef MKSB_WITH_ON_OFF_SUPPORT
if ((MKSB_RSP_POWER == mksb->pRxBuffer[1]) && (5 == mksb->rxIdx)) {
if ( mksb->rxChecksum == mksb->pRxBuffer[mksb->rxIdx] ) {
if ( mksb->pRxBuffer[2] == 0 ) { /* ON */
if ( mksb->actual_state != true ) {
AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u Charging ON"), mksb->idx);
mksb->actual_state = true;
}
} else
if ( mksb->pRxBuffer[2] == 1 ) { /* OFF */
if ( mksb->actual_state != false ) {
AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u Charging OFF"), mksb->idx);
mksb->actual_state = false;
}
} else { /* unknown content */
}
#ifdef MKSB_WITH_SERIAL_DEBUGGING
mksb->cntRxGood++;
} else {
mksb->cntRxBadCRC++;
#endif
}
mFinishReceive(me);
} else
#endif
{ // calc checksum
mksb->rxChecksum += mksb->pRxBuffer[mksb->rxIdx];
}
}
}
mksb->rxIdx++; // more to receive
} else { // buffer full
mFinishReceive(me);
}
}
}
/********************************************************************************************/
/* EVERY SECOND */
void MkSkyBluEverySecond(void * me)
{
int i;
float fValue;
uint8_t phase;
bool updateTotal = false;
MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
phase = mksb->phase_id;
if (Energy->data_valid[phase] > ENERGY_WATCHDOG) {
Energy->voltage[phase] = Energy->current[phase] = Energy->active_power[phase] = 0.0f;
Energy->voltage[phase] = NAN; // mark as invalid
Energy->current[phase] = NAN; // mark as invalid
pMksbInstance_FloatArrays[phase] = NAN; // temperature
phase += Energy->phase_count;
pMksbInstance_FloatArrays[phase] = NAN; // battery voltage
phase += Energy->phase_count;
pMksbInstance_FloatArrays[phase] = NAN; // battery current
} else {
Energy->kWhtoday_delta[phase] += Energy->active_power[phase] * 1000 / 36; // solar energy only
// import the non-resetable solar energy full kWh counter from the charge controller, but with 2 requirements:
// 1. SetOption72 is active (bug@EnergyUpdateTotal?: a call of it impacts kWhtoday, even if option is off)
// 2. the tasmota total is smaller than the total of the charge controller
if ( Settings->flag3.hardware_energy_total ) {
fValue = (float)mksb->energy_total;
if ( Energy->total[phase] < fValue ) {
Energy->import_active[phase] = fValue;
updateTotal = true;
}
}
}
mksb->ev250ms_state = 0; // sync the 250ms state machine
#ifdef MKSB_WITH_SERIAL_DEBUGGING
{
static uint32_t lastDebugLog = 0;
if ( (millis() - lastDebugLog) > (5u * 60000u) ) { // every 5 minutes
if ( mksb->idx_tx ) { // transceiver
AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u serial statistics Tx:%u, Rx+:%u, Rx-total:%u (Rx-CRC:%u)"),
mksb->idx, mksb->cntTx, mksb->cntRxGood, mksb->cntTx - mksb->cntRxGood, mksb->cntRxBadCRC );
} else { // receiver only
AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u serial statistics Rx+:%u, Rx-CRC:%u"),
mksb->idx, mksb->cntRxGood, mksb->cntRxBadCRC );
}
if (mksb->phase_id >= (Energy->phase_count - 1) ) { // last channel logged
lastDebugLog = millis();
}
}
}
#endif
if ( updateTotal == true ) {
EnergyUpdateTotal(); // this also calls EnergyUpdateToday() at the end
} else {
EnergyUpdateToday();
}
}
/********************************************************************************************/
/* EVERY 250 MS */
void MkSkyBluEvery250ms(void * me)
{
static const uint8_t mksb_ser_req_measures[] PROGMEM = { MKSB_CMD_READ_MEASUREMENTS, 0,0,0 };
static const uint8_t mksb_ser_req_write_reg[] PROGMEM = { MKSB_CMD_WRITE_REGISTER, 0 ,0,0, 0,0,0 };
static const uint8_t mksb_ser_req_read_reg[] PROGMEM = { MKSB_CMD_READ_REGISTER, 0 ,0,0, 0,0,0 };
static const uint8_t mksb_ser_req_chrg_off[] PROGMEM = { MKSB_CMD_AUXILARY, 1, 2, 0 };
static const uint8_t mksb_ser_req_chrg_on[] PROGMEM = { MKSB_CMD_AUXILARY, 2, 0, 0 };
int i;
float fVal;
MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
if (mksb->Serial == nullptr) {
return; // serial interface not available
}
if (mksb->idx_tx == 0) {
return; // no transmitter at this channel
}
// if ( mksb->ev250ms_state == 0 ) {
// MkSkyBluRequestStatus();
// } else
if ( mksb->ev250ms_state == 0 ) {
memcpy_P(mksb->txBuffer, mksb_ser_req_measures, sizeof (mksb_ser_req_measures));
mSendSerial(mksb, sizeof (mksb_ser_req_measures));
}
#ifdef MKSB_WITH_REGISTER_SUPPORT
else if ( mksb->ev250ms_state == 3 ) {
if ( mksb->regs_to_read || mksb->regs_to_write ) {
for ( i = 0; i < MKSB_REG_TOTAL; i++ ) {
if ( mksb->regs_to_write & (1 << i) ) { // prio write
memcpy_P(mksb->txBuffer, mksb_ser_req_write_reg, sizeof (mksb_ser_req_write_reg));
// variable register and value
mksb->txBuffer[1] = i + MKSB_REG_FIRST;
mksb->txBuffer[2] = (uint8_t)(mksb->regs_value[i] & 0xFF);
mksb->txBuffer[3] = (uint8_t)(mksb->regs_value[i] >> 8);
mSendSerial(mksb, sizeof (mksb_ser_req_write_reg));
mksb->regs_to_write &= ~(1 << i);
break; // only one per call
} else
if ( mksb->regs_to_read & (1 << i) ) {
memcpy_P(mksb->txBuffer, mksb_ser_req_read_reg, sizeof (mksb_ser_req_read_reg));
// variable register
mksb->txBuffer[1] = i + MKSB_REG_FIRST;
mSendSerial(mksb, sizeof (mksb_ser_req_read_reg));
mksb->regs_to_read &= ~(1 << i);
break; // only one per call
} else {}
}
}
}
#endif
#ifdef MKSB_WITH_ON_OFF_SUPPORT
else if ( mksb->actual_state != mksb->target_state ) {
if ( mksb->target_state == false ) {
memcpy_P(mksb->txBuffer, mksb_ser_req_chrg_off, sizeof (mksb_ser_req_chrg_off));
mSendSerial(mksb, sizeof (mksb_ser_req_chrg_off));
} else {
memcpy_P(mksb->txBuffer, mksb_ser_req_chrg_on, sizeof (mksb_ser_req_chrg_on));
mSendSerial(mksb, sizeof (mksb_ser_req_chrg_on));
}
mksb->actual_state = mksb->target_state;
}
#endif
#ifdef MKSB_WITH_REGISTER_SUPPORT
if ( mksb->regs_to_report )
{
for ( i = 0; i < MKSB_REG_TOTAL; i++ ) {
if ( mksb->regs_to_report & (1 << i) ) {
fVal = ((float)(mksb->regs_value[i])) / 1000.0f;
AddLog( LOG_LEVEL_ERROR, mksb_register_fstrings[i], mksb->idx, &fVal); // log always
mksb->regs_to_report &= ~(1 << i);
break; // once per call
}
}
}
#endif
mksb->ev250ms_state++;
if ( mksb->ev250ms_state >= 4 )
{
mksb->ev250ms_state = 0;
}
}
/********************************************************************************************/
/* ENERGY COMMAND */
bool MkSkyBluEnergyCommand(void * me)
{
bool serviced = true;
uint8_t reg;
char *str;
int32_t value;
int i;
MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
if ((CMND_POWERCAL == Energy->command_code) || (CMND_VOLTAGECAL == Energy->command_code) || (CMND_CURRENTCAL == Energy->command_code)) {
// Service in xdrv_03_energy.ino
} else
if (CMND_ENERGYCONFIG == Energy->command_code) {
AddLog(LOG_LEVEL_DEBUG, PSTR("NRG: EnergyConfig index %d, payload %d, data '%s'"),
XdrvMailbox.index, XdrvMailbox.payload, XdrvMailbox.data ? XdrvMailbox.data : "null" );
if ( XdrvMailbox.data_len == 0 ) { // no arguments: show help
AddLog(LOG_LEVEL_INFO, PSTR("NRG: Usage: EnergyConfig <Tx-Interface> [+,-,Register-Num:1...9] [Register-Value]"));
} else {
str = XdrvMailbox.data;
// 1st argument: transmit interface number 1...8, 0=all
i = strtoul( str, &str, 10 );
if( i != mksb->idx_tx && i != 0 ) {
return serviced; // this instance: not addressed or no transmitter
}
while ((*str != '\0') && isspace(*str)) { str++; }; // Trim spaces
// 2nd argument: +,- or register number
#ifdef MKSB_WITH_ON_OFF_SUPPORT
if ('-' == str[0] ) { // to set controller charging off
mksb->target_state = false;
return serviced;
}
if ('+' == str[0] ) { // to set controller charging active
mksb->target_state = true;
return serviced;
}
#endif
#ifdef MKSB_WITH_REGISTER_SUPPORT
reg = (uint8_t)strtoul( str, &str, 10 ) - MKSB_REG_FIRST;
if ( MKSB_REG_FIRST <= reg && MKSB_REG_LAST >= reg )
{ // valid register number 1...9
while ((*str != '\0') && isspace(*str)) { str++; } // Trim spaces
if ( *str )
{ // 3nd argument: there is a value = write registers value
value = (int32_t)(CharToFloat(str) * 1000.0f);
// write Register: no range check here !
mksb->regs_value[reg] = value; // store value to prepared for write
mksb->regs_to_write |= 1 << reg; // trigger write
} else
{ // 3rd argument: there is no value = read registers value
mksb->regs_to_read |= 1 << reg;
}
return serviced;
} else
{ // invalid register number
mksb->regs_to_read = (1 << MKSB_REG_TOTAL) - 1; // flag all registers to be read
return serviced;
}
#endif
serviced = false;
}
} else {
serviced = false; // Unknown command
}
return serviced;
}
/********************************************************************************************/
/* PUBLISH SENSORS (beyond Energy) */
static void MkSkyBluShow(uint32_t function)
{
uint8_t phase;
bool voltage_common = (Settings->flag6.no_voltage_common) ? false : Energy->voltage_common;
if ( FUNC_JSON_APPEND == function ) {
phase = 0;
// Temperature
ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE "\":%s"),
EnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.temperature_resolution));
phase += Energy->phase_count;
// Battery Voltage
ResponseAppend_P(PSTR(",\"" D_JSON_VOLTAGE " battery\":%s"),
EnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.voltage_resolution, voltage_common));
phase += Energy->phase_count;
// Battery Current
ResponseAppend_P(PSTR(",\"" D_JSON_CURRENT " battery\":%s"),
EnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.current_resolution));
}
#ifdef USE_WEBSERVER
if ( FUNC_WEB_COL_SENSOR == function ) {
phase = 0;
WSContentSend_PD(mksb_HTTP_SNS_TEMPERATURE, WebEnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.temperature_resolution), TempUnit());
phase += Energy->phase_count;
WSContentSend_PD(mksb_HTTP_SNS_BATT_VOLTAGE, WebEnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.voltage_resolution));
phase += Energy->phase_count;
WSContentSend_PD(mksb_HTTP_SNS_BATT_CURRENT, WebEnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.current_resolution));
}
else if ( FUNC_WEB_SENSOR == function )
{
WSContentSend_P( PSTR("MakeSkyBlue " D_SOLAR_POWER " " D_CHARGE) ); // headline after values
} else {}
#endif // USE_WEBSERVER
}
/********************************************************************************************/
/* Reset ENERGY */
void MkSkyBluReset(void * me)
{
int i;
uint8_t phase;
MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
// mksb->temperature = NAN;
mksb->energy_total = 0;
phase = mksb->phase_id;
Energy->total[phase] = 0.0f;
// Energy->data_valid[phase] = 0;
}
/********************************************************************************************/
/* SENSORS INIT */
void MkSkyBluSnsInit(void * me)
{
int i;
MKSB_MODULE_T * mksb;
mksb = (MKSB_MODULE_T *)me;
i = mksb->idx - 1; // inteface receiver index
// Software serial init needs to be done here as earlier (serial) interrupts may lead to Exceptions
if ( mksb->idx_tx ) { // transceiver
mksb->Serial = new TasmotaSerial(Pin(GPIO_MKSKYBLU_RX, i), Pin(GPIO_MKSKYBLU_TX, i), 1);
} else { // receiver only
mksb->Serial = new TasmotaSerial(Pin(GPIO_MKSKYBLU_RX, i), -1, 1);
}
if (mksb->Serial == nullptr) {
AddLog(LOG_LEVEL_ERROR, PSTR("NRG: CH%d Serial alloc failed"), mksb->idx);
return;
}
if (mksb->Serial->begin(MKSB_BAUDRATE)) {
// mksb->pTxBuffer = (char*)(malloc(MKSB_TX_BUFFER_SIZE));
if (mksb->Serial->hardwareSerial()) {
ClaimSerial();
// Use idle serial buffer to save RAM
mksb->pRxBuffer = TasmotaGlobal.serial_in_buffer +
sizeof(TasmotaGlobal.serial_in_buffer) - mksb->idx * MKSB_RX_BUFFER_SIZE;
// from the end, this allows to use other sensors from start simultaneously
} else {
mksb->pRxBuffer = (char*)(malloc(MKSB_RX_BUFFER_SIZE));
}
#ifdef ESP32
AddLog(LOG_LEVEL_DEBUG, PSTR("NRG: CH%d ESP32 Serial UART%d"), mksb->idx, mksb->Serial->getUart());
#endif
} else {
mksb->Serial = nullptr;
AddLog(LOG_LEVEL_ERROR, PSTR("NRG: CH%d Serial init failed"), mksb->idx);
}
}
/********************************************************************************************/
/* DRIVER INIT */
void MkSkyBluDrvInit(void)
{
int i;
MKSB_MODULE_T * mksb = nullptr;
uint8_t phase = 0, u8 = 0;
for( i = 0; i < MAX_MKSKYBLU_IF; i++ ) {
if ( PinUsed(GPIO_MKSKYBLU_RX, i) ) { // check for configured receiver
phase++; // count configured receiver
if ( PinUsed(GPIO_MKSKYBLU_TX, i) ) { // check for configured transmitter
u8++; // count configured transceiver
}
}
}
if ( !u8 ) { // at least one transceiver needed
return; // mkskyblu not configured, driver not active
} else
if (Energy == nullptr ) { // something is wrong with the tasmota energy support
return;
} else
if( phase > sizeof( Energy->data_valid ) ) { // limit to max supported phases, 2025-11-02: 3 at ESP82xx, 8 at ESP32
phase = sizeof( Energy->data_valid );
AddLog(LOG_LEVEL_INFO, PSTR("NRG: Channel count limited to %d"), phase);
}
// one instance per configured receiver
pMskbInstance = (MKSB_MODULE_T *)(malloc(phase * sizeof(MKSB_MODULE_T)));
pMksbInstance_FloatArrays = (float *)(malloc(phase * (MKSB_INSTANCE_FLOATARRAY_SIZE * sizeof(float))));
if ( pMskbInstance == nullptr || pMksbInstance_FloatArrays == nullptr ) {
AddLog(LOG_LEVEL_ERROR, PSTR("NRG: Memory allocation failed"));
return;
}
// at this point we have at least one transceiver configured
mksb = pMskbInstance; // first instance
phase = 0;
for( i = 0; i < MAX_MKSKYBLU_IF; i++ ) {
if ( PinUsed(GPIO_MKSKYBLU_RX, i) ) { // check for configured receiver
mksb->idx = i + 1; // interface receiver index
if ( PinUsed(GPIO_MKSKYBLU_TX, i) ) { // transceiver
mksb->idx_tx = mksb->idx; // interface transmitter index
} else {
mksb->idx_tx = 0; // unknown related transmitter
}
mksb->Serial = nullptr;
mksb->pRxBuffer = nullptr; // allocated at SnsInit
mksb->phase_id = phase++;
// preset / fixed module values
mksb->energy_total = 0;
#ifdef MKSB_WITH_REGISTER_SUPPORT
mksb->regs_to_read = 0;
mksb->regs_to_write = 0;
mksb->regs_to_report = 0;
#endif
#ifdef MKSB_WITH_ON_OFF_SUPPORT
mksb->actual_state = true; // default: charging enabled
mksb->target_state = true; // default: charging enabled
#endif
mksb++; // next instance
}
}
for( i = 0; i < (phase * MKSB_INSTANCE_FLOATARRAY_SIZE); i++ ) {
pMksbInstance_FloatArrays[i] = NAN;
}
// preset / fixed energy values
Energy->phase_count = phase;
Energy->voltage_common = false; // every charge controller has an individual solar voltage
Energy->frequency_common = true;
Energy->type_dc = true; // solar dc charger
Energy->use_overtemp = false; // ESP device acts as separated gateway, charge controller has its own temperature management
Energy->voltage_available = true; // solar power and voltage is provided by serial communication
Energy->current_available = true; // solar current is calculated from power and voltage
AddLog(LOG_LEVEL_INFO, PSTR("NRG: MakeSkyBlue driver initialized with %d channel(s)"), Energy->phase_count);
TasmotaGlobal.energy_driver = XNRG_26;
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xnrg26(uint32_t function)
{
bool result = false;
int i;
MKSB_MODULE_T * mksb;
if ( function == FUNC_PRE_INIT ) {
MkSkyBluDrvInit(); // create all instances
return result;
} else
for( i = 0; i < Energy->phase_count; i++ )
{
mksb = &pMskbInstance[i];
if ( (void *)mksb == nullptr ) {
continue; // instance not configured
}
switch (function) {
case FUNC_LOOP:
MkSkyBluSerialReceive((void *)mksb);
break;
case FUNC_EVERY_250_MSECOND:
MkSkyBluEvery250ms((void *)mksb);
break;
case FUNC_ENERGY_EVERY_SECOND:
MkSkyBluEverySecond((void *)mksb);
break;
case FUNC_JSON_APPEND:
case FUNC_WEB_SENSOR:
case FUNC_WEB_COL_SENSOR:
MkSkyBluShow(function);
return result; // one call for all instances
break;
case FUNC_ENERGY_RESET:
// MkSkyBluReset((void *)mksb); // not used
break;
case FUNC_COMMAND:
result = MkSkyBluEnergyCommand((void *)mksb);
break;
case FUNC_INIT:
MkSkyBluSnsInit((void *)mksb);
break;
}
}
return result;
}
#endif // USE_MAKE_SKY_BLUE
#endif // USE_ENERGY_SENSOR