Krolyxon
Commit 0155f71

Branches

Refs →

budctldocsprotocolrealme_t310.md

671 lines · 55.1 KB

realme Buds T310: control protocol

This is budctl's clean-room description of how to control the realme Buds T310. Code in crates/budctl-core/src/devices/realme_t310/ is written from this file, not from any other project's source (CLAUDE.md rules 2 and 3). Each constant in that module cites an anchor here (Spec: realme_t310.md#anchor).

The T310 speaks the protocol family shared by many OPPO, OnePlus and realme buds. Linux socket, SDP and BlueZ details are in transport.md.

0. Evidence

Every fact carries an evidence level that says how far it can be trusted. A fact that is not yet ours is tested on the owner's buds before budctl relies on it (CLAUDE.md rule 4).

Level Meaning
t310 seen on a real T310 by other users, in a report or log. Not yet verified by budctl
ours-YYYY-MM-DD verified by budctl on the owner's T310 (see the verification log)
vendor how the vendor apps (realme Link, HeyMelody) behave. Not seen on a T310
buds4 seen on a OnePlus Buds 4. The framing is shared, but the values may differ on the T310
derived computed by us from the facts above. Never seen on the wire
guess unverified anywhere. budctl must not send it

1. Identity

2. Transport

3. Frame format

Every message is wrapped in one frame.

AA  len  slice  00  [fsn]  cmd(2, LE)  seq  paylen(2, LE)  payload
Field Size Meaning
AA 1 start of frame
len 1-3 LEB128: 7 bits per byte, low bits first, top bit = another byte follows. It counts every byte after the length itself
slice 1 00 = the whole message in one frame; 01 / 02 / 03 = first / middle / last slice of a long message
00 1 always zero
fsn 1 slice number from 00; present only in slices (01-03)
cmd 2 command id, little endian
seq 1 sequence byte (see §4)
paylen 2 payload length, little endian
payload paylen message body

Whole message. len = 7 + paylen. There is no checksum. buds4

Examples (buds4):

AA 07 00 00 00 01 01 00 00                  handshake 0x0100, seq 01, no payload
AA 0A 00 00 04 04 06 03 00 01 01 01         0x0404 seq 06, payload 01 01 01
AA 0C 00 00 04 02 FF 05 00 03 01 01 08 00   0x0204 push (seq FF), payload 03 01 01 08 00

Long lengths. A 124-byte payload gives len = 131, sent as AA 83 01. A 250-byte reply arrives as AA 81 02 (257). buds4

Slices (vendor):

Frames the buds start (ours-2026-10-27). On the owner's T310, every frame the buds send on their own (requests such as 0x0501 and 0x0204 pushes) has 08 in the slice byte, not 00. The rest of the frame is laid out as a whole message, and len = 7 + paylen holds. Replies carry 00. budctl therefore reads a slice byte of 00 or 08 as a whole message. Other values are not seen and are still dropped. Captured examples:

AA 07 08 00 01 05 0F 00 00                        0x0501 seq 0F, no payload
AA 0F 08 00 04 02 10 08 00 01 03 02 64 01 64 03 00   0x0204 seq 10, battery push 01 03 02 64 01 64 03 00

Receiving:

4. Sequence numbers and replies

5. Command bitmap

Gate table for every command budctl uses or documents here:

Command(s) Gate
0x0100 handshake, 0x0103 product id, 0x0106 battery, 0x010D switch read, 0x0200 offered events always
0x0105 firmware bit 0
0x0108 gesture read, 0x0401 gesture write bit 3
0x0109 wearing bit 4
0x0400 find my earbuds bit 5
0x0403 switch write bit 7
0x010C noise-control read, 0x0404 noise-control write bit 8
0x0405 fit test bit 9
0x010F EQ read, 0x0406 EQ select bit 10
0x0114 current codec bit 17
0x0205 subscribe bit 22
0x0112 dual-connection devices bit 29
0x0122 custom EQ list, 0x0418 custom EQ write bit 34
0x0124 bass level read, 0x041B bass level write bit 38

The T310's command set (t310), from a log of a real T310, as a decoded list:

0100 0101 0102 0103 0104 0105 0106 0107 0108 0109 010B 010C 010D 010F 0112 0114 0115 0116 0122 0124 0205 0400 0401 0403 0404 0405 0406 0407 040B 040D 040E 040F 0416 0418 041B 0F00 0F03 0F04 EF02

6. Session start

The vendor sequence (buds4, matching HeyMelody):

budctl does:

  1. Connect, then wait 300 ms. During the wait, keep answering the buds' requests (§7).

  2. 0x0100. Keep the bitmap.

  3. 0x0103. The id must be 063C12 (§1).

  4. 0x0200 → 00 <count> <codes...>: the event codes the buds can push. Buds 4 offers 01 02 03 04 08 0B F1 F2 F3 (buds4). The owner's T310 offers 01 02 03 04 05 06 0D F1 F4 (ours-2026-10-27).

  5. 0x0205 <count> <codes...> (count first) subscribes. budctl asks for these codes, keeping only the ones the buds offered (vendor):

    • 01 battery, 02 wearing and 03 noise control;
    • 04 (fit-test result) if 0x0405 is allowed.

    Other clients also subscribe to 08 when 0x040D is allowed. budctl does not: this spec does not say what event 08 carries or what 0x040D does, and budctl handles no such event. Add it only once both are documented here with a source.

    Never send 01 01 02 02: that means "count 1, code 01". The 0x8205 ack may never arrive (buds4, Nord Buds 3 Pro), so budctl does not wait for it. The T310 does send it, about 50 ms later: 00 04 01 00 02 00 03 00 04 00, a 00 result for each code (ours-2026-10-27).

  6. Reads, one per request.

Spacing. budctl keeps the captured spacing: at least 200 ms between any two frames it sends. It waits for each reply before sending the next request.

7. Requests from the buds

The buds send some requests of their own. The answer repeats the request's seq and sets bit 15 of its command. Some firmware resets the link 3-5 s after connecting if the time request goes unanswered (Nord Buds 3 Pro, buds4-family capture).

From the buds budctl answers Meaning
0x0500 00 <Unix seconds, u32 LE> time request, sent on connect
0x050C 00 01 00 "is the foreground app on my list?" — no
0x051C 00 00 00 host app capabilities: fast discovery off
0x051D 01 the phone's spatial mode (budctl always answers 01)
a command outside groups 01-05 and 0F 01 unknown request

8. Pushes

budctl reads none of these yet. It skips them while waiting for replies (and prints them with --debug).

9. Reads

9.1 Battery: 0x0106

This command is always allowed. Battery reading is confirmed on a T310 (t310); the byte layout is buds4.

9.2 Firmware: 0x0105

Evidence: buds4

9.3 Codec in use: 0x0114

Evidence: buds4

9.4 Noise control: 0x010C 01 01

9.5 Switches: 0x010D

Id Switch Evidence
05 unknown. The vendor apps always ask it first and ignore the value vendor
06 game mode ("low latency") confirmed on a T310, t310
11 dual connection seen in a T310 log, t310
1A wind-noise reduction realme Link shows it for the T310 (vendor). The T310 answers this id (ours-2026-10-27)
1B 3D sound seen in a T310 log, t310
1D bass boost ("BassWave", "dynamic sound") seen in a T310 log, t310

9.6 EQ preset: 0x010F

Preset names are the vendor app's (vendor).

9.7 Bass level: 0x0124

Evidence: vendor; the T310's command set lists it.

9.8 Touch controls: 0x0108

9.9 Hold noise-control cycle: 0x010C 02 01

Evidence: vendor

9.10 Dual-connection devices: 0x0112

The phones and computers the buds keep for dual connection, and which of them are connected now. budctl only reads this list.

10. Writes

One at a time. budctl adds a write only after the previous one passes its hardware checkpoint (see the roadmap). Sent so far: §10.1 off/transparency/on (W1, verified ours-2026-10-27), the light/medium/deep levels (W2, verified ours-2026-10-27), and §10.2 game mode (W3, verified ours-2026-10-27), and §10.3 the built-in EQ presets (W4, verified ours-2026-10-27), the bass-boost switch 1D (W5a, verified ours-2026-10-27), 3D sound 1B (W5b, verified ours-2026-10-27), wind-noise reduction 1A (W5c, verified ours-2026-10-27), and dual connection 11 (W5d, verified ours-2026-10-27); §10.5 find (W7, verified ours-2026-10-27); §10.6 touch controls (W8, verified ours-2026-10-27); §10.7 the hold cycle (W9, verified ours-2026-10-27); §10.4 the bass level (W6b, acked ours-2026-10-27; the effect is faint by ear and cannot be read back). The fit test (§10.8, W10) is acked but does nothing on the T310; it is dropped. The others are not sent yet.

General rule:

10.1 Noise control: 0x0404 01 01 <mask>

10.2 Switches: 0x0403 <id> <01|00>

10.3 EQ preset: 0x0406 <id>

Evidence: vendor

10.4 Bass level: 0x041B FB 05 <level s8>

Evidence: vendor

10.5 Find my earbuds: 0x0400 01 / 0x0400 00

Evidence: buds4

10.6 Touch controls: 0x0401 <count> (<dev> <btn> <act> <fn>)…

The command and layout are buds4; the realme rules are vendor.

10.7 Hold cycle: 0x0404 02 01 <mask>

Evidence: vendor

10.8 Fit test: 0x0405 01 / 0x0405 00

Evidence: buds4

10.9 Custom EQ: list 0x0122, write 0x0418

Evidence: vendor

11. Not on the T310

12. Open questions (need a capture or a test on the owner's buds)

  1. Which service UUID the T310 offers, and its RFCOMM channel. Answered at HW1: 0000079a-d102-11e1-9b23-00025b00a5a5 on channel 6.
  2. The real bitmap bytes, the product-id reply and the offered events. Answered at HW1; see §5 and §6.
  3. Partly answered at HW1: no 0x0500, but an unanswered 0x0501 with no reset (§7). F2/F4 were offered but not subscribed, so none arrived. What 0x0501 asks for is unknown.
  4. Mostly answered (2026-10-27): with dual connection on and a control app connected on the phone, budctl's status got every reply, and changes budctl made showed up in the phone app. So the phone does not make the channel busy. A local holder gives EBUSY (HW1). With dual connection off, the phone takes the laptop's whole link (exit 2, HW1); whether a phone app ever leaves the buds silent is unknown, so budctl still treats a silent handshake as "channel busy" (exit 3).
  5. The width of the noise-control value: one byte so far, on reads and writes. 08 is the deep level: light, medium and deep read back as 04, 10 and 08 (W2).
  6. Whether the T310 answers id 1A (wind). It does (HW1). What id 05 means is still open: it reads 00.
  7. How long after the game-mode ack the new value shows up in 0x010D: at most 1 s on the T310 (W3). Whether it is sooner was not measured.
  8. The shape of the T310 gesture table is answered at HW1: six entries, <side> <group> <act> <fn>, double tap, triple tap and hold for each bud. The names of the EQ presets in realme Link are still open.
  9. Whether the minimum 200 ms spacing between sends is needed.
  10. Why the T310 never answers 0x0124, with bass boost off (HW1) or on (W6a), although bit 38 is set (§9.7).
  11. How to turn noise cancelling on at the level the buds saved, without forcing deep (§10.1). Would need a realme Link snoop, which the owner declined.

13. Roadmap and hardware checkpoints

The owner runs each checkpoint with --debug and pastes the output. Results go into the verification log.

HW1, read-only:

HW2, read-only: budctl --debug dual (§9.10) with the phone connected too (dual connection on). Done 2026-10-28. A second run with the phone disconnected was skipped: two other phones already showed state 00.

Writes, one per checkpoint, in this order:

Step Write
W1 anc off/transparency/on
W2 anc light/medium/deep
W3 game on/off
W4 eq <preset>
W5 set bass-boost → set spatial → set wind → set dual, each one checkpoint
W6 W6a: read 0x0124 with bass boost on; W6b: bass <level>
W7 find
W8 touch <side> <gesture> <action> (--dry-run first)
W9 touch cycle
W10 fit-test: dropped, the T310 runs no test (§10.8)
W11 W11a: eq lists the custom presets (read-only); W11b: save an edited preset (eq custom-<id> <hz>=<gain>…); W11c: create one (eq new <name>); W11d: rename one; W11e: delete one (eq delete custom-<id>)

The owner will not install realme Link (2026-10-27), so there is no app capture. W6 and W11 use only the vendor-documented bytes in this spec, one checkpoint at a time.

14. Verification log

Date Checkpoint Result Notes
2026-11-01 A2 Android app (docs/android.md#checkpoints): noise control off → anc → off, the idle close, then budctl status on the laptop pass Each write was acked and read back within 0.3-0.4 s, all on one session. Every session closed 10 s after its last command. budctl status on the laptop worked once the app was left. Transparency was not tried on the phone; W1 covers its bytes
2026-11-01 A1 Android app on the phone (Nothing Phone (3a), Android 17), read-only, debug on pass The same session start as HW1, byte for byte, over Android's RFCOMM socket: the same bitmap (BF 1F 5A 20 45 04 40 00), id 063C12, events offered 01 02 03 04 05 06 0D F1 F4, and the 0x8205 ack about 70 ms after the subscribe, skipped as §6 says. Replies came 30-110 ms after each request. A session opened 1.1-1.6 s after the connect began, and a full reading took 2.3 s more. Lid closed: the first Read connected but the buds closed the link at once ("case lid closed?"), and the next said "paired but not connected"; with the lid open, Read worked again. The busy path is untested, since no other control app is left on the phone
2026-10-28 HW2 budctl --debug dual, with the phone and the laptop connected pass, with a fix The T310 answered 0x0112 in 160 ms with four entries: the owner's phone (connected, flags 00), the laptop (connected, flags 01) and two phones that had connected before (state 00). Every len was 3 more than the name length. The addresses came first byte first, so budctl printed them backwards; fixed the same day (§9.10). The addresses and other people's device names are left out of this log on purpose
2026-10-28 W11e eq delete custom-5 pass The delete matched the derived frame byte for byte, and was acked with 00 05. The list read back at once is empty. Deleting the preset in use is still untested. See §10.9
2026-10-28 W11d eq rename custom-5 Gym pass The save with the new name matched the derived frame byte for byte, and was acked with 00 05. The list read back at once has id 05 named Gym, bands unchanged, in use. See §10.9
2026-10-28 W11c eq new Test pass The create frame matched the derived one byte for byte, and was acked with 00 05. The list read back at once has Test as id 05, flat, and in use. See §10.9
2026-10-27 G5 budctl-gui --debug, Gestures page: left triple tap by dropdown and by h/l, transparency into the hold cycle and out pass (protocol) Every 0x0401 write (06, 03, 0B, 03, 06, then 05 to restore it) and both 0x0404 02 01 writes (0B, 09) were acked and read back on the first try. Keys pressed during a write went out one by one as each write finished, and the last one was read back. A session closed after 10 s idle; the next pick opened a new one and wrote at once
2026-10-27 G4 budctl-gui --debug, Equalizer page: the four built-in presets, a band drag on Custom1, then idle pass (protocol) 0x0406 00/01/02/03 were acked and read back on the first try. One drag (1 kHz from −6 to +3) sent one 0x0418 02 write: acked 00 04, and the read-back list shows the new gain with selected 01. The session closed 10 s after the last command, three times; a read when the window got focus after 30 s opened a new session each time. h/l on a band is not covered yet
2026-10-27 G3 budctl-gui --debug: the first reading, then noise control transparency → noise cancelling, game mode on → off pass (protocol) One session for the reading and all four writes, with no new connection between them. The acks and read-backs match W1 and W3; the game-mode read-back, 1 s after the ack, showed the change. The window named the custom preset from the 0x0122 list. No request from the buds came in while the session waited. The log ends about 1 s after the last write, so the 10 s idle close is not in it yet
2026-10-27 status with a control app connected on the phone, dual connection on pass Every read answered, 40-190 ms after its request (HW1: 50-90 ms). Changes budctl made showed up in the phone app. The T310 serves both hosts at once. See question 4
2026-10-27 W11b eq custom-4, eq custom-4 50=+3 16000=-2, then 50=0 16000=0 pass The write matched the derived frame, was acked with 00 04 and read back on the first try. The preset is flat again. Then eq pure-raw, eq custom-4 and status: status showed custom-4, so saving also selects. See §10.9
2026-10-27 W6b bass -5, bass 5, bass 0 (bass boost off) pass (protocol) All acked with 0x841B 00. No read-back is possible. By ear, at most a slight difference. See §10.4
2026-10-27 W11a eq (custom list 0x0122) pass One preset, "Custom1" (id 04, 6 flat bands from 50 Hz), and it is the EQ in use. See §10.9
2026-10-27 W6a status with bass boost on (0x0124) fail: no reply Still no answer in 3 s. budctl does not send it. See §9.7
2026-10-27 W10 fit-test fail: no test Start and stop were acked, but no result push and no sound came in 15 s. The owner dropped the fit test. See §10.8
2026-10-27 W9 touch cycle off transparency anc, then off anc pass Masks 0B and 09 were acked and read back on the first try. The owner confirmed the new cycle with the hold gesture. The cycle is now off → anc. See §10.7
2026-10-27 W8 touch left triple-tap volume-up (--dry-run, then sent), then previous pass The dry run's payload matched the derived frame. Both writes were acked and read back on the first try, 600 ms after the ack. The ack echoes the old table, cut short. The owner confirmed that the new gesture works. The left triple tap is now previous. See §10.6
2026-10-27 W7 find start --yes, find stop pass Both acked. Both buds rang, kept ringing after budctl disconnected, and stopped on find stop. See §10.5
2026-10-27 W5d set dual on, off pass Acked, the immediate read-back showed the change, and the laptop stayed connected. Only one host was connected. See §10.2
2026-10-27 W5c set wind off, on pass (protocol) Acked, and the immediate read-back showed the change. Not audible in music. See §10.2
2026-10-27 W5b set spatial on, off pass Acked, the immediate read-back showed the change, and the owner heard a difference. See §10.2
2026-10-27 W5a set bass-boost on, off pass (protocol) Acked, and the immediate read-back showed the change. The owner heard no clear difference with EQ pure-bass. See §10.2
2026-10-27 W4 eq pure-raw, deep-bass, pure-vocals, pure-bass pass Acked and read back on the first try; the presets sound clearly different. See §10.3
2026-10-27 W3 game on, game off pass Acked, and the 1 s read-back showed the change. No push or prompt. See §10.2
2026-10-27 W2 anc light, anc medium, anc deep; level memory pass Each level acked and read back on the first try. The buds keep the level across a hold-gesture cycle. See §10.1
2026-10-27 W1 anc transparency, anc off, anc on pass Acked, read back on the first try, and the buds' voice prompts matched. See §10.1
2026-10-27 HW1 list, status, --json status, touch, not-connected path, busy path (local holder) pass, with fixes See below. Busy with the phone app as the holder is still untested

HW1, 2026-10-27 (budctl 0.1.0, --debug output pasted by the owner):