Krolyxon
Commit b835c9b

Branches

Refs →

budctlb835c9b

budctl dual: the T310 sends addresses first byte first (HW2)

HW2 passed with a fix. The owner's T310 answered 0x0112 with four
entries: the phone and the laptop connected (the laptop flagged as the
device that asked), and two phones that had connected before, with state
00. The layout matches the spec, but the laptop's entry held its adapter
address in written order, exactly as BlueZ reports it, so budctl printed
every address backwards. Another model's capture had it reversed.

- core: the decoder keeps the address as sent; BdAddr::from_reversed is
  gone. A test replays the HW2 reply, with the addresses and two names
  replaced by made-up ones.
- spec §9.10: evidence moved to ours-2026-10-28 where HW2 showed it; the
  device-kind bits are 0 for phones and the laptop alike. Verification
  log row HW2, without the addresses or other people's device names.
Author
krolyxon
Date
2026-10-28 02:19 +0530
Parent
65fed26
Commit
b835c9b96174f0e0d4b70c6592befb048cd8206d

+57 −26

4 files changed

  1. M crates/budctl-core/src/bt.rs +0 −7
  2. M crates/budctl-core/src/devices/realme_t310/codec.rs +35 −10
  3. M crates/budctl-core/src/devices/realme_t310/mod.rs +1 −1
  4. M docs/protocol/realme_t310.md +21 −8
M crates/budctl-core/src/bt.rs View
@@ -17,13 +17,6 @@ impl BdAddr {
1717 BdAddr(bytes)
1818 }
1919
20− /// An address stored last byte first, as the kernel's `bdaddr_t` and some buds' replies
21− /// store it.
22− pub(crate) fn from_reversed(mut bytes: [u8; 6]) -> Self {
23− bytes.reverse();
24− BdAddr(bytes)
25− }
26−
2720 /// The address as the kernel's `bdaddr_t` stores it: reversed.
2821 /// Spec: transport.md#sockets
2922 pub(crate) fn kernel_order(self) -> [u8; 6] {
M crates/budctl-core/src/devices/realme_t310/codec.rs View
@@ -340,8 +340,8 @@ pub(super) fn decode_codec(reply: &[u8]) -> Result<String> {
340340 Ok(name_of(&CODECS, codec).map_or_else(|| format!("unknown (0x{codec:02X})"), str::to_owned))
341341 }
342342
343−/// Each entry: the address last byte first, `len`, then `len` bytes: state, flags, the name's
344−/// length and the name. Spec: realme_t310.md#dual-devices
343+/// Each entry: the address, first byte first; `len`; then `len` bytes: state, flags, the name's
344+/// length and the name. Spec: realme_t310.md#dual-devices, #dual-address
345345 pub(super) fn decode_dual_devices(reply: &[u8]) -> Result<Vec<DualDevice>> {
346346 let mut r = ok_reply(reply, "the dual-connection list")?;
347347 let count = r.u8()?;
@@ -355,7 +355,7 @@ pub(super) fn decode_dual_devices(reply: &[u8]) -> Result<Vec<DualDevice>> {
355355 let name_len = entry.u8()?;
356356 let name = String::from_utf8_lossy(entry.take(usize::from(name_len))?).into_owned();
357357 devices.push(DualDevice {
358− address: BdAddr::from_reversed(address),
358+ address: BdAddr::new(address),
359359 name,
360360 connected: state == DUAL_CONNECTED,
361361 this_computer: flags & DUAL_THIS != 0,
@@ -1095,21 +1095,46 @@ mod tests {
10951095 assert_eq!(decode_codec(&hex("00 09")).unwrap(), "unknown (0x09)");
10961096 }
10971097
1098+ /// The HW2 reply (realme_t310.md#dual-devices), with the addresses and two of the names
1099+ /// replaced by made-up ones of the same length.
1100+ const HW2_DUAL: &str = "00 04 \
1101+ 02 00 00 00 00 01 15 02 00 12 4E 6F 74 68 69 6E 67 20 50 68 6F 6E 65 20 28 33 61 29 \
1102+ 02 00 00 00 00 02 09 02 01 06 4C 65 67 69 6F 6E \
1103+ 02 00 00 00 00 03 0A 00 00 07 50 68 6F 6E 65 20 41 \
1104+ 02 00 00 00 00 04 0F 00 00 0C 46 72 69 65 6E 64 27 73 20 41 31 36";
1105+
1106+ #[test]
1107+ fn dual_devices_from_hw2() {
1108+ let devices = decode_dual_devices(&hex(HW2_DUAL)).unwrap();
1109+ let rows: Vec<(String, &str, bool, bool)> = devices
1110+ .iter()
1111+ .map(|d| (d.address.to_string(), d.name.as_str(), d.connected, d.this_computer))
1112+ .collect();
1113+ assert_eq!(
1114+ rows,
1115+ [
1116+ ("02:00:00:00:00:01".to_owned(), "Nothing Phone (3a)", true, false),
1117+ ("02:00:00:00:00:02".to_owned(), "Legion", true, true),
1118+ ("02:00:00:00:00:03".to_owned(), "Phone A", false, false),
1119+ ("02:00:00:00:00:04".to_owned(), "Friend's A16", false, false),
1120+ ]
1121+ );
1122+ assert!(devices.iter().all(|d| !d.main_audio && !d.playing));
1123+ }
1124+
10981125 #[test]
1099− fn dual_devices_from_the_spec_example() {
1126+ fn dual_device_flags() {
1127+ // Connected, the main audio device and playing; then all three flags.
11001128 let devices = decode_dual_devices(&hex(
1101− "00 02 66 55 44 33 22 11 0A 02 06 07 50 69 78 65 6C 20 38 CC BB AA 00 1A 2B 07 02 01 04 61 72 63 68",
1129+ "00 02 11 22 33 44 55 66 04 02 06 01 50 11 22 33 44 55 67 04 02 07 01 51",
11021130 ))
11031131 .unwrap();
11041132 let [phone, computer] = devices.as_slice() else {
11051133 panic!("two devices expected: {devices:?}");
11061134 };
11071135 assert_eq!(phone.address.to_string(), "11:22:33:44:55:66");
1108− assert_eq!(phone.name, "Pixel 8");
11091136 assert!(phone.connected && phone.main_audio && phone.playing && !phone.this_computer);
1110− assert_eq!(computer.address.to_string(), "2B:1A:00:AA:BB:CC");
1111− assert_eq!(computer.name, "arch");
1112− assert!(computer.connected && computer.this_computer && !computer.main_audio && !computer.playing);
1137+ assert!(computer.this_computer && computer.main_audio && computer.playing);
11131138 }
11141139
11151140 #[test]
@@ -1119,7 +1144,7 @@ mod tests {
11191144 let [gone] = devices.as_slice() else {
11201145 panic!("one device expected: {devices:?}");
11211146 };
1122− assert_eq!(gone.address.to_string(), "06:05:04:03:02:01");
1147+ assert_eq!(gone.address.to_string(), "01:02:03:04:05:06");
11231148 assert_eq!(gone.name, "A");
11241149 assert!(!gone.connected && !gone.this_computer);
11251150 assert!(decode_dual_devices(&hex("00 00")).unwrap().is_empty());
M crates/budctl-core/src/devices/realme_t310/mod.rs View
@@ -1170,7 +1170,7 @@ mod tests {
11701170 let mut steps = handshake();
11711171 steps.extend([
11721172 tx(0x0112, 0x05, ""),
1173− rx(0x8112, 0x05, "00 01 CC BB AA 00 1A 2B 07 02 01 04 61 72 63 68"),
1173+ rx(0x8112, 0x05, "00 01 2B 1A 00 AA BB CC 07 02 01 04 61 72 63 68"),
11741174 ]);
11751175 let script = Script::new(steps);
11761176 let devices = open(&script).unwrap().dual_devices().unwrap();
M docs/protocol/realme_t310.md View
@@ -368,22 +368,34 @@ Evidence: *vendor*
368368 The phones and computers the buds keep for dual connection, and which of them are connected now. budctl only reads this list.
369369
370370 - **Gate:** bit 29 (*vendor*). The T310's command set lists `0112` (*t310*), and the owner's T310 sets bit 29 (*ours-2026-10-27*).
371−- **Request:** no payload (*vendor*).
372−- **Reply:** `00 <count>`, then `count` entries (*buds4*):
371+- **Request:** no payload. The T310 answers it (*ours-2026-10-28*, HW2).
372+- **Reply:** `00 <count>`, then `count` entries (*ours-2026-10-28*):
373373
374374 | Bytes | Field |
375375 |---|---|
376− | 6 | the device's Bluetooth address, last byte first |
376+ | 6 | the device's Bluetooth address, in the order people write it |
377377 | 1 | `len`: how many bytes of the entry follow |
378378 | 1 | state: `02` connected, `00` not connected |
379379 | 1 | flags |
380380 | 1 | name length |
381381 | name length | the device's name, UTF-8 |
382382
383−- **`len`** covers the state, the flags, the name length and the name, so it is 3 more than the name length (*buds4*). budctl finds the next entry with `len`, and ignores any bytes after the name (*derived*).
384−- **Flags** (*vendor*): bit 0 marks the device that asked (for budctl, the computer it runs on), bit 1 the main audio device, bit 2 a device playing audio. Bits 3-5 give the kind of device; their values are not known, so budctl does not show them.
385−- **A device that disconnects stays in the list**, with state `00` (*buds4*).
386−- **Example** (*derived* from the layout; not seen on a T310): `00 02 66 55 44 33 22 11 0A 02 06 07 50 69 78 65 6C 20 38 CC BB AA 00 1A 2B 07 02 01 04 61 72 63 68` lists two connected devices: "Pixel 8" at `11:22:33:44:55:66`, the main audio device and playing, and "arch" at `2B:1A:00:AA:BB:CC`, the device that asked.
383+- <a id="dual-address"></a>**Address order.** A capture from another model shows the address last byte first (*buds4*). The T310 sends it first byte first: the laptop's entry held its adapter's address exactly as BlueZ reports it (*ours-2026-10-28*, HW2).
384+- **`len`** covers the state, the flags, the name length and the name, so it is 3 more than the name length. It was, in all four entries at HW2 (*ours-2026-10-28*). budctl finds the next entry with `len`, and ignores any bytes after the name (*derived*).
385+- **Flags.**
386+ - Bit 0 marks the device that asked, for budctl the computer it runs on: the laptop's entry had `01` (*ours-2026-10-28*).
387+ - Bit 1 marks the main audio device and bit 2 a device playing audio (*vendor*). No entry had either set at HW2.
388+ - Bits 3-5 give the kind of device (*vendor*), but the T310 left them 0 for phones and the laptop alike (*ours-2026-10-28*), so budctl does not use them.
389+- **Devices that are not connected stay in the list**, with state `00`. At HW2 two phones that had connected before were listed that way (*ours-2026-10-28*).
390+- **Example:** the HW2 reply, with the addresses and two of the names replaced by made-up ones of the same length:
391+
392+ ```
393+ 00 04
394+ 02 00 00 00 00 01 15 02 00 12 "Nothing Phone (3a)" connected
395+ 02 00 00 00 00 02 09 02 01 06 "Legion" connected, the device that asked
396+ 02 00 00 00 00 03 0A 00 00 07 "Phone A" not connected
397+ 02 00 00 00 00 04 0F 00 00 0C "Friend's A16" not connected
398+ ```
387399 - **Push.** Event `06` of `0x0204` carries the same list, `06 <count> <entries…>`, when it changes (*buds4*). The T310 offers event `06` ([§6](#events-offered)), but budctl does not subscribe to it.
388400 - <a id="dual-manager"></a>**No device manager on the T310.** Connecting, disconnecting or unpairing one of these devices, and choosing a preferred one, are other commands (`0x0429`, read `0x0132`). They belong to bit 59, which the owner's T310 leaves clear (*ours-2026-10-27*, [§5](#bitmap-t310)), and the vendor data gives the T310 no device manager (*vendor*). budctl never sends them.
389401
@@ -581,7 +593,7 @@ The owner runs each checkpoint with `--debug` and pastes the output. Results go
581593 - Run `budctl list`, `budctl --debug status`, `budctl --json status` and `budctl --debug touch`.
582594 - Then run `status` again twice: once while the phone app is connected (the busy path), and once with the case lid closed (the not-connected path).
583595
584−**HW2, read-only:** `budctl --debug dual` ([§9.10](#dual-devices)), first with only this computer connected, then with the phone connected too (dual connection on).
596+**HW2, read-only:** `budctl --debug dual` ([§9.10](#dual-devices)) 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`.
585597
586598 **Writes, one per checkpoint, in this order:**
587599
@@ -606,6 +618,7 @@ The owner will not install realme Link (2026-10-27), so there is no app capture.
606618
607619 | Date | Checkpoint | Result | Notes |
608620 |---|---|---|---|
621+| 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](#dual-address)). The addresses and other people's device names are left out of this log on purpose |
609622 | 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](#custom-eq-delete) |
610623 | 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](#custom-eq-rename) |
611624 | 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](#custom-eq-create) |