-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp.py
executable file
·314 lines (264 loc) · 10.8 KB
/
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
#!/usr/bin/python3
import time
import board
import neopixel
import neo7seg
import neobox
import asyncio
import sys
import RPi.GPIO as GPIO
import audio
import timer
import motion
from logger import log as log
import vl53l0x
### DEFINES ############################
green = (0, 255, 0)
red = (255, 0, 0)
blue = (0, 98, 255)
yellow = (255, 221, 0)
black = (0, 0, 0)
purple = (238, 0, 255)
############ GLOBAL APPDATA ##################
class app_data:
def __init__(self):
self.switch = 13 # GPIO13
self.mode_button = 17 # GPIO17
self.modetimer = None
self.last_switch_time = 0
self.motion = None
# game config
self.game_is_starting_up = False # need this so a reset can't be done during starup sequence
self.game_started = False # need this so we dont count shots/misses before the game starts
# shootout game config
# makes on top of box
# misses on bottom of box
self.shootout_timer = None
self.makes = 0
self.misses = 0
self.countdown = 0
self.shootout_neobox_offset_make = 20
self.shootout_neobox_offset_miss = 50
self.shootout_countdown_sec = 60
appd = app_data()
############### MOTION ###########
async def rim_done_moving(repeat, timeout):
settling_time = (appd.motion.motion_settling_time + appd.motion.motion_settling_time_race)
delta = time.monotonic() - appd.last_switch_time
log.debug("rim_done_moving: last switch was {} sec ago".format(delta))
# determine if shot was a miss or make
if delta <= settling_time:
# shot went in within our settleing time - it's a basket
log.debug("made basket")
else:
log.debug("missed basket")
await game_mode_process_miss()
################ GAME MODES #####################################
# to change modes, hold mode button for 3 secs
# push mode button to reset current mode
GAME_MODE_SHOOTOUT = "SH"
GAME_MODE_SHOT_COUNT = "SC"
game_modes = (
GAME_MODE_SHOOTOUT,
GAME_MODE_SHOT_COUNT,
)
# SH - 60sec shootout
# 3(beep),2(beep),1(beep), (BEEEEEEP)
# 7segs countdown, neobox counts makes(top)/misses(bottom)
# (optionally count down on neobox and count shots on 7seg)
# buzzer at end, then display shot %
# SC - shot counter
# no time limit, counts makes on 7seg, plays score/miss sounds and animates neobox
# VS - vs mode
# ???
async def shootout_display_per(repeat, timeout):
per = 0
if (appd.makes + appd.misses) > 0:
per = appd.makes*100/(appd.makes + appd.misses)
appd.neo7seg.set(per, purple)
async def shootout_timer(repeat, timeout):
appd.countdown -= 1
if appd.countdown > 5:
appd.neo7seg.set(appd.countdown, green)
appd.shootout_timer = timer.Timer(1, shootout_timer, True)
elif appd.countdown >= 1:
appd.neo7seg.set(appd.countdown, yellow)
appd.shootout_timer = timer.Timer(1, shootout_timer, True)
await appd.audio.play_beep1()
else:
# done!
appd.neo7seg.set(appd.countdown, red)
log.debug("shootout over!")
await appd.audio.play_buzzer()
timer.Timer(2, shootout_display_per, False)
async def start_game_mode_shootout():
appd.game_is_starting_up = True
appd.game_started = False
appd.makes = 0
appd.misses = 0
appd.shootout_make_and_misses = [black for i in range(100)]
appd.neobox.clear()
if isinstance(appd.shootout_timer, timer.Timer):
appd.shootout_timer.cancel()
# GAME_MODE_SHOOTOUT we need a 3 sec countdown with beeps
appd.neo7seg.set("3", blue)
await appd.audio.play_beep1()
await asyncio.sleep(1)
appd.neo7seg.set("2", blue)
await appd.audio.play_beep1()
await asyncio.sleep(1)
appd.neo7seg.set("1", blue)
await appd.audio.play_beep1()
await asyncio.sleep(1)
appd.neo7seg.set("GO")
await appd.audio.play_beep2()
await asyncio.sleep(1)
appd.countdown = appd.shootout_countdown_sec
appd.neo7seg.set(appd.countdown)
appd.shootout_timer = timer.Timer(1, shootout_timer, True)
appd.game_started = True
appd.game_is_starting_up = False
async def game_mode_process_make():
if appd.game_started == False:
log.debug("ignoring make")
return
appd.makes += 1
if appd.game_mode == GAME_MODE_SHOT_COUNT:
# gather these two asynchronous functions so they run concurrently
# defining the other functions as async and using "await asyncio.sleep()"
# inside them, allow the mpu async timer to also run concurrently
appd.neo7seg.set(appd.makes, red)
await asyncio.gather(appd.neobox.fire_trail(0.25, 1), appd.audio.play_scored_sound(), appd.neo7seg.rainbow_digits(3))
elif appd.game_mode == GAME_MODE_SHOOTOUT:
# count makes/misses on neobox
log.debug("shootout make {}".format(appd.makes))
appd.shootout_make_and_misses[appd.shootout_neobox_offset_make + appd.makes] = green
appd.neobox.set(appd.shootout_make_and_misses)
async def game_mode_process_miss():
if appd.game_started == False:
log.debug("ignoring miss")
return
appd.misses += 1
if appd.game_mode == GAME_MODE_SHOT_COUNT:
await asyncio.gather(appd.audio.play_missed_sound(), appd.neobox.red_box(2))
elif appd.game_mode == GAME_MODE_SHOOTOUT:
log.debug("shootout miss {}".format(appd.misses))
appd.shootout_make_and_misses[appd.shootout_neobox_offset_miss + appd.misses] = red
appd.neobox.set(appd.shootout_make_and_misses)
async def start_game_mode_shotcount():
if isinstance(appd.shootout_timer, timer.Timer):
appd.shootout_timer.cancel()
appd.makes = 0
appd.neo7seg.set(appd.makes)
appd.game_is_starting_up = False
appd.game_started = True
game_start_funcs = {}
game_start_funcs[GAME_MODE_SHOOTOUT] = start_game_mode_shootout
game_start_funcs[GAME_MODE_SHOT_COUNT] = start_game_mode_shotcount
########## MODE BUTTON ##################
async def mode_detect(repeat, timeout):
log.debug("game mode change")
await appd.audio.play_beep2()
for m in range(len(game_modes)):
if game_modes[m] == appd.game_mode:
if m < (len(game_modes) - 1):
appd.game_mode = game_modes[m + 1]
else:
appd.game_mode = game_modes[0]
break
log.debug("changed to mode {}".format(appd.game_mode))
appd.neo7seg.set(appd.game_mode, blue)
time.sleep(1)
# button up will trigger reset and start game
#await game_start_funcs[appd.game_mode]()
async def mode_button_event_async_exp(channel):
log.debug("mode triggered {} val: {}".format(channel, GPIO.input(channel)))
if isinstance(appd.modetimer, timer.Timer):
appd.modetimer.cancel()
if GPIO.input(channel) == True:
# if rising edge - reset current mode
log.debug("resetting")
appd.neo7seg.set("--")
await appd.audio.play_beep1()
time.sleep(1)
await game_start_funcs[appd.game_mode]()
else:
# possible holding for mode reset
appd.modetimer = timer.Timer(3, mode_detect, False)
async def mode_button_event_async(channel):
# https://www.joeltok.com/blog/2020-10/python-asyncio-create-task-fails-silently
# any syntax error inside our coroutine will fail silently.
# we have to explicitly raise any exceptions here
try:
await mode_button_event_async_exp(channel)
except Exception as e: log.error(">>>>Error>>>> {} ".format(e))
def mode_button_event(channel):
if appd.game_started == False and appd.game_is_starting_up == True:
log.debug("ignoring mode button")
return
return asyncio.run_coroutine_threadsafe(mode_button_event_async(channel), appd.loop)
def game_mode_init():
log.debug("setting gamemode {}".format(GAME_MODE_SHOOTOUT))
log.debug("hit reset to start")
appd.game_mode = GAME_MODE_SHOT_COUNT
# require reset button to start
appd.neo7seg.set(appd.game_mode, blue)
################ BASKET SWITCH #############################
async def switch_event_async(channel):
# https://www.joeltok.com/blog/2020-10/python-asyncio-create-task-fails-silently
# any syntax error inside our coroutine will fail silently.
# we have to explicitly raise any exceptions here
try:
await game_mode_process_make()
except Exception as e: log.error(">>>>Error>>>> {} ".format(e))
def switch_event(channel):
#time.sleep(0.1) # need a delay if you want to read the value properly
appd.last_switch_time = time.monotonic()
log.debug("switch triggered {} val: {} time: {}".format(channel, GPIO.input(channel), appd.last_switch_time))
# GPIO event is not on mainthread so we have to store the main event loop in appd
# and then use it here
asyncio.run_coroutine_threadsafe(switch_event_async(channel), appd.loop)
######################## MAIN ##########################
async def mainloop_timer(repeat, timeout):
# mostly for debugging things
timer.Timer(1, mainloop_timer, False)
print("Range: {0}mm".format(appd.lidar.vl53.range))
if __name__ == '__main__':
# Choose an open pin connected to the Data In of the NeoPixel strip, i.e. board.D18
# NeoPixels must be connected to D10, D12, D18 or D21 to work.
pixel_pin = board.D12
# The order of the pixel colors - RGB or GRB. Some NeoPixels have red and green reversed!
# For RGBW NeoPixels, simply change the ORDER to RGBW or GRBW.
ORDER = neopixel.GRB
num_pixels = neo7seg.get_num_pixels(2) + neobox.get_num_pixels()
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.5, auto_write=False, pixel_order=ORDER)
# display some init message
appd.neo7seg = neo7seg.Neo7Seg(pixels, 0, 2)
appd.neo7seg.set("ST")
# neobox is after the 7seg's
appd.neobox = neobox.NeoBox(pixels, neo7seg.get_num_pixels(2))
#appd.neobox.set([red for i in range(5)])
# init Audio
appd.audio = audio.Audio()
# setup the GPIO (neopixel alreayd sets BCM mode)
GPIO.setwarnings(True)
GPIO.setup(appd.switch, GPIO.IN, pull_up_down=GPIO.PUD_UP)
#GPIO.add_event_detect(appd.switch, GPIO.FALLING, callback=switch_event, bouncetime=2000)
GPIO.setup(appd.mode_button, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(appd.mode_button, GPIO.BOTH, callback=mode_button_event, bouncetime=50)
try:
appd.motion = motion.Motion(rim_done_moving)
except Exception as e:
# if the motion box isn't connected (or broken) we'll get here
log.error(">>>>Motion Module Error>>>> {} ".format(e))
appd.audio.play_sound(audio.buzzer)
timer.Timer(0.03, mainloop_timer, True)
game_mode_init()
# run the event loop
appd.loop = asyncio.get_event_loop()
# do this last because it will generate an interrupt right away
appd.lidar = vl53l0x.Lidar(switch_event_async)
appd.loop.run_forever()
appd.loop.close()
# cleanup
GPIO.cleanup()