2026-07-23 12:52:30 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 12:35:04 +08:00
|
|
|
// Stable protocol-level categories for devices managed by cmvr-es. These
|
|
|
|
|
// values intentionally do not reuse the configuration or gRPC API enums:
|
|
|
|
|
// their zero values and supported categories have different semantics.
|
|
|
|
|
enum DeviceKind {
|
|
|
|
|
DEVICE_KIND_UNSPECIFIED = 0;
|
|
|
|
|
DEVICE_KIND_AGV = 1;
|
|
|
|
|
DEVICE_KIND_ARM = 2;
|
|
|
|
|
DEVICE_KIND_BATTERY = 3;
|
|
|
|
|
DEVICE_KIND_BIO_HEAD = 4;
|
|
|
|
|
DEVICE_KIND_CAMERA = 5;
|
|
|
|
|
DEVICE_KIND_CAN_BUS = 6;
|
|
|
|
|
DEVICE_KIND_DEX_HAND = 7;
|
|
|
|
|
DEVICE_KIND_GRIPPER = 8;
|
|
|
|
|
DEVICE_KIND_MICROPHONE = 9;
|
|
|
|
|
DEVICE_KIND_MOTOR = 10;
|
|
|
|
|
DEVICE_KIND_MOTOR_SYSTEM = 11;
|
|
|
|
|
DEVICE_KIND_ROBOT = 12;
|
|
|
|
|
DEVICE_KIND_SPEAKER = 13;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeviceManager's view of a configured entry. REGISTERED means that the
|
|
|
|
|
// manager owns a device record but has no more specific lifecycle signal.
|
|
|
|
|
enum ManagedDeviceState {
|
|
|
|
|
MANAGED_DEVICE_STATE_UNSPECIFIED = 0;
|
|
|
|
|
MANAGED_DEVICE_STATE_DISABLED = 1;
|
|
|
|
|
MANAGED_DEVICE_STATE_INITIALIZING = 2;
|
|
|
|
|
MANAGED_DEVICE_STATE_REGISTERED = 3;
|
|
|
|
|
MANAGED_DEVICE_STATE_READY = 4;
|
|
|
|
|
MANAGED_DEVICE_STATE_RUNNING = 5;
|
|
|
|
|
MANAGED_DEVICE_STATE_STOPPED = 6;
|
|
|
|
|
MANAGED_DEVICE_STATE_ERROR = 7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Health is independent of lifecycle. UNSPECIFIED means that no trustworthy
|
|
|
|
|
// health observation is available and must never be interpreted as healthy.
|
|
|
|
|
enum DeviceHealthStatus {
|
|
|
|
|
DEVICE_HEALTH_STATUS_UNSPECIFIED = 0;
|
|
|
|
|
DEVICE_HEALTH_STATUS_HEALTHY = 1;
|
|
|
|
|
DEVICE_HEALTH_STATUS_DEGRADED = 2;
|
|
|
|
|
DEVICE_HEALTH_STATUS_FAULT = 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
message ManagedDeviceStatus {
|
|
|
|
|
string device_id = 1;
|
|
|
|
|
DeviceKind kind = 2;
|
|
|
|
|
|
|
|
|
|
// Concrete implementation name when a device object exists; otherwise a
|
|
|
|
|
// category label. It is for display/diagnostics only. Consumers use kind,
|
|
|
|
|
// rather than this free-form string, for machine decisions.
|
|
|
|
|
string type_name = 3;
|
|
|
|
|
|
|
|
|
|
bool enabled = 4;
|
|
|
|
|
ManagedDeviceState manager_state = 5;
|
|
|
|
|
DeviceHealthStatus health = 6;
|
|
|
|
|
|
|
|
|
|
// false means that no error is currently confirmed. It does not turn
|
|
|
|
|
// DEVICE_HEALTH_STATUS_UNSPECIFIED into a healthy observation.
|
|
|
|
|
bool has_error = 7;
|
|
|
|
|
string error_message = 8;
|
|
|
|
|
|
|
|
|
|
// Time at which DeviceManager last changed the lifecycle/error record.
|
|
|
|
|
// DeviceManagerSnapshot.sampled_at_unix_ms is the freshness timestamp for
|
|
|
|
|
// the health observation carried by this heartbeat.
|
|
|
|
|
uint64 status_updated_at_unix_ms = 9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
message DeviceManagerSnapshot {
|
|
|
|
|
string manager_name = 1;
|
|
|
|
|
string manager_version = 2;
|
|
|
|
|
string manager_description = 3;
|
|
|
|
|
|
|
|
|
|
// Current cmvr-es senders include only enabled devices. The enabled field in
|
|
|
|
|
// each row and DISABLED enum value remain part of v1 for wire compatibility.
|
|
|
|
|
repeated ManagedDeviceStatus devices = 4;
|
|
|
|
|
uint64 sampled_at_unix_ms = 5;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 12:52:30 +08:00
|
|
|
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;
|
2026-07-24 12:35:04 +08:00
|
|
|
DeviceManagerSnapshot device_manager = 9;
|
2026-07-23 12:52:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|