// SPDX-License-Identifier: GPL-2.0-or-later

/* Osmocom icE1usb driver
 * Copyright (C) 2020-2022 by Harald Welte <laforge@osmocom.org>
 * inspired by osmo-e1d by Sylvain Munaut */

/*  The Osmocom icE1usb is a modern, software-defined open hardware and
    open source firmware design for a USB E1 interface.  You can find more
    information at https://osmocom.org/projects/e1-t1-adapter/wiki/IcE1usb

    The hardware design can be found at https://git.osmocom.org/osmo-e1-hardware

    The FPGA gateware and associated embedded firmware is hosted in the same
    git repository. Some parts are in submodules (be sure to use recursive
    clone)

    This DAHDI driver allows the use of the icE1usb just like any other
    E1/PRI interface device supported by DAHDI.  When using this DAHDI
    driver, osmo-e1d most not be used.  The DAHDI driver is a replacement /
    alternative for osmo-e1d.

*/

#define DEBUG

#include <linux/module.h>
#include <linux/init.h>
#include <linux/usb.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/spinlock.h>
#include <linux/pm_runtime.h>

#include <dahdi/kernel.h>

#include "ice1usb_proto.h"

#define VERSION "0.2"

/* number of isochronous frames per URB */
#define ICE1USB_MAX_ISOC_FRAMES 	4

/* number of URBs per endpoint */
#define ICE1USB_NUM_URBS 	4

#define ieu_dbg(x, fmt, args ...) \
	dev_dbg(&((x)->usb_intf->dev), fmt, ## args)
#define ieu_info(x, fmt, args ...) \
	dev_info(&((x)->usb_intf->dev), fmt, ## args)
#define ieu_warn(x, fmt, args ...) \
	dev_warn(&((x)->usb_intf->dev), fmt, ## args)
#define ieu_err(x, fmt, args ...) \
	dev_err(&((x)->usb_intf->dev), fmt, ## args)

static const struct usb_device_id ice1usb_products[] = {
	{
		USB_DEVICE(0x1d50, 0x6145),
	},
	{}
};

/***********************************************************************
* data structures
***********************************************************************/

/* per-device global state */
struct ice1usb_gpsdo {
	/* USB device we operate on */
	struct usb_device *usb_dev;
	struct usb_interface *usb_intf;

	struct e1usb_gpsdo_status gpsdo_status;
	char fw_build[128];

	/* is the device still present (true) or already absent/unplugged (false) */
	bool present;
	/* spinlock protecting concurrent access to fc, {read,write}chunk_idx, ... */
	spinlock_t lock;
};


enum ice1usb_flags {
	ICE1USB_ISOC_RUNNING,
	ICE1USB_IRQ_RUNNING,
};

/* per-interface state */
struct ice1usb {
	/* USB device and interface we operate on */
	struct usb_device *usb_dev;
	struct usb_interface *usb_intf;
	/* altsetting for 'OFF' state */
	const struct usb_host_interface *alt_off;
	/* altsetting for 'ON' state */
	const struct usb_host_interface *alt_on;
	struct {
		struct usb_anchor iso_in;
		struct usb_anchor iso_out;
		struct usb_anchor iso_fb;
		struct usb_anchor irq;
	} anchor;
	/* USB endpoint numbers */
	struct {
		const struct usb_endpoint_descriptor *iso_in;
		const struct usb_endpoint_descriptor *iso_out;
		const struct usb_endpoint_descriptor *iso_fb;
		const struct usb_endpoint_descriptor *irq;
	} ep;
	struct {
		struct ice1usb_tx_config tx;
		struct ice1usb_rx_config rx;
	} cfg;
	/* last received error interrupt */
	struct ice1usb_irq_err last_err;
	struct {
		uint32_t ovfl;
		uint32_t unfl;
	} count;
	/* enum ice1usb_flags */
	unsigned long flags;
	/* feedback flow-control */
	struct {
		uint32_t r_acc;
		uint32_t r_sw;
	} fc;
	/* DAHDI driver related bits */
	struct {
		struct dahdi_device *dev;
		struct dahdi_span span;
		struct dahdi_chan *chans[31];
		/* [next rx byte] offset into chan[i]->readchunk */
		unsigned int readchunk_idx;
		/* [next tx byte] offset into chan[i]->writechunk */
		unsigned int writechunk_idx;
	} dahdi;
	/* is the device still present (true) or already absent/unplugged (false) */
	bool present;
	/* spinlock protecting concurrent access to fc, {read,write}chunk_idx, ... */
	spinlock_t lock;
	/* maximum number of 32-byte E1 frames to send in one ISO OUT packet */
	unsigned int max_fts;
};

static void ice1usb_free(struct ice1usb *ieu)
{
	usb_put_dev(ieu->usb_dev);
	kfree(ieu);
}

static void e1u_free_channels(struct ice1usb *ieu)
{
	unsigned int i;

	for (i = 0; i < ARRAY_SIZE(ieu->dahdi.chans); i++) {
		kfree(ieu->dahdi.chans[i]);
		ieu->dahdi.chans[i] = NULL;
	}
}

static int e1u_alloc_channels(struct ice1usb *ieu)
{
	unsigned int i;

	for (i = 0; i < ARRAY_SIZE(ieu->dahdi.chans); i++) {
		struct dahdi_chan *chan;
		kfree(ieu->dahdi.chans[i]);
		chan = ieu->dahdi.chans[i] = kzalloc(sizeof(*chan), GFP_KERNEL);
		if (!chan) {
			e1u_free_channels(ieu);
			return -ENOMEM;
		}

		chan->pvt = ieu;
		snprintf(chan->name, sizeof(chan->name)-1, "%s/%d",
			 ieu->dahdi.span.name, i+1);
		chan->chanpos = i+1;
		chan->sigcap = DAHDI_SIG_CLEAR | DAHDI_SIG_MTP2 | DAHDI_SIG_SF;
	}
	return 0;
}

static const char *tx_mode_str(enum ice1usb_tx_mode tx_mode)
{
	switch (tx_mode) {
	case ICE1USB_TX_MODE_TRANSP:
		return "TRANSPARENT";
	case ICE1USB_TX_MODE_TS0:
		return "TS0";
	case ICE1USB_TX_MODE_TS0_CRC4:
		return "TS0_CRC4";
	case ICE1USB_TX_MODE_TS0_CRC4_E:
		return "TS0_CRC4_E";
	default:
		return "unknown";
	}
}

static const char *tx_timing_str(enum ice1usb_tx_timing tx_timing)
{
	switch (tx_timing) {
	case ICE1USB_TX_TIME_SRC_LOCAL:
		return "LOCAL";
	case ICE1USB_TX_TIME_SRC_REMOTE:
		return "REMOTE";
	default:
		return "unknown";
	}
}

static const char *tx_ext_loopback_str(enum ice1usb_tx_ext_loopback ext_lb)
{
	switch (ext_lb) {
	case ICE1USB_TX_EXT_LOOPBACK_OFF:
		return "OFF";
	case ICE1USB_TX_EXT_LOOPBACK_SAME:
		return "SAME";
	case ICE1USB_TX_EXT_LOOPBACK_CROSS:
		return "CROSS";
	default:
		return "unknown";
	}
}

static const char *rx_mode_str(enum ice1usb_rx_mode rx_mode)
{
	switch (rx_mode) {
	case ICE1USB_RX_MODE_FRAME:
		return "FRAME";
	case ICE1USB_RX_MODE_MULTIFRAME:
		return "MULTIFRAME";
	default:
		return "unknown";
	}
}


#define USB_RT_VEND_IF (USB_TYPE_VENDOR | USB_RECIP_INTERFACE)
#define USB_RT_VEND_DEV (USB_TYPE_VENDOR | USB_RECIP_DEVICE)

/* synchronous requests, may block up to 1s, only called from process context! */
static int ice1usb_tx_config(struct ice1usb *ieu)
{
	int rc;
	uint8_t if_num = ieu->usb_intf->cur_altsetting->desc.bInterfaceNumber;

	ieu_info(ieu, "TX-CONFIG (crc4=%s, timing=%s, ext_loopback=%s, tx_yellow_alarm=%u)\n",
		 tx_mode_str(ieu->cfg.tx.mode), tx_timing_str(ieu->cfg.tx.timing),
		 tx_ext_loopback_str(ieu->cfg.tx.ext_loopback), ieu->cfg.tx.alarm);

	rc = usb_control_msg(ieu->usb_dev, usb_sndctrlpipe(ieu->usb_dev, 0),
				ICE1USB_INTF_SET_TX_CFG, USB_RT_VEND_IF,
				0, if_num, &ieu->cfg.tx, sizeof(ieu->cfg.tx),
				USB_CTRL_SET_TIMEOUT);
	if (rc < 0)
		return rc;
	if (rc != sizeof(ieu->cfg.tx))
		return -EIO;
	return 0;
}

static int ice1usb_rx_config(struct ice1usb *ieu)
{
	int rc;
	uint8_t if_num = ieu->usb_intf->cur_altsetting->desc.bInterfaceNumber;

	ieu_info(ieu, "RX-CONFIG (mode=%s)\n",
		 rx_mode_str(ieu->cfg.rx.mode));

	rc = usb_control_msg(ieu->usb_dev, usb_sndctrlpipe(ieu->usb_dev, 0),
				ICE1USB_INTF_SET_RX_CFG, USB_RT_VEND_IF,
				0, if_num, &ieu->cfg.rx, sizeof(ieu->cfg.rx),
				USB_CTRL_SET_TIMEOUT);
	if (rc < 0)
		return rc;
	if (rc != sizeof(ieu->cfg.rx))
		return -EIO;
	return 0;
}


/***********************************************************************
 * ISOCHRONOUS transfers
 ***********************************************************************/

static inline void __fill_isoc_descriptor(struct urb *urb, unsigned int max_pack_size)
{
	unsigned int i, offset = 0;

	for (i = 0; i < ICE1USB_MAX_ISOC_FRAMES; i++) {
		urb->iso_frame_desc[i].offset = offset;
		urb->iso_frame_desc[i].length = max_pack_size;
		offset += max_pack_size;
	};
	urb->number_of_packets = i;
}

/* allocate + submit an isochronous URB for given EP; anchor it */
static int ice1usb_submit_isoc_urb(struct ice1usb *ieu,
				   const struct usb_endpoint_descriptor *ep,
				   struct usb_anchor *anchor,
				   usb_complete_t compl, gfp_t mem_flags)
{
	unsigned int max_pack_size = le16_to_cpu(ep->wMaxPacketSize);
	unsigned int pipe, size, rc;
	struct urb *urb;
	uint8_t *buf;

	urb = usb_alloc_urb(ICE1USB_MAX_ISOC_FRAMES, mem_flags);
	if (!urb)
		return -ENOMEM;

	size = max_pack_size * ICE1USB_MAX_ISOC_FRAMES;
	buf = kmalloc(size, mem_flags);
	if (!buf) {
		usb_free_urb(urb);
		return -ENOMEM;
	}

	if (ep->bEndpointAddress & USB_DIR_IN)
		pipe = usb_rcvisocpipe(ieu->usb_dev, ep->bEndpointAddress);
	else
		pipe = usb_sndisocpipe(ieu->usb_dev, ep->bEndpointAddress);

	usb_fill_int_urb(urb, ieu->usb_dev, pipe, buf, size, compl, ieu,
			 ep->bInterval);
	urb->transfer_flags = URB_FREE_BUFFER | URB_ISO_ASAP;

	__fill_isoc_descriptor(urb, max_pack_size);

	usb_anchor_urb(urb, anchor);

	rc = usb_submit_urb(urb, mem_flags);
	if (rc < 0) {
		/* -EPERM: urb is being killed; -ENODEV: device got disconnected */
		if (rc != -EPERM && rc != -ENODEV && rc != -ENOENT)
			ieu_err(ieu, "EP 0x%02x urb %p submission failed (%d)",
				ep->bEndpointAddress, urb, -rc);
		usb_unanchor_urb(urb);
	}

	/* automatically free the URB (and its transfer buffer) once complete */
	usb_free_urb(urb);

	return rc;
}

/* process one incoming E1 frame (32 bytes; one for each TS) */
static void _span_demux_one_frame(struct ice1usb *ieu, const uint8_t *data)
{
	struct dahdi_span *dspan = &ieu->dahdi.span;
	unsigned int i;

	for (i = 0; i < ARRAY_SIZE(ieu->dahdi.chans); i++) {
		struct dahdi_chan *chan = ieu->dahdi.chans[i];
		chan->readchunk[ieu->dahdi.readchunk_idx] = data[1+i];
	}
	ieu->dahdi.readchunk_idx++;

	/* DAHDI_CHUNKSIZE is 8, meaning every channel (timeslot) wants data for 8
 	 * bytes (PCM samples) every time _dahdi_receive() is called */
	if (ieu->dahdi.readchunk_idx == DAHDI_CHUNKSIZE) {
		_dahdi_receive(dspan);
		ieu->dahdi.readchunk_idx = 0;
	}
}

/* IN endpoint URB completes */
static void iso_in_complete(struct urb *urb)
{
	struct ice1usb *ieu = urb->context;
	unsigned int i;
	int rc;

	//ieu_dbg(ieu, "IN urb %p completion (%d)", urb, urb->status);

	switch (urb->status) {
	case 0:
		for (i = 0; i < urb->number_of_packets; i++) {
			unsigned int offset = urb->iso_frame_desc[i].offset;
			unsigned int length = urb->iso_frame_desc[i].actual_length;
			unsigned long flags;
			unsigned int j;

			if (urb->iso_frame_desc[i].status) {
				ieu_err(ieu, "IN urb %p frame %u status %d",
					urb, i, urb->iso_frame_desc[i].status);
				continue;
			}

			/* process received data */
			spin_lock_irqsave(&ieu->lock, flags);
			for (j = 4; j < length; j += 32)
				_span_demux_one_frame(ieu, urb->transfer_buffer + offset + j);
			spin_unlock_irqrestore(&ieu->lock, flags);
		}
		break;
	case -ENOENT:
	case -ESHUTDOWN:
		/* Avoid suspend failed when usb_kill_urb */
		return;
	default:
		ieu_err(ieu, "IN urb %p completion (%d)", urb, urb->status);
		break;
	}

	/* re-submit unless stopped */
	if (!test_bit(ICE1USB_ISOC_RUNNING, &ieu->flags))
		return;

	usb_anchor_urb(urb, &ieu->anchor.iso_in);
	usb_mark_last_busy(ieu->usb_dev);

	rc = usb_submit_urb(urb, GFP_ATOMIC);
	if (rc < 0) {
		/* -EPERM: urb is being killed; -ENODEV: device got disconnected */
		if (rc != -EPERM && rc != -ENODEV && rc != -ENOENT)
			ieu_err(ieu, "IN urb %p submission failed (%d)", urb, -rc);
		usb_unanchor_urb(urb);
	}
}

/* Feedback endpoint URB completes */
static void iso_fb_complete(struct urb *urb)
{
	struct ice1usb *ieu = urb->context;
	unsigned int i;
	int rc;

	//ieu_dbg(ieu, "FB urb %p completion (%d)", urb, urb->status);

	switch (urb->status) {
	case 0:
		for (i = 0; i < urb->number_of_packets; i++) {
			unsigned int offset = urb->iso_frame_desc[i].offset;
			unsigned int length = urb->iso_frame_desc[i].actual_length;
			const uint8_t *rx = urb->transfer_buffer + offset;
			unsigned long flags;

			if (urb->iso_frame_desc[i].status) {
				ieu_err(ieu, "FB urb %p frame %u status %d",
					urb, i, urb->iso_frame_desc[i].status);
			}

			if (urb->iso_frame_desc[i].status || length < 3)
				continue;

			/* process received data */
			spin_lock_irqsave(&ieu->lock, flags);
			ieu->fc.r_sw = (rx[2] << 16) | (rx[1] << 8) | rx[0];
			spin_unlock_irqrestore(&ieu->lock, flags);
		}
		break;
	case -ENOENT:
	case -ESHUTDOWN:
		/* Avoid suspend failed when usb_kill_urb */
		return;
	default:
		ieu_err(ieu, "FB urb %p completion (%d)", urb, urb->status);
		break;
	}

	/* re-submit unless stopped */
	if (!test_bit(ICE1USB_ISOC_RUNNING, &ieu->flags))
		return;

	usb_anchor_urb(urb, &ieu->anchor.iso_fb);
	usb_mark_last_busy(ieu->usb_dev);

	rc = usb_submit_urb(urb, GFP_ATOMIC);
	if (rc < 0) {
		/* -EPERM: urb is being killed; -ENODEV: device got disconnected */
		if (rc != -EPERM && rc != -ENODEV && rc != -ENOENT)
			dev_err(&ieu->usb_dev->dev, "FB urb %p submission failed (%d)", urb, -rc);
		usb_unanchor_urb(urb);
	}
}

/* multiplex one E1 frame (32 bytes) and write it to 'out'; caller must hold ieu->lock */
static void _span_mux_one_frame(struct ice1usb *ieu, uint8_t *out)
{
	struct dahdi_span *dspan = &ieu->dahdi.span;
	unsigned int i;

	out[0] = 0;

	for (i = 0; i < ARRAY_SIZE(ieu->dahdi.chans); i++) {
		struct dahdi_chan *chan = ieu->dahdi.chans[i];
		out[1+i] = chan->writechunk[ieu->dahdi.writechunk_idx];
	}
	ieu->dahdi.writechunk_idx++;

	/* DAHDI_CHUNKSIZE is 8, meaning every channel (timeslot) wants data for 8
	 * bytes (PCM samples) every time _dahdi_receive() is called */
	if (ieu->dahdi.writechunk_idx == DAHDI_CHUNKSIZE) {
		_dahdi_transmit(dspan);
		ieu->dahdi.writechunk_idx = 0;
	}
}

/* OUT endpoint URB completes */
static void iso_out_complete(struct urb *urb)
{
	struct ice1usb *ieu = urb->context;
	unsigned int i, j;
	int rc;

	//ieu_dbg(ieu, "OUT urb %p completion (%d) %d", urb, urb->status, urb->number_of_packets);

	switch (urb->status) {
	case 0:
		for (i = 0; i < urb->number_of_packets; i++) {
			unsigned int offset = urb->iso_frame_desc[i].offset;
			uint8_t *tx = urb->transfer_buffer + offset;
			unsigned int fts; // frames to send
			unsigned long flags;

			if (urb->iso_frame_desc[i].status) {
				ieu_err(ieu, "OUT urb %p frame %u status %d",
					urb, i, urb->iso_frame_desc[i].status);
			}


			spin_lock_irqsave(&ieu->lock, flags);

			/* flow control */
			ieu->fc.r_acc += ieu->fc.r_sw;
			fts = ieu->fc.r_acc >> 10;
			if (fts < 4)
				fts = 4;
			else if (fts > ieu->max_fts)
				fts = ieu->max_fts;

			ieu->fc.r_acc -= fts << 10;
			if (ieu->fc.r_acc & 0x80000000)
				ieu->fc.r_acc = 0;

			for (j = 0; j < fts; j++)
				_span_mux_one_frame(ieu, tx + 4 + j*32);

			spin_unlock_irqrestore(&ieu->lock, flags);

			urb->iso_frame_desc[i].length = 4 + fts * 32;
		}
		break;
	case -ENOENT:
	case -ESHUTDOWN:
		/* Avoid suspend failed when usb_kill_urb */
		return;
	default:
		ieu_err(ieu, "OUT urb %p completion (%d)", urb, urb->status);
		break;
	}

	/* re-submit unless stopped */
	if (!test_bit(ICE1USB_ISOC_RUNNING, &ieu->flags))
		return;

	usb_anchor_urb(urb, &ieu->anchor.iso_out);
	usb_mark_last_busy(ieu->usb_dev);

	rc = usb_submit_urb(urb, GFP_ATOMIC);
	if (rc < 0) {
		/* -EPERM: urb is being killed; -ENODEV: device got disconnected */
		if (rc != -EPERM && rc != -ENODEV && rc != -ENOENT)
			ieu_err(ieu, "OUT urb %p submission failed (%d)", urb, -rc);
		usb_unanchor_urb(urb);
	}
}

/***********************************************************************
 * INTERRUPT transfers
 ***********************************************************************/

static uint32_t counter_delta(uint16_t old, uint16_t new)
{
	if (new == old)
		return 0;
	else if (new > old)
		return new - old;
	else
		return 0xffff - (old - new);
}

static void ice1usb_update_counters(struct ice1usb *ieu, const struct ice1usb_irq_err *err)
{
	ieu->dahdi.span.count.fe += counter_delta(ieu->last_err.align, err->align);
	ieu->dahdi.span.count.crc4 += counter_delta(ieu->last_err.crc, err->crc);
	/* no DAHDI general counters for overflows / underflows */
	ieu->count.ovfl += counter_delta(ieu->last_err.ovfl, err->ovfl);
	ieu->count.unfl += counter_delta(ieu->last_err.unfl, err->unfl);
	memcpy(&ieu->last_err, err, sizeof(ieu->last_err));
}

#define ALL_RY_ALARMS (DAHDI_ALARM_RED | DAHDI_ALARM_YELLOW | DAHDI_ALARM_BLUE | \
		       DAHDI_ALARM_LFA | DAHDI_ALARM_LMFA | DAHDI_ALARM_LOS)

/* interrupt EP completes: Process and resubmit */
static void ice1usb_irq_complete(struct urb *urb)
{
	const struct ice1usb_irq *irq;
	struct ice1usb *ieu = urb->context;
	int rc;

	//ieu_dbg(ieu, "IRQ urb %p completion (%d)", urb, urb->status);

	if (urb->status == 0 && urb->actual_length >= sizeof(*irq)) {
		const struct ice1usb_irq_err *err;
		unsigned int alarms = 0;
		irq = (struct ice1usb_irq *) urb->transfer_buffer;
		switch (irq->type) {
		case ICE1USB_IRQ_T_ERRCNT:
			err = &irq->u.errors;
			ieu_dbg(ieu, "IRQ: crc=%u, align=%u, ovfl=%u, unfl=%u, flags=%x",
				le16_to_cpu(err->crc), le16_to_cpu(err->align),
				le16_to_cpu(err->ovfl), le16_to_cpu(err->unfl), err->flags);
			/* update alarms.  Other drivers have some de-bouncing timers, I guess
			 * we can get away without doing this. */
			if (err->flags & ICE1USB_ERR_F_LOS)
				alarms |= DAHDI_ALARM_RED | DAHDI_ALARM_LOS;
			if (err->flags & ICE1USB_ERR_F_ALIGN_ERR)
				alarms |= DAHDI_ALARM_RED | DAHDI_ALARM_LFA | DAHDI_ALARM_LMFA;
			if (err->flags & ICE1USB_ERR_F_RAI)
				alarms |= DAHDI_ALARM_YELLOW;
			if (err->flags & ICE1USB_ERR_F_AIS)
				alarms |= DAHDI_ALARM_BLUE;
			ieu->dahdi.span.alarms &= ~ALL_RY_ALARMS;
			ieu->dahdi.span.alarms |= alarms;
			dahdi_alarm_notify(&ieu->dahdi.span);
			ice1usb_update_counters(ieu, err);
			break;
		}
	} else if (urb->status == -ENOENT) {
		/* Avoid suspend failed when usb_kill_urb */
		return;
	} else
		ieu_err(ieu, "IRQ urb %p completion (%d)", urb, urb->status);

	/* re-submit unless stopped */
	if (!test_bit(ICE1USB_IRQ_RUNNING, &ieu->flags))
		return;

	usb_anchor_urb(urb, &ieu->anchor.irq);
	usb_mark_last_busy(ieu->usb_dev);

	rc = usb_submit_urb(urb, GFP_ATOMIC);
	if (rc < 0) {
		/* -EPERM: urb is being killed; -ENODEV: device got disconnected */
		if (rc != -EPERM && rc != -ENODEV && rc != -ENOENT)
			ieu_err(ieu, "IRU urb %p submission failed (%d)", urb, -rc);
		usb_unanchor_urb(urb);
	}
}

/* allocate + submit + anchor an URB for the interrupt endpoint */
static int ice1usb_submit_irq_urb(struct ice1usb *ieu, gfp_t mem_flags)
{
	unsigned int pipe, size, rc;
	struct urb *urb;
	uint8_t *buf;

	if (!ieu->ep.irq)
		return -ENODEV;

	urb = usb_alloc_urb(0, mem_flags);
	if (!urb)
		return -ENOMEM;

	size = le16_to_cpu(ieu->ep.irq->wMaxPacketSize);
	buf = kmalloc(size, mem_flags);
	if (!buf) {
		usb_free_urb(urb);
		return -ENOMEM;
	}

	pipe = usb_rcvintpipe(ieu->usb_dev, ieu->ep.irq->bEndpointAddress);

	usb_fill_int_urb(urb, ieu->usb_dev, pipe, buf, size,
			 ice1usb_irq_complete, ieu, ieu->ep.irq->bInterval);

	urb->transfer_flags |= URB_FREE_BUFFER;

	usb_anchor_urb(urb, &ieu->anchor.irq);

	rc = usb_submit_urb(urb, mem_flags);
	if (rc < 0) {
		/* -EPERM: urb is being killed; -ENODEV: device got disconnected */
		if (rc != -EPERM && rc != -ENODEV && rc != -ENOENT)
			ieu_err(ieu, "IRQ urb %p submission failed (%d)", urb, -rc);
		usb_unanchor_urb(urb);
	}

	/* automatically free the URB (and its transfer buffer) once complete */
	usb_free_urb(urb);

	return rc;
}


/***********************************************************************
 * DAHDI integration
 ***********************************************************************/

static int e1u_d_startup(struct file *file, struct dahdi_span *span);
static int e1u_d_shutdown(struct dahdi_span *span);

static int e1u_d_spanconfig(struct file *file, struct dahdi_span *span,
			    struct dahdi_lineconfig *lc)
{
	struct ice1usb *ieu = container_of(span, struct ice1usb, dahdi.span);
	unsigned int i;
	int rc;

	ieu_dbg(ieu, "entering %s", __FUNCTION__);

	if (lc->sync < 0)
		lc->sync = 0;
	if (lc->sync > 1) {
		ieu_warn(ieu, "Cannot set clock priority on span %d to %d\n",
			 span->spanno, lc->sync);
		lc->sync = 0;
	}

	if (span->lineconfig & DAHDI_CONFIG_CRC4) {
		ieu->cfg.tx.mode = ICE1USB_TX_MODE_TS0_CRC4_E;
		ieu->cfg.rx.mode = ICE1USB_RX_MODE_MULTIFRAME;
	} else {
		ieu->cfg.tx.mode = ICE1USB_TX_MODE_TS0;
		ieu->cfg.rx.mode = ICE1USB_RX_MODE_FRAME;
	}

	if (lc->sync > 0)
		ieu->cfg.tx.timing = ICE1USB_TX_TIME_SRC_REMOTE;
	else
		ieu->cfg.tx.timing = ICE1USB_TX_TIME_SRC_LOCAL;

	/* (re-)set to sane defaults */
	ieu->cfg.tx.ext_loopback = ICE1USB_TX_EXT_LOOPBACK_OFF;
	ieu->cfg.tx.alarm = 0;

	for (i = 0; i < span->channels; i++) {
		struct dahdi_chan *const chan = ieu->dahdi.chans[i];
		chan->sigcap = DAHDI_SIG_CLEAR | DAHDI_SIG_MTP2 | DAHDI_SIG_SF;
	}

	/* If we're already running, then go ahead and apply the changes */
	if (span->flags & DAHDI_FLAG_RUNNING) {
		/* there should probably be a more elegant way to do this, but
		 * we don't expect people to keep re-configuring their span all
		 * day long, so a brief interruption is deemed acceptable */
		e1u_d_shutdown(span);
		rc = ice1usb_tx_config(ieu);
		if (rc < 0)
			return rc;
		rc = ice1usb_rx_config(ieu);
		if (rc < 0)
			return rc;
		rc = e1u_d_startup(file, span);
	} else {
		rc = ice1usb_tx_config(ieu);
		if (rc < 0)
			return rc;
		rc = ice1usb_rx_config(ieu);
	}

	return rc;
}

static int e1u_d_chanconfig(struct file *file, struct dahdi_chan *chan,
			    int sigtype)
{
	struct ice1usb *ieu = chan->pvt;
	bool already_running;

	already_running = ieu->dahdi.span.flags & DAHDI_FLAG_RUNNING;
	ieu_info(ieu, "%sconfigured channel %d (%s) sigtype %d\n",
		 already_running ? "Re":"", chan->channo, chan->name, sigtype);

	/* FIXME: we can probably remove this completely? */
	return 0;
}

static int ice1usb_set_altif(struct ice1usb *ieu, bool on);

static int e1u_d_startup(struct file *file, struct dahdi_span *span)
{
	struct ice1usb *ieu = container_of(span, struct ice1usb, dahdi.span);
	unsigned long flags;
	unsigned int i;
	int rc;

	ieu_dbg(ieu, "entering %s", __FUNCTION__);

	/* Ensure we are in the right altsetting */
	rc = ice1usb_set_altif(ieu, true);
	if (rc < 0)
		goto err;

	if (!test_and_set_bit(ICE1USB_ISOC_RUNNING, &ieu->flags)) {
		for (i = 0; i < ICE1USB_NUM_URBS; i++) {
			rc = ice1usb_submit_isoc_urb(ieu, ieu->ep.iso_in, &ieu->anchor.iso_in,
						     iso_in_complete, GFP_KERNEL);
			if (rc) {
				ieu_err(ieu, "error submitting IN ep URB %u (%d)", i, rc);
				goto err_isoc_in;
			}
			rc = ice1usb_submit_isoc_urb(ieu, ieu->ep.iso_fb, &ieu->anchor.iso_fb,
						     iso_fb_complete, GFP_KERNEL);
			if (rc) {
				ieu_err(ieu, "error submitting FB ep URB %u (%d)", i, rc);
				goto err_isoc_fb;
			}
			rc = ice1usb_submit_isoc_urb(ieu, ieu->ep.iso_out, &ieu->anchor.iso_out,
						     iso_out_complete, GFP_KERNEL);
			if (rc) {
				ieu_err(ieu, "error submitting OUT ep URB %u (%d)", i, rc);
				goto err_isoc_out;
			}
		}
	}

	if (!test_and_set_bit(ICE1USB_IRQ_RUNNING, &ieu->flags)) {
		for (i = 0; i < ICE1USB_NUM_URBS; i++) {
			rc = ice1usb_submit_irq_urb(ieu, GFP_KERNEL);
			if (rc) {
				ieu_err(ieu, "error submitting IRQ ep %u (%d)", i, rc);
				goto err_irq;
			}
		}
	}

	spin_lock_irqsave(&span->lock, flags);
	span->alarms &= ~DAHDI_ALARM_NOTOPEN;
	dahdi_alarm_notify(span);
	spin_unlock_irqrestore(&span->lock, flags);
	return 0;

err_irq:
	usb_kill_anchored_urbs(&ieu->anchor.irq);
	clear_bit(ICE1USB_IRQ_RUNNING, &ieu->flags);
	usb_kill_anchored_urbs(&ieu->anchor.iso_out);
err_isoc_out:
	usb_kill_anchored_urbs(&ieu->anchor.iso_fb);
err_isoc_fb:
	usb_kill_anchored_urbs(&ieu->anchor.iso_in);
err_isoc_in:
	clear_bit(ICE1USB_ISOC_RUNNING, &ieu->flags);
	ice1usb_set_altif(ieu, false);
err:
	spin_lock_irqsave(&span->lock, flags);
	span->alarms |= DAHDI_ALARM_NOTOPEN;
	dahdi_alarm_notify(span);
	spin_unlock_irqrestore(&span->lock, flags);
	return rc;
}

static int e1u_d_shutdown(struct dahdi_span *span)
{
	struct ice1usb *ieu = container_of(span, struct ice1usb, dahdi.span);
	int rc;

	ieu_dbg(ieu, "entering %s", __FUNCTION__);

	if (test_and_clear_bit(ICE1USB_ISOC_RUNNING, &ieu->flags)) {
		usb_kill_anchored_urbs(&ieu->anchor.iso_in);
		usb_kill_anchored_urbs(&ieu->anchor.iso_out);
		usb_kill_anchored_urbs(&ieu->anchor.iso_fb);
	}

	if (test_and_clear_bit(ICE1USB_IRQ_RUNNING, &ieu->flags))
		usb_kill_anchored_urbs(&ieu->anchor.irq);

	/* guard against devices unplugged (see ice1usb_disconnect) */
	if (ieu->present) {
		/* switch to 'off' altsetting */
		rc = ice1usb_set_altif(ieu, false);
		if (rc < 0)
			return rc;
	}

	return 0;
}

/* set some maintenance mode according to 'cmd' */
static int e1u_d_maint(struct dahdi_span *span, int cmd)
{
	struct ice1usb *ieu = container_of(span, struct ice1usb, dahdi.span);
	int rc = 0;

	ieu_dbg(ieu, "entering %s(%d)", __FUNCTION__, cmd);

	switch (cmd) {
	case DAHDI_MAINT_NONE:
		ieu_info(ieu, "Clearing all maint modes\n");
		ieu->cfg.tx.ext_loopback = ICE1USB_TX_EXT_LOOPBACK_OFF;
		rc = ice1usb_tx_config(ieu);
		break;
	case DAHDI_MAINT_NETWORKLINELOOP:
		ieu_info(ieu, "Turning on network line loopback\n");
		ieu->cfg.tx.ext_loopback = ICE1USB_TX_EXT_LOOPBACK_SAME;
		rc = ice1usb_tx_config(ieu);
		break;
	/* TODO: DAHDI_MAINT_*_DEFECT */
	/* TODO: DAHDI_MAINT_ALARM_SIM */
	case DAHDI_RESET_COUNTERS:
		memset(&ieu->dahdi.span.count, 0, sizeof(ieu->dahdi.span.count));
		/* don't reset last_err, as we need it to count the _new_ errors */
		break;
	default:
		ieu_info(ieu, "Unknown E1 maint command: %d\n", cmd);
		return -ENOSYS;
	}

	return rc;
}

static const struct dahdi_span_ops ice1usb_span_ops = {
	.owner = THIS_MODULE,
	.spanconfig = e1u_d_spanconfig,
	.chanconfig = e1u_d_chanconfig,
	.startup = e1u_d_startup,
	.shutdown = e1u_d_shutdown,
	.maint = e1u_d_maint,
};

/***********************************************************************
* USB GPSDO driver (sysfs files) - NOT _interface_ driver
***********************************************************************/

static void ice1usb_gpsdo_free(struct ice1usb_gpsdo *e1d)
{
	usb_put_dev(e1d->usb_dev);
	kfree(e1d);
}

/* The GPS-DO exists only once for the entire USB device, while all the E1 related
 * bits exist once per USB interface! */

/* update our internal, cached GPS-DO state */
static int e1u_update_gpsdo(struct ice1usb_gpsdo *e1d)
{
	uint8_t if_num = e1d->usb_intf->cur_altsetting->desc.bInterfaceNumber;
	int rc;

	rc = usb_control_msg(e1d->usb_dev, usb_rcvctrlpipe(e1d->usb_dev, 0),
				ICE1USB_INTF_GET_GPSDO_STATUS, USB_DIR_IN | USB_RT_VEND_IF,
				0, if_num, &e1d->gpsdo_status, sizeof(e1d->gpsdo_status),
				USB_CTRL_SET_TIMEOUT);
	if (rc < 0) {
		dev_err(&e1d->usb_intf->dev, "Error during GPSDO CTRL GET transfer: %d\n", rc);
		return rc;
	}
	if (rc != sizeof(e1d->gpsdo_status)) {
		dev_err(&e1d->usb_dev->dev, "Short read during GPSDO CTRL GET transfer: %d\n", rc);
		return -EIO;
	}

	return 0;
}

static const char *gpsdo_mode_str(enum ice1usb_gpsdo_mode mode)
{
	switch (mode) {
	case ICE1USB_GPSDO_MODE_DISABLED:
		return "disabled";
	case ICE1USB_GPSDO_MODE_AUTO:
		return "auto";
	default:
		return "invalid";
	}
}

static const char *gpsdo_antenna_state_str(enum ice1usb_gpsdo_antenna_state st)
{
	switch (st) {
	case ICE1USB_GPSDO_ANT_UNKNOWN:
		return "unknown";
	case ICE1USB_GPSDO_ANT_OK:
		return "ok";
	case ICE1USB_GPSDO_ANT_OPEN:
		return "open";
	case ICE1USB_GPSDO_ANT_SHORT:
		return "short";
	default:
		return "invalid";
	}
}

static const char *gpsdo_state_str(enum ice1usb_gpsdo_state st)
{
	switch (st) {
	case ICE1USB_GPSDO_STATE_DISABLED:
		return "disabled";
	case ICE1USB_GPSDO_STATE_CALIBRATE:
		return "calibrate";
	case ICE1USB_GPSDO_STATE_HOLD_OVER:
		return "hold_over";
	case ICE1USB_GPSDO_STATE_TUNE_COARSE:
		return "tune_coarse";
	case ICE1USB_GPSDO_STATE_TUNE_FINE:
		return "tune_fine";
	default:
		return "invalid";
	}
}


static ssize_t e1u_gpsdo_mode_show(struct device *dev, struct device_attribute *attr,
				   char *buf)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);
	int rc = e1u_update_gpsdo(e1d);
	if (rc < 0)
		return rc;
	return sprintf(buf, "%s\n", gpsdo_mode_str(e1d->gpsdo_status.mode));
}

static ssize_t e1u_gpsdo_mode_store(struct device *dev, struct device_attribute *attr,
				    const char *buf, size_t count)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);
	uint8_t if_num = e1d->usb_intf->cur_altsetting->desc.bInterfaceNumber;
	uint16_t imode;
	int rc;

	if (!strcmp(buf, "disabled"))
		imode = ICE1USB_GPSDO_MODE_DISABLED;
	else if (!strcmp(buf, "auto"))
		imode = ICE1USB_GPSDO_MODE_AUTO;
	else
		return -EINVAL;

	rc = usb_control_msg(e1d->usb_dev, usb_sndctrlpipe(e1d->usb_dev, 0),
				ICE1USB_INTF_SET_GPSDO_MODE, USB_RT_VEND_IF,
				imode, if_num, NULL, 0, USB_CTRL_SET_TIMEOUT);
	if (rc < 0) {
		dev_err(&e1d->usb_intf->dev, "Error during GPSDO CTRL SET transfer: %d\n", rc);
		return -EIO;
	}
	e1d->gpsdo_status.mode = imode;

	return count;
}

static DEVICE_ATTR(gpsdo_mode, 0644, e1u_gpsdo_mode_show, e1u_gpsdo_mode_store);

static ssize_t e1u_gpsdo_ant_state_show(struct device *dev,
					 struct device_attribute *attr, char *buf)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);
	int rc = e1u_update_gpsdo(e1d);
	if (rc < 0)
		return rc;
	return sprintf(buf, "%s\n", gpsdo_antenna_state_str(e1d->gpsdo_status.antenna_state));
}

static DEVICE_ATTR(gpsdo_antenna_state, 0444, e1u_gpsdo_ant_state_show, NULL);

static ssize_t e1u_gpsdo_state_show(struct device *dev,
				    struct device_attribute *attr, char *buf)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);
	int rc = e1u_update_gpsdo(e1d);
	if (rc < 0)
		return rc;
	return sprintf(buf, "%s\n", gpsdo_state_str(e1d->gpsdo_status.state));
}

static DEVICE_ATTR(gpsdo_state, 0444, e1u_gpsdo_state_show, NULL);

static ssize_t e1u_gpsdo_valid_fix_show(struct device *dev,
					struct device_attribute *attr, char *buf)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);
	int rc = e1u_update_gpsdo(e1d);
	if (rc < 0)
		return rc;
	return sprintf(buf, "%u\n", e1d->gpsdo_status.valid_fix);
}

static DEVICE_ATTR(gpsdo_valid_fix, 0444, e1u_gpsdo_valid_fix_show, NULL);


static ssize_t e1u_gpsdo_tune_coarse_show(struct device *dev,
					  struct device_attribute *attr, char *buf)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);
	int rc = e1u_update_gpsdo(e1d);
	if (rc < 0)
		return rc;
	return sprintf(buf, "%u\n", e1d->gpsdo_status.tune.coarse);
}

/* TODO: set tuning */
static DEVICE_ATTR(gpsdo_tune_coarse, 0444, e1u_gpsdo_tune_coarse_show, NULL);


static ssize_t e1u_gpsdo_tune_fine_show(struct device *dev,
					struct device_attribute *attr, char *buf)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);
	int rc = e1u_update_gpsdo(e1d);
	if (rc < 0)
		return rc;
	return sprintf(buf, "%u\n", e1d->gpsdo_status.tune.fine);
}

/* TODO: set tuning */
static DEVICE_ATTR(gpsdo_tune_fine, 0444, e1u_gpsdo_tune_fine_show, NULL);

static ssize_t e1u_gpsdo_freq_est_show(struct device *dev,
					struct device_attribute *attr, char *buf)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);
	int rc = e1u_update_gpsdo(e1d);
	if (rc < 0)
		return rc;
	return sprintf(buf, "%u\n", e1d->gpsdo_status.freq_est);
}

static DEVICE_ATTR(gpsdo_freq_est, 0444, e1u_gpsdo_freq_est_show, NULL);

static ssize_t e1u_gpsdo_err_acc_show(struct device *dev,
					struct device_attribute *attr, char *buf)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);
	int rc = e1u_update_gpsdo(e1d);
	if (rc < 0)
		return rc;
	return sprintf(buf, "%d\n", (int)e1d->gpsdo_status.err_acc);
}

static DEVICE_ATTR(gpsdo_err_acc, 0444, e1u_gpsdo_err_acc_show, NULL);

static ssize_t e1u_fw_build_show(struct device *dev,
				 struct device_attribute *attr, char *buf)
{
	struct ice1usb_gpsdo *e1d = dev_get_drvdata(dev);

	return sprintf(buf, "%s\n", e1d->fw_build);
}

static DEVICE_ATTR(fw_build, 0444, e1u_fw_build_show, NULL);

static void create_sysfs_files(struct ice1usb_gpsdo *e1d)
{
	device_create_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_mode);
	device_create_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_antenna_state);
	device_create_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_state);
	device_create_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_valid_fix);
	device_create_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_tune_coarse);
	device_create_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_tune_fine);
	device_create_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_freq_est);
	device_create_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_err_acc);
	device_create_file(&e1d->usb_intf->dev, &dev_attr_fw_build);
}

static void remove_sysfs_files(struct ice1usb_gpsdo *e1d)
{
	device_remove_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_mode);
	device_remove_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_antenna_state);
	device_remove_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_state);
	device_remove_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_valid_fix);
	device_remove_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_tune_coarse);
	device_remove_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_tune_fine);
	device_remove_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_freq_est);
	device_remove_file(&e1d->usb_intf->dev, &dev_attr_gpsdo_err_acc);
	device_remove_file(&e1d->usb_intf->dev, &dev_attr_fw_build);
}

static int ice1usb_gpsdo_probe(struct usb_interface *intf, const struct usb_device_id *prod)
{
	struct usb_device *usb_dev = usb_get_dev(interface_to_usbdev(intf));
	const struct usb_interface_descriptor *ifdesc = &intf->altsetting->desc;
	struct ice1usb_gpsdo *e1d;
	int ret = -ENODEV;
	int rc;

	dev_dbg(&intf->dev, "entering %s", __FUNCTION__);

	if (ifdesc->bInterfaceClass != 0xff || ifdesc->bInterfaceSubClass != 0xE1) {
		dev_dbg(&intf->dev, "Unsupported Interface Class/SubClass %02x/%02x",
			ifdesc->bInterfaceClass, ifdesc->bInterfaceSubClass);
		ret = -ENODEV;
		goto error;
	}

	/* we only support protocol 0 so far */
	if (ifdesc->bInterfaceProtocol != 0xd0) {
		ret = -ENODEV;
		goto error;
	}

	e1d = kzalloc(sizeof(*e1d), GFP_KERNEL);
	if (!e1d) {
		dev_err(&intf->dev, "Out of memory\n");
		ret = -ENOMEM;
		goto error;
	}

	e1d->usb_dev = usb_dev;
	e1d->usb_intf = intf;
	e1d->present = true;
	spin_lock_init(&e1d->lock);

	usb_set_intfdata(intf, e1d);

	/* obtain firmware build information */
	e1d->fw_build[0] = '\0';
	rc = usb_control_msg(e1d->usb_dev, usb_rcvctrlpipe(e1d->usb_dev, 0),
				ICE1USB_DEV_GET_FW_BUILD, USB_DIR_IN | USB_RT_VEND_DEV,
				0, 0, e1d->fw_build, sizeof(e1d->fw_build), USB_CTRL_SET_TIMEOUT);
	if (rc < 0) {
		dev_err(&e1d->usb_intf->dev, "Error during FW BUILD CTRL GET transfer: %d\n", rc);
		ret = rc;
		goto error_free;
	}
	e1d->fw_build[sizeof(e1d->fw_build)-1] = '\0';
	dev_info(&e1d->usb_intf->dev, "icE1usb firmware build: '%s'\n", e1d->fw_build);

	create_sysfs_files(e1d);

	return 0;

error_free:
	usb_set_intfdata(intf, NULL);
	kfree(e1d);
error:
	usb_put_dev(usb_dev);
	return ret;
}

static void ice1usb_gpsdo_disconnect(struct usb_interface *intf)
{
	struct ice1usb_gpsdo *e1d = usb_get_intfdata(intf);

	dev_dbg(&intf->dev, "entering %s", __FUNCTION__);

	if (!e1d)
		return;

	/* avoid any shutdown code from submitting further I/O */
	e1d->present = false;

	remove_sysfs_files(e1d);

	usb_set_intfdata(intf, NULL);

	ice1usb_gpsdo_free(e1d);
}


static struct usb_driver ice1usb_gpsdo_driver = {
	.name = "icE1usb-GPSDO",
	.id_table = ice1usb_products,
	.probe = ice1usb_gpsdo_probe,
	.disconnect = ice1usb_gpsdo_disconnect,
};

/***********************************************************************
 * kernel USB integration / probing
 ***********************************************************************/

/* does the given altsetting contain an EP with wMaxPacketSize == 0 ? */
static bool has_ep_packetsize_zero(const struct usb_host_interface *alt)
{
	unsigned int j;

	/* compute the sum of all wMaxPacketSize */
	for (j = 0; j < alt->desc.bNumEndpoints; j++) {
		const struct usb_endpoint_descriptor *epd = &alt->endpoint[j].desc;
		if (usb_endpoint_xfer_isoc(epd)) {
			if (epd->wMaxPacketSize == 0)
				return true;
		}
	}
	return false;
}

/* find the 'on' altsetting (all ISOC EP wMaxPacketSize != 0) */
static const struct usb_host_interface *find_altsetting_on(struct usb_interface *intf)
{
	unsigned int i;

	for (i = 0; i < intf->num_altsetting; i++) {
		const struct usb_host_interface *alt = &intf->altsetting[i];
		if (!has_ep_packetsize_zero(alt) && alt->desc.bNumEndpoints >= 3)
			return alt;
	}
	return NULL;
}

/* find the 'off' altsetting (all ISOC EP wMaxPacketSize == 0) */
static const struct usb_host_interface *find_altsetting_off(struct usb_interface *intf)
{
	unsigned int i;

	for (i = 0; i < intf->num_altsetting; i++) {
		const struct usb_host_interface *alt = &intf->altsetting[i];
		unsigned int iso_ep_size = 0;
		unsigned int j;

		/* compute the sum of all wMaxPacketSize */
		for (j = 0; j < alt->desc.bNumEndpoints; j++) {
			const struct usb_endpoint_descriptor *epd = &alt->endpoint[j].desc;
			if (usb_endpoint_xfer_isoc(epd))
				iso_ep_size += le16_to_cpu(epd->wMaxPacketSize);
		}

		if (iso_ep_size == 0)
			return alt;
	}
	return NULL;
}

/* find the endpoint numbers within the interface */
static int find_endpoints(struct ice1usb *ieu, const struct usb_host_interface *alt)
{
	unsigned int i;
	int found = 0;

	for (i = 0; i < alt->desc.bNumEndpoints; i++) {
		const struct usb_endpoint_descriptor *epd = &alt->endpoint[i].desc;
		switch (usb_endpoint_type(epd)) {
		case USB_ENDPOINT_XFER_INT:
			if (!ieu->ep.irq && usb_endpoint_dir_in(epd)) {
				ieu->ep.irq = epd;
				found++;
			}
			break;
		case USB_ENDPOINT_XFER_ISOC:
			if (usb_endpoint_dir_in(epd)) {
				if ((epd->bmAttributes & USB_ENDPOINT_USAGE_MASK) ==
				    USB_ENDPOINT_USAGE_FEEDBACK) {
					if (!ieu->ep.iso_fb) {
						ieu->ep.iso_fb = epd;
						found++;
					}
				} else {
					if (!ieu->ep.iso_in) {
						ieu->ep.iso_in = epd;
						found++;
					}
				}
			} else {
				if (!ieu->ep.iso_out) {
					ieu->ep.iso_out = epd;
					found++;
				}
			}
			break;
		default:
			break;
		}
	}
	return found;
}


/* select interface altsetting on / off */
static int ice1usb_set_altif(struct ice1usb *ieu, bool on)
{
	const struct usb_interface_descriptor *desc;
	int rc;

	ieu_dbg(ieu, "entering %s(%d)", __FUNCTION__, on);

	if (on)
		desc = &ieu->alt_on->desc;
	else
		desc = &ieu->alt_off->desc;

	rc = usb_set_interface(ieu->usb_dev, desc->bInterfaceNumber, desc->bAlternateSetting);
	if (rc) {
		ieu_err(ieu, "error setting %s-altif %u (%d)",
			on ? "ON" : "OFF", desc->bInterfaceNumber, rc);
	}

	return rc;
}

static int ice1usb_probe(struct usb_interface *intf, const struct usb_device_id *prod)
{
	struct usb_device *usb_dev = usb_get_dev(interface_to_usbdev(intf));
	const struct usb_interface_descriptor *ifdesc = &intf->altsetting->desc;
	unsigned int max_pack_size;
	struct dahdi_device *ddev;
	struct dahdi_span *dspan;
	struct ice1usb *ieu;
	int ret = -ENODEV;
	int rc;

	dev_dbg(&intf->dev, "entering %s", __FUNCTION__);

	if (ifdesc->bInterfaceClass != 0xff || ifdesc->bInterfaceSubClass != 0xE1) {
		dev_dbg(&intf->dev, "Unsupported Interface Class/SubClass %02x/%02x",
			ifdesc->bInterfaceClass, ifdesc->bInterfaceSubClass);
		return -ENODEV;
	}

	/* we only support protocol 0 so far */
	if (ifdesc->bInterfaceProtocol != 0x00)
		return -ENODEV;

	ieu = kzalloc(sizeof(*ieu), GFP_KERNEL);
	if (!ieu) {
		dev_err(&intf->dev, "Out of memory\n");
		return -ENOMEM;
	}

	ieu->usb_dev = usb_dev;
	ieu->usb_intf = intf;
	ieu->fc.r_acc = 0;
	ieu->fc.r_sw = 8192;
	ieu->present = true;
	spin_lock_init(&ieu->lock);

	/* locate ON / OFF altsettings */
	ieu->alt_off = find_altsetting_off(ieu->usb_intf);
	if (!ieu->alt_off) {
		dev_err(&intf->dev, "Cannot find OFF altsetting");
		goto error;
	}
	ieu->alt_on = find_altsetting_on(ieu->usb_intf);
	if (!ieu->alt_on) {
		dev_err(&intf->dev, "Cannot find ON altsetting");
		goto error;
	}
	dev_info(&intf->dev, "altsetting off=%u, on=%u\n",
		 ieu->alt_off->desc.bAlternateSetting, ieu->alt_on->desc.bAlternateSetting);

	/* locate ON / OFF altsettings */
	if (find_endpoints(ieu, ieu->alt_on) < 4) {
		dev_err(&intf->dev, "Cannot find all endpoints");
		goto error;
	}

	/* compute the maximum number of E1 frames to send in one ISO OUT packet */
	max_pack_size = le16_to_cpu(ieu->ep.iso_out->wMaxPacketSize);
	ieu->max_fts = (max_pack_size - 4) / 32;
	dev_dbg(&intf->dev, "Maximum FTS: %u", ieu->max_fts);

	/* TODO: only one dahdi_device even for multiple USB interfaces? */
	ddev = ieu->dahdi.dev = dahdi_create_device();
	if (!ddev)
		goto error;
	ddev->manufacturer = usb_dev->manufacturer;
	ddev->devicetype = usb_dev->product;
	ddev->hardware_id = usb_dev->serial;
	//ddev->location = USB_BUS_PATH;

	dspan = &ieu->dahdi.span;
	/* TODO: include USB path info? */
	snprintf(dspan->name, sizeof(dspan->name), "icE1usb/1/%d",
		 ieu->alt_on->desc.bInterfaceNumber); //TDOO: device != 1
	snprintf(dspan->desc, sizeof(dspan->desc), "Osmocom icE1USB Card 1 Span %d",
		 ieu->alt_on->desc.bInterfaceNumber); //TODO: device != 1
	dspan->channels = 31;
	dspan->spantype = SPANTYPE_DIGITAL_E1;
	dspan->deflaw = DAHDI_LAW_ALAW;
	dspan->linecompat = DAHDI_CONFIG_HDB3 | DAHDI_CONFIG_CCS | DAHDI_CONFIG_CRC4;
	dspan->chans = ieu->dahdi.chans;
	dspan->flags = 0;
	dspan->offset = 0;
	dspan->ops = &ice1usb_span_ops;

	rc = e1u_alloc_channels(ieu);
	if (rc) {
		ret = -ENOMEM;
		goto err_free_ddev;
	}

	list_add_tail(&dspan->device_node, &ddev->spans);

	rc = dahdi_register_device(ieu->dahdi.dev, &intf->dev);
	if (rc) {
		dev_err(&intf->dev, "Failed to reigster with DAHDI\n");
		goto err_free_chans;
	}

	init_usb_anchor(&ieu->anchor.iso_in);
	init_usb_anchor(&ieu->anchor.iso_out);
	init_usb_anchor(&ieu->anchor.iso_fb);
	init_usb_anchor(&ieu->anchor.irq);

	usb_set_intfdata(intf, ieu);

	return 0;

err_free_chans:
	e1u_free_channels(ieu);
err_free_ddev:
	dahdi_free_device(ieu->dahdi.dev);
error:
	kfree(ieu);
	return ret;
}

static struct usb_driver ice1usb_driver;

static void ice1usb_disconnect(struct usb_interface *intf)
{
	struct ice1usb *ieu = usb_get_intfdata(intf);

	dev_dbg(&intf->dev, "entering %s", __FUNCTION__);

	if (!ieu)
		return;

	/* avoid any shutdown code from submitting further I/O */
	ieu->present = false;

	usb_set_intfdata(intf, NULL);

	/* will in turn call e1u_d_shutdown() which stops all transfers */
	dahdi_unregister_device(ieu->dahdi.dev);

	ice1usb_free(ieu);
}

#if 0
static int ice1usb_suspend(struct usb_interface *intf, pm_message_t message)
{
	struct ice1usb *ieu = usb_get_intfdata(intf);
	/* FIXME */
}

static int ice1usb_resume(struct usb_interface *intf)
{
	struct ice1usb *ieu = usb_get_intfdata(intf);
	/* FIXME */
}
#endif

static struct usb_driver ice1usb_driver = {
	.name = "icE1usb",
	.id_table = ice1usb_products,
	.probe = ice1usb_probe,
	.disconnect = ice1usb_disconnect,
	//.suspend = ice1usb_suspend,
	//.resume = ice1usb_resume,
};


/* cannot use module_usb_driver() as we have both E1 and GPS-DO drivers */
static int __init __ice1usb_init(void)
{
	int rc;

	rc = usb_register_driver(&ice1usb_gpsdo_driver, THIS_MODULE, KBUILD_MODNAME);
	if (rc < 0)
		return rc;

	rc = usb_register_driver(&ice1usb_driver, THIS_MODULE, KBUILD_MODNAME);
	if (rc < 0) {
		usb_deregister(&ice1usb_gpsdo_driver);
		return rc;
	}

	return rc;
}

static void __exit __ice1usb_exit(void)
{
	usb_deregister(&ice1usb_gpsdo_driver);
	usb_deregister(&ice1usb_driver);
}

module_init(__ice1usb_init);
module_exit(__ice1usb_exit);

MODULE_AUTHOR("Harald Welte <laforge@osmocom.org>");
MODULE_DESCRIPTION("Osmocom icE1usb USB E1 interface");
MODULE_LICENSE("GPL");
MODULE_VERSION(VERSION);
