/* xdrv_31_tasmota_client.ino - Support for external microcontroller on serial Copyright (C) 2021 Andre Thomas and Theo Arends 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 . */ #ifdef USE_TASMOTA_CLIENT /*********************************************************************************************\ * Tasmota to microcontroller \*********************************************************************************************/ #define XDRV_31 31 #ifndef USE_TASMOTA_CLIENT_FLASH_SPEED #define USE_TASMOTA_CLIENT_FLASH_SPEED 57600 // Usually 57600 for 3.3V variants and 115200 for 5V variants #endif #ifndef USE_TASMOTA_CLIENT_SERIAL_SPEED #define USE_TASMOTA_CLIENT_SERIAL_SPEED 57600 // Depends on the sketch that is running on the Uno/Pro Mini #endif #define TASMOTA_CLIENT_LIB_VERSION 20191129 #define TASMOTA_CLIENT_TIMEOUT 250 // mSeconds #define CONST_STK_CRC_EOP 0x20 #define CMND_STK_GET_SYNC 0x30 #define CMND_STK_SET_DEVICE 0x42 #define CMND_STK_SET_DEVICE_EXT 0x45 #define CMND_STK_ENTER_PROGMODE 0x50 #define CMND_STK_LEAVE_PROGMODE 0x51 #define CMND_STK_LOAD_ADDRESS 0x55 #define CMND_STK_PROG_PAGE 0x64 /*************************************************\ * Tasmota Client Specific Commands \*************************************************/ #define CMND_START 0xFC #define CMND_END 0xFD #define CMND_FEATURES 0x01 #define CMND_JSON 0x02 #define CMND_FUNC_EVERY_SECOND 0x03 #define CMND_FUNC_EVERY_100_MSECOND 0x04 #define CMND_CLIENT_SEND 0x05 #define CMND_PUBLISH_TELE 0x06 #define CMND_EXECUTE_CMND 0x07 #define PARAM_DATA_START 0xFE #define PARAM_DATA_END 0xFF #include struct SimpleHexParse { uint8_t FlashPage[128]; uint8_t layoverBuffer[16]; uint8_t FlashPageIdx; uint8_t layoverIdx; uint8_t ptr_l; uint8_t ptr_h; bool firstrun; bool EndOfFile; } SHParse; uint8_t SimpleHexParseGetByte(char* hexline, uint8_t idx) { char buff[3]; buff[3] = '\0'; memcpy(&buff, &hexline[(idx*2)-2], 2); return strtol(buff, 0, 16); } uint32_t SimpleHexParseLine(char *hexline) { if (SHParse.layoverIdx) { memcpy(&SHParse.FlashPage, &SHParse.layoverBuffer, SHParse.layoverIdx); SHParse.FlashPageIdx = SHParse.layoverIdx; SHParse.layoverIdx = 0; } // 10 00 00 00 0C945D000C9485000C9485000C948500 84 uint8_t len = SimpleHexParseGetByte(hexline, 1); uint8_t addr_h = SimpleHexParseGetByte(hexline, 2); uint8_t addr_l = SimpleHexParseGetByte(hexline, 3); uint8_t rectype = SimpleHexParseGetByte(hexline, 4); // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: Hexline |%s|, Len %d, Address 0x%02X%02X, RecType %d"), hexline, len, addr_h, addr_l, rectype); if (len > 16) { return 5; } // Error: Line too long if (rectype > 1) { return 6; } // Error: Invalid record type for (uint32_t idx = 0; idx < len; idx++) { if (SHParse.FlashPageIdx < sizeof(SHParse.FlashPage)) { SHParse.FlashPage[SHParse.FlashPageIdx] = SimpleHexParseGetByte(hexline, idx+5); SHParse.FlashPageIdx++; } else { // We have layover bytes SHParse.layoverBuffer[SHParse.layoverIdx] = SimpleHexParseGetByte(hexline, idx+5); SHParse.layoverIdx++; } } if (1 == rectype) { SHParse.EndOfFile = true; while (SHParse.FlashPageIdx < sizeof(SHParse.FlashPage)) { SHParse.FlashPage[SHParse.FlashPageIdx] = 0xFF; SHParse.FlashPageIdx++; } } if (SHParse.FlashPageIdx == sizeof(SHParse.FlashPage)) { if (SHParse.firstrun) { SHParse.firstrun = false; } else { SHParse.ptr_l += 0x40; if (SHParse.ptr_l == 0) { SHParse.ptr_h++; } } } return 0; } struct TCLIENT { uint8_t inverted = LOW; bool type = false; bool SerialEnabled = false; uint8_t waitstate = 0; // We use this so that features detection does not slow down other stuff on startup bool unsupported = false; } TClient; typedef union { uint32_t data; struct { uint32_t func_json_append : 1; // Client supports providing a JSON for TELEPERIOD uint32_t func_every_second : 1; // Client supports receiving a FUNC_EVERY_SECOND callback with no response uint32_t func_every_100_msecond : 1; // Client supports receiving a FUNC_EVERY_100_MSECOND callback with no response uint32_t func_client_send : 1; // Client supports receiving commands with "client send xxx" uint32_t spare4 : 1; uint32_t spare5 : 1; uint32_t spare6 : 1; uint32_t spare7 : 1; uint32_t spare8 : 1; uint32_t spare9 : 1; uint32_t spare10 : 1; uint32_t spare11 : 1; uint32_t spare12 : 1; uint32_t spare13 : 1; uint32_t spare14 : 1; uint32_t spare15 : 1; uint32_t spare16 : 1; uint32_t spare17 : 1; uint32_t spare18 : 1; uint32_t spare19 : 1; uint32_t spare20 : 1; uint32_t spare21 : 1; uint32_t spare22 : 1; uint32_t spare23 : 1; uint32_t spare24 : 1; uint32_t spare25 : 1; uint32_t spare26 : 1; uint32_t spare27 : 1; uint32_t spare28 : 1; uint32_t spare29 : 1; uint32_t spare30 : 1; uint32_t spare31 : 1; }; } TClientFeatureCfg; /* * The structure below must remain 4 byte aligned to be compatible with * Tasmota as master */ struct TCLIENT_FEATURES { uint32_t features_version; TClientFeatureCfg features; } TClientSettings; struct TCLIENT_COMMAND { uint8_t command; uint8_t parameter; uint8_t unused2; uint8_t unused3; } TClientCommand; TasmotaSerial *TasmotaClient_Serial; void TasmotaClient_Reset(void) { if (TClient.SerialEnabled) { digitalWrite(Pin(GPIO_TASMOTACLIENT_RST), !TClient.inverted); delay(1); digitalWrite(Pin(GPIO_TASMOTACLIENT_RST), TClient.inverted); delay(1); digitalWrite(Pin(GPIO_TASMOTACLIENT_RST), !TClient.inverted); delay(5); } } uint8_t TasmotaClient_waitForSerialData(int dataCount, int timeout) { int timer = 0; while (timer < timeout) { if (TasmotaClient_Serial->available() >= dataCount) { return 1; } delay(1); timer++; } return 0; } uint8_t TasmotaClient_receiveData(char* buffer, int size) { uint8_t index = 255; int timer = 0; while (timer < TASMOTA_CLIENT_TIMEOUT) { int data = TasmotaClient_Serial->read(); if (data >= 0) { if (PARAM_DATA_START == data) { index = 0; } // Start of data else if (PARAM_DATA_END == data) { break; } // End of data else if (index < 255) { buffer[index++] = (char)data; // Data if (index == size) { break; } // No EoD received or done } } else { delay(1); timer++; } } if (255 == index) { index = 0; } // AddLog(LOG_LEVEL_DEBUG, PSTR("TCL: ReceiveData")); // AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t*)buffer, index); return index; } uint8_t TasmotaClient_sendBytes(uint8_t* bytes, int count) { // AddLog(LOG_LEVEL_DEBUG, PSTR("TCL: SendBytes")); // AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t*)&bytes, count); TasmotaClient_Serial->write(bytes, count); TasmotaClient_waitForSerialData(2, TASMOTA_CLIENT_TIMEOUT); uint8_t sync = TasmotaClient_Serial->read(); uint8_t ok = TasmotaClient_Serial->read(); if ((sync == 0x14) && (ok == 0x10)) { return 1; } return 0; } uint8_t TasmotaClient_execCmd(uint8_t cmd) { uint8_t bytes[] = { cmd, CONST_STK_CRC_EOP }; return TasmotaClient_sendBytes(bytes, 2); } uint8_t TasmotaClient_execParam(uint8_t cmd, uint8_t* params, int count) { uint8_t bytes[32]; bytes[0] = cmd; int i = 0; while (i < count) { bytes[i + 1] = params[i]; i++; } bytes[i + 1] = CONST_STK_CRC_EOP; return TasmotaClient_sendBytes(bytes, i + 2); } uint32_t TasmotaClient_Flash(uint8_t* data, size_t size) { /* // Don't do this as there is no re-init configured TasmotaClient_Serial->end(); delay(10); TasmotaClient_Serial->begin(USE_TASMOTA_CLIENT_FLASH_SPEED); if (TasmotaClient_Serial->hardwareSerial()) { ClaimSerial(); } */ TasmotaClient_Reset(); uint8_t timeout = 0; while (timeout <= 50) { if (TasmotaClient_execCmd(CMND_STK_GET_SYNC)) { break; } timeout++; delay(1); } if (timeout > 50) { return 1; } // Error: Bootloader could not be found AddLog(LOG_LEVEL_INFO, PSTR("TCL: Found bootloader")); uint8_t ProgParams[] = {0x86, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x03, 0xff, 0xff, 0xff, 0xff, 0x00, 0x80, 0x04, 0x00, 0x00, 0x00, 0x80, 0x00}; if (!TasmotaClient_execParam(CMND_STK_SET_DEVICE, ProgParams, sizeof(ProgParams))) { return 2; // Error: Could not configure device for programming (1) } uint8_t ExtProgParams[] = {0x05, 0x04, 0xd7, 0xc2, 0x00}; if (!TasmotaClient_execParam(CMND_STK_SET_DEVICE_EXT, ExtProgParams, sizeof(ExtProgParams))) { return 3; // Error: Could not configure device for programming (2) } if (!TasmotaClient_execCmd(CMND_STK_ENTER_PROGMODE)) { return 4; // Error: Failed to put bootloader into programming mode } uint8_t header[] = {CMND_STK_PROG_PAGE, 0x00, 0x80, 0x46}; SHParse.FlashPageIdx = 0; SHParse.layoverIdx = 0; SHParse.ptr_l = 0; SHParse.ptr_h = 0; SHParse.firstrun = true; SHParse.EndOfFile = false; char flash_buffer[512]; char thishexline[50]; uint32_t processed = 0; uint32_t position = 0; uint32_t error = 0; uint32_t read = 0; while (read < size) { memcpy(flash_buffer, data + read, sizeof(flash_buffer)); read = read + sizeof(flash_buffer); // AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t*)flash_buffer, 32); for (uint32_t ca = 0; ca < sizeof(flash_buffer); ca++) { processed++; if ((processed <= size) && (!SHParse.EndOfFile)) { // flash_buffer = :100000000C945D000C9485000C9485000C94850084<0x0D><0x0A> if (':' == flash_buffer[ca]) { position = 0; } else if (0x0D == flash_buffer[ca]) { // 100000000C945D000C9485000C9485000C94850084 thishexline[position] = 0; error = SimpleHexParseLine(thishexline); if (error) { break; } // Error 5 and 6 if (SHParse.FlashPageIdx == sizeof(SHParse.FlashPage)) { uint8_t params[] = {SHParse.ptr_l, SHParse.ptr_h}; TasmotaClient_execParam(CMND_STK_LOAD_ADDRESS, params, sizeof(params)); TasmotaClient_Serial->write(header, sizeof(header)); for (uint32_t i = 0; i < sizeof(SHParse.FlashPage); i++) { TasmotaClient_Serial->write(SHParse.FlashPage[i]); } TasmotaClient_Serial->write(CONST_STK_CRC_EOP); TasmotaClient_waitForSerialData(2, TASMOTA_CLIENT_TIMEOUT); TasmotaClient_Serial->read(); TasmotaClient_Serial->read(); SHParse.FlashPageIdx = 0; } } else if (0x0A != flash_buffer[ca]) { if (!isalnum(flash_buffer[ca])) { // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: Size %d, Processed %d"), size, processed); error = 7; // Error: Invalid data break; } if (position < sizeof(thishexline) -2) { thishexline[position++] = flash_buffer[ca]; } } } } if (error) { break; } } TasmotaClient_execCmd(CMND_STK_LEAVE_PROGMODE); return error; // Error or Flash done! } void TasmotaClient_Init(void) { if (TClient.type) { return; } if (10 > TClient.waitstate) { TClient.waitstate++; return; } if (!TClient.SerialEnabled) { if (PinUsed(GPIO_TASMOTACLIENT_RXD) && PinUsed(GPIO_TASMOTACLIENT_TXD) && (PinUsed(GPIO_TASMOTACLIENT_RST) || PinUsed(GPIO_TASMOTACLIENT_RST_INV))) { TasmotaClient_Serial = new TasmotaSerial(Pin(GPIO_TASMOTACLIENT_RXD), Pin(GPIO_TASMOTACLIENT_TXD), 1, 0, 200); if (TasmotaClient_Serial->begin(USE_TASMOTA_CLIENT_SERIAL_SPEED)) { if (TasmotaClient_Serial->hardwareSerial()) { ClaimSerial(); } if (PinUsed(GPIO_TASMOTACLIENT_RST_INV)) { SetPin(Pin(GPIO_TASMOTACLIENT_RST_INV), AGPIO(GPIO_TASMOTACLIENT_RST)); TClient.inverted = HIGH; } pinMode(Pin(GPIO_TASMOTACLIENT_RST), OUTPUT); TClient.SerialEnabled = true; TasmotaClient_Reset(); AddLog(LOG_LEVEL_INFO, PSTR("TCL: Enabled")); } } } if (TClient.SerialEnabled) { // All go for hardware now we need to detect features if there are any TasmotaClient_sendCmnd(CMND_FEATURES, 0); char buffer[sizeof(TClientSettings)]; uint8_t len = TasmotaClient_receiveData(buffer, sizeof(buffer)); // 99 17 34 01 02 00 00 00 if (len == sizeof(TClientSettings)) { memcpy(&TClientSettings, &buffer, sizeof(TClientSettings)); if (TASMOTA_CLIENT_LIB_VERSION == TClientSettings.features_version) { TClient.type = true; AddLog(LOG_LEVEL_INFO, PSTR("TCL: Version %u"), TClientSettings.features_version); } else { if ((!TClient.unsupported) && (TClientSettings.features_version > 0)) { AddLog(LOG_LEVEL_INFO, PSTR("TCL: Version %u not supported!"), TClientSettings.features_version); TClient.unsupported = true; } } } } } bool TasmotaClient_Available(void) { return TClient.SerialEnabled; } void TasmotaClient_Show(void) { if ((TClient.type) && (TClientSettings.features.func_json_append)) { TasmotaClient_sendCmnd(CMND_JSON, 0); char buffer[100]; uint8_t len = TasmotaClient_receiveData(buffer, sizeof(buffer) -1); buffer[len] = '\0'; ResponseAppend_P(PSTR(",\"TasmotaClient\":%s"), buffer); } } void TasmotaClient_sendCmnd(uint8_t cmnd, uint8_t param) { TClientCommand.command = cmnd; TClientCommand.parameter = param; char buffer[sizeof(TClientCommand)+2]; buffer[0] = CMND_START; memcpy(&buffer[1], &TClientCommand, sizeof(TClientCommand)); buffer[sizeof(TClientCommand)+1] = CMND_END; // AddLog(LOG_LEVEL_DEBUG, PSTR("TCL: SendCmnd")); // AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t*)&buffer, sizeof(buffer)); for (uint32_t ca = 0; ca < sizeof(buffer); ca++) { TasmotaClient_Serial->write(buffer[ca]); } } #define D_PRFX_CLIENT "Client" #define D_CMND_CLIENT_RESET "Reset" #define D_CMND_CLIENT_SEND "Send" const char kTasmotaClientCommands[] PROGMEM = D_PRFX_CLIENT "|" D_CMND_CLIENT_RESET "|" D_CMND_CLIENT_SEND; void (* const TasmotaClientCommand[])(void) PROGMEM = { &CmndClientReset, &CmndClientSend }; void CmndClientReset(void) { TasmotaClient_Reset(); TClient.type = false; // Force redetection TClient.waitstate = 7; // give it at least 3 seconds to restart from bootloader TClient.unsupported = false; // Reset unsupported flag ResponseCmndDone(); } void CmndClientSend(void) { if (TClient.SerialEnabled) { if (0 < XdrvMailbox.data_len) { TasmotaClient_sendCmnd(CMND_CLIENT_SEND, XdrvMailbox.data_len); TasmotaClient_Serial->write(char(PARAM_DATA_START)); for (uint32_t idx = 0; idx < XdrvMailbox.data_len; idx++) { TasmotaClient_Serial->write(XdrvMailbox.data[idx]); } TasmotaClient_Serial->write(char(PARAM_DATA_END)); } ResponseCmndDone(); } } void TasmotaClient_ProcessIn(void) { uint8_t cmnd = TasmotaClient_Serial->read(); if (CMND_START == cmnd) { TasmotaClient_waitForSerialData(sizeof(TClientCommand), 50); uint8_t buffer[sizeof(TClientCommand)]; for (uint32_t idx = 0; idx < sizeof(TClientCommand); idx++) { buffer[idx] = TasmotaClient_Serial->read(); } TasmotaClient_Serial->read(); // read trailing byte of command memcpy(&TClientCommand, &buffer, sizeof(TClientCommand)); char inbuf[TClientCommand.parameter+1]; TasmotaClient_waitForSerialData(TClientCommand.parameter, 50); TasmotaClient_Serial->read(); // Read leading byte for (uint32_t idx = 0; idx < TClientCommand.parameter; idx++) { inbuf[idx] = TasmotaClient_Serial->read(); } TasmotaClient_Serial->read(); // Read trailing byte inbuf[TClientCommand.parameter] = '\0'; if (CMND_PUBLISH_TELE == TClientCommand.command) { // We need to publish stat/ with incoming stream as content Response_P(PSTR("{\"TasmotaClient\":")); ResponseAppend_P("%s", inbuf); ResponseJsonEnd(); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR("TasmotaClient")); } if (CMND_EXECUTE_CMND == TClientCommand.command) { // We need to execute the incoming command ExecuteCommand(inbuf, SRC_TCL); } } } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv31(uint8_t function) { bool result = false; switch (function) { case FUNC_EVERY_100_MSECOND: if (TClient.type) { if (TasmotaClient_Serial->available()) { TasmotaClient_ProcessIn(); } if (TClientSettings.features.func_every_100_msecond) { TasmotaClient_sendCmnd(CMND_FUNC_EVERY_100_MSECOND, 0); } } break; case FUNC_EVERY_SECOND: if ((TClient.type) && (TClientSettings.features.func_every_second)) { TasmotaClient_sendCmnd(CMND_FUNC_EVERY_SECOND, 0); } TasmotaClient_Init(); break; case FUNC_JSON_APPEND: if ((TClient.type) && (TClientSettings.features.func_json_append)) { TasmotaClient_Show(); } break; case FUNC_COMMAND: result = DecodeCommand(kTasmotaClientCommands, TasmotaClientCommand); break; } return result; } #endif // USE_TASMOTA_CLIENT