├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── cli ├── Cargo.toml ├── build.rs └── src │ ├── bt.rs │ ├── cli.rs │ └── main.rs ├── docs └── Notes.md ├── libgfps ├── Cargo.toml ├── examples │ ├── gfps_get_battery.rs │ ├── gfps_listen.rs │ └── ring.rs └── src │ ├── lib.rs │ └── msg │ ├── codec.rs │ ├── mod.rs │ └── types.rs └── libmaestro ├── Cargo.toml ├── build.rs ├── examples ├── common │ └── mod.rs ├── maestro_get_battery.rs ├── maestro_listen.rs ├── maestro_read_settings.rs └── maestro_write_settings.rs ├── proto ├── maestro_pw.proto └── pw.rpc.packet.proto └── src ├── hdlc ├── codec.rs ├── consts.rs ├── crc.rs ├── decoder.rs ├── encoder.rs ├── mod.rs └── varint.rs ├── lib.rs ├── protocol ├── addr.rs ├── codec.rs ├── mod.rs └── utils.rs ├── pwrpc ├── client.rs ├── id.rs ├── mod.rs ├── status.rs ├── types.rs └── utils.rs └── service ├── impls ├── dosimeter.rs ├── maestro.rs ├── mod.rs └── multipoint.rs ├── mod.rs └── settings.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | - feature/ci 9 | pull_request: 10 | branches: 11 | - main 12 | - dev 13 | - feature/ci 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | lint: 20 | name: Clippy 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v3 25 | 26 | - name: Install dependencies 27 | run: | 28 | sudo apt-get -y update 29 | sudo apt-get -y install libdbus-1-dev protobuf-compiler 30 | 31 | - name: Install rust 32 | run: | 33 | rustup update stable && rustup default stable 34 | rustup component add clippy 35 | 36 | - name: Run clippy 37 | run: cargo clippy --all --all-features -- -Dwarnings 38 | 39 | test: 40 | name: Test 41 | runs-on: ubuntu-latest 42 | 43 | strategy: 44 | matrix: 45 | toolchain: [stable, nightly] 46 | 47 | steps: 48 | - name: Checkout code 49 | uses: actions/checkout@v3 50 | 51 | - name: Install dependencies 52 | run: | 53 | sudo apt-get -y update 54 | sudo apt-get -y install libdbus-1-dev protobuf-compiler 55 | 56 | - name: Install rust 57 | run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 58 | 59 | - name: Build 60 | run: | 61 | cargo build --all --all-features 62 | cargo build --all --all-features --examples 63 | 64 | - name: Test 65 | run: | 66 | cargo test --all --all-features 67 | cargo build --all --all-features --examples 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /target 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "cli", 6 | "libgfps", 7 | "libmaestro", 8 | ] 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Maximilian Luz 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `pbpctrl` 2 | 3 | Control Google Pixel Buds Pro from the Linux command line. Might or might not work on other Pixel Buds devices. 4 | 5 | Allows reading of battery, hardware, software, and runtime information as well as reading and changing settings (ANC state, equalizer, ...). 6 | 7 | 8 | ## Installation 9 | 10 | ### Arch Linux 11 | 12 | A [`pbpctrl`](https://aur.archlinux.org/packages/pbpctrl) package is provided via the AUR. 13 | Alternatively, the [`pbpctrl-git`](https://aur.archlinux.org/packages/pbpctrl-git) package can be used to directly build from the latest state on the `main` branch. 14 | 15 | ### Installation via `cargo` 16 | 17 | You will need to install the following dependencies: 18 | 19 | - Ubuntu: `libdbus-1-dev pkg-config protobuf-compiler` 20 | - Arch Linux: Please refer to the dependencies (`depends` and `makedepends` fields) in [this PKGBUILD](https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=pbpctrl). 21 | 22 | To build install the binary via cargo, run 23 | ```sh 24 | cargo install pbpctrl --git https://github.com/qzed/pbpctrl/ 25 | ``` 26 | Use the `--tag` option if you want to install a specific tag instead of the latest `main` branch. 27 | 28 | 29 | ## Instructions 30 | 31 | Pair and connect your Pixel Buds Pro before use. 32 | Run `pbpctrl help` for more information. 33 | 34 | 35 | ## Notes on Battery Information 36 | 37 | The Pixel Buds Pro support basic battery information via the AVCPR standard. 38 | Support for this is still experimental in BlueZ and needs to be enabled manually by editing `/etc/bluetooth/main.conf` and setting 39 | ``` 40 | [General] 41 | Experimental = true 42 | ``` 43 | or by starting BlueZ with the `--experimental` flag. 44 | After this, battery status should be provided via UPower. 45 | 46 | Note that this, however, will only provide a single battery meter for both buds combined, and none for the case. 47 | For more detailed information, use `pbpctrl show battery`. 48 | This also allows reading of the case battery as long as one bud is placed in the case (note that the case does not have a Bluetooth receiver itself). 49 | 50 | 51 | ## License 52 | 53 | Licensed under either of 54 | 55 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 56 | - MIT License ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 57 | 58 | at your option. 59 | 60 | ### Contribution 61 | 62 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 63 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pbpctrl" 3 | authors = ["Maximilian Luz "] 4 | version = "0.1.8" 5 | edition = "2024" 6 | license = "MIT/Apache-2.0" 7 | description = "Command-line utility for controlling Google Pixel Buds Pro" 8 | repository = "https://github.com/qzed/pbpctrl" 9 | 10 | [dependencies] 11 | anyhow = "1.0.98" 12 | bluer = { version = "0.17.3", features = ["bluetoothd", "rfcomm"] } 13 | clap = { version = "4.5.37", features = ["derive"] } 14 | futures = "0.3.31" 15 | maestro = { path = "../libmaestro" } 16 | tokio = { version = "1.44.2", features = ["rt", "macros", "signal"] } 17 | tracing = "0.1.41" 18 | tracing-subscriber = "0.3.19" 19 | 20 | [build-dependencies] 21 | bluer = { version = "0.17.3" } 22 | clap = { version = "4.5.37", features = ["derive"] } 23 | clap_complete = "4.5.47" 24 | maestro = { path = "../libmaestro" } 25 | -------------------------------------------------------------------------------- /cli/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use clap::CommandFactory; 3 | use clap_complete::shells; 4 | 5 | #[allow(dead_code)] 6 | #[path = "src/cli.rs"] 7 | mod cli; 8 | use cli::*; 9 | 10 | 11 | fn main() { 12 | let outdir = env::var_os("CARGO_TARGET_DIR") 13 | .or_else(|| env::var_os("OUT_DIR")) 14 | .unwrap(); 15 | 16 | let mut cmd = Args::command(); 17 | 18 | clap_complete::generate_to(shells::Bash, &mut cmd, "pbpctrl", &outdir).unwrap(); 19 | clap_complete::generate_to(shells::Zsh, &mut cmd, "pbpctrl", &outdir).unwrap(); 20 | clap_complete::generate_to(shells::Fish, &mut cmd, "pbpctrl", &outdir).unwrap(); 21 | } 22 | -------------------------------------------------------------------------------- /cli/src/bt.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | 5 | use bluer::{Adapter, Address, Device, Session}; 6 | use bluer::rfcomm::{ProfileHandle, Role, ReqError, Stream, Profile}; 7 | 8 | use futures::StreamExt; 9 | 10 | 11 | const PIXEL_BUDS_CLASS: u32 = 0x240404; 12 | const PIXEL_BUDS2_CLASS: u32 = 0x244404; 13 | 14 | 15 | pub async fn find_maestro_device(adapter: &Adapter) -> Result { 16 | for addr in adapter.device_addresses().await? { 17 | let dev = adapter.device(addr)?; 18 | 19 | let class = dev.class().await?.unwrap_or(0); 20 | if class != PIXEL_BUDS_CLASS && class != PIXEL_BUDS2_CLASS { 21 | continue; 22 | } 23 | 24 | let uuids = dev.uuids().await?.unwrap_or_default(); 25 | if !uuids.contains(&maestro::UUID) { 26 | continue; 27 | } 28 | 29 | tracing::debug!(address=%addr, "found compatible device"); 30 | return Ok(dev); 31 | } 32 | 33 | tracing::debug!("no compatible device found"); 34 | anyhow::bail!("no compatible device found") 35 | } 36 | 37 | pub async fn connect_maestro_rfcomm(session: &Session, dev: &Device) -> Result { 38 | let maestro_profile = Profile { 39 | uuid: maestro::UUID, 40 | role: Some(Role::Client), 41 | require_authentication: Some(false), 42 | require_authorization: Some(false), 43 | auto_connect: Some(false), 44 | ..Default::default() 45 | }; 46 | 47 | tracing::debug!("registering maestro profile"); 48 | let mut handle = session.register_profile(maestro_profile).await?; 49 | 50 | tracing::debug!("connecting to maestro profile"); 51 | let stream = tokio::try_join!( 52 | try_connect_profile(dev), 53 | handle_requests_for_profile(&mut handle, dev.address()), 54 | )?.1; 55 | 56 | Ok(stream) 57 | } 58 | 59 | async fn try_connect_profile(dev: &Device) -> Result<()> { 60 | const RETRY_TIMEOUT: Duration = Duration::from_secs(1); 61 | const MAX_TRIES: u32 = 3; 62 | 63 | let mut i = 0; 64 | while let Err(err) = dev.connect_profile(&maestro::UUID).await { 65 | if i >= MAX_TRIES { return Err(err.into()) } 66 | i += 1; 67 | 68 | tracing::warn!(error=?err, "connecting to profile failed, trying again ({}/{})", i, MAX_TRIES); 69 | 70 | tokio::time::sleep(RETRY_TIMEOUT).await; 71 | } 72 | 73 | tracing::debug!(address=%dev.address(), "maestro profile connected"); 74 | Ok(()) 75 | } 76 | 77 | async fn handle_requests_for_profile(handle: &mut ProfileHandle, address: Address) -> Result { 78 | while let Some(req) = handle.next().await { 79 | tracing::debug!(address=%req.device(), "received new profile connection request"); 80 | 81 | if req.device() == address { 82 | tracing::debug!(address=%req.device(), "accepting profile connection request"); 83 | return Ok(req.accept()?); 84 | } else { 85 | req.reject(ReqError::Rejected); 86 | } 87 | } 88 | 89 | anyhow::bail!("profile terminated without requests") 90 | } 91 | -------------------------------------------------------------------------------- /cli/src/cli.rs: -------------------------------------------------------------------------------- 1 | use bluer::Address; 2 | use clap::{Parser, Subcommand, ValueEnum}; 3 | 4 | use maestro::service::settings; 5 | 6 | 7 | /// Control Google Pixel Buds Pro from the command line 8 | #[derive(Debug, Parser)] 9 | #[command(author, version, about, long_about = None)] 10 | pub struct Args { 11 | /// Device to use (search for compatible device if unspecified) 12 | #[arg(short, long, global=true)] 13 | pub device: Option
, 14 | 15 | #[command(subcommand)] 16 | pub command: Command 17 | } 18 | 19 | #[derive(Debug, Subcommand)] 20 | pub enum Command { 21 | /// Show device information 22 | Show { 23 | #[command(subcommand)] 24 | command: ShowCommand 25 | }, 26 | 27 | /// Read settings value 28 | Get { 29 | #[command(subcommand)] 30 | setting: GetSetting 31 | }, 32 | 33 | /// Write settings value 34 | Set { 35 | #[command(subcommand)] 36 | setting: SetSetting 37 | }, 38 | } 39 | 40 | #[derive(Debug, Subcommand)] 41 | pub enum ShowCommand { 42 | /// Show software information. 43 | Software, 44 | 45 | /// Show hardware information. 46 | Hardware, 47 | 48 | /// Show runtime information. 49 | Runtime, 50 | 51 | /// Show battery status. 52 | Battery, 53 | } 54 | 55 | #[derive(Debug, Subcommand)] 56 | pub enum GetSetting { 57 | /// Get automatic over-the-air update status 58 | AutoOta, 59 | 60 | /// Get on-head-detection state (enabled/disabled) 61 | Ohd, 62 | 63 | /// Get the flag indicating whether the out-of-box experience phase is 64 | /// finished 65 | OobeIsFinished, 66 | 67 | /// Get gesture state (enabled/disabled) 68 | Gestures, 69 | 70 | /// Get diagnostics state (enabled/disabled) 71 | Diagnostics, 72 | 73 | /// Get out-of-box-experience mode state (enabled/disabled) 74 | OobeMode, 75 | 76 | /// Get hold-gesture action 77 | GestureControl, 78 | 79 | /// Get multipoint audio state (enabled/disabled) 80 | Multipoint, 81 | 82 | /// Get adaptive noise-cancelling gesture loop 83 | AncGestureLoop, 84 | 85 | /// Get adaptive noise-cancelling state 86 | Anc, 87 | 88 | /// Get volume-dependent EQ state (enabled/disabled) 89 | VolumeEq, 90 | 91 | /// Get 5-band EQ 92 | Eq, 93 | 94 | /// Get volume balance 95 | Balance, 96 | 97 | /// Get mono output state 98 | Mono, 99 | 100 | /// Get volume exposure notifications state (enabled/disabled) 101 | VolumeExposureNotifications, 102 | 103 | /// Get automatic transparency mode state (enabled/disabled) 104 | SpeechDetection, 105 | } 106 | 107 | #[derive(Debug, Subcommand)] 108 | pub enum SetSetting { 109 | /// Enable/disable automatic over-the-air updates 110 | /// 111 | /// Note: Updates are initiated by the Google Buds app on your phone. This 112 | /// flag controls whether updates can be done automatically when the device 113 | /// is not in use. 114 | AutoOta { 115 | /// Whether to enable or disable automatic over-the-air (OTA) updates 116 | #[arg(action=clap::ArgAction::Set)] 117 | value: bool, 118 | }, 119 | 120 | /// Enable/disable on-head detection 121 | Ohd { 122 | /// Whether to enable or disable on-head detection 123 | #[arg(action=clap::ArgAction::Set)] 124 | value: bool, 125 | }, 126 | 127 | /// Set the flag indicating whether the out-of-box experience phase is 128 | /// finished 129 | /// 130 | /// Note: You normally do not want to change this flag. It is used to 131 | /// indicate whether the out-of-box experience (OOBE) phase has been 132 | /// concluded, i.e., the setup wizard has been run and the device has been 133 | /// set up. 134 | OobeIsFinished { 135 | /// Whether the OOBE setup has been finished 136 | #[arg(action=clap::ArgAction::Set)] 137 | value: bool, 138 | }, 139 | 140 | /// Enable/disable gestures 141 | Gestures { 142 | /// Whether to enable or disable gestures 143 | #[arg(action=clap::ArgAction::Set)] 144 | value: bool, 145 | }, 146 | 147 | /// Enable/disable diagnostics 148 | /// 149 | /// Note: This will also cause the Google Buds app on your phone to send 150 | /// diagnostics data to Google. 151 | Diagnostics { 152 | /// Whether to enable or disable diagnostics 153 | #[arg(action=clap::ArgAction::Set)] 154 | value: bool, 155 | }, 156 | 157 | /// Enable/disable out-of-box-experience mode 158 | /// 159 | /// Note: You normally do not want to enable this mode. It is used to 160 | /// intercept and block touch gestures during the setup wizard. 161 | OobeMode { 162 | /// Whether to enable or disable the out-of-box experience mode 163 | #[arg(action=clap::ArgAction::Set)] 164 | value: bool, 165 | }, 166 | 167 | /// Set hold-gesture action 168 | GestureControl { 169 | /// Left gesture action 170 | #[arg(value_enum)] 171 | left: HoldGestureAction, 172 | 173 | /// Right gesture action 174 | #[arg(value_enum)] 175 | right: HoldGestureAction, 176 | }, 177 | 178 | /// Enable/disable multipoint audio 179 | Multipoint { 180 | /// Whether to enable or disable multipoint audio 181 | #[arg(action=clap::ArgAction::Set)] 182 | value: bool, 183 | }, 184 | 185 | /// Set adaptive noise-cancelling gesture loop 186 | AncGestureLoop { 187 | /// Enable 'off' mode in loop 188 | #[arg(action=clap::ArgAction::Set)] 189 | off: bool, 190 | 191 | /// Enable 'active' mode in loop 192 | #[arg(action=clap::ArgAction::Set)] 193 | active: bool, 194 | 195 | /// Enable 'aware' mode in loop 196 | #[arg(action=clap::ArgAction::Set)] 197 | aware: bool, 198 | }, 199 | 200 | /// Set adaptive noise-cancelling state 201 | Anc { 202 | /// New ANC state or action to change state 203 | #[arg(value_enum)] 204 | value: AncState, 205 | }, 206 | 207 | /// Enable/disable volume-dependent EQ 208 | VolumeEq { 209 | /// Whether to enable or disable volume-dependent EQ 210 | #[arg(action=clap::ArgAction::Set)] 211 | value: bool, 212 | }, 213 | 214 | /// Set 5-band EQ 215 | Eq { 216 | /// Low-bass band (min: -6.0, max: 6.0) 217 | #[arg(value_parser=parse_eq_value)] 218 | low_bass: f32, 219 | 220 | /// Bass band (min: -6.0, max: 6.0) 221 | #[arg(value_parser=parse_eq_value)] 222 | bass: f32, 223 | 224 | /// Mid band (min: -6.0, max: 6.0) 225 | #[arg(value_parser=parse_eq_value)] 226 | mid: f32, 227 | 228 | /// Treble band (min: -6.0, max: 6.0) 229 | #[arg(value_parser=parse_eq_value)] 230 | treble: f32, 231 | 232 | /// Upper treble band (min: -6.0, max: 6.0) 233 | #[arg(value_parser=parse_eq_value)] 234 | upper_treble: f32, 235 | }, 236 | 237 | /// Set volume balance 238 | Balance { 239 | /// Volume balance from -100 (left) to +100 (right) 240 | #[arg(value_parser=parse_balance)] 241 | value: i32, 242 | }, 243 | 244 | /// Set mono output 245 | Mono { 246 | /// Whether to force mono output 247 | #[arg(action=clap::ArgAction::Set)] 248 | value: bool, 249 | }, 250 | 251 | /// Enable/disable volume level exposure notifications 252 | VolumeExposureNotifications { 253 | /// Whether to enable or disable volume level exposure notifications 254 | #[arg(action=clap::ArgAction::Set)] 255 | value: bool, 256 | }, 257 | 258 | /// Enable/disable automatic transparency mode via speech detection 259 | SpeechDetection { 260 | /// Whether to enable or disable the automatic transparency mode via speech detection 261 | #[arg(action=clap::ArgAction::Set)] 262 | value: bool, 263 | }, 264 | } 265 | 266 | #[derive(Debug, ValueEnum, Clone, Copy)] 267 | pub enum AncState { 268 | Off, 269 | Active, 270 | Aware, 271 | CycleNext, 272 | CyclePrev, 273 | } 274 | 275 | #[derive(Debug, ValueEnum, Clone, Copy)] 276 | pub enum HoldGestureAction { 277 | Anc, 278 | Assistant, 279 | } 280 | 281 | impl From for settings::RegularActionTarget { 282 | fn from(value: HoldGestureAction) -> Self { 283 | match value { 284 | HoldGestureAction::Anc => settings::RegularActionTarget::AncControl, 285 | HoldGestureAction::Assistant => settings::RegularActionTarget::AssistantQuery, 286 | } 287 | } 288 | } 289 | 290 | fn parse_eq_value(s: &str) -> std::result::Result { 291 | let val = s.parse().map_err(|e| format!("{e}"))?; 292 | 293 | if val > settings::EqBands::MAX_VALUE { 294 | Err(format!("exceeds maximum of {}", settings::EqBands::MAX_VALUE)) 295 | } else if val < settings::EqBands::MIN_VALUE { 296 | Err(format!("exceeds minimum of {}", settings::EqBands::MIN_VALUE)) 297 | } else { 298 | Ok(val) 299 | } 300 | } 301 | 302 | fn parse_balance(s: &str) -> std::result::Result { 303 | let val = s.parse().map_err(|e| format!("{e}"))?; 304 | 305 | if val > 100 { 306 | Err("exceeds maximum of 100".to_string()) 307 | } else if val < -100 { 308 | Err("exceeds minimum of -100".to_string()) 309 | } else { 310 | Ok(val) 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | mod bt; 2 | mod cli; 3 | 4 | use anyhow::Result; 5 | use clap::{Parser, CommandFactory}; 6 | use futures::{Future, StreamExt}; 7 | 8 | use maestro::protocol::{utils, addr}; 9 | use maestro::pwrpc::client::{Client, ClientHandle}; 10 | use maestro::protocol::codec::Codec; 11 | use maestro::service::MaestroService; 12 | use maestro::service::settings::{self, Setting, SettingValue}; 13 | 14 | use cli::*; 15 | 16 | 17 | #[tokio::main(flavor = "current_thread")] 18 | async fn main() -> Result<()> { 19 | tracing_subscriber::fmt::init(); 20 | 21 | let args = Args::parse(); 22 | 23 | // set up session 24 | let session = bluer::Session::new().await?; 25 | let adapter = session.default_adapter().await?; 26 | 27 | // set up device 28 | let dev = if let Some(address) = args.device { 29 | tracing::debug!("using provided address: {}", address); 30 | adapter.device(address)? 31 | } else { 32 | tracing::debug!("no device specified, searching for compatible one"); 33 | bt::find_maestro_device(&adapter).await? 34 | }; 35 | 36 | // set up profile 37 | let stream = bt::connect_maestro_rfcomm(&session, &dev).await?; 38 | 39 | // set up codec 40 | let codec = Codec::new(); 41 | let stream = codec.wrap(stream); 42 | 43 | // set up RPC client 44 | let mut client = Client::new(stream); 45 | let handle = client.handle(); 46 | 47 | // resolve channel 48 | let channel = utils::resolve_channel(&mut client).await?; 49 | 50 | match args.command { 51 | Command::Show { command } => match command { 52 | ShowCommand::Software => run(client, cmd_show_software(handle, channel)).await, 53 | ShowCommand::Hardware => run(client, cmd_show_hardware(handle, channel)).await, 54 | ShowCommand::Runtime => run(client, cmd_show_runtime(handle, channel)).await, 55 | ShowCommand::Battery => run(client, cmd_show_battery(handle, channel)).await, 56 | }, 57 | Command::Get { setting } => match setting { 58 | GetSetting::AutoOta => { 59 | run(client, cmd_get_setting(handle, channel, settings::id::AutoOtaEnable)).await 60 | }, 61 | GetSetting::Ohd => { 62 | run(client, cmd_get_setting(handle, channel, settings::id::OhdEnable)).await 63 | }, 64 | GetSetting::OobeIsFinished => { 65 | run(client, cmd_get_setting(handle, channel, settings::id::OobeIsFinished)).await 66 | }, 67 | GetSetting::Gestures => { 68 | run(client, cmd_get_setting(handle, channel, settings::id::GestureEnable)).await 69 | }, 70 | GetSetting::Diagnostics => { 71 | run(client, cmd_get_setting(handle, channel, settings::id::DiagnosticsEnable)).await 72 | } 73 | GetSetting::OobeMode => { 74 | run(client, cmd_get_setting(handle, channel, settings::id::OobeMode)).await 75 | }, 76 | GetSetting::GestureControl => { 77 | run(client, cmd_get_setting(handle, channel, settings::id::GestureControl)).await 78 | }, 79 | GetSetting::Multipoint => { 80 | run(client, cmd_get_setting(handle, channel, settings::id::MultipointEnable)).await 81 | }, 82 | GetSetting::AncGestureLoop => { 83 | run(client, cmd_get_setting(handle, channel, settings::id::AncrGestureLoop)).await 84 | } 85 | GetSetting::Anc => { 86 | run(client, cmd_get_setting(handle, channel, settings::id::CurrentAncrState)).await 87 | }, 88 | GetSetting::VolumeEq => { 89 | run(client, cmd_get_setting(handle, channel, settings::id::VolumeEqEnable)).await 90 | }, 91 | GetSetting::Eq => { 92 | run(client, cmd_get_setting(handle, channel, settings::id::CurrentUserEq)).await 93 | }, 94 | GetSetting::Balance => { 95 | run(client, cmd_get_setting(handle, channel, settings::id::VolumeAsymmetry)).await 96 | }, 97 | GetSetting::Mono => { 98 | run(client, cmd_get_setting(handle, channel, settings::id::SumToMono)).await 99 | }, 100 | GetSetting::VolumeExposureNotifications => { 101 | run(client, cmd_get_setting(handle, channel, settings::id::VolumeExposureNotifications)).await 102 | }, 103 | GetSetting::SpeechDetection => { 104 | run(client, cmd_get_setting(handle, channel, settings::id::SpeechDetection)).await 105 | }, 106 | }, 107 | Command::Set { setting } => match setting { 108 | SetSetting::AutoOta { value } => { 109 | let value = SettingValue::AutoOtaEnable(value); 110 | run(client, cmd_set_setting(handle, channel, value)).await 111 | }, 112 | SetSetting::Ohd { value } => { 113 | let value = SettingValue::OhdEnable(value); 114 | run(client, cmd_set_setting(handle, channel, value)).await 115 | }, 116 | SetSetting::OobeIsFinished { value } => { 117 | let value = SettingValue::OobeIsFinished(value); 118 | run(client, cmd_set_setting(handle, channel, value)).await 119 | }, 120 | SetSetting::Gestures { value } => { 121 | let value = SettingValue::GestureEnable(value); 122 | run(client, cmd_set_setting(handle, channel, value)).await 123 | }, 124 | SetSetting::Diagnostics { value } => { 125 | let value = SettingValue::DiagnosticsEnable(value); 126 | run(client, cmd_set_setting(handle, channel, value)).await 127 | }, 128 | SetSetting::OobeMode { value } => { 129 | let value = SettingValue::OobeMode(value); 130 | run(client, cmd_set_setting(handle, channel, value)).await 131 | }, 132 | SetSetting::GestureControl { left, right } => { 133 | let value = settings::GestureControl { left: left.into(), right: right.into() }; 134 | let value = SettingValue::GestureControl(value); 135 | run(client, cmd_set_setting(handle, channel, value)).await 136 | }, 137 | SetSetting::Multipoint { value } => { 138 | let value = SettingValue::MultipointEnable(value); 139 | run(client, cmd_set_setting(handle, channel, value)).await 140 | }, 141 | SetSetting::AncGestureLoop { off, active, aware } => { 142 | let value = settings::AncrGestureLoop { off, active, aware }; 143 | 144 | if !value.is_valid() { 145 | use clap::error::ErrorKind; 146 | 147 | let mut cmd = Args::command(); 148 | let err = cmd.error( 149 | ErrorKind::InvalidValue, 150 | "This command requires at least tow enabled ('true') modes" 151 | ); 152 | err.exit(); 153 | } 154 | 155 | let value = SettingValue::AncrGestureLoop(value); 156 | run(client, cmd_set_setting(handle, channel, value)).await 157 | }, 158 | SetSetting::Anc { value } => { 159 | match value { 160 | AncState::Off => { 161 | let value = SettingValue::CurrentAncrState(settings::AncState::Off); 162 | run(client, cmd_set_setting(handle, channel, value)).await 163 | }, 164 | AncState::Aware => { 165 | let value = SettingValue::CurrentAncrState(settings::AncState::Aware); 166 | run(client, cmd_set_setting(handle, channel, value)).await 167 | }, 168 | AncState::Active => { 169 | let value = SettingValue::CurrentAncrState(settings::AncState::Active); 170 | run(client, cmd_set_setting(handle, channel, value)).await 171 | }, 172 | AncState::CycleNext => { 173 | run(client, cmd_anc_cycle(handle, channel, true)).await 174 | }, 175 | AncState::CyclePrev => { 176 | run(client, cmd_anc_cycle(handle, channel, false)).await 177 | }, 178 | } 179 | }, 180 | SetSetting::VolumeEq { value } => { 181 | let value = SettingValue::VolumeEqEnable(value); 182 | run(client, cmd_set_setting(handle, channel, value)).await 183 | }, 184 | SetSetting::Eq { low_bass, bass, mid, treble, upper_treble } => { 185 | let value = settings::EqBands::new(low_bass, bass, mid, treble, upper_treble); 186 | let value = SettingValue::CurrentUserEq(value); 187 | run(client, cmd_set_setting(handle, channel, value)).await 188 | }, 189 | SetSetting::Balance { value } => { 190 | let value = settings::VolumeAsymmetry::from_normalized(value); 191 | let value = SettingValue::VolumeAsymmetry(value); 192 | run(client, cmd_set_setting(handle, channel, value)).await 193 | }, 194 | SetSetting::Mono { value } => { 195 | let value = SettingValue::SumToMono(value); 196 | run(client, cmd_set_setting(handle, channel, value)).await 197 | }, 198 | SetSetting::VolumeExposureNotifications { value } => { 199 | let value = SettingValue::VolumeExposureNotifications(value); 200 | run(client, cmd_set_setting(handle, channel, value)).await 201 | }, 202 | SetSetting::SpeechDetection { value } => { 203 | let value = SettingValue::SpeechDetection(value); 204 | run(client, cmd_set_setting(handle, channel, value)).await 205 | }, 206 | }, 207 | } 208 | } 209 | 210 | async fn cmd_show_software(handle: ClientHandle, channel: u32) -> Result<()> { 211 | let mut service = MaestroService::new(handle, channel); 212 | 213 | let info = service.get_software_info().await?; 214 | 215 | let fw_ver_case = info.firmware.as_ref() 216 | .and_then(|fw| fw.case.as_ref()) 217 | .map(|fw| fw.version_string.as_str()) 218 | .unwrap_or("unknown"); 219 | 220 | let fw_ver_left = info.firmware.as_ref() 221 | .and_then(|fw| fw.left.as_ref()) 222 | .map(|fw| fw.version_string.as_str()) 223 | .unwrap_or("unknown"); 224 | 225 | let fw_ver_right = info.firmware.as_ref() 226 | .and_then(|fw| fw.right.as_ref()) 227 | .map(|fw| fw.version_string.as_str()) 228 | .unwrap_or("unknown"); 229 | 230 | let fw_unk_case = info.firmware.as_ref() 231 | .and_then(|fw| fw.case.as_ref()) 232 | .map(|fw| fw.unknown.as_str()) 233 | .unwrap_or("unknown"); 234 | 235 | let fw_unk_left = info.firmware.as_ref() 236 | .and_then(|fw| fw.left.as_ref()) 237 | .map(|fw| fw.unknown.as_str()) 238 | .unwrap_or("unknown"); 239 | 240 | let fw_unk_right = info.firmware.as_ref() 241 | .and_then(|fw| fw.right.as_ref()) 242 | .map(|fw| fw.unknown.as_str()) 243 | .unwrap_or("unknown"); 244 | 245 | println!("firmware:"); 246 | println!(" case: {fw_ver_case} ({fw_unk_case})"); 247 | println!(" left bud: {fw_ver_left} ({fw_unk_left})"); 248 | println!(" right bud: {fw_ver_right} ({fw_unk_right})"); 249 | 250 | Ok(()) 251 | } 252 | 253 | async fn cmd_show_hardware(handle: ClientHandle, channel: u32) -> Result<()> { 254 | let mut service = MaestroService::new(handle, channel); 255 | 256 | let info = service.get_hardware_info().await?; 257 | 258 | let serial_case = info.serial_number.as_ref() 259 | .map(|ser| ser.case.as_str()) 260 | .unwrap_or("unknown"); 261 | 262 | let serial_left = info.serial_number.as_ref() 263 | .map(|ser| ser.left.as_str()) 264 | .unwrap_or("unknown"); 265 | 266 | let serial_right = info.serial_number.as_ref() 267 | .map(|ser| ser.right.as_str()) 268 | .unwrap_or("unknown"); 269 | 270 | println!("serial numbers:"); 271 | println!(" case: {serial_case}"); 272 | println!(" left bud: {serial_left}"); 273 | println!(" right bud: {serial_right}"); 274 | 275 | Ok(()) 276 | } 277 | 278 | async fn cmd_show_runtime(handle: ClientHandle, channel: u32) -> Result<()> { 279 | let mut service = MaestroService::new(handle, channel); 280 | 281 | let mut call = service.subscribe_to_runtime_info()?; 282 | 283 | let info = call.stream().next().await 284 | .ok_or_else(|| anyhow::anyhow!("stream terminated without item"))??; 285 | 286 | let bat_level_case = info.battery_info.as_ref() 287 | .and_then(|b| b.case.as_ref()) 288 | .map(|b| b.level); 289 | 290 | let bat_state_case = info.battery_info.as_ref() 291 | .and_then(|b| b.case.as_ref()) 292 | .map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" }) 293 | .unwrap_or("unknown"); 294 | 295 | let bat_level_left = info.battery_info.as_ref() 296 | .and_then(|b| b.left.as_ref()) 297 | .map(|b| b.level); 298 | 299 | let bat_state_left = info.battery_info.as_ref() 300 | .and_then(|b| b.left.as_ref()) 301 | .map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" }) 302 | .unwrap_or("unknown"); 303 | 304 | let bat_level_right = info.battery_info.as_ref() 305 | .and_then(|b| b.right.as_ref()) 306 | .map(|b| b.level); 307 | 308 | let bat_state_right = info.battery_info.as_ref() 309 | .and_then(|b| b.right.as_ref()) 310 | .map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" }) 311 | .unwrap_or("unknown"); 312 | 313 | let place_left = info.placement.as_ref() 314 | .map(|p| if p.left_bud_in_case { "in case" } else { "out of case" }) 315 | .unwrap_or("unknown"); 316 | 317 | let place_right = info.placement.as_ref() 318 | .map(|p| if p.right_bud_in_case { "in case" } else { "out of case" }) 319 | .unwrap_or("unknown"); 320 | 321 | println!("clock: {} ms", info.timestamp_ms); 322 | println!(); 323 | 324 | println!("battery:"); 325 | if let Some(lvl) = bat_level_case { 326 | println!(" case: {lvl}% ({bat_state_case})"); 327 | } else { 328 | println!(" case: unknown"); 329 | } 330 | if let Some(lvl) = bat_level_left { 331 | println!(" left bud: {lvl}% ({bat_state_left})"); 332 | } else { 333 | println!(" left bud: unknown"); 334 | } 335 | if let Some(lvl) = bat_level_right { 336 | println!(" right bud: {lvl}% ({bat_state_right})"); 337 | } else { 338 | println!(" right bud: unknown"); 339 | } 340 | println!(); 341 | 342 | println!("placement:"); 343 | println!(" left bud: {place_left}"); 344 | println!(" right bud: {place_right}"); 345 | 346 | let address = addr::address_for_channel(channel); 347 | let peer_local = address.map(|a| a.source()); 348 | let peer_remote = address.map(|a| a.target()); 349 | 350 | println!(); 351 | println!("connection:"); 352 | if let Some(peer) = peer_local { 353 | println!(" local: {peer:?}"); 354 | } else { 355 | println!(" local: unknown"); 356 | } 357 | if let Some(peer) = peer_remote { 358 | println!(" remote: {peer:?}"); 359 | } else { 360 | println!(" remote: unknown"); 361 | } 362 | 363 | Ok(()) 364 | } 365 | 366 | async fn cmd_show_battery(handle: ClientHandle, channel: u32) -> Result<()> { 367 | let mut service = MaestroService::new(handle, channel); 368 | 369 | let mut call = service.subscribe_to_runtime_info()?; 370 | 371 | let info = call.stream().next().await 372 | .ok_or_else(|| anyhow::anyhow!("stream terminated without item"))??; 373 | 374 | let bat_level_case = info.battery_info.as_ref() 375 | .and_then(|b| b.case.as_ref()) 376 | .map(|b| b.level); 377 | 378 | let bat_state_case = info.battery_info.as_ref() 379 | .and_then(|b| b.case.as_ref()) 380 | .map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" }) 381 | .unwrap_or("unknown"); 382 | 383 | let bat_level_left = info.battery_info.as_ref() 384 | .and_then(|b| b.left.as_ref()) 385 | .map(|b| b.level); 386 | 387 | let bat_state_left = info.battery_info.as_ref() 388 | .and_then(|b| b.left.as_ref()) 389 | .map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" }) 390 | .unwrap_or("unknown"); 391 | 392 | let bat_level_right = info.battery_info.as_ref() 393 | .and_then(|b| b.right.as_ref()) 394 | .map(|b| b.level); 395 | 396 | let bat_state_right = info.battery_info.as_ref() 397 | .and_then(|b| b.right.as_ref()) 398 | .map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" }) 399 | .unwrap_or("unknown"); 400 | 401 | if let Some(lvl) = bat_level_case { 402 | println!("case: {lvl}% ({bat_state_case})"); 403 | } else { 404 | println!("case: unknown"); 405 | } 406 | if let Some(lvl) = bat_level_left { 407 | println!("left bud: {lvl}% ({bat_state_left})"); 408 | } else { 409 | println!("left bud: unknown"); 410 | } 411 | if let Some(lvl) = bat_level_right { 412 | println!("right bud: {lvl}% ({bat_state_right})"); 413 | } else { 414 | println!("right bud: unknown"); 415 | } 416 | 417 | Ok(()) 418 | } 419 | 420 | async fn cmd_get_setting(handle: ClientHandle, channel: u32, setting: T) -> Result<()> 421 | where 422 | T: Setting, 423 | T::Type: std::fmt::Display, 424 | { 425 | let mut service = MaestroService::new(handle, channel); 426 | 427 | let value = service.read_setting(setting).await?; 428 | println!("{value}"); 429 | 430 | Ok(()) 431 | } 432 | 433 | async fn cmd_set_setting(handle: ClientHandle, channel: u32, setting: SettingValue) -> Result<()> { 434 | let mut service = MaestroService::new(handle, channel); 435 | 436 | service.write_setting(setting).await?; 437 | Ok(()) 438 | } 439 | 440 | async fn cmd_anc_cycle(handle: ClientHandle, channel: u32, forward: bool) -> Result<()> { 441 | let mut service = MaestroService::new(handle, channel); 442 | 443 | let enabled = service.read_setting(settings::id::AncrGestureLoop).await?; 444 | let state = service.read_setting(settings::id::CurrentAncrState).await?; 445 | 446 | if let settings::AncState::Unknown(x) = state { 447 | anyhow::bail!("unknown ANC state: {x}"); 448 | } 449 | 450 | let states = [ 451 | (settings::AncState::Active, enabled.active), 452 | (settings::AncState::Off, enabled.off), 453 | (settings::AncState::Aware, enabled.aware), 454 | ]; 455 | 456 | let index = states.iter().position(|(s, _)| *s == state).unwrap(); 457 | 458 | for offs in 1..states.len() { 459 | let next = if forward { 460 | index + offs 461 | } else { 462 | index + states.len() - offs 463 | } % states.len(); 464 | 465 | let (state, enabled) = states[next]; 466 | if enabled { 467 | service.write_setting(SettingValue::CurrentAncrState(state)).await?; 468 | break; 469 | } 470 | } 471 | 472 | Ok(()) 473 | } 474 | 475 | pub async fn run(mut client: Client, task: F) -> Result<()> 476 | where 477 | S: futures::Sink, 478 | S: futures::Stream> + Unpin, 479 | maestro::pwrpc::Error: From, 480 | maestro::pwrpc::Error: From, 481 | F: Future>, 482 | { 483 | tokio::select! { 484 | res = client.run() => { 485 | res?; 486 | anyhow::bail!("client terminated unexpectedly"); 487 | }, 488 | res = task => { 489 | res?; 490 | tracing::trace!("task terminated successfully"); 491 | } 492 | sig = tokio::signal::ctrl_c() => { 493 | sig?; 494 | tracing::trace!("client termination requested"); 495 | }, 496 | } 497 | 498 | client.terminate().await?; 499 | 500 | tracing::trace!("client terminated successfully"); 501 | Ok(()) 502 | } 503 | -------------------------------------------------------------------------------- /docs/Notes.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | The Google Pixel Buds Pro rely on at least two different protocols apart from the standard audio profiles (HSP/HFP, A2DP, AVRCP): 4 | - The Google Fast Pair Service (GFPS) protocol provides support for somewhat standardized events and actions (next to fast-pairing as advertised in its name). 5 | This includes battery status of the individual parts (left/right buds and case), multi-point audio source switching notifications, ringing for find-my-device actions, etc. 6 | See https://developers.google.com/nearby/fast-pair for details. 7 | - The proprietary "Maestro" protocol is used to change settings on the buds (noise-cancelling, equalizer, balance, ...) and likely also update the firmware. 8 | 9 | Note that while AVRCP can provide battery information, this only seems to be a single value for both buds combined and does not include the case. 10 | Detailed battery information can only be obtained via the GFPS protocol. 11 | 12 | 13 | ## Google Fast Pair Service Protocol 14 | 15 | See https://developers.google.com/nearby/fast-pair for a somewhat limited specification. 16 | Unfortunately this is incomplete. 17 | More details can be found in the Android source code, e.g. [here][gfps-android-0] and [here][gfps-android-1]. 18 | The Pixel Buds Pro, however, send additional messages with group and code numbers beyond the ones mentioned there. 19 | 20 | The main part of this protocol is a [RFCOMM channel][gfps-rfcomm] which provides events, including battery notifications. 21 | On the Pixel Buds Pro, this also seems to include events for changes to the ANC status (group 8, code 19). 22 | 23 | [gfps-android-0]: https://cs.android.com/android/platform/superproject/+/master:out/soong/.intermediates/packages/modules/Connectivity/nearby/tests/multidevices/clients/test_support/fastpair_provider/proto/NearbyFastPairProviderLiteProtos/android_common/xref/srcjars.xref/android/nearby/fastpair/provider/EventStreamProtocol.java;drc=cb3bd7c37d630acb613e10f730c532128a02a3d5;l=69 24 | [gfps-android-1]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/Connectivity/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java;l=1199;drc=cb3bd7c37d630acb613e10f730c532128a02a3d5?q=df21fe2c-2515-4fdb-8886-f12c4d67927c&ss=android%2Fplatform%2Fsuperproject 25 | [gfps-rfcomm]: https://developers.google.com/nearby/fast-pair/specifications/extensions/messagestream 26 | 27 | 28 | ## Maestro Protocol 29 | 30 | The "Maestro" protocol is a proprietary protocol for changing settings, used on the Pixel Buds Pro. 31 | It's possible that this is targeted more generally at Google wearable devices. 32 | The protocol not only allows for changing settings or getting hardware/firmware information, but also allows for subscribing to events, such as settings changes. 33 | 34 | The protocol is implemented using the [pigweed RPC library](https://pigweed.dev/pw_rpc/), which is similar to [gRPC](https://grpc.io/) and relies on [protocol buffers](https://developers.google.com/protocol-buffers) for message encoding. 35 | In addition, the RPC messages are wrapped in High-Level Data Link Control (HDLC) U-frames (an example for this is given [here](https://pigweed.dev/pw_hdlc/rpc_example/#module-pw-hdlc-rpc-example)). 36 | -------------------------------------------------------------------------------- /libgfps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gfps" 3 | authors = ["Maximilian Luz "] 4 | version = "0.1.3" 5 | edition = "2024" 6 | license = "MIT/Apache-2.0" 7 | description = "Google Fast Pair Service (GFPS) protocol client library" 8 | repository = "https://github.com/qzed/pbpctrl" 9 | 10 | [dependencies] 11 | bytes = "1.10.1" 12 | num_enum = "0.7.3" 13 | smallvec = { version = "1.15.0", features = ["union"] } 14 | tokio = "1.44.2" 15 | tokio-util = { version = "0.7.14", features = ["codec"] } 16 | uuid = "1.16.0" 17 | 18 | [dev-dependencies] 19 | bluer = { version = "0.17.3", features = ["bluetoothd", "rfcomm"] } 20 | futures = "0.3.31" 21 | pretty-hex = "0.4.1" 22 | tokio = { version = "1.44.2", features = ["rt", "macros"] } 23 | -------------------------------------------------------------------------------- /libgfps/examples/gfps_get_battery.rs: -------------------------------------------------------------------------------- 1 | //! Simple example for receiving battery info via the GFPS RFCOMM channel. 2 | //! 3 | //! Usage: 4 | //! cargo run --example gfps_get_battery -- 5 | 6 | use std::str::FromStr; 7 | 8 | use bluer::{Address, Session, Device}; 9 | use bluer::rfcomm::{Profile, ReqError, Role, ProfileHandle}; 10 | 11 | use futures::StreamExt; 12 | 13 | use gfps::msg::{Codec, DeviceEventCode, EventGroup, BatteryInfo}; 14 | 15 | use num_enum::FromPrimitive; 16 | 17 | 18 | #[tokio::main(flavor = "current_thread")] 19 | async fn main() -> bluer::Result<()> { 20 | // handle command line arguments 21 | let addr = std::env::args().nth(1).expect("need device address as argument"); 22 | let addr = Address::from_str(&addr)?; 23 | 24 | // set up session 25 | let session = Session::new().await?; 26 | let adapter = session.default_adapter().await?; 27 | 28 | // get device 29 | let dev = adapter.device(addr)?; 30 | 31 | // get RFCOMM stream 32 | let stream = { 33 | // register GFPS profile 34 | let profile = Profile { 35 | uuid: gfps::msg::UUID, 36 | role: Some(Role::Client), 37 | require_authentication: Some(false), 38 | require_authorization: Some(false), 39 | auto_connect: Some(false), 40 | ..Default::default() 41 | }; 42 | 43 | let mut profile_handle = session.register_profile(profile).await?; 44 | 45 | // connect profile 46 | connect_device_to_profile(&mut profile_handle, &dev).await? 47 | }; 48 | 49 | // listen to event messages 50 | let codec = Codec::new(); 51 | let mut stream = codec.wrap(stream); 52 | 53 | // The battery status cannot be queried via a normal command. However, it 54 | // is sent right after we connect to the GFPS stream. In addition, multiple 55 | // events are often sent in sequence. Therefore we do the following: 56 | // - Set a deadline for a general timeout. If this passes, we just return 57 | // the current state (and if necessary "unknown"): 58 | // - Use a timestamp for checking whether we have received any new updates 59 | // in a given interval. If we have not received any, we consider the 60 | // state to be "settled" and return the battery info. 61 | // - On battery events we simply store the sent information. We retreive 62 | // the stored information once either of the timeouts kicks in. 63 | let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); 64 | 65 | let mut timestamp = deadline; 66 | let mut bat_left = BatteryInfo::Unknown; 67 | let mut bat_right = BatteryInfo::Unknown; 68 | let mut bat_case = BatteryInfo::Unknown; 69 | 70 | let time_settle = std::time::Duration::from_millis(500); 71 | 72 | loop { 73 | tokio::select! { 74 | // receive and handle events 75 | msg = stream.next() => { 76 | match msg { 77 | Some(Ok(msg)) => { 78 | let group = EventGroup::from_primitive(msg.group); 79 | if group != EventGroup::Device { 80 | continue; 81 | } 82 | 83 | let code = DeviceEventCode::from_primitive(msg.code); 84 | if code == DeviceEventCode::BatteryInfo { 85 | timestamp = std::time::Instant::now(); 86 | 87 | bat_left = BatteryInfo::from_byte(msg.data[0]); 88 | bat_right = BatteryInfo::from_byte(msg.data[1]); 89 | bat_case = BatteryInfo::from_byte(msg.data[2]); 90 | } 91 | }, 92 | Some(Err(err)) => { 93 | Err(err)?; 94 | }, 95 | None => { 96 | let err = std::io::Error::new( 97 | std::io::ErrorKind::ConnectionAborted, 98 | "connection closed" 99 | ); 100 | 101 | Err(err)?; 102 | } 103 | } 104 | }, 105 | // timeout for determining when the state has "settled" 106 | _ = tokio::time::sleep(tokio::time::Duration::from_millis(time_settle.as_millis() as _)) => { 107 | let delta = std::time::Instant::now() - timestamp; 108 | 109 | if delta > time_settle { 110 | break 111 | } 112 | }, 113 | // general deadline 114 | _ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => { 115 | break 116 | }, 117 | } 118 | } 119 | 120 | println!("Battery status:"); 121 | println!(" left bud: {}", bat_left); 122 | println!(" right bud: {}", bat_right); 123 | println!(" case: {}", bat_case); 124 | 125 | Ok(()) 126 | } 127 | 128 | async fn connect_device_to_profile(profile: &mut ProfileHandle, dev: &Device) 129 | -> bluer::Result 130 | { 131 | loop { 132 | tokio::select! { 133 | res = async { 134 | let _ = dev.connect().await; 135 | dev.connect_profile(&gfps::msg::UUID).await 136 | } => { 137 | if let Err(err) = res { 138 | println!("Connecting GFPS profile failed: {:?}", err); 139 | } 140 | tokio::time::sleep(std::time::Duration::from_millis(3000)).await; 141 | }, 142 | req = profile.next() => { 143 | let req = req.expect("no connection request received"); 144 | 145 | if req.device() == dev.address() { 146 | // accept our device 147 | break req.accept(); 148 | } else { 149 | // reject unknown devices 150 | req.reject(ReqError::Rejected); 151 | } 152 | }, 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /libgfps/examples/gfps_listen.rs: -------------------------------------------------------------------------------- 1 | //! Simple example for listening to GFPS messages sent via the RFCOMM channel. 2 | //! 3 | //! Usage: 4 | //! cargo run --example gfps_listen -- 5 | 6 | use std::str::FromStr; 7 | 8 | use bluer::{Address, Session, Device}; 9 | use bluer::rfcomm::{Profile, ReqError, Role, ProfileHandle}; 10 | 11 | use futures::StreamExt; 12 | 13 | use gfps::msg::{ 14 | AcknowledgementEventCode, Codec, DeviceActionEventCode, DeviceCapabilitySyncEventCode, 15 | DeviceConfigurationEventCode, DeviceEventCode, EventGroup, Message, PlatformType, 16 | SassEventCode, LoggingEventCode, BluetoothEventCode, BatteryInfo, 17 | }; 18 | 19 | use num_enum::FromPrimitive; 20 | 21 | 22 | #[tokio::main(flavor = "current_thread")] 23 | async fn main() -> bluer::Result<()> { 24 | // handle command line arguments 25 | let addr = std::env::args().nth(1).expect("need device address as argument"); 26 | let addr = Address::from_str(&addr)?; 27 | 28 | // set up session 29 | let session = Session::new().await?; 30 | let adapter = session.default_adapter().await?; 31 | 32 | println!("Using adapter '{}'", adapter.name()); 33 | 34 | // get device 35 | let dev = adapter.device(addr)?; 36 | let uuids = { 37 | let mut uuids = Vec::from_iter(dev.uuids().await? 38 | .unwrap_or_default() 39 | .into_iter()); 40 | 41 | uuids.sort_unstable(); 42 | uuids 43 | }; 44 | 45 | println!("Found device:"); 46 | println!(" alias: {}", dev.alias().await?); 47 | println!(" address: {}", dev.address()); 48 | println!(" paired: {}", dev.is_paired().await?); 49 | println!(" connected: {}", dev.is_connected().await?); 50 | println!(" UUIDs:"); 51 | for uuid in uuids { 52 | println!(" {}", uuid); 53 | } 54 | println!(); 55 | 56 | // try to reconnect if connection is reset 57 | loop { 58 | let stream = { 59 | // register GFPS profile 60 | println!("Registering GFPS profile..."); 61 | 62 | let profile = Profile { 63 | uuid: gfps::msg::UUID, 64 | role: Some(Role::Client), 65 | require_authentication: Some(false), 66 | require_authorization: Some(false), 67 | auto_connect: Some(false), 68 | ..Default::default() 69 | }; 70 | 71 | let mut profile_handle = session.register_profile(profile).await?; 72 | 73 | // connect profile 74 | println!("Connecting GFPS profile..."); 75 | connect_device_to_profile(&mut profile_handle, &dev).await? 76 | }; 77 | 78 | println!("Profile connected"); 79 | 80 | // listen to event messages 81 | let codec = Codec::new(); 82 | let mut stream = codec.wrap(stream); 83 | 84 | println!("Listening..."); 85 | println!(); 86 | 87 | while let Some(msg) = stream.next().await { 88 | match msg { 89 | Ok(msg) => { 90 | print_message(&msg); 91 | } 92 | Err(e) if e.raw_os_error() == Some(104) => { 93 | // The Pixel Buds Pro can hand off processing between each 94 | // other. On a switch, the connection is reset. Wait a bit 95 | // and then try to reconnect. 96 | println!(); 97 | println!("Connection reset. Attempting to reconnect..."); 98 | tokio::time::sleep(std::time::Duration::from_millis(500)).await; 99 | break; 100 | } 101 | Err(e) => { 102 | Err(e)?; 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | async fn connect_device_to_profile(profile: &mut ProfileHandle, dev: &Device) 110 | -> bluer::Result 111 | { 112 | loop { 113 | tokio::select! { 114 | res = async { 115 | let _ = dev.connect().await; 116 | dev.connect_profile(&gfps::msg::UUID).await 117 | } => { 118 | if let Err(err) = res { 119 | println!("Connecting GFPS profile failed: {:?}", err); 120 | } 121 | tokio::time::sleep(std::time::Duration::from_millis(3000)).await; 122 | }, 123 | req = profile.next() => { 124 | let req = req.expect("no connection request received"); 125 | 126 | if req.device() == dev.address() { 127 | println!("Accepting request..."); 128 | break req.accept(); 129 | } else { 130 | println!("Rejecting unknown device {}", req.device()); 131 | req.reject(ReqError::Rejected); 132 | } 133 | }, 134 | } 135 | } 136 | } 137 | 138 | fn print_message(msg: &Message) { 139 | let group = EventGroup::from_primitive(msg.group); 140 | 141 | match group { 142 | EventGroup::Bluetooth => { 143 | let code = BluetoothEventCode::from_primitive(msg.code); 144 | 145 | println!("Bluetooth (0x{:02X}) :: ", msg.group); 146 | 147 | match code { 148 | BluetoothEventCode::EnableSilenceMode => { 149 | println!("Enable Silence Mode (0x{:02X})", msg.code); 150 | }, 151 | BluetoothEventCode::DisableSilenceMode => { 152 | println!("Disable Silence Mode (0x{:02X})", msg.code); 153 | }, 154 | _ => { 155 | println!("Unknown (0x{:02X})", msg.code); 156 | }, 157 | } 158 | 159 | print_message_body_unknown(msg); 160 | println!(); 161 | } 162 | EventGroup::Logging => { 163 | let code = LoggingEventCode::from_primitive(msg.code); 164 | 165 | println!("Companion App (0x{:02X}) :: ", msg.group); 166 | 167 | match code { 168 | LoggingEventCode::LogFull => { 169 | println!("Log Full (0x{:02X})", msg.code); 170 | } 171 | LoggingEventCode::LogSaveToBuffer => { 172 | println!("Log Save Buffer (0x{:02X})", msg.code); 173 | } 174 | _ => { 175 | println!("Unknown (0x{:02X})", msg.code); 176 | } 177 | } 178 | 179 | print_message_body_unknown(msg); 180 | println!(); 181 | } 182 | EventGroup::Device => { 183 | let code = DeviceEventCode::from_primitive(msg.code); 184 | 185 | print!("Device Information (0x{:02X}) :: ", msg.group); 186 | 187 | match code { 188 | DeviceEventCode::ModelId => { 189 | println!("Model Id (0x{:02X})", msg.code); 190 | println!(" model: {:02X}{:02X}{:02X}", msg.data[0], msg.data[1], msg.data[2]); 191 | } 192 | DeviceEventCode::BleAddress => { 193 | println!("BLE Address (0x{:02X})", msg.code); 194 | println!(" address: {}", Address::new(msg.data[0..6].try_into().unwrap())); 195 | } 196 | DeviceEventCode::BatteryInfo => { 197 | println!("Battery Info (0x{:02X})", msg.code); 198 | 199 | let left = BatteryInfo::from_byte(msg.data[0]); 200 | let right = BatteryInfo::from_byte(msg.data[1]); 201 | let case = BatteryInfo::from_byte(msg.data[2]); 202 | 203 | println!(" left bud: {}", left); 204 | println!(" right bud: {}", right); 205 | println!(" case: {}", case); 206 | } 207 | DeviceEventCode::BatteryTime => { 208 | println!("Remaining Battery Time (0x{:02X})", msg.code); 209 | 210 | let time = match msg.data.len() { 211 | 1 => msg.data[0] as u16, 212 | 2 => u16::from_be_bytes(msg.data[0..2].try_into().unwrap()), 213 | _ => panic!("invalid format"), 214 | }; 215 | 216 | println!(" time: {} minutes", time); 217 | } 218 | DeviceEventCode::ActiveComponentsRequest => { 219 | println!("Active Components Request (0x{:02X})", msg.code); 220 | } 221 | DeviceEventCode::ActiveComponentsResponse => { 222 | println!("Active Components Response (0x{:02X})", msg.code); 223 | println!(" components: {:08b}", msg.data[0]); 224 | } 225 | DeviceEventCode::Capability => { 226 | println!("Capability (0x{:02X})", msg.code); 227 | println!(" capabilities: {:08b}", msg.data[0]); 228 | } 229 | DeviceEventCode::PlatformType => { 230 | println!("Platform Type (0x{:02X})", msg.code); 231 | 232 | let platform = PlatformType::from_primitive(msg.data[0]); 233 | match platform { 234 | PlatformType::Android => { 235 | println!(" platform: Android (0x{:02X})", msg.data[0]); 236 | println!(" SDK version: {:02X?})", msg.data[1]); 237 | } 238 | _ => { 239 | println!(" platform: Unknown (0x{:02X})", msg.data[0]); 240 | println!(" platform data: 0x{:02X?})", msg.data[1]); 241 | } 242 | } 243 | } 244 | DeviceEventCode::FirmwareVersion => { 245 | println!("Firmware Version (0x{:02X})", msg.code); 246 | if let Ok(ver) = std::str::from_utf8(&msg.data) { 247 | println!(" version: {:?}", ver); 248 | } else { 249 | println!(" version: {:02X?}", msg.data); 250 | } 251 | } 252 | DeviceEventCode::SectionNonce => { 253 | println!("Session Nonce (0x{:02X})", msg.code); 254 | println!(" nonce: {:02X?}", msg.data); 255 | } 256 | _ => { 257 | println!("Unknown (0x{:02X})", msg.code); 258 | print_message_body_unknown(msg); 259 | } 260 | } 261 | 262 | println!(); 263 | } 264 | EventGroup::DeviceAction => { 265 | let code = DeviceActionEventCode::from_primitive(msg.code); 266 | 267 | print!("Device Action (0x{:02X}) :: ", msg.group); 268 | 269 | match code { 270 | DeviceActionEventCode::Ring => { 271 | println!("Ring (0x{:02X})", msg.code); 272 | } 273 | _ => { 274 | println!("Unknown (0x{:02X})", msg.code); 275 | } 276 | } 277 | 278 | print_message_body_unknown(msg); 279 | println!(); 280 | } 281 | EventGroup::DeviceConfiguration => { 282 | let code = DeviceConfigurationEventCode::from_primitive(msg.code); 283 | 284 | print!("Device Configuration (0x{:02X}) :: ", msg.group); 285 | 286 | match code { 287 | DeviceConfigurationEventCode::BufferSize => { 288 | println!("Buffer Size (0x{:02X})", msg.code); 289 | } 290 | _ => { 291 | println!("Unknown (0x{:02X})", msg.code); 292 | } 293 | } 294 | 295 | print_message_body_unknown(msg); 296 | println!(); 297 | } 298 | EventGroup::DeviceCapabilitySync => { 299 | let code = DeviceCapabilitySyncEventCode::from_primitive(msg.code); 300 | 301 | print!("Device Cpabilities Sync (0x{:02X}) :: ", msg.group); 302 | 303 | match code { 304 | DeviceCapabilitySyncEventCode::CapabilityUpdate => { 305 | println!("Capability Update (0x{:02X})", msg.code); 306 | } 307 | DeviceCapabilitySyncEventCode::ConfigurableBufferSizeRange => { 308 | println!("Configurable Buffer Size Range (0x{:02X})", msg.code); 309 | } 310 | _ => { 311 | println!("Unknown (0x{:02X})", msg.code); 312 | } 313 | } 314 | 315 | print_message_body_unknown(msg); 316 | println!(); 317 | } 318 | EventGroup::SmartAudioSourceSwitching => { 319 | let code = SassEventCode::from_primitive(msg.code); 320 | 321 | print!("Smart Audio Source Switching (0x{:02X}) :: ", msg.group); 322 | 323 | match code { 324 | SassEventCode::GetCapabilityOfSass => { 325 | println!("Get Capability (0x{:02X})", msg.code); 326 | } 327 | SassEventCode::NotifyCapabilityOfSass => { 328 | println!("Notify Capability (0x{:02X})", msg.code); 329 | } 330 | SassEventCode::SetMultiPointState => { 331 | println!("Set Multi-Point State (0x{:02X})", msg.code); 332 | } 333 | SassEventCode::SwitchAudioSourceBetweenConnectedDevices => { 334 | println!("Switch Audio Source Between Connected Devices (0x{:02X})", msg.code); 335 | } 336 | SassEventCode::SwitchBack => { 337 | println!("Switch Back (0x{:02X})", msg.code); 338 | } 339 | SassEventCode::NotifyMultiPointSwitchEvent => { 340 | println!("Notify Multi-Point (0x{:02X})", msg.code); 341 | } 342 | SassEventCode::GetConnectionStatus => { 343 | println!("Get Connection Status (0x{:02X})", msg.code); 344 | } 345 | SassEventCode::NotifyConnectionStatus => { 346 | println!("Notify Connection Status (0x{:02X})", msg.code); 347 | } 348 | SassEventCode::SassInitiatedConnection => { 349 | println!("SASS Initiated Connection (0x{:02X})", msg.code); 350 | } 351 | SassEventCode::IndicateInUseAccountKey => { 352 | println!("Indicate In-Use Account Key (0x{:02X})", msg.code); 353 | } 354 | SassEventCode::SetCustomData => { 355 | println!("Set Custom Data (0x{:02X})", msg.code); 356 | } 357 | _ => { 358 | println!("Unknown (0x{:02X})", msg.code); 359 | } 360 | } 361 | 362 | print_message_body_unknown(msg); 363 | println!(); 364 | } 365 | EventGroup::Acknowledgement => { 366 | let code = AcknowledgementEventCode::from_primitive(msg.code); 367 | 368 | print!("Acknowledgement (0x{:02X}) ::", msg.group); 369 | 370 | match code { 371 | AcknowledgementEventCode::Ack => { 372 | println!("ACK (0x{:02X})", msg.code); 373 | println!(" group: 0x{:02X}", msg.data[0]); 374 | println!(" code: 0x{:02X}", msg.data[1]); 375 | println!(); 376 | } 377 | AcknowledgementEventCode::Nak => { 378 | println!("NAK (0x{:02X})", msg.code); 379 | match msg.data[0] { 380 | 0x00 => println!(" reason: Not supported (0x00)"), 381 | 0x01 => println!(" reason: Device busy (0x01)"), 382 | 0x02 => println!(" reason: Not allowed due to current state (0x02)"), 383 | _ => println!(" reason: Unknown (0x{:02X})", msg.data[0]), 384 | } 385 | println!(" group: 0x{:02X}", msg.data[1]); 386 | println!(" code: 0x{:02X}", msg.data[2]); 387 | println!(); 388 | } 389 | _ => { 390 | println!("Unknown (0x{:02X})", msg.code); 391 | print_message_body_unknown(msg); 392 | println!(); 393 | } 394 | } 395 | } 396 | _ => { 397 | println!( 398 | "Unknown (0x{:02X}) :: Unknown (0x{:02X})", 399 | msg.group, msg.code 400 | ); 401 | print_message_body_unknown(msg); 402 | println!(); 403 | } 404 | } 405 | } 406 | 407 | fn print_message_body_unknown(msg: &Message) { 408 | let data = pretty_hex::config_hex( 409 | &msg.data, 410 | pretty_hex::HexConfig { 411 | title: false, 412 | ..Default::default() 413 | }, 414 | ); 415 | 416 | for line in data.lines() { 417 | println!(" {}", line); 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /libgfps/examples/ring.rs: -------------------------------------------------------------------------------- 1 | //! Simple example for "ringing" the buds to locate them. 2 | //! 3 | //! WARNING: DO NOT RUN THIS EXAMPLE WITH THE BUDS IN YOUR EAR! YOU HAVE BEEN WARNED. 4 | //! 5 | //! Usage: 6 | //! cargo run --example ring -- 7 | 8 | use std::str::FromStr; 9 | 10 | use bluer::{Address, Session, Device}; 11 | use bluer::rfcomm::{Profile, Role, ProfileHandle, ReqError}; 12 | 13 | use futures::{StreamExt, SinkExt}; 14 | 15 | use gfps::msg::{Codec, Message, EventGroup, DeviceActionEventCode, AcknowledgementEventCode}; 16 | 17 | use num_enum::FromPrimitive; 18 | 19 | use smallvec::smallvec; 20 | 21 | 22 | #[tokio::main(flavor = "current_thread")] 23 | async fn main() -> bluer::Result<()> { 24 | // handle command line arguments 25 | let addr = std::env::args().nth(1).expect("need device address as argument"); 26 | let addr = Address::from_str(&addr)?; 27 | 28 | // set up session 29 | let session = Session::new().await?; 30 | let adapter = session.default_adapter().await?; 31 | 32 | // get device 33 | let dev = adapter.device(addr)?; 34 | 35 | // get RFCOMM stream 36 | let stream = { 37 | // register GFPS profile 38 | let profile = Profile { 39 | uuid: gfps::msg::UUID, 40 | role: Some(Role::Client), 41 | require_authentication: Some(false), 42 | require_authorization: Some(false), 43 | auto_connect: Some(false), 44 | ..Default::default() 45 | }; 46 | 47 | let mut profile_handle = session.register_profile(profile).await?; 48 | 49 | // connect profile 50 | connect_device_to_profile(&mut profile_handle, &dev).await? 51 | }; 52 | 53 | // set up message stream 54 | let codec = Codec::new(); 55 | let mut stream = codec.wrap(stream); 56 | 57 | // send "ring" message 58 | // 59 | // Note: Pixel Buds Pro ignore messages with a timeout. So don't specify 60 | // one here. 61 | let msg = Message { 62 | group: EventGroup::DeviceAction.into(), 63 | code: DeviceActionEventCode::Ring.into(), 64 | data: smallvec![0x03], // 0b01: right, 0b10: left, 0b10|0b01 = 0b11: both 65 | }; 66 | 67 | println!("Ringing buds..."); 68 | stream.send(&msg).await?; 69 | 70 | // An ACK message should come in 1s. Wait for that. 71 | let timeout = tokio::time::Instant::now() + tokio::time::Duration::from_secs(1); 72 | loop { 73 | tokio::select! { 74 | msg = stream.next() => { 75 | match msg { 76 | Some(Ok(msg)) => { 77 | println!("{:?}", msg); 78 | 79 | let group = EventGroup::from_primitive(msg.group); 80 | if group != EventGroup::Acknowledgement { 81 | continue; 82 | } 83 | 84 | let ack_group = EventGroup::from_primitive(msg.data[0]); 85 | if ack_group != EventGroup::DeviceAction { 86 | continue; 87 | } 88 | 89 | let ack_code = DeviceActionEventCode::from_primitive(msg.data[1]); 90 | if ack_code != DeviceActionEventCode::Ring { 91 | continue; 92 | } 93 | 94 | let code = AcknowledgementEventCode::from_primitive(msg.code); 95 | 96 | if code == AcknowledgementEventCode::Ack { 97 | println!("Received ACK for ring command"); 98 | break; 99 | 100 | } else if code == AcknowledgementEventCode::Nak { 101 | println!("Received NAK for ring command"); 102 | 103 | let err = std::io::Error::new( 104 | std::io::ErrorKind::Unsupported, 105 | "ring has been NAK'ed by device" 106 | ); 107 | 108 | Err(err)?; 109 | } 110 | }, 111 | Some(Err(e)) => { 112 | Err(e)?; 113 | }, 114 | None => { 115 | let err = std::io::Error::new( 116 | std::io::ErrorKind::ConnectionAborted, 117 | "connection closed" 118 | ); 119 | 120 | Err(err)?; 121 | } 122 | } 123 | }, 124 | _ = tokio::time::sleep_until(timeout) => { 125 | let err = std::io::Error::new( 126 | std::io::ErrorKind::TimedOut, 127 | "timed out, ring action might be unsupported" 128 | ); 129 | 130 | Err(err)?; 131 | }, 132 | } 133 | } 134 | 135 | // Next, the device will communicate back status updates. This may include 136 | // an initial update to confirm ringing and follow-up updates once the user 137 | // has touched the buds and ringing stops. 138 | // 139 | // Stop this program once we have no more rining or once we have reached a 140 | // timeout of 30s. 141 | let mut timeout = tokio::time::Instant::now() + tokio::time::Duration::from_secs(30); 142 | loop { 143 | tokio::select! { 144 | msg = stream.next() => { 145 | match msg { 146 | Some(Ok(msg)) => { 147 | println!("{:?}", msg); 148 | 149 | let group = EventGroup::from_primitive(msg.group); 150 | if group != EventGroup::DeviceAction { 151 | continue; 152 | } 153 | // send ACK 154 | let ack = Message { 155 | group: EventGroup::Acknowledgement.into(), 156 | code: AcknowledgementEventCode::Ack.into(), 157 | data: smallvec![msg.group, msg.code], 158 | }; 159 | 160 | stream.send(&ack).await?; 161 | 162 | let status = msg.data[0]; 163 | 164 | println!("Received ring update:"); 165 | 166 | if status & 0b01 != 0 { 167 | println!(" right: ringing"); 168 | } else { 169 | println!(" right: not ringing"); 170 | } 171 | 172 | if status & 0b10 != 0 { 173 | println!(" left: ringing"); 174 | } else { 175 | println!(" left: not ringing"); 176 | } 177 | 178 | if status & 0b11 == 0 { 179 | println!("Buds stopped ringing, exiting..."); 180 | return Ok(()); 181 | } 182 | }, 183 | Some(Err(e)) => { 184 | Err(e)?; 185 | }, 186 | None => { 187 | let err = std::io::Error::new( 188 | std::io::ErrorKind::ConnectionAborted, 189 | "connection closed" 190 | ); 191 | 192 | Err(err)?; 193 | } 194 | } 195 | }, 196 | _ = tokio::time::sleep_until(timeout) => { 197 | println!("Sending command to stop ringing..."); 198 | 199 | // send message to stop ringing 200 | let msg = Message { 201 | group: EventGroup::DeviceAction.into(), 202 | code: DeviceActionEventCode::Ring.into(), 203 | data: smallvec![0x00], 204 | }; 205 | 206 | stream.send(&msg).await?; 207 | 208 | timeout = tokio::time::Instant::now() + tokio::time::Duration::from_secs(10); 209 | }, 210 | } 211 | } 212 | } 213 | 214 | async fn connect_device_to_profile(profile: &mut ProfileHandle, dev: &Device) 215 | -> bluer::Result 216 | { 217 | loop { 218 | tokio::select! { 219 | res = async { 220 | let _ = dev.connect().await; 221 | dev.connect_profile(&gfps::msg::UUID).await 222 | } => { 223 | if let Err(err) = res { 224 | println!("Connecting GFPS profile failed: {:?}", err); 225 | } 226 | tokio::time::sleep(std::time::Duration::from_millis(3000)).await; 227 | }, 228 | req = profile.next() => { 229 | let req = req.expect("no connection request received"); 230 | 231 | if req.device() == dev.address() { 232 | // accept our device 233 | break req.accept(); 234 | } else { 235 | // reject unknown devices 236 | req.reject(ReqError::Rejected); 237 | } 238 | }, 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /libgfps/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Library for the Google Fast Pair Service protocol (GFPS). Focussed on 2 | //! communication via the dedicated GFPS RFCOMM channel. 3 | //! 4 | //! See for the specification. 5 | 6 | pub mod msg; 7 | -------------------------------------------------------------------------------- /libgfps/src/msg/codec.rs: -------------------------------------------------------------------------------- 1 | use super::Message; 2 | 3 | use bytes::{Buf, BytesMut, BufMut}; 4 | 5 | use tokio::io::{AsyncRead, AsyncWrite}; 6 | use tokio_util::codec::{Decoder, Framed, Encoder}; 7 | 8 | 9 | const MAX_FRAME_SIZE: u16 = 4096; 10 | 11 | 12 | pub struct Codec {} 13 | 14 | impl Codec { 15 | pub fn new() -> Self { 16 | Self {} 17 | } 18 | 19 | pub fn wrap(self, io: T) -> Framed 20 | where 21 | T: AsyncRead + AsyncWrite, 22 | { 23 | Framed::with_capacity(io, self, MAX_FRAME_SIZE as _) 24 | } 25 | } 26 | 27 | impl Default for Codec { 28 | fn default() -> Self { 29 | Self::new() 30 | } 31 | } 32 | 33 | impl Decoder for Codec { 34 | type Item = Message; 35 | type Error = std::io::Error; 36 | 37 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 38 | if src.len() < 4 { 39 | return Ok(None); 40 | } 41 | 42 | let group = src[0]; 43 | let code = src[1]; 44 | 45 | let mut length = [0; 2]; 46 | length.copy_from_slice(&src[2..4]); 47 | let length = u16::from_be_bytes(length); 48 | 49 | if length > MAX_FRAME_SIZE { 50 | Err(std::io::Error::new( 51 | std::io::ErrorKind::InvalidData, 52 | format!("Frame of length {length} is too large (group: {group}, code: {code})."), 53 | ))?; 54 | } 55 | 56 | let size = 4 + length as usize; 57 | 58 | if src.len() < size as _ { 59 | src.reserve(size - src.len()); 60 | return Ok(None); 61 | } 62 | 63 | let data = src[4..size].into(); 64 | src.advance(size); 65 | 66 | Ok(Some(Message { 67 | group, 68 | code, 69 | data, 70 | })) 71 | } 72 | } 73 | 74 | impl Encoder<&Message> for Codec { 75 | type Error = std::io::Error; 76 | 77 | fn encode(&mut self, msg: &Message, buf: &mut BytesMut) -> Result<(), Self::Error> { 78 | let size = msg.data.len() + 4; 79 | 80 | if size > MAX_FRAME_SIZE as usize { 81 | Err(std::io::Error::new( 82 | std::io::ErrorKind::InvalidInput, 83 | format!("Frame of length {size} is too large."), 84 | ))?; 85 | } 86 | 87 | buf.reserve(size); 88 | buf.put_u8(msg.group); 89 | buf.put_u8(msg.code); 90 | buf.put_slice(&(msg.data.len() as u16).to_be_bytes()); 91 | buf.put_slice(&msg.data); 92 | 93 | Ok(()) 94 | } 95 | } 96 | 97 | 98 | #[cfg(test)] 99 | mod test { 100 | use super::*; 101 | use crate::msg::{EventGroup, DeviceEventCode, Message}; 102 | 103 | use bytes::BytesMut; 104 | 105 | use smallvec::smallvec; 106 | 107 | 108 | #[test] 109 | fn test_encode() { 110 | let mut buf = BytesMut::new(); 111 | let mut codec = Codec::new(); 112 | 113 | let msg = Message { 114 | group: EventGroup::Device.into(), 115 | code: DeviceEventCode::ModelId.into(), 116 | data: smallvec![0x00, 0x01, 0x02, 0x04, 0x05], 117 | }; 118 | 119 | // try to encode the message 120 | codec.encode(&msg, &mut buf) 121 | .expect("error encode message"); 122 | 123 | let raw = [0x03, 0x01, 0x00, 0x05, 0x00, 0x01, 0x02, 0x04, 0x05]; 124 | assert_eq!(&buf[..], &raw[..]); 125 | } 126 | 127 | #[test] 128 | fn test_decode() { 129 | let mut codec = Codec::new(); 130 | 131 | let raw = [0x03, 0x01, 0x00, 0x03, 0x00, 0x01, 0x02]; 132 | let mut buf = BytesMut::from(&raw[..]); 133 | 134 | let msg = Message { 135 | group: EventGroup::Device.into(), 136 | code: DeviceEventCode::ModelId.into(), 137 | data: smallvec![0x00, 0x01, 0x02], 138 | }; 139 | 140 | // try to encode the message 141 | let decoded = codec.decode(&mut buf) 142 | .expect("error decoding message") 143 | .expect("message incomplete"); 144 | 145 | assert_eq!(decoded, msg); 146 | } 147 | 148 | #[test] 149 | fn test_decode_incomplete() { 150 | let mut codec = Codec::new(); 151 | 152 | let raw = [0x03, 0x01, 0x00, 0x03, 0x00]; 153 | let mut buf = BytesMut::from(&raw[..]); 154 | 155 | // try to encode the message 156 | let decoded = codec.decode(&mut buf) 157 | .expect("error decoding message"); 158 | 159 | assert_eq!(decoded, None); 160 | } 161 | 162 | #[test] 163 | fn test_encode_decode() { 164 | let mut buf = BytesMut::new(); 165 | let mut codec = Codec::new(); 166 | 167 | let msg = Message { 168 | group: 0, 169 | code: 0, 170 | data: smallvec![0x00, 0x01, 0x02], 171 | }; 172 | 173 | // try to encode the message 174 | codec.encode(&msg, &mut buf) 175 | .expect("error encode message"); 176 | 177 | // try to decode the message we just encoded 178 | let decoded = codec.decode(&mut buf) 179 | .expect("error decoding message") 180 | .expect("message incomplete"); 181 | 182 | assert_eq!(decoded, msg); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /libgfps/src/msg/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types for GFPS Message Stream via RFCOMM. 2 | 3 | use uuid::{uuid, Uuid}; 4 | 5 | /// UUID under which the GFPS Message Stream is advertised. 6 | /// 7 | /// Defined as `df21fe2c-2515-4fdb-8886-f12c4d67927c`. 8 | pub const UUID: Uuid = uuid!("df21fe2c-2515-4fdb-8886-f12c4d67927c"); 9 | 10 | mod codec; 11 | pub use codec::Codec; 12 | 13 | mod types; 14 | pub use types::*; 15 | -------------------------------------------------------------------------------- /libgfps/src/msg/types.rs: -------------------------------------------------------------------------------- 1 | //! RFCOMM events and event-related enums. 2 | 3 | use std::fmt::Display; 4 | 5 | use num_enum::{IntoPrimitive, FromPrimitive}; 6 | 7 | use smallvec::SmallVec; 8 | 9 | 10 | #[derive(Debug, Clone, PartialEq, Eq)] 11 | pub struct Message { 12 | pub group: u8, 13 | pub code: u8, 14 | pub data: SmallVec<[u8; 8]>, 15 | } 16 | 17 | 18 | #[non_exhaustive] 19 | #[repr(u8)] 20 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 21 | pub enum EventGroup { 22 | Bluetooth = 0x01, 23 | Logging = 0x02, 24 | Device = 0x03, 25 | DeviceAction = 0x04, 26 | DeviceConfiguration = 0x05, 27 | DeviceCapabilitySync = 0x06, 28 | SmartAudioSourceSwitching = 0x07, 29 | Acknowledgement = 0xff, 30 | 31 | #[num_enum(catch_all)] 32 | Unknown(u8) = 0xfe, 33 | } 34 | 35 | #[non_exhaustive] 36 | #[repr(u8)] 37 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 38 | pub enum BluetoothEventCode { 39 | EnableSilenceMode = 0x01, 40 | DisableSilenceMode = 0x02, 41 | 42 | #[num_enum(catch_all)] 43 | Unknown(u8), 44 | } 45 | 46 | #[non_exhaustive] 47 | #[repr(u8)] 48 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 49 | pub enum LoggingEventCode { 50 | LogFull = 0x01, 51 | LogSaveToBuffer = 0x02, 52 | 53 | #[num_enum(catch_all)] 54 | Unknown(u8), 55 | } 56 | 57 | #[non_exhaustive] 58 | #[repr(u8)] 59 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 60 | pub enum DeviceEventCode { 61 | ModelId = 0x01, 62 | BleAddress = 0x02, 63 | BatteryInfo = 0x03, 64 | BatteryTime = 0x04, 65 | ActiveComponentsRequest = 0x05, 66 | ActiveComponentsResponse = 0x06, 67 | Capability = 0x07, 68 | PlatformType = 0x08, 69 | FirmwareVersion = 0x09, 70 | SectionNonce = 0x0a, 71 | 72 | #[num_enum(catch_all)] 73 | Unknown(u8), 74 | } 75 | 76 | #[non_exhaustive] 77 | #[repr(u8)] 78 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 79 | pub enum DeviceActionEventCode { 80 | Ring = 0x01, 81 | 82 | #[num_enum(catch_all)] 83 | Unknown(u8), 84 | } 85 | 86 | #[non_exhaustive] 87 | #[repr(u8)] 88 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 89 | pub enum DeviceConfigurationEventCode { 90 | BufferSize = 0x01, 91 | 92 | #[num_enum(catch_all)] 93 | Unknown(u8), 94 | } 95 | 96 | #[non_exhaustive] 97 | #[repr(u8)] 98 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 99 | pub enum DeviceCapabilitySyncEventCode { 100 | CapabilityUpdate = 0x01, 101 | ConfigurableBufferSizeRange = 0x02, 102 | 103 | #[num_enum(catch_all)] 104 | Unknown(u8), 105 | } 106 | 107 | #[non_exhaustive] 108 | #[repr(u8)] 109 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 110 | pub enum SassEventCode { 111 | GetCapabilityOfSass = 0x10, 112 | NotifyCapabilityOfSass = 0x11, 113 | SetMultiPointState = 0x12, 114 | SwitchAudioSourceBetweenConnectedDevices = 0x30, 115 | SwitchBack = 0x31, 116 | NotifyMultiPointSwitchEvent = 0x32, 117 | GetConnectionStatus = 0x33, 118 | NotifyConnectionStatus = 0x34, 119 | SassInitiatedConnection = 0x40, 120 | IndicateInUseAccountKey = 0x41, 121 | SetCustomData = 0x42, 122 | 123 | #[num_enum(catch_all)] 124 | Unknown(u8), 125 | } 126 | 127 | #[non_exhaustive] 128 | #[repr(u8)] 129 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 130 | pub enum AcknowledgementEventCode { 131 | Ack = 0x01, 132 | Nak = 0x02, 133 | 134 | #[num_enum(catch_all)] 135 | Unknown(u8), 136 | } 137 | 138 | #[non_exhaustive] 139 | #[repr(u8)] 140 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 141 | pub enum PlatformType { 142 | Android = 0x01, 143 | 144 | #[num_enum(catch_all)] 145 | Unknown(u8), 146 | } 147 | 148 | 149 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 150 | pub enum BatteryInfo { 151 | #[default] 152 | Unknown, 153 | Known { 154 | is_charging: bool, 155 | percent: u8, 156 | }, 157 | } 158 | 159 | impl BatteryInfo { 160 | pub fn from_byte(value: u8) -> Self { 161 | if value & 0x7F == 0x7F { 162 | BatteryInfo::Unknown 163 | } else { 164 | BatteryInfo::Known { 165 | is_charging: (value & 0x80) != 0, 166 | percent: value & 0x7F, 167 | } 168 | } 169 | } 170 | 171 | pub fn to_byte(&self) -> u8 { 172 | match self { 173 | BatteryInfo::Unknown => 0xFF, 174 | BatteryInfo::Known { is_charging: true, percent } => 0x80 | (0x7F & percent), 175 | BatteryInfo::Known { is_charging: false, percent } => 0x7F & percent, 176 | } 177 | } 178 | } 179 | 180 | impl Display for BatteryInfo { 181 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 182 | match self { 183 | BatteryInfo::Unknown => { 184 | write!(f, "unknown") 185 | } 186 | BatteryInfo::Known { is_charging: true, percent } => { 187 | write!(f, "{percent}% (charging)") 188 | } 189 | BatteryInfo::Known { is_charging: false, percent } => { 190 | write!(f, "{percent}% (not charging)") 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /libmaestro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maestro" 3 | authors = ["Maximilian Luz "] 4 | version = "0.1.5" 5 | edition = "2024" 6 | license = "MIT/Apache-2.0" 7 | description = "Maestro protocol client implementation for controlling Google Pixel Buds Pro" 8 | repository = "https://github.com/qzed/pbpctrl" 9 | 10 | [dependencies] 11 | arrayvec = "0.7.6" 12 | bytes = "1.10.1" 13 | futures = "0.3.31" 14 | num_enum = "0.7.3" 15 | prost = "0.13.5" 16 | tokio = { version = "1.44.2", features = ["macros"] } 17 | tokio-util = { version = "0.7.14", features = ["codec"] } 18 | tracing = "0.1.41" 19 | uuid = "1.16.0" 20 | 21 | [build-dependencies] 22 | prost-build = "0.13.5" 23 | 24 | [dev-dependencies] 25 | anyhow = "1.0.98" 26 | bluer = { version = "0.17.3", features = ["bluetoothd", "rfcomm"] } 27 | futures = "0.3.31" 28 | pretty-hex = "0.4.1" 29 | tokio = { version = "1.44.2", features = ["rt", "macros", "signal"] } 30 | tracing-subscriber = "0.3.19" 31 | -------------------------------------------------------------------------------- /libmaestro/build.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | 3 | fn main() -> Result<()> { 4 | prost_build::compile_protos(&["proto/pw.rpc.packet.proto"], &["proto/"])?; 5 | prost_build::compile_protos(&["proto/maestro_pw.proto"], &["proto/"])?; 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /libmaestro/examples/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | 5 | use bluer::{Address, Device, Session}; 6 | use bluer::rfcomm::{ProfileHandle, Role, ReqError, Stream, Profile}; 7 | 8 | use futures::StreamExt; 9 | 10 | use maestro::pwrpc::Error; 11 | use maestro::pwrpc::client::Client; 12 | use maestro::pwrpc::types::RpcPacket; 13 | 14 | 15 | pub async fn run_client(mut client: Client) -> Result<()> 16 | where 17 | S: futures::Sink, 18 | S: futures::Stream> + Unpin, 19 | Error: From, 20 | Error: From, 21 | { 22 | tokio::select! { 23 | res = client.run() => { 24 | res?; 25 | }, 26 | sig = tokio::signal::ctrl_c() => { 27 | sig?; 28 | tracing::trace!("client termination requested"); 29 | }, 30 | } 31 | 32 | client.terminate().await?; 33 | Ok(()) 34 | } 35 | 36 | pub async fn connect_maestro_rfcomm(session: &Session, dev: &Device) -> Result { 37 | let maestro_profile = Profile { 38 | uuid: maestro::UUID, 39 | role: Some(Role::Client), 40 | require_authentication: Some(false), 41 | require_authorization: Some(false), 42 | auto_connect: Some(false), 43 | ..Default::default() 44 | }; 45 | 46 | tracing::debug!("registering maestro profile"); 47 | let mut handle = session.register_profile(maestro_profile).await?; 48 | 49 | tracing::debug!("connecting to maestro profile"); 50 | let stream = tokio::try_join!( 51 | try_connect_profile(dev), 52 | handle_requests_for_profile(&mut handle, dev.address()), 53 | )?.1; 54 | 55 | Ok(stream) 56 | } 57 | 58 | async fn try_connect_profile(dev: &Device) -> Result<()> { 59 | const RETRY_TIMEOUT: Duration = Duration::from_secs(1); 60 | const MAX_TRIES: u32 = 3; 61 | 62 | let mut i = 0; 63 | while let Err(err) = dev.connect_profile(&maestro::UUID).await { 64 | if i >= MAX_TRIES { return Err(err.into()) } 65 | i += 1; 66 | 67 | tracing::warn!(error=?err, "connecting to profile failed, trying again ({}/{})", i, MAX_TRIES); 68 | 69 | tokio::time::sleep(RETRY_TIMEOUT).await; 70 | } 71 | 72 | tracing::debug!(address=%dev.address(), "maestro profile connected"); 73 | Ok(()) 74 | } 75 | 76 | async fn handle_requests_for_profile(handle: &mut ProfileHandle, address: Address) -> Result { 77 | while let Some(req) = handle.next().await { 78 | tracing::debug!(address=%req.device(), "received new profile connection request"); 79 | 80 | if req.device() == address { 81 | tracing::debug!(address=%req.device(), "accepting profile connection request"); 82 | return Ok(req.accept()?); 83 | } else { 84 | req.reject(ReqError::Rejected); 85 | } 86 | } 87 | 88 | anyhow::bail!("profile terminated without requests") 89 | } 90 | -------------------------------------------------------------------------------- /libmaestro/examples/maestro_get_battery.rs: -------------------------------------------------------------------------------- 1 | //! Simple example for reading battery info via the Maestro service. 2 | //! 3 | //! Usage: 4 | //! cargo run --example maestro_get_battery -- 5 | 6 | mod common; 7 | 8 | use std::str::FromStr; 9 | 10 | use anyhow::bail; 11 | use bluer::{Address, Session}; 12 | use futures::StreamExt; 13 | 14 | use maestro::protocol::codec::Codec; 15 | use maestro::protocol::types::RuntimeInfo; 16 | use maestro::protocol::utils; 17 | use maestro::pwrpc::client::{Client, ClientHandle}; 18 | use maestro::service::MaestroService; 19 | 20 | 21 | #[tokio::main(flavor = "current_thread")] 22 | async fn main() -> Result<(), anyhow::Error> { 23 | tracing_subscriber::fmt::init(); 24 | 25 | // handle command line arguments 26 | let addr = std::env::args().nth(1).expect("need device address as argument"); 27 | let addr = Address::from_str(&addr)?; 28 | 29 | // set up session 30 | let session = Session::new().await?; 31 | let adapter = session.default_adapter().await?; 32 | 33 | println!("Using adapter '{}'", adapter.name()); 34 | 35 | // get device 36 | let dev = adapter.device(addr)?; 37 | let uuids = { 38 | let mut uuids = Vec::from_iter(dev.uuids().await? 39 | .unwrap_or_default() 40 | .into_iter()); 41 | 42 | uuids.sort_unstable(); 43 | uuids 44 | }; 45 | 46 | println!("Found device:"); 47 | println!(" alias: {}", dev.alias().await?); 48 | println!(" address: {}", dev.address()); 49 | println!(" paired: {}", dev.is_paired().await?); 50 | println!(" connected: {}", dev.is_connected().await?); 51 | println!(" UUIDs:"); 52 | for uuid in uuids { 53 | println!(" {}", uuid); 54 | } 55 | println!(); 56 | 57 | println!("Connecting to Maestro profile"); 58 | let stream = common::connect_maestro_rfcomm(&session, &dev).await?; 59 | 60 | println!("Profile connected"); 61 | 62 | // set up stream for RPC communication 63 | let codec = Codec::new(); 64 | let stream = codec.wrap(stream); 65 | 66 | // set up RPC client 67 | let mut client = Client::new(stream); 68 | let handle = client.handle(); 69 | 70 | // retreive the channel numer 71 | let channel = utils::resolve_channel(&mut client).await?; 72 | 73 | let exec_task = common::run_client(client); 74 | let battery_task = get_battery(handle, channel); 75 | 76 | let info = tokio::select! { 77 | res = exec_task => { 78 | match res { 79 | Ok(_) => bail!("client terminated unexpectedly without error"), 80 | Err(e) => Err(e), 81 | } 82 | }, 83 | res = battery_task => res, 84 | }?; 85 | 86 | let info = info.battery_info 87 | .expect("did not receive battery status in runtime-info-changed event"); 88 | 89 | println!("Battery status:"); 90 | 91 | if let Some(info) = info.case { 92 | match info.state { 93 | 1 => println!(" case: {}% (not charging)", info.level), 94 | 2 => println!(" case: {}% (charging)", info.level), 95 | x => println!(" case: {}% (unknown state: {})", info.level, x), 96 | } 97 | } else { 98 | println!(" case: unknown"); 99 | } 100 | 101 | if let Some(info) = info.left { 102 | match info.state { 103 | 1 => println!(" left: {}% (not charging)", info.level), 104 | 2 => println!(" left: {}% (charging)", info.level), 105 | x => println!(" left: {}% (unknown state: {})", info.level, x), 106 | } 107 | } else { 108 | println!(" left: unknown"); 109 | } 110 | 111 | if let Some(info) = info.right { 112 | match info.state { 113 | 1 => println!(" right: {}% (not charging)", info.level), 114 | 2 => println!(" right: {}% (charging)", info.level), 115 | x => println!(" right: {}% (unknown state: {})", info.level, x), 116 | } 117 | } else { 118 | println!(" right: unknown"); 119 | } 120 | 121 | Ok(()) 122 | } 123 | 124 | async fn get_battery(handle: ClientHandle, channel: u32) -> anyhow::Result { 125 | println!("Reading battery info..."); 126 | println!(); 127 | 128 | let mut service = MaestroService::new(handle, channel); 129 | 130 | let mut call = service.subscribe_to_runtime_info()?; 131 | let rt_info = if let Some(msg) = call.stream().next().await { 132 | msg? 133 | } else { 134 | bail!("did not receive any runtime-info event"); 135 | }; 136 | 137 | call.cancel_and_wait().await?; 138 | Ok(rt_info) 139 | } 140 | -------------------------------------------------------------------------------- /libmaestro/examples/maestro_listen.rs: -------------------------------------------------------------------------------- 1 | //! Simple example for listening to Maestro messages sent via the RFCOMM channel. 2 | //! 3 | //! Usage: 4 | //! cargo run --example maestro_listen -- 5 | 6 | mod common; 7 | 8 | use std::str::FromStr; 9 | 10 | use bluer::{Address, Session}; 11 | use futures::StreamExt; 12 | 13 | use maestro::protocol::codec::Codec; 14 | use maestro::protocol::utils; 15 | use maestro::pwrpc::client::{Client, ClientHandle}; 16 | use maestro::service::{MaestroService, DosimeterService}; 17 | 18 | 19 | #[tokio::main(flavor = "current_thread")] 20 | async fn main() -> Result<(), anyhow::Error> { 21 | tracing_subscriber::fmt::init(); 22 | 23 | // handle command line arguments 24 | let addr = std::env::args().nth(1).expect("need device address as argument"); 25 | let addr = Address::from_str(&addr)?; 26 | 27 | // set up session 28 | let session = Session::new().await?; 29 | let adapter = session.default_adapter().await?; 30 | 31 | println!("Using adapter '{}'", adapter.name()); 32 | 33 | // get device 34 | let dev = adapter.device(addr)?; 35 | let uuids = { 36 | let mut uuids = Vec::from_iter(dev.uuids().await? 37 | .unwrap_or_default() 38 | .into_iter()); 39 | 40 | uuids.sort_unstable(); 41 | uuids 42 | }; 43 | 44 | println!("Found device:"); 45 | println!(" alias: {}", dev.alias().await?); 46 | println!(" address: {}", dev.address()); 47 | println!(" paired: {}", dev.is_paired().await?); 48 | println!(" connected: {}", dev.is_connected().await?); 49 | println!(" UUIDs:"); 50 | for uuid in uuids { 51 | println!(" {}", uuid); 52 | } 53 | println!(); 54 | 55 | // try to reconnect if connection is reset 56 | loop { 57 | println!("Connecting to Maestro profile"); 58 | let stream = common::connect_maestro_rfcomm(&session, &dev).await?; 59 | 60 | println!("Profile connected"); 61 | 62 | // set up stream for RPC communication 63 | let codec = Codec::new(); 64 | let stream = codec.wrap(stream); 65 | 66 | // set up RPC client 67 | let mut client = Client::new(stream); 68 | let handle = client.handle(); 69 | 70 | // retreive the channel numer 71 | let channel = utils::resolve_channel(&mut client).await?; 72 | 73 | let exec_task = common::run_client(client); 74 | let listen_task = run_listener(handle, channel); 75 | 76 | tokio::select! { 77 | res = exec_task => { 78 | match res { 79 | Ok(_) => { 80 | tracing::trace!("client terminated successfully"); 81 | return Ok(()); 82 | }, 83 | Err(e) => { 84 | tracing::error!("client task terminated with error"); 85 | 86 | let cause = e.root_cause(); 87 | if let Some(cause) = cause.downcast_ref::() { 88 | if cause.raw_os_error() == Some(104) { 89 | // The Pixel Buds Pro can hand off processing between each 90 | // other. On a switch, the connection is reset. Wait a bit 91 | // and then try to reconnect. 92 | println!(); 93 | println!("Connection reset. Attempting to reconnect..."); 94 | tokio::time::sleep(std::time::Duration::from_millis(500)).await; 95 | continue; 96 | } 97 | } 98 | 99 | return Err(e); 100 | }, 101 | } 102 | }, 103 | res = listen_task => { 104 | match res { 105 | Ok(_) => { 106 | tracing::error!("server terminated stream"); 107 | return Ok(()); 108 | } 109 | Err(e) => { 110 | tracing::error!("main task terminated with error"); 111 | return Err(e); 112 | } 113 | } 114 | }, 115 | } 116 | } 117 | } 118 | 119 | async fn run_listener(handle: ClientHandle, channel: u32) -> anyhow::Result<()> { 120 | println!("Sending GetSoftwareInfo request"); 121 | println!(); 122 | 123 | let mut service = MaestroService::new(handle.clone(), channel); 124 | let mut dosimeter = DosimeterService::new(handle, channel); 125 | 126 | let info = service.get_software_info().await?; 127 | println!("{:#?}", info); 128 | 129 | let info = dosimeter.fetch_daily_summaries().await?; 130 | println!("{:#?}", info); 131 | 132 | println!(); 133 | println!("Listening to settings changes..."); 134 | println!(); 135 | 136 | let task_rtinfo = run_listener_rtinfo(service.clone()); 137 | let task_settings = run_listener_settings(service.clone()); 138 | let task_dosimeter = run_listener_dosimeter(dosimeter.clone()); 139 | 140 | tokio::select! { 141 | res = task_rtinfo => res, 142 | res = task_settings => res, 143 | res = task_dosimeter => res, 144 | } 145 | } 146 | 147 | async fn run_listener_rtinfo(mut service: MaestroService) -> anyhow::Result<()> { 148 | let mut call = service.subscribe_to_runtime_info()?; 149 | while let Some(msg) = call.stream().next().await { 150 | println!("{:#?}", msg?); 151 | } 152 | 153 | Ok(()) 154 | } 155 | 156 | async fn run_listener_settings(mut service: MaestroService) -> anyhow::Result<()> { 157 | let mut call = service.subscribe_to_settings_changes()?; 158 | while let Some(msg) = call.stream().next().await { 159 | println!("{:#?}", msg?); 160 | } 161 | 162 | Ok(()) 163 | } 164 | 165 | async fn run_listener_dosimeter(mut service: DosimeterService) -> anyhow::Result<()> { 166 | let mut call = service.subscribe_to_live_db()?; 167 | while let Some(msg) = call.stream().next().await { 168 | println!("volume: {:#?} dB", (msg.unwrap().intensity.log10() * 10.0).round()); 169 | } 170 | 171 | Ok(()) 172 | } 173 | -------------------------------------------------------------------------------- /libmaestro/examples/maestro_read_settings.rs: -------------------------------------------------------------------------------- 1 | //! Simple example for reading settings on the Pixel Buds Pro via the Maestro service. 2 | //! 3 | //! Usage: 4 | //! cargo run --example maestro_read_settings -- 5 | 6 | mod common; 7 | 8 | use std::str::FromStr; 9 | 10 | use anyhow::bail; 11 | use bluer::{Address, Session}; 12 | 13 | use maestro::protocol::codec::Codec; 14 | use maestro::protocol::utils; 15 | use maestro::pwrpc::client::{Client, ClientHandle}; 16 | use maestro::service::MaestroService; 17 | use maestro::service::settings::{self, SettingId}; 18 | 19 | 20 | #[tokio::main(flavor = "current_thread")] 21 | async fn main() -> Result<(), anyhow::Error> { 22 | tracing_subscriber::fmt::init(); 23 | 24 | // handle command line arguments 25 | let addr = std::env::args().nth(1).expect("need device address as argument"); 26 | let addr = Address::from_str(&addr)?; 27 | 28 | // set up session 29 | let session = Session::new().await?; 30 | let adapter = session.default_adapter().await?; 31 | 32 | println!("Using adapter '{}'", adapter.name()); 33 | 34 | // get device 35 | let dev = adapter.device(addr)?; 36 | let uuids = { 37 | let mut uuids = Vec::from_iter(dev.uuids().await? 38 | .unwrap_or_default() 39 | .into_iter()); 40 | 41 | uuids.sort_unstable(); 42 | uuids 43 | }; 44 | 45 | println!("Found device:"); 46 | println!(" alias: {}", dev.alias().await?); 47 | println!(" address: {}", dev.address()); 48 | println!(" paired: {}", dev.is_paired().await?); 49 | println!(" connected: {}", dev.is_connected().await?); 50 | println!(" UUIDs:"); 51 | for uuid in uuids { 52 | println!(" {}", uuid); 53 | } 54 | println!(); 55 | 56 | println!("Connecting to Maestro profile"); 57 | let stream = common::connect_maestro_rfcomm(&session, &dev).await?; 58 | 59 | println!("Profile connected"); 60 | 61 | // set up stream for RPC communication 62 | let codec = Codec::new(); 63 | let stream = codec.wrap(stream); 64 | 65 | // set up RPC client 66 | let mut client = Client::new(stream); 67 | let handle = client.handle(); 68 | 69 | // retreive the channel numer 70 | let channel = utils::resolve_channel(&mut client).await?; 71 | 72 | let exec_task = common::run_client(client); 73 | let settings_task = read_settings(handle, channel); 74 | 75 | tokio::select! { 76 | res = exec_task => { 77 | match res { 78 | Ok(_) => bail!("client terminated unexpectedly without error"), 79 | Err(e) => Err(e), 80 | } 81 | }, 82 | res = settings_task => res, 83 | } 84 | } 85 | 86 | async fn read_settings(handle: ClientHandle, channel: u32) -> anyhow::Result<()> { 87 | let mut service = MaestroService::new(handle.clone(), channel); 88 | 89 | println!(); 90 | println!("Read via types:"); 91 | 92 | // read some typed settings via proxy structs 93 | let value = service.read_setting(settings::id::AutoOtaEnable).await?; 94 | println!(" Auto-OTA enabled: {}", value); 95 | 96 | let value = service.read_setting(settings::id::OhdEnable).await?; 97 | println!(" OHD enabled: {}", value); 98 | 99 | let value = service.read_setting(settings::id::OobeIsFinished).await?; 100 | println!(" OOBE finished: {}", value); 101 | 102 | let value = service.read_setting(settings::id::GestureEnable).await?; 103 | println!(" Gestures enabled: {}", value); 104 | 105 | let value = service.read_setting(settings::id::DiagnosticsEnable).await?; 106 | println!(" Diagnostics enabled: {}", value); 107 | 108 | let value = service.read_setting(settings::id::OobeMode).await?; 109 | println!(" OOBE mode: {}", value); 110 | 111 | let value = service.read_setting(settings::id::GestureControl).await?; 112 | println!(" Gesture control: {}", value); 113 | 114 | let value = service.read_setting(settings::id::MultipointEnable).await?; 115 | println!(" Multi-point enabled: {}", value); 116 | 117 | let value = service.read_setting(settings::id::AncrGestureLoop).await?; 118 | println!(" ANCR gesture loop: {}", value); 119 | 120 | let value = service.read_setting(settings::id::CurrentAncrState).await?; 121 | println!(" ANC status: {}", value); 122 | 123 | let value = service.read_setting(settings::id::OttsMode).await?; 124 | println!(" OTTS mode: {}", value); 125 | 126 | let value = service.read_setting(settings::id::VolumeEqEnable).await?; 127 | println!(" Volume-EQ enabled: {}", value); 128 | 129 | let value = service.read_setting(settings::id::CurrentUserEq).await?; 130 | println!(" Current user EQ: {}", value); 131 | 132 | let value = service.read_setting(settings::id::VolumeAsymmetry).await?; 133 | println!(" Volume balance/asymmetry: {}", value); 134 | 135 | let value = service.read_setting(settings::id::SumToMono).await?; 136 | println!(" Mono output: {}", value); 137 | 138 | let value = service.read_setting(settings::id::VolumeExposureNotifications).await?; 139 | println!(" Volume level exposure notifications: {}", value); 140 | 141 | let value = service.read_setting(settings::id::SpeechDetection).await?; 142 | println!(" Speech detection: {}", value); 143 | 144 | // read settings via variant 145 | println!(); 146 | println!("Read via variants:"); 147 | 148 | let value = service.read_setting(SettingId::GestureEnable).await?; 149 | println!(" Gesture enable: {:?}", value); 150 | 151 | Ok(()) 152 | } 153 | -------------------------------------------------------------------------------- /libmaestro/examples/maestro_write_settings.rs: -------------------------------------------------------------------------------- 1 | //! Simple example for changing settings on the Pixel Buds Pro via the Maestro service. 2 | //! 3 | //! Sets active nois ecancelling (ANC) state. 1: off, 2: active, 3: aware. 4 | //! 5 | //! Usage: 6 | //! cargo run --example maestro_write_settings -- 7 | 8 | mod common; 9 | 10 | use std::str::FromStr; 11 | 12 | use anyhow::bail; 13 | use bluer::{Address, Session}; 14 | use maestro::protocol::utils; 15 | use num_enum::FromPrimitive; 16 | 17 | use maestro::protocol::codec::Codec; 18 | use maestro::pwrpc::client::{Client, ClientHandle}; 19 | use maestro::service::MaestroService; 20 | use maestro::service::settings::{AncState, SettingValue}; 21 | 22 | 23 | #[tokio::main(flavor = "current_thread")] 24 | async fn main() -> Result<(), anyhow::Error> { 25 | tracing_subscriber::fmt::init(); 26 | 27 | // handle command line arguments 28 | let addr = std::env::args().nth(1).expect("need device address as argument"); 29 | let addr = Address::from_str(&addr)?; 30 | 31 | let anc_state = std::env::args().nth(2).expect("need ANC state as argument"); 32 | let anc_state = i32::from_str(&anc_state)?; 33 | let anc_state = AncState::from_primitive(anc_state); 34 | 35 | if let AncState::Unknown(x) = anc_state { 36 | bail!("invalid ANC state {x}"); 37 | } 38 | 39 | // set up session 40 | let session = Session::new().await?; 41 | let adapter = session.default_adapter().await?; 42 | 43 | println!("Using adapter '{}'", adapter.name()); 44 | 45 | // get device 46 | let dev = adapter.device(addr)?; 47 | let uuids = { 48 | let mut uuids = Vec::from_iter(dev.uuids().await? 49 | .unwrap_or_default() 50 | .into_iter()); 51 | 52 | uuids.sort_unstable(); 53 | uuids 54 | }; 55 | 56 | println!("Found device:"); 57 | println!(" alias: {}", dev.alias().await?); 58 | println!(" address: {}", dev.address()); 59 | println!(" paired: {}", dev.is_paired().await?); 60 | println!(" connected: {}", dev.is_connected().await?); 61 | println!(" UUIDs:"); 62 | for uuid in uuids { 63 | println!(" {}", uuid); 64 | } 65 | println!(); 66 | 67 | println!("Connecting to Maestro profile"); 68 | let stream = common::connect_maestro_rfcomm(&session, &dev).await?; 69 | 70 | println!("Profile connected"); 71 | 72 | // set up stream for RPC communication 73 | let codec = Codec::new(); 74 | let stream = codec.wrap(stream); 75 | 76 | // set up RPC client 77 | let mut client = Client::new(stream); 78 | let handle = client.handle(); 79 | 80 | // retreive the channel numer 81 | let channel = utils::resolve_channel(&mut client).await?; 82 | 83 | let exec_task = common::run_client(client); 84 | let settings_task = read_settings(handle, channel, anc_state); 85 | 86 | tokio::select! { 87 | res = exec_task => { 88 | match res { 89 | Ok(_) => bail!("client terminated unexpectedly without error"), 90 | Err(e) => Err(e), 91 | } 92 | }, 93 | res = settings_task => res, 94 | } 95 | } 96 | 97 | async fn read_settings(handle: ClientHandle, channel: u32, anc_state: AncState) -> anyhow::Result<()> { 98 | let mut service = MaestroService::new(handle.clone(), channel); 99 | 100 | println!(); 101 | println!("Setting ANC status to '{}'", anc_state); 102 | 103 | service.write_setting(SettingValue::CurrentAncrState(anc_state)).await?; 104 | 105 | Ok(()) 106 | } 107 | -------------------------------------------------------------------------------- /libmaestro/proto/maestro_pw.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package maestro_pw; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | 8 | /* -- Maestro Service --------------------------------------------------------------------------- */ 9 | 10 | message SoftwareInfo { 11 | int32 unknown2 = 2; 12 | FirmwareInfo firmware = 4; 13 | fixed64 unknown5 = 5; 14 | bool unknown6 = 6; 15 | } 16 | 17 | message FirmwareInfo { 18 | // Note: order might not be correct 19 | FirmwareVersion case = 1; 20 | FirmwareVersion right = 2; 21 | FirmwareVersion left = 3; 22 | } 23 | 24 | message FirmwareVersion { 25 | string unknown = 1; 26 | string version_string = 2; 27 | } 28 | 29 | message HardwareInfo { 30 | int32 unknown1 = 1; 31 | int32 unknown2 = 2; 32 | int32 unknown5 = 5; 33 | int32 unknown6 = 6; 34 | SerialNumbers serial_number = 7; 35 | } 36 | 37 | message SerialNumbers { 38 | string case = 1; 39 | string right = 2; 40 | string left = 3; 41 | } 42 | 43 | message RuntimeInfo { 44 | int64 timestamp_ms = 2; // maybe unix time in ms (consistent ~60s difference to actual time) 45 | int32 unknown3 = 3; 46 | BatteryInfo battery_info = 6; 47 | PlacementInfo placement = 7; 48 | } 49 | 50 | message BatteryInfo { 51 | DeviceBatteryInfo case = 1; 52 | DeviceBatteryInfo left = 2; 53 | DeviceBatteryInfo right = 3; 54 | } 55 | 56 | message DeviceBatteryInfo { 57 | int32 level = 1; // battery level in percent 58 | BatteryState state = 2; 59 | } 60 | 61 | enum BatteryState { 62 | BATTERY_STATE_UNKNOWN = 0; 63 | BATTERY_NOT_CHARGING = 1; 64 | BATTERY_CHARGING = 2; 65 | } 66 | 67 | message PlacementInfo { 68 | bool right_bud_in_case = 1; 69 | bool left_bud_in_case = 2; 70 | } 71 | 72 | message WallClockMsg { 73 | // TODO 74 | } 75 | 76 | message ReadSettingMsg { 77 | oneof value_oneof { 78 | AllegroSettingType settings_id = 4; 79 | } 80 | } 81 | 82 | enum AllegroSettingType { 83 | ALLEGRO_SETTING_TYPE_UNKNOWN = 0; 84 | ALLEGRO_AUTO_OTA_ENABLE = 1; 85 | ALLEGRO_OHD_ENABLE = 2; 86 | ALLEGRO_OOBE_IS_FINISHED = 3; 87 | ALLEGRO_GESTURE_ENABLE = 4; 88 | ALLEGRO_DIAGNOSTICS_ENABLE = 5; 89 | ALLEGRO_OOBE_MODE = 6; 90 | ALLEGRO_GESTURE_CONTROL = 7; 91 | ALLEGRO_ANC_ACCESSIBILITY_MODE = 8; 92 | ALLEGRO_ANCR_STATE_ONE_BUD = 9; 93 | ALLEGRO_ANCR_STATE_TWO_BUDS = 10; 94 | ALLEGRO_MULTIPOINT_ENABLE = 11; 95 | ALLEGRO_ANCR_GESTURE_LOOP = 12; 96 | ALLEGRO_CURRENT_ANCR_STATE = 13; 97 | ALLEGRO_OTTS_MODE = 14; 98 | ALLEGRO_VOLUME_EQ_ENABLE = 15; 99 | ALLEGRO_CURRENT_USER_EQ = 16; 100 | ALLEGRO_VOLUME_ASYMMETRY = 17; 101 | ALLEGRO_LAST_SAVED_USER_EQ = 18; 102 | } 103 | 104 | message WriteSettingMsg { 105 | oneof value_oneof { 106 | SettingValue setting = 4; 107 | } 108 | } 109 | 110 | message SettingsRsp { 111 | oneof value_oneof { 112 | SettingValue value = 4; 113 | } 114 | } 115 | 116 | message SettingValue { 117 | oneof value_oneof { 118 | bool auto_ota_enable = 1; 119 | bool ohd_enable = 2; // on-head detection 120 | bool oobe_is_finished = 3; // out-of-box experience? 121 | bool gesture_enable = 4; 122 | bool diagnostics_enable = 5; 123 | bool oobe_mode = 6; 124 | GestureControl gesture_control = 7; 125 | // reading anc_accessibility_mode returns non-zero status (code: 2) 126 | // reading ancr_state_one_bud returns non-zero status (code: 2) 127 | // reading ancr_state_two_buds returns non-zero status (code: 2) 128 | bool multipoint_enable = 11; 129 | AncrGestureLoop ancr_gesture_loop = 12; 130 | AncState current_ancr_state = 13; 131 | int32 otts_mode = 14; // might be bool 132 | bool volume_eq_enable = 15; 133 | EqBands current_user_eq = 16; 134 | int32 volume_asymmetry = 17; // value goes from 0 t0 200 (incl.), even/odd indicates left/right 135 | // reading last_saved_user_eq returns non-zero status (code: 2) 136 | bool sum_to_mono = 19; 137 | // id 20 does not seem to exist (yet?) 138 | bool volume_exposure_notifications = 21; 139 | bool speech_detection = 22; 140 | } 141 | } 142 | 143 | message GestureControl { 144 | DeviceGestureControl left = 1; 145 | DeviceGestureControl right = 2; 146 | } 147 | 148 | message DeviceGestureControl { 149 | oneof value_oneof { 150 | GestureControlType type = 4; 151 | } 152 | } 153 | 154 | message GestureControlType { 155 | RegularActionTarget value = 1; 156 | } 157 | 158 | enum RegularActionTarget { 159 | ACTION_TARGET_UNKNOWN = 0; 160 | ACTION_TARGET_CHECK_NOTIFICATIONS = 1; 161 | ACTION_TARGET_PREVIOUS_TRACK_REPEAT = 2; 162 | ACTION_TARGET_NEXT_TRACK = 3; 163 | ACTION_TARGET_PLAY_PAUSE_TRACK = 4; 164 | ACTION_TARGET_ANC_CONTROL = 5; 165 | ACTION_TARGET_ASSISTANT_QUERY = 6; 166 | } 167 | 168 | message AncrGestureLoop { 169 | bool active = 1; 170 | bool off = 2; 171 | bool aware = 3; 172 | } 173 | 174 | enum AncState { 175 | ANC_STATE_UNKNOWN = 0; 176 | ANC_STATE_OFF = 1; 177 | ANC_STATE_ACTIVE = 2; 178 | ANC_STATE_AWARE = 3; 179 | } 180 | 181 | message EqBands { 182 | // bands go from -6.0 to 6.0 183 | float low_bass = 1; 184 | float bass = 2; 185 | float mid = 3; 186 | float treble = 4; 187 | float upper_treble = 5; 188 | } 189 | 190 | message OobeActionRsp { 191 | OobeAction action = 1; 192 | } 193 | 194 | enum OobeAction { 195 | OOBE_ACTION_UNKNOWN = 0; 196 | OOBE_ACTION_SINGLE_TAP = 1; 197 | OOBE_ACTION_DOUBLE_TAP = 2; 198 | OOBE_ACTION_TRIPLE_TAP = 3; 199 | OOBE_ACTION_HOLD = 4; 200 | OOBE_ACTION_SWIPE_FORWARD = 5; 201 | OOBE_ACTION_SWIPE_BACKWARD = 6; 202 | OOBE_ACTION_SWIPE_UP = 7; 203 | OOBE_ACTION_SWIPE_DOWN = 8; 204 | OOBE_ACTION_HOTWORD = 9; 205 | OOBE_ACTION_LEFT_ON_HEAD = 10; 206 | OOBE_ACTION_LEFT_OFF_HEAD = 11; 207 | OOBE_ACTION_RIGHT_ON_HEAD = 12; 208 | OOBE_ACTION_RIGHT_OFF_HEAD = 13; 209 | OOBE_ACTION_SPECULATIVE_TAP = 14; 210 | OOBE_ACTION_HOLD_END = 15; 211 | OOBE_ACTION_HOLD_CANCEL = 16; 212 | } 213 | 214 | service Maestro { 215 | rpc GetSoftwareInfo(google.protobuf.Empty) returns (SoftwareInfo) {} 216 | rpc GetHardwareInfo(google.protobuf.Empty) returns (HardwareInfo) {} 217 | rpc SubscribeRuntimeInfo(google.protobuf.Empty) returns (stream RuntimeInfo) {} 218 | rpc SetWallClock(WallClockMsg) returns (google.protobuf.Empty) {} 219 | rpc WriteSetting(WriteSettingMsg) returns (google.protobuf.Empty) {} 220 | rpc ReadSetting(ReadSettingMsg) returns (SettingsRsp) {} 221 | rpc SubscribeToSettingsChanges(google.protobuf.Empty) returns (stream SettingsRsp) {} 222 | rpc SubscribeToOobeActions(google.protobuf.Empty) returns (stream OobeActionRsp) {} 223 | } 224 | 225 | 226 | /* -- Multipoint Service ------------------------------------------------------------------------ */ 227 | 228 | message QuietModeStatusEvent { 229 | int32 source = 1; 230 | } 231 | 232 | message ForceMultipointSwitchMsg { 233 | // TODO 234 | } 235 | 236 | service Multipoint { 237 | rpc SubscribeToQuietModeStatus(google.protobuf.Empty) returns (stream QuietModeStatusEvent) {} 238 | rpc ForceMultipointSwitch(ForceMultipointSwitchMsg) returns (google.protobuf.Empty) {} 239 | } 240 | 241 | 242 | /* -- EartipFitTest Service --------------------------------------------------------------------- */ 243 | 244 | message StartEartipFitTestMsg { 245 | // TODO 246 | } 247 | 248 | message EndEartipFitTestMsg { 249 | // TODO 250 | } 251 | 252 | message SubscribeToEartipFitTestResultsMsg { 253 | // TODO 254 | } 255 | 256 | message EartipFitTestResult { 257 | // TODO 258 | } 259 | 260 | service EartipFitTest { 261 | rpc StartTest(StartEartipFitTestMsg) returns (google.protobuf.Empty) {} 262 | rpc EndTest(StartEartipFitTestMsg) returns (google.protobuf.Empty) {} 263 | rpc SubscribeToResults(SubscribeToEartipFitTestResultsMsg) returns (stream EartipFitTestResult) {} 264 | } 265 | 266 | 267 | /* -- JitterBuffer Service ---------------------------------------------------------------------- */ 268 | 269 | message SetJitterBufferSizePreferenceMsg { 270 | // TODO 271 | } 272 | 273 | service JitterBuffer { 274 | rpc SetJitterBufferSizePreference(SetJitterBufferSizePreferenceMsg) returns (google.protobuf.Empty) {} 275 | } 276 | 277 | 278 | /* -- Dosimeter Service ------------------------------------------------------------------------- */ 279 | 280 | message DosimeterSummaryEntry { 281 | int32 unknown1 = 1; 282 | float unknown6 = 6; 283 | } 284 | 285 | message DosimeterSummary { 286 | int32 unknown1 = 1; 287 | repeated DosimeterSummaryEntry unknown2 = 2; 288 | int32 unknown4 = 4; 289 | float unknown5 = 5; 290 | } 291 | 292 | message DosimeterLiveDbMsg { 293 | float intensity = 2; // convert to dB via log10(x) * 10 294 | } 295 | 296 | service Dosimeter { 297 | rpc FetchDailySummaries(google.protobuf.Empty) returns (DosimeterSummary) {} 298 | rpc SubscribeToLiveDb(google.protobuf.Empty) returns (DosimeterLiveDbMsg) {} 299 | } 300 | -------------------------------------------------------------------------------- /libmaestro/proto/pw.rpc.packet.proto: -------------------------------------------------------------------------------- 1 | // Copied from pigweed RPC library with modifications. 2 | // - Changed package specification. 3 | // - Removed java-package option. 4 | // 5 | // Original copyright: 6 | // Copyright 2020 The Pigweed Authors 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); you may 9 | // not use this file except in compliance with the License. You may obtain a 10 | // copy of the License at 11 | // 12 | // https://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 16 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 17 | // License for the specific language governing permissions and limitations 18 | // under the License. 19 | syntax = "proto3"; 20 | 21 | package pw.rpc.packet; 22 | 23 | enum PacketType { 24 | // To simplify identifying the origin of a packet, client-to-server packets 25 | // use even numbers and server-to-client packets use odd numbers. 26 | 27 | // Client-to-server packets 28 | 29 | // The client invokes an RPC. Always the first packet. 30 | REQUEST = 0; 31 | 32 | // A message in a client stream. Always sent after a REQUEST and before a 33 | // CLIENT_STREAM_END. 34 | CLIENT_STREAM = 2; 35 | 36 | // The client received a packet for an RPC it did not request. 37 | CLIENT_ERROR = 4; 38 | 39 | // Deprecated, do not use. Send a CLIENT_ERROR with status CANCELLED instead. 40 | // TODO(b/234879973): Remove this packet type. 41 | DEPRECATED_CANCEL = 6; 42 | 43 | // A client stream has completed. 44 | CLIENT_STREAM_END = 8; 45 | 46 | // Server-to-client packets 47 | 48 | // The RPC has finished. 49 | RESPONSE = 1; 50 | 51 | // Deprecated, do not use. Formerly was used as the last packet in a server 52 | // stream. 53 | // TODO(b/234879973): Remove this packet type. 54 | DEPRECATED_SERVER_STREAM_END = 3; 55 | 56 | // The server was unable to process a request. 57 | SERVER_ERROR = 5; 58 | 59 | // A message in a server stream. 60 | SERVER_STREAM = 7; 61 | } 62 | 63 | message RpcPacket { 64 | // The type of packet. Determines which other fields are used. 65 | PacketType type = 1; 66 | 67 | // Channel through which the packet is sent. 68 | uint32 channel_id = 2; 69 | 70 | // Hash of the fully-qualified name of the service with which this packet is 71 | // associated. For RPC packets, this is the service that processes the packet. 72 | fixed32 service_id = 3; 73 | 74 | // Hash of the name of the method which should process this packet. 75 | fixed32 method_id = 4; 76 | 77 | // The packet's payload, which is an encoded protobuf. 78 | bytes payload = 5; 79 | 80 | // Status code for the RPC response or error. 81 | uint32 status = 6; 82 | 83 | // Unique identifier for the call that initiated this RPC. Optionally set by 84 | // the client in the initial request and sent in all subsequent client 85 | // packets; echoed by the server. 86 | uint32 call_id = 7; 87 | } 88 | -------------------------------------------------------------------------------- /libmaestro/src/hdlc/codec.rs: -------------------------------------------------------------------------------- 1 | use super::{decoder, encoder, Frame}; 2 | 3 | use bytes::BytesMut; 4 | 5 | use tokio::io::{AsyncWrite, AsyncRead}; 6 | use tokio_util::codec::Framed; 7 | 8 | 9 | #[derive(Debug)] 10 | pub enum DecoderError { 11 | Io(std::io::Error), 12 | Decoder(decoder::Error), 13 | } 14 | 15 | impl From for DecoderError { 16 | fn from(value: std::io::Error) -> Self { 17 | Self::Io(value) 18 | } 19 | } 20 | 21 | impl From for DecoderError { 22 | fn from(value: decoder::Error) -> Self { 23 | Self::Decoder(value) 24 | } 25 | } 26 | 27 | 28 | #[derive(Debug, Default)] 29 | pub struct Codec { 30 | dec: decoder::Decoder, 31 | } 32 | 33 | impl Codec { 34 | pub fn new() -> Self { 35 | Self { dec: decoder::Decoder::new() } 36 | } 37 | 38 | pub fn with_capacity(cap: usize) -> Self { 39 | Self { dec: decoder::Decoder::with_capacity(cap) } 40 | } 41 | 42 | pub fn wrap(self, io: T) -> Framed 43 | where 44 | T: AsyncRead + AsyncWrite, 45 | { 46 | Framed::with_capacity(io, self, 4096 as _) 47 | } 48 | } 49 | 50 | impl tokio_util::codec::Encoder<&Frame> for Codec { 51 | type Error = std::io::Error; 52 | 53 | fn encode(&mut self, frame: &Frame, dst: &mut BytesMut) -> Result<(), Self::Error> { 54 | encoder::encode(dst, frame); 55 | Ok(()) 56 | } 57 | } 58 | 59 | impl tokio_util::codec::Decoder for Codec { 60 | type Item = Frame; 61 | type Error = std::io::Error; 62 | 63 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 64 | match self.dec.process(src) { 65 | Ok(x) => Ok(x), 66 | Err(e) => { 67 | tracing::warn!("error decoding data: {e:?}"); 68 | Ok(None) 69 | }, 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /libmaestro/src/hdlc/consts.rs: -------------------------------------------------------------------------------- 1 | //! Flag bytes and bit masks used in the HDLC encoding. 2 | 3 | pub mod flags { 4 | pub const FRAME: u8 = 0x7E; 5 | pub const ESCAPE: u8 = 0x7D; 6 | } 7 | 8 | pub mod escape { 9 | pub const MASK: u8 = 0x20; 10 | } 11 | -------------------------------------------------------------------------------- /libmaestro/src/hdlc/crc.rs: -------------------------------------------------------------------------------- 1 | //! 32-bit CRC implementation. 2 | 3 | #[derive(Debug)] 4 | pub struct Crc32 { 5 | state: u32, 6 | } 7 | 8 | impl Crc32 { 9 | pub fn new() -> Self { 10 | Self::with_state(0xFFFFFFFF) 11 | } 12 | 13 | pub fn with_state(state: u32) -> Self { 14 | Self { state } 15 | } 16 | 17 | pub fn reset(&mut self) { 18 | self.state = 0xFFFFFFFF; 19 | } 20 | 21 | pub fn value(&self) -> u32 { 22 | !self.state 23 | } 24 | 25 | pub fn put_u8(&mut self, byte: u8) -> &mut Self { 26 | self.state = tables::CRC32[((self.state as u8) ^ byte) as usize] ^ (self.state >> 8); 27 | self 28 | } 29 | 30 | pub fn put_bytes<'a, B: IntoIterator>(&mut self, bytes: B) -> &mut Self { 31 | for b in bytes.into_iter().copied() { 32 | self.put_u8(b); 33 | } 34 | 35 | self 36 | } 37 | } 38 | 39 | impl Default for Crc32 { 40 | fn default() -> Self { 41 | Self::new() 42 | } 43 | } 44 | 45 | 46 | pub fn crc32<'a, B: IntoIterator>(bytes: B) -> u32 { 47 | Crc32::new().put_bytes(bytes).value() 48 | } 49 | 50 | 51 | mod tables { 52 | pub const CRC32: [u32; 256] = [ 53 | 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 54 | 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 55 | 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 56 | 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 57 | 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 58 | 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 59 | 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 60 | 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 61 | 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 62 | 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 63 | 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 64 | 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 65 | 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 66 | 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 67 | 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 68 | 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 69 | 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 70 | 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 71 | 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 72 | 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 73 | 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 74 | 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 75 | 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 76 | 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 77 | 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 78 | 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 79 | 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 80 | 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 81 | 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 82 | 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 83 | 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 84 | 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 85 | 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 86 | 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 87 | 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 88 | 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 89 | 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 90 | 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 91 | 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 92 | 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 93 | 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 94 | 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 95 | 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, 96 | ]; 97 | } 98 | 99 | 100 | #[cfg(test)] 101 | mod test { 102 | use super::*; 103 | 104 | #[test] 105 | fn test_crc32() { 106 | assert_eq!(crc32(b"test test test"), 0x235b6a02); 107 | assert_eq!(crc32(b"1234321"), 0xd981751c); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /libmaestro/src/hdlc/decoder.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, BytesMut}; 2 | 3 | use super::consts; 4 | use super::crc; 5 | use super::varint; 6 | use super::Frame; 7 | 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 | pub enum Error { 11 | UnexpectedData, 12 | UnexpectedEndOfFrame, 13 | InvalidChecksum, 14 | InvalidEncoding, 15 | InvalidFrame, 16 | InvalidAddress, 17 | BufferOverflow, 18 | } 19 | 20 | impl From for Error { 21 | fn from(value: varint::DecodeError) -> Self { 22 | match value { 23 | varint::DecodeError::Incomplete => Self::InvalidFrame, 24 | varint::DecodeError::Overflow => Self::InvalidAddress, 25 | } 26 | } 27 | } 28 | 29 | 30 | #[derive(Debug)] 31 | pub struct Decoder { 32 | buf: Vec, 33 | state: (State, EscState), 34 | current_frame_size: usize, 35 | } 36 | 37 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 38 | enum State { 39 | Discard, 40 | Frame, 41 | } 42 | 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 44 | enum EscState { 45 | Normal, 46 | Escape, 47 | } 48 | 49 | impl Decoder { 50 | pub fn new() -> Self { 51 | Self::with_capacity(4096) 52 | } 53 | 54 | pub fn with_capacity(cap: usize) -> Self { 55 | Self { 56 | buf: Vec::with_capacity(cap), 57 | state: (State::Discard, EscState::Normal), 58 | current_frame_size: 0, 59 | } 60 | } 61 | 62 | pub fn process(&mut self, buf: &mut BytesMut) -> Result, Error> { 63 | if buf.is_empty() { 64 | return Ok(None); 65 | } 66 | 67 | loop { 68 | match self.state.0 { 69 | State::Discard => { 70 | // try to find the start of this frame 71 | match find_frame_start(buf) { 72 | // expected: immediate start of frame 73 | Some(0) => { 74 | self.state.0 = State::Frame; 75 | buf.advance(1); 76 | }, 77 | // unexpected: n bytes before start of frame 78 | Some(n) => { 79 | self.state.0 = State::Frame; 80 | buf.advance(n + 1); 81 | return Err(Error::UnexpectedData); 82 | }, 83 | // unexpected: unknown amount of bytes before start of frame 84 | None => { 85 | // check whether the last byte might indicate a start 86 | let n = if buf.last() == Some(&consts::flags::FRAME) { 87 | buf.len() - 1 88 | } else { 89 | buf.len() 90 | }; 91 | 92 | buf.advance(n); 93 | return Err(Error::UnexpectedData); 94 | }, 95 | } 96 | }, 97 | State::Frame => { 98 | // copy and decode to internal buffer 99 | for (i, b) in buf.iter().copied().enumerate() { 100 | match (b, self.state.1) { 101 | (consts::flags::ESCAPE, EscState::Normal) => { 102 | self.state.1 = EscState::Escape; 103 | }, 104 | (consts::flags::ESCAPE, EscState::Escape) => { 105 | buf.advance(i + 1); 106 | self.reset(); 107 | 108 | return Err(Error::InvalidEncoding); 109 | }, 110 | (consts::flags::FRAME, EscState::Normal) => { 111 | buf.advance(i + 1); 112 | 113 | return self.decode_buffered(); 114 | }, 115 | (consts::flags::FRAME, EscState::Escape) => { 116 | buf.advance(i); 117 | self.reset(); 118 | 119 | return Err(Error::UnexpectedEndOfFrame); 120 | }, 121 | (b, EscState::Normal) => { 122 | self.push_byte(b); 123 | }, 124 | (b, EscState::Escape) => { 125 | self.push_byte(b ^ consts::escape::MASK); 126 | self.state.1 = EscState::Normal; 127 | }, 128 | } 129 | } 130 | 131 | buf.advance(buf.remaining()); 132 | return Ok(None); 133 | }, 134 | } 135 | } 136 | } 137 | 138 | fn decode_buffered(&mut self) -> Result, Error> { 139 | // validate minimum frame size 140 | if self.buf.len() < 6 { 141 | self.reset(); 142 | self.state.0 = State::Frame; // the next frame may already start 143 | return Err(Error::InvalidFrame); 144 | } 145 | 146 | // validate checksum 147 | let crc_actual = crc::crc32(&self.buf[..self.buf.len()-4]); 148 | let crc_expect = self.buf[self.buf.len()-4..].try_into().unwrap(); 149 | let crc_expect = u32::from_le_bytes(crc_expect); 150 | 151 | if crc_expect != crc_actual { 152 | self.reset(); 153 | self.state.0 = State::Frame; // the next frame may already start 154 | return Err(Error::InvalidChecksum); 155 | } 156 | 157 | // check for overflow 158 | if self.current_frame_size > self.buf.len() { 159 | self.reset(); 160 | return Err(Error::BufferOverflow); 161 | } 162 | 163 | // decode address 164 | let (address, n) = varint::decode(&self.buf)?; 165 | 166 | // validate minimum remaining frame size 167 | if self.buf.len() < n + 5 { 168 | self.reset(); 169 | return Err(Error::InvalidFrame); 170 | } 171 | 172 | // get control byte and data 173 | let control = self.buf[n]; 174 | let data = self.buf[n+1..self.buf.len()-4].into(); 175 | 176 | let frame = Frame { 177 | address, 178 | control, 179 | data, 180 | }; 181 | 182 | self.reset(); 183 | Ok(Some(frame)) 184 | } 185 | 186 | fn push_byte(&mut self, byte: u8) { 187 | self.current_frame_size += 1; 188 | 189 | if self.buf.len() < self.buf.capacity() { 190 | self.buf.push(byte); 191 | } 192 | } 193 | 194 | fn reset(&mut self) { 195 | self.buf.clear(); 196 | self.state = (State::Discard, EscState::Normal); 197 | self.current_frame_size = 0; 198 | } 199 | } 200 | 201 | impl Default for Decoder { 202 | fn default() -> Self { 203 | Self::new() 204 | } 205 | } 206 | 207 | 208 | fn find_frame_start(buf: &[u8]) -> Option { 209 | buf.windows(2) 210 | .enumerate() 211 | .find(|(_, b)| b[0] == consts::flags::FRAME && b[1] != consts::flags::FRAME) 212 | .map(|(i, _)| i) 213 | } 214 | 215 | 216 | #[cfg(test)] 217 | mod test { 218 | use bytes::BufMut; 219 | 220 | use super::*; 221 | 222 | #[test] 223 | fn test_find_frame_start() { 224 | let buf = [0x7E, 0x01, 0x02, 0x03]; 225 | assert_eq!(find_frame_start(&buf), Some(0)); 226 | 227 | let buf = [0x03, 0x02, 0x01, 0x00, 0x7E, 0x00, 0x01, 0x02, 0x03]; 228 | assert_eq!(find_frame_start(&buf), Some(4)); 229 | 230 | let buf = [0x03, 0x02, 0x01, 0x00, 0x7E, 0x7E, 0x00, 0x01, 0x02, 0x03]; 231 | assert_eq!(find_frame_start(&buf), Some(5)); 232 | 233 | let buf = [0x03, 0x02, 0x01, 0x00, 0x7E]; 234 | assert_eq!(find_frame_start(&buf), None); 235 | 236 | let buf = [0x03, 0x02, 0x01, 0x00, 0x7E, 0x00]; 237 | assert_eq!(find_frame_start(&buf), Some(4)); 238 | 239 | let buf = [0x7E]; 240 | assert_eq!(find_frame_start(&buf), None); 241 | 242 | let buf = []; 243 | assert_eq!(find_frame_start(&buf), None); 244 | } 245 | 246 | #[test] 247 | fn test_frame_decode() { 248 | let data = [ 249 | // message 250 | 0x7e, 0x06, 0x08, 0x09, 0x03, 0x05, 0x06, 0x07, 0x7d, 0x5d, 251 | 0x7d, 0x5e, 0x7f, 0xff, 0xe6, 0x2d, 0x17, 0xc6, 0x7e, 252 | // and trailing bytes 253 | 0x02, 0x01 254 | ]; 255 | 256 | let expect = Frame { 257 | address: 0x010203, 258 | control: 0x03, 259 | data: vec![0x05, 0x06, 0x07, 0x7D, 0x7E, 0x7F, 0xFF].into(), 260 | }; 261 | 262 | let mut dec = Decoder::new(); 263 | 264 | // test standard decoding 265 | let mut buf = BytesMut::from(&data[..data.len()-2]); 266 | assert_eq!(dec.process(&mut buf), Ok(Some(expect.clone()))); 267 | assert_eq!(buf.remaining(), 0); 268 | 269 | // test decoding with trailing bytes 270 | let mut buf = BytesMut::from(&data[..data.len()]); 271 | assert_eq!(dec.process(&mut buf), Ok(Some(expect.clone()))); 272 | assert_eq!(buf.remaining(), 2); 273 | 274 | assert_eq!(dec.process(&mut buf), Err(Error::UnexpectedData)); 275 | assert_eq!(buf.remaining(), 0); 276 | 277 | // test partial decoding / re-entrancy 278 | let mut buf = BytesMut::from(&data[..9]); 279 | assert_eq!(dec.process(&mut buf), Ok(None)); 280 | assert_eq!(buf.remaining(), 0); 281 | 282 | assert_eq!(dec.state, (State::Frame, EscState::Escape)); 283 | 284 | let mut buf = BytesMut::from(&data[9..data.len()-2]); 285 | assert_eq!(dec.process(&mut buf), Ok(Some(expect.clone()))); 286 | assert_eq!(buf.remaining(), 0); 287 | 288 | // test decoding of subsequent frames 289 | let mut buf = BytesMut::new(); 290 | buf.put_slice(&data[..data.len()-2]); 291 | buf.put_slice(&data[..]); 292 | 293 | assert_eq!(dec.process(&mut buf), Ok(Some(expect.clone()))); 294 | assert_eq!(buf.remaining(), data.len()); 295 | 296 | assert_eq!(dec.process(&mut buf), Ok(Some(expect.clone()))); 297 | assert_eq!(buf.remaining(), 2); 298 | 299 | // test decoding of cut-off frame / data loss (with frame being too small) 300 | let mut buf = BytesMut::new(); 301 | buf.put_slice(&data[..5]); 302 | buf.put_slice(&data[..]); 303 | 304 | assert_eq!(dec.process(&mut buf), Err(Error::InvalidFrame)); 305 | assert_eq!(dec.process(&mut buf), Ok(Some(expect.clone()))); 306 | assert_eq!(buf.remaining(), 2); 307 | 308 | // test decoding of cut-off frame / data loss (with data being cut off) 309 | let mut buf = BytesMut::new(); 310 | buf.put_slice(&data[..10]); 311 | buf.put_slice(&data[..]); 312 | 313 | assert_eq!(dec.process(&mut buf), Err(Error::InvalidChecksum)); 314 | assert_eq!(dec.process(&mut buf), Ok(Some(expect.clone()))); 315 | assert_eq!(buf.remaining(), 2); 316 | 317 | // test frame flag as escaped byte 318 | let mut buf = BytesMut::from(&data[..10]); 319 | buf.put_slice(&data[..]); 320 | buf[9] = consts::flags::FRAME; 321 | 322 | assert_eq!(dec.process(&mut buf), Err(Error::UnexpectedEndOfFrame)); 323 | assert_eq!(dec.process(&mut buf), Err(Error::UnexpectedData)); 324 | assert_eq!(dec.process(&mut buf), Ok(Some(expect.clone()))); 325 | assert_eq!(buf.remaining(), 2); 326 | 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /libmaestro/src/hdlc/encoder.rs: -------------------------------------------------------------------------------- 1 | use bytes::{BufMut, BytesMut}; 2 | 3 | use super::{consts, crc::Crc32, varint, Frame}; 4 | 5 | 6 | struct ByteEscape { 7 | buf: B, 8 | } 9 | 10 | impl ByteEscape { 11 | fn new(buf: B) -> Self { 12 | Self { buf } 13 | } 14 | 15 | fn put_u8(&mut self, byte: u8) { 16 | match byte { 17 | consts::flags::ESCAPE | consts::flags::FRAME => self.buf.put_slice(&[ 18 | consts::flags::ESCAPE, 19 | consts::escape::MASK ^ byte 20 | ]), 21 | _ => self.buf.put_u8(byte), 22 | } 23 | } 24 | 25 | fn put_frame_flag(&mut self) { 26 | self.buf.put_u8(super::consts::flags::FRAME) 27 | } 28 | } 29 | 30 | impl ByteEscape<&mut BytesMut> { 31 | fn reserve(&mut self, additional: usize) -> &mut Self { 32 | self.buf.reserve(additional); 33 | self 34 | } 35 | } 36 | 37 | 38 | struct Encoder { 39 | buf: ByteEscape, 40 | crc: Crc32, 41 | } 42 | 43 | impl Encoder { 44 | fn new(buf: B) -> Self { 45 | Self { 46 | buf: ByteEscape::new(buf), 47 | crc: Crc32::new(), 48 | } 49 | } 50 | 51 | fn flag(&mut self) -> &mut Self { 52 | self.buf.put_frame_flag(); 53 | self 54 | } 55 | 56 | fn put_u8(&mut self, byte: u8) -> &mut Self { 57 | self.crc.put_u8(byte); 58 | self.buf.put_u8(byte); 59 | self 60 | } 61 | 62 | fn put_bytes>(&mut self, bytes: T) -> &mut Self { 63 | for b in bytes.into_iter() { 64 | self.put_u8(b); 65 | } 66 | self 67 | } 68 | 69 | fn finalize(&mut self) { 70 | self.put_bytes(self.crc.value().to_le_bytes()); 71 | self.flag(); 72 | } 73 | } 74 | 75 | impl Encoder<&mut BytesMut> { 76 | fn reserve(&mut self, additional: usize) -> &mut Self { 77 | self.buf.reserve(additional); 78 | self 79 | } 80 | } 81 | 82 | 83 | pub fn encode(buf: &mut BytesMut, frame: &Frame) { 84 | Encoder::new(buf) 85 | .reserve(frame.data.len() + 8) // reserve at least data-size + min-frame-size 86 | .flag() // flag 87 | .put_bytes(varint::encode(frame.address)) // address 88 | .put_u8(frame.control) // control 89 | .put_bytes(frame.data.iter().copied()) // data 90 | .reserve(5) // reserve CRC32 + flag 91 | .finalize() // checksum and flag 92 | } 93 | 94 | pub fn encode_bytes(frame: &Frame) -> BytesMut { 95 | let mut buf = BytesMut::new(); 96 | encode(&mut buf, frame); 97 | buf 98 | } 99 | 100 | 101 | #[cfg(test)] 102 | mod test { 103 | use super::*; 104 | 105 | #[test] 106 | fn test_escape_bytes() { 107 | fn e(src: &[u8]) -> Vec { 108 | let mut dst = Vec::new(); 109 | let mut buf = ByteEscape::new(&mut dst); 110 | 111 | for byte in src { 112 | buf.put_u8(*byte); 113 | } 114 | 115 | dst 116 | } 117 | 118 | assert_eq!(e(&[0x00, 0x00]), [0x00, 0x00]); 119 | assert_eq!(e(&[0x7D]), [0x7D, 0x5D]); 120 | assert_eq!(e(&[0x7E]), [0x7D, 0x5E]); 121 | assert_eq!(e(&[0x01, 0x7D, 0x02]), [0x01, 0x7D, 0x5D, 0x02]); 122 | assert_eq!(e(&[0x01, 0x7E, 0x02]), [0x01, 0x7D, 0x5E, 0x02]); 123 | assert_eq!(e(&[0x7D, 0x7E]), [0x7D, 0x5D, 0x7D, 0x5E]); 124 | assert_eq!(e(&[0x7F, 0x5D, 0x7E]), [0x7F, 0x5D, 0x7D, 0x5E]); 125 | } 126 | 127 | #[test] 128 | fn test_encode() { 129 | assert_eq!([ 130 | 0x7e, 0x06, 0x08, 0x09, 0x03, 0x8b, 0x3b, 0xf7, 0x42, 0x7e, 131 | ], &encode_bytes(&Frame { 132 | address: 0x010203, 133 | control: 0x03, 134 | data: vec![].into(), 135 | })[..]); 136 | 137 | assert_eq!([ 138 | 0x7e, 0x06, 0x08, 0x09, 0x03, 0x05, 0x06, 0x07, 0x7d, 0x5d, 139 | 0x7d, 0x5e, 0x7f, 0xff, 0xe6, 0x2d, 0x17, 0xc6, 0x7e, 140 | ], &encode_bytes(&Frame { 141 | address: 0x010203, 142 | control: 0x03, 143 | data: vec![0x05, 0x06, 0x07, 0x7d, 0x7e, 0x7f, 0xff].into(), 144 | })[..]); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /libmaestro/src/hdlc/mod.rs: -------------------------------------------------------------------------------- 1 | //! High-level Data Link Control (HDLC) support library. 2 | 3 | pub mod codec; 4 | pub mod consts; 5 | pub mod crc; 6 | pub mod decoder; 7 | pub mod encoder; 8 | pub mod varint; 9 | 10 | pub use codec::Codec; 11 | 12 | use bytes::BytesMut; 13 | 14 | 15 | #[derive(Debug, Clone, PartialEq, Eq)] 16 | pub struct Frame { 17 | pub address: u32, 18 | pub control: u8, 19 | pub data: Box<[u8]>, 20 | } 21 | 22 | impl Frame { 23 | pub fn decode(buf: &mut BytesMut) -> Result, decoder::Error> { 24 | decoder::Decoder::new().process(buf) 25 | } 26 | 27 | pub fn encode(&self, buf: &mut BytesMut) { 28 | encoder::encode(buf, self) 29 | } 30 | 31 | pub fn encode_bytes(&self) -> BytesMut { 32 | encoder::encode_bytes(self) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libmaestro/src/hdlc/varint.rs: -------------------------------------------------------------------------------- 1 | //! Support for variable length integer encoding as used in the HDLC frame encoding. 2 | 3 | use arrayvec::ArrayVec; 4 | 5 | 6 | pub fn decode<'a, S: IntoIterator>(src: S) -> Result<(u32, usize), DecodeError> { 7 | let mut address = 0; 8 | 9 | for (i, b) in src.into_iter().copied().enumerate() { 10 | address |= ((b >> 1) as u64) << (i * 7); 11 | 12 | if address > u32::MAX as u64 { 13 | Err(DecodeError::Overflow)?; 14 | } 15 | 16 | if b & 0x01 == 0x01 { 17 | return Ok((address as u32, i + 1)); 18 | } 19 | } 20 | 21 | Err(DecodeError::Incomplete) 22 | } 23 | 24 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 25 | pub enum DecodeError { 26 | Incomplete, 27 | Overflow, 28 | } 29 | 30 | 31 | pub fn encode(num: u32) -> Encode { 32 | Encode { num, done: false } 33 | } 34 | 35 | pub fn encode_vec(num: u32) -> ArrayVec { 36 | encode(num).collect() 37 | } 38 | 39 | pub struct Encode { 40 | num: u32, 41 | done: bool, 42 | } 43 | 44 | impl Iterator for Encode { 45 | type Item = u8; 46 | 47 | fn next(&mut self) -> Option { 48 | if (self.num >> 7) != 0 { 49 | let b = ((self.num & 0x7F) as u8) << 1; 50 | self.num >>= 7; 51 | 52 | Some(b) 53 | } else if !self.done { 54 | let b = (((self.num & 0x7F) as u8) << 1) | 1; 55 | self.done = true; 56 | 57 | Some(b) 58 | } else { 59 | None 60 | } 61 | } 62 | } 63 | 64 | 65 | pub const fn num_bytes(value: u32) -> usize { 66 | if value == 0 { 67 | 1 68 | } else { 69 | (u32::BITS - value.leading_zeros()).div_ceil(7) as _ 70 | } 71 | } 72 | 73 | 74 | #[cfg(test)] 75 | mod test { 76 | use super::*; 77 | 78 | #[test] 79 | fn test_decode() { 80 | assert_eq!(decode(&[0x01]).unwrap(), (0x00, 1)); 81 | assert_eq!(decode(&[0x00, 0x00, 0x00, 0x01]).unwrap(), (0x00, 4)); 82 | assert_eq!(decode(&[0x11, 0x00]).unwrap(), (0x0008, 1)); 83 | assert_eq!(decode(&[0x10, 0x21]).unwrap(), (0x0808, 2)); 84 | 85 | assert_eq!(decode(&[0x01]).unwrap(), (0x00, 1)); 86 | assert_eq!(decode(&[0x03]).unwrap(), (0x01, 1)); 87 | 88 | assert_eq!(decode(&[0xff]).unwrap(), (0x7f, 1)); 89 | assert_eq!(decode(&[0x00, 0x03]).unwrap(), (0x80, 2)); 90 | 91 | assert_eq!(decode(&[0xfe, 0xff]).unwrap(), (0x3fff, 2)); 92 | assert_eq!(decode(&[0x00, 0x00, 0x03]).unwrap(), (0x4000, 3)); 93 | 94 | assert_eq!(decode(&[0xfe, 0xfe, 0xff]).unwrap(), (0x1f_ffff, 3)); 95 | assert_eq!(decode(&[0x00, 0x00, 0x00, 0x03]).unwrap(), (0x20_0000, 4)); 96 | 97 | assert_eq!(decode(&[0xfe, 0xfe, 0xfe, 0xff]).unwrap(), (0x0fff_ffff, 4)); 98 | assert_eq!(decode(&[0x00, 0x00, 0x00, 0x00, 0x03]).unwrap(), (0x1000_0000, 5)); 99 | 100 | assert_eq!(decode(&[0xfe, 0x03]).unwrap(), (u8::MAX as _, 2)); 101 | assert_eq!(decode(&[0xfe, 0xfe, 0x07]).unwrap(), (u16::MAX as _, 3)); 102 | assert_eq!(decode(&[0xfe, 0xfe, 0xfe, 0xfe, 0x1f]).unwrap(), (u32::MAX, 5)); 103 | 104 | assert_eq!(decode(&[0xFE]), Err(DecodeError::Incomplete)); 105 | assert_eq!(decode(&[0xFE, 0xFE, 0xFE, 0xFE, 0xFF]), Err(DecodeError::Overflow)); 106 | } 107 | 108 | #[test] 109 | fn test_encode() { 110 | assert_eq!(encode_vec(0x01234)[..], [0x68, 0x49]); 111 | assert_eq!(encode_vec(0x87654)[..], [0xa8, 0xd8, 0x43]); 112 | 113 | assert_eq!(encode_vec(0x00)[..], [0x01]); 114 | assert_eq!(encode_vec(0x01)[..], [0x03]); 115 | 116 | assert_eq!(encode_vec(0x7f)[..], [0xff]); 117 | assert_eq!(encode_vec(0x80)[..], [0x00, 0x03]); 118 | 119 | assert_eq!(encode_vec(0x3fff)[..], [0xfe, 0xff]); 120 | assert_eq!(encode_vec(0x4000)[..], [0x00, 0x00, 0x03]); 121 | 122 | assert_eq!(encode_vec(0x1f_ffff)[..], [0xfe, 0xfe, 0xff]); 123 | assert_eq!(encode_vec(0x20_0000)[..], [0x00, 0x00, 0x00, 0x03]); 124 | 125 | assert_eq!(encode_vec(0x0fff_ffff)[..], [0xfe, 0xfe, 0xfe, 0xff]); 126 | assert_eq!(encode_vec(0x1000_0000)[..], [0x00, 0x00, 0x00, 0x00, 0x03]); 127 | 128 | assert_eq!(encode_vec(u8::MAX as _)[..], [0xfe, 0x03]); 129 | assert_eq!(encode_vec(u16::MAX as _)[..], [0xfe, 0xfe, 0x07]); 130 | assert_eq!(encode_vec(u32::MAX)[..], [0xfe, 0xfe, 0xfe, 0xfe, 0x1f]); 131 | } 132 | 133 | #[test] 134 | fn test_num_bytes() { 135 | assert_eq!(num_bytes(0x00), 1); 136 | assert_eq!(num_bytes(0x01), 1); 137 | 138 | assert_eq!(num_bytes(0x7f), 1); 139 | assert_eq!(num_bytes(0x80), 2); 140 | 141 | assert_eq!(num_bytes(0x3fff), 2); 142 | assert_eq!(num_bytes(0x4000), 3); 143 | 144 | assert_eq!(num_bytes(0x1f_ffff), 3); 145 | assert_eq!(num_bytes(0x20_0000), 4); 146 | 147 | assert_eq!(num_bytes(0x0fff_ffff), 4); 148 | assert_eq!(num_bytes(0x1000_0000), 5); 149 | 150 | assert_eq!(num_bytes(u8::MAX as _), 2); 151 | assert_eq!(num_bytes(u16::MAX as _), 3); 152 | assert_eq!(num_bytes(u32::MAX), 5); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /libmaestro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Library for the Maestro protocol used to change settings (ANC, equalizer, 2 | //! etc.) on the Google Pixel Buds Pro. Might support other Pixel Buds, might 3 | //! not. 4 | 5 | use uuid::{uuid, Uuid}; 6 | 7 | /// UUID under which the Maestro protocol is advertised. 8 | /// 9 | /// Defined as `25e97ff7-24ce-4c4c-8951-f764a708f7b5`. 10 | pub const UUID: Uuid = uuid!("25e97ff7-24ce-4c4c-8951-f764a708f7b5"); 11 | 12 | pub mod hdlc; 13 | pub mod protocol; 14 | pub mod pwrpc; 15 | pub mod service; 16 | -------------------------------------------------------------------------------- /libmaestro/src/protocol/addr.rs: -------------------------------------------------------------------------------- 1 | use num_enum::{FromPrimitive, IntoPrimitive}; 2 | 3 | 4 | #[repr(u8)] 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, IntoPrimitive)] 6 | pub enum Peer { 7 | Unknown = 0, 8 | Host = 1, 9 | Case = 2, 10 | LeftBtCore = 3, 11 | RightBtCore = 4, 12 | LeftSensorHub = 5, 13 | RightSensorHub = 6, 14 | LeftSpiBridge = 7, 15 | RightSpiBridge = 8, 16 | DebugApp = 9, 17 | MaestroA = 10, 18 | LeftTahiti = 11, 19 | RightTahiti = 12, 20 | MaestroB = 13, 21 | 22 | #[num_enum(catch_all)] 23 | Unrecognized(u8) = 0xff, 24 | } 25 | 26 | 27 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 28 | pub struct Address { 29 | value: u32, 30 | } 31 | 32 | impl Address { 33 | pub fn from_value(value: u32) -> Self { 34 | Address { value } 35 | } 36 | 37 | pub fn from_peers(source: Peer, target: Peer) -> Self { 38 | let source: u8 = source.into(); 39 | let target: u8 = target.into(); 40 | 41 | Self::from_value(((source as u32 & 0xf) << 6) | ((target as u32 & 0xf) << 10)) 42 | } 43 | 44 | pub fn value(&self) -> u32 { 45 | self.value 46 | } 47 | 48 | pub fn source(&self) -> Peer { 49 | Peer::from_primitive(((self.value >> 6) & 0x0f) as u8) 50 | } 51 | 52 | pub fn target(&self) -> Peer { 53 | Peer::from_primitive(((self.value >> 10) & 0x0f) as u8) 54 | } 55 | 56 | pub fn swap(&self) -> Self { 57 | Self::from_peers(self.target(), self.source()) 58 | } 59 | 60 | pub fn channel_id(&self) -> Option { 61 | let source = self.source(); 62 | let target = self.target(); 63 | 64 | if source == Peer::MaestroA || source == Peer::MaestroB { 65 | channel_id(source, target) 66 | } else { 67 | channel_id(target, source) 68 | } 69 | } 70 | } 71 | 72 | impl From for Address { 73 | fn from(value: u32) -> Self { 74 | Self::from_value(value) 75 | } 76 | } 77 | 78 | impl From<(Peer, Peer)> for Address { 79 | fn from(peers: (Peer, Peer)) -> Self { 80 | Self::from_peers(peers.0, peers.1) 81 | } 82 | } 83 | 84 | 85 | pub fn channel_id(local: Peer, remote: Peer) -> Option { 86 | match (local, remote) { 87 | (Peer::MaestroA, Peer::Case) => Some(18), 88 | (Peer::MaestroA, Peer::LeftBtCore) => Some(19), 89 | (Peer::MaestroA, Peer::LeftSensorHub) => Some(20), 90 | (Peer::MaestroA, Peer::RightBtCore) => Some(21), 91 | (Peer::MaestroA, Peer::RightSensorHub) => Some(22), 92 | (Peer::MaestroB, Peer::Case) => Some(23), 93 | (Peer::MaestroB, Peer::LeftBtCore) => Some(24), 94 | (Peer::MaestroB, Peer::LeftSensorHub) => Some(25), 95 | (Peer::MaestroB, Peer::RightBtCore) => Some(26), 96 | (Peer::MaestroB, Peer::RightSensorHub) => Some(27), 97 | (_, _) => None, 98 | } 99 | } 100 | 101 | pub fn address_for_channel(channel: u32) -> Option
{ 102 | match channel { 103 | 18 => Some(Address::from_peers(Peer::MaestroA, Peer::Case)), 104 | 19 => Some(Address::from_peers(Peer::MaestroA, Peer::LeftBtCore)), 105 | 20 => Some(Address::from_peers(Peer::MaestroA, Peer::LeftSensorHub)), 106 | 21 => Some(Address::from_peers(Peer::MaestroA, Peer::RightBtCore)), 107 | 22 => Some(Address::from_peers(Peer::MaestroA, Peer::RightSensorHub)), 108 | 23 => Some(Address::from_peers(Peer::MaestroB, Peer::Case)), 109 | 24 => Some(Address::from_peers(Peer::MaestroB, Peer::LeftBtCore)), 110 | 25 => Some(Address::from_peers(Peer::MaestroB, Peer::LeftSensorHub)), 111 | 26 => Some(Address::from_peers(Peer::MaestroB, Peer::RightBtCore)), 112 | 27 => Some(Address::from_peers(Peer::MaestroB, Peer::RightSensorHub)), 113 | _ => None, 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /libmaestro/src/protocol/codec.rs: -------------------------------------------------------------------------------- 1 | use bytes::BytesMut; 2 | 3 | use prost::Message; 4 | 5 | use tokio::io::{AsyncRead, AsyncWrite}; 6 | use tokio_util::codec::{Decoder, Framed, Encoder}; 7 | 8 | use crate::pwrpc::types::RpcPacket; 9 | use crate::hdlc; 10 | 11 | use super::addr; 12 | 13 | 14 | pub struct Codec { 15 | hdlc: hdlc::Codec, 16 | } 17 | 18 | impl Codec { 19 | pub fn new() -> Self { 20 | Self { 21 | hdlc: hdlc::Codec::new(), 22 | } 23 | } 24 | 25 | pub fn wrap(self, io: T) -> Framed 26 | where 27 | T: AsyncRead + AsyncWrite, 28 | { 29 | Framed::with_capacity(io, self, 4096 as _) 30 | } 31 | } 32 | 33 | impl Default for Codec { 34 | fn default() -> Self { 35 | Self::new() 36 | } 37 | } 38 | 39 | impl Decoder for Codec { 40 | type Item = RpcPacket; 41 | type Error = std::io::Error; 42 | 43 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 44 | match self.hdlc.decode(src)? { 45 | Some(frame) => { 46 | if frame.control != 0x03 { 47 | tracing::warn!("unexpected control type: {}", frame.control); 48 | return Ok(None); 49 | } 50 | 51 | let packet = RpcPacket::decode(&frame.data[..])?; 52 | Ok(Some(packet)) 53 | } 54 | None => Ok(None), 55 | } 56 | } 57 | } 58 | 59 | impl Encoder<&RpcPacket> for Codec { 60 | type Error = std::io::Error; 61 | 62 | fn encode(&mut self, packet: &RpcPacket, dst: &mut BytesMut) -> Result<(), Self::Error> { 63 | let address = addr::address_for_channel(packet.channel_id).unwrap(); 64 | 65 | let frame = hdlc::Frame { 66 | address: address.value(), 67 | control: 0x03, 68 | data: packet.encode_to_vec().into(), // TODO: can we avoid these allocations? 69 | }; 70 | 71 | self.hdlc.encode(&frame, dst) 72 | } 73 | } 74 | 75 | impl Encoder for Codec { 76 | type Error = std::io::Error; 77 | 78 | fn encode(&mut self, packet: RpcPacket, dst: &mut BytesMut) -> Result<(), Self::Error> { 79 | self.encode(&packet, dst) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /libmaestro/src/protocol/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod addr; 2 | pub mod codec; 3 | pub mod utils; 4 | 5 | pub mod types { 6 | include!(concat!(env!("OUT_DIR"), "/maestro_pw.rs")); 7 | } 8 | -------------------------------------------------------------------------------- /libmaestro/src/protocol/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::pwrpc::Error; 2 | use crate::pwrpc::client::{Client, Request, UnaryResponse, ClientHandle}; 3 | use crate::pwrpc::id::PathRef; 4 | use crate::pwrpc::types::RpcPacket; 5 | 6 | use super::addr; 7 | use super::addr::Peer; 8 | use super::types::SoftwareInfo; 9 | 10 | 11 | pub async fn resolve_channel(client: &mut Client) -> Result 12 | where 13 | S: futures::Sink, 14 | S: futures::Stream> + Unpin, 15 | Error: From, 16 | Error: From, 17 | { 18 | tracing::trace!("resolving channel"); 19 | 20 | let channels = ( 21 | addr::channel_id(Peer::MaestroA, Peer::Case).unwrap(), 22 | addr::channel_id(Peer::MaestroA, Peer::LeftBtCore).unwrap(), 23 | addr::channel_id(Peer::MaestroA, Peer::RightBtCore).unwrap(), 24 | addr::channel_id(Peer::MaestroB, Peer::Case).unwrap(), 25 | addr::channel_id(Peer::MaestroB, Peer::LeftBtCore).unwrap(), 26 | addr::channel_id(Peer::MaestroB, Peer::RightBtCore).unwrap(), 27 | ); 28 | 29 | let tasks = ( 30 | try_open_channel(client.handle(), channels.0), 31 | try_open_channel(client.handle(), channels.1), 32 | try_open_channel(client.handle(), channels.2), 33 | try_open_channel(client.handle(), channels.3), 34 | try_open_channel(client.handle(), channels.4), 35 | try_open_channel(client.handle(), channels.5), 36 | ); 37 | 38 | let channel = tokio::select! { 39 | // Ensure that the open() calls are registered before we start running 40 | // the client. 41 | biased; 42 | 43 | res = tasks.0 => { res? }, 44 | res = tasks.1 => { res? }, 45 | res = tasks.2 => { res? }, 46 | res = tasks.3 => { res? }, 47 | res = tasks.4 => { res? }, 48 | res = tasks.5 => { res? }, 49 | res = client.run() => { res?; return Err(Error::aborted("client terminated")) } 50 | }; 51 | 52 | tracing::trace!(channel=channel, "channel resolved"); 53 | Ok(channel) 54 | } 55 | 56 | async fn try_open_channel(mut handle: ClientHandle, channel_id: u32) -> Result { 57 | let path = PathRef::new("maestro_pw.Maestro/GetSoftwareInfo"); 58 | let service_id = path.service().hash(); 59 | let method_id = path.method().hash(); 60 | 61 | let req = Request { 62 | channel_id, 63 | service_id, 64 | method_id, 65 | call_id: 0xffffffff, 66 | message: (), 67 | }; 68 | 69 | let mut rsp: UnaryResponse = handle.open_unary(req)?; 70 | 71 | rsp.result().await?; 72 | Ok(channel_id) 73 | } 74 | -------------------------------------------------------------------------------- /libmaestro/src/pwrpc/id.rs: -------------------------------------------------------------------------------- 1 | pub type Hash = u32; 2 | 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq)] 5 | pub struct Id { 6 | name: String, 7 | } 8 | 9 | impl Id { 10 | pub fn new(id: impl Into) -> Self { 11 | Self { name: id.into() } 12 | } 13 | 14 | pub fn name(&self) -> &str { 15 | &self.name 16 | } 17 | 18 | pub fn hash(&self) -> Hash { 19 | hash::hash_65599(&self.name) 20 | } 21 | 22 | pub fn as_ref(&self) -> IdRef<'_> { 23 | IdRef { name: &self.name } 24 | } 25 | } 26 | 27 | impl From for Id 28 | where 29 | S: Into 30 | { 31 | fn from(name: S) -> Self { 32 | Id::new(name) 33 | } 34 | } 35 | 36 | impl<'a> From> for Id { 37 | fn from(id: IdRef<'a>) -> Self { 38 | Id::new(id.name()) 39 | } 40 | } 41 | 42 | 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 44 | pub struct IdRef<'a> { 45 | name: &'a str, 46 | } 47 | 48 | impl<'a> IdRef<'a> { 49 | pub fn new(name: &'a str) -> Self { 50 | Self { name } 51 | } 52 | 53 | pub fn name(&self) -> &'a str { 54 | self.name 55 | } 56 | 57 | pub fn hash(&self) -> Hash { 58 | hash::hash_65599(self.name) 59 | } 60 | } 61 | 62 | impl<'a> From<&'a str> for IdRef<'a> { 63 | fn from(name: &'a str) -> Self { 64 | IdRef::new(name) 65 | } 66 | } 67 | 68 | impl<'a> From<&'a String> for IdRef<'a> { 69 | fn from(name: &'a String) -> Self { 70 | IdRef::new(name) 71 | } 72 | } 73 | 74 | 75 | #[derive(Debug, Clone, PartialEq, Eq)] 76 | pub struct Path { 77 | path: String, 78 | split: usize, 79 | } 80 | 81 | impl Path { 82 | pub fn new(path: impl Into) -> Self { 83 | let path = path.into(); 84 | let split = path.rfind('/').unwrap_or(0); 85 | 86 | Path { path, split } 87 | } 88 | 89 | pub fn service(&self) -> IdRef<'_> { 90 | IdRef::new(&self.path[..self.split]) 91 | } 92 | 93 | pub fn method(&self) -> IdRef<'_> { 94 | if self.split < self.path.len() { 95 | IdRef::new(&self.path[self.split+1..]) 96 | } else { 97 | IdRef::new(&self.path[0..0]) 98 | } 99 | } 100 | 101 | pub fn as_ref(&self) -> PathRef<'_> { 102 | PathRef { path: &self.path, split: self.split } 103 | } 104 | } 105 | 106 | impl From<&str> for Path { 107 | fn from(name: &str) -> Self { 108 | Path::new(name) 109 | } 110 | } 111 | 112 | impl From for Path { 113 | fn from(name: String) -> Self { 114 | Path::new(name) 115 | } 116 | } 117 | 118 | 119 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 120 | pub struct PathRef<'a> { 121 | path: &'a str, 122 | split: usize, 123 | } 124 | 125 | impl<'a> PathRef<'a> { 126 | pub fn new(path: &'a str) -> Self { 127 | let split = path.rfind('/').unwrap_or(0); 128 | 129 | PathRef { path, split } 130 | } 131 | 132 | pub fn service(&self) -> IdRef<'a> { 133 | IdRef::new(&self.path[..self.split]) 134 | } 135 | 136 | pub fn method(&self) -> IdRef<'a> { 137 | if self.split < self.path.len() { 138 | IdRef::new(&self.path[self.split+1..]) 139 | } else { 140 | IdRef::new(&self.path[0..0]) 141 | } 142 | } 143 | } 144 | 145 | impl<'a> From<&'a str> for PathRef<'a> { 146 | fn from(name: &'a str) -> Self { 147 | PathRef::new(name) 148 | } 149 | } 150 | 151 | 152 | mod hash { 153 | const HASH_CONST: u32 = 65599; 154 | 155 | pub fn hash_65599(id: &str) -> u32 { 156 | let mut hash = id.len() as u32; 157 | let mut coef = HASH_CONST; 158 | 159 | for chr in id.chars() { 160 | hash = hash.wrapping_add(coef.wrapping_mul(chr as u32)); 161 | coef = coef.wrapping_mul(HASH_CONST); 162 | } 163 | 164 | hash 165 | } 166 | } 167 | 168 | 169 | #[cfg(test)] 170 | mod test { 171 | use super::*; 172 | 173 | #[test] 174 | fn test_known_id_hashes() { 175 | assert_eq!(IdRef::new("maestro_pw.Maestro").hash(), 0x7ede71ea); 176 | assert_eq!(IdRef::new("GetSoftwareInfo").hash(), 0x7199fa44); 177 | assert_eq!(IdRef::new("SubscribeToSettingsChanges").hash(), 0x2821adf5); 178 | } 179 | 180 | #[test] 181 | fn test_path() { 182 | let pref = PathRef::new("maestro_pw.Maestro/GetSoftwareInfo"); 183 | assert_eq!(pref.service().name(), "maestro_pw.Maestro"); 184 | assert_eq!(pref.service().hash(), 0x7ede71ea); 185 | assert_eq!(pref.method().name(), "GetSoftwareInfo"); 186 | assert_eq!(pref.method().hash(), 0x7199fa44); 187 | 188 | let pref = PathRef::new("maestro_pw.Maestro/SubscribeToSettingsChanges"); 189 | assert_eq!(pref.service().name(), "maestro_pw.Maestro"); 190 | assert_eq!(pref.service().hash(), 0x7ede71ea); 191 | assert_eq!(pref.method().name(), "SubscribeToSettingsChanges"); 192 | assert_eq!(pref.method().hash(), 0x2821adf5); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /libmaestro/src/pwrpc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod id; 3 | pub mod types; 4 | pub mod utils; 5 | 6 | mod status; 7 | pub use status::Error; 8 | pub use status::Status; 9 | -------------------------------------------------------------------------------- /libmaestro/src/pwrpc/status.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 2 | pub enum Status { 3 | Ok = 0, 4 | Cancelled = 1, 5 | Unknown = 2, 6 | InvalidArgument = 3, 7 | DeadlineExceeded = 4, 8 | NotFound = 5, 9 | AlreadyExists = 6, 10 | PermissionDenied = 7, 11 | ResourceExhausted = 8, 12 | FailedPrecondition = 9, 13 | Aborted = 10, 14 | OutOfRange = 11, 15 | Unimplemented = 12, 16 | Internal = 13, 17 | Unavailable = 14, 18 | DataLoss = 15, 19 | Unauthenticated = 16, 20 | } 21 | 22 | impl Status { 23 | pub fn description(&self) -> &'static str { 24 | match self { 25 | Status::Ok => "The operation completed successfully", 26 | Status::Cancelled => "The operation was cancelled", 27 | Status::Unknown => "Unknown error", 28 | Status::InvalidArgument => "Client specified an invalid argument", 29 | Status::DeadlineExceeded => "Deadline expired before operation could complete", 30 | Status::NotFound => "Some requested entity was not found", 31 | Status::AlreadyExists => "Some entity that we attempted to create already exists", 32 | Status::PermissionDenied => "The caller does not have permission to execute the specified operation", 33 | Status::ResourceExhausted => "Some resource has been exhausted", 34 | Status::FailedPrecondition => "The system is not in a state required for the operation's execution", 35 | Status::Aborted => "The operation was aborted", 36 | Status::OutOfRange => "Operation was attempted past the valid range", 37 | Status::Unimplemented => "Operation is not implemented or not supported", 38 | Status::Internal => "Internal error", 39 | Status::Unavailable => "The service is currently unavailable", 40 | Status::DataLoss => "Unrecoverable data loss or corruption", 41 | Status::Unauthenticated => "The request does not have valid authentication credentials", 42 | } 43 | } 44 | } 45 | 46 | impl std::fmt::Display for Status { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | write!(f, "{}", self.description()) 49 | } 50 | } 51 | 52 | impl From for Status { 53 | fn from(value: u32) -> Self { 54 | match value { 55 | 0 => Status::Ok, 56 | 1 => Status::Cancelled, 57 | 2 => Status::Unknown, 58 | 3 => Status::InvalidArgument, 59 | 4 => Status::DeadlineExceeded, 60 | 5 => Status::NotFound, 61 | 6 => Status::AlreadyExists, 62 | 7 => Status::PermissionDenied, 63 | 8 => Status::ResourceExhausted, 64 | 9 => Status::FailedPrecondition, 65 | 10 => Status::Aborted, 66 | 11 => Status::OutOfRange, 67 | 12 => Status::Unimplemented, 68 | 13 => Status::Internal, 69 | 14 => Status::Unavailable, 70 | 15 => Status::DataLoss, 71 | 16 => Status::Unauthenticated, 72 | _ => Status::Unknown, 73 | } 74 | } 75 | } 76 | 77 | impl From for u32 { 78 | fn from(value: Status) -> Self { 79 | value as _ 80 | } 81 | } 82 | 83 | 84 | #[derive(Debug)] 85 | pub struct Error { 86 | code: Status, 87 | message: String, 88 | source: Option>, 89 | } 90 | 91 | impl Error { 92 | pub fn new(code: Status, message: impl Into) -> Self { 93 | Self { 94 | code, 95 | message: message.into(), 96 | source: None, 97 | } 98 | } 99 | 100 | pub fn cancelled(message: impl Into) -> Self { 101 | Self::new(Status::Cancelled, message) 102 | } 103 | 104 | pub fn unknown(message: impl Into) -> Self { 105 | Self::new(Status::Unknown, message) 106 | } 107 | 108 | pub fn invalid_argument(message: impl Into) -> Self { 109 | Self::new(Status::InvalidArgument, message) 110 | } 111 | 112 | pub fn deadline_exceeded(message: impl Into) -> Self { 113 | Self::new(Status::DeadlineExceeded, message) 114 | } 115 | 116 | pub fn not_found(message: impl Into) -> Self { 117 | Self::new(Status::NotFound, message) 118 | } 119 | 120 | pub fn already_exists(message: impl Into) -> Self { 121 | Self::new(Status::AlreadyExists, message) 122 | } 123 | 124 | pub fn permission_denied(message: impl Into) -> Self { 125 | Self::new(Status::PermissionDenied, message) 126 | } 127 | 128 | pub fn resource_exhausted(message: impl Into) -> Self { 129 | Self::new(Status::ResourceExhausted, message) 130 | } 131 | 132 | pub fn failed_precondition(message: impl Into) -> Self { 133 | Self::new(Status::FailedPrecondition, message) 134 | } 135 | 136 | pub fn aborted(message: impl Into) -> Self { 137 | Self::new(Status::Aborted, message) 138 | } 139 | 140 | pub fn out_of_range(message: impl Into) -> Self { 141 | Self::new(Status::OutOfRange, message) 142 | } 143 | 144 | pub fn unimplemented(message: impl Into) -> Self { 145 | Self::new(Status::Unimplemented, message) 146 | } 147 | 148 | pub fn internal(message: impl Into) -> Self { 149 | Self::new(Status::Internal, message) 150 | } 151 | 152 | pub fn unavailable(message: impl Into) -> Self { 153 | Self::new(Status::Unavailable, message) 154 | } 155 | 156 | pub fn data_loss(message: impl Into) -> Self { 157 | Self::new(Status::DataLoss, message) 158 | } 159 | 160 | pub fn unauthenticated(message: impl Into) -> Self { 161 | Self::new(Status::Unauthenticated, message) 162 | } 163 | 164 | pub fn extend( 165 | code: Status, 166 | message: impl Into, 167 | error: impl Into>, 168 | ) -> Self { 169 | Self { 170 | code, 171 | message: message.into(), 172 | source: Some(error.into()), 173 | } 174 | } 175 | 176 | pub fn code(&self) -> Status { 177 | self.code 178 | } 179 | 180 | pub fn message(&self) -> &str { 181 | &self.message 182 | } 183 | } 184 | 185 | impl From for Error { 186 | fn from(code: Status) -> Self { 187 | Self::new(code, code.description()) 188 | } 189 | } 190 | 191 | impl From for Error { 192 | fn from(err: std::io::Error) -> Self { 193 | use std::io::ErrorKind; 194 | 195 | let code = match err.kind() { 196 | ErrorKind::BrokenPipe 197 | | ErrorKind::WouldBlock 198 | | ErrorKind::WriteZero 199 | | ErrorKind::Interrupted => Status::Internal, 200 | ErrorKind::ConnectionRefused 201 | | ErrorKind::ConnectionReset 202 | | ErrorKind::NotConnected 203 | | ErrorKind::AddrInUse 204 | | ErrorKind::AddrNotAvailable => Status::Unavailable, 205 | ErrorKind::AlreadyExists => Status::AlreadyExists, 206 | ErrorKind::ConnectionAborted => Status::Aborted, 207 | ErrorKind::InvalidData => Status::DataLoss, 208 | ErrorKind::InvalidInput => Status::InvalidArgument, 209 | ErrorKind::NotFound => Status::NotFound, 210 | ErrorKind::PermissionDenied => Status::PermissionDenied, 211 | ErrorKind::TimedOut => Status::DeadlineExceeded, 212 | ErrorKind::UnexpectedEof => Status::OutOfRange, 213 | _ => Status::Unknown, 214 | }; 215 | 216 | Error::extend(code, err.to_string(), err) 217 | } 218 | } 219 | 220 | impl From for Error { 221 | fn from(error: prost::DecodeError) -> Self { 222 | Self::extend(Status::InvalidArgument, "failed to decode message", error) 223 | } 224 | } 225 | 226 | impl std::fmt::Display for Error { 227 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 228 | write!(f, "status: {:?}, message: {:?}", self.code, self.message) 229 | } 230 | } 231 | 232 | impl std::error::Error for Error { 233 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 234 | self.source.as_ref().map(|err| (&**err) as _) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /libmaestro/src/pwrpc/types.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 2 | pub enum RpcType { 3 | Unary, 4 | ServerStream, 5 | ClientStream, 6 | BidirectionalStream, 7 | } 8 | 9 | impl RpcType { 10 | pub fn has_server_stream(&self) -> bool { 11 | match *self { 12 | RpcType::ServerStream | RpcType::BidirectionalStream => true, 13 | RpcType::Unary | RpcType::ClientStream => false, 14 | } 15 | } 16 | 17 | pub fn has_client_stream(&self) -> bool { 18 | match *self { 19 | RpcType::ClientStream | RpcType::BidirectionalStream => true, 20 | RpcType::Unary | RpcType::ServerStream => false, 21 | } 22 | } 23 | } 24 | 25 | 26 | mod generated { 27 | include!(concat!(env!("OUT_DIR"), "/pw.rpc.packet.rs")); 28 | } 29 | 30 | pub use generated::PacketType; 31 | pub use generated::RpcPacket; 32 | -------------------------------------------------------------------------------- /libmaestro/src/pwrpc/utils.rs: -------------------------------------------------------------------------------- 1 | //! Miscellaneous utilities and helpers. 2 | 3 | use bytes::{Buf, BufMut}; 4 | 5 | /// An encoded protobuf message. 6 | /// 7 | /// This type represents an encoded protobuf message. Decoding and encoding are 8 | /// essentially no-ops, reading and writing to/from the internal buffer. It is 9 | /// a drop-in replacement for any valid (and invalid) protobuf type. 10 | /// 11 | /// This type is intended for reverse-engineering and testing, e.g., in 12 | /// combination with tools like `protoscope`. 13 | #[derive(Clone, Default)] 14 | pub struct EncodedMessage { 15 | pub data: Vec, 16 | } 17 | 18 | impl std::fmt::Debug for EncodedMessage { 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 20 | write!(f, "{:02x?}", self.data) 21 | } 22 | } 23 | 24 | impl prost::Message for EncodedMessage { 25 | fn encode_raw(&self, buf: &mut impl BufMut) { 26 | buf.put_slice(&self.data[..]) 27 | } 28 | 29 | fn merge_field( 30 | &mut self, 31 | _tag: u32, 32 | _wire_type: prost::encoding::WireType, 33 | _buf: &mut impl Buf, 34 | _ctx: prost::encoding::DecodeContext, 35 | ) -> Result<(), prost::DecodeError> { 36 | unimplemented!("use merge() instead") 37 | } 38 | 39 | fn merge(&mut self, mut buf: impl Buf) -> Result<(), prost::DecodeError> { 40 | let a = self.data.len(); 41 | let b = a + buf.remaining(); 42 | 43 | self.data.resize(b, 0); 44 | buf.copy_to_slice(&mut self.data[a..b]); 45 | 46 | Ok(()) 47 | } 48 | 49 | fn encoded_len(&self) -> usize { 50 | self.data.len() 51 | } 52 | 53 | fn clear(&mut self) { 54 | self.data.clear() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /libmaestro/src/service/impls/dosimeter.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::types::{ 2 | DosimeterSummary, DosimeterLiveDbMsg, 3 | }; 4 | use crate::pwrpc::client::{ClientHandle, ServerStreamRpc, StreamResponse, UnaryRpc}; 5 | use crate::pwrpc::Error; 6 | 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct DosimeterService { 10 | client: ClientHandle, 11 | channel_id: u32, 12 | 13 | rpc_fetch_daily_summaries: UnaryRpc<(), DosimeterSummary>, 14 | rpc_sub_live_db: ServerStreamRpc<(), DosimeterLiveDbMsg>, 15 | } 16 | 17 | impl DosimeterService { 18 | pub fn new(client: ClientHandle, channel_id: u32) -> Self { 19 | Self { 20 | client, 21 | channel_id, 22 | 23 | rpc_fetch_daily_summaries: UnaryRpc::new("maestro_pw.Dosimeter/FetchDailySummaries"), 24 | rpc_sub_live_db: ServerStreamRpc::new("maestro_pw.Dosimeter/SubscribeToLiveDb"), 25 | } 26 | } 27 | 28 | pub async fn fetch_daily_summaries(&mut self) -> Result { 29 | self.rpc_fetch_daily_summaries.call(&mut self.client, self.channel_id, 0, ())? 30 | .result().await 31 | } 32 | 33 | pub fn subscribe_to_live_db(&mut self) -> Result, Error> { 34 | self.rpc_sub_live_db.call(&mut self.client, self.channel_id, 0, ()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libmaestro/src/service/impls/maestro.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::types::{ 2 | self, read_setting_msg, settings_rsp, write_setting_msg, HardwareInfo, OobeActionRsp, 3 | ReadSettingMsg, RuntimeInfo, SettingsRsp, SoftwareInfo, WriteSettingMsg, 4 | }; 5 | use crate::pwrpc::client::{ClientHandle, ServerStreamRpc, StreamResponse, UnaryRpc}; 6 | use crate::pwrpc::Error; 7 | use crate::service::settings::{Setting, SettingId, SettingValue}; 8 | 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct MaestroService { 12 | client: ClientHandle, 13 | channel_id: u32, 14 | 15 | rpc_get_software_info: UnaryRpc<(), SoftwareInfo>, 16 | rpc_get_hardware_info: UnaryRpc<(), HardwareInfo>, 17 | rpc_sub_runtime_info: ServerStreamRpc<(), RuntimeInfo>, 18 | 19 | rpc_write_setting: UnaryRpc, 20 | rpc_read_setting: UnaryRpc, 21 | rpc_sub_settings_changes: ServerStreamRpc<(), SettingsRsp>, 22 | 23 | rpc_sub_oobe_actions: ServerStreamRpc<(), OobeActionRsp>, 24 | } 25 | 26 | impl MaestroService { 27 | pub fn new(client: ClientHandle, channel_id: u32) -> Self { 28 | Self { 29 | client, 30 | channel_id, 31 | 32 | rpc_get_software_info: UnaryRpc::new("maestro_pw.Maestro/GetSoftwareInfo"), 33 | rpc_get_hardware_info: UnaryRpc::new("maestro_pw.Maestro/GetHardwareInfo"), 34 | rpc_sub_runtime_info: ServerStreamRpc::new("maestro_pw.Maestro/SubscribeRuntimeInfo"), 35 | 36 | rpc_write_setting: UnaryRpc::new("maestro_pw.Maestro/WriteSetting"), 37 | rpc_read_setting: UnaryRpc::new("maestro_pw.Maestro/ReadSetting"), 38 | rpc_sub_settings_changes: ServerStreamRpc::new("maestro_pw.Maestro/SubscribeToSettingsChanges"), 39 | 40 | rpc_sub_oobe_actions: ServerStreamRpc::new("maestro_pw.Maestro/SubscribeToOobeActions"), 41 | } 42 | } 43 | 44 | pub async fn get_software_info(&mut self) -> Result { 45 | self.rpc_get_software_info.call(&mut self.client, self.channel_id, 0, ())? 46 | .result().await 47 | } 48 | 49 | pub async fn get_hardware_info(&mut self) -> Result { 50 | self.rpc_get_hardware_info.call(&mut self.client, self.channel_id, 0, ())? 51 | .result().await 52 | } 53 | 54 | pub fn subscribe_to_runtime_info(&mut self) -> Result, Error> { 55 | self.rpc_sub_runtime_info.call(&mut self.client, self.channel_id, 0, ()) 56 | } 57 | 58 | pub async fn write_setting_raw(&mut self, setting: WriteSettingMsg) -> Result<(), Error> { 59 | self.rpc_write_setting.call(&mut self.client, self.channel_id, 0, setting)? 60 | .result().await 61 | } 62 | 63 | pub async fn write_setting(&mut self, setting: SettingValue) -> Result<(), Error> { 64 | let setting = types::SettingValue { 65 | value_oneof: Some(setting.into()), 66 | }; 67 | 68 | let setting = WriteSettingMsg { 69 | value_oneof: Some(write_setting_msg::ValueOneof::Setting(setting)), 70 | }; 71 | 72 | self.write_setting_raw(setting).await 73 | } 74 | 75 | pub async fn read_setting_raw(&mut self, setting: ReadSettingMsg) -> Result { 76 | self.rpc_read_setting.call(&mut self.client, self.channel_id, 0, setting)? 77 | .result().await 78 | } 79 | 80 | pub async fn read_setting_var(&mut self, setting: SettingId) -> Result { 81 | let setting = read_setting_msg::ValueOneof::SettingsId(setting.into()); 82 | let setting = ReadSettingMsg { value_oneof: Some(setting) }; 83 | 84 | let value = self.read_setting_raw(setting).await?; 85 | 86 | let value = value.value_oneof 87 | .ok_or_else(|| Error::invalid_argument("did not receive any settings value"))?; 88 | 89 | let settings_rsp::ValueOneof::Value(value) = value; 90 | 91 | let value = value.value_oneof 92 | .ok_or_else(|| Error::invalid_argument("did not receive any settings value"))?; 93 | 94 | Ok(value.into()) 95 | } 96 | 97 | pub async fn read_setting(&mut self, setting: T) -> Result 98 | where 99 | T: Setting, 100 | { 101 | let value = self.read_setting_var(setting.id()).await?; 102 | 103 | T::from_var(value) 104 | .ok_or_else(|| Error::invalid_argument("failed to decode settings value")) 105 | } 106 | 107 | pub fn subscribe_to_settings_changes(&mut self) -> Result, Error> { 108 | self.rpc_sub_settings_changes.call(&mut self.client, self.channel_id, 0, ()) 109 | } 110 | 111 | pub fn subscribe_to_oobe_actions(&mut self) -> Result, Error> { 112 | self.rpc_sub_oobe_actions.call(&mut self.client, self.channel_id, 0, ()) 113 | } 114 | 115 | // TODO: 116 | // - SetWallClock 117 | } 118 | -------------------------------------------------------------------------------- /libmaestro/src/service/impls/mod.rs: -------------------------------------------------------------------------------- 1 | mod dosimeter; 2 | pub use self::dosimeter::DosimeterService; 3 | 4 | mod maestro; 5 | pub use self::maestro::MaestroService; 6 | 7 | mod multipoint; 8 | pub use self::multipoint::MultipointService; 9 | -------------------------------------------------------------------------------- /libmaestro/src/service/impls/multipoint.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::types::QuietModeStatusEvent; 2 | use crate::pwrpc::client::{ClientHandle, ServerStreamRpc, StreamResponse}; 3 | use crate::pwrpc::Error; 4 | 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct MultipointService { 8 | client: ClientHandle, 9 | channel_id: u32, 10 | 11 | rpc_sub_quiet_mode_status: ServerStreamRpc<(), QuietModeStatusEvent>, 12 | } 13 | 14 | impl MultipointService { 15 | pub fn new(client: ClientHandle, channel_id: u32) -> Self { 16 | Self { 17 | client, 18 | channel_id, 19 | 20 | rpc_sub_quiet_mode_status: ServerStreamRpc::new("maestro_pw.Multipoint/SubscribeToQuietModeStatus"), 21 | } 22 | } 23 | 24 | pub fn subscribe_to_quiet_mode_status(&mut self) -> Result, Error> { 25 | self.rpc_sub_quiet_mode_status.call(&mut self.client, self.channel_id, 0, ()) 26 | } 27 | 28 | // TODO: 29 | // - ForceMultipointSwitch 30 | } 31 | -------------------------------------------------------------------------------- /libmaestro/src/service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod settings; 2 | 3 | mod impls; 4 | pub use impls::{MaestroService, MultipointService, DosimeterService}; 5 | -------------------------------------------------------------------------------- /libmaestro/src/service/settings.rs: -------------------------------------------------------------------------------- 1 | use num_enum::{IntoPrimitive, FromPrimitive}; 2 | 3 | use crate::protocol::types; 4 | 5 | 6 | #[repr(i32)] 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 8 | pub enum SettingId { 9 | AutoOtaEnable = 1, 10 | OhdEnable = 2, 11 | OobeIsFinished = 3, 12 | GestureEnable = 4, 13 | DiagnosticsEnable = 5, 14 | OobeMode = 6, 15 | GestureControl = 7, 16 | AncAccessibilityMode = 8, 17 | AncrStateOneBud = 9, 18 | AncrStateTwoBuds = 10, 19 | MultipointEnable = 11, 20 | AncrGestureLoop = 12, 21 | CurrentAncrState = 13, 22 | OttsMode = 14, 23 | VolumeEqEnable = 15, 24 | CurrentUserEq = 16, 25 | VolumeAsymmetry = 17, 26 | LastSavedUserEq = 18, 27 | SumToMono = 19, 28 | VolumeExposureNotifications = 21, 29 | SpeechDetection = 22, 30 | 31 | #[num_enum(catch_all)] 32 | Unknown(i32), 33 | } 34 | 35 | 36 | #[derive(Debug, Clone, PartialEq)] 37 | pub enum SettingValue { 38 | AutoOtaEnable(bool), 39 | OhdEnable(bool), 40 | OobeIsFinished(bool), 41 | GestureEnable(bool), 42 | DiagnosticsEnable(bool), 43 | OobeMode(bool), 44 | GestureControl(GestureControl), 45 | MultipointEnable(bool), 46 | AncrGestureLoop(AncrGestureLoop), 47 | CurrentAncrState(AncState), 48 | OttsMode(i32), 49 | VolumeEqEnable(bool), 50 | CurrentUserEq(EqBands), 51 | VolumeAsymmetry(VolumeAsymmetry), 52 | SumToMono(bool), 53 | VolumeExposureNotifications(bool), 54 | SpeechDetection(bool), 55 | } 56 | 57 | impl SettingValue { 58 | pub fn id(&self) -> SettingId { 59 | match self { 60 | SettingValue::AutoOtaEnable(_) => SettingId::AutoOtaEnable, 61 | SettingValue::OhdEnable(_) => SettingId::OhdEnable, 62 | SettingValue::OobeIsFinished(_) => SettingId::OobeIsFinished, 63 | SettingValue::GestureEnable(_) => SettingId::GestureEnable, 64 | SettingValue::DiagnosticsEnable(_) => SettingId::DiagnosticsEnable, 65 | SettingValue::OobeMode(_) => SettingId::OobeMode, 66 | SettingValue::GestureControl(_) => SettingId::GestureControl, 67 | SettingValue::MultipointEnable(_) => SettingId::MultipointEnable, 68 | SettingValue::AncrGestureLoop(_) => SettingId::AncrGestureLoop, 69 | SettingValue::CurrentAncrState(_) => SettingId::CurrentAncrState, 70 | SettingValue::OttsMode(_) => SettingId::OttsMode, 71 | SettingValue::VolumeEqEnable(_) => SettingId::VolumeEqEnable, 72 | SettingValue::CurrentUserEq(_) => SettingId::CurrentUserEq, 73 | SettingValue::VolumeAsymmetry(_) => SettingId::VolumeAsymmetry, 74 | SettingValue::SumToMono(_) => SettingId::SumToMono, 75 | SettingValue::VolumeExposureNotifications(_) => SettingId::VolumeExposureNotifications, 76 | SettingValue::SpeechDetection(_) => SettingId::SpeechDetection, 77 | } 78 | } 79 | } 80 | 81 | impl From for SettingValue { 82 | fn from(value: crate::protocol::types::setting_value::ValueOneof) -> Self { 83 | use types::setting_value::ValueOneof; 84 | 85 | match value { 86 | ValueOneof::AutoOtaEnable(x) => SettingValue::AutoOtaEnable(x), 87 | ValueOneof::OhdEnable(x) => SettingValue::OhdEnable(x), 88 | ValueOneof::OobeIsFinished(x) => SettingValue::OobeIsFinished(x), 89 | ValueOneof::GestureEnable(x) => SettingValue::GestureEnable(x), 90 | ValueOneof::DiagnosticsEnable(x) => SettingValue::DiagnosticsEnable(x), 91 | ValueOneof::OobeMode(x) => SettingValue::OobeMode(x), 92 | ValueOneof::GestureControl(x) => SettingValue::GestureControl(GestureControl::from(x)), 93 | ValueOneof::MultipointEnable(x) => SettingValue::MultipointEnable(x), 94 | ValueOneof::AncrGestureLoop(x) => SettingValue::AncrGestureLoop(AncrGestureLoop::from(x)), 95 | ValueOneof::CurrentAncrState(x) => SettingValue::CurrentAncrState(AncState::from_primitive(x)), 96 | ValueOneof::OttsMode(x) => SettingValue::OttsMode(x), 97 | ValueOneof::VolumeEqEnable(x) => SettingValue::VolumeEqEnable(x), 98 | ValueOneof::CurrentUserEq(x) => SettingValue::CurrentUserEq(EqBands::from(x)), 99 | ValueOneof::VolumeAsymmetry(x) => SettingValue::VolumeAsymmetry(VolumeAsymmetry::from_raw(x)), 100 | ValueOneof::SumToMono(x) => SettingValue::SumToMono(x), 101 | ValueOneof::VolumeExposureNotifications(x) => SettingValue::VolumeExposureNotifications(x), 102 | ValueOneof::SpeechDetection(x) => SettingValue::SpeechDetection(x), 103 | } 104 | } 105 | } 106 | 107 | impl From for types::setting_value::ValueOneof { 108 | fn from(value: SettingValue) -> Self { 109 | use types::setting_value::ValueOneof; 110 | 111 | match value { 112 | SettingValue::AutoOtaEnable(x) => ValueOneof::AutoOtaEnable(x), 113 | SettingValue::OhdEnable(x) => ValueOneof::OhdEnable(x), 114 | SettingValue::OobeIsFinished(x) => ValueOneof::OobeIsFinished(x), 115 | SettingValue::GestureEnable(x) => ValueOneof::GestureEnable(x), 116 | SettingValue::DiagnosticsEnable(x) => ValueOneof::DiagnosticsEnable(x), 117 | SettingValue::OobeMode(x) => ValueOneof::OobeMode(x), 118 | SettingValue::GestureControl(x) => ValueOneof::GestureControl(x.into()), 119 | SettingValue::MultipointEnable(x) => ValueOneof::MultipointEnable(x), 120 | SettingValue::AncrGestureLoop(x) => ValueOneof::AncrGestureLoop(x.into()), 121 | SettingValue::CurrentAncrState(x) => ValueOneof::CurrentAncrState(x.into()), 122 | SettingValue::OttsMode(x) => ValueOneof::OttsMode(x), 123 | SettingValue::VolumeEqEnable(x) => ValueOneof::VolumeEqEnable(x), 124 | SettingValue::CurrentUserEq(x) => ValueOneof::CurrentUserEq(x.into()), 125 | SettingValue::VolumeAsymmetry(x) => ValueOneof::VolumeAsymmetry(x.raw()), 126 | SettingValue::SumToMono(x) => ValueOneof::SumToMono(x), 127 | SettingValue::VolumeExposureNotifications(x) => ValueOneof::VolumeExposureNotifications(x), 128 | SettingValue::SpeechDetection(x) => ValueOneof::SpeechDetection(x), 129 | } 130 | } 131 | } 132 | 133 | 134 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 135 | pub struct GestureControl { 136 | pub left: RegularActionTarget, 137 | pub right: RegularActionTarget, 138 | } 139 | 140 | impl From for GestureControl { 141 | fn from(value: types::GestureControl) -> Self { 142 | let left = value.left 143 | .and_then(|v| v.value_oneof) 144 | .map(|types::device_gesture_control::ValueOneof::Type(x)| x) 145 | .map(|v| RegularActionTarget::from_primitive(v.value)) 146 | .unwrap_or(RegularActionTarget::Unknown(-1)); 147 | 148 | let right = value.right 149 | .and_then(|v| v.value_oneof) 150 | .map(|types::device_gesture_control::ValueOneof::Type(x)| x) 151 | .map(|v| RegularActionTarget::from_primitive(v.value)) 152 | .unwrap_or(RegularActionTarget::Unknown(-1)); 153 | 154 | GestureControl { left, right } 155 | } 156 | } 157 | 158 | impl From for types::GestureControl { 159 | fn from(value: GestureControl) -> Self { 160 | use types::device_gesture_control::ValueOneof; 161 | 162 | let left = types::DeviceGestureControl { 163 | value_oneof: Some(ValueOneof::Type(types::GestureControlType { 164 | value: value.left.into(), 165 | })), 166 | }; 167 | 168 | let right = types::DeviceGestureControl { 169 | value_oneof: Some(ValueOneof::Type(types::GestureControlType { 170 | value: value.right.into(), 171 | })), 172 | }; 173 | 174 | Self { 175 | left: Some(left), 176 | right: Some(right), 177 | } 178 | } 179 | } 180 | 181 | impl Default for GestureControl { 182 | fn default() -> Self { 183 | Self { 184 | left: RegularActionTarget::AncControl, 185 | right: RegularActionTarget::AncControl, 186 | } 187 | } 188 | } 189 | 190 | impl std::fmt::Display for GestureControl { 191 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 192 | write!(f, "left: {}, right: {}", self.left, self.right) 193 | } 194 | } 195 | 196 | 197 | #[repr(i32)] 198 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 199 | pub enum RegularActionTarget { 200 | CheckNotifications = 1, 201 | PreviousTrackRepeat = 2, 202 | NextTrack = 3, 203 | PlayPauseTrack = 4, 204 | AncControl = 5, 205 | AssistantQuery = 6, 206 | 207 | #[num_enum(catch_all)] 208 | Unknown(i32), 209 | } 210 | 211 | impl RegularActionTarget { 212 | pub fn as_str(&self) -> &'static str { 213 | match self { 214 | RegularActionTarget::CheckNotifications => "check-notifications", 215 | RegularActionTarget::PreviousTrackRepeat => "previous", 216 | RegularActionTarget::NextTrack => "next", 217 | RegularActionTarget::PlayPauseTrack => "play-pause", 218 | RegularActionTarget::AncControl => "anc", 219 | RegularActionTarget::AssistantQuery => "assistant", 220 | RegularActionTarget::Unknown(_) => "unknown", 221 | } 222 | } 223 | } 224 | 225 | impl std::fmt::Display for RegularActionTarget { 226 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 227 | match self { 228 | RegularActionTarget::CheckNotifications => write!(f, "check-notifications"), 229 | RegularActionTarget::PreviousTrackRepeat => write!(f, "previous"), 230 | RegularActionTarget::NextTrack => write!(f, "next"), 231 | RegularActionTarget::PlayPauseTrack => write!(f, "play-pause"), 232 | RegularActionTarget::AncControl => write!(f, "anc"), 233 | RegularActionTarget::AssistantQuery => write!(f, "assistant"), 234 | RegularActionTarget::Unknown(x) => write!(f, "unknown ({x})"), 235 | } 236 | } 237 | } 238 | 239 | 240 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 241 | pub struct AncrGestureLoop { 242 | pub active: bool, 243 | pub off: bool, 244 | pub aware: bool, 245 | } 246 | 247 | impl AncrGestureLoop { 248 | pub fn is_valid(&self) -> bool { 249 | // at least two need to be set 250 | (self.active as u32 + self.off as u32 + self.aware as u32) >= 2 251 | } 252 | } 253 | 254 | impl From for AncrGestureLoop { 255 | fn from(other: types::AncrGestureLoop) -> Self { 256 | AncrGestureLoop { active: other.active, off: other.off, aware: other.aware } 257 | } 258 | } 259 | 260 | impl From for types::AncrGestureLoop { 261 | fn from(other: AncrGestureLoop) -> Self { 262 | Self { 263 | active: other.active, 264 | off: other.off, 265 | aware: other.aware, 266 | } 267 | } 268 | } 269 | 270 | impl std::fmt::Display for AncrGestureLoop { 271 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 272 | let mut n = 0; 273 | 274 | write!(f, "[")?; 275 | 276 | if self.active { 277 | write!(f, "active")?; 278 | n += 1; 279 | } 280 | 281 | if self.off { 282 | if n > 0 { 283 | write!(f, ", ")?; 284 | } 285 | 286 | write!(f, "off")?; 287 | n += 1; 288 | } 289 | 290 | if self.aware { 291 | if n > 0 { 292 | write!(f, ", ")?; 293 | } 294 | 295 | write!(f, "aware")?; 296 | } 297 | 298 | write!(f, "]") 299 | } 300 | } 301 | 302 | 303 | #[repr(i32)] 304 | #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)] 305 | pub enum AncState { 306 | Off = 1, 307 | Active = 2, 308 | Aware = 3, 309 | 310 | #[num_enum(catch_all)] 311 | Unknown(i32), 312 | } 313 | 314 | impl AncState { 315 | pub fn as_str(&self) -> &'static str { 316 | match self { 317 | AncState::Off => "off", 318 | AncState::Active => "active", 319 | AncState::Aware => "aware", 320 | AncState::Unknown(_) => "unknown", 321 | } 322 | } 323 | } 324 | 325 | // #[derive(Default)] clashes with #[derive(FromPrimitive)] 326 | #[allow(clippy::derivable_impls)] 327 | impl Default for AncState { 328 | fn default() -> Self { 329 | AncState::Off 330 | } 331 | } 332 | 333 | impl std::fmt::Display for AncState { 334 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 335 | match self { 336 | AncState::Off => write!(f, "off"), 337 | AncState::Active => write!(f, "active"), 338 | AncState::Aware => write!(f, "aware"), 339 | AncState::Unknown(x) => write!(f, "unknown ({x})"), 340 | } 341 | } 342 | } 343 | 344 | 345 | #[derive(Debug, Clone, Copy, PartialEq)] 346 | pub struct EqBands { 347 | low_bass: f32, 348 | bass: f32, 349 | mid: f32, 350 | treble: f32, 351 | upper_treble: f32, 352 | } 353 | 354 | impl EqBands { 355 | pub const MIN_VALUE: f32 = -6.0; 356 | pub const MAX_VALUE: f32 = 6.0; 357 | 358 | pub fn new(low_bass: f32, bass: f32, mid: f32, treble: f32, upper_treble: f32) -> Self { 359 | Self { 360 | low_bass: low_bass.clamp(Self::MIN_VALUE, Self::MAX_VALUE), 361 | bass: bass.clamp(Self::MIN_VALUE, Self::MAX_VALUE), 362 | mid: mid.clamp(Self::MIN_VALUE, Self::MAX_VALUE), 363 | treble: treble.clamp(Self::MIN_VALUE, Self::MAX_VALUE), 364 | upper_treble: upper_treble.clamp(Self::MIN_VALUE, Self::MAX_VALUE), 365 | } 366 | } 367 | 368 | pub fn low_bass(&self) -> f32 { 369 | self.low_bass 370 | } 371 | 372 | pub fn bass(&self) -> f32 { 373 | self.bass 374 | } 375 | 376 | pub fn mid(&self) -> f32 { 377 | self.mid 378 | } 379 | 380 | pub fn treble(&self) -> f32 { 381 | self.treble 382 | } 383 | 384 | pub fn upper_treble(&self) -> f32 { 385 | self.upper_treble 386 | } 387 | 388 | pub fn set_low_bass(&mut self, value: f32) { 389 | self.low_bass = value.clamp(Self::MIN_VALUE, Self::MAX_VALUE) 390 | } 391 | 392 | pub fn set_bass(&mut self, value: f32) { 393 | self.bass = value.clamp(Self::MIN_VALUE, Self::MAX_VALUE) 394 | } 395 | 396 | pub fn set_mid(&mut self, value: f32) { 397 | self.mid = value.clamp(Self::MIN_VALUE, Self::MAX_VALUE) 398 | } 399 | 400 | pub fn set_treble(&mut self, value: f32) { 401 | self.treble = value.clamp(Self::MIN_VALUE, Self::MAX_VALUE) 402 | } 403 | 404 | pub fn set_upper_treble(&mut self, value: f32) { 405 | self.upper_treble = value.clamp(Self::MIN_VALUE, Self::MAX_VALUE) 406 | } 407 | } 408 | 409 | impl Default for EqBands { 410 | fn default() -> Self { 411 | Self { 412 | low_bass: 0.0, 413 | bass: 0.0, 414 | mid: 0.0, 415 | treble: 0.0, 416 | upper_treble: 0.0 417 | } 418 | } 419 | } 420 | 421 | impl From for EqBands { 422 | fn from(other: types::EqBands) -> Self { 423 | Self { 424 | low_bass: other.low_bass, 425 | bass: other.bass, 426 | mid: other.mid, 427 | treble: other.treble, 428 | upper_treble: other.upper_treble, 429 | } 430 | } 431 | } 432 | 433 | impl From for types::EqBands { 434 | fn from(other: EqBands) -> Self { 435 | Self { 436 | low_bass: other.low_bass.clamp(EqBands::MIN_VALUE, EqBands::MAX_VALUE), 437 | bass: other.bass.clamp(EqBands::MIN_VALUE, EqBands::MAX_VALUE), 438 | mid: other.mid.clamp(EqBands::MIN_VALUE, EqBands::MAX_VALUE), 439 | treble: other.treble.clamp(EqBands::MIN_VALUE, EqBands::MAX_VALUE), 440 | upper_treble: other.upper_treble.clamp(EqBands::MIN_VALUE, EqBands::MAX_VALUE), 441 | } 442 | } 443 | } 444 | 445 | impl std::fmt::Display for EqBands { 446 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 447 | write!( 448 | f, "[{:.2}, {:.2}, {:.2}, {:.2}, {:.2}]", 449 | self.low_bass, self.bass, self.mid, self.treble, self.upper_treble, 450 | ) 451 | } 452 | } 453 | 454 | 455 | #[derive(Default, Clone, Copy, PartialEq, Eq)] 456 | pub struct VolumeAsymmetry { 457 | value: i32, 458 | } 459 | 460 | impl VolumeAsymmetry { 461 | pub fn from_normalized(value: i32) -> Self { 462 | Self { value: value.clamp(-100, 100) } 463 | } 464 | 465 | pub fn from_raw(value: i32) -> Self { 466 | let direction = value & 0x01; 467 | let value = value >> 1; 468 | 469 | let normalized = if direction != 0 { 470 | value + 1 471 | } else { 472 | - value 473 | }; 474 | 475 | Self { value: normalized } 476 | } 477 | 478 | pub fn raw(&self) -> i32 { 479 | if self.value > 0 { 480 | ((self.value - 1) << 1) | 0x01 481 | } else { 482 | (-self.value) << 1 483 | } 484 | } 485 | 486 | pub fn value(&self) -> i32 { 487 | self.value 488 | } 489 | } 490 | 491 | impl std::fmt::Debug for VolumeAsymmetry { 492 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 493 | write!(f, "{}", self.value) 494 | } 495 | } 496 | 497 | impl std::fmt::Display for VolumeAsymmetry { 498 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 499 | let left = (100 - self.value).min(100); 500 | let right = (100 + self.value).min(100); 501 | 502 | write!(f, "left: {left}%, right: {right}%") 503 | } 504 | } 505 | 506 | 507 | pub trait Setting { 508 | type Type; 509 | 510 | fn id(&self) -> SettingId; 511 | fn from_var(var: SettingValue) -> Option; 512 | } 513 | 514 | impl Setting for SettingId { 515 | type Type = SettingValue; 516 | 517 | fn id(&self) -> SettingId { 518 | *self 519 | } 520 | 521 | fn from_var(var: SettingValue) -> Option { 522 | Some(var) 523 | } 524 | } 525 | 526 | 527 | pub mod id { 528 | use super::*; 529 | 530 | pub struct AutoOtaEnable; 531 | pub struct OhdEnable; 532 | pub struct OobeIsFinished; 533 | pub struct GestureEnable; 534 | pub struct DiagnosticsEnable; 535 | pub struct OobeMode; 536 | pub struct GestureControl; 537 | pub struct MultipointEnable; 538 | pub struct AncrGestureLoop; 539 | pub struct CurrentAncrState; 540 | pub struct OttsMode; 541 | pub struct VolumeEqEnable; 542 | pub struct CurrentUserEq; 543 | pub struct VolumeAsymmetry; 544 | pub struct SumToMono; 545 | pub struct VolumeExposureNotifications; 546 | pub struct SpeechDetection; 547 | 548 | impl Setting for AutoOtaEnable { 549 | type Type = bool; 550 | 551 | fn id(&self) -> SettingId { 552 | SettingId::AutoOtaEnable 553 | } 554 | 555 | fn from_var(var: SettingValue) -> Option { 556 | match var { 557 | SettingValue::AutoOtaEnable(x) => Some(x), 558 | _ => None, 559 | } 560 | } 561 | } 562 | 563 | impl Setting for OhdEnable { 564 | type Type = bool; 565 | 566 | fn id(&self) -> SettingId { 567 | SettingId::OhdEnable 568 | } 569 | 570 | fn from_var(var: SettingValue) -> Option { 571 | match var { 572 | SettingValue::OhdEnable(x) => Some(x), 573 | _ => None, 574 | } 575 | } 576 | } 577 | 578 | impl Setting for OobeIsFinished { 579 | type Type = bool; 580 | 581 | fn id(&self) -> SettingId { 582 | SettingId::OobeIsFinished 583 | } 584 | 585 | fn from_var(var: SettingValue) -> Option { 586 | match var { 587 | SettingValue::OobeIsFinished(x) => Some(x), 588 | _ => None, 589 | } 590 | } 591 | } 592 | 593 | impl Setting for GestureEnable { 594 | type Type = bool; 595 | 596 | fn id(&self) -> SettingId { 597 | SettingId::GestureEnable 598 | } 599 | 600 | fn from_var(var: SettingValue) -> Option { 601 | match var { 602 | SettingValue::GestureEnable(x) => Some(x), 603 | _ => None, 604 | } 605 | } 606 | } 607 | 608 | impl Setting for DiagnosticsEnable { 609 | type Type = bool; 610 | 611 | fn id(&self) -> SettingId { 612 | SettingId::DiagnosticsEnable 613 | } 614 | 615 | fn from_var(var: SettingValue) -> Option { 616 | match var { 617 | SettingValue::DiagnosticsEnable(x) => Some(x), 618 | _ => None, 619 | } 620 | } 621 | } 622 | 623 | impl Setting for OobeMode { 624 | type Type = bool; 625 | 626 | fn id(&self) -> SettingId { 627 | SettingId::OobeMode 628 | } 629 | 630 | fn from_var(var: SettingValue) -> Option { 631 | match var { 632 | SettingValue::OobeMode(x) => Some(x), 633 | _ => None, 634 | } 635 | } 636 | } 637 | 638 | impl Setting for GestureControl { 639 | type Type = super::GestureControl; 640 | 641 | fn id(&self) -> SettingId { 642 | SettingId::GestureControl 643 | } 644 | 645 | fn from_var(var: SettingValue) -> Option { 646 | match var { 647 | SettingValue::GestureControl(x) => Some(x), 648 | _ => None, 649 | } 650 | } 651 | } 652 | 653 | impl Setting for MultipointEnable { 654 | type Type = bool; 655 | 656 | fn id(&self) -> SettingId { 657 | SettingId::MultipointEnable 658 | } 659 | 660 | fn from_var(var: SettingValue) -> Option { 661 | match var { 662 | SettingValue::MultipointEnable(x) => Some(x), 663 | _ => None, 664 | } 665 | } 666 | } 667 | 668 | impl Setting for AncrGestureLoop { 669 | type Type = super::AncrGestureLoop; 670 | 671 | fn id(&self) -> SettingId { 672 | SettingId::AncrGestureLoop 673 | } 674 | 675 | fn from_var(var: SettingValue) -> Option { 676 | match var { 677 | SettingValue::AncrGestureLoop(x) => Some(x), 678 | _ => None, 679 | } 680 | } 681 | } 682 | 683 | impl Setting for CurrentAncrState { 684 | type Type = AncState; 685 | 686 | fn id(&self) -> SettingId { 687 | SettingId::CurrentAncrState 688 | } 689 | 690 | fn from_var(var: SettingValue) -> Option { 691 | match var { 692 | SettingValue::CurrentAncrState(x) => Some(x), 693 | _ => None, 694 | } 695 | } 696 | } 697 | 698 | impl Setting for OttsMode { 699 | type Type = i32; 700 | 701 | fn id(&self) -> SettingId { 702 | SettingId::OttsMode 703 | } 704 | 705 | fn from_var(var: SettingValue) -> Option { 706 | match var { 707 | SettingValue::OttsMode(x) => Some(x), 708 | _ => None, 709 | } 710 | } 711 | } 712 | 713 | impl Setting for VolumeEqEnable { 714 | type Type = bool; 715 | 716 | fn id(&self) -> SettingId { 717 | SettingId::VolumeEqEnable 718 | } 719 | 720 | fn from_var(var: SettingValue) -> Option { 721 | match var { 722 | SettingValue::VolumeEqEnable(x) => Some(x), 723 | _ => None, 724 | } 725 | } 726 | } 727 | 728 | impl Setting for CurrentUserEq { 729 | type Type = EqBands; 730 | 731 | fn id(&self) -> SettingId { 732 | SettingId::CurrentUserEq 733 | } 734 | 735 | fn from_var(var: SettingValue) -> Option { 736 | match var { 737 | SettingValue::CurrentUserEq(x) => Some(x), 738 | _ => None, 739 | } 740 | } 741 | } 742 | 743 | impl Setting for VolumeAsymmetry { 744 | type Type = super::VolumeAsymmetry; 745 | 746 | fn id(&self) -> SettingId { 747 | SettingId::VolumeAsymmetry 748 | } 749 | 750 | fn from_var(var: SettingValue) -> Option { 751 | match var { 752 | SettingValue::VolumeAsymmetry(x) => Some(x), 753 | _ => None, 754 | } 755 | } 756 | } 757 | 758 | impl Setting for SumToMono { 759 | type Type = bool; 760 | 761 | fn id(&self) -> SettingId { 762 | SettingId::SumToMono 763 | } 764 | 765 | fn from_var(var: SettingValue) -> Option { 766 | match var { 767 | SettingValue::SumToMono(x) => Some(x), 768 | _ => None, 769 | } 770 | } 771 | } 772 | 773 | impl Setting for VolumeExposureNotifications { 774 | type Type = bool; 775 | 776 | fn id(&self) -> SettingId { 777 | SettingId::VolumeExposureNotifications 778 | } 779 | 780 | fn from_var(var: SettingValue) -> Option { 781 | match var { 782 | SettingValue::VolumeExposureNotifications(x) => Some(x), 783 | _ => None, 784 | } 785 | } 786 | } 787 | 788 | impl Setting for SpeechDetection { 789 | type Type = bool; 790 | 791 | fn id(&self) -> SettingId { 792 | SettingId::SpeechDetection 793 | } 794 | 795 | fn from_var(var: SettingValue) -> Option { 796 | match var { 797 | SettingValue::SpeechDetection(x) => Some(x), 798 | _ => None, 799 | } 800 | } 801 | } 802 | } 803 | 804 | 805 | #[cfg(test)] 806 | mod test { 807 | use super::*; 808 | 809 | #[test] 810 | fn test_volume_assymetry_conversion() { 811 | for i in 0..=200 { 812 | assert_eq!(VolumeAsymmetry::from_raw(i).raw(), i) 813 | } 814 | } 815 | } 816 | --------------------------------------------------------------------------------