File: _bluetoothsockets.py

package info (click to toggle)
pybluez 0.23-5.1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 2,152 kB
  • sloc: ansic: 4,854; python: 4,319; objc: 3,363; cpp: 1,950; makefile: 190
file content (901 lines) | stat: -rw-r--r-- 34,567 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
# Copyright (c) 2009 Bea Lam. All rights reserved.
#
# This file is part of LightBlue.
#
# LightBlue is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# LightBlue is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with LightBlue.  If not, see <http://www.gnu.org/licenses/>.

# Mac OS X bluetooth sockets implementation.
#
# To-do:
# - allow socket options
#
# if doing security AUTH, should set bool arg when calling
#   openConnection_withPageTimeout_authenticationRequired_() in connect()


import time
import socket as _socket
import threading
import os
import errno
import types

import objc
import Foundation

from . import _IOBluetooth
from . import _lightbluecommon
from . import _macutil
from ._LightAquaBlue import BBServiceAdvertiser, BBBluetoothChannelDelegate

#import sets     # python 2.3

try:
    SHUT_RD, SHUT_WR, SHUT_RDWR = \
        _socket.SHUT_RD, _socket.SHUT_WR, _socket.SHUT_RDWR
except AttributeError:
    # python 2.3
    SHUT_RD, SHUT_WR, SHUT_RDWR = (0, 1, 2)


def _getavailableport(proto):
    # Just advertise a service and see what channel it was assigned, then
    # stop advertising the service and return the channel.
    # It's a hacky way of doing it, but IOBluetooth doesn't seem to provide
    # functionality for just getting an available channel.

    if proto == _lightbluecommon.RFCOMM:
        try:
            result, channelID, servicerecordhandle = BBServiceAdvertiser.addRFCOMMServiceDictionary_withName_UUID_channelID_serviceRecordHandle_(BBServiceAdvertiser.serialPortProfileDictionary(), "DummyService", None, None, None)
        except:
            result, channelID, servicerecordhandle = BBServiceAdvertiser.addRFCOMMServiceDictionary_withName_UUID_channelID_serviceRecordHandle_(BBServiceAdvertiser.serialPortProfileDictionary(), "DummyService", None)
        if result != _macutil.kIOReturnSuccess:
            raise _lightbluecommon.BluetoothError(result, \
                "Could not retrieve an available service channel")
        result = BBServiceAdvertiser.removeService_(servicerecordhandle)
        if result != _macutil.kIOReturnSuccess:
            raise _lightbluecommon.BluetoothError(result, \
                "Could not retrieve an available service channel")
        return channelID

    else:
        raise NotImplementedError("L2CAP server sockets not currently supported")


def _checkaddrpair(address, checkbtaddr=True):
    # will want checkbtaddr=False if the address might be empty string
    # (for binding to a server address)

    if not isinstance(address, tuple):
        raise TypeError("address must be (address, port) tuple, was %s" % \
            type(address))

    if len(address) != 2:
        raise TypeError("address tuple must have 2 items (has %d)" % \
            len(address))

    if not isinstance(address[0], str):
        raise TypeError("address host value must be string, was %s" % \
            type(address[0]))

    if checkbtaddr:
        if not _lightbluecommon._isbtaddr(address[0]):
            raise TypeError("address '%s' is not a bluetooth address" % \
                address[0])

    if not isinstance(address[1], int):
        raise TypeError("address port value must be int, was %s" % \
            type(address[1]))


# from std lib socket module
class _closedsocket(object):
    __slots__ = []
    def _dummy(*args):
        raise _socket.error(errno.EBADF, 'Bad file descriptor')
    send = recv = sendto = recvfrom = __getattr__ = _dummy


# TODO: replace with BytesIO if minimum supported version is python3?
# or just get rid of wrapper class altogether & use bytearray directly if
# multi-threaded usage isn't support (it's not currently).
class _ByteQueue(object):
    def __init__(self):
        self.buffered = bytearray()
        self.lock = threading.Lock()

    def empty(self):
        return len(self.buffered) == 0

    def write(self, data):
        with self.lock:
          self.buffered.extend(data)

    def __len__(self):
        #calculate length without needing to _build_str
        return len(self.buffered)

    def read(self, count):
        with self.lock:
            #get data requested by caller
            result = self.buffered[:count]
            #remove requested data from buffer
            del self.buffered[:count]
        return result


#class _SocketWrapper(_socket._socketobject):
class _SocketWrapper(object):
    """
    A Bluetooth socket object has the same interface as a socket object from
    the Python standard library <socket> module. It also uses the same
    exceptions, raising socket.error for general errors and socket.timeout for
    timeout errors.

    Note that L2CAP sockets are not available on Python For Series 60, and
    only L2CAP client sockets are supported on Mac OS X and Linux.

    A simple client socket example:
        >>> from lightblue import *
        >>> s = socket()        # or socket(L2CAP) to create an L2CAP socket
        >>> s.connect(("00:12:2c:45:8a:7b", 5))
        >>> s.send("hello")
        5
        >>> s.close()

    A simple server socket example:
        >>> from lightblue import *
        >>> s = socket()
        >>> s.bind(("", 0))
        >>> s.listen(1)
        >>> advertise("My RFCOMM Service", s, RFCOMM)
        >>> conn, addr = s.accept()
        >>> print("Connected by", addr)
        Connected by ('00:0D:93:19:C8:68', 5)
        >>> conn.recv(1024)
        "hello"
        >>> conn.close()
        >>> s.close()
    """

    def __init__(self, sock):
        self._sock = sock

    def accept(self):
        sock, addr = self._sock.accept()
        return _SocketWrapper(sock), addr
    accept.__doc__ = _lightbluecommon._socketdocs["accept"]

    def dup(self):
        return _SocketWrapper(self._sock)
    dup.__doc__ = _lightbluecommon._socketdocs["dup"]

    def close(self):
        self._sock.close()

        self._sock = _closedsocket()
        self.send = self.recv = self.sendto = self.recvfrom = self._sock._dummy

        try:
            import lightblue
            lightblue.stopadvertise(self)
        except:
            pass
    close.__doc__ = _lightbluecommon._socketdocs["close"]

    def makefile(self, mode='r', bufsize=-1):
        # use std lib socket's _fileobject
        return _socket._fileobject(self._sock, mode, bufsize)
    makefile.__doc__ = _lightbluecommon._socketdocs["makefile"]

    # delegate all other method calls to internal sock obj
    def __getattr__(self, attr):
        return getattr(self._sock, attr)


# internal _sock object for RFCOMM and L2CAP sockets
class _BluetoothSocket(object):

    _boundports = { _lightbluecommon.L2CAP: set(),
                    _lightbluecommon.RFCOMM: set() }

    # conn is the associated _RFCOMMConnection or _L2CAPConnection
    def __init__(self, conn):
        self.__conn = conn

        if conn is not None and conn.channel is not None:
            self.__remotedevice = conn.channel.getDevice()
        else:
            self.__remotedevice = None

        # timeout=None cos sockets default to blocking mode
        self.__timeout = None

        #self.__isserverspawned = (conn.channel is not None)
        self.__port = 0
        self.__eventlistener = None
        self.__closed = False
        self.__maxqueuedconns = 0
        self.__incomingdata = _ByteQueue()
        self.__queuedchannels = []
        self.__queuedchannels_lock = threading.RLock()

        # whether send or recv has been shut down
        # set initial value to be other than SHUT_WR/SHUT_RD/SHUT_RDWR
        self.__commstate = -1

    def accept(self):
        if not self.__isbound():
            raise _socket.error('Socket not bound')
        if not self.__islistening():
            raise _socket.error('Socket must be listening first')

        def clientconnected():
            return len(self.__queuedchannels) > 0
        if not clientconnected():
            self.__waituntil(clientconnected, "accept timed out")

        self.__queuedchannels_lock.acquire()
        try:
            newchannel = self.__queuedchannels.pop(0)
        finally:
            self.__queuedchannels_lock.release()

        # return (new-socket, addr) pair using the new channel
        newconn = _SOCKET_CLASSES[self.__conn.proto](newchannel)
        sock = _SocketWrapper(_BluetoothSocket(newconn))
        sock.__startevents()
        return (sock, sock.getpeername())

    def bind(self, address):
        _checkaddrpair(address, False)
        if self.__isbound():
            raise _socket.error('Socket is already bound')
        elif self.__isconnected():
            raise _socket.error("Socket is already connected, cannot be bound")

        if self.__conn.proto == _lightbluecommon.L2CAP:
            raise NotImplementedError("L2CAP server sockets not currently supported")

        if address[1] != 0:
            raise _socket.error("must bind to port 0, other ports not supported on Mac OS X")
        address = (address[0], _getavailableport(self.__conn.proto))

        # address must be either empty string or local device address
        if address[0] != "":
            try:
                import lightblue
                localaddr = lightblue.gethostaddr()
            except:
                localaddr = None
            if localaddr is None or address[0] != localaddr:
                raise _socket.error(
                    errno.EADDRNOTAVAIL, os.strerror(errno.EADDRNOTAVAIL))

        # is this port already in use?
        if address[1] in self._boundports[self.__conn.proto]:
            raise _socket.error(errno.EADDRINUSE, os.strerror(errno.EADDRINUSE))

        self._boundports[self.__conn.proto].add(address[1])
        self.__port = address[1]

    def close(self):
        wasconnected = self.__isconnected() or self.__isbound()
        self.__stopevents()

        if self.__conn is not None:
            if self.__isbound():
                self._boundports[self.__conn.proto].discard(self.__port)
            else:
                if self.__conn.channel is not None:
                    self.__conn.channel.setDelegate_(None)
                    self.__conn.channel.closeChannel()

        # disconnect the baseband connection.
        # This will fail if other RFCOMM channels to the remote device are
        # still open (which is what we want, cos we don't know if another
        # process is talking to the device)
        if self.__remotedevice is not None:
            self.__remotedevice.closeConnection()   # returns err code

        # if you don't run the event loop a little here, it's likely you won't
        # be able to reconnect to the same remote device later
        if wasconnected:
            _macutil.waitfor(0.5)


    def connect(self, address):
        if self.__isbound():
            raise _socket.error("Can't connect, socket has been bound")
        elif self.__isconnected():
            raise _socket.error("Socket is already connected")
        _checkaddrpair(address)

        # open a connection to device
        self.__remotedevice = _IOBluetooth.IOBluetoothDevice.withAddressString_(address[0])

        if not self.__remotedevice.isConnected():
            if self.__timeout is None:
                result = self.__remotedevice.openConnection()
            else:
                result = self.__remotedevice.openConnection_withPageTimeout_authenticationRequired_(
                    None, self.__timeout*1000, False)

            if result != _macutil.kIOReturnSuccess:
                if result == _macutil.kBluetoothHCIErrorPageTimeout:
                    if self.__timeout == 0:
                        raise _socket.error(errno.EAGAIN,
                            "Resource temporarily unavailable")
                    else:
                        raise _socket.timeout("connect timed out")
                else:
                    raise _socket.error(result,
                        "Cannot connect to %s, can't open connection." \
                                                            % str(address[0]))

        # open RFCOMM or L2CAP channel
        self.__eventlistener = self.__createlistener()
        result = self.__conn.connect(self.__remotedevice, address[1],
                self.__eventlistener)   # pass listener as cocoa delegate

        if result != _macutil.kIOReturnSuccess:
            self.__remotedevice.closeConnection()
            self.__stopevents()
            self.__eventlistener = None
            raise _socket.error(result,
                    "Cannot connect to %d on %s" % (address[1], address[0]))
            return

        # if you don't run the event loop a little here, it's likely you won't
        # be able to reconnect to the same remote device later
        _macutil.waitfor(0.5)

    def connect_ex(self, address):
        try:
            self.connect(address)
        except _socket.error as err:
            if len(err.args) > 1:
                return err.args[0]
            else:
                # there's no error code, just a message, so this error wasn't
                # from a system call -- so re-raise the exception
                raise _socket.error(err)
        return 0

    def getpeername(self):
        self.__checkconnected()
        addr = _macutil.formatdevaddr(self.__remotedevice.getAddressString())
        return (addr, self._getport())

    def getsockname(self):
        if self.__isbound() or self.__isconnected():
            import lightblue
            return (lightblue.gethostaddr(), self._getport())
        else:
            return ("00:00:00:00:00:00", 0)

    def listen(self, backlog):
        if self.__islistening():
            return

        if not self.__isbound():
            raise _socket.error('Socket not bound')
        if not isinstance(backlog, int):
            raise TypeError("backlog must be int, was %s" % type(backlog))
        if backlog < 0:
            raise ValueError("backlog cannot be negative, was %d" % backlog)

        self.__maxqueuedconns = backlog

        # start listening for client connections
        self.__startevents()


    def _isclosed(self):
        # isOpen() check doesn't work for incoming (server-spawned) channels
        if (self.__conn.proto == _lightbluecommon.RFCOMM and
                self.__conn.channel is not None and
                not self.__conn.channel.isIncoming()):
            return not self.__conn.channel.isOpen()
        return self.__closed


    def recv(self, bufsize, flags=0):
        if self.__commstate in (SHUT_RD, SHUT_RDWR):
            return ""
        self.__checkconnected()

        if not isinstance(bufsize, int):
            raise TypeError("buffer size must be int, was %s" % type(bufsize))
        if bufsize < 0:
            raise ValueError("negative buffersize in recv") # as for tcp
        if bufsize == 0:
            return ""

        # need this to ensure the _isclosed() check is up-to-date
        _macutil.looponce()

        if self._isclosed():
            if len(self.__incomingdata) == 0:
                raise _socket.error(errno.ECONNRESET,
                                    os.strerror(errno.ECONNRESET))
            return self.__incomingdata.read(bufsize)

        # if incoming data buffer is empty, wait until data is available or
        # channel is closed
        def gotdata():
            return not self.__incomingdata.empty() or self._isclosed()
        if not gotdata():
            self.__waituntil(gotdata, "recv timed out")

        # other side closed connection while waiting?
        if self._isclosed() and len(self.__incomingdata) == 0:
            raise _socket.error(errno.ECONNRESET, os.strerror(errno.ECONNRESET))

        return self.__incomingdata.read(bufsize)


    # recvfrom() is really for datagram sockets not stream sockets but it
    # can be implemented anyway.
    def recvfrom(self, bufsize, flags=0):
        # stream sockets return None, instead of address
        return (self.recv(bufsize, flags), None)

    def sendall(self, data, flags=0):
        sentbytescount = self.send(data, flags)
        while sentbytescount < len(data):
            sentbytescount += self.send(data[sentbytescount:], flags)
        return None

    def send(self, data, flags=0):
        # On python 2 this should be OK for backwards compatability as "bytes"
        # is an alias for "str".
        if not isinstance(data, (bytes, bytearray)):
            raise TypeError("data must be bytes, was %s" % type(data))
        if self.__commstate in (SHUT_WR, SHUT_RDWR):
            raise _socket.error(errno.EPIPE, os.strerror(errno.EPIPE))
        self.__checkconnected()

        # do setup for if sock is in non-blocking mode
        if self.__timeout is not None:
            if self.__timeout == 0:
                # in non-blocking mode
                # isTransmissionPaused() is not available for L2CAP sockets,
                # what to do for that?
                if self.__conn.proto == _lightbluecommon.RFCOMM and \
                        self.__conn.channel.isTransmissionPaused():
                    # sending data now will block
                    raise _socket.error(errno.EAGAIN,
                        "Resource temporarily unavailable")
            elif self.__timeout > 0:
                # non-blocking with timeout
                starttime = time.time()

        # loop until all data is sent
        writebuf = data
        bytesleft = len(data)
        mtu = self.__conn.getwritemtu()
        while bytesleft > 0:
            if self.__timeout is not None and self.__timeout > 0:
                if time.time() - starttime > self.__timeout:
                    raise _socket.timeout("send timed out")

            # write the data to the channel (only the allowed amount)
            # the method/selector is the same for L2CAP and RFCOMM channels
            if bytesleft > mtu:
                sendbytecount = mtu
            else:
                sendbytecount = bytesleft
            #result = self.__conn.channel.writeSync_length_(
            #        writebuf[:sendbytecount], sendbytecount)
            result = self.__conn.write(writebuf[:sendbytecount])

            # normal tcp sockets don't seem to actually error on the first
            # send() after a connection has broken; if you try a second time,
            # then you get the (32, 'Broken pipe') socket.error
            if result != _macutil.kIOReturnSuccess:
                raise _socket.error(result, "Error sending data")

            bytesleft -= sendbytecount
            writebuf = writebuf[sendbytecount:] # remove the data just sent

        return len(data) - bytesleft

    # sendto args may be one of:
    # - data, address
    # - data, flags, address
    #
    # The standard behaviour seems to be to ignore the given address if already
    # connected.
    # sendto() is really for datagram sockets not stream sockets but it
    # can be implemented anyway.
    def sendto(self, data, *args):
        if len(args) == 1:
            address = args[0]
            flags = 0
        elif len(args) == 2:
            flags, address = args
        else:
            raise TypeError("sendto takes at most 3 arguments (%d given)" % \
                (len(args) + 1))
        _checkaddrpair(address)

        # must already be connected, cos this is stream socket
        self.__checkconnected()
        return self.send(data, flags)

    def fileno(self):
        raise NotImplementedError

    def getsockopt(self, level, optname, buflen=0):
        # see what options on Linux+s60
        # possibly have socket security option.
        raise _socket.error(
            errno.ENOPROTOOPT, os.strerror(errno.ENOPROTOOPT))

    def setsockopt(self, level, optname, value):
        # see what options on Linux+s60
        # possibly have socket security option.
        raise _socket.error(
            errno.ENOPROTOOPT, os.strerror(errno.ENOPROTOOPT))

    def setblocking(self, flag):
        if flag == 0:
            self.__timeout = 0      # non-blocking
        else:
            self.__timeout = None   # blocking

    def gettimeout(self):
        return self.__timeout

    def settimeout(self, value):
        if value is not None and not isinstance(value, (float, int)):
            msg = "timeout value must be a number or None, was %s" % \
                type(value)
            raise TypeError(msg)
        if value < 0:
            msg = "timeout value cannot be negative, was %d" % value
            raise ValueError(msg)
        self.__timeout = value

    def shutdown(self, how):
        if how not in (SHUT_RD, SHUT_WR, SHUT_RDWR):
            raise _socket.error(22, "Invalid argument")
        self.__commstate = how

    # This method is called from outside this file.
    def _getport(self):
        if self.__isconnected():
            return self.__conn.getport()
        if self.__isbound():
            return self.__port
        raise _lightbluecommon.BluetoothError("socket is neither connected nor bound")

    # This method is called from outside this file.
    def _getchannel(self):
        if self.__conn is None:
            return None
        return self.__conn.channel

    # Called by the event listener when data is available
    # 'channel' is IOBluetoothRFCOMMChannel or IOBluetoothL2CAPChannel object
    def _handle_channeldata(self, channel, data):
        self.__incomingdata.write(data)
        _macutil.interruptwait()

    # Called by the event listener when a client connects to a server socket
    def _handle_channelopened(self, channel):
        # put new channels into a queue, which 'accept' can then pull out
        self.__queuedchannels_lock.acquire()
        try:
            # need to implement max connections
            #if len(self.__queuedchannels) < self.__maxqueuedconns:
            self.__queuedchannels.append(channel)
            _macutil.interruptwait()
        finally:
            self.__queuedchannels_lock.release()

    # Called by the event listener when the channel is closed.
    def _handle_channelclosed(self, channel):
        # beware that this value won't actually be set until the event loop
        # has been driven so that this method is actually called
        self.__closed = True
        _macutil.interruptwait()

    def __waituntil(self, stopwaiting, timeoutmsg):
        """
        Waits until stopwaiting() returns True, or until the wait times out
        (according to the self.__timeout value).

        This is to make a function wait until a buffer has been filled. i.e.
        stopwaiting() should return True when the buffer is no longer empty.
        """
        if not stopwaiting():
            if self.__timeout == 0:
                # in non-blocking mode (immediate timeout)
                # push event loop to really be sure there is no data available
                _macutil.looponce()
                if not stopwaiting():
                    # trying to perform operation now would block
                    raise _socket.error(errno.EAGAIN, os.strerror(errno.EAGAIN))
            else:
                # block and wait until we get data, or time out
                if not _macutil.waituntil(stopwaiting, self.__timeout):
                    raise _socket.timeout(timeoutmsg)

    def __createlistener(self):
        if self.__isbound():
            return _ChannelServerEventListener.alloc().initWithDelegate_port_protocol_(self,
                    self._getport(), self.__conn.proto)
        else:
            listener = _ChannelEventListener.alloc().initWithDelegate_(self)
            if self.__conn.channel is not None:
                self.__conn.channel.setDelegate_(listener.delegate())
                listener.registerclosenotif(self.__conn.channel)
            return listener

    # should not call this if connect() has been called to connect this socket
    def __startevents(self):
        if self.__eventlistener is not None:
            raise _lightbluecommon.BluetoothError("socket already listening")
        self.__eventlistener = self.__createlistener()

    def __stopevents(self):
        if self.__eventlistener is not None:
            self.__eventlistener.close()

    def __islistening(self):
        return self.__eventlistener is not None

    def __checkconnected(self):
        if not self._sock.isconnected():  # i.e. is connected, non-server socket
            # not connected, raise "socket not connected"
            raise _socket.error(errno.ENOTCONN, os.strerror(errno.ENOTCONN))

    # returns whether socket is a bound server socket
    def __isbound(self):
        return self.__port != 0

    def __isconnected(self):
        return self.__conn.channel is not None

    def __checkconnected(self):
        if not self.__isconnected():
            # not connected, raise "socket not connected"
            raise _socket.error(errno.ENOTCONN, os.strerror(errno.ENOTCONN))

    # set method docstrings
    definedmethods = locals()   # i.e. defined methods in _SocketWrapper
    for name, doc in list(_lightbluecommon._socketdocs.items()):
        try:
            definedmethods[name].__doc__ = doc
        except KeyError:
            pass


class _RFCOMMConnection(object):

    proto = _lightbluecommon.RFCOMM

    def __init__(self, channel=None):
        # self.channel is accessed by _BluetoothSocket parent
        self.channel = channel

    def connect(self, device, port, listener):
        # open RFCOMM channel (should timeout actually apply to opening of
        # channel as well? if so need to do timeout with async callbacks)
        try:
            # pyobjc 2.0
            result, self.channel = device.openRFCOMMChannelSync_withChannelID_delegate_(None, port, listener.delegate())
        except TypeError:
            result, self.channel = device.openRFCOMMChannelSync_withChannelID_delegate_(port, listener.delegate())
        if result == _macutil.kIOReturnSuccess:
            self.channel.setDelegate_(listener.delegate())
            listener.registerclosenotif(self.channel)
        else:
            self.channel = None
        return result

    def write(self, data):
        if self.channel is None:
            raise _socket.error("socket not connected")
        return \
            BBBluetoothChannelDelegate.synchronouslyWriteData_toRFCOMMChannel_(
                Foundation.NSData.alloc().initWithBytes_length_(data, len(data)),
                self.channel)

    def getwritemtu(self):
        return self.channel.getMTU()

    def getport(self):
        return self.channel.getChannelID()


class _L2CAPConnection(object):

    proto = _lightbluecommon.L2CAP

    def __init__(self, channel=None):
        # self.channel is accessed by _BluetoothSocket parent
        self.channel = channel

    def connect(self, device, port, listener):
        try:
            # pyobjc 2.0
            result, self.channel = device.openL2CAPChannelSync_withPSM_delegate_(None, port, listener.delegate())
        except TypeError:
            result, self.channel = device.openL2CAPChannelSync_withPSM_delegate_(port, listener.delegate())
        if result == _macutil.kIOReturnSuccess:
            self.channel.setDelegate_(listener.delegate())
            listener.registerclosenotif(self.channel)
        else:
            self.channel = None
        return result

    def write(self, data):
        if self.channel is None:
            raise _socket.error("socket not connected")
        return \
            BBBluetoothChannelDelegate.synchronouslyWriteData_toL2CAPChannel_(
                bytes(data), self.channel)

    def getwritemtu(self):
        return self.channel.getOutgoingMTU()

    def getport(self):
        return self.channel.getPSM()


class _ChannelEventListener(Foundation.NSObject):
    """
    Uses a BBBluetoothChannelDelegate to listen for events on an
    IOBluetoothRFCOMMChannel or IOBluetoothL2CAPChannel, and makes callbacks to
    a specified object when events occur.
    """

    # note this is a NSObject "init", not a python object "__init__"
    def initWithDelegate_(self, cb_obj):
        """
        Arguments:
        - cb_obj: An object that receives callbacks when events occur. This
          object should have:
            - a method '_handle_channeldata' which takes the related channel (a
              IOBluetoothRFCOMMChannel or IOBluetoothL2CAPChannel) and the new
              data (a string) as the arguments.
            - a method '_handle_channelclosed' which takes the related channel
              as the argument.

        If this listener's delegate is passed to the openRFCOMMChannel... or
        openL2CAPChannel... selectors as the delegate, the delegate (and
        therefore this listener) will automatically start receiving events.

        Otherwise, call setDelegate_() on the channel with this listener's
        delegate as the argument to allow this listener to start receiving
        channel events. (This is the only option for server-spawned sockets.)
        """
        self = super(_ChannelEventListener, self).init()
        if cb_obj is None:
            raise TypeError("callback object is None")
        self.__cb_obj = cb_obj
        self.__closenotif = None
        self.__channelDelegate = \
                BBBluetoothChannelDelegate.alloc().initWithDelegate_(self)
        return self
    initWithDelegate_ = objc.selector(initWithDelegate_, signature=b"@@:@")

    def delegate(self):
        return self.__channelDelegate

    @objc.python_method
    def registerclosenotif(self, channel):
        # oddly enough, sometimes the channelClosed: selector doesn't get called
        # (maybe if there's a lot of data being passed?) but this seems to work
        notif = channel.registerForChannelCloseNotification_selector_(self,
                "channelClosedEvent:channel:")
        if notif is not None:
            self.__closenotif = notif

    def close(self):
        if self.__closenotif is not None:
            self.__closenotif.unregister()

    def channelClosedEvent_channel_(self, notif, channel):
        if hasattr(self.__cb_obj, '_handle_channelclosed'):
            self.__cb_obj._handle_channelclosed(channel)
    channelClosedEvent_channel_ = objc.selector(
            channelClosedEvent_channel_, signature=b"v@:@@")

    # implement method from BBBluetoothChannelDelegateObserver protocol:
    # - (void)channelData:(id)channel data:(NSData *)data;
    def channelData_data_(self, channel, data):
        if hasattr(self.__cb_obj, '_handle_channeldata'):
            self.__cb_obj._handle_channeldata(channel, data[:])
    channelData_data_ = objc.selector(channelData_data_, signature=b"v@:@@")

    # implement method from BBBluetoothChannelDelegateObserver protocol:
    # - (void)channelClosed:(id)channel;
    def channelClosed_(self, channel):
        if hasattr(self.__cb_obj, '_handle_channelclosed'):
            self.__cb_obj._handle_channelclosed(channel)
    channelClosed_ = objc.selector(channelClosed_, signature=b"v@:@")


class _ChannelServerEventListener(Foundation.NSObject):
    """
    Listens for server-specific events on a RFCOMM or L2CAP channel (i.e. when a
    client connects) and makes callbacks to a specified object when events
    occur.
    """

    # note this is a NSObject "init", not a python object "__init__"
    def initWithDelegate_port_protocol_(self, cb_obj, port, proto):
        """
        Arguments:
        - cb_obj: to receive callbacks when a client connects to
          to the channel, the callback object should have a method
          '_handle_channelopened' which takes the newly opened
          IOBluetoothRFCOMMChannel or IOBluetoothL2CAPChannel as its argument.
        - port: the channel or PSM that the server is listening on
        - proto: L2CAP or RFCOMM.
        """
        self = super(_ChannelServerEventListener, self).init()
        if cb_obj is None:
            raise TypeError("callback object is None")
        self.__cb_obj = cb_obj
        self.__usernotif = None

        if proto == _lightbluecommon.RFCOMM:
            usernotif = _IOBluetooth.IOBluetoothRFCOMMChannel.registerForChannelOpenNotifications_selector_withChannelID_direction_(self, "newChannelOpened:channel:", port, _macutil.kIOBluetoothUserNotificationChannelDirectionIncoming)
        elif proto == _lightbluecommon.L2CAP:
            usernotif = _IOBluetooth.IOBluetoothL2CAPChannel.registerForChannelOpenNotifications_selector_withPSM_direction_(self, "newChannelOpened:channel:", port, _macutil.kIOBluetoothUserNotificationChannelDirectionIncoming)

        if usernotif is None:
            raise _socket.error("Unable to register for channel-" + \
                "opened notifications on server socket on channel/PSM %d" % \
                port)
        self.__usernotif = usernotif
        return self
    initWithDelegate_port_protocol_ = objc.selector(
        initWithDelegate_port_protocol_, signature=b"@@:@ii")

    def close(self):
        if self.__usernotif is not None:
            self.__usernotif.unregister()

    def newChannelOpened_channel_(self, notif, newChannel):
        """
        Handle when a client connects to the server channel.
        (This method is called for both RFCOMM and L2CAP channels.)
        """
        if newChannel is not None and newChannel.isIncoming():
            # not sure if delegate really needs to be set
            newChannel.setDelegate_(self)

            if hasattr(self.__cb_obj, '_handle_channelopened'):
                self.__cb_obj._handle_channelopened(newChannel)
    # makes this method receive notif and channel as objects
    newChannelOpened_channel_ = objc.selector(
            newChannelOpened_channel_, signature=b"v@:@@")


# -----------------------------------------------------------

# protocol-specific classes
_SOCKET_CLASSES = { _lightbluecommon.RFCOMM: _RFCOMMConnection,
                    _lightbluecommon.L2CAP:  _L2CAPConnection }

def _getsocketobject(proto):
    if proto not in list(_SOCKET_CLASSES.keys()):
        raise ValueError("Unknown socket protocol, must be L2CAP or RFCOMM")
    return _SocketWrapper(_BluetoothSocket(_SOCKET_CLASSES[proto]()))