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
- Bluetooth name:
realme Buds T310. budctl recognizes the buds by BlueZ'sNameproperty, compared exactly. vendor - Product id:
063C12. The reply to0x0103should be00 12 3C 06, which is the id in 3 bytes, little endian. The reply format is buds4; the T310 bytes are derived. - Not a T310. If the product id is different, budctl stops: the device is renamed, or it is some other model.
2. Transport
- Link: classic Bluetooth RFCOMM (SPP), not BLE. The connection is one long-lived byte stream, and the buds may send at any time.
- Service UUIDs. Try them in this order; each is resolved to a channel through SDP (transport.md#sdp). The owner's T310 offers the first, on channel 6 (ours-2026-10-27).
0000079A-D102-11E1-9B23-00025B00A5A5. The vendor app tries it first. buds4/vendor00001107-D102-11E1-9B23-00025B00A5A5. It is kept for other models; on Buds 4 it never connects.
- Fixed channel. Android clients sometimes use a fixed channel 15. budctl does not use a fixed channel.
- One client per host. On one host, only one socket can hold the control channel: a second one gets
EBUSY(HW1), and budctl reports a busy channel instead of retrying. Across hosts, the owner's T310 served the laptop and a control app on the phone at the same time, with dual connection on (ours-2026-10-27, question 4). - Lid close. Closing the case lid ends the link. buds4
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
- A slice frame is
AA len slice 00 fsn <part of cmd/seq/paylen/payload>, withlen = 3 + part length. - The receiver joins the parts in order and parses the result when the
03slice arrives. A middle or last slice with no first slice is dropped. - Senders slice only above about 660 bytes, so budctl never sends slices.
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
- Treat the input as a byte stream: one read may hold several frames or part of one.
- Throw away bytes before
AA. - If the length is implausible (more than 3 length bytes, below 4, or above 4096), drop that
AAand look for the next one. - If the paylen claims more bytes than the frame holds, keep the bytes that are there.
4. Sequence numbers and replies
-
Replies. The sender picks
seq. The reply has the same seq and the command id with bit 15 set (0x0106→0x8106). budctl matches a reply on (seq, cmd), which is also how HeyMelody does it. buds4/vendor -
budctl's seq values. budctl uses
01-BF, counting up and wrapping. Pushes from Buds 4 useFF(buds4); the T310 uses its own counter (§3). -
Status byte. The first payload byte of a reply is a status, and
00means success. A read reply with any other status carries no data. -
Command groups:
Range Group 0x01xxqueries 0x02xxevent registration and pushes 0x03xxfirmware update (budctl never sends these; see the non-goals) 0x04xxwrites 0x05xxrequests from the buds 0x0Fxxprivate
5. Command bitmap
- Reading it. The reply to the handshake
0x0100is00followed by the bitmap bytes. Read them least-significant bit first: bit n is bitn % 8of byten / 8. - Using it. A set bit enables a fixed group of commands. budctl never sends a command whose bit is clear, as the vendor apps don't. Bits beyond the end of the bitmap count as clear. vendor
- Always allowed:
0x0100-0x0104,0x0106,0x010B,0x010D.0x0200belongs to no bit; the vendor app sends it on every connect. vendor/buds4
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 |
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
- Bits that must be set for that list (derived): 0, 2, 3, 4, 5, 7, 8, 9, 10, 11, 17, 19, 20, 22, 29, 34, 38, 42.
- A bitmap reply with exactly those bits:
00 BD 0F 5A 20 44 04(derived). - The owner's T310 answers
00 BF 1F 5A 20 45 04 40 00(ours-2026-10-27; the unit tests use it). That is every bit above plus bits 1, 12, 32 and 54, which gate no command budctl uses. Every bit listed under "what the T310 lacks" is clear. - What the T310 lacks. These commands are absent, so budctl never sends them:
0x012Fbatch query (bit 56): every read goes out alone.0x041E3D-audio source (bit 43).0x0413phone state (bit 27).0x0130/0x0427alert volume (bit 57).0x0133/0x042Dtap sensitivity (bit 61).0x0423game sound (bit 49).0x012A/0x0422spatial type (bit 47).
6. Session start
The vendor sequence (buds4, matching HeyMelody):
- Wait 300 ms after the socket connects, then send one message every 200 ms.
- Order:
0x0100→0x0103→0x0200→0x0205→ reads. - There is no authentication or key exchange.
budctl does:
-
Connect, then wait 300 ms. During the wait, keep answering the buds' requests (§7).
-
0x0100. Keep the bitmap. -
0x0103. The id must be063C12(§1). -
0x0200→00 <count> <codes...>: the event codes the buds can push. Buds 4 offers01 02 03 04 08 0B F1 F2 F3(buds4). The owner's T310 offers01 02 03 04 05 06 0D F1 F4(ours-2026-10-27). -
0x0205 <count> <codes...>(count first) subscribes. budctl asks for these codes, keeping only the ones the buds offered (vendor):01battery,02wearing and03noise control;04(fit-test result) if0x0405is allowed.
Other clients also subscribe to
08when0x040Dis allowed. budctl does not: this spec does not say what event08carries or what0x040Ddoes, 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". The0x8205ack 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, a00result for each code (ours-2026-10-27). -
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).
- Test vector (buds4): at Unix time 1750000000, the request
AA 07 00 00 00 05 02 00 00gets the answerAA 0C 00 00 00 85 02 05 00 00 80 E1 4E 68. - Not answered: other
0x05xxmessages and all pushes. Neither vendor app answers them. - Pushes
F2andF4: other clients acknowledge these. budctl does not yet; see open questions. - On the T310 (ours-2026-10-27): right after connecting, the buds send
0x0501with no payload (slice byte08, their own seq). budctl does not answer it (it is an "other0x05xx" message), and no link reset followed in a 5 s session. No0x0500time request was seen.
8. Pushes
budctl reads none of these yet. It skips them while waiting for replies (and prints them with --debug).
-
0x0204event push. Seq isFF; classify by cmd, because one Buds 4 push used another seq.payload[0]is the event code:Code Event Layout after the code 01battery <count> (<part> <raw>)…(as in §9.1)02wearing <count> (<part> <state>)…03noise control 01 01 <mask LE>= new mode;02 <kind> <mask>= new hold cycle (not a mode)04fit-test result (<dev> <result>)…05game mode <state>. The T310 never sends this (t310)F1a gesture fired <side> <button> <act> <fn> …. Diagnostic only -
0x0503<count> (<id> <value>)…: switch values changed (vendor, other realme models). -
0x0504<id>: the EQ preset changed.
9. Reads
9.1 Battery: 0x0106
This command is always allowed. Battery reading is confirmed on a T310 (t310); the byte layout is buds4.
- Request: no payload.
- Reply:
00 <count> (<part> <raw>)….partis01left,02right or03case.level = raw & 0x7F(percent).charging = raw & 0x80.- A level of 0 means "not known", so leave that part out.
- Example:
00 03 01 64 02 64 03 50→ left 100 %, right 100 %, case 80 %. - No list. A count of 0, or pairs that run past the end, mean no list.
- Lid closed. The case, and a bud inside it, report only while the lid is open (buds4).
9.2 Firmware: 0x0105
Evidence: buds4
- Reply:
00 <count> <UTF-8 text>. The text is a comma-separated list ofdevice,kind,versiontriples, where device1is left,2right and3case. - Display. Show the versions of kind
2, in reply order, joined with dots. HeyMelody shows them this way. If there are none, show the raw text. - Example:
1,2,138,2,2,138,3,1,01,3,2,105→138.138.105. - On the T310 (ours-2026-10-27):
00 02then1,2,1.1.0.43,2,2,1.1.0.43. Each version is already a full dotted version, so joining them with dots would be misleading. When any version contains a dot, budctl shows each distinct version once, joined with/: here1.1.0.43.
9.3 Codec in use: 0x0114
Evidence: buds4
- Reply:
00 <codec>. - Codec values:
01SBC,02AAC,03LDAC,04aptX,05aptX HD,06aptX Adaptive,07LHDC,08LHDC V5. - The T310 offers AAC and SBC (vendor).
9.4 Noise control: 0x010C 01 01
-
Reply:
00 01 01 <mask LE>. The mask is 1-4 bytes; the T310's width is unknown, so read every byte that is there. -
Values. On realme buds the value read back uses the same bits as the write (vendor; tested on the Air7 Pro only):
Mask T310 mode 01off 02transparency 04noise cancelling, light 08noise cancelling, deep. Also plain "noise cancelling on": both are the same value 10noise cancelling, medium -
No Smart. The T310 has no Smart level (
20). -
Decoding
08. budctl shows08as noise cancelling with level deep. This still needs a hardware check (see open questions). -
Noise control as a whole is confirmed on a T310 (t310). The bit values above are vendor.
9.5 Switches: 0x010D
- Request:
<count> <ids…>. budctl asks only for the T310's ids:05 06 11 1A 1B 1D.05 06 11 1B 1Dis the vendor app's query list for the T310 (vendor).1Ais added because the model has the wind-noise row (windMode).
- Reply:
00 <count> (<id> <value>)…. A value of01means on,00off.
- The T310 answers every id it is asked, even ids it lacks (a T310 log: all 23 ids came back). A switch being in the reply therefore proves nothing.
- Writing ids
04 0B 18 27 35got status01, "the buds lack this id" (t310). - That is why budctl asks only for the ids above.
- Writing ids
9.6 EQ preset: 0x010F
Preset names are the vendor app's (vendor).
-
Reply:
00 <id>. -
Built-in presets on realme buds (vendor, unverified on the T310):
id Preset 00Pure raw 01Deep bass 02Pure vocals 03Pure bass -
Any other id is a custom preset. On Buds 4, custom ids start at
04(buds4).
9.7 Bass level: 0x0124
Evidence: vendor; the T310's command set lists it.
- Reply:
00 <min s8> <max s8> <level s8>. The usual range bytes areFB 05, meaning -5..+5. - The T310 does not answer it with bass boost off (ours-2026-10-27, HW1): an empty
0x0124got no reply within 3 s, although bit 38 is set. Bass boost (switch1D) was off at the time. - Not with bass boost on either (ours-2026-10-27, W6a). The vendor data ties the bass level to the bass engine, so the read might have needed bass boost on. With
0x010Dreporting1D 01, an empty0x0124again got no reply within 3 s. budctl does not send it. Without a read, a bass-level write (§10.4) can only be checked by ear.
9.8 Touch controls: 0x0108
-
Reply:
00 <count> (<dev> <btn> <act> <fn>)…. Each entry is 4 bytes. -
dev:01left,02right,04both (on-call rows). -
btn:01main gestures,06the on-call group. -
act:01single tap,02double tap,03triple tap,04hold,05slide,06long hold. -
fnvalues (measured on buds4; realme uses the same values per vendor):fnFunction 00none 01play/pause 03voice assistant 05previous track 06next track 07volume (slide) 08noise-control cycle 09favourite music 0Aswitch track (slide) 0Bvolume up 0Cvolume down 0Dswitch devices 11game mode 1Cdecline call 1Danswer/end call -
What realme Link offers for the T310 (vendor). Each bud is set on its own, as
dev01/02 withbtn01. budctl offers exactly these choices and no others, although the family defines morefnvalues.budctl touch both …changes the left and the right bud's entry in one write; it never writes adev 04row.Gesture Choices double tap none, play/pause, next, voice assistant triple tap none, previous, next, voice assistant, volume up, volume down, game mode hold none, noise-control cycle, voice assistant, game mode -
Table shape. No T310 gesture table has ever been captured. Its shape is unknown, so budctl shows every entry it receives.
9.9 Hold noise-control cycle: 0x010C 02 01
Evidence: vendor
- Reply:
00 02 01 <mask LE>. The mask lists which modes a hold steps through, using the noise-control bits:01off,02transparency,08noise cancelling. - Both buds. On realme buds one setting covers both buds (kind
01). - When it applies. The cycle is used only where a bud's hold gesture is
fn 08.
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:
- A write reply is
0x84xx <status …>, and00means accepted. - Accepted is not the same as applied. Always read the state back and compare it.
- How budctl reads back (budctl's own policy, not a protocol fact): it reads once right after the ack. If the old value is still there, it reads again every 300 ms for up to 2 s. If the value has still not changed, the command fails with exit 4 ("accepted but not applied"). A write that has its own timing, such as game mode (§10.2), says so in its section.
10.1 Noise control: 0x0404 01 01 <mask>
- Payload: one bit set, in the fewest bytes, little endian. The T310 masks are the same as for §9.4: off
01, transparency02, light04, deep or "on"08, medium10. - Reply:
0x8404 <status>.- Status
0Emeans no bud is in an ear (buds4). The T310 has no in-ear sensor (t310), so it may never send it.
- Status
- Check: read
0x010C 01 01again. anc on. budctl writes08(deep), because no "on at the saved level" value is known, and budctl keeps no device state of its own. The buds do remember the level (ours-2026-10-27, W2): afteranc light,anc transparencyand a hold gesture, noise cancelling came back as light (04). How an app turns noise cancelling on at the saved level is unknown. Writing08forces deep, and the saved level cannot be read while noise cancelling is off. Finding out needs an Android HCI snoop of realme Link turning noise cancelling on after a level was chosen (open questions).- Levels on the T310 (ours-2026-10-27, W2):
01 01 04(light),01 01 10(medium) and01 01 08(deep) were each acked with00within 15-30 ms and read back on the first try. - On the T310 (ours-2026-10-27, W1): payloads
01 01 01,01 01 02and01 01 08each got0x8404 00within 20-80 ms. The first read-back, about 200 ms later, already showed the new mask. The buds played their own voice prompts ("Normal Mode", "Transparent Mode", "Noise Cancelling"). They sent no event-03push for the change, although it was subscribed.
10.2 Switches: 0x0403 <id> <01|00>
- Reply:
0x8403 <status>:00accepted01the buds lack this id02or anything else: refused for now
- Check: read
0x010Dagain. - Game mode on the T310. The buds never push the new value (t310). On Buds 4 the change lands about 1 s after the ack, and a read sooner still returns the old value (buds4). So budctl reads back 1 s after the ack, then every 300 ms until 3 s after the ack.
- Bass boost on the owner's T310 (ours-2026-10-27, W5a):
0x0403 1D 01and1D 00were acked with00within 30 ms. The immediate read-back already showed the new value, and no push came. With the EQ on pure-bass, the owner heard no clear difference. - 3D sound on the owner's T310 (ours-2026-10-27, W5b):
0x0403 1B 01and1B 00were acked with00within 30 ms. The immediate read-back showed the new value. The owner heard a difference but could not name it. - Wind-noise reduction on the owner's T310 (ours-2026-10-27, W5c):
0x0403 1A 00and1A 01were acked with00within 35 ms. The immediate read-back showed the new value. It is not audible in music. - Dual connection on the owner's T310 (ours-2026-10-27, W5d):
0x0403 11 01and11 00were acked with00within 50 ms. The immediate read-back showed the new value. The laptop's link stayed up both ways. Only one host was connected (the phone's Bluetooth was off), so turning it off with two hosts connected is still untested. - Game mode on the owner's T310 (ours-2026-10-27, W3):
0x0403 06 01and06 00were acked with00within 13 ms. The read 1 s later already showed the new value. No push came, and the buds played no prompt. - Dual connection. Other clients also send
0x0413afterwards. The T310's bitmap lacks it, so budctl sends only0x0403 11. Turning dual connection off may drop the link to one host, so it is tested last.
10.3 EQ preset: 0x0406 <id>
Evidence: vendor
- Reply:
0x8406 <status>. - Check: read
0x010Fagain. - On the owner's T310 (ours-2026-10-27, W4): ids
00-03were each acked with00within 20-40 ms and read back on the first try. The owner heard a clear difference between them. The names come from the vendor app; the owner did not check by ear that each sound matched its name.
10.4 Bass level: 0x041B FB 05 <level s8>
Evidence: vendor
- Range: -5..+5.
- Check: read
0x0124again. Not possible on the T310, which never answers that read (§9.7), so budctl checks only the0x841Bstatus and the owner listens. - What budctl does:
budctl bass <level>refuses a level outside -5..+5 before sending anything. - Expected frame (derived), for
bass 3with seq05:AA 0A 00 00 1B 04 05 03 00 FB 05 03. - On the owner's T310 (ours-2026-10-27, W6b):
FB 05 FB(-5),FB 05 05(+5) andFB 05 00(0) were each acked with a plain0x841B 00within 35 ms. Unlike the gesture write, the ack carries no data. Bass boost was off. By ear the owner heard at most a slight difference between -5 and +5. Whether the level does more with bass boost on is untested.
10.5 Find my earbuds: 0x0400 01 / 0x0400 00
Evidence: buds4
01starts ringing both buds and00stops it. The reply is0x8400 00.- It is loud.
budctl find startrefuses to run (exit 1) without--yes, and checks that before it connects.find stopneeds no flag. - Only the ack can be checked: there is no state to read back.
- On the owner's T310 (ours-2026-10-27, W7):
0x0400 01and0x0400 00were each acked with0x8400 00within 18 ms.01rang both buds. The ringing continues after budctl disconnects, until00silences it, so budctl does not need to hold the link while the buds ring.
10.6 Touch controls: 0x0401 <count> (<dev> <btn> <act> <fn>)…
The command and layout are buds4; the realme rules are vendor.
- Whole table. The write carries the whole table. Start from a table read in the same session and change only the targeted entries.
- Reply:
0x8401 00. On the owner's T310 (ours-2026-10-27, W8), the00is followed by the table as it was before the write, cut to the length of the write's payload: 25 bytes, so the lastfnbyte is missing. budctl checks only the status byte, and reads the table back to see the change. - Check: read
0x0108again after about 600 ms and compare. - Wrong command ids fail silently. For example,
0x0402gets no reply at all. - What budctl does (budctl's own policy, not a protocol fact):
- It checks the side, gesture and action against the T310's choices before it sends anything.
- It reads
0x0108and changes thefnbyte of the targetedbtn 01entries only. Every other byte, including the order of the entries, is copied from the reply. The payload is the reply without its status byte. - It never adds an entry: if the buds did not report the targeted entry, it refuses.
- If the entry already has the action, it sends nothing.
--dry-runreads the table and prints the payload it would send, then stops.- After the ack it reads the table back 600 ms later, then every 300 ms until 2 s after the ack (read-back).
- On the owner's T310 (ours-2026-10-27, W8):
touch left triple-tap volume-upsent exactly the expected frame below. The ack came within 25 ms, and the first read-back (600 ms after the ack) already showedfn 0B. A second write,volume-up→previous(fn 05), behaved the same way. No push came. The owner confirmed that the new triple tap works. - Expected frame (derived, matched on the wire at W8), for
touch left triple-tap volume-upon the owner's HW1 table, with seq06:AA 20 00 00 01 04 06 19 00 06 01 01 02 01 01 01 03 0B 01 01 04 08 02 01 02 01 02 01 03 06 02 01 04 08.
10.7 Hold cycle: 0x0404 02 01 <mask>
Evidence: vendor
- Mask: the fewest bytes that hold it, and at least 2 modes (vendor).
- Level bits. The buds silently drop level bits (buds4).
- Check: read
0x010C 02 01. - Reply:
0x8404 <status>, as for §10.1. - What budctl does (budctl's own policy):
budctl touch cycle <mode> <mode> [<mode>]acceptsoff(01),transparency(02) andanc(08), at least two of them and none twice, in any order, because the mask has no order. It never sets the level bits04and10. It reads the cycle back as for §10.1. - Expected frame (derived, matched on the wire at W9 apart from the seq byte), for all three modes with seq
06:AA 0A 00 00 04 04 06 03 00 02 01 0B. - On the owner's T310 (ours-2026-10-27, W9):
02 01 0B(off, transparency, noise cancelling) and02 01 09(off, noise cancelling) were each acked with0x8404 00within 60 ms. The first read-back already showed the new mask. The owner confirmed that a hold steps through the new cycle. A cycle that includesoffis accepted. - On the owner's buds the cycle is
0A(transparency and noise cancelling, HW1), and every hold gesture isfn 08, so the cycle applies to both buds.
10.8 Fit test: 0x0405 01 / 0x0405 00
Evidence: buds4
01starts the test and00stops it.- Result: arrives as push
04 (<dev> <result>)…, where1= good,0= average and6= poor. The push needs event04subscribed. - Timing: about 8.6 s on Buds 4. Both buds must be in the ears.
- The T310 runs no test (ours-2026-10-27, W10). With both buds in the owner's ears,
AA 08 00 00 05 04 05 01 00 01(start) got0x8405 00within 19 ms. Then nothing came for 15 s: no push of any kind, and no sound.0x0405 00(stop) got0x8405 00, but after 417 ms, where other writes take about 20 ms. Event04was subscribed and acked. Bit 9 of the bitmap is set, so the bitmap overstates what the T310 does here. - budctl does not send it. The owner dropped the fit test (2026-10-27), and budctl has no
fit-testcommand. The W10 implementation is in git history (commit4c49f6c): it waited 15 s for the event-04push and sent stop if none came.
10.9 Custom EQ: list 0x0122, write 0x0418
Evidence: vendor
-
List reply:
00 <count>then, per preset:<selected> <min> <max> <id> <nameLen> <name> <bands> (<freq u16 LE> <gain s8>)…. -
Write:
0x0418 <action> <min> <max> <id> <nameLen> <name> <bands> (<freq> <gain>)…. The reply is0x8418 00 <id>.01creates a preset; send id00and the buds assign one.02saves the whole preset and selects it.03deletes it.
-
Ids are renumbered after a create or delete, so read the list again each time.
-
Defaults: 6 bands at 62, 250, 1000, 4000, 8000 and 16000 Hz. Range bytes
FA 06mean -6..+6 dB. At most 3 presets. -
The owner's T310 (ours-2026-10-27, W11a) answered the list read within 26 ms with one preset:
00 01 status, 1 preset 01 FA 06 04 selected, -6..+6 dB, id 04 07 43 75 73 74 6F 6D 31 name, 7 bytes: "Custom1" 06 6 bands 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00 50, 250, 1000, 4000, 8000, 16000 Hz, each 0 dB- The lowest band is 50 Hz, not 62. The vendor default above does not hold on the T310; use the bands the buds report.
- The preset was already there when budctl first read it. Its origin is unknown: it may be the factory state, or left over from realme Link.
- It was in use:
0x010Fanswered00 04at the same time, socustom-4is a valid current EQ, and the list'sselectedbyte (01) agrees.
-
What budctl does (W11, one step per checkpoint, no app capture):
- W11a (read-only):
budctl eqwith no preset reads0x010Fand the list. A custom preset shows ascustom-<id>, the name0x010Fdecoding uses, followed by its stored name and bands. - W11b, save (
02):budctl eq custom-<id> <hz>=<gain>…reads the list and copies the preset as listed: range bytes, id, name bytes, and the bands and their order. It changes only the gains of the named bands. A band the preset lacks, a band named twice, or a gain outside the preset's range is refused before anything is sent.budctl eq custom-<id>with no bands sends the preset unchanged, which only selects it. budctl never selects a custom preset with0x0406, since this spec does not say that works. - Check: read the list again, as for read-back. The preset must have the written bands and its
selectedbyte set. - Expected frame (derived), for
eq custom-4 50=+3 16000=-2on the owner's list, with seq06:AA 26 00 00 18 04 06 1F 00 02 FA 06 04 07 43 75 73 74 6F 6D 31 06 32 00 03 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E FE. - On the owner's T310 (ours-2026-10-27, W11b): that frame went out byte for byte. The buds answered
0x8418 00 04within 53 ms, and the list read about 150 ms after the ack already had +3 dB at 50 Hz, -2 dB at 16000 Hz, the name unchanged andselected01. Writing the flat bands back worked the same way. 02selects the preset (ours-2026-10-27): aftereq pure-raw, a bareeq custom-4(the preset saved unchanged) made0x010Freadcustom-4again.- W11c, create (
01), wanted by the owner (2026-10-28):budctl eq new <name>reads the list first. It refuses before sending anything when the buds keep 3 presets already, when a preset has that name already, or when there is no preset to copy the bands from.- The name: 1 to 16 bytes of UTF-8, with no control characters and no spaces at either end. The 16 bytes are budctl's own limit: the protocol allows 255 (one length byte), and the vendor's limit is unknown.
- The new preset copies the range bytes and the bands (frequencies and order) of the first listed preset, with every gain at 0 and id
00. The vendor default bands do not hold on the T310 (see above). - Check: the reply
00 <id>gives the new id. Ids can be renumbered after a create, so budctl reads the list again, as for read-back. It finds the preset by that id and its name, else by its name alone. - A create selects the new preset (ours-2026-10-28, W11c): in the list read after it, the new preset's
selectedbyte is01and the others' are00. - Expected frame (derived), for
eq new Teston the owner's list, with seq06:AA 23 00 00 18 04 06 1C 00 01 FA 06 00 04 54 65 73 74 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00. - On the owner's T310 (ours-2026-10-28, W11c): that frame went out byte for byte. The buds answered
0x8418 00 05within 50 ms. The list read 150 ms after the ack had two presets: Custom1 (id04, unchanged,selected00) and Test (id05, the six bands at 0 dB,selected01). So the reply's id was the listed id: no renumbering on this create.
- W11d, rename, wanted by the owner (2026-10-28):
budctl eq rename custom-<id> <name>reads the list and saves (02) the preset as listed with only its name changed. The name follows the create rules, and another preset's name is refused before anything is sent. Like any save, a rename also selects the preset.- Evidence: ours-2026-10-28 (W11d). A save carries the name; one with a different name renames the preset in place, under the same id.
- Check: read the list again, as for read-back. The preset with that id must have the new name.
- Expected frame (derived), for
eq rename custom-5 Gymon the owner's list after W11c, with seq06:AA 22 00 00 18 04 06 1B 00 02 FA 06 05 03 47 79 6D 06 32 00 00 FA 00 00 E8 03 00 A0 0F 00 40 1F 00 80 3E 00. - On the owner's T310 (ours-2026-10-28, W11d): that frame went out byte for byte, and the buds answered
0x8418 00 05within 100 ms. The list read 100 ms after the ack has id05named "Gym", with the same flat bands andselected01; Custom1 is unchanged.
- Deleting (
03) comes later, if it is wanted.
- W11a (read-only):
11. Not on the T310
- Wear detection. Id
04was refused with01(t310), and there is no in-ear sensor. A bud out of the case reads as "out" in every state. - Hi-Res
18, hearing profile0B, game sound27, hold volume35: refused with01(t310). - Not in the T310 bitmap (§5): alert volume, tap sensitivity, spatial type, 3D-audio source, game EQ, head gestures, personalised ANC and batch query.
12. Open questions (need a capture or a test on the owner's buds)
Which service UUID the T310 offers, and its RFCOMM channel.Answered at HW1:0000079a-d102-11e1-9b23-00025b00a5a5on channel 6.The real bitmap bytes, the product-id reply and the offered events.Answered at HW1; see §5 and §6.- Partly answered at HW1: no
0x0500, but an unanswered0x0501with no reset (§7).F2/F4were offered but not subscribed, so none arrived. What0x0501asks for is unknown. - Mostly answered (2026-10-27): with dual connection on and a control app connected on the phone, budctl's
statusgot every reply, and changes budctl made showed up in the phone app. So the phone does not make the channel busy. A local holder givesEBUSY(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). - The width of the noise-control value: one byte so far, on reads and writes.
08is the deep level: light, medium and deep read back as04,10and08(W2). Whether the T310 answers idIt does (HW1). What id1A(wind).05means is still open: it reads00.- 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. - 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. - Whether the minimum 200 ms spacing between sends is needed.
- Why the T310 never answers
0x0124, with bass boost off (HW1) or on (W6a), although bit 38 is set (§9.7). - 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:
- Run
budctl list,budctl --debug status,budctl --json statusandbudctl --debug touch. - Then run
statusagain twice: once while the phone app is connected (the busy path), and once with the case lid closed (the not-connected path).
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 |
| 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; delete only if wanted |
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-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):
listfindsrealme Buds T310at88:0E:85:46:FE:5C, connected, recognized byName.- SDP: service
0000079a-d102-11e1-9b23-00025b00a5a5on RFCOMM channel 6. - Handshake: bitmap
00 BF 1F 5A 20 45 04 40 00, product id00 12 3C 06(063C12), events offered01 02 03 04 05 06 0D F1 F4. The subscribe04 01 02 03 04is acked. - Reads that answered, with what budctl showed:
- battery
00 03 02 64 01 64 03 00: left and right 100 %, case unknown; - noise control
00 01 01 08: anc, deep; - switches
00 06 05 00 06 00 11 01 1A 01 1B 00 1D 00: game off, dual on, wind on, 3D off, bass boost off; - EQ
00 03: pure-bass; - codec
00 01: SBC; - firmware
1,2,1.1.0.43,2,2,1.1.0.43; - gestures: both buds double tap play/pause, triple tap next, hold noise cycle;
- hold cycle
00 02 01 0A: transparency and anc.
- battery
- Every reply came 50-90 ms after its request.
0x0124(bass level) got no reply in 3 s.- Lid closed:
statusreports "paired but not connected" and exits 2. - Busy path, first try: with the phone app connected, BlueZ listed the buds as not connected to the laptop, so budctl stopped before RFCOMM (exit 2). This is inconclusive: the laptop's link was down, so the control channel was never tried.
- Busy path, local variant: a second socket on the laptop connected to channel 6 and held it, sending nothing. budctl's connect then failed with
EBUSY(os error 16), and budctl reported "control channel busy" and exited 3. budctl sent nothing to the buds. This shows that a local client holding the channel gives exit 3. When the phone holds it, the buds decide what happens, and that is still untested. - Fixes made from this checkpoint:
- Still to confirm by the owner against the phone app: the noise-control mode and level, the EQ preset, the dual and wind switches, the gestures and the hold cycle.