1"""
2Asynchronous (nonblocking) serial io
3
4"""
5from __future__ import absolute_import, division, print_function
6
7import sys
8import os
9import errno
10from collections import deque
11
12# Import ioflo libs
13from ...aid.sixing import *
14from ...aid.consoling import getConsole
15
16console = getConsole()
17
18class ConsoleNb(object):
19    """
20    Class to manage non blocking io on serial console.
21
22    Opens non blocking read file descriptor on console
23    Use instance method close to close file descriptor
24    Use instance methods getline & put to read & write to console
25    Needs os module
26    """
27
28    def __init__(self):
29        """Initialization method for instance.
30
31        """
32        self.fd = None #console file descriptor needs to be opened
33
34    def open(self, port='', canonical=True):
35        """
36        Opens fd on terminal console in non blocking mode.
37
38        port is the serial port device path name
39        or if '' then use os.ctermid() which
40        returns path name of console usually '/dev/tty'
41
42        canonical sets the mode for the port. Canonical means no characters
43        available until a newline
44
45        os.O_NONBLOCK makes non blocking io
46        os.O_RDWR allows both read and write.
47        os.O_NOCTTY don't make this the controlling terminal of the process
48        O_NOCTTY is only for cross platform portability BSD never makes it the
49        controlling terminal
50
51        Don't use print at same time since it will mess up non blocking reads.
52
53        Default is canonical mode so no characters available until newline
54        need to add code to enable  non canonical mode
55
56        It appears that canonical mode only applies to the console. For other
57        serial ports the characters are available immediately
58        """
59        if not port:
60            port = os.ctermid() #default to console
61
62        try:
63            self.fd = os.open(port, os.O_NONBLOCK | os.O_RDWR | os.O_NOCTTY)
64        except OSError as ex:
65            console.terse("os.error = {0}\n".format(ex))
66            return False
67        return True
68
69    def close(self):
70        """Closes fd.
71
72        """
73        if self.fd:
74            os.close(self.fd)
75            self.fd = None
76
77    def getLine(self,bs = 80):
78        """Gets nonblocking line from console up to bs characters including newline.
79
80           Returns empty string if no characters available else returns line.
81           In canonical mode no chars available until newline is entered.
82        """
83        line = ''
84        try:
85            line = os.read(self.fd, bs)
86        except OSError as ex1:  #if no chars available generates exception
87            try: #need to catch correct exception
88                errno = ex1.args[0] #if args not sequence get TypeError
89                if errno == 35:
90                    pass #No characters available
91                else:
92                    raise #re raise exception ex1
93            except TypeError as ex2:  #catch args[0] mismatch above
94                raise ex1 #ignore TypeError, re-raise exception ex1
95
96        return line
97
98    def put(self, data = '\n'):
99        """Writes data string to console.
100
101        """
102        return(os.write(self.fd, data))
103
104class DeviceNb(object):
105    """
106    Class to manage non blocking IO on serial device port.
107
108    Opens non blocking read file descriptor on serial port
109    Use instance method close to close file descriptor
110    Use instance methods get & put to read & write to serial device
111    Needs os module
112    """
113
114    def __init__(self, port=None, speed=9600, bs=1024):
115        """
116        Initialization method for instance.
117
118        port = serial device port path string
119        speed = serial port speed in bps
120        bs = buffer size for reads
121        """
122        self.fd = None #serial device port file descriptor, must be opened first
123        self.port = port or os.ctermid() #default to console
124        self.speed = speed or 9600
125        self.bs = bs or 1024
126        self.opened = False
127
128    def open(self, port=None, speed=None, bs=None):
129        """
130        Opens fd on serial port in non blocking mode.
131
132        port is the serial port device path name or
133        if '' then use os.ctermid() which
134        returns path name of console usually '/dev/tty'
135
136        os.O_NONBLOCK makes non blocking io
137        os.O_RDWR allows both read and write.
138        os.O_NOCTTY don't make this the controlling terminal of the process
139        O_NOCTTY is only for cross platform portability BSD never makes it the
140        controlling terminal
141
142        Don't use print and console at same time since it will mess up non blocking reads.
143
144        Raw mode
145
146        def setraw(fd, when=TCSAFLUSH):
147        Put terminal into a raw mode.
148        mode = tcgetattr(fd)
149        mode[IFLAG] = mode[IFLAG] & ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON)
150        mode[OFLAG] = mode[OFLAG] & ~(OPOST)
151        mode[CFLAG] = mode[CFLAG] & ~(CSIZE | PARENB)
152        mode[CFLAG] = mode[CFLAG] | CS8
153        mode[LFLAG] = mode[LFLAG] & ~(ECHO | ICANON | IEXTEN | ISIG)
154        mode[CC][VMIN] = 1
155        mode[CC][VTIME] = 0
156        tcsetattr(fd, when, mode)
157
158
159        # set up raw mode / no echo / binary
160        cflag |=  (TERMIOS.CLOCAL|TERMIOS.CREAD)
161        lflag &= ~(TERMIOS.ICANON|TERMIOS.ECHO|TERMIOS.ECHOE|TERMIOS.ECHOK|TERMIOS.ECHONL|
162                     TERMIOS.ISIG|TERMIOS.IEXTEN) #|TERMIOS.ECHOPRT
163        for flag in ('ECHOCTL', 'ECHOKE'): # netbsd workaround for Erk
164            if hasattr(TERMIOS, flag):
165                lflag &= ~getattr(TERMIOS, flag)
166
167        oflag &= ~(TERMIOS.OPOST)
168        iflag &= ~(TERMIOS.INLCR|TERMIOS.IGNCR|TERMIOS.ICRNL|TERMIOS.IGNBRK)
169        if hasattr(TERMIOS, 'IUCLC'):
170            iflag &= ~TERMIOS.IUCLC
171        if hasattr(TERMIOS, 'PARMRK'):
172            iflag &= ~TERMIOS.PARMRK
173
174        """
175        if port is not None:
176            self.port = port
177        if speed is not None:
178            self.speed = speed
179        if bs is not None:
180            self.bs = bs
181
182        self.fd = os.open(self.port, os.O_NONBLOCK | os.O_RDWR | os.O_NOCTTY)
183
184        system = platform.system()
185
186        if (system == 'Darwin') or (system == 'Linux'): #use termios to set values
187            import termios
188
189            iflag, oflag, cflag, lflag, ispeed, ospeed, cc = range(7)
190
191            settings = termios.tcgetattr(self.fd)
192            #print(settings)
193
194            settings[lflag] = (settings[lflag] & ~termios.ICANON)
195
196            settings[lflag] = (settings[lflag] & ~termios.ECHO) # no echo
197
198            #ignore carriage returns on input
199            #settings[iflag] = (settings[iflag] | (termios.IGNCR)) #ignore cr
200
201            # 8N1 8bit word no parity one stop bit nohardware handshake ctsrts
202            # to set size have to mask out(clear) CSIZE bits and or in size
203            settings[cflag] = ((settings[cflag] & ~termios.CSIZE) | termios.CS8)
204            # no parity clear PARENB
205            settings[cflag] = (settings[cflag] & ~termios.PARENB)
206            #one stop bit clear CSTOPB
207            settings[cflag] = (settings[cflag] & ~termios.CSTOPB)
208            #no hardware handshake clear crtscts
209            settings[cflag] = (settings[cflag] & ~termios.CRTSCTS)
210
211            # in linux the speed flag does not equal value so always set it
212            speedattr = "B{0}".format(self.speed)  # convert numeric speed to attribute name string
213            speed = getattr(termios, speedattr)
214            settings[ispeed] = speed
215            settings[ospeed] = speed
216
217            termios.tcsetattr(self.fd, termios.TCSANOW, settings)
218            #print(settings)
219
220        self.opened = True
221
222    def reopen(self):
223        """
224        Idempotently open serial device port
225        """
226        self.close()
227        return self.open()
228
229    def close(self):
230        """Closes fd.
231
232        """
233        if self.fd:
234            os.close(self.fd)
235            self.fd = None
236            self.opened = False
237
238    def receive(self):
239        """
240        Reads nonblocking characters from serial device up to bs characters
241        Returns empty bytes if no characters available else returns all available.
242        In canonical mode no chars are available until newline is entered.
243        """
244        data = b''
245        try:
246            data = os.read(self.fd, self.bs)  #if no chars available generates exception
247        except OSError as ex1:  # ex1 is the target instance of the exception
248            if ex1.errno == errno.EAGAIN: #BSD 35, Linux 11
249                pass #No characters available
250            else:
251                raise #re raise exception ex1
252
253        return data
254
255    def send(self, data=b'\n'):
256        """
257        Writes data bytes to serial device port.
258        Returns number of bytes sent
259        """
260        try:
261            count = os.write(self.fd, data)
262        except OSError as ex1:  # ex1 is the target instance of the exception
263            if ex1.errno == errno.EAGAIN: #BSD 35, Linux 11
264                count = 0  # buffer full can't write
265            else:
266                raise #re raise exception ex1
267
268        return count
269
270
271class SerialNb(object):
272    """
273    Class to manage non blocking IO on serial device port using pyserial
274
275    Opens non blocking read file descriptor on serial port
276    Use instance method close to close file descriptor
277    Use instance methods get & put to read & write to serial device
278    Needs os module
279    """
280
281    def __init__(self, port=None, speed=9600, bs=1024):
282        """
283        Initialization method for instance.
284
285        port = serial device port path string
286        speed = serial port speed in bps
287        bs = buffer size for reads
288
289
290        """
291        self.serial = None  # Serial instance
292        self.port = port or os.ctermid() #default to console
293        self.speed = speed or 9600
294        self.bs = bs or 1024
295        self.opened = False
296
297    def open(self, port=None, speed=None, bs=None):
298        """
299        Opens fd on serial port in non blocking mode.
300
301        port is the serial port device path name or
302        if None then use os.ctermid() which returns path name of console
303        usually '/dev/tty'
304        """
305        if port is not None:
306            self.port = port
307        if speed is not None:
308            self.speed = speed
309        if bs is not None:
310            self.bs = bs
311
312        import serial  # import pyserial
313        self.serial = serial.Serial(port=self.port,
314                                    baudrate=self.speed,
315                                    timeout=0,
316                                    writeTimeout=0)
317        #self.serial.nonblocking()
318        self.serial.reset_input_buffer()
319        self.opened = True
320
321    def reopen(self):
322        """
323        Idempotently open serial device port
324        """
325        self.close()
326        return self.open()
327
328    def close(self):
329        """
330        Closes .serial
331        """
332        if self.serial:
333            self.serial.reset_output_buffer()
334            self.serial.close()
335            self.serial = None
336            self.opened = False
337
338    def receive(self):
339        """
340        Reads nonblocking characters from serial device up to bs characters
341        Returns empty bytes if no characters available else returns all available.
342        In canonical mode no chars are available until newline is entered.
343        """
344        data = b''
345        try:
346            data = self.serial.read(self.bs)  #if no chars available generates exception
347        except OSError as ex1:  # ex1 is the target instance of the exception
348            if ex1.errno == errno.EAGAIN: #BSD 35, Linux 11
349                pass #No characters available
350            else:
351                raise #re raise exception ex1
352
353        return data
354
355    def send(self, data=b'\n'):
356        """
357        Writes data bytes to serial device port.
358        Returns number of bytes sent
359        """
360        try:
361            count = self.serial.write(data)
362        except OSError as ex1:  # ex1 is the target instance of the exception
363            if ex1.errno == errno.EAGAIN: #BSD 35, Linux 11
364                count = 0  # buffer full can't write
365            else:
366                raise #re raise exception ex1
367
368        return count
369
370class Driver(object):
371    """
372    Nonblocking Serial Device Port Driver
373    """
374
375    def __init__(self,
376                 name=u'',
377                 uid=0,
378                 port=None,
379                 speed=9600,
380                 bs=1024,
381                 server=None):
382        """
383        Initialization method for instance.
384
385        Parameters:
386            name = user friendly name for driver
387            uid = unique identifier for driver
388            port = serial device port path string
389            speed = serial port speed in bps
390            canonical = canonical mode True or False
391            bs = buffer size for reads
392            server = serial port device server if any
393
394        Attributes:
395           name = user friendly name for driver
396           uid = unique identifier for driver
397           server = serial device server nonblocking
398           txes = deque of data bytes to send
399           rxbs = bytearray of data bytes received
400
401        """
402        self.name = name
403        self.uid = uid
404
405        if not server:
406            try:
407                import serial
408                self.server = SerialNb(port=port,
409                                       speed=speed,
410                                       bs=bs)
411
412            except ImportError as  ex:
413                console.terse("Error: importing pyserial\n{0}\n".format(ex))
414                self.server = DeviceNb(port=port,
415                                       speed=speed,
416                                       bs=bs)
417        else:
418            self.server = server
419
420        self.txes = deque()  # deque of data to send
421        self.rxbs = bytearray()  # byte array of data received
422
423    def serviceReceives(self):
424        """
425        Service receives until no more
426        """
427        while self.server.opened:
428            data = self.server.receive()  # bytes
429            if not data:
430                break
431            self.rxbs.extend(data)
432
433    def serviceReceiveOnce(self):
434        '''
435        Retrieve from server only one reception
436        '''
437        if self.server.opened:
438            data = self.server.receive()
439            if data:
440                self.rxbs.extend(data)
441
442    def clearRxbs(self):
443        """
444        Clear .rxbs
445        """
446        del self.rxbs[:]
447
448    def scan(self, start):
449        """
450        Returns offset of given start byte in self.rxbs
451        Returns None if start is not given or not found
452        If strip then remove any bytes before offset
453        """
454        offset = self.rxbs.find(start)
455        if offset < 0:
456            return None
457        return offset
458
459    def tx(self, data):
460        '''
461        Queue data onto .txes
462        '''
463        self.txes.append(data)
464
465    def _serviceOneTx(self):
466        """
467        Handle one tx data
468        """
469        data = self.txes.popleft()
470        count = self.server.send(data)
471        if count < len(data):  # put back unsent portion
472            self.txes.appendleft(data[count:])
473            return False  # blocked
474        console.profuse("{0}: Sent: {1}\n".format(self.name, data))
475        return True  # send more
476
477    def serviceTxes(self):
478        """
479        Service txes data
480        """
481        while self.txes and self.server.opened:
482            again = self._serviceOneTx()
483            if not again:
484                break  # blocked try again later
485
486    def serviceTxOnce(self):
487        '''
488        Service one data on the .txes deque to send through device
489        '''
490        if self.txes and self.server.opened:
491            self._serviceOneTx()
492
493