forked from auracaster/bumble_mirror
Merge pull request #261 from marshallpierce/mp/rust-realtek-tools
Rust tools for working with Realtek firmware
This commit is contained in:
15
.github/workflows/python-build-test.yml
vendored
15
.github/workflows/python-build-test.yml
vendored
@@ -45,7 +45,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ "3.8", "3.9", "3.10" ]
|
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
|
||||||
|
rust-version: [ "1.70.0", "stable" ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Check out from Git
|
- name: Check out from Git
|
||||||
@@ -62,9 +63,15 @@ jobs:
|
|||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: clippy,rustfmt
|
components: clippy,rustfmt
|
||||||
- name: Rust Lints
|
toolchain: ${{ matrix.rust-version }}
|
||||||
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings
|
|
||||||
- name: Rust Build
|
- name: Rust Build
|
||||||
run: cd rust && cargo build --all-targets
|
run: cd rust && cargo build --all-targets && cargo build --all-features --all-targets
|
||||||
|
# Lints after build so what clippy needs is already built
|
||||||
|
- name: Rust Lints
|
||||||
|
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings
|
||||||
- name: Rust Tests
|
- name: Rust Tests
|
||||||
run: cd rust && cargo test
|
run: cd rust && cargo test
|
||||||
|
# At some point, hook up publishing the binary. For now, just make sure it builds.
|
||||||
|
# Once we're ready to publish binaries, this should be built with `--release`.
|
||||||
|
- name: Build Bumble CLI
|
||||||
|
run: cd rust && cargo build --features bumble-tools --bin bumble
|
||||||
@@ -21,6 +21,9 @@ like loading firmware after a cold start.
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import abc
|
import abc
|
||||||
import logging
|
import logging
|
||||||
|
import pathlib
|
||||||
|
import platform
|
||||||
|
import platformdirs
|
||||||
from . import rtk
|
from . import rtk
|
||||||
|
|
||||||
|
|
||||||
@@ -66,3 +69,22 @@ async def get_driver_for_host(host):
|
|||||||
return driver
|
return driver
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def project_data_dir() -> pathlib.Path:
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
A path to an OS-specific directory for bumble data. The directory is created if
|
||||||
|
it doesn't exist.
|
||||||
|
"""
|
||||||
|
if platform.system() == 'Darwin':
|
||||||
|
# platformdirs doesn't handle macOS right: it doesn't assemble a bundle id
|
||||||
|
# out of author & project
|
||||||
|
return platformdirs.user_data_path(
|
||||||
|
appname='com.google.bumble', ensure_exists=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# windows and linux don't use the com qualifier
|
||||||
|
return platformdirs.user_data_path(
|
||||||
|
appname='bumble', appauthor='google', ensure_exists=True
|
||||||
|
)
|
||||||
|
|||||||
@@ -446,6 +446,11 @@ class Driver:
|
|||||||
# When the environment variable is set, don't look elsewhere
|
# When the environment variable is set, don't look elsewhere
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Then, look where the firmware download tool writes by default
|
||||||
|
if (path := rtk_firmware_dir() / file_name).is_file():
|
||||||
|
logger.debug(f"{file_name} found in project data dir")
|
||||||
|
return path
|
||||||
|
|
||||||
# Then, look in the package's driver directory
|
# Then, look in the package's driver directory
|
||||||
if (path := pathlib.Path(__file__).parent / "rtk_fw" / file_name).is_file():
|
if (path := pathlib.Path(__file__).parent / "rtk_fw" / file_name).is_file():
|
||||||
logger.debug(f"{file_name} found in package dir")
|
logger.debug(f"{file_name} found in package dir")
|
||||||
@@ -646,3 +651,16 @@ class Driver:
|
|||||||
await self.download_firmware()
|
await self.download_firmware()
|
||||||
await self.host.send_command(HCI_Reset_Command(), check_result=True)
|
await self.host.send_command(HCI_Reset_Command(), check_result=True)
|
||||||
logger.info(f"loaded FW image {self.driver_info.fw_name}")
|
logger.info(f"loaded FW image {self.driver_info.fw_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def rtk_firmware_dir() -> pathlib.Path:
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
A path to a subdir of the project data dir for Realtek firmware.
|
||||||
|
The directory is created if it doesn't exist.
|
||||||
|
"""
|
||||||
|
from bumble.drivers import project_data_dir
|
||||||
|
|
||||||
|
p = project_data_dir() / "firmware" / "realtek"
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
return p
|
||||||
|
|||||||
832
rust/Cargo.lock
generated
832
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ documentation = "https://docs.rs/crate/bumble"
|
|||||||
authors = ["Marshall Pierce <marshallpierce@google.com>"]
|
authors = ["Marshall Pierce <marshallpierce@google.com>"]
|
||||||
keywords = ["bluetooth", "ble"]
|
keywords = ["bluetooth", "ble"]
|
||||||
categories = ["api-bindings", "network-programming"]
|
categories = ["api-bindings", "network-programming"]
|
||||||
rust-version = "1.69.0"
|
rust-version = "1.70.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pyo3 = { version = "0.18.3", features = ["macros"] }
|
pyo3 = { version = "0.18.3", features = ["macros"] }
|
||||||
@@ -23,7 +23,16 @@ hex = "0.4.3"
|
|||||||
itertools = "0.11.0"
|
itertools = "0.11.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
thiserror = "1.0.41"
|
thiserror = "1.0.41"
|
||||||
|
|
||||||
|
# CLI
|
||||||
anyhow = { version = "1.0.71", optional = true }
|
anyhow = { version = "1.0.71", optional = true }
|
||||||
|
clap = { version = "4.3.3", features = ["derive"], optional = true }
|
||||||
|
directories = { version = "5.0.1", optional = true }
|
||||||
|
owo-colors = { version = "3.5.0", optional = true }
|
||||||
|
reqwest = { version = "0.11.20", features = ["blocking"], optional = true }
|
||||||
|
rusb = { version = "0.9.2", optional = true }
|
||||||
|
log = { version = "0.4.19", optional = true }
|
||||||
|
env_logger = { version = "0.10.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.28.2", features = ["full"] }
|
tokio = { version = "1.28.2", features = ["full"] }
|
||||||
@@ -32,17 +41,25 @@ nix = "0.26.2"
|
|||||||
anyhow = "1.0.71"
|
anyhow = "1.0.71"
|
||||||
pyo3 = { version = "0.18.3", features = ["macros", "anyhow"] }
|
pyo3 = { version = "0.18.3", features = ["macros", "anyhow"] }
|
||||||
pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime", "attributes", "testing"] }
|
pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime", "attributes", "testing"] }
|
||||||
|
rusb = "0.9.2"
|
||||||
|
rand = "0.8.5"
|
||||||
clap = { version = "4.3.3", features = ["derive"] }
|
clap = { version = "4.3.3", features = ["derive"] }
|
||||||
owo-colors = "3.5.0"
|
owo-colors = "3.5.0"
|
||||||
log = "0.4.19"
|
log = "0.4.19"
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
rusb = "0.9.2"
|
|
||||||
rand = "0.8.5"
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "gen-assigned-numbers"
|
name = "gen-assigned-numbers"
|
||||||
path = "tools/gen_assigned_numbers.rs"
|
path = "tools/gen_assigned_numbers.rs"
|
||||||
required-features = ["bumble-dev-tools"]
|
required-features = ["bumble-codegen"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "bumble"
|
||||||
|
path = "src/main.rs"
|
||||||
|
required-features = ["bumble-tools"]
|
||||||
|
|
||||||
# test entry point that uses pyo3_asyncio's test harness
|
# test entry point that uses pyo3_asyncio's test harness
|
||||||
[[test]]
|
[[test]]
|
||||||
@@ -53,4 +70,7 @@ harness = false
|
|||||||
[features]
|
[features]
|
||||||
anyhow = ["pyo3/anyhow"]
|
anyhow = ["pyo3/anyhow"]
|
||||||
pyo3-asyncio-attributes = ["pyo3-asyncio/attributes"]
|
pyo3-asyncio-attributes = ["pyo3-asyncio/attributes"]
|
||||||
bumble-dev-tools = ["dep:anyhow"]
|
bumble-codegen = ["dep:anyhow"]
|
||||||
|
# separate feature for CLI so that dependencies don't spend time building these
|
||||||
|
bumble-tools = ["dep:clap", "anyhow", "dep:anyhow", "dep:directories", "pyo3-asyncio-attributes", "dep:owo-colors", "dep:reqwest", "dep:rusb", "dep:log", "dep:env_logger"]
|
||||||
|
default = []
|
||||||
@@ -5,7 +5,8 @@ Rust wrappers around the [Bumble](https://github.com/google/bumble) Python API.
|
|||||||
Method calls are mapped to the equivalent Python, and return types adapted where
|
Method calls are mapped to the equivalent Python, and return types adapted where
|
||||||
relevant.
|
relevant.
|
||||||
|
|
||||||
See the `examples` directory for usage.
|
See the CLI in `src/main.rs` or the `examples` directory for how to use the
|
||||||
|
Bumble API.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
@@ -27,6 +28,15 @@ PYTHONPATH=..:~/.virtualenvs/bumble/lib/python3.10/site-packages/ \
|
|||||||
Run the corresponding `battery_server` Python example, and launch an emulator in
|
Run the corresponding `battery_server` Python example, and launch an emulator in
|
||||||
Android Studio (currently, Canary is required) to run netsim.
|
Android Studio (currently, Canary is required) to run netsim.
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
|
||||||
|
Explore the available subcommands:
|
||||||
|
|
||||||
|
```
|
||||||
|
PYTHONPATH=..:[virtualenv site-packages] \
|
||||||
|
cargo run --features bumble-tools --bin bumble -- --help
|
||||||
|
```
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
Run the tests:
|
Run the tests:
|
||||||
@@ -52,5 +62,5 @@ in tests at `pytests/assigned_numbers.rs`.
|
|||||||
To regenerate the assigned number tables based on the Python codebase:
|
To regenerate the assigned number tables based on the Python codebase:
|
||||||
|
|
||||||
```
|
```
|
||||||
PYTHONPATH=.. cargo run --bin gen-assigned-numbers --features bumble-dev-tools
|
PYTHONPATH=.. cargo run --bin gen-assigned-numbers --features bumble-codegen
|
||||||
```
|
```
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use bumble::wrapper::transport::Transport;
|
use bumble::wrapper::{drivers::rtk::DriverInfo, transport::Transport};
|
||||||
use nix::sys::stat::Mode;
|
use nix::sys::stat::Mode;
|
||||||
use pyo3::PyResult;
|
use pyo3::PyResult;
|
||||||
|
|
||||||
@@ -29,3 +29,9 @@ async fn fifo_transport_can_open() -> PyResult<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[pyo3_asyncio::tokio::test]
|
||||||
|
async fn realtek_driver_info_all_drivers() -> PyResult<()> {
|
||||||
|
assert_eq!(12, DriverInfo::all_drivers()?.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
4
rust/resources/test/firmware/realtek/README.md
Normal file
4
rust/resources/test/firmware/realtek/README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
This dir contains samples firmware images in the format used for Realtek chips,
|
||||||
|
but with repetitions of the length of the section as a little-endian 32-bit int
|
||||||
|
for the patch data instead of actual firmware, since we only need the structure
|
||||||
|
to test parsing.
|
||||||
BIN
rust/resources/test/firmware/realtek/rtl8723b_fw_structure.bin
Normal file
BIN
rust/resources/test/firmware/realtek/rtl8723b_fw_structure.bin
Normal file
Binary file not shown.
BIN
rust/resources/test/firmware/realtek/rtl8761bu_fw_structure.bin
Normal file
BIN
rust/resources/test/firmware/realtek/rtl8761bu_fw_structure.bin
Normal file
Binary file not shown.
15
rust/src/cli/firmware/mod.rs
Normal file
15
rust/src/cli/firmware/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
pub(crate) mod rtk;
|
||||||
265
rust/src/cli/firmware/rtk.rs
Normal file
265
rust/src/cli/firmware/rtk.rs
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Realtek firmware tools
|
||||||
|
|
||||||
|
use crate::{Download, Source};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use bumble::wrapper::{
|
||||||
|
drivers::rtk::{Driver, DriverInfo, Firmware},
|
||||||
|
host::{DriverFactory, Host},
|
||||||
|
transport::Transport,
|
||||||
|
};
|
||||||
|
use owo_colors::{colors::css, OwoColorize};
|
||||||
|
use pyo3::PyResult;
|
||||||
|
use std::{fs, path};
|
||||||
|
|
||||||
|
pub(crate) async fn download(dl: Download) -> PyResult<()> {
|
||||||
|
let data_dir = dl
|
||||||
|
.output_dir
|
||||||
|
.or_else(|| {
|
||||||
|
directories::ProjectDirs::from("com", "google", "bumble")
|
||||||
|
.map(|pd| pd.data_local_dir().join("firmware").join("realtek"))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
eprintln!("Could not determine standard data directory");
|
||||||
|
path::PathBuf::from(".")
|
||||||
|
});
|
||||||
|
fs::create_dir_all(&data_dir)?;
|
||||||
|
|
||||||
|
let (base_url, uses_bin_suffix) = match dl.source {
|
||||||
|
Source::LinuxKernel => ("https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/rtl_bt", true),
|
||||||
|
Source::RealtekOpensource => ("https://github.com/Realtek-OpenSource/android_hardware_realtek/raw/rtk1395/bt/rtkbt/Firmware/BT", false),
|
||||||
|
Source::LinuxFromScratch => ("https://anduin.linuxfromscratch.org/sources/linux-firmware/rtl_bt", true),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Downloading");
|
||||||
|
println!("{} {}", "FROM:".green(), base_url);
|
||||||
|
println!("{} {}", "TO:".green(), data_dir.to_string_lossy());
|
||||||
|
|
||||||
|
let url_for_file = |file_name: &str| {
|
||||||
|
let url_suffix = if uses_bin_suffix {
|
||||||
|
file_name
|
||||||
|
} else {
|
||||||
|
file_name.trim_end_matches(".bin")
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut url = base_url.to_string();
|
||||||
|
url.push('/');
|
||||||
|
url.push_str(url_suffix);
|
||||||
|
url
|
||||||
|
};
|
||||||
|
|
||||||
|
let to_download = if let Some(single) = dl.single {
|
||||||
|
vec![(
|
||||||
|
format!("{single}_fw.bin"),
|
||||||
|
Some(format!("{single}_config.bin")),
|
||||||
|
false,
|
||||||
|
)]
|
||||||
|
} else {
|
||||||
|
DriverInfo::all_drivers()?
|
||||||
|
.iter()
|
||||||
|
.map(|di| Ok((di.firmware_name()?, di.config_name()?, di.config_needed()?)))
|
||||||
|
.collect::<PyResult<Vec<_>>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = SimpleClient::new();
|
||||||
|
|
||||||
|
for (fw_filename, config_filename, config_needed) in to_download {
|
||||||
|
println!("{}", "---".yellow());
|
||||||
|
let fw_path = data_dir.join(&fw_filename);
|
||||||
|
let config_path = config_filename.as_ref().map(|f| data_dir.join(f));
|
||||||
|
|
||||||
|
if fw_path.exists() && !dl.overwrite {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!("{} already exists, skipping", fw_path.to_string_lossy())
|
||||||
|
.fg::<css::Orange>()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(cp) = config_path.as_ref() {
|
||||||
|
if cp.exists() && !dl.overwrite {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!("{} already exists, skipping", cp.to_string_lossy())
|
||||||
|
.fg::<css::Orange>()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fw_contents = match client.get(&url_for_file(&fw_filename)).await {
|
||||||
|
Ok(data) => {
|
||||||
|
println!("Downloaded {}: {} bytes", fw_filename, data.len());
|
||||||
|
data
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"{} {} {:?}",
|
||||||
|
"Failed to download".red(),
|
||||||
|
fw_filename.red(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let config_contents = if let Some(cn) = &config_filename {
|
||||||
|
match client.get(&url_for_file(cn)).await {
|
||||||
|
Ok(data) => {
|
||||||
|
println!("Downloaded {}: {} bytes", cn, data.len());
|
||||||
|
Some(data)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if config_needed {
|
||||||
|
eprintln!("{} {} {:?}", "Failed to download".red(), cn.red(), e);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"{}",
|
||||||
|
format!("No config available as {cn}").fg::<css::Orange>()
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::write(&fw_path, &fw_contents)?;
|
||||||
|
if !dl.no_parse && config_filename.is_some() {
|
||||||
|
println!("{} {}", "Parsing:".cyan(), &fw_filename);
|
||||||
|
match Firmware::parse(&fw_contents).map_err(|e| anyhow!("Parse error: {:?}", e)) {
|
||||||
|
Ok(fw) => dump_firmware_desc(&fw),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"{} {:?}",
|
||||||
|
"Could not parse firmware:".fg::<css::Orange>(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((cp, cd)) = config_path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| config_contents.map(|c| (p, c)))
|
||||||
|
{
|
||||||
|
fs::write(cp, &cd)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse(firmware_path: &path::Path) -> PyResult<()> {
|
||||||
|
let contents = fs::read(firmware_path)?;
|
||||||
|
let fw = Firmware::parse(&contents)
|
||||||
|
// squish the error into a string to avoid the error type requiring that the input be
|
||||||
|
// 'static
|
||||||
|
.map_err(|e| anyhow!("Parse error: {:?}", e))?;
|
||||||
|
|
||||||
|
dump_firmware_desc(&fw);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn info(transport: &str, force: bool) -> PyResult<()> {
|
||||||
|
let transport = Transport::open(transport).await?;
|
||||||
|
|
||||||
|
let mut host = Host::new(transport.source()?, transport.sink()?)?;
|
||||||
|
host.reset(DriverFactory::None).await?;
|
||||||
|
|
||||||
|
if !force && !Driver::check(&host).await? {
|
||||||
|
println!("USB device not supported by this RTK driver");
|
||||||
|
} else if let Some(driver_info) = Driver::driver_info_for_host(&host).await? {
|
||||||
|
println!("Driver:");
|
||||||
|
println!(" {:10} {:04X}", "ROM:", driver_info.rom()?);
|
||||||
|
println!(" {:10} {}", "Firmware:", driver_info.firmware_name()?);
|
||||||
|
println!(
|
||||||
|
" {:10} {}",
|
||||||
|
"Config:",
|
||||||
|
driver_info.config_name()?.unwrap_or_default()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("Firmware already loaded or no supported driver for this device.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn load(transport: &str, force: bool) -> PyResult<()> {
|
||||||
|
let transport = Transport::open(transport).await?;
|
||||||
|
|
||||||
|
let mut host = Host::new(transport.source()?, transport.sink()?)?;
|
||||||
|
host.reset(DriverFactory::None).await?;
|
||||||
|
|
||||||
|
match Driver::for_host(&host, force).await? {
|
||||||
|
None => {
|
||||||
|
eprintln!("Firmware already loaded or no supported driver for this device.");
|
||||||
|
}
|
||||||
|
Some(mut d) => d.download_firmware().await?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn drop(transport: &str) -> PyResult<()> {
|
||||||
|
let transport = Transport::open(transport).await?;
|
||||||
|
|
||||||
|
let mut host = Host::new(transport.source()?, transport.sink()?)?;
|
||||||
|
host.reset(DriverFactory::None).await?;
|
||||||
|
|
||||||
|
Driver::drop_firmware(&mut host).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump_firmware_desc(fw: &Firmware) {
|
||||||
|
println!(
|
||||||
|
"Firmware: version=0x{:08X} project_id=0x{:04X}",
|
||||||
|
fw.version(),
|
||||||
|
fw.project_id()
|
||||||
|
);
|
||||||
|
for p in fw.patches() {
|
||||||
|
println!(
|
||||||
|
" Patch: chip_id=0x{:04X}, {} bytes, SVN Version={:08X}",
|
||||||
|
p.chip_id(),
|
||||||
|
p.contents().len(),
|
||||||
|
p.svn_version()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SimpleClient {
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleClient {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client: reqwest::Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, url: &str) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let resp = self.client.get(url).send().await?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(anyhow!("Bad status: {}", resp.status()));
|
||||||
|
}
|
||||||
|
let bytes = resp.bytes().await?;
|
||||||
|
Ok(bytes.as_ref().to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
17
rust/src/cli/mod.rs
Normal file
17
rust/src/cli/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
pub(crate) mod firmware;
|
||||||
|
|
||||||
|
pub(crate) mod usb;
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
//! whether it is a Bluetooth device that uses a non-standard Class, or some other
|
//! whether it is a Bluetooth device that uses a non-standard Class, or some other
|
||||||
//! type of device (there's no way to tell).
|
//! type of device (there's no way to tell).
|
||||||
|
|
||||||
use clap::Parser as _;
|
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
use owo_colors::{OwoColorize, Style};
|
use owo_colors::{OwoColorize, Style};
|
||||||
use rusb::{Device, DeviceDescriptor, Direction, TransferType, UsbContext};
|
use rusb::{Device, DeviceDescriptor, Direction, TransferType, UsbContext};
|
||||||
@@ -31,15 +30,12 @@ use std::{
|
|||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
const USB_DEVICE_CLASS_DEVICE: u8 = 0x00;
|
const USB_DEVICE_CLASS_DEVICE: u8 = 0x00;
|
||||||
const USB_DEVICE_CLASS_WIRELESS_CONTROLLER: u8 = 0xE0;
|
const USB_DEVICE_CLASS_WIRELESS_CONTROLLER: u8 = 0xE0;
|
||||||
const USB_DEVICE_SUBCLASS_RF_CONTROLLER: u8 = 0x01;
|
const USB_DEVICE_SUBCLASS_RF_CONTROLLER: u8 = 0x01;
|
||||||
const USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER: u8 = 0x01;
|
const USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER: u8 = 0x01;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
pub(crate) fn probe(verbose: bool) -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
|
||||||
|
|
||||||
let mut bt_dev_count = 0;
|
let mut bt_dev_count = 0;
|
||||||
let mut device_serials_by_id: HashMap<(u16, u16), HashSet<String>> = HashMap::new();
|
let mut device_serials_by_id: HashMap<(u16, u16), HashSet<String>> = HashMap::new();
|
||||||
for device in rusb::devices()?.iter() {
|
for device in rusb::devices()?.iter() {
|
||||||
@@ -159,7 +155,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
println!("{:26}{}", " Product:".green(), p);
|
println!("{:26}{}", " Product:".green(), p);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.verbose {
|
if verbose {
|
||||||
print_device_details(&device, &device_desc)?;
|
print_device_details(&device, &device_desc)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,11 +328,3 @@ impl From<&DeviceDescriptor> for ClassInfo {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
|
||||||
#[command(author, version, about, long_about = None)]
|
|
||||||
struct Cli {
|
|
||||||
/// Show additional info for each USB device
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
verbose: bool,
|
|
||||||
}
|
|
||||||
17
rust/src/internal/drivers/mod.rs
Normal file
17
rust/src/internal/drivers/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Device drivers
|
||||||
|
|
||||||
|
pub(crate) mod rtk;
|
||||||
253
rust/src/internal/drivers/rtk.rs
Normal file
253
rust/src/internal/drivers/rtk.rs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Drivers for Realtek controllers
|
||||||
|
|
||||||
|
use nom::{bytes, combinator, error, multi, number, sequence};
|
||||||
|
|
||||||
|
/// Realtek firmware file contents
|
||||||
|
pub struct Firmware {
|
||||||
|
version: u32,
|
||||||
|
project_id: u8,
|
||||||
|
patches: Vec<Patch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Firmware {
|
||||||
|
/// Parse a `*_fw.bin` file
|
||||||
|
pub fn parse(input: &[u8]) -> Result<Self, nom::Err<error::Error<&[u8]>>> {
|
||||||
|
let extension_sig = [0x51, 0x04, 0xFD, 0x77];
|
||||||
|
|
||||||
|
let (_rem, (_tag, fw_version, patch_count, payload)) =
|
||||||
|
combinator::all_consuming(combinator::map_parser(
|
||||||
|
// ignore the sig suffix
|
||||||
|
sequence::terminated(
|
||||||
|
bytes::complete::take(
|
||||||
|
// underflow will show up as parse failure
|
||||||
|
input.len().saturating_sub(extension_sig.len()),
|
||||||
|
),
|
||||||
|
bytes::complete::tag(extension_sig.as_slice()),
|
||||||
|
),
|
||||||
|
sequence::tuple((
|
||||||
|
bytes::complete::tag(b"Realtech"),
|
||||||
|
// version
|
||||||
|
number::complete::le_u32,
|
||||||
|
// patch count
|
||||||
|
combinator::map(number::complete::le_u16, |c| c as usize),
|
||||||
|
// everything else except suffix
|
||||||
|
combinator::rest,
|
||||||
|
)),
|
||||||
|
))(input)?;
|
||||||
|
|
||||||
|
// ignore remaining input, since patch offsets are relative to the complete input
|
||||||
|
let (_rem, (chip_ids, patch_lengths, patch_offsets)) = sequence::tuple((
|
||||||
|
// chip id
|
||||||
|
multi::many_m_n(patch_count, patch_count, number::complete::le_u16),
|
||||||
|
// patch length
|
||||||
|
multi::many_m_n(patch_count, patch_count, number::complete::le_u16),
|
||||||
|
// patch offset
|
||||||
|
multi::many_m_n(patch_count, patch_count, number::complete::le_u32),
|
||||||
|
))(payload)?;
|
||||||
|
|
||||||
|
let patches = chip_ids
|
||||||
|
.into_iter()
|
||||||
|
.zip(patch_lengths.into_iter())
|
||||||
|
.zip(patch_offsets.into_iter())
|
||||||
|
.map(|((chip_id, patch_length), patch_offset)| {
|
||||||
|
combinator::map(
|
||||||
|
sequence::preceded(
|
||||||
|
bytes::complete::take(patch_offset),
|
||||||
|
// ignore trailing 4-byte suffix
|
||||||
|
sequence::terminated(
|
||||||
|
// patch including svn version, but not suffix
|
||||||
|
combinator::consumed(sequence::preceded(
|
||||||
|
// patch before svn version or version suffix
|
||||||
|
// prefix length underflow will show up as parse failure
|
||||||
|
bytes::complete::take(patch_length.saturating_sub(8)),
|
||||||
|
// svn version
|
||||||
|
number::complete::le_u32,
|
||||||
|
)),
|
||||||
|
// dummy suffix, overwritten with firmware version
|
||||||
|
bytes::complete::take(4_usize),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|(patch_contents_before_version, svn_version): (&[u8], u32)| {
|
||||||
|
let mut contents = patch_contents_before_version.to_vec();
|
||||||
|
// replace what would have been the trailing dummy suffix with fw version
|
||||||
|
contents.extend_from_slice(&fw_version.to_le_bytes());
|
||||||
|
|
||||||
|
Patch {
|
||||||
|
contents,
|
||||||
|
svn_version,
|
||||||
|
chip_id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)(input)
|
||||||
|
.map(|(_rem, output)| output)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
// look for project id from the end
|
||||||
|
let mut offset = payload.len();
|
||||||
|
let mut project_id: Option<u8> = None;
|
||||||
|
while offset >= 2 {
|
||||||
|
// Won't panic, since offset >= 2
|
||||||
|
let chunk = &payload[offset - 2..offset];
|
||||||
|
let length: usize = chunk[0].into();
|
||||||
|
let opcode = chunk[1];
|
||||||
|
offset -= 2;
|
||||||
|
|
||||||
|
if opcode == 0xFF {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if length == 0 {
|
||||||
|
// report what nom likely would have done, if nom was good at parsing backwards
|
||||||
|
return Err(nom::Err::Error(error::Error::new(
|
||||||
|
chunk,
|
||||||
|
error::ErrorKind::Verify,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if opcode == 0 && length == 1 {
|
||||||
|
project_id = offset
|
||||||
|
.checked_sub(1)
|
||||||
|
.and_then(|index| payload.get(index))
|
||||||
|
.copied();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset -= length;
|
||||||
|
}
|
||||||
|
|
||||||
|
match project_id {
|
||||||
|
Some(project_id) => Ok(Firmware {
|
||||||
|
project_id,
|
||||||
|
version: fw_version,
|
||||||
|
patches,
|
||||||
|
}),
|
||||||
|
None => {
|
||||||
|
// we ran out of file without finding a project id
|
||||||
|
Err(nom::Err::Error(error::Error::new(
|
||||||
|
payload,
|
||||||
|
error::ErrorKind::Eof,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Patch version
|
||||||
|
pub fn version(&self) -> u32 {
|
||||||
|
self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Project id
|
||||||
|
pub fn project_id(&self) -> u8 {
|
||||||
|
self.project_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Patches
|
||||||
|
pub fn patches(&self) -> &[Patch] {
|
||||||
|
&self.patches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Patch in a [Firmware}
|
||||||
|
pub struct Patch {
|
||||||
|
chip_id: u16,
|
||||||
|
contents: Vec<u8>,
|
||||||
|
svn_version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Patch {
|
||||||
|
/// Chip id
|
||||||
|
pub fn chip_id(&self) -> u16 {
|
||||||
|
self.chip_id
|
||||||
|
}
|
||||||
|
/// Contents of the patch, including the 4-byte firmware version suffix
|
||||||
|
pub fn contents(&self) -> &[u8] {
|
||||||
|
&self.contents
|
||||||
|
}
|
||||||
|
/// SVN version
|
||||||
|
pub fn svn_version(&self) -> u32 {
|
||||||
|
self.svn_version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use std::{fs, io, path};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_firmware_rtl8723b() -> anyhow::Result<()> {
|
||||||
|
let fw = Firmware::parse(&firmware_contents("rtl8723b_fw_structure.bin")?)
|
||||||
|
.map_err(|e| anyhow!("{:?}", e))?;
|
||||||
|
|
||||||
|
let fw_version = 0x0E2F9F73;
|
||||||
|
assert_eq!(fw_version, fw.version());
|
||||||
|
assert_eq!(0x0001, fw.project_id());
|
||||||
|
assert_eq!(
|
||||||
|
vec![(0x0001, 0x00002BBF, 22368,), (0x0002, 0x00002BBF, 22496,),],
|
||||||
|
patch_summaries(fw, fw_version)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_firmware_rtl8761bu() -> anyhow::Result<()> {
|
||||||
|
let fw = Firmware::parse(&firmware_contents("rtl8761bu_fw_structure.bin")?)
|
||||||
|
.map_err(|e| anyhow!("{:?}", e))?;
|
||||||
|
|
||||||
|
let fw_version = 0xDFC6D922;
|
||||||
|
assert_eq!(fw_version, fw.version());
|
||||||
|
assert_eq!(0x000E, fw.project_id());
|
||||||
|
assert_eq!(
|
||||||
|
vec![(0x0001, 0x00005060, 14048,), (0x0002, 0xD6D525A4, 30204,),],
|
||||||
|
patch_summaries(fw, fw_version)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn firmware_contents(filename: &str) -> io::Result<Vec<u8>> {
|
||||||
|
fs::read(
|
||||||
|
path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("resources/test/firmware/realtek")
|
||||||
|
.join(filename),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a tuple of (chip id, svn version, contents len, contents sha256)
|
||||||
|
fn patch_summaries(fw: Firmware, fw_version: u32) -> Vec<(u16, u32, usize)> {
|
||||||
|
fw.patches()
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
let contents = p.contents();
|
||||||
|
let mut dummy_contents = dummy_contents(contents.len());
|
||||||
|
dummy_contents.extend_from_slice(&p.svn_version().to_le_bytes());
|
||||||
|
dummy_contents.extend_from_slice(&fw_version.to_le_bytes());
|
||||||
|
assert_eq!(&dummy_contents, contents);
|
||||||
|
(p.chip_id(), p.svn_version(), contents.len())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dummy_contents(len: usize) -> Vec<u8> {
|
||||||
|
let mut vec = (len as u32).to_le_bytes().as_slice().repeat(len / 4 + 1);
|
||||||
|
assert!(vec.len() >= len);
|
||||||
|
// leave room for svn version and firmware version
|
||||||
|
vec.truncate(len - 8);
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
}
|
||||||
20
rust/src/internal/mod.rs
Normal file
20
rust/src/internal/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! It's not clear where to put Rust code that isn't simply a wrapper around Python. Until we have
|
||||||
|
//! a good answer for what to do there, the idea is to put it in this (non-public) module, and
|
||||||
|
//! `pub use` it into the relevant areas of the `wrapper` module so that it's still easy for users
|
||||||
|
//! to discover.
|
||||||
|
|
||||||
|
pub(crate) mod drivers;
|
||||||
@@ -29,3 +29,5 @@
|
|||||||
pub mod wrapper;
|
pub mod wrapper;
|
||||||
|
|
||||||
pub mod adv;
|
pub mod adv;
|
||||||
|
|
||||||
|
pub(crate) mod internal;
|
||||||
|
|||||||
179
rust/src/main.rs
Normal file
179
rust/src/main.rs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! CLI tools for Bumble
|
||||||
|
|
||||||
|
#![deny(missing_docs, unsafe_code)]
|
||||||
|
|
||||||
|
use bumble::wrapper::logging::{bumble_env_logging_level, py_logging_basic_config};
|
||||||
|
use clap::Parser as _;
|
||||||
|
use pyo3::PyResult;
|
||||||
|
use std::{fmt, path};
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
|
||||||
|
#[pyo3_asyncio::tokio::main]
|
||||||
|
async fn main() -> PyResult<()> {
|
||||||
|
env_logger::builder()
|
||||||
|
.filter_level(log::LevelFilter::Info)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
py_logging_basic_config(bumble_env_logging_level("INFO"))?;
|
||||||
|
|
||||||
|
let cli: Cli = Cli::parse();
|
||||||
|
|
||||||
|
match cli.subcommand {
|
||||||
|
Subcommand::Firmware { subcommand: fw } => match fw {
|
||||||
|
Firmware::Realtek { subcommand: rtk } => match rtk {
|
||||||
|
Realtek::Download(dl) => {
|
||||||
|
cli::firmware::rtk::download(dl).await?;
|
||||||
|
}
|
||||||
|
Realtek::Drop { transport } => cli::firmware::rtk::drop(&transport).await?,
|
||||||
|
Realtek::Info { transport, force } => {
|
||||||
|
cli::firmware::rtk::info(&transport, force).await?;
|
||||||
|
}
|
||||||
|
Realtek::Load { transport, force } => {
|
||||||
|
cli::firmware::rtk::load(&transport, force).await?
|
||||||
|
}
|
||||||
|
Realtek::Parse { firmware_path } => cli::firmware::rtk::parse(&firmware_path)?,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Subcommand::Usb { subcommand } => match subcommand {
|
||||||
|
Usb::Probe(probe) => cli::usb::probe(probe.verbose)?,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
struct Cli {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
subcommand: Subcommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Debug, Clone)]
|
||||||
|
enum Subcommand {
|
||||||
|
/// Manage device firmware
|
||||||
|
Firmware {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
subcommand: Firmware,
|
||||||
|
},
|
||||||
|
/// USB operations
|
||||||
|
Usb {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
subcommand: Usb,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Debug, Clone)]
|
||||||
|
enum Firmware {
|
||||||
|
/// Manage Realtek chipset firmware
|
||||||
|
Realtek {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
subcommand: Realtek,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Debug, Clone)]
|
||||||
|
|
||||||
|
enum Realtek {
|
||||||
|
/// Download Realtek firmware
|
||||||
|
Download(Download),
|
||||||
|
/// Drop firmware from a USB device
|
||||||
|
Drop {
|
||||||
|
/// Bumble transport spec. Must be for a USB device.
|
||||||
|
///
|
||||||
|
/// <https://google.github.io/bumble/transports/index.html>
|
||||||
|
#[arg(long)]
|
||||||
|
transport: String,
|
||||||
|
},
|
||||||
|
/// Show driver info for a USB device
|
||||||
|
Info {
|
||||||
|
/// Bumble transport spec. Must be for a USB device.
|
||||||
|
///
|
||||||
|
/// <https://google.github.io/bumble/transports/index.html>
|
||||||
|
#[arg(long)]
|
||||||
|
transport: String,
|
||||||
|
/// Try to resolve driver info even if USB info is not available, or if the USB
|
||||||
|
/// (vendor,product) tuple is not in the list of known compatible RTK USB dongles.
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
force: bool,
|
||||||
|
},
|
||||||
|
/// Load firmware onto a USB device
|
||||||
|
Load {
|
||||||
|
/// Bumble transport spec. Must be for a USB device.
|
||||||
|
///
|
||||||
|
/// <https://google.github.io/bumble/transports/index.html>
|
||||||
|
#[arg(long)]
|
||||||
|
transport: String,
|
||||||
|
/// Load firmware even if the USB info doesn't match.
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
force: bool,
|
||||||
|
},
|
||||||
|
/// Parse a firmware file
|
||||||
|
Parse {
|
||||||
|
/// Firmware file to parse
|
||||||
|
firmware_path: path::PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Args, Debug, Clone)]
|
||||||
|
struct Download {
|
||||||
|
/// Directory to download to. Defaults to an OS-specific path specific to the Bumble tool.
|
||||||
|
#[arg(long)]
|
||||||
|
output_dir: Option<path::PathBuf>,
|
||||||
|
/// Source to download from
|
||||||
|
#[arg(long, default_value_t = Source::LinuxKernel)]
|
||||||
|
source: Source,
|
||||||
|
/// Only download a single image
|
||||||
|
#[arg(long, value_name = "base name")]
|
||||||
|
single: Option<String>,
|
||||||
|
/// Overwrite existing files
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
overwrite: bool,
|
||||||
|
/// Don't print the parse results for the downloaded file names
|
||||||
|
#[arg(long)]
|
||||||
|
no_parse: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, clap::ValueEnum)]
|
||||||
|
enum Source {
|
||||||
|
LinuxKernel,
|
||||||
|
RealtekOpensource,
|
||||||
|
LinuxFromScratch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Source {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Source::LinuxKernel => write!(f, "linux-kernel"),
|
||||||
|
Source::RealtekOpensource => write!(f, "realtek-opensource"),
|
||||||
|
Source::LinuxFromScratch => write!(f, "linux-from-scratch"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Debug, Clone)]
|
||||||
|
enum Usb {
|
||||||
|
/// Probe the USB bus for Bluetooth devices
|
||||||
|
Probe(Probe),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Args, Debug, Clone)]
|
||||||
|
struct Probe {
|
||||||
|
/// Show additional info for each USB device
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
verbose: bool,
|
||||||
|
}
|
||||||
@@ -20,12 +20,17 @@ use crate::{
|
|||||||
core::AdvertisingData,
|
core::AdvertisingData,
|
||||||
gatt_client::{ProfileServiceProxy, ServiceProxy},
|
gatt_client::{ProfileServiceProxy, ServiceProxy},
|
||||||
hci::Address,
|
hci::Address,
|
||||||
|
host::Host,
|
||||||
transport::{Sink, Source},
|
transport::{Sink, Source},
|
||||||
ClosureCallback,
|
ClosureCallback, PyObjectExt,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use pyo3::types::PyDict;
|
use pyo3::{
|
||||||
use pyo3::{intern, types::PyModule, PyObject, PyResult, Python, ToPyObject};
|
intern,
|
||||||
|
types::{PyDict, PyModule},
|
||||||
|
PyObject, PyResult, Python, ToPyObject,
|
||||||
|
};
|
||||||
|
use pyo3_asyncio::tokio::into_future;
|
||||||
use std::path;
|
use std::path;
|
||||||
|
|
||||||
/// A device that can send/receive HCI frames.
|
/// A device that can send/receive HCI frames.
|
||||||
@@ -65,7 +70,7 @@ impl Device {
|
|||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.0
|
||||||
.call_method0(py, intern!(py, "power_on"))
|
.call_method0(py, intern!(py, "power_on"))
|
||||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -76,7 +81,7 @@ impl Device {
|
|||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.0
|
||||||
.call_method1(py, intern!(py, "connect"), (peer_addr,))
|
.call_method1(py, intern!(py, "connect"), (peer_addr,))
|
||||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.map(Connection)
|
.map(Connection)
|
||||||
@@ -89,7 +94,7 @@ impl Device {
|
|||||||
kwargs.set_item("filter_duplicates", filter_duplicates)?;
|
kwargs.set_item("filter_duplicates", filter_duplicates)?;
|
||||||
self.0
|
self.0
|
||||||
.call_method(py, intern!(py, "start_scanning"), (), Some(kwargs))
|
.call_method(py, intern!(py, "start_scanning"), (), Some(kwargs))
|
||||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -123,6 +128,15 @@ impl Device {
|
|||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the host used by the device, if any
|
||||||
|
pub fn host(&mut self) -> PyResult<Option<Host>> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
self.0
|
||||||
|
.getattr(py, intern!(py, "host"))
|
||||||
|
.map(|obj| obj.into_option(Host::from))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Start advertising the data set with [Device.set_advertisement].
|
/// Start advertising the data set with [Device.set_advertisement].
|
||||||
pub async fn start_advertising(&mut self, auto_restart: bool) -> PyResult<()> {
|
pub async fn start_advertising(&mut self, auto_restart: bool) -> PyResult<()> {
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
@@ -131,7 +145,7 @@ impl Device {
|
|||||||
|
|
||||||
self.0
|
self.0
|
||||||
.call_method(py, intern!(py, "start_advertising"), (), Some(kwargs))
|
.call_method(py, intern!(py, "start_advertising"), (), Some(kwargs))
|
||||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -142,7 +156,7 @@ impl Device {
|
|||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.0
|
||||||
.call_method0(py, intern!(py, "stop_advertising"))
|
.call_method0(py, intern!(py, "stop_advertising"))
|
||||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -173,7 +187,7 @@ impl Peer {
|
|||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.0
|
||||||
.call_method0(py, intern!(py, "discover_services"))
|
.call_method0(py, intern!(py, "discover_services"))
|
||||||
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.and_then(|list| {
|
.and_then(|list| {
|
||||||
@@ -207,13 +221,7 @@ impl Peer {
|
|||||||
let class = module.getattr(P::PROXY_CLASS_NAME)?;
|
let class = module.getattr(P::PROXY_CLASS_NAME)?;
|
||||||
self.0
|
self.0
|
||||||
.call_method1(py, intern!(py, "create_service_proxy"), (class,))
|
.call_method1(py, intern!(py, "create_service_proxy"), (class,))
|
||||||
.map(|obj| {
|
.map(|obj| obj.into_option(P::wrap))
|
||||||
if obj.is_none(py) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(P::wrap(obj))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
rust/src/wrapper/drivers/mod.rs
Normal file
17
rust/src/wrapper/drivers/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Device drivers
|
||||||
|
|
||||||
|
pub mod rtk;
|
||||||
141
rust/src/wrapper/drivers/rtk.rs
Normal file
141
rust/src/wrapper/drivers/rtk.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Drivers for Realtek controllers
|
||||||
|
|
||||||
|
use crate::wrapper::{host::Host, PyObjectExt};
|
||||||
|
use pyo3::{intern, types::PyModule, PyObject, PyResult, Python, ToPyObject};
|
||||||
|
use pyo3_asyncio::tokio::into_future;
|
||||||
|
|
||||||
|
pub use crate::internal::drivers::rtk::{Firmware, Patch};
|
||||||
|
|
||||||
|
/// Driver for a Realtek controller
|
||||||
|
pub struct Driver(PyObject);
|
||||||
|
|
||||||
|
impl Driver {
|
||||||
|
/// Locate the driver for the provided host.
|
||||||
|
pub async fn for_host(host: &Host, force: bool) -> PyResult<Option<Self>> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||||
|
.getattr(intern!(py, "Driver"))?
|
||||||
|
.call_method1(intern!(py, "for_host"), (&host.obj, force))
|
||||||
|
.and_then(into_future)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
.map(|obj| obj.into_option(Self))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the host has a known driver.
|
||||||
|
pub async fn check(host: &Host) -> PyResult<bool> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||||
|
.getattr(intern!(py, "Driver"))?
|
||||||
|
.call_method1(intern!(py, "check"), (&host.obj,))
|
||||||
|
.and_then(|obj| obj.extract::<bool>())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the [DriverInfo] for the host, if one matches
|
||||||
|
pub async fn driver_info_for_host(host: &Host) -> PyResult<Option<DriverInfo>> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||||
|
.getattr(intern!(py, "Driver"))?
|
||||||
|
.call_method1(intern!(py, "driver_info_for_host"), (&host.obj,))
|
||||||
|
.and_then(into_future)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
.map(|obj| obj.into_option(DriverInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a command to the device to drop firmware
|
||||||
|
pub async fn drop_firmware(host: &mut Host) -> PyResult<()> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||||
|
.getattr(intern!(py, "Driver"))?
|
||||||
|
.call_method1(intern!(py, "drop_firmware"), (&host.obj,))
|
||||||
|
.and_then(into_future)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load firmware onto the device.
|
||||||
|
pub async fn download_firmware(&mut self) -> PyResult<()> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
self.0
|
||||||
|
.call_method0(py, intern!(py, "download_firmware"))
|
||||||
|
.and_then(|coroutine| into_future(coroutine.as_ref(py)))
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata about a known driver & applicable device
|
||||||
|
pub struct DriverInfo(PyObject);
|
||||||
|
|
||||||
|
impl DriverInfo {
|
||||||
|
/// Returns a list of all drivers that Bumble knows how to handle.
|
||||||
|
pub fn all_drivers() -> PyResult<Vec<DriverInfo>> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
PyModule::import(py, intern!(py, "bumble.drivers.rtk"))?
|
||||||
|
.getattr(intern!(py, "Driver"))?
|
||||||
|
.getattr(intern!(py, "DRIVER_INFOS"))?
|
||||||
|
.iter()?
|
||||||
|
.map(|r| r.map(|h| DriverInfo(h.to_object(py))))
|
||||||
|
.collect::<PyResult<Vec<_>>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The firmware file name to load from the filesystem, e.g. `foo_fw.bin`.
|
||||||
|
pub fn firmware_name(&self) -> PyResult<String> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
self.0
|
||||||
|
.getattr(py, intern!(py, "fw_name"))?
|
||||||
|
.as_ref(py)
|
||||||
|
.extract::<String>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The config file name, if any, to load from the filesystem, e.g. `foo_config.bin`.
|
||||||
|
pub fn config_name(&self) -> PyResult<Option<String>> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
let obj = self.0.getattr(py, intern!(py, "config_name"))?;
|
||||||
|
let handle = obj.as_ref(py);
|
||||||
|
|
||||||
|
if handle.is_none() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
handle
|
||||||
|
.extract::<String>()
|
||||||
|
.map(|s| if s.is_empty() { None } else { Some(s) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether or not config is required.
|
||||||
|
pub fn config_needed(&self) -> PyResult<bool> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
self.0
|
||||||
|
.getattr(py, intern!(py, "config_needed"))?
|
||||||
|
.as_ref(py)
|
||||||
|
.extract::<bool>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ROM id
|
||||||
|
pub fn rom(&self) -> PyResult<u32> {
|
||||||
|
Python::with_gil(|py| self.0.getattr(py, intern!(py, "rom"))?.as_ref(py).extract())
|
||||||
|
}
|
||||||
|
}
|
||||||
71
rust/src/wrapper/host.rs
Normal file
71
rust/src/wrapper/host.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Host-side types
|
||||||
|
|
||||||
|
use crate::wrapper::transport::{Sink, Source};
|
||||||
|
use pyo3::{intern, prelude::PyModule, types::PyDict, PyObject, PyResult, Python};
|
||||||
|
|
||||||
|
/// Host HCI commands
|
||||||
|
pub struct Host {
|
||||||
|
pub(crate) obj: PyObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Host {
|
||||||
|
/// Create a Host that wraps the provided obj
|
||||||
|
pub(crate) fn from(obj: PyObject) -> Self {
|
||||||
|
Self { obj }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Host
|
||||||
|
pub fn new(source: Source, sink: Sink) -> PyResult<Self> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
PyModule::import(py, intern!(py, "bumble.host"))?
|
||||||
|
.getattr(intern!(py, "Host"))?
|
||||||
|
.call((source.0, sink.0), None)
|
||||||
|
.map(|any| Self { obj: any.into() })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a reset command and perform other reset tasks.
|
||||||
|
pub async fn reset(&mut self, driver_factory: DriverFactory) -> PyResult<()> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
let kwargs = match driver_factory {
|
||||||
|
DriverFactory::None => {
|
||||||
|
let kw = PyDict::new(py);
|
||||||
|
kw.set_item("driver_factory", py.None())?;
|
||||||
|
Some(kw)
|
||||||
|
}
|
||||||
|
DriverFactory::Auto => {
|
||||||
|
// leave the default in place
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.obj
|
||||||
|
.call_method(py, intern!(py, "reset"), (), kwargs)
|
||||||
|
.and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py)))
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Driver factory to use when initializing a host
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DriverFactory {
|
||||||
|
/// Do not load drivers
|
||||||
|
None,
|
||||||
|
/// Load appropriate driver, if any is found
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
@@ -31,14 +31,17 @@ pub use pyo3_asyncio;
|
|||||||
pub mod assigned_numbers;
|
pub mod assigned_numbers;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod device;
|
pub mod device;
|
||||||
|
|
||||||
|
pub mod drivers;
|
||||||
pub mod gatt_client;
|
pub mod gatt_client;
|
||||||
pub mod hci;
|
pub mod hci;
|
||||||
|
pub mod host;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
|
||||||
/// Convenience extensions to [PyObject]
|
/// Convenience extensions to [PyObject]
|
||||||
pub trait PyObjectExt {
|
pub trait PyObjectExt: Sized {
|
||||||
/// Get a GIL-bound reference
|
/// Get a GIL-bound reference
|
||||||
fn gil_ref<'py>(&'py self, py: Python<'py>) -> &'py PyAny;
|
fn gil_ref<'py>(&'py self, py: Python<'py>) -> &'py PyAny;
|
||||||
|
|
||||||
@@ -49,6 +52,17 @@ pub trait PyObjectExt {
|
|||||||
{
|
{
|
||||||
Python::with_gil(|py| self.gil_ref(py).extract::<T>())
|
Python::with_gil(|py| self.gil_ref(py).extract::<T>())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If the Python object is a Python `None`, return a Rust `None`, otherwise `Some` with the mapped type
|
||||||
|
fn into_option<T>(self, map_obj: impl Fn(Self) -> T) -> Option<T> {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
if self.gil_ref(py).is_none() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(map_obj(self))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PyObjectExt for PyObject {
|
impl PyObjectExt for PyObject {
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
|
|
||||||
//! GATT profiles
|
//! GATT profiles
|
||||||
|
|
||||||
use crate::wrapper::gatt_client::{CharacteristicProxy, ProfileServiceProxy};
|
use crate::wrapper::{
|
||||||
|
gatt_client::{CharacteristicProxy, ProfileServiceProxy},
|
||||||
|
PyObjectExt,
|
||||||
|
};
|
||||||
use pyo3::{intern, PyObject, PyResult, Python};
|
use pyo3::{intern, PyObject, PyResult, Python};
|
||||||
|
|
||||||
/// Exposes the battery GATT service
|
/// Exposes the battery GATT service
|
||||||
@@ -26,13 +29,7 @@ impl BatteryServiceProxy {
|
|||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.0
|
||||||
.getattr(py, intern!(py, "battery_level"))
|
.getattr(py, intern!(py, "battery_level"))
|
||||||
.map(|level| {
|
.map(|level| level.into_option(CharacteristicProxy))
|
||||||
if level.is_none(py) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(CharacteristicProxy(level))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ install_requires =
|
|||||||
humanize >= 4.6.0; platform_system!='Emscripten'
|
humanize >= 4.6.0; platform_system!='Emscripten'
|
||||||
libusb1 >= 2.0.1; platform_system!='Emscripten'
|
libusb1 >= 2.0.1; platform_system!='Emscripten'
|
||||||
libusb-package == 1.0.26.1; platform_system!='Emscripten'
|
libusb-package == 1.0.26.1; platform_system!='Emscripten'
|
||||||
|
platformdirs == 3.10.0; platform_system!='Emscripten'
|
||||||
prompt_toolkit >= 3.0.16; platform_system!='Emscripten'
|
prompt_toolkit >= 3.0.16; platform_system!='Emscripten'
|
||||||
prettytable >= 3.6.0; platform_system!='Emscripten'
|
prettytable >= 3.6.0; platform_system!='Emscripten'
|
||||||
protobuf >= 3.12.4; platform_system!='Emscripten'
|
protobuf >= 3.12.4; platform_system!='Emscripten'
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ def download_file(base_url, name, remove_suffix):
|
|||||||
@click.command
|
@click.command
|
||||||
@click.option(
|
@click.option(
|
||||||
"--output-dir",
|
"--output-dir",
|
||||||
default=".",
|
default="",
|
||||||
help="Output directory where the files will be saved",
|
help="Output directory where the files will be saved. Defaults to the OS-specific"
|
||||||
|
"app data dir, which the driver will check when trying to find firmware",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@@ -84,7 +85,10 @@ def main(output_dir, source, single, force, parse):
|
|||||||
"""Download RTK firmware images and configs."""
|
"""Download RTK firmware images and configs."""
|
||||||
|
|
||||||
# Check that the output dir exists
|
# Check that the output dir exists
|
||||||
output_dir = pathlib.Path(output_dir)
|
if output_dir == '':
|
||||||
|
output_dir = rtk.rtk_firmware_dir()
|
||||||
|
else:
|
||||||
|
output_dir = pathlib.Path(output_dir)
|
||||||
if not output_dir.is_dir():
|
if not output_dir.is_dir():
|
||||||
print("Output dir does not exist or is not a directory")
|
print("Output dir does not exist or is not a directory")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -61,9 +61,8 @@ async def do_load(usb_transport, force):
|
|||||||
# Get the driver.
|
# Get the driver.
|
||||||
driver = await rtk.Driver.for_host(host, force)
|
driver = await rtk.Driver.for_host(host, force)
|
||||||
if driver is None:
|
if driver is None:
|
||||||
if not force:
|
print("Firmware already loaded or no supported driver for this device.")
|
||||||
print("Firmware already loaded or no supported driver for this device.")
|
return
|
||||||
return
|
|
||||||
|
|
||||||
await driver.download_firmware()
|
await driver.download_firmware()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user