616 lines
20 KiB
Rust
616 lines
20 KiB
Rust
|
|
use std::rc::Rc;
|
||
|
|
|
||
|
|
use bluetooth_core::{
|
||
|
|
core::uuid::Uuid,
|
||
|
|
gatt::{
|
||
|
|
self,
|
||
|
|
ffi::AttributeBackingType,
|
||
|
|
ids::{AttHandle, ConnectionId, ServerId, TransportIndex},
|
||
|
|
mocks::{
|
||
|
|
mock_datastore::{MockDatastore, MockDatastoreEvents},
|
||
|
|
mock_transport::MockAttTransport,
|
||
|
|
},
|
||
|
|
server::{
|
||
|
|
gatt_database::{
|
||
|
|
AttPermissions, GattCharacteristicWithHandle, GattDescriptorWithHandle,
|
||
|
|
GattServiceWithHandle, CHARACTERISTIC_UUID, PRIMARY_SERVICE_DECLARATION_UUID,
|
||
|
|
},
|
||
|
|
services::{
|
||
|
|
gap::DEVICE_NAME_UUID,
|
||
|
|
gatt::{
|
||
|
|
CLIENT_CHARACTERISTIC_CONFIGURATION_UUID, GATT_SERVICE_UUID,
|
||
|
|
SERVICE_CHANGE_UUID,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
GattModule, IndicationError,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
packets::{
|
||
|
|
AttAttributeDataChild, AttBuilder, AttChild, AttErrorCode, AttErrorResponseBuilder,
|
||
|
|
AttFindByTypeValueRequestBuilder, AttFindInformationRequestBuilder,
|
||
|
|
AttFindInformationResponseChild, AttHandleValueConfirmationBuilder,
|
||
|
|
AttHandleValueIndicationBuilder, AttOpcode, AttReadByTypeRequestBuilder,
|
||
|
|
AttReadRequestBuilder, AttReadResponseBuilder, AttWriteRequestBuilder,
|
||
|
|
AttWriteResponseBuilder, GattClientCharacteristicConfigurationBuilder,
|
||
|
|
GattServiceChangedBuilder, GattServiceDeclarationValueBuilder, Serializable,
|
||
|
|
UuidAsAttDataBuilder,
|
||
|
|
},
|
||
|
|
utils::packet::{build_att_data, build_att_view_or_crash},
|
||
|
|
};
|
||
|
|
|
||
|
|
use tokio::{
|
||
|
|
sync::mpsc::{error::TryRecvError, UnboundedReceiver},
|
||
|
|
task::spawn_local,
|
||
|
|
};
|
||
|
|
use utils::start_test;
|
||
|
|
|
||
|
|
mod utils;
|
||
|
|
|
||
|
|
const TCB_IDX: TransportIndex = TransportIndex(1);
|
||
|
|
const SERVER_ID: ServerId = ServerId(2);
|
||
|
|
const CONN_ID: ConnectionId = ConnectionId::new(TCB_IDX, SERVER_ID);
|
||
|
|
|
||
|
|
const ANOTHER_TCB_IDX: TransportIndex = TransportIndex(2);
|
||
|
|
const ANOTHER_SERVER_ID: ServerId = ServerId(3);
|
||
|
|
const ANOTHER_CONN_ID: ConnectionId = ConnectionId::new(ANOTHER_TCB_IDX, ANOTHER_SERVER_ID);
|
||
|
|
|
||
|
|
const SERVICE_HANDLE: AttHandle = AttHandle(6);
|
||
|
|
const CHARACTERISTIC_HANDLE: AttHandle = AttHandle(8);
|
||
|
|
const DESCRIPTOR_HANDLE: AttHandle = AttHandle(9);
|
||
|
|
|
||
|
|
const SERVICE_TYPE: Uuid = Uuid::new(0x0102);
|
||
|
|
const CHARACTERISTIC_TYPE: Uuid = Uuid::new(0x0103);
|
||
|
|
const DESCRIPTOR_TYPE: Uuid = Uuid::new(0x0104);
|
||
|
|
|
||
|
|
const DATA: [u8; 4] = [1, 2, 3, 4];
|
||
|
|
const ANOTHER_DATA: [u8; 4] = [5, 6, 7, 8];
|
||
|
|
|
||
|
|
fn start_gatt_module() -> (gatt::server::GattModule, UnboundedReceiver<(TransportIndex, AttBuilder)>)
|
||
|
|
{
|
||
|
|
let (transport, transport_rx) = MockAttTransport::new();
|
||
|
|
let gatt = GattModule::new(Rc::new(transport));
|
||
|
|
|
||
|
|
(gatt, transport_rx)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn create_server_and_open_connection(
|
||
|
|
gatt: &mut GattModule,
|
||
|
|
) -> UnboundedReceiver<MockDatastoreEvents> {
|
||
|
|
gatt.open_gatt_server(SERVER_ID).unwrap();
|
||
|
|
let (datastore, data_rx) = MockDatastore::new();
|
||
|
|
gatt.register_gatt_service(
|
||
|
|
SERVER_ID,
|
||
|
|
GattServiceWithHandle {
|
||
|
|
handle: SERVICE_HANDLE,
|
||
|
|
type_: SERVICE_TYPE,
|
||
|
|
characteristics: vec![GattCharacteristicWithHandle {
|
||
|
|
handle: CHARACTERISTIC_HANDLE,
|
||
|
|
type_: CHARACTERISTIC_TYPE,
|
||
|
|
permissions: AttPermissions::READABLE
|
||
|
|
| AttPermissions::WRITABLE_WITH_RESPONSE
|
||
|
|
| AttPermissions::INDICATE,
|
||
|
|
descriptors: vec![GattDescriptorWithHandle {
|
||
|
|
handle: DESCRIPTOR_HANDLE,
|
||
|
|
type_: DESCRIPTOR_TYPE,
|
||
|
|
permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITH_RESPONSE,
|
||
|
|
}],
|
||
|
|
}],
|
||
|
|
},
|
||
|
|
datastore,
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
gatt.on_le_connect(CONN_ID).unwrap();
|
||
|
|
data_rx
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_service_read() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
|
||
|
|
create_server_and_open_connection(&mut gatt);
|
||
|
|
|
||
|
|
// act
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttReadRequestBuilder {
|
||
|
|
attribute_handle: SERVICE_HANDLE.into(),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
|
||
|
|
|
||
|
|
// assert
|
||
|
|
assert_eq!(tcb_idx, TCB_IDX);
|
||
|
|
assert_eq!(
|
||
|
|
resp,
|
||
|
|
AttBuilder {
|
||
|
|
opcode: AttOpcode::READ_RESPONSE,
|
||
|
|
_child_: AttReadResponseBuilder {
|
||
|
|
value: build_att_data(GattServiceDeclarationValueBuilder {
|
||
|
|
uuid: SERVICE_TYPE.into()
|
||
|
|
})
|
||
|
|
}
|
||
|
|
.into()
|
||
|
|
}
|
||
|
|
);
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_server_closed_while_connected() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange: set up a connection to a closed server
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
|
||
|
|
// open a server and connect
|
||
|
|
create_server_and_open_connection(&mut gatt);
|
||
|
|
gatt.close_gatt_server(SERVER_ID).unwrap();
|
||
|
|
|
||
|
|
// act: read from the closed server
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttReadRequestBuilder {
|
||
|
|
attribute_handle: SERVICE_HANDLE.into(),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let (_, resp) = transport_rx.recv().await.unwrap();
|
||
|
|
|
||
|
|
// assert that the read failed, but that a response was provided
|
||
|
|
assert_eq!(resp.opcode, AttOpcode::ERROR_RESPONSE);
|
||
|
|
assert_eq!(
|
||
|
|
resp._child_,
|
||
|
|
AttErrorResponseBuilder {
|
||
|
|
opcode_in_error: AttOpcode::READ_REQUEST,
|
||
|
|
handle_in_error: SERVICE_HANDLE.into(),
|
||
|
|
error_code: AttErrorCode::INVALID_HANDLE
|
||
|
|
}
|
||
|
|
.into()
|
||
|
|
)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_characteristic_read() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
|
||
|
|
let data = AttAttributeDataChild::RawData(DATA.into());
|
||
|
|
|
||
|
|
let mut data_rx = create_server_and_open_connection(&mut gatt);
|
||
|
|
|
||
|
|
// act
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttReadRequestBuilder {
|
||
|
|
attribute_handle: CHARACTERISTIC_HANDLE.into(),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let tx = if let MockDatastoreEvents::Read(
|
||
|
|
TCB_IDX,
|
||
|
|
CHARACTERISTIC_HANDLE,
|
||
|
|
AttributeBackingType::Characteristic,
|
||
|
|
tx,
|
||
|
|
) = data_rx.recv().await.unwrap()
|
||
|
|
{
|
||
|
|
tx
|
||
|
|
} else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
tx.send(Ok(data.clone())).unwrap();
|
||
|
|
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
|
||
|
|
|
||
|
|
// assert
|
||
|
|
assert_eq!(tcb_idx, TCB_IDX);
|
||
|
|
assert_eq!(
|
||
|
|
resp,
|
||
|
|
AttBuilder {
|
||
|
|
opcode: AttOpcode::READ_RESPONSE,
|
||
|
|
_child_: AttReadResponseBuilder { value: build_att_data(data) }.into()
|
||
|
|
}
|
||
|
|
);
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_characteristic_write() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
|
||
|
|
let data = AttAttributeDataChild::RawData(DATA.into());
|
||
|
|
|
||
|
|
let mut data_rx = create_server_and_open_connection(&mut gatt);
|
||
|
|
|
||
|
|
// act
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttWriteRequestBuilder {
|
||
|
|
handle: CHARACTERISTIC_HANDLE.into(),
|
||
|
|
value: build_att_data(data.clone()),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let (tx, written_data) = if let MockDatastoreEvents::Write(
|
||
|
|
TCB_IDX,
|
||
|
|
CHARACTERISTIC_HANDLE,
|
||
|
|
AttributeBackingType::Characteristic,
|
||
|
|
written_data,
|
||
|
|
tx,
|
||
|
|
) = data_rx.recv().await.unwrap()
|
||
|
|
{
|
||
|
|
(tx, written_data)
|
||
|
|
} else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
tx.send(Ok(())).unwrap();
|
||
|
|
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
|
||
|
|
|
||
|
|
// assert
|
||
|
|
assert_eq!(tcb_idx, TCB_IDX);
|
||
|
|
assert_eq!(
|
||
|
|
resp,
|
||
|
|
AttBuilder {
|
||
|
|
opcode: AttOpcode::WRITE_RESPONSE,
|
||
|
|
_child_: AttWriteResponseBuilder {}.into()
|
||
|
|
}
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
data.to_vec().unwrap(),
|
||
|
|
written_data.view().get_raw_payload().collect::<Vec<_>>()
|
||
|
|
)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_send_indication() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
|
||
|
|
let data = AttAttributeDataChild::RawData(DATA.into());
|
||
|
|
|
||
|
|
create_server_and_open_connection(&mut gatt);
|
||
|
|
|
||
|
|
// act
|
||
|
|
let pending_indication = spawn_local(
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().send_indication(CHARACTERISTIC_HANDLE, data.clone()),
|
||
|
|
);
|
||
|
|
|
||
|
|
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
|
||
|
|
|
||
|
|
gatt.get_bearer(TCB_IDX)
|
||
|
|
.unwrap()
|
||
|
|
.handle_packet(build_att_view_or_crash(AttHandleValueConfirmationBuilder {}).view());
|
||
|
|
|
||
|
|
// assert
|
||
|
|
assert!(matches!(pending_indication.await.unwrap(), Ok(())));
|
||
|
|
assert_eq!(tcb_idx, TCB_IDX);
|
||
|
|
assert_eq!(
|
||
|
|
resp,
|
||
|
|
AttBuilder {
|
||
|
|
opcode: AttOpcode::HANDLE_VALUE_INDICATION,
|
||
|
|
_child_: AttHandleValueIndicationBuilder {
|
||
|
|
handle: CHARACTERISTIC_HANDLE.into(),
|
||
|
|
value: build_att_data(data),
|
||
|
|
}
|
||
|
|
.into()
|
||
|
|
}
|
||
|
|
);
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_send_indication_and_disconnect() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
|
||
|
|
create_server_and_open_connection(&mut gatt);
|
||
|
|
|
||
|
|
// act: send an indication, then disconnect
|
||
|
|
let pending_indication = spawn_local(gatt.get_bearer(TCB_IDX).unwrap().send_indication(
|
||
|
|
CHARACTERISTIC_HANDLE,
|
||
|
|
AttAttributeDataChild::RawData([1, 2, 3, 4].into()),
|
||
|
|
));
|
||
|
|
transport_rx.recv().await.unwrap();
|
||
|
|
gatt.on_le_disconnect(TCB_IDX).unwrap();
|
||
|
|
|
||
|
|
// assert: the pending indication resolves appropriately
|
||
|
|
assert!(matches!(
|
||
|
|
pending_indication.await.unwrap(),
|
||
|
|
Err(IndicationError::ConnectionDroppedWhileWaitingForConfirmation)
|
||
|
|
));
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_write_to_descriptor() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
|
||
|
|
let data = AttAttributeDataChild::RawData(DATA.into());
|
||
|
|
|
||
|
|
let mut data_rx = create_server_and_open_connection(&mut gatt);
|
||
|
|
|
||
|
|
// act
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttWriteRequestBuilder {
|
||
|
|
handle: DESCRIPTOR_HANDLE.into(),
|
||
|
|
value: build_att_data(data.clone()),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let (tx, written_data) = if let MockDatastoreEvents::Write(
|
||
|
|
TCB_IDX,
|
||
|
|
DESCRIPTOR_HANDLE,
|
||
|
|
AttributeBackingType::Descriptor,
|
||
|
|
written_data,
|
||
|
|
tx,
|
||
|
|
) = data_rx.recv().await.unwrap()
|
||
|
|
{
|
||
|
|
(tx, written_data)
|
||
|
|
} else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
tx.send(Ok(())).unwrap();
|
||
|
|
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
|
||
|
|
|
||
|
|
// assert
|
||
|
|
assert_eq!(tcb_idx, TCB_IDX);
|
||
|
|
assert_eq!(
|
||
|
|
resp,
|
||
|
|
AttBuilder {
|
||
|
|
opcode: AttOpcode::WRITE_RESPONSE,
|
||
|
|
_child_: AttWriteResponseBuilder {}.into()
|
||
|
|
}
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
data.to_vec().unwrap(),
|
||
|
|
written_data.view().get_raw_payload().collect::<Vec<_>>()
|
||
|
|
)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_multiple_servers() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
let data = AttAttributeDataChild::RawData(DATA.into());
|
||
|
|
let another_data = AttAttributeDataChild::RawData(ANOTHER_DATA.into());
|
||
|
|
// open the default server (SERVER_ID on CONN_ID)
|
||
|
|
let mut data_rx_1 = create_server_and_open_connection(&mut gatt);
|
||
|
|
// open a second server and connect to it (ANOTHER_SERVER_ID on ANOTHER_CONN_ID)
|
||
|
|
let (datastore, mut data_rx_2) = MockDatastore::new();
|
||
|
|
gatt.open_gatt_server(ANOTHER_SERVER_ID).unwrap();
|
||
|
|
gatt.register_gatt_service(
|
||
|
|
ANOTHER_SERVER_ID,
|
||
|
|
GattServiceWithHandle {
|
||
|
|
handle: SERVICE_HANDLE,
|
||
|
|
type_: SERVICE_TYPE,
|
||
|
|
characteristics: vec![GattCharacteristicWithHandle {
|
||
|
|
handle: CHARACTERISTIC_HANDLE,
|
||
|
|
type_: CHARACTERISTIC_TYPE,
|
||
|
|
permissions: AttPermissions::READABLE,
|
||
|
|
descriptors: vec![],
|
||
|
|
}],
|
||
|
|
},
|
||
|
|
datastore,
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
gatt.on_le_connect(ANOTHER_CONN_ID).unwrap();
|
||
|
|
|
||
|
|
// act: read from both connections
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttReadRequestBuilder {
|
||
|
|
attribute_handle: CHARACTERISTIC_HANDLE.into(),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
gatt.get_bearer(ANOTHER_TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttReadRequestBuilder {
|
||
|
|
attribute_handle: CHARACTERISTIC_HANDLE.into(),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
// service the first read with `data`
|
||
|
|
let MockDatastoreEvents::Read(
|
||
|
|
TCB_IDX,
|
||
|
|
_, _,
|
||
|
|
tx,
|
||
|
|
) = data_rx_1.recv().await.unwrap() else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
tx.send(Ok(data.clone())).unwrap();
|
||
|
|
// and then the second read with `another_data`
|
||
|
|
let MockDatastoreEvents::Read(
|
||
|
|
ANOTHER_TCB_IDX,
|
||
|
|
_, _,
|
||
|
|
tx,
|
||
|
|
) = data_rx_2.recv().await.unwrap() else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
tx.send(Ok(another_data.clone())).unwrap();
|
||
|
|
|
||
|
|
// receive both response packets
|
||
|
|
let (tcb_idx_1, resp_1) = transport_rx.recv().await.unwrap();
|
||
|
|
let (tcb_idx_2, resp_2) = transport_rx.recv().await.unwrap();
|
||
|
|
|
||
|
|
// assert: the responses were routed to the correct connections
|
||
|
|
assert_eq!(tcb_idx_1, TCB_IDX);
|
||
|
|
assert_eq!(resp_1._child_.to_vec().unwrap(), DATA);
|
||
|
|
assert_eq!(tcb_idx_2, ANOTHER_TCB_IDX);
|
||
|
|
assert_eq!(resp_2._child_.to_vec().unwrap(), ANOTHER_DATA);
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_read_device_name() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
create_server_and_open_connection(&mut gatt);
|
||
|
|
|
||
|
|
// act: try to read the device name
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttReadByTypeRequestBuilder {
|
||
|
|
starting_handle: AttHandle(1).into(),
|
||
|
|
ending_handle: AttHandle(0xFFFF).into(),
|
||
|
|
attribute_type: DEVICE_NAME_UUID.into(),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
|
||
|
|
|
||
|
|
// assert: the name should not be readable
|
||
|
|
assert_eq!(tcb_idx, TCB_IDX);
|
||
|
|
let AttChild::AttErrorResponse(resp) = resp._child_ else {
|
||
|
|
unreachable!("{resp:?}");
|
||
|
|
};
|
||
|
|
assert_eq!(resp.error_code, AttErrorCode::INSUFFICIENT_AUTHENTICATION);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_ignored_service_change_indication() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
create_server_and_open_connection(&mut gatt);
|
||
|
|
|
||
|
|
// act: add a new service
|
||
|
|
let (datastore, _) = MockDatastore::new();
|
||
|
|
gatt.register_gatt_service(
|
||
|
|
SERVER_ID,
|
||
|
|
GattServiceWithHandle {
|
||
|
|
handle: AttHandle(30),
|
||
|
|
type_: SERVICE_TYPE,
|
||
|
|
characteristics: vec![],
|
||
|
|
},
|
||
|
|
datastore,
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
// assert: no packets should be sent
|
||
|
|
assert_eq!(transport_rx.try_recv().unwrap_err(), TryRecvError::Empty);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_service_change_indication() {
|
||
|
|
start_test(async move {
|
||
|
|
// arrange
|
||
|
|
let (mut gatt, mut transport_rx) = start_gatt_module();
|
||
|
|
create_server_and_open_connection(&mut gatt);
|
||
|
|
|
||
|
|
// act: discover the GATT server
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttFindByTypeValueRequestBuilder {
|
||
|
|
starting_handle: AttHandle::MIN.into(),
|
||
|
|
ending_handle: AttHandle::MAX.into(),
|
||
|
|
attribute_type: PRIMARY_SERVICE_DECLARATION_UUID.try_into().unwrap(),
|
||
|
|
attribute_value: build_att_data(UuidAsAttDataBuilder {
|
||
|
|
uuid: GATT_SERVICE_UUID.into(),
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let AttChild::AttFindByTypeValueResponse(resp) = transport_rx.recv().await.unwrap().1._child_ else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
let (starting_handle, ending_handle) = (
|
||
|
|
resp.handles_info[0].clone().found_attribute_handle,
|
||
|
|
resp.handles_info[0].clone().group_end_handle,
|
||
|
|
);
|
||
|
|
// act: discover the service changed characteristic
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttReadByTypeRequestBuilder {
|
||
|
|
starting_handle,
|
||
|
|
ending_handle,
|
||
|
|
attribute_type: CHARACTERISTIC_UUID.into(),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let AttChild::AttReadByTypeResponse(resp) = transport_rx.recv().await.unwrap().1._child_ else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
let service_change_char_handle = resp.data.into_vec().into_iter().find_map(|characteristic| {
|
||
|
|
let AttAttributeDataChild::GattCharacteristicDeclarationValue(decl) = characteristic.value._child_ else {
|
||
|
|
unreachable!();
|
||
|
|
};
|
||
|
|
if decl.uuid == SERVICE_CHANGE_UUID.into() {
|
||
|
|
Some(decl.handle)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
}).unwrap();
|
||
|
|
// act: find the CCC descriptor for the service changed characteristic
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttFindInformationRequestBuilder {
|
||
|
|
starting_handle: service_change_char_handle.clone(),
|
||
|
|
ending_handle: AttHandle::MAX.into(),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let AttChild::AttFindInformationResponse(resp) = transport_rx.recv().await.unwrap().1._child_ else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
let AttFindInformationResponseChild::AttFindInformationShortResponse(resp) = resp._child_ else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
let service_change_descriptor_handle = resp
|
||
|
|
.data
|
||
|
|
.into_vec()
|
||
|
|
.into_iter()
|
||
|
|
.find_map(|attr| {
|
||
|
|
if attr.uuid == CLIENT_CHARACTERISTIC_CONFIGURATION_UUID.try_into().unwrap() {
|
||
|
|
Some(attr.handle)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.unwrap();
|
||
|
|
// act: register for indications on this handle
|
||
|
|
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
|
||
|
|
build_att_view_or_crash(AttWriteRequestBuilder {
|
||
|
|
handle: service_change_descriptor_handle,
|
||
|
|
value: build_att_data(GattClientCharacteristicConfigurationBuilder {
|
||
|
|
notification: 0,
|
||
|
|
indication: 1,
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
.view(),
|
||
|
|
);
|
||
|
|
let AttChild::AttWriteResponse(_) = transport_rx.recv().await.unwrap().1._child_ else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
// act: add a new service
|
||
|
|
let (datastore, _) = MockDatastore::new();
|
||
|
|
gatt.register_gatt_service(
|
||
|
|
SERVER_ID,
|
||
|
|
GattServiceWithHandle {
|
||
|
|
handle: AttHandle(30),
|
||
|
|
type_: SERVICE_TYPE,
|
||
|
|
characteristics: vec![],
|
||
|
|
},
|
||
|
|
datastore,
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
// assert: we got an indication
|
||
|
|
let AttChild::AttHandleValueIndication(indication) = transport_rx.recv().await.unwrap().1._child_ else {
|
||
|
|
unreachable!()
|
||
|
|
};
|
||
|
|
assert_eq!(indication.handle, service_change_char_handle);
|
||
|
|
assert_eq!(
|
||
|
|
indication.value,
|
||
|
|
build_att_data(GattServiceChangedBuilder {
|
||
|
|
start_handle: AttHandle(30).into(),
|
||
|
|
end_handle: AttHandle(30).into(),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|