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",