pw_bluetooth: Create pw_bluetooth API

Create the pw_bluetooth module.

Create the Bluetooth Low Energy C++ API in the pw_bluetooth module.
This API will be used to wrap various BLE implementations.

Bug: 636
Change-Id: I329c8e0f307d30a26581a43cb46cf2d3dbd35da0
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/84180
Pigweed-Auto-Submit: Ben Lawson <benlawson@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Ben Lawson <benlawson@google.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 061cc54..b41d6f3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -26,6 +26,7 @@
 add_subdirectory(pw_assert_zephyr EXCLUDE_FROM_ALL)
 add_subdirectory(pw_base64 EXCLUDE_FROM_ALL)
 add_subdirectory(pw_blob_store EXCLUDE_FROM_ALL)
+add_subdirectory(pw_bluetooth EXCLUDE_FROM_ALL)
 add_subdirectory(pw_build EXCLUDE_FROM_ALL)
 add_subdirectory(pw_build_info EXCLUDE_FROM_ALL)
 add_subdirectory(pw_bytes EXCLUDE_FROM_ALL)
diff --git a/PIGWEED_MODULES b/PIGWEED_MODULES
index dfb7f27..7bf9e03 100644
--- a/PIGWEED_MODULES
+++ b/PIGWEED_MODULES
@@ -11,6 +11,7 @@
 pw_base64
 pw_bloat
 pw_blob_store
+pw_bluetooth
 pw_bluetooth_hci
 pw_boot
 pw_boot_cortex_m
diff --git a/pw_bluetooth/BUILD.bazel b/pw_bluetooth/BUILD.bazel
new file mode 100644
index 0000000..e1623dc
--- /dev/null
+++ b/pw_bluetooth/BUILD.bazel
@@ -0,0 +1,74 @@
+# 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.
+
+load(
+    "//pw_build:pigweed.bzl",
+    "pw_cc_library",
+    "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+    name = "pw_bluetooth",
+    hdrs = [
+        "public/pw_bluetooth/constants.h",
+        "public/pw_bluetooth/gatt/client.h",
+        "public/pw_bluetooth/gatt/constants.h",
+        "public/pw_bluetooth/gatt/error.h",
+        "public/pw_bluetooth/gatt/server.h",
+        "public/pw_bluetooth/gatt/types.h",
+        "public/pw_bluetooth/hci.h",
+        "public/pw_bluetooth/host.h",
+        "public/pw_bluetooth/low_energy/advertising_data.h",
+        "public/pw_bluetooth/low_energy/bond_data.h",
+        "public/pw_bluetooth/low_energy/central.h",
+        "public/pw_bluetooth/low_energy/connection.h",
+        "public/pw_bluetooth/low_energy/peripheral.h",
+        "public/pw_bluetooth/low_energy/security_mode.h",
+        "public/pw_bluetooth/pairing_delegate.h",
+        "public/pw_bluetooth/peer.h",
+        "public/pw_bluetooth/result.h",
+        "public/pw_bluetooth/types.h",
+    ],
+    includes = ["public"],
+    deps = [
+        "//pw_chrono:system_clock",
+        "//pw_containers",
+        "//pw_function",
+        "//pw_status",
+    ],
+)
+
+pw_cc_test(
+    name = "api_test",
+    srcs = [
+        "api_test.cc",
+    ],
+    deps = [
+        ":pw_bluetooth",
+    ],
+)
+
+pw_cc_test(
+    name = "result_test",
+    srcs = [
+        "result_test.cc",
+    ],
+    deps = [
+        "pw_bluetooth",
+    ],
+)
diff --git a/pw_bluetooth/BUILD.gn b/pw_bluetooth/BUILD.gn
new file mode 100644
index 0000000..c6ceb1c
--- /dev/null
+++ b/pw_bluetooth/BUILD.gn
@@ -0,0 +1,77 @@
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.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 = [ ":*" ]
+}
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
+
+pw_source_set("pw_bluetooth") {
+  public_configs = [ ":public_include_path" ]
+  public = [
+    "public/pw_bluetooth/constants.h",
+    "public/pw_bluetooth/gatt/client.h",
+    "public/pw_bluetooth/gatt/constants.h",
+    "public/pw_bluetooth/gatt/error.h",
+    "public/pw_bluetooth/gatt/server.h",
+    "public/pw_bluetooth/gatt/types.h",
+    "public/pw_bluetooth/hci.h",
+    "public/pw_bluetooth/host.h",
+    "public/pw_bluetooth/low_energy/advertising_data.h",
+    "public/pw_bluetooth/low_energy/bond_data.h",
+    "public/pw_bluetooth/low_energy/central.h",
+    "public/pw_bluetooth/low_energy/connection.h",
+    "public/pw_bluetooth/low_energy/peripheral.h",
+    "public/pw_bluetooth/low_energy/security_mode.h",
+    "public/pw_bluetooth/pairing_delegate.h",
+    "public/pw_bluetooth/peer.h",
+    "public/pw_bluetooth/result.h",
+    "public/pw_bluetooth/types.h",
+  ]
+  public_deps = [
+    "$dir_pw_chrono:system_clock",
+    dir_pw_containers,
+    dir_pw_function,
+    dir_pw_status,
+  ]
+}
+
+pw_test_group("tests") {
+  enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+  tests = [
+    ":api_test",
+    ":result_test",
+  ]
+}
+
+pw_test("api_test") {
+  sources = [ "api_test.cc" ]
+  deps = [ ":pw_bluetooth" ]
+}
+
+pw_test("result_test") {
+  sources = [ "result_test.cc" ]
+  deps = [ ":pw_bluetooth" ]
+}
diff --git a/pw_bluetooth/CMakeLists.txt b/pw_bluetooth/CMakeLists.txt
new file mode 100644
index 0000000..5d25a6b
--- /dev/null
+++ b/pw_bluetooth/CMakeLists.txt
@@ -0,0 +1,62 @@
+# 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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_module_library(pw_bluetooth
+  HEADERS
+    public/pw_bluetooth/gatt/client.h
+    public/pw_bluetooth/gatt/constants.h
+    public/pw_bluetooth/gatt/error.h
+    public/pw_bluetooth/gatt/server.h
+    public/pw_bluetooth/gatt/types.h
+    public/pw_bluetooth/low_energy/advertising_data.h
+    public/pw_bluetooth/low_energy/bond_data.h
+    public/pw_bluetooth/low_energy/central.h
+    public/pw_bluetooth/low_energy/connection.h
+    public/pw_bluetooth/low_energy/peripheral.h
+    public/pw_bluetooth/low_energy/security_mode.h
+    public/pw_bluetooth/constants.h
+    public/pw_bluetooth/hci.h
+    public/pw_bluetooth/host.h
+    public/pw_bluetooth/pairing_delegate.h
+    public/pw_bluetooth/peer.h
+    public/pw_bluetooth/result.h
+    public/pw_bluetooth/types.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_containers
+    pw_function
+    pw_status
+    pw_chrono.system_clock
+)
+
+pw_add_test(pw_bluetooth.api_test
+  SOURCES
+    api_test.cc
+  DEPS
+    pw_bluetooth
+  GROUPS
+    pw_bluetooth
+)
+
+pw_add_test(pw_bluetooth.result_test
+  SOURCES
+    result_test.cc
+  DEPS
+    pw_bluetooth
+  GROUPS
+    pw_bluetooth
+)
\ No newline at end of file
diff --git a/pw_bluetooth/README.md b/pw_bluetooth/README.md
new file mode 100644
index 0000000..f1526b2
--- /dev/null
+++ b/pw_bluetooth/README.md
@@ -0,0 +1 @@
+This directory contains Bluetooth Low Energy APIs.
diff --git a/pw_bluetooth/api_test.cc b/pw_bluetooth/api_test.cc
new file mode 100644
index 0000000..4fdd04b
--- /dev/null
+++ b/pw_bluetooth/api_test.cc
@@ -0,0 +1,25 @@
+// 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 "gtest/gtest.h"
+#include "pw_bluetooth/host.h"
+
+namespace pw::bluetooth {
+namespace {
+
+// This empty test ensures that the included API compiles.
+TEST(ApiTest, ApiCompiles) {}
+
+}  // namespace
+}  // namespace pw::bluetooth
\ No newline at end of file
diff --git a/pw_bluetooth/docs.rst b/pw_bluetooth/docs.rst
new file mode 100644
index 0000000..b7cf1a5
--- /dev/null
+++ b/pw_bluetooth/docs.rst
@@ -0,0 +1,18 @@
+.. _module-pw_bluetooth:
+
+================
+pw_bluetooth
+================
+The ``pw_bluetooth`` module contains APIs for the host layer of Bluetooth Low
+Energy. The APIs are a collection of virtual interfaces that must implemented by
+a BLE host stack.
+
+.. note::
+  This module is still under construction, the API is not yet stable.
+
+Callbacks
+===========
+This module contains callback-heavy APIs. Callbacks must not call into the
+``pw_bluetooth`` APIs unless otherwise noted. This includes calls made by
+destroying objects returned by the API. Additionally, callbacks must not block.
+
diff --git a/pw_bluetooth/public/pw_bluetooth/constants.h b/pw_bluetooth/public/pw_bluetooth/constants.h
new file mode 100644
index 0000000..549dff4
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/constants.h
@@ -0,0 +1,22 @@
+// 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
+
+namespace pw::bluetooth {
+
+// The maximum length of a device name. This value was selected based on the HCI
+// and GAP specifications (v5.2, Vol 4, Part E, 7.3.11 and Vol 3, Part C, 12.1).
+constexpr uint8_t kMaxDeviceNameLength = 248;
+
+}  // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/client.h b/pw_bluetooth/public/pw_bluetooth/gatt/client.h
new file mode 100644
index 0000000..4ebf649
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/client.h
@@ -0,0 +1,285 @@
+// 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 <memory>
+#include <span>
+
+#include "pw_bluetooth/gatt/constants.h"
+#include "pw_bluetooth/gatt/error.h"
+#include "pw_bluetooth/gatt/types.h"
+#include "pw_bluetooth/result.h"
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+#include "pw_function/function.h"
+#include "pw_result/result.h"
+
+namespace pw::bluetooth::gatt {
+
+// Represents a GATT service on a remote GATT server.
+// Clients should call `SetErrorCallback` before using in order to handle fatal
+// errors.
+class RemoteService {
+ public:
+  enum class RemoteServiceError {
+    // The service has been modified or removed.
+    kServiceRemoved = 0,
+
+    // The peer serving this service has disconnected.
+    kPeerDisconnected = 1,
+  };
+
+  // Wrapper around a possible truncated value received from the server.
+  struct ReadValue {
+    // Characteristic or descriptor handle.
+    Handle handle;
+
+    // The value of the characteristic or descriptor.
+    Vector<std::byte> value;
+
+    // True if `value` might be truncated (the buffer was completely filled by
+    // the server and the read was a short read).  `ReadCharacteristic` or
+    // `ReadDescriptor` should be used to read the complete value.
+    bool maybe_truncated;
+  };
+
+  // A result returned by `ReadByType`.
+  struct ReadByTypeResult {
+    // Characteristic or descriptor handle.
+    Handle handle;
+
+    // The value of the characteristic or descriptor, if it was read
+    // successfully, or an error explaining why the value could not be read.
+    Result<Error, ReadValue> result;
+  };
+
+  // Represents the supported options to read a long characteristic or
+  // descriptor value from a server. Long values are those that may not fit in a
+  // single message (longer than 22 bytes).
+  struct LongReadOptions {
+    // The byte to start the read at. Must be less than the length of the
+    // value.
+    uint16_t offset = 0;
+
+    // The maximum number of bytes to read.
+    uint16_t max_bytes = kMaxValueLength;
+  };
+
+  // Represents the supported write modes for writing characteristics &
+  // descriptors to the server.
+  enum class WriteMode : uint8_t {
+    // Wait for a response from the server before returning but do not verify
+    // the echo response. Supported for both characteristics and descriptors.
+    kDefault = 0,
+
+    // Every value blob is verified against an echo response from the server.
+    // The procedure is aborted if a value blob has not been reliably delivered
+    // to the peer. Only supported for characteristics.
+    kReliable = 1,
+
+    // Delivery will not be confirmed before returning. Writing without a
+    // response is only supported for short characteristics with the
+    // `WRITE_WITHOUT_RESPONSE` property. The value must fit into a single
+    // message. It is guaranteed that at least 20 bytes will fit into a single
+    // message. If the value does not fit, a `kFailure` error will be produced.
+    // The value will be written at offset 0. Only supported for
+    // characteristics.
+    kWithoutResponse = 2,
+  };
+
+  // Represents the supported options to write a characteristic/descriptor value
+  // to a server.
+  struct WriteOptions {
+    // The mode of the write operation. For descriptors, only
+    // `WriteMode::kDefault` is supported
+    WriteMode mode = WriteMode::kDefault;
+
+    // Request a write starting at the byte indicated.
+    // Must be 0 if `mode` is `WriteMode.kWithoutResponse`.
+    uint16_t offset = 0;
+  };
+
+  using ReadByTypeCallback = Function<void(Result<Vector<ReadByTypeResult>>)>;
+  using ReadCallback = Function<void(Result<ReadValue>)>;
+  using NotificationCallback = Function<void(ReadValue)>;
+
+  // Set a callback that will be called when there is an error with this
+  // RemoteService, after which this RemoteService will be invalid.
+  void SetErrorCallback(Function<void(RemoteServiceError)> error_callback);
+
+  // Calls `characteristic_callback` with the characteristics and descriptors in
+  // this service.
+  void DiscoverCharacteristics(
+      Function<void(Characteristic)>&& characteristic_callback);
+
+  // Reads characteristics and descriptors with the specified type. This method
+  // is useful for reading values before discovery has completed, thereby
+  // reducing latency.
+  // `uuid` - The UUID of the characteristics/descriptors to read.
+  // `result_callback` - Results are returned via this callback. Results may be
+  //   empty if no matching values are read. If reading a value results in a
+  //   permission error, the handle and error will be included.
+  //
+  // This may fail with the following errors:
+  // kInvalidParameters: if `uuid` refers to an internally reserved descriptor
+  //     type (e.g. the Client Characteristic Configuration descriptor).
+  // kTooManyResults: More results were read than can fit
+  //    in a Vector. Consider reading characteristics/descriptors individually
+  //    after performing discovery.
+  // kFailure: The server returned an error not specific to a single result.
+  void ReadByType(Uuid uuid, ReadByTypeCallback&& result_callback);
+
+  // Reads the value of a characteristic.
+  // `handle` - The handle of the characteristic to be read.
+  // `options` - If null, a short read will be performed, which may be truncated
+  //     to what fits in a single message (at least 22 bytes). If long read
+  //     options are present, performs a long read with the indicated options.
+  // `result_callback` - called with the result of the read and the value of the
+  //     characteristic if successful.
+  //
+  // This may fail with the following errors:
+  // kInvalidHandle - if `handle` is invalid
+  // kInvalidParameters - if `options is invalid
+  // kReadNotPermitted or kInsufficient* if the server rejects the request.
+  // kFailure if the server returns an error not covered by the above errors.
+  void ReadCharacteristic(Handle handle,
+                          std::optional<LongReadOptions> options,
+                          ReadCallback&& result_callback);
+
+  // Writes `value` to the characteristic with `handle` using the provided
+  // `options`.  It is not recommended to send additional writes while a write
+  // is already in progress (the server may receive simultaneous writes in any
+  // order).
+  //
+  // Parameters:
+  // `handle` - Handle of the characteristic to be written to
+  // `value` - The value to be written.
+  // `options` - Options that apply to the write.
+  //
+  // This may fail with the following errors:
+  // kInvalidHandle - if `handle` is invalid
+  // kInvalidParameters - if `options is invalid
+  // kWriteNotPermitted or kInsufficient* if the server rejects the request.
+  // kFailure if the server returns an error not covered by the above errors.
+  void WriteCharacteristic(Handle handle,
+                           std::span<const std::byte> value,
+                           WriteOptions options,
+                           Function<void(Result<Error>)>&& result_callback);
+
+  // Reads the value of the characteristic descriptor with `handle` and
+  // returns it in the reply.
+  // `handle` - The descriptor handle to read.
+  // `options` - Options that apply to the read.
+  // `result_callback` - Returns a result containing the value of the descriptor
+  //     on success.
+  //
+  // This may fail with the following errors:
+  // `kInvalidHandle` - if `handle` is invalid.
+  // `kInvalidParameters` - if `options` is invalid.
+  // `kReadNotPermitted` or `INSUFFICIENT_*` - if the server rejects the read
+  // request. `kFailure` - if the server returns an error.
+  void ReadDescriptor(Handle handle,
+                      std::optional<LongReadOptions> options,
+                      ReadCallback&& result_callback);
+
+  void WriteDescriptor(Handle handle,
+                       std::span<const std::byte> value,
+                       WriteOptions options,
+                       Function<void(Result<Error>)>&& result_callback);
+
+  // Subscribe to notifications & indications from the characteristic with
+  // the given `handle`.
+  //
+  // Either notifications or indications will be enabled depending on
+  // characteristic properties. Indications will be preferred if they are
+  // supported. This operation fails if the characteristic does not have the
+  // "notify" or "indicate" property.
+  //
+  // A write request will be issued to configure the characteristic for
+  // notifications/indications if it contains a Client Characteristic
+  // Configuration descriptor. This method fails if an error occurs while
+  // writing to the descriptor.
+  //
+  // On success, `notification_callback` will be called when
+  // the peer sends a notification or indication. Indications are
+  // automatically confirmed.
+  //
+  // Subscriptions can be canceled with `StopNotifications`.
+  //
+  // Parameters:
+  // `handle` - the handle of the characteristic to subscribe to.
+  // `notification_callback` - will be called with the values of
+  //     notifications/indications when received.
+  // `result_callback` - called with the result of enabling
+  //     notifications/indications.
+  //
+  // This may fail with the following errors:
+  // `kFailure` - the characteristic does not support notifications or
+  //     indications.
+  // `kInvalidHandle` - `handle` is invalid.
+  // `kWriteNotPermitted`or `kInsufficient*` - descriptor write error.
+  void RegisterNotificationCallback(
+      Handle handle,
+      NotificationCallback&& notification_callback,
+      Function<void(Result<Error>)> result_callback);
+
+  // Stops notifications for the characteristic with the given `handle`.
+  void StopNotifications(Handle handle);
+};
+
+// Represents a GATT client that interacts with services on a GATT server.
+class Client {
+ public:
+  // Represents a remote GATT service.
+  struct RemoteServiceInfo {
+    // Uniquely identifies this GATT service.
+    Handle handle;
+
+    // Indicates whether this is a primary or secondary service.
+    bool primary;
+
+    // The UUID that identifies the type of this service.
+    // There may be multiple services with the same UUID.
+    Uuid type;
+  };
+
+  virtual ~Client() = default;
+
+  // Enumerates existing services found on the peer that this Client represents,
+  // and provides a stream of updates thereafter. Results can be filtered by
+  // specifying a list of UUIDs in `uuids`. To further interact with services,
+  // clients must obtain a RemoteService protocol by calling ConnectToService().
+  // `uuid_allowlist` - The allowlist of UUIDs to filter services with.
+  // `updated_callback` - Will be called with services that are
+  //     updated/modified.
+  // `removed_callback` - Called with the handles of servies
+  //     that have been removed. Note that handles may be reused.
+  virtual void WatchServices(
+      Vector<Uuid> uuid_allowlist,
+      Function<void(RemoteServiceInfo)>&& updated_callback,
+      Function<void(Handle)>&& removed_callback) = 0;
+
+  // Stops service watching if started by `WatchServices`.
+  virtual void StopWatchingServices();
+
+  // Connects to a RemoteService. Only 1 connection per service is allowed.
+  // `handle` - the handle of the service to connect to.
+  //
+  // This may fail with the following errors:
+  // kInvalidParameters - `handle` does not correspond to a known service.
+  virtual Result<Error, std::unique_ptr<RemoteService>> ConnectToService(
+      Handle handle) = 0;
+};
+
+}  // namespace pw::bluetooth::gatt
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/constants.h b/pw_bluetooth/public/pw_bluetooth/gatt/constants.h
new file mode 100644
index 0000000..7e25501
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/constants.h
@@ -0,0 +1,23 @@
+// 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>
+
+namespace pw::bluetooth::gatt {
+
+// The maximum length of an attribute value.
+constexpr uint16_t kMaxValueLength = 512;
+
+}  // namespace pw::bluetooth::gatt
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/error.h b/pw_bluetooth/public/pw_bluetooth/gatt/error.h
new file mode 100644
index 0000000..3bc9222
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/error.h
@@ -0,0 +1,136 @@
+// 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>
+
+namespace pw::bluetooth::gatt {
+
+/// The values correspond with those in Bluetooth 5.2 Vol. 3 Part G Table 3.4,
+/// and Supplement to the Bluetooth Core Specification v9 Part B Table 1.1,
+/// but this is for ease of reference only.  Clients should *not* rely on these
+/// values remaining constant.  Omitted values from the spec are handled
+/// internally and will not be returned to clients.
+enum class Error : uint16_t {
+  // ATT Errors
+
+  /// The attribute indicated by the handle is invalid. It may have been
+  /// removed.
+  ///
+  /// This may be returned by a LocalService method.
+  kInvalidHandle = 0x1,
+
+  /// This attribute is not readable.
+  kReadNotPermitted = 0x2,
+
+  /// This attribute is not writable.
+  kWriteNotPermitted = 0x3,
+
+  /// Indicates that the response received from the server was invalid.
+  kInvalidPdu = 0x4,
+
+  /// This attribute requires authentication, but the client is not
+  /// authenticated.
+  kInsufficientAuthentication = 0x5,
+
+  /// Indicates that the offset used in a read or write request exceeds the
+  /// bounds of the value.
+  kInvalidOffset = 0x7,
+
+  /// This attribute requires authorization, but the client is not authorized.
+  kInsufficientAuthorization = 0x8,
+
+  /// This attribute requires a connection encrypted by a larger encryption key.
+  kInsufficientEncryptionKeySize = 0xC,
+
+  /// Indicates that the value given in a write request would exceed the maximum
+  /// length allowed for the destionation characteristic or descriptor.
+  kInvalidAttributeValueLength = 0xD,
+
+  /// A general error occurred that can not be classified as one of the more
+  /// specific errors.
+  kUnlikelyError = 0xE,
+
+  /// This attribute requires encryption, but the connection is not encrypted.
+  kInsufficientEncryption = 0xF,
+
+  /// The server had insufficient resources to complete the task.
+  kInsufficientResources = 0x11,
+
+  /// The value was not allowed.
+  kValueNotAllowed = 0x13,
+
+  // Application Errors
+
+  /// Application Errors.  The uses of these are specified at the application
+  /// level.
+  kApplicationError80 = 0x80,
+  kApplicationError81 = 0x81,
+  kApplicationError82 = 0x82,
+  kApplicationError83 = 0x83,
+  kApplicationError84 = 0x84,
+  kApplicationError85 = 0x85,
+  kApplicationError86 = 0x86,
+  kApplicationError87 = 0x87,
+  kApplicationError88 = 0x88,
+  kApplicationError89 = 0x89,
+  kApplicationError8A = 0x8A,
+  kApplicationError8B = 0x8B,
+  kApplicationError8C = 0x8C,
+  kApplicationError8D = 0x8D,
+  kApplicationError8E = 0x8E,
+  kApplicationError8F = 0x8F,
+  kApplicationError90 = 0x90,
+  kApplicationError91 = 0x91,
+  kApplicationError92 = 0x92,
+  kApplicationError93 = 0x93,
+  kApplicationError94 = 0x94,
+  kApplicationError95 = 0x95,
+  kApplicationError96 = 0x96,
+  kApplicationError97 = 0x97,
+  kApplicationError98 = 0x98,
+  kApplicationError99 = 0x99,
+  kApplicationError9A = 0x9A,
+  kApplicationError9B = 0x9B,
+  kApplicationError9C = 0x9C,
+  kApplicationError9D = 0x9D,
+  kApplicationError9E = 0x9E,
+  kApplicationError9F = 0x9F,
+
+  // Common Profile and Service Error Codes
+
+  /// Write request was rejected at the profile or service level.
+  kWriteRequestRejected = 0xFC,
+
+  /// The Client Characteristic Configuration Descriptor was improperly
+  /// configured.
+  kCccDescriptorImproperlyConfigured = 0xFD,
+
+  /// Profile or service procedure already in progress.
+  kProcedureAlreadyInProgress = 0xFE,
+
+  /// A value was out of range at the profile or service level.
+  kOutOfRange = 0xFF,
+
+  // Errors not specified by Bluetooth.
+
+  // One or more of the call parameters are invalid. See the parameter
+  // documentation.
+  kInvalidParameters = 0x101,
+
+  // Generic failure.
+  kFailure = 0x102,
+};
+
+}  // namespace pw::bluetooth::gatt
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/server.h b/pw_bluetooth/public/pw_bluetooth/gatt/server.h
new file mode 100644
index 0000000..b74c572
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/server.h
@@ -0,0 +1,223 @@
+// 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 <memory>
+#include <span>
+
+#include "pw_bluetooth/gatt/error.h"
+#include "pw_bluetooth/gatt/types.h"
+#include "pw_bluetooth/result.h"
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+
+namespace pw::bluetooth::gatt {
+
+// Parameters for registering a local GATT service.
+struct LocalServiceInfo {
+  // Indicates whether this is a primary or secondary service.
+  bool primary;
+
+  // The UUID that identifies the type of this service.
+  // There may be multiple services with the same UUID.
+  Uuid type;
+
+  // The characteristics of this service.
+  Vector<Characteristic> characteristics;
+
+  // Handles of other services that are included by this service.
+  Vector<Handle> includes;
+};
+
+// Interface for serving a local GATT service. This is implemented by the API
+// client.
+class LocalServiceDelegate {
+ public:
+  virtual ~LocalServiceDelegate() = default;
+
+  // Called when there is a fatal error related to this service that forces the
+  // service to close. LocalServiceDelegate methods will no longer be called.
+  // This invalidates the associated LocalService. It is OK to destroy both
+  // LocalServiceDelegate and the associated LocalService from within this
+  // method.
+  virtual void OnError(Error error) = 0;
+
+  // This notifies the current configuration of a particular
+  // characteristic/descriptor for a particular peer. It will be called when the
+  // peer GATT client changes the configuration.
+  //
+  // The Bluetooth stack maintains the state of each peer's configuration across
+  // reconnections. As such, this method will also be called when a peer
+  // connects for each characteristic with the initial, persisted state of the
+  // newly-connected peer's configuration. However, clients should not rely on
+  // this state being persisted indefinitely by the Bluetooth stack.
+  //
+  // Parameters:
+  // `peer_id` - The PeerId of the GATT client associated with this particular
+  //     CCC.
+  // `handle` - The handle of the characteristic associated with the `notify`
+  //     and `indicate` parameters.
+  // `notify` - True if the client has enabled notifications, false otherwise.
+  // `indicate` - True if the client has enabled indications, false otherwise.
+  virtual void CharacteristicConfiguration(PeerId peer_id,
+                                           Handle handle,
+                                           bool notify,
+                                           bool indicate) = 0;
+
+  // Called when a peer requests to read the value of a characteristic or
+  // descriptor. It is guaranteed that the peer satisfies the permssions
+  // associated with this attribute.
+  //
+  // Parameters:
+  // `peer_id` - The PeerId of the GATT client making the read request.
+  // `handle` - The handle of the requested descriptor/characteristic.
+  // `offset` - The offset at which to start reading the requested value.
+  // `result_callback` - Called with the value of the characteristic on success,
+  //     or an Error on failure. The value will be truncated to fit in the MTU
+  //     if necessary. It is OK to call `result_callback` in `ReadValue`.
+  virtual void ReadValue(
+      PeerId peer_id,
+      Handle handle,
+      uint32_t offset,
+      Function<void(Result<Error, std::span<const std::byte>>)>
+          result_callback) = 0;
+
+  // Called when a peer issues a request to write the value of a characteristic
+  // or descriptor. It is guaranteed that the peer satisfies the permissions
+  // associated with this attribute.
+  //
+  // Parameters:
+  // `peer_id` - The PeerId of the GATT client making the write request.
+  // `handle` - The handle of the requested descriptor/characteristic.
+  // `offset` - The offset at which to start writing the requested value. If the
+  //     offset is 0, any existing value should be overwritten by the new value.
+  //     Otherwise, the existing value between offset:(offset + len(value))
+  //     should be changed to `value`.
+  // `value` - The new value for the descriptor/characteristic.
+  // `status_callback` - Called with the result of the write.
+  virtual void WriteValue(PeerId peer_id,
+                          Handle handle,
+                          uint32_t offset,
+                          std::span<const std::byte> value,
+                          Function<void(Result<Error>)> status_callback) = 0;
+
+  // Called when the MTU of a peer is updated. Also called for peers that are
+  // already connected when the server is published. This method is safe to
+  // ignore if you do not care about the MTU. It is intended for use cases where
+  // throughput needs to be optimized.
+  virtual void MtuUpdate(PeerId peer_id, uint16_t mtu) = 0;
+};
+
+// LocalService is valid for the lifetime of a published GATT service. It is
+// used to control the service and send notifications/indications.
+class LocalService {
+ public:
+  // The parameters used to signal a characteristic value change from a
+  // LocalService to a peer.
+  struct ValueChangedParameters {
+    // The PeerIds of the peers to signal. The LocalService should respect the
+    // Characteristic Configuration associated with a peer+handle when deciding
+    // whether to signal it. If empty, all peers are signalled.
+    Vector<PeerId> peer_ids;
+    // The handle of the characteristic value being signaled.
+    Handle handle;
+    // The new value for the descriptor/characteristic.
+    std::span<const std::byte> value;
+  };
+
+  virtual ~LocalService() = default;
+
+  // Returns the unique handle assigned to this service.
+  virtual Handle GetHandle() = 0;
+
+  // Sends a notification to peers. Notifications should be used instead of
+  // indications when the service does *not* require peer confirmation of the
+  // update.
+  //
+  // Notifications should not be sent to peers which have not enabled
+  // notifications on a particular characteristic - if they are sent, they will
+  // not be propagated. The Bluetooth stack will track this configuration for
+  // the lifetime of the service.
+  //
+  // Parameters:
+  // `parameters` - The parameters associated with the changed characteristic.
+  // `completion_callback` - Called when the notification has been sent.
+  //     Additional values should not be notified until this callback is called.
+  virtual void NotifyValue(const ValueChangedParameters& parameters,
+                           Closure completion_callback) = 0;
+
+  // Sends an indication to peers. Indications should be used instead of
+  // notifications when the service *does* require peer confirmation of the
+  // update.
+  //
+  // Indications should not be sent to peers which have not enabled indications
+  // on a particular characteristic - if they are sent, they will not be
+  // propagated. The Bluetooth stack will track this configuration for the
+  // lifetime of the service.
+  //
+  // If any of the peers in `update.peer_ids` fails to confirm the indication
+  // within the ATT transaction timeout (30 seconds per Bluetooth 5.2 Vol. 4
+  // Part G 3.3.3), the link between the peer and the local adapter will be
+  // closed.
+  //
+  // Parameters:
+  // `parameters` - The parameters associated with the changed characteristic.
+  // `confirmation` - When all the peers listed in `parameters.peer_ids` have
+  //     confirmed the indication, `confirmation` is called. If the
+  //     implementation wishes to receive indication confirmations on a per-peer
+  //     basis, they should send this event with a single PeerId in
+  //     `parameters.peer_ids`. Additional values should not be indicated until
+  //     this callback is called.
+  virtual void IndicateValue(const ValueChangedParameters& parameters,
+                             Function<void(Result<Error>)> confirmation) = 0;
+};
+
+// Interface for a GATT server that serves many GATT services.
+class Server {
+ public:
+  enum class PublishServiceError {
+    kInternalError = 0,
+
+    /// Invalid service UUID provided.
+    kInvalidUuid = 1,
+
+    // Invalid service characteristics provided.
+    kInvalidCharacteristics = 2,
+
+    // Invalid service includes provided.
+    kInvalidIncludes = 3,
+  };
+
+  virtual ~Server() = default;
+
+  // Publishes the service defined by `info` and implemented by `delegate` so
+  // that it is available to all remote peers.
+  //
+  // The caller must assign distinct handles to the characteristics and
+  // descriptors listed in `info`. These identifiers will be used in requests
+  // sent to `delegate`. On success, a `LocalService` is returned. When the
+  // `LocalService` is destroyed or an error occurs
+  // (LocalServiceDelegate.OnError), the service will be unpublished.
+  virtual void PublishService(
+      LocalServiceInfo info,
+      LocalServiceDelegate* delegate,
+      Function<
+          void(Result<PublishServiceError, std::unique_ptr<LocalService>>)>&&
+          result_callback) = 0;
+};
+
+}  // namespace pw::bluetooth::gatt
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/types.h b/pw_bluetooth/public/pw_bluetooth/gatt/types.h
new file mode 100644
index 0000000..3a2b2ea
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/types.h
@@ -0,0 +1,119 @@
+// 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 <optional>
+
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+
+namespace pw::bluetooth::gatt {
+
+// A Handle uniquely identifies a service, characteristic, or descriptor.
+enum class Handle : uint64_t {};
+
+// Possible values for the characteristic properties bitfield. These specify
+// the GATT procedures that are allowed for a particular characteristic.
+enum class CharacteristicPropertyBits : uint16_t {
+  kBroadcast = 0x1,
+  kRead = 0x2,
+  kWriteWithoutResponse = 0x4,
+  kWrite = 0x8,
+  kNotify = 0x10,
+  kIndicate = 0x20,
+  kAuthenticatedSignedWrites = 0x40,
+  kReliableWrite = 0x100,
+  kWritableAuxiliaries = 0x200
+};
+
+// Represents encryption, authentication, and authorization permissions that can
+// be assigned to a specific access permission.
+struct SecurityRequirements {
+  // If true, the physical link must be encrypted to access this attribute.
+  bool encryption_required;
+
+  // If true, the physical link must be authenticated to access this
+  // attribute.
+  bool authentication_required;
+
+  // If true, the client needs to be authorized before accessing this
+  // attribute.
+  bool authorization_required;
+};
+
+/// Specifies the access permissions for a specific attribute value.
+struct AttributePermissions {
+  // Specifies whether or not an attribute has the read permission. If null,
+  // then the attribute value cannot be read. Otherwise, it can be read only if
+  // the permissions specified in the SecurityRequirements table are satisfied.
+  std::optional<SecurityRequirements> read;
+
+  // Specifies whether or not an attribute has the write permission. If null,
+  // then the attribute value cannot be written. Otherwise, it can be written
+  // only if the permissions specified in the SecurityRequirements table are
+  // satisfied.
+  std::optional<SecurityRequirements> write;
+
+  // Specifies the security requirements for a client to subscribe to
+  // notifications or indications on a characteristic. A characteristic's
+  // support for notifications or indiciations is specified using the NOTIFY and
+  // INDICATE characteristic properties. If a local characteristic has one of
+  // these properties then this field can not be null. Otherwise, this field
+  // must be left as null.
+  //
+  // This field is ignored for Descriptors.
+  std::optional<SecurityRequirements> update;
+};
+
+// Represents a local or remote GATT characteristic descriptor.
+struct Descriptor {
+  // Uniquely identifies this descriptor within a service.
+  // For local descriptors, the specified handle must be unique
+  // across all characteristic and descriptor handles in this service.
+  Handle handle;
+
+  // The UUID that identifies the type of this descriptor.
+  Uuid type;
+
+  // The attribute permissions of this descriptor. For remote descriptors, this
+  // value will be null until the permissions are discovered via read and write
+  // requests.
+  std::optional<AttributePermissions> permissions;
+};
+
+// Represents a local or remote GATT characteristic.
+struct Characteristic {
+  // Uniquely identifies this characteristic within a service.
+  // For local characteristics, the specified handle must be unique across
+  // all characteristic and descriptor handles in this service.
+  Handle handle;
+
+  // The UUID that identifies the type of this characteristic.
+  Uuid type;
+
+  // The characteristic properties bitfield. See `CharacteristicPropertyBits`
+  // above for possible values.
+  CharacteristicPropertyBits properties;
+
+  // The attribute permissions of this characteristic. For remote
+  // characteristics, this value will be null until the permissions are
+  // discovered via read and write requests.
+  std::optional<AttributePermissions> permissions;
+
+  // The descriptors of this characteristic.
+  Vector<Descriptor> descriptors;
+};
+
+}  // namespace pw::bluetooth::gatt
\ No newline at end of file
diff --git a/pw_bluetooth/public/pw_bluetooth/hci.h b/pw_bluetooth/public/pw_bluetooth/hci.h
new file mode 100644
index 0000000..b71a51d
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/hci.h
@@ -0,0 +1,45 @@
+// 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 <span>
+
+#include "pw_function/function.h"
+
+namespace pw::bluetooth {
+
+// The Hci (Host Controller Interface) class is a shim for transferring packets
+// between the Host and the Controller.
+class Hci {
+ public:
+  using DataCallback = Function<void(std::span<const std::byte>)>;
+
+  virtual ~Hci() = default;
+
+  // Sends an HCI `command` packet to the controller.
+  virtual void SendCommand(std::span<const std::byte> command) = 0;
+
+  // Sets a callback that will be called with HCI event packets received from
+  // the controller.
+  virtual void SetEventCallback(DataCallback callback) = 0;
+
+  // Sends an ACL data packet to the controller.
+  virtual void SendAclData(std::span<const std::byte> data) = 0;
+
+  // Sets a callback that will be called with ACL data packets received from the
+  // controller.
+  virtual void SetReceiveAclCallback(DataCallback callback) = 0;
+};
+
+}  // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/host.h b/pw_bluetooth/public/pw_bluetooth/host.h
new file mode 100644
index 0000000..cd9091c
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/host.h
@@ -0,0 +1,194 @@
+// 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 <optional>
+#include <string_view>
+
+#include "pw_bluetooth/gatt/client.h"
+#include "pw_bluetooth/gatt/server.h"
+#include "pw_bluetooth/hci.h"
+#include "pw_bluetooth/low_energy/bond_data.h"
+#include "pw_bluetooth/low_energy/central.h"
+#include "pw_bluetooth/low_energy/peripheral.h"
+#include "pw_bluetooth/low_energy/security_mode.h"
+#include "pw_bluetooth/pairing_delegate.h"
+#include "pw_bluetooth/peer.h"
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+
+namespace pw::bluetooth {
+
+// Host is the entrypoint API for interacting with a Bluetooth host stack. Host
+// is an abstract class that is implemented by a host stack implementation.
+class Host {
+ public:
+  // Represents the persistent configuration of a single Host instance. This is
+  // used for identity representation in advertisements & bonding secrets
+  // recall.
+  struct PersistentData {
+    // The local Identity Resolving Key used by a Host to generate Resolvable
+    // Private Addresses when privacy is enabled. May be absent for hosts that
+    // do not use LE privacy, or that only use Non-Resolvable Private Addresses.
+    //
+    // NOTE: This key is distributed to LE peers during pairing procedures. The
+    // client must take care to assign an IRK that consistent with the local
+    // Host identity.
+    std::optional<Key> identity_resolving_key;
+
+    // All bonds that use a public identity address must contain the same local
+    // address.
+    Vector<low_energy::BondData> bonds;
+  };
+
+  // The security level required for this pairing. This corresponds to the
+  // security levels defined in the Security Manager Protocol in Core spec v5.3,
+  // Vol 3, Part H, Section 2.3.1
+  enum class PairingSecurityLevel : uint8_t {
+    // Encrypted without person-in-the-middle protection (unauthenticated)
+    kEncrypted,
+    // Encrypted with person-in-the-middle protection (authenticated), although
+    // this level of security does not fully protect against passive
+    // eavesdroppers
+    kAuthenticated,
+    // Encrypted with person-in-the-middle protection (authenticated).
+    // This level of security fully protects against eavesdroppers.
+    kLeSecureConnections,
+  };
+
+  // Whether or not the device should form a bluetooth bond during the pairing
+  // prodecure. As described in Core Spec v5.2, Vol 3, Part C, Sec 4.3
+  enum class BondableMode : uint8_t {
+    // The device will form a bond during pairing with peers
+    kBondable,
+    // The device will not form a bond during pairing with peers
+    kNonBondable,
+  };
+
+  // Parameters that give a caller more fine-grained control over the pairing
+  // process.
+  struct PairingOptions {
+    // Determines the Security Manager security level to pair with.
+    PairingSecurityLevel security_level = PairingSecurityLevel::kAuthenticated;
+
+    // Indicated whether the device should form a bond or not during pairing. If
+    // not present, interpreted as bondable mode.
+    BondableMode bondable_mode = BondableMode::kBondable;
+  };
+
+  // `Close` should complete before `Host` is destroyed.
+  virtual ~Host() = default;
+
+  // Initializes the host stack. Vendor specific controller initialization (e.g.
+  // loading firmware) must be done before initializing `Host`.
+  //
+  // Parameters:
+  // `hci` - Pointer to a concrete `Hci` that the host stack should use to
+  //     communicate with the controller.
+  // `data` - Data to persist from a previous instance of `Host`.
+  // `on_initialization_complete` - Called when initialization is complete.
+  //     Other methods should not be called until initialization completes.
+  virtual void Initialize(
+      Hci* hci,
+      PersistentData data,
+      Function<void(Status)>&& on_initialization_complete) = 0;
+
+  // Safely shuts down the host, ending all active Bluetooth procedures:
+  // - All objects/pointers associated with this host are destroyed/invalidated
+  //   and all connections disconnected.
+  // - All scanning and advertising procedures are stopped.
+  //
+  // The Host may send events or call callbacks as procedures get terminated.
+  // `callback` will be called once all procedures have terminated.
+  virtual void Close(Closure callback) = 0;
+
+  // Returns a pointer to the Central API, which is used to scan and connect to
+  // peers.
+  virtual low_energy::Central* Central() = 0;
+
+  // Returns a pointer to the Peripheral API, which is used to advertise and
+  // accept connections from peers.
+  virtual low_energy::Peripheral* Peripheral() = 0;
+
+  // Returns a pointer to the GATT Server API, which is used to publish GATT
+  // services.
+  virtual gatt::Server* GattServer() = 0;
+
+  // Deletes a peer from the Bluetooth host. If the peer is connected, it will
+  // be disconnected. `peer_id` will no longer refer to any peer.
+  //
+  // Returns `OK` after no peer exists that's identified by `peer_id` (even
+  // if it didn't exist), `ABORTED` if the peer could not be disconnected or
+  // deleted and still exists.
+  virtual Status ForgetPeer(PeerId peer_id) = 0;
+
+  // Enable or disable the LE privacy feature. When enabled, the host will use a
+  // private device address in all LE procedures. When disabled, the public
+  // identity address will be used instead (which is the default).
+  virtual void EnablePrivacy(bool enabled) = 0;
+
+  // Set the GAP LE Security Mode of the host. Only encrypted,
+  // connection-based security modes are supported, i.e. Mode 1 and Secure
+  // Connections Only mode. If the security mode is set to Secure Connections
+  // Only, any existing encrypted connections which do not meet the security
+  // requirements of Secure Connections Only mode will be disconnected.
+  virtual void SetSecurityMode(low_energy::SecurityMode security_mode) = 0;
+
+  // Assigns the pairing delegate that will respond to authentication challenges
+  // using the given I/O capabilities. Calling this method cancels any on-going
+  // pairing procedure started using a previous delegate. Pairing requests will
+  // be rejected if no PairingDelegate has been assigned.
+  virtual void SetPairingDelegate(InputCapability input,
+                                  OutputCapability output,
+                                  PairingDelegate* pairing_delegate) = 0;
+
+  // NOTE: This is intended to satisfy test scenarios that require pairing
+  // procedures to be initiated without relying on service access. In normal
+  // operation, Bluetooth security is enforced during service access.
+  //
+  // Initiates pairing to the peer with the supplied `peer_id` and `options`.
+  // Returns an error if no connected peer with `peer_id` is found or the
+  // pairing procedure fails.
+  //
+  // If `options` specifies a higher security level than the current pairing,
+  // this method attempts to raise the security level. Otherwise this method has
+  // no effect and returns success.
+  //
+  // Returns the following errors via `callback`:
+  // `NOT_FOUND` - The peer `peer_id` was not found.
+  // `ABORTED` - The pairing procedure failed.
+  virtual void Pair(PeerId peer_id,
+                    PairingOptions options,
+                    Function<void(Status)>&& callback) = 0;
+
+  // Configures a callback to be called when new bond data for a peer has been
+  // created. This data should be persisted and used to initialize Host in the
+  // future. New bond data may be received for an already bonded peer, in which
+  // case the new data should overwrite the old data.
+  virtual void SetBondDataCallback(
+      Function<void(low_energy::BondData)>&& callback) = 0;
+
+  // Looks up the `PeerId` corresponding to `address`. If `address` does not
+  // correspond to a known peer, a new `PeerId` will be generated for the
+  // address. If a `PeerId` cannot be generated, std::nullopt will be returned.
+  virtual std::optional<PeerId> PeerIdFromAddress(Address address) = 0;
+
+  // Looks up the Address corresponding to `peer_id`. Returns null if `peer_id`
+  // does not correspond to a known peer.
+  virtual std::optional<Address> DeviceAddressFromPeerId(PeerId peer_id) = 0;
+};
+
+}  // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/advertising_data.h b/pw_bluetooth/public/pw_bluetooth/low_energy/advertising_data.h
new file mode 100644
index 0000000..f04a1b1
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/advertising_data.h
@@ -0,0 +1,66 @@
+// 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 <span>
+
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+
+namespace pw::bluetooth::low_energy {
+
+// A service data field in an advertising data payload.
+struct ServiceData {
+  Uuid uuid;
+  std::span<const std::byte> data;
+};
+
+// A manufacturer data field in an advertising data payload.
+struct ManufacturerData {
+  uint16_t company_id = 0;
+  std::span<const std::byte> data;
+};
+
+// Represents advertising and scan response data that are transmitted by a LE
+// peripheral or broadcaster.
+struct AdvertisingData {
+  // Long or short name of the device.
+  std::string_view name;
+
+  // The appearance of the local device.
+  Appearance appearance = Appearance::kUnknown;
+
+  Vector<Uuid> service_uuids;
+
+  Vector<ServiceData> service_data;
+
+  Vector<ManufacturerData> manufacturer_data;
+
+  // String representing a URI to be advertised, as defined in IETF STD 66:
+  // https://tools.ietf.org/html/std66. Each entry should be a UTF-8 string
+  // including the scheme. For more information, see:
+  // https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml for allowed
+  // schemes;
+  // https://www.bluetooth.com/specifications/assigned-numbers/uri-scheme-name-string-mapping
+  // for code-points used by the system to compress the scheme to save space in
+  // the payload.
+  Vector<std::string_view> uris;
+
+  // Indicates whether the current TX power level should be included in the
+  // advertising data.
+  bool include_tx_power_level = false;
+};
+
+}  // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/bond_data.h b/pw_bluetooth/public/pw_bluetooth/low_energy/bond_data.h
new file mode 100644
index 0000000..5029aa9
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/bond_data.h
@@ -0,0 +1,78 @@
+// 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 <array>
+#include <cstdint>
+#include <optional>
+
+#include "pw_bluetooth/low_energy/connection.h"
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+
+namespace pw::bluetooth::low_energy {
+
+// A 128-bit secret key.
+using Key = std::array<uint8_t, 16>;
+
+/// Represents a LE Long-Term peer key used for link encyrption. The `ediv` and
+/// `rand` fields are zero if distributed using LE Secure Connections pairing.
+struct LongTermKey {
+  Key key;
+  uint16_t ediv;
+  uint16_t rand;
+};
+
+struct BondData {
+  // The identifier that uniquely identifies this peer.
+  PeerId peer_id;
+
+  // The local Bluetooth identity address that this bond is associated with.
+  Address local_address;
+
+  std::optional<DeviceName> name;
+
+  // The identity address of the peer.
+  Address peer_address;
+
+  // The peer's preferred connection parameters, if known.
+  std::optional<RequestedConnectionParameters> connection_parameters;
+
+  // Identity Resolving RemoteKey used to generate and resolve random addresses.
+  Key identity_resolving_remote_key;
+
+  // Connection Signature Resolving RemoteKey used for data signing without
+  // encryption.
+  Key connection_signature_resolving_remote_key;
+
+  // LE long-term key used to encrypt a connection when the peer is in the LE
+  // Peripheral role.
+  //
+  // In legacy pairing (`peer_long_term_key.security.secure_connections` is
+  // false),  this key corresponds to the key distributed by the peer. In Secure
+  // Connections pairing there is only one LTK and `peer_long_term_key` is the
+  // same as `local_long_term_key`.
+  LongTermKey peer_long_term_key;
+
+  // LE long-term key used to encrypt a connection when the peer is in the LE
+  // Central role.
+  //
+  // In legacy pairing (`local_long_term_key.security.secure_connections` is
+  // false), this key corresponds to the key distributed by the local device.
+  // In Secure Connections pairing there is only one LTK and
+  // `local_long_term_key` is the same as `peer_long_term_key`.
+  LongTermKey local_long_term_key;
+};
+
+}  // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/central.h b/pw_bluetooth/public/pw_bluetooth/low_energy/central.h
new file mode 100644
index 0000000..2ce553e
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/central.h
@@ -0,0 +1,244 @@
+// 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 <memory>
+#include <optional>
+
+#include "pw_bluetooth/low_energy/advertising_data.h"
+#include "pw_bluetooth/low_energy/connection.h"
+#include "pw_bluetooth/result.h"
+#include "pw_bluetooth/types.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_containers/vector.h"
+#include "pw_function/function.h"
+
+namespace pw::bluetooth::low_energy {
+
+// Represents the LE central role. Used to scan and connect to peripherals.
+class Central {
+ public:
+  // Represents an ongoing LE scan.
+  class Scan {
+   public:
+    enum class ScanError : uint8_t { kCanceled = 0 };
+
+    virtual ~Scan() = 0;
+
+    // Set a callback that will be called if the scan is stopped due to an error
+    // in the BLE stack.
+    virtual void SetErrorCallback(Function<void(ScanError)>&& callback) = 0;
+  };
+
+  // Filter parameters for use during a scan. A discovered peer only matches the
+  // filter if it satisfies all of the present filter parameters.
+  struct ScanFilter {
+    // Filter based on advertised service UUID.
+    std::optional<Uuid> service_uuid;
+
+    // Filter based on service data containing the given UUID.
+    std::optional<Uuid> service_data_uuid;
+
+    // Filter based on a manufacturer identifier present in the manufacturer
+    // data. If this filter parameter is set, then the advertising payload must
+    // contain manufacturer specific data with the provided company identifier
+    // to satisfy this filter. Manufacturer identifiers can be found at
+    // https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/
+    std::optional<uint16_t> manufacturer_id;
+
+    // Filter based on whether or not a device is connectable. For example, a
+    // client that is only interested in peripherals that it can connect to can
+    // set this to true. Similarly a client can scan only for broadcasters by
+    // setting this to false.
+    std::optional<bool> connectable;
+
+    // Filter results based on a portion of the advertised device name.
+    // Substring matches are allowed.
+    // The name length must be at most pw::bluetooth::kMaxDeviceNameLength.
+    std::optional<std::string_view> name;
+
+    // Filter results based on the path loss of the radio wave. A device that
+    // matches this filter must satisfy the following:
+    //   1. Radio transmission power level and received signal strength must be
+    //      available for the path loss calculation.
+    //   2. The calculated path loss value must be less than, or equal to,
+    //      `max_path_loss`.
+    //
+    // NOTE: This field is calculated using the RSSI and TX Power information
+    // obtained from advertising and scan response data during a scan procedure.
+    // It should NOT be confused with information for an active connection
+    // obtained using the "Path Loss Reporting" feature.
+    std::optional<uint8_t> max_path_loss;
+  };
+
+  // Parameters used during a scan.
+  struct ScanOptions {
+    // List of filters for use during a scan. A peripheral that satisfies any of
+    // these filters will be reported. At least 1 filter must be specified.
+    // While not recommended, clients that require that all peripherals be
+    // reported can specify an empty filter.
+    Vector<ScanFilter> filters;
+
+    // The time interval between scans.
+    // Time = N * 0.625ms
+    // Range: 0x0004 (2.5ms) - 10.24ms (0x4000)
+    // Default: 10ms
+    uint16_t interval = 0x0010;
+
+    // The duration of the scan. The window must be less than or equal to the
+    // interval.
+    // Time = N * 0.625ms
+    // Range: 0x0004 (2.5ms) - 10.24ms (0x4000)
+    // Default: 10ms
+    uint16_t window = 0x0010;
+  };
+
+  // Information obtained from advertising and scan response data broadcast by a
+  // peer.
+  struct ScanData {
+    // The radio transmit power level.
+    // NOTE: This field should NOT be confused with the "connection TX Power
+    // Level" of a peer that is currently connected to the system obtained via
+    // the "Transmit Power reporting" feature.
+    std::optional<uint8_t> tx_power;
+
+    // The appearance of the device.
+    std::optional<Appearance> appearance;
+
+    Vector<Uuid> service_uuids;
+
+    Vector<ServiceData> service_data;
+
+    Vector<ManufacturerData> manufacturer_data;
+
+    // String representing a URI to be advertised, as defined in IETF STD
+    // 66: https://tools.ietf.org/html/std66. Each entry should be a UTF-8
+    // string including the scheme. For more information, see
+    // https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml for
+    // allowed schemes; NOTE: Bluetooth advertising compresses schemas over the
+    // air to save space. See
+    // https://www.bluetooth.com/specifications/assigned-numbers/uri-scheme-name-string-mapping.
+    Vector<std::string_view> uris;
+
+    // The time when this scan data was received.
+    chrono::SystemClock::time_point timestamp;
+  };
+
+  struct ScanResult {
+    // ScanResult is non-copyable becuase strings are only valid in the
+    // result callback.
+    ScanResult(const ScanResult&) = delete;
+    ScanResult& operator=(const ScanResult&) = delete;
+
+    // Uniquely identifies this peer on the current system.
+    PeerId peer_id;
+
+    // Whether or not this peer is connectable. Non-connectable peers are
+    // typically in the LE broadcaster role.
+    bool connectable;
+
+    // The last observed signal strength of this peer. This field is only
+    // present for a peer that is broadcasting. The RSSI can be stale if the
+    // peer has not been advertising.
+    //
+    // NOTE: This field should NOT be confused with the "connection RSSI" of a
+    // peer that is currently connected to the system.
+    std::optional<uint8_t> rssi;
+
+    // Information from advertising and scan response data broadcast by this
+    // peer. This contains the advertising data last received from the peer.
+    ScanData scan_data;
+
+    // The name of this peer. The name is often obtained during a scan procedure
+    // and can get updated during the name discovery procedure following a
+    // connection.
+    //
+    // This field is present if the name is known.
+    std::optional<std::string_view> name;
+
+    // Timestamp of when the information in this `ScanResult` was last updated.
+    chrono::SystemClock::time_point last_updated;
+  };
+
+  // Possible errors returned by `Connect`.
+  enum class ConnectError : uint8_t {
+    // The peer ID is unknown.
+    kUnknownPeer,
+
+    // The `ConnectionOptions` were invalid.
+    kInvalidOptions,
+
+    // A connection to the peer already exists.
+    kAlreadyExists,
+
+    // A connection could not be established.
+    kCouldNotBeEstablished,
+  };
+
+  enum class StartScanError : uint8_t {
+    // A scan is already in progress. Only 1 scan may be active at a time.
+    kScanInProgress,
+    // Some of the scan options are invalid.
+    kInvalidParameters,
+    // An internal error occurred and a scan could not be started.
+    kInternal,
+  };
+
+  virtual ~Central() = default;
+
+  // Connect to the peer with the given identifier.
+  //
+  // The requested `Connection` represents the client's interest in the LE
+  // connection to the peer. Destroying the `Connection` will disconnect from
+  // the peer. Only 1 connection per peer may exist at a time.
+  //
+  // The `Connection` will be closed by the system if the connection to the peer
+  // is lost or an error occurs, as indicated by `Connection.OnError`.
+  //
+  // Parameters:
+  // `id` - Identifier of the peer to initiate a connection to.
+  // `options` - Options used to configure the connection.
+  // `callback` - Called when a connection is successfully established, or an
+  //     error occurs.
+  //
+  // Possible errors are documented in `ConnectError`.
+  virtual void Connect(
+      PeerId peer_id,
+      ConnectionOptions options,
+      Function<void(Result<ConnectError, std::unique_ptr<Connection>>)>&&
+          callback) = 0;
+
+  // Scans for nearby LE peripherals and broadcasters. The lifetime of the scan
+  // session is tied to the returned `Scan` object. Once a scan is started,
+  // `scan_result_callback` will be called with scan results. Only 1 scan may be
+  // active at a time. It is OK to destroy the `Scan` object in
+  // `scan_result_callback` to stop scanning (no more results will be returned).
+  //
+  // Parameters:
+  // `options`  - Options used to configure the scan session.
+  // `scan_result_callback` - If scanning starts successfully,called for LE
+  //     peers that satisfy the filters indicated in `options`. The initial
+  //     calls may report recently discovered peers. Subsequent calls will
+  //     be made only when peers have been scanned or updated since the last
+  //     call.
+  // `scan_started_callback` - Called with a `Scan` object if the
+  //     scan successfully starts, or a `ScanError` otherwise.
+  virtual void Scan(
+      ScanOptions options,
+      Function<void(ScanResult)>&& scan_result_callback,
+      Function<void(Result<StartScanError, std::unique_ptr<Scan>>)>&&
+          scan_started_callback) = 0;
+};
+
+}  // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h b/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h
new file mode 100644
index 0000000..8895c65
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h
@@ -0,0 +1,160 @@
+// 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 "pw_bluetooth/gatt/client.h"
+#include "pw_bluetooth/types.h"
+
+namespace pw::bluetooth::low_energy {
+
+// Actual connection parameters returned by the controller.
+struct ConnectionParameters {
+  // The connection interval indicates the frequency of link layer connection
+  // events over which data channel PDUs can be transmitted. See Core Spec v5.3,
+  // Vol 6, Part B, Section 4.5.1 for more information on the link layer
+  // connection events.
+  // Range: 0x0006 to 0x0C80
+  // Time: N * 1.25 ms
+  // Time Range: 7.5 ms to 4 s.
+  uint16_t interval;
+
+  // The maximum allowed peripheral connection latency in number of connection
+  // events. See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+  // Range: 0x0000 to 0x01F3
+  uint16_t latency;
+
+  // This defines the maximum time between two received data packet PDUs
+  // before the connection is considered lost. See Core Spec v5.3, Vol 6, Part
+  // B, Section 4.5.2.
+  // Range: 0x000A to 0x0C80
+  // Time: N * 10 ms
+  // Time Range: 100 ms to 32 s
+  uint16_t supervision_timeout;
+};
+
+// Connection parameters that either the local device or a peer device are
+// requesting.
+struct RequestedConnectionParameters {
+  // Minimum value for the connection interval. This shall be less than or equal
+  // to `max_interval`. The connection interval indicates the frequency of link
+  // layer connection events over which data channel PDUs can be transmitted.
+  // See Core Spec v5.3, Vol 6, Part B, Section 4.5.1 for more information on
+  // the link layer connection events.
+  // Range: 0x0006 to 0x0C80
+  // Time: N * 1.25 ms
+  // Time Range: 7.5 ms to 4 s.
+  uint16_t min_interval;
+
+  // Maximum value for the connection interval. This shall be greater than or
+  // equal to `min_interval`. The connection interval indicates the frequency
+  // of link layer connection events over which data channel PDUs can be
+  // transmitted.  See Core Spec v5.3, Vol 6, Part B, Section 4.5.1 for more
+  // information on the link layer connection events.
+  // Range: 0x0006 to 0x0C80
+  // Time: N * 1.25 ms
+  // Time Range: 7.5 ms to 4 s.
+  uint16_t max_interval;
+
+  // Maximum peripheral latency for the connection in number of connection
+  // events. See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+  // Range: 0x0000 to 0x01F3
+  uint16_t max_latency;
+
+  // This defines the maximum time between two received data packet PDUs
+  // before the connection is considered lost. See Core Spec v5.3, Vol 6, Part
+  // B, Section 4.5.2.
+  // Range: 0x000A to 0x0C80
+  // Time: N * 10 ms
+  // Time Range: 100 ms to 32 s
+  uint16_t supervision_timeout;
+};
+
+// Represents parameters that are set on a per-connection basis.
+struct ConnectionOptions {
+  // When true, the connection operates in bondable mode. This means pairing
+  // will form a bond, or persist across disconnections, if the peer is also
+  // in bondable mode. When false, the connection operates in non-bondable
+  // mode, which means the local device only allows pairing that does not form
+  // a bond.
+  bool bondable_mode = true;
+
+  // When present, service discovery performed following the connection is
+  // restricted to primary services that match this field. Otherwise, by
+  // default all available services are discovered.
+  std::optional<Uuid> service_filter;
+
+  // When present, specifies the initial connection parameters. Otherwise, the
+  // connection parameters will be selected by the implementation.
+  std::optional<RequestedConnectionParameters> parameters;
+};
+
+/// Class that represents a connection to a peer. This can be used to interact
+/// with GATT services and establish LE L2CAP channels.
+///
+/// This lifetime of this object is tied to that of the LE connection it
+/// represents. Destroying the object results in a disconnection.
+class Connection {
+ public:
+  // Possible errors when updating the connection parameters.
+  enum class ConnectionParameterUpdateError : uint8_t {
+    kFailure,
+    kInvalidParameters,
+    kRejected,
+  };
+
+  // Possible reasons a connection was disconnected.
+  enum class DisconnectReason : uint8_t {
+    kFailure,
+    kRemoteUserTerminatedConnection,
+    // This usually indicates that the link supervision timeout expired.
+    kConnectionTimeout,
+  };
+
+  // If a disconnection has not occurred, destroying this object will result in
+  // disconnection.
+  virtual ~Connection() = default;
+
+  // Sets a callback that will be called when the peer disconnects or there is a
+  // connection error that causes a disconnection. This should be configured by
+  // the client immediately after establishing the connection. `callback` will
+  // not be called for disconnections iniated by the client (e.g. by destroying
+  // `Connection`). It is OK to destroy this object from within `callback`.
+  virtual void SetDisconnectCallback(
+      Function<void(DisconnectReason)>&& callback) = 0;
+
+  // Returns a GATT client to the connected peer that is valid for the lifetime
+  // of this connection. The client is valid for the lifetime of this
+  // connection.
+  virtual gatt::Client* GattClient() = 0;
+
+  // Returns the current ATT Maximum Transmission Unit. By subtracting ATT
+  // headers from the MTU, the maximum payload size of messages can be
+  // calculated.
+  virtual uint16_t AttMtu() = 0;
+
+  // Sets a callback that will be called with the new ATT MTU whenever it is
+  // updated.
+  virtual void SetAttMtuChangeCallback(Function<void(uint16_t)> callback) = 0;
+
+  // Returns the current connection parameters.
+  virtual ConnectionParameters Parameters() = 0;
+
+  // Requests an update to the connection parameters. `callback` will be called
+  // with the result of the request.
+  virtual void RequestConnectionParameterUpdate(
+      RequestedConnectionParameters parameters,
+      Function<void(Result<ConnectionParameterUpdateError>)>&& callback) = 0;
+};
+
+}  // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h b/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h
new file mode 100644
index 0000000..8a77492
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h
@@ -0,0 +1,145 @@
+// 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 <memory>
+
+#include "pw_bluetooth/low_energy/advertising_data.h"
+#include "pw_bluetooth/low_energy/connection.h"
+#include "pw_bluetooth/result.h"
+#include "pw_bluetooth/types.h"
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+
+namespace pw::bluetooth::low_energy {
+
+// `AdvertisedPeripheral` instances are valid for the duration of advertising.
+class AdvertisedPeripheral {
+ public:
+  // Destroying an instance will stop advertising.
+  virtual ~AdvertisedPeripheral() = default;
+
+  // Set a callback that will be called when an error occurs and advertising
+  // has been stopped (invalidating this object). It is OK to destroy this
+  // object from within `callback`.
+  virtual void SetErrorCallback(Closure callback) = 0;
+
+  // For connectable advertisements, this callback will be called when an LE
+  // central connects to the advertisement. It is recommended to set this
+  // callback immediately after advertising starts to avoid dropping
+  // connections.
+  //
+  // The returned Connection can be used to interact with the peer. It also
+  // represents a peripheral's ownership over the connection: the client can
+  // drop the object to request a disconnection. Similarly, the Connection
+  // error handler is called by the system to indicate that the connection to
+  // the peer has been lost. While connections are exclusive among peripherals,
+  // they may be shared with centrals.
+  //
+  // If advertising is not stopped, this callback may be called with multiple
+  // connections over the lifetime of an advertisement. It is OK to destroy
+  // this object from within `callback` in order to stop advertising.
+  virtual void SetConnectionCallback(
+      Function<void(std::unique_ptr<Connection>)> callback) = 0;
+};
+
+// Represents the LE Peripheral role, which advertises and is connected to.
+class Peripheral {
+ public:
+  // The range of the time interval between advertisements. Shorter intervals
+  // result in faster discovery at the cost of higher power consumption. The
+  // exact interval used is determined by the Bluetooth controller.
+  // Time = N * 0.625ms.
+  // Time range: 0x0020 (20ms) - 0x4000 (10.24s)
+  struct AdvertisingIntervalRange {
+    uint16_t min = 0x0800;  // 1.28s
+    uint16_t max = 0x0800;  // 1.28s
+  };
+
+  // Represents the parameters for configuring advertisements.
+  struct AdvertisingParameters {
+    // The fields that will be encoded in the data section of advertising
+    // packets.
+    AdvertisingData data;
+
+    // The fields that are to be sent in a scan response packet. Clients may use
+    // this to send additional data that does not fit inside an advertising
+    // packet on platforms that do not support the advertising data length
+    // extensions.
+    //
+    // If present advertisements will be configured to be scannable.
+    std::optional<AdvertisingData> scan_response;
+
+    // See `AdvertisingIntervalRange` documentation.
+    AdvertisingIntervalRange interval_range;
+
+    // If present, the controller will broadcast connectable advertisements
+    // which allow peers to initiate connections to the Peripheral. The fields
+    // of `ConnectionOptions` will configure any connections set up from
+    // advertising.
+    std::optional<ConnectionOptions> connection_options;
+  };
+
+  // Errors returned by `Advertise`.
+  enum class AdvertiseError {
+    // The operation or parameters requested are not supported on the current
+    // hardware.
+    kNotSupported = 1,
+
+    // The provided advertising data exceeds the maximum allowed length when
+    // encoded.
+    kAdvertisingDataTooLong = 2,
+
+    // The provided scan response data exceeds the maximum allowed length when
+    // encoded.
+    kScanResponseDataTooLong = 3,
+
+    // The requested parameters are invalid.
+    kInvalidParameters = 4,
+
+    // This may be called if the maximum number of simultaneous advertisements
+    // has already been reached.
+    kNotEnoughAdvertisingSlots = 5,
+
+    // Advertising could not be initiated due to a hardware or system error.
+    kFailed = 6,
+  };
+
+  using AdvertiseCallback = Function<void(
+      Result<AdvertiseError, std::unique_ptr<AdvertisedPeripheral>>)>;
+
+  virtual ~Peripheral() = default;
+
+  // Start advertising continuously as a LE peripheral. If advertising cannot
+  // be initiated then `result_callback` will be called with an error. Once
+  // started, advertising can be stopped by destroying the returned
+  // `AdvertisedPeripheral`.
+  //
+  // If the system supports multiple advertising, this may be called as many
+  // times as there are advertising slots. To reconfigure an advertisement,
+  // first close the original advertisement and then initiate a new
+  // advertisement.
+  //
+  // Parameters:
+  // `parameters` - Parameters used while configuring the advertising
+  //     instance.
+  // `result_callback` - Called once advertising has started or failed. On
+  //     success, called with an `AdvertisedPeripheral` that models the lifetime
+  //     of the advertisement. Destroying it will stop advertising.
+  virtual void Advertise(AdvertisingParameters parameters,
+                         AdvertiseCallback&& result_callback) = 0;
+};
+
+}  // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/security_mode.h b/pw_bluetooth/public/pw_bluetooth/low_energy/security_mode.h
new file mode 100644
index 0000000..9fb2250
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/security_mode.h
@@ -0,0 +1,41 @@
+// 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>
+
+namespace pw::bluetooth::low_energy {
+
+// The LE Security Mode of a BLE device determines the possible security
+// properties of the device. The security mode does not make specific guarantees
+// about the current security properties of a device's connections; it sets
+// restrictions on the allowable security properties. See Core Spec v5.2 Vol. 3,
+// Part C 10.2 for more details.
+enum class SecurityMode : uint8_t {
+  // In LE Security Mode 1, communication is secured by encryption, and
+  // BLE-based services may specify varying requirements for authentication, key
+  // size, or Secure Connections protection on the encryption keys.
+  kMode1 = 0,
+
+  // In Secure Connections Only mode, all secure communication must use 128 bit,
+  // authenticated, and LE Secure Connections-generated encryption keys. If
+  // these encryption key properties cannot be satisfied by a device due to
+  // system constraints, any connection involving such a device will not be able
+  // to secure the link at all. This mode does not prevent unencrypted
+  // communication; it merely enforces stricter policies on all encrypted
+  // communication.
+  kSecureConnectionsOnly = 1
+};
+
+}  // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/pairing_delegate.h b/pw_bluetooth/public/pw_bluetooth/pairing_delegate.h
new file mode 100644
index 0000000..c4c5c53
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/pairing_delegate.h
@@ -0,0 +1,119 @@
+// 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 "pw_bluetooth/peer.h"
+#include "pw_function/function.h"
+
+namespace pw::bluetooth {
+
+// Input Capabilities for pairing exchanges.
+// See Core Spec v5.3 Volume 3, Part C, Section 5.2.2.4, Table 5.3.
+enum class InputCapability : uint8_t { kNone, kConfirmation, kKeyboard };
+
+// Output Capabilities for pairing exchanges.
+// See Core Spec v5.3 Volume 3, Part C, Section 5.2.2.4, Table 5.4.
+enum class OutputCapability : uint8_t { kNone, kDisplay };
+
+// Pairing event handler implemented by the API client.
+class PairingDelegate {
+ public:
+  // Different types required by the Security Manager for pairing methods.
+  // Bluetooth SIG has different requirements for different device capabilities.
+  enum class PairingMethod : uint8_t {
+    // The user is asked to accept or reject pairing.
+    kConsent,
+
+    // The user is shown a 6-digit numerical passkey which they must enter on
+    // the
+    // peer device.
+    kPasskeyDisplay,
+
+    // The user is shown a 6-digit numerical passkey which will also shown on
+    // the
+    // peer device. The user must compare the passkeys and accept the pairing if
+    // the passkeys match.
+    kPasskeyComparison,
+
+    // The user is asked to enter a 6-digit passkey.
+    kPasskeyEntry
+  };
+
+  enum class PairingKeypress : uint8_t {
+    // The user has entered a single digit.
+    kDigitEntered,
+
+    // The user has erased a single digit.
+    kDigitErased,
+
+    // The user has cleared the entire passkey.
+    kPasskeyCleared,
+
+    // The user has finished entering the passkey.
+    kPasskeyEntered
+  };
+
+  // Callback for responding to pairing requests.
+  using ResponseCallback =
+      pw::Function<void(bool accept, uint32_t entered_passkey)>;
+
+  // Callback for signaling local keypresses to a peer.
+  using KeypressCallback =
+      pw::Function<void(PeerId peer_id, PairingKeypress keypress)>;
+
+  virtual ~PairingDelegate() = default;
+
+  // Called to initiate a pairing request. The delegate must respond with "true"
+  // or "false" in the callback to either accept or reject the pairing request.
+  // If the pairing method requires a passkey this is returned as well. It is OK
+  // to call `callback` synchronously in this method.
+  //
+  // Any response from this method will be ignored if the `OnPairingComplete`
+  // event has already been sent for `peer`.
+  //
+  // The `displayed_passkey` parameter should be displayed to the user if
+  // `method` equals `PairingMethod::kPasskeyDisplay` or
+  // `PairingMethod.kPasskeyComparison`. Otherwise, this parameter has no
+  // meaning and should be ignored.
+  //
+  // The `entered_passkey` parameter of `callback` only has meaning if `method`
+  // equals `PairingMethod.kPasskeyEntry`. It will be ignored otherwise.
+  virtual void OnPairingRequest(Peer peer,
+                                PairingMethod method,
+                                uint32_t displayed_passkey,
+                                ResponseCallback&& callback) = 0;
+
+  // Called if the pairing procedure for the device with the given ID is
+  // completed. This can be due to successful completion or an error (e.g. due
+  // to cancellation by the peer, a timeout, or disconnection) which is
+  // indicated by `success`.
+  virtual void OnPairingComplete(PeerId peer_id, bool success) = 0;
+
+  // Called to notify keypresses from the peer device during pairing using
+  // `PairingMethod::kPasskeyDisplay`.
+  //
+  // This event is used to provide key press events to the delegate for a
+  // responsive user experience as the user types the passkey on the peer
+  // device. This event will be called once for each keypress.
+  virtual void OnRemoteKeypress(PeerId peer_id, PairingKeypress keypress) = 0;
+
+  // Sets a callback that the client may call on local passkey keypresses during
+  // a `PairingMethod::kPasskeyEntry` pairing request. Signaled keypresses may
+  // be used in the UI of the peer. This should be set immediately by the
+  // Bluetooth stack when the delegate is configured by the client unless
+  // sending local keypresses is not supported.
+  virtual void SetLocalKeypressCallback(KeypressCallback&& callback) = 0;
+};
+
+}  // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/peer.h b/pw_bluetooth/public/pw_bluetooth/peer.h
new file mode 100644
index 0000000..39990dc
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/peer.h
@@ -0,0 +1,41 @@
+// 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 <optional>
+
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+
+namespace pw::bluetooth {
+
+// Information about a remote Bluetooth device.
+struct Peer {
+  // Uniquely identifies this peer on the current system.
+  PeerId peer_id;
+
+  // Bluetooth device address that identifies this peer.
+  // NOTE: Clients should use the `peer_id` field to keep track of peers
+  // instead of their address.
+  Address address;
+
+  // The name of the peer, if known.
+  std::optional<DeviceName> name;
+
+  // The LE appearance property. Present if the appearance information was
+  // obtained over advertising and/or GATT.
+  std::optional<Appearance> appearance;
+};
+
+}  // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/result.h b/pw_bluetooth/public/pw_bluetooth/result.h
new file mode 100644
index 0000000..b543e92
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/result.h
@@ -0,0 +1,101 @@
+// 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 <optional>
+#include <utility>
+
+#include "pw_assert/assert.h"
+
+namespace pw::bluetooth {
+
+// A Result represents the result of an operation which can fail. If it
+// represents an error, it contains an error value. If it represents success, it
+// contains zero or one success value.
+template <typename E, typename... Ts>
+class Result;
+
+// Result specialization for returning OK or an error (E).
+template <typename E>
+class [[nodiscard]] Result<E> {
+ public:
+  constexpr Result() = default;
+  constexpr Result(E error) : error_(error) {}
+
+  constexpr Result(const Result&) = default;
+  constexpr Result& operator=(const Result&) = default;
+
+  constexpr Result(Result&&) = default;
+  constexpr Result& operator=(Result&&) = default;
+
+  [[nodiscard]] constexpr E error() const {
+    PW_ASSERT(error_.has_value());
+    return error_.value();
+  }
+  [[nodiscard]] constexpr bool ok() const { return !error_.has_value(); }
+
+ private:
+  std::optional<E> error_;
+};
+
+// Result specialization for returning some data (T) or an error (E).
+template <typename E, typename T>
+class [[nodiscard]] Result<E, T> {
+ public:
+  constexpr Result(T&& value) : value_(std::move(value)) {}
+  constexpr Result(const T& value) : value_(value) {}
+
+  template <typename... Args>
+  constexpr Result(std::in_place_t, Args&&... args)
+      : value_(std::forward<Args>(args)...) {}
+
+  constexpr Result(E error) : error_(error), ok_(false) {}
+
+  constexpr Result(const Result&) = default;
+  constexpr Result& operator=(const Result&) = default;
+
+  constexpr Result(Result&&) = default;
+  constexpr Result& operator=(Result&&) = default;
+
+  [[nodiscard]] constexpr E error() const {
+    PW_ASSERT(!ok_);
+    return error_;
+  }
+  [[nodiscard]] constexpr bool ok() const { return ok_; }
+
+  constexpr T& value() & {
+    PW_ASSERT(ok_);
+    return value_;
+  }
+
+  constexpr const T& value() const& {
+    PW_ASSERT(ok_);
+    return value_;
+  }
+
+  constexpr T&& value() && {
+    PW_ASSERT(ok_);
+    return std::move(value_);
+  }
+
+ private:
+  union {
+    T value_;
+    E error_;
+  };
+
+  bool ok_ = true;
+};
+
+}  // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/types.h b/pw_bluetooth/public/pw_bluetooth/types.h
new file mode 100644
index 0000000..4918f85
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/types.h
@@ -0,0 +1,103 @@
+// 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 <array>
+#include <cstdint>
+#include <string_view>
+
+namespace pw::bluetooth {
+
+// 64-bit unique value used by the system to identify peer devices.
+using PeerId = uint64_t;
+
+// The device address bytes in little-endian order.
+using Address = std::array<uint8_t, 6>;
+
+using DeviceName = std::string_view;
+
+using Uuid = std::array<uint8_t, 16>;
+
+// A 128-bit secret key.
+using Key = std::array<uint8_t, 16>;
+
+/// Refers to the role of a Bluetooth device in a physical channel piconet.
+enum class ConnectionRole : uint8_t {
+  // The connection initiating device.
+  kCentral,
+  // The advertising device.
+  kPeripheral
+};
+
+/// Possible values for the LE Appearance property which describes the external
+/// appearance of a peer at a high level.
+/// (See the Bluetooth assigned-numbers document:
+/// https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.gap.appearance.xml)
+enum class Appearance : uint16_t {
+  kUnknown = 0,
+  kPhone = 64,
+  kComputer = 128,
+  kWatch = 192,
+  kWatchSports = 193,
+  kClock = 256,
+  kDisplay = 320,
+  kRemoteControl = 384,
+  kEyeGlasses = 448,
+  kTag = 512,
+  kKeyring = 576,
+  kMediaPlayer = 640,
+  kBarcodeScanner = 704,
+  kThermometer = 768,
+  kThermometerEar = 769,
+  kHeartRateSensor = 832,
+  kHeartRateSensorBelt = 833,
+  kBloodPressure = 896,
+  kBloodPressureArm = 897,
+  kBloodPressureWrist = 898,
+  kHid = 960,
+  kHidKeyboard = 961,
+  kHidMouse = 962,
+  kHidJoystick = 963,
+  kHidGamepad = 964,
+  kHidDigitizerTablet = 965,
+  kHidCardReader = 966,
+  kHidDigitalPen = 967,
+  kHidBarcodeScanner = 968,
+  kGlucoseMeter = 1024,
+  kRunningWalkingSensor = 1088,
+  kRunningWalkingSensorInShoe = 1089,
+  kRunningWalkingSensorOnShoe = 1090,
+  kRunningWalkingSensorOnHip = 1091,
+  kCycling = 1152,
+  kCyclingComputer = 1153,
+  kCyclingSpeedSensor = 1154,
+  kCyclingCadenceSensor = 1155,
+  kCyclingPowerSensor = 1156,
+  kCyclingSpeedAndCadenceSensor = 1157,
+  kPulseOximeter = 3136,
+  kPulseOximeterFingertip = 3137,
+  kPulseOximeterWrist = 3138,
+  kWeightScale = 3200,
+  kPersonalMobility = 3264,
+  kPersonalMobilityWheelchair = 3265,
+  kPersonalMobilityScooter = 3266,
+  kGlucoseMonitor = 3328,
+  kSportsActivity = 5184,
+  kSportsActivityLocationDisplay = 5185,
+  kSportsActivityLocationAndNavDisplay = 5186,
+  kSportsActivityLocationPod = 5187,
+  kSportsActivityLocationAndNavPod = 5188,
+};
+
+}  // namespace pw::bluetooth
\ No newline at end of file
diff --git a/pw_bluetooth/result_test.cc b/pw_bluetooth/result_test.cc
new file mode 100644
index 0000000..fb65921
--- /dev/null
+++ b/pw_bluetooth/result_test.cc
@@ -0,0 +1,43 @@
+// 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/result.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::bluetooth {
+namespace {
+
+enum class TestError { kFailure0, kFailure1 };
+
+TEST(ResultTest, NoValue) {
+  EXPECT_TRUE(Result<TestError>().ok());
+
+  Result<TestError> error_result(TestError::kFailure0);
+  ASSERT_FALSE(error_result.ok());
+  EXPECT_EQ(error_result.error(), TestError::kFailure0);
+}
+
+TEST(ResultTest, Value) {
+  Result<TestError, int> ok_result(42);
+  ASSERT_TRUE(ok_result.ok());
+  EXPECT_EQ(ok_result.value(), 42);
+
+  Result<TestError, int> error_result(TestError::kFailure1);
+  ASSERT_FALSE(error_result.ok());
+  EXPECT_EQ(error_result.error(), TestError::kFailure1);
+}
+
+}  // namespace
+}  // namespace pw::bluetooth
diff --git a/pw_build/generated_pigweed_modules_lists.gni b/pw_build/generated_pigweed_modules_lists.gni
index 21d9203..8a80c64 100644
--- a/pw_build/generated_pigweed_modules_lists.gni
+++ b/pw_build/generated_pigweed_modules_lists.gni
@@ -40,6 +40,7 @@
   dir_pw_base64 = get_path_info("../pw_base64", "abspath")
   dir_pw_bloat = get_path_info("../pw_bloat", "abspath")
   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_boot = get_path_info("../pw_boot", "abspath")
   dir_pw_boot_cortex_m = get_path_info("../pw_boot_cortex_m", "abspath")
@@ -175,6 +176,7 @@
     dir_pw_base64,
     dir_pw_bloat,
     dir_pw_blob_store,
+    dir_pw_bluetooth,
     dir_pw_bluetooth_hci,
     dir_pw_boot,
     dir_pw_boot_cortex_m,
@@ -291,6 +293,7 @@
     "$dir_pw_assert:tests",
     "$dir_pw_base64:tests",
     "$dir_pw_blob_store:tests",
+    "$dir_pw_bluetooth:tests",
     "$dir_pw_bluetooth_hci:tests",
     "$dir_pw_bytes:tests",
     "$dir_pw_checksum:tests",
@@ -365,6 +368,7 @@
     "$dir_pw_base64:docs",
     "$dir_pw_bloat:docs",
     "$dir_pw_blob_store:docs",
+    "$dir_pw_bluetooth:docs",
     "$dir_pw_bluetooth_hci:docs",
     "$dir_pw_boot:docs",
     "$dir_pw_boot_cortex_m:docs",