diff --git a/CHANGELOG.md b/CHANGELOG.md index 47eefda92..f8b400112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ All notable changes to this project will be documented in this file. - HASPmota `cpicker` and `msgbox` (#22244) - Support for DALI on ESP8266 - Command ``DaliWeb 1`` to enable light control for DALI broadcast address -- Berry Serial `config` to change parity on-the-fly for RS-485 +- Command ``DaliSend
|,`` to send command (address+256 is repeat) on DALI bus +- Command ``DaliQuery
|,`` to send command (address+256 is repeat) on DALI bus and wait up to DALI_TIMEOUT ms for response +- Berry Serial `config` to change parity on-the-fly for RS-485 (#22285) ### Breaking Changed diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 10e2f87c2..0863a74ae 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -118,6 +118,8 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm ### Added - Command ``SetOption69 1`` to enable Serial Bridge inverted Receive [#22000](https://github.com/arendst/Tasmota/issues/22000) - Command ``DaliWeb 1`` to enable light control for DALI broadcast address +- Command ``DaliSend
|,`` to send command (address+256 is repeat) on DALI bus +- Command ``DaliQuery
|,`` to send command (address+256 is repeat) on DALI bus and wait up to DALI_TIMEOUT ms for response - HX711 optional calibration precision option on command ``Sensor34 2 `` where `` is 1 to 20 [#13983](https://github.com/arendst/Tasmota/issues/13983) - ESP8266 support for one-wire M1601 temperature sensor on DS18x20 GPIO [#21376](https://github.com/arendst/Tasmota/issues/21376) - ESP8266 support for I2C CLK on GPIO16 [#22199](https://github.com/arendst/Tasmota/issues/22199) @@ -142,6 +144,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm - Berry Zigbee improvements to prepare Matter [#22083](https://github.com/arendst/Tasmota/issues/22083) - Berry virtual Energy driver [#22134](https://github.com/arendst/Tasmota/issues/22134) - Berry improve `int64` constructor [#22172](https://github.com/arendst/Tasmota/issues/22172) +- Berry Serial `config` to change parity on-the-fly for RS-485 [#22285](https://github.com/arendst/Tasmota/issues/22285) - LVGL port `colorwheel` from LVGL 8 [#22244](https://github.com/arendst/Tasmota/issues/22244) - HASPmota `cpicker` and `msgbox` [#22244](https://github.com/arendst/Tasmota/issues/22244) - Matter support for Zigbee Temperature, Humidity and Pressure sensors [#22084](https://github.com/arendst/Tasmota/issues/22084) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_75_dali.ino b/tasmota/tasmota_xdrv_driver/xdrv_75_dali.ino index f59478848..3ede93568 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_75_dali.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_75_dali.ino @@ -19,9 +19,13 @@ -------------------------------------------------------------------------------------------- Version yyyymmdd Action Description -------------------------------------------------------------------------------------------- + 0.1.0.5 20241014 update - Add command `DaliSend [repeat]
,` + - Add command `DaliQuery [repeat]
,` + - Send frame twice (repeat) for DALI defined commands + - Add support for receiving backward frame 0.1.0.4 20241013 update - Fix intermittent bad send timing 0.1.0.3 20241010 update - Change DaliDimmer range from 0..254 to 0..100 - - Add command DaliWeb 0|1 to enable persistent Web light controls + - Add command `DaliWeb 0|1` to enable persistent Web light controls 0.1.0.2 20241008 update - Better receive error detection 0.1.0.1 20241007 update - To stablizie communication send DALI datagram twice like Busch-Jaeger does - Change DaliPower 0..2 to act like Tasmota Power (Off, On, Toggle) @@ -42,7 +46,7 @@ #define XDRV_75 75 #ifndef DALI_IN_INVERT -#define DALI_IN_INVERT 0 // DALI RX inverted ? +#define DALI_IN_INVERT 0 // DALI RX inverted #endif #ifndef DALI_OUT_INVERT #define DALI_OUT_INVERT 0 // DALI TX inverted @@ -50,6 +54,9 @@ #ifndef DALI_INIT_STATE #define DALI_INIT_STATE 50 // DALI init dimmer state 50/254 #endif +#ifndef DALI_TIMEOUT +#define DALI_TIMEOUT 50 // DALI backward frame receive timeout (ms) +#endif //#define DALI_DEBUG #ifndef DALI_DEBUG_PIN @@ -66,14 +73,14 @@ const char kDALICommands[] PROGMEM = D_PRFX_DALI "|" // Prefix #ifdef USE_LIGHT "|Web" #endif // USE_LIGHT - "|" D_CMND_DIMMER ; + "|" D_CMND_DIMMER "|Send|Query" ; void (* const DALICommand[])(void) PROGMEM = { &CmndDali, &CmndDaliPower, #ifdef USE_LIGHT &CmndDaliWeb, #endif // USE_LIGHT - &CmndDaliDimmer }; + &CmndDaliDimmer, &CmndDaliSend, &CmndDaliQuery }; struct DALI { uint32_t bit_time; @@ -85,6 +92,7 @@ struct DALI { uint8_t dimmer; bool power; bool available; + bool response; } *Dali = nullptr; /*********************************************************************************************\ @@ -92,6 +100,7 @@ struct DALI { \*********************************************************************************************/ void DaliEnableRxInterrupt(void) { + Dali->available = false; attachInterrupt(Dali->pin_rx, DaliReceiveData, FALLING); } @@ -103,30 +112,65 @@ void DaliDisableRxInterrupt(void) { void IRAM_ATTR DaliReceiveData(void); // Fix ESP8266 ISR not in IRAM! exception void DaliReceiveData(void) { + /* + Forward frame (1 Start bit + 16 data bits) * 2 bits/bit (manchester encoding) + 2 * 2 Stop bits = 38 bits + DALI data 0xFE64 1 1 1 1 1 1 1 0 0 1 1 0 0 1 0 0 Forward frame + Start and Stop bits 1 1 1 + Manchester data 0101010101010101101001011010011010 + Stop bits 1111 + + Backward frame (1 Start bit + 8 data bits) * 2 bits/bit (manchester encoding) + 2 * 2 Stop bits = 22 bits + DALI data 0x64 0 1 1 0 0 1 0 0 Backward frame + Start and Stop bits 1 1 1 + Manchester data 011001011010011010 + Stop bits 1111 + + Bit number 01234567890123456789012345678901234567 + 1 2 3 + */ if (Dali->available) { return; } // Skip if last input is not yet handled uint32_t wait = ESP.getCycleCount() + (Dali->bit_time / 2); int bit_state = 0; bool dali_read; uint32_t received_dali_data = 0; - uint32_t bit_pos = 15; - for (uint32_t i = 0; i < 36; i++) { // (1 Start bit, 16 data bits, 1 stop bit) * 2 bits/bit (manchester encoding) + uint32_t bit_number = 0; + while (bit_number < 38) { while (ESP.getCycleCount() < wait); wait += Dali->bit_time; // Auto roll-over - if (abs(bit_state) <= 2) { // Manchester encoding max 2 consecutive equal bits - dali_read = digitalRead(Dali->pin_rx); + dali_read = digitalRead(Dali->pin_rx); #ifdef DALI_DEBUG - digitalWrite(DALI_DEBUG_PIN, i&1); // Add LogicAnalyzer poll indication + digitalWrite(DALI_DEBUG_PIN, bit_number&1); // Add LogicAnalyzer poll indication #endif // DALI_DEBUG + if (bit_number < 34) { // 34 manchester encoded bits bit_state += (dali_read) ? 1 : -1; - if ((i >= 2) && (i <= 34)) { // 32 manchester encoded data bits - if (i &1) { // 16 data bits - received_dali_data |= ((DALI_IN_INVERT) ? !dali_read : dali_read << bit_pos--); + if (0 == bit_state) { // Manchester encoding total 2 bits is always 0 + if (bit_number > 2) { // Skip start bit + received_dali_data <<= 1; + received_dali_data |= (DALI_IN_INVERT) ? !dali_read : dali_read; } } + else if ((2 == bit_state) && + (bit_number == 19)) { // Possible backward frame detected - Chk stop bits + bit_state = 0; + bit_number = 35; + } + else if (abs(bit_state) > 1) { // Invalid manchester data + break; + } + } else { // 4 high Stop bits + if (bit_state != 0) { // Invalid manchester data + break; + } + else if (dali_read != 1) { // Invalid level of stop bit + bit_state = 1; + break; + } } + bit_number++; } - if (abs(bit_state) <= 2) { // Valid Manchester encoding including start and stop bits - if (Dali->received_dali_data != received_dali_data) { // Skip duplicates + if (0 == bit_state) { // Valid Manchester encoding including start and stop bits + if (Dali->response || // Response from last message send + (Dali->received_dali_data != received_dali_data)) { // Skip duplicates Dali->received_dali_data = received_dali_data; Dali->available = true; // Valid data received } @@ -136,10 +180,20 @@ void DaliReceiveData(void) { /*************** S E N D * P R O C E D U R E ***************/ void DaliSendDataOnce(uint16_t send_dali_data) { + /* + DALI protocol forward frame + DALI data 0xFE64 1 1 1 1 1 1 1 0 0 1 1 0 0 1 0 0 + Start and Stop bits 1 1 1 + Manchester data 0101010101010101101001011010011010 + Stop bits 1111 + + Bit number 01234567890123456789012345678901234567 + 1 2 3 + */ bool bit_value; uint32_t bit_pos = 15; uint32_t wait = ESP.getCycleCount(); - for (uint32_t i = 0; i < 35; i++) { + for (uint32_t i = 0; i < 35; i++) { // 417 * 35 = 14.7 ms if (0 == (i &1)) { // Even bit // Start bit, Stop bit, Data bits bit_value = (0 == i) ? 1 : (34 == i) ? 0 : (bool)((send_dali_data >> bit_pos--) &1); // MSB first @@ -151,32 +205,66 @@ void DaliSendDataOnce(uint16_t send_dali_data) { wait += Dali->bit_time; // Auto roll-over while (ESP.getCycleCount() < wait); } +// delayMicroseconds(1100); // Adds to total 15.8 ms } -void DaliSendData(uint8_t firstByte, uint8_t secondByte) { - Dali->address = firstByte; - Dali->command = secondByte; - if (DALI_BROADCAST_DP == firstByte) { - Dali->power = (secondByte); // State +void DaliSendData(uint32_t adr, uint32_t cmd) { + bool repeat = (adr &0x100); // Set repeat if bit 8 is set + adr &= 0xFF; + cmd &= 0xFF; + + Dali->address = adr; + Dali->command = cmd; + if (DALI_BROADCAST_DP == adr) { + repeat = true; + Dali->power = (cmd); // State if (Dali->power) { - Dali->dimmer = secondByte; // Value + Dali->dimmer = cmd; // Value } } - uint16_t send_dali_data = firstByte << 8; - send_dali_data += secondByte & 0xff; + + if (!repeat && (adr &0x01)) { // YAAAAAA1 Commands where user didn't set repeat + if ((adr >= 0xA1) && (adr <= 0xFD)) { // Special commands + repeat = ((0xA5 == adr) || (0xA7 == adr)); + } else { + // ((cmd >=0) && (cmd <= 31)) // Arc power control commands + repeat = (((cmd >=32) && (cmd <= 143)) || // Configuration commands + ((cmd >=224) && (cmd <= 236))); // Extended configuration commands + // ((cmd >=144) && (cmd <= 223)) // Query commands + // ((cmd >=237) && (cmd <= 255)) // Extended query commands + } + } + + uint16_t send_dali_data = adr << 8 | cmd; DaliDisableRxInterrupt(); delay(3); // Settling time between forward and backward frame - DaliSendDataOnce(send_dali_data); // Takes 14.5 ms - if (DALI_BROADCAST_DP == firstByte) { + DaliSendDataOnce(send_dali_data); // Takes 14.7 ms + if (repeat) { delay(14); // As used by Busch-Jaeger and suggested by DALI protocol (> 9.17 ms) DaliSendDataOnce(send_dali_data); // Takes 14.7 ms } - delay(3); // Block response + delay(2); // Block response DaliEnableRxInterrupt(); } -void DaliPower(uint8_t val) { +int DaliSendWaitResponse(uint32_t adr, uint32_t cmd, uint32_t timeout = DALI_TIMEOUT); +int DaliSendWaitResponse(uint32_t adr, uint32_t cmd, uint32_t timeout) { + Dali->response = true; + DaliSendData(adr, cmd); + while (!Dali->available && timeout--) { // Expect backward frame within DALI_TIMEOUT ms + delay(1); + }; + int result = -1; // DALI NO or no response + if (Dali->available) { + Dali->available = false; + result = Dali->received_dali_data; + } + Dali->response = false; + return result; +} + +void DaliPower(uint32_t val) { DaliSendData(DALI_BROADCAST_DP, val); } @@ -184,7 +272,7 @@ void DaliPower(uint8_t val) { void ResponseAppendDali(void) { uint8_t dimmer = changeUIntScale(Dali->dimmer, 0, 254, 0, 100); - ResponseAppend_P(PSTR("\"" D_PRFX_DALI "\":{\"Power\":\"%s\",\"Dimmer\":%d,\"Address\":%d,\"Command\":%d}"), + ResponseAppend_P(PSTR("\"DALI\":{\"Power\":\"%s\",\"Dimmer\":%d,\"Address\":%d,\"Command\":%d}"), GetStateText(Dali->power), dimmer, Dali->address, Dali->command); } @@ -195,47 +283,49 @@ void ResponseDali(void) { } void DaliInput(void) { - if (Dali->available) { - Dali->address = Dali->received_dali_data >> 8; - Dali->command = Dali->received_dali_data; + if (!Dali->available || Dali->response) { return; } + + Dali->address = Dali->received_dali_data >> 8; + Dali->command = Dali->received_dali_data; #ifdef USE_LIGHT - if (DALI_BROADCAST_DP == Dali->address) { - uint8_t dimmer_old = changeUIntScale(Dali->dimmer, 0, 254, 0, 100); - uint8_t power_old = Dali->power; - Dali->power = (Dali->command); // State - if (Dali->power) { - Dali->dimmer = Dali->command; // Value - } - if (Settings->sbflag1.dali_web) { // DaliWeb 1 - uint8_t dimmer_new = changeUIntScale(Dali->dimmer, 0, 254, 0, 100); - if (power_old != Dali->power) { - ExecuteCommandPower(LightDevice(), Dali->power, SRC_SWITCH); - } - else if (dimmer_old != dimmer_new) { - char scmnd[20]; - snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_DIMMER " %d"), dimmer_new); - ExecuteCommand(scmnd, SRC_SWITCH); - } - } + bool show_response = true; + if (DALI_BROADCAST_DP == Dali->address) { + uint8_t dimmer_old = changeUIntScale(Dali->dimmer, 0, 254, 0, 100); + uint8_t power_old = Dali->power; + Dali->power = (Dali->command); // State + if (Dali->power) { + Dali->dimmer = Dali->command; // Value } - if (!Settings->sbflag1.dali_web) { // DaliWeb 0 - ResponseDali(); - MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_PRFX_DALI)); - } -#else - if (DALI_BROADCAST_DP == Dali->address) { - Dali->power = (Dali->command); // State - if (Dali->power) { - Dali->dimmer = Dali->command; // Value + if (Settings->sbflag1.dali_web) { // DaliWeb 1 + uint8_t dimmer_new = changeUIntScale(Dali->dimmer, 0, 254, 0, 100); + if (power_old != Dali->power) { + ExecuteCommandPower(LightDevice(), Dali->power, SRC_SWITCH); } + else if (dimmer_old != dimmer_new) { + char scmnd[20]; + snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_DIMMER " %d"), dimmer_new); + ExecuteCommand(scmnd, SRC_SWITCH); + } + show_response = false; } + } + if (show_response) { ResponseDali(); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_PRFX_DALI)); + } +#else + if (DALI_BROADCAST_DP == Dali->address) { + Dali->power = (Dali->command); // State + if (Dali->power) { + Dali->dimmer = Dali->command; // Value + } + } + ResponseDali(); + MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_PRFX_DALI)); #endif // USE_LIGHT - Dali->available = false; - } + Dali->available = false; } bool DaliInit(void) { @@ -258,7 +348,8 @@ bool DaliInit(void) { #endif // DALI_DEBUG Dali->dimmer = DALI_INIT_STATE; - Dali->bit_time = ESP.getCpuFreqMHz() * 1000000 / 2400; // Manchester twice 1200 bps = 2400 bps = 417 ms + // Manchester twice 1200 bps = 2400 bps = 417 (protocol 416.76 +/- 10%) us + Dali->bit_time = ESP.getCpuFreqMHz() * 1000000 / 2400; DaliEnableRxInterrupt(); @@ -380,14 +471,14 @@ bool DaliJsonParse(void) { int DALIindex = 0; int ADRindex = 0; int8_t DALIdim = -1; - uint8_t DALIaddr = DALI_BROADCAST_DP; + uint32_t DALIaddr = DALI_BROADCAST_DP; JsonParserToken val = root[PSTR("cmd")]; if (val) { - uint8_t cmd = val.getUInt(); + uint32_t cmd = val.getUInt(); val = root[PSTR("addr")]; if (val) { - uint8_t addr = val.getUInt(); + uint32_t addr = val.getUInt(); AddLog(LOG_LEVEL_DEBUG, PSTR("DLI: cmd = %d, addr = %d"), cmd, addr); DaliSendData(addr, cmd); return true; @@ -397,7 +488,7 @@ bool DaliJsonParse(void) { } val = root[PSTR("addr")]; if (val) { - uint8_t addr = val.getUInt(); + uint32_t addr = val.getUInt(); if ((addr >= 0) && (addr < 64)) { DALIaddr = addr << 1; } @@ -451,6 +542,31 @@ void CmndDaliDimmer(void) { ResponseDali(); } +void CmndDaliSend(void) { + // Send command + // Setting bit 8 will repeat command twice + // DaliSend 0x1a5,255 - DALI Initialise (send twice) + uint32_t values[2] = { 0 }; + uint32_t params = ParseParameters(2, values); + if (2 == params) { + DaliSendData(values[0] &0x1FF, values[1] &0xFF); + ResponseCmndDone(); + } +} + +void CmndDaliQuery(void) { + // Send command and return response or -1 (no response within DALI_TIMEOUT) + // Setting bit 8 will repeat command twice + // DaliQuery 0xff,0x90 - DALI Query status + // DaliQuery 0xff,144 - DALI Query status + uint32_t values[2] = { 0 }; + uint32_t params = ParseParameters(2, values); + if (2 == params) { + int result = DaliSendWaitResponse(values[0] &0x1FF, values[1] &0xFF); + ResponseCmndNumber(result); + } +} + #ifdef USE_LIGHT void CmndDaliWeb(void) { // DaliWeb 0 - Disable GUI light controls