123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
/*******************************************************************************

        copyright:      Copyright (c) 2008 Robin Kreis. All rights reserved

        license:        BSD style: $(LICENSE)

        author:         Robin Kreis

*******************************************************************************/

module tango.io.device.SerialPort;

private import  tango.core.Array : sort;

private import  tango.core.Exception,
                tango.io.device.Device,
                tango.stdc.stringz,
                tango.sys.Common;

version(Windows)
{
private import Integer = tango.text.convert.Integer;
private import tango.stdc.stringz;
}
else
version(Posix)
{
private import  tango.io.FilePath,
                tango.stdc.posix.termios;
}

/*******************************************************************************

        Enables applications to use a serial port (aka COM-port, ttyS).
        Usage is similar to that of File:
        ---
        auto serCond = new SerialPort("ttyS0");
        serCond.speed = 38400;
        serCond.write("Hello world!");
        serCond.close();
        ----

*******************************************************************************/

class SerialPort : Device
{
    private const(char)[]              str;
    private static const(char)[][]     _ports;

    /***************************************************************************

            Create a new SerialPort instance. The port will be opened and
            set to raw mode with 9600-8N1.

            Params:
            port = A string identifying the port. On Posix, this must be a
                   device file like /dev/ttyS0. If the input doesn't begin
                   with "/", "/dev/" is automatically prepended, so "ttyS0"
                   is sufficent. On Windows, this must be a device name like
                   COM1.

    ***************************************************************************/

    this (const(char)[] port)
    {
        create (port);
    }

    /***************************************************************************

            Returns a string describing this serial port.
            For example: "ttyS0", "COM1", "cuad0".

    ***************************************************************************/

    override string toString ()
    {
        return str.idup;
    }

    /***************************************************************************

            Sets the baud rate of this port. Usually, the baud rate can
            only be set to fixed values (common values are 1200 * 2^n).

            Note that for Posix, the specification only mandates speeds up
            to 38400, excluding speeds such as 7200, 14400 and 28800.
            Most Posix systems have chosen to support at least higher speeds
            though.

            See_also: maxSpeed

            Throws: IOException if speed is unsupported.

    ***************************************************************************/

    SerialPort speed (uint speed)
    {
        version(Posix) {
            speed_t *baud = speed in baudRates;
            if(baud is null) {
                throw new IOException("Invalid baud rate.");
            }

            termios options;
            tcgetattr(handle, &options);
            cfsetospeed(&options, *baud);
            tcsetattr(handle, TCSANOW, &options);
        }
        version(Win32) {
            DCB config;
            GetCommState(io.handle, &config);
            config.BaudRate = speed;
            if(!SetCommState(io.handle, &config)) error();
        }
        return this;
    }

    /***************************************************************************

            Tries to enumerate all serial ports. While this usually works on
            Windows, it's more problematic on other OS. Posix provides no way
            to list serial ports, and the only option is searching through
            "/dev".

            Because there's no naming standard for the device files, this method
            must be ported for each OS. This method is also unreliable because
            the user could have created invalid device files, or deleted them.

            Returns:
            A string array of all the serial ports that could be found, in
            alphabetical order. Every string is formatted as a valid argument
            to the constructor, but the port may not be accessible.

    ***************************************************************************/

    static const(char)[][] ports ()
    {
        if(_ports !is null) {
            return _ports;
        }
        version(Windows) {
            // try opening COM1...COM255
            auto pre = `\\.\COM`;
            char[11] p = void;
            char[3] num = void;
            p[0..pre.length] = pre;
            for(int i = 1; i <= 255; ++i) {
                char[] portNum = Integer.format(num, i);
                p[pre.length..pre.length + portNum.length] = portNum;
                p[pre.length + portNum.length] = '\0';
                HANDLE port = CreateFileA(p.ptr, GENERIC_READ | GENERIC_WRITE, 0, null, OPEN_EXISTING, 0, null);
                if(port != INVALID_HANDLE_VALUE) {
                    _ports ~= p[`\\.\`.length..$].dup; // cut the leading \\.\
                    CloseHandle(port);
                }
            }
        } else version(Posix) {
            auto dev = FilePath("/dev".dup);
            FilePath[] serPorts = dev.toList((FilePath path, bool isFolder) {
                if(isFolder) return false;
                version(linux) {
                    auto r = rest(path.name, "ttyUSB");
                    if(r is null) r = rest(path.name, "ttyS");
                    if(r.length == 0) return false;
                    return isInRange(r, '0', '9');
                } else version (darwin) { // untested
                    auto r = rest(path.name, "cu");
                    if(r.length == 0) return false;
                    return true;
                } else version(FreeBSD) { // untested
                    auto r = rest(path.name, "cuaa");
                    if(r is null) r = rest(path.name, "cuad");
                    if(r.length == 0) return false;
                    return isInRange(r, '0', '9');
                } else version(openbsd) { // untested
                    auto r = rest(path.name, "tty");
                    if(r.length != 2) return false;
                    return isInRange(r, '0', '9');
                } else version(solaris) { // untested
                    auto r = rest(path.name, "tty");
                    if(r.length != 1) return false;
                    return isInRange(r, 'a', 'z');
                } else {
                    return false;
                }
            });
            _ports.length = serPorts.length;
            foreach(i, path; serPorts) {
                _ports[i] = path.name;
            }
        }
        sort(_ports);
        return _ports;
    }

    version(Win32) {
        private void create (const(char)[] port)
        {
            str = port;
            io.handle = CreateFileA((`\\.\` ~ port).toStringz(), GENERIC_READ | GENERIC_WRITE, 0, null, OPEN_EXISTING, 0, null);
            if(io.handle is INVALID_HANDLE_VALUE) {
                error();
            }
            DCB config;
            GetCommState(io.handle, &config);
            config.BaudRate = 9600;
            config.ByteSize = 8;
            config.Parity = NOPARITY;
            config.StopBits = ONESTOPBIT;
            config.flag0 |= bm_DCB_fBinary | bm_DCB_fParity;
            if(!SetCommState(io.handle, &config)) error();
        }
    }

    version(Posix) {
        private __gshared static speed_t[uint] baudRates;

        shared static this()
        {
            baudRates[50] = B50;
            baudRates[75] = B75;
            baudRates[110] = B110;
            baudRates[134] = B134;
            baudRates[150] = B150;
            baudRates[200] = B200;
            baudRates[300] = B300;
            baudRates[600] = B600;
            baudRates[1200] = B1200;
            baudRates[1800] = B1800;
            baudRates[2400] = B2400;
            baudRates[9600] = B9600;
            baudRates[4800] = B4800;
            baudRates[19200] = B19200;
            baudRates[38400] = B38400;

            version( linux )
            {
                baudRates[57600] = B57600;
                baudRates[115200] = B115200;
                baudRates[230400] = B230400;
                baudRates[460800] = B460800;
                baudRates[500000] = B500000;
                baudRates[576000] = B576000;
                baudRates[921600] = B921600;
                baudRates[1000000] = B1000000;
                baudRates[1152000] = B1152000;
                baudRates[1500000] = B1500000;
                baudRates[2000000] = B2000000;
                baudRates[2500000] = B2500000;
                baudRates[3000000] = B3000000;
                baudRates[3500000] = B3500000;
                baudRates[4000000] = B4000000;
            }
            else version( FreeBSD )
            {
                baudRates[7200] = B7200;
                baudRates[14400] = B14400;
                baudRates[28800] = B28800;
                baudRates[57600] = B57600;
                baudRates[76800] = B76800;
                baudRates[115200] = B115200;
                baudRates[230400] = B230400;
                baudRates[460800] = B460800;
                baudRates[921600] = B921600;
            }
            else version( solaris )
            {
                baudRates[57600] = B57600;
                baudRates[76800] = B76800;
                baudRates[115200] = B115200;
                baudRates[153600] = B153600;
                baudRates[230400] = B230400;
                baudRates[307200] = B307200;
                baudRates[460800] = B460800;
            }
            else version ( darwin )
            {
                baudRates[7200] = B7200;
                baudRates[14400] = B14400;
                baudRates[28800] = B28800;
                baudRates[57600] = B57600;
                baudRates[76800] = B76800;
                baudRates[115200] = B115200;
                baudRates[230400] = B230400;
            }
        }

        private void create (const(char)[] file)
        {
            if(file.length == 0) throw new IOException("Empty port name");
            if(file[0] != '/') file = "/dev/" ~ file;

            if(file.length > 5 && file[0..5] == "/dev/")
                str = file[5..$];
            else
                str = "SerialPort@" ~ file;

            handle = posix.open(file.toStringz(), O_RDWR | O_NOCTTY | O_NONBLOCK);
            if(handle == -1) {
                error();
            }
            if(posix.fcntl(handle, F_SETFL, 0) == -1) { // disable O_NONBLOCK
                error();
            }

            termios options;
            if(tcgetattr(handle, &options) == -1) {
                error();
            }
            cfsetispeed(&options, B0); // same as output baud rate
            cfsetospeed(&options, B9600);
            makeRaw(&options); // disable echo and special characters
            tcsetattr(handle, TCSANOW, &options);
        }

        private void makeRaw (termios *options)
        {
            options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
                    | INLCR | IGNCR | ICRNL | IXON);
            options.c_oflag &= ~OPOST;
            options.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
            options.c_cflag &= ~(CSIZE | PARENB);
            options.c_cflag |= CS8;
        }


        private static inout(char)[] rest (inout(char)[] str, in char[] prefix) {
            if(str.length < prefix.length) return null;
            if(str[0..prefix.length] != prefix) return null;
            return str[prefix.length..$];
        }

        private static bool isInRange (const(char)[] str, char lower, char upper) {
            foreach(c; str) {
                if(c < lower || c > upper) return false;
            }
            return true;
        }
    }
}