Skip to main content

Overview

wacore is the core WhatsApp protocol implementation for whatsapp-rust. It’s designed to be platform-agnostic with no runtime dependencies on Tokio or specific databases, making it portable across different async runtimes and storage backends.

Philosophy

wacore contains all the core logic for:
  • Binary protocol encoding/decoding
  • Cryptographic primitives (AES-GCM, Signal Protocol)
  • IQ protocol types and specifications
  • Runtime abstraction (Runtime trait for pluggable async executors)
  • Network abstractions (Transport, TransportFactory, HttpClient traits)
  • State management traits (Backend, SignalStore, AppSyncStore, etc.)
  • Message builders and parsers
It has zero runtime dependencies — no Tokio, no async-std, only futures, async-trait, async-lock, and async-channel for async primitives. This makes wacore portable to any async runtime, including WASM targets. The main whatsapp-rust crate provides concrete implementations (Tokio runtime, SQLite storage, ureq HTTP client, Tokio WebSocket transport).

Key Exports

Re-exported Crates

Derive Macros

  • EmptyNode - For protocol nodes with only a tag (no attributes)
  • ProtocolNode - For protocol nodes with string attributes
  • StringEnum - For enums with string representations
See Binary Protocol for usage examples.

Framing

Core Modules

Protocol & Binary

Binary protocol

Type-safe protocol node builders and parsers

xml

XML utilities for protocol nodes

Cryptography

Signal Protocol

Signal Protocol implementation for E2E encryption

noise

Noise Protocol XX for handshake encryption

IQ Protocol

Type-safe IQ request/response specifications:
  • blocklist - Block/unblock contacts
  • chatstate - Typing indicators, presence
  • contacts - Contact synchronization
  • dirty - Dirty bit checking
  • groups - Group management operations
  • keepalive - Connection keepalive
  • mediaconn - Media server connections
  • mex - Message Extension queries
  • passive - Passive IQ handling
  • prekeys - Prekey distribution
  • privacy - Privacy settings
  • props - Server properties and A/B experiment configs. The companion abprops module ships the typed flag registry, and props::WATCHED lists the flags the library itself reads
  • spam_report - Spam reporting
  • tctoken - Temporary client tokens
  • usync - User synchronization. Ships a typed query/response model (UsyncQuery, UsyncProtocol, UsyncResponse, …) covering every USync subprotocol observed in WhatsApp Web (device lists, contact/LID/username lookup, status, bot profiles, features). See USync
See Architecture for the IQ protocol pattern.

A/B props registry

wacore::iq::abprops is an auto-generated, vendored snapshot of WhatsApp Web’s A/B-props registry. Each WA Web registry becomes a pub mod (currently web), and each flag becomes a typed pub const AbProp named after its key in screaming snake case (for example web::PRIVACY_TOKEN_SENDING_ON_GROUP_CREATE). Every entry carries:
  • name — the wire name the server uses in <prop config_code="…"/>
  • code — the numeric config_code
  • value_typeAbPropType::Bool, Int, Float, or Str
  • default — the registry default applied when the server omits the flag
Pass these constants to AbPropsCache (is_enabled, get, get_int, watch, watch_many) instead of raw u32 codes. The library only materializes the consts you reference, so the rest of the ~2,000-flag registry adds no binary weight.
wacore::iq::props::WATCHED is the slice of flags the library itself reads — useful as a starting point if you want to extend the cache’s interest set. A small props::stale module preserves flags the client still references but that the current WA Web bundle no longer ships.

Message Handling

messages

Message encryption and decryption

send

Message sending logic

download

Media download and decryption

upload

Media encryption and upload preparation

State Management

  • Device - Core device state structure
  • DeviceCommand - State mutation commands
  • traits - Backend trait definitions (Backend, SessionStore, etc.)
  • ab_props - In-memory A/B experiment property cache (AbPropsCache), populated from fetch_props() on each connection. Features query this cache using typed flag constants from the abprops registry (e.g., privacy token attachment on group operations).
See State Management for the command pattern.

Runtime & Networking

The runtime module defines the Runtime trait that all async operations go through:

Helper functions

The runtime module also provides two runtime-agnostic helper functions that work with any Runtime implementation: timeout — Race a future against a deadline using the runtime’s sleep implementation:
Returns Ok(value) if the future completes before the duration, or Err(Elapsed) if it times out. This is the runtime-agnostic replacement for tokio::time::timeout — the library uses it internally for phash validation, media retry timeouts, session establishment, and IQ response waiting. blocking — Offload a blocking closure and return its result:
Wraps Runtime::spawn_blocking with a oneshot channel to ferry the closure’s return value back to the caller. On WASM, the closure runs inline since there is only one thread. The net module defines the networking abstractions:
  • Transport — active connection for sending/receiving raw bytes
  • TransportFactory — creates new transport instances and event streams
  • HttpClient — HTTP request execution (buffered and streaming)
  • TransportEventConnected, DataReceived(Bytes), Disconnected
On WASM targets (target_arch = "wasm32"), all Send bounds are automatically removed.

Connection & Pairing

handshake

Noise Protocol handshake

pair

QR code pairing

pair_code

Phone number pairing

shortcake

SHORTCAKE_PASSKEY companion-linking crypto (ephemeral identity commit/reveal, verification code, HKDF/AES-GCM pairing envelope, handoff proof)

net

Transport and HTTP client traits
shortcake is pure and platform-agnostic (no Tokio, wasm-buildable) — it only builds/parses the deterministic protocol payloads. The one non-reproducible step, obtaining a WebAuthn assertion, lives in whatsapp_rust::passkey (the PasskeyAuthenticator seam). See Authentication — Passkey linking.

Specialized Features

appstate

App state synchronization (contacts, settings)

history_sync

Message history synchronization

usync

User device list synchronization

prekeys

Signal Protocol prekey generation

history_sync types

HistoryMsgSecretRecord carries the per-message secret extracted by the history-sync pipeline for E2E key expansion.
SecretBytes avoids heap allocation for secrets ≤32 bytes (covering the typical 32-byte Signal message secret) and falls back to Vec<u8> for larger values.
Implements Deref<Target=[u8]>, From<&[u8]>, From<Vec<u8>>, PartialEq, Eq, and Debug. Use as_slice() or deref for reads; into_vec() to convert to Vec<u8>. For Bytes-owned input, wacore::history_sync also exports:
own_user scopes from_me classification to the local account’s JID (pass None if that check should be skipped). retain_blob controls whether the returned HistorySyncResult keeps a Bytes handle on the original compressed input (compressed_bytes, consumed by LazyHistorySync) — pass false to drop it immediately after parsing. process_history_sync_bytes is a Bytes-based sibling of process_history_sync (unchanged) — both accept every record. process_history_sync_bytes_filtered additionally takes a record_filter predicate that runs against a borrowed HistoryMsgSecretRecordRef<'a> before its owned HistoryMsgSecretRecord counterpart is allocated, so a caller that owns a retention policy (for example, dropping records outside a retention window) can reject them without paying for materialization:
conversation_index is a zero-based counter shared across records from the same conversation, letting a filter cache per-conversation classification instead of recomputing it per record. See Architecture — RAM optimization layers for the measured allocation win. If you want to skip the intermediate owned HistoryMsgSecretRecord entirely, wacore::history_sync also exports a streaming visitor path:
process_history_sync_bytes_with_record_visitor takes a plain FnMut closure for the common case. The closure builds your own row directly from the borrowed HistoryMsgSecretRecordRef. This ensures the owned HistoryMsgSecretRecord is never allocated. process_history_sync_bytes_with_record_sink takes a full HistoryMsgSecretRecordVisitor implementation instead of a closure. The visit return value reports the byte size you retained for that record for accounting. The optional reserve and retained_item_size hooks let you size your own collection (e.g., a batched SQL insert buffer) up front rather than growing it one record at a time. Reach for process_history_sync_bytes_filtered (above) when a simple accept/reject predicate is enough. Reach for the visitor/sink pair when you also want to avoid materializing the owned record.

Time

The time module centralizes all timestamp handling. It exposes two independent clocks that should not be confused:
  • Wall clock (TimeProvider, now_millis, now_utc) — answers “what time is it?”. May jump backwards across NTP syncs, manual adjustments, or leap-second smearing. Backed by chrono::Utc::now() on native targets.
  • Monotonic clock (MonotonicProvider, Instant) — answers “how much time passed?”. Never moves backwards and is immune to NTP adjustments. Backed by std::time::Instant on native targets.
On wasm32-unknown-unknown there is no built-in wall-clock source. If you do not register a provider before the first timestamp, now_millis() returns 0 (Unix epoch) and logs a single warning. Always call set_time_provider with a Date.now()-backed (or equivalent) provider during WASM startup — see Custom wall-clock provider below.
Use the wall clock for stanza timestamps, app-state mutations, and log lines. Use the monotonic clock for timeouts, retry backoff, and latency measurements. Conflating the two silently corrupts elapsed-time logic whenever the system clock is adjusted mid-measurement. Each clock can be overridden globally — useful in environments where the standard implementations are unavailable (e.g., WASM) or for deterministic testing.

Wall-clock functions

Custom wall-clock provider

Implement the TimeProvider trait and call set_time_provider before any time functions are used:
set_time_provider returns Err if a provider has already been set. The provider uses OnceLock internally, so it can only be configured once per process.
On wasm32 targets the epoch fallback is not cached, so a later set_time_provider call still takes effect even if a timestamp was read during startup. A typical browser embedder wires Date.now() through wasm-bindgen:

Instant

Instant is a portable monotonic instant that replaces std::time::Instant, which is unavailable on wasm32-unknown-unknown. On native targets it wraps std::time::Instant via the default MonotonicProvider and exposes nanosecond resolution. The Instant type is Copy and supports Add<Duration> and Sub<Instant> (returning Duration), with saturating arithmetic to prevent overflow.

Custom monotonic provider

Implement the MonotonicProvider trait and call set_monotonic_provider before any Instant is captured. The provider must return nanoseconds since an arbitrary fixed reference and never return a smaller value than a previous call.
On wasm32 targets without a registered provider, the fallback derives nanos from the wall clock (clamped to non-decreasing) and quantizes to milliseconds. Embedders targeting browsers, Node, or WASI should register a sub-millisecond provider for accurate latency measurements.
set_monotonic_provider returns Err if a provider has already been set. Like the wall-clock provider, it uses OnceLock internally and can only be configured once per process.

Utilities

  • client - Client context traits
  • ib - Identity byte utilities
  • proto_helpers - Protobuf conversion helpers
  • reporting_token - Reporting token generation
  • request - Request building utilities
  • stanza - Common stanza builders
  • sticker_pack - Sticker pack creation helpers (see sticker packs)
  • time - Pluggable wall-clock and monotonic-clock providers, plus portable Instant (see time above)
  • types - Common type definitions (JID, events, messages). Includes types::jid utilities for zero-allocation JID comparison (cmp_for_lock_order), buffer-reusing address formatting (write_protocol_address_to), and in-place sorted deduplication (sort_dedup_by_user, sort_dedup_by_device)
  • version - WhatsApp version constants
  • webp - WebP format utilities (animated sticker detection)

webp module

The webp module provides utilities for working with WebP image files. It is re-exported as whatsapp_rust::webp.

is_animated

Detects whether a WebP file contains animation frames by parsing RIFF/VP8X headers and scanning for ANIM/ANMF chunks.
&[u8]
required
Raw WebP file bytes.
Returns true if the WebP file is animated, false otherwise (including for invalid or too-short input). Example:
This function is used internally by create_sticker_pack_zip to set the is_animated field on each sticker proto entry. You can also use it directly when you need to classify WebP files before processing.

Submodule Packages

wacore is split into several workspace crates:

wacore-binary

Location: wacore/binary Binary protocol encoding/decoding using WhatsApp’s custom format.
Key exports:
  • CompactString - Re-export of compact_str::CompactString, used by Jid.user, NodeValue::String, and NodeContent::String
  • jid::{Jid, JidRef, Server, JidExt} - WhatsApp JID types. Server is an enum (#[repr(u8)]) with variants for all known WhatsApp server domains (Pn, Lid, Group, Broadcast, Newsletter, Hosted, HostedLid, Messenger, Interop, Bot, Legacy). JidExt provides helper methods (is_group(), is_newsletter(), etc.) for both owned and borrowed JID types. jid::parse_jid_ref(s: &str) -> Option<JidRef<'_>> parses the common user/group/LID/bot shapes directly into a borrowed JidRef with no allocation; it returns None for edge cases the compatibility fallback handles, so callers that need those cases parse via s.parse::<Jid>() instead. Jid’s own FromStr impl is built on top of parse_jid_ref
  • node::{Node, NodeRef, NodeStr, NodeValue, ValueRef, OwnedNodeRef, AttrsVec} - Protocol node types. Node (owned) for building outgoing stanzas, NodeRef (borrowed) for reading received stanzas, NodeStr for borrowed-or-inline decoded strings, OwnedNodeRef for yoke-based zero-copy self-referential nodes shared as Arc<OwnedNodeRef>. AttrsVec is SmallVec<[(Cow<'static, str>, NodeValue); 2]>, the inline-capable backing store for Attrs (≤2 attrs stay on the stack; see Binary Protocol). The entire NodeRef type family (NodeRef, NodeStr, ValueRef, JidRef, NodeContentRef, OwnedNodeRef) implements serde::Serialize, producing output identical to their owned counterparts — enabling zero-copy serialization without converting to Node first
  • builder::NodeBuilder - Fluent node builder with new(&'static str) / new_dynamic(String), attr(), jid_attr(), children(), bytes(), string_content(), and apply_content() chaining methods
  • marshal::* - Binary marshaling functions
  • attrs::{AttrParser, AttrParserRef} - Attribute parsing utilities for owned Node and borrowed NodeRef respectively
  • token - Token dictionary

wacore-libsignal

Location: wacore/libsignal Signal Protocol implementation for end-to-end encryption.
Key modules:
  • core - Core session cipher logic
  • crypto - Cryptographic primitives (HKDF, HMAC, AES)
  • protocol - Protocol message types
  • store - Store trait definitions
See Signal Protocol for encryption details.

wacore-noise

Location: wacore/noise Noise Protocol implementation for handshake encryption. Supports the XX, IK, and XXfallback patterns to match WhatsApp Web’s behavior on cold start, resumed reconnects, and server-driven recovery.
Key exports:
  • NoiseState - Generic Noise state machine
  • NoiseHandshake - WhatsApp-specific handshake wrapper
  • XxHandshakeState - Three-message XX handshake (cold start / fallback)
  • IkHandshakeState - Resumed IK handshake using a cached server static
  • XxFallbackHandshakeState - Server-driven recovery from a stale IK static, continuing the existing transcript
  • IkServerHelloOutcome - Either Continue (IK succeeds) or Fallback (pivot into XXfallback)
  • VerifiedServerCertChain - Output of XX/XXfallback; persisted by the client to enable IK on the next connect
  • HandshakeUtils - Protocol message building/parsing
  • framing - WebSocket frame encoding
  • build_edge_routing_preintro - Edge routing helper
See WebSocket & Noise Protocol — Noise Protocol Handshake for the full pattern selection and failure-handling logic.

wacore-appstate

Location: wacore/appstate App state synchronization for contacts, settings, and metadata.
Key exports:
  • process_snapshot - Process full state snapshots
  • process_patch - Apply incremental patches
  • Mutation - State mutation records
  • LTHash - LTHash implementation for integrity
  • expand_app_state_keys - Key derivation

wacore-derive

Location: wacore/derive Procedural macros for protocol node generation.

Usage in main library

The main whatsapp-rust crate uses wacore modules throughout:

Design Principles

Platform-Agnostic

No dependencies on:
  • Tokio or any async runtime — uses only futures, async-trait, async-lock, async-channel
  • Specific database implementations
  • File system operations
This allows users to provide their own:
  • Runtime: Tokio (default), async-std, smol, WASM, etc. — implement Runtime (4 methods)
  • Storage: SQLite (default), PostgreSQL, in-memory, etc. — implement Backend (4 sub-traits)
  • Transport: Tokio WebSocket (default), custom protocols — implement TransportFactory + Transport
  • HTTP client: ureq (default), reqwest, surf, etc. — implement HttpClient

Type Safety

Strong typing throughout:
  • Jid with Server enum for WhatsApp identifiers — server type is an enum variant, not a string
  • Node / NodeRef for protocol messages (owned / borrowed)
  • Validated newtypes (e.g., GroupSubject with length limits)
  • Enum variants with StringEnum for protocol values

Zero-copy where possible

  • Cow<'static, str> for owned Node.tag and Attrs keys — known protocol strings (from the token dictionary) are borrowed as static references with zero heap allocation, while unknown strings fall back to owned String
  • NodeStr<'a> for borrowed NodeRef.tag, AttrsRef keys, ValueRef::String, and JidRef.user — a borrowed-or-inline string type where the Owned variant uses CompactString (inline up to 24 bytes) instead of heap-allocated String, reducing allocation pressure during decoding
  • Server enum (#[repr(u8)]) for Jid.server — a Copy type that requires zero allocation, replacing the previous Cow<'static, str> string-based server field
  • OwnedNodeRef — yoke-based self-referential node that owns the decompressed network buffer while NodeRef borrows string/byte payloads directly from it. Received stanzas flow through the system as Arc<OwnedNodeRef> for cheap shared zero-copy access
  • Zero-copy Serialize — the entire NodeRef type family (NodeRef, NodeStr, ValueRef, JidRef, NodeContentRef, OwnedNodeRef) implements serde::Serialize, producing output identical to their owned counterparts. This allows serializing received stanzas directly from the network buffer without converting to owned Node types first. See Binary Protocol — Zero-copy serialization for details
  • NodeRef for borrowed node parsing
  • AttrParserRef for attribute iteration
  • marshal_ref for encoding without cloning

Benchmarks

The project includes two categories of benchmarks: protocol-level (iai-callgrind, deterministic instruction counts) and integration-level (real client operations with allocation tracking).

Protocol benchmarks (iai-callgrind)

wacore includes a suite of iai-callgrind benchmarks that measure instruction counts for core protocol operations. These benchmarks run under Valgrind’s Callgrind tool, producing deterministic, low-noise measurements that are suitable for CI regression tracking.

Prerequisites

Running the protocol benchmarks requires:
  • Nightly Rust (the project pins nightly-2026-04-05)
  • Valgrind installed on your system
  • iai-callgrind-runner (cargo install iai-callgrind-runner --version 0.16.1)

Available suites

Running protocol benchmarks

Each benchmark produces instruction counts and optionally generates flamegraph SVGs for profiling hotspots.

Integration benchmarks

The bench-integration test suite (tests/bench-integration/) measures real-world client operations end-to-end, including wall-clock time and heap allocation counts. It uses a custom CountingAlloc global allocator that tracks every allocation and byte count, then produces customSmallerIsBetter JSON output for CI trend tracking.

Scenarios

Each scenario reports three metrics: alloc_count (number of heap allocations), alloc_bytes (total bytes allocated), and wall_ms (elapsed wall-clock time). Amortized scenarios divide totals by the number of iterations for per-operation costs.

Running integration benchmarks

Integration benchmarks require a mock WhatsApp server (e.g., Bartender):
The output is a JSON array of customSmallerIsBetter entries printed to stdout, suitable for consumption by github-action-benchmark.
Integration benchmarks require the danger-skip-tls-verify feature, which is enabled automatically via the bench-integration crate’s Cargo.toml. You can also enable DHAT heap profiling with --features dhat-heap for detailed allocation flamegraphs.

Allocation optimizations

The library includes several allocation-reduction strategies that the integration benchmarks track:
  • Thread-local zlib pool — The binary protocol decompressor (decompress_zlib_pooled) reuses a thread-local flate2::Decompress instance (~48 KB internal state) and output buffer across calls, avoiding per-frame heap allocations
  • CompactString for JIDs — JID user fields use compact_str::CompactString which stores strings up to 24 bytes inline (no heap allocation), covering the vast majority of phone numbers and LID identifiers
  • Server enum — JID server fields use a #[repr(u8)] enum instead of heap-allocated strings, making JID construction and comparison zero-allocation
  • Zero-copy node decoding — Received stanzas are decoded as NodeRef borrowing directly from the network buffer via OwnedNodeRef (yoke-based self-referential type), avoiding cloning string/byte payloads during decode
  • Arc<str> sharing in LidPnEntryLidPnEntry.lid and .phone_number are Arc<str> instead of String. LidPnCache reuses the entry’s own Arcs as the map keys for both lookup directions, so each identifier is allocated once per mapping rather than once as key and again inside the entry. Because this cache is unbounded by design, the saving compounds over the contact base (~40–80 B per mapping, ~0.5–1 MB for 10k contacts). Constructors accept impl Into<Arc<str>>, so String and &str call sites are unaffected; direct field reads return Arc<str> — use &*entry.lid for &str comparisons. Wire/persistence format is unchanged.
  • Pre-allocated buffers — History sync decompression uses a compressed_size_hint with a 4x multiplier for buffer pre-allocation, reducing Vec reallocation during decompression
  • Inline attribute storageAttrs uses AttrsVec (SmallVec<[...; 2]>) instead of Vec. Nodes with ≤2 attributes (the common per-recipient fanout shapes to and enc) keep their attributes on the stack alongside the node, eliminating the per-node allocation for attribute storage. Allocation count: −27% per DM stanza (15→11 allocs); −40% for group fanout with 800 participants (4012→2412 allocs)
  • Secret-presence pre-scan in history sync — before buffa decodes a HistorySyncMsg (30+ fields, String allocations), a shallow varint walk checks whether the message carries message_secret at any level. Messages without a secret are skipped entirely. Bench (20k messages, secret-dense fixture): −29.5% allocations (56,020 → 39,520), −4.3% allocated bytes. Production blobs are secret-sparse so the saving is larger in practice
  • Inline storage in HistoryMsgSecretRecordchat_id is Arc<str> (allocated once per conversation, shared across all records in that conversation), msg_id is CompactString (inline for typical 20–22 char WA IDs on 64-bit targets; smaller inline limit on 32-bit/wasm32), and secret is SecretBytes (inline for secrets ≤32 bytes, heap for larger)
  • Filter-before-materialize in history syncprocess_history_sync_bytes_filtered runs a caller-supplied retention predicate against a borrowed HistoryMsgSecretRecordRef before the owned HistoryMsgSecretRecord is built; a rejected record is never materialized. Bench (500-conversation blob, upstream PR’s rejection-heavy fixture): allocation churn 21.61 MiB → 14.00 MiB, allocation count ~84k → ~26k. The reduction scales with how much of the record set the predicate rejects — the default process_history_sync_bytes (accept-all) sees none of it
  • Streaming visitor/sink for history-sync recordsprocess_history_sync_bytes_with_record_visitor and process_history_sync_bytes_with_record_sink (via the HistoryMsgSecretRecordVisitor trait) go a step further than the filter predicate above. You can build your own storage row directly from the borrowed HistoryMsgSecretRecordRef. This ensures the intermediate owned HistoryMsgSecretRecord is never allocated at all. Bench (allocator-instrumented synthetic history extraction): 20.20 MiB → 14.43 MiB allocated (-28.6%); CodSpeed history stream-drain memory: 243.8 KB → 115.2 KB (2.1× less)
  • Borrowed JID parsingjid::parse_jid_ref parses common protocol JIDs into a JidRef with zero allocation, falling back to Jid’s owned parser only for edge cases. Cut allocations 4,093 → 97 in the history-sync task’s JID classification path
  • In-place buffered media decrypt — non-streaming HttpClient downloads now authenticate the already-buffered response body and decrypt AES-256-CBC in place (DownloadUtils::verify_and_decrypt_in_place, see download), truncating the MAC/padding tail instead of allocating a second file-sized output buffer. Streaming clients are unaffected. Buffered download/decrypt span: 4.355 MiB → 2.146 MiB

CI integration

The repository includes GitHub Actions workflows for both benchmark types: Protocol benchmarks (.github/workflows/benchmark.yml):
  • Runs on every push to main and on pull requests
  • Uses github-action-benchmark to store baselines and generate trend charts on GitHub Pages
  • A custom parser script (.github/scripts/iai-to-benchmark-json.py) converts iai-callgrind instruction counts into the customSmallerIsBetter JSON format
  • Pull requests receive a collapsed PR comment (via .github/scripts/bench-comment.py) grouping benchmarks into regressions (>5% slower), improvements (>5% faster), and unchanged
  • The PR comment is idempotent — subsequent benchmark runs update the existing comment instead of creating duplicates
Integration benchmarks (.github/workflows/bench-integration.yml):
  • Runs on every push to main and on pull requests
  • Spins up a Bartender mock server as a Docker service container
  • Measures allocation counts, allocation bytes, and wall-clock time for each scenario
  • Pushes to main store the baseline for trend tracking
  • Pull requests compare against the baseline
Raw benchmark output for both suites is uploaded as artifacts (retained for 30 days) for debugging.

Next steps

waproto

Protocol Buffers message definitions

Binary Protocol

Type-safe protocol node pattern

Architecture

IqSpec request/response pairing

State Management

Device state and commands