Module Usage

Right now, pw_snapshot just dictates a format. That means there is no provided system information collection integration, underlying storage, or transport mechanism to fetch a snapshot from a device. These must be set up independently by your project.

Building a Snapshot

Even though a Snapshot is just a proto message, the potential size of the proto makes it important to consider the encoder.

Nanopb is a popular encoder for embedded devices, it’s impractical to use with the pw_snapshot proto. Nanopb works by generating in-memory structs that represent the protobuf message. Repeated, optional, and variable-length fields increase the size of the in-memory struct. The struct representation of snapshot-like protos can quickly near 10KB in size. Allocating 10KB

Pigweed’s pw_protobuf is a better choice as its design is centered around incrementally writing a proto directly to the final wire format. If you only write a few fields in a snapshot, you can do so with minimal memory overhead.

#include "pw_protobuf/encoder.h"
#include "pw_snapshot_protos/snapshot.pwpb.h"

Result<ConstByteSpan> EncodeSnapshot(pw::ByteSpan encode_buffer,
                                     const CrashInfo &crash_info) {
  // Instantiate a generic proto encoder.
  pw::protobuf::NestedEncoder<kMaxNestedProtoDepth> proto_encoder(
      encode_buffer);
  // Get a proto-specific wrapper for the encoder.
  pw::snapshot::Snapshot::Encoder snapshot_encoder(&proto_encoder);
  {  // This scope is required to handle RAII behavior of the submessage.
    // Start writing the Metadata submessage.
    pw::snapshot::Metadata::Encoder metadata_encoder =
        snapshot_encoder.GetMetadataEncoder();
    metadata_encoder.WriteReason(EncodeReasonLog(crash_info));
    metadata_encoder.WriteFatal(true);
    metadata_encoder.WriteProjectName(std::as_bytes(std::span("smart-shoe")));
    metadata_encoder.WriteDeviceName(
        std::as_bytes(std::span("smart-shoe-p1")));
    metadata_encoder.WriteUptime(
        std::chrono::time_point_cast<std::chrono::milliseconds>(
            pw::chrono::SystemClock::now()));
  }
  // Finalize the proto encode so it can be flushed somewhere.
  return proto_encoder.Encode();
}

Custom Project Data

There are two main ways to add custom project-specific data to a snapshot. Tags are the simplest way to capture small snippets of information that require no or minimal post-processing. For more complex data, it’s usually more practical to extend the Snapshot proto.

Tags

Adding a key/value pair to the tags map is straightforward when using pw_protobuf.

{
  pw::Snapshot::TagsEntry::Encoder tags_encoder =
      snapshot_encoder.GetTagsEncoder();
  tags_encoder.WriteKey("BtState");
  tags_encoder.WriteValue("connected");
}

Extending the Proto

Extending the Snapshot proto relies on proto behavior details that are explained in the Snapshot Proto Format. Extending the snapshot proto is as simple as defining a proto message that only declares fields with numbers that are reserved by the Snapshot proto for downstream projects. When encoding your snapshot, you can then write both the upstream Snapshot proto and your project’s custom extension proto message to the same proto encoder.

The upstream snapshot tooling will ignore any project-specific proto data, the proto data can be decoded a second time using a project-specific proto. At that point, any handling logic of the project-specific data would have to be done as part of project-specific tooling.

Analyzing Snapshots

Snapshots can be processed for analysis using the pw_snapshot.process python tool. This tool turns a binary snapshot proto into human readable, actionable information. As some snapshot fields may optionally be tokenized, a pw_tokenizer database or ELF file with embedded pw_tokenizer tokens may optionally be passed to the tool to detokenize applicable fields.

# Example invocation, which dumps to stdout by default.
$ python -m pw_snapshot.processor path/to/serialized_snapshot.bin


        ____ _       __    _____ _   _____    ____  _____ __  ______  ______
       / __ \ |     / /   / ___// | / /   |  / __ \/ ___// / / / __ \/_  __/
      / /_/ / | /| / /    \__ \/  |/ / /| | / /_/ /\__ \/ /_/ / / / / / /
     / ____/| |/ |/ /    ___/ / /|  / ___ |/ ____/___/ / __  / /_/ / / /
    /_/     |__/|__/____/____/_/ |_/_/  |_/_/    /____/_/ /_/\____/ /_/
                  /_____/


                            ▪▄▄▄ ▄▄▄· ▄▄▄▄▄ ▄▄▄· ▄ ·
                            █▄▄▄▐█ ▀█ • █▌ ▐█ ▀█ █
                            █ ▪ ▄█▀▀█   █. ▄█▀▀█ █
                            ▐▌ .▐█ ▪▐▌ ▪▐▌·▐█ ▪▐▌▐▌
                            ▀    ▀  ▀ ·  ▀  ▀  ▀ .▀▀

Device crash cause:
    ../examples/example_rpc.cc: Assert failed: 1+1 == 42

Project name:      gShoe
Device:            GSHOE-QUANTUM_CORE-REV_0.1
Device FW version: QUANTUM_CORE-0.1.325-e4a84b1a
FW build UUID:     ad2d39258c1bc487f07ca7e04991a836fdf7d0a0
Snapshot UUID:     8481bb12a162164f5c74855f6d94ea1a

Thread State
  2 threads running, Main Stack (Handler Mode) active at the time of capture.
                     ~~~~~~~~~~~~~~~~~~~~~~~~~

Thread (INTERRUPT_HANDLER): Main Stack (Handler Mode) <-- [ACTIVE]
Stack info
  Stack used:   0x2001b000 - 0x2001ae20 (480 bytes)
  Stack limits: 0x2001b000 - 0x???????? (size unknown)

Thread (RUNNING): Idle
Stack info
  Stack used:   0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)
  Stack limits: 0x2001ac00 - 0x2001aa00 (512 bytes)

Symbolizing Addresses

The snapshot processor tool has built-in support for symbolization of some data embedded into snapshots. Taking advantage of this requires the use of a project-provided ElfMatcher callback. This is used by the snapshot processor to understand which ELF file should be used to symbolize which snapshot in cases where a snapshot has related snapshots embedded inside of it.

Here’s an example implementation that uses the device name:

# Given a firmware bundle directory, determine the ELF file associated with
# the provided snapshot.
def _snapshot_elf_matcher(fw_bundle_dir: Path,
                          snapshot: snapshot_pb2.Snapshot) -> Optional[Path]:
    metadata = MetadataProcessor(snapshot.metadata, DETOKENIZER)
    if metadata.device_name().startswith('GSHOE_MAIN_CORE'):
        return fw_bundle_dir / 'main.elf'
    if metadata.device_name().startswith('GSHOE_SENSOR_CORE'):
        return fw_bundle_dir / 'sensors.elf'
    return None


# A project specific wrapper to decode snapshots that provides a detokenizer
# and ElfMatcher.
def decode_snapshots(snapshot: bytes, fw_bundle_dir: Path) -> str:

    # This is the actual ElfMatcher, which wraps the helper in a lambda that
    # captures the passed firmware artifacts directory.
    matcher: processor.ElfMatcher = lambda snapshot: _snapshot_elf_matcher(
        fw_bundle_dir, snapshot)
    return processor.process_snapshots(snapshot, DETOKENIZER, matcher)

C++ Utilities

UUID utilities

Snapshot UUIDs are used to uniquely identify snapshots. Pigweed strongly recommends using randomly generated data as a snapshot UUID. The more entropy and random bits, the lower the probability that two devices will produce the same UUID for a snapshot. 16 bytes should be sufficient for most projects, so this module provides UuidSpan and ConstUuidSpan types that can be helpful for referring to UUID-sized byte spans.

Reading a snapshot’s UUID

An in-memory snapshot’s UUID may be read using ReadUuidFromSnapshot().

void NotifyNewSnapshot(ConstByteSpan snapshot) {
  std::array<std::byte, pw::snapshot::kUuidSizeBytes> uuid;
  pw::Result<pw::ConstByteSpan> result =
      pw::snapshot::ReadUuidFromSnapshot(snapshot, uuid);
  if (!result.ok()) {
    PW_LOG_ERROR("Failed to read UUID from new snapshot, error code %d",
                 static_cast<int>(result.status().code()));
    return;
  }
  LogNewSnapshotUuid(result.value());
}