1
2
3
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"
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
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()
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:
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
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
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
157 '''
158 Terminates the server (finishs the listening state).
159 '''
160 BTServer.debug("Calling terminate()")
161 self.isTerminating = True
162
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
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
181
182 @staticmethod
184 '''
185 Returns the library version.
186 @return: the current version of the library
187 '''
188 return BTCOM_VERSION
189
193 Thread.__init__(self)
194 self.server = server
195
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')
208
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])
223 except:
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
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
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
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
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
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
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
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)):
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
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:
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
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
433 '''
434 Returns the Bluetooth MAC address in format "nn:nn:nn:nn:nn::nn"
435 '''
436 return self.macAddress
437
439 '''
440 Returns the Bluetooth channel.
441 '''
442 return self.channel
443
444 @staticmethod
448
449 @staticmethod
451 '''
452 Returns the library version.
453 @return: the current version of the library
454 '''
455 return TCPCOM_VERSION
456
460 Thread.__init__(self)
461 self.client = client
462 self.start()
463
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')
476
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])
486 except:
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