Buttplug sex toy control library
1// Buttplug Rust Source Code File - See https://buttplug.io for more info.
2//
3// Copyright 2016-2024 Nonpolynomial Labs LLC. All rights reserved.
4//
5// Licensed under the BSD 3-Clause license. See LICENSE file in the project root
6// for full license information.
7
8//! Representation and management of devices connected to the server.
9
10use crate::device::{ClientDeviceCommandValue, ClientDeviceOutputCommand};
11
12use crate::{
13 ButtplugClientMessageSender,
14 ButtplugClientResultFuture,
15 create_boxed_future_client_error,
16 device::ClientDeviceFeature,
17};
18use buttplug_core::message::InputType;
19use buttplug_core::{
20 errors::ButtplugDeviceError,
21 message::{
22 ButtplugServerMessageV4,
23 DeviceFeature,
24 DeviceMessageInfoV4,
25 OutputType,
26 StopDeviceCmdV0,
27 },
28 util::stream::convert_broadcast_receiver_to_stream,
29};
30use futures::{FutureExt, Stream, future};
31use getset::{CopyGetters, Getters};
32use log::*;
33use std::collections::BTreeMap;
34use std::{
35 fmt,
36 sync::{
37 Arc,
38 atomic::{AtomicBool, Ordering},
39 },
40};
41use tokio::sync::broadcast;
42
43/// Enum for messages going to a [ButtplugClientDevice] instance.
44#[derive(Clone, Debug)]
45// The message enum is what we'll fly with this most of the time. DeviceRemoved/ClientDisconnect
46// will happen at most once, so we don't care that those contentless traits still take up > 200
47// bytes.
48#[allow(clippy::large_enum_variant)]
49pub enum ButtplugClientDeviceEvent {
50 /// Device has disconnected from server.
51 DeviceRemoved,
52 /// Client has disconnected from server.
53 ClientDisconnect,
54 /// Message was received from server for that specific device.
55 Message(ButtplugServerMessageV4),
56}
57
58#[derive(Getters, CopyGetters, Clone)]
59/// Client-usable representation of device connected to the corresponding
60/// [ButtplugServer][crate::server::ButtplugServer]
61///
62/// [ButtplugClientDevice] instances are obtained from the
63/// [ButtplugClient][super::ButtplugClient], and allow the user to send commands
64/// to a device connected to the server.
65pub struct ButtplugClientDevice {
66 /// Name of the device
67 #[getset(get = "pub")]
68 name: String,
69 /// Display name of the device
70 #[getset(get = "pub")]
71 display_name: Option<String>,
72 /// Index of the device, matching the index in the
73 /// [ButtplugServer][crate::server::ButtplugServer]'s
74 /// [DeviceManager][crate::server::device_manager::DeviceManager].
75 #[getset(get_copy = "pub")]
76 index: u32,
77 /// Actuators and sensors available on the device.
78 #[getset(get = "pub")]
79 device_features: BTreeMap<u32, ClientDeviceFeature>,
80 /// Sends commands from the [ButtplugClientDevice] instance to the
81 /// [ButtplugClient][super::ButtplugClient]'s event loop, which will then send
82 /// the message on to the [ButtplugServer][crate::server::ButtplugServer]
83 /// through the connector.
84 event_loop_sender: ButtplugClientMessageSender,
85 internal_event_sender: broadcast::Sender<ButtplugClientDeviceEvent>,
86 /// True if this [ButtplugClientDevice] is currently connected to the
87 /// [ButtplugServer][crate::server::ButtplugServer].
88 device_connected: Arc<AtomicBool>,
89 /// True if the [ButtplugClient][super::ButtplugClient] that generated this
90 /// [ButtplugClientDevice] instance is still connected to the
91 /// [ButtplugServer][crate::server::ButtplugServer].
92 client_connected: Arc<AtomicBool>,
93}
94
95impl ButtplugClientDevice {
96 /// Creates a new [ButtplugClientDevice] instance
97 ///
98 /// Fills out the struct members for [ButtplugClientDevice].
99 /// `device_connected` and `client_connected` are automatically set to true
100 /// because we assume we're only created connected devices.
101 ///
102 /// # Why is this pub(super)?
103 ///
104 /// There's really no reason for anyone but a
105 /// [ButtplugClient][super::ButtplugClient] to create a
106 /// [ButtplugClientDevice]. A [ButtplugClientDevice] is mostly a shim around
107 /// the [ButtplugClient] that generated it, with some added convenience
108 /// functions for forming device control messages.
109 pub(super) fn new(
110 name: &str,
111 display_name: &Option<String>,
112 index: u32,
113 device_features: &BTreeMap<u32, DeviceFeature>,
114 message_sender: &ButtplugClientMessageSender,
115 ) -> Self {
116 info!(
117 "Creating client device {} with index {} and messages {:?}.",
118 name, index, device_features
119 );
120 let (event_sender, _) = broadcast::channel(256);
121 let device_connected = Arc::new(AtomicBool::new(true));
122 let client_connected = Arc::new(AtomicBool::new(true));
123
124 Self {
125 name: name.to_owned(),
126 display_name: display_name.clone(),
127 index,
128 device_features: device_features
129 .iter()
130 .map(|(i, x)| (*i, ClientDeviceFeature::new(index, *i, x, message_sender)))
131 .collect(),
132 event_loop_sender: message_sender.clone(),
133 internal_event_sender: event_sender,
134 device_connected,
135 client_connected,
136 }
137 }
138
139 pub(crate) fn new_from_device_info(
140 info: &DeviceMessageInfoV4,
141 sender: &ButtplugClientMessageSender,
142 ) -> Self {
143 ButtplugClientDevice::new(
144 info.device_name(),
145 info.device_display_name(),
146 info.device_index(),
147 info.device_features(),
148 sender,
149 )
150 }
151
152 pub fn connected(&self) -> bool {
153 self.device_connected.load(Ordering::Relaxed)
154 }
155
156 pub fn event_stream(&self) -> Box<dyn Stream<Item = ButtplugClientDeviceEvent> + Send + Unpin> {
157 Box::new(Box::pin(convert_broadcast_receiver_to_stream(
158 self.internal_event_sender.subscribe(),
159 )))
160 }
161
162 fn filter_device_actuators(&self, actuator_type: OutputType) -> Vec<ClientDeviceFeature> {
163 self
164 .device_features
165 .iter()
166 .filter(|x| {
167 if let Some(output) = x.1.feature().output() {
168 output.contains(actuator_type)
169 } else {
170 false
171 }
172 })
173 .map(|(_, x)| x)
174 .cloned()
175 .collect()
176 }
177
178 fn set_client_value(
179 &self,
180 client_device_command: &ClientDeviceOutputCommand,
181 ) -> ButtplugClientResultFuture {
182 let features = self.filter_device_actuators(client_device_command.into());
183 if features.is_empty() {
184 // TODO err
185 }
186 let mut fut_vec: Vec<ButtplugClientResultFuture> = vec![];
187 for x in features {
188 let val = x.convert_client_cmd_to_output_cmd(client_device_command);
189 match val {
190 Ok(v) => fut_vec.push(self.event_loop_sender.send_message_expect_ok(v.into())),
191 Err(e) => return future::ready(Err(e)).boxed(),
192 }
193 }
194 async move {
195 futures::future::try_join_all(fut_vec).await?;
196 Ok(())
197 }
198 .boxed()
199 }
200
201 pub fn send_command(
202 &self,
203 client_device_command: &ClientDeviceOutputCommand,
204 ) -> ButtplugClientResultFuture {
205 self.set_client_value(client_device_command)
206 }
207
208 pub fn vibrate_features(&self) -> Vec<ClientDeviceFeature> {
209 self.filter_device_actuators(OutputType::Vibrate)
210 }
211
212 /// Commands device to vibrate, assuming it has the features to do so.
213 pub fn vibrate(&self, level: impl Into<ClientDeviceCommandValue>) -> ButtplugClientResultFuture {
214 let val = level.into();
215 self.set_client_value(&match val {
216 ClientDeviceCommandValue::Int(v) => ClientDeviceOutputCommand::Vibrate(v as u32),
217 ClientDeviceCommandValue::Float(f) => ClientDeviceOutputCommand::VibrateFloat(f),
218 })
219 }
220
221 pub fn has_battery_level(&self) -> bool {
222 self.device_features.iter().any(|x| {
223 x.1
224 .feature()
225 .input()
226 .as_ref()
227 .is_some_and(|x| x.contains(InputType::Battery))
228 })
229 }
230
231 pub fn battery_level(&self) -> ButtplugClientResultFuture<u32> {
232 if let Some(battery) = self.device_features.iter().find(|x| {
233 x.1
234 .feature()
235 .input()
236 .as_ref()
237 .is_some_and(|x| x.contains(InputType::Battery))
238 }) {
239 battery.1.battery_level()
240 } else {
241 create_boxed_future_client_error(
242 ButtplugDeviceError::DeviceFeatureMismatch(
243 "Device does not have battery feature available".to_owned(),
244 )
245 .into(),
246 )
247 }
248 }
249
250 pub fn has_rssi_level(&self) -> bool {
251 self.device_features.iter().any(|x| {
252 x.1
253 .feature()
254 .input()
255 .as_ref()
256 .is_some_and(|x| x.contains(InputType::Rssi))
257 })
258 }
259
260 pub fn rssi_level(&self) -> ButtplugClientResultFuture<i8> {
261 if let Some(rssi) = self.device_features.iter().find(|x| {
262 x.1
263 .feature()
264 .input()
265 .as_ref()
266 .is_some_and(|x| x.contains(InputType::Rssi))
267 }) {
268 rssi.1.rssi_level()
269 } else {
270 create_boxed_future_client_error(
271 ButtplugDeviceError::DeviceFeatureMismatch(
272 "Device does not have RSSI feature available".to_owned(),
273 )
274 .into(),
275 )
276 }
277 }
278
279 /// Commands device to stop all movement.
280 pub fn stop(&self) -> ButtplugClientResultFuture {
281 // All devices accept StopDeviceCmd
282 self
283 .event_loop_sender
284 .send_message_expect_ok(StopDeviceCmdV0::new(self.index).into())
285 }
286
287 pub(crate) fn set_device_connected(&self, connected: bool) {
288 self.device_connected.store(connected, Ordering::Relaxed);
289 }
290
291 pub(crate) fn set_client_connected(&self, connected: bool) {
292 self.client_connected.store(connected, Ordering::Relaxed);
293 }
294
295 pub(crate) fn queue_event(&self, event: ButtplugClientDeviceEvent) {
296 if self.internal_event_sender.receiver_count() == 0 {
297 // We can drop devices before we've hooked up listeners or after the device manager drops,
298 // which is common, so only show this when in debug.
299 debug!("No handlers for device event, dropping event: {:?}", event);
300 return;
301 }
302 self
303 .internal_event_sender
304 .send(event)
305 .expect("Checked for receivers already.");
306 }
307}
308
309impl Eq for ButtplugClientDevice {
310}
311
312impl PartialEq for ButtplugClientDevice {
313 fn eq(&self, other: &Self) -> bool {
314 self.index == other.index
315 }
316}
317
318impl fmt::Debug for ButtplugClientDevice {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 f.debug_struct("ButtplugClientDevice")
321 .field("name", &self.name)
322 .field("index", &self.index)
323 .finish()
324 }
325}