#!/usr/bin/env python
#
# backend for serial IO for POSIX compatible systems, like Linux, OSX
#
# This file is part of pySerial. https://github.com/pyserial/pyserial
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
#
# SPDX-License-Identifier:    BSD-3-Clause
#
# parts based on code from Grant B. Edwards  <grante@visi.com>:
#  ftp://ftp.visi.com/users/grante/python/PosixSerial.py
#
# references: http://www.easysw.com/~mike/serial/serial.html

# Collection of port names (was previously used by number_to_device which was
# removed.
# - Linux                   /dev/ttyS%d (confirmed)
# - cygwin/win32            /dev/com%d (confirmed)
# - openbsd (OpenBSD)       /dev/cua%02d
# - bsd*, freebsd*          /dev/cuad%d
# - darwin (OS X)           /dev/cuad%d
# - netbsd                  /dev/dty%02d (NetBSD 1.6 testing by Erk)
# - irix (IRIX)             /dev/ttyf%d (partially tested) names depending on flow control
# - hp (HP-UX)              /dev/tty%dp0 (not tested)
# - sunos (Solaris/SunOS)   /dev/tty%c (letters, 'a'..'z') (confirmed)
# - aix (AIX)               /dev/tty%d


from __future__ import absolute_import

# pylint: disable=abstract-method
import errno
import fcntl
import os
import select
import struct
import sys
import termios

import serial
from serial.serialutil import SerialBase, SerialException, to_bytes, \
    PortNotOpenError, SerialTimeoutException, Timeout


class PlatformSpecificBase(object):
    BAUDRATE_CONSTANTS = {}

    def _set_special_baudrate(self, baudrate):
        raise NotImplementedError('non-standard baudrates are not supported on this platform')

    def _set_rs485_mode(self, rs485_settings):
        raise NotImplementedError('RS485 not supported on this platform')

    def set_low_latency_mode(self, low_latency_settings):
        raise NotImplementedError('Low latency not supported on this platform')

    def _update_break_state(self):
        """\
        Set break: Controls TXD. When active, no transmitting is possible.
        """
        if self._break_state:
            fcntl.ioctl(self.fd, TIOCSBRK)
        else:
            fcntl.ioctl(self.fd, TIOCCBRK)
    

# some systems support an extra flag to enable the two in POSIX unsupported
# paritiy settings for MARK and SPACE
CMSPAR = 0  # default, for unsupported platforms, override below

# try to detect the OS so that a device can be selected...
# this code block should supply a device() and set_special_baudrate() function
# for the platform
plat = sys.platform.lower()

if plat[:5] == 'linux':    # Linux (confirmed)  # noqa
    import array

    # extra termios flags
    CMSPAR = 0o10000000000  # Use "stick" (mark/space) parity

    # baudrate ioctls
    TCGETS2 = 0x802C542A
    TCSETS2 = 0x402C542B
    BOTHER = 0o010000

    # RS485 ioctls
    TIOCGRS485 = 0x542E
    TIOCSRS485 = 0x542F
    SER_RS485_ENABLED = 0b00000001
    SER_RS485_RTS_ON_SEND = 0b00000010
    SER_RS485_RTS_AFTER_SEND = 0b00000100
    SER_RS485_RX_DURING_TX = 0b00010000

    class PlatformSpecific(PlatformSpecificBase):
        BAUDRATE_CONSTANTS = {
            0:       0o000000,  # hang up
            50:      0o000001,
            75:      0o000002,
            110:     0o000003,
            134:     0o000004,
            150:     0o000005,
            200:     0o000006,
            300:     0o000007,
            600:     0o000010,
            1200:    0o000011,
            1800:    0o000012,
            2400:    0o000013,
            4800:    0o000014,
            9600:    0o000015,
            19200:   0o000016,
            38400:   0o000017,
            57600:   0o010001,
            115200:  0o010002,
            230400:  0o010003,
            460800:  0o010004,
            500000:  0o010005,
            576000:  0o010006,
            921600:  0o010007,
            1000000: 0o010010,
            1152000: 0o010011,
            1500000: 0o010012,
            2000000: 0o010013,
            2500000: 0o010014,
            3000000: 0o010015,
            3500000: 0o010016,
            4000000: 0o010017
        }

        def set_low_latency_mode(self, low_latency_settings):
            buf = array.array('i', [0] * 32)

            try:
                # get serial_struct
                fcntl.ioctl(self.fd, termios.TIOCGSERIAL, buf)

                # set or unset ASYNC_LOW_LATENCY flag
                if low_latency_settings:
                    buf[4] |= 0x2000
                else:
                    buf[4] &= ~0x2000

                # set serial_struct
                fcntl.ioctl(self.fd, termios.TIOCSSERIAL, buf)
            except IOError as e:
                raise ValueError('Failed to update ASYNC_LOW_LATENCY flag to {}: {}'.format(low_latency_settings, e))

        def _set_special_baudrate(self, baudrate):
            # right size is 44 on x86_64, allow for some growth
            buf = array.array('i', [0] * 64)
            try:
                # get serial_struct
                fcntl.ioctl(self.fd, TCGETS2, buf)
                # set custom speed
                buf[2] &= ~termios.CBAUD
                buf[2] |= BOTHER
                buf[9] = buf[10] = baudrate

                # set serial_struct
                fcntl.ioctl(self.fd, TCSETS2, buf)
            except IOError as e:
                raise ValueError('Failed to set custom baud rate ({}): {}'.format(baudrate, e))

        def _set_rs485_mode(self, rs485_settings):
            buf = array.array('i', [0] * 8)  # flags, delaytx, delayrx, padding
            try:
                fcntl.ioctl(self.fd, TIOCGRS485, buf)
                buf[0] |= SER_RS485_ENABLED
                if rs485_settings is not None:
                    if rs485_settings.loopback:
                        buf[0] |= SER_RS485_RX_DURING_TX
                    else:
                        buf[0] &= ~SER_RS485_RX_DURING_TX
                    if rs485_settings.rts_level_for_tx:
                        buf[0] |= SER_RS485_RTS_ON_SEND
                    else:
                        buf[0] &= ~SER_RS485_RTS_ON_SEND
                    if rs485_settings.rts_level_for_rx:
                        buf[0] |= SER_RS485_RTS_AFTER_SEND
                    else:
                        buf[0] &= ~SER_RS485_RTS_AFTER_SEND
                    if rs485_settings.delay_before_tx is not None:
                        buf[1] = int(rs485_settings.delay_before_tx * 1000)
                    if rs485_settings.delay_before_rx is not None:
                        buf[2] = int(rs485_settings.delay_before_rx * 1000)
                else:
                    buf[0] = 0  # clear SER_RS485_ENABLED
                fcntl.ioctl(self.fd, TIOCSRS485, buf)
            except IOError as e:
                raise ValueError('Failed to set RS485 mode: {}'.format(e))


elif plat == 'cygwin':       # cygwin/win32 (confirmed)

    class PlatformSpecific(PlatformSpecificBase):
        BAUDRATE_CONSTANTS = {
            128000: 0x01003,
            256000: 0x01005,
            500000: 0x01007,
            576000: 0x01008,
            921600: 0x01009,
            1000000: 0x0100a,
            1152000: 0x0100b,
            1500000: 0x0100c,
            2000000: 0x0100d,
            2500000: 0x0100e,
            3000000: 0x0100f
        }


elif plat[:6] == 'darwin':   # OS X
    import array
    IOSSIOSPEED = 0x80045402  # _IOW('T', 2, speed_t)

    class PlatformSpecific(PlatformSpecificBase):
        osx_version = os.uname()[2].split('.')
        TIOCSBRK = 0x2000747B # _IO('t', 123)
        TIOCCBRK = 0x2000747A # _IO('t', 122)

        # Tiger or above can support arbitrary serial speeds
        if int(osx_version[0]) >= 8:
            def _set_special_baudrate(self, baudrate):
                # use IOKit-specific call to set up high speeds
                buf = array.array('i', [baudrate])
                fcntl.ioctl(self.fd, IOSSIOSPEED, buf, 1)

        def _update_break_state(self):
            """\
            Set break: Controls TXD. When active, no transmitting is possible.
            """
            if self._break_state:
                fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
            else:
                fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)

elif plat[:3] == 'bsd' or \
     plat[:7] == 'freebsd' or \
     plat[:6] == 'netbsd' or \
     plat[:7] == 'openbsd':

    class ReturnBaudrate(object):
        def __getitem__(self, key):
            return key

    class PlatformSpecific(PlatformSpecificBase):
        # Only tested on FreeBSD:
        # The baud rate may be passed in as
        # a literal value.
        BAUDRATE_CONSTANTS = ReturnBaudrate()

        TIOCSBRK = 0x2000747B # _IO('t', 123)
        TIOCCBRK = 0x2000747A # _IO('t', 122)

        
        def _update_break_state(self):
            """\
            Set break: Controls TXD. When active, no transmitting is possible.
            """
            if self._break_state:
                fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
            else:
                fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)

else:
    class PlatformSpecific(PlatformSpecificBase):
        pass


# load some constants for later use.
# try to use values from termios, use defaults from linux otherwise
TIOCMGET = getattr(termios, 'TIOCMGET', 0x5415)
TIOCMBIS = getattr(termios, 'TIOCMBIS', 0x5416)
TIOCMBIC = getattr(termios, 'TIOCMBIC', 0x5417)
TIOCMSET = getattr(termios, 'TIOCMSET', 0x5418)

# TIOCM_LE = getattr(termios, 'TIOCM_LE', 0x001)
TIOCM_DTR = getattr(termios, 'TIOCM_DTR', 0x002)
TIOCM_RTS = getattr(termios, 'TIOCM_RTS', 0x004)
# TIOCM_ST = getattr(termios, 'TIOCM_ST', 0x008)
# TIOCM_SR = getattr(termios, 'TIOCM_SR', 0x010)

TIOCM_CTS = getattr(termios, 'TIOCM_CTS', 0x020)
TIOCM_CAR = getattr(termios, 'TIOCM_CAR', 0x040)
TIOCM_RNG = getattr(termios, 'TIOCM_RNG', 0x080)
TIOCM_DSR = getattr(termios, 'TIOCM_DSR', 0x100)
TIOCM_CD = getattr(termios, 'TIOCM_CD', TIOCM_CAR)
TIOCM_RI = getattr(termios, 'TIOCM_RI', TIOCM_RNG)
# TIOCM_OUT1 = getattr(termios, 'TIOCM_OUT1', 0x2000)
# TIOCM_OUT2 = getattr(termios, 'TIOCM_OUT2', 0x4000)
if hasattr(termios, 'TIOCINQ'):
    TIOCINQ = termios.TIOCINQ
else:
    TIOCINQ = getattr(termios, 'FIONREAD', 0x541B)
TIOCOUTQ = getattr(termios, 'TIOCOUTQ', 0x5411)

TIOCM_zero_str = struct.pack('I', 0)
TIOCM_RTS_str = struct.pack('I', TIOCM_RTS)
TIOCM_DTR_str = struct.pack('I', TIOCM_DTR)

TIOCSBRK = getattr(termios, 'TIOCSBRK', 0x5427)
TIOCCBRK = getattr(termios, 'TIOCCBRK', 0x5428)


class Serial(SerialBase, PlatformSpecific):
    """\
    Serial port class POSIX implementation. Serial port configuration is
    done with termios and fcntl. Runs on Linux and many other Un*x like
    systems.
    """

    def open(self):
        """\
        Open port with current settings. This may throw a SerialException
        if the port cannot be opened."""
        if self._port is None:
            raise SerialException("Port must be configured before it can be used.")
        if self.is_open:
            raise SerialException("Port is already open.")
        self.fd = None
        # open
        try:
            self.fd = os.open(self.portstr, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
        except OSError as msg:
            self.fd = None
            raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
        #~ fcntl.fcntl(self.fd, fcntl.F_SETFL, 0)  # set blocking

        self.pipe_abort_read_r, self.pipe_abort_read_w = None, None
        self.pipe_abort_write_r, self.pipe_abort_write_w = None, None

        try:
            self._reconfigure_port(force_update=True)

            try:
                if not self._dsrdtr:
                    self._update_dtr_state()
                if not self._rtscts:
                    self._update_rts_state()
            except IOError as e:
                # ignore Invalid argument and Inappropriate ioctl
                if e.errno not in (errno.EINVAL, errno.ENOTTY):
                    raise

            self._reset_input_buffer()

            self.pipe_abort_read_r, self.pipe_abort_read_w = os.pipe()
            self.pipe_abort_write_r, self.pipe_abort_write_w = os.pipe()
            fcntl.fcntl(self.pipe_abort_read_r, fcntl.F_SETFL, os.O_NONBLOCK)
            fcntl.fcntl(self.pipe_abort_write_r, fcntl.F_SETFL, os.O_NONBLOCK)
        except BaseException:
            try:
                os.close(self.fd)
            except Exception:
                # ignore any exception when closing the port
                # also to keep original exception that happened when setting up
                pass
            self.fd = None

            if self.pipe_abort_read_w is not None:
                os.close(self.pipe_abort_read_w)