diff --git a/tasmota/tasmota_xdrv_driver/xdrv_03_energy.ino b/tasmota/tasmota_xdrv_driver/xdrv_03_energy.ino index 6b2f992d3..eb07c4fb2 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_03_energy.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_03_energy.ino @@ -1514,9 +1514,10 @@ bool Xdrv03(uint32_t function) case FUNC_SLEEP_LOOP: XnrgCall(FUNC_LOOP); break; + case FUNC_EVERY_100_MSECOND: case FUNC_EVERY_250_MSECOND: if (TasmotaGlobal.uptime > 4) { - XnrgCall(FUNC_EVERY_250_MSECOND); + XnrgCall(function); } break; case FUNC_EVERY_SECOND: diff --git a/tasmota/tasmota_xdrv_driver/xdrv_03_esp32_energy.ino b/tasmota/tasmota_xdrv_driver/xdrv_03_esp32_energy.ino index 536301409..26e7ec27f 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_03_esp32_energy.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_03_esp32_energy.ino @@ -1916,9 +1916,10 @@ bool Xdrv03(uint32_t function) case FUNC_SLEEP_LOOP: XnrgCall(FUNC_LOOP); break; + case FUNC_EVERY_100_MSECOND: case FUNC_EVERY_250_MSECOND: if (TasmotaGlobal.uptime > 4) { - XnrgCall(FUNC_EVERY_250_MSECOND); + XnrgCall(function); } break; case FUNC_EVERY_SECOND: diff --git a/tasmota/tasmota_xnrg_energy/xnrg_12_solaxX1.ino b/tasmota/tasmota_xnrg_energy/xnrg_12_solaxX1.ino index ec9ee7cd5..29185eab1 100644 --- a/tasmota/tasmota_xnrg_energy/xnrg_12_solaxX1.ino +++ b/tasmota/tasmota_xnrg_energy/xnrg_12_solaxX1.ino @@ -37,9 +37,10 @@ #define D_SOLAX_X1 "SolaxX1" #include +TasmotaSerial *solaxX1Serial; const char kSolaxMode[] PROGMEM = - D_OFF "|" D_SOLAX_MODE_0 "|" D_SOLAX_MODE_1 "|" D_SOLAX_MODE_2 "|" D_SOLAX_MODE_3 "|" D_SOLAX_MODE_4 "|" + D_GATEWAY "|" D_OFF "|" D_SOLAX_MODE_0 "|" D_SOLAX_MODE_1 "|" D_SOLAX_MODE_2 "|" D_SOLAX_MODE_3 "|" D_SOLAX_MODE_4 "|" D_SOLAX_MODE_5 "|" D_SOLAX_MODE_6; const char kSolaxError[] PROGMEM = @@ -118,6 +119,10 @@ struct SOLAXX1_GLOBALDATA { uint8_t QueryID_count = 240; bool Command_QueryID = false;; bool Command_QueryConfig = false; + bool MeterMode = false; + float MeterPower = 5000; + float MeterImport; + float MeterExport; } solaxX1_global; struct SOLAXX1_SENDDATA { @@ -130,8 +135,6 @@ struct SOLAXX1_SENDDATA { uint8_t Payload[16] = {0x00}; } solaxX1_SendData; -TasmotaSerial *solaxX1Serial; - /*********************************************************************************************/ void solaxX1_RS485Send(void) { @@ -143,27 +146,84 @@ void solaxX1_RS485Send(void) { memcpy(message + 7, solaxX1_SendData.FunctionCode, 1); memcpy(message + 8, solaxX1_SendData.DataLength, 1); memcpy(message + 9, solaxX1_SendData.Payload, sizeof(solaxX1_SendData.Payload)); - uint16_t crc = solaxX1_calculateCRC(message, 9 + solaxX1_SendData.DataLength[0]); // calculate out crc bytes - while (solaxX1Serial->available() > 0) { // read serial if any old data is available + solaxX1_RS485SendRaw(message, 9 + solaxX1_SendData.DataLength[0], 0); +} + +void solaxX1_RS485SendMeterFloat(float Value) { + uint8_t MeterResponse[7] = {0x01, 0x04, 0x04, 0x00}; + for (uint8_t i = 0; i <= 3; i++) { // Store bytes in reverse order + MeterResponse[i + 3] = *((char*)(&Value) + 3 - i); + } + solaxX1_RS485SendRaw(MeterResponse, 7, 1); +} + +void solaxX1_RS485SendMeterInt16(int16_t Value) { + uint8_t MeterResponse[5] = {0x01, 0x03, 0x02, 0x00}; + MeterResponse[3] = highByte(Value); + MeterResponse[4] = lowByte(Value); + solaxX1_RS485SendRaw(MeterResponse, 5, 1); +} + +void solaxX1_RS485SendMeterTotalInt(uint32_t Export, uint32_t Import) { + uint8_t MeterResponse[11] = {0x01, 0x03, 0x08, 0x00}; + for (uint8_t i = 0; i <= 3; i++) { // Store bytes in reverse order + MeterResponse[i + 3] = *((char*)(&Export) + 3 - i); + } + for (uint8_t i = 0; i <= 3; i++) { // Store bytes in reverse order + MeterResponse[i + 7] = *((char*)(&Import) + 3 - i); + } + solaxX1_RS485SendRaw(MeterResponse, 11, 1); +} + +void solaxX1_RS485SendRaw(uint8_t *SendBuffer, uint8_t DataLen, uint8_t CRCflag) { + uint16_t crc; + while (solaxX1Serial->available()) { // read serial if any old data is available solaxX1Serial->read(); } - if (PinUsed(GPIO_SOLAXX1_RTS)) { - digitalWrite(Pin(GPIO_SOLAXX1_RTS), HIGH); + if (PinUsed(GPIO_SOLAXX1_RTS)) digitalWrite(Pin(GPIO_SOLAXX1_RTS), HIGH); + solaxX1Serial->flush(); + solaxX1Serial->write(SendBuffer, DataLen); + if (CRCflag) { + crc = solaxX1_calculateCRC_MBUS(SendBuffer, DataLen); // Use CRC MBUS algorithm + solaxX1Serial->write(lowByte(crc)); + solaxX1Serial->write(highByte(crc)); + } else { + crc = solaxX1_calculateCRC(SendBuffer, DataLen); // Use CRC Solax algorithm + solaxX1Serial->write(highByte(crc)); + solaxX1Serial->write(lowByte(crc)); } solaxX1Serial->flush(); - solaxX1Serial->write(message, 9 + solaxX1_SendData.DataLength[0]); - solaxX1Serial->write(highByte(crc)); - solaxX1Serial->write(lowByte(crc)); - solaxX1Serial->flush(); - if (PinUsed(GPIO_SOLAXX1_RTS)) { - digitalWrite(Pin(GPIO_SOLAXX1_RTS), LOW); - } - AddLogBuffer(LOG_LEVEL_DEBUG_MORE, message, 9 + solaxX1_SendData.DataLength[0]); + if (PinUsed(GPIO_SOLAXX1_RTS)) digitalWrite(Pin(GPIO_SOLAXX1_RTS), LOW); + AddLogBuffer(LOG_LEVEL_DEBUG_MORE, SendBuffer, DataLen); } bool solaxX1_RS485Receive(uint8_t *ReadBuffer) { + uint32_t SerWatchdogTime; + + // Read header uint8_t len = 0; - while (solaxX1Serial->available() > 0) { + SerWatchdogTime = millis(); + while (len < 2) { // read exact length because of unaccurate timing of the inverter + if (solaxX1Serial->available()) ReadBuffer[len++] = (uint8_t)solaxX1Serial->read(); + if (millis() > (SerWatchdogTime + 1000)) return true; // No data received -> bail out + } + + // Check and set meter mode + solaxX1_SwitchMeterMode((ReadBuffer[0] == 0x01 || ReadBuffer[0] == 0x02) && (ReadBuffer[1] == 0x03 || ReadBuffer[1] == 0x04)); + + // Read data in meter mode + if (solaxX1_global.MeterMode) { // Metermode + SerWatchdogTime = millis(); + while (len < 8) { // read exact length because of unaccurate timing of the inverter + if (solaxX1Serial->available()) ReadBuffer[len++] = (uint8_t)solaxX1Serial->read(); + if (millis() > (SerWatchdogTime + 1000)) return true; // No data received -> bail out + } + AddLogBuffer(LOG_LEVEL_DEBUG_MORE, ReadBuffer, len); + return false; // Ignore checksum + } // end Metermode + + // Process normal receive + while (solaxX1Serial->available()) { ReadBuffer[len++] = (uint8_t)solaxX1Serial->read(); } AddLogBuffer(LOG_LEVEL_DEBUG_MORE, ReadBuffer, len); @@ -175,11 +235,27 @@ uint16_t solaxX1_calculateCRC(uint8_t *bExternTxPackage, uint8_t bLen) { uint8_t i; uint16_t wChkSum = 0; for (i = 0; i < bLen; i++) { - wChkSum = wChkSum + bExternTxPackage[i]; + wChkSum += bExternTxPackage[i]; } return wChkSum; } +uint16_t solaxX1_calculateCRC_MBUS(uint8_t *frame, uint8_t Len) { + uint16_t crc = 0xFFFF; + for (uint32_t i = 0; i < Len; i++) { + crc ^= frame[i]; + for (uint32_t j = 8; j; j--) { + if ((crc & 0x0001) != 0) { // If the LSB is set + crc >>= 1; // Shift right and XOR 0xA001 + crc ^= 0xA001; + } else { // Else LSB is not set + crc >>= 1; // Just shift right + } + } + } + return crc; +} + void solaxX1_ExtractText(uint8_t *DataIn, uint8_t *DataOut, uint8_t Begin, uint8_t End) { uint8_t i; for (i = Begin; i <= End; i++) { @@ -253,27 +329,78 @@ uint8_t solaxX1_ParseErrorCode(uint32_t code) { return 0; } +void solaxX1_SwitchMeterMode(bool MeterMode) { + if (solaxX1_global.MeterMode == MeterMode) return; + solaxX1_global.MeterMode = MeterMode; + if (MeterMode) { + Energy->data_valid[0] = ENERGY_WATCHDOG; + solaxX1.runMode = -2; + solaxX1.temperature = solaxX1.dc1_voltage = solaxX1.dc1_current = solaxX1.dc1_power = solaxX1.dc2_voltage = solaxX1.dc2_current = solaxX1.dc2_power = 0; + } else { + solaxX1.runMode = -1; + } +} + /*********************************************************************************************/ -void solaxX1_250MSecond(void) { // Every 250 milliseconds +void solaxX1_CyclicTask(void) { // Every 100/250 milliseconds uint8_t DataRead[80] = {0}; uint8_t TempData[16] = {0}; char TempDataChar[32]; float TempFloat; + static uint32_t LastMeterTime; + static uint16_t MtrReg, MtrPwr32, MtrPwr16, MtrImp32, MtrExp32, MrtTot64, MtrRest; if (solaxX1Serial->available()) { - if (solaxX1_RS485Receive(DataRead)) { // CRC-error -> no further action - DEBUG_SENSOR_LOG(PSTR("SX1: Data response CRC error")); + if (solaxX1_RS485Receive(DataRead)) { // CRC or other error -> no further action + AddLog(LOG_LEVEL_ERROR, PSTR("SX1: (CRC) error in received data")); return; } - solaxX1_global.SendRetry_count = 20; // Inverter is responding - - if (DataRead[0] != 0xAA || DataRead[1] != 0x55) { // Check for header - DEBUG_SENSOR_LOG(PSTR("SX1: Check for header failed")); + if (solaxX1_global.MeterMode) { // Metermode + AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: Metermode %02X %02X xx %02X"), DataRead[0], DataRead[1], DataRead[3]); + LastMeterTime = TasmotaGlobal.uptime; + if (DataRead[0] != 0x01) return; // Respond only to requests for meter #1 + switch (DataRead[3]) { + case 0x0B: // received "Register meter request" + //solaxX1_RS485SendMeterInt16(0); // Tell inverter to request int16 values + solaxX1_RS485SendMeterInt16(0xa8); // Tell inverter to request float32 values + MtrReg++; + break; + case 0x0C: // received "Power request (32 bit float)" + solaxX1_RS485SendMeterFloat(solaxX1_global.MeterPower); + MtrPwr32++; + break; + case 0x0E: // received "Power request (16 bit int)" + solaxX1_RS485SendMeterInt16((int16_t)solaxX1_global.MeterPower); + MtrPwr16++; + break; + case 0x48: // received "Import request (32 bit float)" + solaxX1_RS485SendMeterFloat(solaxX1_global.MeterImport); + MtrImp32++; + break; + case 0x4A: // received "Export request (32 bit float)" + solaxX1_RS485SendMeterFloat(solaxX1_global.MeterExport); + MtrExp32++; + break; + case 0x08: // received "Energy total request (2*32 bit uint)" + solaxX1_RS485SendMeterTotalInt((uint32_t)(solaxX1_global.MeterExport * 100.0), (uint32_t)(solaxX1_global.MeterImport * 100.0)); + MrtTot64++; + break; + default: + MtrRest++; + } + AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: MtrReg %d, MtrPwr32 %d, MtrPwr16 %d, MtrImp32 %d, MtrExp32 %d, MrtTot64 %d, MtrRest %d"), MtrReg, MtrPwr32, MtrPwr16, MtrImp32, MtrExp32, MrtTot64, MtrRest); return; } + if (DataRead[0] != 0xAA || DataRead[1] != 0x55) { // Check for header + AddLog(LOG_LEVEL_ERROR, PSTR("SX1: Header check failed: %02X %02X"), DataRead[0], DataRead[1]); + return; + } + + solaxX1_global.SendRetry_count = 20; // Inverter is responding + if (DataRead[6] == 0x11 && DataRead[7] == 0x82) { // received "Response for query (live data)" Energy->data_valid[0] = 0; solaxX1.temperature = (DataRead[9] << 8) | DataRead[10]; // Temperature @@ -416,6 +543,11 @@ void solaxX1_250MSecond(void) { // Every 250 milliseconds } // end solaxX1Serial->available() + if(solaxX1_global.MeterMode) { + if (TasmotaGlobal.uptime > LastMeterTime + 20) solaxX1_SwitchMeterMode(false); // Switch back to normal mode, when no Meter request is received for 20 sec. + return; + } + // DEBUG_SENSOR_LOG(PSTR("SX1: solaxX1_global.AddressAssigned: %d, solaxX1_global.QueryData_count: %d, solaxX1_global.SendRetry_count: %d, solaxX1_global.QueryID_count: %d"), solaxX1_global.AddressAssigned, solaxX1_global.QueryData_count, solaxX1_global.SendRetry_count, solaxX1_global.QueryID_count); if (solaxX1_global.AddressAssigned) { if (!solaxX1_global.QueryData_count) { // normal periodically query @@ -437,13 +569,13 @@ void solaxX1_250MSecond(void) { // Every 250 milliseconds solaxX1_global.SendRetry_count = 20; DEBUG_SENSOR_LOG(PSTR("SX1: Inverter went \"off\"")); Energy->data_valid[0] = ENERGY_WATCHDOG; - solaxX1.temperature = solaxX1.dc1_voltage = solaxX1.dc2_voltage = solaxX1.dc1_current = solaxX1.dc2_current = solaxX1.dc1_power = 0; - solaxX1.dc2_power = Energy->current[0] = Energy->voltage[0] = Energy->frequency[0] = Energy->active_power[0] = 0; + solaxX1.temperature = solaxX1.dc1_voltage = solaxX1.dc1_current = solaxX1.dc1_power = solaxX1.dc2_voltage = solaxX1.dc2_current = solaxX1.dc2_power = 0; solaxX1.runMode = -1; // off(line) solaxX1_global.AddressAssigned = false; } // end Inverter went "off" } else { // sent query for inverters in offline status if (!solaxX1_global.SendRetry_count) { + solaxX1_global.Command_QueryConfig = solaxX1_global.Command_QueryID = false; // Clear commands to be sure solaxX1_global.SendRetry_count = 20; DEBUG_SENSOR_LOG(PSTR("SX1: Sent query for inverters in offline state")); solaxX1_QueryOfflineInverters(); @@ -451,7 +583,7 @@ void solaxX1_250MSecond(void) { // Every 250 milliseconds } solaxX1_global.SendRetry_count--; return; -} // end solaxX1_250MSecond +} // end solaxX1_CyclicTask void solaxX1_SnsInit(void) { AddLog(LOG_LEVEL_INFO, PSTR("SX1: Init - RX-pin: %d, TX-pin: %d, RTS-pin: %d"), Pin(GPIO_SOLAXX1_RX), Pin(GPIO_SOLAXX1_TX), Pin(GPIO_SOLAXX1_RTS)); @@ -461,22 +593,40 @@ void solaxX1_SnsInit(void) { #ifdef ESP32 AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: Serial UART%d"), solaxX1Serial->getUart()); #endif + if (PinUsed(GPIO_SOLAXX1_RTS)) pinMode(Pin(GPIO_SOLAXX1_RTS), OUTPUT); } else { TasmotaGlobal.energy_driver = ENERGY_NONE; } - if (PinUsed(GPIO_SOLAXX1_RTS)) { - pinMode(Pin(GPIO_SOLAXX1_RTS), OUTPUT); - } } void solaxX1_DrvInit(void) { if (PinUsed(GPIO_SOLAXX1_RX) && PinUsed(GPIO_SOLAXX1_TX)) { TasmotaGlobal.energy_driver = XNRG_12; Energy->type_dc = true; // Handle like DC, because U*I from inverter is not valid for apparent power; U*I could be lower than active power + Energy->frequency[0] = 0; // Set value, to make frequency present in output } } bool SolaxX1_cmd(void) { + if (Energy->command_code != CMND_ENERGYCONFIG) return false; // Process unchanged data + + if (!strncasecmp(XdrvMailbox.data, "MeterPower", 10)) { + solaxX1_global.MeterPower = CharToFloat(&XdrvMailbox.data[11]); + ResponseCmndFloat(solaxX1_global.MeterPower, 1); + AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: MeterPower: %3_f"), &solaxX1_global.MeterPower); + return false; + } else if (!strncasecmp(XdrvMailbox.data, "MeterImport", 11)) { + solaxX1_global.MeterImport = CharToFloat(&XdrvMailbox.data[12]); + ResponseCmndFloat(solaxX1_global.MeterImport, 8); + AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: MeterImport: %3_f"), &solaxX1_global.MeterImport); + return false; + } else if (!strncasecmp(XdrvMailbox.data, "MeterExport", 11)) { + solaxX1_global.MeterExport = CharToFloat(&XdrvMailbox.data[12]); + ResponseCmndFloat(solaxX1_global.MeterExport, 8); + AddLog(LOG_LEVEL_DEBUG, PSTR("SX1: MeterExport: %3_f"), &solaxX1_global.MeterExport); + return false; + } + if (!solaxX1_global.AddressAssigned) { AddLog(LOG_LEVEL_INFO, PSTR("SX1: No inverter registered")); return false; @@ -484,12 +634,10 @@ bool SolaxX1_cmd(void) { if (!strcasecmp(XdrvMailbox.data, "ReadIDinfo")) { solaxX1_global.Command_QueryID = true; - AddLog(LOG_LEVEL_INFO, PSTR("SX1: ReadIDinfo sent...")); return true; } else if (!strcasecmp(XdrvMailbox.data, "ReadConfig")) { #ifdef SOLAXX1_READCONFIG solaxX1_global.Command_QueryConfig = true; - AddLog(LOG_LEVEL_INFO, PSTR("SX1: ReadConfig sent...")); return true; #else AddLog(LOG_LEVEL_INFO, PSTR("SX1: Command not available. Please set compiler directive '#define SOLAXX1_READCONFIG'.")); @@ -501,8 +649,9 @@ bool SolaxX1_cmd(void) { } #ifdef USE_WEBSERVER -const char HTTP_SNS_solaxX1_Num[] PROGMEM = "{s}" D_SOLAX_X1 " %s{m}%s{m}{m} %s{e}"; -const char HTTP_SNS_solaxX1_Str[] PROGMEM = "{s}" D_SOLAX_X1 " %s{m}%s{e}"; +const char HTTP_SNS_solaxX1_Num[] PROGMEM = "{s}" D_SOLAX_X1 " %s{m}%s{m}{m}%s{e}"; +const char HTTP_SNS_solaxX1_Str[] PROGMEM = "{s}" D_SOLAX_X1 " %s%s{e}"; +const char HTTP_SNS_solaxX1_Mtr[] PROGMEM = "{s}" D_GATEWAY " %s{m}%s{m}{m}%s{e}"; #endif // USE_WEBSERVER void solaxX1_Show(uint32_t function) { @@ -523,7 +672,7 @@ void solaxX1_Show(uint32_t function) { dtostrfd(solaxX1.dc2_power, Settings->flag2.wattage_resolution, pv2_power); #endif char status[33]; - GetTextIndexed(status, sizeof(status), solaxX1.runMode + 1, kSolaxMode); + GetTextIndexed(status, sizeof(status), solaxX1.runMode + 2, kSolaxMode); switch (function) { case FUNC_JSON_APPEND: @@ -544,10 +693,23 @@ void solaxX1_Show(uint32_t function) { #ifdef USE_WEBSERVER case FUNC_WEB_COL_SENSOR: { String table_align = Settings->flag5.gui_table_align?"right":"left"; + if (solaxX1_global.MeterMode) { + char TempDataChar[33]; + WSContentSend_P(PSTR("
{e}")); + dtostrfd(solaxX1_global.MeterPower, Settings->flag2.wattage_resolution, TempDataChar); + WSContentSend_PD(HTTP_SNS_solaxX1_Mtr, D_POWERUSAGE, table_align.c_str(), TempDataChar, D_UNIT_WATT); + dtostrfd(solaxX1_global.MeterImport, Settings->flag2.energy_resolution, TempDataChar); + WSContentSend_PD(HTTP_SNS_solaxX1_Mtr, "Import", table_align.c_str(), TempDataChar, D_UNIT_KILOWATTHOUR); + dtostrfd(solaxX1_global.MeterExport, Settings->flag2.energy_resolution, TempDataChar); + WSContentSend_PD(HTTP_SNS_solaxX1_Mtr, "Export", table_align.c_str(), TempDataChar, D_UNIT_KILOWATTHOUR); + return; + } static uint32_t LastOnlineTime; if (solaxX1.runMode != -1) LastOnlineTime = TasmotaGlobal.uptime; if (TasmotaGlobal.uptime < LastOnlineTime + 300) { // Hide numeric live data, when inverter is offline for more than 5 min +#ifdef SOLAXX1_PV2 WSContentSend_PD(HTTP_SNS_solaxX1_Num, D_SOLAR_POWER, table_align.c_str(), solar_power, D_UNIT_WATT); +#endif WSContentSend_PD(HTTP_SNS_solaxX1_Num, D_PV1_VOLTAGE, table_align.c_str(), pv1_voltage, D_UNIT_VOLT); WSContentSend_PD(HTTP_SNS_solaxX1_Num, D_PV1_CURRENT, table_align.c_str(), pv1_current, D_UNIT_AMPERE); WSContentSend_PD(HTTP_SNS_solaxX1_Num, D_PV1_POWER, table_align.c_str(), pv1_power, D_UNIT_WATT); @@ -563,6 +725,7 @@ void solaxX1_Show(uint32_t function) { WSContentSend_P(HTTP_SNS_solaxX1_Num, D_UPTIME, table_align.c_str(), String(solaxX1.runtime_total).c_str(), D_UNIT_HOUR); break; } case FUNC_WEB_SENSOR: + if (solaxX1_global.MeterMode) return; char errorCodeString[33]; WSContentSend_P(HTTP_SNS_solaxX1_Str, D_STATUS, status); WSContentSend_P(HTTP_SNS_solaxX1_Str, D_ERROR, GetTextIndexed(errorCodeString, sizeof(errorCodeString), solaxX1_ParseErrorCode(solaxX1.errorCode), kSolaxError)); @@ -580,8 +743,11 @@ bool Xnrg12(uint32_t function) { bool result = false; switch (function) { + case FUNC_EVERY_100_MSECOND: + if (solaxX1_global.MeterMode) solaxX1_CyclicTask(); + break; case FUNC_EVERY_250_MSECOND: - solaxX1_250MSecond(); + if (!solaxX1_global.MeterMode) solaxX1_CyclicTask(); break; #ifdef USE_WEBSERVER case FUNC_WEB_COL_SENSOR: