Version: 3.0 (2025-07-06)
The Bloop Protocol is a binary, synchronous request-response protocol layered over a TLS stream. It is used to communicate between clients (e.g., Bloop Boxes) and the server, supporting features like version negotiation, authentication, NFC-based achievements, audio retrieval and connection management.
Any minor version upgrades should be backward compatible, which means no change to existing messages or message flow. This means that only new messages might be added, which would be determined by the server capability flags.
Any changes to the existing flow will create a new major version. A client should be able to support several major versions if possible. Due to the lack of version negotiation in version 2, support for that must be manually configured if supported.
- Added version negotiation and capabilities bitmask
- Achievement responses include optionally audio hashes
- Added preload (audio manifest) hash mechanism
- Unified message framing and error handling
- Custom extension space reserved for users (opcodes ≥
0x80
) - Message types have been made unique between server and client messages
- Authentication message format adjusted and local IP address added
- Achievement IDs changed to UUIDs
- Quit message added
The message format descriptions in this section use Rust-like struct definitions to describe their layout. The structs are packed, i.e., there are never any alignment gaps.
The following data types are used in the descriptions:
Type | Description |
---|---|
uint8 |
8-bit unsigned integer |
uint32 |
32-bit unsigned integer, little endian |
uint64 |
64-bit unsigned integer, little endian |
uuid |
Fixed 16-byte array, equivalent to [u8; 16] |
nfc_uid |
A uint8 length (must be 4, 7, or 10), followed by the give number of bytes |
ip_addr |
A uint8 indicating IP version (4 or 6) followed by 4 or 16 bytes for IPv4/IPv6 addresses |
hash |
A byte string prefixed with its length as uint8 |
string |
A UTF-8 encoded text string prefixed with its byte length as uint8 |
bytes |
A byte string prefixed with its length as uint32 |
Achievement
conveys the unique identifier of an achievement and, optionally, the hash of the associated audio
file. Using the bytes
type for the audio hash allows signaling the absence of audio (zero-length) or including the
actual hash.
struct Achievement {
// Unique ID of the achievement.
id: uuid,
// Audio file hash. If the length is zero, no audio is available.
audio_hash: hash,
}
All messages in the Bloop protocol have the following format:
struct Message {
// Message type indicating the message purpose.
message_type: uint8,
// Total payload length in bytes, excluding message_type and payload_length.
payload_length: uint32,
// Message payload. The content depends on the message type.
payload: [u8; payload_length],
}
The server and the client must not fragment messages, i.e., a complete message must be sent before continuing. Most messages come in pairs (request and response), in which case the client must wait for the response before sending another message.
At any point the server may send an ErrorResponse
indicating an error condition. This is implied in
the message flow documentation, and only successful paths are explicitly documented. The handling of the ErrorResponse
message depends on the connection phase, as well as the severity of the error.
If the server is not able to recover from an error, the connection is closed immediately after an ErrorResponse
message is sent.
There are two main phases in the lifetime of a Bloop connection: the connection phase and the command phase.
The connection phase is responsible for negotiating the protocol and connection parameters, including authentication. The command phase is the regular operation phase where the server is processing queries sent by the client.
To begin a session, a client opens a connection to the server and sends the ClientHandshake. The server responds in one of two ways:
- ServerHandshake, which also declares the supported server capabilities.
- ErrorResponse, which indicates an unsupported version range.
A server must only respond with a major version within the requested version range. If the returned version is outside the requested range, the client must terminate the connection.
Clients must ignore unknown capability flags but may log them for diagnostics.
The client must now authenticate by sending an Authentication
message. The server will either respond with an
AuthenticationAccepted
message or an ErrorResponse
.
In the command phase, the server expects the client to send one of the following messages:
Instructs the server to verify an NFC UID. On success, the server responds with a BloopAccepted message. If the NFC UID is unknown or has been throttled, i.e., due to too many bloops at the same client, the server will instead respond with an ErrorResponse message.
Ask the server for sending audio data for a given achievement. If audio data exists for the achievement ID, the server responds with an AudioData message. If either, the achievement doesn't exist or it has no audio data attached, the server responds with an ErrorResponse message.
Acts as a keep-alive signal to the server, to which the server will always respond with a Pong message. A server implementation should typically have a connection timeout. Ideally, a client should send a ping message every couple of seconds. This also helps the client to determine if it is still connected, i.e., when the Wi-Fi connection drops.
Recommended: Clients should send a Ping every 5 seconds. Servers should disconnect after 30 seconds of inactivity.
Only available if the sever capability flags contain
0x01
.
The Preload Check allows the client to verify if its cached audio assets are up to date by sending its stored global manifest hash to the server. The server compares this hash against its current audio manifest hash and responds accordingly.
If the manifest hash matches, the server responds with a PreloadMatch message and the client can resume as normal. In case of a mismatch, the server responds with a PreloadMismatch message. In this case, the client can check all returned achievements and download any missing audio files.
While the protocol makes no assumptions about the hash type, it is recommended to use MD5 for its short hash length. This is enough for simple integrity checks.
To calculate the audio manifest hash, the server should select all achievements with audio hashes, sort them by their ID and create a compound hash over all IDs and audio hashes. This guarantees a stable hash which should only change when new achievements are added or audio data have been updated.
The normal termination procedure is that the client sends a Quit message and immediately closes the connection. On receipt of this message, the server cleans up the connection resources and closes the connection.
In some cases the server might disconnect without a client request to do so. In such cases the server will attempt to send an ErrorResponse message to indicate the reason for the disconnection.
All messages have a uint8
message type, of which the range of 0x00
to 0x7f
is reserved. Any message types above
that can be used for custom extensions.
Sent by: server.
Format:
struct ErrorResponse {
// Message type.
message_type: uint8 = 0x00,
// Error code specifying the type of error.
error_code: uint8,
}
Error codes in the range of 0x00
to 0x7f
are reserved, any codes above that can be used for custom extensions.
The following error codes are currently defined:
Code | Severity | Description |
---|---|---|
0x00 |
FATAL | Unexpected message sent by client |
0x01 |
FATAL | Malformed message sent by client |
0x02 |
FATAL | Requested version range not supported |
0x03 |
FATAL | Invalid credentials send in authentication |
0x04 |
ERROR | Unknown NFC UID |
0x05 |
ERROR | NFC UID recognized but throttled |
0x06 |
ERROR | Audio file unavailable for the given achievement |
Any fatal error will cause the connection to be closed, while the client should handle error gracefully.
Sent by: client
Format:
struct ClientHandshake {
// Message type.
message_type: uint8 = 0x01,
// Minimum supported major version.
min_version: uint8,
// Maximum supported major version.
max_version: uint8,
}
Sent by: server
Format:
struct ServerHandshake {
// Message type.
message_type: uint8 = 0x02,
// The accepted version based on the requested version range.
accepted_version: uint8,
// Bitmask of capabilities supported by the server.
capabilities: uint64,
}
The following capability flags are currently supported:
Hex code | Description |
---|---|
0x01 |
Audio file preload |
Sent by: client
Format:
struct Authentication {
// Message type.
message_type: uint8 = 0x03,
// Client ID.
client_id: string,
// Client secreted.
client_secret: string,
// Local IP address of the client, either IPv4 or IPv6.
ip_address: ip_addr,
}
Sent by: server
Format:
struct AuthenticationAccepted {
// Message type.
message_type: uint8 = 0x04,
}
Sent by: client
Format:
struct Ping {
// Message type.
message_type: uint8 = 0x05,
}
Sent by: server
Format:
struct Pong {
// Message type.
message_type: uint8 = 0x06,
}
Sent by: client
Format:
struct Quit {
// Message type.
message_type: uint8 = 0x07,
}
Sent by: client
Format:
struct Bloop {
// Message type.
message_type: uint8 = 0x08,
// NFC UID scanned on the client.
nfc_uid: nfc_uid,
}
Sent by: server
Format:
struct BloopAccepted {
// Message type.
message_type: uint8 = 0x09,
// Number of awarded achievements.
num_achievements: uint8,
// List of all awarded achievements.
achievements: [achievement; num_achievements],
}
Sent by: client
Format:
struct RetrieveAudio {
// Message type.
message_type: uint8 = 0x0a,
// Unique ID of the achievement.
achievement_id: uuid,
}
Sent by: server
Format:
struct AudioData {
// Message type.
message_type: uint8 = 0x0b,
// MP3 audio data.
audio_data: bytes,
}
Sent by: client
Format:
struct PreloadCheck {
// Message type.
message_type: uint8 = 0x0c,
// Stored audio manifest hash. Zero-length indicates no stored hash.
audio_manifest_hash: hash,
}
Sent by: server
Format:
struct PreloadCheck {
// Message type.
message_type: uint8 = 0x0d,
}
Sent by: server
Format:
struct PreloadMismatch {
// Message type.
message_type: uint8 = 0x0e,
// New audio manifest hash.
audio_manifest_hash: hash,
// Number of achievements with audio data.
num_achievements: uint32,
// List of all achievements with audio data.
achievements: [achievement; num_achievements],
}