Skip to content

Commit 34fe47a

Browse files
committed
feat: Add support for AdamHealth Sensor
1 parent df2f128 commit 34fe47a

File tree

4 files changed

+298
-0
lines changed

4 files changed

+298
-0
lines changed

buttplug/buttplug-device-config/buttplug-device-config.json

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4919,6 +4919,67 @@
49194919
}
49204920
]
49214921
},
4922+
"adamhealth": {
4923+
"btle": {
4924+
"names": [
4925+
"Check.ED Sensor"
4926+
],
4927+
"services": {
4928+
"569a1902-b87f-490c-92cb-11ba5ea5167c": {
4929+
"rx": "569a2030-b87f-490c-92cb-11ba5ea5167c"
4930+
}
4931+
}
4932+
},
4933+
"defaults": {
4934+
"name": "AdamHealth Sensor",
4935+
"messages": {
4936+
"SensorReadCmd": [
4937+
{
4938+
"SensorType": "Battery",
4939+
"FeatureDescriptor": "Battery Level",
4940+
"SensorRange": [
4941+
[
4942+
0,
4943+
100
4944+
]
4945+
]
4946+
},
4947+
{
4948+
"SensorType": "Pressure",
4949+
"FeatureDescriptor": "Stretch (analog)",
4950+
"SensorRange": [
4951+
[
4952+
0,
4953+
500
4954+
]
4955+
]
4956+
}
4957+
],
4958+
"SensorSubscribeCmd": [
4959+
{
4960+
"SensorType": "Battery",
4961+
"FeatureDescriptor": "Battery Level",
4962+
"SensorRange": [
4963+
[
4964+
0,
4965+
100
4966+
]
4967+
]
4968+
},
4969+
{
4970+
"SensorType": "Pressure",
4971+
"FeatureDescriptor": "Stretch (analog)",
4972+
"SensorRange": [
4973+
[
4974+
0,
4975+
500
4976+
]
4977+
]
4978+
}
4979+
]
4980+
}
4981+
}
4982+
},
49224983
"aneros": {
49234984
"btle": {
49244985
"names": [

buttplug/buttplug-device-config/buttplug-device-config.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,6 +2467,30 @@ protocols:
24672467
- identifier:
24682468
- Hugo2
24692469
name: Lelo Hugo 2
2470+
adamhealth:
2471+
btle:
2472+
names:
2473+
- Check.ED Sensor
2474+
services:
2475+
569a1902-b87f-490c-92cb-11ba5ea5167c:
2476+
rx: 569a2030-b87f-490c-92cb-11ba5ea5167c
2477+
defaults:
2478+
name: AdamHealth Sensor
2479+
messages:
2480+
SensorReadCmd:
2481+
- SensorType: Battery
2482+
FeatureDescriptor: Battery Level
2483+
SensorRange: [[0, 100]]
2484+
- SensorType: Pressure
2485+
FeatureDescriptor: Stretch (analog)
2486+
SensorRange: [[0, 500]]
2487+
SensorSubscribeCmd:
2488+
- SensorType: Battery
2489+
FeatureDescriptor: Battery Level
2490+
SensorRange: [[0, 100]]
2491+
- SensorType: Pressure
2492+
FeatureDescriptor: Stretch (analog)
2493+
SensorRange: [[0, 500]]
24702494
aneros:
24712495
btle:
24722496
names:
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
use crate::{
9+
core::{
10+
errors::ButtplugDeviceError,
11+
message::{
12+
self,
13+
ButtplugDeviceMessage,
14+
ButtplugMessage,
15+
ButtplugServerDeviceMessage,
16+
ButtplugServerMessage,
17+
Endpoint,
18+
SensorReading,
19+
SensorType,
20+
},
21+
},
22+
server::device::{
23+
hardware::{
24+
Hardware,
25+
HardwareEvent,
26+
HardwareSubscribeCmd,
27+
HardwareUnsubscribeCmd,
28+
},
29+
protocol::{generic_protocol_setup, ProtocolHandler},
30+
},
31+
util::{async_manager, stream::convert_broadcast_receiver_to_stream},
32+
};
33+
use futures::{
34+
future::{self, BoxFuture},
35+
FutureExt,
36+
StreamExt,
37+
};
38+
use std::{
39+
default::Default,
40+
pin::Pin,
41+
sync::{
42+
atomic::{AtomicBool, Ordering::SeqCst},
43+
Arc,
44+
},
45+
};
46+
use tokio::sync::broadcast;
47+
48+
generic_protocol_setup!(AdamHealth, "adamhealth");
49+
50+
pub struct AdamHealth {
51+
// Set of sensors we've subscribed to for updates.
52+
subscribed: Arc<AtomicBool>,
53+
event_stream: broadcast::Sender<ButtplugServerDeviceMessage>,
54+
}
55+
56+
impl Default for AdamHealth {
57+
fn default() -> Self {
58+
let (sender, _) = broadcast::channel(256);
59+
Self {
60+
subscribed: Arc::new(AtomicBool::new(false)),
61+
event_stream: sender,
62+
}
63+
}
64+
}
65+
66+
#[derive(Debug)]
67+
enum AdamDataTag {
68+
UNKNOWN,
69+
PRESSURE,
70+
BATTERY,
71+
}
72+
73+
impl ProtocolHandler for AdamHealth {
74+
fn handle_battery_level_cmd(
75+
&self,
76+
device: Arc<Hardware>,
77+
message: message::SensorReadCmd,
78+
) -> BoxFuture<Result<ButtplugServerMessage, ButtplugDeviceError>> {
79+
self.handle_sensor_read_cmd(device, message)
80+
}
81+
82+
fn handle_sensor_read_cmd(
83+
&self,
84+
device: Arc<Hardware>,
85+
message: message::SensorReadCmd,
86+
) -> BoxFuture<Result<ButtplugServerMessage, ButtplugDeviceError>> {
87+
let sensor_type = message.sensor_type().clone();
88+
let request = message::SensorSubscribeCmd::new(
89+
message.device_index(), 0, sensor_type
90+
);
91+
let mut incoming = self.event_stream();
92+
let fut = self.handle_sensor_subscribe_cmd(device, request);
93+
async move {
94+
let _ = fut.await?;
95+
let mut result = Err(ButtplugDeviceError::DeviceConnectionError("Connection error".to_string()));
96+
while let Some(data) = incoming.next().await {
97+
if let ButtplugServerDeviceMessage::SensorReading(reading) = data {
98+
if reading.sensor_type() == sensor_type {
99+
result = Ok(reading.into());
100+
break
101+
}
102+
}
103+
}
104+
result
105+
}
106+
.boxed()
107+
}
108+
109+
fn event_stream(
110+
&self,
111+
) -> Pin<Box<dyn futures::Stream<Item = ButtplugServerDeviceMessage> + Send>> {
112+
convert_broadcast_receiver_to_stream(self.event_stream.subscribe()).boxed()
113+
}
114+
115+
fn handle_sensor_subscribe_cmd(
116+
&self,
117+
device: Arc<Hardware>,
118+
message: message::SensorSubscribeCmd,
119+
) -> BoxFuture<Result<ButtplugServerMessage, ButtplugDeviceError>> {
120+
if self.subscribed.load(SeqCst) {
121+
return future::ready(Ok(message::Ok::new(message.id()).into())).boxed();
122+
}
123+
// The AdamHealth sensor has a single characteristic that streams interleaved data
124+
// There will be a payload identifier code, and then the data
125+
// and then a different identifier code, and then that data
126+
let subscribed = self.subscribed.clone();
127+
async move {
128+
// If we have no sensors we're currently subscribed to, we'll need to bring up our BLE
129+
// characteristic subscription.
130+
if !subscribed.load(SeqCst) {
131+
device
132+
.subscribe(&HardwareSubscribeCmd::new(Endpoint::Rx))
133+
.await?;
134+
let sender = self.event_stream.clone();
135+
let mut hardware_stream = device.event_stream();
136+
let keep_looping = subscribed.clone();
137+
let device_index = message.device_index();
138+
// If we subscribe successfully, we need to set up our event handler.
139+
let _ = async_manager::spawn(async move {
140+
let mut data_tag = AdamDataTag::UNKNOWN;
141+
while let Ok(info) = hardware_stream.recv().await {
142+
// If we have no receivers, quit.
143+
if sender.receiver_count() == 0 || !keep_looping.load(SeqCst) {
144+
// todo btle unsubscribe
145+
debug!("No active listeners for AdamHealth sensor, returning from task");
146+
return;
147+
}
148+
if let HardwareEvent::Notification(_, endpoint, data) = info {
149+
if endpoint == Endpoint::Rx {
150+
if data == "1300".as_bytes() { // incoming sensor value
151+
data_tag = AdamDataTag::PRESSURE
152+
} else if data == "1301".as_bytes() { // incoming battery value
153+
data_tag = AdamDataTag::BATTERY
154+
} else {
155+
// unhandled dataTag, or sensor measurement
156+
let value = std::str::from_utf8(data.as_slice()).unwrap().parse::<i32>();
157+
if matches!(data_tag, AdamDataTag::PRESSURE) && value.is_ok() {
158+
let sensor_value = value.unwrap().clamp(0, 500);
159+
let result = sender.send(
160+
SensorReading::new(device_index, 0, SensorType::Pressure, vec![sensor_value]).into(),
161+
);
162+
if result.is_err() {
163+
debug!("Hardware device listener for AdamHealth sensor shut down, returning from task.");
164+
return;
165+
}
166+
} else if matches!(data_tag, AdamDataTag::BATTERY) && value.is_ok() { // incoming battery value
167+
let sensor_value = value.unwrap().clamp(0, 100);
168+
let result = sender.send(
169+
SensorReading::new(device_index, 0, SensorType::Battery, vec![sensor_value]).into(),
170+
);
171+
if result.is_err() {
172+
debug!("Hardware device listener for AdamHealth sensor shut down, returning from task.");
173+
return;
174+
}
175+
}
176+
data_tag = AdamDataTag::UNKNOWN;
177+
}
178+
}
179+
}
180+
}
181+
});
182+
}
183+
subscribed.store(true, SeqCst);
184+
Ok(message::Ok::new(message.id()).into())
185+
}
186+
.boxed()
187+
}
188+
189+
fn handle_sensor_unsubscribe_cmd(
190+
&self,
191+
device: Arc<Hardware>,
192+
message: message::SensorUnsubscribeCmd,
193+
) -> BoxFuture<Result<ButtplugServerMessage, ButtplugDeviceError>> {
194+
if !self.subscribed.load(SeqCst) {
195+
return future::ready(Ok(message::Ok::new(message.id()).into())).boxed();
196+
}
197+
async move {
198+
// If we have no sensors we're currently subscribed to, we'll need to end our BLE
199+
// characteristic subscription.
200+
self.subscribed.store(false, SeqCst);
201+
device
202+
.unsubscribe(&HardwareUnsubscribeCmd::new(Endpoint::Rx))
203+
.await?;
204+
Ok(message::Ok::new(message.id()).into())
205+
}
206+
.boxed()
207+
}
208+
}

buttplug/src/server/device/protocol/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod generic_command_manager;
1313
pub mod fleshlight_launch_helper;
1414

1515
// Since users can pick and choose protocols, we need all of these to be public.
16+
pub mod adamhealth;
1617
pub mod adrienlastic;
1718
pub mod aneros;
1819
pub mod ankni;
@@ -191,6 +192,10 @@ pub fn get_default_protocol_map() -> HashMap<String, Arc<dyn ProtocolIdentifierF
191192
map.insert(factory.identifier().to_owned(), factory);
192193
}
193194

195+
add_to_protocol_map(
196+
&mut map,
197+
adamhealth::setup::AdamHealthIdentifierFactory::default(),
198+
);
194199
add_to_protocol_map(
195200
&mut map,
196201
adrienlastic::setup::AdrienLasticIdentifierFactory::default(),

0 commit comments

Comments
 (0)