/**
*******************************************************************************
* @file modbus_coils.c
* @brief Реализация работы с коилами Modbus
*******************************************************************************
* @details
Модуль для доступа к coils внутри программы:
- Функции для доступа к coils по глобальным адресам
- Макросы для доступа к coils по локальным адресам

Модуль обработки команд для coils (битовых данных):
- Чтение coils (0x01) - упаковка битов в байты для передачи
- Запись одиночного coil (0x05) - установка/сброс бита
- Запись множественных coils (0x0F) - распаковка битов из байтов

@section cvalid Валидация данных:
- Проверка соответствия количества байт и регистров
- Валидация адресов через MB_DefineRegistersAddress()
- Обработка исключений при некорректных запросах
******************************************************************************/
#include "modbus_coils.h"

#ifdef MODBUS_ENABLE_COILS

/** 
  * @brief    Выставить/сбросить коил по глобальному адресу.
  * @param    Addr          Адрес коила.
  * @param    WriteVal      Что записать в коил: 0 или 1.
  * @return   ExceptionCode Код исключения если коила по адресу не существует, и ET_NO_ERRORS если все ок.
  *
  * @details  Позволяет обратиться к любому коилу по его глобальному адрессу.
              Вне зависимости от того как коилы размещены в памяти.
  */
MB_ExceptionTypeDef MB_Coil_Write_Global(uint16_t Addr, MB_CoilsOpTypeDef WriteVal)
{
  //---------CHECK FOR ERRORS----------
  MB_ExceptionTypeDef Exception = ET_NO_ERRORS;
  uint16_t *coils;
  uint16_t start_shift = 0; // shift in coils register
  
  //------------WRITE COIL-------------
  Exception = MB_DefineCoilsAddress(&coils, Addr, 1, &start_shift, 1);
  if(Exception == ET_NO_ERRORS)
  {
    switch(WriteVal)
    {
      case SET_COIL:
        *coils |= (1<<start_shift);       
        break;
        
      case RESET_COIL:
        *coils &= ~(1<<start_shift);          
        break;
        
      case TOOGLE_COIL:
        *coils ^= (1<<start_shift);           
        break;
      
    }   
  }
  return Exception;
}


/** 
  * @brief    Считать коил по глобальному адресу.
  * @param    Addr          Адрес коила.
  * @param    Exception     Указатель на переменную для кода исключения, в случае неудачи при чтении.
  * @return   uint16_t      Возвращает весь регистр с маской на запрошенном коиле.
  *
  * @details  Позволяет обратиться к любому коилу по его глобальному адрессу.
              Вне зависимости от того как коилы размещены в памяти.
  */
uint16_t MB_Coil_Read_Global(uint16_t Addr, MB_ExceptionTypeDef *Exception)
{
  //---------CHECK FOR ERRORS----------
  MB_ExceptionTypeDef Exception_tmp;
  if(Exception == NULL)             // if exception is not given to func fill it
    Exception = &Exception_tmp;
    
  uint16_t *coils;
  uint16_t start_shift = 0; // shift in coils register
  
  //------------READ COIL--------------
  *Exception = MB_DefineCoilsAddress(&coils, Addr, 1, &start_shift, 0); 
  if(*Exception == ET_NO_ERRORS)
  {
    return ((*coils)&(1<<start_shift));
  }
  else
  {
    return 0;
  }
}


/**
  * @brief    Обработать функцию Read Coils (01 - 0x01).
  * @param    modbus_msg        Указатель на структуру собщения modbus.
  * @return   fMessageHandled   Статус о результате обработки комманды.
  * @details  Обработка команды Read Coils.
  */
uint8_t MB_Process_Read_Coils(RS_MsgTypeDef *modbus_msg)
{
  //---------CHECK FOR ERRORS----------
  uint16_t *coils;
  uint16_t start_shift = 0; // shift in coils register
  
  modbus_msg->Except_Code = MB_DefineCoilsAddress(&coils, modbus_msg->Addr, modbus_msg->Qnt, &start_shift, 0);
  if(modbus_msg->Except_Code != ET_NO_ERRORS)
    return 0;
  
  //-----------READING COIL------------
  // setup output message data size 
  modbus_msg->ByteCnt = Divide_Up(modbus_msg->Qnt, 8);
  // create mask for coils
  uint16_t mask_for_coils = 0;  // mask for coils that've been chosen
  uint16_t setted_coils = 0;    // value of setted coils
  uint16_t temp_reg = 0;        // temp register for saving coils that hasnt been chosen
  uint16_t coil_cnt = 0;        // counter for processed coils

  // cycle until all registers with requered coils would be processed
  int shift = start_shift;  // set shift to first coil in first register
  int ind = 0;              // index for coils registers and data
  for(; ind <= Divide_Up(start_shift + modbus_msg->Qnt, 16); ind++)
  { 
    //----SET MASK FOR COILS REGISTER----
    mask_for_coils = 0;
    for(; shift < 0x10; shift++)
    {
      mask_for_coils |= 1<<(shift); // choose certain coil
      if(++coil_cnt >= modbus_msg->Qnt)
        break;
    }
    shift = 0; // set shift to zero for the next step
  
    //-----------READ COILS--------------
    modbus_msg->MbData[ind] = (*(coils+ind)&mask_for_coils) >> start_shift;
    if(ind > 0)
      modbus_msg->MbData[ind-1] |= ((*(coils+ind)&mask_for_coils) << 16) >> start_shift;
      
  }
  // т.к. MbData 16-битная, для 8-битной передачи, надо поменять местами верхний и нижний байты
  for(; ind >= 0; --ind)  
    modbus_msg->MbData[ind] = ByteSwap16(modbus_msg->MbData[ind]);
  
  return 1;
}

/**
  * @brief    Обработать функцию Write Single Coils (05 - 0x05).
  * @param    modbus_msg        Указатель на структуру собщения modbus.
  * @return   fMessageHandled   Статус о результате обработки комманды.
  * @details  Обработка команды Write Single Coils.
  */
uint8_t MB_Process_Write_Single_Coil(RS_MsgTypeDef *modbus_msg)
{
  //---------CHECK FOR ERRORS----------
  if ((modbus_msg->Qnt != 0x0000) && (modbus_msg->Qnt != 0xFF00))
  {
    modbus_msg->Except_Code = ET_ILLEGAL_DATA_VALUE;
    return 0;
  }   
  // define position of coil
  uint16_t *coils;
  uint16_t start_shift = 0; // shift in coils register
  modbus_msg->Except_Code = MB_DefineCoilsAddress(&coils, modbus_msg->Addr, 0, &start_shift, 1);
  if(modbus_msg->Except_Code != ET_NO_ERRORS)
    return 0;
  

  //----------WRITTING COIL------------
  if(modbus_msg->Qnt == 0xFF00)
    *(coils) |= 1<<start_shift; // write flags corresponding to received data
  else
    *(coils) &= ~(1<<start_shift); // write flags corresponding to received data
  
  return 1;
}

/**
  * @brief    Обработать функцию Write Multiple Coils (15 - 0x0F).
  * @param    modbus_msg      Указатель на структуру собщения modbus.
  * @return   fMessageHandled Статус о результате обработки комманды.
  * @details  Обработка команды Write Multiple Coils.
  */
uint8_t MB_Process_Write_Miltuple_Coils(RS_MsgTypeDef *modbus_msg)
{
  //---------CHECK FOR ERRORS----------
  if (modbus_msg->ByteCnt != Divide_Up(modbus_msg->Qnt, 8))
  { // if quantity too large OR if quantity and bytes count arent match
    modbus_msg->Except_Code = ET_ILLEGAL_DATA_VALUE;
    return 0;
  } 
  // define position of coil
  uint16_t *coils;          // pointer to coils
  uint16_t start_shift = 0; // shift in coils register
  modbus_msg->Except_Code = MB_DefineCoilsAddress(&coils, modbus_msg->Addr, modbus_msg->Qnt, &start_shift, 1);
  if(modbus_msg->Except_Code != ET_NO_ERRORS)
    return 0;
  
  //----------WRITTING COILS-----------
  // create mask for coils
  uint16_t mask_for_coils = 0;  // mask for coils that've been chosen
  uint32_t setted_coils = 0;    // value of setted coils
  uint16_t temp_reg = 0;        // temp register for saving coils that hasnt been chosen
  uint16_t coil_cnt = 0;        // counter for processed coils

  // cycle until all registers with requered coils would be processed
  int shift = start_shift; // set shift to first coil in first register
  for(int ind = 0; ind <= Divide_Up(start_shift + modbus_msg->Qnt, 16); ind++)
  { 
    //----SET MASK FOR COILS REGISTER----
    mask_for_coils = 0;
    for(; shift < 0x10; shift++)
    {
      mask_for_coils |= 1<<(shift); // choose certain coil
      if(++coil_cnt >= modbus_msg->Qnt)
        break;
    }
    shift = 0; // set shift to zero for the next step
    
    
    
    //-----------WRITE COILS-------------
    // get current coils
    temp_reg = *(coils+ind);
    // set coils
    setted_coils = ByteSwap16(modbus_msg->MbData[ind]) << start_shift;
    if(ind > 0)
    {
      setted_coils |= ((ByteSwap16(modbus_msg->MbData[ind-1]) << start_shift) >> 16);
    }
    // write coils
      
    *(coils+ind) = setted_coils & mask_for_coils;   
     // restore untouched coils
    *(coils+ind) |= temp_reg&(~mask_for_coils);
    
    
    if(coil_cnt >= modbus_msg->Qnt) // if all coils written - break cycle
      break;                        // *kind of unnecessary
  }
  
  return 1;
}

#endif //MODBUS_ENABLE_COILS
