#include "lboot_mbrtu.h"
#include "modbus.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 int f_get_devmode(uint8_t* mode);
static int f_get_bootloader_version(uint16_t* ver);
static int f_get_dev_flags(uint32_t* flags);
static int f_get_features(uint32_t* flags);
static int f_start_write(t_lboot_startwr_info wr_info);
static int f_write(const uint8_t* buf, uint32_t size);
static int f_write_sign(const uint8_t* sign, uint32_t size);
static int f_speccmd(const t_lboot_speccmd_info* cmd_info, uint32_t size);
static int f_close(void);

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;
static int f_opened = 0;
static int f_mb_tout = 1000;
static int f_mb_sign_tout = 4000;
static int f_mb_max_retry_cnt = 0;

static modbus_t *f_mb_st = NULL;

static int32_t lboot_mbrtu_get_deverr(int32_t* deverr);





/** Описание опций интерфейса 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            = -100,
    /** неверно задана скорость RS */
    LBOOT_ERR_MBRTU_INVALID_BAUDRATE = -100,
    /** неверно задана четность для порта RS */
    LBOOT_ERR_MBRTU_INVALID_PARITY   = -101,
    /** неверно задан адрес устройства на шине Modbus */
    LBOOT_ERR_MBRTU_INVALID_ADDRESS  = -102,
    /** ошибка создания контекста Modbus */
    LBOOT_ERR_MBRTU_CREATE_CONTEXT   = -103,
    /** ошибка функции modbus_set_slave() */
    LBOOT_ERR_MBRTU_SET_ADDR         = -104,
    /** ошибка открытия указанного порта */
    LBOOT_ERR_MBRTU_PORT_OPEN        = -105,
    /** ошибка чтения регистров modbus */
    LBOOT_ERR_READ_REGISTERS         = -106,
    LBOOT_ERR_WRITE_REGISTERS        = -107
} t_lboot_mbrtu_errs;

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


/** параметры интерфейса 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,
    f_get_devmode,
    f_get_bootloader_version,
    f_get_dev_flags,
    f_get_features,
    f_start_write,
    lboot_fw_transfer_std_block,
    f_write,
    f_write_sign,
    f_speccmd,
    f_close
};




/************************ адреса регистров на шине Modbus *********************/
#define LBOOT_MB_REG_ADDR_OFFSET    0x0001
#define LBOOT_MB_REGADDR_WR_START   0x8000
#define LBOOT_MB_REGADDR_WR_FIRM    0x8100
#define LBOOT_MB_REGADDR_WR_SIGN    0x8101
#define LBOOT_MB_REGADDR_WR_SPECCMD 0x8200


#define LBOOT_MB_REG_DEVINFO_SIZE (sizeof(t_lboot_devinfo)/2)

#define LBOOT_MB_REG_DEVINFO      LBOOT_MB_REG_ADDR_OFFSET
#define LBOOT_MB_REG_MODE         (LBOOT_MB_REG_ADDR_OFFSET + LBOOT_MB_REG_DEVINFO_SIZE)
#define LBOOT_MB_REG_BOOTVER      (LBOOT_MB_REG_MODE + 1)
#define LBOOT_MB_REG_ERR_L        (LBOOT_MB_REG_BOOTVER + 1)
#define LBOOT_MB_REG_ERR_H        (LBOOT_MB_REG_ERR_L + 1)
#define LBOOT_MB_REG_FLAGS_L      (LBOOT_MB_REG_ERR_H + 1)
#define LBOOT_MB_REG_FLAGS_H      (LBOOT_MB_REG_FLAGS_L + 1)
#define LBOOT_MB_REG_FEATURES_L   (LBOOT_MB_REG_FLAGS_H + 1)
#define LBOOT_MB_REG_FEATURES_H   (LBOOT_MB_REG_FEATURES_L + 1)








static  const char* f_get_err_str(int err) {
    return ((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];
}

static unsigned f_mb_cur_resp_tout = (unsigned)-1;

static void f_mb_set_resp_tout(unsigned response_tout) {
    if (f_mb_cur_resp_tout != response_tout) {
#if LIBMODBUS_VERSION_CHECK(3, 1, 0)
        modbus_set_response_timeout(f_mb_st, response_tout/1000,  (response_tout % 1000) *1000);
#else
        struct timeval tv;
        tv.tv_sec = response_tout/1000;
        tv.tv_usec = (response_tout % 1000) *1000;
        modbus_set_response_timeout(f_mb_st, &tv);
#endif
        f_mb_cur_resp_tout = response_tout;
    }
}

static void f_sleep_interframe() {
    int t35 = f_mb_br > 19200 ? 2 : (3500 * 11 + (f_mb_br - 1)) / f_mb_br;
    SLEEP_MS(t35);
}

static int f_read_input_regs(modbus_t *ctx, int addr, int nb, uint16_t *dest) {
    int err = modbus_read_input_registers(ctx, addr-1, nb, dest);
    f_sleep_interframe();
    return err;
}

static int f_write_regs(modbus_t *ctx, int addr, int nb, const uint16_t *data) {
    int err = modbus_write_registers(ctx, addr-1, nb, data);
    f_sleep_interframe();
    return err;
}


static int f_mb_write_regs_wcheck(int addr, const uint16_t *buf, int size) {
    int err;
    int retry_cnt = 0;
    int out = 0;
    do {
        if (f_write_regs(f_mb_st, addr, size, buf)!=size) {
            int32_t deverr=0;
            if (lboot_mbrtu_get_deverr(&deverr) !=0) {
                err = LBOOT_ERR_WRITE_REGISTERS;
                /* выходим из цикла, т.к. уже были повторные попытки при чтении ошибки */
                out = 1;
            } else if (deverr == 0) {
                err = LBOOT_ERR_WRITE_REGISTERS;
            } else {                
                err = deverr;
                /* если устройство вернуло не 0-ую ошибку, то это не ошибка интерфейса,
                 * соответственно нет необходимости делать повтор */
                out = 1;
            }
        } else {
            err = 0;
            out = 1;
        }
    } while (!out  && (retry_cnt++ < f_mb_max_retry_cnt));
    return err;
}


static int f_mb_read_regs_wretry(int addr, uint16_t *buf, int size) {
    int err;
    int retry_cnt = 0;
    do {
        if (f_read_input_regs(f_mb_st, addr, size, buf)!=size) {
            err = LBOOT_ERR_READ_REGISTERS;
        } else {
            err = 0;
        }
    } while ((err != 0)  && (retry_cnt++ < f_mb_max_retry_cnt));
    return 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:
            f_mb_tout = atoi(optarg);
            break;
        case LBOOT_OPT_MB_SIGN_TOUT:
            f_mb_sign_tout = atoi(optarg);
            break;
        case LBOOT_OPT_MB_RETRY_CNT:
            f_mb_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;
    int con = 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) {
        f_mb_st = NULL;
        /* содаем контекст modbus в соответствии с настройками из опций*/
        f_mb_st = modbus_new_rtu(port, f_mb_br, f_mb_parity, 8, 1);
        if (f_mb_st == NULL) {
            err = LBOOT_ERR_MBRTU_CREATE_CONTEXT;
            *out=1;
        }
    }



    /* устанавливаем соединение (открываем порт) */
    if (!err ) {
        if (modbus_connect(f_mb_st)) {
            err = LBOOT_ERR_MBRTU_PORT_OPEN;
            *out = 1;
        } else {
            con = 1;
        }
    }

    /* устанавливаем адрес устройства */
    if (!err && modbus_set_slave(f_mb_st, f_mb_addr)) {
        err = LBOOT_ERR_MBRTU_SET_ADDR;
        *out = 1;
    }

    if (!err) {
        /* устанавливаем небольшой таймаут, чтобы была возможность повторить запрос,
           в течение небольшого времени */
        f_mb_set_resp_tout(200);
        if (f_read_input_regs(f_mb_st, LBOOT_MB_REG_DEVINFO,
                                        LBOOT_MB_REG_DEVINFO_SIZE, (uint16_t*)info)!=LBOOT_MB_REG_DEVINFO_SIZE) {
            err = LBOOT_ERR_READ_REGISTERS;
        } else {
            /* увеличиваем таймаут на ответ, так как некоторые действия могут
              потребовать значительного времени */
            f_mb_set_resp_tout(f_mb_tout);
        }
    }


    if (err) {
        if (con)
            modbus_close(f_mb_st);

        /* очищаем контекст */
        if (f_mb_st != NULL) {
            modbus_free(f_mb_st);
            f_mb_st = NULL;
        }
    }

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


static int f_get_devmode(uint8_t* mode) {
    uint16_t val;
    int err = 0;

    err = f_mb_read_regs_wretry(LBOOT_MB_REG_MODE, &val,1);
    if (!err) {
        *mode = val&0xFF;
    }

    return err;
}

static  int f_get_bootloader_version(uint16_t* ver) {
    return f_mb_read_regs_wretry(LBOOT_MB_REG_BOOTVER, ver, 1);
}

static int f_get_dev_flags(uint32_t* flags) {
    return f_mb_read_regs_wretry(LBOOT_MB_REG_FLAGS_L, (uint16_t*)flags, 2);
}

static int f_get_features(uint32_t* flags) {
    return f_mb_read_regs_wretry(LBOOT_MB_REG_FEATURES_L, (uint16_t*)flags, 2);
}

static int32_t lboot_mbrtu_get_deverr(int32_t* deverr) {
    int32_t err;
    unsigned tout = f_mb_cur_resp_tout;
    /* изменяем таймаут на обычный (т.к. можем находится в долгой операции,
     * а чтение ошибки долгого ожидания не требует) и восстанавливем
     * его по выходу */
    f_mb_set_resp_tout((unsigned)f_mb_tout);
    err =  f_mb_read_regs_wretry(LBOOT_MB_REG_ERR_L, (uint16_t*)deverr, 2);
    f_mb_set_resp_tout(tout);
    return err;
}


/***************************************************************************//**
  Запись блока данных по Modbus RTU. При нечетном размере последний байт пишется
  отдельно с установленной в ноль старшей частью регистра.
  При неудаче идет попытка считать код ошибки из устройства.
  @param[in] addr  Адрес регистра (с вычитом 1 - как в libmodbus, а не в протоколе)
  @param[in] buf   Буфер для записи
  @param[in] size  Размер буфера в байтах
  @return          Код ошибки
  *****************************************************************************/
static  int f_mb_write_data(int addr, const uint8_t* buf, uint32_t size) {
    int err =0;
    err = f_mb_write_regs_wcheck(addr, (const uint16_t*)buf, size/2);
    /* если было нечетное число, то последний байт записываем отдельным словом */
    if (!err && (size%2)) {
        uint16_t val = buf[size-1];
        err = f_mb_write_regs_wcheck(addr, &val, 1);
    }
    return err;
}



static int f_start_write(t_lboot_startwr_info wr_info) {
    return f_mb_write_data(LBOOT_MB_REGADDR_WR_START, (const uint8_t*)&wr_info,
            sizeof(t_lboot_startwr_info));
}

static int f_write(const uint8_t* buf, uint32_t size) {
    return f_mb_write_data(LBOOT_MB_REGADDR_WR_FIRM, buf, size);
}

static int f_write_sign(const uint8_t* sign, uint32_t size) {
    f_mb_set_resp_tout((unsigned)f_mb_sign_tout);
    int err = f_mb_write_data(LBOOT_MB_REGADDR_WR_SIGN, sign, size);
    f_mb_set_resp_tout((unsigned)f_mb_tout);
    return  err;
}


static int f_speccmd(const t_lboot_speccmd_info* cmd_info, uint32_t size) {
    return f_mb_write_data(LBOOT_MB_REGADDR_WR_SPECCMD, (const uint8_t*)cmd_info,
                           size);
}

static int f_close(void) {
    if (f_mb_st != NULL) {
        /* закрываем соединение,если было открыто */
        if (f_opened && (f_mb_st != NULL)) {
            modbus_close(f_mb_st);
        }
        /* очищаем контекст */
        modbus_free(f_mb_st);
        f_mb_st = NULL;
    }

    return 0;
}





