Buttplug sex toy control library
1#[macro_use]
2extern crate log;
3
4use argh::FromArgs;
5use getset::{CopyGetters, Getters};
6use intiface_engine::{
7 EngineOptions, EngineOptionsBuilder, IntifaceEngine, IntifaceEngineError, IntifaceError,
8};
9use std::fs;
10use tokio::{select, signal::ctrl_c};
11use tracing::Level;
12use tracing_subscriber::{
13 filter::{EnvFilter, LevelFilter},
14 layer::SubscriberExt,
15 util::SubscriberInitExt,
16};
17
18const VERSION: &str = env!("CARGO_PKG_VERSION");
19
20/// command line interface for intiface/buttplug.
21///
22/// Note: Commands are one word to keep compat with C#/JS executables currently.
23#[derive(FromArgs, Getters, CopyGetters)]
24pub struct IntifaceCLIArguments {
25 // Options that do something then exit
26 /// print version and exit.
27 #[argh(switch)]
28 #[getset(get_copy = "pub")]
29 version: bool,
30
31 /// print version and exit.
32 #[argh(switch)]
33 #[getset(get_copy = "pub")]
34 server_version: bool,
35
36 // Options that set up the server networking
37 /// if passed, websocket server listens on all interfaces. Otherwise, only
38 /// listen on 127.0.0.1.
39 #[argh(switch)]
40 #[getset(get_copy = "pub")]
41 websocket_use_all_interfaces: bool,
42
43 /// insecure port for websocket servers.
44 #[argh(option)]
45 #[getset(get_copy = "pub")]
46 websocket_port: Option<u16>,
47
48 /// insecure address for connecting to websocket servers.
49 #[argh(option)]
50 #[getset(get = "pub")]
51 websocket_client_address: Option<String>,
52
53 // Options that set up communications with intiface GUI
54 /// if passed, output json for parent process via websockets
55 #[argh(option)]
56 #[getset(get_copy = "pub")]
57 frontend_websocket_port: Option<u16>,
58
59 // Options that set up Buttplug server parameters
60 /// name of server to pass to connecting clients.
61 #[argh(option)]
62 #[argh(default = "\"Buttplug Server\".to_owned()")]
63 #[getset(get = "pub")]
64 server_name: String,
65
66 /// path to the device configuration file
67 #[argh(option)]
68 #[getset(get = "pub")]
69 device_config_file: Option<String>,
70
71 /// path to user device configuration file
72 #[argh(option)]
73 #[getset(get = "pub")]
74 user_device_config_file: Option<String>,
75
76 /// ping timeout maximum for server (in milliseconds)
77 #[argh(option)]
78 #[argh(default = "0")]
79 #[getset(get_copy = "pub")]
80 max_ping_time: u32,
81
82 /// set log level for output
83 #[allow(dead_code)]
84 #[argh(option)]
85 #[getset(get_copy = "pub")]
86 log: Option<Level>,
87
88 /// turn off bluetooth le device support
89 #[argh(switch)]
90 #[getset(get_copy = "pub")]
91 use_bluetooth_le: bool,
92
93 /// turn off serial device support
94 #[argh(switch)]
95 #[getset(get_copy = "pub")]
96 use_serial: bool,
97
98 /// turn off hid device support
99 #[allow(dead_code)]
100 #[argh(switch)]
101 #[getset(get_copy = "pub")]
102 use_hid: bool,
103
104 /// turn off lovense dongle serial device support
105 #[argh(switch)]
106 #[getset(get_copy = "pub")]
107 use_lovense_dongle_serial: bool,
108
109 /// turn off lovense dongle hid device support
110 #[argh(switch)]
111 #[getset(get_copy = "pub")]
112 use_lovense_dongle_hid: bool,
113
114 /// turn off xinput gamepad device support (windows only)
115 #[argh(switch)]
116 #[getset(get_copy = "pub")]
117 use_xinput: bool,
118
119 /// turn on lovense connect app device support (off by default)
120 #[argh(switch)]
121 #[getset(get_copy = "pub")]
122 use_lovense_connect: bool,
123
124 /// turn on websocket server device comm manager
125 #[argh(switch)]
126 #[getset(get_copy = "pub")]
127 use_device_websocket_server: bool,
128
129 /// port for device websocket server comm manager (defaults to 54817)
130 #[argh(option)]
131 #[getset(get_copy = "pub")]
132 device_websocket_server_port: Option<u16>,
133
134 /// if set, broadcast server port/service info via mdns
135 #[argh(switch)]
136 #[getset(get_copy = "pub")]
137 broadcast_server_mdns: bool,
138
139 /// mdns suffix, will be appended to instance names for advertised mdns services (optional, ignored if broadcast_mdns is not set)
140 #[argh(option)]
141 #[getset(get = "pub")]
142 mdns_suffix: Option<String>,
143
144 /// if set, use repeater mode instead of engine mode
145 #[argh(switch)]
146 #[getset(get_copy = "pub")]
147 repeater: bool,
148
149 /// if set, use repeater mode instead of engine mode
150 #[argh(option)]
151 #[getset(get_copy = "pub")]
152 repeater_port: Option<u16>,
153
154 /// if set, use rest api instead of bringing up server
155 #[argh(option)]
156 #[getset(get = "pub")]
157 rest_api_port: Option<u16>,
158
159 #[cfg(debug_assertions)]
160 /// crash the main thread (that holds the runtime)
161 #[argh(switch)]
162 #[getset(get_copy = "pub")]
163 crash_main_thread: bool,
164
165 #[allow(dead_code)]
166 #[cfg(debug_assertions)]
167 /// crash the task thread (for testing logging/reporting)
168 #[argh(switch)]
169 #[getset(get_copy = "pub")]
170 crash_task_thread: bool,
171}
172
173pub fn setup_console_logging(log_level: Option<Level>) {
174 if log_level.is_some() {
175 tracing_subscriber::registry()
176 .with(tracing_subscriber::fmt::layer())
177 .with(LevelFilter::from(log_level))
178 .try_init()
179 .unwrap();
180 } else {
181 tracing_subscriber::registry()
182 .with(tracing_subscriber::fmt::layer())
183 .with(
184 EnvFilter::try_from_default_env()
185 .or_else(|_| EnvFilter::try_new("info"))
186 .unwrap(),
187 )
188 .try_init()
189 .unwrap();
190 };
191 println!("Intiface Server, starting up with stdout output.");
192}
193
194impl TryFrom<IntifaceCLIArguments> for EngineOptions {
195 type Error = IntifaceError;
196 fn try_from(args: IntifaceCLIArguments) -> Result<Self, IntifaceError> {
197 let mut builder = EngineOptionsBuilder::default();
198
199 if let Some(deviceconfig) = args.device_config_file() {
200 info!(
201 "Intiface CLI Options: External Device Config {}",
202 deviceconfig
203 );
204 match fs::read_to_string(deviceconfig) {
205 Ok(cfg) => builder.device_config_json(&cfg),
206 Err(err) => {
207 return Err(IntifaceError::new(&format!(
208 "Error opening external device configuration: {:?}",
209 err
210 )));
211 }
212 };
213 }
214
215 if let Some(userdeviceconfig) = args.user_device_config_file() {
216 info!(
217 "Intiface CLI Options: User Device Config {}",
218 userdeviceconfig
219 );
220 builder.user_device_config_path(userdeviceconfig);
221 match fs::read_to_string(userdeviceconfig) {
222 Ok(cfg) => {
223 builder.user_device_config_json(&cfg);
224 }
225 Err(err) => {
226 warn!(
227 "Error opening user device configuration, ignoring and creating new file: {:?}",
228 err
229 );
230 }
231 };
232 }
233
234 builder
235 .websocket_use_all_interfaces(args.websocket_use_all_interfaces())
236 .use_bluetooth_le(args.use_bluetooth_le())
237 .use_serial_port(args.use_serial())
238 .use_hid(args.use_hid())
239 .use_lovense_dongle_serial(args.use_lovense_dongle_serial())
240 .use_lovense_dongle_hid(args.use_lovense_dongle_hid())
241 .use_xinput(args.use_xinput())
242 .use_lovense_connect(args.use_lovense_connect())
243 .use_device_websocket_server(args.use_device_websocket_server())
244 .max_ping_time(args.max_ping_time())
245 .server_name(args.server_name())
246 .broadcast_server_mdns(args.broadcast_server_mdns());
247
248 #[cfg(debug_assertions)]
249 {
250 builder
251 .crash_main_thread(args.crash_main_thread())
252 .crash_task_thread(args.crash_task_thread());
253 }
254
255 if let Some(value) = args.websocket_port() {
256 builder.websocket_port(value);
257 }
258 if let Some(value) = args.websocket_client_address() {
259 builder.websocket_client_address(value);
260 }
261 if let Some(value) = args.frontend_websocket_port() {
262 builder.frontend_websocket_port(value);
263 }
264 if let Some(value) = args.device_websocket_server_port() {
265 builder.device_websocket_server_port(value);
266 }
267 if let Some(value) = args.rest_api_port() {
268 builder.rest_api_port(*value);
269 }
270 if args.broadcast_server_mdns()
271 && let Some(value) = args.mdns_suffix() {
272 builder.mdns_suffix(value);
273 }
274 Ok(builder.finish())
275 }
276}
277
278#[tokio::main(flavor = "current_thread")] //#[tokio::main]
279async fn main() -> Result<(), IntifaceEngineError> {
280 let args: IntifaceCLIArguments = argh::from_env();
281 if args.server_version() {
282 println!("{}", VERSION);
283 return Ok(());
284 }
285
286 if args.version() {
287 debug!("Server version command sent, printing and exiting.");
288 println!(
289 "Intiface CLI (Rust Edition) Version {}, Commit {}, Built {}",
290 VERSION,
291 option_env!("VERGEN_GIT_SHA_SHORT").unwrap_or("unknown"),
292 option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown")
293 );
294 return Ok(());
295 }
296
297 if args.frontend_websocket_port().is_none() {
298 setup_console_logging(args.log());
299 }
300
301 let options = EngineOptions::try_from(args).map_err(IntifaceEngineError::from)?;
302 let engine = IntifaceEngine::default();
303 select! {
304 result = engine.run(&options, None, &None) => {
305 if let Err(e) = result {
306 println!("Server errored while running:");
307 println!("{:?}", e);
308 }
309 }
310 _ = ctrl_c() => {
311 info!("Control-c hit, exiting.");
312 engine.stop();
313 }
314 }
315
316 Ok(())
317}