From a87b489b0583f73b178cdac8687b51fadf5f0b0d Mon Sep 17 00:00:00 2001 From: SanJerry007 Date: Mon, 18 May 2026 06:42:37 -0400 Subject: [PATCH] feat(devices): add Yiciyuan YCY-FJB-01 / FJB-02 stroker support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Yiciyuan FJB-01 / FJB-02 (役次元) are JieLi-SoC BLE strokers advertising as "YCY-FJB-01" and "YCY-FJB-02" with three independent actuators: - stroke axis (linear oscillation) - vibe axis (vibration motor) - axis_c (third haptic motor driven in app-recorded patterns) Each axis takes an unsigned 0..=0x14 level; the 16-byte control packet is sent to characteristic ff41 under service ff40 as: [0x35, 0x12, stroke, vibe, axis_c, 0×11] Battery push arrives on the same notify characteristic as a `35 13 01 ` frame mixed with 10Hz uptime ticks (`35 14 ..`); the handler filters by prefix. FJB-01 is verified against physical hardware. FJB-02 is its successor in the same product line; the official app routes both through an identical code path (same vuex state, same hex-stringed motor frame, same BLE service/characteristic UUIDs), so the same protocol module covers it. Hardware verification welcome. Adds: * device-config-v4/protocols/yiciyuan.yml * server/.../protocol_impl/yiciyuan.rs * tests/.../test_yiciyuan_protocol.yaml (FJB-01) * tests/.../test_yiciyuan_protocol_fjb02.yaml (FJB-02) Test commands gated on protocol v3+ since v0-v2 single-axis Vibrate semantics don't map cleanly to a multi-actuator device. All 828 device protocol tests pass on debug build. --- .../src/device/protocol_impl/mod.rs | 5 + .../src/device/protocol_impl/yiciyuan.rs | 254 ++++++++++++++++++ .../buttplug-device-config-v5.json | 99 ++++++- .../device-config/protocols/yiciyuan.yml | 63 +++++ .../device-config/version.yaml | 2 +- .../tests/test_device_protocols.rs | 20 ++ .../test_yiciyuan_protocol.yaml | 68 +++++ .../test_yiciyuan_protocol_fjb02.yaml | 56 ++++ 8 files changed, 565 insertions(+), 2 deletions(-) create mode 100644 crates/buttplug_server/src/device/protocol_impl/yiciyuan.rs create mode 100644 crates/buttplug_server_device_config/device-config/protocols/yiciyuan.yml create mode 100644 crates/buttplug_tests/tests/util/device_test/device_test_case/test_yiciyuan_protocol.yaml create mode 100644 crates/buttplug_tests/tests/util/device_test/device_test_case/test_yiciyuan_protocol_fjb02.yaml diff --git a/crates/buttplug_server/src/device/protocol_impl/mod.rs b/crates/buttplug_server/src/device/protocol_impl/mod.rs index 6da44058f..bb2dc381f 100644 --- a/crates/buttplug_server/src/device/protocol_impl/mod.rs +++ b/crates/buttplug_server/src/device/protocol_impl/mod.rs @@ -125,6 +125,7 @@ pub mod xibao; pub mod xinput; pub mod xiuxiuda; pub mod xuanhuan; +pub mod yiciyuan; pub mod youcups; pub mod youou; pub mod zalo; @@ -585,6 +586,10 @@ pub fn get_default_protocol_map() -> HashMap, + _def: &ServerDeviceDefinition, + ) -> Result, ButtplugDeviceError> { + Ok(Arc::new(Yiciyuan::default())) + } +} + +/// Per-device state. The protocol sends all three axes in every packet, so +/// we keep the last commanded value for each axis here and rebuild the +/// packet on any axis change. +#[derive(Default)] +pub struct Yiciyuan { + stroke: AtomicU8, + vibe: AtomicU8, + axis_c: AtomicU8, +} + +impl Yiciyuan { + fn store(&self, feature_index: u32, value: u32) -> Result<(), ButtplugDeviceError> { + // Map 0..=100 -> 0..=20 (DEVICE_MAX). Round half-up. + let level = ((value.min(100) as u16 * DEVICE_MAX as u16 + 50) / 100) as u8; + match feature_index { + FEATURE_STROKE => self.stroke.store(level, Ordering::Relaxed), + FEATURE_VIBE => self.vibe.store(level, Ordering::Relaxed), + FEATURE_AXIS_C => self.axis_c.store(level, Ordering::Relaxed), + _ => { + return Err(ButtplugDeviceError::ProtocolSpecificError( + "Yiciyuan".to_owned(), + format!("Unknown feature index {}", feature_index), + )); + } + } + Ok(()) + } + + fn build_packet(&self) -> Vec { + // 16-byte motor-state frame: + // [0]=0x35 vendor magic, [1]=0x12 "set motor levels" sub-command, + // [2]=stroke, [3]=vibe, [4]=axis_c, [5..16]=reserved (zero). + let mut packet = vec![0u8; 16]; + packet[0] = 0x35; + packet[1] = 0x12; + packet[2] = self.stroke.load(Ordering::Relaxed); + packet[3] = self.vibe.load(Ordering::Relaxed); + packet[4] = self.axis_c.load(Ordering::Relaxed); + packet + } + + fn handle_axis_cmd( + &self, + feature_index: u32, + value: u32, + ) -> Result, ButtplugDeviceError> { + self.store(feature_index, value)?; + Ok(vec![ + HardwareWriteCmd::new( + &[YICIYUAN_PROTOCOL_UUID], + Endpoint::Tx, + self.build_packet(), + false, + ) + .into(), + ]) + } +} + +impl ProtocolHandler for Yiciyuan { + fn handle_output_oscillate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.handle_axis_cmd(feature_index, speed) + } + + fn handle_output_vibrate_cmd( + &self, + feature_index: u32, + _feature_id: Uuid, + speed: u32, + ) -> Result, ButtplugDeviceError> { + self.handle_axis_cmd(feature_index, speed) + } + + fn handle_input_subscribe_cmd( + &self, + _device_index: u32, + device: Arc, + _feature_index: u32, + feature_id: Uuid, + sensor_type: InputType, + ) -> BoxFuture<'_, Result<(), ButtplugDeviceError>> { + match sensor_type { + InputType::Battery => { + async move { + device + .subscribe(&HardwareSubscribeCmd::new( + feature_id, + Endpoint::RxBLEBattery, + )) + .await?; + Ok(()) + } + } + .boxed(), + _ => future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "Command not implemented for this sensor".to_string(), + ))) + .boxed(), + } + } + + fn handle_input_unsubscribe_cmd( + &self, + device: Arc, + _feature_index: u32, + feature_id: Uuid, + sensor_type: InputType, + ) -> BoxFuture<'_, Result<(), ButtplugDeviceError>> { + match sensor_type { + InputType::Battery => { + async move { + device + .unsubscribe(&HardwareUnsubscribeCmd::new( + feature_id, + Endpoint::RxBLEBattery, + )) + .await?; + Ok(()) + } + } + .boxed(), + _ => future::ready(Err(ButtplugDeviceError::UnhandledCommand( + "Command not implemented for this sensor".to_string(), + ))) + .boxed(), + } + } + + fn handle_battery_level_cmd( + &self, + device_index: u32, + device: Arc, + feature_index: u32, + feature_id: Uuid, + ) -> BoxFuture<'_, Result> { + // The cup pushes battery autonomously at ~1Hz as `35 13 01 P C` on the + // notify characteristic. Subscribe and wait for the first frame whose + // prefix matches `0x35 0x13`. Other notify frames (uptime ticks + // `0x35 0x14 ..`, device-info responses `0x35 0x10 ..`) are skipped. + let mut event_stream = device.event_stream(); + async move { + device + .subscribe(&HardwareSubscribeCmd::new( + feature_id, + Endpoint::RxBLEBattery, + )) + .await?; + while let Ok(event) = event_stream.recv().await { + match event { + HardwareEvent::Notification(_, endpoint, data) => { + if endpoint != Endpoint::RxBLEBattery { + continue; + } + // Battery frame layout: [0]=0x35, [1]=0x13, [2]=0x01, [3]=pct. + if data.len() >= 4 && data[0] == 0x35 && data[1] == 0x13 { + return Ok(InputReadingV4::new( + device_index, + feature_index, + InputTypeReading::Battery(InputValue::new(data[3])), + )); + } + // Not a battery frame — keep waiting for the next notify. + continue; + } + HardwareEvent::Disconnected(_) => { + return Err(ButtplugDeviceError::ProtocolSpecificError( + "Yiciyuan".to_owned(), + "Yiciyuan device disconnected while waiting for battery push.".to_owned(), + )); + } + } + } + Err(ButtplugDeviceError::ProtocolSpecificError( + "Yiciyuan".to_owned(), + "Yiciyuan device event stream closed before battery push arrived.".to_owned(), + )) + } + .boxed() + } +} diff --git a/crates/buttplug_server_device_config/build-config/buttplug-device-config-v5.json b/crates/buttplug_server_device_config/build-config/buttplug-device-config-v5.json index eb78890b3..80b5c92d4 100644 --- a/crates/buttplug_server_device_config/build-config/buttplug-device-config-v5.json +++ b/crates/buttplug_server_device_config/build-config/buttplug-device-config-v5.json @@ -1,7 +1,7 @@ { "version": { "major": 5, - "minor": 5 + "minor": 6 }, "protocols": { "activejoy": { @@ -24282,6 +24282,103 @@ "name": "Xuanhuan Masturbator" } }, + "yiciyuan": { + "communication": [ + { + "btle": { + "names": [ + "YCY-FJB-01", + "YCY-FJB-02" + ], + "services": { + "0000ff40-0000-1000-8000-00805f9b34fb": { + "rxblebattery": "0000ff42-0000-1000-8000-00805f9b34fb", + "tx": "0000ff41-0000-1000-8000-00805f9b34fb" + } + } + } + } + ], + "configurations": [ + { + "id": "e45517ef-4358-4e65-8d78-3ff9447ea1c9", + "identifier": [ + "YCY-FJB-01" + ], + "name": "Yiciyuan FJB-01" + }, + { + "id": "48108f07-5871-445b-9f2a-10ceb1809b23", + "identifier": [ + "YCY-FJB-02" + ], + "name": "Yiciyuan FJB-02" + } + ], + "defaults": { + "features": [ + { + "description": "stroke", + "id": "74f218e9-204e-4600-baf9-43c942b5a6a0", + "index": 0, + "output": { + "oscillate": { + "value": [ + 0, + 100 + ] + } + } + }, + { + "description": "vibrate", + "id": "4bf007b8-e7df-4c4c-8fbe-ea112128a70f", + "index": 1, + "output": { + "vibrate": { + "value": [ + 0, + 100 + ] + } + } + }, + { + "description": "axis c", + "id": "f01acf53-731b-452e-be21-e912a65409c8", + "index": 2, + "output": { + "vibrate": { + "value": [ + 0, + 100 + ] + } + } + }, + { + "description": "battery level", + "id": "5fb5b0a4-aa3f-4e22-9aa3-32a3666d5141", + "index": 3, + "input": { + "battery": { + "command": [ + "Read" + ], + "value": [ + [ + 0, + 100 + ] + ] + } + } + } + ], + "id": "d5987116-2fba-4c30-a7aa-ef567a3bf35d", + "name": "Yiciyuan Device" + } + }, "youcups": { "communication": [ { diff --git a/crates/buttplug_server_device_config/device-config/protocols/yiciyuan.yml b/crates/buttplug_server_device_config/device-config/protocols/yiciyuan.yml new file mode 100644 index 000000000..cf1b037df --- /dev/null +++ b/crates/buttplug_server_device_config/device-config/protocols/yiciyuan.yml @@ -0,0 +1,63 @@ +--- +defaults: + name: Yiciyuan Device + features: + - description: stroke + id: 74f218e9-204e-4600-baf9-43c942b5a6a0 + output: + oscillate: + value: + - 0 + - 100 + index: 0 + - description: vibrate + id: 4bf007b8-e7df-4c4c-8fbe-ea112128a70f + output: + vibrate: + value: + - 0 + - 100 + index: 1 + - description: axis c + id: f01acf53-731b-452e-be21-e912a65409c8 + output: + vibrate: + value: + - 0 + - 100 + index: 2 + - description: battery level + id: 5fb5b0a4-aa3f-4e22-9aa3-32a3666d5141 + input: + battery: + value: + - - 0 + - 100 + command: + - Read + index: 3 + id: d5987116-2fba-4c30-a7aa-ef567a3bf35d +configurations: +# YCY-FJB-01 is the only model verified against physical hardware so far. +# YCY-FJB-02 is its successor in the same product line; the official app's +# code path for both models is identical (same vuex state, same hex-stringed +# 16-byte motor frame, same BLE service/characteristic UUIDs). Adding it +# here so the second model is recognised; flag confirmed device behaviour +# in a future PR once an FJB-02 owner can verify. +- identifier: + - YCY-FJB-01 + name: Yiciyuan FJB-01 + id: e45517ef-4358-4e65-8d78-3ff9447ea1c9 +- identifier: + - YCY-FJB-02 + name: Yiciyuan FJB-02 + id: 48108f07-5871-445b-9f2a-10ceb1809b23 +communication: +- btle: + names: + - YCY-FJB-01 + - YCY-FJB-02 + services: + 0000ff40-0000-1000-8000-00805f9b34fb: + tx: 0000ff41-0000-1000-8000-00805f9b34fb + rxblebattery: 0000ff42-0000-1000-8000-00805f9b34fb diff --git a/crates/buttplug_server_device_config/device-config/version.yaml b/crates/buttplug_server_device_config/device-config/version.yaml index a43561f80..207155de9 100644 --- a/crates/buttplug_server_device_config/device-config/version.yaml +++ b/crates/buttplug_server_device_config/device-config/version.yaml @@ -1,3 +1,3 @@ version: major: 5 - minor: 5 + minor: 6 diff --git a/crates/buttplug_tests/tests/test_device_protocols.rs b/crates/buttplug_tests/tests/test_device_protocols.rs index c0fdf30ab..b5f27ba3e 100644 --- a/crates/buttplug_tests/tests/test_device_protocols.rs +++ b/crates/buttplug_tests/tests/test_device_protocols.rs @@ -145,6 +145,8 @@ async fn load_test_case(test_file: &str) -> DeviceTestCase { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_embedded_v4(test_file: &str) { //tracing_subscriber::fmt::init(); @@ -272,6 +274,8 @@ async fn test_device_protocols_embedded_v4(test_file: &str) { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_json_v4(test_file: &str) { //tracing_subscriber::fmt::init(); @@ -398,6 +402,8 @@ async fn test_device_protocols_json_v4(test_file: &str) { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_embedded_v3(test_file: &str) { //tracing_subscriber::fmt::init(); @@ -525,6 +531,8 @@ async fn test_device_protocols_embedded_v3(test_file: &str) { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_json_v3(test_file: &str) { //tracing_subscriber::fmt::init(); @@ -645,6 +653,8 @@ async fn test_device_protocols_json_v3(test_file: &str) { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_embedded_v2(test_file: &str) { //tracing_subscriber::fmt::init(); @@ -766,6 +776,8 @@ async fn test_device_protocols_embedded_v2(test_file: &str) { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_json_v2(test_file: &str) { util::device_test::client::client_v2::run_json_test_case(&load_test_case(test_file).await).await; @@ -885,6 +897,8 @@ async fn test_device_protocols_json_v2(test_file: &str) { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_embedded_v1(test_file: &str) { //tracing_subscriber::fmt::init(); @@ -1005,6 +1019,8 @@ async fn test_device_protocols_embedded_v1(test_file: &str) { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_json_v1(test_file: &str) { util::device_test::client::client_v1::run_json_test_case(&load_test_case(test_file).await).await; @@ -1078,6 +1094,8 @@ async fn test_device_protocols_json_v1(test_file: &str) { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_embedded_v0(test_file: &str) { //tracing_subscriber::fmt::init(); @@ -1144,6 +1162,8 @@ async fn test_device_protocols_embedded_v0(test_file: &str) { #[test_case("test_xibao_protocol.yaml" ; "Xibao Protocol")] #[test_case("test_xiuxiuda_protocol.yaml" ; "Xiuxiuda Protocol")] #[test_case("test_xuanhuan_protocol.yaml" ; "Xuanhuan Protocol")] +#[test_case("test_yiciyuan_protocol.yaml" ; "Yiciyuan Protocol")] +#[test_case("test_yiciyuan_protocol_fjb02.yaml" ; "Yiciyuan Protocol - FJB-02")] #[tokio::test] async fn test_device_protocols_json_v0(test_file: &str) { util::device_test::client::client_v0::run_json_test_case(&load_test_case(test_file).await).await; diff --git a/crates/buttplug_tests/tests/util/device_test/device_test_case/test_yiciyuan_protocol.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_yiciyuan_protocol.yaml new file mode 100644 index 000000000..b6c01fdad --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_yiciyuan_protocol.yaml @@ -0,0 +1,68 @@ +devices: + - identifier: + name: "YCY-FJB-01" + expected_name: "Yiciyuan FJB-01" +device_commands: + # The cup exposes three actuators (Oscillate + 2× Vibrate). Protocol + # spec versions v0–v2 only model a single Vibrate axis per device, so + # we exercise the meaningful command set under v3+ where ScalarCmd / + # StopDeviceCmd handle multi-feature devices natively. Earlier-version + # tests still verify discovery, identification, and connect. + - !VersionGated + min_spec_version: 3 + commands: + # Initial Stop after connect — clear any axis left running from a + # previous session. The protocol rebuilds the 16-byte frame from + # shared atomic state on every zero call, so the dispatcher emits + # a single all-zero motor frame regardless of feature count. + - !Messages + device_index: 0 + messages: + - !Stop + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0x35, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + write_with_response: false + + # Multi-feature Scalar: stroke 0.5 / vibe 0.75 / axis_c 0.25. + # Each axis lands on its own protocol-handler call which atomically + # updates one byte in the shared state and writes the full frame + # back. The visible write reflects the final combined state + # stroke=10 vibe=15 axis_c=5 in the 0..=0x14 device range. + - !Messages + device_index: 0 + messages: + - !Scalar + - Index: 0 + Scalar: 0.5 + ActuatorType: Oscillate + - Index: 1 + Scalar: 0.75 + ActuatorType: Vibrate + - Index: 2 + Scalar: 0.25 + ActuatorType: Vibrate + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0x35, 0x12, 0x0A, 0x0F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + write_with_response: false + + # Final Stop — every actuator returns to 0. Returns the device to + # the same idle state the test started from. + - !Messages + device_index: 0 + messages: + - !Stop + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0x35, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + write_with_response: false diff --git a/crates/buttplug_tests/tests/util/device_test/device_test_case/test_yiciyuan_protocol_fjb02.yaml b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_yiciyuan_protocol_fjb02.yaml new file mode 100644 index 000000000..77246517f --- /dev/null +++ b/crates/buttplug_tests/tests/util/device_test/device_test_case/test_yiciyuan_protocol_fjb02.yaml @@ -0,0 +1,56 @@ +devices: + - identifier: + name: "YCY-FJB-02" + expected_name: "Yiciyuan FJB-02" +device_commands: + # Same protocol verification as test_yiciyuan_protocol.yaml — FJB-02 uses + # an identical control packet to FJB-01, so we re-run the multi-feature + # Scalar / Stop sequence to make sure the additional device identifier + # routes through the same handler without regression. + - !VersionGated + min_spec_version: 3 + commands: + - !Messages + device_index: 0 + messages: + - !Stop + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0x35, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + write_with_response: false + + - !Messages + device_index: 0 + messages: + - !Scalar + - Index: 0 + Scalar: 0.5 + ActuatorType: Oscillate + - Index: 1 + Scalar: 0.75 + ActuatorType: Vibrate + - Index: 2 + Scalar: 0.25 + ActuatorType: Vibrate + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0x35, 0x12, 0x0A, 0x0F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + write_with_response: false + + - !Messages + device_index: 0 + messages: + - !Stop + - !Commands + device_index: 0 + commands: + - !Write + endpoint: tx + data: [0x35, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + write_with_response: false