syntax = "proto3"; package cmvr.quic_edge.v1; // Every application message on the edge-opened reliable bidirectional stream // is carried by this envelope. message_sequence is strictly increasing per // sender (gaps are allowed) and is independent from the heartbeat sequence used // for liveness acknowledgement. message EdgeControlEnvelope { uint32 protocol_version = 1; uint64 message_sequence = 2; oneof payload { NodeRegisterRequest node_register_request = 10; NodeRegisterResponse node_register_response = 11; NodeHeartbeat node_heartbeat = 12; NodeHeartbeatAck node_heartbeat_ack = 13; MediaSessionOpen media_session_open = 20; MediaTrackDescriptor media_track_descriptor = 21; MediaSessionClose media_session_close = 22; ProtocolError protocol_error = 30; } } message NetworkInterfaceAddress { enum AddressFamily { ADDRESS_FAMILY_UNSPECIFIED = 0; ADDRESS_FAMILY_IPV4 = 1; ADDRESS_FAMILY_IPV6 = 2; } string interface_name = 1; string ip_address = 2; AddressFamily family = 3; bool loopback = 4; } // The existing cmvr-es gRPC server remains the robot-control endpoint. The // edge advertises its current reachable address through the QUIC control plane. message GrpcEndpoint { string host = 1; uint32 port = 2; bool tls = 3; } message NodeDescriptor { string node_id = 1; string boot_id = 2; string software_version = 3; repeated NetworkInterfaceAddress local_interfaces = 4; GrpcEndpoint grpc_endpoint = 5; } // This must be the first application message sent after each QUIC connection // is established. A reconnect always creates a new registration session. message NodeRegisterRequest { NodeDescriptor node = 1; uint64 sent_at_unix_ms = 2; } message NodeRegisterResponse { bool accepted = 1; string session_id = 2; string message = 3; // Zero tells the edge to keep its locally configured interval. uint32 heartbeat_interval_ms = 4; // Derived by the receiver from the authenticated QUIC peer address. It is // not copied from a client-supplied local interface. string observed_source_ip = 5; } // A heartbeat carries a fresh network snapshot so address changes are reported // without opening a second protocol or connection. message NodeHeartbeat { string node_id = 1; string boot_id = 2; string session_id = 3; uint64 sequence = 4; uint64 sent_at_unix_ms = 5; string software_version = 6; repeated NetworkInterfaceAddress local_interfaces = 7; GrpcEndpoint grpc_endpoint = 8; } message NodeHeartbeatAck { bool accepted = 1; uint64 acknowledged_sequence = 2; string message = 3; uint64 server_time_unix_ms = 4; string observed_source_ip = 5; string session_id = 6; } message MediaSessionOpen { string node_id = 1; uint64 session_epoch = 2; // Binds the media epoch to the accepted node registration on this QUIC // connection. Media must not start before this session is assigned. string session_id = 3; } enum MediaKind { MEDIA_KIND_UNSPECIFIED = 0; MEDIA_KIND_VIDEO = 1; MEDIA_KIND_AUDIO = 2; } message MediaTrackDescriptor { uint32 track_id = 1; MediaKind kind = 2; string device_id = 3; string codec = 4; uint64 codec_generation = 5; // The exact MediaSourceHub track and the 32-bit token repeated in every // DATAGRAM header. The full generation remains on the reliable stream. string source_track_id = 6; uint32 codec_generation_token = 7; string payload_format = 8; // Video fields. They are zero for audio tracks. uint32 width = 10; uint32 height = 11; uint32 frames_per_second = 12; // Audio fields. They are zero for video tracks. uint32 sample_rate = 20; uint32 channels = 21; // Decoder initialization bytes, for example AVCC/HVCC or AudioSpecificConfig. // Existing cmvr-es sources may leave this empty when configuration NAL units // are carried in-band. bytes codec_config = 30; } message MediaSessionClose { string reason = 1; string session_id = 2; uint64 session_epoch = 3; } message ProtocolError { uint32 code = 1; string message = 2; uint64 related_message_sequence = 3; bool fatal = 4; }