Talk to the Anker Soundcore Work (D3200) voice recorder pendant thing without the Anker app or cloud: read its status, set its clock, and list, download, and decrypt recordings on your own machine. The core protocol is implemented in Rust and available from Python and JavaScript. Not affiliated with Anker. Licensed under 0BSD: use it for any purpose, without warranty or liability.
crates/
soundcore-d3200-protocol/ protocol, crypto, client flows, test vectors
bestechnic-ota/ BES firmware image inspection
python/
soundcore-d3200/ client: Bluetooth LE, Classic Bluetooth, Wi-Fi
soundcore-d3200-core/ PyO3 bindings and models
bestechnic-ota/ PyO3 bindings for BES inspection
js/
client.ts D3200Client (Web Bluetooth)
src/ wasm-bindgen bindings
Rust has no I/O: each client does its own Bluetooth, Wi-Fi, timers, and file access. Firmware can be inspected but not flashed.
uv add soundcore-d3200 # or "soundcore-d3200[wifi]" for Wi-Fi transfersimport asyncio
from soundcore_d3200 import SoundcoreClient
async def main():
client = await SoundcoreClient.discover()
async with client:
info = await client.get_device_info()
print(info.firmware_version, f"{info.battery.recorder_percent}%")
await client.sync_time() # set the recorder's clock to local time
session, _, _ = await client.establish_encryption()
for recording in await client.list_all_files():
await client.download_file_to(
recording.file_id, session, f"{recording.file_id}.ogg",
recorded_at=recording.recorded_at, # written to the Ogg DATE tag
)
asyncio.run(main())See the Python packages overview and client guide.
Build the wasm package and D3200Client into js/pkg/:
mise run build:jsThen, in a page served from localhost or HTTPS in a browser with Web Bluetooth:
<button id="connect">Connect</button>
<script type="module">
import { D3200Client } from './js/pkg/client.js';
document.querySelector('#connect').addEventListener('click', async () => {
const client = await D3200Client.request(); // device picker; needs a click
try {
const info = await client.deviceInfo();
console.log(info.firmware_version, `${info.battery.recorder_percent}%`);
await client.syncTime();
await client.establishSession();
const files = await client.listFiles(); // [{ file_id, duration_ms, recorded_at, ... }]
if (files.length) {
const ogg = await client.downloadOgg(files.at(-1).file_id);
window.open(URL.createObjectURL(ogg));
}
} finally {
client.disconnect();
}
});
</script>js/demo/index.html is a complete page: run python3 -m http.server from the repository root and open http://localhost:8000/js/demo/. See the JavaScript guide.
| Task | Python (SoundcoreClient) |
JavaScript (D3200Client) |
|---|---|---|
| Device info: firmware, serial, storage | get_device_info() |
deviceInfo() |
| Battery and charging | get_battery(), BatteryEvent |
battery() |
| Set the clock | sync_time() |
syncTime() |
| List recordings, with start times | list_all_files() |
listFiles() |
| Download as Ogg/Opus | download_file_to(), download_file_bytes() |
downloadOgg(), downloadRecording() |
| Stream decrypted Opus packets | stream_file() |
downloadFile() |
| Delete recordings | delete_files() |
— |
| Follow a live recording | stream_realtime() |
— |
| Wi-Fi transfer | open_wifi_hotspot(), create_wifi_transfer() |
— (browsers can't join the hotspot) |
| Pairing and authentication | bind_and_store(), authenticate_from_store() |
— |
| Device logs | download_device_logs_to() |
— |
mise manages the Rust toolchain at the root; Python and JavaScript tools are declared in their respective directories:
mise install # Rust
mise -C python install # uv, when working on Python
mise -C js install # Node and wasm-bindgen, when working on JS
mise run ci # everything CI runs
mise tasks # lint, test, test:rust, test:python, test:js, build:js, check:boundaries