// iMuninn wire protocol — single source of truth for all four ends
// (Swift / Kotlin / Python / C++). See protocol/README.md for channel layout,
// versioning rules and coordinate conventions. Design doc: docs/iMuninn-设计文档.md §6.
// v5 (Hello.data_transport: data over WebSocket for browsers; v4: DeviceConfig.session_params). Every change to this file must bump PROTOCOL_VERSION in
// protocol/VERSION and be committed together with all consumers.
option swift_prefix = "Mn";
option java_package = "io.imuninn.proto";
option java_multiple_files = true;
// ---------------------------------------------------------------------------
// Common header (§6.4) — carried by every data message.
// ---------------------------------------------------------------------------
// Stream this sample belongs to (e.g. "rgb", "imu", "ar_pose", "depth").
// Monotonic per-stream sequence number; gaps mean drops.
// Device monotonic clock, nanoseconds. Same time base for all streams on a
// device (iOS: ARFrame.timestamp / CoreMotion; see §6.5).
uint64 device_time_ns = 3;
// Protocol version this message was produced with (protocol/VERSION).
uint32 protocol_version = 4;
// Config version in effect when this sample was captured (§6.8).
uint32 config_version = 5;
// ---------------------------------------------------------------------------
// Handshake and capabilities (§6.3)
// ---------------------------------------------------------------------------
PLATFORM_UNSPECIFIED = 0;
string device_id = 1; // stable per install
string device_name = 2; // user-visible
string model = 3; // e.g. "iPhone17,1"
uint32 protocol_version = 6;
// Self-describing parameter (§6.8 "参数自描述"). PC tools and the phone UI
// generate controls from this; adding a parameter needs no UI change.
string key = 1; // dotted path, e.g. "rgb.exposure.mode"
// When a change takes effect.
APPLY_IMMEDIATE = 1; // no interruption
APPLY_RESTART_SESSION = 2; // AR / capture session restarted; streams emit a Gap
// Range for numeric kinds.
// Choices for KIND_ENUM.
repeated string choices = 9;
string s = 4; // enum choice or free string
STREAM_KIND_UNSPECIFIED = 0;
STREAM_KIND_CONFIG = 5; // config-change events (recorded as its own MCAP channel)
STREAM_KIND_HEALTH = 6; // DeviceHealth: thermal, battery, phone-side rates
message StreamDescriptor {
repeated ParamSpec params = 4; // parameters scoped to this stream
TRANSPORT_UNSPECIFIED = 0;
TRANSPORT_UDP = 1; // IMU / pose: allow loss, latency first
TRANSPORT_TCP = 2; // video / depth v1
TRANSPORT_WEBSOCKET = 3; // control; also video/depth for web clients
repeated StreamDescriptor streams = 2;
repeated ParamSpec session_params = 3; // e.g. "session.mode"
DeviceConfig config = 4; // current effective config
// ---------------------------------------------------------------------------
// Configuration model (§6.8)
// ---------------------------------------------------------------------------
SESSION_MODE_UNSPECIFIED = 0;
SESSION_MODE_AR = 1; // pose available, main camera only
SESSION_MODE_CAMERA = 2; // full camera control, no pose
uint32 config_version = 1;
repeated StreamConfig streams = 3;
NetworkConfig network = 4;
// Session-level parameters other than mode, keyed by ParamSpec.key from
// Capabilities.session_params (e.g. "black_screen"). In a SetConfig patch only the
// listed keys change. v4.
map<string, Value> session_params = 5;
optional bool enabled = 2; // optional: absent in a SetConfig patch means "unchanged"
map<string, Value> params = 3; // keyed by ParamSpec.key (relative)
EncoderConfig encoder = 4;
DeliveryConfig delivery = 5;
string codec = 1; // "h264", "hevc", "none", "lz4", "zstd"
uint32 keyframe_interval = 3;
DROP_POLICY_UNSPECIFIED = 0;
DROP_POLICY_LATEST_ONLY = 1; // keep newest frame, drop older (video)
DROP_POLICY_NONE = 2; // never drop (control / depth)
DropPolicy drop_policy = 2;
uint32 max_rate_hz = 3; // 0 = unlimited
uint32 bandwidth_limit_bps = 4; // 0 = unlimited
// Control channel RPC-style envelopes (WebSocket, reliable).
uint64 request_id = 1; // echoed in responses; 0 for broadcasts
Hello hello = 10; // phone -> pc on connect
Welcome welcome = 11; // pc -> phone
GetCapabilities get_capabilities = 20;
Capabilities capabilities = 21;
GetConfig get_config = 22;
DeviceConfig config = 23;
SetConfig set_config = 24;
ConfigResult config_result = 25;
ConfigChanged config_changed = 26; // broadcast to all clients
Subscribe subscribe = 30;
SessionControl session_control = 31;
TimeSyncPing time_sync_ping = 40;
TimeSyncPong time_sync_pong = 41;
// Direction: the PHONE is the server. It advertises `_imuninn._tcp` over Bonjour
// and listens for control (WebSocket), data-TCP connections; clients (PCs) connect.
// A client opens the control WebSocket and sends Hello; the phone answers Welcome.
string name = 1; // e.g. hostname or script name
string version = 2; // client library version
message Hello { // client -> phone
uint32 udp_port = 2; // where this client receives DataMessage datagrams (0 = none)
uint32 protocol_version = 3;
// v5: how this client wants its data streams. TCP (default) = the length-prefixed
// stream on Welcome.tcp_port plus UDP for pose / IMU / health when udp_port > 0.
// WEBSOCKET = the client opens a second WebSocket to the control port and sends
// "IMDATA01" + uint32 LE client_id as its first binary frame; the phone then sends
// every subscribed stream (video, depth, pose, IMU, health) on it as binary
// DataMessage frames. Browsers use this; nothing else changes.
Transport data_transport = 4;
message Welcome { // phone -> client
Capabilities capabilities = 2;
uint32 protocol_version = 3;
uint32 tcp_port = 4; // connect here for the length-prefixed DataMessage stream
uint32 client_id = 5; // first 4 bytes (LE) the client writes on that TCP connection
message GetCapabilities {}
// Partial update: only fields present in `patch` change.
uint32 base_config_version = 1; // what the client saw; mismatch -> rejected
uint32 config_version = 1;
repeated FieldResult fields = 2;
bool session_restarted = 3;
message ConfigChanged { DeviceConfig config = 1; }
repeated string stream_ids = 1;
ACTION_START_RECORDING = 3; // local recording on the phone (FileSink)
ACTION_STOP_RECORDING = 4;
message TimeSyncPing { uint64 client_send_ns = 1; }
uint64 client_send_ns = 1;
uint64 device_recv_ns = 2;
uint64 device_send_ns = 3;
// Write lease (§6.8 控制权): one writer at a time, auto-released on disconnect.
ACTION_REVOKED = 5; // phone UI took over
message Error { uint32 code = 1; string message = 2; }
// Periodic health report, stream "health" (§11 thermal throttling visibility).
float battery_level = 3; // 0..1, -1 unknown
BatteryState battery_state = 4;
double pose_hz = 6; // phone-side rates (what the sources produce)
uint64 encoder_dropped = 11;
double uptime_s = 12; // since the app started
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
message Vec3 { double x = 1; double y = 2; double z = 3; }
message Quat { double x = 1; double y = 2; double z = 3; double w = 4; }
Vec3 accel = 2; // m/s^2, device frame
Vec3 gyro = 3; // rad/s, device frame
Vec3 mag = 4; // uT, optional (all zero if absent)
// Camera intrinsics are sent with EVERY frame (§5.4), not only at handshake.
repeated double distortion = 7; // empty for ARKit (already rectified)
TRACKING_STATE_UNSPECIFIED = 0;
TRACKING_STATE_NOT_AVAILABLE = 1;
TRACKING_STATE_LIMITED = 2;
TRACKING_STATE_NORMAL = 3;
// Pose in the native AR convention: right-handed, Y up, camera looks -Z
// (OpenGL). PC SDK converts to OpenCV / ROS (§6.6).
TrackingState tracking = 4;
Intrinsics intrinsics = 5; // intrinsics of the frame this pose belongs to
PIXEL_FORMAT_UNSPECIFIED = 0;
PIXEL_FORMAT_H264_NAL = 1; // Annex-B NAL units
PIXEL_FORMAT_HEVC_NAL = 2;
PIXEL_FORMAT_NV12 = 3; // raw, for future zero-copy path
// Image orientation: ARKit captured images are sensor-native landscape and
// do not rotate with the device (§5.4). `rotation_deg` says how to rotate to
// make the image upright for display; data is always sent as captured.
// `rotation_deg`: rotate the image clockwise by this much to view it upright when
// the phone is held in portrait. On iPhone, every camera (back lenses and the front
// camera) delivers landscape sensor frames that need 90; the field exists so other
// devices can say otherwise. Intrinsics are in the sensor frame.
bytes data = 6; // v1: payload inline (§6.1)
Intrinsics intrinsics = 7;
uint32 rotation_deg = 8; // 0 / 90 / 180 / 270
DEPTH_COMPRESSION_NONE = 0;
DEPTH_COMPRESSION_LZ4 = 1;
DEPTH_COMPRESSION_ZSTD = 2;
// Depth is uint16 millimetres, row-major, little-endian (§5.4), plus an
// optional confidence plane (uint8, 0 = low, 2 = high on ARKit).
DepthCompression compression = 4;
bytes confidence = 6; // empty if unavailable
Intrinsics intrinsics = 7; // depth-camera intrinsics (may differ from rgb)
// Emitted on a stream when a restart interrupts it (§6.8 step 4).
// Envelope for data channels so a single socket can carry mixed streams.
ConfigChanged config_changed = 6;