|
| 1 | +# SPDX-FileCopyrightText: 2026 Liz Clark for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | + |
| 4 | +"""Duplo Color Boombox""" |
| 5 | + |
| 6 | +import board |
| 7 | +import sdcardio |
| 8 | +import storage |
| 9 | +import audiobusio |
| 10 | +import audiomixer |
| 11 | +import audiomp3 |
| 12 | +from digitalio import DigitalInOut, Direction |
| 13 | +from adafruit_as7341 import AS7341 |
| 14 | +from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw |
| 15 | + |
| 16 | +#Power Setup |
| 17 | +external_power = DigitalInOut(board.EXTERNAL_POWER) |
| 18 | +external_power.direction = Direction.OUTPUT |
| 19 | +external_power.value = True |
| 20 | + |
| 21 | +#SD Card setup |
| 22 | + |
| 23 | +spi = board.SPI() |
| 24 | +cs = board.D10 |
| 25 | +sdcard = sdcardio.SDCard(spi, cs) |
| 26 | +vfs = storage.VfsFat(sdcard) |
| 27 | +storage.mount(vfs, "/sd", readonly=True) |
| 28 | + |
| 29 | +#Audio |
| 30 | +mp3 = audiomp3.MP3Decoder(open("/sd/blue.mp3", "rb")) |
| 31 | + |
| 32 | +i2s = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA) |
| 33 | +mixer = audiomixer.Mixer(voice_count=1, sample_rate=mp3.sample_rate, channel_count=1, |
| 34 | + bits_per_sample=mp3.bits_per_sample) |
| 35 | +i2s.play(mixer) |
| 36 | +mixer.voice[0].level = 0.5 |
| 37 | + |
| 38 | +#Color Sensor |
| 39 | +i2c = board.STEMMA_I2C() |
| 40 | +sensor = AS7341(i2c) |
| 41 | +sensor.led_current = 4 # increments in units of 4 |
| 42 | +sensor.led = False |
| 43 | + |
| 44 | +brick_dictionary = [ |
| 45 | + {'song': "/sd/blue.mp3", 'value': (1619, 13852, 19215, 18562, 12912, 8818, 9512, 5440), |
| 46 | + 'color': ((0, 255, 255))}, |
| 47 | + {'song': "/sd/orange.mp3", 'value': (2517, 3983, 5661, 6864, 16857, 39049, 55151, 35696), |
| 48 | + 'color': ((255, 60, 0))}, |
| 49 | + {'song': "/sd/coral.mp3", 'value': (3004, 6712, 7499, 7160, 13858, 46007, 65535, 39278), |
| 50 | + 'color': ((250, 50, 50))}, |
| 51 | + {'song': "/sd/purple.mp3", 'value': (2562, 20704, 19222, 15806, 16624, 18399, 27316, 22525), |
| 52 | + 'color': ((125, 0, 255))}, |
| 53 | + {'song': "/sd/red.mp3", 'value': (1453, 2629, 4050, 4656, 5527, 13403, 31096, 21368), |
| 54 | + 'color': ((255, 0, 0))}, |
| 55 | + {'song': "/sd/yellow.mp3", 'value': (3051, 4446, 8633, 17412, 37450, 48224, 56249, 34726), |
| 56 | + 'color': ((255, 150, 0))}, |
| 57 | + {'song': "/sd/butter.mp3", 'value': (4102, 8631, 13692, 29000, 58147, 63230, 65535, 43143), |
| 58 | + 'color': ((255, 255, 0))}, |
| 59 | + {'song': "/sd/green.mp3", 'value': (1383, 2767, 5850, 12964, 22118, 16304, 12669, 7008), |
| 60 | + 'color': ((120, 255, 0))}, |
| 61 | + {'song': "/sd/lime.mp3", 'value': (3017, 8109, 17948, 36761, 48754, 38009, 31671, 19457), |
| 62 | + 'color': ((150, 255, 0))}, |
| 63 | + {'song': "/sd/lightblue.mp3", 'value': (4188, 29507, 42326, 53214, 54067, 37512, 34129, 23544), |
| 64 | + 'color':((100, 250, 255))}, |
| 65 | + {'song': "/sd/white.mp3", 'value': (5246, 27506, 33883, 44702, 62766, 64254, 65535, 48334), |
| 66 | + 'color':((255, 255, 255))}, |
| 67 | + ] |
| 68 | + |
| 69 | +def find_closest_brick(reading, dictionary, max_distance=50000): |
| 70 | + """ |
| 71 | + Find the brick with the closest matching sensor values. |
| 72 | +
|
| 73 | + Args: |
| 74 | + new_reading: Tuple of 8 sensor values |
| 75 | + brick_dictionary: List of brick dictionaries |
| 76 | + max_distance: Maximum distance to consider a valid match |
| 77 | +
|
| 78 | + Returns: |
| 79 | + Tuple of (matched_brick, distance) or (None, distance) if no good match |
| 80 | + """ |
| 81 | + best_match = None |
| 82 | + best_distance = float('inf') |
| 83 | + |
| 84 | + for brick in dictionary: |
| 85 | + # Calculate total distance (sum of absolute differences) |
| 86 | + distance = sum(abs(v1 - v2) for v1, v2 in zip(reading, brick['value'])) |
| 87 | + |
| 88 | + if distance < best_distance: |
| 89 | + best_distance = distance |
| 90 | + best_match = brick |
| 91 | + |
| 92 | + # Only return a match if it's close enough |
| 93 | + if best_distance > max_distance: |
| 94 | + return None |
| 95 | + |
| 96 | + return best_match |
| 97 | + |
| 98 | +#Rotary STEMMA I2C |
| 99 | +seesaw = seesaw.Seesaw(i2c, 0x36) |
| 100 | +encoder = rotaryio.IncrementalEncoder(seesaw) |
| 101 | +seesaw.pin_mode(24, seesaw.INPUT_PULLUP) |
| 102 | +switch = digitalio.DigitalIO(seesaw, 24) |
| 103 | +switch_state = False |
| 104 | + |
| 105 | +pixel = neopixel.NeoPixel(seesaw, 6, 1) |
| 106 | +pixel.brightness = 1 |
| 107 | +pixel.fill((0, 0, 0)) |
| 108 | + |
| 109 | +last_position = -1 |
| 110 | +volume = 0.5 # volume |
| 111 | +play = False |
| 112 | +play_state = False |
| 113 | + |
| 114 | +mp3 = audiomp3.MP3Decoder(open("/sd/boot.mp3", "rb")) |
| 115 | +mixer.voice[0].play(mp3, loop=False) |
| 116 | + |
| 117 | +while True: |
| 118 | + |
| 119 | + # make clockwise rotation positive |
| 120 | + position = -encoder.position |
| 121 | + |
| 122 | + if position != last_position: |
| 123 | + if position > last_position: |
| 124 | + #decrease volume |
| 125 | + volume = volume - 0.05 |
| 126 | + volume = max(volume, 0) |
| 127 | + else: |
| 128 | + #increase volume |
| 129 | + volume = volume + 0.05 |
| 130 | + volume = min(volume, 1) |
| 131 | + # set the audio volume |
| 132 | + mixer.voice[0].level = volume |
| 133 | + print(volume) |
| 134 | + last_position = position |
| 135 | + |
| 136 | + if not switch.value and not switch_state: |
| 137 | + print("Button pressed") |
| 138 | + switch_state = True |
| 139 | + if not play: |
| 140 | + sensor.led = True |
| 141 | + sensor_color = sensor.all_channels |
| 142 | + matched_brick = find_closest_brick(sensor_color, brick_dictionary) |
| 143 | + print(sensor_color) |
| 144 | + if matched_brick is not None: |
| 145 | + print(matched_brick['song']) |
| 146 | + mp3 = audiomp3.MP3Decoder(open(matched_brick['song'], "rb")) |
| 147 | + pixel.fill(matched_brick['color']) |
| 148 | + play = True |
| 149 | + mixer.voice[0].play(mp3, loop=False) |
| 150 | + play_state = True |
| 151 | + else: |
| 152 | + print("insert brick") |
| 153 | + mp3 = audiomp3.MP3Decoder(open("/sd/uhoh.mp3", "rb")) |
| 154 | + mixer.voice[0].play(mp3, loop=False) |
| 155 | + else: |
| 156 | + if play_state: |
| 157 | + i2s.pause() |
| 158 | + play_state = False |
| 159 | + else: |
| 160 | + i2s.resume() |
| 161 | + play_state = True |
| 162 | + |
| 163 | + if switch.value and switch_state: |
| 164 | + sensor.led = False |
| 165 | + switch_state = False |
| 166 | + print("Button released") |
| 167 | + |
| 168 | + if not mixer.playing: |
| 169 | + play_state = False |
| 170 | + play = False |
| 171 | + pixel.fill((0,0,0)) |
0 commit comments