Skip to content

Commit 42ba1e3

Browse files
author
Kevin J Walters
committed
larson-scanner intended for Cytron Maker Pi Pico
1 parent 6bf28aa commit 42ba1e3

File tree

4 files changed

+372
-0
lines changed

4 files changed

+372
-0
lines changed

audio/scanner-left-16k.wav

78.2 KB
Binary file not shown.

audio/scanner-right-16k.wav

78.2 KB
Binary file not shown.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
### uart-sample-player v1.0
2+
### An ultra-simple wav jukebox based on single byte commands over RX serial
3+
4+
### Tested with Feather nRF52840 and 6.1.0
5+
6+
### copy this file to Feather nRF52840 as code.py
7+
8+
### MIT License
9+
10+
### Copyright (c) 2021 Kevin J. Walters
11+
12+
### Permission is hereby granted, free of charge, to any person obtaining a copy
13+
### of this software and associated documentation files (the "Software"), to deal
14+
### in the Software without restriction, including without limitation the rights
15+
### to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16+
### copies of the Software, and to permit persons to whom the Software is
17+
### furnished to do so, subject to the following conditions:
18+
19+
### The above copyright notice and this permission notice shall be included in all
20+
### copies or substantial portions of the Software.
21+
22+
### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23+
### IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24+
### FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25+
### AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26+
### LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27+
### OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28+
### SOFTWARE.
29+
30+
### This plays wave samples based on the number received
31+
### in a single byte "command" over UART
32+
### The first sample is 1, second sample is 2, etc.
33+
### 0 is ignored
34+
35+
### This is a daughterboard-style workaround for PWM audio
36+
### not always working well at the moment on Pi Pico using wav files
37+
### in 6.2.0 betas
38+
### https://github.com/adafruit/circuitpython/issues/4208
39+
40+
41+
import board
42+
import busio
43+
from audiocore import WaveFile
44+
try:
45+
from audioio import AudioOut
46+
except ImportError:
47+
from audiopwmio import PWMAudioOut as AudioOut
48+
from audiomixer import Mixer
49+
50+
### Using mixer is a workaround for the nRF52840 PWMAudioOut
51+
### not implementing the quiescent_value after a sample
52+
### has completed playing
53+
mixer = Mixer(voice_count=2,
54+
sample_rate=16000,
55+
channel_count=2,
56+
bits_per_sample=16,
57+
samples_signed=True)
58+
59+
wav_files = ("scanner-left-16k.wav",
60+
"scanner-right-16k.wav")
61+
62+
### Use same pins which would be used on a Feather M4 with real DACs
63+
AUDIO_PIN_L = board.A0
64+
AUDIO_PIN_R = board.A1
65+
audio_out = AudioOut(AUDIO_PIN_L,
66+
right_channel=AUDIO_PIN_R)
67+
68+
wav_fh = [open(fn, "rb") for fn in wav_files]
69+
wavs = [WaveFile(fh) for fh in wav_fh]
70+
71+
### Voice 0 behaves strangely
72+
### https://github.com/adafruit/circuitpython/issues/3210
73+
mixer.voice[0].level = 0.0
74+
mixer.voice[1].level = 1.0
75+
audio_out.play(mixer)
76+
77+
audio = mixer.voice[1]
78+
79+
uart = busio.UART(board.TX, board.RX, baudrate=115200)
80+
rx_bytes = bytearray(1)
81+
82+
while True:
83+
if uart.readinto(rx_bytes) and rx_bytes[0]:
84+
try:
85+
wav_obj = wavs[rx_bytes[0] - 1]
86+
audio.play(wav_obj)
87+
except IndexError:
88+
print("No wav file for:", rx_bytes[0])

pico/larson-scanner.py

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
### larson-scanner.py v2.0
2+
### Larson scanner for Cytron Maker Pi Pico
3+
4+
### Tested with Maker Pi Pico and CircuitPython 6.2.0-beta.4
5+
### with audio daughterboard as workaround for
6+
### https://github.com/adafruit/circuitpython/issues/4208
7+
8+
### copy this file to Cytron Maker Pi Pico as code.py
9+
10+
### MIT License
11+
12+
### Copyright (c) 2021 Kevin J. Walters
13+
14+
### Permission is hereby granted, free of charge, to any person obtaining a copy
15+
### of this software and associated documentation files (the "Software"), to deal
16+
### in the Software without restriction, including without limitation the rights
17+
### to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18+
### copies of the Software, and to permit persons to whom the Software is
19+
### furnished to do so, subject to the following conditions:
20+
21+
### The above copyright notice and this permission notice shall be included in all
22+
### copies or substantial portions of the Software.
23+
24+
### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25+
### IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26+
### FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27+
### AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28+
### LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29+
### OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30+
### SOFTWARE.
31+
32+
33+
import time
34+
35+
import board
36+
import busio ### for UART
37+
import pwmio
38+
import digitalio
39+
from audiopwmio import PWMAudioOut as AudioOut
40+
from audiocore import WaveFile
41+
42+
43+
debug = 1
44+
45+
def d_print(level, *args, **kwargs):
46+
"""A simple conditional print for debugging based on global debug level."""
47+
if not isinstance(level, int):
48+
print(level, *args, **kwargs)
49+
elif debug >= level:
50+
print(*args, **kwargs)
51+
52+
53+
AUDIO_DAUGHTERBOARD = True
54+
55+
if AUDIO_DAUGHTERBOARD:
56+
### Using pins presented on ESP-01 to talk to Feather nRF52840
57+
adb_uart = busio.UART(board.GP16, board.GP17, baudrate=115200)
58+
scanner_left = 1
59+
scanner_right = 2
60+
PWMAUDIO_CLASH_PINS = ()
61+
else:
62+
### TODO - PIO PWM would be nice to avoid losing GP2 GP3 for audio
63+
AUDIO_PIN_L = board.GP18
64+
AUDIO_PIN_R = board.GP19
65+
audio_out = AudioOut(AUDIO_PIN_L, right_channel=AUDIO_PIN_R)
66+
left_file = open("scanner-left-16k.wav", "rb")
67+
right_file = open("scanner-right-16k.wav", "rb")
68+
69+
### These crackle very unpleasantly and/or CP just crashes
70+
#scanner_left = WaveFile(left_file)
71+
#scanner_right = WaveFile(right_file)
72+
73+
### This blows up on first play during PWM animation
74+
### but at least it doesn't crackle!!
75+
### https://github.com/adafruit/circuitpython/issues/4431
76+
buffer = bytearray(80 * 1024) ### TODO - remove this
77+
scanner_left = WaveFile(left_file, buffer)
78+
scanner_right = WaveFile(right_file, buffer)
79+
80+
### The use of GP18 also effectively reserves GP19
81+
### and the RP2040 hardware cannot then offer PWM on GP2 and GP3
82+
### PIO PWM could be a solution here when it works in CircuitPython
83+
PWMAUDIO_CLASH_PINS = (board.GP2, board.GP3,
84+
board.GP18, board.GP19)
85+
86+
### Pins and vertical displacement
87+
left_pins = ((board.GP0, 0),
88+
(board.GP1, 1),
89+
# gap
90+
(board.GP2, 3), ### clash with GP18 audio due to PWM architecture
91+
(board.GP3, 4), ### clash with GP18 audio due to PWM architecture
92+
(board.GP4, 5),
93+
(board.GP5, 6),
94+
# gap
95+
(board.GP6, 8),
96+
(board.GP7, 9),
97+
(board.GP8, 10),
98+
(board.GP9, 11),
99+
# gap
100+
(board.GP10, 13),
101+
(board.GP11, 14),
102+
(board.GP12, 15),
103+
(board.GP13, 16),
104+
# gap
105+
(board.GP14, 18),
106+
(board.GP15, 19))
107+
108+
### GP28 is NeoPixel - will be interesting...
109+
right_pins = (# 6 absences (green LED for 3v3)
110+
(board.GP28, 6),
111+
# gap
112+
(board.GP27, 8),
113+
(board.GP26, 9),
114+
# gap
115+
(board.GP27, 8),
116+
(board.GP26, 9),
117+
# gap
118+
(board.GP22, 11),
119+
# gap
120+
(board.GP21, 13),
121+
(board.GP20, 14),
122+
(board.GP19, 15), ### clash with GP18 audio due to PWM architecture
123+
(board.GP18, 16), ### clash with GP18 audio
124+
# gap
125+
(board.GP17, 18),
126+
(board.GP16, 19))
127+
128+
129+
PIN_SPACING_M = 2.54 / 10 / 100
130+
BOARD_LENGTH_M = 20 * PIN_SPACING_M
131+
132+
### This is a duty_cycle value
133+
MAX_BRIGHTNESS = 65535
134+
PWM_LED_FREQUENCY = 7000
135+
136+
137+
class FakePWMOut:
138+
"""A basic, fixed-brightness emulation of the PWMOut object used for
139+
variable brightness LED driving."""
140+
141+
def __init__(self, pin,
142+
frequency=None, ### pylint: disable=unused-argument
143+
duty_cycle=32767):
144+
self._pin = pin
145+
self._duty_cycle = duty_cycle
146+
self._digout = digitalio.DigitalInOut(self._pin)
147+
self._digout.direction = digitalio.Direction.OUTPUT
148+
149+
self.duty_cycle = duty_cycle ### set value using property
150+
151+
152+
@property
153+
def duty_cycle(self):
154+
return self._duty_cycle
155+
156+
@duty_cycle.setter
157+
def duty_cycle(self, value):
158+
self._duty_cycle = value
159+
self._digout.value = (value >= 32768)
160+
161+
162+
def show_points(pwms, pin_posis, pnts):
163+
levels = [0] * len(pwms)
164+
165+
### Iterate over points accumulating the brightness value
166+
### for the pin positions they cover
167+
for pos, rad, bri in pnts:
168+
top = pos - rad
169+
bottom = pos + rad
170+
for idx, pin_pos in enumerate(pin_posis):
171+
if top <= pin_pos <= bottom:
172+
levels[idx] += bri
173+
174+
for idx, level in enumerate(levels):
175+
### Use of min() saturates and
176+
### caps the value within legal duty cycle range
177+
pwms[idx].duty_cycle = min(level, MAX_BRIGHTNESS)
178+
179+
180+
def start_sound(sample):
181+
if sample is not None:
182+
if AUDIO_DAUGHTERBOARD:
183+
adb_uart.write(bytes([sample]))
184+
else:
185+
audio_out.play(sample)
186+
187+
188+
def wait_sound():
189+
if AUDIO_DAUGHTERBOARD:
190+
pass ### not implemented
191+
else:
192+
while audio_out.playing:
193+
pass
194+
195+
196+
def pwm_init(pins, duty_cycle=0):
197+
pwms = []
198+
for p in pins:
199+
if p in PWMAUDIO_CLASH_PINS:
200+
pwms.append(FakePWMOut(p, duty_cycle=duty_cycle))
201+
else:
202+
pwms.append(pwmio.PWMOut(p, frequency=PWM_LED_FREQUENCY,
203+
duty_cycle=duty_cycle))
204+
return pwms
205+
206+
207+
points = []
208+
209+
left_pwms = pwm_init([p for p, d in left_pins])
210+
left_pin_pos = tuple([d * PIN_SPACING_M for p, d in left_pins])
211+
212+
### Indices for point fields
213+
POS = 0
214+
RAD = 1
215+
BRI = 2
216+
217+
start_radius = PIN_SPACING_M / 2.0
218+
219+
### GP8 is about the middle
220+
lead_pos = left_pin_pos[8]
221+
### position, size, brightness
222+
points.append([lead_pos, start_radius, 65535])
223+
for _ in range(5):
224+
points.append([lead_pos, points[-1][RAD], points[-1][BRI] >> 1])
225+
trail_spc = 12
226+
last_pos = [0] * ((len(points) - 1) * trail_spc)
227+
228+
### Track if sound effect has been started
229+
left_se = False
230+
right_se = False
231+
232+
left_se_pos = left_pin_pos[4]
233+
right_se_pos = left_pin_pos[-4]
234+
235+
### The real thing goes "out of bounds"
236+
far_left = left_pin_pos[0] - PIN_SPACING_M * 2
237+
far_right = left_pin_pos[-1] + PIN_SPACING_M * 2
238+
239+
### 1.333 seconds to go from one side to other
240+
target_speed_mpns = (far_right - far_left) / 1.333 / 1e9
241+
speed_mpns = 0.0
242+
accel_mpns2 = target_speed_mpns / 15.0 / 1e9
243+
244+
direction = -1 ### to the left
245+
246+
start_ns = time.monotonic_ns()
247+
last_move_ns = start_ns
248+
249+
250+
while True:
251+
### Move the main dot and trailing dots
252+
while far_left <= lead_pos <= far_right:
253+
show_points(left_pwms, left_pin_pos, points)
254+
255+
now_ns = time.monotonic_ns()
256+
elapsed_ns = now_ns - last_move_ns
257+
if speed_mpns < target_speed_mpns:
258+
speed_mpns += accel_mpns2 * elapsed_ns
259+
260+
lead_pos += direction * speed_mpns * elapsed_ns
261+
last_move_ns = now_ns
262+
263+
### Start the scanner return sound as it approaches either end
264+
if direction > 0 and not right_se and lead_pos >= right_se_pos:
265+
start_sound(scanner_right)
266+
right_se = True
267+
elif direction < 0 and not left_se and lead_pos <= left_se_pos:
268+
start_sound(scanner_left)
269+
left_se = True
270+
271+
### Move the trailing points and set main point's new position
272+
for tr_idx in range(1, len(points)):
273+
points[tr_idx][POS] = last_pos[tr_idx * trail_spc - 1]
274+
points[0][POS] = lead_pos
275+
276+
### Shuffle the point position history along and add new position
277+
last_pos = [lead_pos] + last_pos[:-1]
278+
279+
### Put position back into bounds and change direction
280+
lead_pos = far_right if direction > 0 else far_left
281+
direction = 0 - direction
282+
283+
### Clear sound start flags
284+
left_se = right_se = False

0 commit comments

Comments
 (0)