pw_bluetooth_profiles: Implement Device Information Service (DIS)
Added a new pw_bluetooth_profiles module to start implementing basic
GATT services that can be reused by other projects.
The DIS version 1.1 contains up to 9 characteristics with read-only
values, representing properties that are typically static for the
lifetime of a given firmware running on a given device, but some of
which may be only known at runtime. All characteristics are optional and
devices may choose to only expose some of them. This implementation
provides a way to define the subset of characteristics to expose at
compile time, allowing gatt::LocalServiceInfo and the span of
Characteristic to be stored in flash.
Bug: 255633576
Test: Added unit tests.
Change-Id: Ic836bc56b935fb9d6980d8c4f267c8538b99c223
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/118971
Commit-Queue: Ben Lawson <benlawson@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Reviewed-by: Ben Lawson <benlawson@google.com>
diff --git a/PIGWEED_MODULES b/PIGWEED_MODULES
index 34de8ac..7b4eb59 100644
--- a/PIGWEED_MODULES
+++ b/PIGWEED_MODULES
@@ -14,6 +14,7 @@
pw_blob_store
pw_bluetooth
pw_bluetooth_hci
+pw_bluetooth_profiles
pw_boot
pw_boot_cortex_m
pw_build
diff --git a/pw_bluetooth/BUILD.bazel b/pw_bluetooth/BUILD.bazel
index 8e058b1..dffe324 100644
--- a/pw_bluetooth/BUILD.bazel
+++ b/pw_bluetooth/BUILD.bazel
@@ -86,7 +86,7 @@
"result_test.cc",
],
deps = [
- "pw_bluetooth",
+ ":pw_bluetooth",
],
)
@@ -96,6 +96,6 @@
"uuid_test.cc",
],
deps = [
- "pw_bluetooth",
+ ":pw_bluetooth",
],
)
diff --git a/pw_bluetooth/public/pw_bluetooth/uuid.h b/pw_bluetooth/public/pw_bluetooth/uuid.h
index deead9b..b102dbd 100644
--- a/pw_bluetooth/public/pw_bluetooth/uuid.h
+++ b/pw_bluetooth/public/pw_bluetooth/uuid.h
@@ -83,6 +83,8 @@
// all 16-bit and 32-bit short UUIDs.
static constexpr const Uuid& BluetoothBase();
+ constexpr Uuid() : uuid_() {}
+
// Create a UUID combining 96-bits from a base UUID with a 16-bit or 32-bit
// value. 16-bit values will be extended to 32-bit ones, meaning the that the
// 16 most significant bits will be set to 0 regardless of the value on the
diff --git a/pw_bluetooth_profiles/BUILD.bazel b/pw_bluetooth_profiles/BUILD.bazel
new file mode 100644
index 0000000..680f9f0
--- /dev/null
+++ b/pw_bluetooth_profiles/BUILD.bazel
@@ -0,0 +1,51 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# Bazel does not yet support building docs.
+filegroup(
+ name = "docs",
+ srcs = ["docs.rst"],
+)
+
+# Device Information Service 1.1
+pw_cc_library(
+ name = "device_info_service",
+ srcs = [
+ "device_info_service.cc",
+ ],
+ hdrs = [
+ "public/pw_bluetooth_profiles/device_info_service.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_assert",
+ "//pw_bluetooth",
+ ],
+)
+
+pw_cc_test(
+ name = "device_info_service_test",
+ srcs = ["device_info_service_test.cc"],
+ deps = [":device_info_service"],
+)
diff --git a/pw_bluetooth_profiles/BUILD.gn b/pw_bluetooth_profiles/BUILD.gn
new file mode 100644
index 0000000..d09ff93
--- /dev/null
+++ b/pw_bluetooth_profiles/BUILD.gn
@@ -0,0 +1,50 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+# Device Information Service 1.1
+pw_source_set("device_info_service") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_bluetooth_profiles/device_info_service.h" ]
+ public_deps = [
+ dir_pw_bluetooth,
+ dir_pw_span,
+ ]
+ deps = [ dir_pw_assert ]
+ sources = [ "device_info_service.cc" ]
+}
+
+pw_test_group("tests") {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+ tests = [ ":device_info_service_test" ]
+}
+
+pw_test("device_info_service_test") {
+ sources = [ "device_info_service_test.cc" ]
+ deps = [ ":device_info_service" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_bluetooth_profiles/CMakeLists.txt b/pw_bluetooth_profiles/CMakeLists.txt
new file mode 100644
index 0000000..d6cfa75
--- /dev/null
+++ b/pw_bluetooth_profiles/CMakeLists.txt
@@ -0,0 +1,40 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# CMake does not yet support building docs.
+
+# Device Information Service 1.1
+pw_add_module_library(pw_bluetooth_profiles.device_info_service
+ HEADERS
+ public/pw_bluetooth_profiles/device_info_service.h
+ SOURCES
+ device_info_service.cc
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bluetooth
+ PRIVATE_DEPS
+ pw_assert
+)
+
+pw_add_test(pw_bluetooth_profiles.device_info_service_test
+ SOURCES
+ device_info_service_test.cc
+ PRIVATE_DEPS
+ pw_bluetooth_profiles.device_info_service
+ GROUPS
+ pw_bluetooth_profiles
+)
diff --git a/pw_bluetooth_profiles/device_info_service.cc b/pw_bluetooth_profiles/device_info_service.cc
new file mode 100644
index 0000000..d20e366
--- /dev/null
+++ b/pw_bluetooth_profiles/device_info_service.cc
@@ -0,0 +1,67 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_bluetooth_profiles/device_info_service.h"
+
+#include "pw_assert/check.h"
+
+namespace pw::bluetooth_profiles {
+
+void DeviceInfoServiceImpl::PublishService(
+ bluetooth::gatt::Server* gatt_server,
+ Callback<
+ void(bluetooth::Result<bluetooth::gatt::Server::PublishServiceError>)>
+ result_callback) {
+ PW_CHECK(publish_service_callback_ == nullptr);
+ publish_service_callback_ = std::move(result_callback);
+ this->delegate_.SetServicePtr(nullptr);
+ return gatt_server->PublishService(
+ service_info_,
+ &delegate_,
+ [this](bluetooth::gatt::Server::PublishServiceResult result) {
+ if (result.ok()) {
+ this->publish_service_callback_({});
+ this->delegate_.SetServicePtr(std::move(result.value()));
+ } else {
+ this->publish_service_callback_(result.error());
+ }
+ });
+}
+
+void DeviceInfoServiceImpl::Delegate::OnError(
+ bluetooth::gatt::Error /* error */) {
+ local_service_.reset();
+}
+
+void DeviceInfoServiceImpl::Delegate::ReadValue(
+ bluetooth::PeerId /* peer_id */,
+ bluetooth::gatt::Handle handle,
+ uint32_t offset,
+ Function<void(
+ bluetooth::Result<bluetooth::gatt::Error, span<const std::byte>>)>&&
+ result_callback) {
+ const uint32_t value_index = static_cast<uint32_t>(handle);
+ if (value_index >= values_.size()) {
+ result_callback(bluetooth::gatt::Error::kInvalidHandle);
+ return;
+ }
+ span<const std::byte> value = values_[value_index];
+ if (offset > value.size()) {
+ result_callback(bluetooth::gatt::Error::kInvalidOffset);
+ return;
+ }
+ result_callback({std::in_place, value.subspan(offset)});
+}
+
+} // namespace pw::bluetooth_profiles
diff --git a/pw_bluetooth_profiles/device_info_service_test.cc b/pw_bluetooth_profiles/device_info_service_test.cc
new file mode 100644
index 0000000..153027e
--- /dev/null
+++ b/pw_bluetooth_profiles/device_info_service_test.cc
@@ -0,0 +1,170 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#include "pw_bluetooth_profiles/device_info_service.h"
+
+#include <string_view>
+
+#include "gtest/gtest.h"
+#include "pw_bluetooth/gatt/server.h"
+
+using namespace std::string_view_literals;
+
+namespace pw::bluetooth_profiles {
+namespace {
+
+class FakeGattServer final : public bluetooth::gatt::Server {
+ public:
+ // Server overrides:
+ void PublishService(
+ const bluetooth::gatt::LocalServiceInfo& info,
+ bluetooth::gatt::LocalServiceDelegate* delegate,
+ Function<void(PublishServiceResult)>&& result_callback) override {
+ ASSERT_EQ(delegate_, nullptr);
+ delegate_ = delegate;
+ ASSERT_EQ(published_info_, nullptr);
+ published_info_ = &info;
+ local_service_.emplace(this);
+ result_callback(
+ PublishServiceResult(std::in_place, &local_service_.value()));
+ }
+
+ // PublishService call argument getters:
+ const bluetooth::gatt::LocalServiceInfo* published_info() const {
+ return published_info_;
+ }
+ bluetooth::gatt::LocalServiceDelegate* delegate() const { return delegate_; }
+
+ private:
+ class FakeLocalService final : public bluetooth::gatt::LocalService {
+ public:
+ explicit FakeLocalService(FakeGattServer* fake_server)
+ : fake_server_(fake_server) {}
+
+ // LocalService overrides:
+ void NotifyValue(const ValueChangedParameters& /* parameters */,
+ Closure&& /* completion_callback */) override {
+ FAIL(); // Unimplemented
+ }
+ void IndicateValue(
+ const ValueChangedParameters& /* parameters */,
+ Function<void(
+ bluetooth::Result<bluetooth::gatt::Error>)>&& /* confirmation */)
+ override {
+ FAIL(); // Unimplemented
+ }
+
+ private:
+ void UnpublishService() override { fake_server_->local_service_.reset(); }
+
+ FakeGattServer* fake_server_;
+ };
+
+ // The LocalServiceInfo passed when PublishService was called.
+ const bluetooth::gatt::LocalServiceInfo* published_info_ = nullptr;
+
+ bluetooth::gatt::LocalServiceDelegate* delegate_ = nullptr;
+
+ std::optional<FakeLocalService> local_service_;
+};
+
+TEST(DeviceInfoServiceTest, PublishAndReadTest) {
+ FakeGattServer fake_server;
+
+ constexpr auto kUsedFields = DeviceInfo::Field::kModelNumber |
+ DeviceInfo::Field::kSerialNumber |
+ DeviceInfo::Field::kSoftwareRevision;
+ DeviceInfo device_info = {};
+ const auto kModelNumber = "model"sv;
+ device_info.model_number = as_bytes(span{kModelNumber});
+ device_info.serial_number = as_bytes(span{"parallel_number"sv});
+ device_info.software_revision = as_bytes(span{"rev123"sv});
+
+ DeviceInfoService<kUsedFields, bluetooth::gatt::Handle{123}>
+ device_info_service(device_info);
+
+ bool called = false;
+ device_info_service.PublishService(
+ &fake_server,
+ [&called](
+ bluetooth::Result<bluetooth::gatt::Server::PublishServiceError> res) {
+ EXPECT_TRUE(res.ok());
+ called = true;
+ });
+ // The FakeGattServer calls the PublishService callback right away so our
+ // callback should have been called already.
+ EXPECT_TRUE(called);
+
+ ASSERT_NE(fake_server.delegate(), nullptr);
+ ASSERT_NE(fake_server.published_info(), nullptr);
+
+ // Test that the published info looks correct.
+ EXPECT_EQ(3u, fake_server.published_info()->characteristics.size());
+
+ // Test that we can read the characteristics.
+ for (auto& characteristic : fake_server.published_info()->characteristics) {
+ bool read_callback_called = false;
+ fake_server.delegate()->ReadValue(
+ bluetooth::PeerId{1234},
+ characteristic.handle,
+ /*offset=*/0,
+ [&read_callback_called](bluetooth::Result<bluetooth::gatt::Error,
+ span<const std::byte>> res) {
+ EXPECT_TRUE(res.ok());
+ EXPECT_NE(0u, res.value().size());
+ read_callback_called = true;
+ });
+ // The DeviceInfoService always calls the callback from within ReadValue().
+ EXPECT_TRUE(read_callback_called);
+ }
+
+ // Check the actual values.
+ // The order of the characteristics in the LocalServiceInfo must be the order
+ // in which the fields are listed in kCharacteristicFields, so the first
+ // characteristic is the Model Number.
+ span<const std::byte> read_value;
+ fake_server.delegate()->ReadValue(
+ bluetooth::PeerId{1234},
+ fake_server.published_info()->characteristics[0].handle,
+ /*offset=*/0,
+ [&read_value](bluetooth::Result<bluetooth::gatt::Error,
+ span<const std::byte>> res) {
+ EXPECT_TRUE(res.ok());
+ read_value = res.value();
+ });
+ EXPECT_EQ(read_value.size(), kModelNumber.size()); // "model" string.
+ // DeviceInfoService keeps references to the values provides in the
+ // DeviceInfo struct, not copies.
+ EXPECT_EQ(read_value.data(),
+ reinterpret_cast<const std::byte*>(kModelNumber.data()));
+
+ // Read with an offset.
+ const size_t kReadOffset = 3;
+ fake_server.delegate()->ReadValue(
+ bluetooth::PeerId{1234},
+ fake_server.published_info()->characteristics[0].handle,
+ kReadOffset,
+ [&read_value](bluetooth::Result<bluetooth::gatt::Error,
+ span<const std::byte>> res) {
+ EXPECT_TRUE(res.ok());
+ read_value = res.value();
+ });
+ EXPECT_EQ(read_value.size(), kModelNumber.size() - kReadOffset);
+ EXPECT_EQ(
+ read_value.data(),
+ reinterpret_cast<const std::byte*>(kModelNumber.data()) + kReadOffset);
+}
+
+} // namespace
+} // namespace pw::bluetooth_profiles
diff --git a/pw_bluetooth_profiles/docs.rst b/pw_bluetooth_profiles/docs.rst
new file mode 100644
index 0000000..a2ffbe7
--- /dev/null
+++ b/pw_bluetooth_profiles/docs.rst
@@ -0,0 +1,76 @@
+.. _module-pw_bluetooth_profiles:
+
+=====================
+pw_bluetooth_profiles
+=====================
+
+.. attention::
+
+ ``pw_bluetooth_profiles`` is under construction, depends on the experimental
+ ``pw_bluetooth`` module and may see significant breaking API changes.
+
+The ``pw_bluetooth_profiles`` module provides a collection of implementations
+for basic Bluetooth profiles built on top of the ``pw_bluetooth`` module API.
+These profiles are independent from each other
+
+--------------------------
+Device Information Service
+--------------------------
+The ``device_info_service`` target implements the Device Information Service
+(DIS) as defined in the specification version 1.1. It exposes up to nine
+different basic properties of the device such as the model, manufacturer or
+serial number, all of which are optional. This module implements the GATT
+server-side service (``bluetooth::gatt::LocalServiceDelegate``) with the
+following limitations:
+
+ - The subset of properties exposed and their values are constant throughout the
+ life of the service.
+ - The subset of properties is defined at compile time, but the values may be
+ defined at runtime before service initialization. For example, the serial
+ number property might be different for different devices running the same
+ code.
+ - All property values must be available in memory while the service is
+ published. Rather than using a callback mechanism to let the user produce the
+ property value at run-time, this module expects those values to be readily
+ available when initialized, but they can be stored in read-only memory.
+
+Usage
+-----
+The main intended usage of the service consists on creating and publishing the
+service, leaving it published forever referencing the values passed on
+initialization.
+
+The subset of properties exposed is a template parameter bit field
+(``DeviceInfo::Field``) and can't be changed at run-time. The ``pw::span``
+values referenced in the ``DeviceInfo`` struct must remain available after
+initialization to avoid copying them to RAM in the service, but the
+``DeviceInfo`` struct itself can be destroyed after initialization.
+
+Example code:
+
+.. code-block:: cpp
+
+ using pw::bluetooth_profiles::DeviceInfo;
+ using pw::bluetooth_profiles::DeviceInfoService;
+
+ // Global serial number for the device, initialized elsewhere.
+ pw::InlineString serial_number(...);
+
+ // Select which fields to expose at compile-time with a constexpr template
+ // parameter.
+ constexpr auto kUsedFields = DeviceInfo::Field::kModelNumber |
+ DeviceInfo::Field::kSerialNumber |
+ DeviceInfo::Field::kSoftwareRevision;
+
+ // Create a DeviceInfo with the values. Values are referenced from the
+ // service, not copied, so they must remain available while the service is
+ // published.
+ DeviceInfo device_info = {};
+ device_info.model_number = pw::as_bytes(pw::span{"My Model"sv});
+ device_info.software_revision = pw::as_bytes(pw::span{REVISION_MACRO});
+ device_info.serial_number = pw::as_bytes(
+ pw::span(serial_number.data(), serial_number.size()));
+
+ DeviceInfoService<kUsedFields, pw::bluetooth::gatt::Handle{123}>
+ device_info_service{device_info};
+ device_info_service.PublishService(...);
diff --git a/pw_bluetooth_profiles/public/pw_bluetooth_profiles/device_info_service.h b/pw_bluetooth_profiles/public/pw_bluetooth_profiles/device_info_service.h
new file mode 100644
index 0000000..ca4636b
--- /dev/null
+++ b/pw_bluetooth_profiles/public/pw_bluetooth_profiles/device_info_service.h
@@ -0,0 +1,291 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+#include "pw_bluetooth/assigned_uuids.h"
+#include "pw_bluetooth/gatt/error.h"
+#include "pw_bluetooth/gatt/server.h"
+#include "pw_bluetooth/gatt/types.h"
+#include "pw_span/span.h"
+
+namespace pw::bluetooth_profiles {
+
+// Device information to be exposed by the Device Information Service, according
+// to the DIS spec 1.1. All fields are optional.
+struct DeviceInfo {
+ // Bitmask of the fields present in the DeviceInfoService, each one
+ // corresponding to one of the possible characteristics in the Device
+ // Information Service.
+ enum class Field : uint16_t {
+ kManufacturerName = 1u << 0,
+ kModelNumber = 1u << 1,
+ kSerialNumber = 1u << 2,
+ kHardwareRevision = 1u << 3,
+ kFirmwareRevision = 1u << 4,
+ kSoftwareRevision = 1u << 5,
+ kSystemId = 1u << 6,
+ kRegulatoryInformation = 1u << 7,
+ kPnpId = 1u << 8,
+ };
+
+ // Manufacturer Name String
+ span<const std::byte> manufacturer_name;
+
+ // Model Number String
+ span<const std::byte> model_number;
+
+ // Serial Number String
+ span<const std::byte> serial_number;
+
+ // Hardware Revision String
+ span<const std::byte> hardware_revision;
+
+ // Firmware Revision String
+ span<const std::byte> firmware_revision;
+
+ // Software Revision String
+ span<const std::byte> software_revision;
+
+ // System ID
+ span<const std::byte> system_id;
+
+ // IEEE 11073-20601 Regulatory Certification Data List
+ span<const std::byte> regulatory_information;
+
+ // PnP ID
+ span<const std::byte> pnp_id;
+};
+
+// Helper operator| to allow combining multiple DeviceInfo::Field values.
+static inline constexpr DeviceInfo::Field operator|(DeviceInfo::Field left,
+ DeviceInfo::Field right) {
+ return static_cast<DeviceInfo::Field>(static_cast<uint16_t>(left) |
+ static_cast<uint16_t>(right));
+}
+
+static inline constexpr bool operator&(DeviceInfo::Field left,
+ DeviceInfo::Field right) {
+ return (static_cast<uint16_t>(left) & static_cast<uint16_t>(right)) != 0;
+}
+
+// Shared implementation of the DeviceInfoService<> template class of elements
+// that don't depend on the template parameters.
+class DeviceInfoServiceImpl {
+ public:
+ DeviceInfoServiceImpl(const bluetooth::gatt::LocalServiceInfo& service_info,
+ span<const span<const std::byte>> values)
+ : service_info_(service_info), delegate_(values) {}
+
+ // Publish this service on the passed gatt::Server. The service may be
+ // published only on one Server at a time.
+ void PublishService(
+ bluetooth::gatt::Server* gatt_server,
+ Callback<
+ void(bluetooth::Result<bluetooth::gatt::Server::PublishServiceError>)>
+ result_callback);
+
+ protected:
+ using GattCharacteristicUuid = bluetooth::GattCharacteristicUuid;
+
+ // A struct for describing each one of the optional characteristics available.
+ struct FieldDescriptor {
+ DeviceInfo::Field field_value;
+ span<const std::byte> DeviceInfo::*field_pointer;
+ bluetooth::Uuid characteristic_type;
+ };
+
+ // List of all the fields / characteristics available in the DIS, mapping the
+ // characteristic UUID type to the corresponding field in the DeviceInfo
+ // struct.
+ static constexpr size_t kNumFields = 9;
+ static constexpr std::array<FieldDescriptor, kNumFields>
+ kCharacteristicFields = {{
+ {DeviceInfo::Field::kManufacturerName,
+ &DeviceInfo::manufacturer_name,
+ GattCharacteristicUuid::kManufacturerNameString},
+ {DeviceInfo::Field::kModelNumber,
+ &DeviceInfo::model_number,
+ GattCharacteristicUuid::kModelNumberString},
+ {DeviceInfo::Field::kSerialNumber,
+ &DeviceInfo::serial_number,
+ GattCharacteristicUuid::kSerialNumberString},
+ {DeviceInfo::Field::kHardwareRevision,
+ &DeviceInfo::hardware_revision,
+ GattCharacteristicUuid::kHardwareRevisionString},
+ {DeviceInfo::Field::kFirmwareRevision,
+ &DeviceInfo::firmware_revision,
+ GattCharacteristicUuid::kFirmwareRevisionString},
+ {DeviceInfo::Field::kSoftwareRevision,
+ &DeviceInfo::software_revision,
+ GattCharacteristicUuid::kSoftwareRevisionString},
+ {DeviceInfo::Field::kSystemId,
+ &DeviceInfo::system_id,
+ GattCharacteristicUuid::kSystemId},
+ {DeviceInfo::Field::kRegulatoryInformation,
+ &DeviceInfo::regulatory_information,
+ GattCharacteristicUuid::
+ kIeee1107320601RegulatoryCertificationDataList},
+ {DeviceInfo::Field::kPnpId,
+ &DeviceInfo::pnp_id,
+ GattCharacteristicUuid::kPnpId},
+ }};
+
+ private:
+ class Delegate : public bluetooth::gatt::LocalServiceDelegate {
+ public:
+ explicit Delegate(span<const span<const std::byte>> values)
+ : values_(values) {}
+ ~Delegate() override = default;
+
+ void SetServicePtr(bluetooth::gatt::LocalService::Ptr service) {
+ local_service_ = std::move(service);
+ }
+
+ // LocalServiceDelegate overrides
+ void OnError(bluetooth::gatt::Error error) override;
+
+ void ReadValue(bluetooth::PeerId peer_id,
+ bluetooth::gatt::Handle handle,
+ uint32_t offset,
+ Function<void(bluetooth::Result<bluetooth::gatt::Error,
+ span<const std::byte>>)>&&
+ result_callback) override;
+
+ void WriteValue(bluetooth::PeerId /* peer_id */,
+ bluetooth::gatt::Handle /* handle */,
+ uint32_t /* offset */,
+ span<const std::byte> /* value */,
+ Function<void(bluetooth::Result<bluetooth::gatt::Error>)>&&
+ status_callback) override {
+ status_callback(bluetooth::gatt::Error::kUnlikelyError);
+ }
+
+ void CharacteristicConfiguration(bluetooth::PeerId /* peer_id */,
+ bluetooth::gatt::Handle /* handle */,
+ bool /* notify */,
+ bool /* indicate */) override {
+ // No indications or notifications are supported by this service.
+ }
+
+ void MtuUpdate(bluetooth::PeerId /* peer_id*/,
+ uint16_t /* mtu */) override {
+ // MTU is ignored.
+ }
+
+ private:
+ // LocalService smart pointer returned by the pw_bluetooth API once the
+ // service is published. This field is unused since we don't generate any
+ // Notification or Indication, but deleting this object unpublishes the
+ // service.
+ bluetooth::gatt::LocalService::Ptr local_service_;
+
+ // Device information values for the service_info_ characteristics. The
+ // characteristic Handle is the index into the values_ span.
+ span<const span<const std::byte>> values_;
+ };
+
+ // GATT service info.
+ const bluetooth::gatt::LocalServiceInfo& service_info_;
+
+ // Callback to be called after the service is published.
+ Callback<void(
+ bluetooth::Result<bluetooth::gatt::Server::PublishServiceError>)>
+ publish_service_callback_;
+
+ // The LocalServiceDelegate implementation.
+ Delegate delegate_;
+};
+
+// Device Information Service exposing only the subset of characteristics
+// specified by the bitmask kPresentFields.
+template <DeviceInfo::Field kPresentFields,
+ bluetooth::gatt::Handle kServiceHandle>
+class DeviceInfoService : public DeviceInfoServiceImpl {
+ public:
+ // Handle used to reference this service from other services.
+ static constexpr bluetooth::gatt::Handle kHandle = kServiceHandle;
+
+ // Construct a DeviceInfoService exposing the values provided in the
+ // `device_info` for the subset of characteristics selected by kPresentFields.
+ // DeviceInfo fields for characteristics not selected by kPresentFields are
+ // ignored. The `device_info` reference doesn't need to be kept alive after
+ // the constructor returns, however the data pointed to by the various fields
+ // in `device_info` must be kept available while the service is published.
+ explicit constexpr DeviceInfoService(const DeviceInfo& device_info)
+ : DeviceInfoServiceImpl(kServiceInfo, span{values_}) {
+ size_t count = 0;
+ // Get the device information only for the fields we care about.
+ for (const auto& field : kCharacteristicFields) {
+ if (field.field_value & kPresentFields) {
+ values_[count] = device_info.*(field.field_pointer);
+ count++;
+ }
+ }
+ }
+
+ private:
+ // Return the total number of selected characteristics on this service.
+ static constexpr size_t NumCharacteristics() {
+ size_t ret = 0;
+ for (const auto& field : kCharacteristicFields) {
+ if (field.field_value & kPresentFields) {
+ ret++;
+ }
+ }
+ return ret;
+ }
+ static constexpr size_t kNumCharacteristics = NumCharacteristics();
+
+ // Factory method to build the list of characteristics needed for a given
+ // subset of fields.
+ static constexpr std::array<bluetooth::gatt::Characteristic,
+ kNumCharacteristics>
+ BuildServiceInfoCharacteristics() {
+ std::array<bluetooth::gatt::Characteristic, kNumCharacteristics> ret = {};
+ size_t count = 0;
+ for (const auto& field : kCharacteristicFields) {
+ if (field.field_value & kPresentFields) {
+ ret[count] = bluetooth::gatt::Characteristic{
+ .handle = bluetooth::gatt::Handle(count),
+ .type = field.characteristic_type,
+ .properties = bluetooth::gatt::CharacteristicPropertyBits::kRead,
+ .permissions = bluetooth::gatt::AttributePermissions{},
+ .descriptors = {},
+ };
+ count++;
+ }
+ }
+ return ret;
+ }
+ // Static constexpr array of characteristics for the current subset of fields
+ // kPresentFields.
+ static constexpr auto kServiceInfoCharacteristics =
+ BuildServiceInfoCharacteristics();
+
+ // GATT Service information.
+ static constexpr auto kServiceInfo = bluetooth::gatt::LocalServiceInfo{
+ .handle = kServiceHandle,
+ .primary = true,
+ .type = bluetooth::GattServiceUuid::kDeviceInformation,
+ .characteristics = kServiceInfoCharacteristics,
+ .includes = {},
+ };
+
+ // Storage std::array for the span<const std::byte> of values.
+ std::array<span<const std::byte>, kNumCharacteristics> values_;
+};
+
+} // namespace pw::bluetooth_profiles
diff --git a/pw_build/generated_pigweed_modules_lists.gni b/pw_build/generated_pigweed_modules_lists.gni
index 2550de0..188b55b 100644
--- a/pw_build/generated_pigweed_modules_lists.gni
+++ b/pw_build/generated_pigweed_modules_lists.gni
@@ -43,6 +43,8 @@
dir_pw_blob_store = get_path_info("../pw_blob_store", "abspath")
dir_pw_bluetooth = get_path_info("../pw_bluetooth", "abspath")
dir_pw_bluetooth_hci = get_path_info("../pw_bluetooth_hci", "abspath")
+ dir_pw_bluetooth_profiles =
+ get_path_info("../pw_bluetooth_profiles", "abspath")
dir_pw_boot = get_path_info("../pw_boot", "abspath")
dir_pw_boot_cortex_m = get_path_info("../pw_boot_cortex_m", "abspath")
dir_pw_build = get_path_info("../pw_build", "abspath")
@@ -188,6 +190,7 @@
dir_pw_blob_store,
dir_pw_bluetooth,
dir_pw_bluetooth_hci,
+ dir_pw_bluetooth_profiles,
dir_pw_boot,
dir_pw_boot_cortex_m,
dir_pw_build,
@@ -321,6 +324,7 @@
"$dir_pw_blob_store:tests",
"$dir_pw_bluetooth:tests",
"$dir_pw_bluetooth_hci:tests",
+ "$dir_pw_bluetooth_profiles:tests",
"$dir_pw_boot:tests",
"$dir_pw_boot_cortex_m:tests",
"$dir_pw_build:tests",
@@ -454,6 +458,7 @@
"$dir_pw_blob_store:docs",
"$dir_pw_bluetooth:docs",
"$dir_pw_bluetooth_hci:docs",
+ "$dir_pw_bluetooth_profiles:docs",
"$dir_pw_boot:docs",
"$dir_pw_boot_cortex_m:docs",
"$dir_pw_build:docs",