Add embedded SDK

The first release, v1.0.0-embedded, of Nearby SDK for embedded devices.
diff --git a/embedded/Makefile b/embedded/Makefile
new file mode 100644
index 0000000..7c33226
--- /dev/null
+++ b/embedded/Makefile
@@ -0,0 +1,181 @@
+# Makefile arguments
+#
+#
+# Delete the default suffix rules.
+.SUFFIXES:
+.SECONDARY:
+
+GTEST_DIR   = ../third_party/gtest/googletest/
+GMOCK_DIR   = ../third_party/gtest/googlemock/
+
+ARCH        ?= host
+OUT_DIR_NAME ?= $(ARCH)
+OUT_DIR     = out/$(OUT_DIR_NAME)
+DIST_DIR    = dist/$(OUT_DIR_NAME)
+
+CFLAGS := -Wall
+COMMON_INCLUDE_DIRS :=
+CLIENT_INCLUDES :=
+# For ARCH variants like gLinux_hw set to gLinux.
+ARCH_COMMON_NAME := $(ARCH)
+ARCH_COMMON_DIR = common/source/$(ARCH_COMMON_NAME)
+CLIENT_DIR = client/source
+CLIENT_SRCS :=
+
+NEARBY_LIB_NAME = libnearby.a
+
+NAME = $(OUT_DIR)/$(NEARBY_LIB_NAME)
+
+all : $(NAME)
+
+COMMON_DIR  = common/source
+COMMON_C_FILES :=
+
+# All output objects for the build system should be added to ALL_OBJS.
+ALL_OBJS :=
+# Extra dependencies of the dist target.
+DIST_LIST :=
+
+# Name of the libnearby static library that we distribute.
+DIST_NEARBY_LIB_NAME ?= $(DIST_DIR)/$(NEARBY_LIB_NAME)
+
+# Kokoro on Windows is wonky. We can't shell out to git because we are in
+# cygwin, so this is pre-computed by continuous.bat
+ifneq ($(FIRMWARE_VERSION_FILENAME),)
+FIRMWARE_VERSION := $(shell cat $(FIRMWARE_VERSION_FILENAME))
+else
+FIRMWARE_VERSION_FILENAME = $(OUT_DIR)/FIRMWARE_VERSION
+PHONY_VERSION_FILENAME = $(OUT_DIR)/PHONY_FIRMWARE_VERSION
+FIRMWARE_VERSION := $(shell git -c core.fileMode=false describe --always --dirty --exclude="*")
+FIRMWARE_VERSION_FILENAME: $(PHONY_VERSION_FILENAME)
+
+ifneq ($(KOKORO_RELEASE_BUILD),1)
+FIRMWARE_VERSION := $(FIRMWARE_VERSION)ENG
+endif
+
+# Update the phony version on every build.
+.PHONY: $(PHONY_VERSION_FILENAME)
+$(PHONY_VERSION_FILENAME):
+	@mkdir -p $(dir $@) ;\
+	echo $(FIRMWARE_VERSION) > $@ ;\
+
+# Only update this version if necessary (if it's different from the phony). This
+# tricks make into not updating dependencies of FIRMWARE_VERSION_FILENAME unless
+# the file is updated. .PHONY is not transitive.
+$(FIRMWARE_VERSION_FILENAME): $(PHONY_VERSION_FILENAME)
+	@cmp $@ $< 2>> /dev/null || cp -v $< $@
+
+endif
+
+# Note $(notdir) below converts a path to just the base file name
+define compile_c
+	mkdir -p $(dir $@)
+	$(CC) -c $(1) -o $@ -D__NEARBY_SHORT_FILE__='"$(notdir $<)"' $<
+endef
+
+# Include all of the target specific variables and rules.
+# The $(ARCH).mk files should only depend on the variables listed above.
+include target/$(ARCH)/config.mk
+
+# Set trace verbosity from target specific variable
+CFLAGS += -DNEARBY_TRACE_LEVEL=NEARBY_TRACE_LEVEL_$(NEARBY_TRACE_LEVEL)
+# if directory doesn't exist, raise an error so we don't fail later with more cryptic warnings
+ifeq ($(wildcard common/target/$(ARCH_COMMON_NAME)),)
+$(error need to create directory common/target/$(ARCH_COMMON_NAME) )
+endif
+
+COMMON_INCLUDE_DIRS += \
+    -I. \
+    -I$(ARCH_COMMON_DIR) \
+    -Icommon/target \
+    -Icommon/target/$(ARCH_COMMON_NAME) \
+    -I$(COMMON_DIR)
+
+CLIENT_INCLUDES += \
+    -I$(CLIENT_DIR) \
+    -Iclient/target
+
+COMMON_C_FILES += \
+    $(wildcard $(COMMON_DIR)/*.c)
+
+CLIENT_SRCS += \
+    $(wildcard $(CLIENT_DIR)/*.c)
+
+CFLAGS += $(COMMON_INCLUDE_DIRS)
+
+CROSS_COMPILE ?= arm-none-eabi-
+CC     ?= $(CROSS_COMPILE)gcc
+AR     ?= $(CROSS_COMPILE)ar
+ALL_C_FILES = $(COMMON_C_FILES)
+COMMON_OBJS = $(patsubst %.c,$(OUT_DIR)/%.o,$(ALL_C_FILES))
+CLIENT_OBJS = $(patsubst %.c,$(OUT_DIR)/%.o,$(CLIENT_SRCS))
+NEARBY_LIB_OBJS = $(COMMON_OBJS) $(CLIENT_OBJS)
+
+NEARBY_HEADERS += $(patsubst common/target/%.h,$(DIST_DIR)/%.h,$(wildcard common/target/*.h))
+NEARBY_HEADERS += $(patsubst common/target/$(ARCH_COMMON_NAME)/%.h,$(DIST_DIR)/%.h,$(wildcard common/target/$(ARCH_COMMON_NAME)/*.h))
+NEARBY_HEADERS += $(patsubst client/target/%,$(DIST_DIR)/%,$(wildcard client/target/*.h))
+
+NEARBY_DIST_HEADERS := $(NEARBY_HEADERS)
+
+DIST_LIST += $(NEARBY_DIST_HEADERS)
+DIST_LIST += $(DIST_NEARBY_LIB_NAME)
+
+$(DIST_DIR)/nearby.h: $(FIRMWARE_VERSION_FILENAME)
+$(DIST_DIR)/nearby.h: common/target/nearby.h
+	@mkdir -p $(dir $@)
+	$(call copy_dist_header)
+	sed -i -e "s/\(define\s*NEARBY_BUILD_ID\).*/\1 \"$(FIRMWARE_VERSION)\"/" $@
+
+define copy_dist_header
+	@mkdir -p $(dir $@)
+	@test "$$(head -n 2 $< | grep 'Copyright [0-9]\{4\} Google LLC.')" != "" || \
+		(echo ; echo  "=====ERROR=====" ; echo "Missing copyright in $<" ; exit 1)
+	unifdef -t -b -x2 $(UNIFDEF_ARG) -o - $<  | cat - >$@
+	@# (mkartoz) Should be able to specify $@ as the output directly from
+	@# unifdef but, if you do that, the file never shows up on kokoro. Piping to
+	@# cat is a workaround.
+endef
+
+# Do not bypass the whole build process if the dist directory exists
+.PHONY: dist
+dist : all \
+     $(DIST_LIST)
+
+$(DIST_DIR)/$(NEARBY_LIB_NAME) : $(NAME)
+	mkdir -p $(dir $@)
+	cp -uv $< $@
+
+$(DIST_DIR)/%.h : common/target/%.h
+	$(call copy_dist_header)
+
+$(DIST_DIR)/%.h : common/target/$(ARCH_COMMON_NAME)/%.h
+	$(call copy_dist_header)
+
+$(NAME): $(NEARBY_LIB_OBJS)
+	mkdir -p $(dir $@)
+	$(AR) rcs $@ $(NEARBY_LIB_OBJS)
+
+# If the hash has changed then we need to rebuild nearby.o and nearby_client.o
+$(filter %/nearby_client.o, $(NEARBY_LIB_OBJS)): $(FIRMWARE_VERSION_FILENAME)
+$(filter %/nearby.o, $(NEARBY_LIB_OBJS)): $(FIRMWARE_VERSION_FILENAME)
+
+$(CLIENT_OBJS) : $(OUT_DIR)/%.o : %.c
+	$(call compile_c, $(CFLAGS) -Icommon/target $(CLIENT_INCLUDES))
+
+$(COMMON_OBJS) : $(OUT_DIR)/%.o: %.c
+	$(call compile_c,$(CFLAGS))
+
+ALL_OBJS += $(NEARBY_LIB_OBJS) $(CLIENT_OBJS)
+
+DEPFILES = $(ALL_OBJS:.o=.d)
+
+-include $(DEPFILES)
+
+.PHONY: clean
+clean:
+	rm -f $(NAME)
+	rm -f $(ALL_OBJS) $(DEPFILES)
+	rm -f $(FIRMWARE_VERSION_FILENAME) $(PHONY_VERSION_FILENAME)
+	rm -f $(DIST_LIST) $(TEST_BINARIES)
+
+-include target/$(ARCH)/rules.mk
diff --git a/embedded/README.md b/embedded/README.md
new file mode 100644
index 0000000..83ce5a9
--- /dev/null
+++ b/embedded/README.md
@@ -0,0 +1,31 @@
+# Nearby embedded SDK library
+
+This repository contains Nearby SDK library for embedded systems. Nearby SDK
+implements the Fast Pair protocol and its future versions per
+https://developers.google.com/nearby/fast-pair/spec
+
+
+## Threading model
+
+Nearby SDK assumes a single-threading model. All calls to the SDK must be on the same thread, or protected with a mutex. That includes:
+- client API, for example `nearby_fp_client_SetAdvertisement()`
+- ble and other event callbacks
+- timers
+
+## Porting guidelines
+
+1. Follow the steps at [Fast Pair Help](https://developers.google.com/nearby/fast-pair/help) to
+   register a Model Id for your device.
+1. Review `nearby_config.h` and disable features, if any, that you don't
+   wish to support.
+1. Implement the HAL defined in `nearby_platform_*.h` headers.
+1. Set platform specific compile flags in `config.mk` - see 
+   `target/gLinux/config.mk` for inspiration, and add your platform in
+   `build.sh`, or use your own build system.
+1. Compile with `./build.sh <your platform>`
+1. Start advertising in your application. For example
+    ```
+    nearby_fp_client_Init(NULL);
+    nearby_fp_client_SetAdvertisement(NEARBY_FP_ADVERTISEMENT_DISCOVERABLE);
+    ```
+1. Use [Fast Pair Validator](https://play.google.com/store/apps/details?id=com.google.location.nearby.apps.fastpair.validator) to verify that your device is behaving correctly.
\ No newline at end of file
diff --git a/embedded/build.sh b/embedded/build.sh
new file mode 100755
index 0000000..f23c12e
--- /dev/null
+++ b/embedded/build.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+#
+# Parameters:
+#   ${1} architecture (default: gLinux)
+#   ${2} make target (e.g. "dist")
+#     (remaining params are also passed to make)
+#
+# Checks for environment variables:
+#     NEARBY_MAKEFILE_DEBUG: if set, passes -d argument to make
+#     many others, not fully documented here yet
+#
+# e.g.
+#  ./build.sh gLinux dist DEBUG=1
+
+if [ "${1}" == "help" ] ; then
+  echo "Usage:"
+  echo "    ./build.sh <target> [dist] [args]"
+  echo ""
+  echo "Optional arguments:"
+  echo "    DEBUG=1"
+  echo "    -j72                           # parallelizes build using 72 cores"
+  exit -1;
+fi
+
+if [ "${1}" != "" ] ; then
+  ARCH="${1}"
+  shift 1
+else
+  ARCH=gLinux
+fi
+
+CC=arm-none-eabi-gcc
+AR=arm-none-eabi-ar
+
+UNAME_OUT="$(uname -s)"
+case "${UNAME_OUT}" in
+    Linux*)     MACHINE=linux;;
+    Darwin*)    MACHINE=mac;;
+    CYGWIN*)    MACHINE=cygwin;;
+    MINGW*)     MACHINE=mingw;;
+    *)          MACHINE=windows;;
+esac
+
+if [ "${ARCH}" = "gLinux" ] ; then
+  if [ -x /usr/bin/clang ] ; then
+    CC=clang
+  else
+    CC=clang-3.8
+  fi
+  AR=ar
+else
+  echo "Invalid target specified on command line ${ARCH}"
+  exit 1
+fi
+
+export NEARBY=$(cd `dirname ${0}` && pwd)
+
+PLATFORM=$(uname -s | grep -i '\(mingw\|cygwin\)')
+echo $PLATFORM
+
+make_args=
+if [ ! -z "$NEARBY_MAKEFILE_DEBUG" ]; then
+  echo "NEARBY_MAKEFILE_DEBUG is set, passing -d to make"
+  make_args+=" -d"
+fi
+
+make \
+  $make_args \
+  -C "${NEARBY}" \
+  CC="${CC}" \
+  AR="${AR}" \
+  ARCH="${ARCH}" \
+  $@
+
+make_rc=$?
+
+# NOTE: if make fails, it is important that the bad return code is bubbled up
+# to the caller of this script.
+# We don't want automated processes to keep going if there was a build error.
+
+if [ $make_rc -ne 0 ]; then
+  exit $make_rc
+fi
diff --git a/embedded/client/source/nearby_fp_client.c b/embedded/client/source/nearby_fp_client.c
new file mode 100644
index 0000000..5965754
--- /dev/null
+++ b/embedded/client/source/nearby_fp_client.c
@@ -0,0 +1,1454 @@
+// Copyright 2022 Google LLC
+//
+// 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 "nearby_fp_client.h"
+
+#include <string.h>
+
+#include "nearby.h"
+#include "nearby_fp_library.h"
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+#include "nearby_platform_battery.h"
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+#ifdef NEARBY_FP_MESSAGE_STREAM
+#include "nearby_message_stream.h"
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+#include "nearby_assert.h"
+#include "nearby_platform_audio.h"
+#include "nearby_platform_ble.h"
+#include "nearby_platform_bt.h"
+#include "nearby_platform_os.h"
+#include "nearby_platform_persistence.h"
+#include "nearby_platform_se.h"
+#include "nearby_trace.h"
+#include "nearby_utils.h"
+
+#define ENCRYPTED_REQUEST_LENGTH 16
+#define PUBLIC_KEY_LENGTH 64
+#define REQUEST_BT_ADDRESS_OFFSET 2
+
+// block pairing attempts for 5 minutes after 10 failures
+#define MAX_PAIRING_FAILURE_COUNT 10
+#define REJECT_PAIRING_TIMEOUT_MS (5 * 60 * 1000)
+#define PASSKEY_MAX_WAIT_TIME_MS 10000
+#define WAIT_FOR_PAIRING_REQUEST_TIME_MS 10000
+#define ACCOUNT_KEY_WRITE_TIME_MS 60000
+#define RETRO_PAIRING_REQUEST_TIME_MS 60000
+// Defines the timeout when waiting for pairing result when the Seeker writes
+// the Account Key before/during pairing.
+#define WAIT_FOR_PAIRING_RESULT_AFTER_ACCOUNT_KEY_TIME_MS 60000
+#define KEY_BASED_PAIRING_REQUEST_FLAG 0x00
+#define ACTION_REQUEST_FLAG 0x10
+
+// KBPR - Key Based Pairing Request
+#define KBPR_INITIATE_PAIRING_MASK (1 << 6)
+#define KBPR_NOTIFY_EXISTING_NAME_MASK (1 << 5)
+#define KBPR_RETROACTIVELY_WRITE_ACCOUNT_KEY_MASK (1 << 4)
+#define KBPR_SEEKER_ADDRESS_OFFSET 8
+
+#define ACTION_REQUEST_DEVICE_ACTION_MASK (1 << 7)
+#define ACTION_REQUEST_WILL_WRITE_DATA_CHARACTERISTIC_MASK (1 << 6)
+
+#define SEEKER_PASSKEY_MESSAGE_TYPE 0x02
+#define PROVIDER_PASSKEY_MESSAGE_TYPE 0x03
+#define ACCOUNT_KEY_WRITE_MESSAGE_TYPE 0x04
+
+#define PERSONALIZED_NAME_DATA_ID 1
+
+#define INVALID_BATTERY_LEVEL (-1)
+
+#define INVALID_PEER_ADDRESS 0
+
+// One byte per left, right and charging case
+#define BATTERY_LEVELS_SIZE 3
+
+// Size of battery remaining time (16 bits)
+#define BATTERY_TIME_SIZE 2
+
+// The BLE address should be rotated on average every 1024 seconds
+#define ADDRESS_ROTATION_PERIOD_MS 1024000
+
+enum PairingState {
+  kPairingStateIdle,
+  kPairingStateWaitingForPairingRequest,
+  kPairingStateWaitingForPasskey,
+  kPairingStateWaitingForPairingResult,
+  kPairingStateWaitingForAccountKeyWrite,
+  kPairingStateWaitingForAdditionalData
+} pairing_state;
+static const nearby_fp_client_Callbacks* client_callbacks;
+static unsigned int timeout_start_ms;
+static uint8_t pairing_failure_count;
+static unsigned int reject_pairing_time_start_ms;
+static uint64_t peer_public_address;
+static int advertisement_mode;
+static uint8_t account_key[ACCOUNT_KEY_SIZE_BYTES];
+static uint8_t pending_account_key[ACCOUNT_KEY_SIZE_BYTES];
+static uint64_t gatt_peer_address;
+static uint32_t address_rotation_timestamp;
+static void* address_rotation_task = NULL;
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+static uint8_t additional_data_id;
+#endif /* NEARBY_FP_ENABLE_ADDITIONAL_DATA */
+
+#define RETURN_IF_ERROR(X)                        \
+  do {                                            \
+    nearby_platform_status status = X;            \
+    if (kNearbyStatusOK != status) return status; \
+  } while (0)
+
+#define BIT(b) (1 << b)
+#define ISSET(v, b) (v & BIT(b))
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+// Callback triggered when a complete message is received over message stream
+static void OnMessageReceived(uint64_t peer_address,
+                              nearby_message_stream_Message* message);
+static void SendBleAddressUpdatedToAll();
+
+typedef struct {
+  nearby_message_stream_State state;
+  uint8_t buffer[MAX_MESSAGE_STREAM_PAYLOAD_SIZE +
+                 sizeof(nearby_message_stream_Metadata)];
+  uint8_t capabilities;
+  uint8_t platform_type;
+  uint8_t platform_build;
+} rfcomm_input;
+
+static void InitRfcommInput(uint64_t peer_address, rfcomm_input* input) {
+  memset(input, 0, sizeof(*input));
+  input->state.peer_address = peer_address;
+  input->state.on_message_received = OnMessageReceived;
+  input->state.length = sizeof(input->buffer);
+  input->state.buffer = input->buffer;
+  // defaults to: companion app installed, silence mode supported
+  input->capabilities = BIT(MESSAGE_CODE_CAPABILITIES_COMPANION_APP_INSTALLED) |
+                        BIT(MESSAGE_CODE_CAPABILITIES_SILENCE_MODE_SUPPORTED);
+  nearby_message_stream_Init(&input->state);
+}
+
+static rfcomm_input rfcomm_inputs[NEARBY_MAX_RFCOMM_CONNECTIONS];
+
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_RETROACTIVE_PAIRING
+typedef struct {
+  uint64_t peer_public_address;
+  uint64_t peer_le_address;
+  unsigned int retroactive_pairing_time_start_ms;
+} retroactive_pairing_peer;
+
+static retroactive_pairing_peer
+    retroactive_pairing_list[NEARBY_MAX_RETROACTIVE_PAIRING];
+
+static bool AddRetroactivePairingPeer(uint64_t peer_address) {
+  for (int i = 0; i < NEARBY_MAX_RETROACTIVE_PAIRING; i++) {
+    retroactive_pairing_peer* peer = &retroactive_pairing_list[i];
+
+    if (nearby_platform_GetCurrentTimeMs() -
+            peer->retroactive_pairing_time_start_ms >
+        RETRO_PAIRING_REQUEST_TIME_MS) {
+      peer->peer_public_address = INVALID_PEER_ADDRESS;
+      peer->peer_le_address = INVALID_PEER_ADDRESS;
+    }
+
+    if (peer->peer_public_address == peer_address) {
+      return false;
+    }
+  }
+
+  for (int i = 0; i < NEARBY_MAX_RETROACTIVE_PAIRING; i++) {
+    retroactive_pairing_peer* peer = &retroactive_pairing_list[i];
+    if (peer->peer_public_address == INVALID_PEER_ADDRESS) {
+      NEARBY_TRACE(INFO, "timer set for retroactive pairing");
+      peer->peer_public_address = peer_address;
+      peer->retroactive_pairing_time_start_ms =
+          nearby_platform_GetCurrentTimeMs();
+      return true;
+    }
+  }
+  return false;
+}
+
+static bool SetRetroactivePairingPeerLe(uint64_t peer_public_address,
+                                        uint64_t peer_le_address) {
+  for (int i = 0; i < NEARBY_MAX_RETROACTIVE_PAIRING; i++) {
+    retroactive_pairing_peer* peer = &retroactive_pairing_list[i];
+    if (peer->peer_public_address == peer_public_address) {
+      peer->peer_le_address = peer_le_address;
+      return true;
+    }
+  }
+  return false;
+}
+
+static bool RetroactivePairingPeerPending(uint64_t peer_address) {
+  for (int i = 0; i < NEARBY_MAX_RETROACTIVE_PAIRING; i++) {
+    retroactive_pairing_peer* peer = &retroactive_pairing_list[i];
+    if (peer->peer_public_address == peer_address ||
+        peer->peer_le_address == peer_address) {
+      return true;
+    }
+  }
+  return false;
+}
+
+static bool RetroactivePairingPeerTimeout(uint64_t peer_address) {
+  for (int i = 0; i < NEARBY_MAX_RETROACTIVE_PAIRING; i++) {
+    retroactive_pairing_peer* peer = &retroactive_pairing_list[i];
+    if ((peer->peer_public_address == peer_address ||
+         peer->peer_le_address == peer_address) &&
+        (nearby_platform_GetCurrentTimeMs() -
+             peer->retroactive_pairing_time_start_ms >
+         RETRO_PAIRING_REQUEST_TIME_MS)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+static void RemoveRetroactivePairingPeer(uint64_t peer_address) {
+  for (int i = 0; i < NEARBY_MAX_RETROACTIVE_PAIRING; i++) {
+    retroactive_pairing_peer* peer = &retroactive_pairing_list[i];
+    if (peer->peer_public_address == peer_address ||
+        peer->peer_le_address == peer_address) {
+      peer->peer_public_address = INVALID_PEER_ADDRESS;
+      peer->peer_le_address = INVALID_PEER_ADDRESS;
+    }
+  }
+}
+#endif /* NEARBY_FP_RETROACTIVE_PAIRING */
+
+static bool BtAddressMatch(uint8_t* a, uint8_t* b) {
+  return 0 == memcmp(a, b, BT_ADDRESS_LENGTH);
+}
+
+static bool ShowPairingIndicator() {
+  return advertisement_mode & NEARBY_FP_ADVERTISEMENT_PAIRING_UI_INDICATOR;
+}
+
+static bool ShowBatteryIndicator() {
+  return advertisement_mode & NEARBY_FP_ADVERTISEMENT_BATTERY_UI_INDICATOR;
+}
+
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+static bool IncludeBatteryInfo() {
+  return advertisement_mode & NEARBY_FP_ADVERTISEMENT_INCLUDE_BATTERY_INFO;
+}
+
+static bool IsInPairingMode() {
+  return nearby_platform_IsInPairingMode() ||
+         (pairing_state != kPairingStateIdle &&
+          pairing_state != kPairingStateWaitingForAccountKeyWrite &&
+          pairing_state != kPairingStateWaitingForAdditionalData);
+}
+
+// Gets battery info. Returns the input BatteryInfo or NULL on error.
+static nearby_platform_BatteryInfo* PrepareBatteryInfo(
+    nearby_platform_BatteryInfo* battery_info) {
+  nearby_platform_status status;
+
+  battery_info->is_charging = false;
+  battery_info->right_bud_battery_level = battery_info->left_bud_battery_level =
+      battery_info->charging_case_battery_level = INVALID_BATTERY_LEVEL;
+  battery_info->remaining_time_minutes = 0;
+  status = nearby_platform_GetBatteryInfo(battery_info);
+  if (status != kNearbyStatusOK) {
+    NEARBY_TRACE(ERROR, "GetBatteryInfo() failed with error %d", status);
+    return NULL;
+  }
+  return battery_info;
+}
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+static nearby_platform_status SendBatteryInfoMessage(uint64_t peer_address) {
+  uint8_t levels[BATTERY_LEVELS_SIZE];
+  nearby_message_stream_Message message = {
+      .message_group = MESSAGE_GROUP_DEVICE_INFORMATION_EVENT,
+      .message_code = MESSAGE_CODE_BATTERY_UPDATED,
+      .length = sizeof(levels),
+      .data = levels};
+  nearby_platform_BatteryInfo battery_info;
+  if (!PrepareBatteryInfo(&battery_info)) return kNearbyStatusOK;
+
+  SerializeBatteryInfo(message.data, &battery_info);
+  return nearby_message_stream_Send(peer_address, &message);
+}
+
+static nearby_platform_status SendBatteryTimeMessage(uint64_t peer_address) {
+  uint8_t time[BATTERY_TIME_SIZE];
+  nearby_message_stream_Message message = {
+      .message_group = MESSAGE_GROUP_DEVICE_INFORMATION_EVENT,
+      .message_code = MESSAGE_CODE_REMAINING_BATTERY_TIME,
+      .length = 1,
+      .data = time};
+  nearby_platform_BatteryInfo battery_info;
+  if (!PrepareBatteryInfo(&battery_info)) return kNearbyStatusOK;
+
+  if (battery_info.remaining_time_minutes > 255) {
+    message.length = sizeof(int16_t);
+    message.data[0] = battery_info.remaining_time_minutes >> 8;
+    message.data[1] = battery_info.remaining_time_minutes & 0xff;
+  } else {
+    message.data[0] = battery_info.remaining_time_minutes;
+  }
+  return nearby_message_stream_Send(peer_address, &message);
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+static void AccountKeyRejected() {
+  if (++pairing_failure_count == MAX_PAIRING_FAILURE_COUNT) {
+    reject_pairing_time_start_ms = nearby_platform_GetCurrentTimeMs();
+  }
+}
+
+static void DiscardAccountKey() { memset(account_key, 0, sizeof(account_key)); }
+
+static bool ShouldTimeout(unsigned int timeout_ms) {
+  return nearby_platform_GetCurrentTimeMs() - timeout_start_ms > timeout_ms;
+}
+
+static bool HasPendingAccountKey() {
+  return pending_account_key[0] == ACCOUNT_KEY_WRITE_MESSAGE_TYPE;
+}
+
+static void DiscardPendingAccountKey() {
+  memset(pending_account_key, 0, sizeof(pending_account_key));
+}
+
+static void RotateBleAddress() {
+  address_rotation_timestamp = nearby_platform_GetCurrentTimeMs();
+  nearby_platform_SetAdvertisement(NULL, 0, kDisabled);
+#ifdef NEARBY_FP_HAVE_BLE_ADDRESS_ROTATION
+  uint64_t address = nearby_platform_RotateBleAddress();
+  NEARBY_TRACE(INFO, "Rotated BLE address to: 0x%lx", address);
+#else
+  unsigned i;
+  uint64_t address = 0;
+  for (i = 0; i < BT_ADDRESS_LENGTH; i++) {
+    address = (address << 8) ^ nearby_platform_Rand();
+  }
+  address |= (uint64_t)1 << 46;
+  address &= ~((uint64_t)1 << 47);
+  NEARBY_TRACE(WARNING, "Rotate address to: 0x%lx", address);
+  nearby_platform_SetBleAddress(address);
+#ifdef NEARBY_FP_MESSAGE_STREAM
+  SendBleAddressUpdatedToAll();
+#endif
+#endif /* NEARBY_FP_HAVE_BLE_ADDRESS_ROTATION */
+}
+
+static nearby_platform_status SendKeyBasedPairingResponse(
+    uint64_t peer_address) {
+  nearby_platform_status status;
+  uint8_t raw[AES_MESSAGE_SIZE_BYTES], encrypted[AES_MESSAGE_SIZE_BYTES];
+  nearby_fp_CreateRawKeybasedPairingResponse(raw);
+  status = nearby_platform_Aes128Encrypt(raw, encrypted, account_key);
+  if (status != kNearbyStatusOK) {
+    NEARBY_TRACE(ERROR, "Failed to encrypt key-based pairing response");
+    return status;
+  }
+  status = nearby_platform_GattNotify(peer_address, kKeyBasedPairing, encrypted,
+                                      sizeof(encrypted));
+  if (status != kNearbyStatusOK) {
+    NEARBY_TRACE(ERROR, "Failed to notify on key-based pairing response");
+  }
+  return status;
+}
+
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+static nearby_platform_status NotifyPersonalizedName(uint64_t peer_address) {
+  NEARBY_TRACE(VERBOSE, "NotifyPersonalizedName");
+  uint8_t data[ADDITIONAL_DATA_HEADER_SIZE + PERSONALIZED_NAME_MAX_SIZE];
+  size_t length = PERSONALIZED_NAME_MAX_SIZE;
+  nearby_platform_status status;
+  status = nearby_platform_LoadValue(
+      kStoredKeyPersonalizedName, data + ADDITIONAL_DATA_HEADER_SIZE, &length);
+  if (kNearbyStatusOK != status) {
+    NEARBY_TRACE(WARNING, "Failed to load personalized name, status: %d",
+                 status);
+    return status;
+  }
+  if (!length) NEARBY_TRACE(WARNING, "Empty personalized name");
+
+  length += ADDITIONAL_DATA_HEADER_SIZE;
+  status = nearby_fp_EncodeAdditionalData(data, length, account_key);
+  if (kNearbyStatusOK != status) {
+    NEARBY_TRACE(ERROR, "Failed to encrypt additional data, status: %d",
+                 status);
+    return status;
+  }
+  status =
+      nearby_platform_GattNotify(peer_address, kAdditionalData, data, length);
+  if (kNearbyStatusOK != status) {
+    NEARBY_TRACE(ERROR, "Failed to notify on additional data, status: %d",
+                 status);
+  }
+  return kNearbyStatusOK;
+}
+
+static nearby_platform_status SaveAdditionalData(const uint8_t* data,
+                                                 size_t length) {
+  if (additional_data_id == PERSONALIZED_NAME_DATA_ID) {
+    char name[PERSONALIZED_NAME_MAX_SIZE * sizeof(uint8_t) / sizeof(char) + 1];
+    memcpy(name, data, length);
+    name[length * sizeof(uint8_t) / sizeof(char)] = '\0';
+    NEARBY_TRACE(INFO, "Saving personalized name: %s", name);
+    nearby_platform_SetDeviceName(name);
+    return nearby_platform_SaveValue(kStoredKeyPersonalizedName, data, length);
+  }
+  NEARBY_TRACE(WARNING, "Unsupported data id 0x%02x", additional_data_id);
+  return kNearbyStatusUnsupported;
+}
+
+static nearby_platform_status OnAdditionalDataWrite(uint64_t peer_address,
+                                                    const uint8_t* request,
+                                                    size_t length) {
+  NEARBY_TRACE(VERBOSE, "OnAdditionalDataWrite");
+  nearby_platform_status status;
+  if (pairing_state != kPairingStateWaitingForAdditionalData) {
+    NEARBY_TRACE(WARNING,
+                 "Not expecting a write to Additional Data. Current state: %d",
+                 pairing_state);
+    return kNearbyStatusError;
+  }
+  status =
+      nearby_fp_DecodeAdditionalData((uint8_t*)request, length, account_key);
+  DiscardAccountKey();
+  if (kNearbyStatusOK == status) {
+    status = SaveAdditionalData(request + ADDITIONAL_DATA_HEADER_SIZE,
+                                length - ADDITIONAL_DATA_HEADER_SIZE);
+  }
+  pairing_state = kPairingStateIdle;
+  return kNearbyStatusUnsupported;
+}
+#endif /* NEARBY_FP_ENABLE_ADDITIONAL_DATA */
+
+static nearby_platform_status HandleKeyBasedPairingRequest(
+    uint64_t peer_address, uint8_t request[ENCRYPTED_REQUEST_LENGTH]) {
+  NEARBY_TRACE(VERBOSE, "HandleKeyBasedPairingRequest");
+  int flags;
+  flags = request[1];
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+  if (flags & KBPR_NOTIFY_EXISTING_NAME_MASK) {
+    NotifyPersonalizedName(peer_address);
+  }
+#endif /* NEARBY_FP_ENABLE_ADDITIONAL_DATA */
+
+  if (flags & KBPR_RETROACTIVELY_WRITE_ACCOUNT_KEY_MASK) {
+    if (flags & KBPR_INITIATE_PAIRING_MASK)
+      NEARBY_TRACE(WARNING,
+                   "received flag Initiate pairing in retroactive pairing");
+
+#ifdef NEARBY_FP_RETROACTIVE_PAIRING
+    uint64_t peer_public_address =
+        nearby_utils_GetBigEndian48(request + KBPR_SEEKER_ADDRESS_OFFSET);
+
+    if (RetroactivePairingPeerPending(peer_public_address) == false) {
+      NEARBY_TRACE(
+          ERROR, "Ignoring retroactive pairing request from unexpected client");
+      RemoveRetroactivePairingPeer(peer_public_address);
+      return kNearbyStatusError;
+    }
+
+    if (SetRetroactivePairingPeerLe(peer_public_address, peer_address) ==
+        false) {
+      NEARBY_TRACE(ERROR, "Cannot set le address for retroactive pairing");
+      RemoveRetroactivePairingPeer(peer_public_address);
+      return kNearbyStatusError;
+    }
+
+    return kNearbyStatusOK;
+#else
+    return kNearbyStatusUnsupported;
+#endif /* NEARBY_FP_RETROACTIVE_PAIRING */
+  }
+
+  nearby_platform_SetFastPairCapabilities();
+  if (flags & KBPR_INITIATE_PAIRING_MASK) {
+    peer_public_address =
+        nearby_utils_GetBigEndian48(request + KBPR_SEEKER_ADDRESS_OFFSET);
+    NEARBY_TRACE(INFO, "Send pairing request to 0x%llx", peer_public_address);
+    nearby_platform_SendPairingRequest(peer_public_address);
+    pairing_state = kPairingStateWaitingForPasskey;
+  } else {
+    pairing_state = kPairingStateWaitingForPairingRequest;
+    timeout_start_ms = nearby_platform_GetCurrentTimeMs();
+  }
+  return kNearbyStatusOK;
+}
+
+static nearby_platform_status HandleActionRequest(
+    uint64_t peer_address, uint8_t request[ENCRYPTED_REQUEST_LENGTH]) {
+  NEARBY_TRACE(VERBOSE, "HandleActionRequest");
+  int flags;
+  flags = request[1];
+  if (flags & ACTION_REQUEST_DEVICE_ACTION_MASK) {
+    NEARBY_TRACE(VERBOSE, "Device action not implemented");
+    return kNearbyStatusUnimplemented;
+  }
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+  if (flags & ACTION_REQUEST_WILL_WRITE_DATA_CHARACTERISTIC_MASK) {
+    pairing_state = kPairingStateWaitingForAdditionalData;
+    additional_data_id = request[10];
+  }
+  return kNearbyStatusOK;
+#else
+  return kNearbyStatusUnsupported;
+#endif /* NEARBY_FP_ENABLE_ADDITIONAL_DATA */
+}
+
+static nearby_platform_status OnWriteKeyBasedPairing(uint64_t peer_address,
+                                                     const uint8_t* request,
+                                                     size_t length) {
+  nearby_platform_status status;
+  uint8_t decrypted_request[ENCRYPTED_REQUEST_LENGTH];
+  uint8_t ble_address[BT_ADDRESS_LENGTH];
+  uint8_t public_address[BT_ADDRESS_LENGTH];
+
+  NEARBY_TRACE(VERBOSE, "OnWriteKeyBasedPairing");
+  if (pairing_failure_count >= MAX_PAIRING_FAILURE_COUNT) {
+    unsigned int current_time = nearby_platform_GetCurrentTimeMs();
+    if (current_time - reject_pairing_time_start_ms <
+        REJECT_PAIRING_TIMEOUT_MS) {
+      NEARBY_TRACE(INFO, "Too many failed pairing attempts");
+      return kNearbyStatusOK;
+    } else {
+      NEARBY_TRACE(INFO, "Timeout expired. Allow pairing attempts");
+      pairing_failure_count = 0;
+    }
+  }
+  nearby_utils_CopyBigEndian(ble_address, nearby_platform_GetBleAddress(),
+                             BT_ADDRESS_LENGTH);
+  nearby_utils_CopyBigEndian(public_address, nearby_platform_GetPublicAddress(),
+                             BT_ADDRESS_LENGTH);
+  // When the device is nondiscoverable, accept a saved account key.
+  // or accept a new account key for retroactive pairing
+  // When the device is discoverable, we can accept a new account key too.
+  if (length == ENCRYPTED_REQUEST_LENGTH + PUBLIC_KEY_LENGTH) {
+    uint8_t key[ACCOUNT_KEY_SIZE_BYTES];
+    uint8_t remote_public_key[PUBLIC_KEY_LENGTH];
+    memcpy(remote_public_key, request + ENCRYPTED_REQUEST_LENGTH,
+           PUBLIC_KEY_LENGTH);
+    status = nearby_fp_CreateSharedSecret(remote_public_key, key);
+    if (status != kNearbyStatusOK) {
+      NEARBY_TRACE(ERROR, "Failed to create shared key, error: %d", status);
+      return status;
+    }
+    status = nearby_platform_Aes128Decrypt(request, decrypted_request, key);
+    if (status != kNearbyStatusOK) {
+      NEARBY_TRACE(ERROR, "Failed to decrypt request, error: %d", status);
+      return status;
+    }
+    if (!BtAddressMatch(ble_address,
+                        decrypted_request + REQUEST_BT_ADDRESS_OFFSET) &&
+        !BtAddressMatch(public_address,
+                        decrypted_request + REQUEST_BT_ADDRESS_OFFSET)) {
+      NEARBY_TRACE(INFO, "Invalid incoming BT address %02x%02x%02x%02x%02x%02x",
+                   decrypted_request[REQUEST_BT_ADDRESS_OFFSET],
+                   decrypted_request[REQUEST_BT_ADDRESS_OFFSET + 1],
+                   decrypted_request[REQUEST_BT_ADDRESS_OFFSET + 2],
+                   decrypted_request[REQUEST_BT_ADDRESS_OFFSET + 3],
+                   decrypted_request[REQUEST_BT_ADDRESS_OFFSET + 4],
+                   decrypted_request[REQUEST_BT_ADDRESS_OFFSET + 5]);
+      AccountKeyRejected();
+      return kNearbyStatusOK;
+    }
+    memcpy(account_key, key, ACCOUNT_KEY_SIZE_BYTES);
+  } else if (length == ENCRYPTED_REQUEST_LENGTH) {
+    // try each key in the persisted Account Key List
+    int num_keys = nearby_fp_GetAccountKeyCount();
+    int i;
+    for (i = 0; i < num_keys; i++) {
+      const uint8_t* key = nearby_fp_GetAccountKey(i);
+      status = nearby_platform_Aes128Decrypt(request, decrypted_request, key);
+      if (status != kNearbyStatusOK) {
+        NEARBY_TRACE(ERROR, "Failed to decrypt request, error: %d", status);
+        return status;
+      }
+      if (BtAddressMatch(ble_address,
+                         decrypted_request + REQUEST_BT_ADDRESS_OFFSET) ||
+          BtAddressMatch(public_address,
+                         decrypted_request + REQUEST_BT_ADDRESS_OFFSET)) {
+        NEARBY_TRACE(VERBOSE, "Matched key number: %d", i);
+        nearby_fp_CopyAccountKey(account_key, i);
+        nearby_fp_MarkAccountKeyAsActive(i);
+        nearby_fp_SaveAccountKeys();
+        break;
+      }
+    }
+    if (i == num_keys) {
+      NEARBY_TRACE(VERBOSE, "No key matched");
+      AccountKeyRejected();
+      return kNearbyStatusOK;
+    }
+  } else {
+    NEARBY_TRACE(WARNING, "Unexpected key based pairing request length %d",
+                 length);
+    return kNearbyStatusError;
+  }
+  pairing_failure_count = 0;
+  gatt_peer_address = peer_address;
+
+  DiscardPendingAccountKey();
+
+  status = SendKeyBasedPairingResponse(peer_address);
+  if (status != kNearbyStatusOK) return status;
+
+  // TODO(jsobczak): Note that at the end of the packet there is a salt
+  // attached. When possible, these salts should be tracked, and if the Provider
+  // receives a request containing an already used salt, the request should be
+  // ignored to prevent replay attacks.
+  if (decrypted_request[0] == KEY_BASED_PAIRING_REQUEST_FLAG) {
+    return HandleKeyBasedPairingRequest(peer_address, decrypted_request);
+  } else if (decrypted_request[0] == ACTION_REQUEST_FLAG) {
+    return HandleActionRequest(peer_address, decrypted_request);
+  }
+  return kNearbyStatusOK;
+}
+
+static nearby_platform_status NotifyProviderPasskey(uint64_t peer_address) {
+  uint8_t raw_passkey_block[AES_MESSAGE_SIZE_BYTES];
+  uint8_t encrypted[AES_MESSAGE_SIZE_BYTES];
+  uint32_t provider_passkey;
+  nearby_platform_status status;
+  provider_passkey = nearby_platfrom_GetPairingPassKey();
+
+  raw_passkey_block[0] = PROVIDER_PASSKEY_MESSAGE_TYPE;
+  nearby_utils_CopyBigEndian(raw_passkey_block + 1, provider_passkey, 3);
+  int i;
+  for (i = 4; i < AES_MESSAGE_SIZE_BYTES; i++) {
+    raw_passkey_block[i] = nearby_platform_Rand();
+  }
+  status =
+      nearby_platform_Aes128Encrypt(raw_passkey_block, encrypted, account_key);
+  if (status != kNearbyStatusOK) {
+    NEARBY_TRACE(ERROR, "Failed to encrypt passkey block");
+    return status;
+  }
+  status = nearby_platform_GattNotify(peer_address, kPasskey, encrypted,
+                                      sizeof(encrypted));
+  if (status != kNearbyStatusOK) {
+    NEARBY_TRACE(ERROR, "Failed to notify on passkey characteristic");
+  }
+  return status;
+}
+
+static nearby_platform_status OnPasskeyWrite(uint64_t peer_address,
+                                             const uint8_t* request,
+                                             size_t length) {
+  NEARBY_TRACE(VERBOSE, "OnPasskeyWrite");
+  uint8_t raw_passkey_block[AES_MESSAGE_SIZE_BYTES];
+  nearby_platform_status status;
+  uint32_t seeker_passkey;
+  if (pairing_state != kPairingStateWaitingForPasskey) {
+    NEARBY_TRACE(INFO, "Not expecting a passkey write. Current state: %d",
+                 pairing_state);
+    return kNearbyStatusError;
+  }
+  if (gatt_peer_address != peer_address) {
+    NEARBY_TRACE(INFO, "Ignoring passkey write from unexpected client");
+    return kNearbyStatusError;
+  }
+  if (ShouldTimeout(PASSKEY_MAX_WAIT_TIME_MS)) {
+    NEARBY_TRACE(INFO, "Not expecting a passkey write");
+    return kNearbyStatusTimeout;
+  }
+  if (length != AES_MESSAGE_SIZE_BYTES) {
+    NEARBY_TRACE(INFO, "Passkey: expected %d bytes, got %d",
+                 AES_MESSAGE_SIZE_BYTES, length);
+    return kNearbyStatusError;
+  }
+  status =
+      nearby_platform_Aes128Decrypt(request, raw_passkey_block, account_key);
+  if (status != kNearbyStatusOK) {
+    NEARBY_TRACE(WARNING, "Failed to decrypt passkey block");
+    DiscardAccountKey();
+    return status;
+  }
+  if (raw_passkey_block[0] != SEEKER_PASSKEY_MESSAGE_TYPE) {
+    NEARBY_TRACE(WARNING, "Unexpected passkey message type 0x%x",
+                 raw_passkey_block[0]);
+    return kNearbyStatusError;
+  }
+  pairing_state = kPairingStateWaitingForPairingResult;
+  NotifyProviderPasskey(peer_address);
+  seeker_passkey = nearby_utils_GetBigEndian24(raw_passkey_block + 1);
+  nearby_platform_SetRemotePasskey(seeker_passkey);
+  return kNearbyStatusOK;
+}
+
+static nearby_platform_status SetNonDiscoverableAdvertisement() {
+  uint8_t advertisement[NON_DISCOVERABLE_ADV_SIZE_BYTES];
+  size_t length;
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+  nearby_platform_BatteryInfo battery_info;
+  length = nearby_fp_CreateNondiscoverableAdvertisementWithBattery(
+      advertisement, sizeof(advertisement), ShowPairingIndicator(),
+      ShowBatteryIndicator(),
+      IncludeBatteryInfo() ? PrepareBatteryInfo(&battery_info) : NULL);
+#else
+  length = nearby_fp_CreateNondiscoverableAdvertisement(
+      advertisement, sizeof(advertisement), ShowPairingIndicator());
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+  length += nearby_fp_AppendTxPower(advertisement + length,
+                                    sizeof(advertisement) - length,
+                                    nearby_platform_GetTxLevel());
+  return nearby_platform_SetAdvertisement(advertisement, length,
+                                          kNoLargerThan250ms);
+}
+
+// Steps executed after successful pairing with a seeker and receiving the
+// account key.
+static void RunPostPairingSteps(uint64_t peer_address,
+                                const uint8_t* account_key) {
+  nearby_fp_AddAccountKey(account_key);
+  nearby_fp_SaveAccountKeys();
+  DiscardPendingAccountKey();
+#ifdef NEARBY_FP_RETROACTIVE_PAIRING
+  if (RetroactivePairingPeerPending(peer_address)) {
+    RemoveRetroactivePairingPeer(peer_address);
+
+    if (RetroactivePairingPeerPending(peer_address)) {
+      NEARBY_TRACE(WARNING, "peer is still pending 0x%llx", peer_address);
+    }
+    if (pairing_state != kPairingStateIdle) {
+      NEARBY_TRACE(WARNING,
+                   "Another fp pairing process has launched, do not change "
+                   "pairing state");
+      return;
+    }
+  }
+#endif /* NEARBY_FP_RETROACTIVE_PAIRING */
+
+  pairing_state = kPairingStateIdle;
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+  pairing_state = kPairingStateWaitingForAdditionalData;
+  additional_data_id = PERSONALIZED_NAME_DATA_ID;
+#endif
+  if (advertisement_mode & NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE) {
+    NEARBY_TRACE(INFO, "Account key added, update advertisement");
+    nearby_platform_SetAdvertisement(NULL, 0, kDisabled);
+    SetNonDiscoverableAdvertisement();
+  }
+}
+static nearby_platform_status OnAccountKeyWrite(uint64_t peer_address,
+                                                const uint8_t* request,
+                                                size_t length) {
+  NEARBY_TRACE(VERBOSE, "OnAccountKeyWrite");
+  uint8_t decrypted_request[AES_MESSAGE_SIZE_BYTES];
+  nearby_platform_status status;
+  bool wait_until_paired = false;
+
+#ifdef NEARBY_FP_RETROACTIVE_PAIRING
+  if (RetroactivePairingPeerPending(peer_address)) {
+    if (RetroactivePairingPeerTimeout(peer_address)) {
+      NEARBY_TRACE(ERROR,
+                   "Not expecting an retroactive pairing request. Timeout");
+      RemoveRetroactivePairingPeer(peer_address);
+      return kNearbyStatusTimeout;
+    }
+  } else {
+#endif /* NEARBY_FP_RETROACTIVE_PAIRING */
+    if (pairing_state == kPairingStateWaitingForPairingRequest ||
+        pairing_state == kPairingStateWaitingForPairingResult ||
+        pairing_state == kPairingStateWaitingForPasskey) {
+      NEARBY_TRACE(VERBOSE, "Account key write before paired event");
+      wait_until_paired = true;
+    } else if (pairing_state != kPairingStateWaitingForAccountKeyWrite) {
+      NEARBY_TRACE(INFO,
+                   "Not expecting an account key write. Current state: %d",
+                   pairing_state);
+      return kNearbyStatusError;
+    }
+    if (gatt_peer_address != peer_address &&
+        peer_public_address != peer_address) {
+      NEARBY_TRACE(INFO, "Ignoring account key write from unexpected client");
+      return kNearbyStatusError;
+    }
+
+    if (ShouldTimeout(ACCOUNT_KEY_WRITE_TIME_MS)) {
+      NEARBY_TRACE(INFO, "Not expecting an account key write. Timeout");
+      return kNearbyStatusTimeout;
+    }
+#ifdef NEARBY_FP_RETROACTIVE_PAIRING
+  }
+#endif /* NEARBY_FP_RETROACTIVE_PAIRING */
+  if (length != AES_MESSAGE_SIZE_BYTES) {
+    NEARBY_TRACE(INFO, "Account key: expected %d bytes, got %d",
+                 AES_MESSAGE_SIZE_BYTES, length);
+    return kNearbyStatusError;
+  }
+  status =
+      nearby_platform_Aes128Decrypt(request, decrypted_request, account_key);
+  if (status != kNearbyStatusOK) {
+    NEARBY_TRACE(WARNING, "Failed to decrypt account key block");
+    return status;
+  }
+  if (decrypted_request[0] != ACCOUNT_KEY_WRITE_MESSAGE_TYPE) {
+    NEARBY_TRACE(WARNING, "Unexpected account key message type 0x%x",
+                 decrypted_request[0]);
+    return kNearbyStatusError;
+  }
+  if (wait_until_paired) {
+    memcpy(pending_account_key, decrypted_request, ACCOUNT_KEY_SIZE_BYTES);
+    nearby_platform_StartTimer(
+        DiscardPendingAccountKey,
+        WAIT_FOR_PAIRING_RESULT_AFTER_ACCOUNT_KEY_TIME_MS);
+    return kNearbyStatusOK;
+  }
+  RunPostPairingSteps(peer_address, decrypted_request);
+  return kNearbyStatusOK;
+}
+
+static nearby_platform_status OnGattWrite(
+    uint64_t peer_address, nearby_fp_Characteristic characteristic,
+    const uint8_t* request, size_t length) {
+  switch (characteristic) {
+    case kKeyBasedPairing:
+      return OnWriteKeyBasedPairing(peer_address, request, length);
+    case kPasskey:
+      return OnPasskeyWrite(peer_address, request, length);
+    case kAccountKey:
+      return OnAccountKeyWrite(peer_address, request, length);
+    case kAdditionalData:
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+      return OnAdditionalDataWrite(peer_address, request, length);
+#else
+      break;
+#endif /* NEARBY_FP_ENABLE_ADDITIONAL_DATA */
+    case kModelId:
+    case kFirmwareRevision:
+      break;
+  }
+  return kNearbyStatusUnsupported;
+}
+
+static nearby_platform_status OnGattRead(
+    uint64_t peer_address, nearby_fp_Characteristic characteristic,
+    uint8_t* output, size_t* length) {
+  switch (characteristic) {
+    case kModelId:
+      return nearby_fp_GattReadModelId(output, length);
+    case kKeyBasedPairing:
+    case kPasskey:
+    case kAccountKey:
+    case kFirmwareRevision:
+    case kAdditionalData:
+      break;
+  }
+  return kNearbyStatusUnsupported;
+}
+
+static void OnPairingRequest(uint64_t peer_address) {
+  NEARBY_TRACE(VERBOSE, "Pairing request from 0x%lx", peer_address);
+  if (pairing_state == kPairingStateWaitingForPairingRequest) {
+    if (ShouldTimeout(WAIT_FOR_PAIRING_REQUEST_TIME_MS)) {
+      pairing_state = kPairingStateIdle;
+    } else {
+      pairing_state = kPairingStateWaitingForPasskey;
+      peer_public_address = peer_address;
+      timeout_start_ms = nearby_platform_GetCurrentTimeMs();
+    }
+  }
+}
+
+static void OnPaired(uint64_t peer_address) {
+  NEARBY_TRACE(INFO, "Paired with 0x%lx", peer_address);
+  if (peer_public_address == peer_address && HasPendingAccountKey()) {
+    NEARBY_TRACE(INFO, "Saving pending account key");
+    RunPostPairingSteps(peer_address, pending_account_key);
+    return;
+  }
+  peer_public_address = peer_address;
+  if (pairing_state == kPairingStateWaitingForPairingResult) {
+    pairing_state = kPairingStateWaitingForAccountKeyWrite;
+    timeout_start_ms = nearby_platform_GetCurrentTimeMs();
+  }
+#ifdef NEARBY_FP_RETROACTIVE_PAIRING
+  else if (AddRetroactivePairingPeer(peer_address) == false) {
+    NEARBY_TRACE(WARNING, "No more timer for retroactive pairing");
+  }
+#endif /* NEARBY_FP_RETROACTIVE_AIRING */
+}
+
+static void OnPairingFailed(uint64_t peer_address) {
+  NEARBY_TRACE(ERROR, "Pairing failed with 0x%lx", peer_address);
+  DiscardAccountKey();
+  DiscardPendingAccountKey();
+}
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+// Finds and initializes an unused |rfcomm_input|. Returns NULL if not found.
+static rfcomm_input* FindAvailableRfcommInput(uint64_t peer_address) {
+  for (int i = 0; i < NEARBY_MAX_RFCOMM_CONNECTIONS; i++) {
+    rfcomm_input* input = &rfcomm_inputs[i];
+    if (input->state.peer_address == INVALID_PEER_ADDRESS ||
+        input->state.peer_address == peer_address) {
+      InitRfcommInput(peer_address, input);
+      return input;
+    }
+  }
+  return NULL;
+}
+
+// Gets |rfcomm_input| for |peer_address|. Returns NULL if not found.
+static rfcomm_input* GetRfcommInput(uint64_t peer_address) {
+  for (int i = 0; i < NEARBY_MAX_RFCOMM_CONNECTIONS; i++) {
+    if (rfcomm_inputs[i].state.peer_address == peer_address) {
+      return &rfcomm_inputs[i];
+    }
+  }
+  return NULL;
+}
+
+nearby_platform_status nearby_fp_client_SetSilenceMode(uint64_t peer_address,
+                                                       bool enable) {
+  rfcomm_input* input = GetRfcommInput(peer_address);
+  if (!input) {
+    return kNearbyStatusError;
+  }
+  nearby_message_stream_Message message = {
+      .message_group = MESSAGE_GROUP_BLUETOOTH,
+      .message_code = MESSAGE_CODE_DISABLE_SILENCE_MODE,
+      .length = 0};
+  if (enable) message.message_code = MESSAGE_CODE_ENABLE_SILENCE_MODE;
+  if (!input) {
+    return kNearbyStatusError;
+  }
+  if (!ISSET(input->capabilities,
+             MESSAGE_CODE_CAPABILITIES_SILENCE_MODE_SUPPORTED)) {
+    return kNearbyStatusUnsupported;
+  }
+  return nearby_message_stream_Send(peer_address, &message);
+}
+
+nearby_platform_status nearby_fp_client_SignalLogBufferFull(
+    uint64_t peer_address) {
+  rfcomm_input* input = GetRfcommInput(peer_address);
+  if (!input) {
+    return kNearbyStatusError;
+  }
+  if (!ISSET(input->capabilities,
+             MESSAGE_CODE_CAPABILITIES_COMPANION_APP_INSTALLED)) {
+    return kNearbyStatusOK;
+  }
+  nearby_message_stream_Message message = {
+      .message_group = MESSAGE_GROUP_COMPANION_APP_EVENT,
+      .message_code = MESSAGE_CODE_LOG_BUFFER_FULL,
+      .length = 0};
+  return nearby_message_stream_Send(peer_address, &message);
+}
+
+static nearby_platform_status SendBleAddressUpdated(uint64_t peer_address) {
+  uint8_t buffer[BT_ADDRESS_LENGTH];
+  nearby_message_stream_Message message = {
+      .message_group = MESSAGE_GROUP_DEVICE_INFORMATION_EVENT,
+      .message_code = MESSAGE_CODE_BLE_ADDRESS_UPDATED,
+      .length = BT_ADDRESS_LENGTH,
+      .data = buffer};
+  nearby_utils_CopyBigEndian(message.data, nearby_platform_GetBleAddress(),
+                             BT_ADDRESS_LENGTH);
+  return nearby_message_stream_Send(peer_address, &message);
+}
+
+static void SendBleAddressUpdatedToAll() {
+  uint8_t buffer[BT_ADDRESS_LENGTH];
+  nearby_message_stream_Message message = {
+      .message_group = MESSAGE_GROUP_DEVICE_INFORMATION_EVENT,
+      .message_code = MESSAGE_CODE_BLE_ADDRESS_UPDATED,
+      .length = BT_ADDRESS_LENGTH,
+      .data = buffer};
+  nearby_utils_CopyBigEndian(message.data, nearby_platform_GetBleAddress(),
+                             BT_ADDRESS_LENGTH);
+  for (int i = 0; i < NEARBY_MAX_RFCOMM_CONNECTIONS; i++) {
+    rfcomm_input* input = &rfcomm_inputs[i];
+    if (input->state.peer_address != INVALID_PEER_ADDRESS) {
+      nearby_message_stream_Send(input->state.peer_address, &message);
+    }
+  }
+}
+
+static void OnMessageStreamConnected(uint64_t peer_address) {
+  nearby_platform_status status;
+  uint8_t buffer[MAX_MESSAGE_STREAM_PAYLOAD_SIZE];
+  nearby_message_stream_Message message;
+  size_t length = sizeof(buffer);
+  rfcomm_input* input;
+
+  NEARBY_TRACE(VERBOSE, "OnMessageStreamConnected 0x%lx", peer_address);
+  input = FindAvailableRfcommInput(peer_address);
+  if (!input) {
+    NEARBY_TRACE(WARNING, "Too many concurrent RFCOMM connections");
+    return;
+  }
+
+  message.data = buffer;
+  nearby_fp_GattReadModelId(message.data, &length);
+  message.length = length;
+  message.message_group = MESSAGE_GROUP_DEVICE_INFORMATION_EVENT;
+  message.message_code = MESSAGE_CODE_MODEL_ID;
+
+  status = nearby_message_stream_Send(peer_address, &message);
+  if (kNearbyStatusOK != status) {
+    NEARBY_TRACE(ERROR, "Failed to send model id, status: %d", status);
+    return;
+  }
+  status = SendBleAddressUpdated(peer_address);
+  if (kNearbyStatusOK != status) {
+    NEARBY_TRACE(ERROR, "Failed to send ble address, status: %d", status);
+    return;
+  }
+
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+  status = SendBatteryInfoMessage(peer_address);
+  if (kNearbyStatusOK != status) {
+    NEARBY_TRACE(ERROR, "Failed to send battery info, status: %d", status);
+    return;
+  }
+  status = SendBatteryTimeMessage(peer_address);
+  if (kNearbyStatusOK != status) {
+    NEARBY_TRACE(ERROR, "Failed to send battery info, status: %d", status);
+    return;
+  }
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+  if (client_callbacks != NULL && client_callbacks->on_event != NULL) {
+    nearby_event_MessageStreamConnected payload = {.peer_address =
+                                                       peer_address};
+    nearby_event_Event event = {
+        .event_type = kNearbyEventMessageStreamConnected,
+        .payload = (uint8_t*)&payload};
+    client_callbacks->on_event(&event);
+  }
+}
+
+static void OnMessageStreamDisconnected(uint64_t peer_address) {
+  rfcomm_input* input;
+
+  NEARBY_TRACE(VERBOSE, "OnMessageStreamDisconnected 0x%lx", peer_address);
+  input = GetRfcommInput(peer_address);
+  if (!input) {
+    NEARBY_TRACE(WARNING, "Unexpected disconnection from 0x%lx", peer_address);
+    return;
+  }
+  input->state.peer_address = INVALID_PEER_ADDRESS;
+
+  if (client_callbacks != NULL && client_callbacks->on_event != NULL) {
+    nearby_event_MessageStreamDisconnected payload = {.peer_address =
+                                                          peer_address};
+    nearby_event_Event event = {
+        .event_type = kNearbyEventMessageStreamDisconnected,
+        .payload = (uint8_t*)&payload};
+    client_callbacks->on_event(&event);
+  }
+#ifdef NEARBY_FP_RETROACTIVE_PAIRING
+  RemoveRetroactivePairingPeer(peer_address);
+#endif /*NEARBY_FP_RETROACTIVE_PAIRING */
+}
+
+static void OnMessageStreamReceived(uint64_t peer_address,
+                                    const uint8_t* message, size_t length) {
+  rfcomm_input* input = GetRfcommInput(peer_address);
+  if (!input) {
+    NEARBY_TRACE(WARNING, "Unexpected RFCOMM message from 0x%lx", peer_address);
+    return;
+  }
+  nearby_message_stream_Read(&input->state, message, length);
+}
+
+static nearby_platform_status VerifyMessageLength(
+    uint64_t peer_address, const nearby_message_stream_Message* message,
+    size_t expected_length) {
+  if (message->length != expected_length) {
+    NEARBY_TRACE(WARNING, "Invalid message(%d) length %d, expected %d",
+                 message->message_code, message->length, expected_length);
+    nearby_message_stream_SendNack(peer_address, message, /* fail reason= */ 0);
+    return kNearbyStatusInvalidInput;
+  }
+  return kNearbyStatusOK;
+}
+
+// Sends either ACK or NACK response depending on |status|
+static nearby_platform_status SendResponse(
+    uint64_t peer_address, const nearby_message_stream_Message* message,
+    nearby_platform_status status) {
+  if (kNearbyStatusOK == status) {
+    return nearby_message_stream_SendAck(peer_address, message);
+  } else {
+    // TODO(jsobczak): What should be the default fail reason?
+    uint8_t fail_reason = status == kNearbyStatusRedundantAction
+                              ? FAIL_REASON_REDUNDANT_DEVICE_ACTION
+                              : 0;
+    return nearby_message_stream_SendNack(peer_address, message, fail_reason);
+  }
+}
+
+static void PrepareActiveComponentResponse(
+    nearby_message_stream_Message* message) {
+  NEARBY_ASSERT(message->length >= 2 * sizeof(uint16_t));
+  message->message_code = MESSAGE_CODE_ACTIVE_COMPONENT_RESPONSE;
+  message->length = 1;
+  message->data[0] = nearby_platform_GetEarbudLeftStatus() << 1 |
+                     nearby_platform_GetEarbudRightStatus();
+}
+
+static nearby_platform_status HandleGeneralMessage(
+    uint64_t peer_address, nearby_message_stream_Message* message) {
+  rfcomm_input* input = GetRfcommInput(peer_address);
+  NEARBY_ASSERT(input != NULL);
+  uint8_t buffer[MAX_MESSAGE_STREAM_PAYLOAD_SIZE];
+  nearby_message_stream_Message reply;
+  reply.data = buffer;
+  reply.length = sizeof(buffer);
+  switch (message->message_group) {
+    case MESSAGE_GROUP_DEVICE_INFORMATION_EVENT: {
+      reply.message_group = MESSAGE_GROUP_DEVICE_INFORMATION_EVENT;
+      switch (message->message_code) {
+        case MESSAGE_CODE_ACTIVE_COMPONENT_REQUEST: {
+          PrepareActiveComponentResponse(&reply);
+          return nearby_message_stream_Send(peer_address, &reply);
+        }
+        case MESSAGE_CODE_CAPABILITIES: {
+          RETURN_IF_ERROR(VerifyMessageLength(peer_address, message, 1));
+          uint8_t flags = message->data[0];
+          NEARBY_TRACE(INFO, "Set capabilities: 0x%x", flags);
+          input->capabilities = flags;
+          return kNearbyStatusOK;
+        }
+        case MESSAGE_CODE_PLATFORM_TYPE: {
+          RETURN_IF_ERROR(VerifyMessageLength(peer_address, message, 2));
+          uint8_t type = message->data[0];
+          uint8_t build = message->data[1];
+          NEARBY_TRACE(INFO, "Set platform type: 0x%x:0x%x", type, build);
+          input->platform_type = type;
+          input->platform_build = build;
+          return kNearbyStatusOK;
+        }
+      }
+    }
+    case MESSAGE_GROUP_DEVICE_ACTION_EVENT: {
+      reply.message_group = MESSAGE_GROUP_DEVICE_ACTION_EVENT;
+      switch (message->message_code) {
+        case MESSAGE_CODE_RING: {
+          if (message->length < 1 || message->length > 2) {
+            NEARBY_TRACE(WARNING, "Invalid message(%d) length %d",
+                         message->message_code, message->length);
+            nearby_message_stream_SendNack(peer_address, message,
+                                           /* fail reason= */ 0);
+            return kNearbyStatusInvalidInput;
+          }
+          uint8_t command = message->data[0];
+          uint16_t timeout = 0;
+          if (message->length > 1) timeout = message->data[1];
+          NEARBY_TRACE(INFO, "Set ring device: 0x%x %d", command, timeout);
+          return SendResponse(peer_address, message,
+                              nearby_platform_Ring(command, timeout * 10));
+        }
+      }
+    }
+  }
+  return kNearbyStatusOK;
+}
+
+static void OnMessageReceived(uint64_t peer_address,
+                              nearby_message_stream_Message* message) {
+  nearby_platform_status status;
+  status = HandleGeneralMessage(peer_address, message);
+  if (kNearbyStatusOK != status) {
+    NEARBY_TRACE(WARNING, "Processing stream message failed with %d", status);
+    return;
+  }
+  if (client_callbacks != NULL && client_callbacks->on_event != NULL) {
+    nearby_event_MessageStreamReceived payload = {
+        .peer_address = peer_address,
+        .message_group = message->message_group,
+        .message_code = message->message_code,
+        .length = message->length,
+        .data = message->data};
+    nearby_event_Event event = {.event_type = kNearbyEventMessageStreamReceived,
+                                .payload = (uint8_t*)&payload};
+    client_callbacks->on_event(&event);
+  }
+}
+
+nearby_platform_status nearby_fp_client_SendMessage(
+    uint64_t peer_address, const nearby_message_stream_Message* message) {
+  return nearby_message_stream_Send(peer_address, message);
+}
+
+nearby_platform_status nearby_fp_client_SendAck(
+    const nearby_event_MessageStreamReceived* message) {
+  nearby_message_stream_Message stream_message = {
+      .message_group = message->message_group,
+      .message_code = message->message_code,
+  };
+  return nearby_message_stream_SendAck(message->peer_address, &stream_message);
+}
+
+nearby_platform_status nearby_fp_client_SendNack(
+    const nearby_event_MessageStreamReceived* message, uint8_t fail_reason) {
+  nearby_message_stream_Message stream_message = {
+      .message_group = message->message_group,
+      .message_code = message->message_code,
+  };
+  return nearby_message_stream_SendNack(message->peer_address, &stream_message,
+                                        fail_reason);
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+static void OnBatteryChanged(void) {
+  nearby_platform_status status;
+  if (pairing_state != kPairingStateIdle &&
+      pairing_state != kPairingStateWaitingForAccountKeyWrite &&
+      pairing_state != kPairingStateWaitingForAdditionalData) {
+    NEARBY_TRACE(ERROR, "%s: device is in pairing process", __func__);
+    return;
+  }
+
+  if (IncludeBatteryInfo() &&
+      (advertisement_mode & NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE)) {
+    status = nearby_platform_SetAdvertisement(NULL, 0, kDisabled);
+    if (status != kNearbyStatusOK) {
+      NEARBY_TRACE(ERROR, "Failed to update battery change, status: %d",
+                   status);
+      return;
+    }
+
+    status = SetNonDiscoverableAdvertisement();
+    if (status != kNearbyStatusOK) {
+      NEARBY_TRACE(ERROR, "Failed to update battery change, status: %d",
+                   status);
+      return;
+    }
+  }
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+  for (int i = 0; i < NEARBY_MAX_RFCOMM_CONNECTIONS; i++) {
+    rfcomm_input* input = &rfcomm_inputs[i];
+    if (input->state.peer_address != INVALID_PEER_ADDRESS) {
+      status = SendBatteryInfoMessage(input->state.peer_address);
+      if (status != kNearbyStatusOK)
+        NEARBY_TRACE(ERROR, "Failed to send battery change, status: %d",
+                     status);
+      status = SendBatteryTimeMessage(input->state.peer_address);
+      if (status != kNearbyStatusOK)
+        NEARBY_TRACE(ERROR, "Failed to send battery change, status: %d",
+                     status);
+    }
+  }
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+}
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+static const nearby_platform_BleInterface kBleInterface = {
+    .on_gatt_write = OnGattWrite,
+    .on_gatt_read = OnGattRead,
+};
+
+static const nearby_platform_BtInterface kBtInterface = {
+    .on_pairing_request = OnPairingRequest,
+    .on_paired = OnPaired,
+    .on_pairing_failed = OnPairingFailed,
+#ifdef NEARBY_FP_MESSAGE_STREAM
+    .on_message_stream_connected = OnMessageStreamConnected,
+    .on_message_stream_disconnected = OnMessageStreamDisconnected,
+    .on_message_stream_received = OnMessageStreamReceived,
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+};
+
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+static nearby_platform_BatteryInterface kBatteryInterface = {
+    .on_battery_changed = OnBatteryChanged,
+};
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+static nearby_platform_status EnterDisabledMode() {
+  nearby_platform_SetDefaultCapabilities();
+  return nearby_platform_SetAdvertisement(NULL, 0, kDisabled);
+}
+
+static nearby_platform_status EnterDiscoverableMode() {
+  size_t length;
+  uint8_t advertisement[DISCOVERABLE_ADV_SIZE_BYTES];
+  if (pairing_state != kPairingStateIdle &&
+      pairing_state != kPairingStateWaitingForAccountKeyWrite &&
+      pairing_state != kPairingStateWaitingForAdditionalData) {
+    NEARBY_TRACE(ERROR, "%s: device is in pairing process", __func__);
+    return kNearbyStatusError;
+  }
+  length = nearby_fp_CreateDiscoverableAdvertisement(advertisement,
+                                                     sizeof(advertisement));
+  length += nearby_fp_AppendTxPower(advertisement + length,
+                                    sizeof(advertisement) - length,
+                                    nearby_platform_GetTxLevel());
+  return nearby_platform_SetAdvertisement(advertisement, length,
+                                          kNoLargerThan100ms);
+}
+
+static nearby_platform_status EnterNonDiscoverableMode() {
+  if (pairing_state != kPairingStateIdle &&
+      pairing_state != kPairingStateWaitingForAccountKeyWrite &&
+      pairing_state != kPairingStateWaitingForAdditionalData) {
+    NEARBY_TRACE(ERROR, "%s: device is in pairing process", __func__);
+    return kNearbyStatusError;
+  }
+  nearby_platform_SetDefaultCapabilities();
+  return SetNonDiscoverableAdvertisement();
+}
+
+static bool ShouldRotateBleAddress(int mode) {
+  if (IsInPairingMode()) {
+    return false;
+  }
+  // We should rotate if advertisement mode changes to discoverable to prevent
+  // replay attacks.
+  return (mode & NEARBY_FP_ADVERTISEMENT_DISCOVERABLE) &&
+         (!(advertisement_mode & NEARBY_FP_ADVERTISEMENT_DISCOVERABLE));
+}
+
+static uint32_t GetRotationDelayMs() {
+  uint32_t delay_ms = ADDRESS_ROTATION_PERIOD_MS;
+  // Rotation should happen every 1024 seconds on average. It is required that
+  // the precise point at which the beacon starts advertising the new identifier
+  // is randomized within the window. This logic should give us +/-200 seconds
+  // variability.
+  for (int i = 0; i < 5; i++) {
+    delay_ms += (50 << i) * (int8_t)nearby_platform_Rand();
+  }
+  return delay_ms;
+}
+
+static void MaybeRotateBleAddress();
+
+static void CancelAddressRotationTimer() {
+  void* task = address_rotation_task;
+  address_rotation_task = NULL;
+  if (task != NULL) {
+    nearby_platform_CancelTimer(task);
+  }
+}
+
+static void ScheduleAddressRotation() {
+  CancelAddressRotationTimer();
+  address_rotation_task =
+      nearby_platform_StartTimer(MaybeRotateBleAddress, GetRotationDelayMs());
+}
+
+static nearby_platform_status UpdateAdvertisements() {
+  if (advertisement_mode == NEARBY_FP_ADVERTISEMENT_NONE) {
+    return EnterDisabledMode();
+  }
+  if (advertisement_mode & NEARBY_FP_ADVERTISEMENT_DISCOVERABLE) {
+    return EnterDiscoverableMode();
+  }
+  if (advertisement_mode & NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE) {
+    return EnterNonDiscoverableMode();
+  }
+  return kNearbyStatusUnsupported;
+}
+
+static void MaybeRotateBleAddress() {
+  ScheduleAddressRotation();
+  if (IsInPairingMode()) {
+    return;
+  }
+  RotateBleAddress();
+  UpdateAdvertisements();
+}
+
+static bool NeedsPeriodicAddressRotation() {
+  // FP spec says we should rotate BLE adress every ~15 minutes when advertising
+  return (advertisement_mode & (NEARBY_FP_ADVERTISEMENT_DISCOVERABLE |
+                                NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE)) != 0;
+}
+
+nearby_platform_status nearby_fp_client_SetAdvertisement(int mode) {
+  if (advertisement_mode == mode) {
+    return kNearbyStatusOK;
+  }
+  if (ShouldRotateBleAddress(mode)) {
+    CancelAddressRotationTimer();
+    RotateBleAddress();
+  }
+  advertisement_mode = mode;
+  if (NeedsPeriodicAddressRotation() && address_rotation_task == NULL) {
+    ScheduleAddressRotation();
+  }
+  return UpdateAdvertisements();
+}
+
+nearby_platform_status nearby_fp_client_GetSeekerInfo(
+    nearby_fp_client_SeekerInfo* seeker_info, size_t* seeker_info_length) {
+  int sl = *seeker_info_length;
+  int inx = 0;
+  *seeker_info_length = inx;
+  for (int i = 0; i < NEARBY_MAX_RFCOMM_CONNECTIONS; i++) {
+    if (rfcomm_inputs[i].state.peer_address != INVALID_PEER_ADDRESS) {
+      if (inx >= sl) return kNearbyStatusInvalidInput;
+      seeker_info[inx].peer_address = rfcomm_inputs[i].state.peer_address;
+      seeker_info[inx].capabilities = rfcomm_inputs[i].capabilities;
+      seeker_info[inx].platform_type = rfcomm_inputs[i].platform_type;
+      seeker_info[inx].platform_build = rfcomm_inputs[i].platform_build;
+      inx++;
+      *seeker_info_length = inx;
+    }
+  }
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_fp_client_Init(
+    const nearby_fp_client_Callbacks* callbacks) {
+  nearby_platform_status status;
+
+  client_callbacks = callbacks;
+  pairing_state = kPairingStateIdle;
+  pairing_failure_count = 0;
+#ifdef NEARBY_FP_MESSAGE_STREAM
+  memset(rfcomm_inputs, 0, sizeof(rfcomm_inputs));
+#endif
+  advertisement_mode = NEARBY_FP_ADVERTISEMENT_NONE;
+  address_rotation_task = NULL;
+  peer_public_address = 0;
+  DiscardPendingAccountKey();
+
+  status = nearby_platform_OsInit();
+  if (status != kNearbyStatusOK) return status;
+
+  status = nearby_platform_SecureElementInit();
+  if (status != kNearbyStatusOK) return status;
+
+  status = nearby_platform_BtInit(&kBtInterface);
+  if (status != kNearbyStatusOK) return status;
+
+  status = nearby_platform_BleInit(&kBleInterface);
+  if (status != kNearbyStatusOK) return status;
+
+  status = nearby_platform_PersistenceInit();
+  if (status != kNearbyStatusOK) return status;
+
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+  status = nearby_platform_BatteryInit(&kBatteryInterface);
+  if (status != kNearbyStatusOK) return status;
+#endif
+
+  status = nearby_fp_LoadAccountKeys();
+  if (status != kNearbyStatusOK) return status;
+
+  RotateBleAddress();
+
+  return status;
+}
diff --git a/embedded/client/source/nearby_fp_client.h b/embedded/client/source/nearby_fp_client.h
new file mode 100644
index 0000000..65e1705
--- /dev/null
+++ b/embedded/client/source/nearby_fp_client.h
@@ -0,0 +1,109 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_FP_CLIENT_H
+#define NEARBY_FP_CLIENT_H
+
+// clang-format off
+#include "nearby_config.h"
+// clang-format on
+
+#include "nearby.h"
+#include "nearby_event.h"
+#include "nearby_message_stream.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef struct {
+  // Optional event callback
+  void (*on_event)(nearby_event_Event* event);
+} nearby_fp_client_Callbacks;
+
+// Seeker information structure
+// Returns current information on a given seeker.
+typedef struct {
+  uint64_t peer_address;   // address of peer/seeker
+  uint8_t capabilities;    // Capability bits
+                           // Bit 0: Companion app is installed/not installed.
+                           // Bit 1: Silence mode is supported/not supported.
+  uint8_t platform_type;   // platform type code (Android = 0x01)
+  uint8_t platform_build;  // Platform build number (Android Pie=0x1c)
+} nearby_fp_client_SeekerInfo;
+
+// No advertisement, default state
+#define NEARBY_FP_ADVERTISEMENT_NONE 0x00
+// The device is discoverable and can be paired with
+#define NEARBY_FP_ADVERTISEMENT_DISCOVERABLE 0x01
+// The device is not discoverable
+#define NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE 0x02
+// Ask the Seeker to show pairing UI indication. This flag can be combined with
+// NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE
+#define NEARBY_FP_ADVERTISEMENT_PAIRING_UI_INDICATOR 0x04
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+// Include battery and charging info in the advertisement. This flag can be
+// combined with NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE
+#define NEARBY_FP_ADVERTISEMENT_INCLUDE_BATTERY_INFO 0x08
+// Ask the Seeker to show Battery UI indication. This flag can be combined with
+// NEARBY_FP_ADVERTISEMENT_INCLUDE_BATTERY_INFO
+#define NEARBY_FP_ADVERTISEMENT_BATTERY_UI_INDICATOR 0x10
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+// Sets Fast Pair advertisement type
+nearby_platform_status nearby_fp_client_SetAdvertisement(int mode);
+
+// Initalizes Fast Pair provider. The |callbacks| are optional - can be NULL.
+nearby_platform_status nearby_fp_client_Init(
+    const nearby_fp_client_Callbacks* callbacks);
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+// Serializes and sends |message| over Message Stream
+nearby_platform_status nearby_fp_client_SendMessage(
+    uint64_t peer_address, const nearby_message_stream_Message* message);
+
+// Sends out an ACK message for a received |message|. Note that not all messages
+// require an ACK
+nearby_platform_status nearby_fp_client_SendAck(
+    const nearby_event_MessageStreamReceived* message);
+
+// Sends out a NACK message for a received |message| with |fail_reason| reason.
+nearby_platform_status nearby_fp_client_SendNack(
+    const nearby_event_MessageStreamReceived* message, uint8_t fail_reason);
+
+// Sends out a enable or disable silence mode, which stops audio from being
+// routed to the provider.
+nearby_platform_status nearby_fp_client_SetSilenceMode(uint64_t peer_address,
+                                                       bool enable);
+
+// Sends log buffer full message to seeker, which passes it on to the companion
+// app to manage log collection.
+nearby_platform_status nearby_fp_client_SignalLogBufferFull(
+    uint64_t peer_address);
+
+// Gets information on currently connected seekers.
+// An array of `SeekerInfo` structures is passed in, along with the length.
+// On output, `seeker_info_length` is the actual number of structures read.
+// Returns an error if input array is too small to hold information about
+// all connected seekers.
+nearby_platform_status nearby_fp_client_GetSeekerInfo(
+    nearby_fp_client_SeekerInfo* seeker_info, size_t* seeker_info_length);
+
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_FP_CLIENT_H */
diff --git a/embedded/client/tests/gLinux/audio.cc b/embedded/client/tests/gLinux/audio.cc
new file mode 100644
index 0000000..e77e719
--- /dev/null
+++ b/embedded/client/tests/gLinux/audio.cc
@@ -0,0 +1,22 @@
+// Copyright 2022 Google LLC
+//
+// 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 "fakes.h"
+#include "nearby.h"
+#include "nearby_fp_client.h"
+#include "nearby_platform_audio.h"
+
+bool nearby_platform_GetEarbudRightStatus() { return false; }
+
+bool nearby_platform_GetEarbudLeftStatus() { return false; }
diff --git a/embedded/client/tests/gLinux/battery.cc b/embedded/client/tests/gLinux/battery.cc
new file mode 100644
index 0000000..e0725a8
--- /dev/null
+++ b/embedded/client/tests/gLinux/battery.cc
@@ -0,0 +1,62 @@
+// Copyright 2022 Google LLC
+//
+// 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 "fakes.h"
+#include "nearby.h"
+#include "nearby_platform_battery.h"
+
+static nearby_platform_BatteryInfo test_battery_info = {
+    .is_charging = true,
+    .right_bud_battery_level = 80,
+    .left_bud_battery_level = 85,
+    .charging_case_battery_level = 90,
+    .remaining_time_minutes = 100};
+static nearby_platform_status get_battery_info_result = kNearbyStatusOK;
+
+static const nearby_platform_BatteryInterface* battery_interface;
+nearby_platform_status nearby_platform_GetBatteryInfo(
+    nearby_platform_BatteryInfo* battery_info) {
+  (*battery_info) = test_battery_info;
+  return get_battery_info_result;
+}
+
+void nearby_test_fakes_SetIsCharging(bool charging) {
+  test_battery_info.is_charging = charging;
+}
+
+void nearby_test_fakes_SetRightBudBatteryLevel(unsigned battery_level) {
+  test_battery_info.right_bud_battery_level = battery_level;
+}
+
+void nearby_test_fakes_SetLeftBudBatteryLevel(unsigned battery_level) {
+  test_battery_info.left_bud_battery_level = battery_level;
+}
+
+void nearby_test_fakes_SetChargingCaseBatteryLevel(unsigned battery_level) {
+  test_battery_info.charging_case_battery_level = battery_level;
+}
+
+void nearby_test_fakes_BatteryTime(uint16_t battery_time) {
+  test_battery_info.remaining_time_minutes = battery_time;
+}
+
+void nearby_test_fakes_SetGetBatteryInfoResult(nearby_platform_status status) {
+  get_battery_info_result = status;
+}
+
+nearby_platform_status nearby_platform_BatteryInit(
+    nearby_platform_BatteryInterface* callbacks) {
+  battery_interface = callbacks;
+  return kNearbyStatusOK;
+}
diff --git a/embedded/client/tests/gLinux/ble.cc b/embedded/client/tests/gLinux/ble.cc
new file mode 100644
index 0000000..4ed89d2
--- /dev/null
+++ b/embedded/client/tests/gLinux/ble.cc
@@ -0,0 +1,127 @@
+// Copyright 2022 Google LLC
+//
+// 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 <map>
+#include <vector>
+
+// clang-format off
+#include "nearby_config.h"
+// clang-format on
+
+#include "fakes.h"
+#include "nearby_platform_ble.h"
+#include "nearby_platform_se.h"
+
+constexpr uint64_t kDefaultBleAddress = 0xbabababa;
+static uint64_t ble_address = kDefaultBleAddress;
+static uint64_t peer_address = 0x345678ab;
+static const nearby_platform_BleInterface* ble_interface;
+static std::map<nearby_fp_Characteristic, std::vector<uint8_t>> notifications;
+static std::vector<uint8_t> advertisement;
+static nearby_fp_AvertisementInterval interval;
+
+std::map<nearby_fp_Characteristic, std::vector<uint8_t>>&
+nearby_test_fakes_GetGattNotifications() {
+  return notifications;
+}
+
+// Gets BLE address.
+uint64_t nearby_platform_GetBleAddress() { return ble_address; }
+
+// Sets BLE address. Returns address after change, which may be different than
+// requested address.
+uint64_t nearby_platform_SetBleAddress(uint64_t address) {
+  ble_address = address;
+  return ble_address;
+}
+
+#ifdef NEARBY_FP_HAVE_BLE_ADDRESS_ROTATION
+// Rotates BLE address to a random resolvable private address (RPA). Returns
+// address after change.
+uint64_t nearby_platform_RotateBleAddress() {
+  unsigned i;
+  uint64_t address = 0;
+  for (i = 0; i < sizeof(address); i++) {
+    address = (address << 8) ^ nearby_platform_Rand();
+  }
+  address |= (uint64_t)1 << 46;
+  address &= ~((uint64_t)1 << 47);
+  nearby_platform_SetBleAddress(address);
+  return address;
+}
+#endif /* NEARBY_FP_HAVE_BLE_ADDRESS_ROTATION */
+
+// Sends a notification to the connected GATT client.
+nearby_platform_status nearby_platform_GattNotify(
+    uint64_t peer_address, nearby_fp_Characteristic characteristic,
+    const uint8_t* message, size_t length) {
+  notifications.emplace(characteristic,
+                        std::vector<uint8_t>(message, message + length));
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_platform_SetAdvertisement(
+    const uint8_t* payload, size_t length,
+    nearby_fp_AvertisementInterval interval) {
+  if (payload == NULL || length == 0) {
+    advertisement.clear();
+  } else {
+    advertisement.assign(payload, payload + length);
+  }
+  ::interval = interval;
+  return kNearbyStatusOK;
+}
+
+// Initializes BLE
+nearby_platform_status nearby_platform_BleInit(
+    const nearby_platform_BleInterface* callbacks) {
+  ble_interface = callbacks;
+  notifications.clear();
+  advertisement.clear();
+  interval = kDisabled;
+  ble_address = kDefaultBleAddress;
+  return kNearbyStatusOK;
+}
+
+std::vector<uint8_t>& nearby_test_fakes_GetAdvertisement() {
+  return advertisement;
+}
+
+nearby_platform_status nearby_test_fakes_GattReadModelId(uint8_t* output,
+                                                         size_t* length) {
+  return ble_interface->on_gatt_read(peer_address, kModelId, output, length);
+}
+
+nearby_platform_status nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+    const uint8_t* request, size_t length) {
+  return ble_interface->on_gatt_write(peer_address, kKeyBasedPairing, request,
+                                      length);
+}
+
+nearby_platform_status nearby_fp_fakes_ReceivePasskey(const uint8_t* request,
+                                                      size_t length) {
+  return ble_interface->on_gatt_write(peer_address, kPasskey, request, length);
+}
+
+nearby_platform_status nearby_fp_fakes_ReceiveAccountKeyWrite(
+    const uint8_t* request, size_t length) {
+  return ble_interface->on_gatt_write(peer_address, kAccountKey, request,
+                                      length);
+}
+
+nearby_platform_status nearby_fp_fakes_ReceiveAdditionalData(
+    const uint8_t* request, size_t length) {
+  return ble_interface->on_gatt_write(peer_address, kAdditionalData, request,
+                                      length);
+}
diff --git a/embedded/client/tests/gLinux/bt.cc b/embedded/client/tests/gLinux/bt.cc
new file mode 100644
index 0000000..43c5d59
--- /dev/null
+++ b/embedded/client/tests/gLinux/bt.cc
@@ -0,0 +1,145 @@
+// Copyright 2022 Google LLC
+//
+// 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 <cstring>
+#include <vector>
+
+#include "nearby_platform_bt.h"
+
+static const uint32_t kFastPairId = 0x101112;
+static const int8_t kTxLevel = 33;
+static const uint64_t kPublicAddress = 0xA0A1A2A3A4A5;
+static const uint32_t kLocalPasskey = 123456;
+static uint32_t remote_passkey;
+static uint64_t remote_address;
+static uint64_t paired_peer_address;
+static std::vector<uint8_t> rfcomm_output;
+static std::vector<char> device_name;
+static bool pairing_mode = false;
+
+static const nearby_platform_BtInterface* bt_interface;
+// Returns Fast Pair Model Id.
+uint32_t nearby_platform_GetModelId() { return kFastPairId; }
+
+int8_t nearby_platform_GetTxLevel() { return kTxLevel; }
+
+// Returns public BR/EDR address
+uint64_t nearby_platform_GetPublicAddress() { return kPublicAddress; }
+
+// Returns passkey used during pairing
+uint32_t nearby_platfrom_GetPairingPassKey() { return kLocalPasskey; }
+
+void nearby_platform_SetRemotePasskey(uint32_t passkey) {
+  remote_passkey = passkey;
+  if (remote_passkey == kLocalPasskey &&
+      remote_address != 0 & bt_interface != NULL) {
+    paired_peer_address = remote_address;
+    bt_interface->on_paired(remote_address);
+  }
+}
+
+nearby_platform_status nearby_platform_SendPairingRequest(
+    uint64_t remote_party_br_edr_address) {
+  remote_address = remote_party_br_edr_address;
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_platform_SetFastPairCapabilities() {
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_platform_SetDefaultCapabilities() {
+  remote_address = 0;
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_platform_BtInit(
+    const nearby_platform_BtInterface* callbacks) {
+  bt_interface = callbacks;
+  remote_address = 0;
+  remote_passkey = 0;
+  paired_peer_address = 0;
+  pairing_mode = false;
+  return kNearbyStatusOK;
+}
+
+uint64_t nearby_test_fakes_GetPairingRequestAddress() { return remote_address; }
+
+uint32_t nearby_test_fakes_GetRemotePasskey() { return remote_passkey; }
+
+void nearby_test_fakes_SimulatePairing(uint64_t peer_address) {
+  remote_address = peer_address;
+  if (bt_interface != NULL) {
+    bt_interface->on_pairing_request(peer_address);
+  }
+}
+
+uint64_t nearby_test_fakes_GetPairedDevice() { return paired_peer_address; }
+
+void nearby_test_fakes_DevicePaired(uint64_t peer_address) {
+  bt_interface->on_paired(peer_address);
+}
+
+nearby_platform_status nearby_platform_SendMessageStream(uint64_t peer_address,
+                                                         const uint8_t* message,
+                                                         size_t length) {
+  for (int i = 0; i < length; i++) {
+    rfcomm_output.push_back(message[i]);
+  }
+  return kNearbyStatusOK;
+}
+
+std::vector<uint8_t>& nearby_test_fakes_GetRfcommOutput() {
+  return rfcomm_output;
+}
+
+nearby_platform_status nearby_platform_SetDeviceName(const char* name) {
+  // + 1 for null terminator
+  device_name = std::vector<char>(name, name + std::strlen(name) + 1);
+  return kNearbyStatusOK;
+}
+
+// Gets null-terminated device name string in UTF-8 encoding
+// pass buffer size in char, and get string length in char.
+nearby_platform_status nearby_platform_GetDeviceName(char* name,
+                                                     size_t* length) {
+  if (*length < device_name.size()) {
+    return kNearbyStatusResourceExhausted;
+  }
+  *length = device_name.size();
+  std::memcpy(name, device_name.data(), device_name.size());
+  return kNearbyStatusOK;
+}
+
+bool nearby_platform_IsInPairingMode() { return pairing_mode; }
+
+void nearby_test_fakes_SetInPairingMode(bool in_pairing_mode) {
+  pairing_mode = in_pairing_mode;
+}
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+void nearby_test_fakes_MessageStreamConnected(uint64_t peer_address) {
+  bt_interface->on_message_stream_connected(peer_address);
+}
+
+void nearby_test_fakes_MessageStreamDisconnected(uint64_t peer_address) {
+  bt_interface->on_message_stream_disconnected(peer_address);
+}
+
+void nearby_test_fakes_MessageStreamReceived(uint64_t peer_address,
+                                             const uint8_t* message,
+                                             size_t length) {
+  bt_interface->on_message_stream_received(peer_address, message, length);
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
diff --git a/embedded/client/tests/gLinux/fakes.h b/embedded/client/tests/gLinux/fakes.h
new file mode 100644
index 0000000..abbeeb2
--- /dev/null
+++ b/embedded/client/tests/gLinux/fakes.h
@@ -0,0 +1,134 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+// Copyright 2021 Google LLC.
+#ifndef NEARBY_TEST_FAKES_H
+#define NEARBY_TEST_FAKES_H
+#include <map>
+#include <queue>
+#include <vector>
+
+#include "nearby.h"
+#include "nearby_platform_ble.h"
+
+void nearby_test_fakes_SetRandomNumber(unsigned int value);
+void nearby_test_fakes_SetRandomNumberSequence(std::vector<uint8_t>& value);
+
+void nearby_test_fakes_SetAccountKeys(const uint8_t* input, size_t length);
+std::vector<uint8_t> nearby_test_fakes_GetRawAccountKeys();
+
+nearby_platform_status nearby_test_fakes_GattReadModelId(uint8_t* output,
+                                                         size_t* length);
+
+nearby_platform_status nearby_test_fakes_SetAntiSpoofingKey(
+    const uint8_t private_key[32], const uint8_t public_key[64]);
+
+nearby_platform_status nearby_test_fakes_GenSec256r1Secret(
+    const uint8_t remote_party_public_key[64], uint8_t secret[32]);
+
+nearby_platform_status nearby_test_fakes_Aes128Decrypt(
+    const uint8_t input[AES_MESSAGE_SIZE_BYTES],
+    uint8_t output[AES_MESSAGE_SIZE_BYTES],
+    const uint8_t key[AES_MESSAGE_SIZE_BYTES]);
+nearby_platform_status nearby_test_fakes_Aes128Encrypt(
+    const uint8_t input[AES_MESSAGE_SIZE_BYTES],
+    uint8_t output[AES_MESSAGE_SIZE_BYTES],
+    const uint8_t key[AES_MESSAGE_SIZE_BYTES]);
+
+std::vector<uint8_t>& nearby_test_fakes_GetAdvertisement();
+
+std::map<nearby_fp_Characteristic, std::vector<uint8_t>>&
+nearby_test_fakes_GetGattNotifications();
+
+void nearby_test_fakes_SimulatePairing(uint64_t peer_address);
+nearby_platform_status nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+    const uint8_t* request, size_t length);
+nearby_platform_status nearby_fp_fakes_ReceivePasskey(const uint8_t* request,
+                                                      size_t length);
+nearby_platform_status nearby_fp_fakes_ReceiveAccountKeyWrite(
+    const uint8_t* request, size_t length);
+nearby_platform_status nearby_fp_fakes_ReceiveAdditionalData(
+    const uint8_t* request, size_t length);
+uint64_t nearby_test_fakes_GetPairingRequestAddress();
+uint32_t nearby_test_fakes_GetRemotePasskey();
+uint64_t nearby_test_fakes_GetPairedDevice();
+void nearby_test_fakes_DevicePaired(uint64_t peer_address);
+
+class AccountKeyList {
+ public:
+  explicit AccountKeyList(const std::vector<uint8_t>& raw_values) {
+    if (raw_values.size() == 0) return;
+    int key_count = raw_values[0];
+    for (int i = 0; i < key_count; i++) {
+      const uint8_t* p = raw_values.data() + 1 + (i * ACCOUNT_KEY_SIZE_BYTES);
+      keys_.emplace_back(std::vector<uint8_t>(p, p + ACCOUNT_KEY_SIZE_BYTES));
+    }
+  }
+
+  size_t size() { return keys_.size(); }
+
+  std::vector<std::vector<uint8_t>>& GetKeys() { return keys_; }
+
+  std::vector<uint8_t> GetRawFormat() {
+    std::vector<uint8_t> result;
+    result.push_back(size());
+    for (auto& key : keys_) {
+      for (auto& v : key) {
+        result.push_back(v);
+      }
+    }
+    return result;
+  }
+
+ private:
+  std::vector<std::vector<uint8_t>> keys_;
+};
+
+void nearby_test_fakes_SetAccountKeys(AccountKeyList& keys);
+
+AccountKeyList nearby_test_fakes_GetAccountKeys();
+
+void nearby_test_fakes_SetIsCharging(bool charging);
+
+void nearby_test_fakes_SetRightBudBatteryLevel(unsigned battery_level);
+
+void nearby_test_fakes_SetLeftBudBatteryLevel(unsigned battery_level);
+
+void nearby_test_fakes_SetChargingCaseBatteryLevel(unsigned battery_level);
+
+void nearby_test_fakes_BatteryTime(uint16_t battery_time);
+
+void nearby_test_fakes_SetGetBatteryInfoResult(nearby_platform_status status);
+
+std::vector<uint8_t>& nearby_test_fakes_GetRfcommOutput();
+
+void nearby_test_fakes_MessageStreamConnected(uint64_t peer_address);
+
+void nearby_test_fakes_MessageStreamDisconnected(uint64_t peer_address);
+
+void nearby_test_fakes_MessageStreamReceived(uint64_t peer_address,
+                                             const uint8_t* message,
+                                             size_t length);
+
+uint32_t nearby_test_fakes_GetNextTimerMs();
+// Sets current time and triggers the timer callback if it expired
+void nearby_test_fakes_SetCurrentTimeMs(uint32_t ms);
+
+void nearby_test_fakes_SetInPairingMode(bool in_pairing_mode);
+
+uint8_t nearby_test_fakes_GetRingCommand(void);
+
+uint16_t nearby_test_fakes_GetRingTimeout(void);
+
+#endif /* NEARBY_TEST_FAKES_H */
diff --git a/embedded/client/tests/gLinux/os.cc b/embedded/client/tests/gLinux/os.cc
new file mode 100644
index 0000000..40d2189
--- /dev/null
+++ b/embedded/client/tests/gLinux/os.cc
@@ -0,0 +1,82 @@
+// Copyright 2022 Google LLC
+//
+// 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 <cstddef>
+
+#include "nearby_assert.h"
+#include "nearby_platform_os.h"
+
+typedef void (*timer)();
+static timer timer_callback;
+static uint32_t timer_trigger_time;
+static bool timer_has_run;
+static uint32_t current_time;
+static constexpr nearby_platform_RingingInfo kDefaultRingingInfo = {
+    .ring_state = kRingStateStoppedReasonTimeout,
+    .num_components = 3,
+    .components = 0,
+    .timeout = 0,
+};
+
+static nearby_platform_RingingInfo ringing_info = kDefaultRingingInfo;
+
+// Gets current time in ms.
+unsigned int nearby_platform_GetCurrentTimeMs() { return current_time; }
+
+// Starts a timer. Returns an opaque timer handle or null on error.
+void* nearby_platform_StartTimer(void (*callback)(), unsigned int delay_ms) {
+  timer_callback = callback;
+  timer_trigger_time = nearby_platform_GetCurrentTimeMs() + delay_ms;
+  timer_has_run = false;
+  return (void*)timer_callback;
+}
+
+// Cancels a timer
+nearby_platform_status nearby_platform_CancelTimer(void* timer) {
+  NEARBY_ASSERT(timer == timer_callback);
+  timer_callback = NULL;
+  timer_trigger_time = 0;
+  return kNearbyStatusOK;
+}
+
+uint32_t nearby_test_fakes_GetNextTimerMs() { return timer_trigger_time; }
+
+void nearby_test_fakes_SetCurrentTimeMs(uint32_t ms) {
+  current_time = ms;
+  if (!timer_has_run && timer_callback != NULL && timer_trigger_time <= ms) {
+    timer_has_run = true;
+    timer_callback();
+  }
+}
+
+nearby_platform_status nearby_platform_Ring(uint8_t command, uint16_t timeout) {
+  ringing_info.components = command;
+  ringing_info.timeout = timeout;
+  ringing_info.ring_state = kRingStateStarted;
+  return kNearbyStatusOK;
+}
+
+uint8_t nearby_test_fakes_GetRingCommand(void) {
+  return ringing_info.components;
+}
+
+uint16_t nearby_test_fakes_GetRingTimeout(void) { return ringing_info.timeout; }
+
+nearby_platform_status nearby_platform_OsInit() {
+  current_time = 0;
+  timer_callback = NULL;
+  timer_trigger_time = 0;
+  timer_has_run = false;
+  return kNearbyStatusOK;
+}
diff --git a/embedded/client/tests/gLinux/persistence.cc b/embedded/client/tests/gLinux/persistence.cc
new file mode 100644
index 0000000..dfb438c
--- /dev/null
+++ b/embedded/client/tests/gLinux/persistence.cc
@@ -0,0 +1,69 @@
+// Copyright 2022 Google LLC
+//
+// 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 <algorithm>
+#include <map>
+#include <vector>
+
+#include "fakes.h"
+#include "nearby_platform_persistence.h"
+
+static std::map<nearby_fp_StoredKey, std::vector<uint8_t>> storage;
+nearby_platform_status nearby_platform_LoadValue(nearby_fp_StoredKey key,
+                                                 uint8_t* output,
+                                                 size_t* length) {
+  auto search = storage.find(key);
+  if (search != storage.end()) {
+    size_t value_length = search->second.size();
+    if (value_length > *length) {
+      *length = 0;
+      return kNearbyStatusResourceExhausted;
+    }
+    *length = value_length;
+    std::copy(search->second.begin(), search->second.end(), output);
+  } else {
+    // key not found.
+    *length = 0;
+  }
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_platform_SaveValue(nearby_fp_StoredKey key,
+                                                 const uint8_t* input,
+                                                 size_t length) {
+  storage[key].assign(input, input + length);
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_platform_PersistenceInit() {
+  storage.clear();
+  return kNearbyStatusOK;
+}
+
+void nearby_test_fakes_SetAccountKeys(const uint8_t* input, size_t length) {
+  storage[kStoredKeyAccountKeyList].assign(input, input + length);
+}
+
+std::vector<uint8_t> nearby_test_fakes_GetRawAccountKeys() {
+  return storage[kStoredKeyAccountKeyList];
+}
+
+void nearby_test_fakes_SetAccountKeys(AccountKeyList& keys) {
+  auto v = keys.GetRawFormat();
+  nearby_test_fakes_SetAccountKeys(v.data(), v.size());
+}
+
+AccountKeyList nearby_test_fakes_GetAccountKeys() {
+  return AccountKeyList(nearby_test_fakes_GetRawAccountKeys());
+}
diff --git a/embedded/client/tests/gLinux/se.cc b/embedded/client/tests/gLinux/se.cc
new file mode 100644
index 0000000..9039a06
--- /dev/null
+++ b/embedded/client/tests/gLinux/se.cc
@@ -0,0 +1,249 @@
+// Copyright 2022 Google LLC
+//
+// 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 <openssl/bn.h>
+#include <openssl/ec.h>
+#include <openssl/evp.h>
+#include <openssl/sha.h>
+
+#include <cerrno>
+#include <iomanip>
+#include <iostream>
+#include <memory>
+#include <queue>
+#include <sstream>
+#include <string>
+
+#include "fakes.h"
+#include "nearby_platform_se.h"
+
+static unsigned int random_value = 0;
+static std::queue<uint8_t> random_sequence;
+
+static std::unique_ptr<EVP_PKEY, void (*)(EVP_PKEY *)> anti_spoofing_key(
+    NULL, EVP_PKEY_free);
+
+static std::string ArrayToString(const uint8_t *data, size_t length) {
+  std::stringstream output;
+  output << "0x" << std::hex << std::setfill('0') << std::setw(2);
+  for (int i = 0; i < length; i++) {
+    output << (unsigned)data[i];
+  }
+  return output.str();
+}
+
+// Generates a random number.
+uint8_t nearby_platform_Rand() {
+  if (random_sequence.empty()) {
+    return random_value;
+  } else {
+    uint8_t v = random_sequence.front();
+    random_sequence.pop();
+    return v;
+  }
+}
+
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+static SHA256_CTX sha256_context;
+
+nearby_platform_status nearby_platform_Sha256Start() {
+  SHA256_Init(&sha256_context);
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_platform_Sha256Update(const void *data,
+                                                    size_t length) {
+  SHA256_Update(&sha256_context, data, length);
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_platform_Sha256Finish(uint8_t out[32]) {
+  SHA256_Final(out, &sha256_context);
+  return kNearbyStatusOK;
+}
+#endif /* NEARBY_FP_ENABLE_ADDITIONAL_DATA */
+
+// Encrypts a data block with AES128 in ECB mode.
+nearby_platform_status nearby_platform_Aes128Encrypt(const uint8_t input[16],
+                                                     uint8_t output[16],
+                                                     const uint8_t key[16]) {
+  EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
+  int input_length = 16;
+  int output_length = 16;
+
+  EVP_EncryptInit(ctx, EVP_aes_128_ecb(), key, NULL);
+
+  if (1 !=
+      EVP_EncryptUpdate(ctx, output, &output_length, input, input_length)) {
+    return kNearbyStatusError;
+  }
+
+  EVP_CIPHER_CTX_free(ctx);
+  return kNearbyStatusOK;
+}
+
+// Encrypts a data block with AES128 in ECB mode.
+nearby_platform_status nearby_platform_Aes128Decrypt(const uint8_t input[16],
+                                                     uint8_t output[16],
+                                                     const uint8_t key[16]) {
+  EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
+  int input_length = 16;
+  int output_length = 16;
+
+  EVP_DecryptInit(ctx, EVP_aes_128_ecb(), key, NULL);
+
+  if (1 !=
+      EVP_DecryptUpdate(ctx, output, &output_length, input, input_length)) {
+    return kNearbyStatusError;
+  }
+
+  EVP_CIPHER_CTX_free(ctx);
+  return kNearbyStatusOK;
+}
+
+static EC_POINT *load_public_key(const uint8_t public_key[64]) {
+  BN_CTX *bn_ctx;
+  EC_KEY *key;
+  EC_POINT *point;
+  const EC_GROUP *group;
+  uint8_t oct_key[65];
+
+  oct_key[0] = 0x04;
+  memcpy(oct_key + 1, public_key, 64);
+  bn_ctx = BN_CTX_new();
+  key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
+  group = EC_KEY_get0_group(key);
+  point = EC_POINT_new(group);
+  if (1 != EC_POINT_oct2point(group, point, oct_key, sizeof(oct_key), bn_ctx))
+    return NULL;
+  BN_CTX_free(bn_ctx);
+  EC_KEY_free(key);
+  return point;
+}
+
+// Generates a shared sec256p1 secret using remote party public key and this
+// device's private key.
+nearby_platform_status nearby_platform_GenSec256r1Secret(
+    const uint8_t remote_party_public_key[64], uint8_t secret[32]) {
+  EVP_PKEY_CTX *ctx;
+  EVP_PKEY *peerkey;
+  EC_POINT *peer_point;
+  EC_KEY *ec_peer_key;
+  size_t secret_len;
+
+  /* Create the context for the shared secret derivation */
+  if (NULL == (ctx = EVP_PKEY_CTX_new(anti_spoofing_key.get(), NULL)))
+    return kNearbyStatusError;
+
+  /* Initialise */
+  if (1 != EVP_PKEY_derive_init(ctx)) return kNearbyStatusError;
+
+  if (NULL == (peer_point = load_public_key(remote_party_public_key)))
+    return kNearbyStatusError;
+
+  ec_peer_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
+
+  if (1 != EC_KEY_set_public_key(ec_peer_key, peer_point))
+    return kNearbyStatusError;
+
+  peerkey = EVP_PKEY_new();
+  if (1 != EVP_PKEY_assign_EC_KEY(peerkey, ec_peer_key))
+    return kNearbyStatusError;
+
+  /* Provide the peer public key */
+  if (1 != EVP_PKEY_derive_set_peer(ctx, peerkey)) return kNearbyStatusError;
+
+  /* Determine buffer length for shared secret */
+  if (1 != EVP_PKEY_derive(ctx, NULL, &secret_len)) return kNearbyStatusError;
+
+  if (secret_len != 32) return kNearbyStatusError;
+
+  /* Derive the shared secret */
+  if (1 != (EVP_PKEY_derive(ctx, secret, &secret_len)))
+    return kNearbyStatusError;
+
+  EVP_PKEY_CTX_free(ctx);
+  EC_POINT_free(peer_point);
+  EVP_PKEY_free(peerkey);
+
+  std::cout << "Secret: " << ArrayToString(secret, 32) << std::endl;
+  return kNearbyStatusOK;
+}
+
+// Initializes secure element module
+nearby_platform_status nearby_platform_SecureElementInit() {
+  random_value = 0;
+  random_sequence = std::queue<uint8_t>();
+  return kNearbyStatusOK;
+}
+
+void nearby_test_fakes_SetRandomNumber(unsigned int value) {
+  random_value = value;
+}
+
+void nearby_test_fakes_SetRandomNumberSequence(std::vector<uint8_t> &value) {
+  for (auto &v : value) random_sequence.push(v);
+}
+
+static BIGNUM *load_private_key(const uint8_t private_key[32]) {
+  uint8_t buffer[37];
+  buffer[0] = buffer[1] = buffer[2] = 0;
+  buffer[3] = 33;
+  buffer[4] = 0;
+  memcpy(buffer + 5, private_key, 32);
+  return BN_mpi2bn(buffer, sizeof(buffer), NULL);
+}
+
+nearby_platform_status nearby_test_fakes_SetAntiSpoofingKey(
+    const uint8_t private_key[32], const uint8_t public_key[64]) {
+  EC_KEY *key;
+  BIGNUM *prv;
+  EC_POINT *pub;
+
+  prv = load_private_key(private_key);
+  if (prv == NULL) return kNearbyStatusError;
+  pub = load_public_key(public_key);
+  if (pub == NULL) return kNearbyStatusError;
+
+  key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
+
+  if (1 != EC_KEY_set_private_key(key, prv)) return kNearbyStatusError;
+  if (1 != EC_KEY_set_public_key(key, pub)) return kNearbyStatusError;
+
+  anti_spoofing_key.reset(EVP_PKEY_new());
+  if (1 != EVP_PKEY_assign_EC_KEY(anti_spoofing_key.get(), key))
+    return kNearbyStatusError;
+  BN_free(prv);
+  EC_POINT_free(pub);
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_test_fakes_GenSec256r1Secret(
+    const uint8_t remote_party_public_key[64], uint8_t secret[32]) {
+  return nearby_platform_GenSec256r1Secret(remote_party_public_key, secret);
+}
+
+nearby_platform_status nearby_test_fakes_Aes128Decrypt(
+    const uint8_t input[AES_MESSAGE_SIZE_BYTES],
+    uint8_t output[AES_MESSAGE_SIZE_BYTES],
+    const uint8_t key[AES_MESSAGE_SIZE_BYTES]) {
+  return nearby_platform_Aes128Decrypt(input, output, key);
+}
+
+nearby_platform_status nearby_test_fakes_Aes128Encrypt(
+    const uint8_t input[AES_MESSAGE_SIZE_BYTES],
+    uint8_t output[AES_MESSAGE_SIZE_BYTES],
+    const uint8_t key[AES_MESSAGE_SIZE_BYTES]) {
+  return nearby_platform_Aes128Encrypt(input, output, key);
+}
diff --git a/embedded/client/tests/gLinux/trace.cc b/embedded/client/tests/gLinux/trace.cc
new file mode 100644
index 0000000..2a56567
--- /dev/null
+++ b/embedded/client/tests/gLinux/trace.cc
@@ -0,0 +1,42 @@
+// Copyright 2022 Google LLC
+//
+// 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 <stdlib.h>
+
+#include <cstdarg>
+#include <cstdio>
+#include <iostream>
+#include <sstream>
+
+#include "nearby_platform_trace.h"
+
+void nearby_platform_Trace(nearby_platform_TraceLevel level,
+                           const char *filename, int lineno, const char *fmt,
+                           ...) {
+  char buff[1024];
+  va_list args;
+  va_start(args, fmt);
+
+  vsnprintf(buff, sizeof(buff), fmt, args);
+  std::cout << filename << ":" << lineno << " " << buff << std::endl;
+  va_end(args);
+}
+
+void nearby_platfrom_CrashOnAssert(const char *filename, int lineno,
+                                   const char *reason) {
+  fprintf(stderr, "ASSERT %s, %s:%d\n", reason, filename, lineno);
+  abort();
+}
+
+void nearby_platform_TraceInit(void) {}
diff --git a/embedded/client/tests/message_stream_test.cc b/embedded/client/tests/message_stream_test.cc
new file mode 100644
index 0000000..e4604e2
--- /dev/null
+++ b/embedded/client/tests/message_stream_test.cc
@@ -0,0 +1,359 @@
+// Copyright 2022 Google LLC
+//
+// 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 <deque>
+#include <iostream>
+#include <vector>
+
+#include "fakes.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "nearby.h"
+#include "nearby_message_stream.h"
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+constexpr uint64_t kPeerAddress = 0x101112;
+constexpr size_t kBufferSize = 64;
+constexpr size_t kMaxPayloadSize =
+    kBufferSize - sizeof(nearby_message_stream_Metadata);
+constexpr size_t kHeaderSize = 4;
+
+using ::testing::ElementsAreArray;
+
+class StreamMessage {
+ public:
+  explicit StreamMessage(nearby_message_stream_Message* message)
+      : StreamMessage(message->message_group, message->message_code,
+                      std::vector<uint8_t>(message->data,
+                                           message->data + message->length)) {}
+  StreamMessage(uint8_t group, uint8_t code) : group_(group), code_(code) {}
+  StreamMessage(uint8_t group, uint8_t code, std::vector<uint8_t> data)
+      : group_(group), code_(code), data_(data) {}
+
+  bool operator==(const StreamMessage& b) const {
+    return this->group_ == b.group_ && this->code_ == b.code_ &&
+           this->data_ == b.data_;
+  }
+
+ private:
+  unsigned int group_;
+  unsigned int code_;
+  std::vector<uint8_t> data_;
+  friend std::ostream& operator<<(std::ostream& os,
+                                  const StreamMessage& message);
+};
+
+static std::string VecToString(std::vector<uint8_t> data) {
+  std::stringstream output;
+  output << "0x" << std::hex;
+  for (int i = 0; i < data.size(); i++) {
+    output << std::setfill('0') << std::setw(2) << (unsigned)data[i];
+  }
+  return output.str();
+}
+
+std::ostream& operator<<(std::ostream& os, const StreamMessage& message) {
+  os << std::hex;
+  os << "group: 0x" << std::setfill('0') << std::setw(2) << message.group_;
+  os << " code: 0x" << std::setfill('0') << std::setw(2) << message.code_;
+  os << std::dec << " length: " << message.data_.size();
+  if (message.data_.size() > 0) {
+    os << " data: " << VecToString(message.data_);
+  }
+  return os;
+}
+
+uint8_t buffer[kBufferSize];
+
+void OnMessageReceived(uint64_t peer_address,
+                       nearby_message_stream_Message* message);
+
+class MessageStreamTest : public ::testing::Test {
+ public:
+  void Read(const uint8_t* data, size_t length) {
+    nearby_message_stream_Read(&stream_state_, data, length);
+  }
+
+  void ReadByteByByte(const uint8_t* data, size_t length) {
+    for (int i = 0; i < length; i++) {
+      nearby_message_stream_Read(&stream_state_, data + i, 1);
+    }
+  }
+
+  void Send(const nearby_message_stream_Message* message) {
+    nearby_message_stream_Send(kPeerAddress, message);
+  }
+
+  void SendAck(const nearby_message_stream_Message* message) {
+    nearby_message_stream_SendAck(kPeerAddress, message);
+  }
+
+  void SendNack(const nearby_message_stream_Message* message,
+                uint8_t fail_reason) {
+    nearby_message_stream_SendNack(kPeerAddress, message, fail_reason);
+  }
+
+ protected:
+  void SetUp() override;
+
+  std::deque<StreamMessage> received_messages_;
+
+ private:
+  void AddMessage(nearby_message_stream_Message* message) {
+    received_messages_.emplace_back(StreamMessage(message));
+  }
+  // To allow access to |AddMessage|
+  friend void OnMessageReceived(uint64_t peer_address,
+                                nearby_message_stream_Message* message);
+
+  nearby_message_stream_State stream_state_ = {
+      .on_message_received = OnMessageReceived,
+      .peer_address = kPeerAddress,
+      .length = sizeof(buffer),
+      .buffer = buffer};
+} * test_fixture;
+
+void MessageStreamTest::SetUp() {
+  test_fixture = this;
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_message_stream_Init(&stream_state_);
+}
+
+void OnMessageReceived(uint64_t peer_address,
+                       nearby_message_stream_Message* message) {
+  EXPECT_EQ(kPeerAddress, peer_address);
+  test_fixture->AddMessage(message);
+}
+
+TEST_F(MessageStreamTest, ReadWholeMessageWithNoData) {
+  uint8_t group = 120;
+  uint8_t code = 130;
+  uint8_t message[] = {group, code, 0, 0};
+
+  Read(message, sizeof(message));
+
+  ASSERT_EQ(1, received_messages_.size());
+  ASSERT_EQ(StreamMessage(group, code), received_messages_[0]);
+}
+
+TEST_F(MessageStreamTest, ReadMessageInChunksWithNoData) {
+  uint8_t group = 120;
+  uint8_t code = 130;
+  uint8_t message[] = {group, code, 0, 0};
+
+  ReadByteByByte(message, sizeof(message));
+
+  ASSERT_EQ(1, received_messages_.size());
+  ASSERT_EQ(StreamMessage(group, code), received_messages_[0]);
+}
+
+TEST_F(MessageStreamTest, ReadWholeMessageSmallPayload) {
+  uint8_t group = 120;
+  uint8_t code = 130;
+  uint8_t message[] = {group, code, 0, 1, 30};
+
+  Read(message, sizeof(message));
+
+  ASSERT_EQ(1, received_messages_.size());
+  ASSERT_EQ(StreamMessage(group, code,
+                          std::vector<uint8_t>(message + kHeaderSize,
+                                               message + sizeof(message))),
+            received_messages_[0]);
+}
+
+TEST_F(MessageStreamTest, ReadMessageInChunksSmallPayload) {
+  uint8_t group = 120;
+  uint8_t code = 130;
+  uint8_t message[] = {group, code, 0, 1, 30};
+
+  ReadByteByByte(message, sizeof(message));
+
+  ASSERT_EQ(1, received_messages_.size());
+  ASSERT_EQ(StreamMessage(group, code,
+                          std::vector<uint8_t>(message + kHeaderSize,
+                                               message + sizeof(message))),
+            received_messages_[0]);
+}
+
+TEST_F(MessageStreamTest, ReadWholeMessageMaximumPayloadSize) {
+  uint8_t group = 120;
+  uint8_t code = 130;
+  uint8_t message[kHeaderSize + kMaxPayloadSize];
+  message[0] = group;
+  message[1] = code;
+  message[2] = kMaxPayloadSize >> 8;
+  message[3] = kMaxPayloadSize;
+  for (unsigned i = 0; i < kMaxPayloadSize; i++) {
+    message[kHeaderSize + i] = i;
+  }
+
+  Read(message, sizeof(message));
+
+  ASSERT_EQ(1, received_messages_.size());
+  ASSERT_EQ(StreamMessage(group, code,
+                          std::vector<uint8_t>(message + kHeaderSize,
+                                               message + sizeof(message))),
+            received_messages_[0]);
+}
+
+TEST_F(MessageStreamTest, ReadWholeMessageTruncatedPayload) {
+  uint8_t group = 120;
+  uint8_t code = 130;
+  constexpr size_t kPayloadSize = 0x102;
+  uint8_t message[kHeaderSize + kPayloadSize];
+  message[0] = group;
+  message[1] = code;
+  message[2] = 0x01;
+  message[3] = 0x02;
+  for (unsigned i = 0; i < kPayloadSize; i++) {
+    message[kHeaderSize + i] = i;
+  }
+
+  Read(message, sizeof(message));
+
+  ASSERT_EQ(1, received_messages_.size());
+  ASSERT_EQ(StreamMessage(
+                group, code,
+                std::vector<uint8_t>(message + kHeaderSize,
+                                     message + kHeaderSize + kMaxPayloadSize)),
+            received_messages_[0]);
+}
+
+TEST_F(MessageStreamTest, ReadMessageInChunksTruncatedPayload) {
+  uint8_t group = 120;
+  uint8_t code = 130;
+  constexpr size_t kPayloadSize = 0x102;
+  uint8_t message[kHeaderSize + kPayloadSize];
+  message[0] = group;
+  message[1] = code;
+  message[2] = 0x01;
+  message[3] = 0x02;
+  for (unsigned i = 0; i < kPayloadSize; i++) {
+    message[kHeaderSize + i] = i;
+  }
+
+  ReadByteByByte(message, sizeof(message));
+
+  ASSERT_EQ(1, received_messages_.size());
+  ASSERT_EQ(StreamMessage(
+                group, code,
+                std::vector<uint8_t>(message + kHeaderSize,
+                                     message + kHeaderSize + kMaxPayloadSize)),
+            received_messages_[0]);
+}
+
+TEST_F(MessageStreamTest, ReadTwoMessages) {
+  uint8_t group = 120;
+  uint8_t code = 130;
+  uint8_t group2 = 121;
+  uint8_t code2 = 131;
+  uint8_t message[] = {group, code, 0, 1, 30, group2, code2, 0, 2, 31, 32};
+
+  Read(message, sizeof(message));
+
+  ASSERT_EQ(2, received_messages_.size());
+  ASSERT_EQ(StreamMessage(group, code,
+                          std::vector<uint8_t>(message + 4, message + 5)),
+            received_messages_[0]);
+  ASSERT_EQ(StreamMessage(group2, code2,
+                          std::vector<uint8_t>(message + 9, message + 11)),
+            received_messages_[1]);
+}
+
+TEST_F(MessageStreamTest, ReadTwoMessagesByteByByte) {
+  uint8_t group = 120;
+  uint8_t code = 130;
+  uint8_t group2 = 121;
+  uint8_t code2 = 131;
+  uint8_t message[] = {group, code, 0, 1, 30, group2, code2, 0, 2, 31, 32};
+
+  ReadByteByByte(message, sizeof(message));
+
+  ASSERT_EQ(2, received_messages_.size());
+  ASSERT_EQ(StreamMessage(group, code,
+                          std::vector<uint8_t>(message + 4, message + 5)),
+            received_messages_[0]);
+  ASSERT_EQ(StreamMessage(group2, code2,
+                          std::vector<uint8_t>(message + 9, message + 11)),
+            received_messages_[1]);
+}
+
+TEST_F(MessageStreamTest, SendMessageNoPayload) {
+  nearby_message_stream_Message message{
+      .message_group = 10,
+      .message_code = 20,
+      .length = 0,
+      .data = nullptr,
+  };
+  constexpr uint8_t kExpectedOutput[] = {10, 20, 0, 0};
+
+  Send(&message);
+
+  ASSERT_THAT(kExpectedOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+
+TEST_F(MessageStreamTest, SendMessageWithPayload) {
+  uint8_t payload[] = {20, 21, 22, 23, 24};
+  nearby_message_stream_Message message{
+      .message_group = 10,
+      .message_code = 11,
+      .length = sizeof(payload),
+      .data = payload,
+  };
+  constexpr uint8_t kExpectedOutput[] = {10, 11, 0, sizeof(payload), 20, 21,
+                                         22, 23, 24};
+
+  Send(&message);
+
+  ASSERT_THAT(kExpectedOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+
+TEST_F(MessageStreamTest, SendAck) {
+  nearby_message_stream_Message message{
+      .message_group = 10,
+      .message_code = 20,
+      .length = 0,
+      .data = nullptr,
+  };
+  constexpr uint8_t kExpectedOutput[] = {0xFF, 1, 0, 2, 10, 20};
+
+  SendAck(&message);
+
+  ASSERT_THAT(kExpectedOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+
+TEST_F(MessageStreamTest, SendNack) {
+  nearby_message_stream_Message message{
+      .message_group = 10,
+      .message_code = 20,
+      .length = 0,
+      .data = nullptr,
+  };
+  constexpr uint8_t kFailReason = 0x88;
+  constexpr uint8_t kExpectedOutput[] = {0xFF, 2, 0, 3, kFailReason, 10, 20};
+
+  SendNack(&message, kFailReason);
+
+  ASSERT_THAT(kExpectedOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+int main(int argc, char** argv) {
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/embedded/client/tests/smoke_test.cc b/embedded/client/tests/smoke_test.cc
new file mode 100644
index 0000000..9f2ebf9
--- /dev/null
+++ b/embedded/client/tests/smoke_test.cc
@@ -0,0 +1,2199 @@
+// Copyright 2022 Google LLC
+//
+// 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 <cerrno>
+#include <iomanip>
+#include <iostream>
+#include <memory>
+#include <sstream>
+#include <string>
+
+#include "fakes.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "nearby.h"
+#include "nearby_event.h"
+#include "nearby_fp_client.h"
+#include "nearby_fp_library.h"
+#include "nearby_platform_ble.h"
+#include "nearby_platform_persistence.h"
+#include "nearby_utils.h"
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-const-variable"
+
+constexpr uint8_t kBobPrivateKey[32] = {
+    0x02, 0xB4, 0x37, 0xB0, 0xED, 0xD6, 0xBB, 0xD4, 0x29, 0x06, 0x4A,
+    0x4E, 0x52, 0x9F, 0xCB, 0xF1, 0xC4, 0x8D, 0x0D, 0x62, 0x49, 0x24,
+    0xD5, 0x92, 0x27, 0x4B, 0x7E, 0xD8, 0x11, 0x93, 0xD7, 0x63};
+constexpr uint8_t kBobPublicKey[64] = {
+    0xF7, 0xD4, 0x96, 0xA6, 0x2E, 0xCA, 0x41, 0x63, 0x51, 0x54, 0x0A,
+    0xA3, 0x43, 0xBC, 0x69, 0x0A, 0x61, 0x09, 0xF5, 0x51, 0x50, 0x06,
+    0x66, 0xB8, 0x3B, 0x12, 0x51, 0xFB, 0x84, 0xFA, 0x28, 0x60, 0x79,
+    0x5E, 0xBD, 0x63, 0xD3, 0xB8, 0x83, 0x6F, 0x44, 0xA9, 0xA3, 0xE2,
+    0x8B, 0xB3, 0x40, 0x17, 0xE0, 0x15, 0xF5, 0x97, 0x93, 0x05, 0xD8,
+    0x49, 0xFD, 0xF8, 0xDE, 0x10, 0x12, 0x3B, 0x61, 0xD2};
+
+constexpr uint8_t kAlicePrivateKey[32] = {
+    0xD7, 0x5E, 0x54, 0xC7, 0x7D, 0x76, 0x24, 0x89, 0xE5, 0x7C, 0xFA,
+    0x92, 0x37, 0x43, 0xF1, 0x67, 0x77, 0xA4, 0x28, 0x3D, 0x99, 0x80,
+    0x0B, 0xAC, 0x55, 0x58, 0x48, 0x38, 0x93, 0xE5, 0xB0, 0x6D};
+constexpr uint8_t kAlicePublicKey[64] = {
+    0x36, 0xAC, 0x68, 0x2C, 0x50, 0x82, 0x15, 0x66, 0x8F, 0xBE, 0xFE,
+    0x24, 0x7D, 0x01, 0xD5, 0xEB, 0x96, 0xE6, 0x31, 0x8E, 0x85, 0x5B,
+    0x2D, 0x64, 0xB5, 0x19, 0x5D, 0x38, 0xEE, 0x7E, 0x37, 0xBE, 0x18,
+    0x38, 0xC0, 0xB9, 0x48, 0xC3, 0xF7, 0x55, 0x20, 0xE0, 0x7E, 0x70,
+    0xF0, 0x72, 0x91, 0x41, 0x9A, 0xCE, 0x2D, 0x28, 0x14, 0x3C, 0x5A,
+    0xDB, 0x2D, 0xBD, 0x98, 0xEE, 0x3C, 0x8E, 0x4F, 0xBF};
+
+constexpr uint8_t kExpectedSharedSecret[32] = {
+    0x9D, 0xAD, 0xE4, 0xF8, 0x6A, 0xC3, 0x48, 0x8B, 0xBA, 0xC2, 0xAC,
+    0x34, 0xB5, 0xFE, 0x68, 0xA0, 0xEE, 0x5A, 0x67, 0x06, 0xF5, 0x43,
+    0xD9, 0x06, 0x1A, 0xD5, 0x78, 0x89, 0x49, 0x8A, 0xE6, 0xBA};
+
+constexpr uint8_t kExpectedAesKey[16] = {0xB0, 0x7F, 0x1F, 0x17, 0xC2, 0x36,
+                                         0xCB, 0xD3, 0x35, 0x23, 0xC5, 0x15,
+                                         0xF3, 0x50, 0xAE, 0x57};
+
+constexpr uint64_t kRemoteDevice = 0xB0B1B2B3B4B5;
+
+constexpr uint8_t kTxPower = 33;
+constexpr uint8_t kDiscoverableAdvertisement[] = {
+    6, 0x16, 0x2C, 0xFE, 0x10, 0x11, 0x12, 2, 0x0A, kTxPower};
+
+constexpr uint8_t kSeekerAccountKey[16] = {0x04, 20, 21, 22, 23, 24, 25, 26,
+                                           27,   28, 29, 30, 31, 32, 33, 34};
+constexpr uint8_t kSeekerAccountKey2[16] = {0x04, 50, 51, 52, 53, 54, 55, 56,
+                                            57,   58, 59, 60, 61, 62, 63, 64};
+
+using ::testing::ElementsAreArray;
+
+static std::string VecToString(std::vector<uint8_t> data) {
+  std::stringstream output;
+  output << "0x" << std::hex;
+  for (int i = 0; i < data.size(); i++) {
+    output << std::setfill('0') << std::setw(2) << (unsigned)data[i];
+  }
+  return output.str();
+}
+
+// static std::string VecToString(uint8_t* start, uint8_t* end) {
+//   return VecToString(std::vector<uint8_t>(start, end));
+// }
+
+class Event {
+ public:
+  explicit Event(nearby_event_Type type) : type_(type) {}
+  explicit Event(const nearby_event_Event* event) : Event(event->event_type) {}
+
+  nearby_event_Type GetType() const { return type_; }
+
+  virtual bool operator==(const Event& b) const { return type_ == b.type_; }
+
+  virtual ~Event() {}
+
+ protected:
+  virtual std::string ToString() const {
+    std::stringstream output;
+    output << "Event type: " << type_;
+    return output.str();
+  }
+  nearby_event_Type type_;
+  friend std::ostream& operator<<(std::ostream& os, const Event& event);
+};
+
+class MessageStreamConnectedEvent : public Event {
+ public:
+  explicit MessageStreamConnectedEvent(uint64_t peer_address)
+      : Event(kNearbyEventMessageStreamConnected),
+        peer_address_(peer_address) {}
+  explicit MessageStreamConnectedEvent(
+      const nearby_event_MessageStreamConnected* payload)
+      : Event(kNearbyEventMessageStreamConnected),
+        peer_address_(payload->peer_address) {
+    EXPECT_NE(nullptr, payload);
+  }
+  explicit MessageStreamConnectedEvent(const nearby_event_Event* event)
+      : MessageStreamConnectedEvent(
+            (const nearby_event_MessageStreamConnected*)event->payload) {
+    EXPECT_EQ(kNearbyEventMessageStreamConnected, type_);
+  }
+
+  virtual bool operator==(const Event& b) const override {
+    if (type_ != b.GetType()) return false;
+    const MessageStreamConnectedEvent* event =
+        (const MessageStreamConnectedEvent*)&b;
+    return peer_address_ == event->peer_address_;
+  }
+
+  std::string ToString() const override {
+    std::stringstream output;
+    output << "Event type: " << type_ << " peer_address: " << peer_address_;
+    return output.str();
+  }
+
+ private:
+  uint64_t peer_address_;
+};
+
+class MessageStreamDisconnectedEvent : public Event {
+ public:
+  explicit MessageStreamDisconnectedEvent(uint64_t peer_address)
+      : Event(kNearbyEventMessageStreamDisconnected),
+        peer_address_(peer_address) {}
+  explicit MessageStreamDisconnectedEvent(
+      const nearby_event_MessageStreamDisconnected* payload)
+      : Event(kNearbyEventMessageStreamDisconnected),
+        peer_address_(payload->peer_address) {
+    EXPECT_NE(nullptr, payload);
+  }
+  explicit MessageStreamDisconnectedEvent(const nearby_event_Event* event)
+      : MessageStreamDisconnectedEvent(
+            (const nearby_event_MessageStreamDisconnected*)event->payload) {
+    EXPECT_EQ(kNearbyEventMessageStreamDisconnected, type_);
+  }
+
+  virtual bool operator==(const Event& b) const override {
+    if (type_ != b.GetType()) return false;
+    const MessageStreamDisconnectedEvent* event =
+        (const MessageStreamDisconnectedEvent*)&b;
+    return peer_address_ == event->peer_address_;
+  }
+
+  std::string ToString() const override {
+    std::stringstream output;
+    output << "Event type: " << type_ << " peer_address: " << peer_address_;
+    return output.str();
+  }
+
+ private:
+  uint64_t peer_address_;
+};
+
+class MessageStreamReceivedEvent : public Event {
+ public:
+  explicit MessageStreamReceivedEvent(
+      const nearby_event_MessageStreamReceived* payload)
+      : Event(kNearbyEventMessageStreamReceived) {
+    EXPECT_NE(nullptr, payload);
+
+    peer_address_ = payload->peer_address;
+    group_ = payload->message_group;
+    code_ = payload->message_code;
+    if (payload->length > 0) {
+      data_ =
+          std::vector<uint8_t>(payload->data, payload->data + payload->length);
+    }
+  }
+  explicit MessageStreamReceivedEvent(const nearby_event_Event* event)
+      : MessageStreamReceivedEvent(
+            (const nearby_event_MessageStreamReceived*)event->payload) {
+    EXPECT_EQ(kNearbyEventMessageStreamReceived, event->event_type);
+  }
+
+  virtual bool operator==(const Event& b) const override {
+    if (type_ != b.GetType()) return false;
+    const MessageStreamReceivedEvent* event =
+        (const MessageStreamReceivedEvent*)&b;
+    return peer_address_ == event->peer_address_ && group_ == event->group_ &&
+           code_ == event->code_ && data_ == event->data_;
+  }
+
+  std::string ToString() const override {
+    std::stringstream output;
+    output << "Event type: " << type_ << " peer_address: " << peer_address_
+           << " group: " << (int)group_ << " code: " << (int)code_
+           << " length: " << data_.size();
+    if (data_.size() > 0) {
+      output << " data: " << VecToString(data_);
+    }
+    return output.str();
+  }
+
+ private:
+  uint64_t peer_address_;
+  uint8_t group_;
+  uint8_t code_;
+  std::vector<uint8_t> data_;
+};
+
+std::ostream& operator<<(std::ostream& os, const Event& event) {
+  os << event.ToString();
+  return os;
+}
+
+static std::unique_ptr<Event> GetEvent(const nearby_event_Event* event) {
+  switch (event->event_type) {
+    case kNearbyEventMessageStreamConnected:
+      return std::make_unique<MessageStreamConnectedEvent>(event);
+    case kNearbyEventMessageStreamDisconnected:
+      return std::make_unique<MessageStreamDisconnectedEvent>(event);
+    case kNearbyEventMessageStreamReceived:
+      return std::make_unique<MessageStreamReceivedEvent>(event);
+  }
+  return std::make_unique<Event>(event);
+}
+
+std::vector<std::unique_ptr<Event>> message_stream_events;
+static void OnEventCallback(nearby_event_Event* event) {
+  message_stream_events.push_back(GetEvent(event));
+}
+
+constexpr nearby_fp_client_Callbacks kClientCallbacks = {.on_event =
+                                                             OnEventCallback};
+
+static void WriteToAccountKey() {
+  uint8_t encrypted_account_key_write_request[16];
+  nearby_test_fakes_Aes128Encrypt(
+      kSeekerAccountKey, encrypted_account_key_write_request, kExpectedAesKey);
+  nearby_fp_fakes_ReceiveAccountKeyWrite(
+      encrypted_account_key_write_request,
+      sizeof(encrypted_account_key_write_request));
+}
+
+int GetCapability(uint64_t peer_address) {
+  nearby_fp_client_SeekerInfo seeker_infos[NEARBY_MAX_RFCOMM_CONNECTIONS];
+  size_t sl = NEARBY_MAX_RFCOMM_CONNECTIONS;
+  nearby_fp_client_GetSeekerInfo(seeker_infos, &sl);
+
+  for (int i = 0; i < sl; i++) {
+    if (seeker_infos[i].peer_address == peer_address) {
+      return seeker_infos[i].capabilities;
+    }
+  }
+  return -1;
+}
+
+// |flags| from Table 1.2.1: Raw Request (type 0x00)  in FP specification
+static void Pair(uint8_t flags) {
+  uint8_t salt = 0xAB;
+
+  nearby_test_fakes_SetAntiSpoofingKey(kBobPrivateKey, kBobPublicKey);
+  nearby_test_fakes_SetRandomNumber(salt);
+  nearby_fp_client_SetAdvertisement(NEARBY_FP_ADVERTISEMENT_DISCOVERABLE);
+
+  uint8_t request[16];
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = flags;
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // Seeker's address
+  request[8] = 0xB0;
+  request[9] = 0xB1;
+  request[10] = 0xB2;
+  request[11] = 0xB3;
+  request[12] = 0xB4;
+  request[13] = 0xB5;
+  // salt
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  uint8_t encrypted[16 + 64];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+  // Seeker responds
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+  // BT negotatiates passkey 123456 (0x01E240)
+  uint8_t raw_passkey_block[16] = {0x02, 0x01, 0xE2, 0x40};
+  uint8_t encrypted_passkey_block[16];
+  nearby_test_fakes_Aes128Encrypt(raw_passkey_block, encrypted_passkey_block,
+                                  kExpectedAesKey);
+  // Seeker sends the passkey to provider
+  nearby_fp_fakes_ReceivePasskey(encrypted_passkey_block,
+                                 sizeof(encrypted_passkey_block));
+
+  // Seeker sends their account key
+  WriteToAccountKey();
+}
+
+TEST(NearbyFpClient, Init) {
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_Init(NULL));
+}
+
+TEST(NearbyFpClient, AccountKeyListIsEmpty) {
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_Init(NULL));
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(0, keys.size());
+}
+
+TEST(NearbyFpClient, CopyBigEndian_4bytes) {
+  uint8_t buffer[4];
+  uint32_t value = 0x01020304;
+
+  nearby_utils_CopyBigEndian(buffer, value, 4);
+
+  ASSERT_EQ(1, buffer[0]);
+  ASSERT_EQ(2, buffer[1]);
+  ASSERT_EQ(3, buffer[2]);
+  ASSERT_EQ(4, buffer[3]);
+}
+
+TEST(NearbyFpClient, CopyBigEndian_3bytes) {
+  uint8_t buffer[3];
+  uint32_t value = 0x00010203;
+
+  nearby_utils_CopyBigEndian(buffer, value, 3);
+
+  ASSERT_EQ(1, buffer[0]);
+  ASSERT_EQ(2, buffer[1]);
+  ASSERT_EQ(3, buffer[2]);
+}
+
+TEST(NearbyFpClient, AdvertisementDiscoverable) {
+  const int kBufferSize = DISCOVERABLE_ADV_SIZE_BYTES;
+  uint8_t buffer[kBufferSize];
+  nearby_fp_client_Init(NULL);
+
+  size_t written =
+      nearby_fp_CreateDiscoverableAdvertisement(buffer, kBufferSize);
+  written += nearby_fp_AppendTxPower(buffer + written, kBufferSize - written,
+                                     kTxPower);
+
+  ASSERT_EQ(kBufferSize, written);
+  ASSERT_THAT(std::vector<uint8_t>(buffer, buffer + kBufferSize),
+              ElementsAreArray(kDiscoverableAdvertisement));
+}
+
+TEST(NearbyFpClient, AdvertisementNondiscoverable_noKeys) {
+  const int kBufferSize = 9;
+  uint8_t buffer[kBufferSize];
+  const uint8_t kExpectedResult[] = {5,    0x16, 0x2C, 0xFE,    0x00,
+                                     0x00, 2,    0x0A, kTxPower};
+  nearby_fp_client_Init(NULL);
+
+  size_t written =
+      nearby_fp_CreateNondiscoverableAdvertisement(buffer, kBufferSize, false);
+  written += nearby_fp_AppendTxPower(buffer + written, kBufferSize - written,
+                                     kTxPower);
+
+  ASSERT_EQ(kBufferSize, written);
+  ASSERT_THAT(std::vector<uint8_t>(buffer, buffer + kBufferSize),
+              ElementsAreArray(kExpectedResult));
+}
+
+TEST(NearbyFpClient, AdvertisementNondiscoverable_oneKey) {
+  const int kBufferSize = 15;
+  uint8_t buffer[kBufferSize];
+  uint8_t salt = 0xC7;
+  uint8_t account_keys[] = {1,    0x11, 0x22, 0x33, 0x44, 0x55,
+                            0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
+                            0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
+  const uint8_t kExpectedResult[] = {11,   0x16, 0x2C, 0xFE, 0x00,
+                                     0x42, 0x0A, 0x42, 0x88, 0x10,
+                                     0x11, salt, 2,    0x0A, kTxPower};
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetAccountKeys(account_keys, sizeof(account_keys));
+  nearby_test_fakes_SetRandomNumber(salt);
+  nearby_fp_LoadAccountKeys();
+
+  size_t written =
+      nearby_fp_CreateNondiscoverableAdvertisement(buffer, kBufferSize, false);
+  written += nearby_fp_AppendTxPower(buffer + written, kBufferSize - written,
+                                     kTxPower);
+
+  ASSERT_EQ(kBufferSize, written);
+  ASSERT_THAT(std::vector<uint8_t>(buffer, buffer + kBufferSize),
+              ElementsAreArray(kExpectedResult));
+}
+
+TEST(NearbyFpClient, AdvertisementNondiscoverable_twoKeys) {
+  const int kBufferSize = 16;
+  uint8_t buffer[kBufferSize];
+  uint8_t salt = 0xC7;
+  uint8_t account_keys[] = {
+      2,    0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00,
+      0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x11, 0x22, 0x22, 0x33,
+      0x33, 0x44, 0x44, 0x55, 0x55, 0x66, 0x66, 0x77, 0x77, 0x88, 0x88};
+  const uint8_t kExpectedResult[] = {12,   0x16, 0x2C, 0xFE,    0x00, 0x52,
+                                     0x2F, 0xBA, 0x06, 0x42,    0x00, 0x11,
+                                     salt, 2,    0x0A, kTxPower};
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetAccountKeys(account_keys, sizeof(account_keys));
+  nearby_test_fakes_SetRandomNumber(salt);
+  nearby_fp_LoadAccountKeys();
+
+  size_t written =
+      nearby_fp_CreateNondiscoverableAdvertisement(buffer, kBufferSize, false);
+  written += nearby_fp_AppendTxPower(buffer + written, kBufferSize - written,
+                                     kTxPower);
+
+  ASSERT_EQ(kBufferSize, written);
+  ASSERT_THAT(std::vector<uint8_t>(buffer, buffer + kBufferSize),
+              ElementsAreArray(kExpectedResult));
+}
+
+TEST(NearbyFpClient, GattReadModelId) {
+  uint8_t buffer[3];
+  size_t length = sizeof(buffer);
+  nearby_fp_client_Init(NULL);
+
+  ASSERT_EQ(kNearbyStatusOK,
+            nearby_test_fakes_GattReadModelId(buffer, &length));
+
+  ASSERT_EQ(sizeof(buffer), length);
+  ASSERT_EQ(0x10, buffer[0]);
+  ASSERT_EQ(0x11, buffer[1]);
+  ASSERT_EQ(0x12, buffer[2]);
+}
+
+TEST(NearbyFpClient, SetAntiSpoofingKey) {
+  nearby_fp_client_Init(NULL);
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kBobPrivateKey, kBobPublicKey));
+}
+
+TEST(NearbyFpClient, GenSec256r1Secret_bobAlice) {
+  uint8_t secret[32];
+  nearby_fp_client_Init(NULL);
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kBobPrivateKey, kBobPublicKey));
+  ASSERT_EQ(kNearbyStatusOK,
+            nearby_test_fakes_GenSec256r1Secret(kAlicePublicKey, secret));
+
+  for (int i = 0; i < sizeof(secret); i++) {
+    ASSERT_EQ(kExpectedSharedSecret[i], secret[i])
+        << "Difference at position: " << i;
+  }
+}
+
+TEST(NearbyFpClient, GenSec256r1Secret_aliceBob) {
+  uint8_t secret[32];
+  nearby_fp_client_Init(NULL);
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kAlicePrivateKey, kAlicePublicKey));
+  ASSERT_EQ(kNearbyStatusOK,
+            nearby_test_fakes_GenSec256r1Secret(kBobPublicKey, secret));
+
+  for (int i = 0; i < sizeof(secret); i++) {
+    ASSERT_EQ(kExpectedSharedSecret[i], secret[i])
+        << "Difference at position: " << i;
+  }
+}
+
+TEST(NearbyFpClient, CreateSharedSecret_aliceBob) {
+  uint8_t secret[16];
+  nearby_fp_client_Init(NULL);
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kAlicePrivateKey, kAlicePublicKey));
+
+  ASSERT_EQ(kNearbyStatusOK,
+            nearby_fp_CreateSharedSecret(kBobPublicKey, secret));
+
+  for (int i = 0; i < sizeof(secret); i++) {
+    ASSERT_EQ(kExpectedAesKey[i], secret[i]) << "Difference at position: " << i;
+  }
+}
+
+TEST(NearbyFpClient, CreateSharedSecret_bobAlice) {
+  uint8_t secret[16];
+  nearby_fp_client_Init(NULL);
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kBobPrivateKey, kBobPublicKey));
+
+  ASSERT_EQ(kNearbyStatusOK,
+            nearby_fp_CreateSharedSecret(kAlicePublicKey, secret));
+
+  for (int i = 0; i < sizeof(secret); i++) {
+    ASSERT_EQ(kExpectedAesKey[i], secret[i]) << "Difference at position: " << i;
+  }
+}
+
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+TEST(NearbyFpClient,
+     SetAdvertisementWithBatteryNotification_AdvertisementWithPairingUI) {
+  uint8_t salt = 0xC7;
+  uint8_t account_keys[] = {
+      5,    0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
+      0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44,
+      0x44, 0x55, 0x55, 0x66, 0x66, 0x77, 0x77, 0x88, 0x88, 0x03, 0x13, 0x23,
+      0x33, 0x43, 0x53, 0x63, 0x73, 0x83, 0x93, 0xA3, 0xB3, 0xC3, 0xD3, 0xE3,
+      0xF3, 0x04, 0x14, 0x24, 0x34, 0x44, 0x54, 0x64, 0x74, 0x84, 0x94, 0xA4,
+      0xB4, 0xC4, 0xD4, 0xE4, 0xF4, 0x05, 0x15, 0x25, 0x35, 0x45, 0x55, 0x65,
+      0x75, 0x85, 0x95, 0xA5, 0xB5, 0xC5, 0xD5, 0xE5, 0xF5,
+  };
+  const uint8_t kExpectedResult[] = {
+      0x10, 0x16, 0x2c, 0xfe, 0x00, 0x90, 0x03, 0x78, 0x95, 0x67,
+      0x0c, 0xc3, 0x0a, 0xcc, 0x56, 0x11, salt, 0x02, 0x0a, kTxPower};
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetAntiSpoofingKey(kBobPrivateKey, kBobPublicKey);
+  nearby_test_fakes_SetAccountKeys(account_keys, sizeof(account_keys));
+  nearby_test_fakes_SetRandomNumber(salt);
+  nearby_fp_LoadAccountKeys();
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE |
+                                 NEARBY_FP_ADVERTISEMENT_PAIRING_UI_INDICATOR));
+
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kExpectedResult));
+}
+
+TEST(
+    NearbyFpClient,
+    SetAdvertisementWithBatteryNotification_AdvertisementNoPairingUIWithBatteryUI) {
+  uint8_t salt = 0xC7;
+  uint8_t account_keys[] = {
+      5,    0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
+      0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44,
+      0x44, 0x55, 0x55, 0x66, 0x66, 0x77, 0x77, 0x88, 0x88, 0x03, 0x13, 0x23,
+      0x33, 0x43, 0x53, 0x63, 0x73, 0x83, 0x93, 0xA3, 0xB3, 0xC3, 0xD3, 0xE3,
+      0xF3, 0x04, 0x14, 0x24, 0x34, 0x44, 0x54, 0x64, 0x74, 0x84, 0x94, 0xA4,
+      0xB4, 0xC4, 0xD4, 0xE4, 0xF4, 0x05, 0x15, 0x25, 0x35, 0x45, 0x55, 0x65,
+      0x75, 0x85, 0x95, 0xA5, 0xB5, 0xC5, 0xD5, 0xE5, 0xF5,
+  };
+  const uint8_t kExpectedResult[] = {0x14, 0x16, 0x2c, 0xfe, 0x00, 0x92,
+                                     0x30, 0xa6, 0x17, 0x10, 0x0c, 0x6c,
+                                     0xa9, 0xea, 0xf7, 0x11, salt, 0x33,
+                                     0xd5, 0xd0, 0xda, 2,    0x0a, kTxPower};
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetAntiSpoofingKey(kBobPrivateKey, kBobPublicKey);
+  nearby_test_fakes_SetAccountKeys(account_keys, sizeof(account_keys));
+  nearby_test_fakes_SetRandomNumber(salt);
+  nearby_fp_LoadAccountKeys();
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE |
+                                 NEARBY_FP_ADVERTISEMENT_BATTERY_UI_INDICATOR |
+                                 NEARBY_FP_ADVERTISEMENT_INCLUDE_BATTERY_INFO));
+
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kExpectedResult));
+}
+
+TEST(
+    NearbyFpClient,
+    SetAdvertisementWithBatteryNotification_Charging_AdvertisementContainsBatteryInfo) {
+  uint8_t salt = 0xC7;
+  uint8_t account_keys[] = {
+      5,    0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
+      0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44,
+      0x44, 0x55, 0x55, 0x66, 0x66, 0x77, 0x77, 0x88, 0x88, 0x03, 0x13, 0x23,
+      0x33, 0x43, 0x53, 0x63, 0x73, 0x83, 0x93, 0xA3, 0xB3, 0xC3, 0xD3, 0xE3,
+      0xF3, 0x04, 0x14, 0x24, 0x34, 0x44, 0x54, 0x64, 0x74, 0x84, 0x94, 0xA4,
+      0xB4, 0xC4, 0xD4, 0xE4, 0xF4, 0x05, 0x15, 0x25, 0x35, 0x45, 0x55, 0x65,
+      0x75, 0x85, 0x95, 0xA5, 0xB5, 0xC5, 0xD5, 0xE5, 0xF5,
+  };
+  const uint8_t kExpectedResult[] = {0x14, 0x16, 0x2c, 0xfe, 0x00, 0x90,
+                                     0x30, 0xa6, 0x17, 0x10, 0x0c, 0x6c,
+                                     0xa9, 0xea, 0xf7, 0x11, salt, 0x33,
+                                     0xd5, 0xd0, 0xda, 2,    0x0a, kTxPower};
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetAntiSpoofingKey(kBobPrivateKey, kBobPublicKey);
+  nearby_test_fakes_SetAccountKeys(account_keys, sizeof(account_keys));
+  nearby_test_fakes_SetRandomNumber(salt);
+  nearby_fp_LoadAccountKeys();
+  nearby_test_fakes_SetIsCharging(true);
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE |
+                                 NEARBY_FP_ADVERTISEMENT_PAIRING_UI_INDICATOR |
+                                 NEARBY_FP_ADVERTISEMENT_BATTERY_UI_INDICATOR |
+                                 NEARBY_FP_ADVERTISEMENT_INCLUDE_BATTERY_INFO));
+
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kExpectedResult));
+}
+
+TEST(
+    NearbyFpClient,
+    SetAdvertisementWithBatteryNotification_NotCharging_AdvertisementContainsBatteryInfo) {
+  uint8_t salt = 0xC7;
+  uint8_t account_keys[] = {
+      5,    0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
+      0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44,
+      0x44, 0x55, 0x55, 0x66, 0x66, 0x77, 0x77, 0x88, 0x88, 0x03, 0x13, 0x23,
+      0x33, 0x43, 0x53, 0x63, 0x73, 0x83, 0x93, 0xA3, 0xB3, 0xC3, 0xD3, 0xE3,
+      0xF3, 0x04, 0x14, 0x24, 0x34, 0x44, 0x54, 0x64, 0x74, 0x84, 0x94, 0xA4,
+      0xB4, 0xC4, 0xD4, 0xE4, 0xF4, 0x05, 0x15, 0x25, 0x35, 0x45, 0x55, 0x65,
+      0x75, 0x85, 0x95, 0xA5, 0xB5, 0xC5, 0xD5, 0xE5, 0xF5,
+  };
+  const uint8_t kExpectedResult[] = {0x14, 0x16, 0x2c, 0xfe, 0x00, 0x90,
+                                     0x46, 0x84, 0x1e, 0x84, 0x2e, 0x27,
+                                     0x05, 0x92, 0xcc, 0x11, salt, 0x33,
+                                     0x55, 0x50, 0x5a, 2,    0x0a, kTxPower};
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetAntiSpoofingKey(kBobPrivateKey, kBobPublicKey);
+  nearby_test_fakes_SetAccountKeys(account_keys, sizeof(account_keys));
+  nearby_test_fakes_SetRandomNumber(salt);
+  nearby_fp_LoadAccountKeys();
+  nearby_test_fakes_SetIsCharging(false);
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE |
+                                 NEARBY_FP_ADVERTISEMENT_PAIRING_UI_INDICATOR |
+                                 NEARBY_FP_ADVERTISEMENT_BATTERY_UI_INDICATOR |
+                                 NEARBY_FP_ADVERTISEMENT_INCLUDE_BATTERY_INFO));
+
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kExpectedResult));
+}
+
+TEST(
+    NearbyFpClient,
+    SetAdvertisementWithBatteryNotification_GetBatteryInfoFails_AdvertismentIsValid) {
+  uint8_t salt = 0xC7;
+  uint8_t account_keys[] = {
+      5,    0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
+      0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44,
+      0x44, 0x55, 0x55, 0x66, 0x66, 0x77, 0x77, 0x88, 0x88, 0x03, 0x13, 0x23,
+      0x33, 0x43, 0x53, 0x63, 0x73, 0x83, 0x93, 0xA3, 0xB3, 0xC3, 0xD3, 0xE3,
+      0xF3, 0x04, 0x14, 0x24, 0x34, 0x44, 0x54, 0x64, 0x74, 0x84, 0x94, 0xA4,
+      0xB4, 0xC4, 0xD4, 0xE4, 0xF4, 0x05, 0x15, 0x25, 0x35, 0x45, 0x55, 0x65,
+      0x75, 0x85, 0x95, 0xA5, 0xB5, 0xC5, 0xD5, 0xE5, 0xF5,
+  };
+  const uint8_t kExpectedResult[] = {
+      0x10, 0x16, 0x2c, 0xfe, 0x00, 0x90, 0x03, 0x78, 0x95, 0x67,
+      0x0c, 0xc3, 0x0a, 0xcc, 0x56, 0x11, salt, 2,    0x0a, kTxPower};
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetAntiSpoofingKey(kBobPrivateKey, kBobPublicKey);
+  nearby_test_fakes_SetAccountKeys(account_keys, sizeof(account_keys));
+  nearby_test_fakes_SetRandomNumber(salt);
+  nearby_fp_LoadAccountKeys();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusUnsupported);
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE |
+                                 NEARBY_FP_ADVERTISEMENT_PAIRING_UI_INDICATOR |
+                                 NEARBY_FP_ADVERTISEMENT_BATTERY_UI_INDICATOR |
+                                 NEARBY_FP_ADVERTISEMENT_INCLUDE_BATTERY_INFO));
+
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kExpectedResult));
+}
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient,
+     RfcommConnected_HasBatteryInfo_SendsModelIdBleAddressAndBatteryInfo) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kExpectedRfcommOutput[] = {// Model Id
+                                               3, 1, 0, 3, 0x10, 0x11, 0x12,
+                                               // Ble Address
+                                               3, 2, 0, 6, 0x6b, 0xab, 0xab,
+                                               0xab, 0xab, 0xab,
+                                               // Battery level
+                                               3, 3, 0, 3, 0xd5, 0xd0, 0xda,
+                                               // Battery remaining time
+                                               3, 4, 0, 1, 100};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_BatteryTime(100);
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetIsCharging(true);
+
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusOK);
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  ASSERT_EQ(1, message_stream_events.size());
+  ASSERT_EQ(MessageStreamConnectedEvent(kPeerAddress),
+            *message_stream_events[0]);
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient, EnableSilenceMode_RfcommConnected) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kExpectedRfcommOutput[] = {// Model Id
+                                               3, 1, 0, 3, 0x10, 0x11, 0x12,
+                                               // Ble Address
+                                               3, 2, 0, 6, 0x6b, 0xab, 0xab,
+                                               0xab, 0xab, 0xab,
+                                               // Battery level
+                                               3, 3, 0, 3, 0xd5, 0xd0, 0xda,
+                                               // Battery remaining time
+                                               3, 4, 0, 1, 100,
+                                               // Enable silence mode
+                                               1, 1, 0, 0};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_BatteryTime(100);
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_fp_client_SetSilenceMode(kPeerAddress, true);
+
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient, DisableSilenceMode_RfcommConnected) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kExpectedRfcommOutput[] = {// Model Id
+                                               3, 1, 0, 3, 0x10, 0x11, 0x12,
+                                               // Ble Address
+                                               3, 2, 0, 6, 0x6b, 0xab, 0xab,
+                                               0xab, 0xab, 0xab,
+                                               // Battery level
+                                               3, 3, 0, 3, 0xd5, 0xd0, 0xda,
+                                               // Battery remaining time
+                                               3, 4, 0, 1, 100,
+                                               // Disable silence mode
+                                               1, 2, 0, 0};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_BatteryTime(100);
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_fp_client_SetSilenceMode(kPeerAddress, false);
+
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient, BatteryLevelLongForm_RfcommConnected) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kExpectedRfcommOutput[] = {// Model Id
+                                               3, 1, 0, 3, 0x10, 0x11, 0x12,
+                                               // Ble Address
+                                               3, 2, 0, 6, 0x6b, 0xab, 0xab,
+                                               0xab, 0xab, 0xab,
+                                               // Battery level
+                                               3, 3, 0, 3, 0xd5, 0xd0, 0xda,
+                                               // Battery remaining time (256)
+                                               3, 4, 0, 2, 1, 0,
+                                               // Disable silence mode
+                                               1, 2, 0, 0};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_BatteryTime(0x100);
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_fp_client_SetSilenceMode(kPeerAddress, false);
+
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient, EnableSilenceMode_NoRfcommConnection_ReturnsError) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+
+  ASSERT_EQ(kNearbyStatusError,
+            nearby_fp_client_SetSilenceMode(kPeerAddress, true));
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient, SignalLogBufferFull_RfcommConnected) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kExpectedRfcommOutput[] = {// Model Id
+                                               3, 1, 0, 3, 0x10, 0x11, 0x12,
+                                               // Ble Address
+                                               3, 2, 0, 6, 0x6b, 0xab, 0xab,
+                                               0xab, 0xab, 0xab,
+                                               // Battery level
+                                               3, 3, 0, 3, 0xd5, 0xd0, 0xda,
+                                               // Battery remaining time
+                                               3, 4, 0, 1, 100,
+                                               // Signal log buffer full
+                                               2, 1, 0, 0};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_BatteryTime(100);
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_fp_client_SignalLogBufferFull(kPeerAddress);
+
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient,
+     ReceiveActiveComponentsRequest_SendsActiveComponentResponse) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kPeerMessage[] = {// active components request
+                                      3, 5, 0, 0};
+  const nearby_event_MessageStreamReceived kExpectedMessage = {
+      .peer_address = kPeerAddress,
+      .message_group = kPeerMessage[0],
+      .message_code = kPeerMessage[1],
+      .length = kPeerMessage[2] * 256 + kPeerMessage[3],
+      .data = NULL};
+  constexpr uint8_t kExpectedRfcommOutput[] = {// Model Id
+                                               3, 1, 0, 3, 0x10, 0x11, 0x12,
+                                               // Ble Address
+                                               3, 2, 0, 6, 0x6b, 0xab, 0xab,
+                                               0xab, 0xab, 0xab,
+                                               // Battery level
+                                               3, 3, 0, 3, 0xd5, 0xd0, 0xda,
+                                               // Battery remaining time
+                                               3, 4, 0, 1, 100,
+                                               // Active component response
+                                               3, 6, 0, 1, 0x00};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_BatteryTime(100);
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_test_fakes_MessageStreamReceived(kPeerAddress, kPeerMessage,
+                                          sizeof(kPeerMessage));
+
+  ASSERT_EQ(MessageStreamReceivedEvent(&kExpectedMessage),
+            *message_stream_events[1]);
+
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient, ReceiveCapabilities) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kCapabilities = 0x11;
+  constexpr uint8_t kPeerMessage[] = {// Seeker capabilities request,
+                                      // Companion app, silence mode.
+                                      3, 7, 0, 1, kCapabilities};
+  const nearby_event_MessageStreamReceived kExpectedMessage = {
+      .peer_address = kPeerAddress,
+      .message_group = kPeerMessage[0],
+      .message_code = kPeerMessage[1],
+      .length = kPeerMessage[2] * 256 + kPeerMessage[3],
+      .data = (uint8_t*)kPeerMessage + 4};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_test_fakes_MessageStreamReceived(kPeerAddress, kPeerMessage,
+                                          sizeof(kPeerMessage));
+
+  ASSERT_EQ(MessageStreamReceivedEvent(&kExpectedMessage),
+            *message_stream_events[1]);
+  ASSERT_EQ(kCapabilities, GetCapability(kPeerAddress));
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient, ReceivePlatformType) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kPeerMessage[] = {// platform type request,
+                                      // Android, Pie SDK
+                                      3, 8, 0, 2, 0x01, 0x1c};
+  const nearby_event_MessageStreamReceived kExpectedMessage = {
+      .peer_address = kPeerAddress,
+      .message_group = kPeerMessage[0],
+      .message_code = kPeerMessage[1],
+      .length = kPeerMessage[2] * 256 + kPeerMessage[3],
+      .data = (uint8_t*)kPeerMessage + 4};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_test_fakes_MessageStreamReceived(kPeerAddress, kPeerMessage,
+                                          sizeof(kPeerMessage));
+
+  ASSERT_EQ(MessageStreamReceivedEvent(&kExpectedMessage),
+            *message_stream_events[1]);
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient, ReceiveRingRequest) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kRingTimeSeconds = 100;
+  constexpr uint16_t kRingTimeDeciseconds = 10 * kRingTimeSeconds;
+  constexpr uint8_t kPeerMessage[] = {
+      // Ring request, both buds,
+      // 100 seconds.
+      4,
+      1,
+      0,
+      2,
+      MESSAGE_CODE_RING_LEFT | MESSAGE_CODE_RING_RIGHT,
+      kRingTimeSeconds};
+  const nearby_event_MessageStreamReceived kExpectedMessage = {
+      .peer_address = kPeerAddress,
+      .message_group = kPeerMessage[0],
+      .message_code = kPeerMessage[1],
+      .length = kPeerMessage[2] * 256 + kPeerMessage[3],
+      .data = (uint8_t*)kPeerMessage + 4};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_test_fakes_MessageStreamReceived(kPeerAddress, kPeerMessage,
+                                          sizeof(kPeerMessage));
+
+  ASSERT_EQ(MessageStreamReceivedEvent(&kExpectedMessage),
+            *message_stream_events[1]);
+  ASSERT_EQ(nearby_test_fakes_GetRingCommand(),
+            MESSAGE_CODE_RING_LEFT | MESSAGE_CODE_RING_RIGHT);
+  ASSERT_EQ(nearby_test_fakes_GetRingTimeout(), kRingTimeDeciseconds);
+}
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+TEST(NearbyFpClient, Pairing_ProviderInitiated) {
+  uint8_t salt = 0xAB;
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_Init(NULL));
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kBobPrivateKey, kBobPublicKey));
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_DISCOVERABLE));
+  nearby_test_fakes_SetRandomNumber(salt);
+
+  // Provider sets the advertisement
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kDiscoverableAdvertisement));
+
+  uint8_t request[16];
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 0x40;  // bit 1 (msb) set
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // Seeker's address
+  request[8] = 0xB0;
+  request[9] = 0xB1;
+  request[10] = 0xB2;
+  request[11] = 0xB3;
+  request[12] = 0xB4;
+  request[13] = 0xB5;
+  // salt
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  uint8_t encrypted[16 + 64];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+  // Seeker responds
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  auto response = nearby_test_fakes_GetGattNotifications().at(kKeyBasedPairing);
+  std::cout << VecToString(response) << std::endl;
+  uint8_t decrypted_response[16];
+  uint8_t expected_decrypted_response[16] = {0x01, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4,
+                                             0xA5, salt, salt, salt, salt, salt,
+                                             salt, salt, salt, salt};
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_decrypted_response[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  // Provider sends pairing request
+  ASSERT_EQ(kRemoteDevice, nearby_test_fakes_GetPairingRequestAddress());
+
+  // BT negotatiates passkey 123456 (0x01E240)
+  uint8_t raw_passkey_block[16] = {0x02, 0x01, 0xE2, 0x40};
+  uint8_t encrypted_passkey_block[16];
+  nearby_test_fakes_Aes128Encrypt(raw_passkey_block, encrypted_passkey_block,
+                                  kExpectedAesKey);
+  // Seeker sends the passkey to provider
+  nearby_fp_fakes_ReceivePasskey(encrypted_passkey_block,
+                                 sizeof(encrypted_passkey_block));
+
+  // Provider sends the passkey to seeker
+  response = nearby_test_fakes_GetGattNotifications().at(kPasskey);
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  uint8_t expected_passkey_block[16] = {0x03, 0x01, 0xE2, 0x40, salt, salt,
+                                        salt, salt, salt, salt, salt, salt,
+                                        salt, salt, salt, salt};
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_passkey_block[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  ASSERT_EQ(123456, nearby_test_fakes_GetRemotePasskey());
+  ASSERT_EQ(kRemoteDevice, nearby_test_fakes_GetPairedDevice());
+
+  // Seeker sends their account key
+  WriteToAccountKey();
+
+  std::cout << "Account keys: "
+            << VecToString(nearby_test_fakes_GetRawAccountKeys()) << std::endl;
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(1, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+}
+
+// Pairing flow where the Seeker writes to the account key a little bit too
+// early - while the BT bonding is still in progress
+TEST(NearbyFpClient, Pairing_WriteKeyBeforePaired_PairingSuccessful) {
+  uint8_t salt = 0xAB;
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_Init(NULL));
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kBobPrivateKey, kBobPublicKey));
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_DISCOVERABLE));
+  nearby_test_fakes_SetRandomNumber(salt);
+
+  // Provider sets the advertisement
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kDiscoverableAdvertisement));
+
+  uint8_t request[16];
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 0x40;  // bit 1 (msb) set
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // Seeker's address
+  request[8] = 0xB0;
+  request[9] = 0xB1;
+  request[10] = 0xB2;
+  request[11] = 0xB3;
+  request[12] = 0xB4;
+  request[13] = 0xB5;
+  // salt
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  uint8_t encrypted[16 + 64];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+  // Seeker responds
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  auto response = nearby_test_fakes_GetGattNotifications().at(kKeyBasedPairing);
+  std::cout << VecToString(response) << std::endl;
+  uint8_t decrypted_response[16];
+  uint8_t expected_decrypted_response[16] = {0x01, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4,
+                                             0xA5, salt, salt, salt, salt, salt,
+                                             salt, salt, salt, salt};
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_decrypted_response[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  // Provider sends pairing request
+  ASSERT_EQ(kRemoteDevice, nearby_test_fakes_GetPairingRequestAddress());
+
+  // Seeker sends their account key
+  WriteToAccountKey();
+
+  // BT negotatiates passkey 123456 (0x01E240)
+  uint8_t raw_passkey_block[16] = {0x02, 0x01, 0xE2, 0x40};
+  uint8_t encrypted_passkey_block[16];
+  nearby_test_fakes_Aes128Encrypt(raw_passkey_block, encrypted_passkey_block,
+                                  kExpectedAesKey);
+  // Seeker sends the passkey to provider
+  nearby_fp_fakes_ReceivePasskey(encrypted_passkey_block,
+                                 sizeof(encrypted_passkey_block));
+
+  // Provider sends the passkey to seeker
+  response = nearby_test_fakes_GetGattNotifications().at(kPasskey);
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  uint8_t expected_passkey_block[16] = {0x03, 0x01, 0xE2, 0x40, salt, salt,
+                                        salt, salt, salt, salt, salt, salt,
+                                        salt, salt, salt, salt};
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_passkey_block[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  ASSERT_EQ(123456, nearby_test_fakes_GetRemotePasskey());
+  ASSERT_EQ(kRemoteDevice, nearby_test_fakes_GetPairedDevice());
+
+  std::cout << "Account keys: "
+            << VecToString(nearby_test_fakes_GetRawAccountKeys()) << std::endl;
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(1, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+}
+
+TEST(NearbyFpClient, Pairing_SeekerInitiated_PairingSuccessful) {
+  uint8_t salt = 0xAB;
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_Init(NULL));
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kBobPrivateKey, kBobPublicKey));
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_DISCOVERABLE));
+  nearby_test_fakes_SetRandomNumber(salt);
+
+  // Provider sets the advertisement
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kDiscoverableAdvertisement));
+
+  uint8_t request[16];
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 0x00;  // bit 1 (msb) cleared
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // salt
+  for (int i = 8; i < 16; i++) {
+    request[i] = 0xCD + i;
+  }
+  uint8_t encrypted[16 + 64];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+  // Seeker responds
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  auto response = nearby_test_fakes_GetGattNotifications().at(kKeyBasedPairing);
+  std::cout << VecToString(response) << std::endl;
+  uint8_t decrypted_response[16];
+  uint8_t expected_decrypted_response[16] = {0x01, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4,
+                                             0xA5, salt, salt, salt, salt, salt,
+                                             salt, salt, salt, salt};
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_decrypted_response[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  // Seeker sends pairing request
+  nearby_test_fakes_SimulatePairing(kRemoteDevice);
+
+  // BT negotatiates passkey 123456 (0x01E240)
+  uint8_t raw_passkey_block[16] = {0x02, 0x01, 0xE2, 0x40};
+  uint8_t encrypted_passkey_block[16];
+  nearby_test_fakes_Aes128Encrypt(raw_passkey_block, encrypted_passkey_block,
+                                  kExpectedAesKey);
+  // Seeker sends the passkey to provider
+  nearby_fp_fakes_ReceivePasskey(encrypted_passkey_block,
+                                 sizeof(encrypted_passkey_block));
+
+  // Provider sends the passkey to seeker
+  response = nearby_test_fakes_GetGattNotifications().at(kPasskey);
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  uint8_t expected_passkey_block[16] = {0x03, 0x01, 0xE2, 0x40, salt, salt,
+                                        salt, salt, salt, salt, salt, salt,
+                                        salt, salt, salt, salt};
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_passkey_block[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  ASSERT_EQ(123456, nearby_test_fakes_GetRemotePasskey());
+  ASSERT_EQ(kRemoteDevice, nearby_test_fakes_GetPairedDevice());
+
+  // Seeker sends their account key
+  WriteToAccountKey();
+
+  std::cout << "Account keys: "
+            << VecToString(nearby_test_fakes_GetRawAccountKeys()) << std::endl;
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(1, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+}
+
+TEST(NearbyFpClient, Pairing_SeekerInitiatedWriteKeyEarly_PairingSuccessful) {
+  uint8_t salt = 0xAB;
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_Init(NULL));
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kBobPrivateKey, kBobPublicKey));
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_DISCOVERABLE));
+  nearby_test_fakes_SetRandomNumber(salt);
+
+  // Provider sets the advertisement
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kDiscoverableAdvertisement));
+
+  uint8_t request[16];
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 0x00;  // bit 1 (msb) cleared
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // salt
+  for (int i = 8; i < 16; i++) {
+    request[i] = 0xCD + i;
+  }
+  uint8_t encrypted[16 + 64];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+  // Seeker responds
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  auto response = nearby_test_fakes_GetGattNotifications().at(kKeyBasedPairing);
+  std::cout << VecToString(response) << std::endl;
+  uint8_t decrypted_response[16];
+  uint8_t expected_decrypted_response[16] = {0x01, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4,
+                                             0xA5, salt, salt, salt, salt, salt,
+                                             salt, salt, salt, salt};
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_decrypted_response[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  // Seeker sends pairing request
+  nearby_test_fakes_SimulatePairing(kRemoteDevice);
+
+  // Seeker sends their account key
+  WriteToAccountKey();
+
+  // BT negotatiates passkey 123456 (0x01E240)
+  uint8_t raw_passkey_block[16] = {0x02, 0x01, 0xE2, 0x40};
+  uint8_t encrypted_passkey_block[16];
+  nearby_test_fakes_Aes128Encrypt(raw_passkey_block, encrypted_passkey_block,
+                                  kExpectedAesKey);
+  // Seeker sends the passkey to provider
+  nearby_fp_fakes_ReceivePasskey(encrypted_passkey_block,
+                                 sizeof(encrypted_passkey_block));
+
+  // Provider sends the passkey to seeker
+  response = nearby_test_fakes_GetGattNotifications().at(kPasskey);
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  uint8_t expected_passkey_block[16] = {0x03, 0x01, 0xE2, 0x40, salt, salt,
+                                        salt, salt, salt, salt, salt, salt,
+                                        salt, salt, salt, salt};
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_passkey_block[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  ASSERT_EQ(123456, nearby_test_fakes_GetRemotePasskey());
+  ASSERT_EQ(kRemoteDevice, nearby_test_fakes_GetPairedDevice());
+
+  std::cout << "Account keys: "
+            << VecToString(nearby_test_fakes_GetRawAccountKeys()) << std::endl;
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(1, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+}
+
+TEST(NearbyFpClient, Pair_AccountKeyStorageFull_AddsNewKey) {
+  uint8_t account_keys[] = {
+      5,    0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA,
+      0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44,
+      0x44, 0x55, 0x55, 0x66, 0x66, 0x77, 0x77, 0x88, 0x88, 0x03, 0x13, 0x23,
+      0x33, 0x43, 0x53, 0x63, 0x73, 0x83, 0x93, 0xA3, 0xB3, 0xC3, 0xD3, 0xE3,
+      0xF3, 0x04, 0x14, 0x24, 0x34, 0x44, 0x54, 0x64, 0x74, 0x84, 0x94, 0xA4,
+      0xB4, 0xC4, 0xD4, 0xE4, 0xF4, 0x05, 0x15, 0x25, 0x35, 0x45, 0x55, 0x65,
+      0x75, 0x85, 0x95, 0xA5, 0xB5, 0xC5, 0xD5, 0xE5, 0xF5,
+  };
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetAccountKeys(account_keys, sizeof(account_keys));
+  nearby_fp_LoadAccountKeys();
+
+  Pair(0x40);
+
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(5, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+}
+
+TEST(NearbyFpClient, ReadModelId) {
+  std::vector<uint8_t> expected_model = {0x10, 0x11, 0x12};
+  uint8_t model[3];
+  size_t length = sizeof(model);
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_Init(NULL));
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_GattReadModelId(model, &length));
+
+  ASSERT_EQ(sizeof(model), length);
+  ASSERT_EQ(expected_model, std::vector<uint8_t>(model, model + length));
+}
+
+TEST(NearbyFpClient, Aes128Encrypt) {
+  uint8_t input[16] = {0xF3, 0x0F, 0x4E, 0x78, 0x6C, 0x59, 0xA7, 0xBB,
+                       0xF3, 0x87, 0x3B, 0x5A, 0x49, 0xBA, 0x97, 0xEA};
+  uint8_t key[16] = {0xA0, 0xBA, 0xF0, 0xBB, 0x95, 0x1F, 0xF7, 0xB6,
+                     0xCF, 0x5E, 0x3F, 0x45, 0x61, 0xC3, 0x32, 0x1D};
+  uint8_t expected_output[16] = {0xAC, 0x9A, 0x16, 0xF0, 0x95, 0x3A,
+                                 0x3F, 0x22, 0x3D, 0xD1, 0x0C, 0xF5,
+                                 0x36, 0xE0, 0x9E, 0x9C};
+  uint8_t output[16];
+
+  ASSERT_EQ(kNearbyStatusOK,
+            nearby_test_fakes_Aes128Encrypt(input, output, key));
+
+  for (int i = 0; i < sizeof(expected_output); i++) {
+    ASSERT_EQ(expected_output[i], output[i]) << "Difference at position: " << i;
+  }
+}
+
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+TEST(NearbyFpClient, HmacSha256) {
+  const uint8_t kData[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0xEE,
+                           0x4A, 0x24, 0x83, 0x73, 0x80, 0x52, 0xE4, 0x4E, 0x9B,
+                           0x2A, 0x14, 0x5E, 0x5D, 0xDF, 0xAA, 0x44, 0xB9, 0xE5,
+                           0x53, 0x6A, 0xF4, 0x38, 0xE1, 0xE5, 0xC6};
+  const uint8_t kKey[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
+                          0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
+  const uint8_t kExpectedResult[] = {
+      0x55, 0xEC, 0x5E, 0x60, 0x55, 0xAF, 0x6E, 0x92, 0x61, 0x8B, 0x7D,
+      0x87, 0x10, 0xD4, 0x41, 0x37, 0x09, 0xAB, 0x5D, 0xA2, 0x7C, 0xA2,
+      0x6A, 0x66, 0xF5, 0x2E, 0x5A, 0xD4, 0xE8, 0x20, 0x90, 0x52};
+  uint8_t result[32];
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_HmacSha256(result, kKey, sizeof(kKey),
+                                                  kData, sizeof(kData)));
+
+  ASSERT_THAT(result, ElementsAreArray(kExpectedResult));
+}
+
+TEST(NearbyFpClient, AesCtr) {
+  uint8_t message[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0xEE,
+                       0x4A, 0x24, 0x83, 0x73, 0x80, 0x52, 0xE4, 0x4E, 0x9B,
+                       0x2A, 0x14, 0x5E, 0x5D, 0xDF, 0xAA, 0x44, 0xB9, 0xE5,
+                       0x53, 0x6A, 0xF4, 0x38, 0xE1, 0xE5, 0xC6};
+  const uint8_t kKey[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
+                          0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
+  const uint8_t kExpectedResult[] = {
+      0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x53, 0x6F, 0x6D, 0x65,
+      0x6F, 0x6E, 0x65, 0x27, 0x73, 0x20, 0x47, 0x6F, 0x6F, 0x67, 0x6C, 0x65,
+      0x20, 0x48, 0x65, 0x61, 0x64, 0x70, 0x68, 0x6F, 0x6E, 0x65};
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_AesCtr(message, sizeof(message), kKey));
+
+  ASSERT_THAT(message, ElementsAreArray(kExpectedResult));
+}
+
+TEST(NearbyFpClient, DecodeAdditionalData) {
+  uint8_t message[] = {0x55, 0xEC, 0x5E, 0x60, 0x55, 0xAF, 0x6E, 0x92, 0x00,
+                       0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0xEE, 0x4A,
+                       0x24, 0x83, 0x73, 0x80, 0x52, 0xE4, 0x4E, 0x9B, 0x2A,
+                       0x14, 0x5E, 0x5D, 0xDF, 0xAA, 0x44, 0xB9, 0xE5, 0x53,
+                       0x6A, 0xF4, 0x38, 0xE1, 0xE5, 0xC6};
+  const uint8_t kKey[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
+                          0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
+  const uint8_t kExpectedResult[] = {
+      0x55, 0xEC, 0x5E, 0x60, 0x55, 0xAF, 0x6E, 0x92, 0x00, 0x01, 0x02,
+      0x03, 0x04, 0x05, 0x06, 0x07, 0x53, 0x6F, 0x6D, 0x65, 0x6F, 0x6E,
+      0x65, 0x27, 0x73, 0x20, 0x47, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x20,
+      0x48, 0x65, 0x61, 0x64, 0x70, 0x68, 0x6F, 0x6E, 0x65};
+
+  ASSERT_EQ(kNearbyStatusOK,
+            nearby_fp_DecodeAdditionalData(message, sizeof(message), kKey));
+
+  ASSERT_THAT(message, ElementsAreArray(kExpectedResult));
+}
+
+TEST(NearbyFpClient, EncodeAdditionalData) {
+  uint8_t message[] = {0,    0,    0,    0,    0,    0,    0,    0,    0,
+                       0,    0,    0,    0,    0,    0,    0,    0x53, 0x6F,
+                       0x6D, 0x65, 0x6F, 0x6E, 0x65, 0x27, 0x73, 0x20, 0x47,
+                       0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x20, 0x48, 0x65, 0x61,
+                       0x64, 0x70, 0x68, 0x6F, 0x6E, 0x65};
+  std::vector<uint8_t> random_numbers = {0, 1, 2, 3, 4, 5, 6, 7};
+  const uint8_t kKey[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
+                          0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
+  const uint8_t kExpectedResult[] = {
+      0x55, 0xEC, 0x5E, 0x60, 0x55, 0xAF, 0x6E, 0x92, 0x00, 0x01, 0x02,
+      0x03, 0x04, 0x05, 0x06, 0x07, 0xEE, 0x4A, 0x24, 0x83, 0x73, 0x80,
+      0x52, 0xE4, 0x4E, 0x9B, 0x2A, 0x14, 0x5E, 0x5D, 0xDF, 0xAA, 0x44,
+      0xB9, 0xE5, 0x53, 0x6A, 0xF4, 0x38, 0xE1, 0xE5, 0xC6};
+  nearby_test_fakes_SetRandomNumberSequence(random_numbers);
+
+  ASSERT_EQ(kNearbyStatusOK,
+            nearby_fp_EncodeAdditionalData(message, sizeof(message), kKey));
+
+  ASSERT_THAT(message, ElementsAreArray(kExpectedResult));
+}
+
+TEST(NearbyFpClient, PairAndGetPersonalizedName) {
+  uint8_t name[] = {0x53, 0x6F, 0x6D, 0x65, 0x6F, 0x6E, 0x65, 0x27, 0x73,
+                    0x20, 0x47, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x20, 0x48,
+                    0x65, 0x61, 0x64, 0x70, 0x68, 0x6F, 0x6E, 0x65};
+  nearby_fp_client_Init(NULL);
+  ASSERT_EQ(kNearbyStatusOK,
+            nearby_platform_SaveValue(kStoredKeyPersonalizedName, name,
+                                      sizeof(name)));
+
+  Pair(0x60);
+
+  auto additional_data =
+      nearby_test_fakes_GetGattNotifications().at(kAdditionalData);
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_DecodeAdditionalData(
+                                 additional_data.data(), additional_data.size(),
+                                 kExpectedAesKey));
+  ASSERT_THAT(std::vector<uint8_t>(
+                  additional_data.begin() + ADDITIONAL_DATA_HEADER_SIZE,
+                  additional_data.end()),
+              ElementsAreArray(name));
+}
+
+TEST(NearbyFpClient, PairAndSetPersonalizedName) {
+  uint8_t name[] = {0,    0,    0,    0,    0,    0,    0,    0,    0,
+                    0,    0,    0,    0,    0,    0,    0,    0x53, 0x6F,
+                    0x6D, 0x65, 0x6F, 0x6E, 0x65, 0x27, 0x73, 0x20, 0x47,
+                    0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x20, 0x48, 0x65, 0x61,
+                    0x64, 0x70, 0x68, 0x6F, 0x6E, 0x65};
+  nearby_fp_EncodeAdditionalData(name, sizeof(name), kSeekerAccountKey);
+  uint8_t request[16];
+  request[0] = 0x10;  // action request
+  request[1] = 0x40;  // additional data characteristic
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  request[8] = 0;   // message group, ignored
+  request[9] = 0;   // message code, ignored
+  request[10] = 1;  // data ID, personalized name
+  // salt
+  request[11] = 0x67;
+  request[12] = 0x89;
+  request[13] = 0xAB;
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  uint8_t encrypted[16];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kSeekerAccountKey);
+  nearby_fp_client_Init(NULL);
+  Pair(0x40);
+
+  // Seeker wants to set personalized name
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+  nearby_fp_fakes_ReceiveAdditionalData(name, sizeof(name));
+
+  uint8_t result[sizeof(name) - ADDITIONAL_DATA_HEADER_SIZE];
+  size_t length = sizeof(result);
+  ASSERT_EQ(kNearbyStatusOK, nearby_platform_LoadValue(
+                                 kStoredKeyPersonalizedName, result, &length));
+  ASSERT_EQ(length, sizeof(result));
+  ASSERT_THAT(std::vector<uint8_t>(name + ADDITIONAL_DATA_HEADER_SIZE,
+                                   name + sizeof(name)),
+              ElementsAreArray(result));
+}
+#endif /* NEARBY_FP_ENABLE_ADDITIONAL_DATA */
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+TEST(NearbyFpClient, RfcommConnected_NoBatteryInfo_SendsModelIdAndBleAddress) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kExpectedRfcommOutput[] = {// Model Id
+                                               3, 1, 0, 3, 0x10, 0x11, 0x12,
+                                               // Ble Address
+                                               3, 2, 0, 6, 0x6b, 0xab, 0xab,
+                                               0xab, 0xab, 0xab};
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusUnimplemented);
+
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  ASSERT_EQ(1, message_stream_events.size());
+  ASSERT_EQ(MessageStreamConnectedEvent(kPeerAddress),
+            *message_stream_events[0]);
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+
+#ifdef NEARBY_FP_RETROACTIVE_PAIRING
+TEST(NearbyFpClient, RetroactivePair) {
+  nearby_fp_client_Init(NULL);
+  constexpr uint64_t kPeerAddress = 0xB0B1B2B3B4B5;
+  nearby_test_fakes_DevicePaired(kPeerAddress);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusOK);
+
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  uint8_t salt = 0xAB;
+
+  nearby_test_fakes_SetAntiSpoofingKey(kBobPrivateKey, kBobPublicKey);
+  nearby_test_fakes_SetRandomNumber(salt);
+
+  uint8_t request[16];
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 1 << 4;
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // Seeker's address
+  request[8] = 0xB0;
+  request[9] = 0xB1;
+  request[10] = 0xB2;
+  request[11] = 0xB3;
+  request[12] = 0xB4;
+  request[13] = 0xB5;
+  // salt
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  uint8_t encrypted[16 + 64];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+
+  // Seeker responds
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  // Seeker sends their account key
+  uint8_t encrypted_account_key_write_request[16];
+  nearby_test_fakes_Aes128Encrypt(
+      kSeekerAccountKey, encrypted_account_key_write_request, kExpectedAesKey);
+  nearby_fp_fakes_ReceiveAccountKeyWrite(
+      encrypted_account_key_write_request,
+      sizeof(encrypted_account_key_write_request));
+
+  std::cout << "Account keys: "
+            << VecToString(nearby_test_fakes_GetRawAccountKeys()) << std::endl;
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(1, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+}
+
+TEST(NearbyFpClient, RetroactivePairAfterInitialPair) {
+  constexpr uint64_t kPeerAddress = 0xB0B1B2B3B4B5;
+  uint8_t salt = 0xAB;
+
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_Init(NULL));
+  ASSERT_EQ(kNearbyStatusOK, nearby_test_fakes_SetAntiSpoofingKey(
+                                 kBobPrivateKey, kBobPublicKey));
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_client_SetAdvertisement(
+                                 NEARBY_FP_ADVERTISEMENT_DISCOVERABLE));
+  nearby_test_fakes_SetRandomNumber(salt);
+
+  // Provider sets the advertisement
+  ASSERT_THAT(nearby_test_fakes_GetAdvertisement(),
+              ElementsAreArray(kDiscoverableAdvertisement));
+
+  uint8_t request[16];
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 0x40;  // bit 1 (msb) set
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // Seeker's address
+  request[8] = 0xB0;
+  request[9] = 0xB1;
+  request[10] = 0xB2;
+  request[11] = 0xB3;
+  request[12] = 0xB4;
+  request[13] = 0xB5;
+  // salt
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  uint8_t encrypted[16 + 64];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+  // Seeker responds
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  auto response = nearby_test_fakes_GetGattNotifications().at(kKeyBasedPairing);
+  std::cout << VecToString(response) << std::endl;
+  uint8_t decrypted_response[16];
+  uint8_t expected_decrypted_response[16] = {0x01, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4,
+                                             0xA5, salt, salt, salt, salt, salt,
+                                             salt, salt, salt, salt};
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_decrypted_response[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  // Provider sends pairing request
+  ASSERT_EQ(kRemoteDevice, nearby_test_fakes_GetPairingRequestAddress());
+
+  // BT negotatiates passkey 123456 (0x01E240)
+  uint8_t raw_passkey_block[16] = {0x02, 0x01, 0xE2, 0x40};
+  uint8_t encrypted_passkey_block[16];
+  nearby_test_fakes_Aes128Encrypt(raw_passkey_block, encrypted_passkey_block,
+                                  kExpectedAesKey);
+  // Seeker sends the passkey to provider
+  nearby_fp_fakes_ReceivePasskey(encrypted_passkey_block,
+                                 sizeof(encrypted_passkey_block));
+
+  // Provider sends the passkey to seeker
+  response = nearby_test_fakes_GetGattNotifications().at(kPasskey);
+  nearby_test_fakes_Aes128Decrypt(response.data(), decrypted_response,
+                                  kExpectedAesKey);
+  uint8_t expected_passkey_block[16] = {0x03, 0x01, 0xE2, 0x40, salt, salt,
+                                        salt, salt, salt, salt, salt, salt,
+                                        salt, salt, salt, salt};
+  for (int i = 0; i < sizeof(expected_decrypted_response); i++) {
+    ASSERT_EQ(expected_passkey_block[i], decrypted_response[i])
+        << "Difference at position: " << i;
+  }
+
+  ASSERT_EQ(123456, nearby_test_fakes_GetRemotePasskey());
+  ASSERT_EQ(kRemoteDevice, nearby_test_fakes_GetPairedDevice());
+
+  // Seeker sends their account key
+  WriteToAccountKey();
+
+  std::cout << "Account keys: "
+            << VecToString(nearby_test_fakes_GetRawAccountKeys()) << std::endl;
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(1, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusOK);
+
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  // Retroactive pairing
+
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 1 << 4;
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // Seeker's address
+  request[8] = 0xB0;
+  request[9] = 0xB1;
+  request[10] = 0xB2;
+  request[11] = 0xB3;
+  request[12] = 0xB4;
+  request[13] = 0xB5;
+  // salt
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+
+  // Seeker responds
+  ASSERT_NE(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  // Seeker sends their account key
+  uint8_t encrypted_account_key_write_request[16];
+  nearby_test_fakes_Aes128Encrypt(
+      kSeekerAccountKey2, encrypted_account_key_write_request, kExpectedAesKey);
+  ASSERT_NE(kNearbyStatusOK, nearby_fp_fakes_ReceiveAccountKeyWrite(
+                                 encrypted_account_key_write_request,
+                                 sizeof(encrypted_account_key_write_request)));
+
+  keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(1, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+}
+
+TEST(NearbyFpClient, RetroactivePairTwice) {
+  nearby_fp_client_Init(NULL);
+  constexpr uint64_t kPeerAddress = 0xB0B1B2B3B4B5;
+  nearby_test_fakes_DevicePaired(kPeerAddress);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusOK);
+
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  uint8_t salt = 0xAB;
+
+  nearby_test_fakes_SetAntiSpoofingKey(kBobPrivateKey, kBobPublicKey);
+  nearby_test_fakes_SetRandomNumber(salt);
+
+  uint8_t request[16];
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 1 << 4;
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // Seeker's address
+  request[8] = 0xB0;
+  request[9] = 0xB1;
+  request[10] = 0xB2;
+  request[11] = 0xB3;
+  request[12] = 0xB4;
+  request[13] = 0xB5;
+  // salt
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  uint8_t encrypted[16 + 64];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+
+  // Seeker responds
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  // Seeker sends their account key
+  uint8_t encrypted_account_key_write_request[16];
+  nearby_test_fakes_Aes128Encrypt(
+      kSeekerAccountKey, encrypted_account_key_write_request, kExpectedAesKey);
+  ASSERT_EQ(kNearbyStatusOK, nearby_fp_fakes_ReceiveAccountKeyWrite(
+                                 encrypted_account_key_write_request,
+                                 sizeof(encrypted_account_key_write_request)));
+
+  std::cout << "Account keys: "
+            << VecToString(nearby_test_fakes_GetRawAccountKeys()) << std::endl;
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(1, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+
+  nearby_test_fakes_SetRandomNumber(salt);
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 1 << 4;
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // Seeker's address
+  request[8] = 0xB0;
+  request[9] = 0xB1;
+  request[10] = 0xB2;
+  request[11] = 0xB3;
+  request[12] = 0xB4;
+  request[13] = 0xB5;
+  // salt
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+
+  // Seeker responds
+  ASSERT_NE(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  // Seeker sends their account key
+  nearby_test_fakes_Aes128Encrypt(
+      kSeekerAccountKey2, encrypted_account_key_write_request, kExpectedAesKey);
+  ASSERT_NE(kNearbyStatusOK, nearby_fp_fakes_ReceiveAccountKeyWrite(
+                                 encrypted_account_key_write_request,
+                                 sizeof(encrypted_account_key_write_request)));
+
+  keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(1, keys.size());
+  ASSERT_EQ(std::vector<uint8_t>(kSeekerAccountKey,
+                                 kSeekerAccountKey + sizeof(kExpectedAesKey)),
+            keys.GetKeys()[0]);
+}
+
+TEST(NearbyFpClient, RetroactivePairWrongBtAddress) {
+  nearby_fp_client_Init(NULL);
+  constexpr uint64_t kPeerAddress = 0xB0B1B2B3B4B5;
+  nearby_test_fakes_DevicePaired(kPeerAddress);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusOK);
+
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  uint8_t salt = 0xAB;
+
+  nearby_test_fakes_SetAntiSpoofingKey(kBobPrivateKey, kBobPublicKey);
+  nearby_test_fakes_SetRandomNumber(salt);
+
+  uint8_t request[16];
+  request[0] = 0x00;  // key-based pairing request
+  request[1] = 1 << 4;
+  // Provider's public address
+  request[2] = 0xA0;
+  request[3] = 0xA1;
+  request[4] = 0xA2;
+  request[5] = 0xA3;
+  request[6] = 0xA4;
+  request[7] = 0xA5;
+  // Seeker's address
+  request[8] = 0xC0;
+  request[9] = 0xC1;
+  request[10] = 0xC2;
+  request[11] = 0xC3;
+  request[12] = 0xC4;
+  request[13] = 0xC5;
+  // salt
+  request[14] = 0xCD;
+  request[15] = 0xEF;
+  uint8_t encrypted[16 + 64];
+  nearby_test_fakes_Aes128Encrypt(request, encrypted, kExpectedAesKey);
+  memcpy(encrypted + 16, kAlicePublicKey, 64);
+
+  // Seeker responds
+  ASSERT_NE(kNearbyStatusOK, nearby_fp_fakes_ReceiveKeyBasedPairingRequest(
+                                 encrypted, sizeof(encrypted)));
+
+  // Seeker sends their account key
+  uint8_t encrypted_account_key_write_request[16];
+  nearby_test_fakes_Aes128Encrypt(
+      kSeekerAccountKey, encrypted_account_key_write_request, kExpectedAesKey);
+  ASSERT_NE(kNearbyStatusOK, nearby_fp_fakes_ReceiveAccountKeyWrite(
+                                 encrypted_account_key_write_request,
+                                 sizeof(encrypted_account_key_write_request)));
+
+  auto keys = nearby_test_fakes_GetAccountKeys();
+  ASSERT_EQ(0, keys.size());
+}
+#endif /* NEARBY_FP_RETROACTIVE_PAIRING */
+
+TEST(NearbyFpClient, RfcommConnected_ClientDisconnects_EmitsDisconnectEvent) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusUnimplemented);
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_test_fakes_MessageStreamDisconnected(kPeerAddress);
+
+  ASSERT_EQ(2, message_stream_events.size());
+  ASSERT_EQ(MessageStreamConnectedEvent(kPeerAddress),
+            *message_stream_events[0]);
+  ASSERT_EQ(MessageStreamDisconnectedEvent(kPeerAddress),
+            *message_stream_events[1]);
+}
+
+TEST(NearbyFpClient, RfcommConnected_PeerSendsMessage_PassMessageToClientApp) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kPeerMessage[] = {101, 102, 0, 4, 81, 82, 83, 84};
+  const nearby_event_MessageStreamReceived kExpectedMessage = {
+      .peer_address = kPeerAddress,
+      .message_group = kPeerMessage[0],
+      .message_code = kPeerMessage[1],
+      .length = kPeerMessage[2] * 256 + kPeerMessage[3],
+      .data = (uint8_t*)kPeerMessage + 4,
+  };
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusUnimplemented);
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+
+  nearby_test_fakes_MessageStreamReceived(kPeerAddress, kPeerMessage,
+                                          sizeof(kPeerMessage));
+
+  ASSERT_EQ(2, message_stream_events.size());
+  ASSERT_EQ(MessageStreamConnectedEvent(kPeerAddress),
+            *message_stream_events[0]);
+  ASSERT_EQ(MessageStreamReceivedEvent(&kExpectedMessage),
+            *message_stream_events[1]);
+}
+
+TEST(NearbyFpClient,
+     RfcommConnected_ConnectAndDisconnect_CanHandleManySessions) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kPeerMessage[] = {101, 102, 0, 4, 81, 82, 83, 84};
+  const nearby_event_MessageStreamReceived kExpectedMessage = {
+      .peer_address = kPeerAddress,
+      .message_group = kPeerMessage[0],
+      .message_code = kPeerMessage[1],
+      .length = kPeerMessage[2] * 256 + kPeerMessage[3],
+      .data = (uint8_t*)kPeerMessage + 4,
+  };
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusUnimplemented);
+
+  for (int i = 1; i < NEARBY_MAX_RFCOMM_CONNECTIONS + 10; i++) {
+    nearby_test_fakes_MessageStreamConnected(kPeerAddress + i);
+    nearby_test_fakes_MessageStreamDisconnected(kPeerAddress + i);
+  }
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress);
+  nearby_test_fakes_MessageStreamReceived(kPeerAddress, kPeerMessage,
+                                          sizeof(kPeerMessage));
+
+  int events = message_stream_events.size();
+  ASSERT_EQ(MessageStreamConnectedEvent(kPeerAddress),
+            *message_stream_events[events - 2]);
+  ASSERT_EQ(MessageStreamReceivedEvent(&kExpectedMessage),
+            *message_stream_events[events - 1]);
+}
+
+#if NEARBY_MAX_RFCOMM_CONNECTIONS > 1
+TEST(NearbyFpClient, RfcommConnected_TwoInterleavedConnections_ParsesMessages) {
+  constexpr uint64_t kPeerAddress1 = 0x123456;
+  constexpr uint8_t kPeerMessage1[] = {101, 102, 0, 4, 81, 82, 83, 84};
+  const nearby_event_MessageStreamReceived kExpectedMessage1 = {
+      .peer_address = kPeerAddress1,
+      .message_group = kPeerMessage1[0],
+      .message_code = kPeerMessage1[1],
+      .length = kPeerMessage1[2] * 256 + kPeerMessage1[3],
+      .data = (uint8_t*)kPeerMessage1 + 4,
+  };
+  constexpr uint64_t kPeerAddress2 = 0x7890ab;
+  constexpr uint8_t kPeerMessage2[] = {201, 202, 0, 5, 91, 92, 93, 94, 95};
+  const nearby_event_MessageStreamReceived kExpectedMessage2 = {
+      .peer_address = kPeerAddress2,
+      .message_group = kPeerMessage2[0],
+      .message_code = kPeerMessage2[1],
+      .length = kPeerMessage2[2] * 256 + kPeerMessage2[3],
+      .data = (uint8_t*)kPeerMessage2 + 4,
+  };
+  nearby_fp_client_Init(&kClientCallbacks);
+  Pair(0x40);
+  message_stream_events.clear();
+  nearby_test_fakes_GetRfcommOutput().clear();
+  nearby_test_fakes_SetGetBatteryInfoResult(kNearbyStatusUnimplemented);
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress1);
+  nearby_test_fakes_MessageStreamConnected(kPeerAddress2);
+
+  // Send the messages byte by byte, interleaving bytes from both connections
+  // to verify that the parses handles the streams separately
+  for (int i = 0; i < std::max(sizeof(kPeerMessage1), sizeof(kPeerMessage2));
+       i++) {
+    if (i < sizeof(kPeerMessage1)) {
+      nearby_test_fakes_MessageStreamReceived(kPeerAddress1, kPeerMessage1 + i,
+                                              1);
+    }
+    if (i < sizeof(kPeerMessage2)) {
+      nearby_test_fakes_MessageStreamReceived(kPeerAddress2, kPeerMessage2 + i,
+                                              1);
+    }
+  }
+
+  ASSERT_EQ(4, message_stream_events.size());
+  ASSERT_EQ(MessageStreamConnectedEvent(kPeerAddress1),
+            *message_stream_events[0]);
+  ASSERT_EQ(MessageStreamConnectedEvent(kPeerAddress2),
+            *message_stream_events[1]);
+  ASSERT_EQ(MessageStreamReceivedEvent(&kExpectedMessage1),
+            *message_stream_events[2]);
+  ASSERT_EQ(MessageStreamReceivedEvent(&kExpectedMessage2),
+            *message_stream_events[3]);
+}
+#endif /* NEARBY_MAX_RFCOMM_CONNECTIONS > 1 */
+
+TEST(NearbyFpClient, SendMessageStreamMessage) {
+  constexpr nearby_message_stream_Message kMessage{
+      .message_group = 20,
+      .message_code = 10,
+  };
+  constexpr uint8_t kExpectedRfcommOutput[] = {20, 10, 0, 0};
+  nearby_fp_client_Init(&kClientCallbacks);
+  nearby_test_fakes_GetRfcommOutput().clear();
+
+  nearby_fp_client_SendMessage(0x123456, &kMessage);
+
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+
+TEST(NearbyFpClient, SendAck) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr nearby_event_MessageStreamReceived kMessage{
+      .peer_address = kPeerAddress,
+      .message_group = 20,
+      .message_code = 10,
+  };
+  constexpr uint8_t kExpectedRfcommOutput[] = {0xFF, 1, 0, 2, 20, 10};
+  nearby_fp_client_Init(&kClientCallbacks);
+  nearby_test_fakes_GetRfcommOutput().clear();
+
+  nearby_fp_client_SendAck(&kMessage);
+
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+
+TEST(NearbyFpClient, SendNack) {
+  constexpr uint64_t kPeerAddress = 0x123456;
+  constexpr uint8_t kFailReason = 30;
+  constexpr nearby_event_MessageStreamReceived kMessage{
+      .peer_address = kPeerAddress,
+      .message_group = 20,
+      .message_code = 10,
+  };
+  constexpr uint8_t kExpectedRfcommOutput[] = {0xFF,        2,  0, 3,
+                                               kFailReason, 20, 10};
+  nearby_fp_client_Init(&kClientCallbacks);
+  nearby_test_fakes_GetRfcommOutput().clear();
+
+  nearby_fp_client_SendNack(&kMessage, kFailReason);
+
+  ASSERT_THAT(kExpectedRfcommOutput,
+              ElementsAreArray(nearby_test_fakes_GetRfcommOutput()));
+}
+
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+
+TEST(NearbyFpClient, TimerTriggered_RotatesBleAddress) {
+  uint64_t firstAddress, secondAddress, thirdAddress;
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetInPairingMode(false);
+  nearby_fp_client_SetAdvertisement(NEARBY_FP_ADVERTISEMENT_DISCOVERABLE);
+  firstAddress = nearby_platform_GetBleAddress();
+
+  nearby_test_fakes_SetRandomNumber(30);
+  nearby_test_fakes_SetCurrentTimeMs(nearby_test_fakes_GetNextTimerMs());
+  secondAddress = nearby_platform_GetBleAddress();
+  nearby_test_fakes_SetRandomNumber(31);
+  nearby_test_fakes_SetCurrentTimeMs(nearby_test_fakes_GetNextTimerMs());
+  thirdAddress = nearby_platform_GetBleAddress();
+
+  ASSERT_NE(firstAddress, secondAddress);
+  ASSERT_NE(secondAddress, thirdAddress);
+}
+
+TEST(NearbyFpClient, TimerTriggered_InPairingMode_DoesntRotateBleAddress) {
+  uint64_t firstAddress, secondAddress, thirdAddress;
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetInPairingMode(false);
+  nearby_fp_client_SetAdvertisement(NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE);
+  firstAddress = nearby_platform_GetBleAddress();
+
+  nearby_test_fakes_SetInPairingMode(true);
+  nearby_test_fakes_SetRandomNumber(30);
+  nearby_test_fakes_SetCurrentTimeMs(nearby_test_fakes_GetNextTimerMs());
+  secondAddress = nearby_platform_GetBleAddress();
+
+  nearby_test_fakes_SetInPairingMode(false);
+  nearby_test_fakes_SetRandomNumber(31);
+  nearby_test_fakes_SetCurrentTimeMs(nearby_test_fakes_GetNextTimerMs());
+  thirdAddress = nearby_platform_GetBleAddress();
+
+  ASSERT_EQ(firstAddress, secondAddress);
+  ASSERT_NE(secondAddress, thirdAddress);
+}
+
+TEST(NearbyFpClient, AdvertiseDisoverable_RotatesBleAddress) {
+  uint64_t firstAddress;
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetInPairingMode(false);
+  firstAddress = nearby_platform_GetBleAddress();
+
+  nearby_test_fakes_SetRandomNumber(32);
+  nearby_fp_client_SetAdvertisement(NEARBY_FP_ADVERTISEMENT_DISCOVERABLE);
+
+  ASSERT_NE(firstAddress, nearby_platform_GetBleAddress());
+}
+
+TEST(NearbyFpClient,
+     AdvertiseDisoverable_InPairingMode_DoesntRotatesBleAddress) {
+  uint64_t firstAddress;
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetInPairingMode(true);
+  firstAddress = nearby_platform_GetBleAddress();
+
+  nearby_test_fakes_SetRandomNumber(32);
+  nearby_fp_client_SetAdvertisement(NEARBY_FP_ADVERTISEMENT_DISCOVERABLE);
+
+  ASSERT_EQ(firstAddress, nearby_platform_GetBleAddress());
+}
+
+TEST(NearbyFpClient,
+     ChangeAdvertisementType_InPairingMode_DoesntRotateBleAddress) {
+  uint64_t firstAddress;
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetInPairingMode(true);
+  nearby_fp_client_SetAdvertisement(NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE);
+  firstAddress = nearby_platform_GetBleAddress();
+
+  nearby_test_fakes_SetRandomNumber(34);
+  nearby_fp_client_SetAdvertisement(NEARBY_FP_ADVERTISEMENT_DISCOVERABLE);
+
+  ASSERT_EQ(firstAddress, nearby_platform_GetBleAddress());
+}
+
+TEST(NearbyFpClient, ChangeAdvertisementFlags_DoesntRotateBleAddress) {
+  uint64_t firstAddress;
+  nearby_fp_client_Init(NULL);
+  nearby_test_fakes_SetInPairingMode(false);
+  nearby_fp_client_SetAdvertisement(NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE);
+  firstAddress = nearby_platform_GetBleAddress();
+
+  nearby_test_fakes_SetRandomNumber(36);
+  nearby_fp_client_SetAdvertisement(
+      NEARBY_FP_ADVERTISEMENT_NON_DISCOVERABLE |
+      NEARBY_FP_ADVERTISEMENT_PAIRING_UI_INDICATOR);
+
+  ASSERT_EQ(firstAddress, nearby_platform_GetBleAddress());
+}
+
+#pragma GCC diagnostic pop
+
+int main(int argc, char** argv) {
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/embedded/common/source/nearby_assert.h b/embedded/common/source/nearby_assert.h
new file mode 100644
index 0000000..2a2b01b
--- /dev/null
+++ b/embedded/common/source/nearby_assert.h
@@ -0,0 +1,45 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_ASSERT_H
+#define NEARBY_ASSERT_H
+
+#include "nearby.h"
+#include "nearby_platform_trace.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#ifdef __NEARBY_SHORT_FILE__
+#define NEARBY_ASSERT(cond)                                                  \
+  do {                                                                       \
+    if (!(cond)) {                                                           \
+      nearby_platfrom_CrashOnAssert(__NEARBY_SHORT_FILE__, __LINE__, #cond); \
+    }                                                                        \
+  } while (0)
+#else
+#define NEARBY_ASSERT(cond)                                     \
+  do {                                                          \
+    if (!(cond)) {                                              \
+      nearby_platfrom_CrashOnAssert(__FILE__, __LINE__, #cond); \
+    }                                                           \
+  } while (0)
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_ASSERT_H */
diff --git a/embedded/common/source/nearby_config.h b/embedded/common/source/nearby_config.h
new file mode 100644
index 0000000..37f7e5e
--- /dev/null
+++ b/embedded/common/source/nearby_config.h
@@ -0,0 +1,46 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_CONFIG_H
+#define NEARBY_CONFIG_H
+
+// Support FP Battery Notification extension
+#define NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+
+// Support FP Additional Data extension
+#define NEARBY_FP_ENABLE_ADDITIONAL_DATA
+
+// Personalized name max size in bytes
+#define PERSONALIZED_NAME_MAX_SIZE 64
+
+// Support FP Message Stream extension
+#define NEARBY_FP_MESSAGE_STREAM
+
+// Does the platform have a native BLE address rotation routine?
+// #define NEARBY_FP_HAVE_BLE_ADDRESS_ROTATION
+
+// The maximum size in bytes of additional data in a message in Message Stream.
+// Bigger payloads will be truncated.
+#define MAX_MESSAGE_STREAM_PAYLOAD_SIZE 8
+
+// The maximum number of concurrent RFCOMM connections
+#define NEARBY_MAX_RFCOMM_CONNECTIONS 2
+
+// Support Retroactive pairing extension
+#define NEARBY_FP_RETROACTIVE_PAIRING
+
+// The maximum number of concurrent retroactive pairing process
+#define NEARBY_MAX_RETROACTIVE_PAIRING 2
+
+#endif /* NEARBY_CONFIG_H */
diff --git a/embedded/common/source/nearby_event.h b/embedded/common/source/nearby_event.h
new file mode 100644
index 0000000..74d7690
--- /dev/null
+++ b/embedded/common/source/nearby_event.h
@@ -0,0 +1,140 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_EVENT_H
+#define NEARBY_EVENT_H
+
+#include "nearby.h"
+
+// Types of events that can be emitted by Nearby library
+typedef enum {
+  // A client connected over RFCOMM to the message stream endpoint defined in
+  // Fast Pair specification
+  // Payload: |nearby_event_MessageStreamConnected|
+  kNearbyEventMessageStreamConnected = 0,
+  // An RFCOMM client disconnected
+  // Payload: |nearby_event_MessageStreamDisonnected|
+  kNearbyEventMessageStreamDisconnected,
+  // A client has sent a message over the message stream. Some incoming messages
+  // are consumed by the Nearby library, in which case the event is not emitted.
+  // Payload: |nearby_event_MessageStreamReceived|
+  kNearbyEventMessageStreamReceived
+} nearby_event_Type;
+
+// An event emitted by Nearby library
+typedef struct {
+  // The type of the event
+  nearby_event_Type event_type;
+  // Optional event payload. |payload| should be cast to one the structs defined
+  // below.
+  uint8_t *payload;
+} nearby_event_Event;
+
+// Payload for |kNearbyEventMessageStreamConnected| event.
+typedef struct {
+  // The client BT address
+  uint64_t peer_address;
+} nearby_event_MessageStreamConnected;
+
+// Payload for |kNearbyEventMessageStreamDisconnected| event.
+typedef struct {
+  // The client BT address
+  uint64_t peer_address;
+} nearby_event_MessageStreamDisconnected;
+
+// Message group Bluetooth
+#define MESSAGE_GROUP_BLUETOOTH 1
+
+// If the device supports silence mode, the client app should send either
+// ENABLE_SILENCE_MODE or DISABLE_SILENCE_MODE message to the seeker once RFCOMM
+// is connected to ensure the correct state is propagated.
+// Direction: Provider -> Seeker
+// Handled by: Client app
+#define MESSAGE_CODE_ENABLE_SILENCE_MODE 1
+
+// Direction: Provider -> Seeker
+// Handled by: Client app
+#define MESSAGE_CODE_DISABLE_SILENCE_MODE 2
+
+// Message group Companion App Event
+#define MESSAGE_GROUP_COMPANION_APP_EVENT 2
+
+// Direction: Provider -> Seeker
+// Handled by: Client app
+#define MESSAGE_CODE_LOG_BUFFER_FULL 1
+
+// Message group Device Information Event
+#define MESSAGE_GROUP_DEVICE_INFORMATION_EVENT 3
+
+// Direction: Provider -> Seeker
+// Handled by: Nearby Fast Pair library
+#define MESSAGE_CODE_MODEL_ID 1
+
+// Direction: Provider -> Seeker
+// Handled by: Nearby Fast Pair library
+#define MESSAGE_CODE_BLE_ADDRESS_UPDATED 2
+
+// Direction: Provider -> Seeker
+// Handled by: Nearby Fast Pair library
+#define MESSAGE_CODE_BATTERY_UPDATED 3
+
+// Direction: Provider -> Seeker
+// Handled by: Nearby Fast Pair library
+#define MESSAGE_CODE_REMAINING_BATTERY_TIME 4
+
+// Direction: Seeker -> Provider
+// Handled by: Client app
+#define MESSAGE_CODE_ACTIVE_COMPONENT_REQUEST 5
+
+// Direction: Provider -> Seeker
+// Handled by: Client app
+#define MESSAGE_CODE_ACTIVE_COMPONENT_RESPONSE 6
+
+// Direction: Provider -> Seeker
+// Handled by: Client app
+#define MESSAGE_CODE_CAPABILITIES 7
+
+#define MESSAGE_CODE_CAPABILITIES_COMPANION_APP_INSTALLED 0
+#define MESSAGE_CODE_CAPABILITIES_SILENCE_MODE_SUPPORTED 1
+
+// Direction: Seeker -> Provider
+// Handled by: Client app
+#define MESSAGE_CODE_PLATFORM_TYPE 8
+
+// Message group Device Action Event
+#define MESSAGE_GROUP_DEVICE_ACTION_EVENT 4
+
+// Direction: Seeker -> Provider
+// Handled by: Client app
+#define MESSAGE_CODE_RING 1
+
+#define MESSAGE_CODE_RING_LEFT 0
+#define MESSAGE_CODE_RING_RIGHT 1
+
+// Payload for |kNearbyEventMessageStreamReceived| event. See
+// https://developers.google.com/nearby/fast-pair/spec#MessageStream
+typedef struct {
+  // The peer BT address
+  uint64_t peer_address;
+  // Message group
+  uint8_t message_group;
+  // Message code
+  uint8_t message_code;
+  // If |length| is 0, then there's no additional data in the message
+  size_t length;
+  // Optional. Addtional message data
+  uint8_t *data;
+} nearby_event_MessageStreamReceived;
+
+#endif /* NEARBY_EVENT_H */
diff --git a/embedded/common/source/nearby_fp_library.c b/embedded/common/source/nearby_fp_library.c
new file mode 100644
index 0000000..adf656f
--- /dev/null
+++ b/embedded/common/source/nearby_fp_library.c
@@ -0,0 +1,452 @@
+// Copyright 2022 Google LLC
+//
+// 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 "nearby_fp_library.h"
+
+#include <string.h>
+
+#include "nearby.h"
+#include "nearby_assert.h"
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+#include "nearby_platform_battery.h"
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+#include "nearby_platform_bt.h"
+#include "nearby_platform_persistence.h"
+#include "nearby_platform_se.h"
+#include "nearby_trace.h"
+#include "nearby_utils.h"
+
+#define ACCOUNT_KEY_LIST_SIZE_BYTES 81
+#define SHOW_PAIRING_INDICATION_BYTE 0
+#define DONT_SHOW_PAIRING_INDICATION_BYTE 2
+#define SHOW_BATTERY_INDICATION_BYTE 0x33
+#define DONT_SHOW_BATTERY_INDICATION_BYTE 0x34
+#define BATTERY_INFO_CHARGING (1 << 7)
+#define BATTERY_INFO_NOT_CHARGING 0
+// In the battery values, the highest bit indicates charging, the lower 7 are
+// the battery level
+#define BATTERY_LEVEL_MASK 0x7F
+#define SALT_FIELD_LENGTH_AND_TYPE_BYTE 0x11
+#define SALT_SIZE_BYTES 1
+#define BATTERY_INFO_SIZE_BYTES 4
+#define KEY_BASED_PAIRING_RESPONSE_FLAG 0x01
+#define GAP_DATA_TYPE_SERVICE_DATA_UUID 0x16
+#define FP_SERVICE_UUID 0xFE2C
+#define GAP_DATA_TYPE_TX_POWER_LEVEL_UUID 0x0A
+#define TX_POWER_DATA_SIZE 2
+
+static uint8_t account_key_list[ACCOUNT_KEY_LIST_SIZE_BYTES];
+
+static uint8_t sha_buffer[32];
+static uint8_t key_and_salt[ACCOUNT_KEY_SIZE_BYTES + SALT_SIZE_BYTES +
+                            BATTERY_INFO_SIZE_BYTES];
+
+static size_t GetAccountKeyListUsedSize() {
+  return nearby_fp_GetAccountKeyOffset(nearby_fp_GetAccountKeyCount());
+}
+
+size_t nearby_fp_GetAccountKeyCount() { return account_key_list[0]; }
+
+size_t nearby_fp_GetAccountKeyOffset(unsigned key_number) {
+  return 1 + key_number * ACCOUNT_KEY_SIZE_BYTES;
+}
+
+const uint8_t* nearby_fp_GetAccountKey(unsigned key_number) {
+  NEARBY_ASSERT(key_number < nearby_fp_GetAccountKeyCount());
+  return account_key_list + nearby_fp_GetAccountKeyOffset(key_number);
+}
+
+void nearby_fp_MarkAccountKeyAsActive(unsigned key_number) {
+  NEARBY_ASSERT(key_number < nearby_fp_GetAccountKeyCount());
+  uint8_t tmp[ACCOUNT_KEY_SIZE_BYTES];
+  if (key_number == 0) return;
+  // Move the key to the top of the list
+  nearby_fp_CopyAccountKey(tmp, key_number);
+  memmove(account_key_list + nearby_fp_GetAccountKeyOffset(1),
+          account_key_list + nearby_fp_GetAccountKeyOffset(0),
+          key_number * ACCOUNT_KEY_SIZE_BYTES);
+  memcpy(account_key_list + nearby_fp_GetAccountKeyOffset(0), tmp,
+         ACCOUNT_KEY_SIZE_BYTES);
+}
+
+void nearby_fp_CopyAccountKey(uint8_t* dest, unsigned key_number) {
+  size_t offset = nearby_fp_GetAccountKeyOffset(key_number);
+  NEARBY_ASSERT(offset + ACCOUNT_KEY_SIZE_BYTES <= ACCOUNT_KEY_LIST_SIZE_BYTES);
+  memcpy(dest, account_key_list + offset, ACCOUNT_KEY_SIZE_BYTES);
+}
+
+static unsigned combineNibbles(unsigned high, unsigned low) {
+  return ((high << 4) & 0xF0) | (low & 0x0F);
+}
+
+void nearby_fp_AddAccountKey(const uint8_t key[ACCOUNT_KEY_SIZE_BYTES]) {
+  // Find if the key is already on the list
+  unsigned i;
+  size_t key_count;
+  size_t length;
+  size_t max_bytes;
+  key_count = nearby_fp_GetAccountKeyCount();
+  for (i = 0; i < key_count; i++) {
+    if (!memcmp(key, nearby_fp_GetAccountKey(i), ACCOUNT_KEY_SIZE_BYTES)) {
+      nearby_fp_MarkAccountKeyAsActive(i);
+      return;
+    }
+  }
+  // Insert `key` at the list top
+  length = key_count * ACCOUNT_KEY_SIZE_BYTES;
+  max_bytes = ACCOUNT_KEY_LIST_SIZE_BYTES - nearby_fp_GetAccountKeyOffset(1);
+  if (length > max_bytes) {
+    // Buffer is full, the last key will fall off the edge
+    length = max_bytes;
+  } else {
+    // We have room for one more key
+    account_key_list[0]++;
+  }
+  memmove(account_key_list + nearby_fp_GetAccountKeyOffset(1),
+          account_key_list + nearby_fp_GetAccountKeyOffset(0), length);
+  memcpy(account_key_list + nearby_fp_GetAccountKeyOffset(0), key,
+         ACCOUNT_KEY_SIZE_BYTES);
+}
+
+size_t nearby_fp_CreateDiscoverableAdvertisement(uint8_t* output,
+                                                 size_t length) {
+  NEARBY_ASSERT(length >= DISCOVERABLE_ADV_SIZE_BYTES);
+
+  size_t i = 0;
+
+  // data size
+  output[i++] = 1 + FP_SERVICE_UUID_SIZE + FP_MODEL_ID_SIZE;
+
+  // data type service
+  output[i++] = GAP_DATA_TYPE_SERVICE_DATA_UUID;
+
+  // service data uuid
+  nearby_utils_CopyLittleEndian(output + i, FP_SERVICE_UUID,
+                                FP_SERVICE_UUID_SIZE);
+  i += FP_SERVICE_UUID_SIZE;
+
+  // service data
+  uint32_t model = nearby_platform_GetModelId();
+  nearby_utils_CopyBigEndian(output + i, model, FP_MODEL_ID_SIZE);
+  i += FP_MODEL_ID_SIZE;
+
+  return i;
+}
+
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+void SerializeBatteryInfo(uint8_t* output,
+                          const nearby_platform_BatteryInfo* battery_info) {
+  uint8_t charging = battery_info->is_charging ? BATTERY_INFO_CHARGING
+                                               : BATTERY_INFO_NOT_CHARGING;
+  output[0] =
+      charging | (battery_info->left_bud_battery_level & BATTERY_LEVEL_MASK);
+  output[1] =
+      charging | (battery_info->right_bud_battery_level & BATTERY_LEVEL_MASK);
+  output[2] = charging |
+              (battery_info->charging_case_battery_level & BATTERY_LEVEL_MASK);
+}
+static void AddBatteryInfo(uint8_t* output, size_t length,
+                           bool show_ui_indicator,
+                           const nearby_platform_BatteryInfo* battery_info) {
+  NEARBY_ASSERT(length >= BATTERY_INFO_SIZE_BYTES);
+  output[0] = show_ui_indicator ? SHOW_BATTERY_INDICATION_BYTE
+                                : DONT_SHOW_BATTERY_INDICATION_BYTE;
+  SerializeBatteryInfo(output + 1, battery_info);
+}
+
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+static size_t CreateNondiscoverableAdvertisement(
+    uint8_t* output, size_t length, bool show_pairing_indicator
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+    ,
+    bool show_battery_indicator, const nearby_platform_BatteryInfo* battery_info
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+) {
+  NEARBY_TRACE(VERBOSE, "nearby_fp_CreateNondiscoverableAdvertisement");
+  const unsigned kHeaderSize = FP_SERVICE_UUID_SIZE + 2;
+  NEARBY_ASSERT(length >= kHeaderSize);
+
+  unsigned i = 1;
+
+  // service data UUID
+  output[i++] = GAP_DATA_TYPE_SERVICE_DATA_UUID;
+
+  // service uuid
+  nearby_utils_CopyLittleEndian(output + i, FP_SERVICE_UUID,
+                                FP_SERVICE_UUID_SIZE);
+  i += FP_SERVICE_UUID_SIZE;
+
+  // service data
+  uint8_t salt = nearby_platform_Rand();
+  size_t n = nearby_fp_GetAccountKeyCount();
+  if (n == 0) {
+    const unsigned kMessageSize = 2;
+    NEARBY_ASSERT(length >= i + kMessageSize);
+    output[i++] = 0x00;
+    output[i++] = 0x00;
+  } else {
+    const unsigned kFlagFilterAndSaltSize = 4;
+    const size_t s = (6 * n + 15) / 5;
+    const size_t kAccountKeyDataSize = s + kFlagFilterAndSaltSize;
+    NEARBY_ASSERT(length >= i + kAccountKeyDataSize);
+    unsigned used_key_and_salt_size = ACCOUNT_KEY_SIZE_BYTES + SALT_SIZE_BYTES;
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+    // We need to add the battery info first because it's used as salt
+    if (battery_info != NULL) {
+      uint8_t battery_chunk_offset = i + kAccountKeyDataSize;
+      uint8_t* battery_chunk = output + battery_chunk_offset;
+      used_key_and_salt_size += BATTERY_INFO_SIZE_BYTES;
+      AddBatteryInfo(battery_chunk, length - battery_chunk_offset,
+                     show_battery_indicator, battery_info);
+      memcpy(key_and_salt + ACCOUNT_KEY_SIZE_BYTES + SALT_SIZE_BYTES,
+             battery_chunk, BATTERY_INFO_SIZE_BYTES);
+    }
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+    // flags
+    output[i++] = 0;
+    // filter length and type
+    output[i++] = combineNibbles(s, show_pairing_indicator
+                                        ? SHOW_PAIRING_INDICATION_BYTE
+                                        : DONT_SHOW_PAIRING_INDICATION_BYTE);
+    memset(output + i, 0, s);
+    key_and_salt[ACCOUNT_KEY_SIZE_BYTES] = salt;
+    unsigned k, j;
+    for (k = 0; k < n; k++) {
+      nearby_fp_CopyAccountKey(key_and_salt, k);
+      nearby_fp_Sha256(sha_buffer, key_and_salt, used_key_and_salt_size);
+      for (j = 0; j < 8; j++) {
+        uint32_t x = nearby_utils_GetBigEndian32(sha_buffer + 4 * j);
+        uint32_t m = x % (s * 8);
+        output[i + (m / 8)] |= (1 << (m % 8));
+      }
+    }
+    i += s;
+    output[i++] = SALT_FIELD_LENGTH_AND_TYPE_BYTE;
+    output[i++] = salt;
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+    if (battery_info != NULL) {
+      i += BATTERY_INFO_SIZE_BYTES;
+    }
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+  }
+
+  // service data size
+  output[0] = i - 1;
+
+  return i;
+}
+
+size_t nearby_fp_CreateNondiscoverableAdvertisement(
+    uint8_t* output, size_t length, bool show_pairing_indicator) {
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+  return CreateNondiscoverableAdvertisement(
+      output, length, show_pairing_indicator, false, NULL);
+#else
+  return CreateNondiscoverableAdvertisement(output, length,
+                                            show_pairing_indicator);
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+}
+
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+// |battery_info| can be NULL
+size_t nearby_fp_CreateNondiscoverableAdvertisementWithBattery(
+    uint8_t* output, size_t length, bool show_pairing_indicator,
+    bool show_battery_indicator,
+    const nearby_platform_BatteryInfo* battery_info) {
+  return CreateNondiscoverableAdvertisement(
+      output, length, show_pairing_indicator, show_battery_indicator,
+      battery_info);
+}
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+size_t nearby_fp_AppendTxPower(uint8_t* advertisement, size_t length,
+                               int8_t tx_power) {
+  size_t offset = 0;
+
+  NEARBY_ASSERT(length >= 1 + TX_POWER_DATA_SIZE);
+
+  // tx power level data size
+  advertisement[offset++] = TX_POWER_DATA_SIZE;
+
+  // tx power level UUID
+  advertisement[offset++] = GAP_DATA_TYPE_TX_POWER_LEVEL_UUID;
+
+  // tx power level
+  advertisement[offset++] = tx_power;
+
+  return offset;
+}
+
+nearby_platform_status nearby_fp_LoadAccountKeys() {
+  size_t length = sizeof(account_key_list);
+  memset(account_key_list, 0, length);
+  return nearby_platform_LoadValue(kStoredKeyAccountKeyList, account_key_list,
+                                   &length);
+}
+
+nearby_platform_status nearby_fp_SaveAccountKeys() {
+  return nearby_platform_SaveValue(kStoredKeyAccountKeyList, account_key_list,
+                                   GetAccountKeyListUsedSize());
+}
+
+nearby_platform_status nearby_fp_GattReadModelId(uint8_t* output,
+                                                 size_t* length) {
+  NEARBY_ASSERT(*length >= FP_MODEL_ID_SIZE);
+  uint32_t model = nearby_platform_GetModelId();
+  nearby_utils_CopyBigEndian(output, model, FP_MODEL_ID_SIZE);
+  *length = FP_MODEL_ID_SIZE;
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_fp_CreateSharedSecret(
+    const uint8_t remote_public_key[64],
+    uint8_t output[ACCOUNT_KEY_SIZE_BYTES]) {
+  nearby_platform_status status;
+  uint8_t secret[32];
+  uint8_t hash[32];
+  status = nearby_platform_GenSec256r1Secret(remote_public_key, secret);
+  if (status != kNearbyStatusOK) return status;
+
+  status = nearby_fp_Sha256(hash, secret, sizeof(secret));
+  if (status != kNearbyStatusOK) return status;
+
+  memcpy(output, hash, ACCOUNT_KEY_SIZE_BYTES);
+  return status;
+}
+
+nearby_platform_status nearby_fp_CreateRawKeybasedPairingResponse(
+    uint8_t output[AES_MESSAGE_SIZE_BYTES]) {
+  output[0] = KEY_BASED_PAIRING_RESPONSE_FLAG;
+  nearby_utils_CopyBigEndian(output + 1, nearby_platform_GetPublicAddress(),
+                             BT_ADDRESS_LENGTH);
+  int i;
+  for (i = 1 + BT_ADDRESS_LENGTH; i < AES_MESSAGE_SIZE_BYTES; i++) {
+    output[i] = nearby_platform_Rand();
+  }
+  return kNearbyStatusOK;
+}
+
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+#define HMAC_SHA256_KEY_SIZE 64
+#define OPAD 0x5C
+#define IPAD 0x36
+#define NONCE_SIZE 8
+#define ADDITIONAL_DATA_SHA_SIZE 8
+
+static void PadKey(uint8_t output[HMAC_SHA256_KEY_SIZE], const uint8_t* key,
+                   size_t key_length, uint8_t pad) {
+  int i;
+  for (i = 0; i < key_length; i++) {
+    *output++ = *key++ ^ pad;
+  }
+  memset(output, pad, HMAC_SHA256_KEY_SIZE - key_length);
+}
+
+#define RETURN_IF_ERROR(X)                        \
+  do {                                            \
+    nearby_platform_status status = X;            \
+    if (kNearbyStatusOK != status) return status; \
+  } while (0)
+
+static nearby_platform_status HmacSha256(uint8_t out[32],
+                                         uint8_t hmac_key[HMAC_SHA256_KEY_SIZE],
+                                         const uint8_t* data,
+                                         size_t data_length) {
+  RETURN_IF_ERROR(nearby_platform_Sha256Start());
+  RETURN_IF_ERROR(nearby_platform_Sha256Update(hmac_key, HMAC_SHA256_KEY_SIZE));
+  RETURN_IF_ERROR(nearby_platform_Sha256Update(data, data_length));
+  RETURN_IF_ERROR(nearby_platform_Sha256Finish(out));
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_fp_HmacSha256(uint8_t out[32], const uint8_t* key,
+                                            size_t key_length,
+                                            const uint8_t* data,
+                                            size_t data_length) {
+  uint8_t hmac_key[HMAC_SHA256_KEY_SIZE];
+  // out = HASH(Key XOR ipad, data)
+  PadKey(hmac_key, key, key_length, IPAD);
+  RETURN_IF_ERROR(HmacSha256(out, hmac_key, data, data_length));
+  // out = HASH(Key XOR opad, out)
+  PadKey(hmac_key, key, key_length, OPAD);
+  return HmacSha256(out, hmac_key, out, 32);
+}
+
+nearby_platform_status nearby_fp_AesCtr(
+    uint8_t* message, size_t message_length,
+    const uint8_t key[AES_MESSAGE_SIZE_BYTES]) {
+  uint8_t key_stream[AES_MESSAGE_SIZE_BYTES];
+  uint8_t iv[AES_MESSAGE_SIZE_BYTES];
+  size_t offset = NONCE_SIZE;
+  memset(iv, 0, AES_MESSAGE_SIZE_BYTES - NONCE_SIZE);
+  memcpy(iv + AES_MESSAGE_SIZE_BYTES - NONCE_SIZE, message, NONCE_SIZE);
+
+  while (offset < message_length) {
+    int i;
+    int bytes_left = message_length - offset;
+    RETURN_IF_ERROR(nearby_platform_Aes128Encrypt(iv, key_stream, key));
+    for (i = 0; i < bytes_left && i < sizeof(key_stream); i++) {
+      message[offset++] ^= key_stream[i];
+    }
+    iv[0]++;
+  }
+  return kNearbyStatusOK;
+}
+
+nearby_platform_status nearby_fp_DecodeAdditionalData(
+    uint8_t* data, size_t length, const uint8_t key[ACCOUNT_KEY_SIZE_BYTES]) {
+  NEARBY_ASSERT(length > ADDITIONAL_DATA_SHA_SIZE + NONCE_SIZE);
+  uint8_t sha[32];
+  RETURN_IF_ERROR(nearby_fp_HmacSha256(sha, key, ACCOUNT_KEY_SIZE_BYTES,
+                                       data + ADDITIONAL_DATA_SHA_SIZE,
+                                       length - ADDITIONAL_DATA_SHA_SIZE));
+  if (memcmp(sha, data, ADDITIONAL_DATA_SHA_SIZE) != 0) {
+    NEARBY_TRACE(WARNING, "Additional Data SHA check failed");
+    return kNearbyStatusError;
+  }
+  return nearby_fp_AesCtr(data + ADDITIONAL_DATA_SHA_SIZE,
+                          length - ADDITIONAL_DATA_SHA_SIZE, key);
+}
+
+nearby_platform_status nearby_fp_EncodeAdditionalData(
+    uint8_t* data, size_t length, const uint8_t key[ACCOUNT_KEY_SIZE_BYTES]) {
+  NEARBY_ASSERT(length >= ADDITIONAL_DATA_SHA_SIZE + NONCE_SIZE);
+  uint8_t sha[32];
+  int i;
+  for (i = 0; i < NONCE_SIZE; i++) {
+    data[ADDITIONAL_DATA_SHA_SIZE + i] = nearby_platform_Rand();
+  }
+  RETURN_IF_ERROR(nearby_fp_AesCtr(data + ADDITIONAL_DATA_SHA_SIZE,
+                                   length - ADDITIONAL_DATA_SHA_SIZE, key));
+  RETURN_IF_ERROR(nearby_fp_HmacSha256(sha, key, ACCOUNT_KEY_SIZE_BYTES,
+                                       data + ADDITIONAL_DATA_SHA_SIZE,
+                                       length - ADDITIONAL_DATA_SHA_SIZE));
+  memcpy(data, sha, ADDITIONAL_DATA_SHA_SIZE);
+  return kNearbyStatusOK;
+}
+#endif /* NEARBY_FP_ENABLE_ADDITIONAL_DATA */
+
+// Computes sha256 sum.
+nearby_platform_status nearby_fp_Sha256(uint8_t out[32], const void* data,
+                                        size_t length) {
+  nearby_platform_status status = nearby_platform_Sha256Start();
+  if (status == kNearbyStatusOK) {
+    status = nearby_platform_Sha256Update(data, length);
+    if (status == kNearbyStatusOK) {
+      status = nearby_platform_Sha256Finish(out);
+    }
+  }
+  return status;
+}
diff --git a/embedded/common/source/nearby_fp_library.h b/embedded/common/source/nearby_fp_library.h
new file mode 100644
index 0000000..2e27623
--- /dev/null
+++ b/embedded/common/source/nearby_fp_library.h
@@ -0,0 +1,187 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_FP_LIBRARY_H
+#define NEARBY_FP_LIBRARY_H
+
+// clang-format off
+#include "nearby_config.h"
+// clang-format on
+
+#include "nearby.h"
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+#include "nearby_platform_battery.h"
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#define BT_ADDRESS_LENGTH 6
+#define DISCOVERABLE_ADV_SIZE_BYTES 10
+// Sufficient for 5 account keys and battery notification
+#define NON_DISCOVERABLE_ADV_SIZE_BYTES 24
+
+#define ADDITIONAL_DATA_HEADER_SIZE 16
+
+// Creates advertisement with the Model Id. Returns the number of bytes written
+// to |output|.
+//
+// output - Advertisement data output buffer.
+// length - Length of output buffer.
+size_t nearby_fp_CreateDiscoverableAdvertisement(uint8_t* output,
+                                                 size_t length);
+
+// Creates advertisement with Account Key Data. Returns the number of bytes
+// written to |output|.
+//
+// output            - Advertisement data output buffer.
+// length            - Length of data output buffer.
+// show_ui_indicator - Ask seeker to show UI indication.
+size_t nearby_fp_CreateNondiscoverableAdvertisement(
+    uint8_t* output, size_t length, bool show_pairing_indicator);
+
+#ifdef NEARBY_FP_ENABLE_BATTERY_NOTIFICATION
+void SerializeBatteryInfo(uint8_t* output,
+                          const nearby_platform_BatteryInfo* battery_info);
+
+// Creates advertisement with Account Key Data and optionally battery info.
+// |battery_info| can be NULL. Returns the number of bytes written to |output|.
+//
+// output            - Advertisement data output buffer.
+// length            - Length of output buffer.
+// show_ui_indicator - Ask seeker to show UI indication.
+// battery_info      - Battery status data structure.
+size_t nearby_fp_CreateNondiscoverableAdvertisementWithBattery(
+    uint8_t* output, size_t length, bool show_pairing_indicator,
+    bool show_battery_indicator,
+    const nearby_platform_BatteryInfo* battery_info);
+#endif /* NEARBY_FP_ENABLE_BATTERY_NOTIFICATION */
+
+// Appends TX power stanza to the advertisement. |Advertisement| should point
+// past the end of advertisement created with one of the 'Create*' functions.
+//
+// advertisement - Pointer to end of advertising data.
+// length        - Remaining length of advertising data.
+// power         - Power level.
+size_t nearby_fp_AppendTxPower(uint8_t* advertisement, size_t length,
+                               int8_t tx_power);
+
+// Loads account keys from NV storage.
+nearby_platform_status nearby_fp_LoadAccountKeys();
+// Saves account keys to NV storage.
+nearby_platform_status nearby_fp_SaveAccountKeys();
+
+// Gets Specific key by number to buffer
+//
+// dest       - Buffer for key fetched.
+// key_number - Number of key to fetch.
+void nearby_fp_CopyAccountKey(uint8_t* dest, unsigned key_number);
+
+// Gets number of active keys.
+size_t nearby_fp_GetAccountKeyCount();
+// Gets offset of key by key number. Returns the offset.
+//
+// key_number - ordinal number of key to return.
+size_t nearby_fp_GetAccountKeyOffset(unsigned key_number);
+// Gets pointer to given key by ordinal number.
+//
+// key_number - Ordinal number of key to return.
+const uint8_t* nearby_fp_GetAccountKey(unsigned key_number);
+// Marks account key as active by moving it to the top of the key list.
+//
+// key_number - Original number of key to mark active.
+void nearby_fp_MarkAccountKeyAsActive(unsigned key_number);
+// Adds key to key list. Inserts key to top of list.
+//
+// key - Buffer containing key to insert.
+void nearby_fp_AddAccountKey(const uint8_t key[ACCOUNT_KEY_SIZE_BYTES]);
+// Gets fast pair model ID
+//
+// output - Buffer to hold model ID.
+// length - On input, contains the length of the buffer.
+//          On output, returns the length of the model ID returned.
+nearby_platform_status nearby_fp_GattReadModelId(uint8_t* output,
+                                                 size_t* length);
+// Creates shared secret from remote public key.
+//
+// remote_public_key - 512 bit peer public key.
+// output            - Buffer that returns the shared secret.
+nearby_platform_status nearby_fp_CreateSharedSecret(
+    const uint8_t remote_public_key[64],
+    uint8_t output[ACCOUNT_KEY_SIZE_BYTES]);
+
+// Creates raw key based paring response.
+//
+// output - Buffer returning the pairing response.
+nearby_platform_status nearby_fp_CreateRawKeybasedPairingResponse(
+    uint8_t output[AES_MESSAGE_SIZE_BYTES]);
+
+#ifdef NEARBY_FP_ENABLE_ADDITIONAL_DATA
+// Calculates HMAC SHA256 hash.
+//
+// out        - Output buffer for hash.
+// key        - Input key to calculate hash.
+// key_length - Length of key.
+// data       - Data to calculate hash.
+// data_lenth - Data length.
+nearby_platform_status nearby_fp_HmacSha256(uint8_t out[32], const uint8_t* key,
+                                            size_t key_length,
+                                            const uint8_t* data,
+                                            size_t data_length);
+
+// The first 8 bytes of the message are the nonce. The message is
+// encrypted/decrypted in place.
+//
+// message        - Message buffer in/out.
+// message_length - Length of message.
+// key            - Key.
+nearby_platform_status nearby_fp_AesCtr(
+    uint8_t* message, size_t message_length,
+    const uint8_t key[AES_MESSAGE_SIZE_BYTES]);
+
+// Decodes data package read from Additional Data characteristics. The decoding
+// happens in-place. Returns an error if HMAC-SHA checksum is invalid.
+//
+// data   - Data to decode.
+// length - Length of data.
+// key    - Key.
+nearby_platform_status nearby_fp_DecodeAdditionalData(
+    uint8_t* data, size_t length, const uint8_t key[ACCOUNT_KEY_SIZE_BYTES]);
+
+// Encodes data package for writing to Additional Data characteristics. The
+// encoding happens in-place. The first 16 bytes of the data buffer are used for
+// checksum and nonce, so the actual message must begin at offset 16.
+//
+// data   - Data to encode.
+// length - Length of data.
+// key    - Key.
+nearby_platform_status nearby_fp_EncodeAdditionalData(
+    uint8_t* data, size_t length, const uint8_t key[ACCOUNT_KEY_SIZE_BYTES]);
+
+#endif /* NEARBY_FP_ENABLE_ADDITIONAL_DATA */
+
+// Computes sha256 sum.
+//
+// out    - Output 256 bit sum.
+// data   - Data to compute sha256 sum over.
+// length - Length of data.
+nearby_platform_status nearby_fp_Sha256(uint8_t out[32], const void* data,
+                                        size_t length);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // NEARBY_FP_LIBRARY_H
diff --git a/embedded/common/source/nearby_message_stream.c b/embedded/common/source/nearby_message_stream.c
new file mode 100644
index 0000000..19d64d6
--- /dev/null
+++ b/embedded/common/source/nearby_message_stream.c
@@ -0,0 +1,126 @@
+// Copyright 2022 Google LLC
+//
+// 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 "nearby_message_stream.h"
+
+#include "nearby_platform_bt.h"
+
+#define HEADER_SIZE 4
+#define ACK_MESSAGE_SIZE 6
+#define NACK_MESSAGE_SIZE 7
+#define ACKNOWLEDGEMENT_GROUP 0xFF
+#define ACK_CODE 1
+#define NACK_CODE 2
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+void nearby_message_stream_Init(const nearby_message_stream_State* state) {
+  nearby_message_stream_Metadata* metadata =
+      (nearby_message_stream_Metadata*)state->buffer;
+  memset(metadata, 0, sizeof(nearby_message_stream_Metadata));
+  metadata->message.data =
+      state->buffer + sizeof(nearby_message_stream_Metadata);
+}
+
+void nearby_message_stream_Read(const nearby_message_stream_State* state,
+                                const uint8_t* data, size_t length) {
+  nearby_message_stream_Metadata* metadata =
+      (nearby_message_stream_Metadata*)state->buffer;
+  nearby_message_stream_Message* message = &metadata->message;
+  uint16_t available_space =
+      state->length - sizeof(nearby_message_stream_Metadata);
+  while (length > 0) {
+    switch (metadata->bytes_read) {
+      case 0:
+        message->message_group = *data;
+        break;
+      case 1:
+        message->message_code = *data;
+        break;
+      case 2:
+        message->length = ((uint16_t)*data) << 8;
+        break;
+      case 3:
+        message->length += *data;
+        break;
+      default: {
+        uint16_t offset = metadata->bytes_read - HEADER_SIZE;
+        if (offset < message->length && offset < available_space) {
+          message->data[offset] = *data;
+        }
+      }
+    }
+    data++;
+    length--;
+    metadata->bytes_read++;
+    if (metadata->bytes_read - HEADER_SIZE == message->length) {
+      if (message->length > available_space) {
+        // Message truncated
+        message->length = available_space;
+      }
+      state->on_message_received(state->peer_address, message);
+      message->length = 0;
+      metadata->bytes_read = 0;
+    }
+  }
+}
+
+nearby_platform_status nearby_message_stream_Send(
+    uint64_t peer_address, const nearby_message_stream_Message* message) {
+  nearby_platform_status status;
+  uint8_t header[HEADER_SIZE];
+  header[0] = message->message_group;
+  header[1] = message->message_code;
+  header[2] = message->length >> 8;
+  header[3] = message->length;
+  status =
+      nearby_platform_SendMessageStream(peer_address, header, sizeof(header));
+  if (kNearbyStatusOK == status && message->length > 0) {
+    status = nearby_platform_SendMessageStream(peer_address, message->data,
+                                               message->length);
+  }
+  return status;
+}
+
+nearby_platform_status nearby_message_stream_SendAck(
+    uint64_t peer_address, const nearby_message_stream_Message* message) {
+  uint8_t ack[ACK_MESSAGE_SIZE];
+  ack[0] = ACKNOWLEDGEMENT_GROUP;
+  ack[1] = ACK_CODE;
+  ack[2] = 0;
+  ack[3] = ACK_MESSAGE_SIZE - HEADER_SIZE;
+  ack[4] = message->message_group;
+  ack[5] = message->message_code;
+  return nearby_platform_SendMessageStream(peer_address, ack, sizeof(ack));
+}
+
+nearby_platform_status nearby_message_stream_SendNack(
+    uint64_t peer_address, const nearby_message_stream_Message* message,
+    uint8_t fail_reason) {
+  uint8_t nack[NACK_MESSAGE_SIZE];
+  nack[0] = ACKNOWLEDGEMENT_GROUP;
+  nack[1] = NACK_CODE;
+  nack[2] = 0;
+  nack[3] = NACK_MESSAGE_SIZE - HEADER_SIZE;
+  nack[4] = fail_reason;
+  nack[5] = message->message_group;
+  nack[6] = message->message_code;
+  return nearby_platform_SendMessageStream(peer_address, nack, sizeof(nack));
+}
+
+uint16_t nearby_message_stream_GetMaxPayloadSize(
+    const nearby_message_stream_State* state) {
+  return state->length - sizeof(nearby_message_stream_Metadata);
+}
+
+#endif /* NEARBY_FP_MESSAGE_STREAM */
diff --git a/embedded/common/source/nearby_message_stream.h b/embedded/common/source/nearby_message_stream.h
new file mode 100644
index 0000000..58d4e1e
--- /dev/null
+++ b/embedded/common/source/nearby_message_stream.h
@@ -0,0 +1,100 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_MESSAGE_STREAM_H
+#define NEARBY_MESSAGE_STREAM_H
+
+// clang-format off
+#include "nearby_config.h"
+// clang-format on
+
+#include "nearby.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+// Message group and codes for ACK and NACK messages
+#define ACKNOWLEDGEMENT_GROUP 0xFF
+#define ACK_CODE 1
+#define NACK_CODE 2
+
+#define FAIL_REASON_REDUNDANT_DEVICE_ACTION 4
+
+// Message sent and received over the message stream
+// See: https://developers.google.com/nearby/fast-pair/spec#MessageStream
+typedef struct {
+  uint8_t message_group;
+  uint8_t message_code;
+  // |data| length in native encoding
+  uint16_t length;
+  // message payload in big endian encoding
+  uint8_t* data;
+} nearby_message_stream_Message;
+
+// State and configuration of message stream parser
+typedef struct {
+  // Callback triggered when a complete message is read from the input stream
+  void (*on_message_received)(uint64_t peer_address,
+                              nearby_message_stream_Message* message);
+  // |peer_address| that is passed to |on_message_received|
+  uint64_t peer_address;
+  // Length of |buffer|
+  size_t length;
+  // The buffer used for storing an incoming message. The parser uses the buffer
+  // to store nearby_message_stream_Metadata, so not all of the space is
+  // available for message payload
+  uint8_t* buffer;
+} nearby_message_stream_State;
+
+typedef struct {
+  nearby_message_stream_Message message;
+  uint16_t bytes_read;
+} nearby_message_stream_Metadata;
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+// Initializes the parser
+void nearby_message_stream_Init(const nearby_message_stream_State* state);
+
+// Reads and deserializes data from an input stream. It is OK to pass in
+// incomplete packets. When the parses reads a complete message, it calls
+// |on_message_received|. If the message payload is too big to fit the buffer -
+// bigger than GetMaxPayloadSize(), then the payload is truncated.
+void nearby_message_stream_Read(const nearby_message_stream_State* state,
+                                const uint8_t* data, size_t length);
+
+// Serializes and sends |message| out using nearby_platform_SendMessageStream()
+nearby_platform_status nearby_message_stream_Send(
+    uint64_t peer_address, const nearby_message_stream_Message* message);
+
+// Sends out an ACK message for a received |message|. Note that not all messages
+// require an ACK
+nearby_platform_status nearby_message_stream_SendAck(
+    uint64_t peer_address, const nearby_message_stream_Message* message);
+
+// Sends out a NACK message for a received |message| with |fail_reason| reason.
+nearby_platform_status nearby_message_stream_SendNack(
+    uint64_t peer_address, const nearby_message_stream_Message* message,
+    uint8_t fail_reason);
+
+// Returns the maximum message payload size
+uint16_t nearby_message_stream_GetMaxPayloadSize(
+    const nearby_message_stream_State* state);
+
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // NEARBY_MESSAGE_STREAM_H
diff --git a/embedded/common/source/nearby_trace.h b/embedded/common/source/nearby_trace.h
new file mode 100644
index 0000000..27f55da
--- /dev/null
+++ b/embedded/common/source/nearby_trace.h
@@ -0,0 +1,84 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_TRACE_H
+#define NEARBY_TRACE_H
+
+#include "nearby.h"
+#include "nearby_platform_trace.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+// Must be kept in sync with TraceLevel in nearby_platform_trace.h
+#define NEARBY_TRACE_LEVEL_VERBOSE (1)
+#define NEARBY_TRACE_LEVEL_DEBUG (2)
+#define NEARBY_TRACE_LEVEL_INFO (3)
+#define NEARBY_TRACE_LEVEL_WARNING (4)
+#define NEARBY_TRACE_LEVEL_ERROR (5)
+#define NEARBY_TRACE_LEVEL_OFF (6)
+
+#if NEARBY_TRACE_LEVEL < NEARBY_TRACE_LEVEL_VERBOSE || \
+    NEARBY_TRACE_LEVEL > NEARBY_TRACE_LEVEL_OFF
+#error "Invalid definition of NEARBY_TRACE_LEVEL."
+#endif
+
+#if NEARBY_TRACE_LEVEL > NEARBY_TRACE_LEVEL_VERBOSE
+#define NEARBY_TRACE_VERBOSE(level, ...)
+#else
+#define NEARBY_TRACE_VERBOSE(level, ...) _NEARBY_TRACE(level, __VA_ARGS__)
+#define NEARBY_ALL_ENUM_TO_STR_ENABLE 1
+#endif
+
+#if NEARBY_TRACE_LEVEL > NEARBY_TRACE_LEVEL_DEBUG
+#define NEARBY_TRACE_DEBUG(level, ...)
+#else
+#define NEARBY_TRACE_DEBUG(level, ...) _NEARBY_TRACE(level, __VA_ARGS__)
+#endif
+
+#if NEARBY_TRACE_LEVEL > NEARBY_TRACE_LEVEL_INFO
+#define NEARBY_TRACE_INFO(level, ...)
+#else
+#define NEARBY_TRACE_INFO(level, ...) _NEARBY_TRACE(level, __VA_ARGS__)
+#endif
+
+#if NEARBY_TRACE_LEVEL > NEARBY_TRACE_LEVEL_WARNING
+#define NEARBY_TRACE_WARNING(level, ...)
+#else
+#define NEARBY_TRACE_WARNING(level, ...) _NEARBY_TRACE(level, __VA_ARGS__)
+#endif
+
+#if NEARBY_TRACE_LEVEL > NEARBY_TRACE_LEVEL_ERROR
+#define NEARBY_TRACE_ERROR(level, ...)
+#else
+#define NEARBY_TRACE_ERROR(level, ...) _NEARBY_TRACE(level, __VA_ARGS__)
+#endif
+
+#ifndef __NEARBY_SHORT_FILE__
+#define __NEARBY_SHORT_FILE__ __FILE__
+#endif
+
+#define _NEARBY_TRACE(level, ...)                               \
+  nearby_platform_Trace(level, __NEARBY_SHORT_FILE__, __LINE__, \
+                        "@g " __VA_ARGS__);
+
+#define NEARBY_TRACE(level, ...)                                               \
+  NEARBY_TRACE_##level((nearby_platform_TraceLevel)NEARBY_TRACE_LEVEL_##level, \
+                       __VA_ARGS__)
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_TRACE_H */
diff --git a/embedded/common/source/nearby_utils.c b/embedded/common/source/nearby_utils.c
new file mode 100644
index 0000000..5fa665c
--- /dev/null
+++ b/embedded/common/source/nearby_utils.c
@@ -0,0 +1,96 @@
+// Copyright 2022 Google LLC
+//
+// 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 "nearby_utils.h"
+
+uint8_t nearby_utils_GetByte(uint64_t source, int byteNumber) {
+  return source >> (8 * byteNumber);
+}
+
+void nearby_utils_CopyBigEndian(uint8_t* dest, uint64_t source, int bytes) {
+  int i;
+  for (i = 0; i < bytes; i++) {
+    dest[bytes - i - 1] = nearby_utils_GetByte(source, i);
+  }
+}
+
+void nearby_utils_CopyLittleEndian(uint8_t* dest, uint64_t source, int bytes) {
+  int i;
+  for (i = 0; i < bytes; i++) {
+    dest[i] = nearby_utils_GetByte(source, i);
+  }
+}
+
+uint32_t nearby_utils_GetBigEndian24(uint8_t* buffer) {
+  return (256 * 256 * buffer[0]) + (256 * buffer[1]) + buffer[2];
+}
+
+uint32_t nearby_utils_GetBigEndian32(uint8_t* buffer) {
+  return (256 * 256 * 256 * buffer[0]) + (256 * 256 * buffer[1]) +
+         (256 * buffer[2]) + buffer[3];
+}
+
+uint64_t nearby_utils_GetBigEndian48(uint8_t* b) {
+  return (((uint64_t)b[0]) << 40) | (((uint64_t)b[1]) << 32) |
+         (((uint64_t)b[2]) << 24) | (((uint64_t)b[3]) << 16) |
+         (((uint64_t)b[4]) << 8) | (uint64_t)b[5];
+}
+
+static char NibbleToHex(uint8_t value) {
+  if (value < 10) return '0' + value;
+  value -= 10;
+  if (value < 6) return 'A' + value;
+  return '?';
+}
+
+const char* nearby_utils_ArrayToString(const uint8_t* data, size_t length) {
+  static char buffer[80];
+  size_t buffer_size = sizeof(buffer);
+  size_t num_bytes;
+  if (2 * length < buffer_size - 1) {
+    num_bytes = 2 * length;
+    buffer[num_bytes] = 0;
+  } else {
+    // The buffer is too small to hold all of data. Let's truncate the data and
+    // append an ellipsis.
+    num_bytes = buffer_size - 4;
+    buffer[buffer_size - 1] = 0;
+    buffer[buffer_size - 2] = '.';
+    buffer[buffer_size - 3] = '.';
+    buffer[buffer_size - 4] = '.';
+    num_bytes = buffer_size - 4;
+  }
+  const uint8_t* input = data;
+  char* output = buffer;
+  for (size_t i = 0; i < num_bytes; i += 2) {
+    uint8_t x = *input++;
+    *output++ = NibbleToHex(x >> 4);
+    *output++ = NibbleToHex(x & 0x0F);
+  }
+  return buffer;
+}
+
+const char* nearby_utils_MacToString(uint64_t address) {
+  static char buffer[18];
+  char* p = buffer + sizeof(buffer) - 1;
+  *p-- = 0;
+  while (p > buffer) {
+    *p-- = NibbleToHex(address & 0xF);
+    address >>= 4;
+    *p-- = NibbleToHex(address & 0xF);
+    address >>= 4;
+    if (p >= buffer) *p-- = ':';
+  }
+  return buffer;
+}
diff --git a/embedded/common/source/nearby_utils.cc b/embedded/common/source/nearby_utils.cc
new file mode 100644
index 0000000..c643dbf
--- /dev/null
+++ b/embedded/common/source/nearby_utils.cc
@@ -0,0 +1,14 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
diff --git a/embedded/common/source/nearby_utils.h b/embedded/common/source/nearby_utils.h
new file mode 100644
index 0000000..5e24fba
--- /dev/null
+++ b/embedded/common/source/nearby_utils.h
@@ -0,0 +1,45 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_UTILS_H
+#define NEARBY_UTILS_H
+
+#include "nearby_types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+uint8_t nearby_utils_GetByte(uint64_t source, int byteNumber);
+
+void nearby_utils_CopyBigEndian(uint8_t* dest, uint64_t source, int bytes);
+void nearby_utils_CopyLittleEndian(uint8_t* dest, uint64_t source, int bytes);
+
+uint32_t nearby_utils_GetBigEndian24(uint8_t* buffer);
+uint32_t nearby_utils_GetBigEndian32(uint8_t* buffer);
+uint64_t nearby_utils_GetBigEndian48(uint8_t* buffer);
+
+// Converts the data to a hex string for debugging. Returns a pointer to a
+// static buffer. Don't call it twice in the same log message!
+const char* nearby_utils_ArrayToString(const uint8_t* data, size_t data_length);
+
+// Converts BT MAC address to string for debugging. Returns a pointer to a
+// static buffer. Don't call it twice in the same log message!
+const char* nearby_utils_MacToString(uint64_t address);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_UTILS_H */
diff --git a/embedded/common/target/gLinux/nearby_types.h b/embedded/common/target/gLinux/nearby_types.h
new file mode 100644
index 0000000..8abec31
--- /dev/null
+++ b/embedded/common/target/gLinux/nearby_types.h
@@ -0,0 +1,31 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_TYPES_H
+#define NEARBY_TYPES_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // NEARBY_TYPES_H
diff --git a/embedded/common/target/nearby.h b/embedded/common/target/nearby.h
new file mode 100644
index 0000000..9293618
--- /dev/null
+++ b/embedded/common/target/nearby.h
@@ -0,0 +1,44 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_H
+#define NEARBY_H
+
+#include "nearby_types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#define AES_MESSAGE_SIZE_BYTES 16
+#define FP_SERVICE_UUID_SIZE 2
+#define FP_MODEL_ID_SIZE 3
+#define ACCOUNT_KEY_SIZE_BYTES AES_MESSAGE_SIZE_BYTES
+
+typedef enum {
+  kNearbyStatusOK = 0,
+  kNearbyStatusError,
+  kNearbyStatusTimeout,
+  kNearbyStatusResourceExhausted,
+  kNearbyStatusUnimplemented,
+  kNearbyStatusUnsupported,
+  kNearbyStatusInvalidInput,
+  kNearbyStatusRedundantAction
+} nearby_platform_status;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_H */
diff --git a/embedded/common/target/nearby_platform_audio.h b/embedded/common/target/nearby_platform_audio.h
new file mode 100644
index 0000000..d7ebdbc
--- /dev/null
+++ b/embedded/common/target/nearby_platform_audio.h
@@ -0,0 +1,21 @@
+// Copyright 2021 Google LLC.
+#ifndef NEARBY_PLATFORM_AUDIO_H
+#define NEARBY_PLATFORM_AUDIO_H
+
+#include "nearby.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+// Returns true if right earbud is active
+bool nearby_platform_GetEarbudRightStatus();
+
+// Returns true if left earbud is active
+bool nearby_platform_GetEarbudLeftStatus();
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_PLATFORM_AUDIO_H */
diff --git a/embedded/common/target/nearby_platform_battery.h b/embedded/common/target/nearby_platform_battery.h
new file mode 100644
index 0000000..f12f4d7
--- /dev/null
+++ b/embedded/common/target/nearby_platform_battery.h
@@ -0,0 +1,56 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_PLATFORM_BATTERY_H
+#define NEARBY_PLATFORM_BATTERY_H
+
+#include "nearby.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef struct {
+  // Set to true if the device is charging.
+  bool is_charging;
+  // Battery level of the right bud. 0 - 100 range.
+  uint8_t right_bud_battery_level;
+  // Battery level of the left bud. 0 - 100 range.
+  uint8_t left_bud_battery_level;
+  // Battery level of the charging case . 0 - 100 range.
+  uint8_t charging_case_battery_level;
+  // Battery remaining time in minutes, 0-65335 range.
+  uint16_t remaining_time_minutes;
+} nearby_platform_BatteryInfo;
+
+typedef struct {
+  void (*on_battery_changed)(void);
+} nearby_platform_BatteryInterface;
+
+// Gets battery and charging info
+//
+// battery_info - Battery status structure.
+nearby_platform_status nearby_platform_GetBatteryInfo(
+    nearby_platform_BatteryInfo* battery_info);
+
+// Initializes battery module
+//
+// battery_interface - Battery status callback events.
+nearby_platform_status nearby_platform_BatteryInit(
+    nearby_platform_BatteryInterface* battery_interface);
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_PLATFORM_BATTERY_H */
diff --git a/embedded/common/target/nearby_platform_ble.h b/embedded/common/target/nearby_platform_ble.h
new file mode 100644
index 0000000..50baaa5
--- /dev/null
+++ b/embedded/common/target/nearby_platform_ble.h
@@ -0,0 +1,94 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_PLATFORM_BLE_H
+#define NEARBY_PLATFORM_BLE_H
+
+#include "nearby.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef enum {
+  kModelId = 0,       // FE2C1233-8366-4814-8EB0-01DE32100BEA (read)
+  kKeyBasedPairing,   // FE2C1234-8366-4814-8EB0-01DE32100BEA (write, notify)
+  kPasskey,           // FE2C1235-8366-4814-8EB0-01DE32100BEA (write, notify)
+  kAccountKey,        // FE2C1236-8366-4814-8EB0-01DE32100BEA (write)
+  kFirmwareRevision,  // 0x2A26
+  kAdditionalData,    // FE2C1237-8366-4814-8EB0-01DE32100BEA
+} nearby_fp_Characteristic;
+
+typedef enum {
+  kDisabled,
+  kNoLargerThan100ms,
+  kNoLargerThan250ms
+} nearby_fp_AvertisementInterval;
+
+typedef struct {
+  nearby_platform_status (*on_gatt_write)(
+      uint64_t peer_address, nearby_fp_Characteristic characteristic,
+      const uint8_t* request, size_t length);
+  nearby_platform_status (*on_gatt_read)(
+      uint64_t peer_address, nearby_fp_Characteristic characteristic,
+      uint8_t* output, size_t* length);
+
+} nearby_platform_BleInterface;
+
+// Gets BLE address.
+uint64_t nearby_platform_GetBleAddress();
+
+// Sets BLE address. Returns address after change, which may be different than
+// requested address.
+//
+// address - BLE address to set.
+uint64_t nearby_platform_SetBleAddress(uint64_t address);
+
+#ifdef NEARBY_FP_HAVE_BLE_ADDRESS_ROTATION
+// Rotates BLE address to a random resolvable private address (RPA). Returns
+// address after change.
+uint64_t nearby_platform_RotateBleAddress();
+#endif /* NEARBY_FP_HAVE_BLE_ADDRESS_ROTATION */
+
+// Sends a notification to the connected GATT client.
+//
+// peer_address   - Address of peer.
+// characteristic - Characteristic UUID
+// message        - Message buffer to transmit.
+// length         - Length of message in buffer.
+nearby_platform_status nearby_platform_GattNotify(
+    uint64_t peer_address, nearby_fp_Characteristic characteristic,
+    const uint8_t* message, size_t length);
+
+// Sets the Fast Pair advertisement payload and starts advertising at a given
+// interval.
+//
+// payload  - Advertising data.
+// length   - Length of data.
+// interval - Advertising interval code.
+nearby_platform_status nearby_platform_SetAdvertisement(
+    const uint8_t* payload, size_t length,
+    nearby_fp_AvertisementInterval interval);
+
+// Initializes BLE
+//
+// ble_interface - GATT read and write callbacks structure.
+nearby_platform_status nearby_platform_BleInit(
+    const nearby_platform_BleInterface* ble_interface);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_PLATFORM_BLE_H */
diff --git a/embedded/common/target/nearby_platform_bt.h b/embedded/common/target/nearby_platform_bt.h
new file mode 100644
index 0000000..13f6bc1
--- /dev/null
+++ b/embedded/common/target/nearby_platform_bt.h
@@ -0,0 +1,110 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_PLATFORM_BT_H
+#define NEARBY_PLATFORM_BT_H
+
+// clang-format off
+#include "nearby_config.h"
+// clang-format on
+
+#include "nearby.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef struct {
+  void (*on_pairing_request)(uint64_t peer_address);
+  void (*on_paired)(uint64_t peer_address);
+  void (*on_pairing_failed)(uint64_t peer_address);
+#ifdef NEARBY_FP_MESSAGE_STREAM
+  void (*on_message_stream_connected)(uint64_t peer_address);
+  void (*on_message_stream_disconnected)(uint64_t peer_address);
+  void (*on_message_stream_received)(uint64_t peer_address,
+                                     const uint8_t* message, size_t length);
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+} nearby_platform_BtInterface;
+
+// Returns Fast Pair Model Id.
+uint32_t nearby_platform_GetModelId();
+
+// Returns tx power level.
+int8_t nearby_platform_GetTxLevel();
+
+// Returns public BR/EDR address
+uint64_t nearby_platform_GetPublicAddress();
+
+// Returns passkey used during pairing
+uint32_t nearby_platfrom_GetPairingPassKey();
+
+// Provides the passkey received from the remote party.
+// The system should compare local and remote party and accept/decline pairing
+// request accordingly.
+//
+// passkey - Passkey
+void nearby_platform_SetRemotePasskey(uint32_t passkey);
+
+// Sends a pairing request to the Seeker
+//
+// remote_party_br_edr_address - BT address of peer.
+nearby_platform_status nearby_platform_SendPairingRequest(
+    uint64_t remote_party_br_edr_address);
+
+// Switches the device capabilities field back to default so that new
+// pairings continue as expected.
+nearby_platform_status nearby_platform_SetDefaultCapabilities();
+
+// Switches the device capabilities field to Fast Pair required configuration:
+// DisplayYes/No so that `confirm passkey` pairing method will be used.
+nearby_platform_status nearby_platform_SetFastPairCapabilities();
+
+// Sets null-terminated device name string in UTF-8 encoding
+//
+// name - Zero terminated string name of device.
+nearby_platform_status nearby_platform_SetDeviceName(const char* name);
+
+// Gets null-terminated device name string in UTF-8 encoding
+// pass buffer size in char, and get string length in char.
+//
+// name   - Buffer to return name string.
+// length - On input, the size of the name buffer.
+//          On output, returns size of name in buffer.
+nearby_platform_status nearby_platform_GetDeviceName(char* name,
+                                                     size_t* length);
+
+// Returns true if the device is in pairing mode (either fast-pair or manual).
+bool nearby_platform_IsInPairingMode();
+
+#ifdef NEARBY_FP_MESSAGE_STREAM
+// Sends message stream through RFCOMM channel initiated by Seeker
+//
+// peer_address - Peer address.
+// message      - Contents of message.
+// length       - Length of message
+nearby_platform_status nearby_platform_SendMessageStream(uint64_t peer_address,
+                                                         const uint8_t* message,
+                                                         size_t length);
+#endif /* NEARBY_FP_MESSAGE_STREAM */
+// Initializes BT module
+//
+// bt_interface - BT callbacks event structure.
+nearby_platform_status nearby_platform_BtInit(
+    const nearby_platform_BtInterface* bt_interface);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_PLATFORM_BT_H */
diff --git a/embedded/common/target/nearby_platform_os.h b/embedded/common/target/nearby_platform_os.h
new file mode 100644
index 0000000..0682add
--- /dev/null
+++ b/embedded/common/target/nearby_platform_os.h
@@ -0,0 +1,80 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_PLATFORM_OS_H
+#define NEARBY_PLATFORM_OS_H
+
+#include "nearby.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef enum {
+  kRingStateStarted = 0,
+  kRingStateFailedToStartOrStop,
+  kRingStateStoppedReasonTimeout,
+  kRingStateStoppedReasonUserAction,
+  kRingStateStoppedReasonNearbyRequest
+} nearby_platform_RingState;
+
+typedef struct {
+  nearby_platform_RingState ring_state;
+  // The number of components capable of ringing
+  // 0 - the device is incapable of ringing
+  // 1 - only a single component is capable of ringing
+  // 2 - left and right bud can ring
+  // 3 - left and right bud, and the case can ring
+  uint8_t num_components;
+  // A bitmap indicating what (sub)components are ringing. See
+  // `nearby_platform_Ring` for details
+  uint8_t components;
+  // Remaining ringing timeout in deciseconds
+  uint16_t timeout;
+} nearby_platform_RingingInfo;
+
+// Gets current time in ms.
+unsigned int nearby_platform_GetCurrentTimeMs();
+
+// Starts a timer. Returns an opaque timer handle or null on error.
+//
+// callback - Function to call when timer matures.
+// delay_ms - Number of milliseconds to run the timer.
+void* nearby_platform_StartTimer(void (*callback)(), unsigned int delay_ms);
+
+// Cancels a timer
+//
+// timer - Timer handle returned by StartTimer.
+nearby_platform_status nearby_platform_CancelTimer(void* timer);
+
+// Initializes OS module
+nearby_platform_status nearby_platform_OsInit();
+
+// Starts ringing
+//
+// `command` - the requested ringing state as a bitmap:
+// Bit 1 (0x01): ring right
+// Bit 2 (0x02): ring left
+// Bit 3 (0x04): ring case
+// Alternatively, `command` hold a special value of 0xFF to ring all
+// components that can ring.
+// `timeout` - the timeout in deciseconds. The timeout overrides the one already
+// in effect if any component of the device is already ringing.
+nearby_platform_status nearby_platform_Ring(uint8_t command, uint16_t timeout);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_PLATFORM_OS_H */
diff --git a/embedded/common/target/nearby_platform_persistence.h b/embedded/common/target/nearby_platform_persistence.h
new file mode 100644
index 0000000..d18b8ed
--- /dev/null
+++ b/embedded/common/target/nearby_platform_persistence.h
@@ -0,0 +1,55 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_PLATFORM_PERSISTENCE_H
+#define NEARBY_PLATFORM_PERSISTENCE_H
+
+#include "nearby.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef enum {
+  kStoredKeyAccountKeyList,
+  kStoredKeyPersonalizedName
+} nearby_fp_StoredKey;
+
+// Loads stored key
+//
+// key    - Type of key to fetch.
+// output - Buffer to contain retrieved key.
+// length - On input, contains the size of the output buffer.
+//          On output, contains the Length of key.
+nearby_platform_status nearby_platform_LoadValue(nearby_fp_StoredKey key,
+                                                 uint8_t* output,
+                                                 size_t* length);
+
+// Saves stored key
+//
+// key    - Type of key to store.
+// output - Buffer containing key to store.
+// length - Length of key.
+nearby_platform_status nearby_platform_SaveValue(nearby_fp_StoredKey key,
+                                                 const uint8_t* input,
+                                                 size_t length);
+
+// Initializes persistence module
+nearby_platform_status nearby_platform_PersistenceInit();
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_PLATFORM_PERSISTENCE_H */
diff --git a/embedded/common/target/nearby_platform_se.h b/embedded/common/target/nearby_platform_se.h
new file mode 100644
index 0000000..1ce6ba7
--- /dev/null
+++ b/embedded/common/target/nearby_platform_se.h
@@ -0,0 +1,80 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_PLATFORM_SE_H
+#define NEARBY_PLATFORM_SE_H
+// clang-format off
+#include "nearby_config.h"
+// clang-format on
+#include "nearby.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+// Generates a random number.
+uint8_t nearby_platform_Rand();
+
+// Computes the sha256 incrementally. Sha256Start() is called first, then
+// Sha256Update() one or more times, and finally Sha256Finish().
+nearby_platform_status nearby_platform_Sha256Start();
+
+// Intermediate sha256 compute.
+//
+// data   - Block of data to compute into sha256.
+// length - Length of data to process.
+nearby_platform_status nearby_platform_Sha256Update(const void* data,
+                                                    size_t length);
+
+// Finishes sha256 compute.
+//
+// out - Contains the final 256 bit sha.
+nearby_platform_status nearby_platform_Sha256Finish(uint8_t out[32]);
+
+// Encrypts a data block with AES128 in ECB mode.
+//
+// input - Input data block to be encrypted.
+// output - Resulting encrypted block.
+// key    - 128 bit key to use for encryption.
+nearby_platform_status nearby_platform_Aes128Encrypt(
+    const uint8_t input[AES_MESSAGE_SIZE_BYTES],
+    uint8_t output[AES_MESSAGE_SIZE_BYTES],
+    const uint8_t key[AES_MESSAGE_SIZE_BYTES]);
+
+// Decrypts a data block with AES128 in ECB mode.
+//
+// input - Input data block to be decrypted.
+// output - Resulting decrypted block.
+// key    - 128 bit key to use for decryption.
+nearby_platform_status nearby_platform_Aes128Decrypt(
+    const uint8_t input[AES_MESSAGE_SIZE_BYTES],
+    uint8_t output[AES_MESSAGE_SIZE_BYTES],
+    const uint8_t key[AES_MESSAGE_SIZE_BYTES]);
+
+// Generates a shared sec256p1 secret using remote party public key and this
+// device's private key.
+//
+// remote_party_public_key - Remote key.
+// secret                  - 256 bit shared secret.
+nearby_platform_status nearby_platform_GenSec256r1Secret(
+    const uint8_t remote_party_public_key[64], uint8_t secret[32]);
+
+// Initializes secure element module
+nearby_platform_status nearby_platform_SecureElementInit();
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_PLATFORM_SE_H */
diff --git a/embedded/common/target/nearby_platform_trace.h b/embedded/common/target/nearby_platform_trace.h
new file mode 100644
index 0000000..d38233f
--- /dev/null
+++ b/embedded/common/target/nearby_platform_trace.h
@@ -0,0 +1,60 @@
+// Copyright 2022 Google LLC
+//
+// 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.
+
+#ifndef NEARBY_PLATFORM_TRACE_H
+#define NEARBY_PLATFORM_TRACE_H
+
+#include "nearby.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef enum {
+  kTraceLevelUnknown = 0,
+  kTraceLevelVerbose = 1,
+  kTraceLevelDebug = 2,
+  kTraceLevelInfo = 3,
+  kTraceLevelWarning = 4,
+  kTraceLevelError = 5,
+} nearby_platform_TraceLevel;
+
+// Generates conditional trace line. This is usually wrapped in a macro to
+// provide compiler parameters.
+//
+// level    - Debug level, higher is less messages.
+// filename - Name of file calling trace.
+// lineno   - Source line of trace call.
+// fmt      - printf() style format string (%).
+// ...      - A series of parameters indicated by the fmt string.
+void nearby_platform_Trace(nearby_platform_TraceLevel level,
+                           const char *filename, int lineno, const char *fmt,
+                           ...);
+
+// Processes assert. Processes a failed assertion.
+//
+// filename - Name of file calling assert.
+// lineno   - Source line of assert call
+// reason   - String message indicating reason for assert.
+void nearby_platfrom_CrashOnAssert(const char *filename, int lineno,
+                                   const char *reason);
+
+// Initializes trace module.
+void nearby_platform_TraceInit(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NEARBY_PLATFORM_TRACE_H */
diff --git a/embedded/release_notes.md b/embedded/release_notes.md
new file mode 100644
index 0000000..4570b42
--- /dev/null
+++ b/embedded/release_notes.md
@@ -0,0 +1,14 @@
+### Nearby SDK for embedded systems release notes
+
+## v1.0.0-embedded
+Initial release supporting GFPS 3.1 per [specification](https://developers.google.com/nearby/fast-pair/specifications/introduction).
+
+Supported features include:
+* Initial pairing with discoverable advertisement
+* Subsequent pairing with non-discoverable advertisement
+* Retroactive Active Key for provisioning after manual pairing
+* Battery Notification
+* Personalized Name
+* RFCOMM message stream
+* Unit-tests on a simulated gLinux platform implementation 
+
diff --git a/embedded/target/gLinux/config.mk b/embedded/target/gLinux/config.mk
new file mode 100644
index 0000000..81de763
--- /dev/null
+++ b/embedded/target/gLinux/config.mk
@@ -0,0 +1,28 @@
+CFLAGS_EXTRA ?=
+CFLAGS += -g \
+          -MD \
+          -Werror \
+          -Wno-deprecated-declarations \
+          -DARCH_GLINUX \
+          -fsanitize=address \
+          -fno-omit-frame-pointer \
+          -DARCH_GLINUX \
+          -DNEARBY_ALL_MODULE_DEBUG \
+          -DNEARBY_UNIT_TEST_ENABLED \
+          $(CFLAGS_EXTRA)
+
+NEARBY_TRACE_LEVEL = VERBOSE
+
+ifeq ($(OPTIMIZED_BUILD),1)
+CFLAGS += -O2
+else
+CFLAGS += -O0
+endif
+
+TEST_INCLUDES = \
+                -I$(GTEST_DIR)/include -I$(GTEST_DIR) \
+                -I$(GMOCK_DIR)/include -I$(GMOCK_DIR) \
+                $(CLIENT_INCLUDES) \
+                -Iclient/tests/ \
+                -Iclient/tests/$(ARCH)/ \
+                -Iclient/tests/mocks/
\ No newline at end of file
diff --git a/embedded/target/gLinux/rules.mk b/embedded/target/gLinux/rules.mk
new file mode 100644
index 0000000..9c464f1
--- /dev/null
+++ b/embedded/target/gLinux/rules.mk
@@ -0,0 +1,87 @@
+ifneq ($(EXCLUDE_COMMON_TEST_SRCS),1)
+TEST_SRCS += $(wildcard $(COMMON_DIR)/tests/*.cc)
+endif
+
+TEST_SRCS += $(wildcard client/tests/*.cc)
+
+TEST_SRCS := $(filter-out $(TEST_SRCS_EXCLUDE), $(TEST_SRCS))
+
+TEST_OBJS += $(patsubst %.cc,$(OUT_DIR)/%.o,$(TEST_SRCS))
+GLINUX_TARGET_SRCS := $(wildcard client/tests/glinux/*.cc)
+GTEST_SRCS = $(GTEST_DIR)/src/gtest-all.cc $(GMOCK_DIR)/src/gmock-all.cc
+EMPTY_TARGET_SRCS := $(wildcard client/tests/empty_target/*.c)
+
+GTEST_OBJS := $(patsubst %.cc,$(OUT_DIR)/%.o,$(GTEST_SRCS))
+
+GLINUX_TARGET_OBJS := $(patsubst %.cc,$(OUT_DIR)/%.o,$(GLINUX_TARGET_SRCS))
+EMPTY_TARGET_OBJS := $(patsubst %.c,$(OUT_DIR)/%.o,$(EMPTY_TARGET_SRCS))
+
+ALL_TEST_OBJS += $(GLINUX_TARGET_OBJS)
+ALL_TEST_OBJS += $(TEST_OBJS)
+ALL_TEST_OBJS += $(GTEST_OBJS)
+ALL_TEST_OBJS += $(EMPTY_TARGET_OBJS)
+
+# The glinux target layer
+$(GLINUX_TARGET_OBJS) : $(OUT_DIR)/%.o: %.cc
+	$(call compile_c,$(TEST_INCLUDES) -I. -std=c++14 -c $(CFLAGS))
+
+$(EMPTY_TARGET_OBJS) : $(OUT_DIR)/%.o: %.c
+	$(info "compiling empty target object $<")
+	$(call compile_c,-I. $(EMPTY_TARGET_INC) -std=c99 -c $(CFLAGS))
+
+$(TEST_OBJS) : $(FIRMWARE_VERSION_FILENAME)
+$(TEST_OBJS) : $(OUT_DIR)/%.o: %.cc
+	$(call compile_c,$(TEST_INCLUDES) -I. -std=c++14 $(CFLAGS))
+
+$(GTEST_OBJS) : $(OUT_DIR)/%.o : %.cc
+	$(call compile_c,$(TEST_INCLUDES) -std=c++14 $(CFLAGS))
+
+TESTS_TO_RUN = $(patsubst %.cc,$(OUT_DIR)/%_run,$(TEST_SRCS))
+.PHONY: $(TESTS_TO_RUN)
+
+# Static pattern rule to execute the test binary.
+$(TESTS_TO_RUN) : %_run : %
+	mkdir -p $(OUT_DIR)/test_logs/$(notdir $<)_logs/
+	./$< --gtest_output=xml:$(OUT_DIR)/test_logs/$(notdir $<)_logs/sponge_log.xml
+
+TARGET_OBJS ?= $(GLINUX_TARGET_OBJS)
+
+TARGET_OS_SRCS := $(wildcard client/tests/gLinux/*.cc)
+TARGET_OS_OBJS ?= $(patsubst %.cc,$(OUT_DIR)/%.o,$(TARGET_OS_SRCS))
+$(TARGET_OS_OBJS) : $(OUT_DIR)/%.o: %.cc
+	$(call compile_c,$(TEST_INCLUDES) -I. -std=c++14 $(CFLAGS))
+
+ALL_TEST_OBJS += $(TEST_OBJS)
+ALL_TEST_OBJS += $(TARGET_OS_OBJS)
+
+ALL_TEST_OBJS += $(TARGET_OS_OBJS)
+ALL_OBJS += $(ALL_TEST_OBJS)
+# Must include the .d files for test sources here since gLinux.rules is included
+# by makefile after after it includes $(DEPFILES).
+-include $(ALL_TEST_OBJS:.o=.d)
+
+EMPTY_TARGET_TEST = $(OUT_DIR)/client/tests/empty_target_test
+$(EMPTY_TARGET_TEST) : TARGET_OBJS = $(EMPTY_TARGET_OBJS)
+$(EMPTY_TARGET_TEST) : $(EMPTY_TARGET_OBJS)
+TEST_BINARIES = $(patsubst %.cc,$(OUT_DIR)/%,$(TEST_SRCS))
+$(TEST_BINARIES) : $(NAME) $(GTEST_OBJS) $(GLINUX_TARGET_OBJS)
+$(TEST_BINARIES) : $(TARGET_OS_OBJS)
+
+$(TEST_BINARIES) : % : %.o
+	mkdir -p $(dir $@)
+	$(CC) -o $@ $< \
+		$(CFLAGS) \
+		$(TEST_INCLUDES) \
+		$(GTEST_OBJS) \
+		$(TARGET_OBJS) \
+    $(TARGET_OS_OBJS) \
+		-L /usr/local/lib \
+		-std=c++14 \
+		-L $(OUT_DIR) -lnearby -lcrypto -lssl -lpthread -lstdc++
+
+run_tests: $(TESTS_TO_RUN)
+
+tests: $(TEST_BINARIES)
+
+run_tests : tests
+.PHONY : tests run_tests