-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Document MuSe/Love Spouse Protocol #2
Comments
Taking the above documentation, here's some ESP32 code that implements the above and is able to control these toys: #include <Arduino.h>
#include <NimBLEDevice.h>
static uint16_t companyId = 0xFFF0;
#define MANUFACTURER_DATA_PREFIX 0x6D, 0xB6, 0x43, 0xCE, 0x97, 0xFE, 0x42, 0x7C
uint8_t manufacturerDataList[][11] = {
// Stop all channels
{MANUFACTURER_DATA_PREFIX, 0xE5, 0x15, 0x7D},
// Set all channels to speed 1
{MANUFACTURER_DATA_PREFIX, 0xE4, 0x9C, 0x6C},
// Set all channels to speed 2
{MANUFACTURER_DATA_PREFIX, 0xE7, 0x07, 0x5E},
// Set all channels to speed 3
{MANUFACTURER_DATA_PREFIX, 0xE6, 0x8E, 0x4F},
// Stop 1st channel (only for toys with 2 channels)
{MANUFACTURER_DATA_PREFIX, 0xD5, 0x96, 0x4C},
// Set 1st channel to speed 1 (only for toys with 2 channels)
{MANUFACTURER_DATA_PREFIX, 0xD4, 0x1F, 0x5D},
// Set 1st channel to speed 2 (only for toys with 2 channels)
{MANUFACTURER_DATA_PREFIX, 0xD7, 0x84, 0x6F},
// Set 1st channel to speed 3 (only for toys with 2 channels)
{MANUFACTURER_DATA_PREFIX, 0xD6, 0x0D, 0x7E},
// Stop 2nd channel (only for toys with 2 channels)
{MANUFACTURER_DATA_PREFIX, 0xA5, 0x11, 0x3F},
// Set 2nd channel to speed 1 (only for toys with 2 channels)
{MANUFACTURER_DATA_PREFIX, 0xA4, 0x98, 0x2E},
// Set 2nd channel to speed 2 (only for toys with 2 channels)
{MANUFACTURER_DATA_PREFIX, 0xA7, 0x03, 0x1C},
// Set 2nd channel to speed 3 (only for toys with 2 channels)
{MANUFACTURER_DATA_PREFIX, 0xA6, 0x8A, 0x0D},
};
const char *deviceName = "MuSE_Advertiser";
void setup() {
Serial.begin(115200);
Serial.println("Starting BLE...");
NimBLEDevice::init(deviceName);
}
void advertiseManufacturerData(uint8_t index) {
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->stop();
uint8_t *manufacturerData = manufacturerDataList[index];
Serial.print("Advertising index: ");
Serial.print(index);
Serial.print(", data: ");
for (int i = 0; i < 11; i++) {
Serial.print(manufacturerDataList[index][i], HEX);
if (i < 10) {
Serial.print(", ");
}
}
Serial.println();
Serial.flush(); // Flush to ensure data is sent before delay
pAdvertising->setManufacturerData(std::string((char *)&companyId, 2) + std::string((char *)manufacturerData, 11));
// Set properties: scannable, connectable, and use legacy advertising
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x12);
pAdvertising->setMinPreferred(0x02);
// Start advertising
pAdvertising->start();
}
void loop() {
for (uint8_t i = 0; i < sizeof(manufacturerDataList) / sizeof(manufacturerDataList[0]); i++) {
// set advertisement for 2 seconds
for (uint8_t j = 0; j < 10; j++) {
advertiseManufacturerData(i);
delay(200);
}
// set stop devices for 1 second
for (uint8_t k = 0; k < 5; k++) {
advertiseManufacturerData(0);
delay(200);
}
}
} |
I have done some more sniffing, thanks to this info, and I was able to capture the values for both the Classic Mode as well as Independent Mode (Mode 1) (which I believe is in reference to motor 1). Modes 1, 2 and 3 of classic mode correspond with the previously-mentioned values. I do not have any dual-motor devices to test, but can capture the second motor advertisement values from the app if they prove useful to the development of xtoys.app. I think it may be viable to create an ESP32 gateway firmware that could replicate a buttplug.io compatible protocol over wifi or serial, and extend functionality. Here are the values I've captured:
|
@jptrsn I just made a project with that idea. https://github.com/Paxy/xtoys_LS_GW/ |
I did just that: https://github.com/IngeniousKink/LVS-Gateway: ESP32 firmware that poses as a Lovense toy and broadcasts the manufacturer data to MuSE/Love Spouse devices. |
Hi, I'm researching the MuSe protocol in app Leten. And "decrypted" it. |
For toy 8131 (a buttplug with an LED), the LED is controlled via the channel 2 commands, and each of the commands maps to a color: 0xA5113F - Off (it doesn't turn fully off, instead it's a dim blinking blue light) |
Hi! Did someone try to make an iOS app for such devices? |
Not possible. iOS does not expose ble peripheral capabilities. |
I was able to decode the MuSe protocol using Ghidra and AI. And later I found out that this is the Fastcon BLE implementation from BroadLink. //Java
public class Main {
public static void main(String[] args) {
byte[] broadcastPrefix = {0x77, 0x62, 0x4d, 0x53, 0x45};
byte[] command = {0x30};
int length = broadcastPrefix.length;
int length2 = command.length;
int length3 = broadcastPrefix.length + command.length + 0x05;
byte[] data = new byte[length3];
get_rf_payload(broadcastPrefix, length, command, length2, data);
for (int i = 0; i < length3; i++) {
System.out.print("0x");
System.out.print(Integer.toHexString((int)data[i] & 0xff).toUpperCase());
System.out.print(", ");
}
}
public static void get_rf_payload(byte[] bArr, int length, byte[] bArr2, int length2, byte[] bArr3) {
byte[] ctx_25 = new byte[7];
byte[] ctx_3F = new byte[7];
whitening_init(0x25, ctx_25); //1100101
whitening_init(0x3f, ctx_3F); //1111111
int length_24 = 0x12 + length + length2;
int length_26 = length_24 + 0x02;
byte[] result_25 = new byte[length_26];
byte[] result_3f = new byte[length_26];
byte[] resultbuf = new byte[length_26];
resultbuf[0x0f] = 0x71;//const buf[0x0f-0x11]
resultbuf[0x10] = 0x0f;
resultbuf[0x11] = 0x55;
if (length > 0) {
for (byte j = 0; j < length; j++) { //flip bArr[] and write to buf[0x12-0x16]
resultbuf[0x12 + length - j - 0x01] = bArr[j];
}
}
if (length2 > 0) {
for (byte j = 0; j < length2; j++) { //flip bArr2[] and write to buf[0x17]
resultbuf[length_24 - j - 0x01] = bArr2[j];
}
}
for (byte i = 0; i < 0x03 + length; i++) { //invert_8 byte buf[0x0f-0x16]
resultbuf[0x0f + i] = invert_8(resultbuf[0x0f + i]);
}
int crc16 = check_crc16(bArr, length, bArr2, length2); //write crc16 to buf[0x18-0x19]
resultbuf[length_24] = (byte)crc16;
resultbuf[length_24 + 1] = (byte)(crc16 >> 8);
whitenging_encode(resultbuf, 0x2 + length + length2, ctx_3F, 0x12, result_3f);
whitenging_encode(resultbuf, length_26, ctx_25, 0x00, result_25);
for (byte i = 0; i < length_26; i++) { //XOR result_25[] and result_3f[]
result_25[i] ^= result_3f[i];
}
System.arraycopy(result_25, 0x0f, bArr3, 0, 0x0b); //copy result_25[0x0f-0x19] to bArr3
}
public static void whitening_init(int val, byte[] ctx) {
ctx[0] = 1;
ctx[1] = (byte) ((val >> 5) & 1);
ctx[2] = (byte) ((val >> 4) & 1);
ctx[3] = (byte) ((val >> 3) & 1);
ctx[4] = (byte) ((val >> 2) & 1);
ctx[5] = (byte) ((val >> 1) & 1);
ctx[6] = (byte) (val & 1);
}
public static int check_crc16(byte[] addr, int addrLength, byte[] data, int dataLength) {
int crc = 0xffff;
for (int i = addrLength - 1; i >= 0; i--) {
crc ^= addr[i] << 8;
for (int ii = 0; ii < 8; ii++) {
if ((crc & 0x8000) != 0) {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
for (int i = 0; i < dataLength; i++) {
crc ^= invert_8(data[i]) << 8;
for (int ii = 0; ii < 8; ii++) {
if ((crc & 0x8000) != 0) {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
crc = ~invert_16(crc) & 0xffff;
return crc;
}
public static byte invert_8(byte value) {
byte result = 0;
for (byte i = 0; i < 8; i++) {
result <<= 1;
result |= (value & 1);
value >>= 1;
}
return result;
}
public static int invert_16(int value) {
int result = 0;
for (int i = 0; i < 16; i++) {
result <<= 1;
result |= (value & 1);
value >>= 1;
}
return result;
}
public static void whitenging_encode(byte[] data, int len, byte[] ctx, int offset, byte[] result) {
System.arraycopy(data, 0, result, 0, len);
for (int i = 0; i < len; i++) {
int var6 = ctx[6];
int var5 = ctx[5];
int var4 = ctx[4];
int var3 = ctx[3];
int var52 = var5 ^ ctx[2];
int var41 = var4 ^ ctx[1];
int var63 = var6 ^ ctx[3];
int var630 = var63 ^ ctx[0];
ctx[0] = (byte)(var52 ^ var6);
ctx[1] = (byte)var630;
ctx[2] = (byte)var41;
ctx[3] = (byte)var52;
ctx[4] = (byte)(var52 ^ var3);
ctx[5] = (byte)(var630 ^ var4);
ctx[6] = (byte)(var41 ^ var5);
int c = result[i + offset];
result[i + offset] = (byte)(((c & 0x80) ^ ((var52 ^ var6) << 7)) +
((c & 0x40) ^ (var630 << 6)) +
((c & 0x20) ^ (var41 << 5)) +
((c & 0x10) ^ (var52 << 4)) +
((c & 0x08) ^ (var63 << 3)) +
((c & 0x04) ^ (var4 << 2)) +
((c & 0x02) ^ (var5 << 1)) +
((c & 0x01) ^ (var6)));
}
}
} |
@denialtek I was able to control the toy from Windows with a little cheating. In a bluetooth packet a flag is transmitted that is 3 bytes long. in Windows it is not possible to transmit flags, but it seems that it is enough to compensate for the length of the packet by 3 bytes. |
The toy is controlled by creating a BLE advertiser and setting specific manufacturer data.
Properties: Scannable, Connectable, Legacy
Company ID: 0xFFF0 (though other IDs seem to also work)
Manufacturer Data: 11 bytes in the format of 0x6DB643CE97FE427Cxxxxxx
Valid values for the last 3 bytes:
0xE5157D - stop all channels
0xE49C6C - set all channels to speed 1
0xE7075E - set all channels to speed 2
0xE68E4F - set all channels to speed 3
Only for use by toys with 2 channels:
0xD5964C- stop 1st channel
0xD41F5D - set 1st channel to speed 1
0xD7846F - set 1st channel to speed 2
0xD60D7E - set 1st channel to speed 3
0xA5113F - stop 2nd channel
0xA4982E - set 2nd channel to speed 1
0xA7031C - set 2nd channel to speed 2
0xA68A0D - set 2nd channel to speed 3
Some toys with both stroke and vibrate functionality have the vibrate on channel 1, and some have it on channel 2. There doesn't appear to be a way of knowing which functionality is on which channel.
The commands listed above correlate with the first 3 "modes" in the app, which are low/medium/high and are the only non-pattern modes.
Some commands trigger the toy to change patterns when the advertisement stops (ex. 0xE0B82A triggers a fast pulse pattern, but then a slower pulse pattern once you stop sending that data). To prevent the toy from thinking the advertisement data has stopped the advertisement interval needs to be at least 250ms.
The text was updated successfully, but these errors were encountered: