feat(quic): add robot ID to registration and heartbeat

This commit is contained in:
linbo 2026-08-04 15:20:29 +08:00
parent 60098ee01e
commit 08d58b2ccf
6 changed files with 63 additions and 19 deletions

View File

@ -7,6 +7,7 @@ quic_edge {
server_port: 4433 server_port: 4433
alpn: "cmvr-quic-edge/1" alpn: "cmvr-quic-edge/1"
node_id: "cmvr-edge" node_id: "cmvr-edge"
robot_id: "CN-CMVR-MBLRV1-CHAGAN-20260731-001"
software_version: "0.1" software_version: "0.1"
# The existing cmvr-es gRPC server remains the robot-control endpoint. "auto" # The existing cmvr-es gRPC server remains the robot-control endpoint. "auto"

View File

@ -217,6 +217,7 @@ void populateDescriptor(const config::QuicEdgeConfig& config,
{ {
if (!descriptor) return; if (!descriptor) return;
descriptor->set_node_id(node_id); descriptor->set_node_id(node_id);
descriptor->set_robot_id(config.robot_id());
descriptor->set_boot_id(boot_id); descriptor->set_boot_id(boot_id);
descriptor->set_software_version(software_version); descriptor->set_software_version(software_version);
const auto interfaces = const auto interfaces =
@ -442,6 +443,10 @@ bool QuicEdgeService::validateConfig(const config::QuicEdgeConfig& config,
setError(error, "QUIC edge task id is empty"); setError(error, "QUIC edge task id is empty");
return false; return false;
} }
if (config.robot_id().empty()) {
setError(error, "QUIC edge robot_id is empty");
return false;
}
if (config.server_host().empty()) { if (config.server_host().empty()) {
setError(error, "QUIC edge server_host is empty"); setError(error, "QUIC edge server_host is empty");
return false; return false;
@ -1097,6 +1102,7 @@ bool QuicEdgeService::sendNodeRegistration(std::string* error)
<< ", message_sequence=" << envelope.message_sequence() << ", message_sequence=" << envelope.message_sequence()
<< ", payload_bytes=" << serialized.size() << ", payload_bytes=" << serialized.size()
<< ", node_id=" << node.node_id() << ", node_id=" << node.node_id()
<< ", robot_id=" << node.robot_id()
<< ", boot_id=" << node.boot_id() << ", boot_id=" << node.boot_id()
<< ", grpc_endpoint=" << node.grpc_endpoint().host() << ", grpc_endpoint=" << node.grpc_endpoint().host()
<< ':' << node.grpc_endpoint().port() << ':' << node.grpc_endpoint().port()
@ -1145,6 +1151,7 @@ bool QuicEdgeService::sendHeartbeat(const std::uint64_t sequence,
envelope.set_message_sequence(control_message_sequence_++); envelope.set_message_sequence(control_message_sequence_++);
auto* heartbeat = envelope.mutable_node_heartbeat(); auto* heartbeat = envelope.mutable_node_heartbeat();
heartbeat->set_node_id(node_id_); heartbeat->set_node_id(node_id_);
heartbeat->set_robot_id(config_.robot_id());
heartbeat->set_boot_id(boot_id_); heartbeat->set_boot_id(boot_id_);
heartbeat->set_session_id(session_id); heartbeat->set_session_id(session_id);
heartbeat->set_sequence(sequence); heartbeat->set_sequence(sequence);
@ -1163,16 +1170,17 @@ bool QuicEdgeService::sendHeartbeat(const std::uint64_t sequence,
return false; return false;
} }
if (!sendControlEnvelope(serialized, error)) return false; if (!sendControlEnvelope(serialized, error)) return false;
// CMVR_LOG(INFO) << "[QuicEdgeService] sent NodeHeartbeat" CMVR_LOG(INFO) << "[QuicEdgeService] sent NodeHeartbeat"
// << ", message_sequence=" << envelope.message_sequence() << ", message_sequence=" << envelope.message_sequence()
// << ", payload_bytes=" << serialized.size() << ", payload_bytes=" << serialized.size()
// << ", session=" << session_id << ", robot_id=" << heartbeat->robot_id()
// << ", heartbeat_sequence=" << sequence << ", session=" << session_id
// << ", grpc_endpoint=" << heartbeat->grpc_endpoint().host() << ", heartbeat_sequence=" << sequence
// << ':' << heartbeat->grpc_endpoint().port() << ", grpc_endpoint=" << heartbeat->grpc_endpoint().host()
// << ", interfaces=" << heartbeat->local_interfaces_size() << ':' << heartbeat->grpc_endpoint().port()
// << ", devices=" << heartbeat->device_manager().devices_size() << ", interfaces=" << heartbeat->local_interfaces_size()
// << ", sent_at_unix_ms=" << sent_at_unix_ms; << ", devices=" << heartbeat->device_manager().devices_size()
<< ", sent_at_unix_ms=" << sent_at_unix_ms;
return true; return true;
} }

View File

@ -105,6 +105,7 @@ public:
if (envelope.has_node_register_request()) { if (envelope.has_node_register_request()) {
const auto& request = envelope.node_register_request(); const auto& request = envelope.node_register_request();
last_registered_node_id_ = request.node().node_id(); last_registered_node_id_ = request.node().node_id();
last_registered_robot_id_ = request.node().robot_id();
last_grpc_endpoint_port_ = request.node().grpc_endpoint().port(); last_grpc_endpoint_port_ = request.node().grpc_endpoint().port();
last_interface_count_ = request.node().local_interfaces_size(); last_interface_count_ = request.node().local_interfaces_size();
cmvr::quic_edge::v1::EdgeControlEnvelope response; cmvr::quic_edge::v1::EdgeControlEnvelope response;
@ -253,6 +254,12 @@ public:
return last_registered_node_id_; return last_registered_node_id_;
} }
std::string lastRegisteredRobotId() const
{
std::lock_guard lock(mutex_);
return last_registered_robot_id_;
}
std::uint32_t lastGrpcEndpointPort() const std::uint32_t lastGrpcEndpointPort() const
{ {
std::lock_guard lock(mutex_); std::lock_guard lock(mutex_);
@ -335,6 +342,7 @@ private:
std::uint32_t heartbeat_ack_delay_ms_{0U}; std::uint32_t heartbeat_ack_delay_ms_{0U};
std::uint64_t server_message_sequence_{0}; std::uint64_t server_message_sequence_{0};
std::string last_registered_node_id_; std::string last_registered_node_id_;
std::string last_registered_robot_id_;
std::uint32_t last_grpc_endpoint_port_{0}; std::uint32_t last_grpc_endpoint_port_{0};
int last_interface_count_{0}; int last_interface_count_{0};
std::vector<std::vector<std::uint8_t>> controls_; std::vector<std::vector<std::uint8_t>> controls_;
@ -359,6 +367,7 @@ config::QuicEdgeConfig validConfig(const std::string& source_track_id)
config.set_server_port(4433); config.set_server_port(4433);
config.set_alpn("cmvr-quic-edge/1"); config.set_alpn("cmvr-quic-edge/1");
config.set_node_id("test-node"); config.set_node_id("test-node");
config.set_robot_id("CN-CMVR-MBLRV1-CHAGAN-20260731-001");
config.set_software_version("test-version"); config.set_software_version("test-version");
config.set_grpc_endpoint_host("auto"); config.set_grpc_endpoint_host("auto");
config.set_grpc_endpoint_port(50052U); config.set_grpc_endpoint_port(50052U);
@ -507,6 +516,8 @@ bool testServiceWithSharedHub()
service.stats().heartbeats_acknowledged >= 1U; service.stats().heartbeats_acknowledged >= 1U;
})); }));
CHECK_TRUE(transport_view->lastRegisteredNodeId() == "test-node"); CHECK_TRUE(transport_view->lastRegisteredNodeId() == "test-node");
CHECK_TRUE(transport_view->lastRegisteredRobotId() ==
"CN-CMVR-MBLRV1-CHAGAN-20260731-001");
CHECK_TRUE(transport_view->lastGrpcEndpointPort() == 50052U); CHECK_TRUE(transport_view->lastGrpcEndpointPort() == 50052U);
CHECK_TRUE(transport_view->connectCount() == 1U); CHECK_TRUE(transport_view->connectCount() == 1U);
@ -664,6 +675,8 @@ bool testDeviceManagerSnapshotInHeartbeat()
const auto heartbeat = transport_view->lastHeartbeat(); const auto heartbeat = transport_view->lastHeartbeat();
service.stop(); service.stop();
CHECK_TRUE(heartbeat.robot_id() ==
"CN-CMVR-MBLRV1-CHAGAN-20260731-001");
CHECK_TRUE(heartbeat.has_device_manager()); CHECK_TRUE(heartbeat.has_device_manager());
CHECK_TRUE(heartbeat.device_manager().manager_name() == CHECK_TRUE(heartbeat.device_manager().manager_name() ==
"edge-device-manager"); "edge-device-manager");
@ -1016,6 +1029,20 @@ bool testLifecycleStateGuards()
return true; return true;
} }
bool testRobotIdIsRequired()
{
media::MediaSourceHub hub;
auto config = validPresenceOnlyConfig();
config.clear_robot_id();
auto transport = std::make_unique<FakeTransport>();
quic_edge::QuicEdgeService service(
std::move(config), std::move(transport), hub);
std::string error;
CHECK_TRUE(!service.initialize(&error));
CHECK_TRUE(error.find("robot_id") != std::string::npos);
return true;
}
} // namespace } // namespace
int main() int main()
@ -1031,7 +1058,7 @@ int main()
!testRegistrationRejectionBacksOff() || !testRegistrationRejectionBacksOff() ||
!testHeartbeatAckRequiresSessionId() || !testHeartbeatAckRequiresSessionId() ||
!testSlowMediaStartDoesNotBlockHeartbeat() || !testSlowMediaStartDoesNotBlockHeartbeat() ||
!testLifecycleStateGuards()) { !testLifecycleStateGuards() || !testRobotIdIsRequired()) {
return 1; return 1;
} }
std::cout << "quic_edge_protocol_test: PASS\n"; std::cout << "quic_edge_protocol_test: PASS\n";

View File

@ -83,6 +83,10 @@ message QuicEdgeConfig {
// IP reporting and heartbeat continue without a camera or microphone. // IP reporting and heartbeat continue without a camera or microphone.
repeated QuicEdgeTrackConfig tracks = 20; repeated QuicEdgeTrackConfig tracks = 20;
bool include_loopback_interfaces = 21; bool include_loopback_interfaces = 21;
// Immutable robot product identity, for example:
// CN-CMVR-MBLRV1-CHAGAN-20260731-001.
string robot_id = 22;
} }
message QuicEdgeRootConfig { message QuicEdgeRootConfig {

View File

@ -121,8 +121,9 @@ explicit.
The legal session order is: The legal session order is:
1. The edge sends `NodeRegisterRequest` as its first application message after 1. The edge sends `NodeRegisterRequest` as its first application message after
every QUIC connect or reconnect. It includes `node_id`, `boot_id`, software every QUIC connect or reconnect. It includes `node_id`, the immutable
version, the current IPv4/IPv6 interface snapshot and advertised gRPC endpoint. `robot_id`, `boot_id`, software version, the current IPv4/IPv6 interface
snapshot and advertised gRPC endpoint.
2. The gateway replies with `NodeRegisterResponse`. Media and heartbeat must not 2. The gateway replies with `NodeRegisterResponse`. Media and heartbeat must not
start until `accepted=true` and a non-empty `session_id` are received. start until `accepted=true` and a non-empty `session_id` are received.
3. The edge sends `NodeHeartbeat` at the negotiated interval. The gateway returns 3. The edge sends `NodeHeartbeat` at the negotiated interval. The gateway returns
@ -189,12 +190,13 @@ registered QUIC connection. The edge clamps the negotiated value to its
supported safety range and returns to the local value on reconnect until a new supported safety range and returns to the local value on reconnect until a new
registration response is accepted. registration response is accepted.
`device_manager = 9` is an additive protobuf field in `NodeHeartbeat`, so this `NodeDescriptor.robot_id = 6`, `NodeHeartbeat.device_manager = 9` and
extension remains QUIC edge protocol v1. Existing gateways ignore the unknown `NodeHeartbeat.robot_id = 10` are additive protobuf fields, so these extensions
field. Updated gateways must continue accepting older v1 heartbeats where remain QUIC edge protocol v1. Existing gateways ignore unknown fields. Updated
`device_manager` is absent and must not treat an absent snapshot as an empty, gateways must continue accepting older v1 messages where these fields are absent
healthy DeviceManager. Enum values may only be appended; existing numeric and must not treat an absent device snapshot as an empty, healthy DeviceManager.
meanings must never be renumbered or reused. Enum values may only be appended; existing numeric meanings must never be
renumbered or reused.
DATAGRAM negotiation is required only when at least one media track is enabled. DATAGRAM negotiation is required only when at least one media track is enabled.
The reliable registration and heartbeat path remains valid for a zero-track The reliable registration and heartbeat path remains valid for a zero-track

View File

@ -128,6 +128,7 @@ message NodeDescriptor {
string software_version = 3; string software_version = 3;
repeated NetworkInterfaceAddress local_interfaces = 4; repeated NetworkInterfaceAddress local_interfaces = 4;
GrpcEndpoint grpc_endpoint = 5; GrpcEndpoint grpc_endpoint = 5;
string robot_id = 6;
} }
// This must be the first application message sent after each QUIC connection // This must be the first application message sent after each QUIC connection
@ -162,6 +163,7 @@ message NodeHeartbeat {
repeated NetworkInterfaceAddress local_interfaces = 7; repeated NetworkInterfaceAddress local_interfaces = 7;
GrpcEndpoint grpc_endpoint = 8; GrpcEndpoint grpc_endpoint = 8;
DeviceManagerSnapshot device_manager = 9; DeviceManagerSnapshot device_manager = 9;
string robot_id = 10;
} }
message NodeHeartbeatAck { message NodeHeartbeatAck {