// 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 #include #include #include #include "pw_assert/check.h" #include "pw_bluetooth/emboss_util.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_sync/mutex.h" namespace pw::bluetooth { // 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 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& callback) { Status status = DumpSnoopLogFileHeader(callback); if (!status.ok()) { return status; } for (const auto& entry : queue_) { std::pair 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, proxy::H4PacketInterface& hci_packet) { std::lock_guard lock(queue_lock_); size_t hci_packet_length_to_include = std::min( hci_packet.GetHciSpan().size(), static_cast(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 result = MakeEmbossWriter(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( std::chrono::time_point_cast( system_clock_.now()) .time_since_epoch() .count())); // write h4 type writer.packet_h4_type().Write(static_cast(hci_packet.GetH4Type())); // write hci packet pw::span hci_packet_trimmed{hci_packet.GetHciSpan().data(), 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})); } /// Generates the snoop log file header /// /// @returns the file header Status Snoop::DumpSnoopLogFileHeader( const Function& callback) { std::array file_header_data; pw::Result result = MakeEmbossWriter(file_header_data); if (!result.ok()) { return result.status(); } emboss::snoop_log::FileHeaderWriter writer = result.value(); constexpr std::array kBtSnoopIdentificationPatternData = { 0x62, 0x74, 0x73, 0x6E, 0x6F, 0x6F, 0x70, 0x00}; auto identification_pattern_storage = writer.identification_pattern().BackingStorage(); PW_ASSERT(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 callback(as_bytes(span(file_header_data))); } } // namespace pw::bluetooth