English | 简体中文
An offline TOTP authenticator (2FA) for the Waveshare ESP32-S3-Touch-ePaper-1.54 V2.
The device shows rolling 6-digit codes on a 1.54″ e-paper display. Long-press an account row to either speak the code (Chinese digits) or type it into a host over a Bluetooth HID keyboard, swipe to change pages, and sync time over Wi-Fi only when you ask. Secrets can be stored AES-CBC encrypted in config.yaml; the decryption key lives only in firmware (crypto.yaml).
- TOTP: HMAC-SHA1 (RFC 6238), 30 s period, 6 digits, up to 12 accounts (3 per page).
- Optional secret encryption:
config.yamlholds Base64(IV‖ciphertext);data/crypto.yamlis compile-time embedded only. - E-paper UI: 5×7 font; 6 px vertical progress bar (updates every 3 s); page dots; partial refresh with periodic full refresh.
- Touch (FT6336): swipe paging, swipe-left for Info / swipe-right for Bluetooth, long-press to speak/type, tap/long-press NTP sync, tap/long-press Bluetooth connect/pair.
- Bluetooth HID keyboard: pairs to a host as a BLE keyboard and types the code on long-press; mode is
read/type/auto(type when connected, else speak); turns off on sleep / power-off / low-refresh. - Speech: embedded 0–9 PCM via ES8311; volume, speed, gap configurable.
- UI sound effects: distinct tap / long-press / swipe tones, volume configurable.
- On-demand NTP: Wi-Fi STA → SNTP → PCF85063 RTC → disconnect.
- Power: auto deep sleep, low-refresh idle mode, PWR long-press cuts VBAT; configurable BOOT short/long actions (full-refresh / Bluetooth connect / wake-hold / Bluetooth pair), with per-item wake-hold ignore.
- Config: YAML; load order SD card → SPIFFS → built-in defaults.
Target board: ESP32-S3-Touch-ePaper-1.54 V2 (ESP32-S3-PICO-1-N8R8, 8 MB Flash / 8 MB PSRAM).
| Peripheral | Chip | Bus |
|---|---|---|
| E-paper | SSD1681 200×200 | SPI2 |
| RTC | PCF85063 @ 0x51 | I2C0 |
| Touch | FT6336 @ 0x38 | I2C0 |
| Audio | ES8311 | I2S + I2C0 |
| Storage | microSD | 1-bit SDMMC |
Pin map: main/user_config.h. Details: HARDWARE.md.
Non-touch boards run with touch disabled (no paging, speech, or manual sync).
- ESP-IDF v5.x
- Target:
esp32s3
. ~/esp/esp-idf/export.sh
idf.py build
idf.py -p /dev/ttyUSB0 flash monitorUse sdkconfig.defaults for FatFS LFN (required for /sdcard/miniTOTP/config.yaml). Prefer idf.py build over repeated set-target.
macOS: ./flash.sh or ./flash.sh --monitor
Before build:
cp data/crypto.yaml.example data/crypto.yaml
# Edit algorithm / key_hexBuild embeds data/config.yaml (SPIFFS), data/crypto.yaml, and data/audio/*.pcm.
python3 sim_server.py
# Open http://localhost:8765Mirrors the firmware: progress bar, page dots, Info/Bluetooth screens (two-line MAC, status, connect/pair button), sync wait blink, and UI sound effects. Double-click the canvas to simulate a long-press (speak/type); the text box under the canvas shows what would be typed over Bluetooth. See simulator.html for the full layout (dual canvas, crypto tools, config editor).
/sdcard/miniTOTP/config.yaml(case variants; needs FatFS LFN)/spiffs/config.yaml(fromdata/config.yamlat flash time)- Built-in defaults in
main/config.c
The repo data/config.yaml is a demo template (e.g. sleep_timeout_sec: 600) and may differ from firmware fallbacks (e.g. 60).
wifi_networks:
- ssid: home
pass: your_password
ntp_server: pool.ntp.org
timezone_offset: 8
auto_sleep_enable: 1
sleep_timeout_sec: 60
low_refresh_enable: 1
low_refresh_timeout_sec: 120
# Full refresh after every N partial refreshes; 0=off, 1=every time, 2=every 2nd...
full_refresh_every_n: 10
# BOOT short: none | full_refresh | bluetooth
boot_short_action: full_refresh
# BOOT long (~1s): none | wake_hold | bluetooth_pair
boot_long_action: wake_hold
# Per-item ignore while wake-hold is active (each 0/1)
boot_wake_hold_ignore:
sleep: 1
low_refresh: 1
bluetooth: 0
# --- Bluetooth HID keyboard ---
# bluetooth_enable: disabled | enabled (on, no auto-advertise) | auto (advertise at boot)
bluetooth_enable: enabled
bluetooth_name: miniTOTP
# --- Speech ---
audio_volume: 80
audio_speed: 100 # 50-200, 100 = normal
audio_gap_ms: 120 # gap between digits, 0 = continuous
# --- UI sound effects ---
sfx_volume: 60
# --- Account long-press: read | type (Bluetooth) | auto (type if connected) ---
account_long_press: auto
type_key_interval_ms: 80 # per-key delay when typing over Bluetooth
batt_v_empty: 3.0
batt_v_full: 4.20
display_rotate: 0
accounts: # up to 12, 3 per page, swipe to page
- name: GitHub
secret: JBSWY3DPEHPK3PXP # plain Base32
- name: Work
secret_kind: encrypted # optional: Base64(IV‖ciphertext), needs crypto.yaml
secret: "Base64..."Up to 8 Wi-Fi networks; Info sync tries them in order. crypto.yaml is not read from SD/SPIFFS.
Encrypt on host:
python3 tools/encrypt_secret.py --algorithm aes-256-cbc \
--key-hex <same as crypto.yaml> --plaintext JBSWY3DPEHPK3PXP| Action | Effect |
|---|---|
| Swipe up/down (main) | Change page |
| Swipe left (main) | Info screen |
| Swipe right (main) | Bluetooth screen |
| Swipe left/right (Info / Bluetooth) | Back to main |
| Long-press account | Speak code / type over Bluetooth (per account_long_press) |
| Tap/long-press Sync (Info) | Wi-Fi + NTP |
| Tap Bluetooth button | Enable / disconnect & disable Bluetooth |
| Long-press Bluetooth button | Enter pairing (blinking indicator) |
| PWR short | Deep sleep / wake (GPIO18 ext1) |
| PWR long (~1 s) | Power off (VBAT cut) |
| BOOT short | boot_short_action (default full refresh) |
| BOOT long | boot_long_action (default wake hold) |
| Touch / PWR | Exit low-refresh mode |
First use: flash config + crypto → Info sync → offline.
| Symptom | Fix |
|---|---|
| SD config not loaded | Enable CONFIG_FATFS_LFN_HEAP; check miniTOTP/config.yaml path |
Encrypted account shows 000 000 |
Rebuild with valid data/crypto.yaml; matching key_hex |
| Codes wrong after sync | Re-sync on Info; check Wi-Fi / ntp_server |
| Cannot wake after USB power-off | Update firmware (PWR ext1 wake on shutdown path) |
miniTOTP/
├── main/ # Firmware (see module headers for API docs)
│ ├── bt_hid/ # BLE HID keyboard profile + wrapper
│ └── audio/ # Embedded digit PCM (d0..d9) + SFX
├── data/ # config.yaml (SPIFFS), crypto.yaml (firmware-embedded)
├── scripts/gen_digit_audio.sh # Regenerate digit speech (macOS `say`)
├── tools/encrypt_secret.py # Host-side AES secret encryption
├── README.md / README.zh-CN.md
├── CHANGELOG.md / CHANGELOG.zh-CN.md
├── HARDWARE.md / HARDWARE.zh-CN.md
├── simulator.html # Web simulator
└── flash.sh
- TOTP uses UTC from
rtc_get_unix_cached();timezone_offsetis display-only. - RTC cache ~250 ms; forced refresh near TOTP period edges.
- Deep sleep wake: PWR GPIO18 ext1 (not BOOT).
- E-paper: full baseline → partial updates → full LUT refresh every
full_refresh_every_ntop-bar redraws (0 = off). - Bluetooth security: only pairing accepts non-whitelisted connections; after bonding it re-advertises whitelist-only so the bonded host keeps exclusivity. Turns off on sleep / power-off / low-refresh unless ignored by wake-hold.
See CHANGELOG.md.
Third-party components (e.g. esp_codec_dev) use their own licenses.