# ALPINE Rust SDK
`alpine-protocol-sdk` is a high-level wrapper around the published `alpine-protocol-rs`
protocol artifacts. It keeps discovery, handshake, and streaming lifecycles explicit so
application code can reason about each step without diving into the lower-level
protocol helpers.
## When to use the SDK vs the protocol layer
- **Protocol layer** (`alpine-protocol-rs`) gives you access to every message, frame, and
handshake primitive and is useful if you already have a transport layer or
want to implement a custom state machine.
- **SDK** (`alpine-protocol-sdk`) builds on top of the protocol layer and manages sockets,
keep-alive, and profile lifetimes so you can focus on discovery → connect →
streaming flows.
## Quick lifecycle
1. Fetch and cache the root-signed attesters bundle (e.g., from Ops) and build a trust view.
2. Use the discovery runner (recommended) or `DiscoveryClient` (low-level) to find devices.
The runner performs unicast/broadcast/cached fallbacks and optional subnet scans.
3. Call `AlpineClient::connect` with the discovered identity, capability set,
and a credential pair; the SDK spins up the transport plus the keep-alive task.
4. Call `AlpineClient::start_stream`, pass a `StreamProfile`, and track the
returned `config_id`.
5. Use `send_frame` to push encoded `FrameEnvelope`s or `send_control` for
control envelopes.
6. Call `AlpineClient::ping`, `status`, `health`, `identity`, or `metadata` to
send the corresponding control command and receive typed replies when the
device returns structured CBOR payloads.
## Example
```ignore
use alpine_protocol_sdk::{
AlpineClient,
DiscoveryRunOptions,
run_discovery_with_options,
TrustConfig,
load_or_fetch_trust_view,
};
use alpine_protocol_rs::{crypto::identity::NodeCredentials, messages::DeviceIdentity};
use std::net::{SocketAddr, IpAddr, Ipv4Addr};
#[tokio::main]
async fn main() -> Result<(), alpine_protocol_sdk::AlpineSdkError> {
let trust = TrustConfig::new("https://panel.y-link.no/attesters/latest".into())
.with_root_pubkey(alpine_protocol_sdk::parse_root_pubkey_base64(
"BASE64_ED25519_ROOT_PUBKEY",
)?);
let trust_view = load_or_fetch_trust_view(&trust).await?;
let mut opts = DiscoveryRunOptions::default();
opts.attester_registry = Some(trust_view.registry.clone());
let outcome = run_discovery_with_options(
SocketAddr::new(IpAddr::V4(Ipv4Addr::BROADCAST), 19455),
opts,
)
.await?;
let remote = SocketAddr::new(outcome.peer.ip(), outcome.peer.port());
let identity = DeviceIdentity {
device_id: outcome.reply.device_id.clone(),
manufacturer_id: outcome.reply.manufacturer_id.clone(),
model_id: outcome.reply.model_id.clone(),
hardware_rev: outcome.reply.hardware_rev.clone(),
firmware_rev: outcome.reply.firmware_rev.clone(),
};
let credentials = NodeCredentials::load("path/to/credentials")?;
let capabilities = outcome.reply.capabilities.clone();
let mut client = AlpineClient::connect(
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
remote,
identity,
capabilities,
credentials,
)
.await?;
let config_id = client.start_stream(alpine_protocol_rs::profile::StreamProfile::auto())?;
println!("Streaming with config id {}", config_id);
Ok(())
}
```
## Discovery runner behavior
The runner is designed to "just work" on most networks:
- Unicast (if a concrete target is provided).
- Broadcast on all IPv4 interfaces (default).
- Cached unicast fallbacks (if you provide cached targets).
- Optional multicast (`prefer_multicast = true`).
- Optional subnet scan (opt-in via `scan_subnets = true`).
These behaviors are explicit in `DiscoveryRunOptions`; aggressive scanning is opt-in.
## Discovery diagnostics
If you need per-attempt visibility (e.g., broadcast blocked vs unicast denied),
use the report helpers:
- `run_discovery_with_report`
- `run_discovery_with_options_report`
These return a `DiscoveryRunReport` with a `result` plus a list of `DiscoveryAttempt`
entries that include target, mode, local bind, and any error details observed.
Common discovery error hints:
- `discovery channel permission denied` → UDP send/recv blocked by OS policy or firewall.
- `broadcast discovery blocked` → network policy prevents broadcast.
- `multicast discovery unavailable` → multicast not permitted on this network.
- `discovery timed out` → no replies observed before the timeout.
Discovery trust state:
- Use `DiscoveryOutcome::trust_state()` to get a strict `DeviceTrustState` enum
instead of parsing the optional attestation error string.
- Use `DiscoveryOutcome::require_trusted()` to obtain a `TrustedDiscoveryOutcome`
or a clear `AlpineSdkError::UntrustedDevice`.
Discovery retries:
- Use `run_discovery_with_retry` or `run_discovery_with_options_retry` with a
`DiscoveryRetryPolicy` to add exponential backoff across discovery runs.
## Trust bundle setup
To verify device identity attestations, you must supply a root public key and bundle URL.
The SDK does not auto-load environment variables; your application should configure them.
Recommended variables for your app or CLI:
- `ALPINE_ATTESTERS_URL`
- `ALPINE_ROOT_PUBKEY_B64`
## Control helper note
The `ping`, `status`, `health`, `identity`, and `metadata` helpers send a vendor
control command payload (`{ "command": "status" }`) via `ControlOp::Vendor`.
Ensure your device firmware responds to those vendor commands (not only `get_status`).
## Standardized status probe
Use `AlpineClient::probe_status()` for a single canonical liveness + health check.
It runs `ping`, then `status`, and optionally `health` if status is unavailable.
The `ProbeResult` includes a normalized `ProbeState` (`Online`, `Degraded`, `Offline`)
along with per-step errors and round-trip timings.
Use `ProbeResult::to_device_state(trust_state)` to normalize probe + trust into a
`DeviceState` snapshot suitable for UI status badges.
## Control options
Use `AlpineClient::control_with_options` with `ControlOptions` to add per-call
timeouts and retries. The retry policy uses exponential backoff capped by
`backoff_max_ms`.
## Connect policy
Use `connect_with_policy` to enforce discovery → trust → handshake → first probe.
The default `ConnectPolicy` requires a trusted identity, probes after handshake,
and rejects `Offline` (and optionally `Degraded`) devices.
If you have a `DiscoveryRunReport`, use `connect_with_policy_from_report` to
include discovery attempt diagnostics in any discovery/probe failure.
## LAN defaults and cache helpers
Use `DiscoveryRunOptions::defaults_for_lan()` for a safe LAN baseline.
Use `discover_with_cache` or `discover_with_cache_report` to try cached targets
before falling back to broadcast.
Every exported module in this crate has `///` documentation so the generated
docs on docs.rs describe the same lifecycle described here.
## Discovery dry run
Use `discovery_dry_run` to inspect the computed interfaces and broadcast targets
without sending any packets. This is useful when debugging permissions or NIC
selection issues.
## Standard vs vendor status
If your device implements the standard control op, call
`AlpineClient::status_standard()` (uses `ControlOp::GetStatus`).
If your device only implements vendor commands, call `status_vendor()` or
`status()` (vendor command payload).
## Safe client wrapper
`SafeClient` wraps an `AlpineClient` and enforces a probe before control or
stream calls. Use `SafeClientOptions` to require online or accept degraded.
## Session guard
`SessionGuard` tracks idle time and lets you expire or close sessions after
an inactivity timeout.
## Trust policy helper
Use `enforce_trust_policy` with `TrustPolicy` to apply strict or warn-only
trust behavior based on `DeviceTrustState`.
## Logging helpers
Use `init_pretty_logging()` or `init_json_logging()` to set up a default
`tracing` subscriber with env-based filtering.
## Troubleshooting
See `sdk/docs/troubleshooting.md` for common failure modes and debugging tips.
## Common failure modes and remediation
- Discovery timed out: confirm the device is powered on, on the same subnet, and UDP is permitted.
- Broadcast or multicast blocked: switch to unicast or cached targets; ask network admin to allow UDP broadcast.
- Device untrusted: load a trusted attesters bundle and validate `ALPINE_ROOT_PUBKEY_B64`.
- Standard status rejected: use `status_vendor()` or `status()` for vendor-only devices.
- Incompatible protocol: upgrade/downgrade SDK or firmware to match `ALPINE_VERSION`.
- Unsupported environment: avoid WSL/Docker without host networking for UDP discovery/control.