#include "lboot_mbrtu.h"
#include "lboot_mb_shared.h"
#include "modbus-rtu.h"
#include "getopt.h"
#include "stdlib.h"
#include <string.h>

/** максимальный размер блока */
#define LBOOT_MBRTU_MAX_BLOCK_SIZE  200



static int f_parse_option(int opt);
static const char* f_get_opt_descr(void);
static const char *f_get_err_str(int err);
static int f_open(const char* devname, const char* serial, const void* params,
                   t_lboot_devinfo* info, int* out);

static char f_mb_parity = 'E';
static int f_mb_br = 115200;
static const char *f_mb_port = "COM1";
static int f_mb_addr = 192;





/** Описание опций интерфейса Modbus */
static const char* f_opt_descr =
"modbus rtu options:\n"\
"    --mb-port=port-name  - com port name (for instance \"COM1\" on Windows \n"\
"                             or  /dev/ttyUSB0 on Linux)\n"\
"    --mb-baudrate=br     - com port baudrate in bod/s between 110 and 115200\n"\
"    --mb-parity=parity   - com port parity bit: 'N' or 'n' for none\n"\
"                                                'E' or 'e' for even\n"\
"                                                'O' or 'o' for odd\n"\
"    --mb-addr=addr       - device address on Modbus bus, must be in\n"\
"                             interval [1; 247]\n"\
"    --mb-tout=ms         - modbus command response wait timeout in ms\n"\
"    --mb-sign-tout=ms    - modbus signature check response wait timeout in ms\n"\
"    --mb-retry-cnt=n     - modbus read/write registers retry count on error\n";

/** Коды ошибок для интерфейса Modbus RTU */
typedef enum {
    LBOOT_ERR_MBRTU_FIRST            = -150,
    /** неверно задана скорость RS */
    LBOOT_ERR_MBRTU_INVALID_BAUDRATE = -150,
    /** неверно задана четность для порта RS */
    LBOOT_ERR_MBRTU_INVALID_PARITY   = -151,
    /** неверно задан адрес устройства на шине Modbus */
    LBOOT_ERR_MBRTU_INVALID_ADDRESS  = -152,
    /** ошибка создания контекста Modbus */
    LBOOT_ERR_MBRTU_CREATE_CONTEXT   = -153,
    /** ошибка открытия указанного порта */
    LBOOT_ERR_MBRTU_PORT_OPEN        = -154,
} t_lboot_mbrtu_errs;

static const char* f_err_str[] = {
    "invalid baudrate value",
    "invalid parity valude (must be 'N', 'E' or 'O')",
    "invalid modbus slave device address (must be in [1,247])",
    "Modbus context creation error",
    "Can't open com port",
};


/** параметры интерфейса ModbusRTU и его интерфейсные функции */
const t_boot_iface g_mbrtu_intf_info = {
    { "mbrtu",
      "Modbus RTU protocol over RS232/RS485 interface",
      NULL,
      NULL,
      f_parse_option,
      f_get_opt_descr,
      f_get_err_str,
    },
    NULL,
    LBOOT_MBRTU_MAX_BLOCK_SIZE,    
    f_open,
    lboot_mb_get_devmode,
    lboot_mb_get_bootloader_version,
    lboot_mb_get_dev_flags,
    lboot_mb_get_features,
    lboot_mb_start_write,
    lboot_fw_transfer_std_block,
    lboot_mb_write,
    lboot_mb_write_sign,
    lboot_mb_speccmd,
    lboot_mb_close
};


static const char *f_get_err_str(int err) {
    const char *mb_err_str = lboot_mb_get_err_str(err);
    return mb_err_str != NULL ? mb_err_str :
               (((err > LBOOT_ERR_MBRTU_FIRST) ||
            (err <  (LBOOT_ERR_MBRTU_FIRST -
                     (int32_t)(sizeof(f_err_str)/sizeof(f_err_str[0]))+1))) ?
                     NULL : f_err_str[LBOOT_ERR_MBRTU_FIRST-err]);
}



/***************************************************************************//**
  Разбор опций, специфичных для протокола ModbusRTU - задание настроек адреса
  по протоколу Modubs и настроек RS (скорость, четность, порт).
  Использует переменную optarg для получения доступа к значению опции
  @param[in] opt  Код опции
  @return         Код ошибки
  *****************************************************************************/
static int f_parse_option(int opt) {
    int err = 0;
    switch (opt) {
        case LBOOT_OPT_MB_PORT:
            f_mb_port = optarg;
            break;
        case LBOOT_OPT_MB_ADDR:
            /* получаем адрес и проверяем, что лижит в допустимых пределах адреса
               в соответствии с протоколом Modbus */
            f_mb_addr = atoi(optarg);
            if (!f_mb_addr || (f_mb_addr >= 248))
                err = LBOOT_ERR_MBRTU_INVALID_ADDRESS;
            break;
        case LBOOT_OPT_MB_BR:
            f_mb_br = atoi(optarg);
            if ((f_mb_br < 110) || (f_mb_br > 115200))
                err = LBOOT_ERR_MBRTU_INVALID_BAUDRATE;
            break;
        case LBOOT_OPT_MB_PARITY:
            /* проверяем параметр четности в rs - всегда 1 символ, 'N', 'E' или 'O'
              принимаем также строчные буквы, преобразовывая их к заглавным */
            if (strlen(optarg)!=1) {
                err = LBOOT_ERR_MBRTU_INVALID_PARITY;
            } else {
                f_mb_parity = optarg[0];
                if (f_mb_parity == 'n') {
                    f_mb_parity = 'N';
                } else if (f_mb_parity == 'e') {
                    f_mb_parity = 'E';
                } else if (f_mb_parity == 'o') {
                    f_mb_parity = 'O';
                }

                if ((f_mb_parity != 'N') && (f_mb_parity != 'E') && (f_mb_parity!='O'))
                    err = LBOOT_ERR_MBRTU_INVALID_PARITY;
            }
            break;
        case LBOOT_OPT_MB_TOUT:
            err = lboot_mb_set_tout(atoi(optarg));
            break;
        case LBOOT_OPT_MB_SIGN_TOUT:
            err = lboot_mb_set_sign_tout(atoi(optarg));
            break;
        case LBOOT_OPT_MB_RETRY_CNT:
            lboot_mb_set_max_retry_cnt(atoi(optarg));
            break;
        default:
            err = LBOOT_ERR_INVALID_OPTION;
            break;
    }
    return err;
}

/***************************************************************************//**
  Функция возвращает строку с описанием опций, специфичных для протокола Modbus
  *****************************************************************************/
static  const char* f_get_opt_descr(void) {
    return f_opt_descr;
}




static int f_open(const char* devname, const char* serial, const void* params,
                        t_lboot_devinfo* info, int* out) {
    int err = 0;
    const char *port = NULL;
#ifdef _WIN32
    char *port_modified = NULL;
    /* для Windows название порта должно начинаться с \\.\, иначе нельзя будет
      соединиться с портом старше 10-го - если заданное имя уже не начинается
      с '\', то формируем новую строку, добавляя в начало \\.\ */
    if (f_mb_port[0]!='\\') {
        int len = strlen(f_mb_port);
        port_modified = malloc(len + 5);
        if (port_modified != NULL) {
            strcpy(port_modified, "\\\\.\\");
            strcat(port_modified, f_mb_port);
        } else {
            err = LBOOT_ERR_MEMORY_ALLOC;
        }
        port = port_modified;
    } else {
        port = f_mb_port;
    }
#else
    port = f_mb_port;
#endif

    if (!err) {
        /* содаем контекст modbus в соответствии с настройками из опций*/
        modbus_t *mb = modbus_new_rtu(port, f_mb_br, f_mb_parity, 8, 1);
        if (mb == NULL) {
            err = LBOOT_ERR_MBRTU_CREATE_CONTEXT;            
        } else {
            err = lboot_mb_open(mb, f_mb_addr, LBOOT_ERR_MBRTU_PORT_OPEN);
        }

        if (err) {
            *out = 1;
        }
    }

    if (!err) {
        int t35 = f_mb_br > 19200 ? 2 : (3500 * 11 + (f_mb_br - 1)) / f_mb_br;
        lboot_mb_set_interframe_delay(t35);

        /* устанавливаем небольшой таймаут, чтобы была возможность повторить запрос,
           в течение небольшого времени */
        lboot_mb_set_resp_tout(200);
        err = lboot_mb_read_dev_info(info);
        if (!err) {
            /* увеличиваем таймаут на ответ, так как некоторые действия могут
              потребовать значительного времени */
            lboot_mb_restore_resp_tout();
        }
    }


    if (err) {
        lboot_mb_close();
    }

#ifdef _WIN32
    if (port_modified != NULL)
        free(port_modified);
#endif
    return err;
}















