|
| 1 | +# cpx-reaction-timer v1.0.1 |
| 2 | +# A human reaction timer using light and sound |
| 3 | + |
| 4 | +# Measures the time it takes for user to press the right button |
| 5 | +# in response to alternate first NeoPixel and beeps from onboard speaker, |
| 6 | +# prints times and statistics in Mu friendly format. |
| 7 | + |
| 8 | +import os |
| 9 | +import time |
| 10 | +import math |
| 11 | +import random |
| 12 | +import array |
| 13 | +import gc |
| 14 | +import board |
| 15 | +import digitalio |
| 16 | +import analogio |
| 17 | + |
| 18 | +# This code works on both CPB and CPX boards by bringing |
| 19 | +# in classes with same name |
| 20 | +try: |
| 21 | + from audiocore import RawSample |
| 22 | +except ImportError: |
| 23 | + from audioio import RawSample |
| 24 | +try: |
| 25 | + from audioio import AudioOut |
| 26 | +except ImportError: |
| 27 | + from audiopwmio import PWMAudioOut as AudioOut |
| 28 | + |
| 29 | +import neopixel |
| 30 | + |
| 31 | +def seed_with_noise(): |
| 32 | + """Set random seed based on four reads from analogue pads. |
| 33 | + Disconnected pads on CPX produce slightly noisy 12bit ADC values. |
| 34 | + Shuffling bits around a little to distribute that noise.""" |
| 35 | + a2 = analogio.AnalogIn(board.A2) |
| 36 | + a3 = analogio.AnalogIn(board.A3) |
| 37 | + a4 = analogio.AnalogIn(board.A4) |
| 38 | + a5 = analogio.AnalogIn(board.A5) |
| 39 | + random_value = ((a2.value >> 4) + (a3.value << 1) + |
| 40 | + (a4.value << 6) + (a5.value << 11)) |
| 41 | + for pin in (a2, a3, a4, a5): |
| 42 | + pin.deinit() |
| 43 | + random.seed(random_value) |
| 44 | + |
| 45 | +# Without os.urandom() the random library does not set a useful seed |
| 46 | +try: |
| 47 | + os.urandom(4) |
| 48 | +except NotImplementedError: |
| 49 | + seed_with_noise() |
| 50 | + |
| 51 | +# Turn the speaker on |
| 52 | +speaker_enable = digitalio.DigitalInOut(board.SPEAKER_ENABLE) |
| 53 | +speaker_enable.direction = digitalio.Direction.OUTPUT |
| 54 | +speaker_enable.value = True |
| 55 | + |
| 56 | +audio = AudioOut(board.SPEAKER) |
| 57 | + |
| 58 | +# Number of seconds |
| 59 | +SHORTEST_DELAY = 3.0 |
| 60 | +LONGEST_DELAY = 7.0 |
| 61 | + |
| 62 | +red = (40, 0, 0) |
| 63 | +black = (0, 0, 0) |
| 64 | + |
| 65 | +A4refhz = 440 |
| 66 | +midpoint = 32768 |
| 67 | +twopi = 2 * math.pi |
| 68 | + |
| 69 | +def sawtooth(angle): |
| 70 | + """A sawtooth function like math.sin(angle). |
| 71 | + Input of 0 returns 1.0, pi returns 0.0, 2*pi returns -1.0.""" |
| 72 | + |
| 73 | + return 1.0 - angle % twopi / twopi * 2 |
| 74 | + |
| 75 | +# make a sawtooth wave between +/- each value in volumes |
| 76 | +# phase shifted so it starts and ends near midpoint |
| 77 | +vol = 32767 |
| 78 | +sample_len = 10 |
| 79 | +waveraw = array.array("H", |
| 80 | + [midpoint + |
| 81 | + round(vol * sawtooth((idx + 0.5) / sample_len |
| 82 | + * twopi |
| 83 | + + math.pi)) |
| 84 | + for idx in range(sample_len)]) |
| 85 | + |
| 86 | +beep = RawSample(waveraw, sample_rate=sample_len * A4refhz) |
| 87 | + |
| 88 | +# play something to get things inside audio libraries initialised |
| 89 | +audio.play(beep, loop=True) |
| 90 | +time.sleep(0.1) |
| 91 | +audio.stop() |
| 92 | +audio.play(beep) |
| 93 | + |
| 94 | +# brightness 1.0 saves memory by removing need for a second buffer |
| 95 | +# 10 is number of NeoPixels on CPX/CPB |
| 96 | +numpixels = 10 |
| 97 | +pixels = neopixel.NeoPixel(board.NEOPIXEL, numpixels, brightness=1.0) |
| 98 | + |
| 99 | +# B is right (usb at top) |
| 100 | +button_right = digitalio.DigitalInOut(board.BUTTON_B) |
| 101 | +button_right.switch_to_input(pull=digitalio.Pull.DOWN) |
| 102 | + |
| 103 | +def wait_finger_off_and_random_delay(): |
| 104 | + """Ensure finger is not touching the button then execute random delay.""" |
| 105 | + while button_right.value: |
| 106 | + pass |
| 107 | + duration = (SHORTEST_DELAY + |
| 108 | + random.random() * (LONGEST_DELAY - SHORTEST_DELAY)) |
| 109 | + time.sleep(duration) |
| 110 | + |
| 111 | + |
| 112 | +def update_stats(stats, test_type, test_num, duration): |
| 113 | + """Update stats dict and return data in tuple for printing.""" |
| 114 | + stats[test_type]["values"].append(duration) |
| 115 | + stats[test_type]["sum"] += duration |
| 116 | + stats[test_type]["mean"] = stats[test_type]["sum"] / test_num |
| 117 | + |
| 118 | + if test_num > 1: |
| 119 | + # Calculate (sample) variance |
| 120 | + var_s = (sum([(x - stats[test_type]["mean"])**2 |
| 121 | + for x in stats[test_type]["values"]]) |
| 122 | + / (test_num - 1)) |
| 123 | + else: |
| 124 | + var_s = 0.0 |
| 125 | + |
| 126 | + stats[test_type]["sd_sample"] = var_s ** 0.5 |
| 127 | + |
| 128 | + return ("Trial " + str(test_num), test_type, duration, |
| 129 | + stats[test_type]["mean"], stats[test_type]["sd_sample"]) |
| 130 | + |
| 131 | +run = 1 |
| 132 | +statistics = {"visual": {"values": [], "sum": 0.0, "mean": 0.0, |
| 133 | + "sd_sample": 0.0}, |
| 134 | + "auditory": {"values": [], "sum": 0.0, "mean": 0.0, |
| 135 | + "sd_sample": 0.0}, |
| 136 | + "tactile": {"values": [], "sum": 0.0, "mean": 0.0, |
| 137 | + "sd_sample": 0.0}} |
| 138 | + |
| 139 | +print("# Trialnumber, time, mean, standarddeviation") |
| 140 | +# serial console output is printed as tuple to allow Mu to graph it |
| 141 | +while True: |
| 142 | + # Visual test using first NeoPixel |
| 143 | + wait_finger_off_and_random_delay() |
| 144 | + # do GC now to reduce likelihood of occurrence during reaction timing |
| 145 | + gc.collect() |
| 146 | + pixels[0] = red |
| 147 | + start_t = time.monotonic() |
| 148 | + while not button_right.value: |
| 149 | + pass |
| 150 | + react_t = time.monotonic() |
| 151 | + reaction_dur = react_t - start_t |
| 152 | + print(update_stats(statistics, "visual", run, reaction_dur)) |
| 153 | + pixels[0] = black |
| 154 | + |
| 155 | + # Auditory test using onboard speaker and 444.4Hz beep |
| 156 | + wait_finger_off_and_random_delay() |
| 157 | + # do GC now to reduce likelihood of occurrence during reaction timing |
| 158 | + gc.collect() |
| 159 | + audio.play(beep, loop=True) |
| 160 | + start_t = time.monotonic() |
| 161 | + while not button_right.value: |
| 162 | + pass |
| 163 | + react_t = time.monotonic() |
| 164 | + reaction_dur = react_t - start_t |
| 165 | + print(update_stats(statistics, "auditory", run, reaction_dur)) |
| 166 | + audio.stop() |
| 167 | + audio.play(beep) # ensure speaker is left near midpoint |
| 168 | + |
| 169 | + run += 1 |
0 commit comments