blob: f5d280606088414a0d9818768159d1dc17d90b2b [file]
// Copyright 2025 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#include "pw_bluetooth/snoop.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <mutex>
#include "pw_assert/check.h"
#include "pw_bluetooth/emboss_util.h"
#include "pw_bluetooth/hci_common.emb.h"
#include "pw_bluetooth/hci_data.emb.h"
#include "pw_bluetooth/hci_events.emb.h"
#include "pw_bluetooth/snoop.emb.h"
#include "pw_bluetooth_proxy/h4_packet.h"
#include "pw_bytes/span.h"
#include "pw_chrono/system_clock.h"
#include "pw_containers/inline_var_len_entry_queue.h"
#include "pw_hex_dump/hex_dump.h"
#include "pw_log/log.h"
#include "pw_result/result.h"
#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_stream/stream.h"
#include "pw_string/string.h"
#include "pw_string/string_builder.h"
#include "pw_sync/mutex.h"
#include "pw_trace/trace.h"
namespace pw::bluetooth {
namespace {
/// Copies as many items from src into dest as possible,
/// returning the number of items copied.
/// Note: The spans must not overlap.
template <typename T>
size_t CopyMax(span<const T> src, span<T> dest) {
size_t n = std::min(src.size(), dest.size());
std::copy_n(src.begin(), n, dest.begin());
return n;
}
constexpr uint32_t kEmbossFileVersion = 1;
constexpr size_t kHeaderSize = 16;
// BtSnoopEpoch starts at 12am Jan 1st 0AD this is the delta in micro seconds
// with respect to the unix epoch, see:
// https://repository.root-me.org/R%C3%A9seau/EN%20-%20fte.com%20-%20BT%20Snoop%20File%20Format.pdf
// Also see the wireshark implementation:
// https://github.com/wireshark/wireshark/blob/e1033766e925762fb9b07788c2f1a90e4c04ba93/wiretap/btsnoop.c#L65
// and
// https://github.com/wireshark/wireshark/blob/e1033766e925762fb9b07788c2f1a90e4c04ba93/wiretap/btsnoop.c#L203
constexpr auto kBtSnoopEpochDelta =
std::chrono::microseconds(0x00dcddb30f2f8000ULL);
// Generates the snoop log file header
//
// @returns the file header
Result<std::array<uint8_t, kHeaderSize>> GetSnoopLogFileHeader() {
std::array<uint8_t, kHeaderSize> file_header_data;
pw::Result<emboss::snoop_log::FileHeaderWriter> result =
MakeEmbossWriter<emboss::snoop_log::FileHeaderWriter>(file_header_data);
if (!result.ok()) {
return result.status();
}
emboss::snoop_log::FileHeaderWriter writer = result.value();
constexpr std::array<uint8_t, 8> kBtSnoopIdentificationPatternData = {
0x62, 0x74, 0x73, 0x6E, 0x6F, 0x6F, 0x70, 0x00};
auto identification_pattern_storage =
writer.identification_pattern().BackingStorage();
PW_CHECK(kBtSnoopIdentificationPatternData.size() ==
identification_pattern_storage.SizeInBytes());
std::copy(kBtSnoopIdentificationPatternData.begin(),
kBtSnoopIdentificationPatternData.end(),
identification_pattern_storage.begin());
writer.version_number().Write(kEmbossFileVersion);
writer.datalink_type().Write(emboss::snoop_log::DataLinkType::HCI_UART_H4);
return file_header_data;
}
void AddTrace(emboss::snoop_log::PacketFlags emboss_packet_flag,
const proxy::H4PacketInterface& hci_packet) {
pw::InlineString<32> trace_tag;
pw::StringBuilder trace_tag_builder(trace_tag);
uint8_t type = static_cast<uint8_t>(hci_packet.GetH4Type());
// Direction uses 0 for incoming, 1 for outcomming packet to match android
// BTSL traces.
uint8_t direction;
switch (emboss_packet_flag) {
case emboss::snoop_log::PacketFlags::RECEIVED:
direction = 0;
break;
case emboss::snoop_log::PacketFlags::SENT:
direction = 1;
break;
default:
PW_UNREACHABLE;
direction = static_cast<uint8_t>(-1);
}
size_t size = hci_packet.GetHciSpan().size();
trace_tag_builder << type << "/" << direction << "/" << size;
switch (hci_packet.GetH4Type()) {
case emboss::H4PacketType::EVENT: {
const auto header =
emboss::MakeEventHeaderView(hci_packet.GetHciSpan().data(),
emboss::EventHeaderView::SizeInBytes());
if (!header.IsComplete()) {
break;
}
trace_tag_builder.Format("/%02x", header.event_code_uint().Read());
const emboss::EventCode event_code = header.event_code().Read();
if (event_code == emboss::EventCode::LE_META_EVENT) {
const auto le_meta_event =
emboss::MakeLEMetaEventView(hci_packet.GetHciSpan().data(),
emboss::LEMetaEventView::SizeInBytes());
if (!le_meta_event.IsComplete()) {
break;
}
trace_tag_builder.Format("/%02x", le_meta_event.subevent_code().Read());
} else if (event_code == emboss::EventCode::VENDOR_DEBUG) {
const auto vendor_debug_event = emboss::MakeVendorDebugEventView(
hci_packet.GetHciSpan().data(),
emboss::VendorDebugEventView::SizeInBytes());
if (!vendor_debug_event.IsComplete()) {
break;
}
trace_tag_builder.Format("/%02x",
vendor_debug_event.subevent_code().Read());
}
} break;
case emboss::H4PacketType::COMMAND: {
const auto header = emboss::MakeCommandHeaderView(
hci_packet.GetHciSpan().data(),
emboss::CommandHeaderView::SizeInBytes());
if (!header.IsComplete()) {
break;
}
trace_tag_builder.Format("/%02x",
static_cast<uint8_t>(header.opcode().Read()));
} break;
case emboss::H4PacketType::ACL_DATA: {
const auto header = emboss::MakeAclDataFrameHeaderView(
hci_packet.GetHciSpan().data(),
emboss::AclDataFrameHeaderView::SizeInBytes());
if (!header.IsComplete()) {
break;
}
trace_tag_builder.Format("/%03x/", header.handle().Read());
trace_tag_builder << header.packet_boundary_flag().Read();
} break;
case emboss::H4PacketType::UNKNOWN:
case emboss::H4PacketType::SYNC_DATA:
case emboss::H4PacketType::ISO_DATA:
break;
}
PW_TRACE_INSTANT_DATA("BTSL",
"Bluetooth",
"@pw_arg_label=%s",
trace_tag_builder.c_str(),
trace_tag_builder.size());
}
} // namespace
Snoop::Reader::Reader(Snoop& snoop)
: snoop_(&snoop),
entry_iterator_(snoop_->queue_.begin()),
offset_in_entry_(0),
header_offset_(0),
was_enabled_(snoop_->Disable()) {}
Snoop::Reader::Reader(Snoop::Reader&& other)
: snoop_(other.snoop_),
entry_iterator_(std::move(other.entry_iterator_)),
offset_in_entry_(other.offset_in_entry_),
header_offset_(other.header_offset_),
was_enabled_(other.was_enabled_) {
other.snoop_ = nullptr;
}
Snoop::Reader& Snoop::Reader::operator=(Snoop::Reader&& other) {
if (this == &other) {
return *this;
}
// Not supported due to one Reader policy.
PW_CHECK(!snoop_);
snoop_ = other.snoop_;
other.snoop_ = nullptr;
entry_iterator_ = std::move(other.entry_iterator_);
offset_in_entry_ = other.offset_in_entry_;
header_offset_ = other.header_offset_;
was_enabled_ = other.was_enabled_;
return *this;
}
Snoop::Reader::~Reader() {
if (snoop_ && was_enabled_) {
snoop_->ClearReader();
snoop_->Enable();
}
}
StatusWithSize Snoop::Reader::DoRead(ByteSpan dest) {
size_t bytes_written = 0;
// Header
if (header_offset_ < kHeaderSize) {
Result<std::array<uint8_t, kHeaderSize>> header = GetSnoopLogFileHeader();
if (!header.ok()) {
return StatusWithSize::Internal();
}
const size_t num_copied =
CopyMax(as_bytes(span(*header)).subspan(header_offset_), dest);
header_offset_ += num_copied;
bytes_written += num_copied;
dest = dest.subspan(num_copied);
}
if (dest.empty()) {
return StatusWithSize(bytes_written);
}
std::lock_guard lock(snoop_->queue_lock_);
// Queue
while (!dest.empty() && entry_iterator_ != snoop_->queue_.end()) {
const auto& entry = *entry_iterator_;
const auto data_parts = entry.contiguous_data();
ConstByteSpan src;
if (offset_in_entry_ < data_parts.first.size()) {
src = data_parts.first.subspan(offset_in_entry_);
} else {
const size_t offset_in_part2 = offset_in_entry_ - data_parts.first.size();
src = data_parts.second.subspan(offset_in_part2);
}
const size_t num_copied = CopyMax(src, dest);
offset_in_entry_ += num_copied;
bytes_written += num_copied;
dest = dest.subspan(num_copied);
if (offset_in_entry_ == entry.size()) {
++entry_iterator_;
offset_in_entry_ = 0;
}
}
if (bytes_written == 0) {
return StatusWithSize::OutOfRange();
} else {
return StatusWithSize(bytes_written);
}
}
Result<Snoop::Reader> Snoop::GetReader() {
{
std::lock_guard lock(queue_lock_);
if (reader_active_) {
return Status::FailedPrecondition();
}
reader_active_ = true;
}
return Snoop::Reader(*this);
}
void Snoop::ClearReader() {
std::lock_guard lock(queue_lock_);
reader_active_ = false;
}
// Dump the snoop log to the log as a hex string
Status Snoop::DumpToLog() {
PW_LOG_INFO("Snoop Log Start");
PW_LOG_INFO("Step 1: Copy and paste the hex data into a text file");
PW_LOG_INFO("Step 2: Remove any extra text (e.g. file, timestamp, etc)");
PW_LOG_INFO("Step 3: $ xxd -r -p input.hex output.snoop");
PW_LOG_INFO("Step 4: $ wireshark output.snoop");
Status status = Dump([](ConstByteSpan data) {
std::array<char, 80> temp;
pw::dump::FormattedHexDumper hex_dumper(temp);
hex_dumper.flags.prefix_mode =
pw::dump::FormattedHexDumper::AddressMode::kDisabled;
hex_dumper.flags.show_ascii = false;
hex_dumper.flags.bytes_per_line = 32;
hex_dumper.flags.group_every = 32;
hex_dumper.flags.show_header = false;
Status hex_dumper_status = hex_dumper.BeginDump(data);
if (!hex_dumper_status.ok()) {
return hex_dumper_status;
}
while (hex_dumper.DumpLine().ok()) {
PW_LOG_INFO("%s", temp.data());
}
return pw::OkStatus();
});
PW_LOG_INFO("Snoop Log End");
return status;
}
/// Dump the snoop log via callback without locking
///
/// The callback will be invoked multiple times as the circular buffer is
/// traversed.
///
/// Note: this function does NOT lock the snoop log. Do not invoke it unless
/// the snoop log is not being used. For example, use this API to read the
/// snoop log in a crash handler where mutexes are not allowed to be taken.
///
/// @param callback callback to invoke
Status Snoop::DumpUnlocked(
const Function<Status(ConstByteSpan data)>& callback) {
Result<std::array<uint8_t, 16>> result = GetSnoopLogFileHeader();
if (!result.ok()) {
return result.status();
}
Status status = callback(as_bytes(span(result.value())));
if (!status.ok()) {
return status;
}
for (const auto& entry : queue_) {
std::pair<ConstByteSpan, ConstByteSpan> data = entry.contiguous_data();
status = callback(data.first);
if (!status.ok()) {
return status;
}
if (!data.second.empty()) {
status = callback(data.second);
if (!status.ok()) {
return status;
}
}
}
return OkStatus();
}
void Snoop::AddEntry(emboss::snoop_log::PacketFlags emboss_packet_flag,
const proxy::H4PacketInterface& hci_packet) {
std::lock_guard lock(queue_lock_);
if (!is_enabled_) {
return;
}
if (PW_TRACE_ENABLE) {
AddTrace(emboss_packet_flag, hci_packet);
}
size_t hci_packet_length_to_include = std::min(
hci_packet.GetHciSpan().size(),
static_cast<size_t>(scratch_buffer_.size() -
emboss::snoop_log::EntryHeader::MaxSizeInBytes() -
/* hci type*/ 1));
size_t total_entry_size = hci_packet_length_to_include + /* hci type*/ 1 +
emboss::snoop_log::EntryHeader::MaxSizeInBytes();
// Ensure the scratch buffer can fit the entire entry
PW_CHECK_INT_GE(scratch_buffer_.size(), total_entry_size);
pw::Result<emboss::snoop_log::EntryWriter> result =
MakeEmbossWriter<emboss::snoop_log::EntryWriter>(scratch_buffer_);
PW_CHECK_OK(result);
emboss::snoop_log::EntryWriter writer = result.value();
writer.header().original_length().Write(hci_packet.GetHciSpan().size() +
/* hci type*/ 1);
writer.header().included_length().Write(hci_packet_length_to_include +
/* hci type*/ 1);
writer.header().packet_flags().Write(emboss_packet_flag);
writer.header().cumulative_drops().Write(0);
writer.header().timestamp_us().Write(static_cast<int64_t>(
(std::chrono::time_point_cast<std::chrono::microseconds>(
system_clock_.now()) +
kBtSnoopEpochDelta)
.time_since_epoch()
.count()));
// write h4 type
writer.packet_h4_type().Write(static_cast<uint8_t>(hci_packet.GetH4Type()));
// write hci packet
pw::span<const uint8_t> hci_packet_trimmed =
hci_packet.GetHciSpan().first(hci_packet_length_to_include);
PW_CHECK(TryToCopyToEmbossStruct(/*emboss_dest=*/writer.packet_hci_data(),
/*src=*/hci_packet_trimmed));
// save the entry!
queue_.push_overwrite(
as_bytes(span{scratch_buffer_.data(), total_entry_size}));
}
} // namespace pw::bluetooth