/*
  xdrv_89_esp32_dali.ino - DALI support for Tasmota

  Copyright (C) 2022  Andrei Kazmirtsuk aka eeak

  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 <http://www.gnu.org/licenses/>.

  --------------------------------------------------------------------------------------------
  Version yyyymmdd  Action    Description
  --------------------------------------------------------------------------------------------
  0.0.0.1 20221027  publish - initial version
*/

#ifdef ESP32
#ifdef USE_DALI

/*********************************************************************************************\
 * DALI support for Tasmota
\*********************************************************************************************/

#define XDRV_89              89

#ifndef DALI_TIMER
    #define DALI_TIMER 0    // Default timer
#endif

#define BROADCAST_DP        0b11111110 // 0xFE
#define DALI_TOPIC          "DALI"

enum
{
    DALI_NO_ACTION,
    DALI_SENDING_DATA,
    DALI_RECEIVING_DATA,
    DALI_ERROR
};

// http and json defines
#define D_NAME_DALI "DALI"

const char S_JSON_DALI_COMMAND_NVALUE[] PROGMEM = "{\"" D_NAME_DALI "\":{\"%s\":%d}}";
const char kDALI_Commands[] PROGMEM  = D_CMND_DALI_POWER "|" D_CMND_DALI_DIMMER;

enum DALI_Commands {         // commands for Console
  CMND_DALI_PWR,
  CMND_DALI_DIM,
};

struct DALI {
    uint16_t send_dali_data;     // data to send to DALI bus
    uint16_t received_dali_data; // data received from DALI bus
    uint8_t flag;                // DALI status flag
    uint8_t bit_count;           // nr of rec/send bits
    uint16_t tick_count;         // nr of ticks of the timer
    bool former_val;             // bit value in previous tick of timer
    hw_timer_t *timer;           // hardware timer
} *Dali = nullptr;



/*********************************************************************************************\
 * DALI low level
\*********************************************************************************************/

/**
* @brief  This function handles hardware timer Handler.
* @param  None
* @retval None
*/
void IRAM_ATTR DALI_Tick_Handler(void)
{
    if (getDaliFlag() == DALI_RECEIVING_DATA)
    {
        receive_tick();
    }
    else if (getDaliFlag() == DALI_SENDING_DATA)
    {
        send_tick();
    }
}

/**
* @brief  This function enable data transfer start interrupt.
* @param  None
* @retval None
*/
void enableDaliRxInterrupt() {
    Dali->flag = DALI_NO_ACTION;
    timerAlarmDisable(Dali->timer);
    attachInterrupt(Pin(GPIO_DALI_RX), receiveDaliData, FALLING);
}

/**
* @brief  This function disable data transfer start interrupt.
* @param  None
* @retval None
*/
void disableRxInterrupt() {
    timerAlarmEnable(Dali->timer);
	detachInterrupt(Pin(GPIO_DALI_RX));
}

/**
* @brief   receiving flag status
* @param  None
* @retval uint8_t flag
*/
uint8_t getDaliFlag(void)
{
  return Dali->flag;
}

/**
* @brief   DALI data received callback
* @param  None
* @retval uint8_t flag
*/
void DataReceivedCallback() {
    AddLog(LOG_LEVEL_DEBUG, PSTR("DLI: Received: %d %d"), Dali->received_dali_data>>9, Dali->received_dali_data&0xff);
}

/*************** R E C E I V E * P R O C E D U R E S *******/

/**
* @brief  receive data from DALI bus
* @param  None
* @retval None
*/
void receiveDaliData()
{
    // null variables
    Dali->received_dali_data = 0;
    Dali->bit_count = 0;
    Dali->tick_count = 0;
    Dali->former_val = true;

    Dali->flag = DALI_RECEIVING_DATA;
    
	disableRxInterrupt();
}

/**
* @brief  Get state of DALIIN pin
* @param  None
* @retval bool status
*/
bool get_DALIIN(void)
{
    bool dali_read = digitalRead(Pin(GPIO_DALI_RX));
    return (false == DALI_IN_INVERT) ? dali_read : !dali_read;
}

/**
* @brief   receiving data from DALI bus
* @param  None
* @retval None
*
* |--------|----|---------------------------|----|
* 0        24   32                          160  176
*    wait   start      data                  stop
*/
void receive_tick(void)
{
    // four ticks per bit
    bool actual_val = get_DALIIN();
    Dali->tick_count++;
    
    // edge detected
    if(actual_val != Dali->former_val)
    {
        switch(Dali->bit_count)
        {
            case 0:
                if (Dali->tick_count > 2)
                {
                    Dali->tick_count = 0;
                    Dali->bit_count  = 1; // start bit
                }
                break;
            case 17:      // 1st stop bit
                if(Dali->tick_count > 6) { // stop bit error, no edge should exist
                    Dali->flag = DALI_ERROR;
				}
                break;
            default:      // other bits
                if(Dali->tick_count > 6)
                {
                    Dali->received_dali_data |= (actual_val << (16-Dali->bit_count));
                    Dali->bit_count++;
                    Dali->tick_count = 0;
                }
                break;
        }
    }else // voltage level stable
    {
        switch(Dali->bit_count)
        {
            case 0:
                if(Dali->tick_count==8) {	// too long start bit
                    Dali->flag = DALI_ERROR;
				}
                break;
            case 17:
                // First stop bit
                if (Dali->tick_count==8)
                {
                    if (actual_val==0) // wrong level of stop bit
                    {
                        Dali->flag = DALI_ERROR;
                    }
                    else
                    {
                        Dali->bit_count++;
                        Dali->tick_count = 0;
                    }
                }
                break;
            case 18:
                // Second stop bit
                if (Dali->tick_count==8)
                {
                    enableDaliRxInterrupt();
                    DataReceivedCallback();
					
                }
                break;
            default: // normal bits
                if(Dali->tick_count==10)
                { // too long delay before edge
                    Dali->flag = DALI_ERROR;
                }
                break;
        }
    }
    Dali->former_val = actual_val;
    if(getDaliFlag() == DALI_ERROR)
    {
		enableDaliRxInterrupt();
    }
}


/*************** S E N D * P R O C E D U R E S *************/

/**
* @brief   Set value to the DALIOUT pin
* @param  bool
* @retval None
*/
void set_DALIOUT(bool pin_value)
{
    digitalWrite(Pin(GPIO_DALI_TX), pin_value == DALI_OUT_INVERT ? LOW : HIGH);
}

/**
* @brief   gets state of the DALIOUT pin
* @param   None
* @retval bool state of the DALIOUT pin
*/
bool get_DALIOUT(void)
{
    bool dali_read = digitalRead(Pin(GPIO_DALI_TX));
    return (false == DALI_OUT_INVERT) ? dali_read : !dali_read;
}

/**
* @brief   Send data to DALI bus
* @param   byteToSend
* @retval None
*/
void sendDaliData(uint8_t firstByte, uint8_t secondByte)
{
	Dali->send_dali_data = firstByte << 8;
	Dali->send_dali_data += secondByte & 0xff;
	Dali->bit_count = 0;
	Dali->tick_count = 0;

    Dali->flag = DALI_SENDING_DATA;

	disableRxInterrupt();
}

/**
* @brief   DALI protocol physical layer for slave device
* @param   None
* @retval  None
*
* |--------|----|---------------------------|----|
* 0        24   32                          160  176
*    wait   start      data                  stop
*/
void send_tick(void)
{
	// access to the routine just every 4 ticks = every half bit
	if ((Dali->tick_count & 0x03) == 0)
	{
		if (Dali->tick_count < 160)
		{
			// settling time between forward and backward frame
			if (Dali->tick_count < 24)
			{
				Dali->tick_count++;
				return;
			}

			// start of the start bit
			if (Dali->tick_count == 24)
			{
				//   GPIOB->ODR ^= GPIO_ODR_7;
				set_DALIOUT(false);
				Dali->tick_count++;
				return;
			}

			// edge of the start bit
			// 28 ticks = 28/9600 = 2,92ms = delay between forward and backward message frame
			if (Dali->tick_count == 28)
			{
				set_DALIOUT(true);
				Dali->tick_count++;
				return;
			}

			// bit value (edge) selection
			bool bit_value = (bool)((Dali->send_dali_data >> (15 - Dali->bit_count)) & 0x01);

			// Every half bit -> Manchester coding
			if (!((Dali->tick_count - 24) & 0x0007))
			{									// div by 8
				if (get_DALIOUT() == bit_value) // former value of bit = new value of bit
					set_DALIOUT((bool)(1 - bit_value));
			}

			// Generate edge for actual bit
			if (!((Dali->tick_count - 28) & 0x0007))
			{
				set_DALIOUT(bit_value);
				Dali->bit_count++;
			}
		}
		else
		{ // end of data byte, start of stop bits
			if (Dali->tick_count == 160)
			{
				set_DALIOUT(true); // start of stop bit
			}

			// end of stop bits, no settling time
			if (Dali->tick_count == 176)
			{
				enableDaliRxInterrupt();
			}
		}
	}
	Dali->tick_count++;

	return;
}

/***********************************************************/

void DaliPreInit() {
    if (!PinUsed(GPIO_DALI_TX) || !PinUsed(GPIO_DALI_RX)) { return; }
    AddLog(LOG_LEVEL_INFO, PSTR("DLI: Init - RX-pin: %d, TX-pin: %d"), Pin(GPIO_DALI_RX), Pin(GPIO_DALI_TX));
    // pinMode(LED, OUTPUT);
	pinMode(Pin(GPIO_DALI_TX), OUTPUT);
	digitalWrite(Pin(GPIO_DALI_TX), HIGH);
	pinMode(Pin(GPIO_DALI_RX), INPUT);

	Dali = (DALI*)calloc(1,sizeof(DALI));
    if (!Dali) {
        AddLog(LOG_LEVEL_INFO, PSTR("DLI: Memory allocation error"));
        return;
    }
     Dali->timer = timerBegin(DALI_TIMER, 13, true);
    timerAttachInterrupt(Dali->timer, &DALI_Tick_Handler, true);
    timerAlarmWrite(Dali->timer, 641, true);
    
    attachInterrupt(Pin(GPIO_DALI_RX), receiveDaliData, FALLING);
	enableDaliRxInterrupt();
}

void DaliPwr(uint8_t val){
    sendDaliData(BROADCAST_DP, val);
}

bool DaliCmd(void)
{
    char command[CMDSZ];
    uint8_t name_len = strlen(D_NAME_DALI);
    if (!strncasecmp_P(XdrvMailbox.topic, PSTR(D_NAME_DALI), name_len))
    {
        uint32_t command_code = GetCommandCode(command, sizeof(command), XdrvMailbox.topic + name_len, kDALI_Commands);
        switch (command_code)
        {
        case CMND_DALI_PWR:
            if (XdrvMailbox.data_len)
            {
                if (254 >= XdrvMailbox.payload)
                {
                    DaliPwr(XdrvMailbox.payload);
                }
            }
            Response_P(S_JSON_DALI_COMMAND_NVALUE, command, XdrvMailbox.payload);
            break;
        default:
            return false;
        }
        return true;
    }
    else
    {
        return false;
    }
}


bool DaliMqtt()
{
    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--; // repreents the number of items

    if (cnt < 3)
    { // not for us?
        AddLog(LOG_LEVEL_INFO,PSTR("cnt: %d < 3"), cnt);
        return false;
    }

    int DALIindex = 0;
    int ADRindex = 0;
    int CMDindex = 0;
    uint8_t DALIaddr = BROADCAST_DP;
    if (strcasecmp_P(items[cnt - 3], PSTR(DALI_TOPIC)) != 0)
    {
        if (strcasecmp_P(items[cnt - 2], PSTR(DALI_TOPIC)) != 0)
        {
            if (strcasecmp_P(items[cnt - 1], PSTR(DALI_TOPIC)) != 0)
            {
                return false; // not for us
            }
            else
            {
                if (true == DaliJsonParse()) { return true; }
            }
        }
        else
        {
            DALIindex = cnt - 2;
            CMDindex = cnt - 1;
        }
    }
    else
    {
        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) {
        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("command not recognized: %s"), items[CMDindex]);
        return false; // not for us
    }

    AddLog(LOG_LEVEL_INFO,PSTR("Dali value %d on address %d"), value, DALIaddr);
    sendDaliData(DALIaddr, level);

    return true;
}

bool DaliJsonParse()
{
    bool served = false;
    JsonParser parser((char *)XdrvMailbox.data);
    JsonParserObject root = parser.getRootObject();
    if (root)
    {
        int DALIindex = 0;
        int ADRindex = 0;
        int8_t DALIdim = -1;
        uint8_t DALIaddr = BROADCAST_DP;

        JsonParserToken val = root[PSTR("cmd")];
        if (val)
        {
            uint8_t cmd = val.getUInt();
            val = root[PSTR("addr")];
            if (val)
            {
                uint8_t addr = val.getUInt();
                AddLog(LOG_LEVEL_DEBUG, PSTR("DLI: cmd = %d, addr = %d"), cmd, addr);
                sendDaliData(addr, cmd);
                return true;
            }
            else
            {
                return false;
            }
        }
        val = root[PSTR("addr")];
        if (val)
        {
            uint8_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;
        }

        sendDaliData(DALIaddr, DALIdim);
        served = true;
    }
    
    return served;
}

/*********************************************************************************************\
 * Interface
\*********************************************************************************************/

bool Xdrv89(uint32_t function)
{
    bool result = false;

    if (FUNC_INIT == function)
    {
        DaliPreInit();
    }
    else if (Dali)
    {
        switch (function)
        {
        case FUNC_MQTT_DATA:
            result = DaliMqtt();
            break;
        case FUNC_COMMAND:
            result = DaliCmd();
            break;
        case FUNC_ACTIVE:
            result = true;
            break;
        }
    }
    return result;
}

#endif  // USE_DALI
#endif  // ESP32