diff options
Diffstat (limited to 'drivers/tty/ipwireless/tty.c')
| -rw-r--r-- | drivers/tty/ipwireless/tty.c | 648 | 
1 files changed, 648 insertions, 0 deletions
diff --git a/drivers/tty/ipwireless/tty.c b/drivers/tty/ipwireless/tty.c new file mode 100644 index 00000000000..17ee3bf0926 --- /dev/null +++ b/drivers/tty/ipwireless/tty.c @@ -0,0 +1,648 @@ +/* + * IPWireless 3G PCMCIA Network Driver + * + * Original code + *   by Stephen Blackheath <stephen@blacksapphire.com>, + *      Ben Martel <benm@symmetric.co.nz> + * + * Copyrighted as follows: + *   Copyright (C) 2004 by Symmetric Systems Ltd (NZ) + * + * Various driver changes and rewrites, port to new kernels + *   Copyright (C) 2006-2007 Jiri Kosina + * + * Misc code cleanups and updates + *   Copyright (C) 2007 David Sterba + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/ppp_defs.h> +#include <linux/if.h> +#include <linux/ppp-ioctl.h> +#include <linux/sched.h> +#include <linux/serial.h> +#include <linux/slab.h> +#include <linux/tty.h> +#include <linux/tty_driver.h> +#include <linux/tty_flip.h> +#include <linux/uaccess.h> + +#include "tty.h" +#include "network.h" +#include "hardware.h" +#include "main.h" + +#define IPWIRELESS_PCMCIA_START 	(0) +#define IPWIRELESS_PCMCIA_MINORS	(24) +#define IPWIRELESS_PCMCIA_MINOR_RANGE	(8) + +#define TTYTYPE_MODEM    (0) +#define TTYTYPE_MONITOR  (1) +#define TTYTYPE_RAS_RAW  (2) + +struct ipw_tty { +	struct tty_port port; +	int index; +	struct ipw_hardware *hardware; +	unsigned int channel_idx; +	unsigned int secondary_channel_idx; +	int tty_type; +	struct ipw_network *network; +	unsigned int control_lines; +	struct mutex ipw_tty_mutex; +	int tx_bytes_queued; +	int closing; +}; + +static struct ipw_tty *ttys[IPWIRELESS_PCMCIA_MINORS]; + +static struct tty_driver *ipw_tty_driver; + +static char *tty_type_name(int tty_type) +{ +	static char *channel_names[] = { +		"modem", +		"monitor", +		"RAS-raw" +	}; + +	return channel_names[tty_type]; +} + +static struct ipw_tty *get_tty(int index) +{ +	/* +	 * The 'ras_raw' channel is only available when 'loopback' mode +	 * is enabled. +	 * Number of minor starts with 16 (_RANGE * _RAS_RAW). +	 */ +	if (!ipwireless_loopback && index >= +			 IPWIRELESS_PCMCIA_MINOR_RANGE * TTYTYPE_RAS_RAW) +		return NULL; + +	return ttys[index]; +} + +static int ipw_open(struct tty_struct *linux_tty, struct file *filp) +{ +	struct ipw_tty *tty = get_tty(linux_tty->index); + +	if (!tty) +		return -ENODEV; + +	mutex_lock(&tty->ipw_tty_mutex); + +	if (tty->closing) { +		mutex_unlock(&tty->ipw_tty_mutex); +		return -ENODEV; +	} +	if (tty->port.count == 0) +		tty->tx_bytes_queued = 0; + +	tty->port.count++; + +	tty->port.tty = linux_tty; +	linux_tty->driver_data = tty; +	tty->port.low_latency = 1; + +	if (tty->tty_type == TTYTYPE_MODEM) +		ipwireless_ppp_open(tty->network); + +	mutex_unlock(&tty->ipw_tty_mutex); + +	return 0; +} + +static void do_ipw_close(struct ipw_tty *tty) +{ +	tty->port.count--; + +	if (tty->port.count == 0) { +		struct tty_struct *linux_tty = tty->port.tty; + +		if (linux_tty != NULL) { +			tty->port.tty = NULL; +			linux_tty->driver_data = NULL; + +			if (tty->tty_type == TTYTYPE_MODEM) +				ipwireless_ppp_close(tty->network); +		} +	} +} + +static void ipw_hangup(struct tty_struct *linux_tty) +{ +	struct ipw_tty *tty = linux_tty->driver_data; + +	if (!tty) +		return; + +	mutex_lock(&tty->ipw_tty_mutex); +	if (tty->port.count == 0) { +		mutex_unlock(&tty->ipw_tty_mutex); +		return; +	} + +	do_ipw_close(tty); + +	mutex_unlock(&tty->ipw_tty_mutex); +} + +static void ipw_close(struct tty_struct *linux_tty, struct file *filp) +{ +	ipw_hangup(linux_tty); +} + +/* Take data received from hardware, and send it out the tty */ +void ipwireless_tty_received(struct ipw_tty *tty, unsigned char *data, +			unsigned int length) +{ +	int work = 0; + +	mutex_lock(&tty->ipw_tty_mutex); + +	if (!tty->port.count) { +		mutex_unlock(&tty->ipw_tty_mutex); +		return; +	} +	mutex_unlock(&tty->ipw_tty_mutex); + +	work = tty_insert_flip_string(&tty->port, data, length); + +	if (work != length) +		printk(KERN_DEBUG IPWIRELESS_PCCARD_NAME +				": %d chars not inserted to flip buffer!\n", +				length - work); + +	if (work) +		tty_flip_buffer_push(&tty->port); +} + +static void ipw_write_packet_sent_callback(void *callback_data, +					   unsigned int packet_length) +{ +	struct ipw_tty *tty = callback_data; + +	/* +	 * Packet has been sent, so we subtract the number of bytes from our +	 * tally of outstanding TX bytes. +	 */ +	tty->tx_bytes_queued -= packet_length; +} + +static int ipw_write(struct tty_struct *linux_tty, +		     const unsigned char *buf, int count) +{ +	struct ipw_tty *tty = linux_tty->driver_data; +	int room, ret; + +	if (!tty) +		return -ENODEV; + +	mutex_lock(&tty->ipw_tty_mutex); +	if (!tty->port.count) { +		mutex_unlock(&tty->ipw_tty_mutex); +		return -EINVAL; +	} + +	room = IPWIRELESS_TX_QUEUE_SIZE - tty->tx_bytes_queued; +	if (room < 0) +		room = 0; +	/* Don't allow caller to write any more than we have room for */ +	if (count > room) +		count = room; + +	if (count == 0) { +		mutex_unlock(&tty->ipw_tty_mutex); +		return 0; +	} + +	ret = ipwireless_send_packet(tty->hardware, IPW_CHANNEL_RAS, +			       buf, count, +			       ipw_write_packet_sent_callback, tty); +	if (ret == -1) { +		mutex_unlock(&tty->ipw_tty_mutex); +		return 0; +	} + +	tty->tx_bytes_queued += count; +	mutex_unlock(&tty->ipw_tty_mutex); + +	return count; +} + +static int ipw_write_room(struct tty_struct *linux_tty) +{ +	struct ipw_tty *tty = linux_tty->driver_data; +	int room; + +	/* FIXME: Exactly how is the tty object locked here .. */ +	if (!tty) +		return -ENODEV; + +	if (!tty->port.count) +		return -EINVAL; + +	room = IPWIRELESS_TX_QUEUE_SIZE - tty->tx_bytes_queued; +	if (room < 0) +		room = 0; + +	return room; +} + +static int ipwireless_get_serial_info(struct ipw_tty *tty, +				      struct serial_struct __user *retinfo) +{ +	struct serial_struct tmp; + +	if (!retinfo) +		return (-EFAULT); + +	memset(&tmp, 0, sizeof(tmp)); +	tmp.type = PORT_UNKNOWN; +	tmp.line = tty->index; +	tmp.port = 0; +	tmp.irq = 0; +	tmp.flags = 0; +	tmp.baud_base = 115200; +	tmp.close_delay = 0; +	tmp.closing_wait = 0; +	tmp.custom_divisor = 0; +	tmp.hub6 = 0; +	if (copy_to_user(retinfo, &tmp, sizeof(*retinfo))) +		return -EFAULT; + +	return 0; +} + +static int ipw_chars_in_buffer(struct tty_struct *linux_tty) +{ +	struct ipw_tty *tty = linux_tty->driver_data; + +	if (!tty) +		return 0; + +	if (!tty->port.count) +		return 0; + +	return tty->tx_bytes_queued; +} + +static int get_control_lines(struct ipw_tty *tty) +{ +	unsigned int my = tty->control_lines; +	unsigned int out = 0; + +	if (my & IPW_CONTROL_LINE_RTS) +		out |= TIOCM_RTS; +	if (my & IPW_CONTROL_LINE_DTR) +		out |= TIOCM_DTR; +	if (my & IPW_CONTROL_LINE_CTS) +		out |= TIOCM_CTS; +	if (my & IPW_CONTROL_LINE_DSR) +		out |= TIOCM_DSR; +	if (my & IPW_CONTROL_LINE_DCD) +		out |= TIOCM_CD; + +	return out; +} + +static int set_control_lines(struct ipw_tty *tty, unsigned int set, +			     unsigned int clear) +{ +	int ret; + +	if (set & TIOCM_RTS) { +		ret = ipwireless_set_RTS(tty->hardware, tty->channel_idx, 1); +		if (ret) +			return ret; +		if (tty->secondary_channel_idx != -1) { +			ret = ipwireless_set_RTS(tty->hardware, +					  tty->secondary_channel_idx, 1); +			if (ret) +				return ret; +		} +	} +	if (set & TIOCM_DTR) { +		ret = ipwireless_set_DTR(tty->hardware, tty->channel_idx, 1); +		if (ret) +			return ret; +		if (tty->secondary_channel_idx != -1) { +			ret = ipwireless_set_DTR(tty->hardware, +					  tty->secondary_channel_idx, 1); +			if (ret) +				return ret; +		} +	} +	if (clear & TIOCM_RTS) { +		ret = ipwireless_set_RTS(tty->hardware, tty->channel_idx, 0); +		if (tty->secondary_channel_idx != -1) { +			ret = ipwireless_set_RTS(tty->hardware, +					  tty->secondary_channel_idx, 0); +			if (ret) +				return ret; +		} +	} +	if (clear & TIOCM_DTR) { +		ret = ipwireless_set_DTR(tty->hardware, tty->channel_idx, 0); +		if (tty->secondary_channel_idx != -1) { +			ret = ipwireless_set_DTR(tty->hardware, +					  tty->secondary_channel_idx, 0); +			if (ret) +				return ret; +		} +	} +	return 0; +} + +static int ipw_tiocmget(struct tty_struct *linux_tty) +{ +	struct ipw_tty *tty = linux_tty->driver_data; +	/* FIXME: Exactly how is the tty object locked here .. */ + +	if (!tty) +		return -ENODEV; + +	if (!tty->port.count) +		return -EINVAL; + +	return get_control_lines(tty); +} + +static int +ipw_tiocmset(struct tty_struct *linux_tty, +	     unsigned int set, unsigned int clear) +{ +	struct ipw_tty *tty = linux_tty->driver_data; +	/* FIXME: Exactly how is the tty object locked here .. */ + +	if (!tty) +		return -ENODEV; + +	if (!tty->port.count) +		return -EINVAL; + +	return set_control_lines(tty, set, clear); +} + +static int ipw_ioctl(struct tty_struct *linux_tty, +		     unsigned int cmd, unsigned long arg) +{ +	struct ipw_tty *tty = linux_tty->driver_data; + +	if (!tty) +		return -ENODEV; + +	if (!tty->port.count) +		return -EINVAL; + +	/* FIXME: Exactly how is the tty object locked here .. */ + +	switch (cmd) { +	case TIOCGSERIAL: +		return ipwireless_get_serial_info(tty, (void __user *) arg); + +	case TIOCSSERIAL: +		return 0;	/* Keeps the PCMCIA scripts happy. */ +	} + +	if (tty->tty_type == TTYTYPE_MODEM) { +		switch (cmd) { +		case PPPIOCGCHAN: +			{ +				int chan = ipwireless_ppp_channel_index( +							tty->network); + +				if (chan < 0) +					return -ENODEV; +				if (put_user(chan, (int __user *) arg)) +					return -EFAULT; +			} +			return 0; + +		case PPPIOCGUNIT: +			{ +				int unit = ipwireless_ppp_unit_number( +						tty->network); + +				if (unit < 0) +					return -ENODEV; +				if (put_user(unit, (int __user *) arg)) +					return -EFAULT; +			} +			return 0; + +		case FIONREAD: +			{ +				int val = 0; + +				if (put_user(val, (int __user *) arg)) +					return -EFAULT; +			} +			return 0; +		case TCFLSH: +			return tty_perform_flush(linux_tty, arg); +		} +	} +	return -ENOIOCTLCMD; +} + +static int add_tty(int j, +		    struct ipw_hardware *hardware, +		    struct ipw_network *network, int channel_idx, +		    int secondary_channel_idx, int tty_type) +{ +	ttys[j] = kzalloc(sizeof(struct ipw_tty), GFP_KERNEL); +	if (!ttys[j]) +		return -ENOMEM; +	ttys[j]->index = j; +	ttys[j]->hardware = hardware; +	ttys[j]->channel_idx = channel_idx; +	ttys[j]->secondary_channel_idx = secondary_channel_idx; +	ttys[j]->network = network; +	ttys[j]->tty_type = tty_type; +	mutex_init(&ttys[j]->ipw_tty_mutex); +	tty_port_init(&ttys[j]->port); + +	tty_port_register_device(&ttys[j]->port, ipw_tty_driver, j, NULL); +	ipwireless_associate_network_tty(network, channel_idx, ttys[j]); + +	if (secondary_channel_idx != -1) +		ipwireless_associate_network_tty(network, +						 secondary_channel_idx, +						 ttys[j]); +	/* check if we provide raw device (if loopback is enabled) */ +	if (get_tty(j)) +		printk(KERN_INFO IPWIRELESS_PCCARD_NAME +		       ": registering %s device ttyIPWp%d\n", +		       tty_type_name(tty_type), j); + +	return 0; +} + +struct ipw_tty *ipwireless_tty_create(struct ipw_hardware *hardware, +				      struct ipw_network *network) +{ +	int i, j; + +	for (i = 0; i < IPWIRELESS_PCMCIA_MINOR_RANGE; i++) { +		int allfree = 1; + +		for (j = i; j < IPWIRELESS_PCMCIA_MINORS; +				j += IPWIRELESS_PCMCIA_MINOR_RANGE) +			if (ttys[j] != NULL) { +				allfree = 0; +				break; +			} + +		if (allfree) { +			j = i; + +			if (add_tty(j, hardware, network, +					IPW_CHANNEL_DIALLER, IPW_CHANNEL_RAS, +					TTYTYPE_MODEM)) +				return NULL; + +			j += IPWIRELESS_PCMCIA_MINOR_RANGE; +			if (add_tty(j, hardware, network, +					IPW_CHANNEL_DIALLER, -1, +					TTYTYPE_MONITOR)) +				return NULL; + +			j += IPWIRELESS_PCMCIA_MINOR_RANGE; +			if (add_tty(j, hardware, network, +					IPW_CHANNEL_RAS, -1, +					TTYTYPE_RAS_RAW)) +				return NULL; + +			return ttys[i]; +		} +	} +	return NULL; +} + +/* + * Must be called before ipwireless_network_free(). + */ +void ipwireless_tty_free(struct ipw_tty *tty) +{ +	int j; +	struct ipw_network *network = ttys[tty->index]->network; + +	for (j = tty->index; j < IPWIRELESS_PCMCIA_MINORS; +			j += IPWIRELESS_PCMCIA_MINOR_RANGE) { +		struct ipw_tty *ttyj = ttys[j]; + +		if (ttyj) { +			mutex_lock(&ttyj->ipw_tty_mutex); +			if (get_tty(j)) +				printk(KERN_INFO IPWIRELESS_PCCARD_NAME +				       ": deregistering %s device ttyIPWp%d\n", +				       tty_type_name(ttyj->tty_type), j); +			ttyj->closing = 1; +			if (ttyj->port.tty != NULL) { +				mutex_unlock(&ttyj->ipw_tty_mutex); +				tty_vhangup(ttyj->port.tty); +				/* FIXME: Exactly how is the tty object locked here +				   against a parallel ioctl etc */ +				/* FIXME2: hangup does not mean all processes +				 * are gone */ +				mutex_lock(&ttyj->ipw_tty_mutex); +			} +			while (ttyj->port.count) +				do_ipw_close(ttyj); +			ipwireless_disassociate_network_ttys(network, +							     ttyj->channel_idx); +			tty_unregister_device(ipw_tty_driver, j); +			tty_port_destroy(&ttyj->port); +			ttys[j] = NULL; +			mutex_unlock(&ttyj->ipw_tty_mutex); +			kfree(ttyj); +		} +	} +} + +static const struct tty_operations tty_ops = { +	.open = ipw_open, +	.close = ipw_close, +	.hangup = ipw_hangup, +	.write = ipw_write, +	.write_room = ipw_write_room, +	.ioctl = ipw_ioctl, +	.chars_in_buffer = ipw_chars_in_buffer, +	.tiocmget = ipw_tiocmget, +	.tiocmset = ipw_tiocmset, +}; + +int ipwireless_tty_init(void) +{ +	int result; + +	ipw_tty_driver = alloc_tty_driver(IPWIRELESS_PCMCIA_MINORS); +	if (!ipw_tty_driver) +		return -ENOMEM; + +	ipw_tty_driver->driver_name = IPWIRELESS_PCCARD_NAME; +	ipw_tty_driver->name = "ttyIPWp"; +	ipw_tty_driver->major = 0; +	ipw_tty_driver->minor_start = IPWIRELESS_PCMCIA_START; +	ipw_tty_driver->type = TTY_DRIVER_TYPE_SERIAL; +	ipw_tty_driver->subtype = SERIAL_TYPE_NORMAL; +	ipw_tty_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV; +	ipw_tty_driver->init_termios = tty_std_termios; +	ipw_tty_driver->init_termios.c_cflag = +	    B9600 | CS8 | CREAD | HUPCL | CLOCAL; +	ipw_tty_driver->init_termios.c_ispeed = 9600; +	ipw_tty_driver->init_termios.c_ospeed = 9600; +	tty_set_operations(ipw_tty_driver, &tty_ops); +	result = tty_register_driver(ipw_tty_driver); +	if (result) { +		printk(KERN_ERR IPWIRELESS_PCCARD_NAME +		       ": failed to register tty driver\n"); +		put_tty_driver(ipw_tty_driver); +		return result; +	} + +	return 0; +} + +void ipwireless_tty_release(void) +{ +	int ret; + +	ret = tty_unregister_driver(ipw_tty_driver); +	put_tty_driver(ipw_tty_driver); +	if (ret != 0) +		printk(KERN_ERR IPWIRELESS_PCCARD_NAME +			": tty_unregister_driver failed with code %d\n", ret); +} + +int ipwireless_tty_is_modem(struct ipw_tty *tty) +{ +	return tty->tty_type == TTYTYPE_MODEM; +} + +void +ipwireless_tty_notify_control_line_change(struct ipw_tty *tty, +					  unsigned int channel_idx, +					  unsigned int control_lines, +					  unsigned int changed_mask) +{ +	unsigned int old_control_lines = tty->control_lines; + +	tty->control_lines = (tty->control_lines & ~changed_mask) +		| (control_lines & changed_mask); + +	/* +	 * If DCD is de-asserted, we close the tty so pppd can tell that we +	 * have gone offline. +	 */ +	if ((old_control_lines & IPW_CONTROL_LINE_DCD) +			&& !(tty->control_lines & IPW_CONTROL_LINE_DCD) +			&& tty->port.tty) { +		tty_hangup(tty->port.tty); +	} +} +  | 
