An ESP32-S3 firmware for the Waveshare ESP32-S3-ePaper-1.54 that combines a voice note recorder with a WiFi-connected AI meeting recorder — displayed on a 200×200 B&W e-paper screen with partial refresh (~300ms updates).
Built by merging two open-source projects:
- pala_note — e-paper voice recorder with RTC, SD card, and battery UI (original hardware abstraction, SSD1681 display driver, and display layer)
- MeetingRecorder by techiesms — WiFi meeting recorder with ElevenLabs STT, AI summaries, and web dashboard
All credit to the original authors for the foundations. This project adds multi-provider AI support, Markdown output, a unified hardware layer for the SSD1681 B&W panel with partial refresh, a voice notes portal, an optional webhook push, and merges both feature sets into a single firmware behind one web dashboard.
- Record voice memos directly to SD card as WAV files
- Tag each note on-device after recording (configurable tags via
config.json) - Browse and manage notes by tag on the e-paper display
- Sync: one-tap transcription via ElevenLabs Scribe — WAVs deleted after successful transcription, TXT transcripts retained
- RTC-stamped filenames
- Battery level indicator
- Deep sleep with wake on button press
- Optional webhook push on transcription — see Pushing notes elsewhere
- Record long meetings in 15-second WAV chunks to SD card (Core 0 audio capture task)
- Automatic transcription via ElevenLabs Scribe v1 (with speaker diarization) — no sync step needed
- AI summary via a configurable provider — no reflash needed to switch
- Summary generated using single-call or map-reduce strategy depending on meeting length
- Start/stop recording from the device buttons or from the web dashboard
A single dark-mode SPA served from port 80. The device shows its own address on the idle screen — the station IP when it's joined to your WiFi, or 192.168.4.1 when it's running its own hotspot. notemeet.local also works wherever mDNS resolves.
Joining the device's hotspot (notemeet / recorder123 by default, changeable in Settings) opens the dashboard automatically via a captive portal, the same way hotel WiFi does.
| Tab | What it does |
|---|---|
| Dashboard | Live recording status, start/stop controls |
| Transcript | Live transcript as meeting chunks arrive |
| Summary | AI-generated meeting summary with Retry button to regenerate if the API call fails |
| History | Past meetings — download .md files, delete, or regenerate summary |
| Notes | Browse and manage voice notes — filter by tag, play audio, download TXT |
| Settings | WiFi, device hotspot, API keys, AI provider/model, timezone, webhook (with a Send test payload button) |
Configurable at runtime via the web dashboard or config.json on the SD card — no reflash required:
| Provider | Models (examples) |
|---|---|
| OpenAI | gpt-4o-mini (default), gpt-4o, gpt-4.1-mini |
| Anthropic | claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5 |
gemini-1.5-pro |
|
| Ollama | any locally-served model |
| Component | Part |
|---|---|
| Board | Waveshare ESP32-S3-ePaper-1.54 |
| MCU | ESP32-S3 (8 MB flash, 8 MB OPI PSRAM) |
| Display | 200×200 B&W e-paper, SSD1681 controller, partial refresh |
| Audio | ES8311 codec (I2C + I2S) |
| Storage | MicroSD via SDMMC 1-bit |
| RTC | PCF85063 (NTP-synced over WiFi) |
Display refresh: Boot clear ~2s (full refresh, once). All subsequent UI updates ~300ms via partial refresh. No full-screen flash during normal use.
Partial refresh: The SSD1681 supports partial refresh natively. The firmware loads the partial LUT once on boot via
EPD_Init_Partial()and allrefresh()calls thereafter useEPD_DisplayPart()— only changed pixels are driven.
- Install the pioarduino ESP32 core (v3.3.9 or later)
- Board manager URL:
https://raw.githubusercontent.com/pioarduino/platform-espressif32/refs/heads/main/package_esp32_index.json
- Board manager URL:
- Select board:
ESP32S3 Dev Module - Partition scheme:
16M Flash (3MB APP/9.9MB FATFS)or similar with enough app space - PSRAM:
OPI PSRAM
ArduinoJson≥ 7.xWebServer(bundled with ESP32 core)ESPmDNS(bundled with ESP32 core)
Format the SD card as FAT32. Optionally pre-create config.json in the root with your credentials (see below) — or skip this and configure via the web dashboard on first boot.
{
"ssid": "your-wifi-network",
"pass": "your-wifi-password",
"openai_key": "sk-...",
"el_key": "your-elevenlabs-key",
"ai_provider": "openai",
"ai_model": "gpt-4o-mini",
"webhook_url": "",
"tags": ["Note", "Work", "Meeting", "Buy", "Private"]
}The tags array seeds the on-device tag list on first boot. Once tags are saved to the SD card (/notes/tags.txt), that file takes precedence.
webhook_url is optional — leave it empty to disable. When set, the device POSTs each transcribed note and finished meeting to that endpoint. See Pushing notes elsewhere.
No
secrets.hrequired. All credentials live inconfig.jsonon the SD card and are managed at runtime — no recompile needed.
Open pala_meet.ino in Arduino IDE and click Upload.
Upload tip: If the upload fails with "port busy" or "no such file", close the Serial Monitor first, then hold BOOT and tap RESET to enter bootloader mode.
On first boot (or whenever config.json has no WiFi credentials), the device starts its own access point:
| AP name | Password | Dashboard URL |
|---|---|---|
notemeet |
recorder123 |
http://192.168.4.1 |
Steps:
- Power on the device — the idle screen shows the address it's reachable on
- On your phone or laptop, connect to the
notemeetWiFi network (passwordrecorder123) - The dashboard opens automatically — the device runs a captive portal, so joining the hotspot triggers the same "sign in to network" sheet a hotel WiFi would. If it doesn't appear, browse to
http://192.168.4.1 - Go to the Settings tab
- Enter your home WiFi SSID and password, your OpenAI API key, and your ElevenLabs API key — then click Save
- The device connects to your home WiFi. Your laptop/phone can now rejoin your home network too
- From here on, reach the dashboard at the IP shown on the device's idle screen, or at
http://notemeet.local
Which address? The idle screen always shows the live one — the station IP when the device is on your WiFi, or
192.168.4.1when it's running its own hotspot.notemeet.localalso works when mDNS resolves, but it's unreliable on some corporate and guest networks, and generally won't resolve while you're connected to the device's own hotspot. The on-screen IP never lies; a static DHCP reservation in your router keeps it stable.
Hotspot name: configurable under Settings → Device Hotspot. Defaults to
notemeet/recorder123.
- Power on with the PWR button (GPIO18).
- The display clears to white (~2s full refresh, once per boot).
- The partial-refresh LUT is loaded — all subsequent screen updates are ~300ms.
- The device connects to WiFi, syncs time via NTP, and starts the AP + web server.
- The idle screen appears.
| From | Short REC | Long REC | Double REC | PWR |
|---|---|---|---|---|
| Idle | Open menu | Start recording | Open menu | Open menu |
| Menu | Select item | Start recording | Back to idle | Next item |
| Recording | Stop recording | Stop recording | — | — |
| Tag select | Confirm tag | — | — | Next tag |
| Settings | Toggle / select | Back to menu | Back to menu | Next item |
| Submenus / lists | Confirm / select | Back | Back | Next item |
Long-press REC is a universal shortcut to start a voice note from anywhere in top-level navigation. The Settings screen shows a back: dbl-click hint in the footer.
GPIO0 note: REC is on GPIO0 (ESP32-S3 strapping/boot pin). The firmware uses a release-latch pattern — the button must be seen HIGH (released) before any press is accepted in the idle state, preventing boot events from triggering a recording.
| Item | What it does |
|---|---|
| Notes | Browse recorded voice notes, filtered by tag |
| Tags | Manage tags (add, delete, view note counts) |
| Meet | Enter meeting recording mode |
| Sync | Transcribe untranscribed voice notes via ElevenLabs |
| Settings | Sounds on/off · Device info |
- Long-press REC from the idle screen or menu.
- The display shows a recording indicator. Speak your note.
- Tap REC to stop (toggle). The WAV is saved to SD.
- Choose a tag with PWR (cycle) and confirm with REC.
- Run Sync from the menu to transcribe — ElevenLabs Scribe processes the WAV and saves a TXT transcript. The WAV is deleted after successful transcription.
From the device:
- From the main menu, select Meet.
- Long-press REC to start. Audio is recorded in 15-second chunks on Core 0; transcription and rolling summaries run concurrently on Core 1.
- Tap REC to stop. A final AI summary pass runs automatically.
From the dashboard:
- Open the dashboard (the address on the device's idle screen) and go to the Dashboard tab.
- Click Start — the device begins recording immediately (no button press needed).
- Click Stop — the device finishes the current chunk and generates the final summary.
Open the Transcript and Summary tabs to follow along live. If the summary fails (e.g., API key issue), use the Retry button on the Summary tab to regenerate from the saved transcript without re-recording.
Browse to the address shown on the device's idle screen. On the same WiFi that's the station IP (or http://notemeet.local where mDNS resolves); on the device's own hotspot it's http://192.168.4.1, which the captive portal opens for you.
Settings saved via the dashboard are written to config.json on the SD card immediately and take effect without a reboot.
Set a Webhook URL in Settings and the device POSTs JSON every time a voice note is transcribed (during Sync) or a meeting summary is finalised — enough to file notes into Obsidian, a task manager, or anything else that accepts an HTTP POST.
{
"type": "note",
"tag": "Work",
"text": "Pick up milk",
"timestamp": "2026-07-25T14:30:00Z"
}type is note, meeting, or test. Meetings send title, summary, and the full transcript instead of tag/text — so a long meeting is a large payload. The device retries three times with backoff and treats only a 2xx as success.
Send test payload in Settings fires a synthetic test payload through the whole chain and reports the status the device actually received, so you can verify a receiver without recording anything.
Make the 2xx mean something. Configure your receiver to respond after it has done the work, not on receipt. n8n's default webhook mode returns
200 {"message":"Workflow was started"}before running a single node, which makes the device's success check meaningless and hides every downstream failure.
automation/ contains a ready-made n8n workflow that files notes and meetings into an Obsidian vault as Markdown, plus n8n_flow.md covering setup and the failure modes worth knowing about — including that a file written on the n8n host still has to reach the machine running Obsidian, which is its own supervised dependency.
| Task | Core | Stack | Role |
|---|---|---|---|
loop() |
1 | default | State machine, button handling, display updates |
recordTask |
0 | 8 KB | Audio capture: audio_playback_read → WAV chunks → chunkQueue |
processTask |
1 | 20 KB | ElevenLabs STT + rolling/final AI summary; also handles SD delete, factory reset, and summary regeneration |
webTask |
1 | 6 KB | HTTP server — serves the dashboard SPA and all /api/* routes |
recordTask idles (20ms delay loop) when no meeting is active. processTask blocks on chunkQueue with a 100ms timeout.
recordTask (Core 0)
└─ audio_playback_read() every ~128ms (8 KB stereo → 4 KB mono PCM)
└─ writes chunk_N.wav every 15 seconds
└─ xQueueSend(chunkQueue, path)
│
▼
processTask (Core 1)
└─ xQueueReceive(chunkQueue)
└─ transcribeAudio() → ElevenLabs Scribe
└─ generateSummary() → rolling summary (GPT)
└─ on finalStop: generateSummary(full transcript) → final summary
└─ saves full_transcript.md + summary_final.md to SD card
All display calls go through draw.cpp → epaper_driver_bsp.cpp (SSD1681):
- 1bpp frame buffer (5,000 bytes for 200×200) written directly to the panel — no color conversion
- Boot sequence: full refresh (clears panel) →
EPD_DisplayPartBaseImage()(primes both SSD1681 frame planes) →EPD_Init_Partial()(loads partial LUT) - All subsequent
refresh()calls useEPD_DisplayPart()— partial refresh, ~300ms fullRefresh()available for mode transitions if a clean slate is needed
pala_meet.ino — Main sketch, globals, state machine
types.h — Shared enums, state definitions, DEFAULT_TAGS
config.h — Hardware pin assignments, timing constants
globals.h — Root extern declarations (display, notes, tags)
src/
api/ — ElevenLabs STT + multi-provider AI dispatcher
app/ — Buttons, battery, draw, notes, record, sleep, UI, network
app/network.cpp — ElevenLabs note transcription, sync flow, notes portal
audio/ — ES8311 audio BSP (I2S read/write, volume)
codec_board/ — Board-level codec init
config/ — SD card config.json load/save (credentials + default tags)
core/globals.h — Meet-side extern declarations (WiFi, AI keys, queues)
display/ — E-paper driver BSP (SSD1681 B&W, 1bpp, partial refresh)
esp_codec_dev/ — ESP codec device drivers
i2c_bsp/ — I2C bus setup
json/ — ArduinoJson v7 shim (ShubhJson)
meet/ — Meeting coordinator + recordTask (record_task.cpp)
power/ — Board power management (battery latch, EPD power pin)
process/ — Chunked STT + rolling/final summary pipeline
time/ — NTP sync helpers
web/ — HTTP handlers (web_handlers.cpp) + dashboard SPA (html_pages.h)
automation/ — Optional webhook receiver: n8n workflow + setup notes
- pala_note — original e-paper recorder firmware (hardware abstraction, SSD1681 display driver, button handling, audio BSP)
- MeetingRecorder by techiesms — WiFi meeting recorder, ElevenLabs integration, web dashboard architecture
- ElevenLabs Scribe — speech-to-text with speaker diarization
- Waveshare ESP32-S3-ePaper-1.54 — official board documentation and display driver reference
This project inherits from open-source originals. Please check the licenses of the upstream projects before any commercial use. Original modifications in this repo are released under MIT.