/*---------------------------------------------------------*\
| serial_port.cpp                                           |
|                                                           |
|   Cross Platform Serial COM Library for Windows and Linux |
|   This library provides access to serial ports with a     |
|   common API for both Windows and Linux systems           |
|                                                           |
|   Adam Honse (calcprogrammer1@gmail.com)      21 Jan 2013 |
|                                                           |
|   This file is part of the OpenRGB project                |
|   SPDX-License-Identifier: GPL-2.0-only                   |
\*---------------------------------------------------------*/

#include "serial_port.h"

/*---------------------------------------------------------*\
|  serial_port (constructor)                                |
|    The default constructor does not initialize the serial |
|    port                                                   |
\*---------------------------------------------------------*/
serial_port::serial_port()
{
    /*-----------------------------------------------------*\
    | Set default port configuration but do not open        |
    \*-----------------------------------------------------*/
    this->baud_rate     = 9600;
    this->parity        = SERIAL_PORT_PARITY_NONE;
    this->size          = SERIAL_PORT_SIZE_8;
    this->stop_bits     = SERIAL_PORT_STOP_BITS_1;
    this->flow_control  = false;
}

/*---------------------------------------------------------*\
|  serial_port (constructor)                                |
|    When created with port information, the constructor    |
|    will automatically open port <name> at baud rate <baud>|
\*---------------------------------------------------------*/
serial_port::serial_port(const char * name, unsigned int baud)
{
    /*-----------------------------------------------------*\
    | Set default port configuration and open               |
    \*-----------------------------------------------------*/
    this->baud_rate     = baud;
    this->parity        = SERIAL_PORT_PARITY_NONE;
    this->size          = SERIAL_PORT_SIZE_8;
    this->stop_bits     = SERIAL_PORT_STOP_BITS_1;
    this->flow_control  = false;

    serial_open(name);
}

/*---------------------------------------------------------*\
|  serial_port (constructor)                                |
|    When created with port information, the constructor    |
|    will automatically open port <name> at baud rate <baud>|
|    with the given port configuration                      |
\*---------------------------------------------------------*/
serial_port::serial_port
    (
    const char *            name,
    unsigned int            baud,
    serial_port_parity      parity,
    serial_port_size        size,
    serial_port_stop_bits   stop_bits,
    bool                    flow_control
    )
{
    /*-----------------------------------------------------*\
    | Set default port configuration and open               |
    \*-----------------------------------------------------*/
    this->baud_rate     = baud;
    this->parity        = parity;
    this->size          = size;
    this->stop_bits     = stop_bits;
    this->flow_control  = flow_control;

    serial_open(name);
}

/*---------------------------------------------------------*\
|  ~serial_port (destructor)                                |
|    Closes the port before destroying the object           |
\*---------------------------------------------------------*/
serial_port::~serial_port()
{
    serial_close();
}

/*---------------------------------------------------------*\
|  serial_open                                              |
|    Opens the serial port using stored information         |
|    Sets the baud rate to the stored baud rate             |
|    8 data bits, no parity, one stop bit                   |
\*---------------------------------------------------------*/
bool serial_port::serial_open()
{
    /*-----------------------------------------------------*\
    | Windows-specific code path for serial port opening    |
    \*-----------------------------------------------------*/
#ifdef _WIN32
    // On Windows, ports above 9 need to have "\\.\" prepended to their path. For ports below 9, this is optional.
    // https://support.microsoft.com/en-us/topic/howto-specify-serial-ports-larger-than-com9-db9078a5-b7b6-bf00-240f-f749ebfd913e
    char full_path[100];
    snprintf(full_path, sizeof(full_path), "\\\\.\\%s", port_name);

    /*-----------------------------------------*\
    | Open the port read/write                  |
    \*-----------------------------------------*/
    file_descriptor = CreateFile(full_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if(file_descriptor == INVALID_HANDLE_VALUE)
    {
        return false;
    }

    /*-----------------------------------------*\
    | Get the port configuration options        |
    \*-----------------------------------------*/
    SetupComm(file_descriptor, 1, 128);
    GetCommState(file_descriptor, &dcb);

    /*-----------------------------------------*\
    | Configure baud rate                       |
    \*-----------------------------------------*/
    dcb.BaudRate = baud_rate;

    /*-----------------------------------------*\
    | Configure parity                          |
    \*-----------------------------------------*/
    switch(parity)
    {
        case SERIAL_PORT_PARITY_NONE:
            dcb.Parity = NOPARITY;
            break;

        case SERIAL_PORT_PARITY_ODD:
            dcb.Parity = ODDPARITY;
            break;

        case SERIAL_PORT_PARITY_EVEN:
            dcb.Parity = EVENPARITY;
            break;
    }

    /*-----------------------------------------*\
    | Configure size                            |
    \*-----------------------------------------*/
    switch(size)
    {
        case SERIAL_PORT_SIZE_8:
            dcb.ByteSize = 8;
            break;

        case SERIAL_PORT_SIZE_7:
            dcb.ByteSize = 7;
            break;

        case SERIAL_PORT_SIZE_6:
            dcb.ByteSize = 6;
            break;

        case SERIAL_PORT_SIZE_5:
            dcb.ByteSize = 5;
            break;
    }

    /*-----------------------------------------*\
    | Configure stop bits                       |
    \*-----------------------------------------*/
    if(stop_bits == SERIAL_PORT_STOP_BITS_2)
    {
        dcb.StopBits = TWOSTOPBITS;
    }
    else
    {
        dcb.StopBits = ONESTOPBIT;
    }

    /*-----------------------------------------*\
    | Configure flow control                    |
    \*-----------------------------------------*/
    if(flow_control)
    {
        dcb.fRtsControl = RTS_CONTROL_ENABLE;
    }
    else
    {
        dcb.fRtsControl = RTS_CONTROL_DISABLE;
    }

    /*-----------------------------------------*\
    | Configure additional parameters           |
    \*-----------------------------------------*/
    dcb.fAbortOnError = FALSE;                  //Abort on error
    dcb.fOutX = FALSE;                          //XON/XOFF off for transmit
    dcb.fInX = FALSE;                           //XON/XOFF off for receive
    dcb.fOutxCtsFlow = FALSE;                   //Turn off CTS flow control
    dcb.fOutxDsrFlow = FALSE;                   //Turn off DSR flow control
    dcb.fDtrControl = DTR_CONTROL_DISABLE;      //Disable DTR control

    /*-----------------------------------------*\
    | Set the port configuration options        |
    \*-----------------------------------------*/
    SetCommState(file_descriptor, &dcb);

    /*-----------------------------------------*\
    | Set the port timeouts                     |
    \*-----------------------------------------*/
    COMMTIMEOUTS timeouts                   = {0};
    timeouts.ReadIntervalTimeout            = 50;
    timeouts.ReadTotalTimeoutConstant       = 50;
    timeouts.ReadTotalTimeoutMultiplier     = 10;
    timeouts.WriteTotalTimeoutConstant      = 50;
    timeouts.WriteTotalTimeoutMultiplier    = 10;

    SetCommTimeouts(file_descriptor, &timeouts);
#endif

    /*-----------------------------------------------------*\
    | Linux-specific code path for serial port opening      |
    \*-----------------------------------------------------*/
#ifdef __linux__
    /*-----------------------------------------*\
    | Open the port read/write with no delay    |
    \*-----------------------------------------*/
    file_descriptor = open(port_name, O_RDWR | O_NOCTTY | O_NDELAY);

    if(file_descriptor < 0)
    {
        return false;
    }

    /*-----------------------------------------*\
    | Set an advisory lock on the port and      |
    | abort port setup if already locked        |
    \*-----------------------------------------*/
    if(flock(file_descriptor, LOCK_EX | LOCK_NB) < 0)
    {
        close(file_descriptor);
        return false;
    }

    /*-----------------------------------------*\
    | Get the port configuration options        |
    \*-----------------------------------------*/
    struct termios2 options;
    ioctl(file_descriptor, TCGETS2, &options);

    /*-----------------------------------------*\
    | Configure baud rate                       |
    \*-----------------------------------------*/
    options.c_cflag &= ~CBAUD;
    options.c_cflag |= CBAUDEX;

    options.c_ispeed = baud_rate;
    options.c_ospeed = baud_rate;

    /*-----------------------------------------*\
    | Configure parity                          |
    \*-----------------------------------------*/
    switch(parity)
    {
        case SERIAL_PORT_PARITY_NONE:
            options.c_cflag &= ~PARENB;
            options.c_cflag &= ~PARODD;
            break;

        case SERIAL_PORT_PARITY_ODD:
            options.c_cflag |= PARENB;
            options.c_cflag |= PARODD;
            break;

        case SERIAL_PORT_PARITY_EVEN:
            options.c_cflag |= PARENB;
            options.c_cflag &= ~PARODD;
            break;
    }

    /*-----------------------------------------*\
    | Configure size                            |
    \*-----------------------------------------*/
    options.c_cflag &= ~CSIZE;

    switch(size)
    {
        case SERIAL_PORT_SIZE_8:
            options.c_cflag |= CS8;
            break;

        case SERIAL_PORT_SIZE_7:
            options.c_cflag |= CS7;
            break;

        case SERIAL_PORT_SIZE_6:
            options.c_cflag |= CS6;
            break;

        case SERIAL_PORT_SIZE_5:
            options.c_cflag |= CS5;
            break;
    }

    /*-----------------------------------------*\
    | Configure stop bits                       |
    \*-----------------------------------------*/
    if(stop_bits == SERIAL_PORT_STOP_BITS_2)
    {
        options.c_cflag |= CSTOPB;
    }
    else
    {
        options.c_cflag &= ~CSTOPB;
    }

    /*-----------------------------------------*\
    | Configure flow control                    |
    \*-----------------------------------------*/
    if(flow_control)
    {
        options.c_cflag |= CRTSCTS;
    }
    else
    {
        options.c_cflag &= ~CRTSCTS;
    }

    /*-----------------------------------------*\
    | Configure additional parameters           |
    \*-----------------------------------------*/
    options.c_lflag &= ~ICANON;
    options.c_lflag &= ~ECHO;                                               // Disable echo
    options.c_lflag &= ~ECHOE;                                              // Disable erasure
    options.c_lflag &= ~ECHONL;                                             // Disable new-line echo
    options.c_lflag &= ~ISIG;                                               // Disable interpretation of INTR, QUIT and SUSP
    options.c_lflag &= ~IEXTEN;                                             // Disable input processing

    options.c_iflag &= ~(IXON | IXOFF | IXANY);                             // Turn off s/w flow ctrl
    options.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL);    // Disable any special handling of received bytes

    options.c_oflag &= ~OPOST;                                              // Disable output processing;

    /*-----------------------------------------*\
    | Set the port configuration options        |
    \*-----------------------------------------*/
    ioctl(file_descriptor, TCSETS2, &options);
#endif

    /*-----------------------------------------------------*\
    | MacOS-specific code path for serial port opening      |
    \*-----------------------------------------------------*/
#ifdef __APPLE__
    /*-----------------------------------------*\
    | Open the port read/write with no delay    |
    \*-----------------------------------------*/
    file_descriptor = open(port_name, O_RDWR | O_NOCTTY | O_NDELAY);

    if(file_descriptor < 0)
    {
        return false;
    }

    /*-----------------------------------------*\
    | Get the port configuration options        |
    \*-----------------------------------------*/
    struct termios options;
    tcgetattr(file_descriptor, &options);

    /*-----------------------------------------*\
    | Configure parity                          |
    \*-----------------------------------------*/
    switch(parity)
    {
        case SERIAL_PORT_PARITY_NONE:
            options.c_cflag &= ~PARENB;
            options.c_cflag &= ~PARODD;
            break;

        case SERIAL_PORT_PARITY_ODD:
            options.c_cflag |= PARENB;
            options.c_cflag |= PARODD;
            break;

        case SERIAL_PORT_PARITY_EVEN:
            options.c_cflag |= PARENB;
            options.c_cflag &= ~PARODD;
            break;
    }

    /*-----------------------------------------*\
    | Configure size                            |
    \*-----------------------------------------*/
    options.c_cflag &= ~CSIZE;

    switch(size)
    {
        case SERIAL_PORT_SIZE_8:
            options.c_cflag |= CS8;
            break;

        case SERIAL_PORT_SIZE_7:
            options.c_cflag |= CS7;
            break;

        case SERIAL_PORT_SIZE_6:
            options.c_cflag |= CS6;
            break;

        case SERIAL_PORT_SIZE_5:
            options.c_cflag |= CS5;
            break;
    }

    /*-----------------------------------------*\
    | Configure stop bits                       |
    \*-----------------------------------------*/
    if(stop_bits == SERIAL_PORT_STOP_BITS_2)
    {
        options.c_cflag |= CSTOPB;
    }
    else
    {
        options.c_cflag &= ~CSTOPB;
    }

    /*-----------------------------------------*\
    | Configure flow control                    |
    \*-----------------------------------------*/
    if(flow_control)
    {
        options.c_cflag |= CRTSCTS;
    }
    else
    {
        options.c_cflag &= ~CRTSCTS;
    }

    /*-----------------------------------------*\
    | Configure additional parameters           |
    \*-----------------------------------------*/
    options.c_lflag &= ~(ICANON | IEXTEN | ISIG | ECHO);

    options.c_iflag &= ~(INLCR | ICRNL);
    options.c_iflag |= IGNPAR | IGNBRK;

    options.c_oflag &= ~(OPOST | ONLCR | OCRNL);

    options.c_cc[VTIME] = 1;
    options.c_cc[VMIN]  = 0;

    /*-----------------------------------------*\
    | Set the port configuration options        |
    \*-----------------------------------------*/
    if(tcsetattr(file_descriptor, TCSANOW, &options) < 0)
    {
        close(file_descriptor);
        return false;
    }

    /*-----------------------------------------*\
    | Configure baud rate                       |
    \*-----------------------------------------*/
    ioctl(file_descriptor, IOSSIOSPEED, &baud_rate);
#endif

    /*-----------------------------------------------------*\
    | Return true if successful                             |
    \*-----------------------------------------------------*/
    return true;
}

/*---------------------------------------------------------*\
|  serial_open                                              |
|    Opens the serial port <name> without changing stored   |
|    baud rate                                              |
\*---------------------------------------------------------*/
bool serial_port::serial_open(const char * name)
{
    return serial_open(name, baud_rate);
}

/*---------------------------------------------------------*\
|  serial_open                                              |
|    Opens the serial port <name> at baud rate <baud>       |
\*---------------------------------------------------------*/
bool serial_port::serial_open(const char* name, unsigned int baud)
{
    snprintf(port_name,sizeof(port_name),"%s",name);
    baud_rate = baud;
    return serial_open();
}

/*---------------------------------------------------------*\
|  serial_close                                             |
|    Closes the serial port                                 |
\*---------------------------------------------------------*/
void serial_port::serial_close()
{
    /*-----------------------------------------------------*\
    | Windows-specific code path for serial close           |
    \*-----------------------------------------------------*/
#ifdef _WIN32
    CloseHandle(file_descriptor);
#endif

    /*-----------------------------------------------------*\
    | Linux-specific code path for serial close             |
    \*-----------------------------------------------------*/
#ifdef __linux__
    flock(file_descriptor, LOCK_UN | LOCK_NB);
    close(file_descriptor);
#endif

    /*-----------------------------------------------------*\
    | MacOS-specific code path for serial close             |
    \*-----------------------------------------------------*/
#ifdef __APPLE__
    close(file_descriptor);
#endif
}

/*---------------------------------------------------------*\
|  serial_read                                              |
|    Reads <length> bytes from the serial port into <buffer>|
|    Returns the number of bytes actually read              |
|    If less than <length> bytes are available, it will read|
|    all available bytes                                    |
\*---------------------------------------------------------*/
int serial_port::serial_read(char * buffer, int length)
{
    /*-----------------------------------------------------*\
    | Windows-specific code path for serial read            |
    \*-----------------------------------------------------*/
#ifdef _WIN32
    DWORD bytesread;
    ReadFile(file_descriptor, buffer, length, &bytesread, NULL);
    return bytesread;
#endif

    /*-----------------------------------------------------*\
    | Linux-specific code path for serial read              |
    \*-----------------------------------------------------*/
#ifdef __linux__
    int bytesread;
    bytesread = read(file_descriptor, buffer, length);
    return bytesread;
#endif

    /*-----------------------------------------------------*\
    | MacOS-specific code path for serial read              |
    \*-----------------------------------------------------*/
#ifdef __APPLE__
    int bytesread;
    bytesread = read(file_descriptor, buffer, length);
    return bytesread;
#endif

    /*-----------------------------------------------------*\
    | Return 0 on unsupported platforms                     |
    \*-----------------------------------------------------*/
    return 0;
}

/*---------------------------------------------------------*\
|  serial_write                                             |
|    Writes <length> bytes to the serial port from <buffer> |
|    Returns the number of bytes actually written           |
|    Does not check for null-termination, so if <length> is |
|    greater than the number of bytes in <buffer>, it will  |
|    read past <buffer> and may cause a segfault            |
\*---------------------------------------------------------*/
int serial_port::serial_write(char * buffer, int length)
{
    /*-----------------------------------------------------*\
    | Windows-specific code path for serial write           |
    \*-----------------------------------------------------*/
#ifdef _WIN32
    DWORD byteswritten;
    WriteFile(file_descriptor, buffer, length, &byteswritten, NULL);
    return byteswritten;
#endif

    /*-----------------------------------------------------*\
    | Linux-specific code path for serial write             |
    \*-----------------------------------------------------*/
#ifdef __linux__
    int byteswritten;
    tcdrain(file_descriptor);
    byteswritten = write(file_descriptor, buffer, length);
    tcdrain(file_descriptor);
    return byteswritten;
#endif

    /*-----------------------------------------------------*\
    | MacOS-specific code path for serial write             |
    \*-----------------------------------------------------*/
#ifdef __APPLE__
    int byteswritten;
    tcdrain(file_descriptor);
    byteswritten = write(file_descriptor, buffer, length);
    tcdrain(file_descriptor);
    return byteswritten;
#endif

    /*-----------------------------------------------------*\
    | Return 0 on unsupported platforms                     |
    \*-----------------------------------------------------*/
    return 0;
}

/*---------------------------------------------------------*\
|  serial_flush                                             |
\*---------------------------------------------------------*/
void serial_port::serial_flush_rx()
{
#ifdef _WIN32
    PurgeComm(file_descriptor, PURGE_RXABORT | PURGE_RXCLEAR);
#endif

#ifdef __linux__
    tcflush(file_descriptor, TCIFLUSH);
#endif

#ifdef __APPLE__
    tcflush(file_descriptor, TCIFLUSH);
#endif
}

/*---------------------------------------------------------*\
|  serial_flush_tx                                          |
\*---------------------------------------------------------*/
void serial_port::serial_flush_tx()
{
#ifdef _WIN32
    PurgeComm(file_descriptor, PURGE_TXABORT | PURGE_TXCLEAR);
#endif

#ifdef __linux__
    tcflush(file_descriptor, TCOFLUSH);
#endif

#ifdef __APPLE__
    tcflush(file_descriptor, TCOFLUSH);
#endif
}

/*---------------------------------------------------------*\
|  serial_break                                             |
\*---------------------------------------------------------*/
void serial_port::serial_break()
{
    /*-----------------------------------------------------*\
    | Windows-specific code path for serial break           |
    \*-----------------------------------------------------*/
#ifdef _WIN32
    SetCommBreak(file_descriptor);
    Sleep(1);
    ClearCommBreak(file_descriptor);
#endif

    /*-----------------------------------------------------*\
    | Linux-specific code path for serial break             |
    \*-----------------------------------------------------*/
#ifdef __linux__
    //Send break for at least 1 ms
    ioctl(file_descriptor, TIOCSBRK);
    usleep(1000);
    ioctl(file_descriptor, TIOCCBRK);
#endif

    /*-----------------------------------------------------*\
    | MacOS-specific code path for serial break             |
    \*-----------------------------------------------------*/
#ifdef __APPLE__
    //Send break for at least 1 ms
    ioctl(file_descriptor, TIOCSBRK);
    usleep(1000);
    ioctl(file_descriptor, TIOCCBRK);
#endif
}

void serial_port::serial_set_dtr(bool dtr)
{
    /*-----------------------------------------------------*\
    | Windows-specific code path for serial set DTR         |
    \*-----------------------------------------------------*/
#ifdef _WIN32
    if(dtr)
    {
        EscapeCommFunction(file_descriptor, SETDTR);
    }
    else
    {
        EscapeCommFunction(file_descriptor, CLRDTR);
    }
#endif

    /*-----------------------------------------------------*\
    | Linux-specific code path for serial set DTR           |
    \*-----------------------------------------------------*/
#ifdef __linux__
    const int DTRFLAG = TIOCM_DTR;
    if(dtr)
    {
        ioctl(file_descriptor, TIOCMBIS, &DTRFLAG);
    }
    else
    {
        ioctl(file_descriptor, TIOCMBIC, &DTRFLAG);
    }
#endif

    /*-----------------------------------------------------*\
    | MacOS-specific code path for serial set DTR           |
    \*-----------------------------------------------------*/
#ifdef __APPLE__
    const int DTRFLAG = TIOCM_DTR;
    if(dtr)
    {
        ioctl(file_descriptor, TIOCMBIS, &DTRFLAG);
    }
    else
    {
        ioctl(file_descriptor, TIOCMBIC, &DTRFLAG);
    }
#endif
}

void serial_port::serial_set_rts(bool rts)
{
    /*-----------------------------------------------------*\
    | Windows-specific code path for serial set RTS         |
    \*-----------------------------------------------------*/
#ifdef _WIN32
    if(rts)
    {
        EscapeCommFunction(file_descriptor, SETRTS);
    }
    else
    {
        EscapeCommFunction(file_descriptor, CLRRTS);
    }
#endif

    /*-----------------------------------------------------*\
    | Linux-specific code path for serial set RTS           |
    \*-----------------------------------------------------*/
#ifdef __linux__
    const int RTSFLAG = TIOCM_RTS;
    if(rts)
    {
        ioctl(file_descriptor, TIOCMBIS, &RTSFLAG);
    }
    else
    {
        ioctl(file_descriptor, TIOCMBIC, &RTSFLAG);
    }
#endif

    /*-----------------------------------------------------*\
    | MacOS-specific code path for serial set RTS           |
    \*-----------------------------------------------------*/
#ifdef __APPLE__
    const int RTSFLAG = TIOCM_RTS;
    if(rts)
    {
        ioctl(file_descriptor, TIOCMBIS, &RTSFLAG);
    }
    else
    {
        ioctl(file_descriptor, TIOCMBIC, &RTSFLAG);
    }
#endif
}
