Skip to content

Commit a5fbe1f

Browse files
committed
Fix EQ preset command (0xF010) and document share format
The previous EQ preset command (0x4006) was wrong, so presets had no effect. earctl confirms the real command set: EQ preset is 0xF010 with payload [mode, 0x00]. - protocol.py: correct EQ preset frames; start the frame sequence number at 1 to match the reference implementation. - docs/PROTOCOL.md: document the verified command table and the decoded Nothing X Advanced-EQ QR share format (gzip+base64, 8 bands of gain/frequency/Q float32 + profile name). https://claude.ai/code/session_014KrpftzUgVFGFXVUU3DUTT
1 parent 50cf7ce commit a5fbe1f

3 files changed

Lines changed: 30 additions & 9 deletions

File tree

docs/PROTOCOL.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,33 @@ This is implemented as `crc16ansi()` + `build_frame()` in
4141
so frames are now generated programmatically instead of hardcoded.
4242

4343
Source of the algorithm: Gadgetbridge `CheckSums.getCRC16ansi` and
44-
`NothingProtocol`. Note: the real Ear (3) appears to **not strictly validate**
45-
the CRC (frames with a wrong CRC still took effect), but we compute it correctly
46-
anyway for compatibility.
44+
`NothingProtocol`, and `DaanHessen/earctl` for the command set. Note: the real
45+
Ear (3) appears to **not strictly validate** the CRC (frames with a wrong CRC
46+
still took effect), but we compute it correctly anyway for compatibility.
4747

48-
Commands: `0xf00f` = ANC set, `0xc01e` = ANC status query, `0x4006` = EQ preset
49-
(EQ command/payload is a reconstruction, pending hardware confirmation).
48+
### Commands (from earctl, verified frame format)
49+
50+
| Command | Value | Payload |
51+
|----------------------------|----------|----------------------------------|
52+
| Set ANC | `0xF00F` | `[01, mode, 00]` |
53+
| ANC status (request) | `0xC01E` | `[03]` |
54+
| Set EQ preset | `0xF010` | `[mode, 00]` (0=Balanced, 1=More Bass, 2=More Treble, 3=Voice) |
55+
| Set custom EQ (3-band) | `0xF041` | bass/mid/treble gains |
56+
| Request advanced EQ | `0xC04C` ||
57+
| Enable advanced EQ | `0xF04F` ||
58+
59+
### Nothing X share format (Advanced EQ QR codes)
60+
61+
A shared Nothing X "Advanced equaliser profile" QR encodes **gzip + base64** of:
62+
63+
```
64+
00 | 0x60 (= 96, band-data length) | 8 bands | 01 | name_len | name (ASCII)
65+
```
66+
67+
Each band is three little-endian float32 values: **gain (dB), frequency (Hz),
68+
Q**. Example (Balanced Bass, band 1): gain `+6.0`, freq `30.0`, Q `0.1`.
69+
Decoded from real QR samples; lets us import/export Nothing-X-compatible
70+
profiles.
5071

5172
### Known ANC frames (13-byte frames)
5273

src/nothing_ear/bluetooth/protocol.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
CMD_ANC = 0xF00F
3030
CMD_ANC_STATUS = 0xC01E
31-
CMD_EQ = 0x4006
31+
CMD_EQ = 0xF010
3232

3333

3434
class ANCMode(Enum):
@@ -95,7 +95,7 @@ def anc_frame(mode: ANCMode, fsn: int = 0) -> bytes:
9595

9696

9797
def eq_preset_frame(preset: EQPreset, fsn: int = 0) -> bytes:
98-
return build_frame(CMD_EQ, bytes([0x01, 0x01, _EQ_VALUE[preset]]), fsn)
98+
return build_frame(CMD_EQ, bytes([_EQ_VALUE[preset], 0x00]), fsn)
9999

100100

101101
def query_anc_frame(fsn: int = 0) -> bytes:
@@ -120,7 +120,7 @@ def __init__(self, address: str, channel: int = RFCOMM_CHANNEL):
120120
self.address = address
121121
self.channel = channel
122122
self._sock: socket.socket | None = None
123-
self._fsn = 0
123+
self._fsn = 1
124124

125125
def connect(self) -> None:
126126
sock = socket.socket(

tests/test_protocol.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def test_all_eq_frames_are_valid_and_have_selector():
4545
for preset, value in expected.items():
4646
data = bytes.fromhex(EQ_COMMANDS[preset])
4747
assert data[0] == PACKET_PREAMBLE
48-
assert data[10] == value
48+
assert data[8] == value
4949

5050

5151
def test_every_eq_preset_has_a_frame():

0 commit comments

Comments
 (0)