Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/configs/sdkconfig.defaults.trouble
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Trouble uses ESP-IDF's Bluetooth controller without Bluedroid or NimBLE.
CONFIG_BT_ENABLED=y
CONFIG_BT_CONTROLLER_ONLY=y
CONFIG_BT_CONTROLLER_ENABLED=y

# Required on chips that let the application select the controller HCI
# interface. Other chips use VHCI for controller-only mode automatically.
CONFIG_BT_LE_HCI_INTERFACE_USE_RAM=y

# Trouble reads into a 259-byte HCI buffer. An ACL payload of 251 bytes occupies
# 256 bytes with the ACL header and H4 packet indicator.
CONFIG_BT_LE_ACL_BUF_SIZE=251

# The Trouble runner and host resources exceed ESP-IDF's default main-task stack.
CONFIG_ESP_MAIN_TASK_STACK_SIZE=32768

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HostResources should not be allocated on-stack, but staticlaly in bss or on heap. It is another topic that they regressed it recently, where such an allocation was not possible anymore due to the Controller re-generification. Fortunately, fixed in main: embassy-rs/trouble#645

4 changes: 3 additions & 1 deletion .github/workflows/ci-esp-idf-next.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ on:
- cron: '50 6 * * *'

env:
rust_toolchain: nightly
# Temporary workaround for rust-lang/rust#158168. Remove after the fix reaches nightly:
# https://github.com/rust-lang/rust/pull/160170
rust_toolchain: nightly-2026-07-29

jobs:
compile:
Expand Down
59 changes: 58 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ on:
workflow_dispatch:

env:
rust_toolchain: nightly
# Temporary workaround for rust-lang/rust#158168. Remove after the fix reaches nightly:
# https://github.com/rust-lang/rust/pull/160170
rust_toolchain: nightly-2026-07-29
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
Expand Down Expand Up @@ -226,3 +228,58 @@ jobs:
GATEWAY_IP: "192.168.1.1"
GATEWAY_NETMASK: "24"
run: cargo build --examples --features experimental --target ${{ matrix.target }} -Zbuild-std=std,panic_abort

# Trouble uses ESP-IDF's controller-only Bluetooth mode, which is mutually exclusive with
# the Bluedroid and NimBLE configurations exercised above. Build the real beacon path with
# representative Xtensa, RISC-V, and RAM-HCI targets at both supported version boundaries.
compile-trouble:
name: "Trouble / ${{ matrix.target }} / ${{ matrix.idf-version }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- riscv32imc-esp-espidf
- riscv32imac-esp-espidf
- xtensa-esp32-espidf
idf-version:
- v5.3.4
- v6.0
steps:
- name: Setup | Checkout
uses: actions/checkout@v3

- name: Setup | Rust
if: matrix.target == 'riscv32imc-esp-espidf'
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.rust_toolchain }}
components: rustfmt, clippy, rust-src

- name: Install Rust for Xtensa
if: matrix.target != 'riscv32imc-esp-espidf'
uses: esp-rs/xtensa-toolchain@v1.6
with:
default: true
ldproxy: true

- name: Setup | ldproxy
if: matrix.target == 'riscv32imc-esp-espidf'
run: |
curl -L https://github.com/esp-rs/embuild/releases/latest/download/ldproxy-x86_64-unknown-linux-gnu.zip -o $HOME/.cargo/bin/ldproxy.zip
unzip "$HOME/.cargo/bin/ldproxy.zip" -d "$HOME/.cargo/bin/"
chmod a+x $HOME/.cargo/bin/ldproxy

- name: Clippy | Trouble beacon
env:
ESP_IDF_VERSION: ${{ matrix.idf-version }}
ESP_IDF_SDKCONFIG_DEFAULTS: "${{ github.workspace }}/.github/configs/sdkconfig.defaults.trouble"
RUSTFLAGS: "--cfg espidf_time64"
run: cargo clippy --example trouble_beacon --features trouble,embassy-time-driver --target ${{ matrix.target }} -Zbuild-std=std,panic_abort -- -Dwarnings

- name: Build | Trouble beacon
env:
ESP_IDF_VERSION: ${{ matrix.idf-version }}
ESP_IDF_SDKCONFIG_DEFAULTS: "${{ github.workspace }}/.github/configs/sdkconfig.defaults.trouble"
RUSTFLAGS: "--cfg espidf_time64"
run: cargo build --example trouble_beacon --features trouble,embassy-time-driver --target ${{ matrix.target }} -Zbuild-std=std,panic_abort
4 changes: 3 additions & 1 deletion .github/workflows/publish-dry-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ name: PublishDryRun
on: workflow_dispatch

env:
rust_toolchain: nightly
# Temporary workaround for rust-lang/rust#158168. Remove after the fix reaches nightly:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert, this script does not run every day.

# https://github.com/rust-lang/rust/pull/160170
rust_toolchain: nightly-2026-07-29

jobs:
publishdryrun:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ name: Publish
on: workflow_dispatch

env:
rust_toolchain: nightly
# Temporary workaround for rust-lang/rust#158168. Remove after the fix reaches nightly:
# https://github.com/rust-lang/rust/pull/160170
rust_toolchain: nightly-2026-07-29

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

CRATE_NAME: esp-idf-svc

jobs:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ remote_component = { name = "espressif/lan87xx", version = "1.*" }
- Compatibility with ESP-IDF V6.0, and some pre-release 6.0.x.
- Added support for the Generic Ethernet PHY driver: particularly useful on ESP-IDF 6.0+ as it is built-in.
- Added type-safe wrappers for the NimBLE low-resource-use BLE stack: GAP, GATT Server, GATT Client, L2CAP. See `examples/ble_*.rs`
- Bluetooth: Added an ESP-IDF VHCI `bt-hci` transport for third-party BLE hosts such as Trouble, enabled by the new `trouble` feature. See `examples/trouble_beacon.rs`.
- TLS: Async server handshake (requires ESP-IDF 5.5.0): `EspAsyncTls::negotiate_server`. Also check the new `tls_server_async` example.

## [0.52.1] - 2026-03-10
Expand Down
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ embassy-time-driver = ["dep:embassy-time-driver", "embassy-time-queue-utils"]
alloc = ["esp-idf-hal/alloc", "embedded-svc/alloc", "uncased/alloc"]
nightly = ["embedded-svc/nightly", "esp-idf-hal/nightly"]
experimental = ["embedded-svc/experimental", "esp-idf-hal/experimental"]
trouble = [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

"alloc",
"dep:bt-hci",
"dep:embassy-sync",
"dep:embedded-io",
"critical-section",
"embassy-sync",
]

# Propagated esp-idf-hal features
critical-section = ["esp-idf-hal/critical-section"]
Expand Down Expand Up @@ -57,17 +65,22 @@ embassy-time-queue-utils = { version = "0.3", optional = true }
embassy-futures = "0.1.2"
embedded-storage = { version = "0.3", optional = true }
futures-io = { version = "0.3", optional = true }
bt-hci = { version = "0.9", optional = true, default-features = false }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the trouble feature would mean bt-hci becomes non-optional. Oh well. See also my previous comment on bt-hci-drvier (= bt-hci-transport) as a (much better) alternative.

embassy-sync = { version = "0.7", optional = true }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per my earlier comment, let's try not to introduce this.

embedded-io = { version = "0.7", optional = true }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary to list explicitly. comes via embedded-svc anyway


[build-dependencies]
embuild = "0.33.3"

[dev-dependencies]
anyhow = "1"
embassy-time = { version = "0.5", features = ["generic-queue-16"] }
esp-idf-sys = { version = "0.37.1", features = ["binstart"] }
futures = "0.3"
serde = { version = "1", default-features = false, features = ["derive"] }
serde_json = { version = "1", default-features = false, features = ["alloc"] }
postcard = "1"
trouble-host = { version = "0.7", features = ["log"] }
# `async-io-mini` is a fork of `async-io` optimized for ESP-IDF: much smaller
# RAM footprint (no large reactor thread stack, no per-fd heap bookkeeping)
async-io = { version = "0.4", package = "async-io-mini", default-features = false, features = ["futures-io"] }
Expand Down
112 changes: 112 additions & 0 deletions examples/trouble_beacon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! Non-connectable BLE beacon using the Trouble host with ESP-IDF's controller.
//!
//! Enable the `trouble` and `embassy-time-driver` Cargo features and use a
//! controller-only ESP-IDF configuration such as
//! `.github/configs/sdkconfig.defaults.trouble`.

#![allow(unknown_lints)]
#![allow(unexpected_cfgs)]

#[cfg(all(
not(any(esp32s2, esp32p4)),
feature = "trouble",
esp_idf_bt_enabled,
esp_idf_bt_controller_only,
))]
fn main() -> anyhow::Result<()> {
example::main()
}

#[cfg(not(all(
not(any(esp32s2, esp32p4)),
feature = "trouble",
esp_idf_bt_enabled,
esp_idf_bt_controller_only,
)))]
fn main() -> anyhow::Result<()> {
panic!(
"This example requires the `trouble` feature and a controller-only configuration on a chip with a BLE radio"
);
}

#[cfg(all(
not(any(esp32s2, esp32p4)),
feature = "trouble",
esp_idf_bt_enabled,
esp_idf_bt_controller_only,
))]
mod example {
use anyhow::Result;
use bt_hci::controller::ExternalController;
use embassy_futures::select::{select, Either};
use trouble_host::prelude::*;

use esp_idf_svc::bt_controller::{Ble, EspBtController, EspVhciTransport};
use esp_idf_svc::hal::peripherals::Peripherals;
use esp_idf_svc::hal::task::block_on;
use esp_idf_svc::nvs::EspDefaultNvsPartition;

type Controller = ExternalController<EspVhciTransport<'static>, 10>;

pub fn main() -> Result<()> {
esp_idf_svc::sys::link_patches();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer necessary, as it is called automatically by the startup code in esp-idf-sys. The other two explicit link calls should also not be necessary, unless you hit an actual linking issue.

let _ = esp_idf_svc::hal::task::critical_section::link();
let _ = esp_idf_svc::timer::embassy_time_driver::link();
esp_idf_svc::log::EspLogger::initialize_default();

let peripherals = Peripherals::take()?;

// Keep NVS alive for ESP-IDF's PHY calibration data.
let _nvs = EspDefaultNvsPartition::take()?;

let controller = EspBtController::<Ble>::new(peripherals.modem)?;
let transport = EspVhciTransport::new(controller)?;
let controller = ExternalController::<_, 10>::new(transport);

block_on(run(controller));

Ok(())
}

async fn run(controller: Controller) {
let address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
let mut resources: HostResources<Controller, DefaultPacketPool, 0, 0, 1> =
HostResources::new();
let stack = trouble_host::new(controller, &mut resources)
.set_random_address(address)
.build();
let mut peripheral = stack.peripheral();
let mut runner = stack.runner();

let mut adv_data = [0; 31];
let len = AdStructure::encode_slice(
&[
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
AdStructure::CompleteLocalName(b"ESP-IDF Trouble"),
],
&mut adv_data,
)
.unwrap();

log::info!("Starting Trouble beacon as {address:?}");

match select(runner.run(), async {
let _advertiser = peripheral
.advertise(
&AdvertisementParameters::default(),
Advertisement::NonconnectableNonscannableUndirected {
adv_data: &adv_data[..len],
},
)
.await
.unwrap();

core::future::pending::<()>().await;
})
.await
{
Either::First(result) => result.unwrap(),
Either::Second(()) => unreachable!("the advertising task never completes"),
}
}
}
Loading
Loading