root/src/add-ons/kernel/drivers/ports/usb_serial/SerialDevice.cpp
/*
 * Copyright (c) 2007-2008 by Michael Lotz
 * Heavily based on the original usb_serial driver which is:
 *
 * Copyright (c) 2003 by Siarzhuk Zharski <imker@gmx.li>
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Alexander von Gluck IV, kallisti5@unixzen.com
 */


#include <new>

#include "SerialDevice.h"
#include "USB3.h"

#include "ACM.h"
#include "FTDI.h"
#include "KLSI.h"
#include "Option.h"
#include "Prolific.h"
#include "Silicon.h"
#include "WinChipHead.h"

#include <sys/ioctl.h>


SerialDevice::SerialDevice(usb_device device, uint16 vendorID,
        uint16 productID, const char *description)
        :       fDevice(device),
                fVendorID(vendorID),
                fProductID(productID),
                fDescription(description),
                fDeviceOpen(false),
                fDeviceRemoved(false),
                fControlPipe(0),
                fReadPipe(0),
                fWritePipe(0),
                fBufferArea(-1),
                fReadBuffer(NULL),
                fReadBufferSize(ROUNDUP(DEF_BUFFER_SIZE, 16)),
                fOutputBuffer(NULL),
                fOutputBufferSize(ROUNDUP(DEF_BUFFER_SIZE, 16)),
                fWriteBuffer(NULL),
                fWriteBufferSize(ROUNDUP(DEF_BUFFER_SIZE, 16)),
                fInterruptBuffer(NULL),
                fInterruptBufferSize(16),
                fDoneRead(-1),
                fDoneWrite(-1),
                fControlOut(0),
                fInputStopped(false),
                fMasterTTY(NULL),
                fSlaveTTY(NULL),
                fSystemTTYCookie(NULL),
                fDeviceTTYCookie(NULL),
                fInputThread(-1),
                fStopThreads(false)
{
        memset(&fTTYConfig, 0, sizeof(termios));
        fTTYConfig.c_cflag = B9600 | CS8 | CREAD;
}


SerialDevice::~SerialDevice()
{
        Removed();

        if (fDoneRead >= 0)
                delete_sem(fDoneRead);
        if (fDoneWrite >= 0)
                delete_sem(fDoneWrite);

        if (fBufferArea >= 0)
                delete_area(fBufferArea);
}


status_t
SerialDevice::Init()
{
        fDoneRead = create_sem(0, "usb_serial:done_read");
        if (fDoneRead < 0)
                return fDoneRead;

        fDoneWrite = create_sem(0, "usb_serial:done_write");
        if (fDoneWrite < 0)
                return fDoneWrite;

        size_t totalBuffers = fReadBufferSize + fOutputBufferSize + fWriteBufferSize
                + fInterruptBufferSize;
        fBufferArea = create_area("usb_serial:buffers_area", (void **)&fReadBuffer,
                B_ANY_KERNEL_ADDRESS, ROUNDUP(totalBuffers, B_PAGE_SIZE), B_CONTIGUOUS,
                B_KERNEL_READ_AREA | B_KERNEL_WRITE_AREA);
        if (fBufferArea < 0)
                return fBufferArea;

        fOutputBuffer = fReadBuffer + fReadBufferSize;
        fWriteBuffer = fOutputBuffer + fOutputBufferSize;
        fInterruptBuffer = fWriteBuffer + fWriteBufferSize;
        return B_OK;
}


void
SerialDevice::SetControlPipe(usb_pipe handle)
{
        fControlPipe = handle;
}


void
SerialDevice::SetReadPipe(usb_pipe handle)
{
        fReadPipe = handle;
}


void
SerialDevice::SetWritePipe(usb_pipe handle)
{
        fWritePipe = handle;
}


inline int32
baud_index_to_speed(int index)
{
        switch (index) {
                case B0: return 0;
                case B50: return 50;
                case B75: return 75;
                case B110: return 110;
                case B134: return 134;
                case B150: return 150;
                case B200: return 200;
                case B300: return 300;
                case B600: return 600;
                case B1200: return 1200;
                case B1800: return 1800;
                case B2400: return 2400;
                case B4800: return 4800;
                case B9600: return 9600;
                case B19200: return 19200;
                case B31250: return 31250;
                case B38400: return 38400;
                case B57600: return 57600;
                case B115200: return 115200;
                case B230400: return 230400;
        }

        TRACE_ALWAYS("invalid baud index %d\n", index);
        return -1;
}


void
SerialDevice::SetModes(struct termios *tios)
{
        TRACE_FUNCRES(trace_termios, tios);

        uint8 baud = tios->c_cflag & CBAUD;
        int32 speed;
        if (baud == CBAUD) {
                speed = tios->c_ospeed + (tios->c_ospeed_high << 16);
        } else {
                speed = baud_index_to_speed(baud);
        }

        // update our master config in full
        memcpy(&fTTYConfig, tios, sizeof(termios));
        fTTYConfig.c_cflag &= ~CBAUD;
        fTTYConfig.c_cflag |= baud;

        // only apply the relevant parts to the device side
        termios config;
        memset(&config, 0, sizeof(termios));
        config.c_cflag = tios->c_cflag;
        config.c_cflag &= ~CBAUD;
        config.c_cflag |= baud;

        // update the termios of the device side
        gTTYModule->tty_control(fDeviceTTYCookie, TCSETA, &config, sizeof(termios));

        SetHardwareFlowControl((tios->c_cflag & CRTSCTS) != 0);

        usb_cdc_line_coding lineCoding;
        lineCoding.speed = speed;
        lineCoding.stopbits = (tios->c_cflag & CSTOPB)
                ? USB_CDC_LINE_CODING_2_STOPBITS : USB_CDC_LINE_CODING_1_STOPBIT;

        if (tios->c_cflag & PARENB) {
                lineCoding.parity = USB_CDC_LINE_CODING_EVEN_PARITY;
                if (tios->c_cflag & PARODD)
                        lineCoding.parity = USB_CDC_LINE_CODING_ODD_PARITY;
        } else
                lineCoding.parity = USB_CDC_LINE_CODING_NO_PARITY;

        lineCoding.databits = (tios->c_cflag & CS8) ? 8 : 7;

        if (memcmp(&lineCoding, &fLineCoding, sizeof(usb_cdc_line_coding)) != 0) {
                fLineCoding.speed = lineCoding.speed;
                fLineCoding.stopbits = lineCoding.stopbits;
                fLineCoding.databits = lineCoding.databits;
                fLineCoding.parity = lineCoding.parity;
                TRACE("send to modem: speed %d sb: 0x%08x db: 0x%08x parity: 0x%08x\n",
                        fLineCoding.speed, fLineCoding.stopbits, fLineCoding.databits,
                        fLineCoding.parity);
                SetLineCoding(&fLineCoding);
        }
}


bool
SerialDevice::Service(struct tty *tty, uint32 op, void *buffer, size_t length)
{
        if (!fDeviceOpen)
                return false;

        if (tty != fMasterTTY)
                return false;

        switch (op) {
                case TTYENABLE:
                {
                        bool enable = *(bool *)buffer;
                        TRACE("TTYENABLE: %sable\n", enable ? "en" : "dis");

                        gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWDCD, enable);
                        gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWCTS, enable);

                        fControlOut = enable ? USB_CDC_CONTROL_SIGNAL_STATE_DTR
                                | USB_CDC_CONTROL_SIGNAL_STATE_RTS : 0;
                        SetControlLineState(fControlOut);
                        return true;
                }

                case TTYISTOP:
                        fInputStopped = *(bool *)buffer;
                        TRACE("TTYISTOP: %sstopped\n", fInputStopped ? "" : "not ");
                        gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWCTS,
                                !fInputStopped);
                        return true;

                case TTYGETSIGNALS:
                        TRACE("TTYGETSIGNALS\n");
                        gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWDCD,
                                (fControlOut & (USB_CDC_CONTROL_SIGNAL_STATE_DTR
                                        | USB_CDC_CONTROL_SIGNAL_STATE_RTS)) != 0);
                        gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWCTS,
                                !fInputStopped);
                        gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWDSR, false);
                        gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWRI, false);
                        return true;

                case TTYSETMODES:
                        TRACE("TTYSETMODES\n");
                        SetModes((struct termios *)buffer);
                        return true;

                case TTYSETDTR:
                case TTYSETRTS:
                {
                        bool set = *(bool *)buffer;
                        uint8 bit = op == TTYSETDTR ? USB_CDC_CONTROL_SIGNAL_STATE_DTR
                                : USB_CDC_CONTROL_SIGNAL_STATE_RTS;
                        if (set)
                                fControlOut |= bit;
                        else
                                fControlOut &= ~bit;

                        SetControlLineState(fControlOut);
                        return true;
                }

                case TTYOSTART:
                case TTYOSYNC:
                case TTYSETBREAK:
                case TTYFLUSH:
                        TRACE("TTY other\n");
                        return true;
        }

        return false;
}


status_t
SerialDevice::Open(uint32 flags)
{
        status_t status = B_OK;

        if (fDeviceOpen)
                return B_BUSY;

        if (fDeviceRemoved)
                return B_DEV_NOT_READY;

        status = gTTYModule->tty_create(usb_serial_service, NULL, &fMasterTTY);
        if (status != B_OK) {
                TRACE_ALWAYS("open: failed to init master tty\n");
                return status;
        }

        status = gTTYModule->tty_create(usb_serial_service, fMasterTTY, &fSlaveTTY);
        if (status != B_OK) {
                TRACE_ALWAYS("open: failed to init slave tty\n");
                gTTYModule->tty_destroy(fMasterTTY);
                return status;
        }

        status = gTTYModule->tty_create_cookie(fMasterTTY, fSlaveTTY, O_RDWR, &fSystemTTYCookie);
        if (status != B_OK) {
                TRACE_ALWAYS("open: failed to init system tty cookie\n");
                gTTYModule->tty_destroy(fMasterTTY);
                gTTYModule->tty_destroy(fSlaveTTY);
                return status;
        }

        status = gTTYModule->tty_create_cookie(fSlaveTTY, fMasterTTY, O_RDWR, &fDeviceTTYCookie);
        if (status != B_OK) {
                TRACE_ALWAYS("open: failed to init device tty cookie\n");
                gTTYModule->tty_destroy_cookie(fSystemTTYCookie);
                gTTYModule->tty_destroy(fMasterTTY);
                gTTYModule->tty_destroy(fSlaveTTY);
                return status;
        }

        ResetDevice();

        fStopThreads = false;

        fInputThread = spawn_kernel_thread(_InputThread,
                "usb_serial input thread", B_NORMAL_PRIORITY, this);
        if (fInputThread < 0) {
                TRACE_ALWAYS("open: failed to spawn input thread\n");
                return fInputThread;
        }

        resume_thread(fInputThread);

        fControlOut = USB_CDC_CONTROL_SIGNAL_STATE_DTR
                | USB_CDC_CONTROL_SIGNAL_STATE_RTS;
        SetControlLineState(fControlOut);

        status = gUSBModule->queue_interrupt(fControlPipe, fInterruptBuffer, fInterruptBufferSize,
                _InterruptCallbackFunction, this);
        if (status < B_OK)
                TRACE_ALWAYS("failed to queue initial interrupt\n");

        // set our config (will propagate to the slave config as well in SetModes()
        gTTYModule->tty_control(fSystemTTYCookie, TCSETA, &fTTYConfig,
                sizeof(termios));

        fDeviceOpen = true;
        return B_OK;
}


status_t
SerialDevice::Read(char *buffer, size_t *numBytes)
{
        if (fDeviceRemoved) {
                *numBytes = 0;
                return B_DEV_NOT_READY;
        }

        return gTTYModule->tty_read(fSystemTTYCookie, buffer, numBytes);
}


status_t
SerialDevice::Write(const char *buffer, size_t *numBytes)
{
        if (fDeviceRemoved) {
                *numBytes = 0;
                return B_DEV_NOT_READY;
        }

        size_t bytesLeft = *numBytes;
        *numBytes = 0;

        while (bytesLeft > 0) {
                size_t length = MIN(bytesLeft, 256);
                        // TODO: This is an ugly hack; We use a small buffer size so that
                        // we don't overrun the tty line buffer and cause it to block. While
                        // that isn't a problem, we shouldn't just hardcode the value here.

                status_t result = gTTYModule->tty_write(fSystemTTYCookie, buffer,
                        &length);
                if (result != B_OK) {
                        TRACE_ALWAYS("failed to write to tty: %s\n", strerror(result));
                        return result;
                }

                buffer += length;
                *numBytes += length;
                bytesLeft -= length;

                while (true) {
                        // Write to the device as long as there's anything in the tty buffer
                        int readable = 0;
                        gTTYModule->tty_control(fDeviceTTYCookie, FIONREAD, &readable,
                                sizeof(readable));
                        if (readable == 0)
                                break;

                        result = _WriteToDevice();
                        if (result != B_OK) {
                                TRACE_ALWAYS("failed to write to device: %s\n",
                                        strerror(result));
                                return result;
                        }
                }
        }

        if (*numBytes > 0)
                return B_OK;

        return B_ERROR;
}


status_t
SerialDevice::Control(uint32 op, void *arg, size_t length)
{
        if (fDeviceRemoved)
                return B_DEV_NOT_READY;

        return gTTYModule->tty_control(fSystemTTYCookie, op, arg, length);
}


status_t
SerialDevice::Select(uint8 event, uint32 ref, selectsync *sync)
{
        if (fDeviceRemoved)
                return B_DEV_NOT_READY;

        return gTTYModule->tty_select(fSystemTTYCookie, event, ref, sync);
}


status_t
SerialDevice::DeSelect(uint8 event, selectsync *sync)
{
        if (fDeviceRemoved)
                return B_DEV_NOT_READY;

        return gTTYModule->tty_deselect(fSystemTTYCookie, event, sync);
}


status_t
SerialDevice::Close()
{
        OnClose();

        fStopThreads = true;
        fInputStopped = false;
        fDeviceOpen = false;

        if (!fDeviceRemoved) {
                gUSBModule->cancel_queued_transfers(fReadPipe);
                gUSBModule->cancel_queued_transfers(fWritePipe);
                gUSBModule->cancel_queued_transfers(fControlPipe);
        }

        gTTYModule->tty_close_cookie(fSystemTTYCookie);
        gTTYModule->tty_close_cookie(fDeviceTTYCookie);

        int32 result = B_OK;
        wait_for_thread(fInputThread, &result);
        fInputThread = -1;

        gTTYModule->tty_destroy_cookie(fSystemTTYCookie);
        gTTYModule->tty_destroy_cookie(fDeviceTTYCookie);

        gTTYModule->tty_destroy(fMasterTTY);
        gTTYModule->tty_destroy(fSlaveTTY);

        fMasterTTY = NULL;
        fSlaveTTY = NULL;
        fSystemTTYCookie = NULL;
        fDeviceTTYCookie = NULL;
        return B_OK;
}


status_t
SerialDevice::Free()
{
        return B_OK;
}


void
SerialDevice::Removed()
{
        if (fDeviceRemoved)
                return;

        // notifies us that the device was removed
        fDeviceRemoved = true;

        // we need to ensure that we do not use the device anymore
        fStopThreads = true;
        fInputStopped = false;
        gUSBModule->cancel_queued_transfers(fReadPipe);
        gUSBModule->cancel_queued_transfers(fWritePipe);
        gUSBModule->cancel_queued_transfers(fControlPipe);
}


status_t
SerialDevice::AddDevice(const usb_configuration_info *config)
{
        // default implementation - does nothing
        return B_ERROR;
}


status_t
SerialDevice::ResetDevice()
{
        // default implementation - does nothing
        return B_OK;
}


status_t
SerialDevice::SetLineCoding(usb_cdc_line_coding *coding)
{
        // default implementation - does nothing
        return B_NOT_SUPPORTED;
}


status_t
SerialDevice::SetControlLineState(uint16 state)
{
        // default implementation - does nothing
        return B_NOT_SUPPORTED;
}


status_t
SerialDevice::SetHardwareFlowControl(bool enable)
{
        // default implementation - does nothing
        return B_NOT_SUPPORTED;
}


void
SerialDevice::OnRead(char **buffer, size_t *numBytes)
{
        // default implementation - does nothing
}


void
SerialDevice::OnWrite(const char *buffer, size_t *numBytes, size_t *packetBytes)
{
        memcpy(fWriteBuffer, buffer, *numBytes);
}


void
SerialDevice::OnClose()
{
        // default implementation - does nothing
}


int32
SerialDevice::_InputThread(void *data)
{
        SerialDevice *device = (SerialDevice *)data;

        while (!device->fStopThreads) {
                status_t status = gUSBModule->queue_bulk(device->fReadPipe,
                        device->fReadBuffer, device->fReadBufferSize,
                        device->_ReadCallbackFunction, data);
                if (status < B_OK) {
                        TRACE_ALWAYS("input thread: queueing failed with error: 0x%08x\n",
                                status);
                        return status;
                }

                status = acquire_sem_etc(device->fDoneRead, 1, B_CAN_INTERRUPT, 0);
                if (status < B_OK) {
                        TRACE_ALWAYS("input thread: failed to get read done sem 0x%08x\n",
                                status);
                        return status;
                }

                if (device->fStatusRead != B_OK) {
                        TRACE("input thread: device status error 0x%08x\n",
                                device->fStatusRead);
                        if (device->fStatusRead == B_DEV_STALLED
                                && gUSBModule->clear_feature(device->fReadPipe,
                                        USB_FEATURE_ENDPOINT_HALT) != B_OK) {
                                TRACE_ALWAYS("input thread: failed to clear halt feature\n");
                                return B_ERROR;
                        }

                        continue;
                }

                char *buffer = device->fReadBuffer;
                size_t readLength = device->fActualLengthRead;
                device->OnRead(&buffer, &readLength);
                if (readLength == 0)
                        continue;

                while (device->fInputStopped)
                        snooze(100);

                status = gTTYModule->tty_write(device->fDeviceTTYCookie, buffer,
                        &readLength);
                if (status != B_OK) {
                        TRACE_ALWAYS("input thread: failed to write into TTY\n");
                        return status;
                }
        }

        return B_OK;
}


status_t
SerialDevice::_WriteToDevice()
{
        char *buffer = fOutputBuffer;
        size_t bytesLeft = fOutputBufferSize;
        status_t status = gTTYModule->tty_read(fDeviceTTYCookie, buffer,
                &bytesLeft);
        if (status != B_OK) {
                TRACE_ALWAYS("write to device: failed to read from TTY: %s\n",
                        strerror(status));
                return status;
        }

        while (!fDeviceRemoved && bytesLeft > 0) {
                size_t length = MIN(bytesLeft, fWriteBufferSize);
                size_t packetLength = length;
                OnWrite(buffer, &length, &packetLength);

                status = gUSBModule->queue_bulk(fWritePipe, fWriteBuffer, packetLength,
                        _WriteCallbackFunction, this);
                if (status != B_OK) {
                        TRACE_ALWAYS("write to device: queueing failed with status "
                                "0x%08x\n", status);
                        return status;
                }

                status = acquire_sem_etc(fDoneWrite, 1, B_CAN_INTERRUPT, 0);
                if (status != B_OK) {
                        TRACE_ALWAYS("write to device: failed to get write done sem "
                                "0x%08x\n", status);
                        return status;
                }

                if (fStatusWrite != B_OK) {
                        TRACE("write to device: device status error 0x%08x\n",
                                fStatusWrite);
                        if (fStatusWrite == B_DEV_STALLED) {
                                status = gUSBModule->clear_feature(fWritePipe,
                                        USB_FEATURE_ENDPOINT_HALT);
                                if (status != B_OK) {
                                        TRACE_ALWAYS("write to device: failed to clear device "
                                                "halt\n");
                                        return B_ERROR;
                                }
                        }

                        continue;
                }

                buffer += length;
                bytesLeft -= length;
        }

        return B_OK;
}


void
SerialDevice::_ReadCallbackFunction(void *cookie, status_t status, void *data,
        size_t actualLength)
{
        TRACE_FUNCALLS("read callback: cookie: 0x%08x status: 0x%08x data: 0x%08x "
                "length: %lu\n", cookie, status, data, actualLength);

        SerialDevice *device = (SerialDevice *)cookie;
        device->fActualLengthRead = actualLength;
        device->fStatusRead = status;
        release_sem_etc(device->fDoneRead, 1, B_DO_NOT_RESCHEDULE);
}


void
SerialDevice::_WriteCallbackFunction(void *cookie, status_t status, void *data,
        size_t actualLength)
{
        TRACE_FUNCALLS("write callback: cookie: 0x%08x status: 0x%08x data: 0x%08x "
                "length: %lu\n", cookie, status, data, actualLength);

        SerialDevice *device = (SerialDevice *)cookie;
        device->fActualLengthWrite = actualLength;
        device->fStatusWrite = status;
        release_sem_etc(device->fDoneWrite, 1, B_DO_NOT_RESCHEDULE);
}


void
SerialDevice::_InterruptCallbackFunction(void *cookie, status_t status,
        void *data, size_t actualLength)
{
        TRACE_FUNCALLS("interrupt callback: cookie: 0x%08x status: 0x%08x data: "
                "0x%08x len: %lu\n", cookie, status, data, actualLength);

        SerialDevice *device = (SerialDevice *)cookie;
        device->fActualLengthInterrupt = actualLength;
        device->fStatusInterrupt = status;

        // ToDo: maybe handle those somehow?

        if (status == B_OK && !device->fDeviceRemoved) {
                status = gUSBModule->queue_interrupt(device->fControlPipe,
                        device->fInterruptBuffer, device->fInterruptBufferSize,
                        device->_InterruptCallbackFunction, device);
        }
}


SerialDevice *
SerialDevice::MakeDevice(usb_device device, uint16 vendorID,
        uint16 productID)
{
        // FTDI Serial Device
        for (uint32 i = 0; i < sizeof(kFTDIDevices)
                / sizeof(kFTDIDevices[0]); i++) {
                if (vendorID == kFTDIDevices[i].vendorID
                        && productID == kFTDIDevices[i].productID) {
                        return new(std::nothrow) FTDIDevice(device, vendorID, productID,
                                kFTDIDevices[i].deviceName);
                }
        }

        // KLSI Serial Device
        for (uint32 i = 0; i < sizeof(kKLSIDevices)
                / sizeof(kKLSIDevices[0]); i++) {
                if (vendorID == kKLSIDevices[i].vendorID
                        && productID == kKLSIDevices[i].productID) {
                        return new(std::nothrow) KLSIDevice(device, vendorID, productID,
                                kKLSIDevices[i].deviceName);
                }
        }

        // Prolific Serial Device
        for (uint32 i = 0; i < sizeof(kProlificDevices)
                / sizeof(kProlificDevices[0]); i++) {
                if (vendorID == kProlificDevices[i].vendorID
                        && productID == kProlificDevices[i].productID) {
                        return new(std::nothrow) ProlificDevice(device, vendorID, productID,
                                kProlificDevices[i].deviceName);
                }
        }

        // Silicon Serial Device
        for (uint32 i = 0; i < sizeof(kSiliconDevices)
                / sizeof(kSiliconDevices[0]); i++) {
                if (vendorID == kSiliconDevices[i].vendorID
                        && productID == kSiliconDevices[i].productID) {
                        return new(std::nothrow) SiliconDevice(device, vendorID, productID,
                                kSiliconDevices[i].deviceName);
                }
        }

        // WinChipHead Serial Device
        for (uint32 i = 0; i < sizeof(kWCHDevices)
                / sizeof(kWCHDevices[0]); i++) {
                if (vendorID == kWCHDevices[i].vendorID
                        && productID == kWCHDevices[i].productID) {
                        return new(std::nothrow) WCHDevice(device, vendorID, productID,
                                kWCHDevices[i].deviceName);
                }
        }

        // Option Serial Device
        for (uint32 i = 0; i < sizeof(kOptionDevices)
                / sizeof(kOptionDevices[0]); i++) {
                if (vendorID == kOptionDevices[i].vendorID
                        && productID == kOptionDevices[i].productID) {
                        return new(std::nothrow) OptionDevice(device, vendorID, productID,
                                kOptionDevices[i].deviceName);
                }
        }

        // Otherwise, return standard ACM device
        return new(std::nothrow) ACMDevice(device, vendorID, productID,
                "CDC ACM compatible device");
}