-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathkivy_museum_app.py
executable file
·329 lines (291 loc) · 13.8 KB
/
kivy_museum_app.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__status__ = "Testing"
# necessary library imports
import os, sys
os.environ['KIVY_GL_BACKEND'] = 'gl'
#os.environ['KIVY_BCM_DISPMANX_ID'] = '3' # tells Kivy which display to work with (may need to be changed for tablet interface)
import RPi.GPIO as GPIO
import time, random
import pigpio
import subprocess, threading, multiprocessing
import pygame, pigpio
from math import copysign
import traceback
import threading
from functools import partial
import serial
# kivy library imports
from kivy.app import App
from kivy.core.window import Window
from kivy.graphics import Color, Rectangle, Line
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.switch import Switch
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelHeader
from kivy.clock import Clock
from kivy.properties import ListProperty, NumericProperty
from kivy.core.text import Label as CoreLabel
from kivy.event import EventDispatcher
from kivy.uix.slider import Slider
# start pigpio daemon
subprocess.call("sudo pigpiod &", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# define GPIO pins as variables for readability
GPIO_LASER = 27
GPIO_SERVOPIN_X = 17 # 540 (right)-2300 (left)
GPIO_SERVOPIN_Y = 4 #550-1750
GPIO_START_BUTTON = 13
# servo variables
servo_step_length = 5
servo_pause = 0.01
# GPIO setup
GPIO.setmode(GPIO.BCM) # set pin numbering
GPIO.setup(GPIO_LASER, GPIO.OUT) # laser control setup
# start pygame to play audio files
pygame.init()
# initialize audio files
audiofile_dir = "./audiofiles/"
audiofile_list = os.listdir(audiofile_dir)
audiofile_list.sort()
audiofile_list.pop()
del audiofile_list[0]
audiofiles = []
for audiofile in audiofile_list:
audiofiles.append(pygame.mixer.Sound(audiofile_dir+audiofile))
#full_audio = pygame.mixer.Sound("./audiofiles/fdr_full.wav")
#sample0 = pygame.mixer.Sound("./audiofiles/sample_0.wav")
#sample1 = pygame.mixer.Sound("./audiofiles/sample_1.wav")
#sample2 = pygame.mixer.Sound("./audiofiles/sample_2.wav")
#sample3 = pygame.mixer.Sound("./audiofiles/sample_3.wav")
#sample4 = pygame.mixer.Sound("./audiofiles/sample_4.wav")
# start pigpio instance
servo = pigpio.pi()
# define and set initial orientation
initial_x = 1050
initial_y = 800
servo.set_servo_pulsewidth(GPIO_SERVOPIN_X, initial_x)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_Y, initial_y)
# zero servos
time.sleep(0.1)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_X, 0)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_Y, 0)
###########################
## Define time sequences and coordinates
# Main Presentation
coordinates_main = [(969, 572), (793, 742), (1071, 700), (687, 573), (692, 892), (989, 551), (719, 880), (951, 754), (1181, 640), (929, 649), (854, 724), (603, 592), (804, 777), (815, 698), (652, 632), (804, 883), (627, 585), (932, 879), (627, 736), (1121, 711)]
timestamps_main = [15, 29, 41, 56, 66, 76, 90, 104, 114, 125, 139, 154, 164, 176, 186, 201, 216, 226, 241, 252]
# Option 1
coordinates_option1 = [(862, 584), (1340, 799), (939, 596), (878, 792), (864, 570), (966, 894), (1350, 831), (860, 690), (1297, 640), (971, 833), (1298, 885), (883, 678), (1298, 894), (871, 853), (694, 846), (1004, 703), (834, 876), (1393, 669), (993, 719), (1114, 886)]
timestamps_option1 = [13, 23, 36, 51, 63, 75, 90, 105, 118, 133, 143, 155, 169, 184, 196, 208, 221, 235, 248, 263]
# Option 2
coordinates_option2 = [(1231, 870), (814, 761), (1177, 659), (673, 710), (834, 640), (900, 625), (1388, 895), (1353, 738), (765, 577), (1216, 778), (611, 852), (770, 606), (1189, 721), (688, 900), (927, 702), (902, 706), (730, 670), (842, 725), (759, 815), (629, 711)]
timestamps_option2 = [5, 20, 30, 41, 56, 66, 80, 90, 105, 116, 129, 140, 154, 169, 182, 197, 210, 224, 236, 249]
# Option 3
coordinates_option3 = [(853, 758), (607, 750), (704, 693), (1269, 592), (1136, 743), (1281, 839), (1042, 631), (1243, 667), (1186, 645), (866, 740), (1338, 765), (751, 664), (1066, 760), (1145, 582), (726, 896), (935, 816), (939, 851), (1136, 674), (1280, 550), (776, 895)]
timestamps_option3 = [3, 24, 36, 51, 65, 75, 87, 102, 115, 130, 142, 154, 169, 183, 193, 208, 222, 232, 244, 255]
###########################
class RootWidget(FloatLayout):
'''
Main class for defining app layout and button functionality. Below we define the iniital X and Y coordinates and set
the stop playing flag to False.
'''
X_coord = NumericProperty(initial_x)
Y_coord = NumericProperty(initial_y)
stop_playing = False
def __init__(self, **kwargs):
'''
Here we define the buttons, their locations, and callbacks. Buttons can easily be added or these modified as necessary as the code is extremely generalized.
To do this, one must define the coordinate list, the corresponding timestamps in seconds when the laser should point in that direction, and the audio list
entry (ID). N.B. Python indexing starts at 0.
'''
super(RootWidget, self).__init__(**kwargs)
main_presentation = Button(text="Main\nPresentation", halign='center', size_hint=(.35, .85), pos_hint={'center_x': .25, 'center_y': .5},font_size='25sp')
main_presentation.bind(on_press = partial(self.button_callback, coord_list = coordinates_main, timestamp_list = timestamps_main, audio_id = 0))
self.add_widget(main_presentation)
option1 = Button(text="Option 1",size_hint=(.25, .25), pos_hint={'center_x': .7, 'center_y': .8},font_size='25sp')
option1.bind(on_press = partial(self.button_callback, coord_list = coordinates_option1, timestamp_list = timestamps_option1, audio_id = 1))
self.add_widget(option1)
option2 = Button(text="Option 2",size_hint=(.25, .25), pos_hint={'center_x': .7, 'center_y': .5},font_size='25sp')
option2.bind(on_press = partial(self.button_callback, coord_list = coordinates_option2, timestamp_list = timestamps_option2, audio_id = 2))
self.add_widget(option2)
option3 = Button(text="Option 3",size_hint=(.25, .25), pos_hint={'center_x': .7, 'center_y': .2},font_size='25sp')
option3.bind(on_press = partial(self.button_callback, coord_list = coordinates_option3, timestamp_list = timestamps_option3, audio_id = 3))
self.add_widget(option3)
shutdown = Button(text="Shutdown",size_hint=(.1, .1), pos_hint={'center_x': .9, 'center_y': .12},font_size='25sp')
shutdown.bind(on_press = self.shutdown_callback)
self.add_widget(shutdown)
def servo_stepper(self, TARGET_X, TARGET_Y):
'''
Method to handle the movement of the servos from current to target coordinates. The purpose of discretizing the laser movement
is to minimize noise from the servos.
Input:
TARGET_X: target pulsewidth x-axis
TARGET_Y: target pulsewidth y-axis
Output:
Limits the servo movement to maximum increments of 50 from current
location to target x,y location. This is to reduce the noise
produced by servo movement.
'''
# get delta between target and current positions
delta_x = TARGET_X - self.X_coord
delta_y = TARGET_Y - self.Y_coord
# loop to step from current to target positions and stop if stop_playing is True
while abs(delta_x) > servo_step_length or abs(delta_y) > servo_step_length or self.stop_playing:
# is the loop passed because stop_playing is True, leave the loop
if self.stop_playing:
return
# else update the x and y coordinates with brief delay to allow the movement to be executed.
if abs(delta_x) > servo_step_length:
# update coordinates in code
self.X_coord += copysign(servo_step_length, delta_x)
# send command to servo pin
servo.set_servo_pulsewidth(GPIO_SERVOPIN_X, self.X_coord)
# brief pause
time.sleep(servo_pause)
# tell the servo pin to stop (minimizes jittering)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_X, 0)
# brief pause
time.sleep(servo_pause)
if abs(delta_y) > servo_step_length:
self.Y_coord += copysign(servo_step_length, delta_y)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_Y, self.Y_coord)
time.sleep(servo_pause)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_Y, 0)
time.sleep(servo_pause)
# update deltas
delta_x = TARGET_X - self.X_coord
delta_y = TARGET_Y - self.Y_coord
# brief pause
time.sleep(servo_pause)
# when close enough (as per the while loop criterion), take remainder step
self.X_coord += delta_x
self.Y_coord += delta_y
servo.set_servo_pulsewidth(GPIO_SERVOPIN_X, self.X_coord)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_Y, self.Y_coord)
# zero pwm's
servo.set_servo_pulsewidth(GPIO_SERVOPIN_X, 0)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_Y, 0)
def step_time_control(self, input_coords, input_timestamps):
'''
This method is the one to be opened on a thread as it directs when to move the laser based on the input timestamps.
'''
# get current timestamp
first_timestamp = time.time()
# loop through coordinate list
for i_coords in range(len(input_coords)):
# continue only when the clock reaches the current target timestamp or if the stop_playing is True
while int(time.time()) - first_timestamp < input_timestamps[i_coords] or self.stop_playing:
# if sel_playing is True end the function and the thread
if self.stop_playing:
return
# continue to wait until we reach the target time
time.sleep(0.1)
# then turn off the laser if it isn't already on
GPIO.output(GPIO_LASER, 0)
# take next movement
self.servo_stepper(input_coords[i_coords][0], input_coords[i_coords][1])
# turn laser on
GPIO.output(GPIO_LASER, 1)
def laser_thread(self, coordinate_list, timestamp_list):
'''
Method to launch thread for laser/servo sequence.
'''
# at start set stop_playing to False to allow the thread to run
# as this may be True from a prior interruption
self.stop_playing = False
# launch thread with coordinate and timestamp arguments
self.thread = threading.Thread(target = self.step_time_control,
args = (coordinate_list, timestamp_list))
# start thread
self.thread.start()
def stop_laser(self):
'''
Method to terminate thread if running.
'''
# set stop_playing flag to True to trigger stopping in thread
self.stop_playing = True
# terminate thread unless the thread doesn't exist
try:
self.thread.join()
self.thread.terminate()
del(self.thread)
except:
AttributeError
# turn laser off
GPIO.output(GPIO_LASER, 0)
def button_callback(self, instance, coord_list, timestamp_list, audio_id):
'''
Method to define button callback behavior.
'''
# stop servo/laser sequence
self.stop_laser()
# stop audio by stopping all available audio files
for audiofile in audiofiles:
audiofile.stop()
# start new servo/laser sequence with a pause before playing
time.sleep(1) # 1 second pause
self.laser_thread(coord_list, timestamp_list)
# play selected audio file
audiofiles[audio_id].play()
def shutdown_callback(self, obj):
'''
Stop button and close window callback
'''
# terminate laster sequence
self.stop_laser()
time.sleep(1)
# stop app
App.get_running_app().stop()
# close window
Window.close()
class MuralApp(App):
'''
Final application assembly into a Kivy App class
'''
def on_stop(self):
# if the shutdown button was called, do the following to shutdown and clean up
# stop audio
for audiofile in audiofiles:
audiofile.stop()
# stop pigpio
servo.stop()
time.sleep(0.1)
# end GPIO
GPIO.cleanup()
def build(self):
self.root = root = RootWidget()
root.bind(size=self._update_rect, pos=self._update_rect)
with root.canvas.before:
# RGBA coloring scheme
#Color(0.851, 0.851, 0.851, 1)
Color(140/255, 150/255, 150/255, 1)
self.rect = Rectangle(size=root.size, pos=root.pos)
return root
def _update_rect(self, instance, value):
self.rect.pos = instance.pos
self.rect.size = instance.size
if __name__ == '__main__':
# if running this file as main start the app
try:
MuralApp().run()
# only uncomment the following line for full shutdown functionality
#subprocess.call("sudo shutdown now", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except KeyboardInterrupt:
# if a keyboard command was used to terminate the app, do the following to clean up
# stop audio
for audiofile in audiofiles:
audiofile.stop()
# reset and zero pwm
time.sleep(0.1)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_X, 0)
servo.set_servo_pulsewidth(GPIO_SERVOPIN_Y, 0)
time.sleep(0.1)
# stop pigpio
servo.stop()
time.sleep(0.1)
# stop laser if not already
GPIO.output(GPIO_LASER, 0)
time.sleep(0.1)
# end GPIO