/
dispatcher.py
423 lines (368 loc) · 16.8 KB
/
dispatcher.py
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
# Copyright 2016-2021 Peppy Player peppy.player@gmail.com
#
# This file is part of Peppy Player.
#
# Peppy Player 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.
#
# Peppy Player 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 Peppy Player. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
import pygame
from pygame.time import Clock
from util.config import BUTTON_TYPE, USAGE, USE_LIRC, USE_ROTARY_ENCODERS, SCREEN_INFO, \
FRAME_RATE, SHOW_MOUSE_EVENTS, FLIP_TOUCH_XY, WIDTH, HEIGHT, MULTI_TOUCH, GPIO, ROTARY_VOLUME_UP, ROTARY_VOLUME_DOWN, ROTARY_VOLUME_MUTE, \
ROTARY_NAVIGATION_LEFT, ROTARY_NAVIGATION_RIGHT, ROTARY_NAVIGATION_SELECT, ROTARY_JITTER_FILTER, USE_BUTTONS, \
BUTTON_LEFT, BUTTON_RIGHT, BUTTON_UP, BUTTON_DOWN, BUTTON_SELECT, BUTTON_VOLUME_UP, BUTTON_VOLUME_DOWN, \
BUTTON_MUTE, BUTTON_PLAY_PAUSE, BUTTON_NEXT, BUTTON_PREVIOUS, BUTTON_HOME, BUTTON_POWEROFF
from util.keys import SELECT_EVENT_TYPE, kbd_keys, KEY_SUB_TYPE, SUB_TYPE_KEYBOARD, KEY_ACTION, KEY_KEYBOARD_KEY, USER_EVENT_TYPE, \
VOICE_EVENT_TYPE, KEY_END, REST_EVENT_TYPE
from event.gpiobutton import GpioButton
from event.i2cbuttons import I2CButtons
# Maps IR remote control keys to keyboard keys
lirc_keyboard_map = {"options": pygame.K_m,
"power": pygame.K_END,
"home": pygame.K_HOME,
"pause": pygame.K_SPACE,
"play": pygame.K_SPACE,
"ok": pygame.K_RETURN,
"left": pygame.K_LEFT,
"right": pygame.K_RIGHT,
"up": pygame.K_UP,
"down": pygame.K_DOWN,
"next": pygame.K_PAGEUP,
"previous": pygame.K_PAGEDOWN,
"mute": pygame.K_x,
"back": pygame.K_ESCAPE,
"setup": pygame.K_s,
"root": pygame.K_r,
"parent": pygame.K_p,
"audio": pygame.K_a,
"0": pygame.K_0,
"1": pygame.K_1,
"2": pygame.K_2,
"3": pygame.K_3,
"4": pygame.K_4,
"5": pygame.K_5,
"6": pygame.K_6,
"7": pygame.K_7,
"8": pygame.K_8,
"9": pygame.K_9}
class EventDispatcher(object):
""" Event Dispatcher
This class runs two separate event loops:
- Main event loop which handles mouse, keyboard and user events
- LIRC event loop which handles LIRC events
"""
def __init__(self, screensaver_dispatcher, util):
""" Initializer
:param screensaver_dispatcher: reference to screensaver dispatcher used for forwarding events
:param util: utility object which keeps configuration settings and utility methods
"""
self.screensaver_dispatcher = screensaver_dispatcher
self.config = util.config
self.frame_rate = self.config[SCREEN_INFO][FRAME_RATE]
self.screen_width = self.config[SCREEN_INFO][WIDTH]
self.screen_height = self.config[SCREEN_INFO][HEIGHT]
self.flip_touch_xy = self.config[SCREEN_INFO][FLIP_TOUCH_XY]
self.multi_touch = self.config[SCREEN_INFO][MULTI_TOUCH]
self.show_mouse_events = self.config[SHOW_MOUSE_EVENTS]
self.screensaver_dispatcher.frame_rate = self.frame_rate
self.lirc = None
self.lirc_thread = None
self.init_lirc()
self.init_buttons()
self.init_rotary_encoders()
self.volume_initialized = False
self.screensaver_was_running = False
self.run_dispatcher = True
self.mouse_events = [pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION]
self.user_events = [USER_EVENT_TYPE, VOICE_EVENT_TYPE, REST_EVENT_TYPE]
self.multi_touch_screen = None
self.mts_state = [False for _ in range(10)]
self.move_enabled = False
self.poweroff_flag = 0
def set_current_screen(self, current_screen):
""" Set current screen.
All events are applicable for the current screen only.
Logo screensaver needs current screen to get the current logo.
:param current_screen: reference to the current screen
"""
self.current_screen = current_screen
self.screensaver_dispatcher.set_current_screen(current_screen)
def init_lirc(self):
""" LIRC initializer.
Starts new thread for IR events handling.
It's not executed if IR remote was disabled in config.txt.
"""
if not self.config[USAGE][USE_LIRC]:
return
try:
import pylirc
self.lirc = pylirc
self.lirc.init("radio")
self.lirc.blocking(0)
except ImportError:
logging.error("PYLIRC library not found")
def init_rotary_encoders(self):
""" Rotary encoders (RE) initializer.
This is executed only if RE enabled in config.txt.
RE events will be wrapped into keyboard events.
"""
if not self.config[GPIO][USE_ROTARY_ENCODERS]:
return
volume_up = self.config[GPIO][ROTARY_VOLUME_UP]
volume_down = self.config[GPIO][ROTARY_VOLUME_DOWN]
mute = self.config[GPIO][ROTARY_VOLUME_MUTE]
move_left = self.config[GPIO][ROTARY_NAVIGATION_LEFT]
move_right = self.config[GPIO][ROTARY_NAVIGATION_RIGHT]
select = self.config[GPIO][ROTARY_NAVIGATION_SELECT]
jitter_filter = self.config[GPIO][ROTARY_JITTER_FILTER]
from event.rotary import RotaryEncoder
if volume_up and volume_down and mute and jitter_filter:
try:
RotaryEncoder(int(volume_up), int(volume_down), int(mute), pygame.K_KP_PLUS, pygame.K_KP_MINUS, pygame.K_x, int(jitter_filter))
except Exception as e:
logging.debug(e)
if move_right and move_left and select and jitter_filter:
try:
RotaryEncoder(int(move_right), int(move_left), int(select), pygame.K_RIGHT, pygame.K_LEFT, pygame.K_RETURN, int(jitter_filter))
except Exception as e:
logging.debug(e)
def init_buttons(self):
""" GPIO buttons initializer.
This is executed only if buttons were enabled in config.txt.
Button events will be wrapped into keyboard events.
"""
if not self.config[GPIO][USE_BUTTONS] or not self.config[GPIO][BUTTON_TYPE]:
return
if self.config[GPIO][BUTTON_TYPE] == "GPIO":
self.init_gpio_buttons()
elif self.config[GPIO][BUTTON_TYPE] == "I2C":
self.i2c_buttons = I2CButtons(self.config)
def init_gpio_buttons(self):
""" Initializae GPIO buttons """
self.init_gpio_button(self.config[GPIO][BUTTON_LEFT], pygame.K_LEFT)
self.init_gpio_button(self.config[GPIO][BUTTON_RIGHT], pygame.K_RIGHT)
self.init_gpio_button(self.config[GPIO][BUTTON_UP], pygame.K_UP)
self.init_gpio_button(self.config[GPIO][BUTTON_DOWN], pygame.K_DOWN)
self.init_gpio_button(self.config[GPIO][BUTTON_SELECT], pygame.K_RETURN)
self.init_gpio_button(self.config[GPIO][BUTTON_VOLUME_UP], pygame.K_KP_PLUS)
self.init_gpio_button(self.config[GPIO][BUTTON_VOLUME_DOWN], pygame.K_KP_MINUS)
self.init_gpio_button(self.config[GPIO][BUTTON_MUTE], pygame.K_x)
self.init_gpio_button(self.config[GPIO][BUTTON_PLAY_PAUSE], pygame.K_SPACE)
self.init_gpio_button(self.config[GPIO][BUTTON_NEXT], pygame.K_RIGHT)
self.init_gpio_button(self.config[GPIO][BUTTON_PREVIOUS], pygame.K_LEFT)
self.init_gpio_button(self.config[GPIO][BUTTON_HOME], pygame.K_HOME)
self.init_gpio_button(self.config[GPIO][BUTTON_POWEROFF], pygame.K_END)
def init_gpio_button(self, pin, key):
""" Initialize GPIO button
:param pin: GPIO pin number
:param key: keyboard key
"""
if pin:
try:
GpioButton(int(pin), key)
except Exception as e:
logging.debug(e)
def handle_lirc_event(self, code):
""" LIRC event handler.
To simplify event handling it wraps IR events into user event with keyboard sub-type.
For one IR event it generates two events - one for key down and one for key up.
:param code: IR code
"""
if self.screensaver_dispatcher.saver_running:
self.screensaver_dispatcher.cancel_screensaver()
return
d = {}
d[KEY_SUB_TYPE] = SUB_TYPE_KEYBOARD
d[KEY_ACTION] = pygame.KEYDOWN
d[KEY_KEYBOARD_KEY] = None
try:
d[KEY_KEYBOARD_KEY] = lirc_keyboard_map[code[0]]
if code[0] == "power":
if self.poweroff_flag == 1:
self.shutdown()
else:
self.poweroff_flag = 1
else:
self.poweroff_flag = 0
logging.debug("Received IR key: %s", d[KEY_KEYBOARD_KEY])
except KeyError:
logging.debug("Received not supported key: %s", code[0])
pass
if d[KEY_KEYBOARD_KEY]:
event = pygame.event.Event(USER_EVENT_TYPE, **d)
event.source = "lirc"
pygame.event.post(event)
d[KEY_ACTION] = pygame.KEYUP
event = pygame.event.Event(USER_EVENT_TYPE, **d)
event.source = "lirc"
pygame.event.post(event)
def handle_keyboard_event(self, event):
""" Keyboard event handler.
Wraps keyboard events into user event. Exits upon Ctrl-C.
Distinguishes key up and key down.
:param event: event to handle
"""
keys = pygame.key.get_pressed()
if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]) and event.key == pygame.K_c:
self.shutdown(event)
elif event.type == pygame.KEYDOWN or event.type == pygame.KEYUP:
if event.type == pygame.KEYUP:
if event.key == pygame.K_END:
if self.poweroff_flag == 1:
self.shutdown(event)
else:
self.poweroff_flag = 1
else:
self.poweroff_flag = 0
if self.screensaver_dispatcher.saver_running:
if event.type == pygame.KEYUP:
self.screensaver_dispatcher.cancel_screensaver(event)
return
self.handle_event(event)
d = {}
d[KEY_SUB_TYPE] = SUB_TYPE_KEYBOARD
d[KEY_ACTION] = event.type
d[KEY_KEYBOARD_KEY] = event.key
event = pygame.event.Event(USER_EVENT_TYPE, **d)
pygame.event.post(event)
def handle_event(self, event):
""" Forward event to the current screen and screensaver dispatcher
:param event: event to handle
"""
self.screensaver_dispatcher.handle_event(event)
self.current_screen.handle_event(event)
def handle_single_touch(self):
""" Handle single touch events """
for event in pygame.event.get():
source = getattr(event, "source", None)
if source != "browser" and self.flip_touch_xy and event.type in self.mouse_events: # not browser event
x, y = event.pos
new_x = self.screen_width - x - 1
new_y = self.screen_height - y - 1
event.pos = (new_x, new_y)
s = str(event)
if self.show_mouse_events:
logging.debug("Received event: %s", s)
if event.type == pygame.QUIT:
self.shutdown(event)
elif (event.type == pygame.KEYDOWN or event.type == pygame.KEYUP) and source != "lirc":
self.handle_keyboard_event(event)
elif event.type in self.mouse_events or event.type in self.user_events:
if self.screensaver_dispatcher.saver_running:
if event.type == pygame.MOUSEBUTTONUP:
self.screensaver_dispatcher.cancel_screensaver(event)
return
else:
self.handle_poweroff(event)
self.handle_event(event)
elif event.type == SELECT_EVENT_TYPE:
self.handle_event(event)
def get_event(self, event_type, x, y):
""" Prepare event
:param event_type: event type
:param x: X coordinate
:param y: Y coordinate
:return: event
"""
event = pygame.event.Event(event_type)
event.pos = (x, y)
event.button = 1
return event
def handle_multi_touch(self):
""" Handle multi-touch events """
for touch in self.multi_touch_screen.poll():
if self.mts_state[touch.slot] != touch.valid:
if touch.valid: # pressed
self.handle_event(self.get_event(pygame.MOUSEBUTTONDOWN, touch.x, touch.y))
self.move_enabled = True
else: # released
self.handle_event(self.get_event(pygame.MOUSEBUTTONUP, touch.x, touch.y))
self.move_enabled = False
self.mts_state[touch.slot] = touch.valid
else:
if self.move_enabled and touch.valid: # move
self.handle_event(self.get_event(pygame.MOUSEMOTION, touch.x, touch.y))
for event in pygame.event.get():
s = str(event)
source = getattr(event, "source", None)
if self.show_mouse_events:
logging.debug("Received event: %s", s)
if event.type == pygame.QUIT:
self.shutdown(event)
elif (event.type == pygame.KEYDOWN or event.type == pygame.KEYUP) and source != "lirc":
self.handle_keyboard_event(event)
elif event.type in self.user_events:
self.handle_poweroff(event)
self.handle_event(event)
elif (event.type in self.mouse_events) and source == "browser":
self.poweroff_flag = 0
self.handle_event(event)
def handle_poweroff(self, event):
""" Handle poweroff hardware button
:param event: event object
"""
if event.type == USER_EVENT_TYPE and hasattr(event, "action") and event.action == pygame.KEYUP:
k = getattr(event, "keyboard_key", None)
if k and k == kbd_keys[KEY_END]:
if self.poweroff_flag == 1:
self.shutdown(event)
else:
self.poweroff_flag = 1
else:
self.poweroff_flag = 0
if event.type == pygame.MOUSEBUTTONUP:
self.poweroff_flag = 0
def get_handler(self):
""" Get either single or multi touch handler
:return: event handler
"""
handler = None
if self.multi_touch:
try:
from event.ft5406peppy import Touchscreen
self.multi_touch_screen = Touchscreen()
handler = self.handle_multi_touch
except Exception as e:
logging.debug("%s", str(e))
else:
handler = self.handle_single_touch
if not handler:
os._exit(0)
else:
return handler
def dispatch(self, player, shutdown):
""" Dispatch events.
Runs the main event loop. Redirects events to corresponding handler.
Distinguishes four types of events:
- Quit event - when user closes window (Windows only)
- Keyboard events
- Mouse events
- User Events
:param player: reference to player object
"param shutdown: shutdown method to use when user exits
"""
self.player = player
self.shutdown = shutdown
handler = self.get_handler()
pygame.event.clear()
clock = Clock()
while self.run_dispatcher:
handler()
if self.lirc != None:
code = self.lirc.nextcode()
if code != None:
self.handle_lirc_event(code)
self.current_screen.refresh()
self.screensaver_dispatcher.refresh()
clock.tick(self.frame_rate)