buttplugio / docs.buttplug.io

11 stars 7 forks source link

Document MuSe/Love Spouse Protocol #2

Open denialtek opened 1 year ago

denialtek commented 1 year ago

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.

AndroSphinx commented 1 year ago

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);
    }
  }
}
jptrsn commented 1 year ago

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:

// Classic Mode
uint8_t manufacturerDataList[][11] = {
    // 0 Stop all channels 
    {MANUFACTURER_DATA_PREFIX, 0xE5, 0x15, 0x7D},
    // 1 Set all channels to speed 1 (Mode 1)
    {MANUFACTURER_DATA_PREFIX, 0xE4, 0x9C, 0x6C},
    // 2 Set all channels to speed 2 (Mode 2)
    {MANUFACTURER_DATA_PREFIX, 0xE7, 0x07, 0x5E},
    // 3 Set all channels to speed 3 (Mode 3)
    {MANUFACTURER_DATA_PREFIX, 0xE6, 0x8E, 0x4F},
    // 4 (Mode 4)
    {MANUFACTURER_DATA_PREFIX, 0xE1, 0x31, 0x3B},
    // 5 (Mode 5)
    {MANUFACTURER_DATA_PREFIX, 0xE0, 0xB8, 0x2A},
    // 6 (Mode 6)
    {MANUFACTURER_DATA_PREFIX, 0xE3, 0x23, 0x18},
    // 7 (Mode 7)
    {MANUFACTURER_DATA_PREFIX, 0xE2, 0xAA, 0x09},
    // 8 (Mode 8)
    {MANUFACTURER_DATA_PREFIX, 0xED, 0x5D, 0xF1},
    // 9 (Mode 9)
    {MANUFACTURER_DATA_PREFIX, 0xEC, 0xD4, 0xE0}
};
// Independent mode, vibe 1
uint8_t manufacturerDataList[][11] = {
    // 0 Stop all channels 
    {MANUFACTURER_DATA_PREFIX, 0xE5, 0x15, 0x7D},
    // 1 (Mode 1)
    {MANUFACTURER_DATA_PREFIX, 0xD4, 0x1F, 0x5D},
    // 2 (Mode 2)
    {MANUFACTURER_DATA_PREFIX, 0xD7, 0x84, 0x6F},
    // 3 (Mode 3)
    {MANUFACTURER_DATA_PREFIX, 0xD6, 0x0D, 0x7E},
    // 4 (Mode 4)
    {MANUFACTURER_DATA_PREFIX, 0xD1, 0xB2, 0x0A},
    // 5 (Mode 5)
    {MANUFACTURER_DATA_PREFIX, 0xD0, 0x3B, 0x1B},
    // 6 (Mode 6)
    {MANUFACTURER_DATA_PREFIX, 0xD3, 0xA0, 0x29},
    // 7 (Mode 7)
    {MANUFACTURER_DATA_PREFIX, 0xD2, 0x29, 0x38},
    // 8 (Mode 8)
    {MANUFACTURER_DATA_PREFIX, 0xDD, 0xDE, 0xC0},
    // 9 (Mode 9)
    {MANUFACTURER_DATA_PREFIX, 0xDC, 0x57, 0xD1}
};
Paxy commented 1 year ago

@jptrsn I just made a project with that idea. https://github.com/Paxy/xtoys_LS_GW/

IngeniousKink commented 1 year ago

create an ESP32 gateway firmware

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.

arz321 commented 1 year ago

Hi, I'm researching the MuSe protocol in app Leten. And "decrypted" it. In the process of research, I managed to download the entire database and there are more than 1000 toys in it. And also i made an application to send packages https://github.com/arz321/MuSe-Protocol

denialtek commented 7 months ago

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) 0xA4982E - Sky Blue 0xA7031C - Deep Blue 0xA68A0D - Dark Green 0xA13579 - Light Purple 0xA0BC68 - Light Blue 0xA3275A - Orange 0xA2AE4B - Red 0xAD59B3 - Rose Red 0xACD0A2 - Light Green

kidjuniper commented 7 months ago

Hi! Did someone try to make an iOS app for such devices?

qdot commented 7 months ago

Hi! Did someone try to make an iOS app for such devices?

Not possible. iOS does not expose ble peripheral capabilities.

arz321 commented 6 months ago

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. Example of use, we substitute the numbers from the barcode into the link https://lovespouse.zlmicro.com/index.php?g=App&m=Diyapp&a=getproductdetail&barcode=8131&userid=-1 We take, from json, BroadcastPrefix "77 62 4d 53 45" and the stop command "30" And we get 0x6D, 0xB6, 0x43, 0xCE, 0x97, 0xFE, 0x42, 0x7C, 0xE5, 0x15, 0x7D p.s. Later I will do it for esp32

//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)));
        }
    }
}
arz321 commented 6 months ago

@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. origin windows image