2026-02-01 15:27:30 +00:00
|
|
|
use embassy_time::Timer;
|
|
|
|
|
use trouble_host::prelude::*;
|
|
|
|
|
use esp_radio::ble::controller::BleConnector;
|
|
|
|
|
use esp_radio::Controller as RadioController;
|
|
|
|
|
use log::{warn,info};
|
|
|
|
|
use embassy_futures::join::join;
|
|
|
|
|
use embassy_futures::select::select;
|
|
|
|
|
|
2026-02-02 21:19:39 +00:00
|
|
|
use alloc::vec::Vec;
|
2026-02-01 15:27:30 +00:00
|
|
|
|
|
|
|
|
/// Max number of connections
|
|
|
|
|
const CONNECTIONS_MAX: usize = 1;
|
|
|
|
|
|
|
|
|
|
/// Max number of L2CAP channels.
|
|
|
|
|
// XXX why not 0?
|
|
|
|
|
const L2CAP_CHANNELS_MAX: usize = 2; // Signal + att
|
|
|
|
|
|
|
|
|
|
// GATT Server definition
|
|
|
|
|
#[gatt_server]
|
|
|
|
|
struct Server {
|
|
|
|
|
wifi_provisioning_service: WifiProvisioningService,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[gatt_service(uuid = "76be25c2-0000-44fc-a97c-bcb56c69c926")]
|
|
|
|
|
struct WifiProvisioningService {
|
|
|
|
|
#[characteristic(uuid = "76be25c2-0801-44fc-a97c-bcb56c69c926", read, notify, value = 0)]
|
|
|
|
|
connection_state: u8,
|
|
|
|
|
|
|
|
|
|
#[characteristic(uuid = "76be25c2-0802-44fc-a97c-bcb56c69c926", read, notify, value = 0)]
|
|
|
|
|
max_network_index: u8,
|
|
|
|
|
|
2026-02-01 16:45:53 +00:00
|
|
|
#[characteristic(uuid = "76be25c2-0803-44fc-a97c-bcb56c69c926", read, write, value = 0)]
|
2026-02-01 15:27:30 +00:00
|
|
|
current_network_index: u8,
|
|
|
|
|
|
|
|
|
|
// for arrays > 32 we must supply a default value : see
|
|
|
|
|
// https://github.com/embassy-rs/trouble/issues/320
|
|
|
|
|
#[characteristic(uuid = "76be25c2-0804-44fc-a97c-bcb56c69c926",
|
|
|
|
|
value = [0; 40],
|
|
|
|
|
read, notify)]
|
|
|
|
|
current_network: [u8; 40],
|
|
|
|
|
|
|
|
|
|
#[characteristic(uuid = "76be25c2-0805-44fc-a97c-bcb56c69c926",
|
|
|
|
|
value = [0; 64],
|
|
|
|
|
write)]
|
|
|
|
|
secret: [u8; 64]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub async fn run_ble(radio_init: &RadioController<'static>,
|
2026-02-02 21:19:39 +00:00
|
|
|
peripheral: esp_hal::peripherals::BT<'_>,
|
|
|
|
|
networks: Vec<[u8; 40]>) {
|
2026-02-01 15:27:30 +00:00
|
|
|
// let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller");
|
|
|
|
|
let connector = BleConnector::new(&radio_init, peripheral, Default::default()).expect("connector");
|
|
|
|
|
let controller: ExternalController<_, 20> = ExternalController::new(connector);
|
|
|
|
|
|
|
|
|
|
// Using a fixed "random" address can be useful for testing. In real scenarios, one would
|
|
|
|
|
// use e.g. the MAC 6 byte array as the address (how to get that varies by the platform).
|
|
|
|
|
let address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
|
|
|
|
|
info!("Our address = {:?}", address);
|
|
|
|
|
|
|
|
|
|
let mut resources: HostResources<DefaultPacketPool, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> = HostResources::new();
|
|
|
|
|
let stack = trouble_host::new(controller, &mut resources).set_random_address(address);
|
|
|
|
|
let Host {
|
|
|
|
|
mut peripheral, runner, ..
|
|
|
|
|
} = stack.build();
|
|
|
|
|
|
|
|
|
|
info!("Starting advertising and GATT service");
|
|
|
|
|
let server = Server::new_with_config(GapConfig::Peripheral(PeripheralConfig {
|
|
|
|
|
name: "eculocate",
|
|
|
|
|
appearance: &appearance::power_device::GENERIC_POWER_DEVICE,
|
|
|
|
|
}))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let _ = join(
|
|
|
|
|
ble_task(runner),
|
|
|
|
|
async {
|
|
|
|
|
loop {
|
|
|
|
|
match advertise("Eculocate", &mut peripheral, &server).await {
|
|
|
|
|
Ok(conn) => {
|
|
|
|
|
// set up tasks when the connection is established to a central, so they don't run when no one is connected.
|
2026-02-02 21:19:39 +00:00
|
|
|
let a = gatt_events_task(&server, &conn, &networks);
|
2026-02-01 15:27:30 +00:00
|
|
|
let b = custom_task(&server, &conn, &stack);
|
|
|
|
|
// run until any task ends (usually because the connection has been closed),
|
|
|
|
|
// then return to advertising state.
|
|
|
|
|
select(a, b).await;
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
panic!("[adv] error: {:?}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// This is a background task that is required to run forever alongside any other BLE tasks.
|
|
|
|
|
///
|
|
|
|
|
async fn ble_task<C: Controller, P: PacketPool>(mut runner: Runner<'_, C, P>) {
|
|
|
|
|
loop {
|
|
|
|
|
if let Err(e) = runner.run().await {
|
|
|
|
|
panic!("[ble_task] error: {:?}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Stream Events until the connection closes.
|
|
|
|
|
///
|
|
|
|
|
/// This function will handle the GATT events and process them.
|
|
|
|
|
/// This is how we interact with read and write requests.
|
2026-02-02 21:19:39 +00:00
|
|
|
async fn gatt_events_task<P: PacketPool>(server: &Server<'_>,
|
|
|
|
|
conn: &GattConnection<'_, '_, P>,
|
|
|
|
|
networks: &Vec<[u8; 40]> ) -> Result<(), Error> {
|
|
|
|
|
let wps = &server.wifi_provisioning_service;
|
|
|
|
|
for ap in networks {
|
|
|
|
|
info!("ap {:?}", ap)
|
|
|
|
|
};
|
|
|
|
|
let max_index : u8 = (networks.len() & 0xff).try_into().unwrap();
|
|
|
|
|
server.set(&wps.max_network_index, &max_index);
|
|
|
|
|
let current_index = wps.current_network_index;
|
2026-02-01 15:27:30 +00:00
|
|
|
let reason = loop {
|
|
|
|
|
match conn.next().await {
|
|
|
|
|
GattConnectionEvent::Disconnected { reason } => break reason,
|
|
|
|
|
GattConnectionEvent::Gatt { event } => {
|
2026-02-01 16:43:45 +00:00
|
|
|
match &event {
|
|
|
|
|
// // are these clauses important? they don't do anything
|
|
|
|
|
// // except log
|
|
|
|
|
// GattEvent::Read(event) => {
|
|
|
|
|
// if event.handle() == level.handle {
|
|
|
|
|
// let value = server.get(&level);
|
|
|
|
|
// info!("[gatt] Read Event to Level Characteristic: {:?}", value);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
GattEvent::Write(event) => {
|
2026-02-02 21:19:39 +00:00
|
|
|
info!("[gatt] write handle {:?} {:?}", event.handle(),
|
|
|
|
|
event.data());
|
2026-02-01 16:43:45 +00:00
|
|
|
if event.handle() == current_index.handle {
|
2026-02-02 21:19:39 +00:00
|
|
|
let i = (event.data()[0]) as usize;
|
|
|
|
|
info!("[gatt] switch current index: {:?}", i);
|
|
|
|
|
if i == 0 {
|
|
|
|
|
server.set(&wps.current_network, &[0u8;40]);
|
|
|
|
|
} else if i <= networks.len() {
|
|
|
|
|
server.set(&wps.current_network,
|
|
|
|
|
&(networks[i - 1]));
|
|
|
|
|
} else {
|
|
|
|
|
warn!("index {:?} out of range", i);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-01 16:43:45 +00:00
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
};
|
|
|
|
|
// This step is also performed at drop(), but writing
|
|
|
|
|
// it explicitly is necessary in order to ensure reply
|
|
|
|
|
// is sent.
|
2026-02-01 15:27:30 +00:00
|
|
|
match event.accept() {
|
|
|
|
|
Ok(reply) => reply.send().await,
|
|
|
|
|
Err(e) => warn!("[gatt] error sending response: {:?}", e),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
_ => {} // ignore other Gatt Connection Events
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
info!("[gatt] disconnected: {:?}", reason);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create an advertiser to use to connect to a BLE Central, and wait for it to connect.
|
|
|
|
|
async fn advertise<'values, 'server, C: Controller>(
|
|
|
|
|
name: &'values str,
|
|
|
|
|
peripheral: &mut Peripheral<'values, C, DefaultPacketPool>,
|
|
|
|
|
server: &'server Server<'values>,
|
|
|
|
|
) -> Result<GattConnection<'values, 'server, DefaultPacketPool>, BleHostError<C::Error>> {
|
|
|
|
|
let mut advertiser_data = [0; 31];
|
|
|
|
|
let len = AdStructure::encode_slice(
|
|
|
|
|
&[
|
|
|
|
|
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
|
|
|
|
|
AdStructure::ServiceUuids16(&[[0x0f, 0x18]]),
|
|
|
|
|
AdStructure::CompleteLocalName(name.as_bytes()),
|
|
|
|
|
],
|
|
|
|
|
&mut advertiser_data[..],
|
|
|
|
|
)?;
|
|
|
|
|
let advertiser = peripheral
|
|
|
|
|
.advertise(
|
|
|
|
|
&Default::default(),
|
|
|
|
|
Advertisement::ConnectableScannableUndirected {
|
|
|
|
|
adv_data: &advertiser_data[..len],
|
|
|
|
|
scan_data: &[],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
info!("[adv] advertising");
|
|
|
|
|
let conn = advertiser.accept().await?.with_attribute_server(server)?;
|
|
|
|
|
info!("[adv] connection established");
|
|
|
|
|
Ok(conn)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn custom_task<C: Controller, P: PacketPool>(
|
|
|
|
|
_server: &Server<'_>,
|
|
|
|
|
conn: &GattConnection<'_, '_, P>,
|
|
|
|
|
stack: &Stack<'_, C, P>,
|
|
|
|
|
) {
|
|
|
|
|
// let mut tick: u8 = 0;
|
|
|
|
|
// let level = server.battery_service.level;
|
|
|
|
|
// let rpm = server.engine_speed_service.rpm;
|
|
|
|
|
loop {
|
|
|
|
|
// tick = tick.wrapping_add(1);
|
|
|
|
|
// info!("[custom_task] notifying connection of tick {}", tick);
|
|
|
|
|
|
|
|
|
|
// // if the characteristic does not support notifications,
|
|
|
|
|
// // this would return an error. Use `set` instead, in that case.
|
|
|
|
|
// // See inline comments in trouble-host/src/attribute.rs around
|
|
|
|
|
// // line 631
|
|
|
|
|
|
|
|
|
|
// if level.notify(conn, &tick).await.is_err() {
|
|
|
|
|
// info!("[custom_task] error notifying connection");
|
|
|
|
|
// break;
|
|
|
|
|
// };
|
|
|
|
|
// read RSSI (Received Signal Strength Indicator) of the connection.
|
|
|
|
|
if let Ok(rssi) = conn.raw().rssi(stack).await {
|
|
|
|
|
info!("[custom_task] RSSI: {:?}", rssi);
|
|
|
|
|
} else {
|
|
|
|
|
info!("[custom_task] error getting RSSI");
|
|
|
|
|
break;
|
|
|
|
|
};
|
|
|
|
|
// let new_rpm = 1200 + 20 * i32::from(tick);
|
|
|
|
|
// rpm.notify(conn, &new_rpm).await.expect("failed to notify");
|
|
|
|
|
|
|
|
|
|
Timer::after_secs(2).await;
|
|
|
|
|
}
|
|
|
|
|
}
|