[asterisk-addons-commits] dcb: trunk r421 - in /trunk: ./ configs/ doc/

SVN commits to the Asterisk addons project asterisk-addons-commits at lists.digium.com
Tue Aug 7 20:59:40 CDT 2007


Author: dcb
Date: Tue Aug  7 20:59:39 2007
New Revision: 421

URL: http://svn.digium.com/view/asterisk-addons?view=rev&rev=421
Log:
Add support for multiple Bluetooth Adapters and therefore multiple concurrent devices.
Restrict adapters to one device at a time. Multiple devices on an adapter simply does not work.
Note: config file changes as a result of this.

Modified:
    trunk/chan_mobile.c
    trunk/configs/mobile.conf.sample
    trunk/doc/chan_mobile.txt

Modified: trunk/chan_mobile.c
URL: http://svn.digium.com/view/asterisk-addons/trunk/chan_mobile.c?view=diff&rev=421&r1=420&r2=421
==============================================================================
--- trunk/chan_mobile.c (original)
+++ trunk/chan_mobile.c Tue Aug  7 20:59:39 2007
@@ -74,7 +74,6 @@
 static int prefformat = AST_FORMAT_SLINEAR;
 
 static int discovery_interval = 60;	/* The device discovery interval, default 60 seconds. */
-static int sco_socket;			/* This is global so it can be closed on module unload outside of the listener thread */
 static sdp_session_t *sdp_session;
 
 enum mbl_type {
@@ -106,18 +105,32 @@
 	MBL_STATE_OUTSMS2
 };
 
+struct adapter_pvt {
+	int dev_id;						/* device id */
+	int hci_socket;					/* device descriptor */
+	char id[31];					/* the 'name' from mobile.conf */
+	bdaddr_t addr;					/* adddress of adapter */
+	char inuse;						/* are we in use ? */
+	int sco_socket;
+	AST_LIST_ENTRY(adapter_pvt) entry;
+};
+
+static AST_LIST_HEAD_STATIC(adapters, adapter_pvt);
+
 struct mbl_pvt {
 	struct ast_channel *owner;		/* Channel we belong to, possibly NULL */
 	struct ast_frame fr;			/* "null" frame */
-	enum mbl_type type;			/* Phone or Headset */
-	char id[31];				/* The id from mobile.conf */
-	char bdaddr[18];			/* the bdaddr of the device */
-	char context[AST_MAX_CONTEXT];		/* the context for incoming calls */
-	char connected;				/* is it connected? */
-	int rfcomm_port;			/* rfcomm port number */
-	int rfcomm_socket;			/* rfcomm socket descriptor */
+	enum mbl_type type;				/* Phone or Headset */
+	char id[31];					/* The id from mobile.conf */
+	bdaddr_t addr;					/* address of device */
+	struct adapter_pvt *adapter;	/* the adapter we use */
+	char context[AST_MAX_CONTEXT];	/* the context for incoming calls */
+	char connected;					/* is it connected? */
+	int rfcomm_port;				/* rfcomm port number */
+	int rfcomm_socket;				/* rfcomm socket descriptor */
 	char rfcomm_buf[256];
-	int sco_socket;				/* sco socket descriptor */
+	int sco_socket;					/* sco socket descriptor */
+	pthread_t sco_listener_thread;	/* inbound sco listener for this device */	
 	enum mbl_state state;			/* monitor thread current state */
 	pthread_t monitor_thread;		/* monitor thread handle */
 	char sco_in_buf[48 + AST_FRIENDLY_OFFSET];
@@ -126,7 +139,7 @@
 	int sco_out_len;
 	char dial_number[AST_MAX_EXTENSION];	/* number for the monitor thread to dial */
 	int dial_timeout;
-	char ciev_call_0[4];			/* dynamically build reponse strings */
+	char ciev_call_0[4];			/* dynamically built reponse strings */
 	char ciev_call_1[4];
 	char ciev_callsetup_0[4];
 	char ciev_callsetup_1[4];
@@ -148,8 +161,6 @@
 
 /* The discovery thread */
 static pthread_t discovery_thread = AST_PTHREADT_NULL;
-/* The sco listener thread */
-static pthread_t sco_listener_thread = AST_PTHREADT_NULL;
 
 /* CLI stuff */
 static const char show_usage[] =
@@ -167,6 +178,7 @@
 static int do_show_devices(int, int, char **);
 static int do_search_devices(int, int, char **);
 static int do_send_rfcomm(int, int, char **);
+static void *do_sco_listen(void *data);
 
 static struct ast_cli_entry mbl_cli[] = {
 	{{"mobile", "show", "devices", NULL}, do_show_devices, "Show Bluetooth Cell / Mobile devices", show_usage},
@@ -205,7 +217,7 @@
 
 static int rfcomm_write(struct mbl_pvt *pvt, char *buf);
 static int rfcomm_read(struct mbl_pvt *pvt, char *buf, char flush, int timeout);
-static int sco_connect(char *bdaddr);
+static int sco_connect(bdaddr_t src, bdaddr_t dst);
 static int sdp_search(char *addr, int profile);
 
 static const struct ast_channel_tech mbl_tech = {
@@ -228,12 +240,14 @@
 {
 
 	struct mbl_pvt *pvt;
-
-	#define FORMAT "%-15.15s %-17.17s %-9.9s %-5.5s %-3.3s\n"
-
-	ast_cli(fd, FORMAT, "ID", "Address", "Connected", "State", "SMS");
+	char bdaddr[18];
+
+	#define FORMAT "%-15.15s %-17.17s %-15.15s %-9.9s %-5.5s %-3.3s\n"
+
+	ast_cli(fd, FORMAT, "ID", "Address", "Adapter", "Connected", "State", "SMS");
 	AST_LIST_TRAVERSE(&devices, pvt, entry) {
-		ast_cli(fd, FORMAT, pvt->id, pvt->bdaddr, pvt->connected?"Yes":"No", (pvt->state == MBL_STATE_IDLE)?"Free":(pvt->state < MBL_STATE_IDLE)?"Init":"Busy", (pvt->has_sms)?"Yes":"No");
+		ba2str(&pvt->addr, bdaddr);
+		ast_cli(fd, FORMAT, pvt->id, bdaddr, pvt->adapter->id, pvt->connected?"Yes":"No", (pvt->state == MBL_STATE_IDLE)?"Free":(pvt->state < MBL_STATE_IDLE)?"Init":"Busy", (pvt->has_sms)?"Yes":"No");
 	}	
 
 	return RESULT_SUCCESS;
@@ -243,10 +257,10 @@
 static int do_search_devices(int fd, int argc, char **argv)
 {
 
-	int hci_socket;
+	struct adapter_pvt *adapter;
 	inquiry_info *ii = NULL;
 	int max_rsp, num_rsp;
-	int dev_id, len, flags;
+	int len, flags;
 	int i, phport, hsport;
 	char addr[19] = {0};
 	char name[31] = {0};
@@ -254,20 +268,28 @@
 	#define FORMAT2 "%-17.17s %-30.30s %-6.6s %-7.7s %-4.4s\n"
 	#define FORMAT3 "%-17.17s %-30.30s %-6.6s %-7.7s %d\n"
 
-	dev_id = hci_get_route(NULL);
-	hci_socket = hci_open_dev(dev_id);
+	/* find a free adapter */
+	AST_LIST_TRAVERSE(&adapters, adapter, entry) {
+		if (!adapter->inuse)
+			break;
+	}
+	if (!adapter) {
+		ast_cli(fd, "All Bluetooth adapters are in use at this time.\n");
+		return RESULT_SUCCESS;
+	}
+	
 	len  = 8;
 	max_rsp = 255;
 	flags = IREQ_CACHE_FLUSH;
 
 	ii = (inquiry_info*)malloc(max_rsp * sizeof(inquiry_info));
-	num_rsp = hci_inquiry(dev_id, len, max_rsp, NULL, &ii, flags);
+	num_rsp = hci_inquiry(adapter->dev_id, len, max_rsp, NULL, &ii, flags);
 	if (num_rsp > 0) {
 		ast_cli(fd, FORMAT2, "Address", "Name", "Usable", "Type", "Port");
 		for (i = 0; i < num_rsp; i++) {
 			ba2str(&(ii+i)->bdaddr, addr);
 			name[0] = 0x00;
-			if (hci_read_remote_name(hci_socket, &(ii+i)->bdaddr, sizeof(name)-1, name, 0) < 0)
+			if (hci_read_remote_name(adapter->hci_socket, &(ii+i)->bdaddr, sizeof(name)-1, name, 0) < 0)
 				strcpy(name, "[unknown]");
 			phport = sdp_search(addr, HANDSFREE_AGW_PROFILE_ID);
 			if (!phport)
@@ -281,8 +303,6 @@
 
 	free(ii);
 
-	hci_close_dev(hci_socket);
-
 	return RESULT_SUCCESS;
 
 }
@@ -366,7 +386,7 @@
 		if (dest && (dest[0] != 0x00)) {
 			message = args;
 			if (!message || (message[0] == 0x00)) {
-				ast_log(LOG_ERROR,"NULL Message to be sent-- SMS will not be sent.\n");
+				ast_log(LOG_ERROR,"NULL Message to be sent -- SMS will not be sent.\n");
 				return -1;
 			}
 		} else {
@@ -391,17 +411,17 @@
 	}
 	
 	if (!pvt->connected) {
-		ast_log(LOG_ERROR,"bluetooth device %s wasn't connected -- SMS will not be sent.\n",device);
+		ast_log(LOG_ERROR,"Bluetooth device %s wasn't connected -- SMS will not be sent.\n",device);
 		return -1;
 	}
 	
 	if (!pvt->has_sms) {
-		ast_log(LOG_ERROR,"bluetooth device %s doesn't handle SMS -- SMS will not be sent.\n",device);
+		ast_log(LOG_ERROR,"Bluetooth device %s doesn't handle SMS -- SMS will not be sent.\n",device);
 		return -1;
 	}
 	
 	if (pvt->state != MBL_STATE_IDLE) {
-		ast_log(LOG_ERROR,"bluetooth device %s isn't IDLE -- SMS will not be sent.\n",device);
+		ast_log(LOG_ERROR,"Bluetooth device %s isn't IDLE -- SMS will not be sent.\n",device);
 		return -1;
 	}
 	
@@ -422,7 +442,7 @@
 	char *dest_dev = NULL;
 	char *dest_num = NULL;
 	int oldformat;
-
+	
 	if (!data) {
 		ast_log(LOG_WARNING, "Channel requested with no data\n");
 		*cause = AST_CAUSE_INCOMPATIBLE_DESTINATION;
@@ -461,9 +481,6 @@
 		return NULL;
 	}
 
-	pvt->sco_out_ptr = pvt->sco_out_buf;
-	pvt->sco_out_len = 0;
-
 	chn = mbl_new(AST_STATE_DOWN, pvt, NULL);
 	if (!chn) {
 		ast_log(LOG_WARNING, "Unable to allocate channel structure\n");
@@ -481,10 +498,11 @@
 	struct mbl_pvt *pvt;
 	char *dest_dev = NULL;
 	char *dest_num = NULL;
-
+	
 	dest_dev = ast_strdupa((char *)dest);
 
 	pvt = ast->tech_pvt;
+	ast_mutex_lock(&pvt->owner->lock);
 
 	if (pvt->type == MBL_TYPE_PHONE) {
 		dest_num = strchr(dest_dev, '/');
@@ -497,6 +515,7 @@
 
 	if ((ast->_state != AST_STATE_DOWN) && (ast->_state != AST_STATE_RESERVED)) {
 		ast_log(LOG_WARNING, "mbl_call called on %s, neither down nor reserved\n", ast->name);
+		ast_mutex_unlock(&pvt->owner->lock);
 		return -1;
 	}
 
@@ -510,7 +529,8 @@
 		pvt->state = MBL_STATE_RING;
 	}
 
-
+	ast_mutex_unlock(&pvt->owner->lock);
+	
 	return 0;
 
 }
@@ -519,7 +539,7 @@
 {
 
 	struct mbl_pvt *pvt;
-
+	
 	if (!ast->tech_pvt) {
 		ast_log(LOG_WARNING, "Asked to hangup channel not connected\n");
 		return 0;
@@ -556,7 +576,7 @@
 {
 
 	struct mbl_pvt *pvt;
-
+	
 	pvt = ast->tech_pvt;
 
 	rfcomm_write(pvt, "ATA\r");
@@ -630,7 +650,7 @@
 	struct mbl_pvt *pvt = ast->tech_pvt;
 	int r;
 	struct ast_frame *f;
-
+	
 	if (!pvt->owner) {
 		return &ast_null_frame;
 	}
@@ -729,7 +749,7 @@
 {
 
 	struct mbl_pvt *pvt = oldchan->tech_pvt;
-
+	
 	if (pvt && pvt->owner == oldchan)
 		pvt->owner = newchan;
 
@@ -743,7 +763,7 @@
 	char *device;
 	int res = AST_DEVICE_INVALID;
 	struct mbl_pvt *pvt;
-
+	
 	device = ast_strdupa(S_OR(data, ""));
 
 	ast_debug(1, "Checking device state for device %s\n", device);
@@ -771,7 +791,21 @@
 
 	struct ast_channel *chn;
 
-	chn = ast_channel_alloc(1, state, 0, 0, 0, 0, 0, 0, "Mobile/%s-%04lx", pvt->id, ast_random() & 0xffff);
+	pvt->fr.frametype = AST_FRAME_VOICE;
+	pvt->fr.subclass = AST_FORMAT_SLINEAR;
+	pvt->fr.datalen = 48;
+	pvt->fr.samples = 24;
+	pvt->fr.src = "Mobile";
+	pvt->fr.offset = AST_FRIENDLY_OFFSET;
+	pvt->fr.mallocd = 0;
+	pvt->fr.delivery.tv_sec = 0;
+	pvt->fr.delivery.tv_usec = 0;
+	pvt->fr.data = pvt->sco_in_buf + AST_FRIENDLY_OFFSET;
+	pvt->sco_out_ptr = pvt->sco_out_buf;
+	pvt->sco_out_len = 0;
+	pvt->sent_answer = 0;
+	
+	chn = ast_channel_alloc(1, state, cid_num, pvt->id, 0, 0, pvt->context, 0, "Mobile/%s-%04lx", pvt->id, ast_random() & 0xffff);
 	if (chn) {
 		chn->tech = &mbl_tech;
 		chn->nativeformats = prefformat;
@@ -780,43 +814,36 @@
 		chn->writeformat = prefformat;
 		chn->readformat = prefformat;
 		chn->readq.first = NULL;
-		pvt->fr.frametype = AST_FRAME_VOICE;
-		pvt->fr.subclass = AST_FORMAT_SLINEAR;
-		pvt->fr.datalen = 48;
-		pvt->fr.samples = 24;
-		pvt->fr.src = "Mobile";
-		pvt->fr.offset = AST_FRIENDLY_OFFSET;
-		pvt->fr.mallocd = 0;
-		pvt->fr.delivery.tv_sec = 0;
-		pvt->fr.delivery.tv_usec = 0;
-		pvt->fr.data = pvt->sco_in_buf + AST_FRIENDLY_OFFSET;
 		chn->tech_pvt = pvt;
 		if (state == AST_STATE_RING)
 			chn->rings = 1;
-		ast_copy_string(chn->context, pvt->context, sizeof(chn->context));
-		ast_copy_string(chn->exten, "s", sizeof(chn->exten));
 		ast_string_field_set(chn, language, "en");
-		if (cid_num)
-			chn->cid.cid_num = ast_strdup(cid_num);
-		chn->cid.cid_name = ast_strdup(pvt->id);
 		pvt->owner = chn;
 
-	}
-
-	return chn;
-
-}
-
-static int rfcomm_connect(char *bdaddr, int remote_channel) {
-
-	bdaddr_t dst;
+		return chn;
+	}
+
+	return NULL;
+
+}
+
+static int rfcomm_connect(bdaddr_t src, bdaddr_t dst, int remote_channel) {
+
 	struct sockaddr_rc addr;
 	int s;
-
-	str2ba(bdaddr, &dst);
-
+	
 	if ((s = socket(PF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM)) < 0) {
 		ast_debug(1, "socket() failed (%d).\n", errno);
+		return -1;
+	}
+
+	memset(&addr, 0, sizeof(addr));
+    addr.rc_family = AF_BLUETOOTH;
+    bacpy(&addr.rc_bdaddr, &src);
+    addr.rc_channel = (uint8_t) 1;
+    if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+		ast_debug(1, "bind() failed (%d).\n", errno);
+		close(s);
 		return -1;
 	}
 
@@ -923,17 +950,23 @@
 
 }
 
-static int sco_connect(char *bdaddr)
-{
-
-	bdaddr_t dst;
+static int sco_connect(bdaddr_t src, bdaddr_t dst)
+{
+
 	struct sockaddr_sco addr;
 	int s;
 
-	str2ba(bdaddr, &dst);
-
 	if ((s = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO)) < 0) {
 		ast_debug(1, "socket() failed (%d).\n", errno);
+		return -1;
+	}
+
+	memset(&addr, 0, sizeof(addr));
+    addr.sco_family = AF_BLUETOOTH;
+    bacpy(&addr.sco_bdaddr, &src);
+    if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+		ast_debug(1, "bind() failed (%d).\n", errno);
+		close(s);
 		return -1;
 	}
 
@@ -1211,20 +1244,20 @@
 					pvt->state = MBL_STATE_PREIDLE;
 				} else if (strstr(buf, "ERROR")) {
 					pvt->has_sms = 0;
-					ast_log(LOG_NOTICE,"Device %s has no bluetooth SMS capability.\n", pvt->id);
+					ast_log(LOG_NOTICE, "Device %s has no bluetooth SMS capability.\n", pvt->id);
 					pvt->state = MBL_STATE_PREIDLE;
 				}
 				break;
 			case MBL_STATE_PREIDLE: /* Nothing handled here, wait for timeout, then off we go... */
 				break;
 			case MBL_STATE_IDLE:
-				ast_debug(1, "Device %s %s [%s]\n", pvt->bdaddr, pvt->id, buf);
+				ast_debug(1, "Device %s [%s]\n", pvt->id, buf);
 				if (strstr(buf, "RING")) {
 					pvt->state = MBL_STATE_RING;
 				} else if (strstr(buf, "+CIEV:")) {
 					if (strstr(buf, pvt->ciev_callsetup_3)) {	/* User has dialed out on handset */
-						monitor = 0;				/* We disconnect now, as he is    */
-					}						/* probably leaving BT range...   */
+						monitor = 0;							/* We disconnect now, as he is    */
+					}											/* probably leaving BT range...   */
 				}
 				break;
 			case MBL_STATE_DIAL: /* Nothing handled here, we need to wait for a timeout */
@@ -1259,9 +1292,6 @@
 							cid_num[pcide - pcids - 1] = 0x00;
 						}
 					}
-					pvt->sco_out_ptr = pvt->sco_out_buf;
-					pvt->sco_out_len = 0;
-					pvt->sent_answer = 0;
 					chn = mbl_new(AST_STATE_RING, pvt, cid_num);
 					if (chn) {
 						if (ast_pbx_start(chn)) {
@@ -1277,9 +1307,6 @@
 				}
 				break;
 			case MBL_STATE_RING2:
-				pvt->sco_out_ptr = pvt->sco_out_buf;
-				pvt->sco_out_len = 0;
-				pvt->sent_answer = 0;
 				chn = mbl_new(AST_STATE_RING, pvt, cid_num);
 				if (chn) {
 					if (ast_pbx_start(chn)) {
@@ -1378,8 +1405,8 @@
 				pvt->state++;
 				rfcomm_write(pvt, "AT+CMER=3,0,0,1\r");
  			} else if (pvt->state == MBL_STATE_INIT3) { /* Some devices dont respond to AT+CMER=3,0,0,1 properly. VK 2020 for example */
-                                 pvt->state++;
-                                 rfcomm_write(pvt, "AT+CLIP=1\r");
+				pvt->state++;
+				rfcomm_write(pvt, "AT+CLIP=1\r");
 			} else if (pvt->state == MBL_STATE_PREIDLE) {
 				pvt->connected = 1;
 				ast_verbose(VERBOSE_PREFIX_3 "Bluetooth Device %s initialised and ready.\n", pvt->id);
@@ -1437,7 +1464,14 @@
 	pvt->sco_socket = -1;
 	pvt->connected = 0;
 	pvt->monitor_thread = AST_PTHREADT_NULL;
-
+	
+	pthread_cancel(pvt->sco_listener_thread);
+	pthread_join(pvt->sco_listener_thread, NULL);
+	
+	close(pvt->adapter->sco_socket);
+	
+	pvt->adapter->inuse = 0;
+	
 	return NULL;
 
 }
@@ -1477,7 +1511,6 @@
 				if (strstr(buf, "AT+CKPD=")) {
 					ast_channel_lock(pvt->owner);
 					pvt->owner->fds[0] = pvt->sco_socket;
-					ast_log(LOG_NOTICE,"pvt-sco_socket used for fds in headphone code\n");
 					ast_channel_unlock(pvt->owner);
 					ast_queue_control(pvt->owner, AST_CONTROL_ANSWER);
 					pvt->state = MBL_STATE_INCOMING;
@@ -1504,9 +1537,8 @@
 				ast_verbose(VERBOSE_PREFIX_3 "Bluetooth Device %s initialised and ready.\n", pvt->id);
 				pvt->state = MBL_STATE_IDLE;
 			} else if (pvt->state == MBL_STATE_RING) {
-				pvt->sco_socket = sco_connect(pvt->bdaddr);
+				pvt->sco_socket = sco_connect(pvt->adapter->addr, pvt->addr);
 				if (pvt->sco_socket > -1) {
-					ast_log(LOG_NOTICE,"sco_connect returned -1 in state RING\n");
 					ast_setstate(pvt->owner, AST_STATE_RINGING);
 					ast_queue_control(pvt->owner, AST_CONTROL_RINGING);
 					pvt->state = MBL_STATE_RING2;
@@ -1524,6 +1556,15 @@
 
 	}
 
+	close(pvt->rfcomm_socket);
+	close(pvt->sco_socket);
+	pvt->sco_socket = -1;
+	pvt->connected = 0;
+	pvt->monitor_thread = AST_PTHREADT_NULL;
+	
+
+	pvt->adapter->inuse = 0;
+	
 	return NULL;
 
 }
@@ -1536,6 +1577,11 @@
 			pvt->monitor_thread = AST_PTHREADT_NULL;
 			return 0;
 		}
+		/* we are a phone, so spin the sco listener on the adapter as well */
+		if (ast_pthread_create_background(&pvt->sco_listener_thread, NULL, do_sco_listen, pvt->adapter) < 0) {
+			ast_log(LOG_ERROR, "Unable to create sco listener thread for device %s.\n", pvt->id);
+		}
+		
 	} else {
 		if (ast_pthread_create_background(&pvt->monitor_thread, NULL, do_monitor_headset, pvt) < 0) {
 			pvt->monitor_thread = AST_PTHREADT_NULL;
@@ -1562,17 +1608,23 @@
 static void *do_discovery(void *data)
 {
 
-	struct mbl_pvt *pvt = data;
+	struct adapter_pvt *adapter;
+	struct mbl_pvt *pvt;
 
 	for (;;) {
-		AST_LIST_TRAVERSE(&devices, pvt, entry) {
-			if (!pvt->connected) {
-				if ((pvt->rfcomm_socket = rfcomm_connect(pvt->bdaddr, pvt->rfcomm_port)) > -1) {
-					pvt->state = 0;
-					if (start_monitor(pvt)) {
-						pvt->connected = 1;
-						if (option_verbose > 2)
-							ast_verbose(VERBOSE_PREFIX_3 "Bluetooth Device %s has connected.\n", pvt->id);
+		AST_LIST_TRAVERSE(&adapters, adapter, entry) {
+			if (!adapter->inuse) {
+				AST_LIST_TRAVERSE(&devices, pvt, entry) {
+					if (!pvt->connected && !strcmp(adapter->id, pvt->adapter->id)) {
+						if ((pvt->rfcomm_socket = rfcomm_connect(adapter->addr, pvt->addr, pvt->rfcomm_port)) > -1) {
+							pvt->state = 0;
+							if (start_monitor(pvt)) {
+								pvt->connected = 1;
+								adapter->inuse = 1;
+								if (option_verbose > 2)
+									ast_verbose(VERBOSE_PREFIX_3 "Bluetooth Device %s has connected.\n", pvt->id);
+							}
+						}
 					}
 				}
 			}
@@ -1600,52 +1652,51 @@
 	int ns;
 	bdaddr_t local;
 	struct sockaddr_sco addr;
+	char saddr[18];
 	struct sco_options so;
 	socklen_t len;
 	int opt = 1;
-	char saddr[18];
 	socklen_t addrlen;
 	struct mbl_pvt *pvt;
-
-	hci_devba(0, &local);
-
-	if ((sco_socket = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO)) < 0) {
+	struct adapter_pvt *adapter = (struct adapter_pvt *) data;
+
+	if ((adapter->sco_socket = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO)) < 0) {
 		ast_log(LOG_ERROR, "Unable to create sco listener socket.\n");
 		return NULL;
 	}
 	memset(&addr, 0, sizeof(addr));
 	addr.sco_family = AF_BLUETOOTH;
-	bacpy(&addr.sco_bdaddr, &local);
-	if (bind(sco_socket, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+	bacpy(&addr.sco_bdaddr, &adapter->addr);
+	if (bind(adapter->sco_socket, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
 		ast_log(LOG_ERROR, "Unable to bind sco listener socket. (%d)\n", errno);
-		close(sco_socket);
+		close(adapter->sco_socket);
 		return NULL;
 	}
-	if (setsockopt(sco_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
+	if (setsockopt(adapter->sco_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
 		ast_log(LOG_ERROR, "Unable to setsockopt sco listener socket.\n");
-		close(sco_socket);
+		close(adapter->sco_socket);
 		return NULL;
 	}
-	if (listen(sco_socket, 5) < 0) {
+	if (listen(adapter->sco_socket, 5) < 0) {
 		ast_log(LOG_ERROR, "Unable to listen sco listener socket.\n");
-		close(sco_socket);
+		close(adapter->sco_socket);
 		return NULL;
 	}
 	while (1) {
-		addrlen = sizeof(struct sockaddr);
+		addrlen = sizeof(struct sockaddr_sco);
 		ast_log(LOG_NOTICE, "About to accept the sco_socket...\n");
-		if ((ns = accept(sco_socket, (struct sockaddr *)&addr, &addrlen)) > -1) {
+		if ((ns = accept(adapter->sco_socket, (struct sockaddr *)&addr, &addrlen)) > -1) {
 			ast_log(LOG_NOTICE, "sco_socket returns %d...\n",ns);
-			ba2str(&addr.sco_bdaddr, saddr);
 
 			len = sizeof(so);
 			getsockopt(ns, SOL_SCO, SCO_OPTIONS, &so, &len);
 
+			ba2str(&addr.sco_bdaddr, saddr);
 			ast_debug(1, "Incoming Audio Connection from device %s MTU is %d\n", saddr, so.mtu);
 
 			pvt = NULL;
 			AST_LIST_TRAVERSE(&devices, pvt, entry) {
-				if (!strcmp(pvt->bdaddr, saddr))
+				if (!bacmp(&pvt->addr, &addr.sco_bdaddr)) 
 					break;
 			}
 			if (pvt) {
@@ -1674,8 +1725,11 @@
 	struct ast_config *cfg = NULL;
 	char *cat = NULL;
 	struct ast_variable *var;
-	const char *address, *port, *context, *type, *skip;
+	const char *id, *address, *useadapter, *port, *context, *type, *skip;
 	struct mbl_pvt *pvt;
+	struct adapter_pvt *adapter;
+	uint16_t vs;
+	char nadapters = 0;
 
 	cfg = ast_config_load(MBL_CONFIG);
 	if (!cfg)
@@ -1686,24 +1740,77 @@
 			discovery_interval = atoi(var->value);
 	}
 
+	/* load adapters first */
 	cat = ast_category_browse(cfg, NULL);
 	while (cat) {
-		if (strcasecmp(cat, "general")) {
+		if (!strcasecmp(cat, "adapter")) {
+			id = ast_variable_retrieve(cfg, cat, "id");
+			address = ast_variable_retrieve(cfg, cat, "address");
+			ast_debug(1, "Loading adapter %s %s.\n", id, address);
+			if (id && address) {
+				if ((adapter = ast_malloc(sizeof(struct adapter_pvt)))) {
+					ast_copy_string(adapter->id, id, sizeof(adapter->id));
+					str2ba(address, &adapter->addr);
+					adapter->inuse = 0;
+					adapter->dev_id = hci_devid(address);
+					adapter->hci_socket = hci_open_dev(adapter->dev_id);
+										
+					if (adapter->dev_id < 0 || adapter->hci_socket < 0) {
+						ast_log(LOG_ERROR, "Unable to open adapter %s. It wont be enabled.\n", adapter->id);
+						free(adapter);
+					} else {
+						hci_read_voice_setting(adapter->hci_socket, &vs, 1000);
+						vs = htobs(vs);
+						if (vs != 0x0060) {
+							ast_log(LOG_ERROR, "Incorrect voice setting for adapter %s, it must be 0x0060 - see 'man hciconfig' for details.\n", adapter->id);
+							hci_close_dev(adapter->hci_socket);
+							free(adapter);
+						} else {
+							AST_LIST_INSERT_HEAD(&adapters, adapter, entry);
+							nadapters++;
+						}
+					}
+				}
+			} else
+				ast_log(LOG_ERROR, "id/address missing for adapter %s. It wont be enabled.\n", cat);
+		}
+		cat = ast_category_browse(cfg, cat);	
+	}
+	
+	if (!nadapters) {
+		ast_log(LOG_WARNING, "***********************************************************************\n");
+		ast_log(LOG_WARNING, "No Adapters defined. Please review mobile.conf. See sample for details.\n");
+		ast_log(LOG_WARNING, "***********************************************************************\n");
+	}
+	
+	/* now load devices */
+	cat = ast_category_browse(cfg, NULL);
+	while (cat) {
+		if (strcasecmp(cat, "general") && strcasecmp(cat, "adapter")) {
 			ast_debug(1, "Loading device %s.\n", cat);
 			address = ast_variable_retrieve(cfg, cat, "address");
+			useadapter = ast_variable_retrieve(cfg, cat, "adapter");
 			port = ast_variable_retrieve(cfg, cat, "port");
 			context = ast_variable_retrieve(cfg, cat, "context");
-			ast_log(LOG_NOTICE, "context for non-general category %s was %s\n", cat, context);
 			type = ast_variable_retrieve(cfg, cat, "type");
 			skip = ast_variable_retrieve(cfg, cat, "dtmfskip");
-			if (address && port) {
+			if (address && port && useadapter) {
+				/* find the adapter */
+				AST_LIST_TRAVERSE(&adapters, adapter, entry) {
+					if (!strcmp(adapter->id, useadapter))
+						break;
+				}
+				if (!adapter) {
+					ast_log(LOG_ERROR, "Device %s configured to use unknown adapter %s. It wont be enabled.\n", cat, useadapter);
+					break;
+				}
 				if ((pvt = ast_malloc(sizeof(struct mbl_pvt)))) {
 					if (type && !strcmp(type, "headset"))
 						pvt->type = MBL_TYPE_HEADSET;
 					else
 						pvt->type = MBL_TYPE_PHONE;
 					ast_copy_string(pvt->id, cat, sizeof(pvt->id));
-					ast_copy_string(pvt->bdaddr, address, sizeof(pvt->bdaddr));
+					str2ba(address, &pvt->addr);
 					ast_copy_string(pvt->context, S_OR(context, "default"), sizeof(pvt->context));
 					pvt->connected = 0;
 					pvt->state = MBL_STATE_INIT;
@@ -1724,10 +1831,11 @@
 					pvt->skip_frames = 0;
 					ast_dsp_set_features(pvt->dsp, DSP_FEATURE_DTMF_DETECT);
 					ast_dsp_digitmode(pvt->dsp, DSP_DIGITMODE_DTMF | DSP_DIGITMODE_RELAXDTMF);
+					pvt->adapter = adapter;
 					AST_LIST_INSERT_HEAD(&devices, pvt, entry);
 				}
 			} else {
-				ast_log(LOG_ERROR, "Device %s has no address/port configured. It wont be enabled.\n", cat);
+				ast_log(LOG_ERROR, "Device %s has no address/port/adapter configured. It wont be enabled.\n", cat);
 			}
 		}
 		cat = ast_category_browse(cfg, cat);
@@ -1750,6 +1858,7 @@
 {
 
 	struct mbl_pvt *pvt;
+	struct adapter_pvt *adapter;
 
 	/* First, take us out of the channel loop */
 	ast_channel_unregister(&mbl_tech);
@@ -1759,14 +1868,7 @@
 		pthread_cancel(discovery_thread);
 		pthread_join(discovery_thread, NULL);
 	}
-	/* Kill the sco listener thread */
-	if (sco_listener_thread != AST_PTHREADT_NULL) {
-		pthread_cancel(sco_listener_thread);
-		pthread_join(sco_listener_thread, NULL);
-	}
-	if ((close(sco_socket) == -1))
-		ast_log(LOG_ERROR, "Unable to close sco_socket %d.\n", errno);
-
+	
 	/* Unregister the CLI & APP */
 	ast_cli_unregister_multiple(mbl_cli, sizeof(mbl_cli) / sizeof(mbl_cli[0]));
 	ast_unregister_application(app_mblstatus);
@@ -1787,6 +1889,12 @@
 		ast_dsp_free(pvt->dsp);
 		free(pvt);
 	}
+	
+	/* Destroy the adapter list */
+	while ((adapter = AST_LIST_REMOVE_HEAD(&adapters, entry))) {
+		hci_close_dev(adapter->hci_socket);
+		free(adapter);
+	}
 
 	if (sdp_session)
 		sdp_close(sdp_session);
@@ -1799,7 +1907,7 @@
 {
 
 	int dev_id, s;
-	uint16_t vs;
+	struct adapter_pvt *adapter;
 
 	/* Check if we have Bluetooth, no point loading otherwise... */
 	dev_id = hci_get_route(NULL);
@@ -1809,14 +1917,6 @@
 		return AST_MODULE_LOAD_DECLINE;
 	}
 
-	hci_read_voice_setting(s, &vs, 1000);
-	vs = htobs(vs);
-	if (vs != 0x0060) {
-		ast_log(LOG_ERROR, "Bluetooth voice setting must be 0x0060 - see hciconfig hci0 voice.\n");
-		hci_close_dev(s);
-		return AST_MODULE_LOAD_DECLINE;
-	}
-
 	hci_close_dev(s);
 
 	if (!mbl_load_config()) {
@@ -1831,14 +1931,7 @@
 		ast_log(LOG_ERROR, "Unable to create discovery thread.\n");
 		return AST_MODULE_LOAD_DECLINE;
 	}
-	/* Spin the sco listener thread */
-	if (ast_pthread_create_background(&sco_listener_thread, NULL, do_sco_listen, NULL) < 0) {
-		ast_log(LOG_ERROR, "Unable to create sco listener thread.\n");
-		pthread_cancel(discovery_thread);
-		pthread_join(discovery_thread, NULL);
-		return AST_MODULE_LOAD_DECLINE;
-	}
-
+	
 	ast_cli_register_multiple(mbl_cli, sizeof(mbl_cli) / sizeof(mbl_cli[0]));
 	ast_register_application(app_mblstatus, mbl_status_exec, mblstatus_synopsis, mblstatus_desc);
 	ast_register_application(app_mblsendsms, mbl_sendsms_exec, mblsendsms_synopsis, mblsendsms_desc);
@@ -1855,5 +1948,5 @@
 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Bluetooth Mobile Device Channel Driver",
 		.load = load_module,
 		.unload = unload_module,
-		.reload = reload_module,
+		.reload = reload_module
 );

Modified: trunk/configs/mobile.conf.sample
URL: http://svn.digium.com/view/asterisk-addons/trunk/configs/mobile.conf.sample?view=diff&rev=421&r1=420&r2=421
==============================================================================
--- trunk/configs/mobile.conf.sample (original)
+++ trunk/configs/mobile.conf.sample Tue Aug  7 20:59:39 2007
@@ -1,30 +1,53 @@
 ;
 ; mobile.conf
+; configuration file for chan_mobile
 ;
 
 [general]
-interval=60		; Number of seconds between trying to connect to devices. 
+interval=10		; Number of seconds between trying to connect to devices. 
+
+; The following is a list of adapters we use.
+; id must be unique and address is the bdaddr of the adapter from hciconfig.
+; Each adapter may only have one device (headset or phone) connected at a time.
+; Add an [adapter] entry for each adapter you have.
+
+[adapter]
+id=blue
+address=00:09:DD:60:01:A3
+
+[adapter]
+id=dlink
+address=00:80:C8:35:52:78
 
 ; The following is a list of the devices we deal with.
 ; Every device listed below will be available for calls in and out of Asterisk. 
-; Discovered devices not in this list are not available.
+; Each device needs an adapter=xxxx entry which determines which bluetooth adapter is used.
 ; Use the CLI command 'mobile search' to discover devices.
 ; Use the CLI command 'mobile show devices' to see device status.
 ;
-; To place out through a cell phone use Dial(Mobile/[device]/NNN.....) in your dialplan.
+; To place a call out through a mobile phone use Dial(Mobile/[device]/NNN.....) in your dialplan.
 ; To call a headset use Dial(Mobile/[device]).
 
-;[dave]
-;address=00:12:56:90:6E:00
-;port=4
-;context=incoming-mobile
+[LGTU550]
+address=00:E0:91:7F:46:44	; the address of the phone
+port=4				; the rfcomm port number (from mobile search)
+context=incoming-mobile		; dialplan context for incoming calls
+adapter=dlink			; adapter to use
 
-;[blackberry]
-;address=00:0F:86:0E:AE:42
-;port=2
-;context=incoming-mobile
+[6310i]
+address=00:60:57:32:7E:B1
+port=13
+context=incoming-mobile
+adapter=dlink
 
-;[headset]
-;address=00:0B:9E:11:74:A5
-;port=1
-;type=headset
+[headset]
+address=00:0B:9E:11:AE:C6
+port=1
+type=headset			; This is a headset, not a Phone !
+adapter=blue
+
+[headset1]
+address=00:0B:9E:11:74:A5
+port=1
+type=headset
+adapter=dlink

Modified: trunk/doc/chan_mobile.txt
URL: http://svn.digium.com/view/asterisk-addons/trunk/doc/chan_mobile.txt?view=diff&rev=421&r1=420&r2=421
==============================================================================
--- trunk/doc/chan_mobile.txt (original)
+++ trunk/doc/chan_mobile.txt Tue Aug  7 20:59:39 2007
@@ -2,24 +2,27 @@
 
 Asterisk Channel Driver to allow Bluetooth Cell/Mobile Phones to be used as FXO devices, and Headsets as FXS devices.
 
+
 Features :-
 
-Multiple cell phones can be connected.
+Multiple Bluetooth Adapters supported.
+Multiple phones can be connected.
 Multiple headsets can be connected.
-Asterisk automatically connects to each configured cell phone / headset when it comes in range.
+Asterisk automatically connects to each configured mobile phone / headset when it comes in range.
 CLI command to discover bluetooth devices.
-Inbound calls on the cell network to the cell phones are handled by Asterisk, just like inbound calls on a Zap channel.
+Inbound calls on the mobile network to the mobile phones are handled by Asterisk, just like inbound calls on a Zap channel.
 CLI passed through on inbound calls.
-Dial outbound on a cell phone using Dial(Mobile/device/nnnnnnn) in the dialplan.
+Dial outbound on a mobile phone using Dial(Mobile/device/nnnnnnn) in the dialplan.
 Dial a headset using Dial(Mobile/device) in the dialplan.
-Application MobileStatus can be used in the dialplan to see if a cell phone / headset is connected.
+Application MobileStatus can be used in the dialplan to see if a mobile phone / headset is connected.
 Supports devicestate for dialplan hinting.
 Supports Inbound and Outbound SMS.
 
-Using chan_mobile :-
+
+Requirements :-
 
 In order to use chan_mobile, you must have a working bluetooth subsystem on your Asterisk box.
-This means a working bluetooth adapter, and the BlueZ packages.
+This means one or more working bluetooth adapters, and the BlueZ packages.
 
 Any bluetooth adapter supported by the Linux kernel will do, including usb bluetooth dongles.
 
@@ -27,20 +30,40 @@
 You also need libbluetooth, and libbluetooth-dev if you are compiling Asterisk from source.
 
 You need to get bluetooth working with your phone before attempting to use chan_mobile.
-This means 'pairing' your phone with your Asterisk box. I dont describe how to do this here as the process
-differs from distro to distro. You only need to pair once.
-
-However, the easist way to pair, is to use you cell phone to search for bluetooth devices, select your Asterisk box
-and enter the requested PIN.
-
-See www.bluez.org for other details about setting up Bluetooth under Linux.
-
-Assuming you have bluetooth working ok:-
-
-Load chan_mobile.so
+This means 'pairing' your phone or headset with your Asterisk box. I dont describe how to do this here as the process
+differs from distro to distro. You only need to pair once per adapter.
+
+See www.bluez.org for details about setting up Bluetooth under Linux.
+
+
+Concepts :-
+
+chan_mobile deals with both bluetooth adapters and bluetooth devices. This means you need to tell chan_mobile about the
+bluetooth adapters installed in your server as well as the devices (phones / headsets) you wish to use.
+
+chan_mobile currently only allows one device (phone or headset) to be connected to an adapter at a time. This means you need
+one adapter for each device you wish to use simultaneously. Much effort has gone into trying to make multiple devices per adapter
+work, but in short it doesnt.
+
+Periodically chan_mobile looks at each configured adapter, and if it is not in use (i.e. no device connected) will initiate a
+search for devices configured to use this adapater that may be in range. If it finds one it will connect the device and it
+will be available for Asterisk to use. When the device goes out of range, chan_mobile will disconnect the device and the adapter
+will become available for other devices.
+
+
+Configuring chan_mobile :-
+
+The configuration file for chan_mobile is /etc/asterisk/mobile.conf. It is a normal Asterisk config file consisting of sections and key=value pairs.
+
+See configs/mobile.conf.sample for an example and an explanation of the configuration.
+
+
+Using chan_mobile :-
+
+chan_mobile.so must be loaded either by loading it using the Asterisk CLI, or by adding it to /etc/asterisk/modules.conf
 
 Search for your bluetooth devices using the CLI command 'mobile search'. Be patient with this command as
-it will take 8 - 10 seconds to do the discovery.
+it will take 8 - 10 seconds to do the discovery. This requires a free adapter.
 
 Headsets will generally have to be put into 'pairing' mode before they will show up here.
 
@@ -53,7 +76,7 @@
 00:0B:9E:11:74:A5 Hello II Plus                  Yes    Headset 1
 00:0F:86:0E:AE:42 Daves Blackberry               Yes    Phone   7
 
-This is a list of all bluetooth devices seen and whether or not they are usable with chan_cellphone.
+This is a list of all bluetooth devices seen and whether or not they are usable with chan_mobile.
 The Address field contains the 'bd address' of the device. This is like an ethernet mac address.
 The Name field is whatever is configured into the device as its name.
 The Usable field tells you whether or not the device supports the Bluetooth Handsfree Profile or Headset profile.
@@ -61,62 +84,23 @@
 The Port field is the number to put in the configuration file.
 
 Choose which device(s) you want to use and edit /etc/asterisk/mobile.conf. There is a sample included
-with the Asterisk source under configs/mobile.conf.sample.
-
-Assuming we want to use the devices above, mobile.conf needs to look like this :-
-
-===================================================================================
-;
-; mobile.conf
-;
-
-[general]
-interval=60             ; Number of seconds between trying to connect to devices.
-
-; The following is a list of the devices we deal with.
-; Every device listed below will be available for calls in and out of Asterisk.
-; Discovered devices not in this list are not available.
-; Use the CLI command 'mobile search' to discover devices.
-; Use the CLI command 'mobile show devices' to see device status.
-;
-; To place a call use Dial(Mobile/[device]/NNN.....) in your dialplan.
-
-[dave]
-address=00:12:56:90:6E:00
-port=4
-context=incoming-mobile
-
-[headset]
-address=00:0B:9E:11:74:A5
-port=1
-type=headset
-===================================================================================
+with the Asterisk-addons source under configs/mobile.conf.sample.
 
 Be sure to configure the right bd address and port number from the search. If you want inbound
 calls on a device to go to a specific context, add a context= line, otherwise the default will
-be used. The 'id' of the device [bitinbrackets] can be anything you like, just make the unique.
-
-If your are configuring a Headset be sure to include the type=headset line, if left out it defaults
+be used. The 'id' of the device [bitinbrackets] can be anything you like, just make it unique.
+
+If you are configuring a Headset be sure to include the type=headset line, if left out it defaults
 to phone.
-
-Having done this, unload chan_mobile and load it again.
 
 The CLI command 'mobile show devices' can be used at any time to show the status of configured devices,
 and whether or not the device is capable of sending / receiving SMS via bluetooth.
 
-*CLI> mobile show devices
-ID              Address           Connected State SMS
-blackberry      00:0F:86:0E:AE:42 Yes       Free  Yes
-dave            00:12:56:90:6E:00 Yes       Free  No
-headset         00:0B:9E:11:74:A5 Yes       Free  No
+*CLI> mobile show devices 
+ID              Address           Adapter         Connected State SMS
+headset         00:0B:9E:11:AE:C6 blue            No        Init  No 
+LGTU550         00:E0:91:7F:46:44 dlink           No        Init  No 
 *CLI>
-
-
-All being well Asterisk will now try and establish a connection to each configured device. If it cant
-it will retry after 'interval' seconds, infinately.
-
-This means that as your cell phone comes into range and goes out of range, Asterisk will automatically
-connect and disconnect from it. You dont need to worry about it.
 
 As each phone is connected you will see a message on the Asterisk console :-
 
@@ -124,34 +108,19 @@
     -- Bluetooth Device blackberry has connected.
     -- Bluetooth Device dave has connected.
 
-If someone calls your cell phone now, Asterisk will handle the call and it will be sent into the
-context you specified, or the default context. Mostly likely this means some SIP phone somewhere will
-ring, pick it up and take the call.
-
 To make outbound calls, add something to you Dialplan like the following :- (modify to suit)
 
-; Calls via TU500
-exten => _9X.,1,Dial(Mobile/dave/${EXTEN:1},45)
+; Calls via LGTU5500
+exten => _9X.,1,Dial(Mobile/LGTU550/${EXTEN:1},45)
 exten => _9X.,n,Hangup
-; Calls via Blackberry
-exten => _8X.,1,Dial(Mobile/blackberry/${EXTEN:1},45)
-exten => _8X.,n,Hangup
-
-Pick up a SIP phone and dial 9<number of pizza shop> and the call vill go via the device 'dave' in
-mobile.conf.
-
-To incoming calls to a headset do something like this :-
-
-[incoming-context]
-exten => s,1,Dial(Mobile/headset,30)
-exten => s,n,Hangup()
+
 
 To dial out on a headset, you need to use some other mechanism, because the headset is not likely
 to have all the needed buttons on it. res_clioriginate is good for this :-
 
 *CLI> originate Mobile/headset extension NNNNN at context
 
-This will call your headset, once you answer Asterisk will call NNNNN at context context
+This will call your headset, once you answer, Asterisk will call NNNNN at context context
 
 Dialplan hints :-
 
@@ -166,8 +135,8 @@
 to determine the 'state' of a device.
 
 For example, suppose you wanted to call dave's extension, but only if he was in the office. You could
-test to see if his cell phone was attached to Asterisk, if it is dial his extension, otherwise dial his
-cell phone.
+test to see if his mobile phone was attached to Asterisk, if it is dial his extension, otherwise dial his
+mobile phone.
 
 exten => 40,1,MobileStatus(dave,DAVECELL)
 exten => 40,2,GotoIf($["${DAVECELL}" = "1"]?3:5)
@@ -185,7 +154,7 @@
 
 SMS Sending / Receiving
 
-If Asterisk has detected your cell phone is capable of SMS via bluetooth, you will be able to send and
+If Asterisk has detected your mobile phone is capable of SMS via bluetooth, you will be able to send and
 receive SMS.
 
 Incoming SMS's cause Asterisk to create an inbound call to the context you defined in mobile.conf or the default
@@ -219,7 +188,7 @@
 DTMF detection varies from phone to phone. There is a configuration variable that allows you to tune
 this to your needs. e.g. in mobile.conf
 
-[dave]
+[LGTU550]
 address=00:12:56:90:6E:00
 port=4
 context=incoming-mobile
@@ -233,10 +202,8 @@
 
 Different phone manufacturers have different interpretations of the Bluetooth Handsfree Profile Spec.

[... 34 lines stripped ...]



More information about the asterisk-addons-commits mailing list