/* $Id: tpam_commands.c,v 1.1.2.4 2001/11/06 20:58:30 kai Exp $
 *
 * Turbo PAM ISDN driver for Linux. (Kernel Driver - ISDN commands)
 *
 * Copyright 2001 Stelian Pop <stelian.pop@fr.alcove.com>, Alcôve
 *
 * This software may be used and distributed according to the terms
 * of the GNU General Public License, incorporated herein by reference.
 *
 * For all support questions please contact: <support@auvertech.fr>
 *
 */

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <asm/io.h>

#include <linux/isdn/tpam.h>
#include "tpam.h"

/* Local functions prototypes */
static int tpam_command_ioctl_dspload(tpam_card *, unsigned long);
static int tpam_command_ioctl_dspsave(tpam_card *, unsigned long);
static int tpam_command_ioctl_dsprun(tpam_card *);
static int tpam_command_ioctl_loopmode(tpam_card *, u8);
static int tpam_command_dial(tpam_card *, u32, u8 *);
static int tpam_command_setl2(tpam_card *, u32, u8);
static int tpam_command_acceptd(tpam_card *, u32);
static int tpam_command_acceptb(tpam_card *, u32);
static int tpam_command_hangup(tpam_card *, u32);
static int tpam_command_proceed(tpam_card *, u32);
static void tpam_statcallb_run(unsigned long);
static void tpam_statcallb(tpam_card *, isdn_ctrl);

/*
 * Function called when the ISDN link level send a command to the driver.
 *
 * 	c: ISDN command.
 *
 * Return: 0 if OK, <0 on errors.
 */
int tpam_command(isdn_ctrl *c) {
	tpam_card *card;
	unsigned long argp;

	pr_debug("TurboPAM(tpam_command) card=%d, command=%d\n",
		c->driver, c->command);	

	/* search for the board */
	if (!(card = tpam_findcard(c->driver))) {
		printk(KERN_ERR "TurboPAM(tpam_command): invalid driverId %d\n",
		       c->driver);	
		return -ENODEV;
	}

	/* dispatch the command */
	switch (c->command) {
		case ISDN_CMD_IOCTL:
			argp = c->parm.userdata;
			switch (c->arg) {
				case TPAM_CMD_DSPLOAD:
					return tpam_command_ioctl_dspload(card,
									  argp);
				case TPAM_CMD_DSPSAVE:
					return tpam_command_ioctl_dspsave(card,
									  argp);
				case TPAM_CMD_DSPRUN:
					return tpam_command_ioctl_dsprun(card);
				case TPAM_CMD_LOOPMODEON:
					return tpam_command_ioctl_loopmode(card,
									   1);
				case TPAM_CMD_LOOPMODEOFF:
					return tpam_command_ioctl_loopmode(card,
									   0);
				default:
					pr_debug("TurboPAM(tpam_command): "
						"invalid tpam ioctl %ld\n", 
						c->arg);	
					return -EINVAL;
			}
		case ISDN_CMD_DIAL:
			return tpam_command_dial(card, c->arg, 
						 c->parm.setup.phone);
		case ISDN_CMD_ACCEPTD:
			return tpam_command_acceptd(card, c->arg);
		case ISDN_CMD_ACCEPTB:
			return tpam_command_acceptb(card, c->arg);
		case ISDN_CMD_HANGUP:
			return tpam_command_hangup(card, c->arg);
		case ISDN_CMD_SETL2:
			return tpam_command_setl2(card, c->arg & 0xff, 
						  c->arg >> 8);
		case ISDN_CMD_PROCEED:
			return tpam_command_proceed(card, c->arg);
		default:
			pr_debug("TurboPAM(tpam_command): "
				"unknown or unused isdn ioctl %d\n", 
				c->command);	
			return -EINVAL;
	}

	/* not reached */
	return -EINVAL;
}

/*
 * Load some data into the board's memory.
 *
 * 	card: the board
 * 	arg: IOCTL argument containing the user space address of 
 * 		the tpam_dsp_ioctl structure describing the IOCTL.
 *
 * Return: 0 if OK, <0 on errors.
 */
static int tpam_command_ioctl_dspload(tpam_card *card, unsigned long arg) {
	tpam_dsp_ioctl tdl;

	pr_debug("TurboPAM(tpam_command_ioctl_dspload): card=%d\n", card->id);

	/* get the IOCTL parameter from userspace */
	if (copy_from_user(&tdl, (void __user *)arg, sizeof(tpam_dsp_ioctl)))
		return -EFAULT;

	/* if the board's firmware was started, protect against writes
	 * to unallowed memory areas. If the board's firmware wasn't started,
	 * all is allowed. */
	if (card->running && tpam_verify_area(tdl.address, tdl.data_len)) 
		return -EPERM;

	/* write the data in the board's memory */
	return copy_from_user_to_pam(card, tdl.address, 
				     (void __user *)arg + sizeof(tpam_dsp_ioctl), 
				     tdl.data_len);
}

/*
 * Extract some data from the board's memory.
 *
 * 	card: the board
 * 	arg: IOCTL argument containing the user space address of 
 * 		the tpam_dsp_ioctl structure describing the IOCTL.
 *
 * Return: 0 if OK, <0 on errors.
 */
static int tpam_command_ioctl_dspsave(tpam_card *card, unsigned long arg) {
	tpam_dsp_ioctl tdl;

	pr_debug("TurboPAM(tpam_command_ioctl_dspsave): card=%d\n", card->id);

	/* get the IOCTL parameter from userspace */
	if (copy_from_user(&tdl, (void __user *)arg, sizeof(tpam_dsp_ioctl)))
		return -EFAULT;

	/* protect against read from unallowed memory areas */
	if (tpam_verify_area(tdl.address, tdl.data_len)) 
		return -EPERM;

	/* read the data from the board's memory */
	return copy_from_pam_to_user(card, (void __user *)arg + sizeof(tpam_dsp_ioctl),
				     tdl.address, tdl.data_len);
}

/*
 * Launch the board's firmware. This function must be called after the 
 * firmware was loaded into the board's memory using TPAM_CMD_DSPLOAD 
 * IOCTL commands. After launching the firmware, this function creates
 * the NCOs and waits for their creation.
 *
 * 	card: the board
 *
 * Return: 0 if OK, <0 on errors.
 */
static int tpam_command_ioctl_dsprun(tpam_card *card) {
	u32 signature = 0, i;
	unsigned long timeout;
	isdn_ctrl ctrl;
	struct sk_buff *skb;

	pr_debug("TurboPAM(tpam_command_ioctl_dsprun): card=%d\n", card->id);

	/* board must _not_ be running */
	if (card->running)
		return -EBUSY;

	/* reset the board */
	spin_lock_irq(&card->lock);
	copy_to_pam_dword(card, TPAM_MAGICNUMBER_REGISTER, 0xdeadface);
	readl(card->bar0 + TPAM_DSPINT_REGISTER);
	readl(card->bar0 + TPAM_HINTACK_REGISTER);
	spin_unlock_irq(&card->lock);
	
	/* wait for the board signature */
	timeout = jiffies + SIGNATURE_TIMEOUT;
	while (time_before(jiffies, timeout)) {
		spin_lock_irq(&card->lock);
		signature = copy_from_pam_dword(card, 
						TPAM_MAGICNUMBER_REGISTER);
		spin_unlock_irq(&card->lock);
		if (signature == TPAM_MAGICNUMBER)
			break;
		set_current_state(TASK_UNINTERRUPTIBLE);
		schedule_timeout(2);
	}

	/* signature not present -> board not started */
	if (signature != TPAM_MAGICNUMBER) {
		printk(KERN_ERR "TurboPAM(tpam_command_ioctl_dsprun): "
		       "card=%d, signature 0x%lx, expected 0x%lx\n", 
		       card->id, (unsigned long)signature, 
		       (unsigned long)TPAM_MAGICNUMBER);
		printk(KERN_ERR "TurboPAM(tpam_command_ioctl_dsprun): "
		       "card=%d, firmware not started\n", card->id);
		return -EIO;
	}

	/* the firmware is started */
	printk(KERN_INFO "TurboPAM: card=%d, firmware started\n", card->id);

	/* init the CRC routines */
	init_CRC();

	/* create all the NCOs */
	for (i = 0; i < TPAM_NBCHANNEL; ++i)
		if ((skb = build_ACreateNCOReq("")))
			tpam_enqueue(card, skb);

	/* wait for NCO creation confirmation */
	timeout = jiffies + NCOCREATE_TIMEOUT;
	while (time_before(jiffies, timeout)) {
		if (card->channels_tested == TPAM_NBCHANNEL)
			break;
		set_current_state(TASK_UNINTERRUPTIBLE);
		schedule_timeout(2);
	}

	card->running = 1;

	if (card->channels_tested != TPAM_NBCHANNEL)
		printk(KERN_ERR "TurboPAM(tpam_command_ioctl_dsprun): "
		       "card=%d, tried to init %d channels, "
		       "got reply from only %d channels\n", card->id, 
		       TPAM_NBCHANNEL, card->channels_tested);

	/* if all the channels were not initialized, signal to the ISDN
	 * link layer that fact that some channels are not usable */
	if (card->channels_used != TPAM_NBCHANNEL)
		for (i = card->channels_used; i < TPAM_NBCHANNEL; ++i) {
			ctrl.driver = card->id;
			ctrl.command = ISDN_STAT_DISCH;
			ctrl.arg = i;
			ctrl.parm.num[0] = 0;
			(* card->interface.statcallb)(&ctrl);
		}

	printk(KERN_INFO "TurboPAM: card=%d, ready, %d channels available\n", 
	       card->id, card->channels_used);

	/* let's rock ! */
	ctrl.driver = card->id;
	ctrl.command = ISDN_STAT_RUN;
	ctrl.arg = 0;
	tpam_statcallb(card, ctrl);

	return 0;
}

/* 
 * Set/reset the board's looptest mode.
 *
 * 	card: the board
 * 	mode: if 1, sets the board's looptest mode, if 0 resets it.
 *
 * Return: 0 if OK, <0 if error.
 */
static int tpam_command_ioctl_loopmode(tpam_card *card, u8 mode) {

	/* board must be running */
	if (!card->running)
		return -ENODEV;

	card->loopmode = mode;
	return 0;
}

/*
 * Issue a dial command. This function builds and sends a CConnectReq.
 * 
 * 	card: the board
 * 	channel: the channel number
 * 	phone: the remote phone number (EAZ)
 *
 * Return: 0 if OK, <0 if error.
 */
static int tpam_command_dial(tpam_card *card, u32 channel, u8 *phone) {
	struct sk_buff *skb;
	isdn_ctrl ctrl;

	pr_debug("TurboPAM(tpam_command_dial): card=%d, channel=%lu, phone=%s\n",
		card->id, (unsigned long)channel, phone);

	/* board must be running */
	if (!card->running)
		return -ENODEV;

	/* initialize channel parameters */
	card->channels[channel].realhdlc = card->channels[channel].hdlc;
	card->channels[channel].hdlcshift = 0;
	card->channels[channel].readytoreceive = 0;

	/* build and send a CConnectReq */
	skb = build_CConnectReq(card->channels[channel].ncoid, phone, 
				card->channels[channel].realhdlc);
	if (!skb)
		return -ENOMEM;
	tpam_enqueue(card, skb);

	/* making a connection in modem mode is slow and causes the ISDN
	 * link layer to hangup the connection before even it gets a chance
	 * to establish... All we can do is simulate a successful connection
	 * for now, and send a DHUP later if the connection fails */
	if (!card->channels[channel].realhdlc) {
		ctrl.driver = card->id;
		ctrl.command = ISDN_STAT_DCONN;
		ctrl.arg = channel;
		tpam_statcallb(card, ctrl);
	}
	
	return 0;
}

/*
 * Set the level2 protocol (modem or HDLC).
 *
 * 	card: the board
 * 	channel: the channel number
 * 	proto: the level2 protocol (one of ISDN_PROTO_L2*)
 *
 * Return: 0 if OK, <0 if error.
 */
static int tpam_command_setl2(tpam_card *card, u32 channel, u8 proto) {

	pr_debug("TurboPAM(tpam_command_setl2): card=%d, channel=%lu, proto=%d\n",
		card->id, (unsigned long)channel, proto);

	/* board must be running */
	if (!card->running)
		return -ENODEV;

	/* set the hdlc/modem mode */
	switch (proto) {
		case ISDN_PROTO_L2_HDLC:
			card->channels[channel].hdlc = 1;
			break;
		case ISDN_PROTO_L2_MODEM:
			card->channels[channel].hdlc = 0;
			break;
		default:
			return -EINVAL;
	}
	return 0;
}

/*
 * Accept a D-channel connection (incoming connection). This function
 * builds and sends a CConnectRsp message and signals DCONN to the ISDN
 * link level.
 *
 * 	card: the board
 * 	channel: the channel number
 *
 * Return: 0 if OK, <0 if error.
 */
static int tpam_command_acceptd(tpam_card *card, u32 channel) {
	isdn_ctrl ctrl;
	struct sk_buff *skb;

	pr_debug("TurboPAM(tpam_command_acceptd): card=%d, channel=%lu\n",
		card->id, (unsigned long)channel);

	/* board must be running */
	if (!card->running)
		return -ENODEV;

	/* build and send a CConnectRsp */
	skb = build_CConnectRsp(card->channels[channel].ncoid);
	if (!skb)
		return -ENOMEM;
	tpam_enqueue(card, skb);

	/* issue DCONN to the ISDN link level */
	ctrl.driver = card->id;
	ctrl.command = ISDN_STAT_DCONN;
	ctrl.arg = channel;
	tpam_statcallb(card, ctrl);
	return 0;
}

/*
 * Accepts a B-channel connection. This is not used by the driver, 
 * since the TurboPAM is an active card hiding its B-channels from
 * us. We just signal BCONN to the ISDN link layer.
 *
 * 	card: the board
 * 	channel: the channel number
 *
 * Return: 0 if OK, <0 if error.
 */
static int tpam_command_acceptb(tpam_card *card, u32 channel) {
	isdn_ctrl ctrl;

	pr_debug("TurboPAM(tpam_command_acceptb): card=%d, channel=%lu\n",
		card->id, (unsigned long)channel);

	/* board must be running */
	if (!card->running)
		return -ENODEV;

	/* issue BCONN to the ISDN link level */
	ctrl.driver = card->id;
	ctrl.command = ISDN_STAT_BCONN;
	ctrl.arg = channel;
	ctrl.parm.num[0] = '\0';
	tpam_statcallb(card, ctrl);
	return 0;
}

/*
 * Hang up a connection. This function builds and sends a CDisconnectReq.
 *
 * 	card: the board
 * 	channel: the channel number.
 *
 * Return: 0 if OK, <0 if error.
 */
static int tpam_command_hangup(tpam_card *card, u32 channel) {
	struct sk_buff *skb;

	pr_debug("TurboPAM(tpam_command_hangup): card=%d, channel=%lu\n",
		card->id, (unsigned long)channel);

	/* board must be running */
	if (!card->running)
		return -ENODEV;

	/* build and send a CDisconnectReq */
	skb = build_CDisconnectReq(card->channels[channel].ncoid);
	if (!skb)
		return -ENOMEM;
	tpam_enqueue(card, skb);
	return 0;
}

/*
 * Proceed with an incoming connection. This function builds and sends a 
 * CConnectRsp.
 *
 * 	card: the board
 * 	channel: the channel number.
 *
 * Return: 0 if OK, <0 if error.
 */
static int tpam_command_proceed(tpam_card *card, u32 channel) {
	struct sk_buff *skb;

	pr_debug("TurboPAM(tpam_command_proceed): card=%d, channel=%lu\n",
		card->id, (unsigned long)channel);

	/* board must be running */
	if (!card->running)
		return -ENODEV;

	/* build and send a CConnectRsp */
	skb = build_CConnectRsp(card->channels[channel].ncoid);
	if (!skb)
		return -ENOMEM;
	tpam_enqueue(card, skb);
	return 0;
}

/*
 * Send data through the board. This function encodes the data depending
 * on the connection type (modem or HDLC), then builds and sends a U3DataReq.
 *
 * 	driverId: the driver id (really meaning here the board)
 * 	channel: the channel number
 * 	ack: data needs to be acknowledged upon send
 * 	skb: sk_buff containing the data
 *
 * Return: size of data send if OK, <0 if error.
 */
int tpam_writebuf_skb(int driverId, int channel, int ack, struct sk_buff *skb) {
	tpam_card *card;
	int orig_size = skb->len;
	void *finaldata;
	u32 finallen;

	pr_debug("TurboPAM(tpam_writebuf_skb): "
		"card=%d, channel=%ld, ack=%d, data size=%d\n", 
		driverId, (unsigned long)channel, ack, skb->len);

	/* find the board based on its driver ID */
	if (!(card = tpam_findcard(driverId))) {
		printk(KERN_ERR "TurboPAM(tpam_writebuf_skb): "
		       "invalid driverId %d\n", driverId);	
		return -ENODEV;
	}

	/* board must be running */
	if (!card->running)
		return -ENODEV;

	/* allocate some temporary memory */
	if (!(finaldata = (void *)__get_free_page(GFP_ATOMIC))) {
		printk(KERN_ERR "TurboPAM(tpam_writebuf_skb): "
		       "get_free_page failed\n");
		return -ENOMEM;
	}

	/* encode the data */
	if (!card->channels[channel].realhdlc) {
		/* modem mode */
		hdlc_encode_modem(skb->data, skb->len, finaldata, &finallen);
	}
	else {
		/* HDLC mode */
		void *tempdata;
		u32 templen;

		if (!(tempdata = (void *)__get_free_page(GFP_ATOMIC))) {
			printk(KERN_ERR "TurboPAM(tpam_writebuf_skb): "
			       "get_free_page failed\n");
			free_page((unsigned long)finaldata);
			return -ENOMEM;
		}
		hdlc_no_accm_encode(skb->data, skb->len, tempdata, &templen);
		finallen = tpam_hdlc_encode(tempdata, finaldata, 
				       &card->channels[channel].hdlcshift, 
				       templen);
		free_page((unsigned long)tempdata);
	}

	/* free the old sk_buff */
	kfree_skb(skb);

	/* build and send a U3DataReq */
	skb = build_U3DataReq(card->channels[channel].ncoid, finaldata, 
			      finallen, ack, orig_size);
	if (!skb) {
		free_page((unsigned long)finaldata);
		return -ENOMEM;
	}
	tpam_enqueue_data(&card->channels[channel], skb);

	/* free the temporary memory */
	free_page((unsigned long)finaldata);
	return orig_size;
}

/*
 * Treat a received ACreateNCOCnf message.
 *
 * 	card: the board
 * 	skb: the received message
 */
void tpam_recv_ACreateNCOCnf(tpam_card *card, struct sk_buff *skb) {
	u32 ncoid;
	u8 status;
	u32 channel;

	pr_debug("TurboPAM(tpam_recv_ACreateNCOCnf): card=%d\n", card->id);

	/* parse the message contents */
	if (parse_ACreateNCOCnf(skb, &status, &ncoid))
		return;

	/* if the card is alreay running, it means that this message
	 * arrives too late... */
	if (card->running) {
		printk(KERN_ERR "TurboPAM(tpam_recv_ACreateNCOCnf): "
		       "ACreateNCOCnf received too late, status=%d\n", status);
		return;
	}

	/* the NCO creation failed, the corresponding channel will
	 * be unused */
	if (status) {
		printk(KERN_ERR "TurboPAM(tpam_recv_ACreateNCOCnf): "
		       "ACreateNCO failed, status=%d\n", status);
		card->channels_tested++;
		return;
	}

	/* find the first free channel and assign the nco ID to it */
	if ((channel = tpam_findchannel(card, TPAM_NCOID_INVALID)) == TPAM_CHANNEL_INVALID) {
		printk(KERN_ERR "TurboPAM(tpam_recv_ACreateNCOCnf): "
		       "All channels are assigned\n");
		return;
	}
	card->channels[channel].ncoid = ncoid;
	card->channels_tested++;
	card->channels_used++;
}

/*
 * Treat a received ADestroyNCOCnf message. Not used by the driver.
 *
 * 	card: the board
 * 	skb: the received message
 */
void tpam_recv_ADestroyNCOCnf(tpam_card *card, struct sk_buff *skb) {
	u32 ncoid;
	u8 status;
	u32 channel;

	pr_debug("TurboPAM(tpam_recv_ADestroyNCOCnf): card=%d\n", card->id);

	/* parse the message contents */
	if (parse_ADestroyNCOCnf(skb, &status, &ncoid))
		return;
	
	if (status) {
		printk(KERN_ERR "TurboPAM(tpam_recv_ADestroyNCOCnf): "
		       "ADestroyNCO failed, status=%d\n", status);
		return;
	}

	/* clears the channel's nco ID */
	if ((channel = tpam_findchannel(card, ncoid)) == TPAM_CHANNEL_INVALID) {
		printk(KERN_ERR "TurboPAM(tpam_recv_ADestroyNCOCnf): "
		       "ncoid invalid %lu\n", (unsigned long)ncoid);
		return;
	}

	card->channels[channel].ncoid = TPAM_NCOID_INVALID;
}

/*
 * Treat a received CConnectCnf message.
 *
 * 	card: the board
 * 	skb: the received message
 */
void tpam_recv_CConnectCnf(tpam_card *card, struct sk_buff *skb) {
	u32 ncoid;
	u32 channel;
	isdn_ctrl ctrl;

	pr_debug("TurboPAM(tpam_recv_CConnectCnf): card=%d\n", card->id);

	/* parse the message contents */
	if (parse_CConnectCnf(skb, &ncoid))
		return;

	/* find the channel by its nco ID */
	if ((channel = tpam_findchannel(card, ncoid)) == TPAM_CHANNEL_INVALID) {
		printk(KERN_ERR "TurboPAM(tpam_recv_CConnectCnf): "
		       "ncoid invalid %lu\n", (unsigned long)ncoid);
		return;
	}

	/* issue a DCONN command to the ISDN link layer if we are in HDLC mode.
	 * In modem mode, we alreay did it - the ISDN timer kludge */
	if (card->channels[channel].realhdlc) {
		ctrl.driver = card->id;
		ctrl.command = ISDN_STAT_DCONN;
		ctrl.arg = channel;
		(* card->interface.statcallb)(&ctrl);
	}
}

/*
 * Treat a received CConnectInd message. This function signals a ICALL
 * to the ISDN link layer.
 *
 * 	card: the board
 * 	skb: the received message
 */
void tpam_recv_CConnectInd(tpam_card *card, struct sk_buff *skb) {
	u32 ncoid;
	u32 channel;
	u8 hdlc, plan, screen;
	u8 calling[PHONE_MAXIMUMSIZE], called[PHONE_MAXIMUMSIZE];
	isdn_ctrl ctrl;
	int status;

	pr_debug("TurboPAM(tpam_recv_CConnectInd): card=%d\n", card->id);

	/* parse the message contents */
	if (parse_CConnectInd(skb, &ncoid, &hdlc, calling, called, &plan, &screen))
		return;

	/* find the channel by its nco ID */
	if ((channel = tpam_findchannel(card, ncoid)) == TPAM_CHANNEL_INVALID) {
		printk(KERN_ERR "TurboPAM(tpam_recv_CConnectInd): "
		       "ncoid invalid %lu\n", (unsigned long)ncoid);
		return;
	}

	/* initialize the channel parameters */
	card->channels[channel].realhdlc = hdlc;
	card->channels[channel].hdlcshift = 0;
	card->channels[channel].readytoreceive = 0;

	/* issue a ICALL command to the ISDN link layer */
	ctrl.driver = card->id;
	ctrl.command = ISDN_STAT_ICALL;
	ctrl.arg = channel;
	memcpy(ctrl.parm.setup.phone, calling, 32);
	memcpy(ctrl.parm.setup.eazmsn, called, 32);
	ctrl.parm.setup.si1 = 7;	/* data capability */
	ctrl.parm.setup.si2 = 0;
	ctrl.parm.setup.plan = plan;
	ctrl.parm.setup.screen = screen;

	status = (* card->interface.statcallb)(&ctrl);
	switch (status) {
		case 1:
		case 4:
			/* call accepted, link layer will send us a ACCEPTD 
			 * command later */
			pr_debug("TurboPAM(tpam_recv_CConnectInd): "
				"card=%d, channel=%d, icall waiting, status=%d\n", 
				card->id, channel, status);
			break;
		default:
			/* call denied, we build and send a CDisconnectReq */
			pr_debug("TurboPAM(tpam_recv_CConnectInd): "
				"card=%d, channel=%d, icall denied, status=%d\n", 
				card->id, channel, status);
			skb = build_CDisconnectReq(ncoid);
			if (!skb)
				return;
			tpam_enqueue(card, skb);
	}
}

/*
 * Treat a received CDisconnectInd message. This function signals a DHUP and
 * a BHUP to the ISDN link layer.
 *
 * 	card: the board
 * 	skb: the received message
 */
void tpam_recv_CDisconnectInd(tpam_card *card, struct sk_buff *skb) {
	u32 ncoid;
	u32 channel;
	u32 cause;
	isdn_ctrl ctrl;

	pr_debug("TurboPAM(tpam_recv_CDisconnectInd): card=%d\n", card->id);

	/* parse the message contents */
	if (parse_CDisconnectInd(skb, &ncoid, &cause))
		return;

	/* find the channel by its nco ID */
	if ((channel = tpam_findchannel(card, ncoid)) == TPAM_CHANNEL_INVALID) {
		printk(KERN_ERR "TurboPAM(tpam_recv_CDisconnectInd): "
		       "ncoid invalid %lu\n", (unsigned long)ncoid);
		return;
	}

	/* build and send a CDisconnectRsp */
	skb = build_CDisconnectRsp(ncoid);
	if (!skb)
		return;
	tpam_enqueue(card, skb);

	/* issue a DHUP to the ISDN link layer */
	ctrl.driver = card->id;
	ctrl.command = ISDN_STAT_DHUP;
	ctrl.arg = channel;
	(* card->interface.statcallb)(&ctrl);

	/* issue a BHUP to the ISDN link layer */
	ctrl.driver = card->id;
	ctrl.command = ISDN_STAT_BHUP;
	ctrl.arg = channel;
	(* card->interface.statcallb)(&ctrl);
}

/*
 * Treat a received CDisconnectCnf message. This function signals a DHUP and
 * a BHUP to the ISDN link layer.
 *
 * 	card: the board
 * 	skb: the received message
 */
void tpam_recv_CDisconnectCnf(tpam_card *card, struct sk_buff *skb) {
	u32 ncoid;
	u32 channel;
	u32 cause;
	isdn_ctrl ctrl;

	pr_debug("TurboPAM(tpam_recv_CDisconnectCnf): card=%d\n", card->id);

	/* parse the message contents */
	if (parse_CDisconnectCnf(skb, &ncoid, &cause))
		return;

	/* find the channel by its nco ID */
	if ((channel = tpam_findchannel(card, ncoid)) == TPAM_CHANNEL_INVALID) {
		printk(KERN_ERR "TurboPAM(tpam_recv_CDisconnectCnf): "
		       "ncoid invalid %lu\n", (unsigned long)ncoid);
		return;
	}

	/* issue a DHUP to the ISDN link layer */
	ctrl.driver = card->id;
	ctrl.command = ISDN_STAT_DHUP;
	ctrl.arg = channel;
	(* card->interface.statcallb)(&ctrl);

	/* issue a BHUP to the ISDN link layer */
	ctrl.driver = card->id;
	ctrl.command = ISDN_STAT_BHUP;
	ctrl.arg = channel;
	(* card->interface.statcallb)(&ctrl);
}

/*
 * Treat a received U3DataInd message. This function decodes the data
 * depending on the connection type (modem or HDLC) and passes it to the
 * ISDN link layer by using rcvcallb_skb.
 *
 * 	card: the board
 * 	skb: the received message + data
 */
void tpam_recv_U3DataInd(tpam_card *card, struct sk_buff *skb) {
	u32 ncoid;
	u32 channel;
	u8 *data;
	u16 len;
	struct sk_buff *result;

	pr_debug("TurboPAM(tpam_recv_U3DataInd): card=%d, datalen=%d\n",
		card->id, skb->len);

	/* parse the message contents */
	if (parse_U3DataInd(skb, &ncoid, &data, &len))
		return;

	/* find the channel by its nco ID */
	if ((channel = tpam_findchannel(card, ncoid)) == TPAM_CHANNEL_INVALID) {
		printk(KERN_ERR "TurboPAM(tpam_recv_U3DataInd): "
		       "ncoid invalid %lu\n", (unsigned long)ncoid);
		return;
	}

	/* decode the data */
	if (card->channels[ncoid].realhdlc) {
		/* HDLC mode */
		u8 *tempdata;
		u32 templen;

		if (!(tempdata = (void *)__get_free_page(GFP_ATOMIC))) {
			printk(KERN_ERR "TurboPAM(tpam_recv_U3DataInd): "
			       "get_free_page failed\n");
			return;
		}
		templen = tpam_hdlc_decode(data, tempdata, len);
		templen = hdlc_no_accm_decode(tempdata, templen);
		if (!(result = alloc_skb(templen, GFP_ATOMIC))) {
			printk(KERN_ERR "TurboPAM(tpam_recv_U3DataInd): "
			       "alloc_skb failed\n");
			free_page((unsigned long)tempdata);
			return;
		}
		memcpy(skb_put(result, templen), tempdata, templen);
		free_page((unsigned long)tempdata);
	}
	else {
		/* modem mode */
		if (!(result = alloc_skb(len, GFP_ATOMIC))) {
			printk(KERN_ERR "TurboPAM(tpam_recv_U3DataInd): "
			       "alloc_skb failed\n");
			return;
		}
		memcpy(skb_put(result, len), data, len);
	}

	/* In loop mode, resend the data immediately */
	if (card->loopmode) {
		struct sk_buff *loopskb;

		if (!(loopskb = alloc_skb(skb->len, GFP_ATOMIC))) {
			printk(KERN_ERR "TurboPAM(tpam_recv_U3DataInd): "
			       "alloc_skb failed\n");
			kfree_skb(result);
			return;
		}
		memcpy(skb_put(loopskb, result->len), result->data, 
		       result->len);
		if (tpam_writebuf_skb(card->id, channel, 0, loopskb) < 0)
			kfree_skb(loopskb);
	}

	/* pass the data to the ISDN link layer */
	(* card->interface.rcvcallb_skb)(card->id, channel, result);
}

/*
 * Treat a received U3ReadyToReceiveInd message. This function sets the
 * channel ready flag and triggers the send of data if the channel becomed
 * ready.
 *
 * 	card: the board
 * 	skb: the received message + data
 */
void tpam_recv_U3ReadyToReceiveInd(tpam_card *card, struct sk_buff *skb) {
	u32 ncoid;
	u32 channel;
	u8 ready;

	pr_debug("TurboPAM(tpam_recv_U3ReadyToReceiveInd): card=%d\n", card->id);

	/* parse the message contents */
	if (parse_U3ReadyToReceiveInd(skb, &ncoid, &ready))
		return;

	/* find the channel by its nco ID */
	if ((channel = tpam_findchannel(card, ncoid)) == TPAM_CHANNEL_INVALID) {
		printk(KERN_ERR "TurboPAM(tpam_recv_U3ReadyToReceiveInd): "
		       "ncoid invalid %lu\n", (unsigned long)ncoid);
		return;
	}

	/* set the readytoreceive flag */
	card->channels[channel].readytoreceive = ready;

	/* if the channel just becomed ready, trigger the send of queued data */
	if (ready)
		tpam_enqueue_data(&card->channels[channel], NULL);
}

/*
 * Runs the delayed statcallb when its timer expires.
 *
 * 	parm: pointer to the tpam_statcallb_data statcallb argument.
 */
static void tpam_statcallb_run(unsigned long parm) {
	tpam_statcallb_data *ds = (tpam_statcallb_data *)parm;

	pr_debug("TurboPAM(tpam_statcallb_run)\n");

	(* ds->card->interface.statcallb)(&ds->ctrl);

	kfree(ds->timer);
	kfree(ds);
}

/*
 * Queues a statcallb call for delayed invocation.
 *
 * 	card: the board
 * 	ctrl: the statcallb argument
 */
static void tpam_statcallb(tpam_card *card, isdn_ctrl ctrl) {
	struct timer_list *timer;
	tpam_statcallb_data *ds;

	pr_debug("TurboPAM(tpam_statcallb): card=%d\n", card->id);

	if (!(timer = (struct timer_list *) kmalloc(sizeof(struct timer_list), 
						    GFP_ATOMIC))) {
		printk(KERN_ERR "TurboPAM: tpam_statcallb: kmalloc failed!\n");
		return;
	}

	if (!(ds = (tpam_statcallb_data *) kmalloc(sizeof(tpam_statcallb_data),
						   GFP_ATOMIC))) {
		printk(KERN_ERR "TurboPAM: tpam_statcallb: kmalloc failed!\n");
		kfree(timer);
		return;
	}
	ds->card = card;
	ds->timer = timer;
	memcpy(&ds->ctrl, &ctrl, sizeof(isdn_ctrl));

	init_timer(timer);
	timer->function = tpam_statcallb_run;
	timer->data = (unsigned long)ds;
	timer->expires = jiffies + HZ / 10;   /* 0.1 second */
	add_timer(timer);
}
