/* xdrv_75_dali.ino - DALI support for Tasmota Copyright (C) 2022 Andrei Kazmirtsuk aka eeak 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 . -------------------------------------------------------------------------------------------- Version yyyymmdd Action Description -------------------------------------------------------------------------------------------- 0.1.0.6 20241014 update - Fix received light command loop - Add send collision detection 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 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) - Keep last Dimmer value as default power on 0.1.0.0 20241006 rewrite - Add support for ESP8266 - Fix decoding of received DALI data - Refactor command `DaliPower 0..254` controlling Broadcast devices - Add command `DaliDimmer 0..254` controlling Broadcast devices 0.0.0.1 20221027 publish - Initial version */ #ifdef USE_DALI /*********************************************************************************************\ * DALI support for Tasmota \*********************************************************************************************/ #define XDRV_75 75 #ifndef DALI_IN_INVERT #define DALI_IN_INVERT 0 // DALI RX inverted #endif #ifndef DALI_OUT_INVERT #define DALI_OUT_INVERT 0 // DALI TX inverted #endif #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 #define DALI_DEBUG_PIN 4 #endif #define DALI_BROADCAST_DP 0b11111110 // 0xFE = 254 #define DALI_TOPIC "DALI" #define D_PRFX_DALI "Dali" const char kDALICommands[] PROGMEM = D_PRFX_DALI "|" // Prefix "|" D_CMND_POWER #ifdef USE_LIGHT "|Web" #endif // USE_LIGHT "|" D_CMND_DIMMER "|Send|Query" ; void (* const DALICommand[])(void) PROGMEM = { &CmndDali, &CmndDaliPower, #ifdef USE_LIGHT &CmndDaliWeb, #endif // USE_LIGHT &CmndDaliDimmer, &CmndDaliSend, &CmndDaliQuery }; struct DALI { uint32_t bit_time; uint16_t received_dali_data; // Data received from DALI bus uint8_t pin_rx; uint8_t pin_tx; uint8_t address; uint8_t command; uint8_t dimmer; bool power; bool available; bool response; bool light_sync; } *Dali = nullptr; /*********************************************************************************************\ * DALI low level \*********************************************************************************************/ void DaliEnableRxInterrupt(void) { Dali->available = false; attachInterrupt(Dali->pin_rx, DaliReceiveData, FALLING); } void DaliDisableRxInterrupt(void) { detachInterrupt(Dali->pin_rx); } /*************** R E C E I V E * P R O C E D U R E *********/ 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_number = 0; while (bit_number < 38) { while (ESP.getCycleCount() < wait); wait += Dali->bit_time; // Auto roll-over dali_read = digitalRead(Dali->pin_rx); if (DALI_IN_INVERT) { dali_read != dali_read; } #ifdef DALI_DEBUG 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 (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_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 (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 } } } /*************** 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; bool pin_value; bool dali_read; bool collision = false; uint32_t bit_pos = 15; uint32_t wait = ESP.getCycleCount(); uint32_t bit_number = 0; while (bit_number < 35) { // 417 * 35 = 14.7 ms if (!collision) { if (0 == (bit_number &1)) { // Even bit // Start bit, Stop bit, Data bits bit_value = (0 == bit_number) ? 1 : (34 == bit_number) ? 0 : (bool)((send_dali_data >> bit_pos--) &1); // MSB first } else { // Odd bit bit_value = !bit_value; // Complement bit } pin_value = bit_value ? LOW : HIGH; // Invert bit } else { if (34 == bit_number) { pin_value = HIGH; // Set to idle } } digitalWrite(Dali->pin_tx, (pin_value == DALI_OUT_INVERT) ? LOW : HIGH); wait += Dali->bit_time; // Auto roll-over while (ESP.getCycleCount() < wait); if (!collision) { dali_read = digitalRead(Dali->pin_rx); if (DALI_IN_INVERT) { dali_read != dali_read; } if ((HIGH == pin_value) && (LOW == dali_read)) { // Collision if write is 1 and bus is 0 collision = true; pin_value = LOW; bit_number = 29; // Keep bus low for 4 bits AddLog(LOG_LEVEL_DEBUG, PSTR("DLI: Send collision")); } } bit_number++; } // delayMicroseconds(1100); // Adds to total 15.8 ms } 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 = cmd; // Value } } 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.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(2); // Block response DaliEnableRxInterrupt(); } 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); } /***********************************************************/ void ResponseAppendDali(void) { uint8_t dimmer = changeUIntScale(Dali->dimmer, 0, 254, 0, 100); ResponseAppend_P(PSTR("\"DALI\":{\"Power\":\"%s\",\"Dimmer\":%d,\"Address\":%d,\"Command\":%d}"), GetStateText(Dali->power), dimmer, Dali->address, Dali->command); } void ResponseDali(void) { Response_P(PSTR("{")); ResponseAppendDali(); ResponseJsonEnd(); } void DaliInput(void) { if (!Dali->available || Dali->response) { return; } Dali->address = Dali->received_dali_data >> 8; Dali->command = Dali->received_dali_data; #ifdef USE_LIGHT 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 1 uint8_t dimmer_new = changeUIntScale(Dali->dimmer, 0, 254, 0, 100); if (power_old != Dali->power) { Dali->light_sync = true; // Block local loop 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); Dali->light_sync = true; // Block local loop 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; } #ifdef USE_LIGHT bool DaliSetChannels(void) { if (Settings->sbflag1.dali_web) { // DaliWeb 1 if (Dali->light_sync) { // Block local loop Dali->light_sync = false; } else { uint8_t value = ((uint8_t*)XdrvMailbox.data)[0]; if (255 == value) { value = 254; } // Max Dali value DaliPower(value); } } return true; } #endif // USE_LIGHT bool DaliInit(void) { if (!PinUsed(GPIO_DALI_TX) || !PinUsed(GPIO_DALI_RX)) { return false; } Dali = (DALI*)calloc(sizeof(DALI), 1); if (!Dali) { return false; } Dali->pin_rx = Pin(GPIO_DALI_RX); Dali->pin_tx = Pin(GPIO_DALI_TX); AddLog(LOG_LEVEL_INFO, PSTR("DLI: GPIO%d(RX) and GPIO%d(TX)"), Dali->pin_rx, Dali->pin_tx); pinMode(Dali->pin_tx, OUTPUT); digitalWrite(Dali->pin_tx, HIGH); pinMode(Dali->pin_rx, INPUT); #ifdef DALI_DEBUG pinMode(DALI_DEBUG_PIN, OUTPUT); digitalWrite(DALI_DEBUG_PIN, HIGH); #endif // DALI_DEBUG Dali->dimmer = DALI_INIT_STATE; // Manchester twice 1200 bps = 2400 bps = 417 (protocol 416.76 +/- 10%) us Dali->bit_time = ESP.getCpuFreqMHz() * 1000000 / 2400; DaliEnableRxInterrupt(); #ifdef USE_LIGHT if (!Settings->sbflag1.dali_web) { // DaliWeb 0 return false; } UpdateDevicesPresent(1); TasmotaGlobal.light_type = LT_SERIAL1; // Single channel return true; #else return false; #endif // USE_LIGHT } /*********************************************************************************************\ * Experimental - Not functioning \*********************************************************************************************/ bool DaliMqtt(void) { /* XdrvMailbox.topic = topic; XdrvMailbox.index = strlen(topic); XdrvMailbox.data = (char*)data; XdrvMailbox.data_len = data_len; This won't work as there is currently no subscribe done */ char stopic[TOPSZ]; strncpy(stopic, XdrvMailbox.topic, TOPSZ); XdrvMailbox.topic[TOPSZ - 1] = 0; char *items[10]; char *p = stopic; int cnt = 0; do { items[cnt] = strtok(p, "/"); cnt++; p = nullptr; } while (items[cnt - 1]); cnt--; // represents the number of items AddLog(LOG_LEVEL_DEBUG, PSTR("DLI: Cnt %d, Topic '%s', Payload '%s'"), cnt, XdrvMailbox.topic, XdrvMailbox.data); if (cnt < 3) { // not for us? AddLog(LOG_LEVEL_INFO, PSTR("DLI: Cnt %d < 3"), cnt); return false; } int DALIindex = 0; int ADRindex = 0; int CMDindex = 0; uint8_t DALIaddr = DALI_BROADCAST_DP; if (strcasecmp_P(items[cnt - 3], PSTR(DALI_TOPIC)) != 0) { // dali // cmnd if (strcasecmp_P(items[cnt - 2], PSTR(DALI_TOPIC)) != 0) { // dali // device return false; // not for us } else { // cmnd/dali/percent DALIindex = cnt - 2; CMDindex = cnt - 1; } } else { // dali/percent/2 20 DALIindex = cnt - 3; CMDindex = cnt - 2; ADRindex = cnt - 1; DALIaddr = ((int)CharToFloat(items[ADRindex])) << 1; } uint8_t level; uint8_t value = (uint8_t)CharToFloat(XdrvMailbox.data); if (strcasecmp_P(items[CMDindex], PSTR("percent")) == 0) { // dali/percent/ float percent = (float)(254 * value * 0.01); level = (uint8_t)percent; } else if (strcasecmp_P(items[CMDindex], PSTR("level")) == 0) { level = value; } else { AddLog(LOG_LEVEL_INFO,PSTR("DLI: Command not recognized: %s"), items[CMDindex]); return false; // not for us } AddLog(LOG_LEVEL_INFO,PSTR("DLI: Dali value %d on address %d"), value, DALIaddr); DaliSendData(DALIaddr, level); return true; } /*********************************************************************************************\ * Commands \*********************************************************************************************/ bool DaliJsonParse(void) { // {"addr":254,"cmd":100} // {"addr":2} // {"dim":3} bool served = false; JsonParser parser((char *)XdrvMailbox.data); JsonParserObject root = parser.getRootObject(); if (root) { int DALIindex = 0; int ADRindex = 0; int8_t DALIdim = -1; uint32_t DALIaddr = DALI_BROADCAST_DP; JsonParserToken val = root[PSTR("cmd")]; if (val) { uint32_t cmd = val.getUInt(); val = root[PSTR("addr")]; if (val) { uint32_t addr = val.getUInt(); AddLog(LOG_LEVEL_DEBUG, PSTR("DLI: cmd = %d, addr = %d"), cmd, addr); DaliSendData(addr, cmd); return true; } else { return false; } } val = root[PSTR("addr")]; if (val) { uint32_t addr = val.getUInt(); if ((addr >= 0) && (addr < 64)) { DALIaddr = addr << 1; } } val = root[PSTR("dim")]; if (val) { uint8_t dim = val.getUInt(); if (dim < 255) { DALIdim = dim; } } DaliSendData(DALIaddr, DALIdim); served = true; } return served; } void CmndDali(void) { // Dali {"addr":254,"cmd":100} - Any address and/or command if (XdrvMailbox.data_len > 0) { DaliJsonParse(); } ResponseDali(); } void CmndDaliPower(void) { // DaliPower 0 - Power off // DaliPower 1 - Power on to last dimmer state // DaliPower 2 - Toggle power off or last dimmer state // DaliPower 3..254 - Equals DaliDimmer command if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 254)) { if (XdrvMailbox.payload <= 2) { if (2 == XdrvMailbox.payload) { XdrvMailbox.payload = (Dali->power) ? 0 : 1; } if (1 == XdrvMailbox.payload) { XdrvMailbox.payload = Dali->dimmer; } } DaliPower(XdrvMailbox.payload); } ResponseDali(); } void CmndDaliDimmer(void) { // DaliDimmer 0..100 - Set power off or dimmer state if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { uint8_t dimmer = changeUIntScale(XdrvMailbox.payload, 0, 100, 0, 254); DaliPower(dimmer); } 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 // DaliWeb 1 - Enable GUI light controls if (XdrvMailbox.data_len > 0) { Settings->sbflag1.dali_web = XdrvMailbox.payload &1; TasmotaGlobal.restart_flag = 2; } ResponseCmndStateText(Settings->sbflag1.dali_web); } #endif // USE_LIGHT /*********************************************************************************************\ * Presentation \*********************************************************************************************/ void DaliShow(bool json) { if (json) { ResponseAppend_P(PSTR(",")); ResponseAppendDali(); } } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv75(uint32_t function) { bool result = false; if (FUNC_MODULE_INIT == function) { result = DaliInit(); } else if (Dali) { switch (function) { case FUNC_LOOP: DaliInput(); break; case FUNC_MQTT_DATA: result = DaliMqtt(); break; #ifdef USE_LIGHT case FUNC_SET_CHANNELS: result = DaliSetChannels(); break; #endif // USE_LIGHT case FUNC_JSON_APPEND: DaliShow(true); break; #ifdef USE_WEBSERVER case FUNC_WEB_SENSOR: DaliShow(false); break; #endif // USE_WEBSERVER case FUNC_COMMAND: result = DecodeCommand(kDALICommands, DALICommand); break; case FUNC_ACTIVE: result = true; break; } } return result; } #endif // USE_DALI