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

View File

@ -105,6 +105,7 @@ public:
if (envelope.has_node_register_request()) {
const auto& request = envelope.node_register_request();
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_interface_count_ = request.node().local_interfaces_size();
cmvr::quic_edge::v1::EdgeControlEnvelope response;
@ -253,6 +254,12 @@ public:
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::lock_guard lock(mutex_);
@ -335,6 +342,7 @@ private:
std::uint32_t heartbeat_ack_delay_ms_{0U};
std::uint64_t server_message_sequence_{0};
std::string last_registered_node_id_;
std::string last_registered_robot_id_;
std::uint32_t last_grpc_endpoint_port_{0};
int last_interface_count_{0};
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_alpn("cmvr-quic-edge/1");
config.set_node_id("test-node");
config.set_robot_id("CN-CMVR-MBLRV1-CHAGAN-20260731-001");
config.set_software_version("test-version");
config.set_grpc_endpoint_host("auto");
config.set_grpc_endpoint_port(50052U);
@ -507,6 +516,8 @@ bool testServiceWithSharedHub()
service.stats().heartbeats_acknowledged >= 1U;
}));
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->connectCount() == 1U);
@ -664,6 +675,8 @@ bool testDeviceManagerSnapshotInHeartbeat()
const auto heartbeat = transport_view->lastHeartbeat();
service.stop();
CHECK_TRUE(heartbeat.robot_id() ==
"CN-CMVR-MBLRV1-CHAGAN-20260731-001");
CHECK_TRUE(heartbeat.has_device_manager());
CHECK_TRUE(heartbeat.device_manager().manager_name() ==
"edge-device-manager");
@ -1016,6 +1029,20 @@ bool testLifecycleStateGuards()
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
int main()
@ -1031,7 +1058,7 @@ int main()
!testRegistrationRejectionBacksOff() ||
!testHeartbeatAckRequiresSessionId() ||
!testSlowMediaStartDoesNotBlockHeartbeat() ||
!testLifecycleStateGuards()) {
!testLifecycleStateGuards() || !testRobotIdIsRequired()) {
return 1;
}
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.
repeated QuicEdgeTrackConfig tracks = 20;
bool include_loopback_interfaces = 21;
// Immutable robot product identity, for example:
// CN-CMVR-MBLRV1-CHAGAN-20260731-001.
string robot_id = 22;
}
message QuicEdgeRootConfig {

View File

@ -121,8 +121,9 @@ explicit.
The legal session order is:
1. The edge sends `NodeRegisterRequest` as its first application message after
every QUIC connect or reconnect. It includes `node_id`, `boot_id`, software
version, the current IPv4/IPv6 interface snapshot and advertised gRPC endpoint.
every QUIC connect or reconnect. It includes `node_id`, the immutable
`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
start until `accepted=true` and a non-empty `session_id` are received.
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
registration response is accepted.
`device_manager = 9` is an additive protobuf field in `NodeHeartbeat`, so this
extension remains QUIC edge protocol v1. Existing gateways ignore the unknown
field. Updated gateways must continue accepting older v1 heartbeats where
`device_manager` is absent and must not treat an absent snapshot as an empty,
healthy DeviceManager. Enum values may only be appended; existing numeric
meanings must never be renumbered or reused.
`NodeDescriptor.robot_id = 6`, `NodeHeartbeat.device_manager = 9` and
`NodeHeartbeat.robot_id = 10` are additive protobuf fields, so these extensions
remain QUIC edge protocol v1. Existing gateways ignore unknown fields. Updated
gateways must continue accepting older v1 messages where these fields are absent
and must not treat an absent device snapshot as an empty, healthy DeviceManager.
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.
The reliable registration and heartbeat path remains valid for a zero-track

View File

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