|
@@ -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]
|