Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
pytestdebug.log
*.actual

# Translations
*.mo
Expand Down
31 changes: 25 additions & 6 deletions Client/BluetoothWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,18 @@ Buffer BluetoothWrapper::sendCommandAndReadResponse(const std::vector<char>& byt

void BluetoothWrapper::_waitForAck()
{
auto msg = this->_readMessage();
this->_seqNumber = msg.seqNumber;
// Notifications can arrive between a SET and its ACK. _readMessage() acknowledges DATA_MDR frames;
// keep consuming until the device's actual ACK arrives so it cannot poison the next command.
for (int i = 0; i < 16; i++)
{
auto msg = this->_readMessage();
if (msg.dataType == DATA_TYPE::ACK)
{
this->_seqNumber = msg.seqNumber;
return;
}
}
throw RecoverableException("No acknowledgment received from device", true);
}

CommandSerializer::Message BluetoothWrapper::_readMessage()
Expand All @@ -118,11 +128,17 @@ CommandSerializer::Message BluetoothWrapper::_readMessage()
{
numRecvd = this->_connector->recv(buf, sizeof(buf));
}
if (numRecvd <= 0)
{
throw RecoverableException("Bluetooth connection closed while reading a frame", true);
}

size_t messageStart = 0;
size_t messageEnd = numRecvd;
// Bytes outside START/END markers are transport noise, not message data. When a frame spans recv()
// calls, the next chunk starts inside the frame and therefore begins at byte zero.
size_t messageStart = ongoingMessage ? 0 : static_cast<size_t>(numRecvd);
size_t messageEnd = static_cast<size_t>(numRecvd);

for (size_t i = 0; i < numRecvd; i++)
for (int i = 0; i < numRecvd; i++)
{
if (buf[i] == START_MARKER)
{
Expand All @@ -146,6 +162,10 @@ CommandSerializer::Message BluetoothWrapper::_readMessage()
}

msgBytes.insert(msgBytes.end(), buf + messageStart, buf + messageEnd);
if (msgBytes.size() > MAX_BLUETOOTH_MESSAGE_SIZE)
{
throw RecoverableException("Bluetooth frame exceeds the maximum message size", true);
}
} while (!messageFinished);

auto msg = CommandSerializer::unpackBtMessage(msgBytes);
Expand All @@ -160,4 +180,3 @@ CommandSerializer::Message BluetoothWrapper::_readMessage()

return msg;
}

2 changes: 1 addition & 1 deletion Client/ByteMagic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ std::string MACBytesToString(unsigned char addr[])
{
// Create hex string from the bytes.
std::stringstream mac_addr_str;
for (int i = 0; i < MAC_ADDR_BYTES_LEN; i++)
for (size_t i = 0; i < MAC_ADDR_BYTES_LEN; i++)
{
mac_addr_str << std::hex << (((addr[MAC_ADDR_BYTES_LEN - 1 - i] & 0xF0) >> 4) & 0xF);
mac_addr_str << std::hex << (addr[MAC_ADDR_BYTES_LEN - 1 - i] & 0x0F);
Expand Down
37 changes: 13 additions & 24 deletions Client/CommandSerializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ constexpr unsigned char ESCAPED_BYTE_SENTRY = 61;
constexpr unsigned char ESCAPED_60 = 44;
constexpr unsigned char ESCAPED_61 = 45;
constexpr unsigned char ESCAPED_62 = 46;
constexpr int MAX_STEPS_WH_1000_XM3 = 19;
constexpr int MAX_AMBIENT_SOUND_LEVEL_V1 = 20;

namespace CommandSerializer
{
Expand Down Expand Up @@ -53,7 +53,7 @@ namespace CommandSerializer
{
if (i == src.size() - 1)
{
throw std::runtime_error("No data left for escaped byte data");
throw RecoverableException("No data left for escaped byte data", true);
}
i = i + 1;
switch (src[i])
Expand All @@ -71,7 +71,7 @@ namespace CommandSerializer
break;

default:
throw std::runtime_error("Unexpected escaped byte");
throw RecoverableException("Unexpected escaped byte", true);
break;
}
}
Expand Down Expand Up @@ -141,7 +141,7 @@ namespace CommandSerializer
//1 (type) + 1 (seq) + 4 (size) + 1 (checksum) = 7 bytes minimum, before any payload.
if (unescaped.size() < 7)
{
throw std::runtime_error("Invalid message: Smaller than the minimum message size");
throw RecoverableException("Invalid message: Smaller than the minimum message size", true);
}

unsigned int dataSize =
Expand All @@ -150,10 +150,14 @@ namespace CommandSerializer
((unsigned int)(unsigned char)unescaped[4] << 8) |
((unsigned int)(unsigned char)unescaped[5]);

if (unescaped.size() < (size_t)6 + dataSize + 1)
if (dataSize > unescaped.size() - 7)
{
throw RecoverableException("Invalid message: declared size exceeds received data", true);
}
if (dataSize != unescaped.size() - 7)
{
throw RecoverableException("Invalid message: trailing data after checksum", true);
}

//Checksum covers everything from DATA_TYPE up to (not including) the checksum byte, over the unescaped bytes.
if ((unsigned char)unescaped[6 + dataSize] != _sumChecksum(unescaped.data(), 6 + dataSize))
Expand All @@ -168,32 +172,18 @@ namespace CommandSerializer
return ret;
}

NC_DUAL_SINGLE_VALUE getDualSingleForAsmLevel(char asmLevel)
Buffer serializeNcAndAsmSetting(NC_ASM_EFFECT ncAsmEffect, NC_ASM_SETTING_TYPE ncAsmSettingType, NC_DUAL_SINGLE_VALUE ncValue, ASM_SETTING_TYPE asmSettingType, ASM_ID asmId, char asmLevel)
{
NC_DUAL_SINGLE_VALUE val = NC_DUAL_SINGLE_VALUE::OFF;
if (asmLevel > MAX_STEPS_WH_1000_XM3)
{
throw std::runtime_error("Exceeded max steps");
}
else if (asmLevel == 1)
{
val = NC_DUAL_SINGLE_VALUE::SINGLE;
}
else if (asmLevel == 0)
if (asmLevel < 0 || asmLevel > MAX_AMBIENT_SOUND_LEVEL_V1)
{
val = NC_DUAL_SINGLE_VALUE::DUAL;
throw std::runtime_error("Ambient sound level must be between 0 and 20");
}
return val;
}

Buffer serializeNcAndAsmSetting(NC_ASM_EFFECT ncAsmEffect, NC_ASM_SETTING_TYPE ncAsmSettingType, ASM_SETTING_TYPE asmSettingType, ASM_ID asmId, char asmLevel)
{
Buffer ret;
ret.push_back(static_cast<unsigned char>(COMMAND_TYPE::NCASM_SET_PARAM));
ret.push_back(static_cast<unsigned char>(NC_ASM_INQUIRED_TYPE::NOISE_CANCELLING_AND_AMBIENT_SOUND_MODE));
ret.push_back(static_cast<unsigned char>(ncAsmEffect));
ret.push_back(static_cast<unsigned char>(ncAsmSettingType));
ret.push_back(static_cast<unsigned char>(getDualSingleForAsmLevel(asmLevel)));
ret.push_back(static_cast<unsigned char>(ncValue));
ret.push_back(static_cast<unsigned char>(asmSettingType));
ret.push_back(static_cast<unsigned char>(asmId));
ret.push_back(asmLevel);
Expand Down Expand Up @@ -226,4 +216,3 @@ namespace CommandSerializer
}

}

5 changes: 1 addition & 4 deletions Client/CommandSerializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
#include "Exceptions.h"

constexpr int MINIMUM_VOICE_FOCUS_STEP = 2;
constexpr unsigned int ASM_LEVEL_DISABLED = -1;

namespace CommandSerializer
{
Expand Down Expand Up @@ -37,10 +36,8 @@ namespace CommandSerializer

Message unpackBtMessage(const Buffer& src);

NC_DUAL_SINGLE_VALUE getDualSingleForAsmLevel(char asmLevel);
Buffer serializeNcAndAsmSetting(NC_ASM_EFFECT ncAsmEffect, NC_ASM_SETTING_TYPE ncAsmSettingType, ASM_SETTING_TYPE asmSettingType, ASM_ID asmId, char asmLevel);
Buffer serializeNcAndAsmSetting(NC_ASM_EFFECT ncAsmEffect, NC_ASM_SETTING_TYPE ncAsmSettingType, NC_DUAL_SINGLE_VALUE ncValue, ASM_SETTING_TYPE asmSettingType, ASM_ID asmId, char asmLevel);
//v2 protocol variant (WH-CH720N, XM5, newer XM4, etc) - different NCASM_SET_PARAM byte layout than v1.
Buffer serializeNcAndAsmSettingV2(NC_ASM_EFFECT ncAsmEffect, NC_ASM_SETTING_TYPE_V2 ncAsmSettingType, ASM_ID voicePassthrough, char asmLevel);
Buffer serializeVPTSetting(VPT_INQUIRED_TYPE type, unsigned char preset);
}

47 changes: 46 additions & 1 deletion Client/Constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ inline unsigned char SERVICE_UUID_IN_BYTES[] = { // this is the SERVICE_UUID but
0xb3, 0x2d, 0xe3, 0x16, 0xf5, 0xe0, 0x69, 0xba
};

// Second-generation Sony protocol (WH-CH720N, WF/WH-1000XM5, newer XM4/WF-C700N units, etc).
// Second-generation Sony protocol (WH-CH720N, WF/WH-1000XM5, WF-C700N, etc).
inline constexpr auto SERVICE_UUID_V2 = "956C7B26-D49A-4BA8-B03F-B17D393CB6E2";
inline unsigned char SERVICE_UUID_V2_IN_BYTES[] = { // this is the SERVICE_UUID_V2 but in bytes
0x95, 0x6c, 0x7b, 0x26, 0xd4, 0x9a, 0x4b, 0xa8,
Expand All @@ -27,6 +27,37 @@ enum class SonyProtocolVersion : signed char
V2 = 1
};

// First-generation MDR protocol used by the WH-1000XM4 and earlier models. Several values overlap
// with v2 but mean something entirely different, so call sites must select a namespace only after
// the SDP service UUID has established the protocol version.
namespace V1Command
{
inline constexpr unsigned char INIT_REQUEST = 0x00;
inline constexpr unsigned char INIT_REPLY = 0x01;
inline constexpr unsigned char FW_GET = 0x04;
inline constexpr unsigned char FW_RET = 0x05;
inline constexpr unsigned char BATTERY_GET = 0x10;
inline constexpr unsigned char BATTERY_RET = 0x11;
inline constexpr unsigned char CODEC_GET = 0x18;
inline constexpr unsigned char CODEC_RET = 0x19;
inline constexpr unsigned char POWER_OFF = 0x22;
inline constexpr unsigned char EQ_GET = 0x56;
inline constexpr unsigned char EQ_RET = 0x57;
inline constexpr unsigned char EQ_SET = 0x58;
inline constexpr unsigned char AMBIENT_GET = 0x66;
inline constexpr unsigned char AMBIENT_RET = 0x67;
inline constexpr unsigned char AMBIENT_SET = 0x68;
inline constexpr unsigned char DSEE_GET = 0xe6;
inline constexpr unsigned char DSEE_RET = 0xe7;
inline constexpr unsigned char DSEE_SET = 0xe8;
inline constexpr unsigned char BTNMODE_GET = 0xf6;
inline constexpr unsigned char BTNMODE_RET = 0xf7;
inline constexpr unsigned char BTNMODE_SET = 0xf8;
inline constexpr unsigned char SUB_AUTO_POWER_OFF = 0x04;
inline constexpr unsigned char SUB_SPEAK_TO_CHAT = 0x05;
inline constexpr unsigned char MANUAL_EQ = 0xff;
}

// Second-generation (v2) inquiry/response command opcodes (the first payload byte).
// Byte layouts reverse-engineered by GadgetBridge (SonyProtocolImplV2); verified against WH-CH720N.
namespace V2Command
Expand Down Expand Up @@ -55,6 +86,20 @@ namespace V2Command
inline constexpr unsigned char BTNMODE_SET = 0xf8; // SET f8 <sub> <value...>
inline constexpr unsigned char SUB_ADAPTIVE_VOLUME = 0x0a;
inline constexpr unsigned char SUB_SPEAK_TO_CHAT = 0x0c;
// Touch-sensor family. On the WH-ULT900N (ULT WEAR) subtype 0xd1 carries the multipoint
// ("Connect to 2 devices simultaneously") state; the enable bit is inverted (00=on, 01=off).
// Source: GadgetBridge SonyProtocolImplV2 setWideAreaTap/handleTouchSensor + getConnectTwoDevices
// (commits 8cbc309346, e8520865f3, 5d7b37d1b7 - hardware-verified against the WH-ULT900N).
inline constexpr unsigned char TOUCH_GET = 0xd6; // GET d6 <sub> -> RET d7 <sub> 00 <00=on/01=off>
inline constexpr unsigned char TOUCH_RET = 0xd7;
inline constexpr unsigned char TOUCH_SET = 0xd8; // SET d8 <sub> 00 <00=on/01=off>
inline constexpr unsigned char SUB_MULTIPOINT = 0xd1;
// System-control family (reboot=0x16, multipoint apply=0x06). Multipoint SETs are committed with
// the fixed frame 98 00 06 01 - identical bytes for both ON and OFF; the touch-sensor frame above
// carries the actual state. The matching RET (97 00 06 xx) always reports 01, so it is never used
// as a state source.
inline constexpr unsigned char SYSCTL_SET = 0x98;
inline constexpr unsigned char SUB_SYSCTL_MULTIPOINT = 0x06;
}

// v2 equalizer preset ids (byte value sent/received at EQ payload[2]).
Expand Down
Loading