buttplugio / stpihkal

Repo deprecated, STPIHKAL moved to docs.buttplug.io repo
https://docs.buttplug.io/
92 stars 21 forks source link

Document Satisfyer Protocol #99

Open XDjackieXD opened 4 years ago

XDjackieXD commented 4 years ago

Here the information I found out about the Satisfyer devices (Specifically the Royal One):

If the device was paired to a host before, it has to be reset by holding the button for 15s (it should do 5 fast vibrations once the button has been held long enough) to be able to connect to it from another host. When it isn't paired, connecting also works without pairing first. After that it can be made visible by holding the button for 4s (2 fast vibrations). It doesn't send a name in the BLE advertisement packets (or I couldn't find it). Only after the first connection the host knows the name (seems to be GATT attribute device name).

The Service of interest is "51361500-c5e7-47c7-8a6e-47ebc99d80e8" ("live control" according to the android app code). It exposes two Characteristics:

When the motor value is set to 0 for a few seconds it seems to exit "start" mode and requires the start message to be sent again.

Standard ble services it exposes:

Other exposed vendor specific Services that I haven't figured out the usage yet are

XDjackieXD commented 3 years ago

The Satisfyer Love Triangle works exactly the same way but the "51361502-c5e7-47c7-8a6e-47ebc99d80e8" (motor value) attribute now expects 8 bytes of data where the lower 4 bytes represent the suction motor and the higher 4 bytes the vibration motor. for example 0x0000006400000000 turns on the vibration motor to max (decimal 100) and the suction off. 0x0000000000000040 turns on the suction device to 64% but the vibration motor off.

denialtek commented 3 years ago

I had to write the same value to each of the 4 bytes or else the vibrator/suction would stutter off and on quickly after each adjustment (testing on the Satisfyer Curvy 3). ex. 0x6464646400000000 or 0x0000000040404040

XDjackieXD commented 3 years ago

I thought so too at first but playing around with it it seems to suffice to always send the correct amount of bytes (4 Bytes per motor) and have the LSB be the motor value. at least that works 100% reliably for both satisfyer toys I could try it with (tested with chromium ble tools and the nrf ble app)

XDjackieXD commented 3 years ago

@denialtek I must have done something wrong during my initial testing but I can reproduce your stutter problem now (with an initial implementation in buttplug-rs by @blackspherefollower). I suspect that the four bytes are some form of timed sequence to reduce the overhead by sending 4 values of a vibration pattern per packet but I haven't had time to check this further (I could not get it to a point where I could feel the 4 values consecutively so I'd probably have to check with an accelerometer and a scope or something like that ^^)

liamdm commented 2 years ago

Last byte controls the target speed of the motor, the first three bytes are used to control what happens to the motor while it's transitioning to the next speed from what I can tell (so the first few milliseconds of the new command). If you set the first three bytes to the same value as the target speed, the transition will be smooth (no stuttering) between commands. But if not, say there is a 3ms transition between motor speeds, the first byte seems to be the speed target for the first 1ms of the transition, the second for the 2ms and third for 3ms. It's not enough time for the motor to respond reliably but it adds a bit of jerk and additional random jitter (probably used for more fine control and responsiveness with the touch input from the app).

~You can test this if you set the bytes to (100, 100, 100, 50) and then send one command every second, you will notice there is a bit of additional motor speed increase every second, whereas if you set it to (50, 50, 50, 50) this jerk is gone. But if you send (100, 100, 100, 100) it's smooth again, but (50, 50, 50, 100) you can feel a dip every second. I think this is probably used to achieve a jolt in the motor which I think makes the vibration different than if the motor had a linear speed transition.~

~You also see if you send (100, 100, 100, 50) -jump-> (100, 100, 100, 50) -no jump-> (100, 100, 100 ,100) -no jump-> (100, 100, 100 ,100)...~

Update, even easier was to test, you can see this if you send bytes (100, 100, 100, 0), you can feel the first three bytes have control only over the first couple milliseconds of the command, resulting in a very short pulse, but then the motor is stopped for the rest. And sending (100, 0, 0, 0) gives an incredibly short pulse (that feels even weaker).

As for the 600 range UUID's, there are some more in the newer version:

private static final UUID alarm =               UUID.fromString("51361603-c5e7-47c7-8a6e-47ebc99d80e8");
private static final UUID heating =             UUID.fromString("51361604-c5e7-47c7-8a6e-47ebc99d80e8");
private static final UUID keepAliveControl =    UUID.fromString("51361605-c5e7-47c7-8a6e-47ebc99d80e8");
private static final UUID pumpControl =         UUID.fromString("51361606-c5e7-47c7-8a6e-47ebc99d80e8");
private static final UUID lockControl =         UUID.fromString("51361607-c5e7-47c7-8a6e-47ebc99d80e8");

Interestingly, heating should be set to 0x28 or 40 to be turned on. In Java/Android I don't need to give it the full 4 bytes, I just write new byte[]{ 0x28 } to the characteristic, but on other platforms maybe you need to specify the full byte structure.

blackspherefollower commented 2 years ago

@liamdm Which device were you testing with?

I've just received a "Heat Climax" which appears to send a heat command of 0x01 0x28 0xff (which then gets updates from the device with the 3rd byte seemly reflecting the actual temperature).

The 4-byte motor control is common across most of the Satisfyer range where either 1 and 2 motors are used: I suspect that you're using a single motor device and the first 2 bytes are unused by your device.

That said, it looks like some of the newer Satisfyers (FW 2.x.x?) have 4-byte per-motor control scheme: the "Double Flex" has 3 motors and takes 12-bytes, and it does look like the values sent are a ramp from the last speed to the new speed.

liamdm commented 2 years ago

@blackspherefollower So the heat command to control the heat via the heating characteristic I observed was only a single byte value (possibly to set the state). I get the feeling that 0x28 is a temporary state that is "warming up" because the actual constant I saw in the APK code was different, but I have verified this working on my heat climax as being able to turn the heating on and off by sending that. -- I'm not super familiar with BLE characteristics, can there be a different byte length for reading and writing, because I can definitely see the temperature stuff in the APK code, but I was only trying to turn the heating on and off and that's where I found the 0x28 worked (but I was sending that as a single byte), but then if I read from that would it return a 3 byte sequence?

As for the motor control, agreed, it might be due to it being a single motor device, as I was also testing this on a heat climax (have a three motor device now though and want to check this too) - but the first and second byte definitely had an impact (on the transition speed). Maybe this is some quirk of how the single motor devices handles these values - or it's just the newer Satisfyer devices (maybe v3+ have this system). If you'd like I can send through some java code that you can build into an Android app to test this?

blackspherefollower commented 2 years ago

@liamdm I suspect the original Satisfyer devices had the same intermediate speed value, but just one byte: [ motor2-intermediate, motor2-final, motor1-intermediate, motor1-final ] and the newer have 4-bytes per motor, with a variable length data field.

I generally test with the nRF Connect app (really easy to send/receive packets on mobile. And capture data by analysing the btsnoop_hci.log, but I will take a look at the APK to see if there's a clear firmware based protocol switch.

blackspherefollower commented 2 years ago

Looks like the heatbeat was added in fw2.0.24 and keepalive was added in fw2.0.34 (not sure what either do).

It looks like the heater result is [ boolean heating, int targetTemp, int currentTemp ]. I must have misread my wireshark trace: I only see 01 or 00 sent to that characteristic.

And it looks like the vibration commands haven't changed, my memory was just playing tricks on me. Looks like ALL Satisfyer devices can be controlled by 4-byte per vibe (4 for 1 vibe, 8 for 2, 12 for 3).

Furthermore, we can identify the devices prior to connection by checking the manufacturer advertisement data (I've previously pushed back on this approach since I couldn't see a way to map all the Satisfyer devices without buying them all, but as it turns out, products.json is right there for the reading)

blackspherefollower commented 2 years ago

Digging further, I think the logic looks like:

if( firmwareVersion < 2.0.24) {
  // ping by setting last motor speed
} else if(firmwareVersion < 2.0.34) {
  // ping by setting controlMode = 87
} else {
  // no ping - keep-alive set on device?
}

if( oneOf( Sexy Secret, Little Secret, Signet Ring, Strong One, Mighty One, Powerful One, Royal One, Plug-ilicious 1, Plug-ilicious 2 ) )  {
// wearable
timeout = 28800s
} else {
timeout = backgroundMode ? 660s : 28800s
}
"51361605-c5e7-47c7-8a6e-47ebc99d80e8".write(timeout)
blackspherefollower commented 2 years ago

This might only apply to live control, but motor updates are sent by the app once very .5 seconds. Without frequent updates, the device locks you out regardless of any of the other keep-alive/heatbeat things.

The frequency seems to differ slightly between devices, but for the Plug-ilicous devices 3 seconds is not frequent enough, whilst 1 second seems to be fine.

blackspherefollower commented 1 year ago

SO! I got a Air Pump Booty 5+ (it has a pump!) Pump rx/tx: 51361606-c5e7-47c7-8a6e-47ebc99d80e8 Commands: 0x0000XXXX