summaryrefslogblamecommitdiffstats
path: root/drivers/isdn/isdnloop/isdnloop.c
blob: 23afba46433e2611f5a47535e5f94693f9e6ba9a (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463










                                                                       











                                                                            
                                    













































































                                                                                         
                                                               


                                                                         
                                                                    














































































































































































                                                                                            
                                                       






                                                             
                                                            
















































































                                                                                                                 
                                                                       




                                                                                 
                                                                            

                            
                                                       

                                                                 
                                                            




























                                                                                                   
                                                               







                                                                     
                                                                    




















































































































































                                                                                  
                                                       








                                                                            
                                                            

































                                                     
                                                       







                                                                       
                                                            













                                                  
                                                       
                                      
                                                            






















































                                                                                                   
                                                                               



                                                                                           
                                                                                                    






                                                                               
                                                                                            

                                                 
                                                                                            














































































































































































































































































































                                                                                                       
                                                       









                                                       
                                                            





































                                                                         
                                                       



                                                                                          
                                                                                    



                                                               
                                                                                    







                                                                                         
                                                                                    



                                                               
                                                                                    






                                                            
                                                                            









                                                                                        
                                                            




































                                                                                                       


                                                                                                   




















































































































































































































































































































                                                                                                                
                                                 











































































                                                                
/* $Id: isdnloop.c,v 1.11.6.7 2001/11/11 19:54:31 kai Exp $
 *
 * ISDN low-level module implementing a dummy loop driver.
 *
 * Copyright 1997 by Fritz Elfert (fritz@isdn4linux.de)
 *
 * This software may be used and distributed according to the terms
 * of the GNU General Public License, incorporated herein by reference.
 *
 */

#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/sched.h>
#include "isdnloop.h"

static char *revision = "$Revision: 1.11.6.7 $";
static char *isdnloop_id = "loop0";

MODULE_DESCRIPTION("ISDN4Linux: Pseudo Driver that simulates an ISDN card");
MODULE_AUTHOR("Fritz Elfert");
MODULE_LICENSE("GPL");
module_param(isdnloop_id, charp, 0);
MODULE_PARM_DESC(isdnloop_id, "ID-String of first card");

static int isdnloop_addcard(char *);

/*
 * Free queue completely.
 *
 * Parameter:
 *   card    = pointer to card struct
 *   channel = channel number
 */
static void
isdnloop_free_queue(isdnloop_card * card, int channel)
{
	struct sk_buff_head *queue = &card->bqueue[channel];

	skb_queue_purge(queue);
	card->sndcount[channel] = 0;
}

/*
 * Send B-Channel data to another virtual card.
 * This routine is called via timer-callback from isdnloop_pollbchan().
 *
 * Parameter:
 *   card = pointer to card struct.
 *   ch   = channel number (0-based)
 */
static void
isdnloop_bchan_send(isdnloop_card * card, int ch)
{
	isdnloop_card *rcard = card->rcard[ch];
	int rch = card->rch[ch], len, ack;
	struct sk_buff *skb;
	isdn_ctrl cmd;

	while (card->sndcount[ch]) {
		if ((skb = skb_dequeue(&card->bqueue[ch]))) {
			len = skb->len;
			card->sndcount[ch] -= len;
			ack = *(skb->head); /* used as scratch area */
			cmd.driver = card->myid;
			cmd.arg = ch;
			if (rcard){
				rcard->interface.rcvcallb_skb(rcard->myid, rch, skb);
			} else {
				printk(KERN_WARNING "isdnloop: no rcard, skb dropped\n");
				dev_kfree_skb(skb);

			};
			cmd.command = ISDN_STAT_BSENT;
			cmd.parm.length = len;
			card->interface.statcallb(&cmd);
		} else
			card->sndcount[ch] = 0;
	}
}

/*
 * Send/Receive Data to/from the B-Channel.
 * This routine is called via timer-callback.
 * It schedules itself while any B-Channel is open.
 *
 * Parameter:
 *   data = pointer to card struct, set by kernel timer.data
 */
static void
isdnloop_pollbchan(unsigned long data)
{
	isdnloop_card *card = (isdnloop_card *) data;
	unsigned long flags;

	if (card->flags & ISDNLOOP_FLAGS_B1ACTIVE)
		isdnloop_bchan_send(card, 0);
	if (card->flags & ISDNLOOP_FLAGS_B2ACTIVE)
		isdnloop_bchan_send(card, 1);
	if (card->flags & (ISDNLOOP_FLAGS_B1ACTIVE | ISDNLOOP_FLAGS_B2ACTIVE)) {
		/* schedule b-channel polling again */
		spin_lock_irqsave(&card->isdnloop_lock, flags);
		card->rb_timer.expires = jiffies + ISDNLOOP_TIMER_BCREAD;
		add_timer(&card->rb_timer);
		card->flags |= ISDNLOOP_FLAGS_RBTIMER;
		spin_unlock_irqrestore(&card->isdnloop_lock, flags);
	} else
		card->flags &= ~ISDNLOOP_FLAGS_RBTIMER;
}

/*
 * Parse ICN-type setup string and fill fields of setup-struct
 * with parsed data.
 *
 * Parameter:
 *   setup = setup string, format: [caller-id],si1,si2,[called-id]
 *   cmd   = pointer to struct to be filled.
 */
static void
isdnloop_parse_setup(char *setup, isdn_ctrl * cmd)
{
	char *t = setup;
	char *s = strchr(t, ',');

	*s++ = '\0';
	strlcpy(cmd->parm.setup.phone, t, sizeof(cmd->parm.setup.phone));
	s = strchr(t = s, ',');
	*s++ = '\0';
	if (!strlen(t))
		cmd->parm.setup.si1 = 0;
	else
		cmd->parm.setup.si1 = simple_strtoul(t, NULL, 10);
	s = strchr(t = s, ',');
	*s++ = '\0';
	if (!strlen(t))
		cmd->parm.setup.si2 = 0;
	else
		cmd->parm.setup.si2 =
		    simple_strtoul(t, NULL, 10);
	strlcpy(cmd->parm.setup.eazmsn, s, sizeof(cmd->parm.setup.eazmsn));
	cmd->parm.setup.plan = 0;
	cmd->parm.setup.screen = 0;
}

typedef struct isdnloop_stat {
	char *statstr;
	int command;
	int action;
} isdnloop_stat;
/* *INDENT-OFF* */
static isdnloop_stat isdnloop_stat_table[] =
{
	{"BCON_",          ISDN_STAT_BCONN, 1}, /* B-Channel connected        */
	{"BDIS_",          ISDN_STAT_BHUP,  2}, /* B-Channel disconnected     */
	{"DCON_",          ISDN_STAT_DCONN, 0}, /* D-Channel connected        */
	{"DDIS_",          ISDN_STAT_DHUP,  0}, /* D-Channel disconnected     */
	{"DCAL_I",         ISDN_STAT_ICALL, 3}, /* Incoming call dialup-line  */
	{"DSCA_I",         ISDN_STAT_ICALL, 3}, /* Incoming call 1TR6-SPV     */
	{"FCALL",          ISDN_STAT_ICALL, 4}, /* Leased line connection up  */
	{"CIF",            ISDN_STAT_CINF,  5}, /* Charge-info, 1TR6-type     */
	{"AOC",            ISDN_STAT_CINF,  6}, /* Charge-info, DSS1-type     */
	{"CAU",            ISDN_STAT_CAUSE, 7}, /* Cause code                 */
	{"TEI OK",         ISDN_STAT_RUN,   0}, /* Card connected to wallplug */
	{"E_L1: ACT FAIL", ISDN_STAT_BHUP,  8}, /* Layer-1 activation failed  */
	{"E_L2: DATA LIN", ISDN_STAT_BHUP,  8}, /* Layer-2 data link lost     */
	{"E_L1: ACTIVATION FAILED",
			   ISDN_STAT_BHUP,  8},         /* Layer-1 activation failed  */
	{NULL, 0, -1}
};
/* *INDENT-ON* */


/*
 * Parse Status message-strings from virtual card.
 * Depending on status, call statcallb for sending messages to upper
 * levels. Also set/reset B-Channel active-flags.
 *
 * Parameter:
 *   status  = status string to parse.
 *   channel = channel where message comes from.
 *   card    = card where message comes from.
 */
static void
isdnloop_parse_status(u_char * status, int channel, isdnloop_card * card)
{
	isdnloop_stat *s = isdnloop_stat_table;
	int action = -1;
	isdn_ctrl cmd;

	while (s->statstr) {
		if (!strncmp(status, s->statstr, strlen(s->statstr))) {
			cmd.command = s->command;
			action = s->action;
			break;
		}
		s++;
	}
	if (action == -1)
		return;
	cmd.driver = card->myid;
	cmd.arg = channel;
	switch (action) {
		case 1:
			/* BCON_x */
			card->flags |= (channel) ?
			    ISDNLOOP_FLAGS_B2ACTIVE : ISDNLOOP_FLAGS_B1ACTIVE;
			break;
		case 2:
			/* BDIS_x */
			card->flags &= ~((channel) ?
					 ISDNLOOP_FLAGS_B2ACTIVE : ISDNLOOP_FLAGS_B1ACTIVE);
			isdnloop_free_queue(card, channel);
			break;
		case 3:
			/* DCAL_I and DSCA_I */
			isdnloop_parse_setup(status + 6, &cmd);
			break;
		case 4:
			/* FCALL */
			sprintf(cmd.parm.setup.phone, "LEASED%d", card->myid);
			sprintf(cmd.parm.setup.eazmsn, "%d", channel + 1);
			cmd.parm.setup.si1 = 7;
			cmd.parm.setup.si2 = 0;
			cmd.parm.setup.plan = 0;
			cmd.parm.setup.screen = 0;
			break;
		case 5:
			/* CIF */
			strlcpy(cmd.parm.num, status + 3, sizeof(cmd.parm.num));
			break;
		case 6:
			/* AOC */
			snprintf(cmd.parm.num, sizeof(cmd.parm.num), "%d",
			     (int) simple_strtoul(status + 7, NULL, 16));
			break;
		case 7:
			/* CAU */
			status += 3;
			if (strlen(status) == 4)
				snprintf(cmd.parm.num, sizeof(cmd.parm.num), "%s%c%c",
				     status + 2, *status, *(status + 1));
			else
				strlcpy(cmd.parm.num, status + 1, sizeof(cmd.parm.num));
			break;
		case 8:
			/* Misc Errors on L1 and L2 */
			card->flags &= ~ISDNLOOP_FLAGS_B1ACTIVE;
			isdnloop_free_queue(card, 0);
			cmd.arg = 0;
			cmd.driver = card->myid;
			card->interface.statcallb(&cmd);
			cmd.command = ISDN_STAT_DHUP;
			cmd.arg = 0;
			cmd.driver = card->myid;
			card->interface.statcallb(&cmd);
			cmd.command = ISDN_STAT_BHUP;
			card->flags &= ~ISDNLOOP_FLAGS_B2ACTIVE;
			isdnloop_free_queue(card, 1);
			cmd.arg = 1;
			cmd.driver = card->myid;
			card->interface.statcallb(&cmd);
			cmd.command = ISDN_STAT_DHUP;
			cmd.arg = 1;
			cmd.driver = card->myid;
			break;
	}
	card->interface.statcallb(&cmd);
}

/*
 * Store a cwcharacter into ringbuffer for reading from /dev/isdnctrl
 *
 * Parameter:
 *   card = pointer to card struct.
 *   c    = char to store.
 */
static void
isdnloop_putmsg(isdnloop_card * card, unsigned char c)
{
	ulong flags;

	spin_lock_irqsave(&card->isdnloop_lock, flags);
	*card->msg_buf_write++ = (c == 0xff) ? '\n' : c;
	if (card->msg_buf_write == card->msg_buf_read) {
		if (++card->msg_buf_read > card->msg_buf_end)
			card->msg_buf_read = card->msg_buf;
	}
	if (card->msg_buf_write > card->msg_buf_end)
		card->msg_buf_write = card->msg_buf;
	spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}

/*
 * Poll a virtual cards message queue.
 * If there are new status-replies from the card, copy them to
 * ringbuffer for reading on /dev/isdnctrl and call
 * isdnloop_parse_status() for processing them. Watch for special
 * Firmware bootmessage and parse it, to get the D-Channel protocol.
 * If there are B-Channels open, initiate a timer-callback to
 * isdnloop_pollbchan().
 * This routine is called periodically via timer interrupt.
 *
 * Parameter:
 *   data = pointer to card struct
 */
static void
isdnloop_polldchan(unsigned long data)
{
	isdnloop_card *card = (isdnloop_card *) data;
	struct sk_buff *skb;
	int avail;
	int left;
	u_char c;
	int ch;
	unsigned long flags;
	u_char *p;
	isdn_ctrl cmd;

	if ((skb = skb_dequeue(&card->dqueue)))
		avail = skb->len;
	else
		avail = 0;
	for (left = avail; left > 0; left--) {
		c = *skb->data;
		skb_pull(skb, 1);
		isdnloop_putmsg(card, c);
		card->imsg[card->iptr] = c;
		if (card->iptr < 59)
			card->iptr++;
		if (!skb->len) {
			avail++;
			isdnloop_putmsg(card, '\n');
			card->imsg[card->iptr] = 0;
			card->iptr = 0;
			if (card->imsg[0] == '0' && card->imsg[1] >= '0' &&
			  card->imsg[1] <= '2' && card->imsg[2] == ';') {
				ch = (card->imsg[1] - '0') - 1;
				p = &card->imsg[3];
				isdnloop_parse_status(p, ch, card);
			} else {
				p = card->imsg;
				if (!strncmp(p, "DRV1.", 5)) {
					printk(KERN_INFO "isdnloop: (%s) %s\n", CID, p);
					if (!strncmp(p + 7, "TC", 2)) {
						card->ptype = ISDN_PTYPE_1TR6;
						card->interface.features |= ISDN_FEATURE_P_1TR6;
						printk(KERN_INFO
						       "isdnloop: (%s) 1TR6-Protocol loaded and running\n", CID);
					}
					if (!strncmp(p + 7, "EC", 2)) {
						card->ptype = ISDN_PTYPE_EURO;
						card->interface.features |= ISDN_FEATURE_P_EURO;
						printk(KERN_INFO
						       "isdnloop: (%s) Euro-Protocol loaded and running\n", CID);
					}
					continue;

				}
			}
		}
	}
	if (avail) {
		cmd.command = ISDN_STAT_STAVAIL;
		cmd.driver = card->myid;
		cmd.arg = avail;
		card->interface.statcallb(&cmd);
	}
	if (card->flags & (ISDNLOOP_FLAGS_B1ACTIVE | ISDNLOOP_FLAGS_B2ACTIVE))
		if (!(card->flags & ISDNLOOP_FLAGS_RBTIMER)) {
			/* schedule b-channel polling */
			card->flags |= ISDNLOOP_FLAGS_RBTIMER;
			spin_lock_irqsave(&card->isdnloop_lock, flags);
			del_timer(&card->rb_timer);
			card->rb_timer.function = isdnloop_pollbchan;
			card->rb_timer.data = (unsigned long) card;
			card->rb_timer.expires = jiffies + ISDNLOOP_TIMER_BCREAD;
			add_timer(&card->rb_timer);
			spin_unlock_irqrestore(&card->isdnloop_lock, flags);
		}
	/* schedule again */
	spin_lock_irqsave(&card->isdnloop_lock, flags);
	card->st_timer.expires = jiffies + ISDNLOOP_TIMER_DCREAD;
	add_timer(&card->st_timer);
	spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}

/*
 * Append a packet to the transmit buffer-queue.
 *
 * Parameter:
 *   channel = Number of B-channel
 *   skb     = packet to send.
 *   card    = pointer to card-struct
 * Return:
 *   Number of bytes transferred, -E??? on error
 */
static int
isdnloop_sendbuf(int channel, struct sk_buff *skb, isdnloop_card * card)
{
	int len = skb->len;
	unsigned long flags;
	struct sk_buff *nskb;

	if (len > 4000) {
		printk(KERN_WARNING
		       "isdnloop: Send packet too large\n");
		return -EINVAL;
	}
	if (len) {
		if (!(card->flags & (channel) ? ISDNLOOP_FLAGS_B2ACTIVE : ISDNLOOP_FLAGS_B1ACTIVE))
			return 0;
		if (card->sndcount[channel] > ISDNLOOP_MAX_SQUEUE)
			return 0;
		spin_lock_irqsave(&card->isdnloop_lock, flags);
		nskb = dev_alloc_skb(skb->len);
		if (nskb) {
			memcpy(skb_put(nskb, len), skb->data, len);
			skb_queue_tail(&card->bqueue[channel], nskb);
			dev_kfree_skb(skb);
		} else
			len = 0;
		card->sndcount[channel] += len;
		spin_unlock_irqrestore(&card->isdnloop_lock, flags);
	}
	return len;
}

/*
 * Read the messages from the card's ringbuffer
 *
 * Parameter:
 *   buf  = pointer to buffer.
 *   len  = number of bytes to read.
 *   user = flag, 1: called from userlevel 0: called from kernel.
 *   card = pointer to card struct.
 * Return:
 *   number of bytes actually transferred.
 */
static int
isdnloop_readstatus(u_char __user *buf, int len, isdnloop_card * card)
{
	int count;
	u_char __user *p;

	for (p = buf, count = 0; count < len; p++, count++) {
		if (card->msg_buf_read == card->msg_buf_write)
			return count;
		put_user(*card->msg_buf_read++, p);
		if (card->msg_buf_read > card->msg_buf_end)
			card->msg_buf_read = card->msg_buf;
	}
	return count;
}

/*
 * Simulate a card's response by appending it to the cards
 * message queue.
 *
 * Parameter:
 *   card = pointer to card struct.
 *   s    = pointer to message-string.
 *   ch   = channel: 0 = generic messages, 1 and 2 = D-channel messages.
 * Return:
 *   0 on success, 1 on memory squeeze.
 */
static int
isdnloop_fake(isdnloop_card * card, char *s, int ch)
{
	struct sk_buff *skb;
	int len = strlen(s) + ((ch >= 0) ? 3 : 0);

	if (!(skb = dev_alloc_skb(len))) {
		printk(KERN_WARNING "isdnloop: Out of memory in isdnloop_fake\n");
		return 1;
	}
	if (ch >= 0)
		sprintf(skb_put(skb, 3), "%02d;", ch);
	memcpy(skb_put(skb, strlen(s)), s, strlen(s));
	skb_queue_tail(&card->dqueue, skb);
	return 0;
}
/* *INDENT-OFF* */
static isdnloop_stat isdnloop_cmd_table[] =
{
	{"BCON_R",         0,  1},	/* B-Channel connect        */
	{"BCON_I",         0, 17},	/* B-Channel connect ind    */
	{"BDIS_R",         0,  2},	/* B-Channel disconnect     */
	{"DDIS_R",         0,  3},	/* D-Channel disconnect     */
	{"DCON_R",         0, 16},	/* D-Channel connect        */
	{"DSCA_R",         0,  4},	/* Dial 1TR6-SPV     */
	{"DCAL_R",         0,  5},	/* Dial */
	{"EAZC",           0,  6},	/* Clear EAZ listener */
	{"EAZ",            0,  7},	/* Set EAZ listener */
	{"SEEAZ",          0,  8},	/* Get EAZ listener */
	{"MSN",            0,  9},	/* Set/Clear MSN listener */
	{"MSALL",          0, 10},	/* Set multi MSN listeners */
	{"SETSIL",         0, 11},	/* Set SI list     */
	{"SEESIL",         0, 12},	/* Get SI list     */
	{"SILC",           0, 13},	/* Clear SI list     */
	{"LOCK",           0, -1},	/* LOCK channel     */
	{"UNLOCK",         0, -1},	/* UNLOCK channel     */
	{"FV2ON",          1, 14},	/* Leased mode on               */
	{"FV2OFF",         1, 15},	/* Leased mode off              */
	{NULL, 0, -1}
};
/* *INDENT-ON* */


/*
 * Simulate an error-response from a card.
 *
 * Parameter:
 *   card = pointer to card struct.
 */
static void
isdnloop_fake_err(isdnloop_card * card)
{
	char buf[60];

	sprintf(buf, "E%s", card->omsg);
	isdnloop_fake(card, buf, -1);
	isdnloop_fake(card, "NAK", -1);
}

static u_char ctable_eu[] =
{0x00, 0x11, 0x01, 0x12};
static u_char ctable_1t[] =
{0x00, 0x3b, 0x01, 0x3a};

/*
 * Assemble a simplified cause message depending on the
 * D-channel protocol used.
 *
 * Parameter:
 *   card = pointer to card struct.
 *   loc  = location: 0 = local, 1 = remote.
 *   cau  = cause: 1 = busy, 2 = nonexistent callerid, 3 = no user responding.
 * Return:
 *   Pointer to buffer containing the assembled message.
 */
static char *
isdnloop_unicause(isdnloop_card * card, int loc, int cau)
{
	static char buf[6];

	switch (card->ptype) {
		case ISDN_PTYPE_EURO:
			sprintf(buf, "E%02X%02X", (loc) ? 4 : 2, ctable_eu[cau]);
			break;
		case ISDN_PTYPE_1TR6:
			sprintf(buf, "%02X44", ctable_1t[cau]);
			break;
		default:
			return ("0000");
	}
	return (buf);
}

/*
 * Release a virtual connection. Called from timer interrupt, when
 * called party did not respond.
 *
 * Parameter:
 *   card = pointer to card struct.
 *   ch   = channel (0-based)
 */
static void
isdnloop_atimeout(isdnloop_card * card, int ch)
{
	unsigned long flags;
	char buf[60];

	spin_lock_irqsave(&card->isdnloop_lock, flags);
	if (card->rcard) {
		isdnloop_fake(card->rcard[ch], "DDIS_I", card->rch[ch] + 1);
		card->rcard[ch]->rcard[card->rch[ch]] = NULL;
		card->rcard[ch] = NULL;
	}
	isdnloop_fake(card, "DDIS_I", ch + 1);
	/* No user responding */
	sprintf(buf, "CAU%s", isdnloop_unicause(card, 1, 3));
	isdnloop_fake(card, buf, ch + 1);
	spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}

/*
 * Wrapper for isdnloop_atimeout().
 */
static void
isdnloop_atimeout0(unsigned long data)
{
	isdnloop_card *card = (isdnloop_card *) data;
	isdnloop_atimeout(card, 0);
}

/*
 * Wrapper for isdnloop_atimeout().
 */
static void
isdnloop_atimeout1(unsigned long data)
{
	isdnloop_card *card = (isdnloop_card *) data;
	isdnloop_atimeout(card, 1);
}

/*
 * Install a watchdog for a user, not responding.
 *
 * Parameter:
 *   card = pointer to card struct.
 *   ch   = channel to watch for.
 */
static void
isdnloop_start_ctimer(isdnloop_card * card, int ch)
{
	unsigned long flags;

	spin_lock_irqsave(&card->isdnloop_lock, flags);
	init_timer(&card->c_timer[ch]);
	card->c_timer[ch].expires = jiffies + ISDNLOOP_TIMER_ALERTWAIT;
	if (ch)
		card->c_timer[ch].function = isdnloop_atimeout1;
	else
		card->c_timer[ch].function = isdnloop_atimeout0;
	card->c_timer[ch].data = (unsigned long) card;
	add_timer(&card->c_timer[ch]);
	spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}

/*
 * Kill a pending channel watchdog.
 *
 * Parameter:
 *   card = pointer to card struct.
 *   ch   = channel (0-based).
 */
static void
isdnloop_kill_ctimer(isdnloop_card * card, int ch)
{
	unsigned long flags;

	spin_lock_irqsave(&card->isdnloop_lock, flags);
	del_timer(&card->c_timer[ch]);
	spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}

static u_char si2bit[] =
{0, 1, 0, 0, 0, 2, 0, 4, 0, 0};
static u_char bit2si[] =
{1, 5, 7};

/*
 * Try finding a listener for an outgoing call.
 *
 * Parameter:
 *   card = pointer to calling card.
 *   p    = pointer to ICN-type setup-string.
 *   lch  = channel of calling card.
 *   cmd  = pointer to struct to be filled when parsing setup.
 * Return:
 *   0 = found match, alerting should happen.
 *   1 = found matching number but it is busy.
 *   2 = no matching listener.
 *   3 = found matching number but SI does not match.
 */
static int
isdnloop_try_call(isdnloop_card * card, char *p, int lch, isdn_ctrl * cmd)
{
	isdnloop_card *cc = cards;
	unsigned long flags;
	int ch;
	int num_match;
	int i;
	char *e;
	char nbuf[32];

	isdnloop_parse_setup(p, cmd);
	while (cc) {
		for (ch = 0; ch < 2; ch++) {
			/* Exclude ourself */
			if ((cc == card) && (ch == lch))
				continue;
			num_match = 0;
			switch (cc->ptype) {
				case ISDN_PTYPE_EURO:
					for (i = 0; i < 3; i++)
						if (!(strcmp(cc->s0num[i], cmd->parm.setup.phone)))
							num_match = 1;
					break;
				case ISDN_PTYPE_1TR6:
					e = cc->eazlist[ch];
					while (*e) {
						sprintf(nbuf, "%s%c", cc->s0num[0], *e);
						if (!(strcmp(nbuf, cmd->parm.setup.phone)))
							num_match = 1;
						e++;
					}
			}
			if (num_match) {
				spin_lock_irqsave(&card->isdnloop_lock, flags);
				/* channel idle? */
				if (!(cc->rcard[ch])) {
					/* Check SI */
					if (!(si2bit[cmd->parm.setup.si1] & cc->sil[ch])) {
						spin_unlock_irqrestore(&card->isdnloop_lock, flags);
						return 3;
					}
					/* ch is idle, si and number matches */
					cc->rcard[ch] = card;
					cc->rch[ch] = lch;
					card->rcard[lch] = cc;
					card->rch[lch] = ch;
					spin_unlock_irqrestore(&card->isdnloop_lock, flags);
					return 0;
				} else {
					spin_unlock_irqrestore(&card->isdnloop_lock, flags);
					/* num matches, but busy */
					if (ch == 1)
						return 1;
				}
			}
		}
		cc = cc->next;
	}
	return 2;
}

/*
 * Depending on D-channel protocol and caller/called, modify
 * phone number.
 *
 * Parameter:
 *   card   = pointer to card struct.
 *   phone  = pointer phone number.
 *   caller = flag: 1 = caller, 0 = called.
 * Return:
 *   pointer to new phone number.
 */
static char *
isdnloop_vstphone(isdnloop_card * card, char *phone, int caller)
{
	int i;
	static char nphone[30];

	if (!card) {
		printk("BUG!!!\n");
		return "";
	}
	switch (card->ptype) {
		case ISDN_PTYPE_EURO:
			if (caller) {
				for (i = 0; i < 2; i++)
					if (!(strcmp(card->s0num[i], phone)))
						return (phone);
				return (card->s0num[0]);
			}
			return (phone);
			break;
		case ISDN_PTYPE_1TR6:
			if (caller) {
				sprintf(nphone, "%s%c", card->s0num[0], phone[0]);
				return (nphone);
			} else
				return (&phone[strlen(phone) - 1]);
			break;
	}
	return "";
}

/*
 * Parse an ICN-type command string sent to the 'card'.
 * Perform misc. actions depending on the command.
 *
 * Parameter:
 *   card = pointer to card struct.
 */
static void
isdnloop_parse_cmd(isdnloop_card * card)
{
	char *p = card->omsg;
	isdn_ctrl cmd;
	char buf[60];
	isdnloop_stat *s = isdnloop_cmd_table;
	int action = -1;
	int i;
	int ch;

	if ((card->omsg[0] != '0') && (card->omsg[2] != ';')) {
		isdnloop_fake_err(card);
		return;
	}
	ch = card->omsg[1] - '0';
	if ((ch < 0) || (ch > 2)) {
		isdnloop_fake_err(card);
		return;
	}
	p += 3;
	while (s->statstr) {
		if (!strncmp(p, s->statstr, strlen(s->statstr))) {
			action = s->action;
			if (s->command && (ch != 0)) {
				isdnloop_fake_err(card);
				return;
			}
			break;
		}
		s++;
	}
	if (action == -1)
		return;
	switch (action) {
		case 1:
			/* 0x;BCON_R */
			if (card->rcard[ch - 1]) {
				isdnloop_fake(card->rcard[ch - 1], "BCON_I",
					      card->rch[ch - 1] + 1);
				isdnloop_fake(card, "BCON_C", ch);
			}
			break;
		case 17:
			/* 0x;BCON_I */
			if (card->rcard[ch - 1]) {
				isdnloop_fake(card->rcard[ch - 1], "BCON_C",
					      card->rch[ch - 1] + 1);
			}
			break;
		case 2:
			/* 0x;BDIS_R */
			isdnloop_fake(card, "BDIS_C", ch);
			if (card->rcard[ch - 1]) {
				isdnloop_fake(card->rcard[ch - 1], "BDIS_I",
					      card->rch[ch - 1] + 1);
			}
			break;
		case 16:
			/* 0x;DCON_R */
			isdnloop_kill_ctimer(card, ch - 1);
			if (card->rcard[ch - 1]) {
				isdnloop_kill_ctimer(card->rcard[ch - 1], card->rch[ch - 1]);
				isdnloop_fake(card->rcard[ch - 1], "DCON_C",
					      card->rch[ch - 1] + 1);
				isdnloop_fake(card, "DCON_C", ch);
			}
			break;
		case 3:
			/* 0x;DDIS_R */
			isdnloop_kill_ctimer(card, ch - 1);
			if (card->rcard[ch - 1]) {
				isdnloop_kill_ctimer(card->rcard[ch - 1], card->rch[ch - 1]);
				isdnloop_fake(card->rcard[ch - 1], "DDIS_I",
					      card->rch[ch - 1] + 1);
				card->rcard[ch - 1] = NULL;
			}
			isdnloop_fake(card, "DDIS_C", ch);
			break;
		case 4:
			/* 0x;DSCA_Rdd,yy,zz,oo */
			if (card->ptype != ISDN_PTYPE_1TR6) {
				isdnloop_fake_err(card);
				return;
			}
			/* Fall through */
		case 5:
			/* 0x;DCAL_Rdd,yy,zz,oo */
			p += 6;
			switch (isdnloop_try_call(card, p, ch - 1, &cmd)) {
				case 0:
					/* Alerting */
					sprintf(buf, "D%s_I%s,%02d,%02d,%s",
					   (action == 4) ? "SCA" : "CAL",
						isdnloop_vstphone(card, cmd.parm.setup.eazmsn, 1),
						cmd.parm.setup.si1,
						cmd.parm.setup.si2,
					isdnloop_vstphone(card->rcard[ch - 1],
					       cmd.parm.setup.phone, 0));
					isdnloop_fake(card->rcard[ch - 1], buf, card->rch[ch - 1] + 1);
					/* Fall through */
				case 3:
					/* si1 does not match, don't alert but start timer */
					isdnloop_start_ctimer(card, ch - 1);
					break;
				case 1:
					/* Remote busy */
					isdnloop_fake(card, "DDIS_I", ch);
					sprintf(buf, "CAU%s", isdnloop_unicause(card, 1, 1));
					isdnloop_fake(card, buf, ch);
					break;
				case 2:
					/* No such user */
					isdnloop_fake(card, "DDIS_I", ch);
					sprintf(buf, "CAU%s", isdnloop_unicause(card, 1, 2));
					isdnloop_fake(card, buf, ch);
					break;
			}
			break;
		case 6:
			/* 0x;EAZC */
			card->eazlist[ch - 1][0] = '\0';
			break;
		case 7:
			/* 0x;EAZ */
			p += 3;
			strcpy(card->eazlist[ch - 1], p);
			break;
		case 8:
			/* 0x;SEEAZ */
			sprintf(buf, "EAZ-LIST: %s", card->eazlist[ch - 1]);
			isdnloop_fake(card, buf, ch + 1);
			break;
		case 9:
			/* 0x;MSN */
			break;
		case 10:
			/* 0x;MSNALL */
			break;
		case 11:
			/* 0x;SETSIL */
			p += 6;
			i = 0;
			while (strchr("0157", *p)) {
				if (i)
					card->sil[ch - 1] |= si2bit[*p - '0'];
				i = (*p++ == '0');
			}
			if (*p)
				isdnloop_fake_err(card);
			break;
		case 12:
			/* 0x;SEESIL */
			sprintf(buf, "SIN-LIST: ");
			p = buf + 10;
			for (i = 0; i < 3; i++)
				if (card->sil[ch - 1] & (1 << i))
					p += sprintf(p, "%02d", bit2si[i]);
			isdnloop_fake(card, buf, ch + 1);
			break;
		case 13:
			/* 0x;SILC */
			card->sil[ch - 1] = 0;
			break;
		case 14:
			/* 00;FV2ON */
			break;
		case 15:
			/* 00;FV2OFF */
			break;
	}
}

/*
 * Put command-strings into the of the 'card'. In reality, execute them
 * right in place by calling isdnloop_parse_cmd(). Also copy every
 * command to the read message ringbuffer, preceeding it with a '>'.
 * These mesagges can be read at /dev/isdnctrl.
 *
 * Parameter:
 *   buf  = pointer to command buffer.
 *   len  = length of buffer data.
 *   user = flag: 1 = called form userlevel, 0 called from kernel.
 *   card = pointer to card struct.
 * Return:
 *   number of bytes transferred (currently always equals len).
 */
static int
isdnloop_writecmd(const u_char * buf, int len, int user, isdnloop_card * card)
{
	int xcount = 0;
	int ocount = 1;
	isdn_ctrl cmd;

	while (len) {
		int count = len;
		u_char *p;
		u_char msg[0x100];

		if (count > 255)
			count = 255;
		if (user) {
			if (copy_from_user(msg, buf, count))
				return -EFAULT;
		} else
			memcpy(msg, buf, count);
		isdnloop_putmsg(card, '>');
		for (p = msg; count > 0; count--, p++) {
			len--;
			xcount++;
			isdnloop_putmsg(card, *p);
			card->omsg[card->optr] = *p;
			if (*p == '\n') {
				card->omsg[card->optr] = '\0';
				card->optr = 0;
				isdnloop_parse_cmd(card);
				if (len) {
					isdnloop_putmsg(card, '>');
					ocount++;
				}
			} else {
				if (card->optr < 59)
					card->optr++;
			}
			ocount++;
		}
	}
	cmd.command = ISDN_STAT_STAVAIL;
	cmd.driver = card->myid;
	cmd.arg = ocount;
	card->interface.statcallb(&cmd);
	return xcount;
}

/*
 * Delete card's pending timers, send STOP to linklevel
 */
static void
isdnloop_stopcard(isdnloop_card * card)
{
	unsigned long flags;
	isdn_ctrl cmd;

	spin_lock_irqsave(&card->isdnloop_lock, flags);
	if (card->flags & ISDNLOOP_FLAGS_RUNNING) {
		card->flags &= ~ISDNLOOP_FLAGS_RUNNING;
		del_timer(&card->st_timer);
		del_timer(&card->rb_timer);
		del_timer(&card->c_timer[0]);
		del_timer(&card->c_timer[1]);
		cmd.command = ISDN_STAT_STOP;
		cmd.driver = card->myid;
		card->interface.statcallb(&cmd);
	}
	spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}

/*
 * Stop all cards before unload.
 */
static void
isdnloop_stopallcards(void)
{
	isdnloop_card *p = cards;

	while (p) {
		isdnloop_stopcard(p);
		p = p->next;
	}
}

/*
 * Start a 'card'. Simulate card's boot message and set the phone
 * number(s) of the virtual 'S0-Interface'. Install D-channel
 * poll timer.
 *
 * Parameter:
 *   card  = pointer to card struct.
 *   sdefp = pointer to struct holding ioctl parameters.
 * Return:
 *   0 on success, -E??? otherwise.
 */
static int
isdnloop_start(isdnloop_card * card, isdnloop_sdef * sdefp)
{
	unsigned long flags;
	isdnloop_sdef sdef;
	int i;

	if (card->flags & ISDNLOOP_FLAGS_RUNNING)
		return -EBUSY;
	if (copy_from_user((char *) &sdef, (char *) sdefp, sizeof(sdef)))
		return -EFAULT;
	spin_lock_irqsave(&card->isdnloop_lock, flags);
	switch (sdef.ptype) {
		case ISDN_PTYPE_EURO:
			if (isdnloop_fake(card, "DRV1.23EC-Q.931-CAPI-CNS-BASIS-20.02.96",
					  -1)) {
				spin_unlock_irqrestore(&card->isdnloop_lock, flags);
				return -ENOMEM;
			}
			card->sil[0] = card->sil[1] = 4;
			if (isdnloop_fake(card, "TEI OK", 0)) {
				spin_unlock_irqrestore(&card->isdnloop_lock, flags);
				return -ENOMEM;
			}
			for (i = 0; i < 3; i++)
				strcpy(card->s0num[i], sdef.num[i]);
			break;
		case ISDN_PTYPE_1TR6:
			if (isdnloop_fake(card, "DRV1.04TC-1TR6-CAPI-CNS-BASIS-29.11.95",
					  -1)) {
				spin_unlock_irqrestore(&card->isdnloop_lock, flags);
				return -ENOMEM;
			}
			card->sil[0] = card->sil[1] = 4;
			if (isdnloop_fake(card, "TEI OK", 0)) {
				spin_unlock_irqrestore(&card->isdnloop_lock, flags);
				return -ENOMEM;
			}
			strcpy(card->s0num[0], sdef.num[0]);
			card->s0num[1][0] = '\0';
			card->s0num[2][0] = '\0';
			break;
		default:
			spin_unlock_irqrestore(&card->isdnloop_lock, flags);
			printk(KERN_WARNING "isdnloop: Illegal D-channel protocol %d\n",
			       sdef.ptype);
			return -EINVAL;
	}
	init_timer(&card->st_timer);
	card->st_timer.expires = jiffies + ISDNLOOP_TIMER_DCREAD;
	card->st_timer.function = isdnloop_polldchan;
	card->st_timer.data = (unsigned long) card;
	add_timer(&card->st_timer);
	card->flags |= ISDNLOOP_FLAGS_RUNNING;
	spin_unlock_irqrestore(&card->isdnloop_lock, flags);
	return 0;
}

/*
 * Main handler for commands sent by linklevel.
 */
static int
isdnloop_command(isdn_ctrl * c, isdnloop_card * card)
{
	ulong a;
	int i;
	char cbuf[60];
	isdn_ctrl cmd;
	isdnloop_cdef cdef;

	switch (c->command) {
		case ISDN_CMD_IOCTL:
			memcpy(&a, c->parm.num, sizeof(ulong));
			switch (c->arg) {
				case ISDNLOOP_IOCTL_DEBUGVAR:
					return (ulong) card;
				case ISDNLOOP_IOCTL_STARTUP:
					if (!access_ok(VERIFY_READ, (void *) a, sizeof(isdnloop_sdef)))
						return -EFAULT;
					return (isdnloop_start(card, (isdnloop_sdef *) a));
					break;
				case ISDNLOOP_IOCTL_ADDCARD:
					if (copy_from_user((char *)&cdef,
							   (char *)a,
							   sizeof(cdef)))
						return -EFAULT;
					return (isdnloop_addcard(cdef.id1));
					break;
				case ISDNLOOP_IOCTL_LEASEDCFG:
					if (a) {
						if (!card->leased) {
							card->leased = 1;
							while (card->ptype == ISDN_PTYPE_UNKNOWN)
								schedule_timeout_interruptible(10);
							schedule_timeout_interruptible(10);
							sprintf(cbuf, "00;FV2ON\n01;EAZ1\n02;EAZ2\n");
							i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
							printk(KERN_INFO
							       "isdnloop: (%s) Leased-line mode enabled\n",
							       CID);
							cmd.command = ISDN_STAT_RUN;
							cmd.driver = card->myid;
							cmd.arg = 0;
							card->interface.statcallb(&cmd);
						}
					} else {
						if (card->leased) {
							card->leased = 0;
							sprintf(cbuf, "00;FV2OFF\n");
							i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
							printk(KERN_INFO
							       "isdnloop: (%s) Leased-line mode disabled\n",
							       CID);
							cmd.command = ISDN_STAT_RUN;
							cmd.driver = card->myid;
							cmd.arg = 0;
							card->interface.statcallb(&cmd);
						}
					}
					return 0;
				default:
					return -EINVAL;
			}
			break;
		case ISDN_CMD_DIAL:
			if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
				return -ENODEV;
			if (card->leased)
				break;
			if ((c->arg & 255) < ISDNLOOP_BCH) {
				char *p;
				char dial[50];
				char dcode[4];

				a = c->arg;
				p = c->parm.setup.phone;
				if (*p == 's' || *p == 'S') {
					/* Dial for SPV */
					p++;
					strcpy(dcode, "SCA");
				} else
					/* Normal Dial */
					strcpy(dcode, "CAL");
				strcpy(dial, p);
				sprintf(cbuf, "%02d;D%s_R%s,%02d,%02d,%s\n", (int) (a + 1),
					dcode, dial, c->parm.setup.si1,
				c->parm.setup.si2, c->parm.setup.eazmsn);
				i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
			}
			break;
		case ISDN_CMD_ACCEPTD:
			if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
				return -ENODEV;
			if (c->arg < ISDNLOOP_BCH) {
				a = c->arg + 1;
				cbuf[0] = 0;
				switch (card->l2_proto[a - 1]) {
					case ISDN_PROTO_L2_X75I:
						sprintf(cbuf, "%02d;BX75\n", (int) a);
						break;
#ifdef CONFIG_ISDN_X25
					case ISDN_PROTO_L2_X25DTE:
						sprintf(cbuf, "%02d;BX2T\n", (int) a);
						break;
					case ISDN_PROTO_L2_X25DCE:
						sprintf(cbuf, "%02d;BX2C\n", (int) a);
						break;
#endif
					case ISDN_PROTO_L2_HDLC:
						sprintf(cbuf, "%02d;BTRA\n", (int) a);
						break;
				}
				if (strlen(cbuf))
					i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
				sprintf(cbuf, "%02d;DCON_R\n", (int) a);
				i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
			}
			break;
		case ISDN_CMD_ACCEPTB:
			if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
				return -ENODEV;
			if (c->arg < ISDNLOOP_BCH) {
				a = c->arg + 1;
				switch (card->l2_proto[a - 1]) {
					case ISDN_PROTO_L2_X75I:
						sprintf(cbuf, "%02d;BCON_R,BX75\n", (int) a);
						break;
#ifdef CONFIG_ISDN_X25
					case ISDN_PROTO_L2_X25DTE:
						sprintf(cbuf, "%02d;BCON_R,BX2T\n", (int) a);
						break;
					case ISDN_PROTO_L2_X25DCE:
						sprintf(cbuf, "%02d;BCON_R,BX2C\n", (int) a);
						break;
#endif
					case ISDN_PROTO_L2_HDLC:
						sprintf(cbuf, "%02d;BCON_R,BTRA\n", (int) a);
						break;
					default:
						sprintf(cbuf, "%02d;BCON_R\n", (int) a);
				}
				printk(KERN_DEBUG "isdnloop writecmd '%s'\n", cbuf);
				i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
				break;
		case ISDN_CMD_HANGUP:
				if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
					return -ENODEV;
				if (c->arg < ISDNLOOP_BCH) {
					a = c->arg + 1;
					sprintf(cbuf, "%02d;BDIS_R\n%02d;DDIS_R\n", (int) a, (int) a);
					i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
				}
				break;
		case ISDN_CMD_SETEAZ:
				if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
					return -ENODEV;
				if (card->leased)
					break;
				if (c->arg < ISDNLOOP_BCH) {
					a = c->arg + 1;
					if (card->ptype == ISDN_PTYPE_EURO) {
						sprintf(cbuf, "%02d;MS%s%s\n", (int) a,
							c->parm.num[0] ? "N" : "ALL", c->parm.num);
					} else
						sprintf(cbuf, "%02d;EAZ%s\n", (int) a,
							c->parm.num[0] ? c->parm.num : (u_char *) "0123456789");
					i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
				}
				break;
		case ISDN_CMD_CLREAZ:
				if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
					return -ENODEV;
				if (card->leased)
					break;
				if (c->arg < ISDNLOOP_BCH) {
					a = c->arg + 1;
					if (card->ptype == ISDN_PTYPE_EURO)
						sprintf(cbuf, "%02d;MSNC\n", (int) a);
					else
						sprintf(cbuf, "%02d;EAZC\n", (int) a);
					i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
				}
				break;
		case ISDN_CMD_SETL2:
				if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
					return -ENODEV;
				if ((c->arg & 255) < ISDNLOOP_BCH) {
					a = c->arg;
					switch (a >> 8) {
						case ISDN_PROTO_L2_X75I:
							sprintf(cbuf, "%02d;BX75\n", (int) (a & 255) + 1);
							break;
#ifdef CONFIG_ISDN_X25
						case ISDN_PROTO_L2_X25DTE:
							sprintf(cbuf, "%02d;BX2T\n", (int) (a & 255) + 1);
							break;
						case ISDN_PROTO_L2_X25DCE:
							sprintf(cbuf, "%02d;BX2C\n", (int) (a & 255) + 1);
							break;
#endif
						case ISDN_PROTO_L2_HDLC:
							sprintf(cbuf, "%02d;BTRA\n", (int) (a & 255) + 1);
							break;
						case ISDN_PROTO_L2_TRANS:
							sprintf(cbuf, "%02d;BTRA\n", (int) (a & 255) + 1);
							break;
						default:
							return -EINVAL;
					}
					i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
					card->l2_proto[a & 255] = (a >> 8);
				}
				break;
		case ISDN_CMD_SETL3:
				if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
					return -ENODEV;
				return 0;
		default:
				return -EINVAL;
			}
	}
	return 0;
}

/*
 * Find card with given driverId
 */
static inline isdnloop_card *
isdnloop_findcard(int driverid)
{
	isdnloop_card *p = cards;

	while (p) {
		if (p->myid == driverid)
			return p;
		p = p->next;
	}
	return (isdnloop_card *) 0;
}

/*
 * Wrapper functions for interface to linklevel
 */
static int
if_command(isdn_ctrl * c)
{
	isdnloop_card *card = isdnloop_findcard(c->driver);

	if (card)
		return (isdnloop_command(c, card));
	printk(KERN_ERR
	       "isdnloop: if_command called with invalid driverId!\n");
	return -ENODEV;
}

static int
if_writecmd(const u_char __user *buf, int len, int id, int channel)
{
	isdnloop_card *card = isdnloop_findcard(id);

	if (card) {
		if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
			return -ENODEV;
		return (isdnloop_writecmd(buf, len, 1, card));
	}
	printk(KERN_ERR
	       "isdnloop: if_writecmd called with invalid driverId!\n");
	return -ENODEV;
}

static int
if_readstatus(u_char __user *buf, int len, int id, int channel)
{
	isdnloop_card *card = isdnloop_findcard(id);

	if (card) {
		if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
			return -ENODEV;
		return (isdnloop_readstatus(buf, len, card));
	}
	printk(KERN_ERR
	       "isdnloop: if_readstatus called with invalid driverId!\n");
	return -ENODEV;
}

static int
if_sendbuf(int id, int channel, int ack, struct sk_buff *skb)
{
	isdnloop_card *card = isdnloop_findcard(id);

	if (card) {
		if (!card->flags & ISDNLOOP_FLAGS_RUNNING)
			return -ENODEV;
		/* ack request stored in skb scratch area */
		*(skb->head) = ack;
		return (isdnloop_sendbuf(channel, skb, card));
	}
	printk(KERN_ERR
	       "isdnloop: if_sendbuf called with invalid driverId!\n");
	return -ENODEV;
}

/*
 * Allocate a new card-struct, initialize it
 * link it into cards-list and register it at linklevel.
 */
static isdnloop_card *
isdnloop_initcard(char *id)
{
	isdnloop_card *card;
	int i;

	if (!(card = (isdnloop_card *) kmalloc(sizeof(isdnloop_card), GFP_KERNEL))) {
		printk(KERN_WARNING
		 "isdnloop: (%s) Could not allocate card-struct.\n", id);
		return (isdnloop_card *) 0;
	}
	memset((char *) card, 0, sizeof(isdnloop_card));
	card->interface.owner = THIS_MODULE;
	card->interface.channels = ISDNLOOP_BCH;
	card->interface.hl_hdrlen  = 1; /* scratch area for storing ack flag*/ 
	card->interface.maxbufsize = 4000;
	card->interface.command = if_command;
	card->interface.writebuf_skb = if_sendbuf;
	card->interface.writecmd = if_writecmd;
	card->interface.readstat = if_readstatus;
	card->interface.features = ISDN_FEATURE_L2_X75I |
#ifdef CONFIG_ISDN_X25
	    ISDN_FEATURE_L2_X25DTE |
	    ISDN_FEATURE_L2_X25DCE |
#endif
	    ISDN_FEATURE_L2_HDLC |
	    ISDN_FEATURE_L3_TRANS |
	    ISDN_FEATURE_P_UNKNOWN;
	card->ptype = ISDN_PTYPE_UNKNOWN;
	strlcpy(card->interface.id, id, sizeof(card->interface.id));
	card->msg_buf_write = card->msg_buf;
	card->msg_buf_read = card->msg_buf;
	card->msg_buf_end = &card->msg_buf[sizeof(card->msg_buf) - 1];
	for (i = 0; i < ISDNLOOP_BCH; i++) {
		card->l2_proto[i] = ISDN_PROTO_L2_X75I;
		skb_queue_head_init(&card->bqueue[i]);
	}
	skb_queue_head_init(&card->dqueue);
	card->isdnloop_lock = SPIN_LOCK_UNLOCKED;
	card->next = cards;
	cards = card;
	if (!register_isdn(&card->interface)) {
		cards = cards->next;
		printk(KERN_WARNING
		       "isdnloop: Unable to register %s\n", id);
		kfree(card);
		return (isdnloop_card *) 0;
	}
	card->myid = card->interface.channels;
	return card;
}

static int
isdnloop_addcard(char *id1)
{
	isdnloop_card *card;

	if (!(card = isdnloop_initcard(id1))) {
		return -EIO;
	}
	printk(KERN_INFO
	       "isdnloop: (%s) virtual card added\n",
	       card->interface.id);
	return 0;
}

static int __init
isdnloop_init(void)
{
	char *p;
	char rev[10];

	if ((p = strchr(revision, ':'))) {
		strcpy(rev, p + 1);
		p = strchr(rev, '$');
		*p = 0;
	} else
		strcpy(rev, " ??? ");
	printk(KERN_NOTICE "isdnloop-ISDN-driver Rev%s\n", rev);

	if (isdnloop_id)
		return (isdnloop_addcard(isdnloop_id));

	return 0;
}

static void __exit
isdnloop_exit(void)
{
	isdn_ctrl cmd;
	isdnloop_card *card = cards;
	isdnloop_card *last;
	int i;

	isdnloop_stopallcards();
	while (card) {
		cmd.command = ISDN_STAT_UNLOAD;
		cmd.driver = card->myid;
		card->interface.statcallb(&cmd);
		for (i = 0; i < ISDNLOOP_BCH; i++)
			isdnloop_free_queue(card, i);
		card = card->next;
	}
	card = cards;
	while (card) {
		last = card;
		skb_queue_purge(&card->dqueue);
		card = card->next;
		kfree(last);
	}
	printk(KERN_NOTICE "isdnloop-ISDN-driver unloaded\n");
}

module_init(isdnloop_init);
module_exit(isdnloop_exit);