/* xlgt_05_sonoff_l1.ino - Sonoff L1 led support for Tasmota Copyright (C) 2021 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_LIGHT #ifdef USE_SONOFF_L1 /*********************************************************************************************\ * Sonoff L1 \*********************************************************************************************/ #define XLGT_05 5 //#define SONOFF_L1_START_DELAY // Sync Nuvotron power state with Tasmota on power up //#define SONOFF_L1_ALLOW_REMOTE_INTERRUPT // During schemes 2..4 #define SONOFF_L1_DEBUG1 // Add send and receive logging #define SONOFF_L1_BUFFER_SIZE 140 #define SONOFF_L1_MODE_COLORFUL 1 // [Color key] Colorful (static color) #define SONOFF_L1_MODE_COLORFUL_GRADIENT 2 // [SMOOTH] Colorful Gradient #define SONOFF_L1_MODE_COLORFUL_BREATH 3 // [FADE] Colorful Breath #define SONOFF_L1_MODE_DIY_GRADIENT 4 // DIY Gradient (fade in and out) [Speed 1- 100, color] #define SONOFF_L1_MODE_DIY_PULSE 5 // DIY Pulse (faster fade in and out) [Speed 1- 100, color] #define SONOFF_L1_MODE_DIY_BREATH 6 // DIY Breath (toggle on/off) [Speed 1- 100, color] #define SONOFF_L1_MODE_DIY_STROBE 7 // DIY Strobe (faster toggle on/off) [Speed 1- 100, color] #define SONOFF_L1_MODE_RGB_GRADIENT 8 // RGB Gradient #define SONOFF_L1_MODE_RGB_PULSE 9 // [STROBE] RGB Pulse #define SONOFF_L1_MODE_RGB_BREATH 10 // RGB Breath #define SONOFF_L1_MODE_RGB_STROBE 11 // [FLASH] RGB strobe #define SONOFF_L1_MODE_SYNC_TO_MUSIC 12 // Sync to music [Speed 1- 100, sensitivity 1 - 10] struct SNFL1 { #ifdef SONOFF_L1_ALLOW_REMOTE_INTERRUPT uint32_t unlock = 0; bool receive_ready = true; #endif #ifdef SONOFF_L1_START_DELAY char buffer[SONOFF_L1_BUFFER_SIZE]; #endif uint8_t color[3]; uint8_t dimmer; uint8_t power; } Snfl1; /********************************************************************************************/ #ifdef SONOFF_L1_START_DELAY #include Ticker SnfL1StartDelay; void SnfL1SendDelayed(void) { SnfL1Send(); } void SnfL1Send(void) { #ifdef SONOFF_L1_DEBUG1 AddLog_P(LOG_LEVEL_DEBUG, PSTR("SL1: Send %s"), Snfl1.buffer); #endif Serial.print(Snfl1.buffer); Serial.write(0x1B); Serial.flush(); } void SnfL1SerialSendOk(void) { snprintf_P(Snfl1.buffer, sizeof(Snfl1.buffer), PSTR("AT+SEND=ok")); SnfL1Send(); } #else void SnfL1Send(const char *buffer) { #ifdef SONOFF_L1_DEBUG1 AddLog_P(LOG_LEVEL_DEBUG, PSTR("SL1: Send %s"), buffer); #endif Serial.print(buffer); Serial.write(0x1B); Serial.flush(); } void SnfL1SerialSendOk(void) { char buffer[16]; snprintf_P(buffer, sizeof(buffer), PSTR("AT+SEND=ok")); SnfL1Send(buffer); } #endif // SONOFF_L1_START_DELAY bool SnfL1SerialInput(void) { if (TasmotaGlobal.serial_in_byte != 0x1B) { if (TasmotaGlobal.serial_in_byte_counter >= SONOFF_L1_BUFFER_SIZE) { TasmotaGlobal.serial_in_byte_counter = 0; } if (TasmotaGlobal.serial_in_byte_counter || (!TasmotaGlobal.serial_in_byte_counter && ('A' == TasmotaGlobal.serial_in_byte))) { // A from AT TasmotaGlobal.serial_in_buffer[TasmotaGlobal.serial_in_byte_counter++] = TasmotaGlobal.serial_in_byte; } } else { TasmotaGlobal.serial_in_buffer[TasmotaGlobal.serial_in_byte_counter++] = 0x00; // AT+RESULT="sequence":"1554682835320" // AT+UPDATE="sequence":"34906","switch":"on","light_type":1,"colorR":0,"colorG":16,"colorB":0,"bright":6,"mode":1 // AT+UPDATE="switch":"on","light_type":1,"colorR":255,"colorG":0,"colorB":0,"bright":6,"mode":1,"speed":100,"sensitive":10 #ifdef SONOFF_L1_DEBUG1 AddLog_P(LOG_LEVEL_DEBUG, PSTR("SL1: Rcvd %s"), TasmotaGlobal.serial_in_buffer); #endif if (!strncmp(TasmotaGlobal.serial_in_buffer +3, "RESULT", 6)) { #ifdef SONOFF_L1_ALLOW_REMOTE_INTERRUPT Snfl1.receive_ready = true; #endif } else if (!strncmp(TasmotaGlobal.serial_in_buffer +3, "UPDATE", 6)) { char cmnd_dimmer[20]; char cmnd_color[20]; char *end_str; char *string = TasmotaGlobal.serial_in_buffer +10; char *token = strtok_r(string, ",", &end_str); bool color_updated[3] = { false, false, false }; bool switch_state = false; bool is_power_change = false; bool is_color_change = false; bool is_brightness_change = false; while (token != nullptr) { char* end_token; char* token2 = strtok_r(token, ":", &end_token); char* token3 = strtok_r(nullptr, ":", &end_token); if (!strncmp(token2, "\"sequence\"", 10)) { // AddLog_P(LOG_LEVEL_DEBUG, PSTR("SL1: Rcvd sequence %s"), token3); token = nullptr; } else if (!strncmp(token2, "\"switch\"", 8)) { switch_state = !strncmp(token3, "\"on\"", 4) ? true : false; // AddLog_P(LOG_LEVEL_DEBUG, PSTR("SL1: Rcvd switch %d (%d)"), switch_state, Light.power); is_power_change = (switch_state != Light.power); } else if (!strncmp(token2, "\"color", 6)) { char color_channel_name = token2[6]; int color_index; switch(color_channel_name) { case 'R': color_index = 0; break; case 'G': color_index = 1; break; case 'B': color_index = 2; break; } int color_value = atoi(token3); Snfl1.color[color_index] = color_value; color_updated[color_index] = true; bool all_color_channels_updated = color_updated[0] && color_updated[1] && color_updated[2]; if (all_color_channels_updated) { // AddLog_P(LOG_LEVEL_DEBUG, PSTR("SL1: Rcvd color R%d G%d B%d (R%d G%d B%d)"), // Snfl1.color[0], Snfl1.color[1], Snfl1.color[2], // Settings.light_color[0], Settings.light_color[1], Settings.light_color[2]); is_color_change = (Light.power && (memcmp(Snfl1.color, Settings.light_color, 3) != 0)); } snprintf_P(cmnd_color, sizeof(cmnd_color), PSTR(D_CMND_COLOR "2 %02x%02x%02x"), Snfl1.color[0], Snfl1.color[1], Snfl1.color[2]); } else if (!strncmp(token2, "\"bright\"", 8)) { uint8_t dimmer = atoi(token3); // AddLog_P(LOG_LEVEL_DEBUG, PSTR("SL1: Rcvd dimmer %d (%d)"), dimmer, Settings.light_dimmer); is_brightness_change = (Light.power && (dimmer > 0) && (dimmer != Settings.light_dimmer)); snprintf_P(cmnd_dimmer, sizeof(cmnd_dimmer), PSTR(D_CMND_DIMMER " %d"), dimmer); } token = strtok_r(nullptr, ",", &end_str); } if (is_power_change) { if (Settings.light_scheme > 0) { if (!switch_state) { // If power off RC button pressed stop schemes char cmnd_scheme[20]; snprintf_P(cmnd_scheme, sizeof(cmnd_scheme), PSTR(D_CMND_SCHEME " 0")); ExecuteCommand(cmnd_scheme, SRC_REMOTE); } } else { ExecuteCommandPower(1, switch_state, SRC_REMOTE); } } else if (is_brightness_change) { ExecuteCommand(cmnd_dimmer, SRC_REMOTE); } else if (Light.power && is_color_change) { if (0 == Settings.light_scheme) { // Fix spurious color receptions when scheme > 0 if (Settings.light_fade) { // Disable fade as RC button colors overrule and are immediate supressing ghost colors char cmnd_fade[20]; snprintf_P(cmnd_fade, sizeof(cmnd_fade), PSTR(D_CMND_FADE " 0")); ExecuteCommand(cmnd_fade, SRC_REMOTE); } if (Settings.light_correction) { // Disable ledtable as RC button colors overrule and are immediate supressing ghost colors char cmnd_fade[20]; snprintf_P(cmnd_fade, sizeof(cmnd_fade), PSTR(D_CMND_LEDTABLE " 0")); ExecuteCommand(cmnd_fade, SRC_REMOTE); } ExecuteCommand(cmnd_color, SRC_REMOTE); } } } SnfL1SerialSendOk(); return true; } TasmotaGlobal.serial_in_byte = 0; return false; } /********************************************************************************************/ bool SnfL1SetChannels(void) { #ifdef SONOFF_L1_ALLOW_REMOTE_INTERRUPT if (Snfl1.receive_ready || TimeReached(Snfl1.unlock)) { #endif uint8_t power = Light.power; bool power_changed = (Snfl1.power != power); Snfl1.power = power; uint8_t dimmer = light_state.getDimmer(); bool dimmer_changed = (Snfl1.dimmer != dimmer); Snfl1.dimmer = dimmer; uint8_t *scale_col = (uint8_t*)XdrvMailbox.topic; bool color_changed = false; if (!power_changed) { for (uint32_t i = 0; i < 3; i++) { if ((Snfl1.color[i] < scale_col[i] -5) || (Snfl1.color[i] > scale_col[i] +5)) { color_changed = true; // Allow scale-up margins of +/-5 } Snfl1.color[i] = scale_col[i]; } } if (!power_changed && !dimmer_changed && !color_changed) { return true; } #ifdef SONOFF_L1_START_DELAY snprintf_P(Snfl1.buffer, sizeof(Snfl1.buffer), PSTR("AT+UPDATE=\"sequence\":\"%d%03d\",\"switch\":\"%s\",\"light_type\":1,\"colorR\":%d,\"colorG\":%d,\"colorB\":%d,\"bright\":%d,\"mode\":%d"), LocalTime(), millis()%1000, Snfl1.power ? "on" : "off", Snfl1.color[0], Snfl1.color[1], Snfl1.color[2], Snfl1.dimmer, SONOFF_L1_MODE_COLORFUL); static bool first_call = true; if (first_call) { SnfL1StartDelay.once_ms(900, SnfL1SendDelayed); // Allow startup time for Nuvotron microcontroller first_call = false; } else { SnfL1Send(); } #else char buffer[SONOFF_L1_BUFFER_SIZE]; snprintf_P(buffer, sizeof(buffer), PSTR("AT+UPDATE=\"sequence\":\"%d%03d\",\"switch\":\"%s\",\"light_type\":1,\"colorR\":%d,\"colorG\":%d,\"colorB\":%d,\"bright\":%d,\"mode\":%d"), LocalTime(), millis()%1000, Snfl1.power ? "on" : "off", Snfl1.color[0], Snfl1.color[1], Snfl1.color[2], Snfl1.dimmer, SONOFF_L1_MODE_COLORFUL); SnfL1Send(buffer); #endif // SONOFF_L1_START_DELAY #ifdef SONOFF_L1_ALLOW_REMOTE_INTERRUPT Snfl1.unlock = millis() + 500; // Allow time for the RC Snfl1.receive_ready = false; } #endif return true; } bool SnfL1ModuleSelected(void) { if (SONOFF_L1 == TasmotaGlobal.module_type) { if (PinUsed(GPIO_RXD) && PinUsed(GPIO_TXD)) { SetSerial(19200, TS_SERIAL_8N1); Snfl1.power = !Light.power; Snfl1.dimmer = !light_state.getDimmer(); TasmotaGlobal.light_type = LT_RGB; TasmotaGlobal.light_driver = XLGT_05; AddLog_P(LOG_LEVEL_DEBUG, PSTR("LGT: Sonoff L1 Found")); return true; } } return false; } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xlgt05(uint8_t function) { bool result = false; switch (function) { case FUNC_SERIAL: result = SnfL1SerialInput(); break; case FUNC_SET_CHANNELS: result = SnfL1SetChannels(); break; case FUNC_MODULE_INIT: result = SnfL1ModuleSelected(); break; } return result; } #endif // USE_SONOFF_L1 #endif // USE_LIGHT