/*      $NetBSD: ubt.c,v 1.67 2024/07/05 04:31:52 rin Exp $     */

/*-
* Copyright (c) 2006 Itronix Inc.
* All rights reserved.
*
* Written by Iain Hibbert for Itronix Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
* 3. The name of Itronix Inc. may not be used to endorse
*    or promote products derived from this software without specific
*    prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY ITRONIX INC. ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL ITRONIX INC. BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Copyright (c) 2002, 2003 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Lennart Augustsson ([email protected]) and
* David Sainty ([email protected]).
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* This driver originally written by Lennart Augustsson and David Sainty,
* but was mostly rewritten for the NetBSD Bluetooth protocol stack by
* Iain Hibbert for Itronix, Inc using the FreeBSD ng_ubt.c driver as a
* reference.
*/

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: ubt.c,v 1.67 2024/07/05 04:31:52 rin Exp $");

#ifdef _KERNEL_OPT
#include "opt_usb.h"
#endif

#include <sys/param.h>
#include <sys/device.h>
#include <sys/ioctl.h>
#include <sys/kernel.h>
#include <sys/kmem.h>
#include <sys/mbuf.h>
#include <sys/proc.h>
#include <sys/sysctl.h>
#include <sys/systm.h>

#include <dev/usb/usb.h>
#include <dev/usb/usbdi.h>
#include <dev/usb/usbdi_util.h>
#include <dev/usb/usbdevs.h>

#include <netbt/bluetooth.h>
#include <netbt/hci.h>

/*******************************************************************************
*
*      debugging stuff
*/
#undef DPRINTF
#undef DPRINTFN

#ifdef UBT_DEBUG
int     ubt_debug = 0;

#define DPRINTF(...)            do {            \
       if (ubt_debug) {                        \
               printf("%s: ", __func__);       \
               printf(__VA_ARGS__);            \
       }                                       \
} while (/* CONSTCOND */0)

#define DPRINTFN(n, ...)        do {            \
       if (ubt_debug > (n)) {                  \
               printf("%s: ", __func__);       \
               printf(__VA_ARGS__);            \
       }                                       \
} while (/* CONSTCOND */0)

SYSCTL_SETUP(sysctl_hw_ubt_debug_setup, "sysctl hw.ubt_debug setup")
{

       sysctl_createv(NULL, 0, NULL, NULL,
               CTLFLAG_PERMANENT | CTLFLAG_READWRITE,
               CTLTYPE_INT, "ubt_debug",
               SYSCTL_DESCR("ubt debug level"),
               NULL, 0,
               &ubt_debug, sizeof(ubt_debug),
               CTL_HW, CTL_CREATE, CTL_EOL);
}
#else
#define DPRINTF(...)
#define DPRINTFN(...)
#endif

/*******************************************************************************
*
*      ubt softc structure
*
*/

/* buffer sizes */
/*
* NB: although ACL packets can extend to 65535 bytes, most devices
* have max_acl_size at much less (largest I have seen is 384)
*/
#define UBT_BUFSIZ_CMD          (HCI_CMD_PKT_SIZE - 1)
#define UBT_BUFSIZ_ACL          (2048 - 1)
#define UBT_BUFSIZ_EVENT        (HCI_EVENT_PKT_SIZE - 1)

/* Transmit timeouts */
#define UBT_CMD_TIMEOUT         USBD_DEFAULT_TIMEOUT
#define UBT_ACL_TIMEOUT         USBD_DEFAULT_TIMEOUT

/*
* ISOC transfers
*
* xfer buffer size depends on the frame size, and the number
* of frames per transfer is fixed, as each frame should be
* 1ms worth of data. This keeps the rate that xfers complete
* fairly constant. We use multiple xfers to keep the hardware
* busy
*/
#define UBT_NXFERS              3       /* max xfers to queue */
#define UBT_NFRAMES             10      /* frames per xfer */

struct ubt_isoc_xfer {
       struct ubt_softc        *softc;
       struct usbd_xfer        *xfer;
       uint8_t                 *buf;
       uint16_t                 size[UBT_NFRAMES];
       int                      busy;
};

struct ubt_softc {
       device_t                 sc_dev;
       struct usbd_device      *sc_udev;
       int                      sc_refcnt;
       int                      sc_dying;
       int                      sc_enabled;

       /* Control Interface */
       struct usbd_interface *  sc_iface0;

       /* Commands (control) */
       struct usbd_xfer        *sc_cmd_xfer;
       uint8_t                 *sc_cmd_buf;
       int                      sc_cmd_busy;   /* write active */
       MBUFQ_HEAD()             sc_cmd_queue;  /* output queue */

       /* Events (interrupt) */
       int                      sc_evt_addr;   /* endpoint address */
       struct usbd_pipe        *sc_evt_pipe;
       uint8_t                 *sc_evt_buf;

       /* ACL data (in) */
       int                      sc_aclrd_addr; /* endpoint address */
       struct usbd_pipe        *sc_aclrd_pipe; /* read pipe */
       struct usbd_xfer        *sc_aclrd_xfer; /* read xfer */
       uint8_t                 *sc_aclrd_buf;  /* read buffer */
       int                      sc_aclrd_busy; /* reading */

       /* ACL data (out) */
       int                      sc_aclwr_addr; /* endpoint address */
       struct usbd_pipe        *sc_aclwr_pipe; /* write pipe */
       struct usbd_xfer        *sc_aclwr_xfer; /* write xfer */
       uint8_t                 *sc_aclwr_buf;  /* write buffer */
       int                      sc_aclwr_busy; /* write active */
       MBUFQ_HEAD()             sc_aclwr_queue;/* output queue */

       /* ISOC interface */
       struct usbd_interface   *sc_iface1;     /* ISOC interface */
       struct sysctllog        *sc_log;        /* sysctl log */
       int                      sc_config;     /* current config no */
       int                      sc_alt_config; /* no of alternates */

       /* SCO data (in) */
       int                      sc_scord_addr; /* endpoint address */
       struct usbd_pipe        *sc_scord_pipe; /* read pipe */
       int                      sc_scord_size; /* frame length */
       struct ubt_isoc_xfer     sc_scord[UBT_NXFERS];
       struct mbuf             *sc_scord_mbuf; /* current packet */

       /* SCO data (out) */
       int                      sc_scowr_addr; /* endpoint address */
       struct usbd_pipe        *sc_scowr_pipe; /* write pipe */
       int                      sc_scowr_size; /* frame length */
       struct ubt_isoc_xfer     sc_scowr[UBT_NXFERS];
       struct mbuf             *sc_scowr_mbuf; /* current packet */
       int                      sc_scowr_busy; /* write active */
       MBUFQ_HEAD()             sc_scowr_queue;/* output queue */

       /* Protocol structure */
       struct hci_unit         *sc_unit;
       struct bt_stats          sc_stats;

       /* Successfully attached */
       int                      sc_ok;
};

/*******************************************************************************
*
* Bluetooth unit/USB callback routines
*
*/
static int ubt_enable(device_t);
static void ubt_disable(device_t);

static void ubt_xmit_cmd(device_t, struct mbuf *);
static void ubt_xmit_cmd_start(struct ubt_softc *);
static void ubt_xmit_cmd_complete(struct usbd_xfer *,
                               void *, usbd_status);

static void ubt_xmit_acl(device_t, struct mbuf *);
static void ubt_xmit_acl_start(struct ubt_softc *);
static void ubt_xmit_acl_complete(struct usbd_xfer *,
                               void *, usbd_status);

static void ubt_xmit_sco(device_t, struct mbuf *);
static void ubt_xmit_sco_start(struct ubt_softc *);
static void ubt_xmit_sco_start1(struct ubt_softc *, struct ubt_isoc_xfer *);
static void ubt_xmit_sco_complete(struct usbd_xfer *,
                               void *, usbd_status);

static void ubt_recv_event(struct usbd_xfer *,
                               void *, usbd_status);

static void ubt_recv_acl_start(struct ubt_softc *);
static void ubt_recv_acl_complete(struct usbd_xfer *,
                               void *, usbd_status);

static void ubt_recv_sco_start1(struct ubt_softc *, struct ubt_isoc_xfer *);
static void ubt_recv_sco_complete(struct usbd_xfer *,
                               void *, usbd_status);

static void ubt_stats(device_t, struct bt_stats *, int);

static const struct hci_if ubt_hci = {
       .enable = ubt_enable,
       .disable = ubt_disable,
       .output_cmd = ubt_xmit_cmd,
       .output_acl = ubt_xmit_acl,
       .output_sco = ubt_xmit_sco,
       .get_stats = ubt_stats,
       .ipl = IPL_SOFTUSB,
};

/*******************************************************************************
*
* USB Autoconfig stuff
*
*/

static int      ubt_match(device_t, cfdata_t, void *);
static void     ubt_attach(device_t, device_t, void *);
static int      ubt_detach(device_t, int);
static int      ubt_activate(device_t, enum devact);

CFATTACH_DECL_NEW(ubt, sizeof(struct ubt_softc), ubt_match, ubt_attach,
   ubt_detach, ubt_activate);

static int ubt_set_isoc_config(struct ubt_softc *);
static int ubt_sysctl_config(SYSCTLFN_PROTO);
static void ubt_abortdealloc(struct ubt_softc *);

/*
* To match or ignore, add details to the ubt_dev list.
* Use value of -1 to indicate a wildcard
* To override another entry, add details earlier
*/
static const struct ubt_devno {
       int                     vendor;
       int                     product;
       int                     class;
       int                     subclass;
       int                     proto;
       int                     match;
} ubt_dev[] = {
       {   /* ignore Broadcom 2033 without firmware */
           USB_VENDOR_BROADCOM,
           USB_PRODUCT_BROADCOM_BCM2033NF,
           -1,
           -1,
           -1,
           UMATCH_NONE
       },
       {   /* Apple Bluetooth Host Controller MacbookPro 7,1 */
           USB_VENDOR_APPLE,
           USB_PRODUCT_APPLE_BLUETOOTH_HOST_1,
           -1,
           -1,
           -1,
           UMATCH_VENDOR_PRODUCT
       },
       {   /* Apple Bluetooth Host Controller iMac 11,1 */
           USB_VENDOR_APPLE,
           USB_PRODUCT_APPLE_BLUETOOTH_HOST_2,
           -1,
           -1,
           -1,
           UMATCH_VENDOR_PRODUCT
       },
       {   /* Apple Bluetooth Host Controller MacBookPro 8,2 */
           USB_VENDOR_APPLE,
           USB_PRODUCT_APPLE_BLUETOOTH_HOST_3,
           -1,
           -1,
           -1,
           UMATCH_VENDOR_PRODUCT
       },
       {   /* Apple Bluetooth Host Controller MacBookAir 3,1 3,2*/
           USB_VENDOR_APPLE,
           USB_PRODUCT_APPLE_BLUETOOTH_HOST_4,
           -1,
           -1,
           -1,
           UMATCH_VENDOR_PRODUCT
       },
       {   /* Apple Bluetooth Host Controller MacBookAir 4,1 */
           USB_VENDOR_APPLE,
           USB_PRODUCT_APPLE_BLUETOOTH_HOST_5,
           -1,
           -1,
           -1,
           UMATCH_VENDOR_PRODUCT
       },
       {   /* Apple Bluetooth Host Controller MacMini 5,1 */
           USB_VENDOR_APPLE,
           USB_PRODUCT_APPLE_BLUETOOTH_HOST_6,
           -1,
           -1,
           -1,
           UMATCH_VENDOR_PRODUCT
       },
       {   /* Apple Bluetooth Host Controller MacBookAir 6,1 */
           USB_VENDOR_APPLE,
           USB_PRODUCT_APPLE_BLUETOOTH_HOST_7,
           -1,
           -1,
           -1,
           UMATCH_VENDOR_PRODUCT
       },
       {   /* Apple Bluetooth Host Controller MacBookPro 9,2 */
           USB_VENDOR_APPLE,
           USB_PRODUCT_APPLE_BLUETOOTH_HOST_8,
           -1,
           -1,
           -1,
           UMATCH_VENDOR_PRODUCT
       },
       {   /* Broadcom chips with PatchRAM support */
           USB_VENDOR_BROADCOM,
           -1,
           UDCLASS_VENDOR,
           UDSUBCLASS_RF,
           UDPROTO_BLUETOOTH,
           UMATCH_VENDOR_DEVCLASS_DEVPROTO
       },
       {   /* Broadcom based device with PatchRAM support */
           USB_VENDOR_FOXCONN,
           -1,
           UDCLASS_VENDOR,
           UDSUBCLASS_RF,
           UDPROTO_BLUETOOTH,
           UMATCH_VENDOR_DEVCLASS_DEVPROTO
       },
       {   /* Broadcom based device with PatchRAM support */
           USB_VENDOR_LITEON,
           -1,
           UDCLASS_VENDOR,
           UDSUBCLASS_RF,
           UDPROTO_BLUETOOTH,
           UMATCH_VENDOR_DEVCLASS_DEVPROTO
       },
       {   /* Broadcom based device with PatchRAM support */
           USB_VENDOR_BELKIN,
           -1,
           UDCLASS_VENDOR,
           UDSUBCLASS_RF,
           UDPROTO_BLUETOOTH,
           UMATCH_VENDOR_DEVCLASS_DEVPROTO
       },
       {   /* Broadcom based device with PatchRAM support */
           USB_VENDOR_TOSHIBA,
           -1,
           UDCLASS_VENDOR,
           UDSUBCLASS_RF,
           UDPROTO_BLUETOOTH,
           UMATCH_VENDOR_DEVCLASS_DEVPROTO
       },
       {   /* Broadcom based device with PatchRAM support */
           USB_VENDOR_ASUSTEK,
           -1,
           UDCLASS_VENDOR,
           UDSUBCLASS_RF,
           UDPROTO_BLUETOOTH,
           UMATCH_VENDOR_DEVCLASS_DEVPROTO
       },
       {   /* Generic Bluetooth SIG compliant devices */
           -1,
           -1,
           UDCLASS_WIRELESS,
           UDSUBCLASS_RF,
           UDPROTO_BLUETOOTH,
           UMATCH_DEVCLASS_DEVSUBCLASS_DEVPROTO
       },
};

static int
ubt_match(device_t parent, cfdata_t match, void *aux)
{
       struct usb_attach_arg *uaa = aux;
       size_t i;

       DPRINTFN(50, "ubt_match\n");

       for (i = 0; i < __arraycount(ubt_dev); i++) {
               if (ubt_dev[i].vendor != -1
                   && ubt_dev[i].vendor != (int)uaa->uaa_vendor)
                       continue;
               if (ubt_dev[i].product != -1
                   && ubt_dev[i].product != (int)uaa->uaa_product)
                       continue;
               if (ubt_dev[i].class != -1
                   && ubt_dev[i].class != uaa->uaa_class)
                       continue;
               if (ubt_dev[i].subclass != -1
                   && ubt_dev[i].subclass != uaa->uaa_subclass)
                       continue;
               if (ubt_dev[i].proto != -1
                   && ubt_dev[i].proto != uaa->uaa_proto)
                       continue;

               return ubt_dev[i].match;
       }

       return UMATCH_NONE;
}

static void
ubt_attach(device_t parent, device_t self, void *aux)
{
       struct ubt_softc *sc = device_private(self);
       struct usb_attach_arg *uaa = aux;
       usb_config_descriptor_t *cd;
       usb_endpoint_descriptor_t *ed;
       const struct sysctlnode *node;
       char *devinfop;
       int err;
       uint8_t count, i;

       DPRINTFN(50, "ubt_attach: sc=%p\n", sc);

       sc->sc_dev = self;
       sc->sc_udev = uaa->uaa_device;

       MBUFQ_INIT(&sc->sc_cmd_queue);
       MBUFQ_INIT(&sc->sc_aclwr_queue);
       MBUFQ_INIT(&sc->sc_scowr_queue);

       aprint_naive("\n");
       aprint_normal("\n");

       devinfop = usbd_devinfo_alloc(sc->sc_udev, 0);
       aprint_normal_dev(self, "%s\n", devinfop);
       usbd_devinfo_free(devinfop);

       /*
        * Move the device into the configured state
        */
       err = usbd_set_config_index(sc->sc_udev, 0, 1);
       if (err) {
               aprint_error_dev(self,
                   "failed to set configuration idx 0: %s\n",
                   usbd_errstr(err));

               return;
       }

       /*
        * Interface 0 must have 3 endpoints
        *      1) Interrupt endpoint to receive HCI events
        *      2) Bulk IN endpoint to receive ACL data
        *      3) Bulk OUT endpoint to send ACL data
        */
       err = usbd_device2interface_handle(sc->sc_udev, 0, &sc->sc_iface0);
       if (err) {
               aprint_error_dev(self,
                   "Could not get interface 0 handle %s (%d)\n",
                   usbd_errstr(err), err);

               return;
       }

       sc->sc_evt_addr = -1;
       sc->sc_aclrd_addr = -1;
       sc->sc_aclwr_addr = -1;

       count = 0;
       (void)usbd_endpoint_count(sc->sc_iface0, &count);

       for (i = 0 ; i < count ; i++) {
               int dir, type;

               ed = usbd_interface2endpoint_descriptor(sc->sc_iface0, i);
               if (ed == NULL) {
                       aprint_error_dev(self,
                           "could not read endpoint descriptor %d\n", i);

                       return;
               }

               dir = UE_GET_DIR(ed->bEndpointAddress);
               type = UE_GET_XFERTYPE(ed->bmAttributes);

               if (dir == UE_DIR_IN && type == UE_INTERRUPT)
                       sc->sc_evt_addr = ed->bEndpointAddress;
               else if (dir == UE_DIR_IN && type == UE_BULK)
                       sc->sc_aclrd_addr = ed->bEndpointAddress;
               else if (dir == UE_DIR_OUT && type == UE_BULK)
                       sc->sc_aclwr_addr = ed->bEndpointAddress;
       }

       if (sc->sc_evt_addr == -1) {
               aprint_error_dev(self,
                   "missing INTERRUPT endpoint on interface 0\n");

               return;
       }
       if (sc->sc_aclrd_addr == -1) {
               aprint_error_dev(self,
                   "missing BULK IN endpoint on interface 0\n");

               return;
       }
       if (sc->sc_aclwr_addr == -1) {
               aprint_error_dev(self,
                   "missing BULK OUT endpoint on interface 0\n");

               return;
       }

       /*
        * Interface 1 must have 2 endpoints
        *      1) Isochronous IN endpoint to receive SCO data
        *      2) Isochronous OUT endpoint to send SCO data
        *
        * and will have several configurations, which can be selected
        * via a sysctl variable. We select config 0 to start, which
        * means that no SCO data will be available.
        */
       err = usbd_device2interface_handle(sc->sc_udev, 1, &sc->sc_iface1);
       if (err) {
               aprint_error_dev(self,
                   "Could not get interface 1 handle %s (%d)\n",
                   usbd_errstr(err), err);

               return;
       }

       cd = usbd_get_config_descriptor(sc->sc_udev);
       if (cd == NULL) {
               aprint_error_dev(self, "could not get config descriptor\n");

               return;
       }

       sc->sc_alt_config = usbd_get_no_alts(cd, 1);

       /* set initial config */
       err = ubt_set_isoc_config(sc);
       if (err) {
               aprint_error_dev(self, "ISOC config failed\n");

               return;
       }

       /* Attach HCI */
       sc->sc_unit = hci_attach_pcb(&ubt_hci, sc->sc_dev, 0);

       usbd_add_drv_event(USB_EVENT_DRIVER_ATTACH, sc->sc_udev, sc->sc_dev);

       /* sysctl set-up for alternate configs */
       sysctl_createv(&sc->sc_log, 0, NULL, &node,
               0,
               CTLTYPE_NODE, device_xname(sc->sc_dev),
               SYSCTL_DESCR("ubt driver information"),
               NULL, 0,
               NULL, 0,
               CTL_HW,
               CTL_CREATE, CTL_EOL);

       if (node != NULL) {
               sysctl_createv(&sc->sc_log, 0, NULL, NULL,
                       CTLFLAG_READWRITE,
                       CTLTYPE_INT, "config",
                       SYSCTL_DESCR("configuration number"),
                       ubt_sysctl_config, 0,
                       (void *)sc, 0,
                       CTL_HW, node->sysctl_num,
                       CTL_CREATE, CTL_EOL);

               sysctl_createv(&sc->sc_log, 0, NULL, NULL,
                       CTLFLAG_READONLY,
                       CTLTYPE_INT, "alt_config",
                       SYSCTL_DESCR("number of alternate configurations"),
                       NULL, 0,
                       &sc->sc_alt_config, sizeof(sc->sc_alt_config),
                       CTL_HW, node->sysctl_num,
                       CTL_CREATE, CTL_EOL);

               sysctl_createv(&sc->sc_log, 0, NULL, NULL,
                       CTLFLAG_READONLY,
                       CTLTYPE_INT, "sco_rxsize",
                       SYSCTL_DESCR("max SCO receive size"),
                       NULL, 0,
                       &sc->sc_scord_size, sizeof(sc->sc_scord_size),
                       CTL_HW, node->sysctl_num,
                       CTL_CREATE, CTL_EOL);

               sysctl_createv(&sc->sc_log, 0, NULL, NULL,
                       CTLFLAG_READONLY,
                       CTLTYPE_INT, "sco_txsize",
                       SYSCTL_DESCR("max SCO transmit size"),
                       NULL, 0,
                       &sc->sc_scowr_size, sizeof(sc->sc_scowr_size),
                       CTL_HW, node->sysctl_num,
                       CTL_CREATE, CTL_EOL);
       }

       sc->sc_ok = 1;

       if (!pmf_device_register(self, NULL, NULL))
               aprint_error_dev(self, "couldn't establish power handler\n");

       return;
}

static int
ubt_detach(device_t self, int flags)
{
       struct ubt_softc *sc = device_private(self);
       int s;

       DPRINTF("sc=%p flags=%d\n", sc, flags);

       pmf_device_deregister(self);

       sc->sc_dying = 1;

       if (!sc->sc_ok)
               return 0;

       /* delete sysctl nodes */
       sysctl_teardown(&sc->sc_log);

       /* Detach HCI interface */
       if (sc->sc_unit) {
               hci_detach_pcb(sc->sc_unit);
               sc->sc_unit = NULL;
       }

       /*
        * Abort all pipes. Causes processes waiting for transfer to wake.
        *
        * Actually, hci_detach_pcb() above will call ubt_disable() which
        * may call ubt_abortdealloc(), but lets be sure since doing it
        * twice wont cause an error.
        */
       ubt_abortdealloc(sc);

       /* wait for all processes to finish */
       s = splusb();
       if (sc->sc_refcnt-- > 0)
               usb_detach_waitold(sc->sc_dev);

       splx(s);

       usbd_add_drv_event(USB_EVENT_DRIVER_DETACH, sc->sc_udev, sc->sc_dev);

       DPRINTFN(1, "driver detached\n");

       return 0;
}

static int
ubt_activate(device_t self, enum devact act)
{
       struct ubt_softc *sc = device_private(self);

       DPRINTFN(1, "sc=%p, act=%d\n", sc, act);

       switch (act) {
       case DVACT_DEACTIVATE:
               sc->sc_dying = 1;
               return 0;
       default:
               return EOPNOTSUPP;
       }
}

/* set ISOC configuration */
static int
ubt_set_isoc_config(struct ubt_softc *sc)
{
       usb_endpoint_descriptor_t *ed;
       int rd_addr, wr_addr, rd_size, wr_size;
       uint8_t count, i;
       int err;

       err = usbd_set_interface(sc->sc_iface1, sc->sc_config);
       if (err != USBD_NORMAL_COMPLETION) {
               aprint_error_dev(sc->sc_dev,
                   "Could not set config %d on ISOC interface. %s (%d)\n",
                   sc->sc_config, usbd_errstr(err), err);

               return err == USBD_IN_USE ? EBUSY : EIO;
       }

       /*
        * We wont get past the above if there are any pipes open, so no
        * need to worry about buf/xfer/pipe deallocation. If we get an
        * error after this, the frame quantities will be 0 and no SCO
        * data will be possible.
        */

       sc->sc_scord_size = rd_size = 0;
       sc->sc_scord_addr = rd_addr = -1;

       sc->sc_scowr_size = wr_size = 0;
       sc->sc_scowr_addr = wr_addr = -1;

       count = 0;
       (void)usbd_endpoint_count(sc->sc_iface1, &count);

       for (i = 0 ; i < count ; i++) {
               ed = usbd_interface2endpoint_descriptor(sc->sc_iface1, i);
               if (ed == NULL) {
                       aprint_error_dev(sc->sc_dev,
                           "could not read endpoint descriptor %d\n", i);

                       return EIO;
               }

               DPRINTFN(5, "%s: endpoint type %02x (%02x) addr %02x (%s)\n",
                       device_xname(sc->sc_dev),
                       UE_GET_XFERTYPE(ed->bmAttributes),
                       UE_GET_ISO_TYPE(ed->bmAttributes),
                       ed->bEndpointAddress,
                       UE_GET_DIR(ed->bEndpointAddress) ? "in" : "out");

               if (UE_GET_XFERTYPE(ed->bmAttributes) != UE_ISOCHRONOUS)
                       continue;

               if (UE_GET_DIR(ed->bEndpointAddress) == UE_DIR_IN) {
                       rd_addr = ed->bEndpointAddress;
                       rd_size = UGETW(ed->wMaxPacketSize);
               } else {
                       wr_addr = ed->bEndpointAddress;
                       wr_size = UGETW(ed->wMaxPacketSize);
               }
       }

       if (rd_addr == -1) {
               aprint_error_dev(sc->sc_dev,
                   "missing ISOC IN endpoint on interface config %d\n",
                   sc->sc_config);

               return ENOENT;
       }
       if (wr_addr == -1) {
               aprint_error_dev(sc->sc_dev,
                   "missing ISOC OUT endpoint on interface config %d\n",
                   sc->sc_config);

               return ENOENT;
       }

       if (rd_size > MLEN) {
               aprint_error_dev(sc->sc_dev, "rd_size=%d exceeds MLEN\n",
                   rd_size);

               return EOVERFLOW;
       }

       if (wr_size > MLEN) {
               aprint_error_dev(sc->sc_dev, "wr_size=%d exceeds MLEN\n",
                   wr_size);

               return EOVERFLOW;
       }

       sc->sc_scord_size = rd_size;
       sc->sc_scord_addr = rd_addr;

       sc->sc_scowr_size = wr_size;
       sc->sc_scowr_addr = wr_addr;

       return 0;
}

/* sysctl helper to set alternate configurations */
static int
ubt_sysctl_config(SYSCTLFN_ARGS)
{
       struct sysctlnode node;
       struct ubt_softc *sc;
       int t, error;

       node = *rnode;
       sc = node.sysctl_data;

       t = sc->sc_config;
       node.sysctl_data = &t;
       error = sysctl_lookup(SYSCTLFN_CALL(&node));
       if (error || newp == NULL)
               return error;

       if (t < 0 || t >= sc->sc_alt_config)
               return EINVAL;

       /* This may not change when the unit is enabled */
       if (sc->sc_enabled)
               return EBUSY;

       KERNEL_LOCK(1, curlwp);
       sc->sc_config = t;
       error = ubt_set_isoc_config(sc);
       KERNEL_UNLOCK_ONE(curlwp);
       return error;
}

static void
ubt_abortdealloc(struct ubt_softc *sc)
{
       int i;

       DPRINTFN(1, "sc=%p\n", sc);

       /* Abort all pipes */
       usbd_abort_default_pipe(sc->sc_udev);

       if (sc->sc_evt_pipe != NULL) {
               usbd_abort_pipe(sc->sc_evt_pipe);
       }

       if (sc->sc_aclrd_pipe != NULL) {
               usbd_abort_pipe(sc->sc_aclrd_pipe);
       }

       if (sc->sc_aclwr_pipe != NULL) {
               usbd_abort_pipe(sc->sc_aclwr_pipe);
       }

       if (sc->sc_scord_pipe != NULL) {
               usbd_abort_pipe(sc->sc_scord_pipe);
       }

       if (sc->sc_scowr_pipe != NULL) {
               usbd_abort_pipe(sc->sc_scowr_pipe);
       }

       /* Free event buffer */
       if (sc->sc_evt_buf != NULL) {
               kmem_free(sc->sc_evt_buf, UBT_BUFSIZ_EVENT);
               sc->sc_evt_buf = NULL;
       }

       /* Free all xfers and xfer buffers (implicit) */
       if (sc->sc_cmd_xfer != NULL) {
               usbd_destroy_xfer(sc->sc_cmd_xfer);
               sc->sc_cmd_xfer = NULL;
               sc->sc_cmd_buf = NULL;
       }

       if (sc->sc_aclrd_xfer != NULL) {
               usbd_destroy_xfer(sc->sc_aclrd_xfer);
               sc->sc_aclrd_xfer = NULL;
               sc->sc_aclrd_buf = NULL;
       }

       if (sc->sc_aclwr_xfer != NULL) {
               usbd_destroy_xfer(sc->sc_aclwr_xfer);
               sc->sc_aclwr_xfer = NULL;
               sc->sc_aclwr_buf = NULL;
       }

       for (i = 0 ; i < UBT_NXFERS ; i++) {
               if (sc->sc_scord[i].xfer != NULL) {
                       usbd_destroy_xfer(sc->sc_scord[i].xfer);
                       sc->sc_scord[i].xfer = NULL;
                       sc->sc_scord[i].buf = NULL;
               }

               if (sc->sc_scowr[i].xfer != NULL) {
                       usbd_destroy_xfer(sc->sc_scowr[i].xfer);
                       sc->sc_scowr[i].xfer = NULL;
                       sc->sc_scowr[i].buf = NULL;
               }
       }

       if (sc->sc_evt_pipe != NULL) {
               usbd_close_pipe(sc->sc_evt_pipe);
               sc->sc_evt_pipe = NULL;
       }

       if (sc->sc_aclrd_pipe != NULL) {
               usbd_close_pipe(sc->sc_aclrd_pipe);
               sc->sc_aclrd_pipe = NULL;
       }

       if (sc->sc_aclwr_pipe != NULL) {
               usbd_close_pipe(sc->sc_aclwr_pipe);
               sc->sc_aclwr_pipe = NULL;
       }

       if (sc->sc_scord_pipe != NULL) {
               usbd_close_pipe(sc->sc_scord_pipe);
               sc->sc_scord_pipe = NULL;
       }

       if (sc->sc_scowr_pipe != NULL) {
               usbd_close_pipe(sc->sc_scowr_pipe);
               sc->sc_scowr_pipe = NULL;
       }

       /* Free partial SCO packets */
       m_freem(sc->sc_scord_mbuf);
       sc->sc_scord_mbuf = NULL;

       m_freem(sc->sc_scowr_mbuf);
       sc->sc_scowr_mbuf = NULL;

       /* Empty mbuf queues */
       MBUFQ_DRAIN(&sc->sc_cmd_queue);
       MBUFQ_DRAIN(&sc->sc_aclwr_queue);
       MBUFQ_DRAIN(&sc->sc_scowr_queue);
}

/*******************************************************************************
*
* Bluetooth Unit/USB callbacks
*
*/
static int
ubt_enable(device_t self)
{
       struct ubt_softc *sc = device_private(self);
       usbd_status err;
       int s, i, error;

       DPRINTFN(1, "sc=%p\n", sc);

       if (sc->sc_enabled)
               return 0;

       s = splusb();

       /* Events */
       sc->sc_evt_buf = kmem_alloc(UBT_BUFSIZ_EVENT, KM_SLEEP);
       err = usbd_open_pipe_intr(sc->sc_iface0,
                                 sc->sc_evt_addr,
                                 USBD_SHORT_XFER_OK,
                                 &sc->sc_evt_pipe,
                                 sc,
                                 sc->sc_evt_buf,
                                 UBT_BUFSIZ_EVENT,
                                 ubt_recv_event,
                                 USBD_DEFAULT_INTERVAL);
       if (err != USBD_NORMAL_COMPLETION) {
               error = EIO;
               goto bad;
       }

       /* Commands */
       struct usbd_pipe *pipe0 = usbd_get_pipe0(sc->sc_udev);
       error = usbd_create_xfer(pipe0, UBT_BUFSIZ_CMD, USBD_FORCE_SHORT_XFER,
           0, &sc->sc_cmd_xfer);
       if (error)
               goto bad;
       sc->sc_cmd_buf = usbd_get_buffer(sc->sc_cmd_xfer);
       sc->sc_cmd_busy = 0;

       /* ACL read */
       err = usbd_open_pipe(sc->sc_iface0, sc->sc_aclrd_addr,
                               USBD_EXCLUSIVE_USE, &sc->sc_aclrd_pipe);
       if (err != USBD_NORMAL_COMPLETION) {
               error = EIO;
               goto bad;
       }
       error = usbd_create_xfer(sc->sc_aclrd_pipe, UBT_BUFSIZ_ACL,
           0, 0, &sc->sc_aclrd_xfer);
       if (error)
               goto bad;
       sc->sc_aclrd_buf = usbd_get_buffer(sc->sc_aclrd_xfer);
       sc->sc_aclrd_busy = 0;
       ubt_recv_acl_start(sc);

       /* ACL write */
       err = usbd_open_pipe(sc->sc_iface0, sc->sc_aclwr_addr,
                               USBD_EXCLUSIVE_USE, &sc->sc_aclwr_pipe);
       if (err != USBD_NORMAL_COMPLETION) {
               error = EIO;
               goto bad;
       }
       error = usbd_create_xfer(sc->sc_aclwr_pipe, UBT_BUFSIZ_ACL,
           USBD_FORCE_SHORT_XFER, 0, &sc->sc_aclwr_xfer);
       if (error)
               goto bad;
       sc->sc_aclwr_buf = usbd_get_buffer(sc->sc_aclwr_xfer);
       sc->sc_aclwr_busy = 0;

       /* SCO read */
       if (sc->sc_scord_size > 0) {
               err = usbd_open_pipe(sc->sc_iface1, sc->sc_scord_addr,
                                       USBD_EXCLUSIVE_USE, &sc->sc_scord_pipe);
               if (err != USBD_NORMAL_COMPLETION) {
                       error = EIO;
                       goto bad;
               }

               for (i = 0 ; i < UBT_NXFERS ; i++) {
                       error = usbd_create_xfer(sc->sc_scord_pipe,
                           sc->sc_scord_size * UBT_NFRAMES,
                           0, UBT_NFRAMES,
                           &sc->sc_scord[i].xfer);
                       if (error)
                               goto bad;

                       sc->sc_scord[i].buf =
                           usbd_get_buffer(sc->sc_scord[i].xfer);
                       sc->sc_scord[i].softc = sc;
                       sc->sc_scord[i].busy = 0;
                       ubt_recv_sco_start1(sc, &sc->sc_scord[i]);
               }
       }

       /* SCO write */
       if (sc->sc_scowr_size > 0) {
               err = usbd_open_pipe(sc->sc_iface1, sc->sc_scowr_addr,
                                       USBD_EXCLUSIVE_USE, &sc->sc_scowr_pipe);
               if (err != USBD_NORMAL_COMPLETION) {
                       error = EIO;
                       goto bad;
               }

               for (i = 0 ; i < UBT_NXFERS ; i++) {
                       error = usbd_create_xfer(sc->sc_scowr_pipe,
                           sc->sc_scowr_size * UBT_NFRAMES,
                           USBD_FORCE_SHORT_XFER, UBT_NFRAMES,
                           &sc->sc_scowr[i].xfer);
                       if (error)
                               goto bad;
                       sc->sc_scowr[i].buf =
                           usbd_get_buffer(sc->sc_scowr[i].xfer);
                       sc->sc_scowr[i].softc = sc;
                       sc->sc_scowr[i].busy = 0;
               }

               sc->sc_scowr_busy = 0;
       }

       sc->sc_enabled = 1;
       splx(s);
       return 0;

bad:
       ubt_abortdealloc(sc);
       splx(s);
       return error;
}

static void
ubt_disable(device_t self)
{
       struct ubt_softc *sc = device_private(self);
       int s;

       DPRINTFN(1, "sc=%p\n", sc);

       if (sc->sc_enabled == 0)
               return;

       s = splusb();
       ubt_abortdealloc(sc);

       sc->sc_enabled = 0;
       splx(s);
}

static void
ubt_xmit_cmd(device_t self, struct mbuf *m)
{
       struct ubt_softc *sc = device_private(self);
       int s;

       KASSERT(sc->sc_enabled);

       s = splusb();
       MBUFQ_ENQUEUE(&sc->sc_cmd_queue, m);

       if (sc->sc_cmd_busy == 0)
               ubt_xmit_cmd_start(sc);

       splx(s);
}

static void
ubt_xmit_cmd_start(struct ubt_softc *sc)
{
       usb_device_request_t req;
       usbd_status status;
       struct mbuf *m;
       int len;

       if (sc->sc_dying)
               return;

       if (MBUFQ_FIRST(&sc->sc_cmd_queue) == NULL)
               return;

       MBUFQ_DEQUEUE(&sc->sc_cmd_queue, m);
       KASSERT(m != NULL);

       DPRINTFN(15, "%s: xmit CMD packet (%d bytes)\n",
                       device_xname(sc->sc_dev), m->m_pkthdr.len);

       sc->sc_refcnt++;
       sc->sc_cmd_busy = 1;

       len = m->m_pkthdr.len - 1;
       m_copydata(m, 1, len, sc->sc_cmd_buf);
       m_freem(m);

       memset(&req, 0, sizeof(req));
       req.bmRequestType = UT_WRITE_CLASS_DEVICE;
       USETW(req.wLength, len);

       usbd_setup_default_xfer(sc->sc_cmd_xfer,
                               sc->sc_udev,
                               sc,
                               UBT_CMD_TIMEOUT,
                               &req,
                               sc->sc_cmd_buf,
                               len,
                               USBD_FORCE_SHORT_XFER,
                               ubt_xmit_cmd_complete);

       status = usbd_transfer(sc->sc_cmd_xfer);

       KASSERT(status != USBD_NORMAL_COMPLETION);

       if (status != USBD_IN_PROGRESS) {
               DPRINTF("usbd_transfer status=%s (%d)\n",
                       usbd_errstr(status), status);

               sc->sc_refcnt--;
               sc->sc_cmd_busy = 0;
       }
}

static void
ubt_xmit_cmd_complete(struct usbd_xfer *xfer,
                       void * h, usbd_status status)
{
       struct ubt_softc *sc = h;
       uint32_t count;

       DPRINTFN(15, "%s: CMD complete status=%s (%d)\n",
                       device_xname(sc->sc_dev), usbd_errstr(status), status);

       sc->sc_cmd_busy = 0;

       if (--sc->sc_refcnt < 0) {
               DPRINTF("sc_refcnt=%d\n", sc->sc_refcnt);
               usb_detach_wakeupold(sc->sc_dev);
               return;
       }

       if (sc->sc_dying) {
               DPRINTF("sc_dying\n");
               return;
       }

       if (status != USBD_NORMAL_COMPLETION) {
               DPRINTF("status=%s (%d)\n",
                       usbd_errstr(status), status);

               sc->sc_stats.err_tx++;
               return;
       }

       usbd_get_xfer_status(xfer, NULL, NULL, &count, NULL);
       sc->sc_stats.cmd_tx++;
       sc->sc_stats.byte_tx += count;

       ubt_xmit_cmd_start(sc);
}

static void
ubt_xmit_acl(device_t self, struct mbuf *m)
{
       struct ubt_softc *sc = device_private(self);
       int s;

       KASSERT(sc->sc_enabled);

       s = splusb();
       MBUFQ_ENQUEUE(&sc->sc_aclwr_queue, m);

       if (sc->sc_aclwr_busy == 0)
               ubt_xmit_acl_start(sc);

       splx(s);
}

static void
ubt_xmit_acl_start(struct ubt_softc *sc)
{
       struct mbuf *m;
       usbd_status status;
       int len;

       if (sc->sc_dying)
               return;

       if (MBUFQ_FIRST(&sc->sc_aclwr_queue) == NULL)
               return;

       sc->sc_refcnt++;
       sc->sc_aclwr_busy = 1;

       MBUFQ_DEQUEUE(&sc->sc_aclwr_queue, m);
       KASSERT(m != NULL);

       DPRINTFN(15, "%s: xmit ACL packet (%d bytes)\n",
                       device_xname(sc->sc_dev), m->m_pkthdr.len);

       len = m->m_pkthdr.len - 1;
       if (len > UBT_BUFSIZ_ACL) {
               DPRINTF("%s: truncating ACL packet (%d => %d)!\n",
                       device_xname(sc->sc_dev), len, UBT_BUFSIZ_ACL);

               len = UBT_BUFSIZ_ACL;
       }

       m_copydata(m, 1, len, sc->sc_aclwr_buf);
       m_freem(m);

       sc->sc_stats.acl_tx++;
       sc->sc_stats.byte_tx += len;

       usbd_setup_xfer(sc->sc_aclwr_xfer,
                       sc,
                       sc->sc_aclwr_buf,
                       len,
                       USBD_FORCE_SHORT_XFER,
                       UBT_ACL_TIMEOUT,
                       ubt_xmit_acl_complete);

       status = usbd_transfer(sc->sc_aclwr_xfer);

       KASSERT(status != USBD_NORMAL_COMPLETION);

       if (status != USBD_IN_PROGRESS) {
               DPRINTF("usbd_transfer status=%s (%d)\n",
                       usbd_errstr(status), status);

               sc->sc_refcnt--;
               sc->sc_aclwr_busy = 0;
       }
}

static void
ubt_xmit_acl_complete(struct usbd_xfer *xfer,
               void * h, usbd_status status)
{
       struct ubt_softc *sc = h;

       DPRINTFN(15, "%s: ACL complete status=%s (%d)\n",
               device_xname(sc->sc_dev), usbd_errstr(status), status);

       sc->sc_aclwr_busy = 0;

       if (--sc->sc_refcnt < 0) {
               usb_detach_wakeupold(sc->sc_dev);
               return;
       }

       if (sc->sc_dying)
               return;

       if (status != USBD_NORMAL_COMPLETION) {
               DPRINTF("status=%s (%d)\n",
                       usbd_errstr(status), status);

               sc->sc_stats.err_tx++;

               if (status == USBD_STALLED)
                       usbd_clear_endpoint_stall_async(sc->sc_aclwr_pipe);
               else
                       return;
       }

       ubt_xmit_acl_start(sc);
}

static void
ubt_xmit_sco(device_t self, struct mbuf *m)
{
       struct ubt_softc *sc = device_private(self);
       int s;

       KASSERT(sc->sc_enabled);

       s = splusb();
       MBUFQ_ENQUEUE(&sc->sc_scowr_queue, m);

       if (sc->sc_scowr_busy == 0)
               ubt_xmit_sco_start(sc);

       splx(s);
}

static void
ubt_xmit_sco_start(struct ubt_softc *sc)
{
       int i;

       if (sc->sc_dying || sc->sc_scowr_size == 0)
               return;

       for (i = 0 ; i < UBT_NXFERS ; i++) {
               if (sc->sc_scowr[i].busy)
                       continue;

               ubt_xmit_sco_start1(sc, &sc->sc_scowr[i]);
       }
}

static void
ubt_xmit_sco_start1(struct ubt_softc *sc, struct ubt_isoc_xfer *isoc)
{
       struct mbuf *m;
       uint8_t *buf;
       int num, len, size, space;

       space = sc->sc_scowr_size * UBT_NFRAMES;
       buf = isoc->buf;
       len = 0;

       /*
        * Fill the request buffer with data from the queue,
        * keeping any leftover packet on our private hook.
        *
        * Complete packets are passed back up to the stack
        * for disposal, since we can't rely on the controller
        * to tell us when it has finished with them.
        */

       m = sc->sc_scowr_mbuf;
       while (space > 0) {
               if (m == NULL) {
                       MBUFQ_DEQUEUE(&sc->sc_scowr_queue, m);
                       if (m == NULL)
                               break;

                       m_adj(m, 1);    /* packet type */
               }

               if (m->m_pkthdr.len > 0) {
                       size = MIN(m->m_pkthdr.len, space);

                       m_copydata(m, 0, size, buf);
                       m_adj(m, size);

                       buf += size;
                       len += size;
                       space -= size;
               }

               if (m->m_pkthdr.len == 0) {
                       sc->sc_stats.sco_tx++;
                       if (!hci_complete_sco(sc->sc_unit, m))
                               sc->sc_stats.err_tx++;

                       m = NULL;
               }
       }
       sc->sc_scowr_mbuf = m;

       DPRINTFN(15, "isoc=%p, len=%d, space=%d\n", isoc, len, space);

       if (len == 0)   /* nothing to send */
               return;

       sc->sc_refcnt++;
       sc->sc_scowr_busy = 1;
       sc->sc_stats.byte_tx += len;
       isoc->busy = 1;

       /*
        * calculate number of isoc frames and sizes
        */

       for (num = 0 ; len > 0 ; num++) {
               size = MIN(sc->sc_scowr_size, len);

               isoc->size[num] = size;
               len -= size;
       }

       usbd_setup_isoc_xfer(isoc->xfer,
                            isoc,
                            isoc->size,
                            num,
                            USBD_FORCE_SHORT_XFER,
                            ubt_xmit_sco_complete);

       usbd_transfer(isoc->xfer);
}

static void
ubt_xmit_sco_complete(struct usbd_xfer *xfer,
               void * h, usbd_status status)
{
       struct ubt_isoc_xfer *isoc = h;
       struct ubt_softc *sc;
       int i;

       KASSERT(xfer == isoc->xfer);
       sc = isoc->softc;

       DPRINTFN(15, "isoc=%p, status=%s (%d)\n",
               isoc, usbd_errstr(status), status);

       isoc->busy = 0;

       for (i = 0 ; ; i++) {
               if (i == UBT_NXFERS) {
                       sc->sc_scowr_busy = 0;
                       break;
               }

               if (sc->sc_scowr[i].busy)
                       break;
       }

       if (--sc->sc_refcnt < 0) {
               usb_detach_wakeupold(sc->sc_dev);
               return;
       }

       if (sc->sc_dying)
               return;

       if (status != USBD_NORMAL_COMPLETION) {
               DPRINTF("status=%s (%d)\n",
                       usbd_errstr(status), status);

               sc->sc_stats.err_tx++;

               if (status == USBD_STALLED)
                       usbd_clear_endpoint_stall_async(sc->sc_scowr_pipe);
               else
                       return;
       }

       ubt_xmit_sco_start(sc);
}

/*
* load incoming data into an mbuf with
* leading type byte
*/
static struct mbuf *
ubt_mbufload(uint8_t *buf, int count, uint8_t type)
{
       struct mbuf *m;

       MGETHDR(m, M_DONTWAIT, MT_DATA);
       if (m == NULL)
               return NULL;

       *mtod(m, uint8_t *) = type;
       m->m_pkthdr.len = m->m_len = MHLEN;
       m_copyback(m, 1, count, buf);   // (extends if necessary)
       if (m->m_pkthdr.len != MAX(MHLEN, count + 1)) {
               m_freem(m);
               return NULL;
       }

       m->m_pkthdr.len = count + 1;
       m->m_len = MIN(MHLEN, m->m_pkthdr.len);

       return m;
}

static void
ubt_recv_event(struct usbd_xfer *xfer, void * h, usbd_status status)
{
       struct ubt_softc *sc = h;
       struct mbuf *m;
       uint32_t count;
       void *buf;

       DPRINTFN(15, "sc=%p status=%s (%d)\n",
                   sc, usbd_errstr(status), status);

       if (status != USBD_NORMAL_COMPLETION || sc->sc_dying)
               return;

       usbd_get_xfer_status(xfer, NULL, &buf, &count, NULL);

       if (count < sizeof(hci_event_hdr_t) - 1) {
               DPRINTF("dumped undersized event (count = %d)\n", count);
               sc->sc_stats.err_rx++;
               return;
       }

       sc->sc_stats.evt_rx++;
       sc->sc_stats.byte_rx += count;

       m = ubt_mbufload(buf, count, HCI_EVENT_PKT);
       if (m == NULL || !hci_input_event(sc->sc_unit, m))
               sc->sc_stats.err_rx++;
}

static void
ubt_recv_acl_start(struct ubt_softc *sc)
{
       usbd_status status;

       DPRINTFN(15, "sc=%p\n", sc);

       if (sc->sc_aclrd_busy || sc->sc_dying) {
               DPRINTF("sc_aclrd_busy=%d, sc_dying=%d\n",
                       sc->sc_aclrd_busy,
                       sc->sc_dying);

               return;
       }

       sc->sc_refcnt++;
       sc->sc_aclrd_busy = 1;

       usbd_setup_xfer(sc->sc_aclrd_xfer,
                       sc,
                       sc->sc_aclrd_buf,
                       UBT_BUFSIZ_ACL,
                       USBD_SHORT_XFER_OK,
                       USBD_NO_TIMEOUT,
                       ubt_recv_acl_complete);

       status = usbd_transfer(sc->sc_aclrd_xfer);

       KASSERT(status != USBD_NORMAL_COMPLETION);

       if (status != USBD_IN_PROGRESS) {
               DPRINTF("usbd_transfer status=%s (%d)\n",
                       usbd_errstr(status), status);

               sc->sc_refcnt--;
               sc->sc_aclrd_busy = 0;
       }
}

static void
ubt_recv_acl_complete(struct usbd_xfer *xfer,
               void * h, usbd_status status)
{
       struct ubt_softc *sc = h;
       struct mbuf *m;
       uint32_t count;
       void *buf;

       DPRINTFN(15, "sc=%p status=%s (%d)\n",
                       sc, usbd_errstr(status), status);

       sc->sc_aclrd_busy = 0;

       if (--sc->sc_refcnt < 0) {
               DPRINTF("refcnt = %d\n", sc->sc_refcnt);
               usb_detach_wakeupold(sc->sc_dev);
               return;
       }

       if (sc->sc_dying) {
               DPRINTF("sc_dying\n");
               return;
       }

       if (status != USBD_NORMAL_COMPLETION) {
               DPRINTF("status=%s (%d)\n",
                       usbd_errstr(status), status);

               sc->sc_stats.err_rx++;

               if (status == USBD_STALLED)
                       usbd_clear_endpoint_stall_async(sc->sc_aclrd_pipe);
               else
                       return;
       } else {
               usbd_get_xfer_status(xfer, NULL, &buf, &count, NULL);

               if (count < sizeof(hci_acldata_hdr_t) - 1) {
                       DPRINTF("dumped undersized packet (%d)\n", count);
                       sc->sc_stats.err_rx++;
               } else {
                       sc->sc_stats.acl_rx++;
                       sc->sc_stats.byte_rx += count;

                       m = ubt_mbufload(buf, count, HCI_ACL_DATA_PKT);
                       if (m == NULL || !hci_input_acl(sc->sc_unit, m))
                               sc->sc_stats.err_rx++;
               }
       }

       /* and restart */
       ubt_recv_acl_start(sc);
}

static void
ubt_recv_sco_start1(struct ubt_softc *sc, struct ubt_isoc_xfer *isoc)
{
       int i;

       DPRINTFN(15, "sc=%p, isoc=%p\n", sc, isoc);

       if (isoc->busy || sc->sc_dying || sc->sc_scord_size == 0) {
               DPRINTF("%s%s%s\n",
                       isoc->busy ? " busy" : "",
                       sc->sc_dying ? " dying" : "",
                       sc->sc_scord_size == 0 ? " size=0" : "");

               return;
       }

       sc->sc_refcnt++;
       isoc->busy = 1;

       for (i = 0 ; i < UBT_NFRAMES ; i++)
               isoc->size[i] = sc->sc_scord_size;

       usbd_setup_isoc_xfer(isoc->xfer,
                            isoc,
                            isoc->size,
                            UBT_NFRAMES,
                            USBD_SHORT_XFER_OK,
                            ubt_recv_sco_complete);

       usbd_transfer(isoc->xfer);
}

static void
ubt_recv_sco_complete(struct usbd_xfer *xfer,
               void * h, usbd_status status)
{
       struct ubt_isoc_xfer *isoc = h;
       struct ubt_softc *sc;
       struct mbuf *m;
       uint32_t count;
       uint8_t *ptr, *frame;
       int i, size, got, want;

       KASSERT(isoc != NULL);
       KASSERT(isoc->xfer == xfer);

       sc = isoc->softc;
       isoc->busy = 0;

       if (--sc->sc_refcnt < 0) {
               DPRINTF("refcnt=%d\n", sc->sc_refcnt);
               usb_detach_wakeupold(sc->sc_dev);
               return;
       }

       if (sc->sc_dying) {
               DPRINTF("sc_dying\n");
               return;
       }

       if (status != USBD_NORMAL_COMPLETION) {
               DPRINTF("status=%s (%d)\n",
                       usbd_errstr(status), status);

               sc->sc_stats.err_rx++;

               if (status == USBD_STALLED) {
                       usbd_clear_endpoint_stall_async(sc->sc_scord_pipe);
                       goto restart;
               }

               return;
       }

       usbd_get_xfer_status(xfer, NULL, NULL, &count, NULL);
       if (count == 0)
               goto restart;

       DPRINTFN(15, "sc=%p, isoc=%p, count=%u\n",
                       sc, isoc, count);

       sc->sc_stats.byte_rx += count;

       /*
        * Extract SCO packets from ISOC frames. The way we have it,
        * no SCO packet can be bigger than MHLEN. This is unlikely
        * to actually happen, but if we ran out of mbufs and lost
        * sync then we may get spurious data that makes it seem that
        * way, so we discard data that wont fit. This doesnt really
        * help with the lost sync situation alas.
        */

       m = sc->sc_scord_mbuf;
       if (m != NULL) {
               sc->sc_scord_mbuf = NULL;
               ptr = mtod(m, uint8_t *) + m->m_pkthdr.len;
               got = m->m_pkthdr.len;
               want = sizeof(hci_scodata_hdr_t);
               if (got >= want)
                       want += mtod(m, hci_scodata_hdr_t *)->length ;
       } else {
               ptr = NULL;
               got = 0;
               want = 0;
       }

       for (i = 0 ; i < UBT_NFRAMES ; i++) {
               frame = isoc->buf + (i * sc->sc_scord_size);

               while (isoc->size[i] > 0) {
                       size = isoc->size[i];

                       if (m == NULL) {
                               MGETHDR(m, M_DONTWAIT, MT_DATA);
                               if (m == NULL) {
                                       aprint_error_dev(sc->sc_dev,
                                           "out of memory (xfer halted)\n");

                                       sc->sc_stats.err_rx++;
                                       return;         /* lost sync */
                               }

                               ptr = mtod(m, uint8_t *);
                               *ptr++ = HCI_SCO_DATA_PKT;
                               got = 1;
                               want = sizeof(hci_scodata_hdr_t);
                       }

                       if (got + size > want)
                               size = want - got;

                       memcpy(ptr, frame, size);

                       ptr += size;
                       got += size;
                       frame += size;

                       if (got == want) {
                               /*
                                * If we only got a header, add the packet
                                * length to our want count. Send complete
                                * packets up to protocol stack.
                                */
                               if (want == sizeof(hci_scodata_hdr_t)) {
                                       uint32_t len =
                                           mtod(m, hci_scodata_hdr_t *)->length;
                                       want += len;
                                       if (len == 0 || want > MHLEN) {
                                               aprint_error_dev(sc->sc_dev,
                                                   "packet too large %u "
                                                   "(lost sync)\n", len);
                                               sc->sc_stats.err_rx++;
                                               return;
                                       }
                               }

                               if (got == want) {
                                       m->m_pkthdr.len = m->m_len = got;
                                       sc->sc_stats.sco_rx++;
                                       if (!hci_input_sco(sc->sc_unit, m))
                                               sc->sc_stats.err_rx++;

                                       m = NULL;
                               }
                       }

                       isoc->size[i] -= size;
               }
       }

       if (m != NULL) {
               m->m_pkthdr.len = m->m_len = got;
               sc->sc_scord_mbuf = m;
       }

restart: /* and restart */
       ubt_recv_sco_start1(sc, isoc);
}

void
ubt_stats(device_t self, struct bt_stats *dest, int flush)
{
       struct ubt_softc *sc = device_private(self);
       int s;

       s = splusb();
       memcpy(dest, &sc->sc_stats, sizeof(struct bt_stats));

       if (flush)
               memset(&sc->sc_stats, 0, sizeof(struct bt_stats));

       splx(s);
}