root/src/add-ons/kernel/drivers/network/ether/usb_ecm/ECMDevice.cpp
/*
        Driver for USB Ethernet Control Model devices
        Copyright (C) 2008 Michael Lotz <mmlr@mlotz.ch>
        Distributed under the terms of the MIT license.
*/
#include <ether_driver.h>
#include <net/if_media.h>
#include <string.h>
#include <stdlib.h>

#include "ECMDevice.h"
#include "Driver.h"

ECMDevice::ECMDevice(usb_device device)
        :       fStatus(B_ERROR),
                fOpen(false),
                fRemoved(false),
                fInsideNotify(0),
                fDevice(device),
                fControlInterfaceIndex(0),
                fDataInterfaceIndex(0),
                fMACAddressIndex(0),
                fMaxSegmentSize(0),
                fNotifyEndpoint(0),
                fReadEndpoint(0),
                fWriteEndpoint(0),
                fNotifyReadSem(-1),
                fNotifyWriteSem(-1),
                fNotifyBuffer(NULL),
                fNotifyBufferLength(0),
                fLinkStateChangeSem(-1),
                fHasConnection(false),
                fDownstreamSpeed(0),
                fUpstreamSpeed(0)
{
        const usb_device_descriptor *deviceDescriptor
                = gUSBModule->get_device_descriptor(device);

        if (deviceDescriptor == NULL) {
                TRACE_ALWAYS("failed to get device descriptor\n");
                return;
        }

        fVendorID = deviceDescriptor->vendor_id;
        fProductID = deviceDescriptor->product_id;

        fNotifyBufferLength = 64;
        fNotifyBuffer = (uint8 *)malloc(fNotifyBufferLength);
        if (fNotifyBuffer == NULL) {
                TRACE_ALWAYS("out of memory for notify buffer allocation\n");
                return;
        }

        fNotifyReadSem = create_sem(0, DRIVER_NAME"_notify_read");
        if (fNotifyReadSem < B_OK) {
                TRACE_ALWAYS("failed to create read notify sem\n");
                return;
        }

        fNotifyWriteSem = create_sem(0, DRIVER_NAME"_notify_write");
        if (fNotifyWriteSem < B_OK) {
                TRACE_ALWAYS("failed to create write notify sem\n");
                return;
        }

        if (_SetupDevice() != B_OK) {
                TRACE_ALWAYS("failed to setup device\n");
                return;
        }

        if (_ReadMACAddress(fDevice, fMACAddress) != B_OK) {
                TRACE_ALWAYS("failed to read mac address\n");
                return;
        }

        fStatus = B_OK;
}


ECMDevice::~ECMDevice()
{
        if (fNotifyReadSem >= B_OK)
                delete_sem(fNotifyReadSem);
        if (fNotifyWriteSem >= B_OK)
                delete_sem(fNotifyWriteSem);

        if (!fRemoved)
                gUSBModule->cancel_queued_transfers(fNotifyEndpoint);

        free(fNotifyBuffer);
}


status_t
ECMDevice::Open()
{
        if (fOpen)
                return B_BUSY;
        if (fRemoved)
                return B_ERROR;

        // reset the device by switching the data interface to the disabled first
        // interface and then enable it by setting the second actual data interface
        const usb_configuration_info *config
                = gUSBModule->get_configuration(fDevice);

        gUSBModule->set_alt_interface(fDevice,
                &config->interface[fDataInterfaceIndex].alt[0]);

        // update to the changed config
        config = gUSBModule->get_configuration(fDevice);
        gUSBModule->set_alt_interface(fDevice,
                &config->interface[fDataInterfaceIndex].alt[1]);
        gUSBModule->set_alt_interface(fDevice,
                &config->interface[fControlInterfaceIndex].alt[0]);

        // update again
        config = gUSBModule->get_configuration(fDevice);
        usb_interface_info *interface = config->interface[fDataInterfaceIndex].active;
        if (interface->endpoint_count < 2) {
                TRACE_ALWAYS("setting the data alternate interface failed\n");
                return B_ERROR;
        }

        if (!(interface->endpoint[0].descr->endpoint_address & USB_ENDPOINT_ADDR_DIR_IN))
                fWriteEndpoint = interface->endpoint[0].handle;
        else
                fReadEndpoint = interface->endpoint[0].handle;

        if (interface->endpoint[1].descr->endpoint_address & USB_ENDPOINT_ADDR_DIR_IN)
                fReadEndpoint = interface->endpoint[1].handle;
        else
                fWriteEndpoint = interface->endpoint[1].handle;

        if (fReadEndpoint == 0 || fWriteEndpoint == 0) {
                TRACE_ALWAYS("no read and write endpoints found\n");
                return B_ERROR;
        }

        if (gUSBModule->queue_interrupt(fNotifyEndpoint, fNotifyBuffer,
                fNotifyBufferLength, _NotifyCallback, this) != B_OK) {
                // we cannot use notifications - hardcode to active connection
                fHasConnection = true;
                fDownstreamSpeed = 1000 * 1000 * 10; // 10Mbps
                fUpstreamSpeed = 1000 * 1000 * 10; // 10Mbps
        }

        // the device should now be ready
        fOpen = true;
        return B_OK;
}


status_t
ECMDevice::Close()
{
        if (fRemoved) {
                fOpen = false;
                return B_OK;
        }

        gUSBModule->cancel_queued_transfers(fNotifyEndpoint);
        gUSBModule->cancel_queued_transfers(fReadEndpoint);
        gUSBModule->cancel_queued_transfers(fWriteEndpoint);

        // put the device into non-connected mode again by switching the data
        // interface to the disabled alternate
        const usb_configuration_info *config
                = gUSBModule->get_configuration(fDevice);

        gUSBModule->set_alt_interface(fDevice,
                &config->interface[fDataInterfaceIndex].alt[0]);

        fOpen = false;
        return B_OK;
}


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


status_t
ECMDevice::Read(uint8 *buffer, size_t *numBytes)
{
        if (fRemoved) {
                *numBytes = 0;
                return B_DEVICE_NOT_FOUND;
        }

        status_t result = gUSBModule->queue_bulk(fReadEndpoint, buffer, *numBytes,
                _ReadCallback, this);
        if (result != B_OK) {
                *numBytes = 0;
                return result;
        }

        result = acquire_sem_etc(fNotifyReadSem, 1, B_CAN_INTERRUPT, 0);
        if (result < B_OK) {
                *numBytes = 0;
                return result;
        }

        if (fStatusRead != B_OK && fStatusRead != B_CANCELED && !fRemoved) {
                TRACE_ALWAYS("device status error 0x%08" B_PRIx32 "\n", fStatusRead);
                result = gUSBModule->clear_feature(fReadEndpoint,
                        USB_FEATURE_ENDPOINT_HALT);
                if (result != B_OK) {
                        TRACE_ALWAYS("failed to clear halt state on read\n");
                        *numBytes = 0;
                        return result;
                }
        }

        *numBytes = fActualLengthRead;
        return B_OK;
}


status_t
ECMDevice::Write(const uint8 *buffer, size_t *numBytes)
{
        if (fRemoved) {
                *numBytes = 0;
                return B_DEVICE_NOT_FOUND;
        }

        status_t result = gUSBModule->queue_bulk(fWriteEndpoint, (uint8 *)buffer,
                *numBytes, _WriteCallback, this);
        if (result != B_OK) {
                *numBytes = 0;
                return result;
        }

        result = acquire_sem_etc(fNotifyWriteSem, 1, B_CAN_INTERRUPT, 0);
        if (result < B_OK) {
                *numBytes = 0;
                return result;
        }

        if (fStatusWrite != B_OK && fStatusWrite != B_CANCELED && !fRemoved) {
                TRACE_ALWAYS("device status error 0x%08" B_PRIx32 "\n", fStatusWrite);
                result = gUSBModule->clear_feature(fWriteEndpoint,
                        USB_FEATURE_ENDPOINT_HALT);
                if (result != B_OK) {
                        TRACE_ALWAYS("failed to clear halt state on write\n");
                        *numBytes = 0;
                        return result;
                }
        }

        *numBytes = fActualLengthWrite;
        return B_OK;
}


status_t
ECMDevice::Control(uint32 op, void *buffer, size_t length)
{
        switch (op) {
                case ETHER_INIT:
                        return B_OK;

                case ETHER_GETADDR:
                        memcpy(buffer, &fMACAddress, sizeof(fMACAddress));
                        return B_OK;

                case ETHER_GETFRAMESIZE:
                        *(uint32 *)buffer = fMaxSegmentSize;
                        return B_OK;

                case ETHER_SET_LINK_STATE_SEM:
                        fLinkStateChangeSem = *(sem_id *)buffer;
                        return B_OK;

                case ETHER_GET_LINK_STATE:
                {
                        ether_link_state *state = (ether_link_state *)buffer;
                        state->media = IFM_ETHER | IFM_FULL_DUPLEX
                                | (fHasConnection ? IFM_ACTIVE : 0);
                        state->quality = 1000;
                        state->speed = fDownstreamSpeed;
                        return B_OK;
                }

                default:
                        TRACE_ALWAYS("unsupported ioctl %" B_PRIu32 "\n", op);
        }

        return B_DEV_INVALID_IOCTL;
}


void
ECMDevice::Removed()
{
        fRemoved = true;
        fHasConnection = false;
        fDownstreamSpeed = fUpstreamSpeed = 0;

        // the notify hook is different from the read and write hooks as it does
        // itself schedule traffic (while the other hooks only release a semaphore
        // to notify another thread which in turn safly checks for the removed
        // case) - so we must ensure that we are not inside the notify hook anymore
        // before returning, as we would otherwise violate the promise not to use
        // any of the pipes after returning from the removed hook
        while (atomic_add(&fInsideNotify, 0) != 0)
                snooze(100);

        gUSBModule->cancel_queued_transfers(fNotifyEndpoint);
        gUSBModule->cancel_queued_transfers(fReadEndpoint);
        gUSBModule->cancel_queued_transfers(fWriteEndpoint);

        if (fLinkStateChangeSem >= B_OK)
                release_sem_etc(fLinkStateChangeSem, 1, B_DO_NOT_RESCHEDULE);
}


status_t
ECMDevice::CompareAndReattach(usb_device device)
{
        const usb_device_descriptor *deviceDescriptor
                = gUSBModule->get_device_descriptor(device);

        if (deviceDescriptor == NULL) {
                TRACE_ALWAYS("failed to get device descriptor\n");
                return B_ERROR;
        }

        if (deviceDescriptor->vendor_id != fVendorID
                && deviceDescriptor->product_id != fProductID) {
                // this certainly isn't the same device
                return B_BAD_VALUE;
        }

        // this might be the same device that was replugged - read the MAC address
        // (which should be at the same index) to make sure
        uint8 macBuffer[6];
        if (_ReadMACAddress(device, macBuffer) != B_OK
                || memcmp(macBuffer, fMACAddress, sizeof(macBuffer)) != 0) {
                // reading the MAC address failed or they are not the same
                return B_BAD_VALUE;
        }

        // this is the same device that was replugged - clear the removed state,
        // re-setup the endpoints and transfers and open the device if it was
        // previously opened
        fDevice = device;
        fRemoved = false;
        status_t result = _SetupDevice();
        if (result != B_OK) {
                fRemoved = true;
                return result;
        }

        // in case notifications do not work we will have a hardcoded connection
        // need to register that and notify the network stack ourselfs if this is
        // the case as the open will not result in a corresponding notification
        bool noNotifications = fHasConnection;

        if (fOpen) {
                fOpen = false;
                result = Open();
                if (result == B_OK && noNotifications && fLinkStateChangeSem >= B_OK)
                        release_sem_etc(fLinkStateChangeSem, 1, B_DO_NOT_RESCHEDULE);
        }

        return B_OK;
}


status_t
ECMDevice::_SetupDevice()
{
        const usb_device_descriptor *deviceDescriptor
                = gUSBModule->get_device_descriptor(fDevice);

        if (deviceDescriptor == NULL) {
                TRACE_ALWAYS("failed to get device descriptor\n");
                return B_ERROR;
        }

        uint8 controlIndex = 0;
        uint8 dataIndex = 0;
        bool foundUnionDescriptor = false;
        bool foundEthernetDescriptor = false;
        bool found = false;
        const usb_configuration_info *config = NULL;
        for (int i = 0; i < deviceDescriptor->num_configurations && !found; i++) {
                config = gUSBModule->get_nth_configuration(fDevice, i);
                if (config == NULL)
                        continue;

                for (size_t j = 0; j < config->interface_count && !found; j++) {
                        const usb_interface_info *interface = config->interface[j].active;
                        usb_interface_descriptor *descriptor = interface->descr;
                        if (descriptor->interface_class != USB_INTERFACE_CLASS_CDC
                                || descriptor->interface_subclass != USB_INTERFACE_SUBCLASS_ECM
                                || interface->generic_count == 0) {
                                continue;
                        }

                        // try to find and interpret the union and ethernet functional
                        // descriptors
                        foundUnionDescriptor = foundEthernetDescriptor = false;
                        for (size_t k = 0; k < interface->generic_count; k++) {
                                usb_generic_descriptor *generic = &interface->generic[k]->generic;
                                if (generic->length >= 5
                                        && generic->data[0] == FUNCTIONAL_SUBTYPE_UNION) {
                                        controlIndex = generic->data[1];
                                        dataIndex = generic->data[2];
                                        foundUnionDescriptor = true;
                                } else if (generic->length >= sizeof(ethernet_functional_descriptor)
                                        && generic->data[0] == FUNCTIONAL_SUBTYPE_ETHERNET) {
                                        ethernet_functional_descriptor *ethernet
                                                = (ethernet_functional_descriptor *)generic->data;
                                        fMACAddressIndex = ethernet->mac_address_index;
                                        fMaxSegmentSize = ethernet->max_segment_size;
                                        foundEthernetDescriptor = true;
                                }

                                if (foundUnionDescriptor && foundEthernetDescriptor) {
                                        found = true;
                                        break;
                                }
                        }
                }
        }

        if (!foundUnionDescriptor) {
                TRACE_ALWAYS("did not find a union descriptor\n");
                return B_ERROR;
        }

        if (!foundEthernetDescriptor) {
                TRACE_ALWAYS("did not find an ethernet descriptor\n");
                return B_ERROR;
        }

        // set the current configuration
        gUSBModule->set_configuration(fDevice, config);
        if (controlIndex >= config->interface_count) {
                TRACE_ALWAYS("control interface index invalid\n");
                return B_ERROR;
        }

        // check that the indicated control interface fits our needs
        usb_interface_info *interface = config->interface[controlIndex].active;
        usb_interface_descriptor *descriptor = interface->descr;
        if ((descriptor->interface_class != USB_INTERFACE_CLASS_CDC
                || descriptor->interface_subclass != USB_INTERFACE_SUBCLASS_ECM)
                || interface->endpoint_count == 0) {
                TRACE_ALWAYS("control interface invalid\n");
                return B_ERROR;
        }

        fControlInterfaceIndex = controlIndex;
        fNotifyEndpoint = interface->endpoint[0].handle;
        fNotifyBufferLength = interface->endpoint[0].descr->max_packet_size;

        if (dataIndex >= config->interface_count) {
                TRACE_ALWAYS("data interface index invalid\n");
                return B_ERROR;
        }

        // check that the indicated data interface fits our needs
        if (config->interface[dataIndex].alt_count < 2) {
                TRACE_ALWAYS("data interface does not provide two alternate interfaces\n");
                return B_ERROR;
        }

        // alternate 0 is the disabled, endpoint-less default interface
        interface = &config->interface[dataIndex].alt[1];
        descriptor = interface->descr;
        if (descriptor->interface_class != USB_INTERFACE_CLASS_CDC_DATA
                || interface->endpoint_count < 2) {
                TRACE_ALWAYS("data interface invalid\n");
                return B_ERROR;
        }

        fDataInterfaceIndex = dataIndex;
        return B_OK;
}


status_t
ECMDevice::_ReadMACAddress(usb_device device, uint8 *buffer)
{
        if (fMACAddressIndex == 0)
                return B_BAD_VALUE;

        size_t actualLength = 0;
        size_t macStringLength = 26;
        uint8 macString[macStringLength];
        uint8 langid[4];

        status_t result = gUSBModule->get_descriptor(device, USB_DESCRIPTOR_STRING,
                0, 0, langid, sizeof(langid), &actualLength);
        if (result != B_OK)
                return result;

        result = gUSBModule->get_descriptor(device, USB_DESCRIPTOR_STRING,
                fMACAddressIndex, ((langid[3] << 8) | langid[2]),
                macString, macStringLength, &actualLength);
        if (result != B_OK)
                return result;

        if (actualLength != macStringLength) {
                TRACE_ALWAYS("did not retrieve full mac address\n");
                return B_ERROR;
        }

        char macPart[3];
        macPart[2] = 0;
        for (int32 i = 0; i < 6; i++) {
                macPart[0] = macString[2 + i * 4 + 0];
                macPart[1] = macString[2 + i * 4 + 2];
                buffer[i] = strtol(macPart, NULL, 16);
        }

        TRACE_ALWAYS("read mac address: %02x:%02x:%02x:%02x:%02x:%02x\n",
                buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5]);
        return B_OK;
}


void
ECMDevice::_ReadCallback(void *cookie, int32 status, void *data,
        size_t actualLength)
{
        ECMDevice *device = (ECMDevice *)cookie;
        device->fActualLengthRead = actualLength;
        device->fStatusRead = status;
        release_sem_etc(device->fNotifyReadSem, 1, B_DO_NOT_RESCHEDULE);
}


void
ECMDevice::_WriteCallback(void *cookie, int32 status, void *data,
        size_t actualLength)
{
        ECMDevice *device = (ECMDevice *)cookie;
        device->fActualLengthWrite = actualLength;
        device->fStatusWrite = status;
        release_sem_etc(device->fNotifyWriteSem, 1, B_DO_NOT_RESCHEDULE);
}


void
ECMDevice::_NotifyCallback(void *cookie, int32 status, void *data,
        size_t actualLength)
{
        ECMDevice *device = (ECMDevice *)cookie;
        atomic_add(&device->fInsideNotify, 1);
        if (status == B_CANCELED || device->fRemoved) {
                atomic_add(&device->fInsideNotify, -1);
                return;
        }

        if (status == B_OK && actualLength >= sizeof(cdc_notification)) {
                bool linkStateChange = false;
                cdc_notification *notification
                        = (cdc_notification *)device->fNotifyBuffer;

                switch (notification->notification_code) {
                        case CDC_NOTIFY_NETWORK_CONNECTION:
                                TRACE("connection state change to %d\n", notification->value);
                                device->fHasConnection = notification->value > 0;
                                linkStateChange = true;
                                break;

                        case CDC_NOTIFY_CONNECTION_SPEED_CHANGE:
                        {
                                if (notification->data_length < sizeof(cdc_connection_speed)
                                        || actualLength < sizeof(cdc_notification)
                                        + sizeof(cdc_connection_speed)) {
                                        TRACE_ALWAYS("not enough data in connection speed change\n");
                                        break;
                                }

                                cdc_connection_speed *speed;
                                speed = (cdc_connection_speed *)&notification->data[0];
                                device->fUpstreamSpeed = speed->upstream_speed;
                                device->fDownstreamSpeed = speed->downstream_speed;
                                device->fHasConnection = true;
                                TRACE("connection speed change to %" B_PRId32 "/%" B_PRId32 "\n",
                                        speed->downstream_speed, speed->upstream_speed);
                                linkStateChange = true;
                                break;
                        }

                        default:
                                TRACE_ALWAYS("unsupported notification 0x%02x\n",
                                        notification->notification_code);
                                break;
                }

                if (linkStateChange && device->fLinkStateChangeSem >= B_OK)
                        release_sem_etc(device->fLinkStateChangeSem, 1, B_DO_NOT_RESCHEDULE);
        }

        if (status != B_OK) {
                TRACE_ALWAYS("device status error 0x%08" B_PRIx32 "\n", status);
                if (gUSBModule->clear_feature(device->fNotifyEndpoint,
                        USB_FEATURE_ENDPOINT_HALT) != B_OK)
                        TRACE_ALWAYS("failed to clear halt state in notify hook\n");
        }

        // schedule next notification buffer
        gUSBModule->queue_interrupt(device->fNotifyEndpoint, device->fNotifyBuffer,
                device->fNotifyBufferLength, _NotifyCallback, device);
        atomic_add(&device->fInsideNotify, -1);
}