/*-
 * Copyright (c) 2004 Dag-Erling Coïdan Smørgrav
 * All rights reserved.
 *
 * 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
 *    in this position and unchanged.
 * 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 the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
 *
 * $FreeBSD: src/sys/dev/usb/ucycom.c,v 1.4 2005/10/16 20:22:56 phk Exp $
 */

/*
 * Device driver for Cypress CY7C637xx and CY7C640/1xx series USB to
 * RS232 bridges.
 *
 * Normally, a driver for a USB-to-serial chip would hang off the ucom(4)
 * driver, but ucom(4) was written under the assumption that all USB-to-
 * serial chips use bulk pipes for I/O, while the Cypress parts use HID
 * reports.
 */

#include <sys/cdefs.h>
__FBSDID("$FreeBSD: src/sys/dev/usb/ucycom.c,v 1.4 2005/10/16 20:22:56 phk Exp $");

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/bus.h>
#include <sys/tty.h>

#include "usbdevs.h"
#include <dev/usb/usb.h>
#include <dev/usb/usb_port.h>
#include <dev/usb/usbdi.h>
#include <dev/usb/usbdi_util.h>
#include <dev/usb/usbhid.h>
#include <dev/usb/hid.h>

#define UCYCOM_EP_INPUT		 0
#define UCYCOM_EP_OUTPUT	 1

#define UCYCOM_MAX_IOLEN	 32U

struct ucycom_softc {
	device_t		 sc_dev;
	struct tty		*sc_tty;
	int			 sc_error;
	unsigned long		 sc_cintr;
	unsigned long		 sc_cin;
	unsigned long		 sc_clost;
	unsigned long		 sc_cout;

	/* usb parameters */
	usbd_device_handle	 sc_usbdev;
	usbd_interface_handle	 sc_iface;
	usbd_pipe_handle	 sc_pipe;
	uint8_t			 sc_iep; /* input endpoint */
	uint8_t			 sc_fid; /* feature report id*/
	uint8_t			 sc_iid; /* input report id */
	uint8_t			 sc_oid; /* output report id */
	size_t			 sc_flen; /* feature report length */
	size_t			 sc_ilen; /* input report length */
	size_t			 sc_olen; /* output report length */
	uint8_t			 sc_ibuf[UCYCOM_MAX_IOLEN];

	/* model and settings */
	uint32_t		 sc_model;
#define	MODEL_CY7C63743		 0x63743
#define	MODEL_CY7C64013		 0x64013
	uint32_t		 sc_baud;
	uint8_t			 sc_cfg;
#define UCYCOM_CFG_RESET	 0x80
#define UCYCOM_CFG_PARODD	 0x20
#define UCYCOM_CFG_PAREN	 0x10
#define UCYCOM_CFG_STOPB	 0x08
#define UCYCOM_CFG_DATAB	 0x03
	uint8_t			 sc_ist; /* status flags from last input */
	uint8_t			 sc_ost; /* status flags for next output */

	/* flags */
	char			 sc_dying;
};

static int ucycom_probe(device_t);
static int ucycom_attach(device_t);
static int ucycom_detach(device_t);
static t_open_t ucycom_open;
static t_close_t ucycom_close;
static void ucycom_start(struct tty *);
static void ucycom_stop(struct tty *, int);
static int ucycom_param(struct tty *, struct termios *);
static int ucycom_configure(struct ucycom_softc *, uint32_t, uint8_t);
static void ucycom_intr(usbd_xfer_handle, usbd_private_handle, usbd_status);

static device_method_t ucycom_methods[] = {
	DEVMETHOD(device_probe, ucycom_probe),
	DEVMETHOD(device_attach, ucycom_attach),
	DEVMETHOD(device_detach, ucycom_detach),
	{ 0, 0 }
};

static driver_t ucycom_driver = {
	"ucycom",
	ucycom_methods,
	sizeof(struct ucycom_softc),
};

static devclass_t ucycom_devclass;

DRIVER_MODULE(ucycom, uhub, ucycom_driver, ucycom_devclass, usbd_driver_load, 0);
MODULE_VERSION(ucycom, 1);
MODULE_DEPEND(ucycom, usb, 1, 1, 1);

/*
 * Supported devices
 */

static struct ucycom_device {
	uint16_t		 vendor;
	uint16_t		 product;
	uint32_t		 model;
} ucycom_devices[] = {
	{ USB_VENDOR_DELORME, USB_PRODUCT_DELORME_EARTHMATE, MODEL_CY7C64013 },
	{ 0, 0, 0 },
};

#define UCYCOM_DEFAULT_RATE	 4800
#define UCYCOM_DEFAULT_CFG	 0x03 /* N-8-1 */

/*****************************************************************************
 *
 * Driver interface
 *
 */

static int
ucycom_probe(device_t dev)
{
	struct usb_attach_arg *uaa;
	struct ucycom_device *ud;

	uaa = device_get_ivars(dev);
	if (uaa->iface != NULL)
		return (UMATCH_NONE);
	for (ud = ucycom_devices; ud->model != 0; ++ud)
		if (ud->vendor == uaa->vendor && ud->product == uaa->product)
			return (UMATCH_VENDOR_PRODUCT);
	return (UMATCH_NONE);
}

static int
ucycom_attach(device_t dev)
{
	struct usb_attach_arg *uaa;
	struct ucycom_softc *sc;
	struct ucycom_device *ud;
	usb_endpoint_descriptor_t *ued;
	char *devinfo;
	void *urd;
	int error, urdlen;

	/* get arguments and softc */
	uaa = device_get_ivars(dev);
	sc = device_get_softc(dev);
	bzero(sc, sizeof *sc);
	sc->sc_dev = dev;
	sc->sc_usbdev = uaa->device;

	/* get device description */
	/* XXX usb_devinfo() has little or no overflow protection */
	devinfo = malloc(1024, M_USBDEV, M_WAITOK);
	usbd_devinfo(sc->sc_usbdev, 0, devinfo);
	device_set_desc_copy(dev, devinfo);
	device_printf(dev, "%s\n", devinfo);
	free(devinfo, M_USBDEV);

	/* get chip model */
	for (ud = ucycom_devices; ud->model != 0; ++ud)
		if (ud->vendor == uaa->vendor && ud->product == uaa->product)
			sc->sc_model = ud->model;
	if (sc->sc_model == 0) {
		device_printf(dev, "unsupported device\n");
		return (ENXIO);
	}
	device_printf(dev, "Cypress CY7C%X USB to RS232 bridge\n", sc->sc_model);

	/* select configuration */
	error = usbd_set_config_index(sc->sc_usbdev, 0, 1 /* verbose */);
	if (error != 0) {
		device_printf(dev, "failed to select configuration: %s\n",
		    usbd_errstr(error));
		return (ENXIO);
	}

	/* get first interface handle */
	error = usbd_device2interface_handle(sc->sc_usbdev, 0, &sc->sc_iface);
	if (error != 0) {
		device_printf(dev, "failed to get interface handle: %s\n",
		    usbd_errstr(error));
		return (ENXIO);
	}

	/* get report descriptor */
	error = usbd_read_report_desc(sc->sc_iface, &urd, &urdlen, M_USBDEV);
	if (error != 0) {
		device_printf(dev, "failed to get report descriptor: %s\n",
		    usbd_errstr(error));
		return (ENXIO);
	}

	/* get report sizes */
	sc->sc_flen = hid_report_size(urd, urdlen, hid_feature, &sc->sc_fid);
	sc->sc_ilen = hid_report_size(urd, urdlen, hid_input, &sc->sc_iid);
	sc->sc_olen = hid_report_size(urd, urdlen, hid_output, &sc->sc_oid);

	if (sc->sc_ilen > UCYCOM_MAX_IOLEN || sc->sc_olen > UCYCOM_MAX_IOLEN) {
		device_printf(dev, "I/O report size too big (%zu, %zu, %u)\n",
		    sc->sc_ilen, sc->sc_olen, UCYCOM_MAX_IOLEN);
		return (ENXIO);
	}

	/* get and verify input endpoint descriptor */
	ued = usbd_interface2endpoint_descriptor(sc->sc_iface, UCYCOM_EP_INPUT);
	if (ued == NULL) {
		device_printf(dev, "failed to get input endpoint descriptor\n");
		return (ENXIO);
	}
	if (UE_GET_DIR(ued->bEndpointAddress) != UE_DIR_IN) {
		device_printf(dev, "not an input endpoint\n");
		return (ENXIO);
	}
	if ((ued->bmAttributes & UE_XFERTYPE) != UE_INTERRUPT) {
		device_printf(dev, "not an interrupt endpoint\n");
		return (ENXIO);
	}
	sc->sc_iep = ued->bEndpointAddress;

	/* set up tty */
	sc->sc_tty = ttyalloc();
	sc->sc_tty->t_sc = sc;
	sc->sc_tty->t_oproc = ucycom_start;
	sc->sc_tty->t_stop = ucycom_stop;
	sc->sc_tty->t_param = ucycom_param;
	sc->sc_tty->t_open = ucycom_open;
	sc->sc_tty->t_close = ucycom_close;

	SYSCTL_ADD_INT(device_get_sysctl_ctx(dev),
	    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)),
	    OID_AUTO, "intr", CTLFLAG_RD, &sc->sc_cintr, 0,
	    "interrupt count");
	SYSCTL_ADD_INT(device_get_sysctl_ctx(dev),
	    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)),
	    OID_AUTO, "in", CTLFLAG_RD, &sc->sc_cin, 0,
	    "input bytes read");
	SYSCTL_ADD_INT(device_get_sysctl_ctx(dev),
	    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)),
	    OID_AUTO, "lost", CTLFLAG_RD, &sc->sc_clost, 0,
	    "input bytes lost");
	SYSCTL_ADD_INT(device_get_sysctl_ctx(dev),
	    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)),
	    OID_AUTO, "out", CTLFLAG_RD, &sc->sc_cout, 0,
	    "output bytes");

	/* create character device node */
	ttycreate(sc->sc_tty, 0, "y%r", device_get_unit(sc->sc_dev));

	return (0);
}

static int
ucycom_detach(device_t dev)
{
	struct ucycom_softc *sc;

	sc = device_get_softc(dev);

	ttyfree(sc->sc_tty);

	return (0);
}

/*****************************************************************************
 *
 * Device interface
 *
 */

static int
ucycom_open(struct tty *tp, struct cdev *cdev)
{
	struct ucycom_softc *sc = tp->t_sc;
	int error;

	/* set default configuration */
	ucycom_configure(sc, UCYCOM_DEFAULT_RATE, UCYCOM_DEFAULT_CFG);

	/* open interrupt pipe */
	error = usbd_open_pipe_intr(sc->sc_iface, sc->sc_iep, 0,
	    &sc->sc_pipe, sc, sc->sc_ibuf, sc->sc_ilen,
	    ucycom_intr, USBD_DEFAULT_INTERVAL);
	if (error != 0) {
		device_printf(sc->sc_dev, "failed to open interrupt pipe: %s\n",
		    usbd_errstr(error));
		return (ENXIO);
	}

	if (bootverbose)
		device_printf(sc->sc_dev, "%s bypass l_rint()\n",
		    (sc->sc_tty->t_state & TS_CAN_BYPASS_L_RINT) ?
		    "can" : "can't");

	/* done! */
	return (0);
}

static void
ucycom_close(struct tty *tp)
{
	struct ucycom_softc *sc = tp->t_sc;

	/* stop interrupts and close the interrupt pipe */
	usbd_abort_pipe(sc->sc_pipe);
	usbd_close_pipe(sc->sc_pipe);
	sc->sc_pipe = 0;

	return;
}

/*****************************************************************************
 *
 * TTY interface
 *
 */

static void
ucycom_start(struct tty *tty)
{
	struct ucycom_softc *sc = tty->t_sc;
	uint8_t report[sc->sc_olen];
	int error, len;

	while (sc->sc_error == 0 && sc->sc_tty->t_outq.c_cc > 0) {
		switch (sc->sc_model) {
		case MODEL_CY7C63743:
			len = q_to_b(&sc->sc_tty->t_outq,
			    report + 1, sc->sc_olen - 1);
			sc->sc_cout += len;
			report[0] = len;
			len += 1;
			break;
		case MODEL_CY7C64013:
			len = q_to_b(&sc->sc_tty->t_outq,
			    report + 2, sc->sc_olen - 2);
			sc->sc_cout += len;
			report[0] = 0;
			report[1] = len;
			len += 2;
			break;
		default:
			panic("unsupported model (driver error)");
		}

		while (len < sc->sc_olen)
			report[len++] = 0;
		error = usbd_set_report(sc->sc_iface, UHID_OUTPUT_REPORT,
		    sc->sc_oid, report, sc->sc_olen);
#if 0
		if (error != 0) {
			device_printf(sc->sc_dev,
			    "failed to set output report: %s\n",
			    usbd_errstr(error));
			sc->sc_error = error;
		}
#endif
	}
}

static void
ucycom_stop(struct tty *tty, int flags)
{
	struct ucycom_softc *sc;

	sc = tty->t_sc;
	if (bootverbose)
		device_printf(sc->sc_dev, "%s()\n", __func__);
}

static int
ucycom_param(struct tty *tty, struct termios *t)
{
	struct ucycom_softc *sc;
	uint32_t baud;
	uint8_t cfg;
	int error;

	sc = tty->t_sc;

	if (t->c_ispeed != t->c_ospeed)
		return (EINVAL);
	baud = t->c_ispeed;

	if (t->c_cflag & CIGNORE) {
		cfg = sc->sc_cfg;
	} else {
		cfg = 0;
		switch (t->c_cflag & CSIZE) {
		case CS8:
			++cfg;
		case CS7:
			++cfg;
		case CS6:
			++cfg;
		case CS5:
			break;
		default:
			return (EINVAL);
		}
		if (t->c_cflag & CSTOPB)
			cfg |= UCYCOM_CFG_STOPB;
		if (t->c_cflag & PARENB)
			cfg |= UCYCOM_CFG_PAREN;
		if (t->c_cflag & PARODD)
			cfg |= UCYCOM_CFG_PARODD;
	}

	error = ucycom_configure(sc, baud, cfg);
	return (error);
}

/*****************************************************************************
 *
 * Hardware interface
 *
 */

static int
ucycom_configure(struct ucycom_softc *sc, uint32_t baud, uint8_t cfg)
{
	uint8_t report[sc->sc_flen];
	int error;

	switch (baud) {
	case 600:
	case 1200:
	case 2400:
	case 4800:
	case 9600:
	case 19200:
	case 38400:
	case 57600:
#if 0
	/*
	 * Stock chips only support standard baud rates in the 600 - 57600
	 * range, but higher rates can be achieved using custom firmware.
	 */
	case 115200:
	case 153600:
	case 192000:
#endif
		break;
	default:
		return (EINVAL);
	}

	if (bootverbose)
		device_printf(sc->sc_dev, "%d baud, %c-%d-%d\n", baud,
		    (cfg & UCYCOM_CFG_PAREN) ?
		    ((cfg & UCYCOM_CFG_PARODD) ? 'O' : 'E') : 'N',
		    5 + (cfg & UCYCOM_CFG_DATAB),
		    (cfg & UCYCOM_CFG_STOPB) ? 2 : 1);
	report[0] = baud & 0xff;
	report[1] = (baud >> 8) & 0xff;
	report[2] = (baud >> 16) & 0xff;
	report[3] = (baud >> 24) & 0xff;
	report[4] = cfg;
	error = usbd_set_report(sc->sc_iface, UHID_FEATURE_REPORT,
	    sc->sc_fid, report, sc->sc_flen);
	if (error != 0) {
		device_printf(sc->sc_dev, "%s\n", usbd_errstr(error));
		return (EIO);
	}
	sc->sc_baud = baud;
	sc->sc_cfg = cfg;
	return (0);
}

static void
ucycom_intr(usbd_xfer_handle xfer, usbd_private_handle scp, usbd_status status)
{
	struct ucycom_softc *sc = scp;
	uint8_t *data;
	int i, len, lost;

	sc->sc_cintr++;

	switch (sc->sc_model) {
	case MODEL_CY7C63743:
		sc->sc_ist = sc->sc_ibuf[0] & ~0x07;
		len = sc->sc_ibuf[0] & 0x07;
		data = sc->sc_ibuf + 1;
		break;
	case MODEL_CY7C64013:
		sc->sc_ist = sc->sc_ibuf[0] & ~0x07;
		len = sc->sc_ibuf[1];
		data = sc->sc_ibuf + 2;
		break;
	default:
		panic("unsupported model (driver error)");
	}

	switch (status) {
	case USBD_NORMAL_COMPLETION:
		break;
	default:
		/* XXX */
		return;
	}

	if (sc->sc_tty->t_state & TS_CAN_BYPASS_L_RINT) {
		/* XXX flow control! */
		lost = b_to_q(data, len, &sc->sc_tty->t_rawq);
		sc->sc_tty->t_rawcc += len - lost;
		ttwakeup(sc->sc_tty);
	} else {
		for (i = 0, lost = len; i < len; ++i, --lost)
			if (ttyld_rint(sc->sc_tty, data[i]) != 0)
				break;
	}
	sc->sc_cin += len - lost;
	sc->sc_clost += lost;
}
