This library handles the connection lifecycle for Bloop protocol clients: DNS lookup, TLS, protocol handshake, authentication, keep-alive pings, and reconnecting after failures. Applications observe the connection through a status watch and perform request-response exchanges through typed operations.
The wire format lives in the bloop-protocol crate.
use bloop_client_framework::{BloopClient, ConnectionConfig, ConnectionStatus};
let client = BloopClient::builder()
.config(ConnectionConfig {
host: "bloop.example.com".to_string(),
port: 12345,
client_id: "client".to_string(),
client_secret: "secret".to_string(),
})
.build()?;
let mut status = client.status();
status.wait_for(|status| matches!(status, ConnectionStatus::Connected { .. })).await?;
let achievements = client.bloop(nfc_uid).await?;
let audio = client.retrieve_audio(achievement_id).await?;The client can also start without configuration (status Unconfigured) and receive credentials at runtime via
configure, e.g. when they arrive on a provisioning tag. Rejected credentials latch the client until new ones
arrive; use InvalidCredentialsPolicy::Retry to keep retrying instead.
Extensions define their messages with the bloop-protocol derives (direct dependency required, opcodes 0x80 and
above) and pair each request with its response type through the Request trait:
use bloop_client_framework::Request;
use bloop_protocol::{Decode, Encode, Payload};
#[derive(Debug, Encode, Decode, Payload)]
#[bloop(opcode = 0x80)]
struct SubmitScore {
nfc_uid: NfcUid,
score: u32,
}
#[derive(Debug, Encode, Decode, Payload)]
#[bloop(opcode = 0x81)]
struct ScoreAccepted {
rank: u32,
}
impl Request for SubmitScore {
type Response = ScoreAccepted;
}
// Fully typed request-response:
let accepted = client.custom(SubmitScore { nfc_uid, score }).await?;Protocol errors, including extension-defined codes, arrive as RequestError::Error(ErrorResponse). For exchanges
that fall outside the one-to-one mapping there is request_raw.
Work that must happen on every (re)connect, such as preloading extension data, runs inside the connection attempt,
before the status flips to Connected; a hook failure fails the attempt:
let builder = builder.on_connect(|session| {
Box::pin(async move {
let scores = session.custom(FetchHighScores).await?;
// store the list somewhere shared
Ok(())
})
});All server I/O inside the hook must go through the Session: the hook runs on the client's own connection task,
so calling methods on a captured BloopClient handle in the hook deadlocks the client. AudioCache accepts a
&mut Session for the same reason.
AudioCache keeps achievement audio on disk, keyed by achievement ID and audio hash, so server-side updates
invalidate stale files naturally:
let cache = AudioCache::new("/var/cache/bloop/audio");
// On demand, e.g. when a bloop awards an achievement:
if let Some(path) = cache.ensure(&client, &record).await? {
play(path);
}
// Or as a preload after a PreloadOutcome::Mismatch:
let skipped = cache.sync(&client, &achievements).await?;sync returns the IDs of achievements whose audio the server refused to deliver; a non-empty list means the sync
was partial, so don't persist the new manifest hash and the next preload check will retry.
The nfc feature provides a channel-backed reader handle with cancel-safe waits, suitable for select! loops, plus
NDEF text-record parsing. The nfc-mfrc522 feature adds the built-in backend for MFRC522 modules over SPI (Linux):
use bloop_client_framework::nfc::{NfcReader, Mfrc522Config};
let reader = NfcReader::spawn_mfrc522(Mfrc522Config::default()).await?;
let uid = reader.wait_for_card().await?;
let achievements = client.bloop(uid).await?;
reader.wait_for_removal().await?;Custom or emulated backends serve the other end of NfcReader::channel() instead, and serve_mfrc522 runs the
built-in backend blocking on the calling thread for applications that supervise reader threads themselves.
All features are off by default:
[dependencies]
bloop-client-framework = { version = "1", features = ["audio", "nfc-mfrc522"] }| Feature | Description |
|---|---|
audio |
Audio playback for achievement and UI sounds |
nfc |
NFC reader handle, backend channel, and NDEF parsing |
nfc-mfrc522 |
Built-in MFRC522 reader backend (Linux, SPI + GPIO) |
tokio-graceful-shutdown |
Implements IntoSubsystem for the client, quitting cleanly on shutdown |
The server certificate is verified through the operating system's certificate verifier
(rustls-platform-verifier). On Linux this reads the system CA
bundle once at startup, so locally installed CAs (e.g. for self-signed certificates) require an application restart.
RootCertSource::DangerousDisabled skips verification entirely for testing.