

Prawnd
An ESP32-based, Plaud-style voice recorder. Press a button to start/stop recording; the device captures 16 kHz stereo audio to an SD card and POSTs each finished WAV to a webserver you configure on-device.
- Size
- 100 × 60 mm
- Copper layers
- 2
- Thickness
- 1.6 mm
- Parts
- 19
- Nets
- 15
- Pads
- 50
- Tracks, vias
- 102, 0
- Pours
- 1
- Routed
- Yes
- KiCad
- KiCad 10.0 (file 20260206)
- Created
- 6 Jun 2026
- Updated
- 27 Sep 2026
Overview
Prawnd is an open-source printed circuit board by hashtagcyber, from the hashtagcyber/prawnd repository on GitHub. It is a 2-layer board of 100 × 60 mm with 19 parts on 15 nets, routed with 102 tracks and 0 vias.
Its parts include 8 connectors, 3 capacitors, 1 resistor and 1 diode. It belongs with the ESP32 boards of this library.
README
README.mdPrawnd
An ESP32-based, Plaud-style voice recorder. Press a button to start/stop recording; the device captures 16 kHz stereo audio to an SD card and POSTs each finished WAV to a webserver you configure on-device.
- Hardware — Seeed XIAO ESP32C6, 2× INMP441 I²S mics (stereo), SPI microSD adapter, push-button. Wiring diagrams live in
hardware/wiring.md. - Firmware — Arduino-ESP32 via PlatformIO. Source in
firmware/. - Webservice — Node.js + Fastify, SQLite-indexed, Dockerized. Source in
server/.
[!CAUTION]
🔓 HTTPS uploads use
setInsecure()— TLS certificate validation is OFFWhen the configured Upload URL is
https://…, the firmware uploads withWiFiClientSecure::setInsecure()(firmware/src/uploader.cpp), which does the TLS handshake but does NOT validate the server's certificate. The connection is encrypted, but there is no protection against a man-in-the-middle: anyone who can intercept the device's traffic can present a forged certificate, decrypt the upload, and steal the API key (which the device sends asAuthorization: Bearer …).This is a beta convenience only. Before any non-hobby/production use, pin a CA bundle (e.g. ISRG Root X1) via
setCACert()and removesetInsecure(), and treat each device's API key as compromised if its network is ever untrusted. Rotate keys in the web UI if in doubt.
Quick start
# 1. Bring the server up locally
cd server
cp .env.example .env # then edit PRAWND_API_KEY
docker compose up -d --build
curl -s http://localhost:8080/healthz # {"ok":true}
# 2. Build the firmware
cd ../firmware
pio run # first run downloads toolchains (slow)
# 3. Flash it (with the XIAO plugged in via USB-C)
pio run -t upload
pio device monitor # 115200 baudOn first boot the device has no WiFi configured, so it brings up an AP named
Prawnd-XXXX (password prawnd1234). Connect, browse to http://192.168.4.1,
fill in WiFi creds + upload URL + API key, hit save. The device reboots into STA
mode and is ready to record.
Server
Prereqs
- Docker / Docker Compose (Rancher Desktop, OrbStack, or Docker Desktop all fine)
Build & run
cd server
cp .env.example .env # set PRAWND_API_KEY
docker compose up -d --build
docker compose logs -f prawndThe container exposes port 8080, persists data under ./data/ on the host
(SQLite at data/prawnd.sqlite, WAVs under data/audio/<device_id>/<yyyy>/<mm>/).
Browser UI
Visit http://localhost:8080/ for a built-in single-page UI: enter your API key
once (cached in localStorage), filter by device id, and play back any recording
inline. Audio elements stream from /recordings/:id/file?api_key=... directly.
Caveat: the API key ends up in <audio src> attributes, so treat the URL as
secret on the LAN — don't share screenshots of the DOM.
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | / | no | Built-in HTML UI. |
POST | /upload | yes | Upload a WAV body. Headers: Authorization: Bearer <key> (or X-API-Key: <key>), Content-Type: audio/wav, X-Device-Id: <id>, X-Client-Timestamp: <ms> (optional). |
GET | /recordings?limit=&cursor=&device_id= | yes | Cursor-paginated list, newest first. |
GET | /recordings/:id | yes | JSON metadata. |
GET | /recordings/:id/file | yes | Raw audio/wav download. Auth accepts ?api_key= query for <audio> playback. |
GET | /healthz | no | Liveness. |
API key auth accepts any of: Authorization: Bearer <key> header, X-API-Key: <key> header, or ?api_key=<key> query param.
Manual upload (smoke test without firmware)
curl -fsS -X POST \
-H "Authorization: Bearer $PRAWND_API_KEY" \
-H "Content-Type: audio/wav" \
-H "X-Device-Id: bench" \
--data-binary @some.wav \
http://localhost:8080/upload
curl -fsS -H "Authorization: Bearer $PRAWND_API_KEY" http://localhost:8080/recordings | jqStop / clean
docker compose down # stop
rm -rf data && mkdir data # wipe all recordings + dbFirmware
Prereqs
- PlatformIO Core (
brew install platformioon macOS) - A USB-C cable that carries data
- A microSD card formatted FAT32
Build
cd firmware
pio runFirst run pulls the espressif32 platform, the Arduino-ESP32 framework, the
toolchain, and the listed libs — expect several minutes on a fresh machine.
Subsequent builds take seconds. The flashable binary lands at
.pio/build/xiao_esp32c6/firmware.bin.
Flash
Plug the XIAO in via USB-C. The C6 enters bootloader automatically on most ports; if not, hold the BOOT button, tap RESET, then release BOOT.
The compiled artifact is .pio/build/xiao_esp32c6/firmware.bin (~1.1 MB).
pio run -t upload # build + flash
pio run -t upload -t monitor # flash + open serial monitorSet the port explicitly if PlatformIO can't auto-detect:
pio run -t upload --upload-port /dev/cu.usbmodem<TAB>Serial monitor
pio device monitor # 115200 baud (configured in platformio.ini)Optional: battery-monitoring addon
A second PlatformIO env, xiao_esp32c6_batt, compiles in an optional
MAX17048 LiPo fuel-gauge driver. The base xiao_esp32c6 build is unchanged — the
addon is purely a compile-time flag (-DENABLE_BATTERY), so the standard
USB-powered firmware carries no battery code.
pio run -e xiao_esp32c6_batt # build the battery variant
pio run -e xiao_esp32c6_batt -t upload # build + flash itWire a MAX17048 breakout to the XIAO's I²C pins (D4=SDA, D5=SCL) and the BAT
pads — see hardware/wiring.md.
When a gauge is detected, GET /status gains batt_pct and batt_mv fields and
the config page shows the charge level; with no gauge fitted it reports
battery: n/a and behaves identically to the base firmware. The artifact lands
at .pio/build/xiao_esp32c6_batt/firmware.bin.
First-time WiFi + upload setup
- After flash, the device has no NVS config → it boots into AP mode.
- From a phone or laptop, join the WiFi network
Prawnd-XXXX(passwordprawnd1234, whereXXXXis the last 4 hex of the MAC). - Open
http://192.168.4.1. - Enter:
- WiFi SSID + password (your home/office WiFi)
- Upload URL — either:
- Local server:
http://<your-mac-IP>:8080/upload(plain HTTP), or - Hosted beta:
https://prawnd.dev.cobl.io/upload(HTTPS — see the certificate-validation caution at the top of this README).
- Local server:
- API key:
- Local server: must match the server's
PRAWND_API_KEY. - Hosted beta: log in at
https://prawnd.dev.cobl.iowith cobl.io and generate an upload key under API keys.
- Local server: must match the server's
- Device id (optional; auto-derived from MAC)
- Hit Save and reboot. The device reconnects in STA mode.
To find the device's IP after reboot, watch the serial monitor or check your
router's DHCP list. GET http://<device-ip>/status returns its current state.
Troubleshooting the first build
The firmware/platformio.ini deliberately uses the pioarduino platform fork
(https://github.com/pioarduino/platform-espressif32/releases/...) because the
official platform-espressif32 still bundles Arduino-ESP32 v2.x, which has no
ESP32-C6 support. Two snags you may hit on a fresh machine:
Could not find a version that satisfies the requirement cryptographyfrom pip — your global pip is pointing at a local mirror that isn't serving. Override for the one command:PIP_INDEX_URL=https://pypi.org/simple/ pio run.FileNotFoundError: 'package-postinstall.py'from the pioarduino esptool zip — the v4.8.6 release omits the post-install script that PlatformIO tries to run. Stub it once:touch ~/.platformio/packages/tool-esptoolpy/package-postinstall.py, then re-runpio run.
Recording flow
- Short-press button — start recording (LED on board lights, serial logs path)
- Short-press again — stop, patch WAV header, queue for upload, POST to server
- Long-press ≥ 5 s — factory reset (wipes NVS, reboots into AP mode)
Pending uploads (WiFi down, server unreachable) land in /pending/ on the SD
card and retry once a minute while the device is idle and connected. Successfully
uploaded files move to /uploaded/.
End-to-end verification
cd server && docker compose up -d— confirm/healthzreturns 200.cd ../firmware && pio run -t upload && pio device monitor— flash and watch boot logs.- Join the AP, configure with the upload URL pointing at your machine's LAN IP (NOT
localhost— that's the device's own loopback). - Press the button, speak for a few seconds, press again.
- Watch the serial log for
Upload .../ -> 200. curl -fsS -H "Authorization: Bearer $PRAWND_API_KEY" http://localhost:8080/recordings | jq— your recording shows up at the top.- Pull the file:
curl -fsS -H "Authorization: Bearer $PRAWND_API_KEY" http://localhost:8080/recordings/<id>/file -o out.wavand play in VLC/Audacity — both L and R channels should be populated.
Project layout
prawnd/
├── README.md
├── hardware.md # high-level spec
├── hardware/
│ └── wiring.md # per-device wiring diagrams + pin map
├── firmware/
│ ├── platformio.ini
│ ├── include/pins.h
│ └── src/ # main, audio, wav, sd, wifi, portal, uploader, button, config,
│ # battery (optional MAX17048 addon, -DENABLE_BATTERY)
└── server/
├── Dockerfile
├── docker-compose.yml
├── package.json
├── .env.example
└── src/ # server.js, db.js, auth.js, wav.js,
# routes/ (upload, recordings, health, ui)Hardware Links
I purchased the following parts on Amazon:
Parts
19| Qty | Value | Footprint | References |
|---|---|---|---|
| 2 | 100n | C_Disc_D5.0mm_W2.5mm_P5.00mm | C1, C2 |
| 2 | Lanyard | MountingHole_4mm | H1, H2 |
| 1 | +3V3 | TestPoint_Pad_2.0x2.0mm | TP1 |
| 1 | 10u | CP_Radial_D5.0mm_P2.50mm | C3 |
| 1 | 120R | R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical | R1 |
| 1 | BTN | PinHeader_1x02_P2.54mm_Vertical | J8 |
| 1 | ESP32 L | PinHeader_1x07_P2.54mm_Vertical | J1 |
| 1 | ESP32 R | PinHeader_1x07_P2.54mm_Vertical | J2 |
| 1 | GND | TestPoint_Pad_2.0x2.0mm | TP2 |
| 1 | LED_D10.0mm | LED_THT:LED_D10.0mm | D1 |
| 1 | Mic1 L | PinHeader_1x03_P2.54mm_Vertical | J4 |
| 1 | Mic1 R | PinHeader_1x03_P2.54mm_Vertical | J3 |
| 1 | Mic2 L | PinHeader_1x03_P2.54mm_Vertical | J6 |
| 1 | Mic2 R | PinHeader_1x03_P2.54mm_Vertical | J5 |
| 1 | SCL | TestPoint_Pad_2.0x2.0mm | TP4 |
| 1 | SD Card | PinHeader_1x06_P2.54mm_Vertical | J7 |
| 1 | SDA | TestPoint_Pad_2.0x2.0mm | TP3 |
Questions
How big is the Prawnd PCB?
It measures 100 × 60 mm and has 2 copper layers, 1.6 mm thick.
What parts does the Prawnd use?
19 parts. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
Where can I download the Prawnd KiCad files?
From the hashtagcyber/prawnd repository on GitHub, at the commit this page was made from: the board is hardware/carrier/Prawnd.kicad_pcb and its schematic hardware/carrier/Prawnd.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Can I use the Prawnd design in my own project?
Its repository states no licence, so its author keeps all rights: ask hashtagcyber before reusing the design.
Similar boards
- 2 layers43.9 × 27.7 mm

- 2 layers50 × 60 mm

Debate Device
hackclub/OnBoard
💡 Join 1,000 teenagers and make your first PCB with a $100 grant!
19 parts915 - 2 layers102.9 × 96.7 mm

ISA adapter
polpo/picogus
Emulation of ISA sound cards and CD-ROM on Raspberry Pi Pico (GUS, SB/Adlib, MPU-401, Tandy, CMS, Panasonic CD-ROM) with USB mouse/joystick support
19 parts805 - 2 layers60 × 39 mm

- 2 layers70.1 × 43.2 mm

Iaq device
nkitanov/iaq_board
IAQ Board is a DIY (Do-It-Yourself) device for measuring internal air quality
19 parts396 - 2 layers80 × 80 mm

- 2 layers25.4 × 25.4 mm

Ina169
Ardumower/ardumower
Develop an open source robotic lawn mower (HW+SW reference platform)
19 parts282 - 2 layers58.4 × 28.6 mm

New rele
zmaker/arduino_cookbook
gli sketch del nuovo progetto Arduino Cookbook e dei video di YouTube
19 parts269