Merge pull request #10311 from s-hadinger/virtual_ct_phase_2

Commands `CTRange` and `VirtualCT`
This commit is contained in:
Theo Arends 2020-12-30 13:49:20 +01:00 committed by GitHub
commit 70c93dadea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 351 additions and 132 deletions

View File

@ -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``

View File

@ -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"

View File

@ -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 -------------------------------

View File

@ -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)

View File

@ -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

View File

@ -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,68 @@ 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 getCTRange(uint16_t * min_ct, uint16_t * max_ct) {
if (min_ct != nullptr) { *min_ct = Light.vct_ct[0]; }
if (max_ct != nullptr) { *max_ct = Light.vct_ct[CT_PIVOTS-1]; }
}
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 +1350,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 +1423,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 +1915,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 +2021,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 +2029,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<Light.subtype; i++) {
uint32_t adjust = change8to10(Settings.rgbwwTable[i]);
cur_col_10[i] = changeUIntScale(cur_col_10[i], 0, 1023, 0, adjust);
}
for (uint32_t i = 0; i < (rgbwwtable_applied_white ? 3 : Light.subtype); i++) {
uint32_t adjust = change8to10(Settings.rgbwwTable[i]);
cur_col_10[i] = changeUIntScale(cur_col_10[i], 0, 1023, 0, adjust);
}
// final adjusments for PMW ranges, post-gamma correction
@ -2272,56 +2287,170 @@ void calcGammaMultiChannels(uint16_t cur_col_10[5]) {
}
}
void calcGammaBulbs(uint16_t cur_col_10[5]) {
// Apply gamma correction for 8 and 10 bits resolutions, if needed
// First apply combined correction to the overall white power
if ((LST_COLDWARM == Light.subtype) || (LST_RGBCW == Light.subtype)) {
// channels for white are always the last two channels
uint32_t cw1 = Light.subtype - 1; // address for the ColorTone PWM
uint32_t cw0 = Light.subtype - 2; // address for the White Brightness PWM
uint16_t white_bri10 = cur_col_10[cw0] + cur_col_10[cw1]; // cumulated brightness
uint16_t white_bri10_1023 = (white_bri10 > 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 +3174,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)
{

View File

@ -517,7 +517,7 @@ bool TuyaSetChannels(void)
Tuya.Snapshot[0] = changeUIntScale(Light.current_color[0], 0, 255, 0, 100);
Tuya.Snapshot[1] = changeUIntScale(Light.current_color[1], 0, 255, 0, 100);
} else { // CT Light or RGBWC
light_state.getCTRange(&Tuya.CTMin, &Tuya.CTMax); // SetOption82 - Reduce the CT range from 153..500 to 200..380 to accomodate with Alexa range
getCTRange(&Tuya.CTMin, &Tuya.CTMax); // SetOption82 - Reduce the CT range from 153..500 to 200..380 to accomodate with Alexa range
Tuya.Snapshot[0] = light_state.getDimmer();
Tuya.Snapshot[1] = light_state.getCT();
}