Module btpycom
[hide private]
[frames] | no frames]

Source Code for Module btpycom

  1  # btpycom.py 
  2  # Python 2.7 version 
  3  # Author: AP 
  4   
  5  ''' 
  6  Library that implements a event-based Bluetooth client-server system in contrast to the standard stream-based systems. 
  7  Messages are sent in byte blocks (may be UTF-8 or ASCII encoded strings). The null charactor is used as  
  8  End-of-Transmission indicator. The Bluetooth RFCOMM protocol is used. 
  9   
 10  Dependencies: Widcomm packages (Bluez) 
 11  ''' 
 12   
 13  from threading import Thread 
 14  import thread 
 15  import socket 
 16  import time 
 17  import sys 
 18  from bluetooth import * 
 19   
 20  BTCOM_VERSION = "1.00 - March 20, 2017" 
21 22 # ================================== Server ================================ 23 # ---------------------- class BTServer ------------------------ 24 -class BTServer(Thread):
25 ''' 26 Class that represents a Bluetooth server. 27 ''' 28 isVerbose = False 29 CONNECTED = "CONNECTED" 30 LISTENING = "LISTENING" 31 TERMINATED = "TERMINATED" 32 MESSAGE = "MESSAGE" 33
34 - def __init__(self, serviceName, stateChanged, isVerbose = False):
35 ''' 36 Creates a Bluetooth server that listens for a connecting client. 37 The server runs in its own thread, so the 38 constructor returns immediately. State changes invoke the callback 39 onStateChanged(state, msg) where state is one of the following strings: 40 "LISTENING", "CONNECTED", "MESSAGE", "TERMINATED". msg is a string with 41 further information: LISTENING: empty, CONNECTED: connection url, MESSAGE: 42 message received, TERMINATED: empty. The server uses an internal handler 43 thread to detect incoming messages. 44 @param serviceName: the service name the server is exposing 45 @param stateChanged: the callback function to register 46 @param isVerbose: if true, debug messages are written to System.out, default: False 47 ''' 48 Thread.__init__(self) 49 self.serviceName = serviceName 50 self.stateChanged = stateChanged 51 BTServer.isVerbose = isVerbose 52 self.isClientConnected = False 53 self.clientSocket = None 54 self.start()
55
56 - def run(self):
57 BTServer.debug("BTServer thread started") 58 self.serverSocket = BluetoothSocket(RFCOMM) 59 BTServer.debug("Socket created") 60 try: 61 self.serverSocket.bind(("", PORT_ANY)) 62 except socket.error as msg: 63 print "Fatal error while creating BTServer: Bind failed.", msg[0], msg[1] 64 sys.exit() 65 BTServer.debug("Socket bind successful ") 66 try: 67 self.serverSocket.listen(1) 68 except: 69 print "Fatal error while BTServer enters listening mode." 70 sys.exit() 71 72 BTServer.debug("Socket listening engaged.") 73 self.port = self.serverSocket.getsockname()[1] 74 uuid = "94f39d29-7d6d-437d-973b-fba39e49d4ee" 75 76 advertise_service(self.serverSocket, self.serviceName, 77 service_id = uuid, 78 service_classes = [uuid, SERIAL_PORT_CLASS], 79 profiles = [SERIAL_PORT_PROFILE]) 80 BTServer.debug("Service " + self.serviceName + " is exposed") 81 BTServer.debug("Bluetooth server listening at channel " + str(self.port)) 82 try: 83 self.stateChanged(BTServer.LISTENING, str(self.port)) 84 except Exception, e: 85 print "Caught exception in BTServer.LISTENING:", e 86 87 self.serverSocket.settimeout(30) 88 self.isServerRunning = True 89 self.isTerminating = False 90 while self.isServerRunning: 91 isListening = True 92 while isListening: 93 try: 94 BTServer.debug("Calling accept() with timeout = 30 s") 95 self.clientSocket, self.clientInfo = self.serverSocket.accept() # blocking 96 BTServer.debug("Accepted connection from " + self.clientInfo[0] + " at channel " + str(self.clientInfo[1])) 97 isListening = False 98 except: 99 if self.isTerminating: 100 break 101 pass 102 if self.isTerminating: 103 self.isServerRunning = False 104 break 105 BTServer.debug("Accepted connection from " + self.clientInfo[0] + " at channel " + str(self.clientInfo[1])) 106 if self.isClientConnected: # another client is connected 107 BTServer.debug("Returning form blocking accept(). Client refused") 108 self.clientSocket.close() 109 isListening = True 110 continue 111 self.isClientConnected = True 112 self.socketHandler = ServerHandler(self) 113 self.socketHandler.start() 114 try: 115 self.stateChanged(BTServer.CONNECTED, self.clientInfo) 116 except Exception, e: 117 print "Caught exception in BTServer.CONNECTED:", e 118 if self.clientSocket != None: 119 self.clientSocket.close() 120 self.serverSocket.close() 121 self.isClientConnected = False 122 try: 123 self.stateChanged(BTServer.TERMINATED, "") 124 except Exception, e: 125 print "Caught exception in BTServer.TERMINATED:", e 126 self.isServerRunning = False 127 BTServer.debug("BTServer thread terminated")
128
129 - def disconnect(self):
130 ''' 131 Closes the connection with the client and enters 132 the LISTENING state 133 ''' 134 BTServer.debug("Calling Server.disconnect()") 135 if self.isClientConnected: 136 self.isClientConnected = False 137 try: 138 self.stateChanged(BTServer.LISTENING, str(self.port)) 139 except Exception, e: 140 print "Caught exception in BTServer.LISTENING:", e 141 BTServer.debug("Close client socket now") 142 self.clientSocket.close()
143
144 - def sendMessage(self, msg):
145 ''' 146 Sends the information msg to the client (as String, the character \0 (ASCII 0) serves as end of 147 string indicator, it is transparently added and removed) 148 @param msg: the message to send 149 ''' 150 BTServer.debug("sendMessage() with msg: " + msg) 151 if not self.isClientConnected: 152 BTServer.debug("Not connected") 153 return 154 self.clientSocket.sendall(msg + "\0")
155
156 - def terminate(self):
157 ''' 158 Terminates the server (finishs the listening state). 159 ''' 160 BTServer.debug("Calling terminate()") 161 self.isTerminating = True
162
163 - def isConnected(self):
164 ''' 165 Returns True, if a client is connected to the server. 166 @return: True, if the communication link is established 167 ''' 168 return self.isClientConnected
169
170 - def isTerminated(self):
171 ''' 172 Returns True, if the server is in TERMINATED state. 173 @return: True, if the server thread is terminated 174 ''' 175 return self.isServerRunning
176 177 @staticmethod
178 - def debug(msg):
179 if BTServer.isVerbose: 180 print " BTServer-> " + msg
181 182 @staticmethod
183 - def getVersion():
184 ''' 185 Returns the library version. 186 @return: the current version of the library 187 ''' 188 return BTCOM_VERSION
189
190 # ---------------------- class ServerHandler ------------------------ 191 -class ServerHandler(Thread):
192 - def __init__(self, server):
193 Thread.__init__(self) 194 self.server = server
195
196 - def run(self):
197 BTServer.debug("ServerHandler started") 198 bufSize = 4096 199 try: 200 data = bytearray() 201 while True: 202 inBlock = True 203 while inBlock: 204 reply = self.server.clientSocket.recv(bufSize) 205 data.extend(reply) 206 if '\0' in data: 207 junk = data.split('\0') # more than 1 message may be received if 208 # transfer is fast. data: xxxx\0yyyyy\0zzz\0 209 for i in range(len(junk) - 1): 210 BTServer.debug("Received message: " + str(junk[i]) + " len: " + str(len(junk[i]))) 211 if len(junk[i]) > 0: 212 try: 213 self.server.stateChanged(BTServer.MESSAGE, str(junk[i])) 214 except Exception, e: 215 print "Caught exception in BTServer.MESSAGE:", e 216 else: 217 BTServer.debug("Got empty message as EOT") 218 BTServer.debug("ServerHandler thread terminated") 219 self.server.disconnect() 220 return 221 inBlock = False 222 data = bytearray(junk[len(junk) - 1]) # remaining bytes 223 except: # Happens if client is disconnecting 224 BTServer.debug("Exception from blocking conn.recv(), Msg: " + str(sys.exc_info()[0]) + \ 225 " at line # " + str(sys.exc_info()[-1].tb_lineno)) 226 227 self.server.disconnect() 228 BTServer.debug("ServerHandler thread terminated")
229
230 231 # ================================== Client ================================ 232 # -------------------------------- class BTClient -------------------------- 233 -class BTClient():
234 ''' 235 Class that represents a Bluetooth socket based client. To connect a client via Bluetooth, 236 the Bluetooth MAC address (12 hex characters) and the Bluetooth port (channel) of the server 237 must be known (server info). All other server information is irrelevant (Bluetooth friendly name, Bluetooth service name), 238 but the server info may be retrieved by a Bluetooth search from the server name or the service name. 239 ''' 240 isVerbose = False 241 CONNECTING = "CONNECTING" 242 CONNECTION_FAILED = "CONNECTION_FAILED" 243 CONNECTED = "CONNECTED" 244 DISCONNECTED = "DISCONNECTED" 245 MESSAGE = "MESSAGE" 246
247 - def __init__(self, stateChanged, isVerbose = False):
248 ''' 249 Creates a Bluetooth client prepared for a connection with a 250 BTServer. The client uses an internal handler thread to detect incoming messages. 251 State changes invoke the callback 252 onStateChanged(state, msg) where state is one of the following strings: 253 "CONNECTING", "CONNECTION_FAILED", "DISCONNECTED", "MESSAGE". msg is a string with 254 additional information: CONNECTING: serverInfo, CONNECTED: serverInfo, 255 CONNECTION_FAILED: serverInfo, DISCONNECTED: empty, MESSAGE: message received 256 @param stateChanged: the callback function to register 257 @param isVerbose: if true, debug messages are written to System.out 258 ''' 259 self.isClientConnected = False 260 self.isClientConnecting = False 261 self.serviceName = "" 262 self.macAddress = "" 263 self.channel = -1 264 self.stateChanged = stateChanged 265 self.inCallback = False 266 BTClient.isVerbose = isVerbose
267
268 - def sendMessage(self, msg):
269 ''' 270 Sends the information msg to the server (as String, the character \0 271 (ASCII 0) serves as end of string indicator, it is transparently added 272 and removed). 273 @param msg: the message to send 274 ''' 275 BTClient.debug("sendMessage() with msg = " + msg) 276 if not self.isClientConnected: 277 BTClient.debug("sendMessage(): Connection closed.") 278 return 279 try: 280 self.clientSocket.sendall(msg + "\0") 281 except: 282 BTClient.debug("sendMessage(): Connection reset by peer.") 283 self.disconnect(None)
284
285 - def connect(self, serverInfo, timeout):
286 ''' 287 Performs a connection trial to the server with given serverInfo. If the connection trial fails, 288 it is repeated until the timeout is reached. 289 @param serverInfo: a tuple ("nn:nn:nn:nn:nn:nn", channel) with the Bluetooth MAC address string 290 (12 hex characters, upper or lower case, separated by :) and Bluetooth channel (integer), 291 e.g. ("B8:27:EB:04:A6:7E", 1) 292 ''' 293 maxNbRetries = 10 294 try: 295 self.stateChanged(BTClient.CONNECTING, serverInfo) 296 except Exception, e: 297 print "Caught exception in BTClient.CONNECTING:", e 298 nbRetries = 0 299 startTime = time.time() 300 rc = False 301 while (not rc) and time.time() - startTime < timeout and nbRetries < maxNbRetries: 302 BTClient.debug("Starting connect #" + str(nbRetries)) 303 rc = self._connect(serverInfo) 304 if rc == False: 305 nbRetries += 1 306 time.sleep(3) 307 BTClient.debug("connect() returned " + str(rc) + " after " + str(nbRetries) + " retries") 308 if rc: 309 BTClient.debug("Connection established") 310 self.isClientConnected = True 311 try: 312 self.stateChanged(BTClient.CONNECTED, serverInfo ) 313 except Exception, e: 314 print "Caught exception in BTClient.CONNECTED:", e 315 ClientHandler(self) 316 else: 317 BTClient.debug("Connection failed") 318 self.isClientConnected = False 319 try: 320 self.stateChanged(BTClient.CONNECTION_FAILED, serverInfo) 321 except Exception, e: 322 print "Caught exception in BTClient.CONNECTION_FAILED:", e 323 return rc
324
325 - def _connect(self, serverInfo):
326 self.macAddress = serverInfo[0] 327 self.channel = serverInfo[1] 328 self.clientSocket = BluetoothSocket(RFCOMM) 329 try: 330 self.clientSocket.connect((self.macAddress, self.channel)) 331 except: 332 BTClient.debug("Exception from clientSocket.connect(). \n Msg: " + str(sys.exc_info()[0]) + 333 str(sys.exc_info()[1])) 334 return False 335 return True
336
337 - def findServer(self, serverName, timeout):
338 ''' 339 Perform a device inquiry for the given server name. 340 The Bluetooth channel is not inquired, always assumed to be 1. 341 @param timeout: the maximum time in seconds to search 342 @return: a tuple with the Bluetooth MAC address and the Bluetooth channel 1; None if inquiry failed 343 ''' 344 self.serverName = serverName 345 nbRetries = 0 346 startTime = time.time() 347 rc = None 348 while rc == None and time.time() - startTime < timeout: 349 BTClient.debug("Starting inquire #" + str(nbRetries)) 350 rc = self._inquireMAC() 351 if rc == None: 352 nbRetries += 1 353 time.sleep(2) 354 BTClient.debug("findServer returned() " + str(rc) + " after " + str(nbRetries) + " retries") 355 return rc
356
357 - def _inquireMAC(self):
358 BTClient.debug("Calling discover_devices()") 359 devices = discover_devices(duration = 10, lookup_names = True) 360 BTClient.debug("discover_devices() returned: " + str(devices)) 361 for device in devices: 362 if device[1] == self.serverName: 363 return (device[0], 1) 364 return None
365
366 - def findService(self, serviceName, timeout):
367 ''' 368 Perform a service inquiry for the given service name. 369 @param timeout: the maximum time in seconds to search 370 @return: a tuple with the Bluetooth MAC address and the Bluetooth channel; None if inquiry failed 371 ''' 372 self.serviceName = serviceName 373 nbRetries = 0 374 startTime = time.time() 375 rc = None 376 while rc == None and time.time() - startTime < timeout: 377 BTClient.debug("Client: trying to inquire #" + str(nbRetries)) 378 rc = self._inquireService() 379 if rc == None: 380 nbRetries += 1 381 time.sleep(2) 382 return rc
383
384 - def _inquireService(self):
385 BTClient.debug("Calling find_service() with service name: " + self.serviceName) 386 services = find_service(name = self.serviceName, uuid = SERIAL_PORT_CLASS) 387 if services == []: 388 BTClient.debug("find_service() failed to detect service") 389 return None 390 BTClient.debug("find_service() returned: " + str(services)) 391 for i in range(len(services)): # More than one device could expose same service name 392 service = services[i] 393 self.macAddress = service["host"] 394 self.channel = service["port"] 395 BTClient.debug("Inquiry returned successfully with server: " + 396 self.macAddress + ", service name: " + self.serviceName + " at channel " + str(self.channel)) 397 return self.macAddress, self.channel 398 return None
399
400 - def disconnect(self, endOfTransmission = ""):
401 ''' 402 Closes the connection with the server. The endOfTransmission message is sent to the 403 server to notify disconnection. 404 @param endOfTransmission: the message sent to the server (appended by \0) 405 Default: "", None: no notification sent 406 ''' 407 if self.inCallback: # two threads may call in rapid sequence 408 return 409 self.inCallback = True 410 BTClient.debug("Client.disconnect()") 411 if not self.isClientConnected: 412 BTClient.debug("Connection already closed") 413 return 414 if endOfTransmission != None: 415 self.sendMessage(endOfTransmission) 416 self.isClientConnected = False 417 BTClient.debug("Closing socket") 418 self.clientSocket.close() 419 try: 420 self.stateChanged(BTClient.DISCONNECTED, "") 421 except Exception, e: 422 print "Caught exception in BTClient.DISCONNECTED:", e 423 self.inCallback = False
424
425 - def isConnected(self):
426 ''' 427 Returns True of client is connnected to the server. 428 @return: True, if the connection is established 429 ''' 430 return self.isClientConnected
431
432 - def getMacAddress(self):
433 ''' 434 Returns the Bluetooth MAC address in format "nn:nn:nn:nn:nn::nn" 435 ''' 436 return self.macAddress
437
438 - def getChannel(self):
439 ''' 440 Returns the Bluetooth channel. 441 ''' 442 return self.channel
443 444 @staticmethod
445 - def debug(msg):
446 if BTClient.isVerbose: 447 print " BTClient-> " + msg
448 449 @staticmethod
450 - def getVersion():
451 ''' 452 Returns the library version. 453 @return: the current version of the library 454 ''' 455 return TCPCOM_VERSION
456
457 # -------------------------------- class ClientHandler --------------------------- 458 -class ClientHandler(Thread):
459 - def __init__(self, client):
460 Thread.__init__(self) 461 self.client = client 462 self.start()
463
464 - def run(self):
465 BTClient.debug("ClientHandler thread started") 466 bufSize = 4096 467 try: 468 data = bytearray() 469 while True: 470 inBlock = True 471 while inBlock: 472 reply = self.client.clientSocket.recv(bufSize) 473 data.extend(reply) 474 if '\0' in data: 475 junk = data.split('\0') # more than 1 message may be received if 476 # transfer is fast. data: xxxx\0yyyyy\0zzz\0 477 for i in range(len(junk) - 1): 478 BTClient.debug("Received message: " + str(junk[i]) + " len: " + str(len(junk[i]))) 479 if len(junk[i]) > 0: 480 try: 481 self.client.stateChanged(BTServer.MESSAGE, str(junk[i])) 482 except Exception, e: 483 print "Caught exception in BTClient.MESSAGE:", e 484 inBlock = False 485 data = bytearray(junk[len(junk) - 1]) # remaining bytes 486 except: # Happens if client is disconnecting 487 BTClient.debug("Exception from blocking conn.recv(), Msg: " + str(sys.exc_info()[0])) 488 self.client.disconnect() 489 BTClient.debug("ClientHandler thread finished")
490