This project uses the Raspberry Pi Pico 2WH SC1634 (wireless, with header pins).
Each team must provide a micro-USB cable that connects to their laptop to plug into the Pi Pico. The cord must have the data pins connected. Splitter cords with multiple types of connectors fanning out may not have data pins connected. Such micro-USB cords can be found locally at Microcenter, convenience stores, etc. The student laptop is used to program the Pi Pico. The laptop software to program and debug the Pi Pico works on macOS, Windows, and Linux.
This miniproject focuses on using MicroPython using Thonny IDE. Other IDE can be used, including Visual Studio Code or rshell.
05d54958-9aa5-4c9a-bced-b094683c943d.MP4
- Raspberry Pi Pico WH SC1634 (WiFi, Bluetooth, with header pins)
- Freenove Pico breakout board FNK0081
- Piezo Buzzer SameSky CPT-3095C-300
- 10k ohm resistor
- 2 tactile switches
The photoresistor uses the 10k ohm resistor as a voltage divider circuit. The 10k ohm resistor connects to "3V3" and to ADC2. The photoresistor connects to the ADC2 and to AGND. Polarity is not important for this resistor and photoresistor.
The MicroPython
machine.ADC
class is used to read the analog voltage from the photoresistor.
The machine.ADC(id) value corresponds to the "GP" pin number.
On the Pico W, GP28 is ADC2, accessed with machine.ADC(28).
PWM (Pulse Width Modulation) can be used to generate analog signals from digital outputs. The Raspberry Pi Pico has eight PWM groups each with two PWM channels. The Pico WH pinout diagram shows that almost all Pico pins can be used for multiple distinct tasks as configured by MicroPython code or other software. In this exercise, we will generate a PWM signal to drive a speaker.
GP16 is one of the pins that can be used to generate PWM signals. Connect the speaker with the black wire (negative) to GND and the red wire (positive) to GP16.
In a more complete project, we would use additional resistors and capacitors with an amplifer to boost the sound output to a louder level with a bigger speaker. The sound output is quiet but usable for this exercise.
Musical notes correspond to particular base frequencies and typically have rich harmonics in typical musical instruments. An example soundboard showing note frequencies is clickable. Over human history, the corresspondance of notes to frequencies has changed over time and location and musical cultures. For the question below, feel free to use musical scale of your choice!
- Photosensor (LDR):
GP26 / ADC0(machine.ADC(26)) - Buzzer (Piezo):
GP15(PWM capable)
Note: Other branches/variants may use
GP28/ADC2orGP16/18for PWM. Seedoc/Changelog.mdfor differences.
- Connect to Wi‑Fi (reads
wifi_config.json:{ "ssid": "...", "password": "..." }). - Continuously read light sensor (typ. ~1000…100000).
- Map light to frequency range 55 Hz (A1) → 2093 Hz (C7).
- Snap to the nearest musical note via
freq_to_note()(12‑TET). - Run an HTTP server; if no API‑driven sound is active, the default “light‑to‑music” loop plays continuously.
asyncio is used to run the HTTP server and the sensor/audio loop concurrently.
Base URL: http://<pico-ip>/
- GET / → simple HTML with current light reading.
- GET /sensor →
{"raw": <u16>, "norm": <0..1>}. - GET /health →
{"device_id": "<hex>", "status": "ok"}. - POST /play_note (seconds) → body:
{"frequency": <float Hz>, "duration": <float sec>}. - POST /tone (milliseconds + duty) → body:
{"freq": <int Hz>, "ms": <int>, "duty": <0..1>}. - POST /melody → body:
{"notes":[{"frequency":440,"duration":0.5}, ...]}. - POST /stop → stop all sounds immediately.
cURL examples
curl -X POST http://<pico-ip>/play_note -H "Content-Type: application/json" -d '{"frequency":440,"duration":0.5}'
curl -X POST http://<pico-ip>/tone -H "Content-Type: application/json" -d '{"freq":523,"ms":300,"duty":0.5}'
curl -X POST http://<pico-ip>/stop- Use Thonny (MicroPython) to flash
src/main.pyto Pico. - Place
wifi_config.jsonat the Pico root:
{"ssid":"<your-ssid>","password":"<your-password>"}- Reboot; the serial log prints:
Pico IP Address: <ip>.
pip install requests- Dashboard:
python src/dashboard.py(polls/healthand/sensorfor each Pico). - Conductor:
python src/conductor.py(broadcasts a short melody to all Picos).
Update
PICO_IPS = ["<ip1>", "<ip2>", ...]in both scripts.
Ambient Light Wi‑Fi / HTTP
│ ▲
LDR + Divider (GP26/ADC0) │
│ (ADC u16) │ ┌───────────────┐
┌──────▼───────┐ ┌─────┴──────┐ │ dashboard.py │
│ main.py │ │ Web Client │ │ conductor.py │
│ │◄──────►│ (PC/Mac) │ └───────────────┘
│ map → freq │ API └────────────┘
│ freq_to_note│
│ PWM (GP15) │────────► Buzzer (piezo)
└──────────────┘
Two mapping strategies were explored:
- Continuous frequency (linear light→Hz, optional snap):
- Pros: expressive continuous timbre; trivial to implement.
- Cons: jittery under ambient flicker; without snap it “meows”, with snap it may chatter between notes.
- Quantized to semitones (12‑TET):
- Pros: stable, clean harmonies for ensembles; consistent across devices.
- Cons: loses micro‑variations; requires octave range calibration.
Recommendation: classroom demos & ensembles → quantized (optionally add hysteresis). Interactive soundscapes → continuous (add smoothing). See doc/Comparisons.md for details and the team’s final choice.
- Input frequency is obtained and rounded to nearest note using a known musical formula
- Notes are mapped to their corresponding MIDI value
- Note name and octave is extracted using modulo and integer division
- Frequency of note is then returned by the function, which is sent to the buzzer
Note: The frequency is first converted to a note and then converted back to the corresponding frequency. We only want the buzzer to play musical notes, rather than random frequencies that sound off-pitch. We round the frequency to a note and convert back to a frequency in order to avoid having to create a dictionary entry for every note and frequency.
- Connectivity:
/healthreturnsokwithdevice_id. - Sensor:
/sensor.rawchanges with cover/shine;norm ∈ [0,1]. - Default loop: pitch changes with light; silence or low note in darkness (per spec).
- API:
/play_noteoverrides immediately;/stopis instant. /tone:msaffects duration,dutyaffects loudness./melody: in‑order playback, no dropped notes.- Multi‑Pico sync: use
conductor.pyto broadcast 8–16 notes; measure first‑note skew. - Calibration: record
raw_min/maxand adjust mapping. - Safety: keep duty moderate to avoid overheating.
Full plan and scripts: doc/Testing.md.
.
├─ src/
│ ├─ main.py
│ ├─ dashboard.py
│ └─ conductor.py
├─ doc/
│ ├─ Designs.md
│ ├─ Comparisons.md
│ ├─ Testing.md
│ └─ Changelog.md
├─ media/
│ └─ README.txt # put demo.mp4 / audio clips here
└─ README.md
- v1: GP26/ADC0 + GP15; linear light→[A1,C7] then
freq_to_note; API://sensor/health/play_note(sec)/tone(ms+duty)/melody/stop. - v2 (branch candidate): semitone quantization for both sensor and API; optional API‑lock window; constant
DUTY; possible PWM pin variants.
See doc/Changelog.md for full history and breaking changes.
Pico MicroPython time.sleep() doesn't error for negative values even though such are obviously incorrect--it is undefined for a system to sleep for negative time. Duty cycle greater than 1 is undefined, so we clip the duty cycle to the range [0, 1].
- Pico 2WH pinout diagram shows the connections to analog and digital IO.
- Getting Started with Pi Pico book

