Browse Source

Initial keyboard implementation

master
Dirk Alders 4 years ago
parent
commit
460a086149
1 changed files with 314 additions and 0 deletions
  1. 314
    0
      __init__.py

+ 314
- 0
__init__.py View File

@@ -0,0 +1,314 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+"""
5
+keyboard (Keyboard Module)
6
+==========================
7
+
8
+**Author:**
9
+
10
+* Dirk Alders <sudo-dirk@mount-mockery.de>
11
+
12
+**Description:**
13
+
14
+    This Module supports functions and classes for collecting keyboard strokes.
15
+
16
+**Submodules:**
17
+
18
+* :class:`keyboard.keyboard`
19
+* :class:`keyboard.keyboard_csp`
20
+
21
+**Unittest:**
22
+
23
+    See also the :download:`unittest <../../keyboard/_testresults_/unittest.pdf>` documentation.
24
+"""
25
+__DEPENDENCIES__ = ['stringtools', 'task']
26
+import stringtools
27
+import task
28
+
29
+import evdev
30
+import logging
31
+import select
32
+import sys
33
+import time
34
+
35
+logger_name = 'KEYBOARD'
36
+logger = logging.getLogger(logger_name)
37
+
38
+__DESCRIPTION__ = """The Module {\\tt %s} is designed to fetch data from a keyboatd (e.g. RFID-Reader).
39
+For more Information read the sphinx documentation.""" % __name__.replace('_', '\_')
40
+"""The Module Description"""
41
+__INTERPRETER__ = (2, 3)
42
+"""The Tested Interpreter-Versions"""
43
+
44
+
45
+class keyboard(object):
46
+    LOG_PREFIX = 'KBD:'
47
+
48
+    WARN_MSG_INIT_DEVICES = 1
49
+    WARN_MSG_RUNTIME = 2
50
+
51
+    LEFT_SHIFT = 42
52
+    RIGHT_SHIFT = 54
53
+    CAPS_LOCK = 58
54
+    NUM_LOCK = 69
55
+    SCROLL_LOCK = 70
56
+
57
+    STATE_BITS_LEFT = {
58
+        LEFT_SHIFT: 1,
59
+        CAPS_LOCK: 2,     # CAPS LOCK
60
+        29: 4,     # Left Ctrl
61
+        56: 8,     # Alt
62
+        125: 32,   # Left Windows
63
+        NUM_LOCK: 64,    # Num Lock
64
+        SCROLL_LOCK: 128,   # Scroll Lock
65
+    }
66
+
67
+    STATE_BITS_RIGHT = {
68
+        RIGHT_SHIFT: 1,
69
+        97: 4,     # Right Ctrl
70
+        100: 16,   # Alt Gr
71
+        126: 32,   # Right Windows
72
+    }
73
+
74
+    SCANCODES = {
75
+        # Scancode: ASCIICode
76
+        2: u'1', 3: u'2', 4: u'3', 5: u'4', 6: u'5', 7: u'6', 8: u'7', 9: u'8', 10: u'9', 11: u'0', 12: u'-', 13: u'=',
77
+        16: u'q', 17: u'w', 18: u'e', 19: u'r', 20: u't', 21: u'y', 22: u'u', 23: u'i', 24: u'o', 25: u'p', 26: u'[', 27: u']',
78
+        28: u'\n', 30: u'a', 31: u's', 32: u'd', 33: u'f', 34: u'g', 35: u'h', 36: u'j', 37: u'k', 38: u'l', 39: u';', 40: u'"',
79
+        41: u'`', 43: u'\\', 44: u'z', 45: u'x', 46: u'c', 47: u'v', 48: u'b', 49: u'n', 50: u'm', 51: u',', 52: u'.', 53: u'/',
80
+        57: u' '
81
+    }
82
+
83
+    CAPSCODES = {
84
+        2: u'!', 3: u'@', 4: u'#', 5: u'$', 6: u'%', 7: u'^', 8: u'&', 9: u'*', 10: u'(', 11: u')', 12: u'_', 13: u'+',
85
+        16: u'Q', 17: u'W', 18: u'E', 19: u'R', 20: u'T', 21: u'Y', 22: u'U', 23: u'I', 24: u'O', 25: u'P', 26: u'{', 27: u'}',
86
+        30: u'A', 31: u'S', 32: u'D', 33: u'F', 34: u'G', 35: u'H', 36: u'J', 37: u'K', 38: u'L', 39: u':', 40: u'\'', 41: u'~',
87
+        43: u'|', 44: u'Z', 45: u'X', 46: u'C', 47: u'V', 48: u'B', 49: u'N', 50: u'M', 51: u'<', 52: u'>', 53: u'?',
88
+        57: u' '
89
+    }
90
+
91
+    CAPSKEYS = list(range(16, 26)) + list(range(30, 39)) + list(range(44, 51))
92
+
93
+    def __init__(self, device_list):
94
+        if sys.version_info >= (3, 0):
95
+            str_types = [str]
96
+        else:
97
+            str_types = [str, unicode]
98
+        if type(device_list) in str_types:
99
+            device_list = [device_list]
100
+        self.__device_list__ = []
101
+        for device in device_list:
102
+            if device is not None:
103
+                self.__device_list__.append(device)
104
+        self.__devices__ = None
105
+        self.__last_warn_message__ = None
106
+        #
107
+        self.__init_rx_buffer__()
108
+        #
109
+        self.__data_available_callback__ = {}
110
+        #
111
+        self.__state_byte__ = 0
112
+        self.__state_byte_left__ = 0
113
+        self.__state_byte_right__ = 0
114
+        self.__rx_queue__ = task.threaded_queue()
115
+        self.__rx_queue__.enqueue(5, self.__receive_task__)
116
+        self.__rx_queue__.run()
117
+        self.__cb_queue__ = task.threaded_queue()
118
+        self.__cb_queue__.run()
119
+
120
+    def __init_rx_buffer__(self):
121
+        self.__rx_data__ = {}
122
+        for device in self.__device_list__:
123
+            if device is not None:
124
+                self.__rx_data__[device] = ''
125
+
126
+    def __calc_keyboard_lock_state__(self, device):
127
+        leds = device.leds()
128
+        if evdev.ecodes.LED_NUML in leds:
129
+            self.__state_byte_left__ |= self.STATE_BITS_LEFT[self.NUM_LOCK]
130
+        else:
131
+            self.__state_byte_left__ &= ~self.STATE_BITS_LEFT[self.NUM_LOCK]
132
+        if evdev.ecodes.LED_CAPSL in leds:
133
+            self.__state_byte_left__ |= self.STATE_BITS_LEFT[self.CAPS_LOCK]
134
+        else:
135
+            self.__state_byte_left__ &= ~self.STATE_BITS_LEFT[self.CAPS_LOCK]
136
+        if evdev.ecodes.LED_SCROLLL in leds:
137
+            self.__state_byte_left__ |= self.STATE_BITS_LEFT[self.SCROLL_LOCK]
138
+        else:
139
+            self.__state_byte_left__ &= ~self.STATE_BITS_LEFT[self.SCROLL_LOCK]
140
+        self.__state_byte__ = self.__state_byte_left__ | self.__state_byte_right__
141
+
142
+    def __calc_keyboard_state__(self, cevent):
143
+        if cevent.scancode in self.STATE_BITS_LEFT:
144
+            if cevent.scancode not in [self.CAPS_LOCK, self.NUM_LOCK, self.SCROLL_LOCK]:
145
+                if cevent.keystate == 1:
146
+                    self.__state_byte_left__ |= self.STATE_BITS_LEFT[cevent.scancode]
147
+                elif cevent.keystate == 0:
148
+                    self.__state_byte_left__ &= ~self.STATE_BITS_LEFT[cevent.scancode]
149
+            return True
150
+        elif cevent.scancode in self.STATE_BITS_RIGHT:
151
+            if cevent.keystate == 1:
152
+                self.__state_byte_right__ |= self.STATE_BITS_RIGHT[cevent.scancode]
153
+            elif cevent.keystate == 0:
154
+                self.__state_byte_right__ &= ~self.STATE_BITS_RIGHT[cevent.scancode]
155
+            return True
156
+        else:
157
+            return False
158
+
159
+    def __get_ascii__(self, scancode):
160
+        shift = self.__state_byte__ & self.STATE_BITS_LEFT[self.LEFT_SHIFT]
161
+        caps = self.__state_byte__ & self.STATE_BITS_LEFT[self.CAPS_LOCK]
162
+        if not caps and not shift:
163
+            return self.SCANCODES.get(scancode, None)
164
+        elif not caps and shift:
165
+            return self.CAPSCODES.get(scancode, None)
166
+        elif caps and shift:
167
+            return self.SCANCODES.get(scancode, None)
168
+        else:
169
+            return self.CAPSCODES.get(scancode, None)
170
+
171
+    def __append_rx_buffer__(self, device, c):
172
+        self.__rx_data__[device.fn] += c
173
+        if sys.version_info >= (3, 0):
174
+            logger.debug('%s RX[%s] <- "%s"', self.LOG_PREFIX, device.fn, stringtools.hexlify(bytes(c, 'ascii')))
175
+        else:
176
+            logger.debug('%s RX[%s] <- "%s"', self.LOG_PREFIX, device.fn, stringtools.hexlify(c))
177
+        return len(c) > 0
178
+
179
+    def __parse_ascii__(self, cevent, device):
180
+        self.__calc_keyboard_lock_state__(device)
181
+        if self.__calc_keyboard_state__(cevent):
182
+            pass
183
+        else:
184
+            if cevent.keystate == 1:
185
+                c = self.__get_ascii__(cevent.scancode)
186
+                if c is not None:
187
+                    self.__append_rx_buffer__(device, c)
188
+                else:
189
+                    logger.warning('%s No character in dictionary for scancode %d', self.LOG_PREFIX, cevent.scancode)
190
+
191
+    def register_callback(self, callback, device_name):
192
+        """
193
+        :param callback: The callback which will be executed, when data is available.
194
+        :type callback: function
195
+
196
+        This method sets the callback which is executed, if data is available. You need to execute :func:`receive` of the instance
197
+        given with the first argument.
198
+
199
+        .. note:: The :func:`callback` is executed with these arguments:
200
+
201
+            :param self: This communication instance
202
+        """
203
+        self.__data_available_callback__[device_name] = callback
204
+
205
+    def receive(self, device_name, timeout=1, num=None):
206
+        rv = None
207
+        tm = time.time()
208
+        while (num is not None and len(self.__rx_data__.get(device_name, [])) < num) or (num is None and len(self.__rx_data__.get(device_name, [])) < 1):
209
+            if time.time() > tm + timeout:
210
+                logger.warning('%s TIMEOUT (%ss): Not enough data in buffer. Requested %s and buffer size is %d.', self.LOG_PREFIX, repr(timeout), repr(num or 'all'), len(self.__receive_buffer__))
211
+                return None
212
+            time.sleep(0.05)
213
+        if num is None:
214
+            rv = self.__rx_data__[device_name]
215
+        else:
216
+            rv = self.__rx_data__[device_name][:num]
217
+        self.__rx_data__[device_name] = self.__rx_data__[device_name][len(rv):]
218
+        return rv
219
+
220
+    def __receive_task__(self, queue_inst):
221
+        if self.__devices__ is None:
222
+            # A mapping of file descriptors (integers) to InputDevice instances.
223
+            try:
224
+                self.__devices__ = map(evdev.InputDevice, self.__device_list__)
225
+                self.__devices__ = {dev.fd: dev for dev in self.__devices__}
226
+                logger.info('%s Listening to the following Devices: %s', self.LOG_PREFIX, repr(self.__device_list__))
227
+            except OSError:
228
+                self.__devices__ = None
229
+                if self.__last_warn_message__ is not self.WARN_MSG_INIT_DEVICES:
230
+                    logger.warning('%s Error while initialising the devices: %s', self.LOG_PREFIX, repr(self.__device_list__))
231
+                    self.__last_warn_message__ = self.WARN_MSG_INIT_DEVICES
232
+        else:
233
+            r = select.select(self.__devices__, [], [])[0]
234
+            for fd in r:
235
+                try:
236
+                    for event in self.__devices__[fd].read():
237
+                        if event.type == evdev.ecodes.EV_KEY:
238
+                            if self.__devices__[fd].fn in self.__device_list__:
239
+                                self.__parse_ascii__(evdev.categorize(event), self.__devices__[fd])
240
+                except IOError:
241
+                    if self.__last_warn_message__ is not self.WARN_MSG_RUNTIME:
242
+                        logger.warning('%s Error reading from device: %s', self.LOG_PREFIX, repr(self.__devices__[fd].fn))
243
+                        self.__devices__ = None
244
+                        self.__last_warn_message__ = self.WARN_MSG_RUNTIME
245
+            # enqueue required callbacks
246
+            for device_name in self.__device_list__:
247
+                if len(self.__rx_data__.get(device_name, [])) > 0:
248
+                    self.__cb_queue__.enqueue(5, self.__callback_execution_queue__, device_name)
249
+        #
250
+        queue_inst.enqueue(5, self.__receive_task__)
251
+
252
+    def __callback_execution_queue__(self, queue_inst, device_name):
253
+        cb = self.__data_available_callback__.get(device_name)
254
+        if cb is not None and len(self.__rx_data__.get(device_name, [])) > 0:
255
+            logger.debug("%s Executing callback: %s(self, %s)", self.LOG_PREFIX, cb.__name__, repr(device_name))
256
+            cb(self, device_name)
257
+        if len(self.__rx_data__.get(device_name, [])) > 0:
258
+            queue_inst.enqueue(5, self.__callback_execution_queue__, device_name)
259
+
260
+    def close(self):
261
+        """
262
+        This method closes the active keyboard channel, if channel exists.
263
+        """
264
+        self.__rx_queue__.stop()
265
+        self.__rx_queue__.join()
266
+        self.__cb_queue__.stop()
267
+        self.__cb_queue__.join()
268
+
269
+    def __del__(self):
270
+        self.close()
271
+
272
+
273
+class keyboard_csp(keyboard):
274
+    """
275
+    Class to gather keystrokes for specific devices. All data till \\\\n will be collected to a frame.
276
+
277
+    :param device_list: The device to listen to.
278
+    :type device_list: str or list
279
+    :param rx_lvl: The log level for single keystrokes
280
+    :type rx_lvl: int
281
+
282
+    **Example:**
283
+
284
+    .. literalinclude:: ../../keyboard/_examples_/keyboard_csp.py
285
+
286
+    Will result to the following output (if user scans an rfid-tag and types "12345\\\\n"):
287
+
288
+    .. literalinclude:: ../../keyboard/_examples_/keyboard_csp.log
289
+    """
290
+    LOG_PREFIX = 'KBD_CSP:'
291
+
292
+    def __init_rx_buffer__(self):
293
+        self.__rx_data__ = {}
294
+        self.__csp__ = {}
295
+        for device in self.__device_list__:
296
+            if device is not None:
297
+                self.__rx_data__[device] = []
298
+                self.__csp__[device] = stringtools.csp.csp()
299
+
300
+    def __append_rx_buffer__(self, device, c):
301
+        if sys.version_info >= (3, 0):
302
+            content = self.__csp__[device.fn].process(bytes(c, 'ascii'))
303
+        else:
304
+            content = self.__csp__[device.fn].process(str(c))
305
+        if len(content) > 0:
306
+            for msg in content:
307
+                logger.debug('%s RX[%s] <- "%s"', self.LOG_PREFIX, device.fn, stringtools.hexlify(msg))
308
+            self.__rx_data__[device.fn] += content
309
+            return True
310
+        else:
311
+            return False
312
+
313
+    def receive(self, device_name, timeout=1):
314
+        return keyboard.receive(self, device_name, timeout=timeout, num=1)[0]

Loading…
Cancel
Save