diff --git a/CHANGELOG.md b/CHANGELOG.md index bd49bc4c8..d542934fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. ## [9.2.0.2] ### Added - Basic support for ESP32 Odroid Go 16MB binary tasmota32-odroidgo.bin (#8630) +- Command ``CTRange`` to specify the visible CT range the bulb is capable of +- Command ``VirtualCT`` to simulate or fine tune CT bulbs with 3,4,5 channels ### Breaking Changed - Replaced MFRC522 13.56MHz rfid card reader GPIO selection from ``GPIO_SPI_CS`` by ``GPIO_RC522_CS`` diff --git a/tasmota/i18n.h b/tasmota/i18n.h index 17cd2fe53..3fd70be4e 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -425,6 +425,8 @@ #define D_CMND_DIMMER_RANGE "DimmerRange" #define D_CMND_DIMMER_STEP "DimmerStep" #define D_CMND_HSBCOLOR "HSBColor" +#define D_CMND_VIRTUALCT "VirtualCT" +#define D_CMND_CTRANGE "CTRange" #define D_CMND_LED "Led" #define D_CMND_LEDTABLE "LedTable" #define D_CMND_FADE "Fade" diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 7c354c8bf..cc75735cf 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -309,6 +309,7 @@ #define LIGHT_WHITE_BLEND_MODE false // [SetOption105] White Blend Mode - used to be `RGBWWTable` last value `0`, now deprecated in favor of this option #define LIGHT_VIRTUAL_CT false // [SetOption106] Virtual CT - Creates a virtual White ColorTemp for RGBW lights #define LIGHT_VIRTUAL_CT_CW false // [SetOption107] Virtual CT Channel - signals whether the hardware white is cold CW (true) or warm WW (false) +#define LIGHT_VIRTUAL_CT_POINTS 3 // Number of reference points for Virtual CT (min 2, default 3) // -- Energy -------------------------------------- #define ENERGY_VOLTAGE_ALWAYS false // [SetOption21] Enable show voltage even if powered off @@ -494,6 +495,7 @@ #define USE_SONOFF_L1 // Add support for Sonoff L1 led control #define USE_ELECTRIQ_MOODL // Add support for ElectriQ iQ-wifiMOODL RGBW LED controller (+0k3 code) #define USE_LIGHT_PALETTE // Add support for color palette (+0k7 code) +#define USE_LIGHT_VIRTUAL_CT // Add support for Virtual White Color Temperature (+1.1k code) #define USE_DGR_LIGHT_SEQUENCE // Add support for device group light sequencing (requires USE_DEVICE_GROUPS) (+0k2 code) // -- Counter input ------------------------------- diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index 34a3bd2e3..bc1121123 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -941,6 +941,11 @@ void CmndSetoption(void) else if (4 == ptype) { // SetOption82 .. 113 bitWrite(Settings.flag4.data, pindex, XdrvMailbox.payload); switch (pindex) { +#ifdef USE_LIGHT + case 0: // SetOption 82 - (Alexa) Reduced CT range for Alexa (1) + setAlexaCTRange(); + break; +#endif case 3: // SetOption85 - Enable Device Groups case 6: // SetOption88 - PWM Dimmer Buttons control remote devices case 15: // SetOption97 - Set Baud rate for TuyaMCU serial communication (0 = 9600 or 1 = 115200) diff --git a/tasmota/tasmota_configurations.h b/tasmota/tasmota_configurations.h index 52bd19c58..6e4169e82 100644 --- a/tasmota/tasmota_configurations.h +++ b/tasmota/tasmota_configurations.h @@ -508,6 +508,7 @@ // -- Optional light modules ---------------------- //#undef USE_LIGHT // Enable Dimmer/Light support +#undef USE_LIGHT_VIRTUAL_CT // Disable support for Virtual White Color Temperature (SO106) #undef USE_WS2812 // Disable WS2812 Led string using library NeoPixelBus (+5k code, +1k mem, 232 iram) - Disable by // #undef USE_MY92X1 // Disable support for MY92X1 RGBCW led controller as used in Sonoff B1, Ailight and Lohas #undef USE_SM16716 // Disable support for SM16716 RGB LED controller (+0k7 code) @@ -643,6 +644,7 @@ //#undef USE_SONOFF_D1 // Disable support for Sonoff D1 Dimmer (+0k7 code) // -- Optional light modules ---------------------- +#undef USE_LIGHT_VIRTUAL_CT // Disable support for Virtual White Color Temperature (SO106) //#undef USE_LIGHT // Also disable all Dimmer/Light support #undef USE_WS2812 // Disable WS2812 Led string using library NeoPixelBus (+5k code, +1k mem, 232 iram) - Disable by // #undef USE_MY92X1 // Disable support for MY92X1 RGBCW led controller as used in Sonoff B1, Ailight and Lohas diff --git a/tasmota/xdrv_04_light.ino b/tasmota/xdrv_04_light.ino index 2cd84f780..a47c4a4ca 100644 --- a/tasmota/xdrv_04_light.ino +++ b/tasmota/xdrv_04_light.ino @@ -132,6 +132,10 @@ const char kLightCommands[] PROGMEM = "|" // No prefix D_CMND_COLOR "|" D_CMND_COLORTEMPERATURE "|" D_CMND_DIMMER "|" D_CMND_DIMMER_RANGE "|" D_CMND_DIMMER_STEP "|" D_CMND_LEDTABLE "|" D_CMND_FADE "|" D_CMND_RGBWWTABLE "|" D_CMND_SCHEME "|" D_CMND_SPEED "|" D_CMND_WAKEUP "|" D_CMND_WAKEUPDURATION "|" D_CMND_WHITE "|" D_CMND_CHANNEL "|" D_CMND_HSBCOLOR + "|" D_CMND_CTRANGE +#ifdef USE_LIGHT_VIRTUAL_CT + "|" D_CMND_VIRTUALCT +#endif // USE_LIGHT_VIRTUAL_CT #ifdef USE_LIGHT_PALETTE "|" D_CMND_PALETTE #endif // USE_LIGHT_PALETTE @@ -144,6 +148,10 @@ void (* const LightCommand[])(void) PROGMEM = { &CmndColor, &CmndColorTemperature, &CmndDimmer, &CmndDimmerRange, &CmndDimmerStep, &CmndLedTable, &CmndFade, &CmndRgbwwTable, &CmndScheme, &CmndSpeed, &CmndWakeup, &CmndWakeupDuration, &CmndWhite, &CmndChannel, &CmndHsbColor, + &CmndCTRange, +#ifdef USE_LIGHT_VIRTUAL_CT + &CmndVirtualCT, +#endif // USE_LIGHT_VIRTUAL_CT #ifdef USE_LIGHT_PALETTE &CmndPalette, #endif // USE_LIGHT_PALETTE @@ -181,6 +189,12 @@ const uint16_t CT_MAX = 500; // 2000K // Ranges used for Alexa const uint16_t CT_MIN_ALEXA = 200; // also 5000K const uint16_t CT_MAX_ALEXA = 380; // also 2600K +// Virtual CT default values +typedef uint8_t vct_pivot_t[LST_MAX]; +const size_t CT_PIVOTS = LIGHT_VIRTUAL_CT_POINTS; +const vct_pivot_t CT_PIVOTS_RGB PROGMEM = { 255, 255, 255, 0, 0 }; +const vct_pivot_t CT_PIVOTS_CWW PROGMEM = { 0, 0, 0, 255, 0 }; +const vct_pivot_t CT_PIVOTS_WWW PROGMEM = { 0, 0, 0, 0, 255 }; // New version of Gamma correction compute // Instead of a table, we do a multi-linear approximation, which is close enough @@ -308,6 +322,12 @@ struct LIGHT { uint16_t pwm_min = 0; // minimum value for PWM, from DimmerRange, 0..1023 uint16_t pwm_max = 1023; // maxumum value for PWM, from DimmerRange, 0..1023 + + // Virtual CT + uint16_t vct_ct[CT_PIVOTS]; // CT value for each segment +#ifdef USE_LIGHT_VIRTUAL_CT + vct_pivot_t vct_color[CT_PIVOTS]; // array of 3 colors each with 5 values +#endif } Light; power_t LightPower(void) @@ -378,13 +398,6 @@ class LightStateClass { uint8_t _briCT = 255; uint8_t _color_mode = LCM_RGB; // RGB by default - // the CT range below represents the rendered range, - // This is due to Alexa whose CT range is 199..383 - // Hence setting Min=200 and Max=380 makes Alexa use the full range - // Please note that you can still set CT to 153..500, but any - // value below _ct_min_range or above _ct_max_range not change the CT - uint16_t _ct_min_range = CT_MIN; // the minimum CT rendered range - uint16_t _ct_max_range = CT_MAX; // the maximum CT rendered range public: LightStateClass() { @@ -535,22 +548,7 @@ class LightStateClass { inline uint16_t getCT() const { return _ct; // 153..500, or CT_MIN..CT_MAX - } - - // get the CT value within the range into a 10 bits 0..1023 value - uint16_t getCT10bits() const { - return changeUIntScale(_ct, _ct_min_range, _ct_max_range, 0, 1023); - } - - inline void setCTRange(uint16_t ct_min_range, uint16_t ct_max_range) { - _ct_min_range = ct_min_range; - _ct_max_range = ct_max_range; - } - - inline void getCTRange(uint16_t *ct_min_range, uint16_t *ct_max_range) const { - if (ct_min_range) { *ct_min_range = _ct_min_range; } - if (ct_max_range) { *ct_max_range = _ct_max_range; } - } + } // get current color in XY format void getXY(float *x, float *y) { @@ -603,7 +601,7 @@ class LightStateClass { setColorMode(LCM_RGB); // try deactivating CT mode, setColorMode() will check which is legal } else { ct = (ct < CT_MIN ? CT_MIN : (ct > CT_MAX ? CT_MAX : ct)); - _ww = changeUIntScale(ct, _ct_min_range, _ct_max_range, 0, 255); + _ww = changeUIntScale(ct, Light.vct_ct[0], Light.vct_ct[CT_PIVOTS-1], 0, 255); _wc = 255 - _ww; _ct = ct; addCTMode(); @@ -932,15 +930,6 @@ public: return prev; } - void setAlexaCTRange(bool alexa_ct_range) { - // depending on SetOption82, full or limited CT range - if (alexa_ct_range) { - _state->setCTRange(CT_MIN_ALEXA, CT_MAX_ALEXA); // 200..380 - } else { - _state->setCTRange(CT_MIN, CT_MAX); // 153..500 - } - } - inline bool isCTRGBLinked() { return _ct_rgb_linked; } @@ -1190,6 +1179,63 @@ uint8_t change10to8(uint16_t v) { return (0 == v) ? 0 : changeUIntScale(v, 4, 1023, 1, 255); } +/*********************************************************************************************\ + * CT (White Color Temperature) +\*********************************************************************************************/ +// +// Ensure that invariants for Virtual CT are good: +// - CT_MIN <= ct[0] <= ct[1] <= ct[2] <= CT_MAX + +#ifdef USE_LIGHT_VIRTUAL_CT +void checkVirtualCT(void) { + if (Light.vct_ct[0] < CT_MIN) { Light.vct_ct[0] = CT_MIN; } + if (Light.vct_ct[CT_PIVOTS-1] > CT_MAX) { Light.vct_ct[CT_PIVOTS-1] = CT_MAX; } + for (uint32_t i = 0; i < CT_PIVOTS-1; i++) { + if (Light.vct_ct[i+1] < Light.vct_ct[i]) { Light.vct_ct[i+1] = Light.vct_ct[i]; } + } +} +#endif // USE_LIGHT_VIRTUAL_CT + +#ifdef USE_LIGHT_VIRTUAL_CT +// Init default values for virtual CT, depending on the number of channels +void initCTRange(uint32_t channels) { + if (channels == 4) { + if (Settings.flag4.virtual_ct_cw) { // Hardware White is Cold White + memcpy_P(Light.vct_color[0], CT_PIVOTS_CWW, sizeof(Light.vct_color[0])); // Cold white + memcpy_P(Light.vct_color[1], CT_PIVOTS_RGB, sizeof(Light.vct_color[1])); // Warm white + } else { // Hardware White is Warm White + memcpy_P(Light.vct_color[0], CT_PIVOTS_RGB, sizeof(Light.vct_color[0])); // Cold white + memcpy_P(Light.vct_color[1], CT_PIVOTS_CWW, sizeof(Light.vct_color[1])); // Warm white + } + } else if (channels == 5) { + memcpy_P(Light.vct_color[0], CT_PIVOTS_CWW, sizeof(Light.vct_color[0])); // Cold white + memcpy_P(Light.vct_color[1], CT_PIVOTS_WWW, sizeof(Light.vct_color[1])); // Warm white + } else { + memcpy_P(Light.vct_color[0], CT_PIVOTS_RGB, sizeof(Light.vct_color[0])); // Cold white + memcpy_P(Light.vct_color[1], CT_PIVOTS_RGB, sizeof(Light.vct_color[1])); // Warm white + } + for (uint32_t i = 1; i < CT_PIVOTS-1; i++) { + memcpy_P(Light.vct_color[i+1], Light.vct_color[i], sizeof(Light.vct_color[0])); // Copy slot 1 into slot 2 (slot 2 in unused) + } + checkVirtualCT(); +} +#endif // USE_LIGHT_VIRTUAL_CT + +void setCTRange(uint16_t ct_min, uint16_t ct_max) { + Light.vct_ct[0] = ct_min; + for (uint32_t i = 1; i < CT_PIVOTS; i++) { + Light.vct_ct[i] = ct_max; // all slots above [1] are not used + } +} + +void setAlexaCTRange(void) { // depending on SetOption82, full or limited CT range + if (Settings.flag4.alexa_ct_range) { + setCTRange(CT_MIN_ALEXA, CT_MAX_ALEXA); + } else { + setCTRange(CT_MIN, CT_MAX); + } +} + /*********************************************************************************************\ * Gamma correction \*********************************************************************************************/ @@ -1299,9 +1345,14 @@ bool LightModuleInit(void) } else if ((Settings.param[P_RGB_REMAP] & 128) && (LST_RGBW <= pwm_channels)) { // SetOption37 // if RGBW or RGBCW, and SetOption37 >= 128, we manage RGB and W separately, hence adding a device TasmotaGlobal.devices_present++; - } else if ((Settings.flag4.virtual_ct) && (LST_RGBW == pwm_channels)) { - Light.virtual_ct = true; // enabled - TasmotaGlobal.light_type++; // create an additional virtual 5th channel + } else { +#ifdef USE_LIGHT_VIRTUAL_CT + initCTRange(pwm_channels); + if ((Settings.flag4.virtual_ct) && (LST_RGB <= pwm_channels)) { + Light.virtual_ct = true; // enabled + TasmotaGlobal.light_type += 5 - pwm_channels; // pretend it is a 5 channels bulb + } +#endif // USE_LIGHT_VIRTUAL_CT } return (TasmotaGlobal.light_type > LT_BASIC); @@ -1367,7 +1418,7 @@ void LightInit(void) light_controller.setSubType(Light.subtype); light_controller.loadSettings(); - light_controller.setAlexaCTRange(Settings.flag4.alexa_ct_range); + setAlexaCTRange(); light_controller.calcLevels(); // calculate the initial values (#8058) if (LST_SINGLE == Light.subtype) { @@ -1859,7 +1910,6 @@ void LightAnimate(void) bool power_off = false; // make sure we update CT range in case SetOption82 was changed - light_controller.setAlexaCTRange(Settings.flag4.alexa_ct_range); Light.strip_timer_counter++; // set sleep parameter: either settings, @@ -1966,7 +2016,6 @@ void LightAnimate(void) uint16_t cur_col_10[LST_MAX]; // 10 bits resolution Light.update = false; - bool rgbwwtable_applied = false; // did we already applied RGBWWTable (ex: in white_blend_mode or virtual_ct) // first set 8 and 10 bits channels for (uint32_t i = 0; i < LST_MAX; i++) { @@ -1975,58 +2024,19 @@ void LightAnimate(void) cur_col_10[i] = change8to10(Light.new_color[i]); } + bool rgbwwtable_applied_white = false; // did we already applied RGBWWTable to white channels (ex: in white_blend_mode or virtual_ct) if (Light.pwm_multi_channels) { calcGammaMultiChannels(cur_col_10); } else { - calcGammaBulbs(cur_col_10); - - // Now see if we need to mix RGB and True White - // Valid only for LST_RGBW, LST_RGBCW, rgbwwTable[4] is zero, and white is zero (see doc) - if ((LST_RGBW <= Light.subtype) && (Settings.flag4.white_blend_mode) && (0 == cur_col_10[3]+cur_col_10[4])) { - uint32_t min_rgb_10 = min3(cur_col_10[0], cur_col_10[1], cur_col_10[2]); - for (uint32_t i=0; i<3; i++) { - // substract white and adjust according to rgbwwTable - uint32_t adjust10 = change8to10(Settings.rgbwwTable[i]); - cur_col_10[i] = changeUIntScale(cur_col_10[i] - min_rgb_10, 0, 1023, 0, adjust10); - } - - // compute the adjusted white levels for 10 and 8 bits - uint32_t adjust_w_10 = changeUIntScale(Settings.rgbwwTable[3], 0, 255, 0, 1023); - uint32_t white_10 = changeUIntScale(min_rgb_10, 0, 1023, 0, adjust_w_10); // set white power down corrected with rgbwwTable[3] - if (LST_RGBW == Light.subtype) { - // we simply set the white channel - cur_col_10[3] = white_10; - } else { // LST_RGBCW - // we distribute white between cold and warm according to CT value - uint32_t ct = light_state.getCT10bits(); - cur_col_10[4] = changeUIntScale(ct, 0, 1023, 0, white_10); - cur_col_10[3] = white_10 - cur_col_10[4]; - } - rgbwwtable_applied = true; - } else if ((Light.virtual_ct) && (0 == cur_col_10[0]+cur_col_10[1]+cur_col_10[2])) { - // virtual_ct is on and we don't have any RGB set - uint16_t sw_white = Settings.flag4.virtual_ct_cw ? cur_col_10[4] : cur_col_10[3]; // white power for virtual RGB - uint16_t hw_white = Settings.flag4.virtual_ct_cw ? cur_col_10[3] : cur_col_10[4]; // white for hardware LED - uint32_t adjust_sw = change8to10(Settings.flag4.virtual_ct_cw ? Settings.rgbwwTable[4] : Settings.rgbwwTable[3]); - uint32_t adjust_hw = change8to10(Settings.flag4.virtual_ct_cw ? Settings.rgbwwTable[3] : Settings.rgbwwTable[4]); - // set the target channels. Note: Gamma correction was arleady applied - cur_col_10[3] = changeUIntScale(hw_white, 0, 1023, 0, adjust_hw); - cur_col_10[4] = 0; // we don't actually have a 5the channel - sw_white = changeUIntScale(sw_white, 0, 1023, 0, adjust_sw); // pre-adjust virtual channel - for (uint32_t i=0; i<3; i++) { - uint32_t adjust = change8to10(Settings.rgbwwTable[i]); - cur_col_10[i] = changeUIntScale(sw_white, 0, 1023, 0, adjust); - } - rgbwwtable_applied = true; - } + // AddLog_P(LOG_LEVEL_INFO, PSTR(">>> calcGammaBulbs In %03X,%03X,%03X,%03X,%03X"), cur_col_10[0], cur_col_10[1], cur_col_10[2], cur_col_10[3], cur_col_10[4]); + rgbwwtable_applied_white = calcGammaBulbs(cur_col_10); // true means that one PWM channel is used for CT + // AddLog_P(LOG_LEVEL_INFO, PSTR(">>> calcGammaBulbs Out %03X,%03X,%03X,%03X,%03X"), cur_col_10[0], cur_col_10[1], cur_col_10[2], cur_col_10[3], cur_col_10[4]); } // Apply RGBWWTable only if not Settings.flag4.white_blend_mode - if (!rgbwwtable_applied) { - for (uint32_t i = 0; i 1023) ? 1023 : white_bri10; // max 1023 - -#ifdef ESP8266 - if ((PHILIPS == TasmotaGlobal.module_type) || (Settings.flag4.pwm_ct_mode)) { // channel 1 is the color tone, mapped to cold channel (0..255) - // Xiaomi Philips bulbs follow a different scheme: - cur_col_10[cw1] = light_state.getCT10bits(); - // channel 0=intensity, channel1=temperature - if (Settings.light_correction) { // gamma correction - cur_col_10[cw0] = ledGamma10_10(white_bri10_1023); // 10 bits gamma correction - } else { - cur_col_10[cw0] = white_bri10_1023; // no gamma, extend to 10 bits - } - } else -#endif // ESP8266 - if (Settings.light_correction) { - // if sum of both channels is > 255, then channels are probably uncorrelated - if (white_bri10 <= 1031) { // take a margin of 8 above 1023 to account for rounding errors - // we calculate the gamma corrected sum of CW + WW - uint16_t white_bri_gamma10 = ledGamma10_10(white_bri10_1023); - // then we split the total energy among the cold and warm leds - cur_col_10[cw0] = changeUIntScale(cur_col_10[cw0], 0, white_bri10_1023, 0, white_bri_gamma10); - cur_col_10[cw1] = changeUIntScale(cur_col_10[cw1], 0, white_bri10_1023, 0, white_bri_gamma10); - } else { - cur_col_10[cw0] = ledGamma10_10(cur_col_10[cw0]); - cur_col_10[cw1] = ledGamma10_10(cur_col_10[cw1]); - } - } - } +// +// Compute the Gamma correction for CW/WW +// Can be used for 2-channels (channels 0,1) or 5 channels (channels 3,4) +// +// It is implicitly called by calcGammaBulb5Channels() +// +// In: +// - 2 channels CW/WW in 10 bits format (0..1023) +// Out: +// - 2 channels CW/WW in 10 bits format, with Gamma corretion (if enabled), replaced in place +// - white_bri10: global brightness of white channel, split over CW/WW (basically the sum of CW+WW, but it's easier to compute on this basis) +// - white_free_cw: signals that CW/WW are free mode, and not linked via CT. This is used when channels are manually set on a channel per channel basis. CT is ignored +// +void calcGammaBulbCW(uint16_t cw10[2], uint16_t *white_bri10_out, bool *white_free_cw_out) { + uint16_t white_bri10 = cw10[0] + cw10[1]; // cumulated brightness + bool white_free_cw = (white_bri10 > 1031); // take a margin of 8 above 1023 to account for rounding errors + white_bri10 = (white_bri10 > 1023) ? 1023 : white_bri10; // max 1023 if (Settings.light_correction) { - // then apply gamma correction to RGB channels - if (LST_RGB <= Light.subtype) { - for (uint32_t i = 0; i < 3; i++) { - cur_col_10[i] = ledGamma10_10(cur_col_10[i]); - } - } - // If RGBW or Single channel, also adjust White channel - if ((LST_SINGLE == Light.subtype) || (LST_RGBW == Light.subtype)) { - cur_col_10[Light.subtype - 1] = ledGamma10_10(cur_col_10[Light.subtype - 1]); + if (white_free_cw) { + cw10[0] = ledGamma10_10(cw10[0]); + cw10[1] = ledGamma10_10(cw10[1]); + } else { + uint16_t white_bri10_gamma = ledGamma10_10(white_bri10); // gamma corrected white + // now distributed among both channels + cw10[0] = changeUIntScale(cw10[0], 0, white_bri10, 0, white_bri10_gamma); + cw10[1] = changeUIntScale(cw10[1], 0, white_bri10, 0, white_bri10_gamma); + // now use white_bri10_gamma as a reference + white_bri10 = white_bri10_gamma; } } + if (white_bri10_out != nullptr) { *white_bri10_out = white_bri10; } + if (white_free_cw_out != nullptr) { *white_free_cw_out = white_free_cw; } +} + +// +// Calculate the gamma correction for all 5 channels RGBCW +// Computation is valid for 1,3,4,5 channels +// 2-channels bulbs must be handled separately +// +// In: +// - 5 channels RGBCW in 10 bits format (0..1023) +// Out: +// - 5 channels RGBCW in 10 bits format, with Gamma corretion (if enabled), replaced in place +// - white_bri10: global brightness of white channel, split over CW/WW (basically the sum of CW+WW, but it's easier to compute on this basis) +// - white_free_cw: signals that CW/WW are free mode, and not linked via CT. This is used when channels are manually set on a channel per channel basis. CT is ignored +// +void calcGammaBulb5Channels(uint16_t col10[LST_MAX], uint16_t *white_bri10_out, bool *white_free_cw) { + for (uint32_t i = 0; i < 3; i++) { + if (Settings.light_correction) { + col10[i] = ledGamma10_10(col10[i]); + } + } + calcGammaBulbCW(&col10[3], white_bri10_out, white_free_cw); +} + +// sale but converts from 8 bits to 10 bits first +void calcGammaBulb5Channels_8(uint8_t in8[LST_MAX], uint16_t col10[LST_MAX]) { + for (uint32_t i = 0; i < LST_MAX; i++) { + col10[i] = change8to10(in8[i]); + } + calcGammaBulb5Channels(col10, nullptr, nullptr); +} + +bool calcGammaBulbs(uint16_t cur_col_10[5]) { + bool rgbwwtable_applied_white = false; + bool pwm_ct = false; + bool white_free_cw = false; // true if White channels are uncorrelated. Happens when CW+WW>255, i.e. manually setting white channels to exceed to total power of a single channel (may harm the power supply) + // Various values needed for accurate White calculation + // CT value streteched to 0..1023 (from within CT range, so not necessarily from 153 to 500). 0=Cold, 1023=Warm + uint16_t ct = light_state.getCT(); + uint16_t ct_10 = changeUIntScale(ct, Light.vct_ct[0], Light.vct_ct[CT_PIVOTS-1], 0, 1023); + + uint16_t white_bri10 = 0; // White total brightness normalized to 0..1023 + // uint32_t cw1 = Light.subtype - 1; // address for the ColorTone PWM + uint32_t cw0 = Light.subtype - 2; // address for the White Brightness PWM + + // calc basic gamma correction for all types + if ((LST_SINGLE == Light.subtype) || (LST_RGB <= Light.subtype)) { + calcGammaBulb5Channels(cur_col_10, &white_bri10, &white_free_cw); + } else if (LST_COLDWARM == Light.subtype) { + calcGammaBulbCW(cur_col_10, &white_bri10, &white_free_cw); + } + + // Now we know ct_10 and white_bri10 (gamma corrected if needed) + +#ifdef ESP8266 + if ((LST_COLDWARM == Light.subtype) || (LST_RGBCW == Light.subtype)) { + if ((PHILIPS == TasmotaGlobal.module_type) || (Settings.flag4.pwm_ct_mode)) { // channel 1 is the color tone, mapped to cold channel (0..255) + pwm_ct = true; + // Xiaomi Philips bulbs follow a different scheme: + // channel 0=intensity, channel1=temperature + cur_col_10[cw0] = white_bri10; + cur_col_10[cw0+1] = ct_10; + return false; // avoid any interference + } + } +#endif // ESP8266 + + // Now see if we need to mix RGB and White + // Valid only for LST_RGBW, LST_RGBCW, SetOption105 1, and white is zero (see doc) + if ((LST_RGBW <= Light.subtype) && (Settings.flag4.white_blend_mode) && (0 == cur_col_10[3]+cur_col_10[4])) { + uint32_t min_rgb_10 = min3(cur_col_10[0], cur_col_10[1], cur_col_10[2]); + cur_col_10[0] -= min_rgb_10; + cur_col_10[1] -= min_rgb_10; + cur_col_10[2] -= min_rgb_10; + + // Add to white level + uint32_t adjust_w_10 = change8to10(Settings.rgbwwTable[3]); // take the correction factor, bought back to 10 bits + white_bri10 += changeUIntScale(min_rgb_10, 0, 1023, 0, adjust_w_10); // set white power down corrected with rgbwwTable[3] + white_bri10 = (white_bri10 > 1023) ? 1023 : white_bri10; // max 1023 + rgbwwtable_applied_white = true; + } + +#ifdef USE_LIGHT_VIRTUAL_CT + // compute virtual CT, which is suppsed to be compatible with white_blend_mode + if (Light.virtual_ct && (!white_free_cw) && (LST_RGBW <= Light.subtype)) { // any light with a white channel + vct_pivot_t *pivot = &Light.vct_color[0]; + uint16_t *from_ct = &Light.vct_ct[0]; + + for (uint32_t i = 1; i < CT_PIVOTS-1; i++) { + if (ct > Light.vct_ct[i]) { // if above mid-point, take range [1]..[2] instead of [0]..[1] + pivot++; + from_ct++; + } + } + uint16_t from10[LST_MAX]; + uint16_t to10[LST_MAX]; + calcGammaBulb5Channels_8(*pivot, from10); + calcGammaBulb5Channels_8(*(pivot+1), to10); + + vct_pivot_t *pivot1 = pivot + 1; + // AddLog_P(LOG_LEVEL_INFO, PSTR("+++ from_ct %d, to_ct %d [%03X,%03X,%03X,%03X,%03X] - [%03X,%03X,%03X,%03X,%03X]"), + // *from_ct, *(from_ct+1), (*pivot)[0], (*pivot)[1], (*pivot)[2], (*pivot)[3], (*pivot)[4], + // (*pivot1)[0], (*pivot1)[1], (*pivot1)[2], (*pivot1)[3], (*pivot1)[4]); + // AddLog_P(LOG_LEVEL_INFO, PSTR("+++ from10 [%03X,%03X,%03X,%03X,%03X] - to 10 [%03X,%03X,%03X,%03X,%03X]"), + // from10[0],from10[0],from10[0],from10[0],from10[4], + // to10[0],to10[0],to10[0],to10[0],to10[4]); + + // set both CW/WW to zero since their previous value don't count anymore + cur_col_10[3] = 0; + cur_col_10[4] = 0; + + // Add the interpolated point to each component + for (uint32_t i = 0; i < LST_MAX; i++) { + cur_col_10[i] += changeUIntScale(changeUIntScale(ct, *from_ct, *(from_ct+1), from10[i], to10[i]), 0, 1023, 0, white_bri10); + if (cur_col_10[i] > 1023) { cur_col_10[i] = 1023; } + } + } else +#endif // USE_LIGHT_VIRTUAL_CT + // compute the actual levels for CW/WW + // We know ct_10 and white_bri_10 (which may be Gamma corrected) + // cur_col_10[cw0] and cur_col_10[cw1] were unmodified up to now + if (LST_RGBW == Light.subtype) { + cur_col_10[3] = white_bri10; // simple case, we set the White level to the required brightness + } else if ((LST_COLDWARM == Light.subtype) || (LST_RGBCW == Light.subtype)) { + // if sum of both channels is > 255, then channels are probably uncorrelated + if (!white_free_cw) { + // then we split the total energy among the cold and warm leds + cur_col_10[cw0+1] = changeUIntScale(ct_10, 0, 1023, 0, white_bri10); + cur_col_10[cw0] = white_bri10 - cur_col_10[cw0+1]; + } + } + return rgbwwtable_applied_white; } #ifdef USE_DEVICE_GROUPS @@ -3045,6 +3169,83 @@ void CmndWakeupDuration(void) ResponseCmndNumber(Settings.light_wakeup); } +void CmndCTRange(void) +{ + // Format is "CTRange ctmin,ctmax" + // Ex: + // CTRange 153,500 + // CTRange + // CTRange 200,350 + char *p; + strtok_r(XdrvMailbox.data, ",", &p); + if (p != nullptr) { + int32_t ct_min = strtol(XdrvMailbox.data, nullptr, 0); + int32_t ct_max = strtol(p, nullptr, 0); + if ( (ct_min >= CT_MIN) && (ct_min <= CT_MAX) && + (ct_max >= CT_MIN) && (ct_max <= CT_MAX) && + (ct_min <= ct_max) + ) { + setCTRange(ct_min, ct_max); + } else { + return; // error + } + } + Response_P(PSTR("{\"%s\":\"%d,%d\"}"), XdrvMailbox.command, Light.vct_ct[0], Light.vct_ct[CT_PIVOTS-1]); +} + +#ifdef USE_LIGHT_VIRTUAL_CT +void CmndVirtualCT(void) +{ + if (!Settings.flag4.virtual_ct) { + ResponseCmndChar_P(PSTR("You need to enable `SetOption106 1`")); + return; + } + if (XdrvMailbox.data[0] == ('{')) { + // parse JSON + JsonParser parser(XdrvMailbox.data); + JsonParserObject root = parser.getRootObject(); + if (!root) { return; } + + uint32_t idx = 0; + for (auto key : root) { + if (idx >= CT_PIVOTS) { ResponseCmndChar_P(PSTR("Too many points")); return; } + + int32_t ct_val = strtol(key.getStr(), nullptr, 0); + if ((ct_val < CT_MIN) || (ct_val > CT_MAX)) { ResponseCmndChar_P(PSTR("CT out of range")); return; } + char * color = (char*) key.getValue().getStr(); + // call color parser + Light.vct_ct[idx] = ct_val; + if (LightColorEntry(color, strlen(color))) { + memcpy(&Light.vct_color[idx], Light.entry_color, sizeof(Light.vct_color[idx])); + } + idx++; + } + for (uint32_t i = idx-1; i < CT_PIVOTS-1; i++) { + Light.vct_ct[i+1] = Light.vct_ct[i]; + memcpy(&Light.vct_color[i+1], &Light.vct_color[i], sizeof(Light.vct_color[0])); + } + } + checkVirtualCT(); + + Response_P(PSTR("{\"%s\":{"), XdrvMailbox.command); + uint32_t pivot_len = CT_PIVOTS; + vct_pivot_t * pivot = &Light.vct_color[0]; + if (Light.vct_ct[1] >= Light.vct_ct[2]) { pivot_len = 2; } // only 2 points are valid + + bool end = false; + for (uint32_t i = 0; (i < CT_PIVOTS) && !end; i++) { + if ((i >= CT_PIVOTS-1) || (Light.vct_ct[i] >= Light.vct_ct[i+1])) { + end = true; + } + ResponseAppend_P(PSTR("\"%d\":\"%02X%02X%02X%02X%02X\"%c"), Light.vct_ct[i], + (*pivot)[0], (*pivot)[1], (*pivot)[2], (*pivot)[3], (*pivot)[4], + end ? '}' : ','); + pivot++; + } + ResponseJsonEnd(); +} +#endif // USE_LIGHT_VIRTUAL_CT + #ifdef USE_LIGHT_PALETTE void CmndPalette(void) {