├── assets
└── WeeApp.app
│ └── Contents
│ ├── MacOs
│ └── put_executeable_here.txt
│ ├── Resources
│ └── icon.icns
│ └── Info.plist
├── resources.rc
├── addicon.bat
├── examples
├── images
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.png
│ ├── icon.xcf
│ ├── titlebar.png
│ ├── titlebar.xcf
│ ├── progress.svg
│ ├── clear-hover.svg
│ ├── refresh-hover.svg
│ ├── refresh.svg
│ ├── refresh-press.svg
│ ├── clear.svg
│ └── clear-press.svg
├── cli.rs
└── weeapp.rs
├── .travis.yml
├── .gitignore
├── src
├── lib.rs
├── error.rs
├── cache.rs
├── xml.rs
├── rpc.rs
├── device.rs
└── weectrl.rs
├── LICENSE-MIT
├── Cargo.toml
├── README.md
├── LICENSE-APACHE
└── Cargo.lock
/assets/WeeApp.app/Contents/MacOs/put_executeable_here.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources.rc:
--------------------------------------------------------------------------------
1 | ApplicationIcon ICON "examples\images\icon.ico"
--------------------------------------------------------------------------------
/addicon.bat:
--------------------------------------------------------------------------------
1 | rcedit target\release\examples\weeapp.exe --set-icon examples\images\icon.ico
2 |
--------------------------------------------------------------------------------
/examples/images/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/icon.icns
--------------------------------------------------------------------------------
/examples/images/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/icon.ico
--------------------------------------------------------------------------------
/examples/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/icon.png
--------------------------------------------------------------------------------
/examples/images/icon.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/icon.xcf
--------------------------------------------------------------------------------
/examples/images/titlebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/titlebar.png
--------------------------------------------------------------------------------
/examples/images/titlebar.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/titlebar.xcf
--------------------------------------------------------------------------------
/assets/WeeApp.app/Contents/Resources/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/assets/WeeApp.app/Contents/Resources/icon.icns
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: rust
2 |
3 | rust:
4 | - nightly
5 |
6 | os:
7 | - linux
8 |
9 | script:
10 | - cargo build --verbose
11 | - cargo build --verbose --example weeapp
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # These are backup files generated by rustfmt
6 | **/*.rs.bk
7 |
8 | **/.DS_Store
9 | *.res
10 | *.log
11 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! A library for finding and managing Belkin WeMo switches and devices, or compatible devices.
2 | //!
3 | //! WeMo devices use uPnP and HTTP/SOAP for network anouncements and control. The `WeeController`
4 | //! encapsulates this and offers a simpler API for discovery and management.
5 | //! Discovery can be synchronous or asynchronous and a list of known devices is cached on disk
6 | //! for quick startup.
7 |
8 | extern crate reqwest;
9 |
10 | extern crate serde;
11 | extern crate serde_xml_rs;
12 |
13 | extern crate serde_derive;
14 |
15 | extern crate url;
16 |
17 | pub mod weectrl;
18 | pub use weectrl::{
19 | DeviceInfo, DiscoveryMode, Icon, Model, State, StateNotification, WeeController,
20 | };
21 |
22 | pub mod error;
23 |
24 | mod cache;
25 | mod device;
26 | mod rpc;
27 | mod xml;
28 |
--------------------------------------------------------------------------------
/assets/WeeApp.app/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleGetInfoString
6 | WeeApp
7 | CFBundleExecutable
8 | weeapp
9 | CFBundleIdentifier
10 | com.hyperchaotic.weeapp
11 | CFBundleName
12 | WeeApp
13 | CFBundleIconFile
14 | icon.icns
15 | CFBundleShortVersionString
16 | 0.9.6
17 | CFBundleInfoDictionaryVersion
18 | 6.0
19 | CFBundlePackageType
20 | APPL
21 | IFMajorVersion
22 | 0
23 | IFMinorVersion
24 | 1
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Klaus Nielsen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "weectrl"
3 | version = "2.0.2"
4 | authors = ["Klaus Nielsen "]
5 | license = "MIT/Apache-2.0"
6 | readme = "README.md"
7 | keywords = ["WeMo"]
8 | repository = "https://github.com/Hyperchaotic/weectrl"
9 | homepage = "https://github.com/Hyperchaotic/weectrl"
10 | description = "A library and application for controlling WeMo switches."
11 | categories = ["Network Programming"]
12 | edition = "2021"
13 |
14 |
15 | [dependencies]
16 | tracing = "0.1"
17 | tracing-subscriber = { version = "0.3", features = ["local-time"] }
18 | directories = "5"
19 | futures = "0.3"
20 | mime = "0.3.16"
21 | serde-xml-rs = "0.6"
22 | serde_json = "1.0"
23 | serde_derive = "1.0"
24 | serde = { version = "1.0", features = ["derive"] }
25 | url = "2"
26 | reqwest = { version = "0.11", features = ["blocking"] }
27 | hyper = { version = "0.14", features = ["full"] }
28 | tokio = { version = "1", features = ["full"] }
29 |
30 | [profile.release]
31 | strip = true
32 | opt-level = "s"
33 | lto = true
34 | codegen-units = 1
35 | panic = "abort"
36 |
37 | [dev-dependencies]
38 | advmac = "1"
39 | fltk = "^1.5"
40 | fltk-theme = "0.7"
41 |
42 | [lib]
43 | name = "weectrl"
44 | path = "src/lib.rs"
45 |
--------------------------------------------------------------------------------
/examples/images/progress.svg:
--------------------------------------------------------------------------------
1 |
2 |
41 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use reqwest::header::InvalidHeaderValue;
2 | use reqwest::Error as HttpError;
3 | use reqwest::StatusCode;
4 | use serde_xml_rs::Error as SerdeError;
5 | use std::io;
6 | use std::num::ParseIntError;
7 | use url::ParseError;
8 |
9 | /// If a semaphore fails, panic immediately with this message.
10 | pub const FATAL_LOCK: &str = "FATAL Error, Lock failed!";
11 |
12 | #[derive(Debug)]
13 | pub enum Error {
14 | InvalidState,
15 | NoResponse,
16 | UnknownDevice,
17 | UnsupportedDevice,
18 | DeviceAlreadyRegistered,
19 | DeviceError,
20 | ServiceAlreadyRunning,
21 | ServiceNotRunning,
22 | NotSubscribed,
23 | AutoResubscribeRunning,
24 | TimeoutTooShort,
25 | SoapResponseError(SerdeError),
26 | InvalidResponse(StatusCode),
27 | NetworkError(HttpError),
28 | IoError(io::Error),
29 | UrlError(ParseError),
30 | ParseError(ParseIntError),
31 | HttpHeaderError,
32 | }
33 |
34 | impl From for Error {
35 | fn from(_err: InvalidHeaderValue) -> Self {
36 | Self::HttpHeaderError
37 | }
38 | }
39 |
40 | impl From for Error {
41 | fn from(err: ParseIntError) -> Self {
42 | Self::ParseError(err)
43 | }
44 | }
45 |
46 | impl From for Error {
47 | fn from(err: SerdeError) -> Self {
48 | Self::SoapResponseError(err)
49 | }
50 | }
51 |
52 | impl From for Error {
53 | fn from(err: ParseError) -> Self {
54 | Self::UrlError(err)
55 | }
56 | }
57 |
58 | impl From for Error {
59 | fn from(err: io::Error) -> Self {
60 | Self::IoError(err)
61 | }
62 | }
63 |
64 | impl From for Error {
65 | fn from(err: HttpError) -> Self {
66 | Self::NetworkError(err)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/examples/cli.rs:
--------------------------------------------------------------------------------
1 | use futures::prelude::*;
2 | use std::time::Duration;
3 | use tokio::io::{self, AsyncBufReadExt};
4 | use weectrl::{DiscoveryMode, WeeController};
5 |
6 | #[tokio::main]
7 | async fn main() {
8 | // Uncomment below to see tracing from the library
9 | //use tracing_subscriber::fmt::time;
10 | //tracing_subscriber::fmt()
11 | // .with_timer(time::LocalTime::rfc_3339())
12 | // .init();
13 |
14 | println!("Instantiating WeeController.\n");
15 | let controller = WeeController::new();
16 |
17 | // Find switches on the network.
18 | let mut discovery_future = controller.discover_future(
19 | DiscoveryMode::CacheAndBroadcast,
20 | true,
21 | Duration::from_secs(5),
22 | );
23 |
24 | println!("Searching for devices...\n");
25 | while let Some(d) = discovery_future.next().await {
26 | println!(
27 | " Found device {}, ID: {}, state: {:?}.",
28 | d.friendly_name, d.unique_id, d.state
29 | );
30 | let res = controller.subscribe(
31 | &d.unique_id,
32 | Duration::from_secs(120),
33 | true,
34 | Duration::from_secs(5),
35 | );
36 | println!(
37 | " - Subscribed {} - {:?} seconds to resubscribe.\n",
38 | d.friendly_name,
39 | res.unwrap()
40 | );
41 | }
42 |
43 | // Create notification future.
44 | let notifier_future = controller.notify_future();
45 | let notifier = notifier_future.for_each(|o| {
46 | if let Ok(e) = controller.get_info(&o.unique_id) {
47 | println!(
48 | " Device {} sent state update {:?}",
49 | e.friendly_name, o.state
50 | );
51 | }
52 | future::ready(())
53 | });
54 |
55 | // Input buffer, for keyboard cancel on Enter.
56 | let mut buf = String::new();
57 | let mut reader = io::BufReader::new(io::stdin());
58 | let quit = reader.read_line(&mut buf);
59 |
60 | println!("\n\nListening for notifications from switches.");
61 | println!("Press Enter to quit.\n");
62 | tokio::select! {
63 | _ = quit => (),
64 |
65 | _ = notifier => (),
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/examples/images/clear-hover.svg:
--------------------------------------------------------------------------------
1 |
2 |
60 |
--------------------------------------------------------------------------------
/src/cache.rs:
--------------------------------------------------------------------------------
1 | extern crate directories;
2 | use directories::ProjectDirs;
3 |
4 | use tracing::info;
5 |
6 | use serde::{Deserialize, Serialize};
7 |
8 | use std::fs::File;
9 | use std::io::prelude::*;
10 |
11 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
12 | pub struct DeviceAddress {
13 | pub location: String,
14 | pub mac_address: String,
15 | }
16 |
17 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
18 | struct CacheData {
19 | pub cache: Vec,
20 | }
21 |
22 | const CACHE_FILE: &str = "IPCache.dat";
23 |
24 | /// Disk cache of IP addresses of WeMo devices
25 | /// WeMo doesn't always answer broadcast upnp, this cache allows
26 | /// for the persistent storage of known devices for unicast discovery
27 | pub struct DiskCache {
28 | cache_file: Option,
29 | }
30 |
31 | impl DiskCache {
32 | pub fn new() -> Self {
33 | let mut file_path: Option = None;
34 |
35 | if let Some(proj_dirs) = ProjectDirs::from("", "", "WeeCtrl") {
36 | let mut path = proj_dirs.config_dir().to_path_buf();
37 | path.push(CACHE_FILE);
38 | file_path = Some(path);
39 | info!("Cache file: {:#?}", file_path);
40 | }
41 | Self {
42 | cache_file: file_path,
43 | }
44 | }
45 |
46 | /// Write data to cache, errors ignored as everything will still work
47 | /// without a cache, just slower.
48 | pub fn write(&self, addresses: Vec) {
49 | if let Some(ref fpath) = self.cache_file {
50 | let data = CacheData { cache: addresses };
51 | if let Some(prefix) = fpath.parent() {
52 | let _ignore = std::fs::create_dir_all(prefix);
53 |
54 | if let Ok(serialized) = serde_json::to_string(&data) {
55 | if let Ok(mut buffer) = File::create(fpath) {
56 | let _ignore = buffer.write_all(&serialized.into_bytes());
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
63 | pub fn read(&self) -> Option> {
64 | if let Some(ref fpath) = self.cache_file {
65 | if let Ok(mut file) = File::open(fpath) {
66 | let mut s = String::new();
67 | let _ignore = file.read_to_string(&mut s);
68 | let cachedata: Option = serde_json::from_str(&s).ok();
69 | if let Some(data) = cachedata {
70 | return Some(data.cache);
71 | }
72 | }
73 | }
74 | None
75 | }
76 |
77 | pub fn clear(&self) {
78 | if let Some(ref fpath) = self.cache_file {
79 | let _ignore = std::fs::remove_file(fpath);
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/examples/images/refresh-hover.svg:
--------------------------------------------------------------------------------
1 |
2 |
56 |
--------------------------------------------------------------------------------
/examples/images/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
64 |
--------------------------------------------------------------------------------
/examples/images/refresh-press.svg:
--------------------------------------------------------------------------------
1 |
2 |
64 |
--------------------------------------------------------------------------------
/src/xml.rs:
--------------------------------------------------------------------------------
1 | pub use crate::error::Error;
2 |
3 | use serde::Deserialize;
4 |
5 | #[derive(Debug, Deserialize, Clone)]
6 | #[serde(rename = "service")]
7 | pub struct Service {
8 | #[serde(rename = "serviceType")]
9 | pub service_type: String,
10 | #[serde(rename = "serviceId")]
11 | pub service_id: String,
12 | #[serde(rename = "controlURL")]
13 | pub control_url: String,
14 | #[serde(rename = "eventSubURL")]
15 | pub event_sub_url: String,
16 | #[serde(rename = "SCPDURL")]
17 | pub scp_url: String,
18 | }
19 |
20 | #[derive(Debug, Deserialize, Clone)]
21 | #[serde(rename = "icon")]
22 | pub struct Icon {
23 | pub mimetype: String, // >jpg
24 | pub width: String, // >100
25 | pub height: String, // >100
26 | pub depth: String, // >100
27 | pub url: String, // >icon.jpg
28 | }
29 |
30 | #[derive(Debug, Deserialize, Clone)]
31 | #[serde(rename = "iconList")]
32 | pub struct IconList {
33 | pub icon: Vec,
34 | }
35 |
36 | #[derive(Debug, Deserialize, Clone)]
37 | #[serde(rename = "serviceList")]
38 | pub struct ServiceList {
39 | pub service: Vec,
40 | }
41 |
42 | #[derive(Debug, Deserialize, Clone)]
43 | #[serde(rename = "device")]
44 | pub struct Device {
45 | #[serde(rename = "deviceType")]
46 | pub device_type: String, // >urn:Belkin:device:lightswitch:1
47 | #[serde(rename = "friendlyName")]
48 | pub friendly_name: String, // >Kitchen light
49 | pub manufacturer: String, // >Belkin International Inc.
50 | #[serde(rename = "manufacturerURL")]
51 | pub manufacturer_url: String, // >http://www.belkin.com
52 | #[serde(rename = "modelDescription")]
53 | pub model_description: String, // >Belkin Plugin Socket 1.0
54 | #[serde(rename = "modelName")]
55 | pub model_name: String, // >LightSwitch
56 | #[serde(rename = "modelNumber")]
57 | pub model_number: String, // >1.0
58 | #[serde(rename = "modelURL")]
59 | pub model_url: String, // >http://www.belkin.com/plugin/
60 | #[serde(rename = "serialNumber")]
61 | pub serial_number: String, // >221435K13005D9
62 | #[serde(rename = "UDN")]
63 | pub udn: String, // >uuid:Lightswitch-1_0-221435K13005D9
64 | #[serde(rename = "UPC")]
65 | pub upc: String, // >123456789
66 | #[serde(rename = "macAddress")]
67 | pub mac_address: String, // >94103E4830D0
68 | #[serde(rename = "firmwareVersion")]
69 | pub firmware_version: String, // >WeMo_WW_2.00.10937.PVT-OWRT-LS
70 | #[serde(rename = "iconVersion")]
71 | pub icon_version: String, // >0|49153
72 | #[serde(rename = "binaryState")]
73 | pub binary_state: String, // >0
74 | #[serde(rename = "iconList")]
75 | pub icon_list: IconList,
76 | #[serde(rename = "serviceList")]
77 | pub service_list: ServiceList,
78 | #[serde(rename = "presentationURL")]
79 | pub presentation_url: String,
80 | }
81 |
82 | #[derive(Debug, Deserialize, Clone)]
83 | #[serde(rename = "specVersion")]
84 | pub struct SpecVersion {
85 | pub major: String,
86 | pub minor: String,
87 | }
88 |
89 | #[derive(Debug, Deserialize, Clone)]
90 | #[serde(rename = "root")]
91 | pub struct Root {
92 | #[serde(rename = "specVersion")]
93 | pub spec_version: SpecVersion,
94 | pub device: Device,
95 | }
96 |
97 | pub fn parse_services(data: &str) -> Result {
98 | let r: Root = serde_xml_rs::de::from_str(data)?;
99 | Ok(r)
100 | }
101 |
102 | pub fn get_binary_state(data: &str) -> Option {
103 | if data.contains("1") {
104 | return Some(1);
105 | } else if data.contains("0") {
106 | return Some(0);
107 | }
108 | None
109 | }
110 |
111 | pub const SETBINARYSTATEOFF: &str = "
112 |
115 |
116 | \
117 |
118 | \
119 | 0
120 |
121 |
122 | ";
123 |
124 | pub const SETBINARYSTATEON: &str = "
125 |
128 |
129 | \
130 |
131 | \
132 | 1
133 |
134 |
135 | ";
136 |
137 | pub const GETBINARYSTATE: &str = "
138 |
139 |
142 |
143 |
144 | 1
145 |
146 |
147 | ";
148 |
--------------------------------------------------------------------------------
/examples/images/clear.svg:
--------------------------------------------------------------------------------
1 |
2 |
65 |
--------------------------------------------------------------------------------
/examples/images/clear-press.svg:
--------------------------------------------------------------------------------
1 |
2 |
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # weectrl [](https://opensource.org/licenses/MIT) [](https://opensource.org/licenses/Apache-2.0)
2 |
3 | A cross platform library and application for controlling Belkin WeMo switches and Sockets. Written in Rust.
4 |
5 | ## WeeApp, the application
6 |
7 | ### Screenshots
8 | 
9 | 
10 | 
11 | 
12 |
13 | ### Functionality
14 | The "WeeApp" application will scan the local network for Belkin WeMo devices and list what's found. They can be switched on/off from the app and if a device changes state due to external activity (e.g. physical toggle or schedule) this will be reflected in the app UI due to the notification feature.
15 |
16 | Searching for new devices can take 5-6 seconds but the app benefits from the caching feature of the library so previously known devices will be displayed very quickly upon restart.
17 |
18 | * The "paper basket" will forget all devices and erase the disk cache.
19 | * The reload button will load known devices from cache and rescan the network.
20 | * The application remembers size and position of its window.
21 |
22 | ### Platforms
23 | Current version tested on Windows 11, Linux and maOS x84_64.
24 |
25 | ### Building
26 |
27 | The (example) App has certain FLTK_rs [dependencies](https://fltk-rs.github.io/fltk-book/Setup.html).
28 |
29 | ```
30 | cargo build --release --example weeapp
31 | ```
32 | #### Windows addendum
33 | An application icon can be found in `examples/images/icon.ico`. To insert it into the Windows binary use [rcedit][56bbd8db]:
34 | ```
35 | rcedit target\release\examples\weeapp.exe --set-icon examples\images\icon.ico
36 | ```
37 | [56bbd8db]: https://github.com/electron/rcedit/releases "rcedit"
38 |
39 | ## weectrl, the library
40 | ### Functionality
41 | * Discover devices on the network.
42 | * Retrieve detailed device information.
43 | * Switch devices on or off.
44 | * Cache known devices on disk for quick reload.
45 | * Subscription to state change notifications from devices if they're toggled.
46 | * Uses the Tracing crate for logging.
47 |
48 | ### API examples
49 |
50 | #### Initialization and discovery
51 | Create new instance of controller:
52 | ``` rust
53 |
54 | use weectrl::{DeviceInfo, DiscoveryMode, State, StateNotification, WeeController};
55 |
56 | let controller = WeeController::new();
57 | ```
58 |
59 | To discover devices on network or/and in cache. starting by reading list of known devices from the disk cache, then broadcasts network query:
60 | ``` rust
61 |
62 | let rx: std::sync::mpsc::Receiver = controller.discover(
63 | DiscoveryMode::CacheAndBroadcast,
64 | true,
65 | Duration::from_secs(5),
66 | );
67 | ```
68 | Scans both disk cache file and network, will "forget" in-memory list first. Give network devices maximum 5 seconds to respond.
69 | When discovery ends the channel will be closed.
70 |
71 | Futures version of the discover function.
72 | ``` rust
73 |
74 | let notifications: futures::channel::mpsc::UnboundedReceiver = controller.discover_future(
75 | DiscoveryMode::CacheAndBroadcast,
76 | true,
77 | Duration::from_secs(5),
78 | );
79 | ```
80 |
81 |
82 | #### Switching and notifications
83 |
84 | Let `unique_id` be a `DeviceInfo::unique_id` returned by previous discovery.
85 |
86 | Starting listening for notifications from subscribed devices. If called several times only the most recent `rx` channel will receive notifications:
87 | ``` rust
88 |
89 | let rx: std::sync::mpsc::Receiver = controller.notify();
90 | ```
91 | Whenever a switch is toggled a message will appear on the Receiver.
92 |
93 | Futures version.
94 | ``` rust
95 |
96 | let notifications_fut: futures::channel::mpsc::UnboundedReceiver = controller.notify_future();
97 | ```
98 |
99 | Subscribe to notifications for a device and instruct WeeController to automatically refresh subscriptions every 120 seconds before they expire.
100 | As this causes a network request to be made to the switch a 5 second network timeout is set for the request in case the switch doesn't respond:
101 | ``` rust
102 |
103 | let result = controller.subscribe(&unique_id, Duration::from_secs(120), true, Duration::from_secs(5));
104 | ```
105 |
106 | Unsubscribe from notifications from one device:
107 | ``` rust
108 |
109 | let result = controller.unsubscribe(&unique_id, Duration::from_secs(5));
110 | ```
111 |
112 | Unsubscribe from all notifications:
113 | ``` rust
114 |
115 | controller.unsubscribe_all();
116 | ```
117 |
118 | To toggle a switch (on or off):
119 | ``` rust
120 |
121 | let result = controller.set_binary_state(unique_id, State::On, Duration::from_secs(5));
122 | match result {
123 | Ok(state) => println!("Ok({:?})", state),
124 | Err(err) => println!("Err({:?})", err),
125 | }
126 | ```
127 |
128 | To get a switch state:
129 | ``` rust
130 |
131 | let result = controller.get_binary_state(unique_id, Duration::from_secs(5));
132 | match result {
133 | Ok(state) => println!("Ok({:?})", state),
134 | Err(err) => println!("Err({:?})", err),
135 | }
136 | ```
137 |
138 | Retrive icons stored in a switch:
139 | ``` rust
140 |
141 | let list: Result, Error> = controller.get_icons(unique_id, Duration::from_secs(5));
142 | ```
143 |
144 | Retrieve structure with information stored about a switch, from the controller. This is the same structure returned earlier during Discovery:
145 | ``` rust
146 |
147 | let result = controller.get_info(unique_id);
148 | ```
149 |
150 |
151 | ## Compatibility
152 | Theoretically supports and device advertising the `urn:Belkin:service:basicevent:1` service. Tested with Lightswitch and Socket.
153 | Currently only tested with Belkin WeMo LightSwitch and Socket.
154 |
155 | ## License
156 |
157 | Licensed under either of
158 |
159 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
160 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
161 |
--------------------------------------------------------------------------------
/src/rpc.rs:
--------------------------------------------------------------------------------
1 | extern crate mime;
2 |
3 | use reqwest::header;
4 | use reqwest::header::HeaderMap;
5 | use tracing::info;
6 |
7 | use std::sync::mpsc;
8 | use std::time::Duration;
9 |
10 | pub use reqwest::{Method, StatusCode};
11 |
12 | pub use crate::error::Error;
13 |
14 | use url::Url;
15 |
16 | #[derive(Debug)]
17 | pub struct SubscribeResponse {
18 | pub sid: String,
19 | pub timeout: u32,
20 | }
21 |
22 | struct RpcResponse {
23 | pub status: StatusCode,
24 | pub sid: Option,
25 | pub timeout: Option,
26 | }
27 |
28 | /// Perform a HTTP unsubscribe action to specified URL.
29 | pub fn unsubscribe_action(
30 | url: &Url,
31 | sid: &str,
32 | conn_timeout: Duration,
33 | ) -> Result {
34 | info!("Unsubscribe {}.", sid);
35 |
36 | let mut headers = header::HeaderMap::new();
37 | headers.insert("SID", header::HeaderValue::from_str(sid)?);
38 |
39 | let method = reqwest::Method::from_bytes(b"UNSUBSCRIBE").unwrap();
40 |
41 | let response = http_request(&method, &headers, url, "", conn_timeout)?;
42 |
43 | if response.status() != StatusCode::OK {
44 | info!("Unsubscribe {}, Error {}.", sid, response.status());
45 | return Err(Error::InvalidResponse(response.status()));
46 | }
47 |
48 | info!("Unsubscribe {} ok.", sid);
49 | Ok(response.status())
50 | }
51 |
52 | fn rpc_run(
53 | method: &Method,
54 | headers: &HeaderMap,
55 | url: &Url,
56 | conn_timeout: Duration,
57 | ) -> Result {
58 | info!("Request: {:?} {:?}", method.to_string(), &url);
59 |
60 | let response = http_request(method, headers, url, "", conn_timeout)?;
61 |
62 | if response.status() != StatusCode::OK {
63 | return Err(Error::InvalidResponse(response.status()));
64 | }
65 |
66 | let sid = if let Some(sid) = response.headers().get("SID") {
67 | Some(sid.to_str().map_err(|_| (Error::InvalidState))?.to_string())
68 | } else {
69 | None
70 | };
71 |
72 | let tim = if let Some(tim) = response.headers().get("TIMEOUT") {
73 | Some(tim.to_str().map_err(|_| (Error::InvalidState))?.to_string())
74 | } else {
75 | None
76 | };
77 |
78 | Ok(RpcResponse {
79 | status: response.status(),
80 | sid,
81 | timeout: tim,
82 | })
83 | }
84 |
85 | /// Perform a HTTP subscribe action to specified URL, starting a new subscription.
86 | pub fn subscribe(
87 | url: &Url,
88 | sub_timeout: Duration,
89 | callback: &str,
90 | conn_timeout: Duration,
91 | ) -> Result {
92 | info!("Subscribe");
93 |
94 | let method = Method::from_bytes(b"SUBSCRIBE").unwrap();
95 |
96 | let mut headers = HeaderMap::new();
97 | headers.insert("CALLBACK", header::HeaderValue::from_str(callback)?);
98 | headers.insert("NT", header::HeaderValue::from_str("upnp:event")?);
99 | headers.insert(
100 | "TIMEOUT",
101 | header::HeaderValue::from_str(&format!("Second-{}", sub_timeout.as_secs()))?,
102 | );
103 | info!("REQ:");
104 | info!("{:?}", headers);
105 | let rpcresponse = rpc_run(&method, &headers, url, conn_timeout)?;
106 |
107 | handle_subscription_response(rpcresponse)
108 | }
109 |
110 | pub fn http_get_text(url: &Url, conn_timeout: Duration) -> Result {
111 | let headers = HeaderMap::new();
112 |
113 | let response = http_request(&reqwest::Method::GET, &headers, url, "", conn_timeout)?;
114 |
115 | if response.status() != StatusCode::OK {
116 | return Err(Error::InvalidResponse(response.status()));
117 | }
118 |
119 | Ok(response.text()?)
120 | }
121 |
122 | pub fn http_get_bytes(url: &Url, conn_timeout: Duration) -> Result, Error> {
123 | let headers = HeaderMap::new();
124 |
125 | let response = http_request(&reqwest::Method::GET, &headers, url, "", conn_timeout)?;
126 |
127 | if response.status() != StatusCode::OK {
128 | return Err(Error::InvalidResponse(response.status()));
129 | }
130 |
131 | Ok(response.bytes()?.to_vec())
132 | }
133 |
134 | fn handle_subscription_response(response: RpcResponse) -> Result {
135 | if response.status != StatusCode::OK {
136 | return Err(Error::InvalidResponse(response.status));
137 | }
138 |
139 | let sid = match response.sid {
140 | Some(sid) => sid,
141 | None => return Err(Error::DeviceError),
142 | };
143 |
144 | let timeout_string = match response.timeout {
145 | Some(timeout) => timeout,
146 | None => return Err(Error::DeviceError),
147 | };
148 |
149 | let timeout = if timeout_string.starts_with("Second-") {
150 | let (_, number) = timeout_string.split_at("Second-".len());
151 | number.parse::().unwrap_or(0)
152 | } else {
153 | 0
154 | };
155 |
156 | Ok(SubscribeResponse { sid, timeout })
157 | }
158 |
159 | /// Perform a HTTP SOAP action to specified URL.
160 | pub fn soap_action(
161 | url: &Url,
162 | action: &str,
163 | xml: &str,
164 | conn_timeout: Duration,
165 | ) -> Result {
166 | info!("soap_action, url: {:?}", url.to_string());
167 |
168 | let method = Method::GET;
169 |
170 | let mut headers = HeaderMap::new();
171 |
172 | headers.insert("SOAPACTION", header::HeaderValue::from_str(action)?);
173 |
174 | headers.insert(
175 | reqwest::header::CONTENT_LENGTH,
176 | header::HeaderValue::from_str(&xml.len().to_string())?,
177 | );
178 |
179 | headers.insert(
180 | reqwest::header::CONTENT_TYPE,
181 | header::HeaderValue::from_static("text/xml; charset=utf-8"),
182 | );
183 | headers.insert(
184 | reqwest::header::CONNECTION,
185 | header::HeaderValue::from_str("close")?,
186 | );
187 |
188 | let response = http_request(&method, &headers, url, xml, conn_timeout)?;
189 |
190 | if response.status() != StatusCode::OK {
191 | return Err(Error::InvalidResponse(response.status()));
192 | }
193 |
194 | Ok(response.text()?)
195 | }
196 |
197 | // Blocking Reqwest runs an async executor, if the app using this library does the same
198 | // the reactors will be nested and request will explode horribly.
199 | // So we give request its own thread.
200 | pub fn http_request(
201 | method: &Method,
202 | headers: &HeaderMap,
203 | url: &Url,
204 | body: &str,
205 | conn_timeout: Duration,
206 | ) -> Result {
207 | let (tx, rx) = mpsc::channel();
208 |
209 | let (method, headers, url, body) = (
210 | method.clone(),
211 | headers.clone(),
212 | url.clone(),
213 | body.to_string(),
214 | );
215 |
216 | std::thread::spawn(move || {
217 | let client = reqwest::blocking::Client::new();
218 |
219 | let request = reqwest::blocking::Client::request(&client, method.clone(), url.clone())
220 | .headers(headers.clone())
221 | .body(body.to_string())
222 | .timeout(conn_timeout);
223 | let response = request.send();
224 | let _ignore = tx.send(response.map_err(|e| Error::from(e)));
225 | });
226 |
227 | rx.recv().unwrap()
228 | }
229 |
--------------------------------------------------------------------------------
/src/device.rs:
--------------------------------------------------------------------------------
1 | use std::net::IpAddr;
2 | use std::str::FromStr;
3 | use std::sync::mpsc;
4 | use std::sync::{Arc, Mutex};
5 | use std::{thread, time::Duration};
6 | use tracing::info;
7 | use url::Url;
8 |
9 | use crate::error;
10 | use crate::error::Error;
11 | use crate::rpc;
12 | use crate::weectrl::{DeviceInfo, Icon, State};
13 | use crate::xml;
14 |
15 | #[derive(Debug, Clone)]
16 | /// One WeMo or similar device
17 | pub struct Device {
18 | /// IP address of local interface on the same network as the "remote" device.
19 | local_ip: IpAddr,
20 | /// Path for retrieve and toggle switch state.
21 | binary_control_path: Option,
22 | /// Path for subscribing to events
23 | event_path: Option,
24 | /// Subscription ID for notifications, can be updated from daemon thread if it changes.
25 | sid: Option>>,
26 | /// If subscribed for notifications this is used to cancel th daemon.
27 | subscription_daemon: Option>,
28 | // Information about device, including that returned by 'home' url.
29 | pub info: DeviceInfo,
30 | }
31 |
32 | impl Device {
33 | pub fn new(info: DeviceInfo, local_ip: IpAddr) -> Self {
34 | let dev = Self {
35 | local_ip,
36 | binary_control_path: None,
37 | event_path: None,
38 | sid: None,
39 | subscription_daemon: None,
40 | info,
41 | };
42 | info!(
43 | "New device {}, {} @ {}",
44 | dev.info.root.device.friendly_name, dev.info.root.device.mac_address, dev.info.base_url
45 | );
46 | dev
47 | }
48 |
49 | /// Returns copy of current subscription ID.
50 | pub fn sid(&self) -> Option {
51 | if let Some(dev_sid) = self.sid.clone() {
52 | let sid = dev_sid.lock().expect(error::FATAL_LOCK).clone();
53 | return Some(sid);
54 | }
55 | None
56 | }
57 |
58 | /// Any supported device needs to have the Belkin basicevent service. Check for its
59 | /// existence and save the paths for control/subscriptions for future use.
60 | pub fn validate_device(&mut self) -> bool {
61 | for service in &self.info.root.device.service_list.service {
62 | if service.service_type == "urn:Belkin:service:basicevent:1" {
63 | self.binary_control_path = Some(service.control_url.clone());
64 | self.event_path = Some(service.event_sub_url.clone());
65 | return true;
66 | }
67 | }
68 | false
69 | }
70 |
71 | fn cancel_subscription_daemon(&mut self) {
72 | if let Some(daemon) = self.subscription_daemon.clone() {
73 | info!("Cancel subscription daemon for {}", self.info.friendly_name);
74 | let _ = daemon.send(());
75 | self.subscription_daemon = None;
76 | }
77 | }
78 |
79 | /// Send unsubscribe command and cancel the daemon if active.
80 | pub fn unsubscribe(&mut self, conn_timeout: Duration) -> Result {
81 | info!("UnSubscribe {} {:?}", self.info.friendly_name, self.sid);
82 | self.cancel_subscription_daemon();
83 |
84 | if let Some(sid_shared) = self.sid.clone() {
85 | let req_url = Self::make_request_url(&self.info.base_url, &self.event_path)?;
86 | let sid = sid_shared.lock().expect(error::FATAL_LOCK).clone();
87 | let _ = rpc::unsubscribe_action(&req_url, &sid, conn_timeout)?; // TODO check statuscode
88 | self.sid = None;
89 | return Ok(sid);
90 | }
91 | Err(Error::NotSubscribed)
92 | }
93 |
94 | // Concatenate basic URL with request path.
95 | // e.g. "http://192.168.0.10/ with "upnp/event/basicevent1".
96 | fn make_request_url(base_url: &str, req_path: &Option) -> Result {
97 | if let Some(ref path) = *req_path {
98 | let mut req_url = base_url.to_owned();
99 | if req_url.ends_with('/') && path.starts_with('/') {
100 | req_url = req_url.trim_end_matches('/').to_owned();
101 | }
102 | req_url.push_str(path);
103 | return Ok(Url::from_str(&req_url)?);
104 | }
105 | Err(Error::UnsupportedDevice)
106 | }
107 |
108 | /// Send subscribe comand, cancel any currrent daemon and start a new if auto resub on.
109 | pub fn subscribe(
110 | &mut self,
111 | port: u16,
112 | seconds: Duration,
113 | auto_resubscribe: bool,
114 | conn_timeout: Duration,
115 | ) -> Result<(String, u32), Error> {
116 | info!("Subscribe");
117 | let callback = format!("", self.local_ip, port);
118 | let req_url = Self::make_request_url(&self.info.base_url, &self.event_path)?;
119 | self.cancel_subscription_daemon();
120 | let res = rpc::subscribe(&req_url, seconds, &callback, conn_timeout)?;
121 |
122 | let new_sid = Arc::new(Mutex::new(res.sid.clone()));
123 | self.sid = Some(new_sid.clone());
124 |
125 | if auto_resubscribe {
126 | let (tx, rx) = mpsc::channel();
127 | self.subscription_daemon = Some(tx);
128 |
129 | std::thread::Builder::new()
130 | .name(format!("DEV_resub_thread {}", self.info.friendly_name))
131 | .spawn(move || {
132 | Self::subscription_daemon(&rx, &req_url, &new_sid, seconds, &callback);
133 | })?;
134 | }
135 |
136 | Ok((res.sid, res.timeout))
137 | }
138 |
139 | fn subscription_daemon(
140 | rx: &mpsc::Receiver<()>,
141 | url: &Url,
142 | device_sid: &Arc>,
143 | initial_seconds: Duration,
144 | callback: &str,
145 | ) {
146 | use std::sync::mpsc::TryRecvError;
147 |
148 | let mut duration = initial_seconds - Duration::from_secs(10);
149 |
150 | loop {
151 | thread::sleep(duration);
152 | // Were we signalled to end, or channel nonfunctional?
153 | match rx.try_recv() {
154 | Err(TryRecvError::Empty) => (),
155 | _ => break,
156 | }
157 | info!("Resubscribing.");
158 | match rpc::subscribe(url, initial_seconds, callback, Duration::from_secs(5)) {
159 | Ok(res) => {
160 | duration = Duration::from_secs(u64::from(res.timeout - 10)); // Switch returns timeout we need to obey.
161 | let mut sid_ref = device_sid.lock().expect(error::FATAL_LOCK);
162 | sid_ref.clear();
163 | sid_ref.push_str(&res.sid);
164 | }
165 | Err(e) => {
166 | info!("Err: {:?}", e);
167 | continue; // Could be error due to temporary network issue, just keep trying after a bit.
168 | }
169 | };
170 | }
171 | }
172 |
173 | /// Retrieve current switch binarystate.
174 | pub fn fetch_icons(&mut self, conn_timeout: Duration) -> Result, Error> {
175 | let mut icon_list = Vec::new();
176 |
177 | self.info.state = State::Unknown;
178 |
179 | for xmlicon in &self.info.root.device.icon_list.icon {
180 | let width = xmlicon.width.parse::()?;
181 | let height = xmlicon.height.parse::()?;
182 | let depth = xmlicon.depth.parse::()?;
183 |
184 | let req_file = Some(xmlicon.url.clone());
185 | let req_url = Self::make_request_url(&self.info.base_url, &req_file)?;
186 |
187 | let data = rpc::http_get_bytes(&req_url, conn_timeout)?;
188 |
189 | // Image type is unreliable, so we have to peek in the data stream to detect it.
190 | let mimetype: mime::Mime = match data.as_slice() {
191 | // JPEG
192 | [0xFF, 0xD8, 0xFF, ..] => mime::IMAGE_JPEG,
193 | // GIF
194 | [0x47, 0x49, 0x46, 0x38, ..] => mime::IMAGE_GIF,
195 | // PNG
196 | [0x89, 0x50, 0x4E, 0x47, ..] => mime::IMAGE_PNG,
197 | _ => return Err(Error::InvalidResponse(reqwest::StatusCode::IM_A_TEAPOT)),
198 | };
199 |
200 | info!(
201 | "Fetch Icon {}. MimeType returned: {:#?}",
202 | &req_url, &mimetype
203 | );
204 |
205 | let icon = Icon {
206 | mimetype,
207 | width,
208 | height,
209 | depth,
210 | data,
211 | };
212 |
213 | icon_list.push(icon);
214 | }
215 |
216 | Ok(icon_list)
217 | }
218 |
219 | /// Retrieve current switch binarystate.
220 | pub fn fetch_binary_state(&mut self, conn_timeout: Duration) -> Result {
221 | self.info.state = State::Unknown;
222 |
223 | let req_url = Self::make_request_url(&self.info.base_url, &self.binary_control_path)?;
224 |
225 | let http_response = rpc::soap_action(
226 | &req_url,
227 | "\"urn:Belkin:service:basicevent:1#GetBinaryState\"",
228 | xml::GETBINARYSTATE,
229 | conn_timeout,
230 | )?;
231 |
232 | if let Some(state) = xml::get_binary_state(&http_response) {
233 | self.info.state = State::from(state);
234 | return Ok(self.info.state);
235 | }
236 |
237 | return Err(Error::InvalidState);
238 | }
239 |
240 | /// Send command to toggle switch state. If toggeling to same state an Error is returned.
241 | pub fn set_binary_state(
242 | &mut self,
243 | state: State,
244 | conn_timeout: Duration,
245 | ) -> Result {
246 | if state == State::Unknown {
247 | return Err(Error::InvalidState);
248 | }
249 |
250 | let req_url = Self::make_request_url(&self.info.base_url, &self.binary_control_path)?;
251 |
252 | rpc::soap_action(
253 | &req_url,
254 | "\"urn:Belkin:service:basicevent:1#SetBinaryState\"",
255 | &state.to_string(),
256 | conn_timeout,
257 | )?;
258 |
259 | self.info.state = state;
260 | Ok(state)
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/weectrl.rs:
--------------------------------------------------------------------------------
1 | use hyper::body::HttpBody;
2 | use hyper::service::{make_service_fn, service_fn};
3 | use hyper::{Body, HeaderMap, Request, Response, Server};
4 | use std::convert::Infallible;
5 | use std::net::SocketAddr;
6 | use std::str::FromStr;
7 | use std::time::Duration;
8 | use tracing::info;
9 |
10 | use futures::channel::mpsc as mpsc_future;
11 | use std::collections::hash_map::Entry;
12 | use std::collections::HashMap;
13 | use std::sync::{mpsc, Arc, Mutex};
14 |
15 | use std::net::IpAddr;
16 | use std::net::TcpListener;
17 | use std::net::UdpSocket;
18 |
19 | use crate::cache::{DeviceAddress, DiskCache};
20 | use crate::device::Device;
21 | use crate::error;
22 | use crate::error::{Error, FATAL_LOCK};
23 | use crate::xml;
24 | pub use mime::Mime;
25 | use url::Url;
26 | use xml::Root;
27 |
28 | #[derive(Debug, Clone)]
29 | /// Notification from a device on network that binary state have changed.
30 | pub struct StateNotification {
31 | /// Same unique id as known from discovery and used to subscribe.
32 | pub unique_id: String,
33 | /// New binarystate of the device.
34 | pub state: State,
35 | }
36 |
37 | impl From for State {
38 | fn from(u: u8) -> Self {
39 | match u {
40 | 0 => Self::Off,
41 | 1 => Self::On,
42 | _ => Self::Unknown,
43 | }
44 | }
45 | }
46 |
47 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
48 | /// Specify the methods to be used for device discovery.
49 | pub enum DiscoveryMode {
50 | /// Read and verify known devices from disk cache. Don't brodcast uPnP query.
51 | CacheOnly,
52 | /// Read and verify known devices from disk cache, then brodcast uPnP query.
53 | CacheAndBroadcast,
54 | /// Broadcast uPnP query, don't load devices from disk cache. This will cause
55 | /// disk cache to be overwritten with new list of devices.
56 | BroadcastOnly,
57 | }
58 |
59 | #[derive(Debug, Clone, PartialEq, Eq)]
60 | /// Type of device
61 | pub enum Model {
62 | Lightswitch,
63 | Socket,
64 | Unknown(String),
65 | }
66 |
67 | impl std::fmt::Display for Model {
68 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 | match self {
70 | Self::Lightswitch => write!(f, "Light Switch"),
71 | Self::Socket => write!(f, "Socket"),
72 | Self::Unknown(str) => write!(f, "{}", str),
73 | }
74 | }
75 | }
76 |
77 | impl<'a> From<&'a str> for Model {
78 | fn from(string: &'a str) -> Self {
79 | match string {
80 | "LightSwitch" => Self::Lightswitch,
81 | "Socket" => Self::Socket,
82 | _ => Self::Unknown(string.to_owned()),
83 | }
84 | }
85 | }
86 |
87 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
88 | /// Represents whether a device binary state is on or off.
89 | pub enum State {
90 | /// Device is switched on
91 | On,
92 | /// Device is switched off
93 | Off,
94 | /// Device is not responding to queries or commands
95 | Unknown,
96 | }
97 |
98 | impl std::fmt::Display for State {
99 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 | match *self {
101 | Self::On => f.write_str(xml::SETBINARYSTATEON),
102 | Self::Off => f.write_str(xml::SETBINARYSTATEOFF),
103 | Self::Unknown => f.write_str("UNKNOWN STATE"),
104 | }
105 | }
106 | }
107 |
108 | #[derive(Debug, Clone)]
109 | // A WeMo swicth have an associated icon
110 | pub struct Icon {
111 | pub mimetype: Mime,
112 | pub width: u64,
113 | pub height: u64,
114 | pub depth: u64,
115 | pub data: Vec,
116 | }
117 |
118 | #[derive(Debug, Clone)]
119 | // Data returned for each device found during discovery.
120 | pub struct DeviceInfo {
121 | /// Human readable name returned from the device homepage.
122 | pub friendly_name: String,
123 | /// Basic model name, e.g. LightSwitch, Socket.
124 | pub model: Model,
125 | /// Unique identifier for this device, used for issuing commands to controller.
126 | pub unique_id: String,
127 | /// Network hostname, usually the IP address.
128 | pub hostname: String,
129 | /// http address including port number
130 | pub base_url: String,
131 | /// Device "home" URL
132 | pub location: String,
133 | /// Current switch binarystate
134 | pub state: State,
135 | /// Device information, from XML homepage
136 | pub root: Root,
137 | /// Device information, data from XML homepage
138 | pub xml: String,
139 | }
140 |
141 | /// Controller entity used for finding and controlling Belkin WeMo, and compatible, devices.
142 | pub struct WeeController {
143 | cache: Arc>,
144 | devices: Arc>>,
145 | notification_mpsc: Arc>>>,
146 | port: u16,
147 | }
148 |
149 | impl Default for WeeController {
150 | fn default() -> Self {
151 | Self::new()
152 | }
153 | }
154 |
155 | impl Drop for WeeController {
156 | fn drop(&mut self) {
157 | info!("Dropping WeeController");
158 | self.unsubscribe_all();
159 | }
160 | }
161 |
162 | impl WeeController {
163 | #[must_use]
164 | pub fn new() -> Self {
165 | info!("Init");
166 |
167 | // Find a free port number between 8000-9000
168 | let port = Self::get_available_port().unwrap();
169 | let notification_mpsc = Arc::new(Mutex::new(None));
170 | let devices = Arc::new(Mutex::new(HashMap::new()));
171 |
172 | // Start the webserver listeninh for incoming notifications
173 | let tx = notification_mpsc.clone();
174 | let devs = devices.clone();
175 | let _ignore = std::thread::Builder::new()
176 | .name("SUB_server".to_string())
177 | .spawn(move || {
178 | Self::notification_server(tx, devs, port);
179 | })
180 | .expect("UNABLE TO START NOTFICATION THREAD");
181 |
182 | // Build self
183 | Self {
184 | cache: Arc::new(Mutex::new(DiskCache::new())),
185 | devices,
186 | notification_mpsc,
187 | port,
188 | }
189 | }
190 |
191 | /// Forget all known devices, optionalle also clear the on-disk cache.
192 | pub fn clear(&self, clear_cache: bool) {
193 | info!("Clearing list of devices. Clear disk; {:?}.", clear_cache);
194 | self.unsubscribe_all();
195 | if clear_cache {
196 | self.cache.lock().expect(FATAL_LOCK).clear();
197 | }
198 | self.devices.lock().expect(FATAL_LOCK).clear();
199 | }
200 |
201 | /// Set the device binary state (On/Off), e.g. toggle a switch.
202 | pub fn set_binary_state(
203 | &self,
204 | unique_id: &str,
205 | state: State,
206 | conn_timeout: Duration,
207 | ) -> Result {
208 | info!("Set binary state for Device {:?} {:?} ", unique_id, state);
209 | let mut devices = self.devices.lock().expect(FATAL_LOCK);
210 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) {
211 | let device = o.get_mut();
212 | return device.set_binary_state(state, conn_timeout);
213 | }
214 | Err(Error::UnknownDevice)
215 | }
216 |
217 | /// Query the device for the binary state (On/Off)
218 | pub fn get_binary_state(
219 | &self,
220 | unique_id: &str,
221 | conn_timeout: Duration,
222 | ) -> Result {
223 | info!("Get_binary_state for device {:?}.", unique_id);
224 | let mut devices = self.devices.lock().expect(FATAL_LOCK);
225 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) {
226 | let device = o.get_mut();
227 | return device.fetch_binary_state(conn_timeout);
228 | }
229 | Err(Error::UnknownDevice)
230 | }
231 |
232 | /// Query the device for a list of icons
233 | pub fn get_icons(&self, unique_id: &str, conn_timeout: Duration) -> Result, Error> {
234 | info!("Get_icons for device {:?}.", unique_id);
235 | let mut devices = self.devices.lock().expect(FATAL_LOCK);
236 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) {
237 | let device = o.get_mut();
238 | return device.fetch_icons(conn_timeout);
239 | }
240 | Err(Error::UnknownDevice)
241 | }
242 |
243 | /// Return the cached DeviceInfo structure, which was retrieved during discovery.
244 | pub fn get_info(&self, unique_id: &str) -> Result {
245 | info!("Get information for device {:?}.", unique_id);
246 | let mut devices = self.devices.lock().expect(FATAL_LOCK);
247 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) {
248 | let device = o.get_mut();
249 | return Ok(device.info.clone());
250 | }
251 | Err(Error::UnknownDevice)
252 | }
253 |
254 | /// Cancel subscription for notifications from all devices.
255 | // @TODO spin off in threads?
256 | pub fn unsubscribe_all(&self) {
257 | info!("unsubscribe_all.");
258 | let mut devices = self.devices.lock().expect(FATAL_LOCK);
259 | let unique_ids: Vec = devices.keys().cloned().collect();
260 | let mut handles = Vec::with_capacity(10);
261 |
262 | for unique_id in unique_ids {
263 | if let Entry::Occupied(mut o) = devices.entry(unique_id.clone()) {
264 | let mut device = o.get_mut().clone();
265 | handles.push(std::thread::spawn(move || {
266 | info!("unsubscribe {:?}.", unique_id);
267 | let _ignore = device.unsubscribe(Duration::from_secs(5));
268 | }));
269 | }
270 | }
271 | for handle in handles {
272 | let _ignore = handle.join();
273 | }
274 | info!("Done unsubscribe_all.");
275 | }
276 |
277 | /// Cancel subscription for notifications from a device.
278 | pub fn unsubscribe(&self, unique_id: &str, conn_timeout: Duration) -> Result {
279 | let mut devices = self.devices.lock().expect(FATAL_LOCK);
280 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) {
281 | let device = o.get_mut();
282 |
283 | info!("unsubscribe {:?}.", unique_id);
284 | return device.unsubscribe(conn_timeout);
285 | }
286 | Err(Error::UnknownDevice)
287 | }
288 |
289 | /// Subscribe to a device for notifications on state change.
290 | /// Typically 120-600 seconds. If auto_resubscribe==true the subscription will
291 | /// be renewed automatically.
292 | /// Notifications will be returned via the mpsc returned from start_subscription_service.
293 | pub fn subscribe(
294 | &self,
295 | unique_id: &str,
296 | seconds: Duration,
297 | auto_resubscribe: bool,
298 | conn_timeout: Duration,
299 | ) -> Result {
300 | if seconds < Duration::from_secs(15) {
301 | info!("Subscribe: TimeoutTooShort");
302 | return Err(Error::TimeoutTooShort);
303 | }
304 |
305 | let mut devices = self.devices.lock().expect(FATAL_LOCK);
306 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) {
307 | let device = o.get_mut();
308 | info!("subscribe {:?}.", unique_id);
309 | let (_, time) = device.subscribe(self.port, seconds, auto_resubscribe, conn_timeout)?;
310 | return Ok(time);
311 | }
312 | info!("Subscribe: UnknownDevice");
313 | Err(Error::UnknownDevice)
314 | }
315 |
316 | /// Read list of know devices from disk cache as well as new devices responding on the network.
317 | /// Returns immediately and send discovered devices back on the mpsc future as they're found.
318 | /// Allow network devices max `mx` seconds to respond.
319 | /// When discovery ends, after mx seconds and a bit, the channel will be closed.
320 | /// `forget_devices` = true will clear the internal list of devices.
321 | /// Futures alternative to fn Discover(...)
322 | pub fn discover_future(
323 | &self,
324 | mode: DiscoveryMode,
325 | forget_devices: bool,
326 | mx: Duration,
327 | ) -> mpsc_future::UnboundedReceiver {
328 | let rx = self.discover(mode, forget_devices, mx);
329 |
330 | let (tx_fut, rx_fut) = mpsc_future::unbounded();
331 |
332 | // Just wrap the standard mpsc version, easy with unbounded channels
333 | let _ = std::thread::Builder::new()
334 | .name("Discovery future proxy".to_string())
335 | .spawn(move || {
336 | info!("Starting discovery future proxy thread");
337 | while let Ok(device) = rx.recv() {
338 | info!("Future thread Sending {}.", device.friendly_name);
339 | let _ = tx_fut.unbounded_send(device);
340 | }
341 | });
342 | rx_fut
343 | }
344 |
345 | /// Read list of know devices from disk cache as well as new devices responding on the network.
346 | /// Returns immediately and send discovered devices back on the mpsc as they're found.
347 | /// Allow network devices max `mx` seconds to respond.
348 | /// When discovery ends, after mx seconds and a bit, the channel will be closed.
349 | /// `forget_devices` = true will clear the internal list of devices.
350 | pub fn discover(
351 | &self,
352 | mode: DiscoveryMode,
353 | forget_devices: bool,
354 | mx: Duration,
355 | ) -> mpsc::Receiver {
356 | if forget_devices {
357 | self.clear(false);
358 | }
359 |
360 | let (tx, rx) = mpsc::channel();
361 | let devices = self.devices.clone();
362 | let cache = self.cache.clone();
363 |
364 | let _ = std::thread::Builder::new()
365 | .name("SSDP_main".to_string())
366 | .spawn(move || {
367 | info!("Starting discover");
368 |
369 | if mode == DiscoveryMode::CacheOnly || mode == DiscoveryMode::CacheAndBroadcast {
370 | info!("Loading devices from cache.");
371 | if let Some(cache_list) = cache.lock().expect(error::FATAL_LOCK).read() {
372 | info!("Cached devices {:?}", cache_list);
373 |
374 | for cache_entry in cache_list {
375 | let device =
376 | Self::register_device(&cache_entry.location, &devices).ok();
377 | if let Some(new) = device {
378 | let _ignore = tx.send(new);
379 | }
380 | }
381 | }
382 | }
383 |
384 | if mode == DiscoveryMode::BroadcastOnly || mode == DiscoveryMode::CacheAndBroadcast
385 | {
386 | info!("Broadcasting uPnP query.");
387 | // Create Our Search Request
388 |
389 | let devices = devices.clone(); // to move into the new thread
390 | let mut cache_dirty = false;
391 |
392 | let bind_address = Self::get_bind_addr().unwrap();
393 | let socket = UdpSocket::bind(&bind_address).unwrap();
394 |
395 | Self::send_sddp_request(&socket, mx);
396 |
397 | let mut buf = [0u8; 2048];
398 | socket
399 | .set_read_timeout(Some(mx + Duration::from_secs(1)))
400 | .unwrap();
401 |
402 | let (cache_dirt_tx, cache_dirt_rx) = mpsc::channel();
403 |
404 | let mut num_threads = 0;
405 | while let Ok(e) = socket.recv(&mut buf) {
406 | let message = std::str::from_utf8(&buf[..e]).unwrap().to_string();
407 |
408 | // Handle message in different thread
409 | let devs = devices.clone();
410 | let nx = tx.clone();
411 | let cache_dirt = cache_dirt_tx.clone();
412 | num_threads += 1;
413 |
414 | let _ignore = std::thread::Builder::new()
415 | .name(format!("SSDP_handle_msg {}", num_threads).to_string())
416 | .spawn(move || {
417 | if let Some(location) = Self::parse_ssdp_response(&message) {
418 | let device = Self::register_device(&location, &devs).ok();
419 |
420 | device.map_or_else(
421 | || {
422 | let _ignore = cache_dirt.send(false);
423 | },
424 | |new| {
425 | let _ignore = nx.send(new);
426 | let _ignore = cache_dirt.send(true);
427 | },
428 | );
429 | }
430 | });
431 | }
432 |
433 | for _ in 0..num_threads {
434 | if cache_dirt_rx.recv().unwrap() {
435 | info!("CACHE DIRTY");
436 | cache_dirty = true;
437 | }
438 | }
439 |
440 | if cache_dirty {
441 | Self::refresh_cache(&cache, &devices);
442 | }
443 | }
444 |
445 | info!("Done! Ending discover thread.");
446 | });
447 | rx
448 | }
449 |
450 | /// Retrieve a mpsc future (stream) on which to get notifications.
451 | /// Futures alternative to get_notifications.
452 | pub fn notify_future(&self) -> mpsc_future::UnboundedReceiver {
453 | let rx = self.notify();
454 |
455 | let (tx_fut, rx_fut) = mpsc_future::unbounded();
456 |
457 | // Just wrap the standard mpsc version, easy with unbounded channels
458 | let _ = std::thread::Builder::new()
459 | .name("subscription future proxy".to_string())
460 | .spawn(move || {
461 | info!("Starting subscription future proxy thread");
462 | while let Ok(notification) = rx.recv() {
463 | let _ = tx_fut.unbounded_send(notification);
464 | }
465 | });
466 | rx_fut
467 | }
468 |
469 | /// Retrieve a mpsc on which to get notifications.
470 | pub fn notify(&self) -> mpsc::Receiver {
471 | let (tx, rx) = mpsc::channel::();
472 | let mut notification_mpsc = self.notification_mpsc.lock().expect(FATAL_LOCK);
473 | *notification_mpsc = Some(tx);
474 | rx
475 | }
476 |
477 | // Get the IP address of this PC, from the same interface talking to the device.
478 | // It will be needed if subscriptions are used. It's not pretty but easiest way to Make
479 | // sure we can talk to multiple devices on separate interfaces.
480 | fn get_local_ip(location: &str) -> Result {
481 | use std::net::TcpStream;
482 |
483 | // extract ip:port from URL
484 | let location_url = Url::parse(location)?;
485 | if let Some(host) = location_url.host_str() {
486 | let mut destination = String::from(host);
487 | if let Some(port) = location_url.port() {
488 | let port_str = format!(":{}", port);
489 | destination.push_str(&port_str);
490 | }
491 |
492 | // Create TCP connection from which we can get local IP.
493 | let destination_str: &str = &destination;
494 | let stream = TcpStream::connect(&destination_str)?;
495 | let loc_ip = stream.local_addr()?;
496 | return Ok(loc_ip.ip());
497 | }
498 | Err(Error::NoResponse)
499 | }
500 |
501 | // Retrieve a device home page.
502 | fn get_device_home(location: &Url, conn_timeout: Duration) -> Result {
503 | let response = super::rpc::http_get_text(location, conn_timeout)?;
504 | Ok(response)
505 | }
506 |
507 | // Write list of active devices to disk cache
508 | fn refresh_cache(cache: &Arc>, devices: &Arc>>) {
509 | info!("Refreshing cache.");
510 | let mut list = Vec::new();
511 | for (mac, dev) in devices.lock().expect(FATAL_LOCK).iter() {
512 | let location = dev.info.location.clone();
513 | list.push(DeviceAddress {
514 | location,
515 | mac_address: mac.clone(),
516 | });
517 | }
518 | cache.lock().expect(FATAL_LOCK).write(list);
519 | }
520 |
521 | // Given location URL, query a device. If successful add to list of active devices
522 | fn retrieve_device(location: &str, conn_timeout: Duration) -> Result {
523 | let mut url = Url::from_str(location)?;
524 | let xml = Self::get_device_home(&url, conn_timeout)?;
525 | let local_ip = Self::get_local_ip(location)?;
526 | let root: Root = xml::parse_services(&xml)?;
527 |
528 | info!("Device {:?} {:?} ", root.device.friendly_name, location);
529 |
530 | let mut hostname = String::new();
531 | //let mut base_url = Url::parse(location)?;
532 | if let Some(hn) = url.host_str() {
533 | hostname = hn.to_owned();
534 | }
535 | url.set_path("/");
536 |
537 | let model = root.device.model_name.clone();
538 | let model_str: &str = &model;
539 | let info = DeviceInfo {
540 | friendly_name: root.device.friendly_name.clone(),
541 | model: Model::from(model_str),
542 | unique_id: root.device.mac_address.clone(),
543 | hostname,
544 | base_url: url.to_string(),
545 | location: location.to_string(),
546 | state: State::Unknown,
547 | root,
548 | xml,
549 | };
550 | let mut dev = Device::new(info, local_ip);
551 |
552 | if dev.validate_device()
553 | && dev
554 | .fetch_binary_state(Duration::from_secs(5))
555 | .ok()
556 | .is_some()
557 | {
558 | return Ok(dev);
559 | }
560 |
561 | info!("Device not supported.");
562 | Err(Error::UnsupportedDevice)
563 | }
564 |
565 | // Add device to hashmap.
566 | fn register_device(
567 | location: &str,
568 | devices: &Arc>>,
569 | ) -> Result {
570 | let newdev = Self::retrieve_device(location, Duration::from_secs(5))?;
571 | let mut devs = devices.lock().expect(FATAL_LOCK);
572 | let unique_id = newdev.info.root.device.mac_address.clone();
573 |
574 | if let std::collections::hash_map::Entry::Vacant(e) = devs.entry(unique_id) {
575 | info!("Registering dev: {:?}", newdev.info.unique_id);
576 | let info = newdev.info.clone();
577 | e.insert(newdev);
578 | return Ok(info);
579 | }
580 | Err(Error::DeviceAlreadyRegistered)
581 | }
582 |
583 | async fn http_req_handler(
584 | notification_mspsc: Arc>>>,
585 | devices: Arc>>,
586 | req: Request,
587 | ) -> Result, Infallible> {
588 | let (parts, mut stream) = req.into_parts();
589 |
590 | // Retrieve the entire message body
591 | let mut body = String::new();
592 | while let Some(d) = stream.data().await {
593 | if let Ok(s) = d {
594 | body += std::str::from_utf8(&s).unwrap();
595 | }
596 | }
597 |
598 | // Ok full message received, now process it
599 | if body.len() > 0 && body.find("") != None {
600 | // Extract SID and BinaryState
601 | if let Some((notification_sid, state)) = Self::get_sid_and_state(&body, &parts.headers)
602 | {
603 | info!("str, sid {notification_sid} {}", state);
604 | // If we have mathing SID, register and forward statechange
605 | let devs = devices.lock().expect(FATAL_LOCK);
606 | 'search: for (unique_id, dev) in devs.iter() {
607 | if let Some(device_sid) = dev.sid() {
608 | // Found a match, send notification to the client
609 | if notification_sid == device_sid {
610 | info!(
611 | "Server got switch update for {:?}. sid: {:?}. State: {:?}.",
612 | unique_id,
613 | notification_sid,
614 | State::from(state)
615 | );
616 |
617 | let n = StateNotification {
618 | unique_id: unique_id.clone(),
619 | state: State::from(state),
620 | };
621 |
622 | let tx = notification_mspsc.lock().expect(FATAL_LOCK);
623 | match &*tx {
624 | None => info!("No listener, dropping notification."),
625 | Some(tx) => {
626 | let _r = tx.send(n);
627 | }
628 | }
629 | break 'search;
630 | }
631 | }
632 | }
633 | }
634 | }
635 |
636 | Ok(Response::new(Body::from("")))
637 | }
638 |
639 | #[tokio::main()]
640 | async fn notification_server(
641 | notification_mspsc: Arc>>>,
642 | devices: Arc>>,
643 | port: u16,
644 | ) {
645 | // A `MakeService` that produces a `Service` to handle each connection.
646 | let make_service = make_service_fn(move |_| {
647 | let notification_mspsc = notification_mspsc.clone();
648 | let devices = devices.clone();
649 |
650 | // Create a `Service` for responding to the request.
651 | let service = service_fn(move |req| {
652 | Self::http_req_handler(notification_mspsc.clone(), devices.clone(), req)
653 | });
654 |
655 | // Return the service to hyper.
656 | async move { Ok::<_, Infallible>(service) }
657 | });
658 |
659 | let addr: SocketAddr = ([0, 0, 0, 0], port).into();
660 |
661 | info!("Listening to notifications on: {:?}", addr.to_string());
662 |
663 | let server = Server::bind(&addr).serve(make_service);
664 |
665 | if let Err(e) = server.await {
666 | info!("server error: {}", e);
667 | }
668 | }
669 |
670 | fn get_sid_and_state(body: &str, headers: &HeaderMap) -> Option<(String, u8)> {
671 | let mut sid = None;
672 |
673 | let hsid = headers.get("SID");
674 | if let Some(h) = hsid {
675 | if let Ok(f) = h.to_str() {
676 | sid = Some(f.to_string());
677 | }
678 | }
679 |
680 | let state = xml::get_binary_state(body);
681 |
682 | if sid != None && state != None {
683 | return Some((sid.unwrap(), state.unwrap()));
684 | }
685 | None
686 | }
687 |
688 | fn port_is_available(port: u16) -> bool {
689 | TcpListener::bind(("127.0.0.1", port)).is_ok()
690 | }
691 |
692 | fn get_available_port() -> Option {
693 | (8000..9000).find(|port| Self::port_is_available(*port))
694 | }
695 |
696 | // Bind through a connected interface
697 | fn get_bind_addr() -> Result {
698 | let any: SocketAddr = ([0, 0, 0, 0], 0).into();
699 | let dns: SocketAddr = ([1, 1, 1, 1], 80).into();
700 | let socket = UdpSocket::bind(any)?;
701 | socket.connect(dns)?;
702 | let bind_addr = socket.local_addr()?;
703 |
704 | Ok(bind_addr)
705 | }
706 |
707 | fn parse_ssdp_response(response: &str) -> Option {
708 | for line in response.lines() {
709 | let line = line.to_lowercase();
710 | if line.contains("location") {
711 | if let Some(idx) = line.find("http") {
712 | let url = &line[idx..line.len()];
713 | return Some(url.trim().to_string());
714 | }
715 | }
716 | }
717 | None
718 | }
719 |
720 | fn send_sddp_request(socket: &UdpSocket, mx: Duration) {
721 | let msg = format!(
722 | "M-SEARCH * HTTP/1.1\r
723 | Host: 239.255.255.250:1900\r
724 | Man: \"ssdp:discover\"\r
725 | ST: upnp:rootdevice\r
726 | MX: {}\r
727 | Content-Length: 0\r\n\r\n",
728 | mx.as_secs()
729 | );
730 |
731 | let broadcast_address: SocketAddr = ([239, 255, 255, 250], 1900).into();
732 | let _ = socket.set_broadcast(true);
733 | socket.send_to(msg.as_bytes(), &broadcast_address).unwrap();
734 | }
735 | }
736 |
--------------------------------------------------------------------------------
/examples/weeapp.rs:
--------------------------------------------------------------------------------
1 | // On Windows don't create terminal window when opening app in release mode GUI
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | extern crate weectrl;
5 |
6 | use fltk::app::MouseButton;
7 | use tracing::info;
8 |
9 | use weectrl::{DeviceInfo, DiscoveryMode, State, StateNotification, WeeController};
10 |
11 | use fltk::{enums::Color, enums::Event, image::PngImage, image::SvgImage, prelude::*, *};
12 | use fltk_theme::widget_schemes::fluent::colors::*;
13 | use fltk_theme::{SchemeType, WidgetScheme};
14 | use std::collections::HashMap;
15 | use std::sync::mpsc::TryRecvError;
16 | use std::time::Duration;
17 |
18 | extern crate directories;
19 | use directories::ProjectDirs;
20 |
21 | use serde::{Deserialize, Serialize};
22 |
23 | use std::fs::File;
24 | use std::io::prelude::*;
25 |
26 | #[derive(Debug, Clone)]
27 | enum Message {
28 | Reload,
29 | Clear,
30 | StartDiscovery,
31 | PollDiscovery,
32 | EndDiscovery,
33 | AddButton(DeviceInfo),
34 | Notification(StateNotification),
35 | Clicked(DeviceInfo),
36 | }
37 |
38 | struct WeeApp {
39 | app: app::App,
40 | scroll: group::Scroll,
41 | pack: group::Pack,
42 | reloading_frame: frame::Frame,
43 | progress_frame: frame::Frame,
44 | sender: app::Sender,
45 | receiver: app::Receiver,
46 | controller: WeeController,
47 | discovering: bool, // Is discover currently in progress?
48 | buttons: HashMap, // List of deviceID's and indexes of the associated buttons
49 | }
50 |
51 | const SETTINGS_FILE: &str = "Settings.json";
52 |
53 | const SUBSCRIPTION_DURATION: Duration = Duration::from_secs(180);
54 | const SUBSCRIPTION_AUTO_RENEW: bool = true;
55 | const DISCOVERY_MX: Duration = Duration::from_secs(5);
56 | const CONN_TIMEOUT: Duration = Duration::from_secs(5);
57 |
58 | const UNIT_SPACING: i32 = 40;
59 | const BUTTON_HEIGHT: i32 = UNIT_SPACING;
60 | const WINDOW_WIDTH: i32 = 350;
61 | const TOP_BAR_HEIGHT: i32 = UNIT_SPACING + 10;
62 | const LIST_HEIGHT: i32 = 170;
63 | const WINDOW_HEIGHT: i32 = LIST_HEIGHT + TOP_BAR_HEIGHT + UNIT_SPACING;
64 | const SCROLL_WIDTH: i32 = 15;
65 |
66 | const BUTTON_ON_COLOR: Color = Color::from_rgb(114, 159, 207);
67 | const BUTTON_OFF_COLOR: Color = Color::from_rgb(13, 25, 38);
68 |
69 | const WINDOW_ICON: &[u8] = include_bytes!("images/titlebar.png");
70 |
71 | const PROGRESS: &str = include_str!("images/progress.svg");
72 |
73 | const RL_BTN1: &str = include_str!("images/refresh.svg");
74 | const RL_BTN2: &str = include_str!("images/refresh-press.svg");
75 | const RL_BTN3: &str = include_str!("images/refresh-hover.svg");
76 |
77 | const CL_BTN1: &str = include_str!("images/clear.svg");
78 | const CL_BTN2: &str = include_str!("images/clear-press.svg");
79 | const CL_BTN3: &str = include_str!("images/clear-hover.svg");
80 |
81 | const CLEAR_TOOLTIP: &str = "Forget all devices and clear the on-disk list of known devices.";
82 | const RELOAD_TOOLTIP: &str =
83 | "Reload list of devices from on-disk list (if any) and then by network query.";
84 | const SWITCH_TOOLTIP: &str = "Left click to toggle switch. Right click for additional information.";
85 |
86 | macro_rules! read_image {
87 | ($data:ident) => {{
88 | let mut img = SvgImage::from_data($data).unwrap();
89 | img.scale(30, 30, true, true);
90 | img
91 | }};
92 | }
93 |
94 | // Builder for the information dialogue.
95 | macro_rules! build_field {
96 | ($parent:ident, $name:expr, $string:expr) => {
97 | let st = format!("{}: {:}", $name, $string);
98 | let mut name = Box::new(output::Output::default().with_size(0, 20));
99 | name.set_value(&st);
100 | name.set_color(Color::from_hex(0x2e3436));
101 | name.set_label_font(enums::Font::Helvetica);
102 | name.set_frame(enums::FrameType::FlatBox);
103 | name.set_text_size(16);
104 | $parent.add(&*name);
105 | };
106 | }
107 |
108 | impl WeeApp {
109 | pub fn new() -> Self {
110 | let app = app::App::default();
111 | app::background(0, 0, 0);
112 | app::background2(0x00, 0x00, 0x00);
113 | app::foreground(0xff, 0xff, 0xff);
114 | app::set_color(
115 | Color::Selection,
116 | SELECTION_COLOR.0,
117 | SELECTION_COLOR.1,
118 | SELECTION_COLOR.2,
119 | );
120 |
121 | let theme = WidgetScheme::new(SchemeType::Fluent);
122 | theme.apply();
123 |
124 | app::set_font_size(18);
125 |
126 | let (sender, receiver) = app::channel();
127 |
128 | // get version number from Cargp.toml
129 | let version = env!("CARGO_PKG_VERSION");
130 |
131 | // Create main Application window. Double buffered.
132 | let mut main_win =
133 | window::DoubleWindow::default().with_label(&format!("WeeApp {}", version));
134 |
135 | let args: Vec = std::env::args().collect();
136 |
137 | // Option -r resets window to default size
138 | let storage = Storage::new();
139 | if args.len() == 2 && args[1] == "-r" {
140 | storage.clear();
141 | }
142 |
143 | // Set app size/position to saved values, or use defaults
144 | // If no position is set the OS decides.
145 | let settings = storage.read();
146 | if let Some(settings) = settings {
147 | main_win.set_size(settings.w, settings.h);
148 | main_win.set_pos(settings.x, settings.y);
149 |
150 | } else {
151 | main_win.set_size(WINDOW_WIDTH, WINDOW_HEIGHT);
152 | }
153 |
154 | main_win.set_color(Color::Gray0);
155 |
156 | main_win.set_icon(Some(PngImage::from_data(WINDOW_ICON).unwrap()));
157 |
158 | // Load all the images for clear/reload buttons
159 | let image_clear = read_image!(CL_BTN1);
160 | let image_clear_click = read_image!(CL_BTN2);
161 | let image_clear_hover = read_image!(CL_BTN3);
162 |
163 | let mut btn_clear = button::Button::default().with_size(50, 50);
164 | btn_clear.set_frame(enums::FrameType::FlatBox);
165 | btn_clear.set_pos(main_win.w() - UNIT_SPACING - 10 - UNIT_SPACING - 10, 0);
166 | btn_clear.set_image(Some(image_clear.clone()));
167 |
168 | let ic = image_clear.clone();
169 | let icc = image_clear_click.clone();
170 | let ich = image_clear_hover.clone();
171 | btn_clear.emit(sender.clone(), Message::Clear);
172 | btn_clear.set_tooltip(CLEAR_TOOLTIP);
173 |
174 | btn_clear.handle(move |b, e| match e {
175 | Event::Enter | Event::Released => {
176 | b.set_image(Some(ich.clone()));
177 | b.redraw();
178 | true
179 | }
180 | Event::Leave => {
181 | b.set_image(Some(ic.clone()));
182 | b.redraw();
183 | true
184 | }
185 | Event::Push => {
186 | b.set_image(Some(icc.clone()));
187 | true
188 | }
189 | _ => false,
190 | });
191 |
192 | let image_reload = read_image!(RL_BTN1);
193 | let image_reload_click = read_image!(RL_BTN2);
194 | let image_reload_hover = read_image!(RL_BTN3);
195 |
196 | let mut btn_reload = button::Button::default().with_size(50, 50);
197 | btn_reload.set_frame(enums::FrameType::FlatBox);
198 | btn_reload.set_pos(main_win.w() - UNIT_SPACING - 10, 0);
199 | btn_reload.set_image(Some(image_reload.clone()));
200 |
201 | let ir = image_reload.clone();
202 | let irc = image_reload_click.clone();
203 | let irh = image_reload_hover.clone();
204 | btn_reload.set_tooltip(RELOAD_TOOLTIP);
205 | btn_reload.emit(sender.clone(), Message::Reload);
206 |
207 | btn_reload.handle(move |b, e| match e {
208 | Event::Enter | Event::Released => {
209 | b.set_image(Some(irh.clone()));
210 | b.redraw();
211 | true
212 | }
213 | Event::Leave => {
214 | b.set_image(Some(ir.clone()));
215 | b.redraw();
216 | true
217 | }
218 | Event::Push => {
219 | b.set_image(Some(irc.clone()));
220 | true
221 | }
222 | _ => false,
223 | });
224 |
225 | let mut scroll = group::Scroll::new(
226 | UNIT_SPACING,
227 | TOP_BAR_HEIGHT,
228 | main_win.w() - 2 * UNIT_SPACING,
229 | main_win.h() - 2 * UNIT_SPACING - 5,
230 | "",
231 | );
232 |
233 | scroll.set_frame(enums::FrameType::BorderBox);
234 | scroll.set_type(group::ScrollType::VerticalAlways);
235 | scroll.make_resizable(false);
236 | scroll.set_color(Color::from_hex(0x2e3436));
237 | scroll.set_scrollbar_size(SCROLL_WIDTH);
238 |
239 | let mut pack = group::Pack::default()
240 | .with_size(scroll.w() - SCROLL_WIDTH, scroll.h())
241 | .center_of(&scroll);
242 |
243 | pack.set_type(group::PackType::Vertical);
244 | pack.set_spacing(2);
245 | pack.set_color(Color::Red);
246 |
247 | pack.end();
248 |
249 | main_win.end();
250 |
251 | // The part that says "Searching" when looking for new switches on the LAN
252 | let mut reloading_frame = frame::Frame::new(
253 | UNIT_SPACING,
254 | main_win.h() - UNIT_SPACING,
255 | 90,
256 | UNIT_SPACING,
257 | "Searching",
258 | );
259 |
260 | reloading_frame.set_label_color(Color::Yellow);
261 | main_win.add(&reloading_frame);
262 |
263 | // The rotating imagee when searching on the LAN
264 | let mut progress_frame = frame::Frame::default().with_size(16, 16);
265 | progress_frame.set_pos(90 + UNIT_SPACING + 5, main_win.h() - 27);
266 |
267 | main_win.add(&progress_frame);
268 |
269 | let mut sc = scroll.clone();
270 | let mut pa = pack.clone();
271 | let mut reload = btn_reload.clone();
272 | let mut clear = btn_clear.clone();
273 | let mut rlfr = reloading_frame.clone();
274 | let mut prfr = progress_frame.clone();
275 | let controller = WeeController::new();
276 |
277 | main_win.handle(move |w, ev| match ev {
278 | // When quitting the App save Windows size/position
279 | Event::Hide => {
280 | let settings = Settings {
281 | x: w.x(),
282 | y: w.y(),
283 | w: w.w(),
284 | h: w.h(),
285 | };
286 | let storage = Storage::new();
287 | storage.write(settings);
288 | true
289 | }
290 |
291 | Event::Show => {
292 | info!("Event::Show");
293 |
294 | //Sometimes the buttons would mysteriously be double height
295 | //Trying this hack to mitigate
296 | for i in 0..pa.children() {
297 | let mut btn = pa.child(i).unwrap();
298 | btn.set_size(pa.w(), BUTTON_HEIGHT);
299 | }
300 |
301 | true
302 | }
303 |
304 | // When resizing the App window, reposition internal elements accordingly
305 | Event::Resize => {
306 | info!("Event::Resize");
307 |
308 | sc.resize(
309 | UNIT_SPACING,
310 | TOP_BAR_HEIGHT,
311 | w.w() - 2 * UNIT_SPACING,
312 | w.h() - 2 * UNIT_SPACING - 5,
313 | );
314 |
315 | pa.resize(sc.x(), sc.y(), sc.w() - SCROLL_WIDTH, sc.h());
316 |
317 | reload.set_size(50, 50);
318 | reload.set_pos(w.w() - UNIT_SPACING - 10, 0);
319 |
320 | clear.set_size(50, 50);
321 | clear.set_pos(w.w() - UNIT_SPACING - 10 - UNIT_SPACING - 10, 0);
322 |
323 | rlfr.set_size(90, UNIT_SPACING);
324 | rlfr.set_pos(UNIT_SPACING, w.h() - UNIT_SPACING);
325 |
326 | prfr.set_size(16, 16);
327 | prfr.set_pos(90 + UNIT_SPACING + 5, w.h() - 27);
328 |
329 | //Sometimes the buttons would mysteriously be the wrong height
330 | //Trying this hack to mitigate
331 | for i in 0..pa.children() {
332 | let mut btn = pa.child(i).unwrap();
333 | btn.set_size(pa.w(), BUTTON_HEIGHT);
334 | }
335 | true
336 | }
337 | _ => false,
338 | });
339 |
340 | main_win.make_resizable(true);
341 |
342 | // Create thread to receive notifications from switches that have changed state
343 | // It will forward the messages to the UI message loop so it can update the button color
344 | let rx = controller.notify();
345 | let sc = sender.clone();
346 | let _ignore = std::thread::Builder::new()
347 | .name("APP_notifiy".to_string())
348 | .spawn(move || {
349 | while let Ok(notification) = rx.recv() {
350 | sc.send(Message::Notification(notification));
351 | }
352 | })
353 | .unwrap();
354 |
355 | main_win.show();
356 |
357 | // Start looking for switches, through the UI message loop
358 | sender.send(Message::StartDiscovery);
359 |
360 | Self {
361 | app,
362 | pack,
363 | scroll,
364 | reloading_frame,
365 | progress_frame,
366 | sender,
367 | receiver,
368 | controller,
369 | discovering: true,
370 | buttons: HashMap::new(),
371 | }
372 | }
373 |
374 | fn show_popup(device: &DeviceInfo, icons: Option>) {
375 | const WIND_WIDTH: i32 = 500;
376 | const WIND_HEIGHT: i32 = 500;
377 | const PADDING: i32 = 15;
378 | const TAB_WIDTH: i32 = WIND_WIDTH - 2 * PADDING;
379 | const TAB_HEIGHT: i32 = WIND_WIDTH - 2 * PADDING;
380 | const GROUP_HEIGHT: i32 = TAB_HEIGHT - 25;
381 |
382 | let mut window = window::Window::default().with_label(&device.friendly_name);
383 | window.set_size(WIND_WIDTH, WIND_HEIGHT);
384 | window.set_icon(Some(PngImage::from_data(WINDOW_ICON).unwrap()));
385 |
386 | let tab = group::Tabs::new(PADDING, PADDING, TAB_WIDTH, TAB_HEIGHT, "");
387 |
388 | let grp1 = group::Group::new(PADDING, PADDING + 25, TAB_WIDTH, GROUP_HEIGHT, "Info\t\t");
389 |
390 | let mut pack = group::Pack::default()
391 | .with_pos(25, 80)
392 | .with_size(grp1.w() - 25, grp1.h() - 150);
393 |
394 | pack.set_type(group::PackType::Vertical);
395 | pack.set_spacing(2);
396 | pack.set_color(Color::Red);
397 | pack.set_spacing(5);
398 |
399 | pack.end();
400 | grp1.end();
401 |
402 | if let Some(icons) = icons {
403 | let mut frame = frame::Frame::default().with_size(200, 200).center_of(&pack);
404 | frame.set_frame(enums::FrameType::FlatBox);
405 | frame.set_color(Color::from_hex(0x2e3436));
406 |
407 | let icon = icons.get(0).unwrap();
408 |
409 | if icon.mimetype == mime::IMAGE_PNG {
410 | if let Ok(img) = PngImage::from_data(icon.data.as_slice()) {
411 | frame.set_image(Some(img));
412 | }
413 | } else if icon.mimetype == mime::IMAGE_JPEG {
414 | if let Ok(img) = image::JpegImage::from_data(icon.data.as_slice()) {
415 | frame.set_image(Some(img));
416 | }
417 | } else if icon.mimetype == mime::IMAGE_GIF {
418 | if let Ok(img) = image::GifImage::from_data(icon.data.as_slice()) {
419 | frame.set_image(Some(img));
420 | }
421 | }
422 | pack.add(&frame);
423 | }
424 |
425 | let spacer = frame::Frame::new(0, 0, 0, 30, " ");
426 | pack.add(&spacer);
427 | build_field!(pack, " Name", &device.friendly_name);
428 | build_field!(pack, " Model", &device.model);
429 | build_field!(pack, " Hostname", &device.hostname);
430 | build_field!(pack, " Location", &device.location);
431 |
432 | use advmac::{MacAddr6, MacAddrFormat};
433 |
434 | if let Ok(mac) = MacAddr6::parse_str(&device.root.device.mac_address) {
435 | let str = mac.format_string(MacAddrFormat::ColonNotation);
436 | build_field!(pack, " MAC Address", &str);
437 | }
438 |
439 | // TAB 2
440 | let grp2 = group::Group::new(PADDING, PADDING + 25, TAB_WIDTH, GROUP_HEIGHT, "Homepage\t");
441 |
442 | let mut scroll = group::Scroll::new(20, 50, grp2.w() - 20, grp2.h() - 40, "");
443 |
444 | scroll.set_frame(enums::FrameType::BorderBox);
445 | scroll.set_type(group::ScrollType::BothAlways);
446 | scroll.make_resizable(false);
447 | scroll.set_color(Color::from_hex(0x2e3436));
448 | scroll.set_scrollbar_size(SCROLL_WIDTH);
449 |
450 | let mut name = output::MultilineOutput::new(10, 35, grp2.w() + 100, grp2.h() * 5, "");
451 | name.set_value(&device.xml);
452 | name.set_color(Color::from_hex(0x2e3436));
453 | name.set_label_font(enums::Font::CourierBold);
454 | name.set_frame(enums::FrameType::FlatBox);
455 | name.set_text_size(14);
456 |
457 | scroll.end();
458 | scroll.scroll_to(0, 0);
459 | scroll.redraw();
460 |
461 | grp2.end();
462 | tab.end();
463 |
464 | window.end();
465 |
466 | window.show();
467 | }
468 |
469 | pub fn run(mut self) {
470 | let mut disc_mpsc: Option> = None;
471 | let mut timeout_handle: Option = None;
472 | let mut animation = AnimatedSvg::new(PROGRESS);
473 | while self.app.wait() {
474 | if let Some(msg) = self.receiver.recv() {
475 | match msg {
476 | // Clear button pressed, forget all switches and clear the UI
477 | Message::Clear => {
478 | if !self.discovering {
479 | info!("Message::Clear");
480 | self.controller.clear(true);
481 | self.buttons.clear();
482 | self.pack.clear();
483 | self.scroll.redraw();
484 | }
485 | }
486 |
487 | // Reload button pressed, query WeeCtrl for devices in Cache and on LAN
488 | Message::Reload => {
489 | if !self.discovering {
490 | info!("Message::Reload");
491 | self.controller.clear(false);
492 | self.buttons.clear();
493 | self.pack.clear();
494 | self.scroll.redraw();
495 | self.sender.send(Message::StartDiscovery);
496 | }
497 | }
498 |
499 | // WeeCtrl have found a switch, create a button for it
500 | Message::AddButton(device) => {
501 | info!(
502 | "Message::AddButton {:?} {:?}",
503 | device.unique_id, device.friendly_name
504 | );
505 | let _ignore = self.controller.subscribe(
506 | &device.unique_id,
507 | SUBSCRIPTION_DURATION,
508 | SUBSCRIPTION_AUTO_RENEW,
509 | CONN_TIMEOUT,
510 | );
511 |
512 | let mut but = button::Button::default()
513 | .with_label(&format!("{}", device.friendly_name));
514 |
515 | but.set_size(self.pack.w(), BUTTON_HEIGHT);
516 | but.set_tooltip(SWITCH_TOOLTIP);
517 |
518 | if device.state == State::On {
519 | but.set_color(BUTTON_ON_COLOR);
520 | } else {
521 | but.set_color(BUTTON_OFF_COLOR);
522 | }
523 |
524 | but.handle(move |b, e| match e {
525 | Event::Enter => {
526 | b.set_frame(enums::FrameType::DiamondUpBox);
527 | b.redraw();
528 | true
529 | }
530 | Event::Leave => {
531 | b.set_frame(enums::FrameType::UpBox);
532 | b.redraw();
533 | true
534 | }
535 | _ => false,
536 | });
537 |
538 | self.buttons.insert(device.unique_id.clone(), but.clone());
539 | but.emit(self.sender.clone(), Message::Clicked(device));
540 |
541 | self.pack.add(&but);
542 |
543 | self.scroll.scroll_to(0, 0);
544 | self.app.redraw();
545 | }
546 |
547 | // A button was clicked, flip state of the switch and update the color
548 | // of the button according to returned result if it worked.
549 | Message::Clicked(device) => {
550 | info!(
551 | "Message::Clicked MB({:?}) {:?} {:?}",
552 | app::event_mouse_button(),
553 | device.unique_id,
554 | device.friendly_name
555 | );
556 |
557 | if let Some(btn) = self.buttons.get_mut(&device.unique_id) {
558 | if app::event_mouse_button() == MouseButton::Right {
559 | let mut icons: Option> = None;
560 | if let Ok(res) =
561 | self.controller.get_icons(&device.unique_id, CONN_TIMEOUT)
562 | {
563 | if res.len() > 0 {
564 | icons = Some(res);
565 | }
566 | }
567 | Self::show_popup(&device, icons);
568 | } else {
569 | let state = if btn.color() == BUTTON_ON_COLOR {
570 | State::Off
571 | } else {
572 | State::On
573 | };
574 |
575 | if let Ok(ret_state) = self.controller.set_binary_state(
576 | &device.unique_id,
577 | state,
578 | CONN_TIMEOUT,
579 | ) {
580 | if ret_state == State::On {
581 | btn.set_color(BUTTON_ON_COLOR);
582 | } else {
583 | btn.set_color(BUTTON_OFF_COLOR);
584 | }
585 | }
586 | }
587 | }
588 | }
589 |
590 | // Display "Searching..." message and setup a thread to receive
591 | // switches found, the thread will forward them to the UI message loop
592 | // as Message::AddButton(Device) and end after mx seconds when the channel is closed.
593 | Message::StartDiscovery => {
594 | info!("Message::StartDiscovery");
595 | self.discovering = true;
596 | self.reloading_frame.set_label("Searching");
597 | self.reloading_frame.show();
598 |
599 | disc_mpsc = Some(self.controller.discover(
600 | DiscoveryMode::CacheAndBroadcast,
601 | false,
602 | DISCOVERY_MX,
603 | ));
604 |
605 | // Start polling mechanism and UI spinner
606 | let s = self.sender.clone();
607 | timeout_handle = Some(app::add_timeout3(0.05, move |_| {
608 | s.send(Message::PollDiscovery)
609 | }));
610 | }
611 |
612 | // Since we animate the spinner by rotating it at 30fps we might as well
613 | // just poll the discovery future mpsc here.
614 | // When the future returns None we can move to EndDiscovery.
615 | Message::PollDiscovery => {
616 | //info!("Message::PollDiscovery");
617 | if let Some(d) = disc_mpsc.as_mut() {
618 | // Animate spinner
619 | animation.rotate(4);
620 | let mut img = animation.to_svg_image();
621 | img.scale(21, 21, true, true);
622 | self.progress_frame.set_image(Some(img));
623 | self.progress_frame.hide();
624 | self.progress_frame.show();
625 | self.progress_frame.redraw();
626 |
627 | // poll again in 33 ms
628 | if let Some(h) = timeout_handle {
629 | app::repeat_timeout3(0.033, h);
630 | }
631 |
632 | // Check channel for news or disconnection(discovery finished)
633 | match d.try_recv() {
634 | Ok(d) => self.sender.send(Message::AddButton(d)),
635 | Err(TryRecvError::Disconnected) => {
636 | info!("Ending discovery");
637 | if let Some(h) = timeout_handle {
638 | app::remove_timeout3(h);
639 | }
640 | self.sender.send(Message::EndDiscovery);
641 | }
642 | Err(TryRecvError::Empty) => (),
643 | };
644 | }
645 | }
646 |
647 | // Discovery phase ended, update UI accordingly.
648 | Message::EndDiscovery => {
649 | info!("Message::EndDiscovery");
650 | self.discovering = false;
651 | self.progress_frame.hide();
652 | self.reloading_frame.hide();
653 | }
654 |
655 | // A switch have changed state, update the UI accordingly.
656 | Message::Notification(n) => {
657 | info!("Message::Notification: {:?} {:?}", n.unique_id, n.state);
658 |
659 | if let Some(btn) = self.buttons.get_mut(&n.unique_id) {
660 | if n.state == State::On {
661 | btn.set_color(BUTTON_ON_COLOR);
662 | } else {
663 | btn.set_color(BUTTON_OFF_COLOR);
664 | }
665 |
666 | self.app.redraw();
667 | }
668 | }
669 | }
670 | }
671 | }
672 | }
673 | }
674 |
675 | struct AnimatedSvg {
676 | // Svg file with a single rotate(000,x,y) instruction
677 | svg: String,
678 | // Current degrees of rotation
679 | position: u32,
680 | // Location of the hardcoded "000" in "rotate(000" to modify
681 | range: core::ops::Range,
682 | }
683 |
684 | impl AnimatedSvg {
685 | pub fn new(data: &str) -> Self {
686 | let location = data.find("rotate(000").unwrap() + 7;
687 | AnimatedSvg {
688 | svg: data.to_string(),
689 | position: 0,
690 | range: location..location + 3,
691 | }
692 | }
693 |
694 | // Rotate by current position + degrees delta.
695 | // Rotates the svg by modifying the svg data in place
696 | // much faster than string replace but unsafe
697 | // the constructor would have panicked at find(..).unwrap() if it wasn't possible
698 | pub fn rotate(&mut self, degrees: u32) {
699 | self.position += degrees;
700 | if self.position > 360 {
701 | self.position = 0
702 | }
703 | let do_rotate = format!("{:03}", self.position);
704 | let range = self.range.clone();
705 | unsafe {
706 | core::ptr::copy_nonoverlapping(do_rotate.as_ptr(), self.svg[range].as_mut_ptr(), 3);
707 | }
708 | }
709 |
710 | fn to_svg_image(&self) -> SvgImage {
711 | SvgImage::from_data(&self.svg).unwrap()
712 | }
713 | }
714 |
715 | #[derive(Clone, Debug, Serialize, Deserialize)]
716 | pub struct Settings {
717 | x: i32,
718 | y: i32,
719 | w: i32,
720 | h: i32,
721 | }
722 |
723 | pub struct Storage {
724 | cache_file: Option,
725 | }
726 |
727 | impl Default for Storage {
728 | fn default() -> Self {
729 | Self::new()
730 | }
731 | }
732 |
733 | impl Storage {
734 | #[must_use]
735 | pub fn new() -> Self {
736 | let mut file_path: Option = None;
737 |
738 | if let Some(proj_dirs) = ProjectDirs::from("", "", "WeeApp") {
739 | let mut path = proj_dirs.config_dir().to_path_buf();
740 | path.push(SETTINGS_FILE);
741 | file_path = Some(path);
742 | info!("Settings file: {:#?}", file_path);
743 | }
744 | Self {
745 | cache_file: file_path,
746 | }
747 | }
748 |
749 | /// Write data to cache, errors ignored
750 | pub fn write(&self, settings: Settings) {
751 | info!("Saving settings: {:#?}", settings);
752 |
753 | if let Some(ref fpath) = self.cache_file {
754 | let data = settings;
755 | if let Some(prefix) = fpath.parent() {
756 | let _ignore = std::fs::create_dir_all(prefix);
757 |
758 | if let Ok(serialized) = serde_json::to_string(&data) {
759 | if let Ok(mut buffer) = File::create(fpath) {
760 | let _ignore = buffer.write_all(&serialized.into_bytes());
761 | }
762 | }
763 | }
764 | }
765 | }
766 |
767 | pub fn read(&self) -> Option {
768 | if let Some(ref fpath) = self.cache_file {
769 | if let Ok(mut file) = File::open(fpath) {
770 | let mut s = String::new();
771 | let _ignore = file.read_to_string(&mut s);
772 | let data: Option = serde_json::from_str(&s).ok();
773 | info!("read settings: {:#?}", data);
774 | return data;
775 | }
776 | }
777 | None
778 | }
779 |
780 | pub fn clear(&self) {
781 | if let Some(ref fpath) = self.cache_file {
782 | let _ignore = std::fs::remove_file(fpath);
783 | }
784 | }
785 | }
786 |
787 | fn main() {
788 | use tracing_subscriber::fmt::time;
789 |
790 | tracing_subscriber::fmt()
791 | .with_timer(time::LocalTime::rfc_3339())
792 | .init();
793 |
794 | WeeApp::new().run();
795 | }
796 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "advmac"
7 | version = "1.0.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "8323b66cda4fced4580b2e9f39882f3bbfee49a78e7308b426bd15b58eb2ba28"
10 | dependencies = [
11 | "arrayvec",
12 | "rand",
13 | "serde",
14 | ]
15 |
16 | [[package]]
17 | name = "ansi_term"
18 | version = "0.12.1"
19 | source = "registry+https://github.com/rust-lang/crates.io-index"
20 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
21 | dependencies = [
22 | "winapi",
23 | ]
24 |
25 | [[package]]
26 | name = "arrayvec"
27 | version = "0.7.2"
28 | source = "registry+https://github.com/rust-lang/crates.io-index"
29 | checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
30 | dependencies = [
31 | "serde",
32 | ]
33 |
34 | [[package]]
35 | name = "autocfg"
36 | version = "1.1.0"
37 | source = "registry+https://github.com/rust-lang/crates.io-index"
38 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
39 |
40 | [[package]]
41 | name = "base64"
42 | version = "0.13.0"
43 | source = "registry+https://github.com/rust-lang/crates.io-index"
44 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
45 |
46 | [[package]]
47 | name = "bitflags"
48 | version = "1.3.2"
49 | source = "registry+https://github.com/rust-lang/crates.io-index"
50 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
51 |
52 | [[package]]
53 | name = "bitflags"
54 | version = "2.4.1"
55 | source = "registry+https://github.com/rust-lang/crates.io-index"
56 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
57 |
58 | [[package]]
59 | name = "bumpalo"
60 | version = "3.11.0"
61 | source = "registry+https://github.com/rust-lang/crates.io-index"
62 | checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
63 |
64 | [[package]]
65 | name = "bytes"
66 | version = "1.2.1"
67 | source = "registry+https://github.com/rust-lang/crates.io-index"
68 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
69 |
70 | [[package]]
71 | name = "cc"
72 | version = "1.2.26"
73 | source = "registry+https://github.com/rust-lang/crates.io-index"
74 | checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
75 | dependencies = [
76 | "shlex",
77 | ]
78 |
79 | [[package]]
80 | name = "cfg-if"
81 | version = "1.0.0"
82 | source = "registry+https://github.com/rust-lang/crates.io-index"
83 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
84 |
85 | [[package]]
86 | name = "cmake"
87 | version = "0.1.54"
88 | source = "registry+https://github.com/rust-lang/crates.io-index"
89 | checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
90 | dependencies = [
91 | "cc",
92 | ]
93 |
94 | [[package]]
95 | name = "cmk"
96 | version = "0.1.2"
97 | source = "registry+https://github.com/rust-lang/crates.io-index"
98 | checksum = "8fd5de2a10e31b3ec3e8d75e7ccf8281ab3ee55de68f7ab6ffa9e21be8d82f22"
99 |
100 | [[package]]
101 | name = "core-foundation"
102 | version = "0.9.3"
103 | source = "registry+https://github.com/rust-lang/crates.io-index"
104 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
105 | dependencies = [
106 | "core-foundation-sys",
107 | "libc",
108 | ]
109 |
110 | [[package]]
111 | name = "core-foundation-sys"
112 | version = "0.8.3"
113 | source = "registry+https://github.com/rust-lang/crates.io-index"
114 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
115 |
116 | [[package]]
117 | name = "crossbeam-channel"
118 | version = "0.5.6"
119 | source = "registry+https://github.com/rust-lang/crates.io-index"
120 | checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
121 | dependencies = [
122 | "cfg-if",
123 | "crossbeam-utils",
124 | ]
125 |
126 | [[package]]
127 | name = "crossbeam-utils"
128 | version = "0.8.11"
129 | source = "registry+https://github.com/rust-lang/crates.io-index"
130 | checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
131 | dependencies = [
132 | "cfg-if",
133 | "once_cell",
134 | ]
135 |
136 | [[package]]
137 | name = "directories"
138 | version = "5.0.1"
139 | source = "registry+https://github.com/rust-lang/crates.io-index"
140 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
141 | dependencies = [
142 | "dirs-sys",
143 | ]
144 |
145 | [[package]]
146 | name = "dirs-sys"
147 | version = "0.4.1"
148 | source = "registry+https://github.com/rust-lang/crates.io-index"
149 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
150 | dependencies = [
151 | "libc",
152 | "option-ext",
153 | "redox_users",
154 | "windows-sys 0.48.0",
155 | ]
156 |
157 | [[package]]
158 | name = "encoding_rs"
159 | version = "0.8.31"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
162 | dependencies = [
163 | "cfg-if",
164 | ]
165 |
166 | [[package]]
167 | name = "fastrand"
168 | version = "1.8.0"
169 | source = "registry+https://github.com/rust-lang/crates.io-index"
170 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
171 | dependencies = [
172 | "instant",
173 | ]
174 |
175 | [[package]]
176 | name = "fltk"
177 | version = "1.5.9"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | checksum = "ecfe1d97c0be7585b46d52c1e563226a6495a993be2c72dcdd03fe79d29b1f2a"
180 | dependencies = [
181 | "bitflags 2.4.1",
182 | "crossbeam-channel",
183 | "fltk-sys",
184 | "once_cell",
185 | "paste",
186 | "ttf-parser",
187 | ]
188 |
189 | [[package]]
190 | name = "fltk-sys"
191 | version = "1.5.9"
192 | source = "registry+https://github.com/rust-lang/crates.io-index"
193 | checksum = "de39f27a66d3ceefca611ee2db890369c6846fde4de7b5fd7cdedd9ce0c432f8"
194 | dependencies = [
195 | "cmake",
196 | "cmk",
197 | ]
198 |
199 | [[package]]
200 | name = "fltk-theme"
201 | version = "0.7.2"
202 | source = "registry+https://github.com/rust-lang/crates.io-index"
203 | checksum = "3539d27a815514b56af2afa6b8e7c6d6b9274a103239487d5a60daa6340a4868"
204 | dependencies = [
205 | "fltk",
206 | ]
207 |
208 | [[package]]
209 | name = "fnv"
210 | version = "1.0.7"
211 | source = "registry+https://github.com/rust-lang/crates.io-index"
212 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
213 |
214 | [[package]]
215 | name = "foreign-types"
216 | version = "0.3.2"
217 | source = "registry+https://github.com/rust-lang/crates.io-index"
218 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
219 | dependencies = [
220 | "foreign-types-shared",
221 | ]
222 |
223 | [[package]]
224 | name = "foreign-types-shared"
225 | version = "0.1.1"
226 | source = "registry+https://github.com/rust-lang/crates.io-index"
227 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
228 |
229 | [[package]]
230 | name = "form_urlencoded"
231 | version = "1.1.0"
232 | source = "registry+https://github.com/rust-lang/crates.io-index"
233 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
234 | dependencies = [
235 | "percent-encoding",
236 | ]
237 |
238 | [[package]]
239 | name = "futures"
240 | version = "0.3.24"
241 | source = "registry+https://github.com/rust-lang/crates.io-index"
242 | checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
243 | dependencies = [
244 | "futures-channel",
245 | "futures-core",
246 | "futures-executor",
247 | "futures-io",
248 | "futures-sink",
249 | "futures-task",
250 | "futures-util",
251 | ]
252 |
253 | [[package]]
254 | name = "futures-channel"
255 | version = "0.3.24"
256 | source = "registry+https://github.com/rust-lang/crates.io-index"
257 | checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
258 | dependencies = [
259 | "futures-core",
260 | "futures-sink",
261 | ]
262 |
263 | [[package]]
264 | name = "futures-core"
265 | version = "0.3.24"
266 | source = "registry+https://github.com/rust-lang/crates.io-index"
267 | checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
268 |
269 | [[package]]
270 | name = "futures-executor"
271 | version = "0.3.24"
272 | source = "registry+https://github.com/rust-lang/crates.io-index"
273 | checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
274 | dependencies = [
275 | "futures-core",
276 | "futures-task",
277 | "futures-util",
278 | ]
279 |
280 | [[package]]
281 | name = "futures-io"
282 | version = "0.3.24"
283 | source = "registry+https://github.com/rust-lang/crates.io-index"
284 | checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
285 |
286 | [[package]]
287 | name = "futures-macro"
288 | version = "0.3.24"
289 | source = "registry+https://github.com/rust-lang/crates.io-index"
290 | checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
291 | dependencies = [
292 | "proc-macro2",
293 | "quote",
294 | "syn",
295 | ]
296 |
297 | [[package]]
298 | name = "futures-sink"
299 | version = "0.3.24"
300 | source = "registry+https://github.com/rust-lang/crates.io-index"
301 | checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
302 |
303 | [[package]]
304 | name = "futures-task"
305 | version = "0.3.24"
306 | source = "registry+https://github.com/rust-lang/crates.io-index"
307 | checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
308 |
309 | [[package]]
310 | name = "futures-util"
311 | version = "0.3.24"
312 | source = "registry+https://github.com/rust-lang/crates.io-index"
313 | checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
314 | dependencies = [
315 | "futures-channel",
316 | "futures-core",
317 | "futures-io",
318 | "futures-macro",
319 | "futures-sink",
320 | "futures-task",
321 | "memchr",
322 | "pin-project-lite",
323 | "pin-utils",
324 | "slab",
325 | ]
326 |
327 | [[package]]
328 | name = "getrandom"
329 | version = "0.2.7"
330 | source = "registry+https://github.com/rust-lang/crates.io-index"
331 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
332 | dependencies = [
333 | "cfg-if",
334 | "libc",
335 | "wasi",
336 | ]
337 |
338 | [[package]]
339 | name = "h2"
340 | version = "0.3.14"
341 | source = "registry+https://github.com/rust-lang/crates.io-index"
342 | checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be"
343 | dependencies = [
344 | "bytes",
345 | "fnv",
346 | "futures-core",
347 | "futures-sink",
348 | "futures-util",
349 | "http",
350 | "indexmap",
351 | "slab",
352 | "tokio",
353 | "tokio-util",
354 | "tracing",
355 | ]
356 |
357 | [[package]]
358 | name = "hashbrown"
359 | version = "0.12.3"
360 | source = "registry+https://github.com/rust-lang/crates.io-index"
361 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
362 |
363 | [[package]]
364 | name = "hermit-abi"
365 | version = "0.1.19"
366 | source = "registry+https://github.com/rust-lang/crates.io-index"
367 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
368 | dependencies = [
369 | "libc",
370 | ]
371 |
372 | [[package]]
373 | name = "http"
374 | version = "0.2.8"
375 | source = "registry+https://github.com/rust-lang/crates.io-index"
376 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
377 | dependencies = [
378 | "bytes",
379 | "fnv",
380 | "itoa",
381 | ]
382 |
383 | [[package]]
384 | name = "http-body"
385 | version = "0.4.5"
386 | source = "registry+https://github.com/rust-lang/crates.io-index"
387 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
388 | dependencies = [
389 | "bytes",
390 | "http",
391 | "pin-project-lite",
392 | ]
393 |
394 | [[package]]
395 | name = "httparse"
396 | version = "1.8.0"
397 | source = "registry+https://github.com/rust-lang/crates.io-index"
398 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
399 |
400 | [[package]]
401 | name = "httpdate"
402 | version = "1.0.2"
403 | source = "registry+https://github.com/rust-lang/crates.io-index"
404 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
405 |
406 | [[package]]
407 | name = "hyper"
408 | version = "0.14.20"
409 | source = "registry+https://github.com/rust-lang/crates.io-index"
410 | checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
411 | dependencies = [
412 | "bytes",
413 | "futures-channel",
414 | "futures-core",
415 | "futures-util",
416 | "h2",
417 | "http",
418 | "http-body",
419 | "httparse",
420 | "httpdate",
421 | "itoa",
422 | "pin-project-lite",
423 | "socket2",
424 | "tokio",
425 | "tower-service",
426 | "tracing",
427 | "want",
428 | ]
429 |
430 | [[package]]
431 | name = "hyper-tls"
432 | version = "0.5.0"
433 | source = "registry+https://github.com/rust-lang/crates.io-index"
434 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
435 | dependencies = [
436 | "bytes",
437 | "hyper",
438 | "native-tls",
439 | "tokio",
440 | "tokio-native-tls",
441 | ]
442 |
443 | [[package]]
444 | name = "idna"
445 | version = "0.3.0"
446 | source = "registry+https://github.com/rust-lang/crates.io-index"
447 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
448 | dependencies = [
449 | "unicode-bidi",
450 | "unicode-normalization",
451 | ]
452 |
453 | [[package]]
454 | name = "indexmap"
455 | version = "1.9.1"
456 | source = "registry+https://github.com/rust-lang/crates.io-index"
457 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
458 | dependencies = [
459 | "autocfg",
460 | "hashbrown",
461 | ]
462 |
463 | [[package]]
464 | name = "instant"
465 | version = "0.1.12"
466 | source = "registry+https://github.com/rust-lang/crates.io-index"
467 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
468 | dependencies = [
469 | "cfg-if",
470 | ]
471 |
472 | [[package]]
473 | name = "ipnet"
474 | version = "2.5.0"
475 | source = "registry+https://github.com/rust-lang/crates.io-index"
476 | checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
477 |
478 | [[package]]
479 | name = "itoa"
480 | version = "1.0.3"
481 | source = "registry+https://github.com/rust-lang/crates.io-index"
482 | checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
483 |
484 | [[package]]
485 | name = "js-sys"
486 | version = "0.3.60"
487 | source = "registry+https://github.com/rust-lang/crates.io-index"
488 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
489 | dependencies = [
490 | "wasm-bindgen",
491 | ]
492 |
493 | [[package]]
494 | name = "lazy_static"
495 | version = "1.4.0"
496 | source = "registry+https://github.com/rust-lang/crates.io-index"
497 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
498 |
499 | [[package]]
500 | name = "libc"
501 | version = "0.2.132"
502 | source = "registry+https://github.com/rust-lang/crates.io-index"
503 | checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
504 |
505 | [[package]]
506 | name = "lock_api"
507 | version = "0.4.9"
508 | source = "registry+https://github.com/rust-lang/crates.io-index"
509 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
510 | dependencies = [
511 | "autocfg",
512 | "scopeguard",
513 | ]
514 |
515 | [[package]]
516 | name = "log"
517 | version = "0.4.17"
518 | source = "registry+https://github.com/rust-lang/crates.io-index"
519 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
520 | dependencies = [
521 | "cfg-if",
522 | ]
523 |
524 | [[package]]
525 | name = "memchr"
526 | version = "2.5.0"
527 | source = "registry+https://github.com/rust-lang/crates.io-index"
528 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
529 |
530 | [[package]]
531 | name = "mime"
532 | version = "0.3.16"
533 | source = "registry+https://github.com/rust-lang/crates.io-index"
534 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
535 |
536 | [[package]]
537 | name = "mio"
538 | version = "0.8.4"
539 | source = "registry+https://github.com/rust-lang/crates.io-index"
540 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
541 | dependencies = [
542 | "libc",
543 | "log",
544 | "wasi",
545 | "windows-sys 0.36.1",
546 | ]
547 |
548 | [[package]]
549 | name = "native-tls"
550 | version = "0.2.10"
551 | source = "registry+https://github.com/rust-lang/crates.io-index"
552 | checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
553 | dependencies = [
554 | "lazy_static",
555 | "libc",
556 | "log",
557 | "openssl",
558 | "openssl-probe",
559 | "openssl-sys",
560 | "schannel",
561 | "security-framework",
562 | "security-framework-sys",
563 | "tempfile",
564 | ]
565 |
566 | [[package]]
567 | name = "num_cpus"
568 | version = "1.13.1"
569 | source = "registry+https://github.com/rust-lang/crates.io-index"
570 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
571 | dependencies = [
572 | "hermit-abi",
573 | "libc",
574 | ]
575 |
576 | [[package]]
577 | name = "num_threads"
578 | version = "0.1.6"
579 | source = "registry+https://github.com/rust-lang/crates.io-index"
580 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
581 | dependencies = [
582 | "libc",
583 | ]
584 |
585 | [[package]]
586 | name = "once_cell"
587 | version = "1.14.0"
588 | source = "registry+https://github.com/rust-lang/crates.io-index"
589 | checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
590 |
591 | [[package]]
592 | name = "openssl"
593 | version = "0.10.41"
594 | source = "registry+https://github.com/rust-lang/crates.io-index"
595 | checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0"
596 | dependencies = [
597 | "bitflags 1.3.2",
598 | "cfg-if",
599 | "foreign-types",
600 | "libc",
601 | "once_cell",
602 | "openssl-macros",
603 | "openssl-sys",
604 | ]
605 |
606 | [[package]]
607 | name = "openssl-macros"
608 | version = "0.1.0"
609 | source = "registry+https://github.com/rust-lang/crates.io-index"
610 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
611 | dependencies = [
612 | "proc-macro2",
613 | "quote",
614 | "syn",
615 | ]
616 |
617 | [[package]]
618 | name = "openssl-probe"
619 | version = "0.1.5"
620 | source = "registry+https://github.com/rust-lang/crates.io-index"
621 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
622 |
623 | [[package]]
624 | name = "openssl-sys"
625 | version = "0.9.75"
626 | source = "registry+https://github.com/rust-lang/crates.io-index"
627 | checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f"
628 | dependencies = [
629 | "autocfg",
630 | "cc",
631 | "libc",
632 | "pkg-config",
633 | "vcpkg",
634 | ]
635 |
636 | [[package]]
637 | name = "option-ext"
638 | version = "0.2.0"
639 | source = "registry+https://github.com/rust-lang/crates.io-index"
640 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
641 |
642 | [[package]]
643 | name = "parking_lot"
644 | version = "0.12.1"
645 | source = "registry+https://github.com/rust-lang/crates.io-index"
646 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
647 | dependencies = [
648 | "lock_api",
649 | "parking_lot_core",
650 | ]
651 |
652 | [[package]]
653 | name = "parking_lot_core"
654 | version = "0.9.3"
655 | source = "registry+https://github.com/rust-lang/crates.io-index"
656 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
657 | dependencies = [
658 | "cfg-if",
659 | "libc",
660 | "redox_syscall",
661 | "smallvec",
662 | "windows-sys 0.36.1",
663 | ]
664 |
665 | [[package]]
666 | name = "paste"
667 | version = "1.0.9"
668 | source = "registry+https://github.com/rust-lang/crates.io-index"
669 | checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
670 |
671 | [[package]]
672 | name = "percent-encoding"
673 | version = "2.2.0"
674 | source = "registry+https://github.com/rust-lang/crates.io-index"
675 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
676 |
677 | [[package]]
678 | name = "pin-project-lite"
679 | version = "0.2.9"
680 | source = "registry+https://github.com/rust-lang/crates.io-index"
681 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
682 |
683 | [[package]]
684 | name = "pin-utils"
685 | version = "0.1.0"
686 | source = "registry+https://github.com/rust-lang/crates.io-index"
687 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
688 |
689 | [[package]]
690 | name = "pkg-config"
691 | version = "0.3.25"
692 | source = "registry+https://github.com/rust-lang/crates.io-index"
693 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
694 |
695 | [[package]]
696 | name = "proc-macro2"
697 | version = "1.0.43"
698 | source = "registry+https://github.com/rust-lang/crates.io-index"
699 | checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
700 | dependencies = [
701 | "unicode-ident",
702 | ]
703 |
704 | [[package]]
705 | name = "quote"
706 | version = "1.0.21"
707 | source = "registry+https://github.com/rust-lang/crates.io-index"
708 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
709 | dependencies = [
710 | "proc-macro2",
711 | ]
712 |
713 | [[package]]
714 | name = "rand"
715 | version = "0.8.5"
716 | source = "registry+https://github.com/rust-lang/crates.io-index"
717 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
718 | dependencies = [
719 | "rand_core",
720 | ]
721 |
722 | [[package]]
723 | name = "rand_core"
724 | version = "0.6.4"
725 | source = "registry+https://github.com/rust-lang/crates.io-index"
726 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
727 | dependencies = [
728 | "getrandom",
729 | ]
730 |
731 | [[package]]
732 | name = "redox_syscall"
733 | version = "0.2.16"
734 | source = "registry+https://github.com/rust-lang/crates.io-index"
735 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
736 | dependencies = [
737 | "bitflags 1.3.2",
738 | ]
739 |
740 | [[package]]
741 | name = "redox_users"
742 | version = "0.4.3"
743 | source = "registry+https://github.com/rust-lang/crates.io-index"
744 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
745 | dependencies = [
746 | "getrandom",
747 | "redox_syscall",
748 | "thiserror",
749 | ]
750 |
751 | [[package]]
752 | name = "remove_dir_all"
753 | version = "0.5.3"
754 | source = "registry+https://github.com/rust-lang/crates.io-index"
755 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
756 | dependencies = [
757 | "winapi",
758 | ]
759 |
760 | [[package]]
761 | name = "reqwest"
762 | version = "0.11.11"
763 | source = "registry+https://github.com/rust-lang/crates.io-index"
764 | checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92"
765 | dependencies = [
766 | "base64",
767 | "bytes",
768 | "encoding_rs",
769 | "futures-core",
770 | "futures-util",
771 | "h2",
772 | "http",
773 | "http-body",
774 | "hyper",
775 | "hyper-tls",
776 | "ipnet",
777 | "js-sys",
778 | "lazy_static",
779 | "log",
780 | "mime",
781 | "native-tls",
782 | "percent-encoding",
783 | "pin-project-lite",
784 | "serde",
785 | "serde_json",
786 | "serde_urlencoded",
787 | "tokio",
788 | "tokio-native-tls",
789 | "tower-service",
790 | "url",
791 | "wasm-bindgen",
792 | "wasm-bindgen-futures",
793 | "web-sys",
794 | "winreg",
795 | ]
796 |
797 | [[package]]
798 | name = "ryu"
799 | version = "1.0.11"
800 | source = "registry+https://github.com/rust-lang/crates.io-index"
801 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
802 |
803 | [[package]]
804 | name = "schannel"
805 | version = "0.1.20"
806 | source = "registry+https://github.com/rust-lang/crates.io-index"
807 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
808 | dependencies = [
809 | "lazy_static",
810 | "windows-sys 0.36.1",
811 | ]
812 |
813 | [[package]]
814 | name = "scopeguard"
815 | version = "1.1.0"
816 | source = "registry+https://github.com/rust-lang/crates.io-index"
817 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
818 |
819 | [[package]]
820 | name = "security-framework"
821 | version = "2.7.0"
822 | source = "registry+https://github.com/rust-lang/crates.io-index"
823 | checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
824 | dependencies = [
825 | "bitflags 1.3.2",
826 | "core-foundation",
827 | "core-foundation-sys",
828 | "libc",
829 | "security-framework-sys",
830 | ]
831 |
832 | [[package]]
833 | name = "security-framework-sys"
834 | version = "2.6.1"
835 | source = "registry+https://github.com/rust-lang/crates.io-index"
836 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
837 | dependencies = [
838 | "core-foundation-sys",
839 | "libc",
840 | ]
841 |
842 | [[package]]
843 | name = "serde"
844 | version = "1.0.145"
845 | source = "registry+https://github.com/rust-lang/crates.io-index"
846 | checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
847 | dependencies = [
848 | "serde_derive",
849 | ]
850 |
851 | [[package]]
852 | name = "serde-xml-rs"
853 | version = "0.6.0"
854 | source = "registry+https://github.com/rust-lang/crates.io-index"
855 | checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782"
856 | dependencies = [
857 | "log",
858 | "serde",
859 | "thiserror",
860 | "xml-rs",
861 | ]
862 |
863 | [[package]]
864 | name = "serde_derive"
865 | version = "1.0.145"
866 | source = "registry+https://github.com/rust-lang/crates.io-index"
867 | checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
868 | dependencies = [
869 | "proc-macro2",
870 | "quote",
871 | "syn",
872 | ]
873 |
874 | [[package]]
875 | name = "serde_json"
876 | version = "1.0.85"
877 | source = "registry+https://github.com/rust-lang/crates.io-index"
878 | checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
879 | dependencies = [
880 | "itoa",
881 | "ryu",
882 | "serde",
883 | ]
884 |
885 | [[package]]
886 | name = "serde_urlencoded"
887 | version = "0.7.1"
888 | source = "registry+https://github.com/rust-lang/crates.io-index"
889 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
890 | dependencies = [
891 | "form_urlencoded",
892 | "itoa",
893 | "ryu",
894 | "serde",
895 | ]
896 |
897 | [[package]]
898 | name = "sharded-slab"
899 | version = "0.1.4"
900 | source = "registry+https://github.com/rust-lang/crates.io-index"
901 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
902 | dependencies = [
903 | "lazy_static",
904 | ]
905 |
906 | [[package]]
907 | name = "shlex"
908 | version = "1.3.0"
909 | source = "registry+https://github.com/rust-lang/crates.io-index"
910 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
911 |
912 | [[package]]
913 | name = "signal-hook-registry"
914 | version = "1.4.0"
915 | source = "registry+https://github.com/rust-lang/crates.io-index"
916 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
917 | dependencies = [
918 | "libc",
919 | ]
920 |
921 | [[package]]
922 | name = "slab"
923 | version = "0.4.7"
924 | source = "registry+https://github.com/rust-lang/crates.io-index"
925 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
926 | dependencies = [
927 | "autocfg",
928 | ]
929 |
930 | [[package]]
931 | name = "smallvec"
932 | version = "1.9.0"
933 | source = "registry+https://github.com/rust-lang/crates.io-index"
934 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
935 |
936 | [[package]]
937 | name = "socket2"
938 | version = "0.4.7"
939 | source = "registry+https://github.com/rust-lang/crates.io-index"
940 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
941 | dependencies = [
942 | "libc",
943 | "winapi",
944 | ]
945 |
946 | [[package]]
947 | name = "syn"
948 | version = "1.0.100"
949 | source = "registry+https://github.com/rust-lang/crates.io-index"
950 | checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
951 | dependencies = [
952 | "proc-macro2",
953 | "quote",
954 | "unicode-ident",
955 | ]
956 |
957 | [[package]]
958 | name = "tempfile"
959 | version = "3.3.0"
960 | source = "registry+https://github.com/rust-lang/crates.io-index"
961 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
962 | dependencies = [
963 | "cfg-if",
964 | "fastrand",
965 | "libc",
966 | "redox_syscall",
967 | "remove_dir_all",
968 | "winapi",
969 | ]
970 |
971 | [[package]]
972 | name = "thiserror"
973 | version = "1.0.35"
974 | source = "registry+https://github.com/rust-lang/crates.io-index"
975 | checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
976 | dependencies = [
977 | "thiserror-impl",
978 | ]
979 |
980 | [[package]]
981 | name = "thiserror-impl"
982 | version = "1.0.35"
983 | source = "registry+https://github.com/rust-lang/crates.io-index"
984 | checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
985 | dependencies = [
986 | "proc-macro2",
987 | "quote",
988 | "syn",
989 | ]
990 |
991 | [[package]]
992 | name = "thread_local"
993 | version = "1.1.4"
994 | source = "registry+https://github.com/rust-lang/crates.io-index"
995 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
996 | dependencies = [
997 | "once_cell",
998 | ]
999 |
1000 | [[package]]
1001 | name = "time"
1002 | version = "0.3.14"
1003 | source = "registry+https://github.com/rust-lang/crates.io-index"
1004 | checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
1005 | dependencies = [
1006 | "itoa",
1007 | "libc",
1008 | "num_threads",
1009 | ]
1010 |
1011 | [[package]]
1012 | name = "tinyvec"
1013 | version = "1.6.0"
1014 | source = "registry+https://github.com/rust-lang/crates.io-index"
1015 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
1016 | dependencies = [
1017 | "tinyvec_macros",
1018 | ]
1019 |
1020 | [[package]]
1021 | name = "tinyvec_macros"
1022 | version = "0.1.0"
1023 | source = "registry+https://github.com/rust-lang/crates.io-index"
1024 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
1025 |
1026 | [[package]]
1027 | name = "tokio"
1028 | version = "1.21.1"
1029 | source = "registry+https://github.com/rust-lang/crates.io-index"
1030 | checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95"
1031 | dependencies = [
1032 | "autocfg",
1033 | "bytes",
1034 | "libc",
1035 | "memchr",
1036 | "mio",
1037 | "num_cpus",
1038 | "once_cell",
1039 | "parking_lot",
1040 | "pin-project-lite",
1041 | "signal-hook-registry",
1042 | "socket2",
1043 | "tokio-macros",
1044 | "winapi",
1045 | ]
1046 |
1047 | [[package]]
1048 | name = "tokio-macros"
1049 | version = "1.8.0"
1050 | source = "registry+https://github.com/rust-lang/crates.io-index"
1051 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
1052 | dependencies = [
1053 | "proc-macro2",
1054 | "quote",
1055 | "syn",
1056 | ]
1057 |
1058 | [[package]]
1059 | name = "tokio-native-tls"
1060 | version = "0.3.0"
1061 | source = "registry+https://github.com/rust-lang/crates.io-index"
1062 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
1063 | dependencies = [
1064 | "native-tls",
1065 | "tokio",
1066 | ]
1067 |
1068 | [[package]]
1069 | name = "tokio-util"
1070 | version = "0.7.4"
1071 | source = "registry+https://github.com/rust-lang/crates.io-index"
1072 | checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
1073 | dependencies = [
1074 | "bytes",
1075 | "futures-core",
1076 | "futures-sink",
1077 | "pin-project-lite",
1078 | "tokio",
1079 | "tracing",
1080 | ]
1081 |
1082 | [[package]]
1083 | name = "tower-service"
1084 | version = "0.3.2"
1085 | source = "registry+https://github.com/rust-lang/crates.io-index"
1086 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
1087 |
1088 | [[package]]
1089 | name = "tracing"
1090 | version = "0.1.36"
1091 | source = "registry+https://github.com/rust-lang/crates.io-index"
1092 | checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
1093 | dependencies = [
1094 | "cfg-if",
1095 | "pin-project-lite",
1096 | "tracing-attributes",
1097 | "tracing-core",
1098 | ]
1099 |
1100 | [[package]]
1101 | name = "tracing-attributes"
1102 | version = "0.1.22"
1103 | source = "registry+https://github.com/rust-lang/crates.io-index"
1104 | checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
1105 | dependencies = [
1106 | "proc-macro2",
1107 | "quote",
1108 | "syn",
1109 | ]
1110 |
1111 | [[package]]
1112 | name = "tracing-core"
1113 | version = "0.1.29"
1114 | source = "registry+https://github.com/rust-lang/crates.io-index"
1115 | checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7"
1116 | dependencies = [
1117 | "once_cell",
1118 | "valuable",
1119 | ]
1120 |
1121 | [[package]]
1122 | name = "tracing-log"
1123 | version = "0.1.3"
1124 | source = "registry+https://github.com/rust-lang/crates.io-index"
1125 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
1126 | dependencies = [
1127 | "lazy_static",
1128 | "log",
1129 | "tracing-core",
1130 | ]
1131 |
1132 | [[package]]
1133 | name = "tracing-subscriber"
1134 | version = "0.3.15"
1135 | source = "registry+https://github.com/rust-lang/crates.io-index"
1136 | checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b"
1137 | dependencies = [
1138 | "ansi_term",
1139 | "sharded-slab",
1140 | "smallvec",
1141 | "thread_local",
1142 | "time",
1143 | "tracing-core",
1144 | "tracing-log",
1145 | ]
1146 |
1147 | [[package]]
1148 | name = "try-lock"
1149 | version = "0.2.3"
1150 | source = "registry+https://github.com/rust-lang/crates.io-index"
1151 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
1152 |
1153 | [[package]]
1154 | name = "ttf-parser"
1155 | version = "0.25.1"
1156 | source = "registry+https://github.com/rust-lang/crates.io-index"
1157 | checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
1158 |
1159 | [[package]]
1160 | name = "unicode-bidi"
1161 | version = "0.3.8"
1162 | source = "registry+https://github.com/rust-lang/crates.io-index"
1163 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
1164 |
1165 | [[package]]
1166 | name = "unicode-ident"
1167 | version = "1.0.4"
1168 | source = "registry+https://github.com/rust-lang/crates.io-index"
1169 | checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
1170 |
1171 | [[package]]
1172 | name = "unicode-normalization"
1173 | version = "0.1.22"
1174 | source = "registry+https://github.com/rust-lang/crates.io-index"
1175 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
1176 | dependencies = [
1177 | "tinyvec",
1178 | ]
1179 |
1180 | [[package]]
1181 | name = "url"
1182 | version = "2.3.1"
1183 | source = "registry+https://github.com/rust-lang/crates.io-index"
1184 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
1185 | dependencies = [
1186 | "form_urlencoded",
1187 | "idna",
1188 | "percent-encoding",
1189 | ]
1190 |
1191 | [[package]]
1192 | name = "valuable"
1193 | version = "0.1.0"
1194 | source = "registry+https://github.com/rust-lang/crates.io-index"
1195 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
1196 |
1197 | [[package]]
1198 | name = "vcpkg"
1199 | version = "0.2.15"
1200 | source = "registry+https://github.com/rust-lang/crates.io-index"
1201 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
1202 |
1203 | [[package]]
1204 | name = "want"
1205 | version = "0.3.0"
1206 | source = "registry+https://github.com/rust-lang/crates.io-index"
1207 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
1208 | dependencies = [
1209 | "log",
1210 | "try-lock",
1211 | ]
1212 |
1213 | [[package]]
1214 | name = "wasi"
1215 | version = "0.11.0+wasi-snapshot-preview1"
1216 | source = "registry+https://github.com/rust-lang/crates.io-index"
1217 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1218 |
1219 | [[package]]
1220 | name = "wasm-bindgen"
1221 | version = "0.2.83"
1222 | source = "registry+https://github.com/rust-lang/crates.io-index"
1223 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
1224 | dependencies = [
1225 | "cfg-if",
1226 | "wasm-bindgen-macro",
1227 | ]
1228 |
1229 | [[package]]
1230 | name = "wasm-bindgen-backend"
1231 | version = "0.2.83"
1232 | source = "registry+https://github.com/rust-lang/crates.io-index"
1233 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
1234 | dependencies = [
1235 | "bumpalo",
1236 | "log",
1237 | "once_cell",
1238 | "proc-macro2",
1239 | "quote",
1240 | "syn",
1241 | "wasm-bindgen-shared",
1242 | ]
1243 |
1244 | [[package]]
1245 | name = "wasm-bindgen-futures"
1246 | version = "0.4.33"
1247 | source = "registry+https://github.com/rust-lang/crates.io-index"
1248 | checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
1249 | dependencies = [
1250 | "cfg-if",
1251 | "js-sys",
1252 | "wasm-bindgen",
1253 | "web-sys",
1254 | ]
1255 |
1256 | [[package]]
1257 | name = "wasm-bindgen-macro"
1258 | version = "0.2.83"
1259 | source = "registry+https://github.com/rust-lang/crates.io-index"
1260 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
1261 | dependencies = [
1262 | "quote",
1263 | "wasm-bindgen-macro-support",
1264 | ]
1265 |
1266 | [[package]]
1267 | name = "wasm-bindgen-macro-support"
1268 | version = "0.2.83"
1269 | source = "registry+https://github.com/rust-lang/crates.io-index"
1270 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
1271 | dependencies = [
1272 | "proc-macro2",
1273 | "quote",
1274 | "syn",
1275 | "wasm-bindgen-backend",
1276 | "wasm-bindgen-shared",
1277 | ]
1278 |
1279 | [[package]]
1280 | name = "wasm-bindgen-shared"
1281 | version = "0.2.83"
1282 | source = "registry+https://github.com/rust-lang/crates.io-index"
1283 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
1284 |
1285 | [[package]]
1286 | name = "web-sys"
1287 | version = "0.3.60"
1288 | source = "registry+https://github.com/rust-lang/crates.io-index"
1289 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
1290 | dependencies = [
1291 | "js-sys",
1292 | "wasm-bindgen",
1293 | ]
1294 |
1295 | [[package]]
1296 | name = "weectrl"
1297 | version = "2.0.2"
1298 | dependencies = [
1299 | "advmac",
1300 | "directories",
1301 | "fltk",
1302 | "fltk-theme",
1303 | "futures",
1304 | "hyper",
1305 | "mime",
1306 | "reqwest",
1307 | "serde",
1308 | "serde-xml-rs",
1309 | "serde_derive",
1310 | "serde_json",
1311 | "tokio",
1312 | "tracing",
1313 | "tracing-subscriber",
1314 | "url",
1315 | ]
1316 |
1317 | [[package]]
1318 | name = "winapi"
1319 | version = "0.3.9"
1320 | source = "registry+https://github.com/rust-lang/crates.io-index"
1321 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1322 | dependencies = [
1323 | "winapi-i686-pc-windows-gnu",
1324 | "winapi-x86_64-pc-windows-gnu",
1325 | ]
1326 |
1327 | [[package]]
1328 | name = "winapi-i686-pc-windows-gnu"
1329 | version = "0.4.0"
1330 | source = "registry+https://github.com/rust-lang/crates.io-index"
1331 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1332 |
1333 | [[package]]
1334 | name = "winapi-x86_64-pc-windows-gnu"
1335 | version = "0.4.0"
1336 | source = "registry+https://github.com/rust-lang/crates.io-index"
1337 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1338 |
1339 | [[package]]
1340 | name = "windows-sys"
1341 | version = "0.36.1"
1342 | source = "registry+https://github.com/rust-lang/crates.io-index"
1343 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
1344 | dependencies = [
1345 | "windows_aarch64_msvc 0.36.1",
1346 | "windows_i686_gnu 0.36.1",
1347 | "windows_i686_msvc 0.36.1",
1348 | "windows_x86_64_gnu 0.36.1",
1349 | "windows_x86_64_msvc 0.36.1",
1350 | ]
1351 |
1352 | [[package]]
1353 | name = "windows-sys"
1354 | version = "0.48.0"
1355 | source = "registry+https://github.com/rust-lang/crates.io-index"
1356 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
1357 | dependencies = [
1358 | "windows-targets",
1359 | ]
1360 |
1361 | [[package]]
1362 | name = "windows-targets"
1363 | version = "0.48.5"
1364 | source = "registry+https://github.com/rust-lang/crates.io-index"
1365 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
1366 | dependencies = [
1367 | "windows_aarch64_gnullvm",
1368 | "windows_aarch64_msvc 0.48.5",
1369 | "windows_i686_gnu 0.48.5",
1370 | "windows_i686_msvc 0.48.5",
1371 | "windows_x86_64_gnu 0.48.5",
1372 | "windows_x86_64_gnullvm",
1373 | "windows_x86_64_msvc 0.48.5",
1374 | ]
1375 |
1376 | [[package]]
1377 | name = "windows_aarch64_gnullvm"
1378 | version = "0.48.5"
1379 | source = "registry+https://github.com/rust-lang/crates.io-index"
1380 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1381 |
1382 | [[package]]
1383 | name = "windows_aarch64_msvc"
1384 | version = "0.36.1"
1385 | source = "registry+https://github.com/rust-lang/crates.io-index"
1386 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
1387 |
1388 | [[package]]
1389 | name = "windows_aarch64_msvc"
1390 | version = "0.48.5"
1391 | source = "registry+https://github.com/rust-lang/crates.io-index"
1392 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1393 |
1394 | [[package]]
1395 | name = "windows_i686_gnu"
1396 | version = "0.36.1"
1397 | source = "registry+https://github.com/rust-lang/crates.io-index"
1398 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
1399 |
1400 | [[package]]
1401 | name = "windows_i686_gnu"
1402 | version = "0.48.5"
1403 | source = "registry+https://github.com/rust-lang/crates.io-index"
1404 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1405 |
1406 | [[package]]
1407 | name = "windows_i686_msvc"
1408 | version = "0.36.1"
1409 | source = "registry+https://github.com/rust-lang/crates.io-index"
1410 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
1411 |
1412 | [[package]]
1413 | name = "windows_i686_msvc"
1414 | version = "0.48.5"
1415 | source = "registry+https://github.com/rust-lang/crates.io-index"
1416 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1417 |
1418 | [[package]]
1419 | name = "windows_x86_64_gnu"
1420 | version = "0.36.1"
1421 | source = "registry+https://github.com/rust-lang/crates.io-index"
1422 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
1423 |
1424 | [[package]]
1425 | name = "windows_x86_64_gnu"
1426 | version = "0.48.5"
1427 | source = "registry+https://github.com/rust-lang/crates.io-index"
1428 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1429 |
1430 | [[package]]
1431 | name = "windows_x86_64_gnullvm"
1432 | version = "0.48.5"
1433 | source = "registry+https://github.com/rust-lang/crates.io-index"
1434 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1435 |
1436 | [[package]]
1437 | name = "windows_x86_64_msvc"
1438 | version = "0.36.1"
1439 | source = "registry+https://github.com/rust-lang/crates.io-index"
1440 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
1441 |
1442 | [[package]]
1443 | name = "windows_x86_64_msvc"
1444 | version = "0.48.5"
1445 | source = "registry+https://github.com/rust-lang/crates.io-index"
1446 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1447 |
1448 | [[package]]
1449 | name = "winreg"
1450 | version = "0.10.1"
1451 | source = "registry+https://github.com/rust-lang/crates.io-index"
1452 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
1453 | dependencies = [
1454 | "winapi",
1455 | ]
1456 |
1457 | [[package]]
1458 | name = "xml-rs"
1459 | version = "0.8.4"
1460 | source = "registry+https://github.com/rust-lang/crates.io-index"
1461 | checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
1462 |
--------------------------------------------------------------------------------