Compare commits

...

2 Commits

Author SHA1 Message Date
f4be2ffaaa feat(safety): unify device admission and recovery
Add the DeviceManager-owned safety coordinator, shared sensor/control policies, command ledger, service guards, generalized StopAll, and RecoverSafetyState. Preserve device-side hardware checks and AUBO hardware E-stop release reconciliation while keeping software E-stop independently latched.
2026-08-17 08:34:44 +08:00
b2b0b63fe0 chore: remove legacy toppra gitlink 2026-08-17 08:34:10 +08:00
99 changed files with 16911 additions and 1083 deletions

@ -1 +0,0 @@
Subproject commit 3089c7897a5711aceb39d25919aca8c57b5c5948

View File

@ -7,6 +7,7 @@ add_subdirectory(algorithms)
add_subdirectory(simulate)
add_subdirectory(devices)
add_subdirectory(manager/control_authority)
add_subdirectory(manager/safety)
add_subdirectory(manager/device_manager)
add_subdirectory(service/stop_all)
add_subdirectory(manager/media_source_hub)

View File

@ -4,6 +4,18 @@ device_manager {
description: "cmvr edge system version 0.1"
init_all_motors_when_no_active_joints: true
# The unified safety coordinator observes all decisions while the legacy
# gates remain authoritative during staged hardware migration.
safety {
mode: SHADOW
stop_all_timeout_ms: 15000
recovery_timeout_ms: 10000
command_ledger_result_capacity: 4096
command_ledger_total_id_capacity: 262144
event_history_capacity: 2048
fail_startup_on_missing_control_capability: false
}
devices {
id: "mujoco_world"
type: DEVICE_TYPE_MUJOCO_WORLD

View File

@ -6,6 +6,15 @@ grpc_server {
camera_stream_max_pending_frames: 2
camera_stream_max_frame_age_ms: 250
# Current small-scope deployment intentionally keeps the existing clients
# certificate-free. Recovery remains unavailable over the network.
security {
transport_mode: INSECURE
authentication_mode: DISABLED
recovery_exposure: RECOVERY_DISABLED
allow_insecure_non_loopback: true
}
# The RobotArm adapter is implemented, but remains explicitly closed until
# the device itself enables teleop group servo, real hashes are provisioned,
# and group-write timing and independent stop behavior pass hardware review.

View File

@ -106,16 +106,22 @@ cmake --install build
- 后端使用独立 SDK RPC 会话持续读取控制器的 `SafetyModeType`、
`RobotModeType` 和硬件急停来源;首次有效样本前、监控断线或样本过期时,
所有 Move、Speed、Servo 和程序启动请求均按不安全状态拒绝;
- 硬件急停、防护停机、Safety Fault/Violation 会锁存安全事件,并使当前运动
generation 失效。控制器重新报告 `Normal`/`ReducedMode` 不会自动解除锁存;
- 锁存后会终止直接运动与程序、关闭 servo 模式并清理控制器轨迹。只有确认
`ExecId == -1`、普通队列和轨迹队列均为空、运行时已停止且机械臂稳定后,
显式 `torqueOn`/`clearFault`/`unlockProtectiveStop` 才可能恢复运动权限;
- 硬件急停会立即使当前运动 generation 失效,并在急停输入有效期间保持锁存。
检测到硬件急停输入消失且控制器重新报告 `Normal`/`ReducedMode` 后,后端应
自动执行安全恢复确认;防护停机和 Safety Fault/Violation 仍保持显式恢复语义;
- `emergencyStop()` 使用独立的 `SoftwareEmergencyStop` 锁存。即使软件急停在真实
硬件急停有效期间触发,后续硬件采样也不能覆盖该锁存,释放硬件急停开关不会
自动清除软件急停;它只能通过显式安全恢复流程解除;
- 锁存后会终止直接运动与程序、关闭 servo 模式并清理控制器轨迹。硬件急停
自动恢复只有在确认 `ExecId == -1`、普通队列和轨迹队列均为空、运行时已停止
且机械臂稳定后才能解除锁存;如果自动确认失败,则继续保持 fail-closed,
并允许通过 `torqueOn`/`clearFault`/`unlockProtectiveStop` 显式重试恢复;
- 恢复流程不会调用 `resume`、`arbitraryResume`、`startMove`,也不会重新提交
急停前的目标、速度、servo 指令或程序;
- AUBO SDK 未在本地文档中保证急停期间 `clearPath` 的可用性,也未说明释放
急停开关后的控制器恢复时序。因此本实现保持 fail-closed 并在释放后再次清队列,
但“释放开关后零位移”的最终保证仍需真机验证及控制器侧安全配置配合;
急停开关后的控制器恢复时序。因此自动恢复必须在释放后再次清队列并完成上述
安全确认;无法确认时不得解除锁存。“释放开关后零位移”的最终保证仍需真机
验证及控制器侧安全配置配合;
- 只访问控制柜 Standard 数字 IO,不访问工具端 IO、可配置 IO 或安全 IO;
- `set_do` 不修改输出 runstate;
- 只有 `StandardOutputRunState::None` 的通道允许写入,否则返回

View File

@ -174,6 +174,18 @@ public:
return true;
}
bool completeHardwareEmergencyStop(
const bool controller_idle,
const bool cancellation_confirmed)
{
if (!state_->completeHardwareEmergencyStopRecovery(
token_, controller_idle, cancellation_confirmed)) {
return false;
}
completed_ = true;
return true;
}
private:
std::shared_ptr<aubo_internal::SafetyState> state_;
aubo_internal::RecoveryToken token_;
@ -304,6 +316,8 @@ const char* safetyConditionName(
return "SystemEmergencyStop";
case Condition::RobotEmergencyStop:
return "RobotEmergencyStop";
case Condition::SoftwareEmergencyStop:
return "SoftwareEmergencyStop";
case Condition::Fault:
return "Fault";
case Condition::Unknown:
@ -328,6 +342,7 @@ SafetyMode publicSafetyMode(
case Condition::SystemEmergencyStop:
return SafetyMode::SystemEmergencyStop;
case Condition::RobotEmergencyStop:
case Condition::SoftwareEmergencyStop:
return SafetyMode::EmergencyStop;
case Condition::Violation:
case Condition::Fault:
@ -376,6 +391,7 @@ struct AuboSafetyMonitor final {
std::atomic<int> runtime_state{
static_cast<int>(RuntimeState::Stopped)};
std::atomic<int> emergency_stop_source{-1};
std::atomic<bool> hardware_emergency_stop_latched{false};
std::atomic<int> servo_mode_select{0};
std::atomic<std::int64_t> last_sample_ns{0};
std::atomic<bool> cancellation_confirmed{true};
@ -449,6 +465,12 @@ void publishSafetySample(
monitor->servo_mode_select.store(servo_mode_select);
monitor->last_sample_ns.store(monotonicNowNs());
if (emergency_stop_source != 0) {
monitor->hardware_emergency_stop_latched.store(true);
} else if (!current.latched) {
monitor->hardware_emergency_stop_latched.store(false);
}
if (previous.observed != condition ||
(!previous.latched && current.latched)) {
if (current.latched) {
@ -866,7 +888,56 @@ void runSafetyMonitor(
refreshSafetySample(
rpc_client, monitor, robot_interface);
if (monitor->safety_state->snapshot().latched) {
auto safety = monitor->safety_state->snapshot();
const bool hardware_estop_released =
aubo_internal::
shouldAutoRecoverHardwareEmergencyStop(
safety,
monitor
->hardware_emergency_stop_latched
.load(),
monitor->emergency_stop_source.load());
if (hardware_estop_released) {
const auto token =
monitor->safety_state->beginRecovery(
safety.epoch);
if (token.has_value()) {
SafetyRecoveryGuard recovery{
monitor->safety_state, *token};
cancelForSafetyTransition(monitor);
const bool terminated =
enforceControllerTermination(
rpc_client, monitor);
refreshSafetySample(
rpc_client, monitor, robot_interface);
const bool controller_idle =
terminated &&
monitor->emergency_stop_source.load() ==
0 &&
aubo_internal::isMotionSafe(
monitor->safety_state->snapshot()
.observed) &&
controllerStillQuiescent(
rpc_client, robot_interface);
if (recovery.completeHardwareEmergencyStop(
controller_idle,
monitor->cancellation_confirmed
.load())) {
monitor->hardware_emergency_stop_latched
.store(false);
CMVR_LOG(INFO)
<< "[AuboArm] hardware emergency-stop release safely reconciled, id="
<< monitor->arm_id;
} else {
CMVR_LOG(WARNING)
<< "[AuboArm] hardware emergency-stop release remains latched because quiescence could not be confirmed, id="
<< monitor->arm_id;
}
}
safety = monitor->safety_state->snapshot();
}
if (safety.latched) {
if (monitor->cancellation_confirmed.load() &&
!controllerStillQuiescent(
rpc_client, robot_interface)) {
@ -1987,7 +2058,7 @@ Result AuboArm::emergencyStop()
std::lock_guard lock(mutex_);
if (sdk_ && sdk_->safety_monitor) {
sdk_->safety_monitor->safety_state->observe(
aubo_internal::SafetyCondition::RobotEmergencyStop);
aubo_internal::SafetyCondition::SoftwareEmergencyStop);
cancelForSafetyTransition(sdk_->safety_monitor);
}
}
@ -3625,7 +3696,9 @@ Result AuboArm::ensureMotionReady_(
if (condition ==
aubo_internal::SafetyCondition::RobotEmergencyStop ||
condition ==
aubo_internal::SafetyCondition::SystemEmergencyStop) {
aubo_internal::SafetyCondition::SystemEmergencyStop ||
condition ==
aubo_internal::SafetyCondition::SoftwareEmergencyStop) {
code = ArmErrorCode::RobotInEmergencyStop;
} else if (
condition == aubo_internal::SafetyCondition::ProtectiveStop ||

View File

@ -19,6 +19,7 @@ enum class SafetyCondition {
SafeguardStop,
SystemEmergencyStop,
RobotEmergencyStop,
SoftwareEmergencyStop,
Fault,
};
@ -74,8 +75,22 @@ struct SafetySnapshot {
std::uint64_t epoch{0};
bool latched{false};
bool recovery_in_progress{false};
bool software_emergency_stop_latched{false};
};
inline bool shouldAutoRecoverHardwareEmergencyStop(
const SafetySnapshot& snapshot,
const bool hardware_emergency_stop_was_observed,
const int current_emergency_stop_source) noexcept
{
return hardware_emergency_stop_was_observed && snapshot.latched &&
!snapshot.recovery_in_progress &&
!snapshot.software_emergency_stop_latched &&
snapshot.latched_reason == SafetyCondition::RobotEmergencyStop &&
isMotionSafe(snapshot.observed) &&
current_emergency_stop_source == 0;
}
// Hardware safety is an event, not a level. Once an unsafe state has been
// observed, returning to Normal only changes the observed level. A separate,
// explicit recovery must prove that the old controller operation has been
@ -89,6 +104,9 @@ public:
std::lock_guard lock(mutex_);
const bool changed = observed_ != condition;
observed_ = condition;
if (condition == SafetyCondition::SoftwareEmergencyStop) {
software_emergency_stop_latched_ = true;
}
if (isMotionSafe(condition)) {
return;
}
@ -98,7 +116,12 @@ public:
}
latched_ = true;
recovery_in_progress_ = false;
latched_reason_ = condition;
// A physical E-stop sample can continue arriving after a software
// E-stop request. Keep the software stop independently latched so a
// later physical-input release can never clear it automatically.
latched_reason_ = software_emergency_stop_latched_
? SafetyCondition::SoftwareEmergencyStop
: condition;
}
std::optional<SafetyPermit> tryPermit() const
@ -144,6 +167,31 @@ public:
return false;
}
latched_ = false;
recovery_in_progress_ = false;
latched_reason_ = SafetyCondition::Unknown;
software_emergency_stop_latched_ = false;
++epoch_;
return true;
}
// Hardware E-stop release may clear only this software latch. It does not
// power on, release brakes, resume runtime, or issue a motion command.
bool completeHardwareEmergencyStopRecovery(
const RecoveryToken token,
const bool controller_idle,
const bool cancellation_confirmed)
{
std::lock_guard lock(mutex_);
if (!token.valid() || token.epoch != epoch_ || !latched_ ||
!recovery_in_progress_ ||
software_emergency_stop_latched_ ||
latched_reason_ != SafetyCondition::RobotEmergencyStop ||
!isMotionSafe(observed_) || !controller_idle ||
!cancellation_confirmed) {
return false;
}
latched_ = false;
recovery_in_progress_ = false;
latched_reason_ = SafetyCondition::Unknown;
@ -167,7 +215,8 @@ public:
latched_reason_,
epoch_,
latched_,
recovery_in_progress_};
recovery_in_progress_,
software_emergency_stop_latched_};
}
private:
@ -177,6 +226,7 @@ private:
std::uint64_t epoch_{1};
bool latched_{false};
bool recovery_in_progress_{false};
bool software_emergency_stop_latched_{false};
};
} // namespace cmvr::device::aubo_internal

View File

@ -48,10 +48,15 @@ int main()
CHECK_TRUE(state.snapshot().latched);
CHECK_TRUE(!state.beginRecovery(state.snapshot().epoch).has_value());
// Releasing the hardware switch must not unlock motion by itself.
// The observed level alone does not unlock motion. The monitor must first
// prove the old controller operation is fully quiescent.
state.observe(SafetyCondition::Normal);
CHECK_TRUE(state.snapshot().latched);
CHECK_TRUE(!state.tryPermit().has_value());
CHECK_TRUE(!shouldAutoRecoverHardwareEmergencyStop(
state.snapshot(), false, 0));
CHECK_TRUE(shouldAutoRecoverHardwareEmergencyStop(
state.snapshot(), true, 0));
const auto recovery = state.beginRecovery(state.snapshot().epoch);
CHECK_TRUE(recovery.has_value());
@ -60,21 +65,54 @@ int main()
const auto retry = state.beginRecovery(state.snapshot().epoch);
CHECK_TRUE(retry.has_value());
CHECK_TRUE(state.completeRecovery(*retry, true, true, true));
CHECK_TRUE(state.completeHardwareEmergencyStopRecovery(
*retry, true, true));
const auto recovered_permit = state.tryPermit();
CHECK_TRUE(recovered_permit.has_value());
CHECK_TRUE(state.validate(*recovered_permit));
// Releasing a real E-stop must never clear a software-triggered stop that
// was latched while the hardware input was active.
state.observe(SafetyCondition::RobotEmergencyStop);
state.observe(SafetyCondition::SoftwareEmergencyStop);
// The hardware monitor continues publishing the physical E-stop level
// until the switch is released. It must not overwrite the software latch.
state.observe(SafetyCondition::RobotEmergencyStop);
state.observe(SafetyCondition::Normal);
CHECK_TRUE(!shouldAutoRecoverHardwareEmergencyStop(
state.snapshot(), true, 0));
CHECK_TRUE(state.snapshot().software_emergency_stop_latched);
CHECK_TRUE(state.snapshot().latched_reason ==
SafetyCondition::SoftwareEmergencyStop);
const auto software_recovery = state.beginRecovery(
state.snapshot().epoch);
CHECK_TRUE(software_recovery.has_value());
CHECK_TRUE(!state.completeHardwareEmergencyStopRecovery(
*software_recovery, true, true));
state.failRecovery(*software_recovery);
const auto explicit_software_recovery = state.beginRecovery(
state.snapshot().epoch);
CHECK_TRUE(explicit_software_recovery.has_value());
CHECK_TRUE(state.completeRecovery(
*explicit_software_recovery, true, true, true));
CHECK_TRUE(!state.snapshot().software_emergency_stop_latched);
// A new safety event invalidates an in-flight recovery token.
state.observe(SafetyCondition::ProtectiveStop);
state.observe(SafetyCondition::Reduced);
const auto stale_recovery = state.beginRecovery(
state.snapshot().epoch);
CHECK_TRUE(stale_recovery.has_value());
CHECK_TRUE(!state.completeHardwareEmergencyStopRecovery(
*stale_recovery, true, true));
state.failRecovery(*stale_recovery);
const auto explicit_recovery = state.beginRecovery(
state.snapshot().epoch);
CHECK_TRUE(explicit_recovery.has_value());
state.observe(SafetyCondition::SafeguardStop);
state.observe(SafetyCondition::Normal);
CHECK_TRUE(!state.completeRecovery(
*stale_recovery, true, true, true));
*explicit_recovery, true, true, true));
CHECK_TRUE(state.snapshot().latched);
// An old API call must not begin recovery for a newer safety event.

View File

@ -1,5 +1,6 @@
add_library(device_manager STATIC
src/device_factory.cpp
src/device_safety_adapters.cpp
src/device_manager.cpp
)
@ -7,6 +8,7 @@ target_include_directories(device_manager PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(device_manager PRIVATE
cmvr_es::proto
cmvr_es::safety_coordinator
cmvr_es::device::camera
cmvr_es::device::agv
cmvr_es::device::speaker

View File

@ -15,6 +15,7 @@
#include "device_factory.h"
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
#include "manager/safety/include/safety_coordinator.h"
namespace cmvr::device {
@ -47,6 +48,15 @@ namespace cmvr::device {
std::vector<DeviceInventoryEntry> inventorySnapshot() const;
DeviceManagerSnapshot snapshot() const;
safety::SafetyCoordinator& safetyCoordinator() noexcept
{
return *safety_coordinator_;
}
const safety::SafetyCoordinator& safetyCoordinator() const noexcept
{
return *safety_coordinator_;
}
std::string version() const;
std::string name() const;
std::string description() const;
@ -64,6 +74,7 @@ namespace cmvr::device {
std::unordered_map<std::string, DeviceRecord> devices_;
std::unordered_map<std::string, ManagedDeviceSnapshot> device_statuses_;
std::unique_ptr<DeviceFactory> dev_factory_;
std::unique_ptr<safety::SafetyCoordinator> safety_coordinator_;
bool initialized_{false};
explicit DeviceManager(const config::DeviceManagerConfig &cfg);
@ -76,6 +87,13 @@ namespace cmvr::device {
void update_device_status_(const std::string& device_id,
ManagedDeviceState state,
const std::string& error_message = {});
DeviceHealthSnapshot sample_device_health_(
const std::shared_ptr<AbstractDevice>& device) const;
void update_device_health_(const std::string& device_id,
DeviceHealthSnapshot health);
bool register_device_safety_(
const std::shared_ptr<AbstractDevice>& device,
const config::DeviceConfigEntry* config_entry = nullptr);
void stop_devices_(bool update_status = true);
};
} // cmvr

View File

@ -0,0 +1,18 @@
#pragma once
#include <chrono>
#include <memory>
#include "devices/abstract_device.h"
#include "manager/safety/include/safety_participant.h"
namespace cmvr::device {
safety::DeviceSafetyRegistration makeDeviceSafetyRegistration(
const std::shared_ptr<AbstractDevice>& device,
std::chrono::milliseconds configured_maximum_age =
std::chrono::milliseconds::zero(),
std::chrono::milliseconds configured_stop_timeout =
std::chrono::milliseconds::zero());
} // namespace cmvr::device

View File

@ -4,6 +4,7 @@
//
#include "../include/device_manager.h"
#include "../include/device_safety_adapters.h"
#include <algorithm>
#include <chrono>
@ -35,6 +36,68 @@ namespace {
using GroupJointSelection = std::unordered_map<std::string, std::unordered_set<std::string>>;
using MotorJointSelections = std::unordered_map<std::string, GroupJointSelection>;
constexpr std::size_t kMaxDeviceErrorLength = 512;
constexpr auto kSafetyStartupValidationTimeout = std::chrono::seconds(2);
cmvr::safety::SafetyCoordinatorConfig safetyConfigFrom(
const cmvr::config::DeviceManagerConfig& config)
{
cmvr::safety::SafetyCoordinatorConfig result;
if (!config.has_safety()) {
result.enforcement_mode = cmvr::safety::EnforcementMode::Shadow;
return result;
}
const auto& source = config.safety();
switch (source.mode()) {
case cmvr::config::SafetyCoordinatorConfig::LEGACY:
result.enforcement_mode = cmvr::safety::EnforcementMode::Legacy;
break;
case cmvr::config::SafetyCoordinatorConfig::ENFORCE_SELECTED:
result.enforcement_mode =
cmvr::safety::EnforcementMode::EnforceSelected;
break;
case cmvr::config::SafetyCoordinatorConfig::ENFORCE_ALL:
result.enforcement_mode = cmvr::safety::EnforcementMode::EnforceAll;
break;
case cmvr::config::SafetyCoordinatorConfig::SHADOW:
case cmvr::config::SafetyCoordinatorConfig::ENFORCEMENT_MODE_UNSPECIFIED:
default:
result.enforcement_mode = cmvr::safety::EnforcementMode::Shadow;
break;
}
for (const auto& id : source.enforced_device_ids()) {
if (!id.empty()) {
result.enforced_device_ids.insert(id);
}
}
for (const auto& entry : config.devices()) {
if (entry.safety_enforce() && !entry.id().empty()) {
result.enforced_device_ids.insert(entry.id());
}
}
if (source.stop_all_timeout_ms() != 0) {
result.stop_all_timeout =
std::chrono::milliseconds(source.stop_all_timeout_ms());
}
if (source.recovery_timeout_ms() != 0) {
result.recovery_timeout =
std::chrono::milliseconds(source.recovery_timeout_ms());
}
if (source.command_ledger_result_capacity() != 0) {
result.command_ledger.result_capacity =
source.command_ledger_result_capacity();
}
if (source.command_ledger_total_id_capacity() != 0) {
result.command_ledger.total_id_capacity =
source.command_ledger_total_id_capacity();
}
if (source.event_history_capacity() != 0) {
result.event_history_capacity = source.event_history_capacity();
}
result.fail_startup_on_missing_control_capability =
source.fail_startup_on_missing_control_capability();
return result;
}
std::uint64_t unixTimeMs() noexcept
{
@ -170,10 +233,12 @@ std::shared_ptr<DeviceManager> DeviceManager::instance_ = nullptr;
std::mutex DeviceManager::init_mutex_;
DeviceManager::DeviceManager(const config::DeviceManagerConfig& cfg) {
cfg_ = cfg;
dev_factory_ = std::make_unique<DeviceFactory>();
DeviceManager::DeviceManager(const config::DeviceManagerConfig& cfg)
: cfg_(cfg),
dev_factory_(std::make_unique<DeviceFactory>()),
safety_coordinator_(std::make_unique<safety::SafetyCoordinator>(
safetyConfigFrom(cfg)))
{
initialize_device_statuses_();
logSection("Device Plan");
log_device_plan_();
@ -248,6 +313,7 @@ bool DeviceManager::start(){
bool started = false;
std::string error_message;
try {
(void)safety_coordinator_->advanceDeviceGeneration(id);
started = device->start();
if (!started) {
error_message = "device start returned false: " + id;
@ -264,6 +330,8 @@ bool DeviceManager::start(){
<< " threw an unknown exception";
}
if (started) {
const auto health = sample_device_health_(device);
update_device_health_(id, health);
update_device_status_(id, ManagedDeviceState::Running);
CMVR_LOG(INFO) << "[DeviceManager]: Start device " << id << " Success";
} else {
@ -273,13 +341,29 @@ bool DeviceManager::start(){
all_started = false;
}
}
if (all_started) {
const auto coverage = safety_coordinator_->validateStartupCoverage(
safety::SafetyClock::now() + kSafetyStartupValidationTimeout);
if (!coverage.ready) {
all_started = false;
for (const auto& issue : coverage.issues) {
CMVR_LOG(ERROR)
<< "[DeviceManager]: Safety startup coverage failed"
<< ", target=" << issue.target_id
<< ", reason=" << safety::toString(issue.reason)
<< ", detail=" << issue.detail;
}
}
}
if (!all_started) {
CMVR_LOG(ERROR) << "[DeviceManager]: At least one enabled device failed "
"to start; stopping all devices";
CMVR_LOG(ERROR) << "[DeviceManager]: Device or safety startup failed; "
"stopping all devices";
// Rollback is a physical cleanup operation. Preserve the start
// results in the status table so the failure is diagnosable; an
// explicit stop() records Stopped/Error transitions.
stop_devices_(false);
} else {
safety_coordinator_->markStartupComplete();
}
return all_started;
}
@ -338,12 +422,18 @@ void DeviceManager::stop_devices_(const bool update_status) {
update_device_status_(id, ManagedDeviceState::Stopped);
}
CMVR_LOG(INFO) << "[DeviceManager]: Stop device " << id << " Success";
safety_coordinator_->updateDeviceRuntimeState(
id, ManagedDeviceState::Stopped,
sample_device_health_(device));
} else {
if (update_status) {
update_device_status_(
id, ManagedDeviceState::Error, error_message);
}
CMVR_LOG(ERROR) << "[DeviceManager]: Stop device " << id << " Failed";
safety_coordinator_->updateDeviceRuntimeState(
id, ManagedDeviceState::Error,
{DeviceHealthState::Fault, error_message});
}
}
}
@ -445,6 +535,16 @@ void DeviceManager::registerDevice(const std::string& device_id,
devices_.emplace(record.id, std::move(record));
device_statuses_[device_id] = std::move(status);
}
if (!register_device_safety_(device)) {
update_device_status_(
device_id, ManagedDeviceState::Error,
"failed to register device safety capability: " + device_id);
CMVR_LOG(ERROR) << "[DeviceManager]: Failed to register device safety "
"capability, id=" << device_id;
} else {
update_device_health_(
device_id, sample_device_health_(device));
}
CMVR_LOG(INFO) << "[DeviceManager]: Register device success"
<< ", id=" << device_id
<< ", type=" << device->typeName()
@ -508,6 +608,8 @@ void DeviceManager::update_device_status_(
const std::string& device_id,
const ManagedDeviceState state,
const std::string& error_message)
{
DeviceHealthSnapshot health;
{
std::unique_lock lock(devices_mutex_);
auto& status = device_statuses_[device_id];
@ -530,27 +632,20 @@ void DeviceManager::update_device_status_(
: error_message)
: std::string{};
status.status_updated_at_unix_ms = unixTimeMs();
health = status.health;
}
safety_coordinator_->updateDeviceRuntimeState(device_id, state, health);
}
DeviceManagerSnapshot DeviceManager::snapshot() const
{
struct SnapshotSource {
ManagedDeviceSnapshot status;
std::shared_ptr<AbstractDevice> device;
};
std::vector<SnapshotSource> sources;
std::vector<ManagedDeviceSnapshot> sources;
{
std::shared_lock lock(devices_mutex_);
sources.reserve(device_statuses_.size());
for (const auto& [id, stored_status] : device_statuses_) {
SnapshotSource source;
source.status = stored_status;
const auto device_it = devices_.find(id);
if (device_it != devices_.end()) {
source.device = device_it->second.device;
}
sources.push_back(std::move(source));
(void)id;
sources.push_back(stored_status);
}
}
@ -561,36 +656,22 @@ DeviceManagerSnapshot DeviceManager::snapshot() const
result.devices.reserve(sources.size());
for (auto& source : sources) {
if (source.device) {
try {
source.status.health = source.device->healthSnapshot();
} catch (const std::exception& error) {
source.status.health.state = DeviceHealthState::Fault;
source.status.health.error_message = error.what();
} catch (...) {
source.status.health.state = DeviceHealthState::Fault;
source.status.health.error_message =
"device health snapshot threw an unknown exception";
}
}
source.status.health.error_message =
truncateDeviceError(source.status.health.error_message);
source.health.error_message =
truncateDeviceError(source.health.error_message);
const bool lifecycle_error =
source.status.state == ManagedDeviceState::Error;
source.state == ManagedDeviceState::Error;
const bool health_error =
source.status.health.state == DeviceHealthState::Degraded ||
source.status.health.state == DeviceHealthState::Fault;
source.status.abnormal = lifecycle_error || health_error;
if (source.status.error_message.empty()) {
source.status.error_message =
source.status.health.error_message;
source.health.state == DeviceHealthState::Degraded ||
source.health.state == DeviceHealthState::Fault;
source.abnormal = lifecycle_error || health_error;
if (source.error_message.empty()) {
source.error_message = source.health.error_message;
}
source.status.error_message =
truncateDeviceError(source.status.error_message);
if (source.status.status_updated_at_unix_ms == 0) {
source.status.status_updated_at_unix_ms = unixTimeMs();
source.error_message = truncateDeviceError(source.error_message);
if (source.status_updated_at_unix_ms == 0) {
source.status_updated_at_unix_ms = unixTimeMs();
}
result.devices.push_back(std::move(source.status));
result.devices.push_back(std::move(source));
}
std::sort(result.devices.begin(), result.devices.end(),
@ -601,6 +682,78 @@ DeviceManagerSnapshot DeviceManager::snapshot() const
return result;
}
DeviceHealthSnapshot DeviceManager::sample_device_health_(
const std::shared_ptr<AbstractDevice>& device) const
{
if (!device) {
return {
DeviceHealthState::Fault,
"device health target is null"};
}
try {
auto health = device->healthSnapshot();
health.error_message = truncateDeviceError(health.error_message);
return health;
} catch (const std::exception& error) {
return {DeviceHealthState::Fault, truncateDeviceError(error.what())};
} catch (...) {
return {
DeviceHealthState::Fault,
"device health snapshot threw an unknown exception"};
}
}
void DeviceManager::update_device_health_(
const std::string& device_id,
DeviceHealthSnapshot health)
{
ManagedDeviceState lifecycle = ManagedDeviceState::Unknown;
{
std::unique_lock lock(devices_mutex_);
auto& status = device_statuses_[device_id];
status.health = std::move(health);
lifecycle = status.state;
const bool health_error =
status.health.state == DeviceHealthState::Degraded ||
status.health.state == DeviceHealthState::Fault;
status.abnormal =
status.state == ManagedDeviceState::Error || health_error;
if (status.state != ManagedDeviceState::Error) {
status.error_message = status.health.error_message;
}
status.status_updated_at_unix_ms = unixTimeMs();
health = status.health;
}
safety_coordinator_->updateDeviceRuntimeState(
device_id, lifecycle, std::move(health));
}
bool DeviceManager::register_device_safety_(
const std::shared_ptr<AbstractDevice>& device,
const config::DeviceConfigEntry* config_entry)
{
auto maximum_age = std::chrono::milliseconds::zero();
auto stop_timeout = std::chrono::milliseconds::zero();
if (config_entry) {
if (config_entry->maximum_safety_snapshot_age_ms() != 0) {
maximum_age = std::chrono::milliseconds(
config_entry->maximum_safety_snapshot_age_ms());
}
if (config_entry->safety_stop_timeout_ms() != 0) {
stop_timeout = std::chrono::milliseconds(
config_entry->safety_stop_timeout_ms());
}
}
auto registration = makeDeviceSafetyRegistration(
device, maximum_age, stop_timeout);
if (registration.descriptor.device_id.empty()) {
return false;
}
const bool registered =
safety_coordinator_->registerDevice(std::move(registration));
return registered;
}
std::string DeviceManager::version() const {
return cfg_.version().empty() ? "1.0" : cfg_.version();
}
@ -914,6 +1067,8 @@ bool DeviceManager::init_devices_() {
<< ", type=" << record.type_name
<< ", kind=" << toString(record.kind)
<< ", config_file=" << entry.config_file();
const auto registered_device = record.device;
std::string registered_id;
{
std::unique_lock lock(devices_mutex_);
const auto id = record.id;
@ -941,6 +1096,23 @@ bool DeviceManager::init_devices_() {
status.abnormal = false;
status.error_message.clear();
status.status_updated_at_unix_ms = unixTimeMs();
registered_id = id;
}
if (!register_device_safety_(registered_device, &entry)) {
update_device_status_(
registered_id, ManagedDeviceState::Error,
"failed to register device safety capability: " +
registered_id);
CMVR_LOG(ERROR)
<< "[DeviceManager]: Failed to register device safety "
"capability"
<< ", id=" << registered_id
<< ", kind=" << toString(registered_device->kind());
all_initialized = false;
} else {
update_device_health_(
registered_id, sample_device_health_(registered_device));
}
}
return all_initialized;

File diff suppressed because it is too large Load Diff

View File

@ -121,4 +121,33 @@ TEST_F(DeviceManagerLifecycleTest,
EXPECT_EQ(device->stop_calls, 1);
}
TEST_F(DeviceManagerLifecycleTest,
EnforceSelectedCannotStartWithMissingConfiguredTarget)
{
cmvr::config::DeviceManagerConfig config;
auto* safety = config.mutable_safety();
safety->set_mode(
cmvr::config::SafetyCoordinatorConfig::ENFORCE_SELECTED);
safety->add_enforced_device_ids("missing-arm");
auto& manager = cmvr::device::DeviceManager::getInstance(config);
ASSERT_TRUE(manager.initialized());
EXPECT_FALSE(manager.start());
EXPECT_EQ(
manager.safetyCoordinator().snapshot().system_state,
cmvr::safety::SystemAdmissionState::Starting);
}
TEST_F(DeviceManagerLifecycleTest,
EnforceSelectedCannotSilentlyCoverNoDevices)
{
cmvr::config::DeviceManagerConfig config;
config.mutable_safety()->set_mode(
cmvr::config::SafetyCoordinatorConfig::ENFORCE_SELECTED);
auto& manager = cmvr::device::DeviceManager::getInstance(config);
ASSERT_TRUE(manager.initialized());
EXPECT_FALSE(manager.start());
}
} // namespace

View File

@ -434,7 +434,7 @@ bool testConcurrentSnapshotAndRegistration()
return true;
}
bool testInventorySnapshotDoesNotWaitForDeviceHealth()
bool testManagerSnapshotsDoNotWaitForDeviceHealth()
{
DeviceManager::destroyInstance();
cmvr::config::DeviceManagerConfig config;
@ -443,15 +443,26 @@ bool testInventorySnapshotDoesNotWaitForDeviceHealth()
std::make_shared<BlockingHealthDevice>("blocked_health_arm");
auto other_device =
std::make_shared<FakeDevice>("a_camera", DeviceKind::Camera);
manager.registerDevice(blocking_device);
manager.registerDevice(other_device);
auto health_future = std::async(std::launch::async, [&manager] {
return manager.snapshot();
auto registration_future = std::async(
std::launch::async, [&manager, blocking_device] {
manager.registerDevice(blocking_device);
});
if (!blocking_device->waitForHealthCall(std::chrono::seconds(2))) {
blocking_device->releaseHealthCall();
health_future.wait();
registration_future.wait();
return false;
}
auto snapshot_future = std::async(std::launch::async, [&manager] {
return manager.snapshot();
});
if (snapshot_future.wait_for(std::chrono::milliseconds(250)) !=
std::future_status::ready) {
blocking_device->releaseHealthCall();
snapshot_future.wait();
registration_future.wait();
return false;
}
@ -462,12 +473,18 @@ bool testInventorySnapshotDoesNotWaitForDeviceHealth()
std::future_status::ready) {
blocking_device->releaseHealthCall();
inventory_future.wait();
health_future.wait();
registration_future.wait();
return false;
}
const auto snapshot = snapshot_future.get();
const auto inventory = inventory_future.get();
const bool inventory_valid =
const auto* blocked_status =
findDevice(snapshot, "blocked_health_arm");
const bool snapshots_valid =
blocked_status != nullptr &&
blocked_status->state == ManagedDeviceState::Registered &&
blocked_status->health.state == DeviceHealthState::Unknown &&
inventory.size() == 2 && isSorted(inventory) &&
inventory[0].id == "a_camera" &&
inventory[0].kind == DeviceKind::Camera &&
@ -478,8 +495,8 @@ bool testInventorySnapshotDoesNotWaitForDeviceHealth()
blocking_device->health_calls.load() == 1;
blocking_device->releaseHealthCall();
health_future.get();
return inventory_valid && blocking_device->health_calls.load() == 1;
registration_future.get();
return snapshots_valid && blocking_device->health_calls.load() == 1;
}
} // namespace
@ -491,7 +508,7 @@ int main()
testCategoryHealthAdapters() &&
testConfiguredAndDynamicSnapshots() &&
testConcurrentSnapshotAndRegistration() &&
testInventorySnapshotDoesNotWaitForDeviceHealth();
testManagerSnapshotsDoNotWaitForDeviceHealth();
DeviceManager::destroyInstance();
return success ? 0 : 1;
}

View File

@ -39,8 +39,36 @@ if(NOT CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
cmvr_es::common
cmvr_es::proto
cmvr_es::logging
cmvr_es::safety_coordinator
)
add_library(cmvr_es::device_media_source_adapter ALIAS device_media_source_adapter)
if(BUILD_TESTING)
add_executable(device_media_source_adapter_test
tests/device_media_source_adapter_test.cpp
)
target_compile_features(device_media_source_adapter_test PRIVATE cxx_std_17)
target_link_libraries(device_media_source_adapter_test
PRIVATE
cmvr_es::device_media_source_adapter
gtest
gtest_main
)
add_test(
NAME device_media_source_adapter_test
COMMAND device_media_source_adapter_test
)
set(_device_media_source_adapter_test_environment
"LD_LIBRARY_PATH=${CMVR_TEST_EXTERNAL_LIBRARY_PATH}")
if(CMVR_TEST_SYSTEM_LIBSTDCXX)
list(APPEND _device_media_source_adapter_test_environment
"LD_PRELOAD=${CMVR_TEST_SYSTEM_LIBSTDCXX}")
endif()
set_tests_properties(device_media_source_adapter_test PROPERTIES
ENVIRONMENT
"${_device_media_source_adapter_test_environment}"
)
endif()
endif()
option(CMVR_MEDIA_SOURCE_HUB_BUILD_TESTS

View File

@ -10,6 +10,7 @@
#include "devices/camera/abstract_camera.h"
#include "devices/microphone/abstract_microphone.h"
#include "manager/media_source_hub/include/media_source_hub.h"
#include "manager/safety/include/safety_coordinator.h"
namespace cmvr::media {
@ -19,6 +20,13 @@ MediaSourceHub& globalMediaSourceHub();
std::string cameraColorTrackId(const std::string& device_id);
std::string microphoneTrackId(const std::string& device_id);
// Acquires the Coordinator's Sensor/StartActivity lane and performs the
// device endpoint's final hardware check. Keep the returned guard alive until
// the operation which can start the physical media producer has returned.
safety::DispatchGuard beginMediaSourceStartDispatch(
safety::SafetyCoordinator& coordinator,
const std::string& device_id);
// Registration is idempotent for an already registered track. The adapter owns a
// short-lived pump thread and one startStreaming()/stopStreaming() lease only while
// at least one Hub subscription is active. It ensures start() succeeds but deliberately

View File

@ -20,6 +20,8 @@
namespace cmvr::media {
namespace {
std::atomic<std::uint64_t> media_start_sequence{0};
std::string normalizedCodec(std::string codec) {
codec.erase(
std::remove_if(codec.begin(), codec.end(), [](const unsigned char c) {
@ -630,6 +632,43 @@ std::string microphoneTrackId(const std::string& device_id) {
return device_id + "/audio/main";
}
safety::DispatchGuard beginMediaSourceStartDispatch(
safety::SafetyCoordinator& coordinator,
const std::string& device_id)
{
safety::AdmissionRequest request;
request.command = {
"cmvr.internal.MediaSourceHub/StartSource",
safety::CommandIntent::StartActivity,
safety::SafetyPolicyFamily::Sensor,
true,
false};
request.actor.principal_id = "internal:media-source-hub";
request.actor.authenticated = true;
request.command_id = "media-source-start:" + device_id + ':' +
std::to_string(
media_start_sequence.fetch_add(1, std::memory_order_relaxed) + 1U);
request.device_id = device_id;
auto admission = coordinator.admit(request);
if (!admission.permit.has_value()) {
CMVR_LOG(WARNING)
<< "[DeviceMediaSourceAdapter] Media source admission rejected for "
<< device_id << ": " << safety::toString(admission.decision.reason)
<< " (" << admission.decision.detail << ')';
return {};
}
auto dispatch = coordinator.beginDispatch(*admission.permit);
if (!dispatch.acquired()) {
CMVR_LOG(WARNING)
<< "[DeviceMediaSourceAdapter] Media source final check rejected for "
<< device_id << ": "
<< safety::toString(dispatch.hardwareCheck().reason) << " ("
<< dispatch.hardwareCheck().detail << ')';
}
return dispatch;
}
bool ensureCameraMediaSource(
MediaSourceHub& hub,
const std::shared_ptr<device::AbstractCamera>& camera,

View File

@ -0,0 +1,113 @@
#include "manager/media_source_hub/include/device_media_source_adapter.h"
#include <atomic>
#include <chrono>
#include <memory>
#include <string>
#include <gtest/gtest.h>
#include "manager/safety/include/device_safety_endpoint.h"
namespace cmvr::media {
namespace {
class FakeSensorEndpoint final : public safety::DeviceSafetyEndpoint {
public:
explicit FakeSensorEndpoint(std::string device_id)
{
descriptor_.device_id = std::move(device_id);
descriptor_.kind = device::DeviceKind::Camera;
descriptor_.default_policy = safety::SafetyPolicyFamily::Sensor;
descriptor_.maximum_snapshot_age = std::chrono::seconds(1);
descriptor_.supports_active_refresh = true;
}
safety::DeviceSafetyDescriptor descriptor() const override
{
return descriptor_;
}
void bindPublisher(safety::SafetySnapshotPublisher publisher) override
{
publisher_ = std::move(publisher);
}
void requestSafetyRefresh() noexcept override
{
if (!publisher_) {
return;
}
safety::DeviceSafetySnapshot snapshot;
snapshot.device_id = descriptor_.device_id;
snapshot.condition = safety::SafetyCondition::Nominal;
snapshot.device_generation = 1;
snapshot.sample_sequence = ++sequence_;
snapshot.observed_at = safety::SafetyClock::now();
snapshot.connected = safety::TriState::True;
snapshot.operational_ready = safety::TriState::True;
snapshot.quiescent = safety::TriState::True;
snapshot.motion_active = safety::TriState::False;
snapshot.actuator_enabled = safety::TriState::False;
snapshot.emergency_stop_active = safety::TriState::False;
snapshot.protective_stop_active = safety::TriState::False;
snapshot.fault_active = safety::TriState::False;
(void)publisher_(std::move(snapshot));
}
safety::HardwareCheckResult validateBeforeDispatch(
const safety::AdmissionPermit& permit) override
{
++hardware_checks;
last_intent = permit.intent;
return {true, safety::SafetyReason::None, {}};
}
safety::RecoveryCheckResult reconcileAdmissionState(
const safety::RecoveryContext&) override
{
return {true, safety::SafetyReason::None, {}};
}
std::atomic<int> hardware_checks{0};
safety::CommandIntent last_intent{safety::CommandIntent::Observe};
private:
safety::DeviceSafetyDescriptor descriptor_;
safety::SafetySnapshotPublisher publisher_;
std::atomic<std::uint64_t> sequence_{0};
};
TEST(DeviceMediaSourceAdapterTest,
SensorStartUsesFinalCheckAndQuarantineRejectsRestart)
{
safety::SafetyCoordinatorConfig config;
config.enforcement_mode = safety::EnforcementMode::EnforceAll;
safety::SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeSensorEndpoint>("camera");
ASSERT_TRUE(coordinator.registerDevice(
{endpoint->descriptor(), endpoint, {}}));
endpoint->requestSafetyRefresh();
coordinator.updateDeviceRuntimeState(
"camera",
device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
{
auto dispatch = beginMediaSourceStartDispatch(coordinator, "camera");
ASSERT_TRUE(dispatch.acquired());
EXPECT_EQ(endpoint->hardware_checks.load(), 1);
EXPECT_EQ(endpoint->last_intent, safety::CommandIntent::StartActivity);
}
coordinator.quarantineDevice(
"camera", safety::SafetyReason::OutcomeUnknown,
"uncertain-media-start");
auto rejected = beginMediaSourceStartDispatch(coordinator, "camera");
EXPECT_FALSE(rejected.acquired());
EXPECT_EQ(endpoint->hardware_checks.load(), 1);
}
} // namespace
} // namespace cmvr::media

View File

@ -0,0 +1,65 @@
add_library(safety_coordinator STATIC
src/command_ledger.cpp
src/safety_coordinator.cpp
src/safety_reason.cpp
src/safety_snapshot_store.cpp
)
target_include_directories(safety_coordinator PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/cmvr-es
)
target_link_libraries(safety_coordinator PUBLIC
cmvr_es::control_authority
)
add_library(cmvr_es::safety_coordinator ALIAS safety_coordinator)
install(TARGETS safety_coordinator LIBRARY DESTINATION lib)
if(BUILD_TESTING)
add_executable(safety_snapshot_store_test
tests/safety_snapshot_store_test.cpp
)
target_link_libraries(safety_snapshot_store_test PRIVATE
cmvr_es::safety_coordinator
gtest
gtest_main
pthread
)
add_test(
NAME safety_snapshot_store_test
COMMAND safety_snapshot_store_test
)
set_tests_properties(safety_snapshot_store_test PROPERTIES TIMEOUT 10)
add_executable(command_ledger_test
tests/command_ledger_test.cpp
)
target_link_libraries(command_ledger_test PRIVATE
cmvr_es::safety_coordinator
gtest
gtest_main
pthread
)
add_test(
NAME command_ledger_test
COMMAND command_ledger_test
)
set_tests_properties(command_ledger_test PROPERTIES TIMEOUT 10)
add_executable(safety_coordinator_test
tests/safety_coordinator_test.cpp
)
target_link_libraries(safety_coordinator_test PRIVATE
cmvr_es::safety_coordinator
gtest
gtest_main
pthread
)
add_test(
NAME safety_coordinator_test
COMMAND safety_coordinator_test
)
set_tests_properties(safety_coordinator_test PROPERTIES TIMEOUT 15)
endif()

View File

@ -0,0 +1,130 @@
#pragma once
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include "manager/safety/include/safety_types.h"
namespace cmvr::safety {
struct CommandKey {
std::string effective_principal_id;
std::string command_id;
bool operator==(const CommandKey& other) const noexcept
{
return effective_principal_id == other.effective_principal_id &&
command_id == other.command_id;
}
};
struct CommandOutcome {
CommandLifecycle lifecycle{CommandLifecycle::Failed};
SafetyReason reason{SafetyReason::InternalError};
std::string detail;
std::string serialized_response;
std::uint64_t safety_epoch{0};
std::uint64_t device_generation{0};
bool hardware_submission_possible{false};
};
enum class CommandReservationStatus {
AcceptedNew,
JoinedInFlight,
CachedResult,
CommandIdConflict,
ResultEvicted,
LedgerExhausted,
Invalid,
};
class CommandLedger final {
private:
struct State;
public:
struct Config {
std::size_t result_capacity{4096};
std::size_t total_id_capacity{256U * 1024U};
};
class Ticket final {
public:
Ticket() = default;
bool valid() const noexcept { return state_ != nullptr; }
private:
friend class CommandLedger;
explicit Ticket(std::shared_ptr<State> state)
: state_(std::move(state))
{
}
std::shared_ptr<State> state_;
};
struct Reservation {
CommandReservationStatus status{CommandReservationStatus::Invalid};
Ticket ticket;
std::optional<CommandOutcome> cached_outcome;
};
CommandLedger();
explicit CommandLedger(Config config);
Reservation reserve(CommandKey key, std::string payload_hash);
bool setLifecycle(const Ticket& ticket,
CommandLifecycle lifecycle,
std::uint64_t safety_epoch = 0,
std::uint64_t device_generation = 0,
bool hardware_submission_possible = false);
bool complete(const Ticket& ticket, CommandOutcome outcome);
std::optional<CommandOutcome> wait(
const Ticket& ticket,
SafetyClock::time_point deadline = SafetyClock::time_point::max()) const;
std::optional<CommandOutcome> lookup(
const CommandKey& key,
const std::string& payload_hash) const;
std::size_t acceptedIdCount() const;
std::size_t liveRecordCount() const;
std::size_t retiredIdCount() const;
private:
struct KeyHash {
std::size_t operator()(const CommandKey& key) const noexcept;
};
struct State {
CommandKey key;
std::string payload_hash;
mutable std::mutex mutex;
mutable std::condition_variable condition;
CommandLifecycle lifecycle{CommandLifecycle::Reserved};
std::optional<CommandOutcome> outcome;
std::uint64_t safety_epoch{0};
std::uint64_t device_generation{0};
bool hardware_submission_possible{false};
bool terminal{false};
};
static bool validKey_(const CommandKey& key) noexcept;
void trimTerminalResultsLocked_();
const Config config_;
mutable std::mutex mutex_;
std::unordered_map<CommandKey, std::shared_ptr<State>, KeyHash> records_;
std::unordered_map<CommandKey, std::string, KeyHash> retired_ids_;
std::vector<CommandKey> terminal_order_;
std::size_t terminal_result_count_{0};
};
const char* toString(CommandReservationStatus status) noexcept;
} // namespace cmvr::safety

View File

@ -0,0 +1,39 @@
#pragma once
#include <functional>
#include <memory>
#include "manager/safety/include/safety_types.h"
namespace cmvr::safety {
using SafetySnapshotPublisher =
std::function<bool(DeviceSafetySnapshot)>;
class DeviceSafetyEndpoint {
public:
virtual ~DeviceSafetyEndpoint() = default;
virtual DeviceSafetyDescriptor descriptor() const = 0;
virtual void bindPublisher(SafetySnapshotPublisher publisher) = 0;
virtual void requestSafetyRefresh() noexcept = 0;
// Called after a backend/session restart. Implementations must publish
// subsequent samples with this generation or remain fail-closed.
virtual void onDeviceGenerationChanged(
std::uint64_t generation) noexcept
{
(void)generation;
}
virtual HardwareCheckResult validateBeforeDispatch(
const AdmissionPermit& permit) = 0;
virtual RecoveryCheckResult reconcileAdmissionState(
const RecoveryContext& context) = 0;
};
class DeviceSafetyEndpointProvider {
public:
virtual ~DeviceSafetyEndpointProvider() = default;
virtual std::shared_ptr<DeviceSafetyEndpoint> safetyEndpoint() = 0;
};
} // namespace cmvr::safety

View File

@ -0,0 +1,225 @@
#pragma once
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>
#include "manager/safety/include/command_ledger.h"
#include "manager/safety/include/device_safety_endpoint.h"
#include "manager/safety/include/safety_participant.h"
#include "manager/safety/include/safety_snapshot_store.h"
namespace cmvr::safety {
struct SafetyCoordinatorConfig {
EnforcementMode enforcement_mode{EnforcementMode::Shadow};
std::unordered_set<std::string> enforced_device_ids;
std::chrono::milliseconds stop_all_timeout{15000};
std::chrono::milliseconds recovery_timeout{10000};
CommandLedger::Config command_ledger;
std::size_t event_history_capacity{2048};
bool fail_startup_on_missing_control_capability{false};
};
struct AdmissionResult {
AdmissionDecision decision;
std::optional<AdmissionPermit> permit;
};
struct StartupCoverageIssue {
std::string target_id;
SafetyReason reason{SafetyReason::None};
std::string detail;
};
struct StartupCoverageResult {
bool ready{false};
std::vector<StartupCoverageIssue> issues;
};
struct DeviceSafetyStateView {
DeviceSafetyDescriptor descriptor;
SafetySnapshotView safety;
device::ManagedDeviceState lifecycle{
device::ManagedDeviceState::Unknown};
device::DeviceHealthSnapshot health;
DeviceAdmissionState admission_state{DeviceAdmissionState::Observing};
std::vector<SafetyBlocker> blockers;
};
struct ParticipantResultView {
bool recorded{false};
bool success{false};
SafetyReason reason{SafetyReason::None};
std::string detail;
};
struct ParticipantSafetyStateView {
ParticipantDescriptor descriptor;
bool registered{false};
bool barrier_active{false};
bool barrier_retained{false};
std::string operation_id;
std::uint64_t safety_epoch{0};
ParticipantResultView last_request;
ParticipantResultView last_verify;
ParticipantResultView last_release;
};
struct SafetyCoordinatorSnapshot {
SystemAdmissionState system_state{SystemAdmissionState::Starting};
std::uint64_t safety_epoch{0};
std::string service_instance_id;
EnforcementMode enforcement_mode{EnforcementMode::Shadow};
std::string active_operation_id;
std::string active_operation_phase;
std::vector<DeviceSafetyStateView> devices;
std::vector<ParticipantSafetyStateView> participants;
std::vector<SafetyEvent> recent_events;
};
struct SafetyTargetResult {
std::string target_id;
bool success{false};
SafetyReason reason{SafetyReason::None};
std::string detail;
DeviceAdmissionState before_state{DeviceAdmissionState::Observing};
DeviceAdmissionState after_state{DeviceAdmissionState::Observing};
};
struct StopAllResult {
bool success{false};
std::string operation_id;
std::uint64_t previous_safety_epoch{0};
std::uint64_t current_safety_epoch{0};
SystemAdmissionState system_state{SystemAdmissionState::Starting};
std::vector<SafetyTargetResult> targets;
};
enum class RecoveryResultCode {
Recovered,
VerifiedButStillBlocked,
BlockerRemains,
EpochMismatch,
NothingToRecover,
TimedOut,
Failed,
};
struct RecoveryRequest {
std::string recovery_id;
std::vector<std::string> device_ids;
bool all_devices{false};
std::uint64_t expected_safety_epoch{0};
bool verify_only{true};
std::string reason;
SafetyClock::time_point deadline{SafetyClock::time_point::max()};
// Called only for a latch-clearing transaction, after hardware facts have
// been verified and before any software barrier is reconciled or released.
// A false result leaves admission latched.
std::function<bool()> authorize_clear;
};
struct RecoveryResult {
RecoveryResultCode result{RecoveryResultCode::Failed};
std::string recovery_id;
std::uint64_t previous_safety_epoch{0};
std::uint64_t current_safety_epoch{0};
SystemAdmissionState system_state{SystemAdmissionState::Starting};
std::vector<SafetyTargetResult> targets;
};
class SafetyCoordinator;
class DispatchGuard final {
public:
DispatchGuard() noexcept = default;
~DispatchGuard() noexcept;
DispatchGuard(DispatchGuard&& other) noexcept;
DispatchGuard& operator=(DispatchGuard&& other) noexcept;
DispatchGuard(const DispatchGuard&) = delete;
DispatchGuard& operator=(const DispatchGuard&) = delete;
bool acquired() const noexcept { return coordinator_ != nullptr; }
const HardwareCheckResult& hardwareCheck() const noexcept
{
return hardware_check_;
}
private:
friend class SafetyCoordinator;
DispatchGuard(SafetyCoordinator* coordinator,
std::string device_id,
HardwareCheckResult hardware_check) noexcept;
void reset_() noexcept;
SafetyCoordinator* coordinator_{nullptr};
std::string device_id_;
HardwareCheckResult hardware_check_;
};
class SafetyCoordinator final {
public:
explicit SafetyCoordinator(SafetyCoordinatorConfig config = {});
~SafetyCoordinator();
SafetyCoordinator(const SafetyCoordinator&) = delete;
SafetyCoordinator& operator=(const SafetyCoordinator&) = delete;
bool registerDevice(DeviceSafetyRegistration registration);
bool unregisterDevice(const std::string& device_id);
bool registerParticipant(std::shared_ptr<SafetyParticipant> participant);
bool unregisterParticipant(const std::string& participant_id);
void updateDeviceRuntimeState(
const std::string& device_id,
device::ManagedDeviceState lifecycle,
device::DeviceHealthSnapshot health = {});
bool publishSafetySnapshot(DeviceSafetySnapshot snapshot);
std::optional<std::uint64_t> advanceDeviceGeneration(
const std::string& device_id);
StartupCoverageResult validateStartupCoverage(
SafetyClock::time_point deadline);
void markStartupComplete();
void beginShutdown() noexcept;
AdmissionDecision evaluate(const AdmissionRequest& request) const;
AdmissionResult admit(const AdmissionRequest& request);
// Lightweight session check. This validates the coordinator-owned epoch,
// generation, freshness, and admission state without calling the device
// endpoint or entering the hardware dispatch set.
HardwareCheckResult revalidatePermit(
const AdmissionPermit& permit) const;
DispatchGuard beginDispatch(const AdmissionPermit& permit);
void quarantineDevice(const std::string& device_id,
SafetyReason reason,
std::string operation_id = {});
StopAllResult stopAll(
std::string operation_id,
SafetyClock::time_point deadline = SafetyClock::time_point::max());
RecoveryResult recover(const RecoveryRequest& request);
SafetyCoordinatorSnapshot snapshot() const;
SafetySnapshotStore& snapshotStore() noexcept;
const SafetySnapshotStore& snapshotStore() const noexcept;
CommandLedger& commandLedger() noexcept;
const CommandLedger& commandLedger() const noexcept;
const std::string& serviceInstanceId() const noexcept;
const SafetyCoordinatorConfig& config() const noexcept;
private:
friend class DispatchGuard;
struct Impl;
void endDispatch_(const std::string& device_id) noexcept;
std::unique_ptr<Impl> impl_;
};
const char* toString(RecoveryResultCode value) noexcept;
} // namespace cmvr::safety

View File

@ -0,0 +1,88 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <memory>
#include <string>
#include "manager/safety/include/safety_types.h"
namespace cmvr::safety {
class DeviceSafetyEndpoint;
enum class ParticipantPhase {
Ingress,
Scheduler,
ControlSession,
Actuator,
PeripheralActivity,
Verification,
};
struct ParticipantDescriptor {
std::string participant_id;
ParticipantPhase phase{ParticipantPhase::Actuator};
bool required{true};
std::chrono::milliseconds timeout{5000};
};
struct SafetyOperationContext {
std::string operation_id;
std::uint64_t safety_epoch{0};
SafetyClock::time_point deadline{SafetyClock::time_point::max()};
};
struct BarrierToken {
std::string participant_id;
std::string operation_id;
std::uint64_t safety_epoch{0};
std::uint64_t generation{0};
bool valid() const noexcept
{
return !participant_id.empty() && !operation_id.empty() &&
safety_epoch != 0 && generation != 0;
}
};
struct ParticipantResult {
bool success{false};
SafetyReason reason{SafetyReason::StopUnconfirmed};
std::string detail;
};
class SafetyParticipant {
public:
virtual ~SafetyParticipant() = default;
virtual ParticipantDescriptor descriptor() const = 0;
virtual BarrierToken beginBarrier(
const SafetyOperationContext& context) = 0;
virtual ParticipantResult requestQuiesce(
const BarrierToken& token,
const SafetyOperationContext& context) = 0;
virtual ParticipantResult verifyQuiescent(
const BarrierToken& token,
const SafetyOperationContext& context) = 0;
virtual RecoveryCheckResult recoverAdmission(
const BarrierToken& token,
const RecoveryContext& context) = 0;
// Commits the participant's admission reopening. A failed commit must
// leave that participant fail-closed and be retryable through recovery.
virtual ParticipantResult releaseBarrier(
const BarrierToken& token) noexcept = 0;
};
class SafetyParticipantProvider {
public:
virtual ~SafetyParticipantProvider() = default;
virtual std::shared_ptr<SafetyParticipant> safetyParticipant() = 0;
};
struct DeviceSafetyRegistration {
DeviceSafetyDescriptor descriptor;
std::shared_ptr<DeviceSafetyEndpoint> endpoint;
std::shared_ptr<SafetyParticipant> participant;
};
} // namespace cmvr::safety

View File

@ -0,0 +1,48 @@
#pragma once
#include <string>
namespace cmvr::safety {
enum class SafetyReason {
None,
InvalidArgument,
Unauthenticated,
PermissionDenied,
RecoveryRpcDisabled,
DeviceNotFound,
DeviceUnavailable,
UnsupportedCommand,
SystemStarting,
SystemStopping,
SafetyLatched,
SafetyStateMissing,
SafetyStateStale,
HardwareUnsafe,
EmergencyStopActive,
ProtectiveStopActive,
DeviceDisconnected,
DeviceFault,
DeviceNotReady,
DeviceStillMoving,
ControlBusy,
GenerationMismatch,
CommandIdRequired,
CommandIdConflict,
ResultEvicted,
LedgerExhausted,
Backpressure,
DeadlineExceededBeforeDispatch,
OutcomeUnknown,
ParticipantTimeout,
StopUnconfirmed,
RecoveryEpochMismatch,
RecoveryReasonRequired,
RecoveryAuditFailed,
InternalError,
};
const char* toString(SafetyReason reason) noexcept;
bool retryWithSameCommandId(SafetyReason reason) noexcept;
} // namespace cmvr::safety

View File

@ -0,0 +1,54 @@
#pragma once
#include <condition_variable>
#include <optional>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "manager/safety/include/safety_types.h"
namespace cmvr::safety {
class SafetySnapshotStore final {
public:
bool registerDevice(const DeviceSafetyDescriptor& descriptor,
std::uint64_t initial_generation = 1);
bool unregisterDevice(const std::string& device_id);
bool publish(DeviceSafetySnapshot snapshot);
bool markUnknown(const std::string& device_id,
SafetyReason reason,
std::string source_id = {});
std::optional<std::uint64_t> bumpGeneration(
const std::string& device_id);
SafetySnapshotView get(
const std::string& device_id,
SafetyClock::time_point now = SafetyClock::now()) const;
std::vector<SafetySnapshotView> snapshot(
SafetyClock::time_point now = SafetyClock::now()) const;
bool waitForNewerSample(
const std::string& device_id,
std::uint64_t previous_sequence,
SafetyClock::time_point deadline,
SafetySnapshotView& result) const;
private:
struct Slot {
DeviceSafetyDescriptor descriptor;
DeviceSafetySnapshot snapshot;
bool has_sample{false};
};
static SafetySnapshotView viewOf_(
const Slot& slot,
SafetyClock::time_point now);
mutable std::shared_mutex mutex_;
mutable std::condition_variable_any changed_;
std::unordered_map<std::string, Slot> slots_;
};
} // namespace cmvr::safety

View File

@ -0,0 +1,235 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "devices/device_types.h"
#include "manager/safety/include/safety_reason.h"
namespace cmvr::safety {
using SafetyClock = std::chrono::steady_clock;
enum class TriState {
Unknown,
False,
True,
};
enum class SafetyCondition {
Nominal,
Restricted,
Unsafe,
Unknown,
};
enum class CommandIntent {
Observe,
StartActivity,
Configure,
Actuate,
Stop,
ResetFault,
RecoverAdmission,
};
enum class SafetyPolicyFamily {
Sensor,
Control,
};
enum class BlockerScope {
Device,
System,
};
enum class RecoveryRequirement {
RefreshOnly,
ClearSoftwareLatch,
HardwareReleaseRequired,
ManualInspectionRequired,
};
enum class SystemAdmissionState {
Starting,
Open,
Stopping,
Latched,
Recovering,
ShuttingDown,
};
enum class DeviceAdmissionState {
Observing,
Open,
Blocked,
Quarantined,
Recovering,
Removed,
};
enum class EnforcementMode {
Legacy,
Shadow,
EnforceSelected,
EnforceAll,
};
enum class CommandLifecycle {
Received,
Reserved,
RejectedBeforeDispatch,
Admitted,
Dispatching,
AcceptedByHardware,
Completed,
Failed,
CanceledBeforeDispatch,
OutcomeUnknown,
};
struct SafetyBlocker {
SafetyReason reason{SafetyReason::None};
BlockerScope scope{BlockerScope::Device};
RecoveryRequirement recovery_requirement{
RecoveryRequirement::RefreshOnly};
std::string source_id;
std::string operation_id;
std::uint64_t first_observed_at_unix_ms{0};
std::uint64_t last_observed_at_unix_ms{0};
};
struct DeviceSafetySnapshot {
std::string device_id;
SafetyCondition condition{SafetyCondition::Unknown};
std::uint64_t device_generation{0};
std::uint64_t sample_sequence{0};
SafetyClock::time_point observed_at{};
std::uint64_t observed_at_unix_ms{0};
TriState connected{TriState::Unknown};
TriState operational_ready{TriState::Unknown};
TriState quiescent{TriState::Unknown};
TriState motion_active{TriState::Unknown};
TriState actuator_enabled{TriState::Unknown};
TriState emergency_stop_active{TriState::Unknown};
TriState protective_stop_active{TriState::Unknown};
TriState fault_active{TriState::Unknown};
std::vector<SafetyBlocker> blockers;
};
struct DeviceSafetyDescriptor {
std::string device_id;
device::DeviceKind kind{device::DeviceKind::Unknown};
SafetyPolicyFamily default_policy{SafetyPolicyFamily::Sensor};
std::chrono::milliseconds maximum_snapshot_age{1000};
bool requires_safe_stop{false};
bool supports_active_refresh{false};
bool supports_non_enabling_fault_reset{false};
};
struct SafetySnapshotView {
DeviceSafetyDescriptor descriptor;
DeviceSafetySnapshot snapshot;
bool registered{false};
bool has_sample{false};
bool fresh{false};
std::chrono::milliseconds sample_age{
std::chrono::milliseconds::max()};
};
struct CommandActor {
std::string principal_id{"anonymous"};
bool authenticated{false};
std::vector<std::string> roles;
};
struct CommandDescriptor {
std::string full_method_name;
CommandIntent intent{CommandIntent::Observe};
SafetyPolicyFamily policy_family{SafetyPolicyFamily::Sensor};
bool mutating{false};
bool safety_lane{false};
};
struct AdmissionRequest {
CommandDescriptor command;
CommandActor actor;
std::string command_id;
std::string device_id;
std::optional<std::uint64_t> expected_device_generation;
std::uint64_t authority_generation{0};
SafetyClock::time_point deadline{SafetyClock::time_point::max()};
};
struct AdmissionDecision {
bool allowed{false};
bool policy_allowed{false};
bool enforced{false};
SafetyReason reason{SafetyReason::None};
std::string detail;
std::uint64_t safety_epoch{0};
std::uint64_t device_generation{0};
};
struct AdmissionPermit {
AdmissionPermit() = default;
AdmissionPermit(AdmissionPermit&&) noexcept = default;
AdmissionPermit& operator=(AdmissionPermit&&) noexcept = default;
AdmissionPermit(const AdmissionPermit&) = delete;
AdmissionPermit& operator=(const AdmissionPermit&) = delete;
std::string command_id;
std::string device_id;
CommandIntent intent{CommandIntent::Observe};
std::uint64_t safety_epoch{0};
std::uint64_t device_generation{0};
std::uint64_t authority_generation{0};
SafetyClock::time_point deadline{SafetyClock::time_point::max()};
bool policy_allowed{false};
bool enforced{false};
};
struct HardwareCheckResult {
bool safe{false};
SafetyReason reason{SafetyReason::SafetyStateMissing};
std::string detail;
};
struct RecoveryContext {
std::string recovery_id;
std::string reason;
std::uint64_t safety_epoch{0};
SafetyClock::time_point deadline{SafetyClock::time_point::max()};
bool verify_only{true};
};
struct RecoveryCheckResult {
bool reconciled{false};
SafetyReason reason{SafetyReason::None};
std::string detail;
};
struct SafetyEvent {
std::uint64_t sequence{0};
std::uint64_t safety_epoch{0};
std::uint64_t occurred_at_unix_ms{0};
std::string source_id;
std::string operation_id;
SafetyReason reason{SafetyReason::None};
std::string detail;
};
const char* toString(TriState value) noexcept;
const char* toString(SafetyCondition value) noexcept;
const char* toString(CommandIntent value) noexcept;
const char* toString(SafetyPolicyFamily value) noexcept;
const char* toString(SystemAdmissionState value) noexcept;
const char* toString(DeviceAdmissionState value) noexcept;
const char* toString(EnforcementMode value) noexcept;
} // namespace cmvr::safety

View File

@ -0,0 +1,276 @@
#include "manager/safety/include/command_ledger.h"
#include <algorithm>
#include <cctype>
#include <functional>
#include <stdexcept>
#include <utility>
namespace cmvr::safety {
namespace {
constexpr std::size_t kMaxPrincipalIdLength = 256;
constexpr std::size_t kMaxCommandIdLength = 128;
bool validIdentifier(const std::string& value, const std::size_t maximum)
{
if (value.empty() || value.size() > maximum) {
return false;
}
return std::all_of(
value.begin(), value.end(), [](const unsigned char character) {
return std::isalnum(character) || character == '-' ||
character == '_' || character == '.' ||
character == ':' || character == '/';
});
}
} // namespace
CommandLedger::CommandLedger()
: CommandLedger(Config{})
{
}
CommandLedger::CommandLedger(Config config)
: config_(config)
{
if (config_.total_id_capacity == 0 ||
config_.result_capacity > config_.total_id_capacity) {
throw std::invalid_argument("invalid CommandLedger capacity");
}
}
std::size_t CommandLedger::KeyHash::operator()(
const CommandKey& key) const noexcept
{
const auto first = std::hash<std::string>{}(key.effective_principal_id);
const auto second = std::hash<std::string>{}(key.command_id);
return first ^ (second + 0x9e3779b9U + (first << 6U) + (first >> 2U));
}
bool CommandLedger::validKey_(const CommandKey& key) noexcept
{
return validIdentifier(
key.effective_principal_id, kMaxPrincipalIdLength) &&
validIdentifier(key.command_id, kMaxCommandIdLength);
}
CommandLedger::Reservation CommandLedger::reserve(
CommandKey key,
std::string payload_hash)
{
if (!validKey_(key) || payload_hash.empty()) {
return {};
}
std::lock_guard lock(mutex_);
const auto live = records_.find(key);
if (live != records_.end()) {
const auto& state = live->second;
std::lock_guard state_lock(state->mutex);
if (state->payload_hash != payload_hash) {
return {CommandReservationStatus::CommandIdConflict, {}, {}};
}
if (state->terminal && state->outcome.has_value()) {
return {
CommandReservationStatus::CachedResult,
Ticket(state),
state->outcome};
}
return {
CommandReservationStatus::JoinedInFlight,
Ticket(state),
{}};
}
const auto retired = retired_ids_.find(key);
if (retired != retired_ids_.end()) {
return {
retired->second == payload_hash
? CommandReservationStatus::ResultEvicted
: CommandReservationStatus::CommandIdConflict,
{},
{}};
}
if (records_.size() + retired_ids_.size() >=
config_.total_id_capacity) {
return {CommandReservationStatus::LedgerExhausted, {}, {}};
}
auto state = std::make_shared<State>();
state->key = std::move(key);
state->payload_hash = std::move(payload_hash);
const auto inserted = records_.emplace(state->key, state);
if (!inserted.second) {
throw std::logic_error("CommandLedger duplicate insertion");
}
return {
CommandReservationStatus::AcceptedNew,
Ticket(std::move(state)),
{}};
}
bool CommandLedger::setLifecycle(
const Ticket& ticket,
const CommandLifecycle lifecycle,
const std::uint64_t safety_epoch,
const std::uint64_t device_generation,
const bool hardware_submission_possible)
{
if (!ticket.valid()) {
return false;
}
std::lock_guard ledger_lock(mutex_);
const auto found = records_.find(ticket.state_->key);
if (found == records_.end() || found->second != ticket.state_) {
return false;
}
std::lock_guard state_lock(ticket.state_->mutex);
if (ticket.state_->terminal) {
return false;
}
ticket.state_->lifecycle = lifecycle;
ticket.state_->safety_epoch = safety_epoch;
ticket.state_->device_generation = device_generation;
ticket.state_->hardware_submission_possible =
ticket.state_->hardware_submission_possible ||
hardware_submission_possible;
return true;
}
bool CommandLedger::complete(const Ticket& ticket, CommandOutcome outcome)
{
if (!ticket.valid()) {
return false;
}
std::lock_guard ledger_lock(mutex_);
const auto found = records_.find(ticket.state_->key);
if (found == records_.end() || found->second != ticket.state_) {
return false;
}
{
std::lock_guard state_lock(ticket.state_->mutex);
if (ticket.state_->terminal) {
return false;
}
outcome.hardware_submission_possible =
outcome.hardware_submission_possible ||
ticket.state_->hardware_submission_possible;
if (outcome.safety_epoch == 0) {
outcome.safety_epoch = ticket.state_->safety_epoch;
}
if (outcome.device_generation == 0) {
outcome.device_generation = ticket.state_->device_generation;
}
ticket.state_->lifecycle = outcome.lifecycle;
ticket.state_->outcome = std::move(outcome);
ticket.state_->terminal = true;
}
ticket.state_->condition.notify_all();
terminal_order_.push_back(ticket.state_->key);
++terminal_result_count_;
trimTerminalResultsLocked_();
return true;
}
void CommandLedger::trimTerminalResultsLocked_()
{
std::size_t consumed = 0;
while (terminal_result_count_ > config_.result_capacity &&
consumed < terminal_order_.size()) {
const auto key = terminal_order_[consumed++];
const auto found = records_.find(key);
if (found == records_.end()) {
continue;
}
const auto& state = found->second;
std::lock_guard state_lock(state->mutex);
if (!state->terminal) {
continue;
}
retired_ids_.emplace(state->key, state->payload_hash);
records_.erase(found);
--terminal_result_count_;
}
if (consumed != 0) {
terminal_order_.erase(
terminal_order_.begin(),
terminal_order_.begin() + static_cast<std::ptrdiff_t>(consumed));
}
}
std::optional<CommandOutcome> CommandLedger::wait(
const Ticket& ticket,
const SafetyClock::time_point deadline) const
{
if (!ticket.valid()) {
return std::nullopt;
}
std::unique_lock lock(ticket.state_->mutex);
if (deadline == SafetyClock::time_point::max()) {
ticket.state_->condition.wait(
lock, [&ticket] { return ticket.state_->terminal; });
} else if (!ticket.state_->condition.wait_until(
lock, deadline,
[&ticket] { return ticket.state_->terminal; })) {
return std::nullopt;
}
return ticket.state_->outcome;
}
std::optional<CommandOutcome> CommandLedger::lookup(
const CommandKey& key,
const std::string& payload_hash) const
{
std::lock_guard lock(mutex_);
const auto found = records_.find(key);
if (found == records_.end()) {
return std::nullopt;
}
std::lock_guard state_lock(found->second->mutex);
if (found->second->payload_hash != payload_hash ||
!found->second->terminal) {
return std::nullopt;
}
return found->second->outcome;
}
std::size_t CommandLedger::acceptedIdCount() const
{
std::lock_guard lock(mutex_);
return records_.size() + retired_ids_.size();
}
std::size_t CommandLedger::liveRecordCount() const
{
std::lock_guard lock(mutex_);
return records_.size();
}
std::size_t CommandLedger::retiredIdCount() const
{
std::lock_guard lock(mutex_);
return retired_ids_.size();
}
const char* toString(const CommandReservationStatus status) noexcept
{
switch (status) {
case CommandReservationStatus::AcceptedNew: return "AcceptedNew";
case CommandReservationStatus::JoinedInFlight: return "JoinedInFlight";
case CommandReservationStatus::CachedResult: return "CachedResult";
case CommandReservationStatus::CommandIdConflict:
return "CommandIdConflict";
case CommandReservationStatus::ResultEvicted: return "ResultEvicted";
case CommandReservationStatus::LedgerExhausted: return "LedgerExhausted";
case CommandReservationStatus::Invalid: return "Invalid";
}
return "Invalid";
}
} // namespace cmvr::safety

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,92 @@
#include "manager/safety/include/safety_reason.h"
namespace cmvr::safety {
const char* toString(const SafetyReason reason) noexcept
{
switch (reason) {
case SafetyReason::None: return "NONE";
case SafetyReason::InvalidArgument: return "INVALID_ARGUMENT";
case SafetyReason::Unauthenticated: return "UNAUTHENTICATED";
case SafetyReason::PermissionDenied: return "PERMISSION_DENIED";
case SafetyReason::RecoveryRpcDisabled: return "RECOVERY_RPC_DISABLED";
case SafetyReason::DeviceNotFound: return "DEVICE_NOT_FOUND";
case SafetyReason::DeviceUnavailable: return "DEVICE_UNAVAILABLE";
case SafetyReason::UnsupportedCommand: return "UNSUPPORTED_COMMAND";
case SafetyReason::SystemStarting: return "SYSTEM_STARTING";
case SafetyReason::SystemStopping: return "SYSTEM_STOPPING";
case SafetyReason::SafetyLatched: return "SAFETY_LATCHED";
case SafetyReason::SafetyStateMissing: return "SAFETY_STATE_MISSING";
case SafetyReason::SafetyStateStale: return "SAFETY_STATE_STALE";
case SafetyReason::HardwareUnsafe: return "HARDWARE_UNSAFE";
case SafetyReason::EmergencyStopActive: return "EMERGENCY_STOP_ACTIVE";
case SafetyReason::ProtectiveStopActive: return "PROTECTIVE_STOP_ACTIVE";
case SafetyReason::DeviceDisconnected: return "DEVICE_DISCONNECTED";
case SafetyReason::DeviceFault: return "DEVICE_FAULT";
case SafetyReason::DeviceNotReady: return "DEVICE_NOT_READY";
case SafetyReason::DeviceStillMoving: return "DEVICE_STILL_MOVING";
case SafetyReason::ControlBusy: return "CONTROL_BUSY";
case SafetyReason::GenerationMismatch: return "GENERATION_MISMATCH";
case SafetyReason::CommandIdRequired: return "COMMAND_ID_REQUIRED";
case SafetyReason::CommandIdConflict: return "COMMAND_ID_CONFLICT";
case SafetyReason::ResultEvicted: return "RESULT_EVICTED";
case SafetyReason::LedgerExhausted: return "LEDGER_EXHAUSTED";
case SafetyReason::Backpressure: return "BACKPRESSURE";
case SafetyReason::DeadlineExceededBeforeDispatch:
return "DEADLINE_EXCEEDED_BEFORE_DISPATCH";
case SafetyReason::OutcomeUnknown: return "OUTCOME_UNKNOWN";
case SafetyReason::ParticipantTimeout: return "PARTICIPANT_TIMEOUT";
case SafetyReason::StopUnconfirmed: return "STOP_UNCONFIRMED";
case SafetyReason::RecoveryEpochMismatch: return "RECOVERY_EPOCH_MISMATCH";
case SafetyReason::RecoveryReasonRequired: return "RECOVERY_REASON_REQUIRED";
case SafetyReason::RecoveryAuditFailed: return "RECOVERY_AUDIT_FAILED";
case SafetyReason::InternalError: return "INTERNAL_ERROR";
}
return "INTERNAL_ERROR";
}
bool retryWithSameCommandId(const SafetyReason reason) noexcept
{
switch (reason) {
case SafetyReason::RecoveryRpcDisabled:
case SafetyReason::SystemStopping:
return true;
case SafetyReason::None:
case SafetyReason::InvalidArgument:
case SafetyReason::Unauthenticated:
case SafetyReason::PermissionDenied:
case SafetyReason::DeviceNotFound:
case SafetyReason::DeviceUnavailable:
case SafetyReason::UnsupportedCommand:
case SafetyReason::SystemStarting:
case SafetyReason::SafetyLatched:
case SafetyReason::SafetyStateMissing:
case SafetyReason::SafetyStateStale:
case SafetyReason::HardwareUnsafe:
case SafetyReason::EmergencyStopActive:
case SafetyReason::ProtectiveStopActive:
case SafetyReason::DeviceDisconnected:
case SafetyReason::DeviceFault:
case SafetyReason::DeviceNotReady:
case SafetyReason::DeviceStillMoving:
case SafetyReason::ControlBusy:
case SafetyReason::GenerationMismatch:
case SafetyReason::CommandIdRequired:
case SafetyReason::CommandIdConflict:
case SafetyReason::ResultEvicted:
case SafetyReason::LedgerExhausted:
case SafetyReason::Backpressure:
case SafetyReason::DeadlineExceededBeforeDispatch:
case SafetyReason::OutcomeUnknown:
case SafetyReason::ParticipantTimeout:
case SafetyReason::StopUnconfirmed:
case SafetyReason::RecoveryEpochMismatch:
case SafetyReason::RecoveryReasonRequired:
case SafetyReason::RecoveryAuditFailed:
case SafetyReason::InternalError:
return false;
}
return false;
}
} // namespace cmvr::safety

View File

@ -0,0 +1,305 @@
#include "manager/safety/include/safety_snapshot_store.h"
#include <algorithm>
#include <limits>
#include <mutex>
#include <utility>
namespace cmvr::safety {
namespace {
std::uint64_t unixTimeMs() noexcept
{
const auto value = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
return value > 0 ? static_cast<std::uint64_t>(value) : 1U;
}
} // namespace
bool SafetySnapshotStore::registerDevice(
const DeviceSafetyDescriptor& descriptor,
const std::uint64_t initial_generation)
{
if (descriptor.device_id.empty() ||
descriptor.maximum_snapshot_age <= std::chrono::milliseconds::zero() ||
initial_generation == 0) {
return false;
}
Slot slot;
slot.descriptor = descriptor;
slot.snapshot.device_id = descriptor.device_id;
slot.snapshot.device_generation = initial_generation;
slot.snapshot.condition = SafetyCondition::Unknown;
std::unique_lock lock(mutex_);
const auto inserted = slots_.emplace(descriptor.device_id, std::move(slot));
if (inserted.second) {
changed_.notify_all();
}
return inserted.second;
}
bool SafetySnapshotStore::unregisterDevice(const std::string& device_id)
{
std::unique_lock lock(mutex_);
const bool removed = slots_.erase(device_id) != 0;
if (removed) {
changed_.notify_all();
}
return removed;
}
bool SafetySnapshotStore::publish(DeviceSafetySnapshot snapshot)
{
if (snapshot.device_id.empty() || snapshot.device_generation == 0 ||
snapshot.sample_sequence == 0 ||
snapshot.observed_at == SafetyClock::time_point{}) {
return false;
}
std::unique_lock lock(mutex_);
const auto found = slots_.find(snapshot.device_id);
if (found == slots_.end()) {
return false;
}
auto& slot = found->second;
const auto current_generation = slot.snapshot.device_generation;
if (snapshot.device_generation < current_generation) {
return false;
}
if (snapshot.device_generation == current_generation &&
slot.has_sample &&
snapshot.sample_sequence <= slot.snapshot.sample_sequence) {
return false;
}
if (snapshot.observed_at_unix_ms == 0) {
snapshot.observed_at_unix_ms = unixTimeMs();
}
slot.snapshot = std::move(snapshot);
slot.has_sample = true;
changed_.notify_all();
return true;
}
bool SafetySnapshotStore::markUnknown(
const std::string& device_id,
const SafetyReason reason,
std::string source_id)
{
std::unique_lock lock(mutex_);
const auto found = slots_.find(device_id);
if (found == slots_.end()) {
return false;
}
auto& slot = found->second;
DeviceSafetySnapshot snapshot;
snapshot.device_id = device_id;
snapshot.device_generation = slot.snapshot.device_generation;
snapshot.sample_sequence = slot.snapshot.sample_sequence + 1U;
if (snapshot.sample_sequence == 0) {
snapshot.sample_sequence = 1U;
}
snapshot.observed_at = SafetyClock::now();
snapshot.observed_at_unix_ms = unixTimeMs();
snapshot.condition = SafetyCondition::Unknown;
snapshot.blockers.push_back(SafetyBlocker{
reason,
BlockerScope::Device,
RecoveryRequirement::RefreshOnly,
source_id.empty() ? device_id : std::move(source_id),
{},
snapshot.observed_at_unix_ms,
snapshot.observed_at_unix_ms});
slot.snapshot = std::move(snapshot);
slot.has_sample = true;
changed_.notify_all();
return true;
}
std::optional<std::uint64_t> SafetySnapshotStore::bumpGeneration(
const std::string& device_id)
{
std::unique_lock lock(mutex_);
const auto found = slots_.find(device_id);
if (found == slots_.end()) {
return std::nullopt;
}
auto& slot = found->second;
if (slot.snapshot.device_generation ==
std::numeric_limits<std::uint64_t>::max()) {
return std::nullopt;
}
++slot.snapshot.device_generation;
slot.snapshot.sample_sequence = 0;
slot.snapshot.observed_at = {};
slot.snapshot.observed_at_unix_ms = 0;
slot.snapshot.condition = SafetyCondition::Unknown;
slot.snapshot.blockers.clear();
slot.has_sample = false;
changed_.notify_all();
return slot.snapshot.device_generation;
}
SafetySnapshotView SafetySnapshotStore::viewOf_(
const Slot& slot,
const SafetyClock::time_point now)
{
SafetySnapshotView view;
view.descriptor = slot.descriptor;
view.snapshot = slot.snapshot;
view.registered = true;
view.has_sample = slot.has_sample;
if (!slot.has_sample ||
slot.snapshot.observed_at == SafetyClock::time_point{}) {
return view;
}
const auto elapsed = now <= slot.snapshot.observed_at
? SafetyClock::duration::zero()
: now - slot.snapshot.observed_at;
view.sample_age = std::chrono::duration_cast<std::chrono::milliseconds>(
elapsed);
view.fresh = view.sample_age <= slot.descriptor.maximum_snapshot_age;
return view;
}
SafetySnapshotView SafetySnapshotStore::get(
const std::string& device_id,
const SafetyClock::time_point now) const
{
std::shared_lock lock(mutex_);
const auto found = slots_.find(device_id);
if (found == slots_.end()) {
return {};
}
return viewOf_(found->second, now);
}
std::vector<SafetySnapshotView> SafetySnapshotStore::snapshot(
const SafetyClock::time_point now) const
{
std::vector<SafetySnapshotView> result;
std::shared_lock lock(mutex_);
result.reserve(slots_.size());
for (const auto& [id, slot] : slots_) {
(void)id;
result.push_back(viewOf_(slot, now));
}
std::sort(result.begin(), result.end(), [](const auto& lhs, const auto& rhs) {
return lhs.descriptor.device_id < rhs.descriptor.device_id;
});
return result;
}
bool SafetySnapshotStore::waitForNewerSample(
const std::string& device_id,
const std::uint64_t previous_sequence,
const SafetyClock::time_point deadline,
SafetySnapshotView& result) const
{
std::unique_lock lock(mutex_);
const auto ready = [&]() {
const auto found = slots_.find(device_id);
return found == slots_.end() ||
(found->second.has_sample &&
found->second.snapshot.sample_sequence > previous_sequence);
};
if (!changed_.wait_until(lock, deadline, ready)) {
return false;
}
const auto found = slots_.find(device_id);
if (found == slots_.end()) {
return false;
}
result = viewOf_(found->second, SafetyClock::now());
return result.has_sample &&
result.snapshot.sample_sequence > previous_sequence;
}
const char* toString(const TriState value) noexcept
{
switch (value) {
case TriState::Unknown: return "Unknown";
case TriState::False: return "False";
case TriState::True: return "True";
}
return "Unknown";
}
const char* toString(const SafetyCondition value) noexcept
{
switch (value) {
case SafetyCondition::Nominal: return "Nominal";
case SafetyCondition::Restricted: return "Restricted";
case SafetyCondition::Unsafe: return "Unsafe";
case SafetyCondition::Unknown: return "Unknown";
}
return "Unknown";
}
const char* toString(const CommandIntent value) noexcept
{
switch (value) {
case CommandIntent::Observe: return "Observe";
case CommandIntent::StartActivity: return "StartActivity";
case CommandIntent::Configure: return "Configure";
case CommandIntent::Actuate: return "Actuate";
case CommandIntent::Stop: return "Stop";
case CommandIntent::ResetFault: return "ResetFault";
case CommandIntent::RecoverAdmission: return "RecoverAdmission";
}
return "Observe";
}
const char* toString(const SafetyPolicyFamily value) noexcept
{
switch (value) {
case SafetyPolicyFamily::Sensor: return "Sensor";
case SafetyPolicyFamily::Control: return "Control";
}
return "Sensor";
}
const char* toString(const SystemAdmissionState value) noexcept
{
switch (value) {
case SystemAdmissionState::Starting: return "Starting";
case SystemAdmissionState::Open: return "Open";
case SystemAdmissionState::Stopping: return "Stopping";
case SystemAdmissionState::Latched: return "Latched";
case SystemAdmissionState::Recovering: return "Recovering";
case SystemAdmissionState::ShuttingDown: return "ShuttingDown";
}
return "Starting";
}
const char* toString(const DeviceAdmissionState value) noexcept
{
switch (value) {
case DeviceAdmissionState::Observing: return "Observing";
case DeviceAdmissionState::Open: return "Open";
case DeviceAdmissionState::Blocked: return "Blocked";
case DeviceAdmissionState::Quarantined: return "Quarantined";
case DeviceAdmissionState::Recovering: return "Recovering";
case DeviceAdmissionState::Removed: return "Removed";
}
return "Observing";
}
const char* toString(const EnforcementMode value) noexcept
{
switch (value) {
case EnforcementMode::Legacy: return "Legacy";
case EnforcementMode::Shadow: return "Shadow";
case EnforcementMode::EnforceSelected: return "EnforceSelected";
case EnforcementMode::EnforceAll: return "EnforceAll";
}
return "Legacy";
}
} // namespace cmvr::safety

View File

@ -0,0 +1,122 @@
#include "manager/safety/include/command_ledger.h"
#include <atomic>
#include <chrono>
#include <thread>
#include <vector>
#include <gtest/gtest.h>
namespace cmvr::safety {
namespace {
CommandKey key(const std::string& id)
{
return {"anonymous", id};
}
CommandOutcome completedOutcome()
{
CommandOutcome outcome;
outcome.lifecycle = CommandLifecycle::Completed;
outcome.reason = SafetyReason::None;
outcome.serialized_response = "done";
return outcome;
}
TEST(CommandLedgerTest, SameIdJoinsAndDifferentPayloadConflicts)
{
CommandLedger ledger;
const auto first = ledger.reserve(key("command-1"), "payload-a");
ASSERT_EQ(first.status, CommandReservationStatus::AcceptedNew);
EXPECT_EQ(
ledger.reserve(key("command-1"), "payload-a").status,
CommandReservationStatus::JoinedInFlight);
EXPECT_EQ(
ledger.reserve(key("command-1"), "payload-b").status,
CommandReservationStatus::CommandIdConflict);
ASSERT_TRUE(ledger.complete(first.ticket, completedOutcome()));
const auto cached = ledger.reserve(key("command-1"), "payload-a");
ASSERT_EQ(cached.status, CommandReservationStatus::CachedResult);
ASSERT_TRUE(cached.cached_outcome.has_value());
EXPECT_EQ(cached.cached_outcome->serialized_response, "done");
}
TEST(CommandLedgerTest, ConcurrentCallersNeverCreateASecondReservation)
{
CommandLedger ledger;
std::atomic<int> accepted{0};
std::vector<std::thread> workers;
for (int index = 0; index < 16; ++index) {
workers.emplace_back([&] {
const auto result = ledger.reserve(key("shared"), "payload");
if (result.status == CommandReservationStatus::AcceptedNew) {
accepted.fetch_add(1);
}
});
}
for (auto& worker : workers) {
worker.join();
}
EXPECT_EQ(accepted.load(), 1);
EXPECT_EQ(ledger.acceptedIdCount(), 1U);
}
TEST(CommandLedgerTest, OutcomeUnknownIsCachedAndNeverReservedAgain)
{
CommandLedger ledger;
const auto first = ledger.reserve(key("uncertain"), "payload");
ASSERT_EQ(first.status, CommandReservationStatus::AcceptedNew);
CommandOutcome outcome;
outcome.lifecycle = CommandLifecycle::OutcomeUnknown;
outcome.reason = SafetyReason::OutcomeUnknown;
outcome.hardware_submission_possible = true;
ASSERT_TRUE(ledger.complete(first.ticket, outcome));
const auto retry = ledger.reserve(key("uncertain"), "payload");
ASSERT_EQ(retry.status, CommandReservationStatus::CachedResult);
ASSERT_TRUE(retry.cached_outcome.has_value());
EXPECT_EQ(
retry.cached_outcome->lifecycle, CommandLifecycle::OutcomeUnknown);
EXPECT_TRUE(retry.cached_outcome->hardware_submission_possible);
}
TEST(CommandLedgerTest, EvictedResultLeavesAnExactTombstone)
{
CommandLedger ledger({1, 3});
auto first = ledger.reserve(key("first"), "payload-1");
ASSERT_TRUE(ledger.complete(first.ticket, completedOutcome()));
auto second = ledger.reserve(key("second"), "payload-2");
ASSERT_TRUE(ledger.complete(second.ticket, completedOutcome()));
EXPECT_EQ(
ledger.reserve(key("first"), "payload-1").status,
CommandReservationStatus::ResultEvicted);
EXPECT_EQ(
ledger.reserve(key("first"), "different").status,
CommandReservationStatus::CommandIdConflict);
auto third = ledger.reserve(key("third"), "payload-3");
ASSERT_EQ(third.status, CommandReservationStatus::AcceptedNew);
EXPECT_EQ(
ledger.reserve(key("fourth"), "payload-4").status,
CommandReservationStatus::LedgerExhausted);
}
TEST(CommandLedgerTest, WaitTimesOutWithoutChangingExecution)
{
CommandLedger ledger;
const auto reservation = ledger.reserve(key("slow"), "payload");
ASSERT_EQ(reservation.status, CommandReservationStatus::AcceptedNew);
EXPECT_FALSE(ledger.wait(
reservation.ticket,
SafetyClock::now() + std::chrono::milliseconds(5)).has_value());
EXPECT_EQ(
ledger.reserve(key("slow"), "payload").status,
CommandReservationStatus::JoinedInFlight);
}
} // namespace
} // namespace cmvr::safety

View File

@ -0,0 +1,507 @@
#include "manager/safety/include/safety_coordinator.h"
#include <atomic>
#include <chrono>
#include <memory>
#include <string>
#include <gtest/gtest.h>
namespace cmvr::safety {
namespace {
DeviceSafetyDescriptor controlDescriptor()
{
DeviceSafetyDescriptor descriptor;
descriptor.device_id = "arm";
descriptor.kind = device::DeviceKind::Arm;
descriptor.default_policy = SafetyPolicyFamily::Control;
descriptor.maximum_snapshot_age = std::chrono::seconds(1);
descriptor.requires_safe_stop = true;
descriptor.supports_active_refresh = true;
return descriptor;
}
class FakeEndpoint final : public DeviceSafetyEndpoint {
public:
explicit FakeEndpoint(DeviceSafetyDescriptor descriptor)
: descriptor_(std::move(descriptor))
{
}
DeviceSafetyDescriptor descriptor() const override
{
return descriptor_;
}
void bindPublisher(SafetySnapshotPublisher publisher) override
{
publisher_ = std::move(publisher);
}
void requestSafetyRefresh() noexcept override
{
if (!publisher_ || !publish_on_refresh) {
return;
}
DeviceSafetySnapshot snapshot;
snapshot.device_id = descriptor_.device_id;
snapshot.condition = condition;
snapshot.device_generation = generation;
snapshot.sample_sequence = ++sequence;
snapshot.observed_at = SafetyClock::now();
snapshot.connected = connected;
snapshot.operational_ready = ready;
snapshot.quiescent = quiescent;
snapshot.motion_active =
quiescent == TriState::True ? TriState::False : TriState::Unknown;
snapshot.emergency_stop_active = emergency_stop;
snapshot.protective_stop_active = protective_stop;
snapshot.fault_active = fault;
(void)publisher_(std::move(snapshot));
}
HardwareCheckResult validateBeforeDispatch(
const AdmissionPermit&) override
{
++hardware_checks;
return final_check;
}
RecoveryCheckResult reconcileAdmissionState(
const RecoveryContext&) override
{
++recoveries;
return recovery_check;
}
DeviceSafetyDescriptor descriptor_;
SafetySnapshotPublisher publisher_;
SafetyCondition condition{SafetyCondition::Nominal};
TriState connected{TriState::True};
TriState ready{TriState::True};
TriState quiescent{TriState::True};
TriState emergency_stop{TriState::False};
TriState protective_stop{TriState::False};
TriState fault{TriState::False};
HardwareCheckResult final_check{true, SafetyReason::None, {}};
RecoveryCheckResult recovery_check{true, SafetyReason::None, {}};
std::uint64_t generation{1};
std::uint64_t sequence{0};
bool publish_on_refresh{true};
std::atomic<int> hardware_checks{0};
std::atomic<int> recoveries{0};
};
class FakeParticipant final : public SafetyParticipant {
public:
ParticipantDescriptor descriptor() const override
{
return {"arm", ParticipantPhase::Actuator, true,
std::chrono::milliseconds(100)};
}
BarrierToken beginBarrier(
const SafetyOperationContext& context) override
{
++barriers;
return {"arm", context.operation_id, context.safety_epoch,
static_cast<std::uint64_t>(barriers.load())};
}
ParticipantResult requestQuiesce(
const BarrierToken&,
const SafetyOperationContext&) override
{
++stop_requests;
return stop_result;
}
ParticipantResult verifyQuiescent(
const BarrierToken&,
const SafetyOperationContext&) override
{
++verifications;
return verify_result;
}
RecoveryCheckResult recoverAdmission(
const BarrierToken&,
const RecoveryContext&) override
{
++recoveries;
return recovery_result;
}
ParticipantResult releaseBarrier(
const BarrierToken&) noexcept override
{
++releases;
return release_result;
}
ParticipantResult stop_result{true, SafetyReason::None, {}};
ParticipantResult verify_result{true, SafetyReason::None, {}};
RecoveryCheckResult recovery_result{true, SafetyReason::None, {}};
ParticipantResult release_result{true, SafetyReason::None, {}};
std::atomic<int> barriers{0};
std::atomic<int> stop_requests{0};
std::atomic<int> verifications{0};
std::atomic<int> recoveries{0};
std::atomic<int> releases{0};
};
AdmissionRequest actuateRequest()
{
AdmissionRequest request;
request.command = {
"/cmvr.api.ArmService/moveJ",
CommandIntent::Actuate,
SafetyPolicyFamily::Control,
true,
false};
request.device_id = "arm";
request.command_id = "command-1";
request.deadline = SafetyClock::now() + std::chrono::seconds(1);
return request;
}
TEST(SafetyCoordinatorTest, ShadowReportsDenyWithoutChangingLegacyBehavior)
{
SafetyCoordinator coordinator;
ASSERT_TRUE(coordinator.registerDevice({controlDescriptor(), {}, {}}));
coordinator.markStartupComplete();
const auto result = coordinator.admit(actuateRequest());
EXPECT_TRUE(result.decision.allowed);
EXPECT_FALSE(result.decision.policy_allowed);
EXPECT_FALSE(result.decision.enforced);
EXPECT_EQ(result.decision.reason, SafetyReason::SafetyStateMissing);
EXPECT_TRUE(result.permit.has_value());
}
TEST(SafetyCoordinatorTest,
EnforceSelectedStartupRejectsEmptyOrUnknownCoverage)
{
SafetyCoordinatorConfig empty_config;
empty_config.enforcement_mode = EnforcementMode::EnforceSelected;
SafetyCoordinator empty(empty_config);
const auto empty_result = empty.validateStartupCoverage(
SafetyClock::now() + std::chrono::milliseconds(10));
ASSERT_FALSE(empty_result.ready);
ASSERT_EQ(empty_result.issues.size(), 1U);
EXPECT_EQ(empty_result.issues.front().reason, SafetyReason::InvalidArgument);
SafetyCoordinatorConfig missing_config;
missing_config.enforcement_mode = EnforcementMode::EnforceSelected;
missing_config.enforced_device_ids.insert("missing-arm");
SafetyCoordinator missing(missing_config);
const auto missing_result = missing.validateStartupCoverage(
SafetyClock::now() + std::chrono::milliseconds(10));
ASSERT_FALSE(missing_result.ready);
ASSERT_EQ(missing_result.issues.size(), 1U);
EXPECT_EQ(missing_result.issues.front().target_id, "missing-arm");
EXPECT_EQ(missing_result.issues.front().reason, SafetyReason::DeviceNotFound);
}
TEST(SafetyCoordinatorTest,
EnforceAllStartupRequiresEndpointParticipantAndFreshSnapshot)
{
SafetyCoordinatorConfig config;
config.enforcement_mode = EnforcementMode::EnforceAll;
SafetyCoordinator missing_capability(config);
ASSERT_TRUE(missing_capability.registerDevice(
{controlDescriptor(), {}, {}}));
const auto structural = missing_capability.validateStartupCoverage(
SafetyClock::now() + std::chrono::milliseconds(10));
EXPECT_FALSE(structural.ready);
EXPECT_EQ(structural.issues.size(), 2U);
SafetyCoordinator missing_sample(config);
auto silent_endpoint =
std::make_shared<FakeEndpoint>(controlDescriptor());
silent_endpoint->publish_on_refresh = false;
ASSERT_TRUE(missing_sample.registerDevice({
controlDescriptor(),
silent_endpoint,
std::make_shared<FakeParticipant>()}));
const auto stale = missing_sample.validateStartupCoverage(
SafetyClock::now() + std::chrono::milliseconds(10));
ASSERT_FALSE(stale.ready);
ASSERT_EQ(stale.issues.size(), 1U);
EXPECT_EQ(stale.issues.front().reason, SafetyReason::SafetyStateMissing);
}
TEST(SafetyCoordinatorTest,
HardwareUnsafeSnapshotBlocksAdmissionButNotStructuralStartup)
{
SafetyCoordinatorConfig config;
config.enforcement_mode = EnforcementMode::EnforceAll;
SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>(controlDescriptor());
endpoint->condition = SafetyCondition::Unsafe;
endpoint->emergency_stop = TriState::True;
ASSERT_TRUE(coordinator.registerDevice({
controlDescriptor(), endpoint, std::make_shared<FakeParticipant>()}));
coordinator.updateDeviceRuntimeState(
"arm", device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
const auto coverage = coordinator.validateStartupCoverage(
SafetyClock::now() + std::chrono::milliseconds(50));
EXPECT_TRUE(coverage.ready);
coordinator.markStartupComplete();
const auto admission = coordinator.admit(actuateRequest());
EXPECT_FALSE(admission.decision.allowed);
EXPECT_EQ(
admission.decision.reason, SafetyReason::EmergencyStopActive);
}
TEST(SafetyCoordinatorTest, EnforceAllFailsClosedOnUnknownControlState)
{
SafetyCoordinatorConfig config;
config.enforcement_mode = EnforcementMode::EnforceAll;
SafetyCoordinator coordinator(config);
ASSERT_TRUE(coordinator.registerDevice({controlDescriptor(), {}, {}}));
coordinator.markStartupComplete();
const auto result = coordinator.admit(actuateRequest());
EXPECT_FALSE(result.decision.allowed);
EXPECT_FALSE(result.decision.policy_allowed);
EXPECT_TRUE(result.decision.enforced);
EXPECT_FALSE(result.permit.has_value());
}
TEST(SafetyCoordinatorTest, ControlSafetyBitsMustBeExplicitlyFalse)
{
SafetyCoordinatorConfig config;
config.enforcement_mode = EnforcementMode::EnforceAll;
SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>(controlDescriptor());
endpoint->protective_stop = TriState::Unknown;
ASSERT_TRUE(coordinator.registerDevice(
{controlDescriptor(), endpoint, {}}));
coordinator.updateDeviceRuntimeState(
"arm", device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
const auto result = coordinator.admit(actuateRequest());
EXPECT_FALSE(result.decision.allowed);
EXPECT_EQ(result.decision.reason, SafetyReason::ProtectiveStopActive);
ASSERT_EQ(coordinator.snapshot().devices.size(), 1U);
EXPECT_EQ(
coordinator.snapshot().devices.front().admission_state,
DeviceAdmissionState::Blocked);
}
TEST(SafetyCoordinatorTest, EnforcedDispatchRunsFinalHardwareCheck)
{
SafetyCoordinatorConfig config;
config.enforcement_mode = EnforcementMode::EnforceAll;
SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>(controlDescriptor());
ASSERT_TRUE(coordinator.registerDevice(
{controlDescriptor(), endpoint, {}}));
coordinator.updateDeviceRuntimeState(
"arm", device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
auto admission = coordinator.admit(actuateRequest());
ASSERT_TRUE(admission.decision.allowed) << admission.decision.detail;
ASSERT_TRUE(admission.permit.has_value());
auto dispatch = coordinator.beginDispatch(*admission.permit);
EXPECT_TRUE(dispatch.acquired());
EXPECT_EQ(endpoint->hardware_checks.load(), 1);
}
TEST(SafetyCoordinatorTest,
StartActivityMayEnterFromRestrictedButActuationMayNot)
{
SafetyCoordinatorConfig config;
config.enforcement_mode = EnforcementMode::EnforceAll;
SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>(controlDescriptor());
endpoint->condition = SafetyCondition::Restricted;
endpoint->ready = TriState::False;
ASSERT_TRUE(coordinator.registerDevice(
{controlDescriptor(), endpoint, {}}));
coordinator.updateDeviceRuntimeState(
"arm", device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
auto start = actuateRequest();
start.command.intent = CommandIntent::StartActivity;
const auto start_result = coordinator.admit(start);
ASSERT_TRUE(start_result.decision.allowed)
<< start_result.decision.detail;
ASSERT_TRUE(start_result.permit.has_value());
EXPECT_TRUE(coordinator.beginDispatch(*start_result.permit).acquired());
const auto actuation = coordinator.admit(actuateRequest());
EXPECT_FALSE(actuation.decision.allowed);
EXPECT_EQ(actuation.decision.reason, SafetyReason::HardwareUnsafe);
}
TEST(SafetyCoordinatorTest, SuccessfulStopInvalidatesOldPermitAndReopens)
{
SafetyCoordinatorConfig config;
config.enforcement_mode = EnforcementMode::EnforceAll;
SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>(controlDescriptor());
auto participant = std::make_shared<FakeParticipant>();
ASSERT_TRUE(coordinator.registerDevice(
{controlDescriptor(), endpoint, participant}));
coordinator.updateDeviceRuntimeState(
"arm", device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
auto admission = coordinator.admit(actuateRequest());
ASSERT_TRUE(admission.permit.has_value());
const auto old_epoch = admission.permit->safety_epoch;
const auto stopped = coordinator.stopAll(
"stop-1", SafetyClock::now() + std::chrono::seconds(1));
ASSERT_TRUE(stopped.success);
EXPECT_EQ(stopped.system_state, SystemAdmissionState::Open);
EXPECT_GT(stopped.current_safety_epoch, old_epoch);
const auto revalidated =
coordinator.revalidatePermit(*admission.permit);
EXPECT_FALSE(revalidated.safe);
EXPECT_EQ(revalidated.reason, SafetyReason::SafetyLatched);
EXPECT_FALSE(coordinator.beginDispatch(*admission.permit).acquired());
EXPECT_EQ(participant->stop_requests.load(), 1);
EXPECT_EQ(participant->verifications.load(), 1);
const auto snapshot = coordinator.snapshot();
ASSERT_EQ(snapshot.participants.size(), 1U);
EXPECT_EQ(snapshot.participants.front().descriptor.participant_id, "arm");
EXPECT_FALSE(snapshot.participants.front().barrier_active);
EXPECT_FALSE(snapshot.participants.front().barrier_retained);
EXPECT_TRUE(snapshot.participants.front().last_request.recorded);
EXPECT_TRUE(snapshot.participants.front().last_request.success);
EXPECT_TRUE(snapshot.participants.front().last_verify.recorded);
EXPECT_TRUE(snapshot.participants.front().last_verify.success);
EXPECT_TRUE(snapshot.participants.front().last_release.recorded);
EXPECT_TRUE(snapshot.participants.front().last_release.success);
}
TEST(SafetyCoordinatorTest, FailedStopRequiresVerifiedRecovery)
{
SafetyCoordinatorConfig config;
config.enforcement_mode = EnforcementMode::EnforceAll;
SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>(controlDescriptor());
auto participant = std::make_shared<FakeParticipant>();
participant->verify_result = {
false, SafetyReason::StopUnconfirmed, "motion not confirmed"};
ASSERT_TRUE(coordinator.registerDevice(
{controlDescriptor(), endpoint, participant}));
coordinator.updateDeviceRuntimeState(
"arm", device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
const auto stopped = coordinator.stopAll(
"stop-failed", SafetyClock::now() + std::chrono::seconds(1));
ASSERT_FALSE(stopped.success);
ASSERT_EQ(stopped.system_state, SystemAdmissionState::Latched);
const auto latched = coordinator.snapshot();
ASSERT_EQ(latched.participants.size(), 1U);
EXPECT_TRUE(latched.participants.front().barrier_active);
EXPECT_TRUE(latched.participants.front().barrier_retained);
EXPECT_TRUE(latched.participants.front().last_verify.recorded);
EXPECT_FALSE(latched.participants.front().last_verify.success);
EXPECT_FALSE(latched.participants.front().last_release.recorded);
participant->verify_result = {true, SafetyReason::None, {}};
RecoveryRequest request;
request.recovery_id = "recovery-1";
request.all_devices = true;
request.expected_safety_epoch = stopped.current_safety_epoch;
request.verify_only = false;
request.reason = "operator confirmed work cell is clear";
request.deadline = SafetyClock::now() + std::chrono::seconds(1);
const auto recovered = coordinator.recover(request);
EXPECT_EQ(recovered.result, RecoveryResultCode::Recovered);
EXPECT_EQ(recovered.system_state, SystemAdmissionState::Open);
EXPECT_EQ(endpoint->recoveries.load(), 1);
EXPECT_EQ(participant->recoveries.load(), 1);
const auto reopened = coordinator.snapshot();
ASSERT_EQ(reopened.participants.size(), 1U);
EXPECT_FALSE(reopened.participants.front().barrier_active);
EXPECT_FALSE(reopened.participants.front().barrier_retained);
EXPECT_TRUE(reopened.participants.front().last_release.recorded);
EXPECT_TRUE(reopened.participants.front().last_release.success);
}
TEST(SafetyCoordinatorTest, RecoveryCannotIgnoreEmergencyStop)
{
SafetyCoordinator coordinator;
auto endpoint = std::make_shared<FakeEndpoint>(controlDescriptor());
auto participant = std::make_shared<FakeParticipant>();
ASSERT_TRUE(coordinator.registerDevice(
{controlDescriptor(), endpoint, participant}));
coordinator.markStartupComplete();
coordinator.quarantineDevice("arm", SafetyReason::OutcomeUnknown);
const auto before = coordinator.snapshot();
endpoint->emergency_stop = TriState::True;
RecoveryRequest request;
request.recovery_id = "recovery-estop";
request.all_devices = true;
request.expected_safety_epoch = before.safety_epoch;
request.verify_only = false;
request.reason = "test";
request.deadline = SafetyClock::now() + std::chrono::seconds(1);
const auto recovered = coordinator.recover(request);
EXPECT_EQ(recovered.result, RecoveryResultCode::BlockerRemains);
EXPECT_EQ(recovered.system_state, SystemAdmissionState::Latched);
ASSERT_FALSE(recovered.targets.empty());
EXPECT_EQ(
recovered.targets.front().reason,
SafetyReason::EmergencyStopActive);
EXPECT_EQ(endpoint->recoveries.load(), 0);
}
TEST(SafetyCoordinatorTest, RecoveryAuditFailureCannotReleaseLatch)
{
SafetyCoordinator coordinator;
auto endpoint = std::make_shared<FakeEndpoint>(controlDescriptor());
auto participant = std::make_shared<FakeParticipant>();
participant->verify_result = {
false, SafetyReason::StopUnconfirmed, "motion not confirmed"};
ASSERT_TRUE(coordinator.registerDevice(
{controlDescriptor(), endpoint, participant}));
coordinator.markStartupComplete();
const auto stopped = coordinator.stopAll(
"stop-audit", SafetyClock::now() + std::chrono::seconds(1));
ASSERT_FALSE(stopped.success);
participant->verify_result = {true, SafetyReason::None, {}};
RecoveryRequest request;
request.recovery_id = "recovery-audit";
request.all_devices = true;
request.expected_safety_epoch = stopped.current_safety_epoch;
request.verify_only = false;
request.reason = "operator inspected the work cell";
request.deadline = SafetyClock::now() + std::chrono::seconds(1);
request.authorize_clear = [] { return false; };
const auto recovered = coordinator.recover(request);
EXPECT_EQ(recovered.result, RecoveryResultCode::BlockerRemains);
EXPECT_EQ(recovered.system_state, SystemAdmissionState::Latched);
ASSERT_FALSE(recovered.targets.empty());
EXPECT_EQ(
recovered.targets.front().reason,
SafetyReason::RecoveryAuditFailed);
EXPECT_EQ(endpoint->recoveries.load(), 0);
EXPECT_EQ(participant->recoveries.load(), 0);
EXPECT_EQ(participant->releases.load(), 0);
}
} // namespace
} // namespace cmvr::safety

View File

@ -0,0 +1,111 @@
#include "manager/safety/include/safety_snapshot_store.h"
#include <atomic>
#include <chrono>
#include <thread>
#include <gtest/gtest.h>
namespace cmvr::safety {
namespace {
DeviceSafetyDescriptor descriptor()
{
DeviceSafetyDescriptor result;
result.device_id = "arm";
result.kind = device::DeviceKind::Arm;
result.default_policy = SafetyPolicyFamily::Control;
result.maximum_snapshot_age = std::chrono::milliseconds(50);
result.requires_safe_stop = true;
return result;
}
DeviceSafetySnapshot sample(
const std::uint64_t generation,
const std::uint64_t sequence,
const SafetyClock::time_point observed_at = SafetyClock::now())
{
DeviceSafetySnapshot result;
result.device_id = "arm";
result.condition = SafetyCondition::Nominal;
result.device_generation = generation;
result.sample_sequence = sequence;
result.observed_at = observed_at;
result.connected = TriState::True;
result.operational_ready = TriState::True;
result.quiescent = TriState::True;
result.motion_active = TriState::False;
result.emergency_stop_active = TriState::False;
result.protective_stop_active = TriState::False;
result.fault_active = TriState::False;
return result;
}
TEST(SafetySnapshotStoreTest, RejectsOldGenerationAndSequence)
{
SafetySnapshotStore store;
ASSERT_TRUE(store.registerDevice(descriptor(), 3));
EXPECT_FALSE(store.publish(sample(2, 1)));
ASSERT_TRUE(store.publish(sample(3, 2)));
EXPECT_FALSE(store.publish(sample(3, 2)));
EXPECT_FALSE(store.publish(sample(3, 1)));
EXPECT_TRUE(store.publish(sample(4, 1)));
const auto view = store.get("arm");
ASSERT_TRUE(view.registered);
EXPECT_EQ(view.snapshot.device_generation, 4U);
EXPECT_EQ(view.snapshot.sample_sequence, 1U);
}
TEST(SafetySnapshotStoreTest, FreshnessUsesMonotonicObservationTime)
{
SafetySnapshotStore store;
ASSERT_TRUE(store.registerDevice(descriptor()));
const auto observed = SafetyClock::now();
ASSERT_TRUE(store.publish(sample(1, 1, observed)));
EXPECT_TRUE(store.get(
"arm", observed + std::chrono::milliseconds(49)).fresh);
const auto stale = store.get(
"arm", observed + std::chrono::milliseconds(51));
EXPECT_FALSE(stale.fresh);
EXPECT_EQ(stale.sample_age, std::chrono::milliseconds(51));
}
TEST(SafetySnapshotStoreTest, BumpGenerationRequiresANewSample)
{
SafetySnapshotStore store;
ASSERT_TRUE(store.registerDevice(descriptor()));
ASSERT_TRUE(store.publish(sample(1, 8)));
ASSERT_EQ(store.bumpGeneration("arm"), 2U);
const auto view = store.get("arm");
EXPECT_FALSE(view.has_sample);
EXPECT_FALSE(view.fresh);
EXPECT_EQ(view.snapshot.device_generation, 2U);
EXPECT_FALSE(store.publish(sample(1, 9)));
EXPECT_TRUE(store.publish(sample(2, 1)));
}
TEST(SafetySnapshotStoreTest, WaiterObservesOnlyANewerSample)
{
SafetySnapshotStore store;
ASSERT_TRUE(store.registerDevice(descriptor()));
ASSERT_TRUE(store.publish(sample(1, 1)));
std::atomic<bool> published{false};
std::thread writer([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
published.store(store.publish(sample(1, 2)));
});
SafetySnapshotView view;
EXPECT_TRUE(store.waitForNewerSample(
"arm", 1, SafetyClock::now() + std::chrono::seconds(1), view));
writer.join();
EXPECT_TRUE(published.load());
EXPECT_EQ(view.snapshot.sample_sequence, 2U);
}
} // namespace
} // namespace cmvr::safety

View File

@ -6,7 +6,12 @@ add_library(service
grpc/src/media_activity_coordinator.cpp
grpc/src/motor_activity_coordinator.cpp
grpc/src/grpc_camera_service.cpp
grpc/src/grpc_command_transaction.cpp
grpc/src/grpc_error_logging_interceptor.cpp
grpc/src/grpc_recovery_audit.cpp
grpc/src/grpc_safety_proto.cpp
grpc/src/grpc_safety_participants.cpp
grpc/src/grpc_security.cpp
grpc/src/grpc_system_service.cpp
grpc/src/grpc_speaker_service.cpp
grpc/src/grpc_microphone_service.cpp
@ -242,6 +247,56 @@ if(BUILD_TESTING)
ENVIRONMENT "${_grpc_system_test_environment}"
)
add_executable(grpc_security_test
grpc/tests/grpc_security_test.cpp
grpc/src/grpc_security.cpp
)
target_include_directories(grpc_security_test
PRIVATE
${CMAKE_SOURCE_DIR}/cmvr-es
)
target_link_libraries(grpc_security_test
PRIVATE
cmvr_es::proto
gtest
gtest_main
pthread
)
add_test(
NAME grpc_security_test
COMMAND grpc_security_test
)
set_tests_properties(grpc_security_test PROPERTIES
TIMEOUT 10
ENVIRONMENT "${_grpc_system_test_environment}"
)
add_executable(grpc_command_transaction_test
grpc/tests/grpc_command_transaction_test.cpp
grpc/src/grpc_command_transaction.cpp
grpc/src/grpc_safety_proto.cpp
grpc/src/grpc_security.cpp
)
target_include_directories(grpc_command_transaction_test
PRIVATE
${CMAKE_SOURCE_DIR}/cmvr-es
)
target_link_libraries(grpc_command_transaction_test PRIVATE
cmvr_es::safety_coordinator
cmvr_es::proto
gtest
gtest_main
pthread
)
add_test(
NAME grpc_command_transaction_test
COMMAND grpc_command_transaction_test
)
set_tests_properties(grpc_command_transaction_test PROPERTIES
TIMEOUT 10
ENVIRONMENT "${_grpc_system_test_environment}"
)
add_executable(grpc_arm_service_test
grpc/tests/grpc_arm_service_test.cpp
)

View File

@ -9,6 +9,7 @@
#include <string>
#include "cmvr/api/system_command.pb.h"
#include "manager/safety/include/safety_types.h"
namespace cmvr::device {
class DeviceManager;
@ -59,7 +60,8 @@ public:
WaitResult submitAndWait(
const api::ActionQueueCommand_Request& request,
api::ActionQueueCommand_Feedback& feedback,
const std::function<bool()>& waiter_canceled = {});
const std::function<bool()>& waiter_canceled = {},
safety::CommandActor actor = {});
// Starts (or joins) a temporary StopAll round. New action IDs are rejected
// and queued/active actions are canceled. By default the executor also

View File

@ -38,6 +38,7 @@
#include "devices/arm/robot_arm.h"
#include "manager/control_authority/include/control_authority_manager.h"
#include "manager/device_manager/include/device_manager.h"
#include "manager/safety/include/safety_coordinator.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
namespace cmvr::service {
@ -84,6 +85,66 @@ struct PreparedAction {
std::vector<std::string> resource_ids;
};
safety::CommandDescriptor safetyDescriptorFor(
const PreparedStepKind kind)
{
using safety::CommandIntent;
using safety::SafetyPolicyFamily;
switch (kind) {
case PreparedStepKind::ArmMoveJ:
return {"/cmvr.api.ArmService/moveJ", CommandIntent::Actuate,
SafetyPolicyFamily::Control, true, false};
case PreparedStepKind::ArmMoveL:
return {"/cmvr.api.ArmService/moveL", CommandIntent::Actuate,
SafetyPolicyFamily::Control, true, false};
case PreparedStepKind::AgvNavigateToPose:
return {"/cmvr.api.AgvService/navigateToPose",
CommandIntent::Actuate, SafetyPolicyFamily::Control,
true, false};
case PreparedStepKind::AgvNavigateToStation:
return {"/cmvr.api.AgvService/navigateToStation",
CommandIntent::Actuate, SafetyPolicyFamily::Control,
true, false};
case PreparedStepKind::AgvFollowPath:
return {"/cmvr.api.AgvService/followPath",
CommandIntent::Actuate, SafetyPolicyFamily::Control,
true, false};
case PreparedStepKind::Delay:
break;
}
return {};
}
const api::CommandHeader_Request* commandHeaderFor(
const api::ActionStep& step)
{
switch (step.command_case()) {
case api::ActionStep::kArmMoveJ:
return &step.arm_move_j().header();
case api::ActionStep::kArmMoveL:
return &step.arm_move_l().header();
case api::ActionStep::kAgvNavigateToPose:
return &step.agv_navigate_to_pose().header();
case api::ActionStep::kAgvNavigateToStation:
return &step.agv_navigate_to_station().header();
case api::ActionStep::kAgvFollowPath:
return &step.agv_follow_path().header();
case api::ActionStep::kDelay:
case api::ActionStep::COMMAND_NOT_SET:
return nullptr;
}
return nullptr;
}
std::string actionStepCommandId(
const std::string& action_id,
const std::string& step_id)
{
return "action:" + std::to_string(action_id.size()) + ':' +
action_id + ":step:" + std::to_string(step_id.size()) + ':' +
step_id;
}
struct ValidationResult {
bool valid{false};
std::string error;
@ -743,6 +804,7 @@ struct ActionQueueExecutor::Impl {
api::ActionQueueCommand_Request request;
RequestFingerprint fingerprint;
PreparedAction prepared;
safety::CommandActor actor;
Clock::time_point deadline;
std::atomic<bool> cancel_requested{false};
std::atomic<bool> timed_out{false};
@ -1226,7 +1288,8 @@ struct ActionQueueExecutor::Impl {
ActionQueueExecutor::WaitResult submitAndWait(
const api::ActionQueueCommand_Request& request,
api::ActionQueueCommand_Feedback& feedback,
const std::function<bool()>& waiter_canceled)
const std::function<bool()>& waiter_canceled,
safety::CommandActor actor)
{
api::ActionDeduplicationStatus deduplication_status =
api::ACTION_DEDUPLICATION_STATUS_UNSPECIFIED;
@ -1375,6 +1438,7 @@ struct ActionQueueExecutor::Impl {
candidate->request = request;
candidate->fingerprint = *fingerprint;
candidate->prepared = validation.prepared;
candidate->actor = std::move(actor);
candidate->deadline = Clock::now() + total_timeout;
const bool canceled_before_admission =
@ -1870,6 +1934,7 @@ struct ActionQueueExecutor::Impl {
enum class StepOutcome {
Completed,
Rejected,
Failed,
Canceled,
TimedOut,
@ -1880,6 +1945,49 @@ struct ActionQueueExecutor::Impl {
std::string message;
};
safety::AdmissionResult admitStep(
const std::shared_ptr<Record>& record,
const PreparedStep& prepared,
const api::ActionStep& source,
const control::ControlLeaseToken& token,
const Clock::time_point deadline)
{
safety::AdmissionRequest request;
request.command = safetyDescriptorFor(prepared.kind);
request.actor = record->actor;
request.command_id = actionStepCommandId(
record->request.action_id(), source.step_id());
request.device_id = prepared.device_id;
if (const auto* header = commandHeaderFor(source);
header && header->has_expected_device_generation()) {
request.expected_device_generation =
header->expected_device_generation();
}
request.authority_generation = token.generation;
request.deadline = deadline;
return device_manager.safetyCoordinator().admit(request);
}
static std::string admissionFailure(
const safety::AdmissionDecision& decision)
{
return decision.detail.empty()
? std::string("ActionQueue step safety admission was rejected: ") +
safety::toString(decision.reason)
: "ActionQueue step safety admission was rejected: " +
decision.detail;
}
static std::string dispatchFailure(
const safety::HardwareCheckResult& result)
{
return result.detail.empty()
? std::string("ActionQueue step final safety check failed: ") +
safety::toString(result.reason)
: "ActionQueue step final safety check failed: " +
result.detail;
}
StepResult executeArmStep(
const std::shared_ptr<Record>& record,
const PreparedStep& prepared,
@ -1889,6 +1997,26 @@ struct ActionQueueExecutor::Impl {
{
auto& authority =
control::ControlAuthorityManager::instance();
auto safety_admission = admitStep(
record, prepared, source, token, deadline);
if (!safety_admission.permit) {
return {
StepOutcome::Rejected,
admissionFailure(safety_admission.decision)};
}
auto authority_dispatch = authority.tryBeginDispatch(token);
if (!authority_dispatch.acquired()) {
return {
StepOutcome::Canceled,
"RobotArm ActionQueue control was preempted before dispatch"};
}
auto safety_dispatch = device_manager.safetyCoordinator()
.beginDispatch(*safety_admission.permit);
if (!safety_dispatch.acquired()) {
return {
StepOutcome::Rejected,
dispatchFailure(safety_dispatch.hardwareCheck())};
}
auto cancellation_requested =
[record, token, deadline, &authority]() {
return record->cancel_requested.load(
@ -2030,6 +2158,26 @@ struct ActionQueueExecutor::Impl {
{
auto& authority =
control::ControlAuthorityManager::instance();
auto safety_admission = admitStep(
record, prepared, source, token, deadline);
if (!safety_admission.permit) {
return {
StepOutcome::Rejected,
admissionFailure(safety_admission.decision)};
}
auto authority_dispatch = authority.tryBeginDispatch(token);
if (!authority_dispatch.acquired()) {
return {
StepOutcome::Canceled,
"AGV ActionQueue control was preempted before dispatch"};
}
auto safety_dispatch = device_manager.safetyCoordinator()
.beginDispatch(*safety_admission.permit);
if (!safety_dispatch.acquired()) {
return {
StepOutcome::Rejected,
dispatchFailure(safety_dispatch.hardwareCheck())};
}
auto cancellation_requested =
[record, token, deadline, &authority]() {
return record->cancel_requested.load(
@ -2267,6 +2415,11 @@ struct ActionQueueExecutor::Impl {
case StepOutcome::Completed:
++completed_steps;
break;
case StepOutcome::Rejected:
complete(
record, api::ACTION_RESULT_CODE_REJECTED,
completed_steps, step_result.message, index);
return;
case StepOutcome::Failed:
complete(
record, api::ACTION_RESULT_CODE_FAILED,
@ -2330,10 +2483,11 @@ ActionQueueExecutor::~ActionQueueExecutor() = default;
ActionQueueExecutor::WaitResult ActionQueueExecutor::submitAndWait(
const api::ActionQueueCommand_Request& request,
api::ActionQueueCommand_Feedback& feedback,
const std::function<bool()>& waiter_canceled)
const std::function<bool()>& waiter_canceled,
safety::CommandActor actor)
{
const auto result =
impl_->submitAndWait(request, feedback, waiter_canceled);
const auto result = impl_->submitAndWait(
request, feedback, waiter_canceled, std::move(actor));
feedback.set_service_instance_id(impl_->instance_id);
return result;
}

View File

@ -3,6 +3,7 @@
#include <atomic>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
@ -21,9 +22,12 @@ public:
enum class DispatchResult {
Success,
RejectedByStopAll,
RejectedByDispatchFence,
DeviceFailure,
};
using DispatchFence = std::function<bool()>;
struct ActivityToken {
std::string device_id;
std::uint64_t activity_generation{0U};
@ -38,7 +42,8 @@ public:
DispatchResult start(
const std::string& device_id,
const std::shared_ptr<device::AbstractCamera>& camera,
ActivityToken* token = nullptr);
ActivityToken* token = nullptr,
DispatchFence dispatch_fence = {});
// Rolls back only the exact activity created by start(). A newer start for
// the same device is never stopped by an older request finishing late.
@ -48,7 +53,8 @@ public:
// serialized here so it cannot race an operational StopAll stop.
DispatchResult stopLifecycle(
const std::string& device_id,
const std::shared_ptr<device::AbstractCamera>& camera);
const std::shared_ptr<device::AbstractCamera>& camera,
DispatchFence dispatch_fence = {});
// Reconciles a lifecycle stop performed outside CameraService.
void markCameraStopped(const std::string& device_id);

View File

@ -2,6 +2,7 @@
#define CMVR_ES_CAMERA_PTZ_ACTIVITY_REGISTRY_H
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
@ -20,15 +21,19 @@ public:
enum class DispatchResult {
Success,
RejectedByStopAll,
RejectedByDispatchFence,
DeviceFailure,
};
using DispatchFence = std::function<bool()>;
DispatchResult control(
const std::string& device_id,
const std::shared_ptr<device::AbstractCamera>& camera,
device::PtzCommand command,
bool stop,
int speed);
int speed,
DispatchFence dispatch_fence = {});
// Reconciles externally stopped camera PTZ state with this registry. A
// camera backend can call this if it stops PTZ outside CameraService.

View File

@ -1,15 +1,21 @@
#ifndef CMVR_ES_GRPC_AGV_SERVICE_H
#define CMVR_ES_GRPC_AGV_SERVICE_H
#include <memory>
#include "cmvr/api/agv_service.grpc.pb.h"
#include "devices/agv/abstract_agv.h"
#include "manager/device_manager/include/device_manager.h"
namespace cmvr::service {
class GrpcSecurityGateway;
class gRPCAgvServiceImpl final : public api::AgvService::Service {
public:
gRPCAgvServiceImpl();
explicit gRPCAgvServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway);
~gRPCAgvServiceImpl() override = default;
grpc::Status getRuntimeState(grpc::ServerContext* context,
@ -79,6 +85,7 @@ public:
private:
device::DeviceManager& dmgr_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
};
} // namespace cmvr::service

View File

@ -1,14 +1,20 @@
#pragma once
#include <memory>
#include "cmvr/api/arm_service.grpc.pb.h"
#include "devices/arm/robot_arm.h"
#include "manager/device_manager/include/device_manager.h"
namespace cmvr::service {
class GrpcSecurityGateway;
class gRPCArmServiceImpl final : public api::ArmService::Service {
public:
gRPCArmServiceImpl();
explicit gRPCArmServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway);
~gRPCArmServiceImpl() override = default;
grpc::Status torqueOff(grpc::ServerContext* context,
@ -59,6 +65,7 @@ public:
private:
device::DeviceManager& dmgr_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
};
} // namespace cmvr::service

View File

@ -13,6 +13,16 @@
namespace cmvr::service {
class GrpcSecurityGateway;
} // namespace cmvr::service
namespace cmvr::safety {
class SafetyCoordinator;
}
namespace cmvr::service {
namespace arm_teleop = cmvr::api::armteleop::v1;
struct ArmTeleopBackendResult {
@ -75,7 +85,9 @@ public:
explicit ArmTeleopServiceImpl(
std::shared_ptr<ArmTeleopBackend> backend =
makeDisabledArmTeleopBackend(),
control::ControlAuthorityManager* authority = nullptr);
control::ControlAuthorityManager* authority = nullptr,
std::shared_ptr<GrpcSecurityGateway> security_gateway = nullptr,
safety::SafetyCoordinator* safety_coordinator = nullptr);
~ArmTeleopServiceImpl() override = default;
grpc::Status Teleoperate(
@ -86,6 +98,8 @@ public:
private:
std::shared_ptr<ArmTeleopBackend> backend_;
control::ControlAuthorityManager* authority_{nullptr};
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
safety::SafetyCoordinator* safety_coordinator_{nullptr};
};
} // namespace cmvr::service

View File

@ -5,6 +5,8 @@
#ifndef GRPC_CAMERA_SERVICE_H
#define GRPC_CAMERA_SERVICE_H
#include <memory>
#include "cmvr/api/camera_service.grpc.pb.h"
#include "common/base/grpc_utils.h"
#include "manager/device_manager/include/device_manager.h"
@ -13,10 +15,13 @@
namespace cmvr::service {
class GrpcSecurityGateway;
class gRPCCameraServiceImpl final: public api::CameraService::Service {
public:
explicit gRPCCameraServiceImpl(
CameraStreamLowLatencyConfig stream_config = {});
CameraStreamLowLatencyConfig stream_config = {},
std::shared_ptr<GrpcSecurityGateway> security_gateway = nullptr);
~gRPCCameraServiceImpl() override = default;
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetCameraStateCommand_Request* request, api::GetCameraStateCommand_Feedback* response) override;
grpc::Status StartCamera(grpc::ServerContext* context, const api::StartCameraCommand_Request* request, api::StartCameraCommand_Feedback* response) override;
@ -33,6 +38,7 @@ namespace cmvr::service {
private:
device::DeviceManager& dmgr_;
CameraStreamLowLatencyConfig stream_config_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
//双向流读写线程
std::shared_ptr<std::thread> read_thread_ = nullptr;

View File

@ -0,0 +1,211 @@
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <grpcpp/server_context.h>
#include <grpcpp/support/status.h>
#include <google/protobuf/message.h>
#include "cmvr/api/common.pb.h"
#include "manager/safety/include/safety_coordinator.h"
#include "service/grpc/include/grpc_security.h"
namespace cmvr::service {
grpc::Status grpcStatusForSafetyReason(
safety::SafetyReason reason,
const std::string& detail = {});
struct GrpcStreamingSafetyOpen {
std::string full_method_name;
std::string device_id;
std::string session_id;
std::string expected_service_instance_id;
std::optional<std::uint64_t> expected_device_generation;
std::uint64_t authority_generation{0};
safety::SafetyClock::time_point deadline{
safety::SafetyClock::time_point::max()};
};
// Binds a long-lived control stream to one coordinator permit. Stream
// protocols retain their own sequence, watchdog, and control-lease rules;
// this object owns the safety epoch/device-generation checks shared by all of
// them. It deliberately does not use the unary idempotency ledger.
class GrpcStreamingSafetySession final {
public:
GrpcStreamingSafetySession(
safety::SafetyCoordinator& coordinator,
const GrpcRequestContext& request_context,
GrpcStreamingSafetyOpen open);
GrpcStreamingSafetySession(
GrpcStreamingSafetySession&&) noexcept = default;
GrpcStreamingSafetySession& operator=(
GrpcStreamingSafetySession&&) noexcept = default;
GrpcStreamingSafetySession(
const GrpcStreamingSafetySession&) = delete;
GrpcStreamingSafetySession& operator=(
const GrpcStreamingSafetySession&) = delete;
bool admitted() const noexcept { return permit_.has_value(); }
const grpc::Status& status() const noexcept { return status_; }
const safety::AdmissionDecision& admissionDecision() const noexcept
{
return admission_decision_;
}
bool revalidate();
safety::DispatchGuard beginDispatch();
std::uint64_t safetyEpoch() const noexcept;
std::uint64_t deviceGeneration() const noexcept;
std::uint64_t authorityGeneration() const noexcept;
private:
void reject_(safety::SafetyReason reason, std::string detail);
safety::SafetyCoordinator* coordinator_{nullptr};
std::optional<safety::AdmissionPermit> permit_;
safety::AdmissionDecision admission_decision_;
grpc::Status status_;
};
// Owns one unary command from identity reservation through the final hardware
// dispatch fence. Legacy/Shadow calls without a command ID still use admission,
// but deliberately remain outside the idempotency ledger for wire compatibility.
class GrpcCommandTransaction final {
public:
GrpcCommandTransaction(
safety::SafetyCoordinator& coordinator,
GrpcRequestContext request_context,
GrpcMethodPolicy method_policy,
const google::protobuf::Message& request,
google::protobuf::Message& response);
~GrpcCommandTransaction() noexcept;
GrpcCommandTransaction(GrpcCommandTransaction&& other) noexcept;
GrpcCommandTransaction& operator=(
GrpcCommandTransaction&& other) noexcept;
GrpcCommandTransaction(const GrpcCommandTransaction&) = delete;
GrpcCommandTransaction& operator=(const GrpcCommandTransaction&) = delete;
bool shouldExecute() const noexcept { return should_execute_; }
const grpc::Status& status() const noexcept { return status_; }
const safety::AdmissionDecision& admissionDecision() const noexcept
{
return admission_decision_;
}
// Must be called immediately before the first driver/SDK mutation. The
// returned guard remains owned by this transaction until finish().
bool beginDispatch();
// Long-running unary commands may submit more than one hardware command.
// Revalidate the original permit between submissions, then hold the
// returned guard only around one driver/SDK mutation.
bool revalidate();
safety::DispatchGuard beginScopedDispatch();
// Internal mitigation for a command-owned activity. This obtains a fresh
// Stop-lane permit, so an expired/revoked Actuate permit cannot suppress a
// physical stop.
safety::DispatchGuard beginSafetyStopDispatch();
const grpc::Status& dispatchStatus() const noexcept
{
return dispatch_status_;
}
grpc::Status finish(
grpc::Status operation_status,
safety::SafetyReason reason = safety::SafetyReason::None,
std::optional<safety::CommandLifecycle> lifecycle = std::nullopt);
grpc::Status finishException(std::string detail) noexcept;
const std::string& deviceId() const noexcept { return device_id_; }
const std::string& commandId() const noexcept { return command_id_; }
std::uint64_t safetyEpoch() const noexcept
{
return admission_decision_.safety_epoch;
}
std::uint64_t deviceGeneration() const noexcept
{
return admission_decision_.device_generation;
}
private:
void initialize_(const google::protobuf::Message& request);
void rejectBeforeDispatch_(
safety::SafetyReason reason,
std::string detail,
grpc::Status status,
bool complete_reserved_record);
bool restoreOutcome_(const safety::CommandOutcome& outcome);
bool completeLedger_(
safety::CommandLifecycle lifecycle,
safety::SafetyReason reason,
const std::string& detail,
bool hardware_submission_possible) noexcept;
void populateFeedback_(
bool success,
safety::SafetyReason reason,
safety::CommandLifecycle lifecycle,
const std::string& detail);
void abandon_() noexcept;
safety::SafetyCoordinator* coordinator_{nullptr};
GrpcRequestContext request_context_;
GrpcMethodPolicy method_policy_;
google::protobuf::Message* response_{nullptr};
safety::CommandLedger::Ticket ledger_ticket_;
std::optional<safety::AdmissionPermit> permit_;
std::optional<safety::DispatchGuard> dispatch_guard_;
safety::AdmissionDecision admission_decision_;
grpc::Status status_;
grpc::Status dispatch_status_;
std::string device_id_;
std::string command_id_;
std::string payload_hash_;
safety::SafetyClock::time_point deadline_{
safety::SafetyClock::time_point::max()};
bool should_execute_{false};
bool owns_ledger_record_{false};
bool dispatch_started_{false};
bool completed_{false};
std::uint64_t safety_stop_sequence_{0};
};
using GrpcUnaryCommandOperation =
std::function<grpc::Status(GrpcCommandTransaction&)>;
grpc::Status executeRegisteredGrpcCommand(
const std::shared_ptr<GrpcSecurityGateway>& gateway,
grpc::ServerContext* server_context,
safety::SafetyCoordinator& coordinator,
const std::string& full_method_name,
const google::protobuf::Message* request,
google::protobuf::Message* response,
GrpcUnaryCommandOperation operation);
// For a legacy RPC whose validated request selects one of several fixed
// server-side intents. Gateway authorization still uses the registered method
// policy; the supplied policy only narrows Coordinator admission after the
// server has parsed the request (for example PTZ START versus STOP).
grpc::Status executeServerDerivedGrpcCommand(
const std::shared_ptr<GrpcSecurityGateway>& gateway,
grpc::ServerContext* server_context,
safety::SafetyCoordinator& coordinator,
const std::string& full_method_name,
GrpcMethodPolicy effective_policy,
const google::protobuf::Message* request,
google::protobuf::Message* response,
GrpcUnaryCommandOperation operation);
// Stable across processes and protobuf map iteration order. Transport identity
// fields and client timestamps are excluded; device generation remains part of
// the semantic payload.
std::string deterministicGrpcPayloadHash(
const std::string& full_method_name,
const google::protobuf::Message& request);
} // namespace cmvr::service

View File

@ -5,14 +5,19 @@
#ifndef GRPC_DEXHAND_SERVICE_H
#define GRPC_DEXHAND_SERVICE_H
#include <memory>
#include "cmvr/api/dexhand_service.grpc.pb.h"
#include "common/base/grpc_utils.h"
#include "manager/device_manager/include/device_manager.h"
#include "devices/dexhand/abstract_dexhand.h"
namespace cmvr::service {
class GrpcSecurityGateway;
class gRPCDexHandServiceImpl final: public api::DexHandService::Service {
public:
gRPCDexHandServiceImpl();
explicit gRPCDexHandServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway);
~gRPCDexHandServiceImpl() override = default;
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetDexHandStateCommand_Request* request,api::GetDexHandStateCommand_Feedback* response) override;
grpc::Status SetDexHandPos(grpc::ServerContext* context, const cmvr::api::SetDexHandPositionsCommand_Request* request, cmvr::api::SetDexHandPositionsCommand_Feedback* response) override;
@ -24,6 +29,7 @@ namespace cmvr::service {
grpc::Status GetSensorDataStream(grpc::ServerContext* context, grpc::ServerReaderWriter<cmvr::api::GetSensorDataStreamCommand_Feedback, cmvr::api::GetSensorDataStreamCommand_Request>* stream) override;
private:
device::DeviceManager& dmgr_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
};
}

View File

@ -1,15 +1,20 @@
#ifndef BIO_HEAD_SERVICE_H
#define BIO_HEAD_SERVICE_H
#include <memory>
#include "cmvr/api/biohead_service.grpc.pb.h"
#include "manager/device_manager/include/device_manager.h"
#include "devices/biohead/abstract_biohead.h"
namespace cmvr::service
{
class GrpcSecurityGateway;
class gRPCMBioHeadServiceImpl : public api::BioHeadService::Service {
public:
gRPCMBioHeadServiceImpl();
explicit gRPCMBioHeadServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway);
~gRPCMBioHeadServiceImpl() override = default;
grpc::Status SetExpression(grpc::ServerContext* context,
@ -44,6 +49,7 @@ namespace cmvr::service
grpc::Status ExpressionYawn(grpc::ServerContext* context, const cmvr::api::ExpressionYawn_Request* request, cmvr::api::ExpressionYawn_Feedback* response) override;
private:
device::DeviceManager& dmgr_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
};
} // namespace cmvr::service

View File

@ -3,15 +3,24 @@
//
#pragma once
#include <memory>
#include "cmvr/api/hlc_service.grpc.pb.h"
#include "manager/device_manager/include/device_manager.h"
namespace cmvr {
namespace service {
class GrpcSecurityGateway;
class gRPCHlcServiceImpl final : public api::HlcService::Service {
public:
gRPCHlcServiceImpl();
explicit gRPCHlcServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway);
~gRPCHlcServiceImpl() = default;
grpc::Status touch(grpc::ServerContext *context, const cmvr::api::Touch_Request *request, cmvr::api::Touch_Response *response) override;
private:
device::DeviceManager& dmgr_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
};

View File

@ -5,6 +5,7 @@
#ifndef GRPC_MICROPHONE_SERVICE_H
#define GRPC_MICROPHONE_SERVICE_H
#include <memory>
#include "cmvr/api/microphone_service.grpc.pb.h"
#include "manager/device_manager/include/device_manager.h"
#include "devices/microphone/abstract_microphone.h"
@ -12,10 +13,13 @@
namespace cmvr::service
{
class GrpcSecurityGateway;
class gRPCMicroPhoneServiceImpl: public api::MicPhoneService::Service {
public:
gRPCMicroPhoneServiceImpl();
explicit gRPCMicroPhoneServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway);
~gRPCMicroPhoneServiceImpl() override = default;
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetMicStateCommand_Request* request,api::GetMicStateCommand_Feedback* response) override;
grpc::Status StartRecord(grpc::ServerContext* context, const api::StartMicRecordingCommand_Request* request,api::StartMicRecordingCommand_Feedback* response) override;
@ -27,6 +31,7 @@ namespace cmvr::service
grpc::Status GetVolume(grpc::ServerContext* context, const api::GetMicPhoneVolumeCommand_Request* request,api::GetMicPhoneVolumeCommand_Feedback* response) override;
private:
device::DeviceManager& dmgr_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
};
}
#endif //GRPC_MICROPHONE_SERVICE_H

View File

@ -19,6 +19,9 @@ class DeviceManager;
namespace cmvr::service {
class gRPCMotorServiceImplTestAccess;
class GrpcCommandTransaction;
class GrpcSecurityGateway;
struct GrpcRequestContext;
// A deliberately thin synchronous gRPC facade over AbstractMotor. It does not
// schedule trajectories or retain asynchronous operations. The small amount of
@ -27,6 +30,8 @@ class gRPCMotorServiceImplTestAccess;
class gRPCMotorServiceImpl final : public api::MotorService::Service {
public:
gRPCMotorServiceImpl();
explicit gRPCMotorServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway);
~gRPCMotorServiceImpl() override = default;
grpc::Status setZero(grpc::ServerContext* context,
@ -136,7 +141,8 @@ private:
double max_velocity_rad_s,
double acceleration_rad_s2,
const api::MotorWaitOptions& wait,
api::MotorCommandResponse* response);
api::MotorCommandResponse* response,
GrpcCommandTransaction& command);
grpc::Status waitForPosition(grpc::ServerContext* context,
const ResolvedMotor& resolved,
std::uint64_t generation,
@ -154,39 +160,47 @@ private:
grpc::Status setZeroImpl(grpc::ServerContext* context,
const api::SetMotorZeroRequest* request,
api::MotorCommandResponse* response);
api::MotorCommandResponse* response,
GrpcCommandTransaction& command);
grpc::Status moveToZeroImpl(grpc::ServerContext* context,
const api::MoveMotorToZeroRequest* request,
api::MotorCommandResponse* response);
api::MotorCommandResponse* response,
GrpcCommandTransaction& command);
grpc::Status profilePositionImpl(
grpc::ServerContext* context,
const api::ProfilePositionRequest* request,
api::MotorCommandResponse* response);
api::MotorCommandResponse* response,
GrpcCommandTransaction& command);
grpc::Status profileVelocityImpl(
grpc::ServerContext* context,
const api::ProfileVelocityRequest* request,
api::MotorCommandResponse* response);
api::MotorCommandResponse* response,
GrpcCommandTransaction& command);
grpc::Status emergencyStopImpl(
grpc::ServerContext* context,
const api::EmergencyStopRequest* request,
api::MotorCommandResponse* response);
api::MotorCommandResponse* response,
GrpcCommandTransaction& command);
grpc::Status getStatusImpl(grpc::ServerContext* context,
const api::GetMotorStatusRequest* request,
api::GetMotorStatusResponse* response);
grpc::Status setEnabledImpl(
grpc::ServerContext* context,
const api::SetMotorEnabledRequest* request,
api::MotorCommandResponse* response);
api::MotorCommandResponse* response,
GrpcCommandTransaction& command);
grpc::Status streamCyclicPositionImpl(
grpc::ServerContext* context,
grpc::ServerReaderWriter<api::CyclicControlResponse,
api::CyclicPositionRequest>* stream,
std::optional<api::MotorTarget>& cleanup_target);
std::optional<api::MotorTarget>& cleanup_target,
const GrpcRequestContext& request_context);
grpc::Status streamCyclicVelocityImpl(
grpc::ServerContext* context,
grpc::ServerReaderWriter<api::CyclicControlResponse,
api::CyclicVelocityRequest>* stream,
std::optional<api::MotorTarget>& cleanup_target);
std::optional<api::MotorTarget>& cleanup_target,
const GrpcRequestContext& request_context);
void bestEffortQuickStop(const api::MotorTarget& target,
const std::string& error) noexcept;
@ -199,6 +213,7 @@ private:
const std::string& error) const;
device::DeviceManager& dmgr_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
mutable std::mutex states_mutex_;
mutable std::unordered_map<const device::AbstractMotor*,
MotorControlEntry> states_;

View File

@ -0,0 +1,38 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace cmvr::service {
struct RecoveryAuditRecord {
std::uint64_t occurred_at_unix_ms{0};
std::string stage;
std::string correlation_id;
std::string principal_id;
std::string peer;
std::string recovery_id;
std::string reason;
std::string mode;
bool all_devices{false};
std::vector<std::string> device_ids;
std::uint64_t expected_safety_epoch{0};
std::uint64_t previous_safety_epoch{0};
std::uint64_t current_safety_epoch{0};
std::string result;
};
class RecoveryAuditSink {
public:
virtual ~RecoveryAuditSink() = default;
virtual bool append(
const RecoveryAuditRecord& record,
std::string* error = nullptr) noexcept = 0;
};
std::shared_ptr<RecoveryAuditSink> makeFileRecoveryAuditSink(
std::string path);
} // namespace cmvr::service

View File

@ -0,0 +1,43 @@
#pragma once
#include <memory>
namespace cmvr::safety {
class SafetyCoordinator;
}
namespace cmvr::service {
class ActionQueueExecutor;
class StopOperationDispatcher;
class GrpcSafetyParticipantRegistration final {
public:
~GrpcSafetyParticipantRegistration();
GrpcSafetyParticipantRegistration(
GrpcSafetyParticipantRegistration&&) noexcept;
GrpcSafetyParticipantRegistration& operator=(
GrpcSafetyParticipantRegistration&&) noexcept;
GrpcSafetyParticipantRegistration(
const GrpcSafetyParticipantRegistration&) = delete;
GrpcSafetyParticipantRegistration& operator=(
const GrpcSafetyParticipantRegistration&) = delete;
private:
friend std::unique_ptr<GrpcSafetyParticipantRegistration>
registerGrpcSafetyParticipants(
safety::SafetyCoordinator&,
std::shared_ptr<ActionQueueExecutor>,
std::shared_ptr<StopOperationDispatcher>);
struct Impl;
explicit GrpcSafetyParticipantRegistration(std::unique_ptr<Impl> impl);
std::unique_ptr<Impl> impl_;
};
std::unique_ptr<GrpcSafetyParticipantRegistration>
registerGrpcSafetyParticipants(
safety::SafetyCoordinator& coordinator,
std::shared_ptr<ActionQueueExecutor> action_queue,
std::shared_ptr<StopOperationDispatcher> stop_dispatcher);
} // namespace cmvr::service

View File

@ -0,0 +1,37 @@
#pragma once
#include "cmvr/api/safety_command.pb.h"
#include "manager/safety/include/safety_coordinator.h"
namespace cmvr::service {
api::CommandReasonCode toApiSafetyReason(
safety::SafetyReason value) noexcept;
safety::SafetyReason fromApiSafetyReason(
api::CommandReasonCode value) noexcept;
api::SafetyTriState toApiSafetyTriState(
safety::TriState value) noexcept;
api::SafetyCondition toApiSafetyCondition(
safety::SafetyCondition value) noexcept;
api::SystemAdmissionState toApiSystemAdmissionState(
safety::SystemAdmissionState value) noexcept;
api::DeviceAdmissionState toApiDeviceAdmissionState(
safety::DeviceAdmissionState value) noexcept;
api::SafetyBlockerScope toApiSafetyBlockerScope(
safety::BlockerScope value) noexcept;
api::SafetyRecoveryRequirement toApiRecoveryRequirement(
safety::RecoveryRequirement value) noexcept;
api::SafetyOperationResult toApiRecoveryResult(
safety::RecoveryResultCode value) noexcept;
void populateDeviceSafetyState(
const safety::DeviceSafetyStateView& source,
api::DeviceSafetyStateInfo& destination);
void populateSafetyTargetResult(
const safety::SafetyTargetResult& source,
api::SafetyOperationTargetResult& destination);
void populateSafetyParticipantState(
const safety::ParticipantSafetyStateView& source,
api::SafetyParticipantStateInfo& destination);
} // namespace cmvr::service

View File

@ -0,0 +1,268 @@
#pragma once
#include <chrono>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <grpcpp/server_context.h>
#include <grpcpp/support/status.h>
#include "cmvr/config/grpc_server_config/grpc_server_config.pb.h"
#include "manager/safety/include/safety_types.h"
namespace cmvr::service {
enum class GrpcTransportSecurity {
Insecure,
ServerTls,
MutualTls,
};
enum class GrpcAuthenticationMethod {
Disabled,
StaticToken,
Jwt,
TlsClientCertificate,
};
enum class GrpcRole {
Anonymous,
Observer,
Operator,
SafetyAdmin,
};
enum class GrpcAccessClass {
Read,
Mutate,
Stop,
Recover,
};
enum class GrpcRecoveryExposure {
Disabled,
LocalOnly,
Authorized,
};
struct GrpcPrincipal {
std::string id{"anonymous"};
GrpcAuthenticationMethod method{GrpcAuthenticationMethod::Disabled};
bool authenticated{false};
std::vector<GrpcRole> roles{GrpcRole::Anonymous};
};
struct GrpcCallFacts {
std::string correlation_id;
std::string full_method_name;
std::string peer;
std::multimap<std::string, std::string> metadata;
bool transport_encrypted{false};
bool local_peer{false};
std::chrono::steady_clock::time_point received_at;
std::chrono::steady_clock::time_point deadline;
};
struct GrpcRequestContext {
std::string correlation_id;
std::string full_method_name;
std::string peer;
GrpcPrincipal principal;
bool transport_encrypted{false};
bool local_peer{false};
std::chrono::steady_clock::time_point received_at;
std::chrono::steady_clock::time_point deadline;
};
struct GrpcMethodPolicy {
std::string full_method_name;
GrpcAccessClass access{GrpcAccessClass::Read};
GrpcRole minimum_role{GrpcRole::Observer};
safety::CommandIntent command_intent{safety::CommandIntent::Observe};
safety::SafetyPolicyFamily policy_family{
safety::SafetyPolicyFamily::Sensor};
bool mutating{false};
bool safety_lane{false};
safety::CommandDescriptor commandDescriptor() const
{
return {
full_method_name,
command_intent,
policy_family,
mutating,
safety_lane};
}
};
struct GrpcSecurityRuntimeConfig {
GrpcTransportSecurity transport{GrpcTransportSecurity::Insecure};
GrpcAuthenticationMethod authentication{
GrpcAuthenticationMethod::Disabled};
GrpcRecoveryExposure recovery_exposure{
GrpcRecoveryExposure::Disabled};
bool allow_insecure_non_loopback{false};
bool insecure_non_loopback{false};
bool legacy_compatibility{false};
std::string recovery_audit_file;
};
struct GrpcSecurityConfigResult {
bool valid{false};
GrpcSecurityRuntimeConfig config;
std::string error;
std::vector<std::string> warnings;
};
GrpcSecurityConfigResult resolveGrpcSecurityConfig(
const config::GRPCServerConfig& config,
const std::string& effective_host);
bool isLocalGrpcPeer(const std::string& peer) noexcept;
bool isLoopbackGrpcHost(const std::string& host) noexcept;
const char* toString(GrpcTransportSecurity value) noexcept;
const char* toString(GrpcAuthenticationMethod value) noexcept;
const char* toString(GrpcRecoveryExposure value) noexcept;
const char* toString(GrpcRole value) noexcept;
struct GrpcAuthenticationResult {
GrpcPrincipal principal;
grpc::Status status;
bool ok() const noexcept { return status.ok(); }
};
class GrpcAuthenticationProvider {
public:
virtual ~GrpcAuthenticationProvider() = default;
virtual GrpcAuthenticationResult authenticate(
const GrpcCallFacts& facts) const = 0;
};
class DisabledGrpcAuthenticationProvider final
: public GrpcAuthenticationProvider {
public:
GrpcAuthenticationResult authenticate(
const GrpcCallFacts& facts) const override;
};
struct GrpcAuthorizationDecision {
bool allowed{false};
grpc::Status status;
};
class GrpcAuthorizationPolicy {
public:
virtual ~GrpcAuthorizationPolicy() = default;
virtual GrpcAuthorizationDecision authorize(
const GrpcRequestContext& context,
const GrpcMethodPolicy& method) const = 0;
};
class CompatibilityGrpcAuthorizationPolicy final
: public GrpcAuthorizationPolicy {
public:
explicit CompatibilityGrpcAuthorizationPolicy(
GrpcRecoveryExposure recovery_exposure);
GrpcAuthorizationDecision authorize(
const GrpcRequestContext& context,
const GrpcMethodPolicy& method) const override;
private:
GrpcRecoveryExposure recovery_exposure_;
};
class GrpcMethodPolicyRegistry final {
public:
bool registerPolicy(GrpcMethodPolicy policy);
std::optional<GrpcMethodPolicy> find(
const std::string& full_method_name) const;
std::vector<GrpcMethodPolicy> snapshot() const;
private:
std::unordered_map<std::string, GrpcMethodPolicy> policies_;
};
const GrpcMethodPolicyRegistry& defaultGrpcMethodPolicyRegistry();
struct GrpcSecurityAuditRecord {
std::string correlation_id;
std::string full_method_name;
std::string principal_id;
std::string peer;
GrpcAuthenticationMethod authentication{
GrpcAuthenticationMethod::Disabled};
GrpcAccessClass access{GrpcAccessClass::Read};
bool authenticated{false};
bool allowed{false};
grpc::StatusCode status_code{grpc::StatusCode::OK};
};
using GrpcSecurityAuditSink =
std::function<void(const GrpcSecurityAuditRecord&)>;
class GrpcCallGuard final {
public:
GrpcCallGuard(GrpcRequestContext context,
GrpcAuthorizationDecision decision);
bool allowed() const noexcept { return decision_.allowed; }
const grpc::Status& status() const noexcept { return decision_.status; }
const GrpcRequestContext& context() const noexcept { return context_; }
private:
GrpcRequestContext context_;
GrpcAuthorizationDecision decision_;
};
class GrpcSecurityGateway final {
public:
GrpcSecurityGateway(
GrpcSecurityRuntimeConfig config,
std::shared_ptr<const GrpcAuthenticationProvider> authentication,
std::shared_ptr<const GrpcAuthorizationPolicy> authorization,
GrpcSecurityAuditSink audit_sink = {});
GrpcCallGuard beginCall(
grpc::ServerContext* server_context,
const GrpcMethodPolicy& method) const;
GrpcCallGuard beginCall(
GrpcCallFacts facts,
const GrpcMethodPolicy& method) const;
const GrpcSecurityRuntimeConfig& config() const noexcept { return config_; }
private:
GrpcSecurityRuntimeConfig config_;
std::shared_ptr<const GrpcAuthenticationProvider> authentication_;
std::shared_ptr<const GrpcAuthorizationPolicy> authorization_;
GrpcSecurityAuditSink audit_sink_;
};
std::shared_ptr<GrpcSecurityGateway> makeGrpcSecurityGateway(
const GrpcSecurityRuntimeConfig& config,
GrpcSecurityAuditSink audit_sink = {});
std::shared_ptr<GrpcSecurityGateway> makeDefaultGrpcSecurityGateway();
GrpcCallGuard beginRegisteredGrpcCall(
const std::shared_ptr<GrpcSecurityGateway>& gateway,
grpc::ServerContext* server_context,
const std::string& full_method_name);
} // namespace cmvr::service
#define CMVR_GRPC_REQUIRE_REGISTERED_CALL(gateway, server_context, method) \
const auto cmvr_grpc_call_guard = \
::cmvr::service::beginRegisteredGrpcCall( \
gateway, server_context, method); \
if (!cmvr_grpc_call_guard.allowed()) { \
return cmvr_grpc_call_guard.status(); \
}

View File

@ -5,15 +5,20 @@
#ifndef GRPC_SPEAKER_SERVICE_H
#define GRPC_SPEAKER_SERVICE_H
#include <memory>
#include "cmvr/api/speaker_service.grpc.pb.h"
#include "manager/device_manager/include/device_manager.h"
#include "devices/speaker/abstract_speaker.h"
#include "common/base/grpc_utils.h"
namespace cmvr::service {
class GrpcSecurityGateway;
class gRPCSpeakerServiceImpl: public api::SpeakerService::Service {
public:
gRPCSpeakerServiceImpl();
explicit gRPCSpeakerServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway);
~gRPCSpeakerServiceImpl() override = default;
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetSpeakerStateCommand_Request* request,api::GetSpeakerStateCommand_Feedback* response) override;
grpc::Status PlayAudio(grpc::ServerContext* context, const api::PlayAudioCommand_Request* request,api::PlayAudioCommand_Feedback* response) override;
@ -25,6 +30,7 @@ namespace cmvr::service {
grpc::Status GetVolume(grpc::ServerContext* context, const api::GetSpeakerVolumeCommand_Request* request,api::GetSpeakerVolumeCommand_Feedback* response) override;
private:
device::DeviceManager& dmgr_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
};
}

View File

@ -15,6 +15,9 @@
namespace cmvr::service
{
class ActionQueueExecutor;
class RecoveryAuditSink;
class GrpcSafetyParticipantRegistration;
class GrpcSecurityGateway;
class StopOperationDispatcher;
class gRPCSystemServiceImpl: public api::SystemService::Service {
@ -22,6 +25,15 @@ namespace cmvr::service
gRPCSystemServiceImpl();
explicit gRPCSystemServiceImpl(
std::chrono::milliseconds stop_timeout);
explicit gRPCSystemServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway);
gRPCSystemServiceImpl(
std::chrono::milliseconds stop_timeout,
std::shared_ptr<GrpcSecurityGateway> security_gateway);
gRPCSystemServiceImpl(
std::chrono::milliseconds stop_timeout,
std::shared_ptr<GrpcSecurityGateway> security_gateway,
std::shared_ptr<RecoveryAuditSink> recovery_audit_sink);
~gRPCSystemServiceImpl() override;
// Exposed only to synchronize lifecycle concurrency tests.
static bool waitForStopDispatcherDestructionForTesting(
@ -36,13 +48,19 @@ namespace cmvr::service
grpc::Status UpdateParams(grpc::ServerContext* context, const cmvr::api::UpdateParamsCommand_Request* request, cmvr::api::UpdateParamsCommand_Feedback* response) override;
grpc::Status StopAll(grpc::ServerContext* context, const cmvr::api::StopAllCommand_Request* request, cmvr::api::StopAllCommand_Feedback* response) override;
grpc::Status ExecuteActionQueue(grpc::ServerContext* context, const cmvr::api::ActionQueueCommand_Request* request, cmvr::api::ActionQueueCommand_Feedback* response) override;
grpc::Status GetSafetyState(grpc::ServerContext* context, const cmvr::api::GetSafetyStateCommand_Request* request, cmvr::api::GetSafetyStateCommand_Feedback* response) override;
grpc::Status RecoverSafetyState(grpc::ServerContext* context, const cmvr::api::RecoverSafetyStateCommand_Request* request, cmvr::api::RecoverSafetyStateCommand_Feedback* response) override;
private:
device::DeviceManager& dmgr_;
const std::chrono::milliseconds stop_timeout_;
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
std::shared_ptr<RecoveryAuditSink> recovery_audit_sink_;
// Process instances share running jobs through a lifecycle registry.
// The last service owner joins every worker before replacement.
std::shared_ptr<StopOperationDispatcher> stop_dispatcher_;
std::unique_ptr<ActionQueueExecutor> action_queue_;
std::shared_ptr<ActionQueueExecutor> action_queue_;
std::unique_ptr<GrpcSafetyParticipantRegistration>
safety_participant_registration_;
};
}

View File

@ -33,6 +33,12 @@ public:
}
};
struct FinishStopAllResult {
bool ticket_consumed{false};
bool participant_stopped{false};
bool admission_resumed{false};
};
class Session final {
public:
Session() = default;
@ -108,6 +114,14 @@ public:
const StopAllTicket& ticket,
bool all_media_stopped);
FinishStopAllResult finishStopAllDetailed(
const StopAllTicket& ticket,
bool all_media_stopped);
// Test/process teardown hook. Runtime recovery must use a new verified
// StopAll or Recover transaction instead of bypassing this latch.
void clearForTesting() noexcept;
private:
std::shared_ptr<Impl> impl_;
};

View File

@ -35,6 +35,12 @@ public:
}
};
struct FinishStopAllResult {
bool ticket_consumed{false};
bool participant_stopped{false};
bool admission_resumed{false};
};
class Registration final {
public:
Registration() = default;
@ -131,6 +137,10 @@ public:
const StopAllTicket& ticket,
bool all_motors_stopped);
FinishStopAllResult finishStopAllDetailed(
const StopAllTicket& ticket,
bool all_motors_stopped);
// Wakes StopAll after a MotorControlState releases or changes ownership.
void notifyStateChanged() noexcept;

View File

@ -11,6 +11,7 @@ namespace {
template <typename Operation>
CameraOperationalActivityRegistry::DispatchResult dispatchIfAdmitted(
std::mutex& device_mutex,
const CameraOperationalActivityRegistry::DispatchFence& dispatch_fence,
Operation&& operation)
{
std::uint64_t admitted_generation = 0U;
@ -33,6 +34,11 @@ CameraOperationalActivityRegistry::DispatchResult dispatchIfAdmitted(
}
}
if (dispatch_fence && !dispatch_fence()) {
return CameraOperationalActivityRegistry::DispatchResult::
RejectedByDispatchFence;
}
return operation();
}
@ -61,7 +67,8 @@ CameraOperationalActivityRegistry::DispatchResult
CameraOperationalActivityRegistry::start(
const std::string& device_id,
const std::shared_ptr<device::AbstractCamera>& camera,
ActivityToken* token)
ActivityToken* token,
DispatchFence dispatch_fence)
{
if (token) {
*token = {};
@ -71,7 +78,7 @@ CameraOperationalActivityRegistry::start(
}
const auto state = stateForDevice(device_id, true);
return dispatchIfAdmitted(state->mutex, [&] {
return dispatchIfAdmitted(state->mutex, dispatch_fence, [&] {
const auto previous_camera = state->active_camera;
const bool was_active = state->active.load(std::memory_order_acquire);
if (was_active && previous_camera != camera) {
@ -153,14 +160,15 @@ bool CameraOperationalActivityRegistry::stopIfCurrent(
CameraOperationalActivityRegistry::DispatchResult
CameraOperationalActivityRegistry::stopLifecycle(
const std::string& device_id,
const std::shared_ptr<device::AbstractCamera>& camera)
const std::shared_ptr<device::AbstractCamera>& camera,
DispatchFence dispatch_fence)
{
if (device_id.empty() || !camera) {
return DispatchResult::DeviceFailure;
}
const auto state = stateForDevice(device_id, true);
return dispatchIfAdmitted(state->mutex, [&] {
return dispatchIfAdmitted(state->mutex, dispatch_fence, [&] {
if (!camera->stop()) {
return DispatchResult::DeviceFailure;
}

View File

@ -33,17 +33,18 @@ CameraPtzActivityRegistry::control(
const std::shared_ptr<device::AbstractCamera>& camera,
const device::PtzCommand command,
const bool stop,
const int speed)
const int speed,
DispatchFence dispatch_fence)
{
if (device_id.empty() || !camera) {
return DispatchResult::DeviceFailure;
}
// Check admission on both sides of the per-device dispatch queue. A
// command admitted before StopAll but still queued is rejected; one already
// in device I/O is completed before that device's stop begins.
// START checks admission on both sides of the per-device dispatch queue. A
// STOP is a safety-lane operation and remains available while StopAll is
// latched; its Coordinator dispatch fence still runs under the same queue.
std::uint64_t admitted_generation = 0U;
{
if (!stop) {
auto admission = globalStopAllAdmissionGate().lockAdmission();
if (!admission.accepting()) {
return DispatchResult::RejectedByStopAll;
@ -53,7 +54,7 @@ CameraPtzActivityRegistry::control(
const auto state = stateForDevice(device_id, true);
std::lock_guard dispatch_lock(state->mutex);
{
if (!stop) {
auto admission = globalStopAllAdmissionGate().lockAdmission();
if (!admission.accepting() ||
admission.generation() != admitted_generation) {
@ -61,6 +62,14 @@ CameraPtzActivityRegistry::control(
}
}
// The service-level safety transaction performs its final epoch,
// generation, and hardware-state validation here. Keeping the fence under
// the per-device lock prevents a request that waited in this queue from
// dispatching with a stale admission permit.
if (dispatch_fence && !dispatch_fence()) {
return DispatchResult::RejectedByDispatchFence;
}
if (!camera->controlPtz(command, stop, speed)) {
return DispatchResult::DeviceFailure;
}

View File

@ -12,6 +12,8 @@
#include "common/base/logging/logger.h"
#include "manager/control_authority/include/control_authority_manager.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/grpc_security.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
using google::protobuf::util::TimeUtil;
@ -605,14 +607,25 @@ void fillUnifiedMapUpdate(msgs::AgvUnifiedMapUpdate* dst, const device::AgvUnifi
} // namespace
gRPCAgvServiceImpl::gRPCAgvServiceImpl()
: dmgr_(device::DeviceManager::getInstance())
: gRPCAgvServiceImpl(makeDefaultGrpcSecurityGateway())
{
}
grpc::Status gRPCAgvServiceImpl::getRuntimeState(grpc::ServerContext*,
gRPCAgvServiceImpl::gRPCAgvServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: dmgr_(device::DeviceManager::getInstance()),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway())
{
}
grpc::Status gRPCAgvServiceImpl::getRuntimeState(grpc::ServerContext* context,
const api::AgvRuntimeStateCommand_Request* request,
api::AgvRuntimeStateCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.AgvService/getRuntimeState");
try {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
@ -628,10 +641,13 @@ grpc::Status gRPCAgvServiceImpl::getRuntimeState(grpc::ServerContext*,
}
}
grpc::Status gRPCAgvServiceImpl::getNavigationStatus(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::getNavigationStatus(grpc::ServerContext* context,
const api::AgvNavigationStatusCommand_Request* request,
api::AgvNavigationStatusCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.AgvService/getNavigationStatus");
try {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
@ -647,11 +663,14 @@ grpc::Status gRPCAgvServiceImpl::getNavigationStatus(grpc::ServerContext*,
}
}
grpc::Status gRPCAgvServiceImpl::emergencyStop(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::emergencyStop(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/emergencyStop", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -663,20 +682,23 @@ grpc::Status gRPCAgvServiceImpl::emergencyStop(grpc::ServerContext*,
return setControlLeaseConflict(
response, device_id, control_barrier.detail());
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return executeConfirmedAgvStop(
response, agv, control_barrier, "emergencyStop",
[&agv]() { return agv->emergencyStop(); });
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCAgvServiceImpl::clearFault(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::clearFault(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/clearFault", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -693,18 +715,21 @@ grpc::Status gRPCAgvServiceImpl::clearFault(grpc::ServerContext*,
return setControlDispatchFailure(
response, device_id, control_lease, "clearFault");
}
return setResponseResult(response, agv->clearFault());
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(response, agv->clearFault());
});
}
grpc::Status gRPCAgvServiceImpl::navigateToPose(grpc::ServerContext* context,
const api::AgvNavigateToPoseCommand_Request* request,
api::AgvNavigateToPoseCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/navigateToPose", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
if (context && context->IsCancelled()) {
return setNavigationRequestCanceled(response);
}
@ -719,27 +744,31 @@ grpc::Status gRPCAgvServiceImpl::navigateToPose(grpc::ServerContext* context,
return setControlAdmissionFailure(
response, device_id, control_lease);
}
if (!control_lease.current()) {
auto dispatch = control_lease.tryBeginDispatch();
if (!dispatch.acquired()) {
return setControlDispatchFailure(
response, device_id, control_lease, "navigateToPose");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(response, agv->navigateToPose(
toPose2d(request->pose()),
toMotionOptions(
request->options(),
control_lease.cancellationRequested(context)),
toAdapterParams(request->adapter_params())));
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCAgvServiceImpl::navigateToStation(grpc::ServerContext* context,
const api::AgvNavigateToStationCommand_Request* request,
api::AgvNavigateToStationCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/navigateToStation", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
if (context && context->IsCancelled()) {
return setNavigationRequestCanceled(response);
}
@ -754,28 +783,32 @@ grpc::Status gRPCAgvServiceImpl::navigateToStation(grpc::ServerContext* context,
return setControlAdmissionFailure(
response, device_id, control_lease);
}
if (!control_lease.current()) {
auto dispatch = control_lease.tryBeginDispatch();
if (!dispatch.acquired()) {
return setControlDispatchFailure(
response, device_id, control_lease,
"navigateToStation");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(response, agv->navigateToStation(
request->station_id(),
toMotionOptions(
request->options(),
control_lease.cancellationRequested(context)),
toAdapterParams(request->adapter_params())));
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCAgvServiceImpl::followPath(grpc::ServerContext* context,
const api::AgvFollowPathCommand_Request* request,
api::AgvFollowPathCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/followPath", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
if (context && context->IsCancelled()) {
return setNavigationRequestCanceled(response);
}
@ -790,7 +823,8 @@ grpc::Status gRPCAgvServiceImpl::followPath(grpc::ServerContext* context,
return setControlAdmissionFailure(
response, device_id, control_lease);
}
if (!control_lease.current()) {
auto dispatch = control_lease.tryBeginDispatch();
if (!dispatch.acquired()) {
return setControlDispatchFailure(
response, device_id, control_lease, "followPath");
}
@ -799,6 +833,9 @@ grpc::Status gRPCAgvServiceImpl::followPath(grpc::ServerContext* context,
for (const auto& segment : request->path()) {
path.push_back(toPathSegment(segment));
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(
response,
agv->followPath(
@ -806,10 +843,7 @@ grpc::Status gRPCAgvServiceImpl::followPath(grpc::ServerContext* context,
toMotionOptions(
request->options(),
control_lease.cancellationRequested(context))));
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
@ -819,7 +853,10 @@ grpc::Status gRPCAgvServiceImpl::translate(
const api::AgvTranslateCommand_Request* request,
api::AgvTranslateCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/translate", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
if (context && context->IsCancelled()) {
return setNavigationRequestCanceled(response);
}
@ -839,24 +876,25 @@ grpc::Status gRPCAgvServiceImpl::translate(
return setControlDispatchFailure(
response, device_id, control_lease, "translate");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(
response,
agv->translate(toTranslation(request->translation())));
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(
grpc::StatusCode::INTERNAL,
e.what());
}
});
}
grpc::Status gRPCAgvServiceImpl::pauseNavigation(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::pauseNavigation(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/pauseNavigation", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -874,18 +912,21 @@ grpc::Status gRPCAgvServiceImpl::pauseNavigation(grpc::ServerContext*,
response, device_id, control_lease,
"pauseNavigation");
}
return setResponseResult(response, agv->pauseNavigation());
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(response, agv->pauseNavigation());
});
}
grpc::Status gRPCAgvServiceImpl::resumeNavigation(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::resumeNavigation(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/resumeNavigation", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -903,18 +944,21 @@ grpc::Status gRPCAgvServiceImpl::resumeNavigation(grpc::ServerContext*,
response, device_id, control_lease,
"resumeNavigation");
}
return setResponseResult(response, agv->resumeNavigation());
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(response, agv->resumeNavigation());
});
}
grpc::Status gRPCAgvServiceImpl::cancelNavigation(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::cancelNavigation(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/cancelNavigation", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -926,20 +970,23 @@ grpc::Status gRPCAgvServiceImpl::cancelNavigation(grpc::ServerContext*,
return setControlLeaseConflict(
response, device_id, control_barrier.detail());
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return executeConfirmedAgvStop(
response, agv, control_barrier, "cancelNavigation",
[&agv]() { return agv->cancelNavigation(); });
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCAgvServiceImpl::setVelocity(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::setVelocity(grpc::ServerContext* context,
const api::AgvSetVelocityCommand_Request* request,
api::AgvSetVelocityCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/setVelocity", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -956,18 +1003,21 @@ grpc::Status gRPCAgvServiceImpl::setVelocity(grpc::ServerContext*,
return setControlDispatchFailure(
response, device_id, control_lease, "setVelocity");
}
return setResponseResult(response, agv->setVelocity(toVelocity(request->velocity())));
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(response, agv->setVelocity(toVelocity(request->velocity())));
});
}
grpc::Status gRPCAgvServiceImpl::stopVelocityControl(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::stopVelocityControl(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/stopVelocityControl", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -979,19 +1029,21 @@ grpc::Status gRPCAgvServiceImpl::stopVelocityControl(grpc::ServerContext*,
return setControlLeaseConflict(
response, device_id, control_barrier.detail());
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return executeConfirmedAgvStop(
response, agv, control_barrier, "stopVelocityControl",
[&agv]() { return agv->stopVelocityControl(); });
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCAgvServiceImpl::listMaps(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::listMaps(grpc::ServerContext* context,
const api::AgvListMapsCommand_Request* request,
api::AgvListMapsCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.AgvService/listMaps");
try {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
@ -1012,10 +1064,12 @@ grpc::Status gRPCAgvServiceImpl::listMaps(grpc::ServerContext*,
}
}
grpc::Status gRPCAgvServiceImpl::listStations(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::listStations(grpc::ServerContext* context,
const api::AgvListStationsCommand_Request* request,
api::AgvListStationsCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.AgvService/listStations");
try {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
@ -1036,11 +1090,14 @@ grpc::Status gRPCAgvServiceImpl::listStations(grpc::ServerContext*,
}
}
grpc::Status gRPCAgvServiceImpl::switchMap(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::switchMap(grpc::ServerContext* context,
const api::AgvMapCommand_Request* request,
api::AgvMapCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/switchMap", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -1057,18 +1114,21 @@ grpc::Status gRPCAgvServiceImpl::switchMap(grpc::ServerContext*,
return setControlDispatchFailure(
response, device_id, control_lease, "switchMap");
}
return setResponseResult(response, agv->switchMap(request->map_name()));
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(response, agv->switchMap(request->map_name()));
});
}
grpc::Status gRPCAgvServiceImpl::uploadMap(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::uploadMap(grpc::ServerContext* context,
const api::AgvMapCommand_Request* request,
api::AgvMapCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/uploadMap", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -1085,17 +1145,19 @@ grpc::Status gRPCAgvServiceImpl::uploadMap(grpc::ServerContext*,
return setControlDispatchFailure(
response, device_id, control_lease, "uploadMap");
}
return setResponseResult(response, agv->uploadMap(request->map_name(), request->content()));
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(response, agv->uploadMap(request->map_name(), request->content()));
});
}
grpc::Status gRPCAgvServiceImpl::downloadMap(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::downloadMap(grpc::ServerContext* context,
const api::AgvMapCommand_Request* request,
api::AgvMapCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.AgvService/downloadMap");
try {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
@ -1114,11 +1176,14 @@ grpc::Status gRPCAgvServiceImpl::downloadMap(grpc::ServerContext*,
}
}
grpc::Status gRPCAgvServiceImpl::startMapping(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::startMapping(grpc::ServerContext* context,
const api::AgvStartMappingCommand_Request* request,
api::AgvStartMappingCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/startMapping", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -1139,21 +1204,23 @@ grpc::Status gRPCAgvServiceImpl::startMapping(grpc::ServerContext*,
options.dimension = toMapDimension(request->dimension());
options.map_name = request->map_name();
options.real_time = request->real_time();
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
const auto result = agv->startMapping(options);
if (result.ok()) {
response->set_session_id(device_id + "_mapping");
}
return setResponseResult(response, result);
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCAgvServiceImpl::streamMap(grpc::ServerContext* context,
const api::AgvMapStreamCommand_Request* request,
grpc::ServerWriter<api::AgvMapStreamCommand_Feedback>* writer)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.AgvService/streamMap");
try {
const std::string device_id = request->header().device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
@ -1220,11 +1287,14 @@ grpc::Status gRPCAgvServiceImpl::streamMap(grpc::ServerContext* context,
}
}
grpc::Status gRPCAgvServiceImpl::stopMapping(grpc::ServerContext*,
grpc::Status gRPCAgvServiceImpl::stopMapping(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.AgvService/stopMapping", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
if (!agv) {
@ -1241,11 +1311,11 @@ grpc::Status gRPCAgvServiceImpl::stopMapping(grpc::ServerContext*,
return setControlDispatchFailure(
response, device_id, control_lease, "stopMapping");
}
return setResponseResult(response, agv->stopMapping());
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
return setResponseResult(response, agv->stopMapping());
});
}
} // namespace cmvr::service

View File

@ -8,6 +8,8 @@
#include "common/base/logging/logger.h"
#include "manager/control_authority/include/control_authority_manager.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/grpc_security.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
using google::protobuf::util::TimeUtil;
@ -361,15 +363,27 @@ grpc::Status setControlDispatchFailure(
} // namespace
gRPCArmServiceImpl::gRPCArmServiceImpl()
: dmgr_(device::DeviceManager::getInstance())
: gRPCArmServiceImpl(makeDefaultGrpcSecurityGateway())
{
}
grpc::Status gRPCArmServiceImpl::torqueOff(grpc::ServerContext*,
gRPCArmServiceImpl::gRPCArmServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: dmgr_(device::DeviceManager::getInstance()),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway())
{
}
grpc::Status gRPCArmServiceImpl::torqueOff(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/torqueOff", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -381,6 +395,9 @@ grpc::Status gRPCArmServiceImpl::torqueOff(grpc::ServerContext*,
return setControlLeaseConflict(
response, device_id, control_barrier.detail());
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
const auto result = executeConfirmedArmStop(
control_barrier,
"torqueOff",
@ -390,17 +407,17 @@ grpc::Status gRPCArmServiceImpl::torqueOff(grpc::ServerContext*,
logRpcSuccess("torqueOff", device_id);
}
return resultToStatus(result);
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCArmServiceImpl::torqueOn(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/torqueOn", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -417,6 +434,9 @@ grpc::Status gRPCArmServiceImpl::torqueOn(grpc::ServerContext* context,
return setControlDispatchFailure(
response, device_id, control_lease, "torqueOn");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
const auto cancellation_requested =
control_lease.cancellationRequested(context);
const auto result = arm->torqueOn(cancellation_requested);
@ -443,17 +463,17 @@ grpc::Status gRPCArmServiceImpl::torqueOn(grpc::ServerContext* context,
logRpcSuccess("torqueOn", device_id);
}
return resultToStatus(result);
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCArmServiceImpl::moveJ(grpc::ServerContext* context,
const api::MoveJ_Request* request,
api::MoveJ_Response* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/moveJ", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -465,10 +485,14 @@ grpc::Status gRPCArmServiceImpl::moveJ(grpc::ServerContext* context,
return setControlAdmissionFailure(
response, device_id, control_lease);
}
if (!control_lease.current()) {
auto dispatch = control_lease.tryBeginDispatch();
if (!dispatch.acquired()) {
return setControlDispatchFailure(
response, device_id, control_lease, "moveJ");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
auto options = toMotionOptions(
request->options(),
control_lease.cancellationRequested(context));
@ -479,17 +503,17 @@ grpc::Status gRPCArmServiceImpl::moveJ(grpc::ServerContext* context,
<< ", positions=" << request->target().position_size();
}
return setResponseResult(response, result);
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCArmServiceImpl::moveL(grpc::ServerContext* context,
const api::MoveL_Request* request,
api::MoveL_Response* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/moveL", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -501,10 +525,14 @@ grpc::Status gRPCArmServiceImpl::moveL(grpc::ServerContext* context,
return setControlAdmissionFailure(
response, device_id, control_lease);
}
if (!control_lease.current()) {
auto dispatch = control_lease.tryBeginDispatch();
if (!dispatch.acquired()) {
return setControlDispatchFailure(
response, device_id, control_lease, "moveL");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
auto options = toMotionOptions(
request->options(),
control_lease.cancellationRequested(context));
@ -517,17 +545,17 @@ grpc::Status gRPCArmServiceImpl::moveL(grpc::ServerContext* context,
<< ", frame=" << request->frame();
}
return setResponseResult(response, result);
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCArmServiceImpl::speedJ(grpc::ServerContext*,
grpc::Status gRPCArmServiceImpl::speedJ(grpc::ServerContext* context,
const api::SpeedJ_Request* request,
api::SpeedJ_Response* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/speedJ", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -539,10 +567,14 @@ grpc::Status gRPCArmServiceImpl::speedJ(grpc::ServerContext*,
return setControlAdmissionFailure(
response, device_id, control_lease);
}
if (!control_lease.current()) {
auto dispatch = control_lease.tryBeginDispatch();
if (!dispatch.acquired()) {
return setControlDispatchFailure(
response, device_id, control_lease, "speedJ");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
const auto result = arm->speedJ(toJointVelocityCommand(request->velocity()),
request->acceleration(),
request->duration());
@ -553,17 +585,17 @@ grpc::Status gRPCArmServiceImpl::speedJ(grpc::ServerContext*,
<< ", duration=" << request->duration();
}
return setResponseResult(response, result);
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCArmServiceImpl::speedL(grpc::ServerContext*,
grpc::Status gRPCArmServiceImpl::speedL(grpc::ServerContext* context,
const api::SpeedL_Request* request,
api::SpeedL_Response* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/speedL", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -575,10 +607,14 @@ grpc::Status gRPCArmServiceImpl::speedL(grpc::ServerContext*,
return setControlAdmissionFailure(
response, device_id, control_lease);
}
if (!control_lease.current()) {
auto dispatch = control_lease.tryBeginDispatch();
if (!dispatch.acquired()) {
return setControlDispatchFailure(
response, device_id, control_lease, "speedL");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
const auto result = arm->speedL(toCartesianVelocity(request->velocity()),
request->acceleration(),
request->duration(),
@ -590,17 +626,17 @@ grpc::Status gRPCArmServiceImpl::speedL(grpc::ServerContext*,
<< ", frame=" << request->frame();
}
return setResponseResult(response, result);
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCArmServiceImpl::servoJ(grpc::ServerContext*,
grpc::Status gRPCArmServiceImpl::servoJ(grpc::ServerContext* context,
const api::ServoJ_Request* request,
api::ServoJ_Response* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/servoJ", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -617,23 +653,26 @@ grpc::Status gRPCArmServiceImpl::servoJ(grpc::ServerContext*,
return setControlDispatchFailure(
response, device_id, control_lease, "servoJ");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
const auto result = arm->servoJ(toJointPositionCommand(request->target()));
if (result.ok()) {
CMVR_LOG(DEBUG) << "[gRPCArmServiceImpl] (servoJ): success, id=" << device_id
<< ", positions=" << request->target().position_size();
}
return setResponseResult(response, result);
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCArmServiceImpl::stopMotion(grpc::ServerContext*,
grpc::Status gRPCArmServiceImpl::stopMotion(grpc::ServerContext* context,
const api::CommandHeader_Request* request,
api::CommandHeader_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/stopMotion", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -645,6 +684,9 @@ grpc::Status gRPCArmServiceImpl::stopMotion(grpc::ServerContext*,
return setControlLeaseConflict(
response, device_id, control_barrier.detail());
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
const auto result = executeConfirmedArmStop(
control_barrier,
"stopMotion",
@ -654,16 +696,15 @@ grpc::Status gRPCArmServiceImpl::stopMotion(grpc::ServerContext*,
logRpcSuccess("stopMotion", device_id);
}
return resultToStatus(result);
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCArmServiceImpl::getJointState(grpc::ServerContext*,
grpc::Status gRPCArmServiceImpl::getJointState(grpc::ServerContext* context,
const api::JointRequest* request,
api::JointResponse* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.ArmService/getJointState");
try {
const std::string device_id = request->header().device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
@ -690,10 +731,12 @@ grpc::Status gRPCArmServiceImpl::getJointState(grpc::ServerContext*,
}
}
grpc::Status gRPCArmServiceImpl::getPose(grpc::ServerContext*,
grpc::Status gRPCArmServiceImpl::getPose(grpc::ServerContext* context,
const api::GetPose_Request* request,
api::GetPose_Response* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.ArmService/getPose");
try {
const std::string device_id = request->header().device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
@ -715,11 +758,14 @@ grpc::Status gRPCArmServiceImpl::getPose(grpc::ServerContext*,
}
}
grpc::Status gRPCArmServiceImpl::calibrateZeroQ(grpc::ServerContext*,
grpc::Status gRPCArmServiceImpl::calibrateZeroQ(grpc::ServerContext* context,
const api::CalibrateZeroQ_Request* request,
api::CalibrateZeroQ_Response* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/calibrateZeroQ", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -731,44 +777,53 @@ grpc::Status gRPCArmServiceImpl::calibrateZeroQ(grpc::ServerContext*,
return setControlAdmissionFailure(
response, device_id, control_lease);
}
if (!control_lease.current()) {
auto dispatch = control_lease.tryBeginDispatch();
if (!dispatch.acquired()) {
return setControlDispatchFailure(
response, device_id, control_lease, "calibrateZeroQ");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
const auto result = arm->calibrateZeroQ(request->joint_name());
if (result.ok()) {
CMVR_LOG(DEBUG) << "[gRPCArmServiceImpl] (calibrateZeroQ): success, id=" << device_id
<< ", joint=" << request->joint_name();
}
return setResponseResult(response, result);
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
grpc::Status gRPCArmServiceImpl::getPoseMatrix(grpc::ServerContext*,
grpc::Status gRPCArmServiceImpl::getPoseMatrix(grpc::ServerContext* context,
const api::GetPoseMatrix_Request*,
api::GetPoseMatrix_Response* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.ArmService/getPoseMatrix");
fillFeedback(response->mutable_header(), false, "getPoseMatrix is not implemented");
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "getPoseMatrix is not implemented");
}
grpc::Status gRPCArmServiceImpl::computeForwardKinematics(grpc::ServerContext*,
grpc::Status gRPCArmServiceImpl::computeForwardKinematics(grpc::ServerContext* context,
const api::ComputeForwardKinematics_Request*,
api::ComputeForwardKinematics_Response* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.ArmService/computeForwardKinematics");
fillFeedback(response->mutable_header(), false, "computeForwardKinematics is not implemented");
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "computeForwardKinematics is not implemented");
}
grpc::Status gRPCArmServiceImpl::ExecuteJsonCommand(
grpc::ServerContext*,
grpc::ServerContext* context,
const api::JsonDeviceCommand_Request* request,
api::JsonDeviceCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/ExecuteJsonCommand", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->header().device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -785,11 +840,15 @@ grpc::Status gRPCArmServiceImpl::ExecuteJsonCommand(
return setControlAdmissionFailure(
response, device_id, control_lease);
}
if (!control_lease.current()) {
auto dispatch = control_lease.tryBeginDispatch();
if (!dispatch.acquired()) {
return setControlDispatchFailure(
response, device_id, control_lease,
"ExecuteJsonCommand");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
std::string response_json;
const bool success = arm->executeJsonCommand(
@ -803,17 +862,17 @@ grpc::Status gRPCArmServiceImpl::ExecuteJsonCommand(
logRpcSuccess("ExecuteJsonCommand", device_id);
}
return grpc::Status::OK;
} catch (const std::exception& e) {
fillFeedback(response->mutable_header(), false, e.what());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCArmServiceImpl::clearFault(grpc::ServerContext *context,
const cmvr::api::CommandHeader_Request *request,
cmvr::api::CommandHeader_Feedback *response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.ArmService/clearFault", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const std::string device_id = request->device_id();
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
if (!arm) {
@ -830,12 +889,12 @@ grpc::Status gRPCArmServiceImpl::clearFault(grpc::ServerContext *context,
return setControlDispatchFailure(
response, device_id, control_lease, "clearFault");
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
const auto result = arm->clearFault();
fillFeedback(response, result.ok(), result.ok() ? "" : result.message);
return resultToStatus(result);
} catch (const std::exception& e) {
fillFeedback(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
}
});
}
} // namespace cmvr::service

View File

@ -15,6 +15,8 @@
#include <utility>
#include "service/stop_all/include/stop_all_admission_gate.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/grpc_security.h"
namespace cmvr::service {
@ -436,11 +438,17 @@ std::shared_ptr<ArmTeleopBackend> makeDisabledArmTeleopBackend()
ArmTeleopServiceImpl::ArmTeleopServiceImpl(
std::shared_ptr<ArmTeleopBackend> backend,
control::ControlAuthorityManager* authority)
control::ControlAuthorityManager* authority,
std::shared_ptr<GrpcSecurityGateway> security_gateway,
safety::SafetyCoordinator* safety_coordinator)
: backend_(std::move(backend)),
authority_(
authority ? authority
: &control::ControlAuthorityManager::instance())
: &control::ControlAuthorityManager::instance()),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway()),
safety_coordinator_(safety_coordinator)
{
if (!backend_) {
backend_ = makeDisabledArmTeleopBackend();
@ -452,6 +460,9 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
grpc::ServerReaderWriter<arm_teleop::ServerFrame,
arm_teleop::ClientFrame>* stream)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.armteleop.v1.ArmTeleopService/Teleoperate");
if (context == nullptr || stream == nullptr) {
return grpc::Status(
grpc::StatusCode::INTERNAL,
@ -529,6 +540,25 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
authority_->release(control_lease);
});
std::optional<GrpcStreamingSafetySession> safety_session;
if (safety_coordinator_) {
GrpcStreamingSafetyOpen safety_open;
safety_open.full_method_name =
"/cmvr.api.armteleop.v1.ArmTeleopService/Teleoperate";
safety_open.device_id = backend_manifest.robot_id();
safety_open.session_id = session_id;
safety_open.authority_generation = control_lease.generation;
safety_open.deadline = cmvr_grpc_call_guard.context().deadline;
safety_session.emplace(
*safety_coordinator_,
cmvr_grpc_call_guard.context(),
std::move(safety_open));
if (!safety_session->admitted()) {
writeBareRejection(stream, safety_session->status());
return safety_session->status();
}
}
if (first_frame.open().request_force_feedback() &&
!backend_->supportsForceFeedback()) {
const grpc::Status status(
@ -577,6 +607,13 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
writeBareRejection(stream, status);
return status;
}
auto safety_dispatch = safety_session
? safety_session->beginDispatch()
: safety::DispatchGuard{};
if (safety_session && !safety_dispatch.acquired()) {
writeBareRejection(stream, safety_session->status());
return safety_session->status();
}
backend_open_attempted = true;
backend_open = backend_->open(first_frame.open());
}
@ -831,6 +868,17 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
grpc::StatusCode::ABORTED, detail),
true);
}
if (safety_session && !safety_session->revalidate()) {
const std::string detail =
"arm teleoperation safety session was invalidated: " +
safety_session->status().error_message();
return finish(
arm_teleop::SESSION_PHASE_LEASE_LOST,
arm_teleop::STOP_REASON_EMERGENCY_STOP,
detail,
safety_session->status(),
true);
}
std::optional<PendingFrame> pending;
bool ended = false;
@ -1117,6 +1165,20 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
grpc::StatusCode::ABORTED, detail),
true);
}
auto safety_dispatch = safety_session
? safety_session->beginDispatch()
: safety::DispatchGuard{};
if (safety_session && !safety_dispatch.acquired()) {
const std::string detail =
"arm teleoperation setpoint rejected by safety: " +
safety_session->status().error_message();
return finish(
arm_teleop::SESSION_PHASE_LEASE_LOST,
arm_teleop::STOP_REASON_EMERGENCY_STOP,
detail,
safety_session->status(),
true);
}
applied = backend_->applySetpoint(
setpoint, command_deadline);
}

View File

@ -2,7 +2,9 @@
#include "manager/media_source_hub/include/device_media_source_adapter.h"
#include "service/grpc/include/camera_operational_activity_registry.h"
#include "service/grpc/include/camera_ptz_activity_registry.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/media_activity_coordinator.h"
#include "service/grpc/include/grpc_security.h"
//
// Created by xtkuang on 2025/6/1.
//
@ -13,6 +15,7 @@
#include <chrono>
#include <cstdint>
#include <limits>
#include <utility>
using namespace std;
using namespace cmvr::service;
@ -76,9 +79,15 @@ class CameraStreamingLease final {
public:
CameraStreamingLease(
std::shared_ptr<AbstractCamera> camera,
const MediaActivityCoordinator::Session& session)
const MediaActivityCoordinator::Session& session,
cmvr::safety::SafetyCoordinator& coordinator)
: camera_(std::move(camera)) {
(void)session.runIfCurrent([this] {
(void)session.runIfCurrent([this, &coordinator] {
auto dispatch = cmvr::media::beginMediaSourceStartDispatch(
coordinator, camera_ ? camera_->id() : std::string{});
if (!dispatch.acquired()) {
return;
}
active_ = camera_ && camera_->startStreaming();
});
}
@ -128,13 +137,19 @@ grpc::Status rejectStreamDuringStopAll(StreamT* stream)
}
gRPCCameraServiceImpl::gRPCCameraServiceImpl(
CameraStreamLowLatencyConfig stream_config)
CameraStreamLowLatencyConfig stream_config,
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: dmgr_(DeviceManager::getInstance()),
stream_config_(stream_config) {}
stream_config_(stream_config),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway()) {}
grpc::Status gRPCCameraServiceImpl::GetStatus(grpc::ServerContext* context,
const api::GetCameraStateCommand_Request* request, api::GetCameraStateCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.CameraService/GetStatus");
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetStatus): id=" << dev_id;
@ -168,12 +183,15 @@ grpc::Status gRPCCameraServiceImpl::GetStatus(grpc::ServerContext* context,
grpc::Status gRPCCameraServiceImpl::StartCamera(grpc::ServerContext* context,
const api::StartCameraCommand_Request* request, api::StartCameraCommand_Feedback* response)
{
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.CameraService/StartCamera", request, response,
[this, request, response](GrpcCommandTransaction& command) {
auto media_session = globalMediaActivityCoordinator().beginSession();
if (!media_session) {
return failResponse(
response, "Media activities are temporarily paused by StopAll");
}
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StartCamera): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
@ -184,12 +202,18 @@ grpc::Status gRPCCameraServiceImpl::StartCamera(grpc::ServerContext* context,
CameraOperationalActivityRegistry::DispatchResult::DeviceFailure;
const bool start_allowed = media_session.runIfCurrent([&] {
dispatch = globalCameraOperationalActivityRegistry().start(
dev_id, dev);
dev_id, dev, nullptr,
[&command] { return command.beginDispatch(); });
});
if (!start_allowed) {
return failResponse(
response, "Camera start was canceled by StopAll");
}
if (dispatch ==
CameraOperationalActivityRegistry::DispatchResult::
RejectedByDispatchFence) {
return command.dispatchStatus();
}
if (dispatch ==
CameraOperationalActivityRegistry::DispatchResult::
RejectedByStopAll) {
@ -203,19 +227,16 @@ grpc::Status gRPCCameraServiceImpl::StartCamera(grpc::ServerContext* context,
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
catch (const exception &e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCCameraServiceImpl::StopCamera(grpc::ServerContext* context,
const api::StopCameraCommand_Request* request, api::StopCameraCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.CameraService/StopCamera", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StopCamera): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
@ -224,13 +245,19 @@ grpc::Status gRPCCameraServiceImpl::StopCamera(grpc::ServerContext* context,
}
const auto dispatch =
globalCameraOperationalActivityRegistry().stopLifecycle(
dev_id, dev);
dev_id, dev,
[&command] { return command.beginDispatch(); });
if (dispatch ==
CameraOperationalActivityRegistry::DispatchResult::
RejectedByStopAll) {
return failResponse(
response, "Camera control is temporarily paused by StopAll");
}
if (dispatch ==
CameraOperationalActivityRegistry::DispatchResult::
RejectedByDispatchFence) {
return command.dispatchStatus();
}
if (dispatch ==
CameraOperationalActivityRegistry::DispatchResult::DeviceFailure) {
return failResponse(response, "Failed to stop camera: " + dev_id);
@ -239,18 +266,14 @@ grpc::Status gRPCCameraServiceImpl::StopCamera(grpc::ServerContext* context,
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
catch (exception &e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCCameraServiceImpl::GetRGBImage(grpc::ServerContext* context,
const api::GetRGBImageCommand_Request* request, api::GetRGBImageCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.CameraService/GetRGBImage");
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] {
if (context) {
@ -318,6 +341,8 @@ grpc::Status gRPCCameraServiceImpl::GetRGBImage(grpc::ServerContext* context,
grpc::Status gRPCCameraServiceImpl::GetDepthImage(grpc::ServerContext* context,
const api::GetDepthImageCommand_Request* request, api::GetDepthImageCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.CameraService/GetDepthImage");
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] {
if (context) {
@ -388,6 +413,8 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImage(grpc::ServerContext* context,
grpc::Status gRPCCameraServiceImpl::GetRGBDImages(grpc::ServerContext* context,
const api::GetRGBDImagesCommand_Request* request, api::GetRGBDImagesCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.CameraService/GetRGBDImages");
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] {
if (context) {
@ -474,63 +501,90 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImages(grpc::ServerContext* context,
grpc::Status gRPCCameraServiceImpl::StartRecording(grpc::ServerContext* context,
const api::StartCameraRecordingCommand_Request* request, api::StartCameraRecordingCommand_Feedback* response)
{
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.CameraService/StartRecording", request, response,
[this, request, response](GrpcCommandTransaction& command) {
auto media_session = globalMediaActivityCoordinator().beginSession();
if (!media_session) {
return failResponse(
response, "Media activities are temporarily paused by StopAll");
}
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StartRecording): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
if (!dev) {
return failResponse(response, "Camera device not found: " + dev_id);
}
bool dispatch_allowed = false;
if (!media_session.runIfCurrent([&] {
dispatch_allowed = command.beginDispatch();
if (!dispatch_allowed) {
return;
}
dev->startRecording(request->video_path());
})) {
return failResponse(
response, "Camera recording start was canceled by StopAll");
}
if (!dispatch_allowed) {
return command.dispatchStatus();
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
catch (exception &e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCCameraServiceImpl::StopRecording(grpc::ServerContext* context,
const api::StopCameraRecordingCommand_Request* request, api::StopCameraRecordingCommand_Feedback* response)
{
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.CameraService/StopRecording", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StopRecording): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
if (!dev) {
return failResponse(response, "Camera device not found: " + dev_id);
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
dev->stopRecording();
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
catch (exception &e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCCameraServiceImpl::ControlPtz(grpc::ServerContext* context,
const api::ControlPtzCommand_Request* request, api::ControlPtzCommand_Feedback* response)
{
try {
if (!request) {
return grpc::Status(
grpc::StatusCode::INTERNAL, "ControlPtz request is null");
}
const auto registered = defaultGrpcMethodPolicyRegistry().find(
"/cmvr.api.CameraService/ControlPtz");
if (!registered.has_value()) {
return grpc::Status(
grpc::StatusCode::INTERNAL,
"ControlPtz method policy is not registered");
}
auto effective_policy = *registered;
if (request->action() == api::ControlPtzCommand_Action_STOP) {
effective_policy.access = GrpcAccessClass::Stop;
effective_policy.command_intent = safety::CommandIntent::Stop;
effective_policy.safety_lane = true;
}
return executeServerDerivedGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.CameraService/ControlPtz", std::move(effective_policy),
request, response,
[this, request, response](GrpcCommandTransaction& command_tx) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (ControlPtz): id=" << dev_id
<< ", command=" << request->command()
@ -556,11 +610,16 @@ grpc::Status gRPCCameraServiceImpl::ControlPtz(grpc::ServerContext* context,
dev,
command,
stop,
static_cast<int>(request->speed()));
static_cast<int>(request->speed()),
[&command_tx] { return command_tx.beginDispatch(); });
if (dispatch == CameraPtzActivityRegistry::DispatchResult::RejectedByStopAll) {
return failResponse(
response, "PTZ control is temporarily paused by StopAll");
}
if (dispatch ==
CameraPtzActivityRegistry::DispatchResult::RejectedByDispatchFence) {
return command_tx.dispatchStatus();
}
if (dispatch == CameraPtzActivityRegistry::DispatchResult::DeviceFailure) {
CameraState state{};
dev->getState(state);
@ -572,18 +631,15 @@ grpc::Status gRPCCameraServiceImpl::ControlPtz(grpc::ServerContext* context,
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
catch (exception &e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCCameraServiceImpl::GetDepthImageStream(grpc::ServerContext* context
, grpc::ServerReaderWriter<cmvr::api::GetDepthImageStreamCommand_Feedback, cmvr::api::GetDepthImageStreamCommand_Request>* stream){
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.CameraService/GetDepthImageStream");
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] { context->TryCancel(); });
if (!media_session) {
@ -609,7 +665,8 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImageStream(grpc::ServerContext* con
stream->Write(response);
return grpc::Status::OK;
}
CameraStreamingLease stream_lease(dev, media_session);
CameraStreamingLease stream_lease(
dev, media_session, dmgr_.safetyCoordinator());
if (!stream_lease) {
api::GetDepthImageStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
@ -677,6 +734,9 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImageStream(grpc::ServerContext* con
}
grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* context
, grpc::ServerReaderWriter<cmvr::api::GetRGBDImagesStreamCommand_Feedback, cmvr::api::GetRGBDImagesStreamCommand_Request>* stream){
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.CameraService/GetRGBDImagesStream");
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] { context->TryCancel(); });
if (!media_session) {
@ -702,7 +762,8 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* con
stream->Write(response);
return grpc::Status::OK;
}
CameraStreamingLease stream_lease(dev, media_session);
CameraStreamingLease stream_lease(
dev, media_session, dmgr_.safetyCoordinator());
if (!stream_lease) {
api::GetRGBDImagesStreamCommand_Feedback response;
response.mutable_header()->set_success(false);
@ -776,6 +837,9 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* con
}
}
grpc::Status gRPCCameraServiceImpl::GetRGBImageStream(grpc::ServerContext* context, grpc::ServerReaderWriter<cmvr::api::GetRGBImageStreamCommand_Feedback, cmvr::api::GetRGBImageStreamCommand_Request>* stream){
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.CameraService/GetRGBImageStream");
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] { context->TryCancel(); });
if (!media_session) {
@ -820,12 +884,16 @@ grpc::Status gRPCCameraServiceImpl::GetRGBImageStream(grpc::ServerContext* conte
stream->Write(response);
return grpc::Status::OK;
}
auto subscription = media_hub.subscribe(
auto source_dispatch = cmvr::media::beginMediaSourceStartDispatch(
dmgr_.safetyCoordinator(), dev_id);
auto subscription = source_dispatch.acquired()
? media_hub.subscribe(
track_id,
cmvr::media::MediaSourceHub::StartPosition::NEXT_PUBLISHED,
[context, &media_session] {
return context->IsCancelled() || media_session.cancelled();
});
})
: cmvr::media::MediaSourceHub::Subscription{};
if (!subscription) {
api::GetRGBImageStreamCommand_Feedback response;
response.mutable_header()->set_success(false);

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,9 @@
#include "devices/dexhand/rh56dftp_dexhand/include/rh56dftp_dexhand.h"
#include "manager/control_authority/include/control_authority_manager.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/media_activity_coordinator.h"
#include "service/grpc/include/grpc_security.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
using namespace std;
@ -227,12 +229,16 @@ bool dispatchDexHandCommand(
const std::string& device_id,
const std::shared_ptr<AbstractDexHand>& dev,
ScopedDexHandControlLease& lease,
GrpcCommandTransaction& command,
Operation&& operation) {
auto dispatch = lease.tryBeginDispatch();
if (!dispatch.acquired()) {
(void)failControlDispatch(response, device_id, lease);
return false;
}
if (!command.beginDispatch()) {
return false;
}
if (!dev->resumeOperationalActivity()) {
(void)failResponse(
response,
@ -304,10 +310,20 @@ void maybeConfigureRh56FullTactilePolling(const std::shared_ptr<AbstractDexHand>
} // namespace
gRPCDexHandServiceImpl::gRPCDexHandServiceImpl(): dmgr_(DeviceManager::getInstance()) {}
gRPCDexHandServiceImpl::gRPCDexHandServiceImpl()
: gRPCDexHandServiceImpl(makeDefaultGrpcSecurityGateway()) {}
gRPCDexHandServiceImpl::gRPCDexHandServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: dmgr_(DeviceManager::getInstance()),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway()) {}
grpc::Status gRPCDexHandServiceImpl::GetStatus(grpc::ServerContext* context,
const api::GetDexHandStateCommand_Request* request, api::GetDexHandStateCommand_Feedback* response) {
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.DexHandService/GetStatus");
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (GetStatus): id=" << dev_id;
@ -350,7 +366,10 @@ grpc::Status gRPCDexHandServiceImpl::GetStatus(grpc::ServerContext* context,
grpc::Status gRPCDexHandServiceImpl::SetDexHandPos(grpc::ServerContext* context
, const cmvr::api::SetDexHandPositionsCommand_Request* request
, cmvr::api::SetDexHandPositionsCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.DexHandService/SetDexHandPos", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandPos): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
@ -378,27 +397,27 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandPos(grpc::ServerContext* context
dev_id,
dev,
control_lease,
command,
[&] { dev->setPositions(finger_joint_targets); })) {
return grpc::Status::OK;
return command.dispatchStatus().ok()
? grpc::Status::OK
: command.dispatchStatus();
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandPos): success, id=" << dev_id
<< ", values=" << request->values_size();
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCDexHandServiceImpl::SetDexHandAngle(grpc::ServerContext* context
, const cmvr::api::SetDexHandAnglesCommand_Request* request
, cmvr::api::SetDexHandAnglesCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.DexHandService/SetDexHandAngle", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandAngle): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
@ -421,8 +440,11 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandAngle(grpc::ServerContext* contex
dev_id,
dev,
control_lease,
command,
[&] { rh56->setAngles(finger_joint_targets); })) {
return grpc::Status::OK;
return command.dispatchStatus().ok()
? grpc::Status::OK
: command.dispatchStatus();
}
} else {
std::vector<int> finger_joint_targets(static_cast<size_t>(kDexHandDofCount), -1);
@ -435,8 +457,11 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandAngle(grpc::ServerContext* contex
dev_id,
dev,
control_lease,
command,
[&] { dev->setAngles(finger_joint_targets); })) {
return grpc::Status::OK;
return command.dispatchStatus().ok()
? grpc::Status::OK
: command.dispatchStatus();
}
}
@ -445,19 +470,16 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandAngle(grpc::ServerContext* contex
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandAngle): success, id=" << dev_id
<< ", values=" << request->values_size();
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCDexHandServiceImpl::SetDexHandForce(grpc::ServerContext* context
, const cmvr::api::SetDexHandForceCommand_Request* request
, cmvr::api::SetDexHandForceCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.DexHandService/SetDexHandForce", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandForce): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
@ -485,27 +507,27 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandForce(grpc::ServerContext* contex
dev_id,
dev,
control_lease,
command,
[&] { dev->setForce(finger_joint_targets); })) {
return grpc::Status::OK;
return command.dispatchStatus().ok()
? grpc::Status::OK
: command.dispatchStatus();
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandForce): success, id=" << dev_id
<< ", values=" << request->values_size();
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCDexHandServiceImpl::SetDexHandSpeed(grpc::ServerContext* context
, const cmvr::api::SetDexHandSpeedCommand_Request* request
, cmvr::api::SetDexHandSpeedCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.DexHandService/SetDexHandSpeed", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandSpeed): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
@ -533,27 +555,27 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandSpeed(grpc::ServerContext* contex
dev_id,
dev,
control_lease,
command,
[&] { dev->setVelocities(finger_joint_targets); })) {
return grpc::Status::OK;
return command.dispatchStatus().ok()
? grpc::Status::OK
: command.dispatchStatus();
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandSpeed): success, id=" << dev_id
<< ", values=" << request->values_size();
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCDexHandServiceImpl::SetDexHandPresetAct(grpc::ServerContext* context
, const cmvr::api::SetDexHandPresetActCommand_Request* request
, cmvr::api::SetDexHandPresetActCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.DexHandService/SetDexHandPresetAct", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandPresetAct): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
@ -578,26 +600,26 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandPresetAct(grpc::ServerContext* co
dev_id,
dev,
control_lease,
command,
[&] { dev->setPresetAct(presetActId); })) {
return grpc::Status::OK;
return command.dispatchStatus().ok()
? grpc::Status::OK
: command.dispatchStatus();
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandPresetAct): success, id=" << dev_id
<< ", preset_act_id=" << presetActId;
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCDexHandServiceImpl::GetSensorData(grpc::ServerContext* context
, const cmvr::api::GetSensorDataCommand_Request* request
, cmvr::api::GetSensorDataCommand_Feedback* response) {
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.DexHandService/GetSensorData");
auto media_session = globalMediaActivityCoordinator().beginSession();
if (!media_session) {
@ -652,6 +674,9 @@ grpc::Status gRPCDexHandServiceImpl::GetSensorData(grpc::ServerContext* context
grpc::Status gRPCDexHandServiceImpl::GetSensorDataStream(grpc::ServerContext* context
, grpc::ServerReaderWriter<cmvr::api::GetSensorDataStreamCommand_Feedback, cmvr::api::GetSensorDataStreamCommand_Request>* stream)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.DexHandService/GetSensorDataStream");
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] {
if (context) {

View File

@ -5,12 +5,15 @@
#include "manager/device_manager/include/device_manager.h"
#include "common/base/grpc_utils.h"
#include "biohead/biohead_esp32/include/biohead_esp32.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/media_activity_coordinator.h"
#include "service/grpc/include/grpc_security.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
#include <chrono>
#include <algorithm>
#include <iostream>
#include <optional>
#include <utility>
using namespace std;
using namespace cmvr::service;
@ -49,10 +52,61 @@ grpc::Status failStoppedCommand(ResponseT* response)
"Biohead command was rejected because StopAll is in progress or "
"the command was preempted");
}
template <typename RequestT, typename ResponseT, typename Operation>
grpc::Status executeHeadOperationalCommand(
const std::shared_ptr<GrpcSecurityGateway>& security_gateway,
grpc::ServerContext* context,
DeviceManager& device_manager,
const char* full_method_name,
const char* rpc_name,
const RequestT* request,
ResponseT* response,
Operation&& operation)
{
return executeRegisteredGrpcCommand(
security_gateway, context, device_manager.safetyCoordinator(),
full_method_name, request, response,
[&device_manager, request, response, rpc_name,
operation = std::forward<Operation>(operation)](
GrpcCommandTransaction& command) mutable {
const std::string device_id = request->header().device_id();
const auto robot =
device_manager.getDevice<AbstractBiohead>(device_id);
if (!robot) {
return failResponse(
response, "Biohead device not found: " + device_id);
}
const auto activity = admitHeadCommand(robot);
if (!activity) {
return failStoppedCommand(response);
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
if (!operation(*robot, *activity)) {
return failStoppedCommand(response);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(
response->mutable_header()->mutable_timestamp());
logSuccess(rpc_name, device_id);
return grpc::Status::OK;
});
}
}
gRPCMBioHeadServiceImpl::gRPCMBioHeadServiceImpl()
: dmgr_(DeviceManager::getInstance()) {}
: gRPCMBioHeadServiceImpl(makeDefaultGrpcSecurityGateway()) {}
gRPCMBioHeadServiceImpl::gRPCMBioHeadServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: dmgr_(DeviceManager::getInstance()),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway()) {}
// 设置表情(一次性)
@ -60,57 +114,48 @@ grpc::Status gRPCMBioHeadServiceImpl::SetExpression(
grpc::ServerContext* context,
const SetFacialExpression_Request* request,
SetFacialExpression_Feedback* response) {
try {
std::string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
}
const auto activity = admitHeadCommand(robot);
if (!activity) {
return failStoppedCommand(response);
}
return executeHeadOperationalCommand(
security_gateway_, context, dmgr_,
"/cmvr.api.BioHeadService/SetExpression", "SetExpression",
request, response,
[request](AbstractBiohead& robot,
const AbstractBiohead::OperationalToken activity) {
FacialExpressionState expression_state;
expression_state.left_eyebrow_outside_y = request->expression().eyebrow().left_outside_y();
expression_state.left_eyebrow_inside_y = request->expression().eyebrow().left_inside_y();
expression_state.right_eyebrow_outside_y = request->expression().eyebrow().right_outside_y();
expression_state.right_eyebrow_inside_y = request->expression().eyebrow().right_inside_y();
expression_state.left_eye_upper_lid_y = request->expression().eyelid().left_upper_y();
expression_state.left_eye_lower_lid_y = request->expression().eyelid().left_lower_y();
expression_state.right_eye_upper_lid_y = request->expression().eyelid().right_upper_y();
expression_state.right_eye_lower_lid_y = request->expression().eyelid().right_lower_y();
expression_state.left_eye_ball_x = request->expression().eyeball().left_x();
expression_state.left_eye_ball_y = request->expression().eyeball().left_y();
expression_state.right_eye_ball_x = request->expression().eyeball().right_x();
expression_state.right_eye_ball_y = request->expression().eyeball().right_y();
expression_state.left_nose_y = request->expression().nose().left_y();
expression_state.right_nose_y = request->expression().nose().right_y();
expression_state.upper_lip_y = request->expression().mouth().upper_lip_y();
expression_state.lower_lip_y = request->expression().mouth().lower_lip_y();
if (!robot->setExpressionPoseIfCurrent(
*activity, expression_state)) {
return failStoppedCommand(response);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
logSuccess("SetExpression", dev_id);
return grpc::Status::OK;
} catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
expression_state.left_eyebrow_outside_y =
request->expression().eyebrow().left_outside_y();
expression_state.left_eyebrow_inside_y =
request->expression().eyebrow().left_inside_y();
expression_state.right_eyebrow_outside_y =
request->expression().eyebrow().right_outside_y();
expression_state.right_eyebrow_inside_y =
request->expression().eyebrow().right_inside_y();
expression_state.left_eye_upper_lid_y =
request->expression().eyelid().left_upper_y();
expression_state.left_eye_lower_lid_y =
request->expression().eyelid().left_lower_y();
expression_state.right_eye_upper_lid_y =
request->expression().eyelid().right_upper_y();
expression_state.right_eye_lower_lid_y =
request->expression().eyelid().right_lower_y();
expression_state.left_eye_ball_x =
request->expression().eyeball().left_x();
expression_state.left_eye_ball_y =
request->expression().eyeball().left_y();
expression_state.right_eye_ball_x =
request->expression().eyeball().right_x();
expression_state.right_eye_ball_y =
request->expression().eyeball().right_y();
expression_state.left_nose_y =
request->expression().nose().left_y();
expression_state.right_nose_y =
request->expression().nose().right_y();
expression_state.upper_lip_y =
request->expression().mouth().upper_lip_y();
expression_state.lower_lip_y =
request->expression().mouth().lower_lip_y();
return robot.setExpressionPoseIfCurrent(
activity, expression_state);
});
}
@ -119,11 +164,15 @@ grpc::Status gRPCMBioHeadServiceImpl::StreamExpression(
grpc::ServerContext* context,
grpc::ServerReaderWriter<StreamFacialExpression_Feedback, StreamFacialExpression_Request>* stream)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.BioHeadService/StreamExpression");
StreamFacialExpression_Feedback feedback_msg;
std::string dev_id;
std::shared_ptr<AbstractBiohead> robot;
bool first_message = true;
AbstractBiohead::OperationalToken activity{0U};
std::optional<GrpcStreamingSafetySession> safety_session;
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] {
if (context) {
@ -178,8 +227,44 @@ grpc::Status gRPCMBioHeadServiceImpl::StreamExpression(
}
activity = *admitted;
const auto& header = request_msg.header();
GrpcStreamingSafetyOpen safety_open;
safety_open.full_method_name =
"/cmvr.api.BioHeadService/StreamExpression";
safety_open.device_id = dev_id;
safety_open.session_id = header.command_id().empty()
? cmvr_grpc_call_guard.context().correlation_id
: header.command_id();
safety_open.expected_service_instance_id =
header.expected_service_instance_id();
if (header.has_expected_device_generation()) {
safety_open.expected_device_generation =
header.expected_device_generation();
}
safety_open.authority_generation = activity;
safety_open.deadline =
cmvr_grpc_call_guard.context().deadline;
safety_session.emplace(
dmgr_.safetyCoordinator(),
cmvr_grpc_call_guard.context(),
std::move(safety_open));
if (!safety_session->admitted()) {
feedback_msg.mutable_header()->set_success(false);
feedback_msg.mutable_header()->set_error_message(
safety_session->status().error_message());
setCurrentTimestamp(
feedback_msg.mutable_header()->mutable_timestamp());
stream->Write(feedback_msg);
return safety_session->status();
}
first_message = false;
CMVR_LOG(DEBUG) << "[gRPCMBioHeadServiceImpl] (StreamExpression): streaming success, id=" << dev_id;
} else if (!request_msg.header().device_id().empty() &&
request_msg.header().device_id() != dev_id) {
return grpc::Status(
grpc::StatusCode::INVALID_ARGUMENT,
"biohead stream cannot change device_id after its first frame");
}
// ✅ 如果紧急停止触发,直接退出
@ -189,6 +274,21 @@ grpc::Status gRPCMBioHeadServiceImpl::StreamExpression(
break;
}
if (!safety_session || !safety_session->revalidate()) {
const auto status = safety_session
? safety_session->status()
: grpc::Status(
grpc::StatusCode::INTERNAL,
"biohead stream safety session was not initialized");
feedback_msg.mutable_header()->set_success(false);
feedback_msg.mutable_header()->set_error_message(
status.error_message());
setCurrentTimestamp(
feedback_msg.mutable_header()->mutable_timestamp());
stream->Write(feedback_msg);
return status;
}
auto current_time = std::chrono::steady_clock::now();
auto elapsed_time = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - last_control_time);
if (elapsed_time < time_interval) continue;
@ -233,10 +333,25 @@ grpc::Status gRPCMBioHeadServiceImpl::StreamExpression(
expression_state.jaw_y = request_msg.expr().jaw().y();
bool dispatched = false;
grpc::Status dispatch_status = grpc::Status::OK;
const bool current_session = media_session.runIfCurrent([&] {
auto dispatch = safety_session->beginDispatch();
if (!dispatch.acquired()) {
dispatch_status = safety_session->status();
return;
}
dispatched = robot->streamFacialPoseIfCurrent(
activity, expression_state, 0, 0);
});
if (!dispatch_status.ok()) {
feedback_msg.mutable_header()->set_success(false);
feedback_msg.mutable_header()->set_error_message(
dispatch_status.error_message());
setCurrentTimestamp(
feedback_msg.mutable_header()->mutable_timestamp());
stream->Write(feedback_msg);
return dispatch_status;
}
if (!current_session || !dispatched) {
CMVR_LOG(WARNING)
<< "[gRPCMBioHeadServiceImpl] StreamExpression was "
@ -271,6 +386,9 @@ grpc::Status gRPCMBioHeadServiceImpl::GetSystemStatus(
const GetStatus_Request* request,
GetStatus_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.BioHeadService/GetSystemStatus");
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
@ -298,14 +416,19 @@ grpc::Status gRPCMBioHeadServiceImpl::EmergencyStop(
const EmergencyStop_Request* request,
EmergencyStop_Feedback* response)
{
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.BioHeadService/EmergencyStop", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const string dev_id = request->header().device_id();
const auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
return failResponse(
response, "Biohead device not found: " + dev_id);
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
if (!robot->stopOperationalActivity()) {
return failResponse(
response,
@ -313,244 +436,126 @@ grpc::Status gRPCMBioHeadServiceImpl::EmergencyStop(
"stopped: " + dev_id);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
setCurrentTimestamp(
response->mutable_header()->mutable_timestamp());
logSuccess("EmergencyStop", dev_id);
return grpc::Status::OK;
}
catch (const exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCMBioHeadServiceImpl::SpeakStart(grpc::ServerContext* context, const cmvr::api::SpeakStart_Request* request, cmvr::api::SpeakStart_Feedback* response)
{
{
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
}
const auto activity = admitHeadCommand(robot);
if (!activity || !robot->speakStartIfCurrent(*activity)) {
return failStoppedCommand(response);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
logSuccess("SpeakStart", dev_id);
return grpc::Status::OK;
}
catch (const exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
}
return executeHeadOperationalCommand(
security_gateway_, context, dmgr_,
"/cmvr.api.BioHeadService/SpeakStart", "SpeakStart",
request, response,
[](AbstractBiohead& robot,
const AbstractBiohead::OperationalToken activity) {
return robot.speakStartIfCurrent(activity);
});
}
grpc::Status gRPCMBioHeadServiceImpl::SpeakStop(grpc::ServerContext* context, const cmvr::api::SpeakStop_Request* request, cmvr::api::SpeakStop_Feedback* response)
{
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.BioHeadService/SpeakStop", request, response,
[this, request, response](GrpcCommandTransaction& command) {
const string dev_id = request->header().device_id();
const auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
return failResponse(
response, "Biohead device not found: " + dev_id);
}
robot->speakstop(); // 停止执行
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
robot->speakstop();
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
setCurrentTimestamp(
response->mutable_header()->mutable_timestamp());
logSuccess("SpeakStop", dev_id);
return grpc::Status::OK;
}
catch (const exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCMBioHeadServiceImpl::Happy(grpc::ServerContext* context, const cmvr::api::Happy_Request* request, cmvr::api::Happy_Feedback* response)
{
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
}
const auto activity = admitHeadCommand(robot);
if (!activity || !robot->expressionHappyIfCurrent(*activity)) {
return failStoppedCommand(response);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
logSuccess("Happy", dev_id);
return grpc::Status::OK;
}
catch (const exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
return executeHeadOperationalCommand(
security_gateway_, context, dmgr_,
"/cmvr.api.BioHeadService/Happy", "Happy", request, response,
[](AbstractBiohead& robot,
const AbstractBiohead::OperationalToken activity) {
return robot.expressionHappyIfCurrent(activity);
});
}
grpc::Status gRPCMBioHeadServiceImpl::Surprise(grpc::ServerContext* context, const cmvr::api::Surprise_Request* request, cmvr::api::Surprise_Feedback* response)
{
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
}
const auto activity = admitHeadCommand(robot);
if (!activity || !robot->expressionSurprisedIfCurrent(*activity)) {
return failStoppedCommand(response);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
logSuccess("Surprise", dev_id);
return grpc::Status::OK;
}
catch (const exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
return executeHeadOperationalCommand(
security_gateway_, context, dmgr_,
"/cmvr.api.BioHeadService/Surprise", "Surprise", request,
response,
[](AbstractBiohead& robot,
const AbstractBiohead::OperationalToken activity) {
return robot.expressionSurprisedIfCurrent(activity);
});
}
grpc::Status gRPCMBioHeadServiceImpl::ExpressionTired(grpc::ServerContext* context, const cmvr::api::ExpressionTired_Request* request, cmvr::api::ExpressionTired_Feedback* response)
{
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
}
const auto activity = admitHeadCommand(robot);
if (!activity || !robot->expressionTiredIfCurrent(*activity)) {
return failStoppedCommand(response);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
logSuccess("ExpressionTired", dev_id);
return grpc::Status::OK;
}
catch (const exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
return executeHeadOperationalCommand(
security_gateway_, context, dmgr_,
"/cmvr.api.BioHeadService/ExpressionTired", "ExpressionTired",
request, response,
[](AbstractBiohead& robot,
const AbstractBiohead::OperationalToken activity) {
return robot.expressionTiredIfCurrent(activity);
});
}
grpc::Status gRPCMBioHeadServiceImpl::ExpressionAngry(grpc::ServerContext* context, const cmvr::api::ExpressionAngry_Request* request, cmvr::api::ExpressionAngry_Feedback* response)
{
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
}
const auto activity = admitHeadCommand(robot);
if (!activity || !robot->expressionAngryIfCurrent(*activity)) {
return failStoppedCommand(response);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
logSuccess("ExpressionAngry", dev_id);
return grpc::Status::OK;
}
catch (const exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
return executeHeadOperationalCommand(
security_gateway_, context, dmgr_,
"/cmvr.api.BioHeadService/ExpressionAngry", "ExpressionAngry",
request, response,
[](AbstractBiohead& robot,
const AbstractBiohead::OperationalToken activity) {
return robot.expressionAngryIfCurrent(activity);
});
}
grpc::Status gRPCMBioHeadServiceImpl::ExpressionSadness(grpc::ServerContext* context, const cmvr::api::ExpressionSadness_Request* request, cmvr::api::ExpressionSadness_Feedback* response)
{
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
}
const auto activity = admitHeadCommand(robot);
if (!activity || !robot->expressionSadnessIfCurrent(*activity)) {
return failStoppedCommand(response);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
logSuccess("ExpressionSadness", dev_id);
return grpc::Status::OK;
}
catch (const exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
return executeHeadOperationalCommand(
security_gateway_, context, dmgr_,
"/cmvr.api.BioHeadService/ExpressionSadness",
"ExpressionSadness", request, response,
[](AbstractBiohead& robot,
const AbstractBiohead::OperationalToken activity) {
return robot.expressionSadnessIfCurrent(activity);
});
}
grpc::Status gRPCMBioHeadServiceImpl::ExpressionYawn(grpc::ServerContext* context, const cmvr::api::ExpressionYawn_Request* request, cmvr::api::ExpressionYawn_Feedback* response)
{
try {
string dev_id = request->header().device_id();
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
if (!robot) {
return failResponse(response, "Biohead device not found: " + dev_id);
}
const auto activity = admitHeadCommand(robot);
if (!activity || !robot->expressionYawnIfCurrent(*activity)) {
return failStoppedCommand(response);
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
logSuccess("ExpressionYawn", dev_id);
return grpc::Status::OK;
}
catch (const exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
return executeHeadOperationalCommand(
security_gateway_, context, dmgr_,
"/cmvr.api.BioHeadService/ExpressionYawn", "ExpressionYawn",
request, response,
[](AbstractBiohead& robot,
const AbstractBiohead::OperationalToken activity) {
return robot.expressionYawnIfCurrent(activity);
});
}

View File

@ -8,11 +8,14 @@
#include <chrono>
#include <exception>
#include <thread>
#include <utility>
#include <google/protobuf/util/time_util.h>
#include "common/base/logging/logger.h"
#include "manager/task_manager/include/task_manager.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/grpc_security.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
#include "task/touch_screen_task/include/touch_screen_task.h"
@ -41,17 +44,59 @@ void fillTouchResponse(Touch_Response* response,
} // namespace
gRPCHlcServiceImpl::gRPCHlcServiceImpl() = default;
gRPCHlcServiceImpl::gRPCHlcServiceImpl()
: gRPCHlcServiceImpl(makeDefaultGrpcSecurityGateway()) {}
grpc::Status gRPCHlcServiceImpl::touch(grpc::ServerContext *context, const cmvr::api::Touch_Request *request, cmvr::api::Touch_Response *response) {
try {
gRPCHlcServiceImpl::gRPCHlcServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: dmgr_(device::DeviceManager::getInstance()),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway()) {}
grpc::Status gRPCHlcServiceImpl::touch(
grpc::ServerContext* context,
const cmvr::api::Touch_Request* request,
cmvr::api::Touch_Response* response)
{
if (!request || !response) {
return grpc::Status(
grpc::StatusCode::INTERNAL,
"touch received a null request or response");
}
auto touch_task = task::TaskManager::getInstance().getTouchScreenTask();
if (!touch_task) {
const std::string error = "TouchScreenTask not found or not initialized";
const std::string error =
"TouchScreenTask not found or not initialized";
fillTouchResponse(response, false, error);
return grpc::Status(grpc::StatusCode::NOT_FOUND, error);
}
const auto arm_id = touch_task->controlDeviceId();
if (arm_id.empty()) {
const std::string error =
"TouchScreenTask has no resolved arm safety target";
fillTouchResponse(response, false, error);
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, error);
}
// HLC is a composite command. The server-resolved arm, rather than a
// caller-supplied task alias, is the authority and safety target.
api::Touch_Request normalized_request = *request;
if (!request->header().device_id().empty() &&
request->header().device_id() != arm_id &&
request->header().device_id() != touch_task->id()) {
CMVR_LOG(WARNING)
<< "[gRPCHlcServiceImpl] ignoring composite touch target alias '"
<< request->header().device_id() << "'; resolved arm=" << arm_id;
}
normalized_request.mutable_header()->set_device_id(arm_id);
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.HlcService/touch", &normalized_request, response,
[context, request, response, touch_task](
GrpcCommandTransaction& command) {
try {
auto& admission_gate = globalStopAllAdmissionGate();
std::uint64_t admission_generation = 0U;
{
@ -65,24 +110,61 @@ grpc::Status gRPCHlcServiceImpl::touch(grpc::ServerContext *context, const cmvr:
admission_generation = admission.generation();
}
task::TouchScreenTask::SafetyHooks safety_hooks;
safety_hooks.revalidate = [&command] {
return command.revalidate();
};
safety_hooks.dispatch_actuation = [&command](
const task::TouchScreenTask::SafetyHooks::HardwareOperation&
operation) {
auto dispatch = command.beginScopedDispatch();
return dispatch.acquired() && operation();
};
safety_hooks.dispatch_stop = [&command](
const task::TouchScreenTask::SafetyHooks::HardwareOperation&
operation) {
auto dispatch = command.beginSafetyStopDispatch();
return dispatch.acquired() && operation();
};
if (!touch_task->touchIfCurrent(
request->u(), request->v(),
[&admission_gate, admission_generation] {
[&admission_gate, admission_generation, &command] {
auto admission = admission_gate.lockAdmission();
return admission.accepting() &&
admission.generation() == admission_generation;
})) {
admission.generation() == admission_generation &&
command.revalidate();
},
std::move(safety_hooks))) {
const std::string error =
buildTouchFailureMessage(*touch_task, "TouchScreenTask touch request rejected");
fillTouchResponse(response, false, error);
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, error);
return command.dispatchStatus().ok()
? grpc::Status(
grpc::StatusCode::FAILED_PRECONDITION, error)
: command.dispatchStatus();
}
while (touch_task->isBusy()) {
if (context != nullptr && context->IsCancelled()) {
const bool stopped = touch_task->stopActivity();
const std::string error = "touch request cancelled";
fillTouchResponse(response, false, error);
return grpc::Status(grpc::StatusCode::CANCELLED, error);
return grpc::Status(
stopped ? grpc::StatusCode::CANCELLED
: grpc::StatusCode::ABORTED,
stopped ? error
: error + "; arm stop was not confirmed");
}
if (!command.revalidate()) {
(void)touch_task->stopActivity();
fillTouchResponse(
response,
false,
buildTouchFailureMessage(
*touch_task,
"TouchScreenTask safety admission was revoked"));
return command.dispatchStatus();
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
@ -91,7 +173,9 @@ grpc::Status gRPCHlcServiceImpl::touch(grpc::ServerContext *context, const cmvr:
const std::string error =
buildTouchFailureMessage(*touch_task, "TouchScreenTask touch failed");
fillTouchResponse(response, false, error);
return grpc::Status(grpc::StatusCode::INTERNAL, error);
return command.dispatchStatus().ok()
? grpc::Status(grpc::StatusCode::INTERNAL, error)
: command.dispatchStatus();
}
fillTouchResponse(response, true, "");
@ -100,8 +184,9 @@ grpc::Status gRPCHlcServiceImpl::touch(grpc::ServerContext *context, const cmvr:
<< ", phase=" << cmvr::task::TouchScreenTask::phaseToString(touch_task->phase())
<< ", status=" << cmvr::task::TouchScreenTask::statusToString(touch_task->lastStatus());
return grpc::Status::OK;
} catch (const std::exception& e) {
fillTouchResponse(response, false, e.what());
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
} catch (...) {
(void)touch_task->stopActivity();
throw;
}
});
}

View File

@ -1,10 +1,13 @@
#include "common/base/logging/logger.h"
#include "manager/media_source_hub/include/device_media_source_adapter.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/media_activity_coordinator.h"
#include "service/grpc/include/grpc_security.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <limits>
#include <utility>
//
// Created by linbo on 2025/6/13.
// Created by xtkuang on 2025/6/13.
@ -33,10 +36,20 @@ grpc::Status mediaStoppedStatus()
}
gRPCMicroPhoneServiceImpl::gRPCMicroPhoneServiceImpl(): dmgr_(DeviceManager::getInstance()) {}
gRPCMicroPhoneServiceImpl::gRPCMicroPhoneServiceImpl()
: gRPCMicroPhoneServiceImpl(makeDefaultGrpcSecurityGateway()) {}
gRPCMicroPhoneServiceImpl::gRPCMicroPhoneServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: dmgr_(DeviceManager::getInstance()),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway()) {}
grpc::Status gRPCMicroPhoneServiceImpl::GetStatus(grpc::ServerContext* context,
const api::GetMicStateCommand_Request* request, api::GetMicStateCommand_Feedback* response) {
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.MicPhoneService/GetStatus");
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (GetStatus): id=" << dev_id;
@ -70,12 +83,15 @@ grpc::Status gRPCMicroPhoneServiceImpl::GetStatus(grpc::ServerContext* context,
grpc::Status gRPCMicroPhoneServiceImpl::StartRecord(grpc::ServerContext* context,
const api::StartMicRecordingCommand_Request* request, api::StartMicRecordingCommand_Feedback* response) {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MicPhoneService/StartRecord", request, response,
[this, request, response](GrpcCommandTransaction& command) {
auto media_session = globalMediaActivityCoordinator().beginSession();
if (!media_session) {
return failResponse(
response, "Media activities are temporarily paused by StopAll");
}
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (StartRecord): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
@ -83,7 +99,12 @@ grpc::Status gRPCMicroPhoneServiceImpl::StartRecord(grpc::ServerContext* context
return failResponse(response, "Microphone device not found: " + dev_id);
}
bool started = false;
bool dispatch_allowed = false;
const bool start_allowed = media_session.runIfCurrent([&] {
dispatch_allowed = command.beginDispatch();
if (!dispatch_allowed) {
return;
}
started = dev->start();
if (started) {
dev->startRecording(request->file_path());
@ -93,6 +114,9 @@ grpc::Status gRPCMicroPhoneServiceImpl::StartRecord(grpc::ServerContext* context
return failResponse(
response, "Microphone recording start was canceled by StopAll");
}
if (!dispatch_allowed) {
return command.dispatchStatus();
}
if (!started) {
return failResponse(response, "Failed to start microphone: " + dev_id);
}
@ -101,95 +125,97 @@ grpc::Status gRPCMicroPhoneServiceImpl::StartRecord(grpc::ServerContext* context
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (StartRecord): success, id=" << dev_id
<< ", path=" << request->file_path();
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCMicroPhoneServiceImpl::StopRecord(grpc::ServerContext* context,
const api::StopMicRecordingCommand_Request* request, api::StopMicRecordingCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MicPhoneService/StopRecord", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (StopRecord): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
if (!dev) {
return failResponse(response, "Microphone device not found: " + dev_id);
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
dev->stopRecording();
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (StopRecord): success, id=" << dev_id;
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCMicroPhoneServiceImpl::PauseRecord(grpc::ServerContext* context,
const api::PauseMicRecordingCommand_Request* request, api::PauseMicRecordingCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MicPhoneService/PauseRecord", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (PauseRecord): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
if (!dev) {
return failResponse(response, "Microphone device not found: " + dev_id);
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
dev->pause();
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (PauseRecord): success, id=" << dev_id;
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCMicroPhoneServiceImpl::ResumeRecord(grpc::ServerContext* context,
const api::ResumeMicRecordingCommand_Request* request, api::ResumeMicRecordingCommand_Feedback* response) {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MicPhoneService/ResumeRecord", request, response,
[this, request, response](GrpcCommandTransaction& command) {
auto media_session = globalMediaActivityCoordinator().beginSession();
if (!media_session) {
return failResponse(
response, "Media activities are temporarily paused by StopAll");
}
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (ResumeRecord): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
if (!dev) {
return failResponse(response, "Microphone device not found: " + dev_id);
}
if (!media_session.runIfCurrent([&] { dev->resume(); })) {
bool dispatch_allowed = false;
if (!media_session.runIfCurrent([&] {
dispatch_allowed = command.beginDispatch();
if (dispatch_allowed) {
dev->resume();
}
})) {
return failResponse(
response, "Microphone recording resume was canceled by StopAll");
}
if (!dispatch_allowed) {
return command.dispatchStatus();
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (ResumeRecord): success, id=" << dev_id;
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCMicroPhoneServiceImpl::StreamAudio(grpc::ServerContext* context,
const api::StreamMicAudioCommand_Request* request,
grpc::ServerWriter<api::StreamMicAudioCommand_Feedback>* writer) {
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.MicPhoneService/StreamAudio");
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] { context->TryCancel(); });
if (!media_session) {
@ -233,12 +259,16 @@ grpc::Status gRPCMicroPhoneServiceImpl::StreamAudio(grpc::ServerContext* context
return grpc::Status::OK;
}
auto subscription = media_hub.subscribe(
auto source_dispatch = cmvr::media::beginMediaSourceStartDispatch(
dmgr_.safetyCoordinator(), dev_id);
auto subscription = source_dispatch.acquired()
? media_hub.subscribe(
track_id,
cmvr::media::MediaSourceHub::StartPosition::NEXT_PUBLISHED,
[context, &media_session] {
return context->IsCancelled() || media_session.cancelled();
});
})
: cmvr::media::MediaSourceHub::Subscription{};
if (!subscription) {
api::StreamMicAudioCommand_Feedback feedback;
feedback.mutable_header()->set_success(false);
@ -325,30 +355,32 @@ grpc::Status gRPCMicroPhoneServiceImpl::StreamAudio(grpc::ServerContext* context
grpc::Status gRPCMicroPhoneServiceImpl::SetVolume(grpc::ServerContext* context,
const api::SetMicPhoneVolumeCommand_Request* request, api::SetMicPhoneVolumeCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MicPhoneService/SetVolume", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (SetVolume): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
if (!dev) {
return failResponse(response, "Microphone device not found: " + dev_id);
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
dev->setVolume(request->volume());
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (SetVolume): success, id=" << dev_id
<< ", volume=" << request->volume();
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCMicroPhoneServiceImpl::GetVolume(grpc::ServerContext* context,
const api::GetMicPhoneVolumeCommand_Request* request, api::GetMicPhoneVolumeCommand_Feedback* response) {
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.MicPhoneService/GetVolume");
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (GetVolume): id=" << dev_id;

View File

@ -15,6 +15,8 @@
#include "common/base/logging/logger.h"
#include "devices/motor/manager/include/motor_manager.h"
#include "manager/device_manager/include/device_manager.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/grpc_security.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
namespace cmvr::service {
@ -161,7 +163,8 @@ template <typename Response, typename Body, typename Cleanup>
grpc::Status runUnaryGuarded(Response* response,
const char* rpc_name,
Body&& body,
Cleanup&& cleanup)
Cleanup&& cleanup,
GrpcCommandTransaction* command = nullptr)
{
try {
return body();
@ -174,6 +177,9 @@ grpc::Status runUnaryGuarded(Response* response,
}
response->Clear();
fillFeedback(response->mutable_header(), false, error);
if (command) {
return command->finishException(error);
}
return grpc::Status(grpc::StatusCode::INTERNAL, error);
} catch (...) {
const std::string error =
@ -184,6 +190,9 @@ grpc::Status runUnaryGuarded(Response* response,
}
response->Clear();
fillFeedback(response->mutable_header(), false, error);
if (command) {
return command->finishException(error);
}
return grpc::Status(grpc::StatusCode::INTERNAL, error);
}
}
@ -215,7 +224,8 @@ grpc::Status runStreamingGuarded(const char* rpc_name,
}
template <typename Request, typename Apply, typename FillStatus,
typename IsPreempted, typename SetLastError, typename Stop>
typename IsPreempted, typename ValidateSafety,
typename SetLastError, typename Stop>
grpc::Status runCyclicLoop(
grpc::ServerContext* context,
grpc::ServerReaderWriter<api::CyclicControlResponse, Request>* stream,
@ -224,6 +234,7 @@ grpc::Status runCyclicLoop(
Apply&& apply,
FillStatus&& fill_status,
IsPreempted&& is_preempted,
ValidateSafety&& validate_safety,
SetLastError&& set_last_error,
Stop&& stop)
{
@ -398,6 +409,15 @@ grpc::Status runCyclicLoop(
grpc::StatusCode::INTERNAL,
"failed to quick-stop preempted cyclic stream");
}
const auto safety_status = validate_safety();
if (!safety_status.ok()) {
const std::string error =
"cyclic stream safety session was invalidated: " +
safety_status.error_message();
return finishTerminal(
api::CYCLIC_STREAM_FAILED, false, error,
safety_status, true);
}
std::optional<Request> request;
bool ended = false;
@ -548,7 +568,16 @@ gRPCMotorServiceImpl::ControlLease::~ControlLease()
}
gRPCMotorServiceImpl::gRPCMotorServiceImpl()
: dmgr_(device::DeviceManager::getInstance())
: gRPCMotorServiceImpl(makeDefaultGrpcSecurityGateway())
{
}
gRPCMotorServiceImpl::gRPCMotorServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: dmgr_(device::DeviceManager::getInstance()),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway())
{
}
@ -840,11 +869,20 @@ grpc::Status gRPCMotorServiceImpl::setZero(
const api::SetMotorZeroRequest* request,
api::MotorCommandResponse* response)
{
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MotorService/setZero", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
return runUnaryGuarded(
response, "setZero",
[&]() { return setZeroImpl(context, request, response); },
[&]() {
return setZeroImpl(
context, request, response, command);
},
[&](const std::string& error) {
bestEffortQuickStop(request->target(), error);
},
&command);
});
}
@ -853,11 +891,20 @@ grpc::Status gRPCMotorServiceImpl::moveToZero(
const api::MoveMotorToZeroRequest* request,
api::MotorCommandResponse* response)
{
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MotorService/moveToZero", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
return runUnaryGuarded(
response, "moveToZero",
[&]() { return moveToZeroImpl(context, request, response); },
[&]() {
return moveToZeroImpl(
context, request, response, command);
},
[&](const std::string& error) {
bestEffortQuickStop(request->target(), error);
},
&command);
});
}
@ -866,11 +913,20 @@ grpc::Status gRPCMotorServiceImpl::profilePosition(
const api::ProfilePositionRequest* request,
api::MotorCommandResponse* response)
{
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MotorService/profilePosition", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
return runUnaryGuarded(
response, "profilePosition",
[&]() { return profilePositionImpl(context, request, response); },
[&]() {
return profilePositionImpl(
context, request, response, command);
},
[&](const std::string& error) {
bestEffortQuickStop(request->target(), error);
},
&command);
});
}
@ -879,11 +935,20 @@ grpc::Status gRPCMotorServiceImpl::profileVelocity(
const api::ProfileVelocityRequest* request,
api::MotorCommandResponse* response)
{
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MotorService/profileVelocity", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
return runUnaryGuarded(
response, "profileVelocity",
[&]() { return profileVelocityImpl(context, request, response); },
[&]() {
return profileVelocityImpl(
context, request, response, command);
},
[&](const std::string& error) {
bestEffortQuickStop(request->target(), error);
},
&command);
});
}
@ -892,12 +957,16 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPosition(
grpc::ServerReaderWriter<api::CyclicControlResponse,
api::CyclicPositionRequest>* stream)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.MotorService/streamCyclicPosition");
std::optional<api::MotorTarget> cleanup_target;
return runStreamingGuarded(
"streamCyclicPosition",
[&]() {
return streamCyclicPositionImpl(
context, stream, cleanup_target);
context, stream, cleanup_target,
cmvr_grpc_call_guard.context());
},
[&](const std::string& error) {
if (cleanup_target.has_value()) {
@ -911,12 +980,16 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocity(
grpc::ServerReaderWriter<api::CyclicControlResponse,
api::CyclicVelocityRequest>* stream)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.MotorService/streamCyclicVelocity");
std::optional<api::MotorTarget> cleanup_target;
return runStreamingGuarded(
"streamCyclicVelocity",
[&]() {
return streamCyclicVelocityImpl(
context, stream, cleanup_target);
context, stream, cleanup_target,
cmvr_grpc_call_guard.context());
},
[&](const std::string& error) {
if (cleanup_target.has_value()) {
@ -930,11 +1003,20 @@ grpc::Status gRPCMotorServiceImpl::emergencyStop(
const api::EmergencyStopRequest* request,
api::MotorCommandResponse* response)
{
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MotorService/emergencyStop", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
return runUnaryGuarded(
response, "emergencyStop",
[&]() { return emergencyStopImpl(context, request, response); },
[&]() {
return emergencyStopImpl(
context, request, response, command);
},
[&](const std::string& error) {
bestEffortQuickStop(request->target(), error);
},
&command);
});
}
@ -943,6 +1025,8 @@ grpc::Status gRPCMotorServiceImpl::getStatus(
const api::GetMotorStatusRequest* request,
api::GetMotorStatusResponse* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.MotorService/getStatus");
return runUnaryGuarded(
response, "getStatus",
[&]() { return getStatusImpl(context, request, response); },
@ -954,18 +1038,28 @@ grpc::Status gRPCMotorServiceImpl::setEnabled(
const api::SetMotorEnabledRequest* request,
api::MotorCommandResponse* response)
{
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.MotorService/setEnabled", request, response,
[this, context, request, response](GrpcCommandTransaction& command) {
return runUnaryGuarded(
response, "setEnabled",
[&]() { return setEnabledImpl(context, request, response); },
[&]() {
return setEnabledImpl(
context, request, response, command);
},
[&](const std::string& error) {
bestEffortQuickStop(request->target(), error);
},
&command);
});
}
grpc::Status gRPCMotorServiceImpl::setZeroImpl(
grpc::ServerContext* context,
const api::SetMotorZeroRequest* request,
api::MotorCommandResponse* response)
api::MotorCommandResponse* response,
GrpcCommandTransaction& command)
{
const auto started = Clock::now();
ResolvedMotor resolved;
@ -1014,6 +1108,10 @@ grpc::Status gRPCMotorServiceImpl::setZeroImpl(
response->set_elapsed_ms(elapsedMs(started));
return grpc::Status(grpc::StatusCode::ABORTED, error);
}
if (!command.beginDispatch()) {
lease.reset();
return command.dispatchStatus();
}
calibrated = resolved.motor->calibrateZeroQ();
{
std::lock_guard state_lock(resolved.control->mutex);
@ -1074,7 +1172,8 @@ grpc::Status gRPCMotorServiceImpl::runProfilePosition(
const double max_velocity_rad_s,
const double acceleration_rad_s2,
const api::MotorWaitOptions& wait,
api::MotorCommandResponse* response)
api::MotorCommandResponse* response,
GrpcCommandTransaction& command)
{
const auto started = Clock::now();
if (!isFinite(target_position_rad) ||
@ -1131,6 +1230,10 @@ grpc::Status gRPCMotorServiceImpl::runProfilePosition(
response->set_elapsed_ms(elapsedMs(started));
return grpc::Status(grpc::StatusCode::ABORTED, error);
}
if (!command.beginDispatch()) {
lease.reset();
return command.dispatchStatus();
}
resolved.motor->setMode(msgs::RUN_MODE_PROFILE_POSITION);
submitted = resolved.motor->commandProfilePosition(
target_position_rad, max_velocity_rad_s, acceleration_rad_s2);
@ -1355,7 +1458,8 @@ grpc::Status gRPCMotorServiceImpl::waitForPosition(
grpc::Status gRPCMotorServiceImpl::moveToZeroImpl(
grpc::ServerContext* context,
const api::MoveMotorToZeroRequest* request,
api::MotorCommandResponse* response)
api::MotorCommandResponse* response,
GrpcCommandTransaction& command)
{
ResolvedMotor resolved;
auto status = resolveMotor(request->target(), resolved);
@ -1365,13 +1469,14 @@ grpc::Status gRPCMotorServiceImpl::moveToZeroImpl(
}
return runProfilePosition(
context, resolved, 0.0, request->max_velocity_rad_s(),
request->acceleration_rad_s2(), request->wait(), response);
request->acceleration_rad_s2(), request->wait(), response, command);
}
grpc::Status gRPCMotorServiceImpl::profilePositionImpl(
grpc::ServerContext* context,
const api::ProfilePositionRequest* request,
api::MotorCommandResponse* response)
api::MotorCommandResponse* response,
GrpcCommandTransaction& command)
{
ResolvedMotor resolved;
auto status = resolveMotor(request->target(), resolved);
@ -1382,7 +1487,7 @@ grpc::Status gRPCMotorServiceImpl::profilePositionImpl(
return runProfilePosition(
context, resolved, request->target_position_rad(),
request->max_velocity_rad_s(), request->acceleration_rad_s2(),
request->wait(), response);
request->wait(), response, command);
}
grpc::Status gRPCMotorServiceImpl::waitForVelocity(
@ -1515,7 +1620,8 @@ grpc::Status gRPCMotorServiceImpl::waitForVelocity(
grpc::Status gRPCMotorServiceImpl::profileVelocityImpl(
grpc::ServerContext* context,
const api::ProfileVelocityRequest* request,
api::MotorCommandResponse* response)
api::MotorCommandResponse* response,
GrpcCommandTransaction& command)
{
const auto started = Clock::now();
ResolvedMotor resolved;
@ -1578,6 +1684,10 @@ grpc::Status gRPCMotorServiceImpl::profileVelocityImpl(
response->set_elapsed_ms(elapsedMs(started));
return grpc::Status(grpc::StatusCode::ABORTED, error);
}
if (!command.beginDispatch()) {
lease.reset();
return command.dispatchStatus();
}
resolved.motor->setMode(msgs::RUN_MODE_PROFILE_VELOCITY);
submitted = resolved.motor->commandProfileVelocity(
request->target_velocity_rad_s(),
@ -1672,7 +1782,8 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
grpc::ServerContext* context,
grpc::ServerReaderWriter<api::CyclicControlResponse,
api::CyclicPositionRequest>* stream,
std::optional<api::MotorTarget>& cleanup_target)
std::optional<api::MotorTarget>& cleanup_target,
const GrpcRequestContext& request_context)
{
api::CyclicPositionRequest first;
if (!stream->Read(&first) || !first.has_open()) {
@ -1693,6 +1804,28 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
}
const auto generation = lease->generation();
const auto& header = first.open().target().header();
GrpcStreamingSafetyOpen safety_open;
safety_open.full_method_name =
"/cmvr.api.MotorService/streamCyclicPosition";
safety_open.device_id = header.device_id();
safety_open.session_id = header.command_id().empty()
? request_context.correlation_id
: header.command_id();
safety_open.expected_service_instance_id =
header.expected_service_instance_id();
if (header.has_expected_device_generation()) {
safety_open.expected_device_generation =
header.expected_device_generation();
}
safety_open.authority_generation = generation;
safety_open.deadline = request_context.deadline;
GrpcStreamingSafetySession safety_session(
dmgr_.safetyCoordinator(), request_context,
std::move(safety_open));
if (!safety_session.admitted()) {
return safety_session.status();
}
{
std::lock_guard command_lock(resolved.control->command_mutex);
if (context->IsCancelled() ||
@ -1710,6 +1843,10 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
grpc::StatusCode::ABORTED,
"cyclic position open preempted by a stop request");
}
auto safety_dispatch = safety_session.beginDispatch();
if (!safety_dispatch.acquired()) {
return safety_session.status();
}
resolved.motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_POSITION);
std::lock_guard state_lock(resolved.control->mutex);
if (resolved.control->cancel_generation != generation) {
@ -1743,6 +1880,10 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
grpc::StatusCode::INVALID_ARGUMENT,
"cyclic position setpoint contains a non-finite value");
}
auto safety_dispatch = safety_session.beginDispatch();
if (!safety_dispatch.acquired()) {
return safety_session.status();
}
const bool submitted = resolved.motor->commandCyclicPosition(
setpoint.target_position_rad(),
setpoint.has_target_velocity_rad_s()
@ -1770,6 +1911,11 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
std::lock_guard lock(resolved.control->mutex);
return resolved.control->cancel_generation != expected_generation;
},
[&]() {
return safety_session.revalidate()
? grpc::Status::OK
: safety_session.status();
},
[&](const std::string& error) {
setLastError(resolved.control, error);
},
@ -1795,7 +1941,8 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
grpc::ServerContext* context,
grpc::ServerReaderWriter<api::CyclicControlResponse,
api::CyclicVelocityRequest>* stream,
std::optional<api::MotorTarget>& cleanup_target)
std::optional<api::MotorTarget>& cleanup_target,
const GrpcRequestContext& request_context)
{
api::CyclicVelocityRequest first;
if (!stream->Read(&first) || !first.has_open()) {
@ -1816,6 +1963,28 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
}
const auto generation = lease->generation();
const auto& header = first.open().target().header();
GrpcStreamingSafetyOpen safety_open;
safety_open.full_method_name =
"/cmvr.api.MotorService/streamCyclicVelocity";
safety_open.device_id = header.device_id();
safety_open.session_id = header.command_id().empty()
? request_context.correlation_id
: header.command_id();
safety_open.expected_service_instance_id =
header.expected_service_instance_id();
if (header.has_expected_device_generation()) {
safety_open.expected_device_generation =
header.expected_device_generation();
}
safety_open.authority_generation = generation;
safety_open.deadline = request_context.deadline;
GrpcStreamingSafetySession safety_session(
dmgr_.safetyCoordinator(), request_context,
std::move(safety_open));
if (!safety_session.admitted()) {
return safety_session.status();
}
{
std::lock_guard command_lock(resolved.control->command_mutex);
if (context->IsCancelled() ||
@ -1833,6 +2002,10 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
grpc::StatusCode::ABORTED,
"cyclic velocity open preempted by a stop request");
}
auto safety_dispatch = safety_session.beginDispatch();
if (!safety_dispatch.acquired()) {
return safety_session.status();
}
resolved.motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY);
std::lock_guard state_lock(resolved.control->mutex);
if (resolved.control->cancel_generation != generation) {
@ -1864,6 +2037,10 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
grpc::StatusCode::INVALID_ARGUMENT,
"cyclic velocity setpoint contains a non-finite value");
}
auto safety_dispatch = safety_session.beginDispatch();
if (!safety_dispatch.acquired()) {
return safety_session.status();
}
const bool submitted = resolved.motor->commandCyclicVelocity(
setpoint.target_velocity_rad_s());
{
@ -1888,6 +2065,11 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
std::lock_guard lock(resolved.control->mutex);
return resolved.control->cancel_generation != expected_generation;
},
[&]() {
return safety_session.revalidate()
? grpc::Status::OK
: safety_session.status();
},
[&](const std::string& error) {
setLastError(resolved.control, error);
},
@ -1912,7 +2094,8 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
grpc::Status gRPCMotorServiceImpl::emergencyStopImpl(
grpc::ServerContext*,
const api::EmergencyStopRequest* request,
api::MotorCommandResponse* response)
api::MotorCommandResponse* response,
GrpcCommandTransaction& command)
{
const auto started = Clock::now();
ResolvedMotor resolved;
@ -1922,6 +2105,9 @@ grpc::Status gRPCMotorServiceImpl::emergencyStopImpl(
return status;
}
std::lock_guard emergency_lock(resolved.control->emergency_mutex);
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
bool stopped = false;
{
{
@ -1993,7 +2179,8 @@ grpc::Status gRPCMotorServiceImpl::getStatusImpl(
grpc::Status gRPCMotorServiceImpl::setEnabledImpl(
grpc::ServerContext* context,
const api::SetMotorEnabledRequest* request,
api::MotorCommandResponse* response)
api::MotorCommandResponse* response,
GrpcCommandTransaction& command)
{
const auto started = Clock::now();
ResolvedMotor resolved;
@ -2045,6 +2232,10 @@ grpc::Status gRPCMotorServiceImpl::setEnabledImpl(
response->set_elapsed_ms(elapsedMs(started));
return grpc::Status(grpc::StatusCode::ABORTED, error);
}
if (!command.beginDispatch()) {
lease.reset();
return command.dispatchStatus();
}
success = request->enabled()
? resolved.motor->torqueOn()
: resolved.motor->torqueOff();

View File

@ -0,0 +1,164 @@
#include "service/grpc/include/grpc_recovery_audit.h"
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <mutex>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <utility>
#include <google/protobuf/struct.pb.h>
#include <google/protobuf/util/json_util.h>
namespace cmvr::service {
namespace {
void setError(std::string* error, std::string message) noexcept
{
if (!error) {
return;
}
try {
*error = std::move(message);
} catch (...) {
}
}
class FileRecoveryAuditSink final : public RecoveryAuditSink {
public:
explicit FileRecoveryAuditSink(std::string path)
: path_(std::move(path))
{
}
bool append(
const RecoveryAuditRecord& record,
std::string* error) noexcept override
{
try {
google::protobuf::Struct event;
auto* fields = event.mutable_fields();
(*fields)["schema_version"].set_string_value("1");
(*fields)["occurred_at_unix_ms"].set_string_value(
std::to_string(record.occurred_at_unix_ms));
(*fields)["stage"].set_string_value(record.stage);
(*fields)["correlation_id"].set_string_value(
record.correlation_id);
(*fields)["principal_id"].set_string_value(record.principal_id);
(*fields)["peer"].set_string_value(record.peer);
(*fields)["recovery_id"].set_string_value(record.recovery_id);
(*fields)["reason"].set_string_value(record.reason);
(*fields)["mode"].set_string_value(record.mode);
(*fields)["all_devices"].set_bool_value(record.all_devices);
(*fields)["expected_safety_epoch"].set_string_value(
std::to_string(record.expected_safety_epoch));
(*fields)["previous_safety_epoch"].set_string_value(
std::to_string(record.previous_safety_epoch));
(*fields)["current_safety_epoch"].set_string_value(
std::to_string(record.current_safety_epoch));
(*fields)["result"].set_string_value(record.result);
auto* ids = (*fields)["device_ids"].mutable_list_value();
for (const auto& id : record.device_ids) {
ids->add_values()->set_string_value(id);
}
google::protobuf::util::JsonPrintOptions options;
options.preserve_proto_field_names = true;
std::string line;
const auto json_status =
google::protobuf::util::MessageToJsonString(
event, &line, options);
if (!json_status.ok()) {
setError(error, "failed to serialize recovery audit event");
return false;
}
line.push_back('\n');
std::lock_guard lock(mutex_);
if (path_.empty()) {
setError(error, "recovery audit file is not configured");
return false;
}
int flags = O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC;
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
const int descriptor = ::open(path_.c_str(), flags, S_IRUSR | S_IWUSR);
if (descriptor < 0) {
setError(
error,
"failed to open recovery audit file: " +
std::string(std::strerror(errno)));
return false;
}
const auto close_descriptor = [&] { (void)::close(descriptor); };
struct stat metadata {};
if (::fstat(descriptor, &metadata) != 0 ||
!S_ISREG(metadata.st_mode) || metadata.st_uid != ::geteuid() ||
(metadata.st_mode & (S_IRWXG | S_IRWXO)) != 0) {
close_descriptor();
setError(
error,
"recovery audit file must be an owner-only regular file");
return false;
}
std::size_t written = 0;
while (written < line.size()) {
const auto count = ::write(
descriptor,
line.data() + written,
line.size() - written);
if (count < 0 && errno == EINTR) {
continue;
}
if (count <= 0) {
const auto message = std::string(std::strerror(errno));
close_descriptor();
setError(
error,
"failed to write recovery audit file: " + message);
return false;
}
written += static_cast<std::size_t>(count);
}
if (::fdatasync(descriptor) != 0) {
const auto message = std::string(std::strerror(errno));
close_descriptor();
setError(
error,
"failed to sync recovery audit file: " + message);
return false;
}
close_descriptor();
return true;
} catch (const std::exception& exception) {
setError(
error,
"recovery audit append threw: " +
std::string(exception.what()));
return false;
} catch (...) {
setError(error, "recovery audit append threw an unknown exception");
return false;
}
}
private:
std::string path_;
std::mutex mutex_;
};
} // namespace
std::shared_ptr<RecoveryAuditSink> makeFileRecoveryAuditSink(std::string path)
{
return std::make_shared<FileRecoveryAuditSink>(std::move(path));
}
} // namespace cmvr::service

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,397 @@
#include "service/grpc/include/grpc_safety_proto.h"
#include <cstdint>
namespace cmvr::service {
namespace {
const char* participantPhaseName(
const safety::ParticipantPhase phase) noexcept
{
switch (phase) {
case safety::ParticipantPhase::Ingress: return "INGRESS";
case safety::ParticipantPhase::Scheduler: return "SCHEDULER";
case safety::ParticipantPhase::ControlSession:
return "CONTROL_SESSION";
case safety::ParticipantPhase::Actuator: return "ACTUATOR";
case safety::ParticipantPhase::PeripheralActivity:
return "PERIPHERAL_ACTIVITY";
case safety::ParticipantPhase::Verification: return "VERIFICATION";
}
return "UNKNOWN";
}
void populateParticipantResult(
const safety::ParticipantResultView& source,
api::SafetyParticipantResultInfo& destination)
{
destination.set_recorded(source.recorded);
destination.set_success(source.success);
destination.set_reason_code(toApiSafetyReason(source.reason));
destination.set_detail(source.detail);
}
} // namespace
api::CommandReasonCode toApiSafetyReason(
const safety::SafetyReason value) noexcept
{
using safety::SafetyReason;
switch (value) {
case SafetyReason::None: return api::COMMAND_REASON_CODE_NONE;
case SafetyReason::InvalidArgument:
return api::COMMAND_REASON_CODE_INVALID_ARGUMENT;
case SafetyReason::Unauthenticated:
return api::COMMAND_REASON_CODE_UNAUTHENTICATED;
case SafetyReason::PermissionDenied:
return api::COMMAND_REASON_CODE_PERMISSION_DENIED;
case SafetyReason::RecoveryRpcDisabled:
return api::COMMAND_REASON_CODE_RECOVERY_RPC_DISABLED;
case SafetyReason::DeviceNotFound:
return api::COMMAND_REASON_CODE_DEVICE_NOT_FOUND;
case SafetyReason::DeviceUnavailable:
return api::COMMAND_REASON_CODE_DEVICE_UNAVAILABLE;
case SafetyReason::UnsupportedCommand:
return api::COMMAND_REASON_CODE_UNSUPPORTED_COMMAND;
case SafetyReason::SystemStarting:
return api::COMMAND_REASON_CODE_SYSTEM_STARTING;
case SafetyReason::SystemStopping:
return api::COMMAND_REASON_CODE_SYSTEM_STOPPING;
case SafetyReason::SafetyLatched:
return api::COMMAND_REASON_CODE_SAFETY_LATCHED;
case SafetyReason::SafetyStateMissing:
return api::COMMAND_REASON_CODE_SAFETY_STATE_MISSING;
case SafetyReason::SafetyStateStale:
return api::COMMAND_REASON_CODE_SAFETY_STATE_STALE;
case SafetyReason::HardwareUnsafe:
return api::COMMAND_REASON_CODE_HARDWARE_UNSAFE;
case SafetyReason::EmergencyStopActive:
return api::COMMAND_REASON_CODE_EMERGENCY_STOP_ACTIVE;
case SafetyReason::ProtectiveStopActive:
return api::COMMAND_REASON_CODE_PROTECTIVE_STOP_ACTIVE;
case SafetyReason::DeviceDisconnected:
return api::COMMAND_REASON_CODE_DEVICE_DISCONNECTED;
case SafetyReason::DeviceFault:
return api::COMMAND_REASON_CODE_DEVICE_FAULT;
case SafetyReason::DeviceNotReady:
return api::COMMAND_REASON_CODE_DEVICE_NOT_READY;
case SafetyReason::DeviceStillMoving:
return api::COMMAND_REASON_CODE_DEVICE_STILL_MOVING;
case SafetyReason::ControlBusy:
return api::COMMAND_REASON_CODE_CONTROL_BUSY;
case SafetyReason::GenerationMismatch:
return api::COMMAND_REASON_CODE_GENERATION_MISMATCH;
case SafetyReason::CommandIdRequired:
return api::COMMAND_REASON_CODE_COMMAND_ID_REQUIRED;
case SafetyReason::CommandIdConflict:
return api::COMMAND_REASON_CODE_COMMAND_ID_CONFLICT;
case SafetyReason::ResultEvicted:
return api::COMMAND_REASON_CODE_RESULT_EVICTED;
case SafetyReason::LedgerExhausted:
return api::COMMAND_REASON_CODE_LEDGER_EXHAUSTED;
case SafetyReason::Backpressure:
return api::COMMAND_REASON_CODE_BACKPRESSURE;
case SafetyReason::DeadlineExceededBeforeDispatch:
return api::COMMAND_REASON_CODE_DEADLINE_EXCEEDED_BEFORE_DISPATCH;
case SafetyReason::OutcomeUnknown:
return api::COMMAND_REASON_CODE_OUTCOME_UNKNOWN;
case SafetyReason::ParticipantTimeout:
return api::COMMAND_REASON_CODE_PARTICIPANT_TIMEOUT;
case SafetyReason::StopUnconfirmed:
return api::COMMAND_REASON_CODE_STOP_UNCONFIRMED;
case SafetyReason::RecoveryEpochMismatch:
return api::COMMAND_REASON_CODE_RECOVERY_EPOCH_MISMATCH;
case SafetyReason::RecoveryReasonRequired:
return api::COMMAND_REASON_CODE_RECOVERY_REASON_REQUIRED;
case SafetyReason::RecoveryAuditFailed:
return api::COMMAND_REASON_CODE_RECOVERY_AUDIT_FAILED;
case SafetyReason::InternalError:
return api::COMMAND_REASON_CODE_INTERNAL_ERROR;
}
return api::COMMAND_REASON_CODE_INTERNAL_ERROR;
}
safety::SafetyReason fromApiSafetyReason(
const api::CommandReasonCode value) noexcept
{
using safety::SafetyReason;
switch (value) {
case api::COMMAND_REASON_CODE_NONE: return SafetyReason::None;
case api::COMMAND_REASON_CODE_INVALID_ARGUMENT:
return SafetyReason::InvalidArgument;
case api::COMMAND_REASON_CODE_UNAUTHENTICATED:
return SafetyReason::Unauthenticated;
case api::COMMAND_REASON_CODE_PERMISSION_DENIED:
return SafetyReason::PermissionDenied;
case api::COMMAND_REASON_CODE_RECOVERY_RPC_DISABLED:
return SafetyReason::RecoveryRpcDisabled;
case api::COMMAND_REASON_CODE_DEVICE_NOT_FOUND:
return SafetyReason::DeviceNotFound;
case api::COMMAND_REASON_CODE_DEVICE_UNAVAILABLE:
return SafetyReason::DeviceUnavailable;
case api::COMMAND_REASON_CODE_UNSUPPORTED_COMMAND:
return SafetyReason::UnsupportedCommand;
case api::COMMAND_REASON_CODE_SYSTEM_STARTING:
return SafetyReason::SystemStarting;
case api::COMMAND_REASON_CODE_SYSTEM_STOPPING:
return SafetyReason::SystemStopping;
case api::COMMAND_REASON_CODE_SAFETY_LATCHED:
return SafetyReason::SafetyLatched;
case api::COMMAND_REASON_CODE_SAFETY_STATE_MISSING:
return SafetyReason::SafetyStateMissing;
case api::COMMAND_REASON_CODE_SAFETY_STATE_STALE:
return SafetyReason::SafetyStateStale;
case api::COMMAND_REASON_CODE_HARDWARE_UNSAFE:
return SafetyReason::HardwareUnsafe;
case api::COMMAND_REASON_CODE_EMERGENCY_STOP_ACTIVE:
return SafetyReason::EmergencyStopActive;
case api::COMMAND_REASON_CODE_PROTECTIVE_STOP_ACTIVE:
return SafetyReason::ProtectiveStopActive;
case api::COMMAND_REASON_CODE_DEVICE_DISCONNECTED:
return SafetyReason::DeviceDisconnected;
case api::COMMAND_REASON_CODE_DEVICE_FAULT:
return SafetyReason::DeviceFault;
case api::COMMAND_REASON_CODE_DEVICE_NOT_READY:
return SafetyReason::DeviceNotReady;
case api::COMMAND_REASON_CODE_DEVICE_STILL_MOVING:
return SafetyReason::DeviceStillMoving;
case api::COMMAND_REASON_CODE_CONTROL_BUSY:
return SafetyReason::ControlBusy;
case api::COMMAND_REASON_CODE_GENERATION_MISMATCH:
return SafetyReason::GenerationMismatch;
case api::COMMAND_REASON_CODE_COMMAND_ID_REQUIRED:
return SafetyReason::CommandIdRequired;
case api::COMMAND_REASON_CODE_COMMAND_ID_CONFLICT:
return SafetyReason::CommandIdConflict;
case api::COMMAND_REASON_CODE_RESULT_EVICTED:
return SafetyReason::ResultEvicted;
case api::COMMAND_REASON_CODE_LEDGER_EXHAUSTED:
return SafetyReason::LedgerExhausted;
case api::COMMAND_REASON_CODE_BACKPRESSURE:
return SafetyReason::Backpressure;
case api::COMMAND_REASON_CODE_DEADLINE_EXCEEDED_BEFORE_DISPATCH:
return SafetyReason::DeadlineExceededBeforeDispatch;
case api::COMMAND_REASON_CODE_OUTCOME_UNKNOWN:
return SafetyReason::OutcomeUnknown;
case api::COMMAND_REASON_CODE_PARTICIPANT_TIMEOUT:
return SafetyReason::ParticipantTimeout;
case api::COMMAND_REASON_CODE_STOP_UNCONFIRMED:
return SafetyReason::StopUnconfirmed;
case api::COMMAND_REASON_CODE_RECOVERY_EPOCH_MISMATCH:
return SafetyReason::RecoveryEpochMismatch;
case api::COMMAND_REASON_CODE_RECOVERY_REASON_REQUIRED:
return SafetyReason::RecoveryReasonRequired;
case api::COMMAND_REASON_CODE_RECOVERY_AUDIT_FAILED:
return SafetyReason::RecoveryAuditFailed;
case api::COMMAND_REASON_CODE_INTERNAL_ERROR:
case api::COMMAND_REASON_CODE_UNSPECIFIED:
return SafetyReason::InternalError;
}
return SafetyReason::InternalError;
}
api::SafetyTriState toApiSafetyTriState(
const safety::TriState value) noexcept
{
switch (value) {
case safety::TriState::False: return api::SAFETY_TRI_STATE_FALSE;
case safety::TriState::True: return api::SAFETY_TRI_STATE_TRUE;
case safety::TriState::Unknown: break;
}
return api::SAFETY_TRI_STATE_UNKNOWN;
}
api::SafetyCondition toApiSafetyCondition(
const safety::SafetyCondition value) noexcept
{
switch (value) {
case safety::SafetyCondition::Nominal:
return api::SAFETY_CONDITION_NOMINAL;
case safety::SafetyCondition::Restricted:
return api::SAFETY_CONDITION_RESTRICTED;
case safety::SafetyCondition::Unsafe:
return api::SAFETY_CONDITION_UNSAFE;
case safety::SafetyCondition::Unknown: break;
}
return api::SAFETY_CONDITION_UNKNOWN;
}
api::SystemAdmissionState toApiSystemAdmissionState(
const safety::SystemAdmissionState value) noexcept
{
switch (value) {
case safety::SystemAdmissionState::Starting:
return api::SYSTEM_ADMISSION_STATE_STARTING;
case safety::SystemAdmissionState::Open:
return api::SYSTEM_ADMISSION_STATE_OPEN;
case safety::SystemAdmissionState::Stopping:
return api::SYSTEM_ADMISSION_STATE_STOPPING;
case safety::SystemAdmissionState::Latched:
return api::SYSTEM_ADMISSION_STATE_LATCHED;
case safety::SystemAdmissionState::Recovering:
return api::SYSTEM_ADMISSION_STATE_RECOVERING;
case safety::SystemAdmissionState::ShuttingDown:
return api::SYSTEM_ADMISSION_STATE_SHUTTING_DOWN;
}
return api::SYSTEM_ADMISSION_STATE_UNSPECIFIED;
}
api::DeviceAdmissionState toApiDeviceAdmissionState(
const safety::DeviceAdmissionState value) noexcept
{
switch (value) {
case safety::DeviceAdmissionState::Observing:
return api::DEVICE_ADMISSION_STATE_OBSERVING;
case safety::DeviceAdmissionState::Open:
return api::DEVICE_ADMISSION_STATE_OPEN;
case safety::DeviceAdmissionState::Blocked:
return api::DEVICE_ADMISSION_STATE_BLOCKED;
case safety::DeviceAdmissionState::Quarantined:
return api::DEVICE_ADMISSION_STATE_QUARANTINED;
case safety::DeviceAdmissionState::Recovering:
return api::DEVICE_ADMISSION_STATE_RECOVERING;
case safety::DeviceAdmissionState::Removed:
return api::DEVICE_ADMISSION_STATE_REMOVED;
}
return api::DEVICE_ADMISSION_STATE_UNSPECIFIED;
}
api::SafetyBlockerScope toApiSafetyBlockerScope(
const safety::BlockerScope value) noexcept
{
return value == safety::BlockerScope::System
? api::SAFETY_BLOCKER_SCOPE_SYSTEM
: api::SAFETY_BLOCKER_SCOPE_DEVICE;
}
api::SafetyRecoveryRequirement toApiRecoveryRequirement(
const safety::RecoveryRequirement value) noexcept
{
switch (value) {
case safety::RecoveryRequirement::RefreshOnly:
return api::SAFETY_RECOVERY_REQUIREMENT_REFRESH_ONLY;
case safety::RecoveryRequirement::ClearSoftwareLatch:
return api::SAFETY_RECOVERY_REQUIREMENT_CLEAR_SOFTWARE_LATCH;
case safety::RecoveryRequirement::HardwareReleaseRequired:
return api::SAFETY_RECOVERY_REQUIREMENT_HARDWARE_RELEASE_REQUIRED;
case safety::RecoveryRequirement::ManualInspectionRequired:
return api::SAFETY_RECOVERY_REQUIREMENT_MANUAL_INSPECTION_REQUIRED;
}
return api::SAFETY_RECOVERY_REQUIREMENT_UNSPECIFIED;
}
api::SafetyOperationResult toApiRecoveryResult(
const safety::RecoveryResultCode value) noexcept
{
switch (value) {
case safety::RecoveryResultCode::Recovered:
return api::SAFETY_OPERATION_RESULT_RECOVERED;
case safety::RecoveryResultCode::VerifiedButStillBlocked:
return api::SAFETY_OPERATION_RESULT_VERIFIED_BUT_STILL_BLOCKED;
case safety::RecoveryResultCode::BlockerRemains:
return api::SAFETY_OPERATION_RESULT_BLOCKER_REMAINS;
case safety::RecoveryResultCode::EpochMismatch:
return api::SAFETY_OPERATION_RESULT_EPOCH_MISMATCH;
case safety::RecoveryResultCode::NothingToRecover:
return api::SAFETY_OPERATION_RESULT_NOTHING_TO_RECOVER;
case safety::RecoveryResultCode::TimedOut:
return api::SAFETY_OPERATION_RESULT_TIMED_OUT;
case safety::RecoveryResultCode::Failed:
return api::SAFETY_OPERATION_RESULT_FAILED;
}
return api::SAFETY_OPERATION_RESULT_FAILED;
}
void populateDeviceSafetyState(
const safety::DeviceSafetyStateView& source,
api::DeviceSafetyStateInfo& destination)
{
const auto& snapshot = source.safety.snapshot;
destination.set_device_id(source.descriptor.device_id);
destination.set_device_kind(device::toString(source.descriptor.kind));
destination.set_policy_family(
safety::toString(source.descriptor.default_policy));
destination.set_lifecycle_state(device::toString(source.lifecycle));
destination.set_health_state(device::toString(source.health.state));
destination.set_admission_state(
toApiDeviceAdmissionState(source.admission_state));
destination.set_condition(toApiSafetyCondition(snapshot.condition));
destination.set_has_sample(source.safety.has_sample);
destination.set_snapshot_fresh(source.safety.fresh);
if (source.safety.has_sample) {
destination.set_sample_age_ms(static_cast<std::uint64_t>(
source.safety.sample_age.count() < 0
? 0
: source.safety.sample_age.count()));
}
destination.set_sample_sequence(snapshot.sample_sequence);
destination.set_observed_at_unix_ms(snapshot.observed_at_unix_ms);
destination.set_device_generation(snapshot.device_generation);
destination.set_connected(toApiSafetyTriState(snapshot.connected));
destination.set_operational_ready(
toApiSafetyTriState(snapshot.operational_ready));
destination.set_quiescent(toApiSafetyTriState(snapshot.quiescent));
destination.set_motion_active(
toApiSafetyTriState(snapshot.motion_active));
destination.set_actuator_enabled(
toApiSafetyTriState(snapshot.actuator_enabled));
destination.set_emergency_stop_active(
toApiSafetyTriState(snapshot.emergency_stop_active));
destination.set_protective_stop_active(
toApiSafetyTriState(snapshot.protective_stop_active));
destination.set_fault_active(
toApiSafetyTriState(snapshot.fault_active));
for (const auto& blocker : source.blockers) {
auto* target = destination.add_blockers();
target->set_reason_code(toApiSafetyReason(blocker.reason));
target->set_scope(toApiSafetyBlockerScope(blocker.scope));
target->set_recovery_requirement(
toApiRecoveryRequirement(blocker.recovery_requirement));
target->set_source_id(blocker.source_id);
target->set_operation_id(blocker.operation_id);
target->set_first_observed_at_unix_ms(
blocker.first_observed_at_unix_ms);
target->set_last_observed_at_unix_ms(
blocker.last_observed_at_unix_ms);
}
}
void populateSafetyTargetResult(
const safety::SafetyTargetResult& source,
api::SafetyOperationTargetResult& destination)
{
destination.set_target_id(source.target_id);
destination.set_result(
source.success ? api::SAFETY_OPERATION_RESULT_SUCCEEDED
: api::SAFETY_OPERATION_RESULT_FAILED);
destination.set_reason_code(toApiSafetyReason(source.reason));
destination.set_detail(source.detail);
destination.set_before_state(
toApiDeviceAdmissionState(source.before_state));
destination.set_after_state(
toApiDeviceAdmissionState(source.after_state));
}
void populateSafetyParticipantState(
const safety::ParticipantSafetyStateView& source,
api::SafetyParticipantStateInfo& destination)
{
destination.set_participant_id(source.descriptor.participant_id);
destination.set_phase(participantPhaseName(source.descriptor.phase));
destination.set_required(source.descriptor.required);
destination.set_registered(source.registered);
destination.set_barrier_active(source.barrier_active);
destination.set_barrier_retained(source.barrier_retained);
destination.set_operation_id(source.operation_id);
destination.set_safety_epoch(source.safety_epoch);
populateParticipantResult(
source.last_request, *destination.mutable_last_request());
populateParticipantResult(
source.last_verify, *destination.mutable_last_verify());
populateParticipantResult(
source.last_release, *destination.mutable_last_release());
}
} // namespace cmvr::service

View File

@ -0,0 +1,688 @@
#include "service/grpc/include/grpc_security.h"
#include <algorithm>
#include <atomic>
#include <cctype>
#include <cstdint>
#include <iomanip>
#include <initializer_list>
#include <iterator>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <utility>
namespace cmvr::service {
namespace {
constexpr std::size_t kMaxCorrelationIdLength = 64;
std::atomic<std::uint64_t> next_correlation_id{1};
bool isSafeCorrelationId(const std::string& value)
{
if (value.empty() || value.size() > kMaxCorrelationIdLength) {
return false;
}
return std::all_of(
value.begin(), value.end(), [](const unsigned char character) {
return std::isalnum(character) || character == '-' ||
character == '_' || character == '.' || character == ':';
});
}
std::string makeCorrelationId()
{
const auto sequence =
next_correlation_id.fetch_add(1, std::memory_order_relaxed);
const auto now = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
std::ostringstream output;
output << "grpc-" << std::hex << now << '-' << sequence;
return output.str();
}
std::string correlationIdFrom(
const std::multimap<std::string, std::string>& metadata)
{
const auto range = metadata.equal_range("x-correlation-id");
if (range.first != range.second &&
std::next(range.first) == range.second &&
isSafeCorrelationId(range.first->second)) {
return range.first->second;
}
return makeCorrelationId();
}
bool roleAllows(const GrpcPrincipal& principal, const GrpcRole required)
{
const auto rank = [](const GrpcRole role) {
switch (role) {
case GrpcRole::Anonymous: return 0;
case GrpcRole::Observer: return 1;
case GrpcRole::Operator: return 2;
case GrpcRole::SafetyAdmin: return 3;
}
return -1;
};
return std::any_of(
principal.roles.begin(), principal.roles.end(),
[&](const GrpcRole role) { return rank(role) >= rank(required); });
}
GrpcCallFacts factsFrom(
grpc::ServerContext* context,
const GrpcMethodPolicy& method,
const bool encrypted)
{
GrpcCallFacts facts;
facts.full_method_name = method.full_method_name;
facts.transport_encrypted = encrypted;
facts.received_at = std::chrono::steady_clock::now();
facts.deadline = std::chrono::steady_clock::time_point::max();
if (context) {
facts.peer = context->peer();
for (const auto& [key, value] : context->client_metadata()) {
facts.metadata.emplace(
std::string(key.data(), key.size()),
std::string(value.data(), value.size()));
}
const auto deadline = context->deadline();
if (deadline != std::chrono::system_clock::time_point::max()) {
const auto remaining = deadline - std::chrono::system_clock::now();
facts.deadline = remaining <= decltype(remaining)::zero()
? facts.received_at
: facts.received_at +
std::chrono::duration_cast<
std::chrono::steady_clock::duration>(
remaining);
}
}
facts.local_peer = isLocalGrpcPeer(facts.peer);
facts.correlation_id = correlationIdFrom(facts.metadata);
return facts;
}
} // namespace
bool isLoopbackGrpcHost(const std::string& host) noexcept
{
return host == "127.0.0.1" || host == "localhost" || host == "::1" ||
host == "[::1]" || host.rfind("unix:", 0) == 0;
}
bool isLocalGrpcPeer(const std::string& peer) noexcept
{
return peer.rfind("unix:", 0) == 0 ||
peer.rfind("ipv4:127.", 0) == 0 ||
peer == "ipv6:[::1]" || peer == "ipv6:::1";
}
const char* toString(const GrpcTransportSecurity value) noexcept
{
switch (value) {
case GrpcTransportSecurity::Insecure: return "insecure";
case GrpcTransportSecurity::ServerTls: return "server_tls";
case GrpcTransportSecurity::MutualTls: return "mutual_tls";
}
return "unknown";
}
const char* toString(const GrpcAuthenticationMethod value) noexcept
{
switch (value) {
case GrpcAuthenticationMethod::Disabled: return "disabled";
case GrpcAuthenticationMethod::StaticToken: return "static_token";
case GrpcAuthenticationMethod::Jwt: return "jwt";
case GrpcAuthenticationMethod::TlsClientCertificate:
return "tls_client_certificate";
}
return "unknown";
}
const char* toString(const GrpcRecoveryExposure value) noexcept
{
switch (value) {
case GrpcRecoveryExposure::Disabled: return "disabled";
case GrpcRecoveryExposure::LocalOnly: return "local_only";
case GrpcRecoveryExposure::Authorized: return "authorized";
}
return "unknown";
}
const char* toString(const GrpcRole value) noexcept
{
switch (value) {
case GrpcRole::Anonymous: return "anonymous";
case GrpcRole::Observer: return "observer";
case GrpcRole::Operator: return "operator";
case GrpcRole::SafetyAdmin: return "safety_admin";
}
return "unknown";
}
GrpcSecurityConfigResult resolveGrpcSecurityConfig(
const config::GRPCServerConfig& server_config,
const std::string& effective_host)
{
GrpcSecurityConfigResult result;
const bool loopback = isLoopbackGrpcHost(effective_host);
if (!server_config.has_security()) {
result.valid = true;
result.config.transport = GrpcTransportSecurity::Insecure;
result.config.authentication = GrpcAuthenticationMethod::Disabled;
result.config.recovery_exposure = GrpcRecoveryExposure::Disabled;
result.config.allow_insecure_non_loopback = !loopback;
result.config.insecure_non_loopback = !loopback;
result.config.legacy_compatibility = true;
result.warnings.emplace_back(
"missing grpc security config: using one-release legacy "
"INSECURE/DISABLED compatibility with recovery disabled");
return result;
}
const auto& security = server_config.security();
if (security.transport_mode() != config::GRPCSecurityConfig::INSECURE) {
result.error =
"only INSECURE gRPC transport is compiled in this release";
return result;
}
result.config.transport = GrpcTransportSecurity::Insecure;
if (security.authentication_mode() !=
config::GRPCSecurityConfig::DISABLED) {
result.error =
"only DISABLED gRPC authentication is compiled in this release";
return result;
}
result.config.authentication = GrpcAuthenticationMethod::Disabled;
switch (security.recovery_exposure()) {
case config::GRPCSecurityConfig::RECOVERY_DISABLED:
result.config.recovery_exposure = GrpcRecoveryExposure::Disabled;
break;
case config::GRPCSecurityConfig::RECOVERY_LOCAL_ONLY:
result.config.recovery_exposure = GrpcRecoveryExposure::LocalOnly;
break;
case config::GRPCSecurityConfig::RECOVERY_AUTHORIZED:
result.error =
"RECOVERY_AUTHORIZED requires an implemented authentication "
"provider";
return result;
case config::GRPCSecurityConfig::RECOVERY_EXPOSURE_UNSPECIFIED:
default:
result.error = "grpc recovery exposure must be explicit";
return result;
}
result.config.recovery_audit_file = security.audit_file();
if (result.config.recovery_exposure !=
GrpcRecoveryExposure::Disabled &&
result.config.recovery_audit_file.empty()) {
result.error =
"an enabled recovery RPC requires a persistent audit_file";
return result;
}
result.config.allow_insecure_non_loopback =
security.allow_insecure_non_loopback();
result.config.insecure_non_loopback = !loopback;
if (!loopback && !security.allow_insecure_non_loopback()) {
result.error =
"INSECURE/DISABLED gRPC on a non-loopback host requires "
"allow_insecure_non_loopback=true";
return result;
}
if (!loopback) {
result.warnings.emplace_back(
"gRPC is listening without transport encryption or client "
"authentication on a non-loopback host");
}
result.valid = true;
return result;
}
GrpcAuthenticationResult DisabledGrpcAuthenticationProvider::authenticate(
const GrpcCallFacts&) const
{
GrpcAuthenticationResult result;
result.principal.id = "anonymous";
result.principal.method = GrpcAuthenticationMethod::Disabled;
result.principal.authenticated = false;
result.principal.roles = {GrpcRole::Anonymous};
result.status = grpc::Status::OK;
return result;
}
CompatibilityGrpcAuthorizationPolicy::CompatibilityGrpcAuthorizationPolicy(
const GrpcRecoveryExposure recovery_exposure)
: recovery_exposure_(recovery_exposure)
{
}
GrpcAuthorizationDecision CompatibilityGrpcAuthorizationPolicy::authorize(
const GrpcRequestContext& context,
const GrpcMethodPolicy& method) const
{
if (method.access == GrpcAccessClass::Recover) {
switch (recovery_exposure_) {
case GrpcRecoveryExposure::Disabled:
return {
false,
grpc::Status(
grpc::StatusCode::FAILED_PRECONDITION,
"RECOVERY_RPC_DISABLED")};
case GrpcRecoveryExposure::LocalOnly:
if (!context.local_peer) {
return {
false,
grpc::Status(
grpc::StatusCode::PERMISSION_DENIED,
"RecoverSafetyState is restricted to a local peer")};
}
return {true, grpc::Status::OK};
case GrpcRecoveryExposure::Authorized:
if (!context.principal.authenticated ||
!roleAllows(context.principal, GrpcRole::SafetyAdmin)) {
return {
false,
grpc::Status(
context.principal.authenticated
? grpc::StatusCode::PERMISSION_DENIED
: grpc::StatusCode::UNAUTHENTICATED,
"RecoverSafetyState requires SafetyAdmin")};
}
return {true, grpc::Status::OK};
}
}
if (!context.principal.authenticated &&
context.principal.method == GrpcAuthenticationMethod::Disabled) {
return {true, grpc::Status::OK};
}
if (!context.principal.authenticated) {
return {
false,
grpc::Status(
grpc::StatusCode::UNAUTHENTICATED,
"gRPC caller authentication failed")};
}
if (!roleAllows(context.principal, method.minimum_role)) {
return {
false,
grpc::Status(
grpc::StatusCode::PERMISSION_DENIED,
"gRPC caller does not have the required role")};
}
return {true, grpc::Status::OK};
}
bool GrpcMethodPolicyRegistry::registerPolicy(GrpcMethodPolicy policy)
{
if (policy.full_method_name.empty()) {
return false;
}
return policies_.emplace(policy.full_method_name, std::move(policy)).second;
}
std::optional<GrpcMethodPolicy> GrpcMethodPolicyRegistry::find(
const std::string& full_method_name) const
{
const auto found = policies_.find(full_method_name);
if (found == policies_.end()) {
return std::nullopt;
}
return found->second;
}
std::vector<GrpcMethodPolicy> GrpcMethodPolicyRegistry::snapshot() const
{
std::vector<GrpcMethodPolicy> result;
result.reserve(policies_.size());
for (const auto& [name, policy] : policies_) {
(void)name;
result.push_back(policy);
}
std::sort(
result.begin(), result.end(),
[](const auto& lhs, const auto& rhs) {
return lhs.full_method_name < rhs.full_method_name;
});
return result;
}
const GrpcMethodPolicyRegistry& defaultGrpcMethodPolicyRegistry()
{
static const GrpcMethodPolicyRegistry registry = [] {
GrpcMethodPolicyRegistry result;
const auto add = [&result](
const char* service,
const char* method,
const GrpcAccessClass access,
const safety::CommandIntent intent,
const safety::SafetyPolicyFamily family,
const bool mutating,
const bool safety_lane = false) {
GrpcRole role = GrpcRole::Observer;
if (access == GrpcAccessClass::Mutate ||
access == GrpcAccessClass::Stop) {
role = GrpcRole::Operator;
} else if (access == GrpcAccessClass::Recover) {
role = GrpcRole::SafetyAdmin;
}
GrpcMethodPolicy policy;
policy.full_method_name =
std::string("/cmvr.api.") + service + '/' + method;
policy.access = access;
policy.minimum_role = role;
policy.command_intent = intent;
policy.policy_family = family;
policy.mutating = mutating;
policy.safety_lane = safety_lane;
if (!result.registerPolicy(std::move(policy))) {
throw std::logic_error(
std::string("duplicate gRPC method policy: ") +
service + '/' + method);
}
};
const auto add_many = [&add](
const char* service,
const std::initializer_list<const char*> methods,
const GrpcAccessClass access,
const safety::CommandIntent intent,
const safety::SafetyPolicyFamily family,
const bool mutating,
const bool safety_lane = false) {
for (const auto* method : methods) {
add(service, method, access, intent, family, mutating,
safety_lane);
}
};
using safety::CommandIntent;
using safety::SafetyPolicyFamily;
add_many("SystemService",
{"GetSystemInfo", "GetSystemStatus", "GetDeviceList",
"GetSafetyState"},
GrpcAccessClass::Read, CommandIntent::Observe,
SafetyPolicyFamily::Sensor, false);
add("SystemService", "UpdateParams", GrpcAccessClass::Mutate,
CommandIntent::Configure, SafetyPolicyFamily::Sensor, true);
add("SystemService", "StopAll", GrpcAccessClass::Stop,
CommandIntent::Stop, SafetyPolicyFamily::Control, true, true);
add("SystemService", "ExecuteActionQueue", GrpcAccessClass::Mutate,
CommandIntent::Actuate, SafetyPolicyFamily::Control, true);
add("SystemService", "RecoverSafetyState", GrpcAccessClass::Recover,
CommandIntent::RecoverAdmission, SafetyPolicyFamily::Control,
true, true);
add_many("ArmService",
{"getJointState", "getPose", "getPoseMatrix",
"computeForwardKinematics"},
GrpcAccessClass::Read, CommandIntent::Observe,
SafetyPolicyFamily::Control, false);
add_many("ArmService", {"torqueOff", "stopMotion"},
GrpcAccessClass::Stop, CommandIntent::Stop,
SafetyPolicyFamily::Control, true, true);
add("ArmService", "clearFault", GrpcAccessClass::Mutate,
CommandIntent::ResetFault, SafetyPolicyFamily::Control, true);
add("ArmService", "torqueOn", GrpcAccessClass::Mutate,
CommandIntent::StartActivity, SafetyPolicyFamily::Control, true);
add_many("ArmService",
{"moveJ", "moveL", "speedJ", "speedL", "servoJ"},
GrpcAccessClass::Mutate, CommandIntent::Actuate,
SafetyPolicyFamily::Control, true);
add_many("ArmService", {"calibrateZeroQ", "ExecuteJsonCommand"},
GrpcAccessClass::Mutate, CommandIntent::Configure,
SafetyPolicyFamily::Control, true);
add("armteleop.v1.ArmTeleopService", "Teleoperate",
GrpcAccessClass::Mutate,
CommandIntent::Actuate, SafetyPolicyFamily::Control, true);
add_many("AgvService",
{"getRuntimeState", "getNavigationStatus", "listMaps",
"listStations", "downloadMap", "streamMap"},
GrpcAccessClass::Read, CommandIntent::Observe,
SafetyPolicyFamily::Control, false);
add_many("AgvService",
{"emergencyStop", "pauseNavigation", "cancelNavigation",
"stopVelocityControl", "stopMapping"},
GrpcAccessClass::Stop, CommandIntent::Stop,
SafetyPolicyFamily::Control, true, true);
add("AgvService", "clearFault", GrpcAccessClass::Mutate,
CommandIntent::ResetFault, SafetyPolicyFamily::Control, true);
add("AgvService", "resumeNavigation", GrpcAccessClass::Mutate,
CommandIntent::StartActivity, SafetyPolicyFamily::Control, true);
add_many("AgvService",
{"navigateToPose", "navigateToStation", "followPath",
"setVelocity", "translate"},
GrpcAccessClass::Mutate, CommandIntent::Actuate,
SafetyPolicyFamily::Control, true);
add_many("AgvService",
{"switchMap", "uploadMap", "startMapping"},
GrpcAccessClass::Mutate, CommandIntent::Configure,
SafetyPolicyFamily::Control, true);
add("MotorService", "getStatus", GrpcAccessClass::Read,
CommandIntent::Observe, SafetyPolicyFamily::Control, false);
add("MotorService", "emergencyStop", GrpcAccessClass::Stop,
CommandIntent::Stop, SafetyPolicyFamily::Control, true, true);
add_many("MotorService",
{"moveToZero", "profilePosition", "profileVelocity",
"streamCyclicPosition", "streamCyclicVelocity"},
GrpcAccessClass::Mutate, CommandIntent::Actuate,
SafetyPolicyFamily::Control, true);
add_many("MotorService", {"setZero", "setEnabled"},
GrpcAccessClass::Mutate, CommandIntent::Configure,
SafetyPolicyFamily::Control, true);
add_many("DexHandService",
{"GetStatus", "GetSensorData", "GetSensorDataStream"},
GrpcAccessClass::Read, CommandIntent::Observe,
SafetyPolicyFamily::Control, false);
add_many("DexHandService",
{"SetDexHandPos", "SetDexHandAngle", "SetDexHandForce",
"SetDexHandSpeed", "SetDexHandPresetAct"},
GrpcAccessClass::Mutate, CommandIntent::Actuate,
SafetyPolicyFamily::Control, true);
add_many("CameraService",
{"GetStatus", "GetRGBImage", "GetDepthImage",
"GetRGBDImages", "GetRGBImageStream",
"GetDepthImageStream", "GetRGBDImagesStream"},
GrpcAccessClass::Read, CommandIntent::Observe,
SafetyPolicyFamily::Sensor, false);
add_many("CameraService", {"StartCamera", "StartRecording"},
GrpcAccessClass::Mutate, CommandIntent::StartActivity,
SafetyPolicyFamily::Sensor, true);
add_many("CameraService", {"StopCamera", "StopRecording"},
GrpcAccessClass::Stop, CommandIntent::Stop,
SafetyPolicyFamily::Sensor, true, true);
add("CameraService", "ControlPtz", GrpcAccessClass::Mutate,
CommandIntent::Actuate, SafetyPolicyFamily::Control, true);
add_many("MicPhoneService", {"GetStatus", "StreamAudio", "GetVolume"},
GrpcAccessClass::Read, CommandIntent::Observe,
SafetyPolicyFamily::Sensor, false);
add_many("MicPhoneService", {"StartRecord", "ResumeRecord"},
GrpcAccessClass::Mutate, CommandIntent::StartActivity,
SafetyPolicyFamily::Sensor, true);
add_many("MicPhoneService", {"StopRecord", "PauseRecord"},
GrpcAccessClass::Stop, CommandIntent::Stop,
SafetyPolicyFamily::Sensor, true, true);
add("MicPhoneService", "SetVolume", GrpcAccessClass::Mutate,
CommandIntent::Configure, SafetyPolicyFamily::Sensor, true);
add_many("SpeakerService", {"GetStatus", "GetVolume"},
GrpcAccessClass::Read, CommandIntent::Observe,
SafetyPolicyFamily::Sensor, false);
add_many("SpeakerService",
{"PlayAudio", "StreamAudio", "ResumePlayback"},
GrpcAccessClass::Mutate, CommandIntent::StartActivity,
SafetyPolicyFamily::Sensor, true);
add_many("SpeakerService", {"StopPlayback", "PausePlayback"},
GrpcAccessClass::Stop, CommandIntent::Stop,
SafetyPolicyFamily::Sensor, true, true);
add("SpeakerService", "SetVolume", GrpcAccessClass::Mutate,
CommandIntent::Configure, SafetyPolicyFamily::Sensor, true);
add("BioHeadService", "GetSystemStatus", GrpcAccessClass::Read,
CommandIntent::Observe, SafetyPolicyFamily::Control, false);
add_many("BioHeadService", {"EmergencyStop", "SpeakStop"},
GrpcAccessClass::Stop, CommandIntent::Stop,
SafetyPolicyFamily::Control, true, true);
add_many("BioHeadService",
{"SetExpression", "StreamExpression", "SpeakStart",
"Happy", "Surprise", "ExpressionTired",
"ExpressionAngry", "ExpressionSadness",
"ExpressionYawn"},
GrpcAccessClass::Mutate, CommandIntent::Actuate,
SafetyPolicyFamily::Control, true);
add("HlcService", "touch", GrpcAccessClass::Mutate,
CommandIntent::Actuate, SafetyPolicyFamily::Control, true);
add("TestService", "Call", GrpcAccessClass::Read,
CommandIntent::Observe, SafetyPolicyFamily::Sensor, false);
return result;
}();
return registry;
}
GrpcCallGuard::GrpcCallGuard(
GrpcRequestContext context,
GrpcAuthorizationDecision decision)
: context_(std::move(context)), decision_(std::move(decision))
{
}
GrpcSecurityGateway::GrpcSecurityGateway(
GrpcSecurityRuntimeConfig config,
std::shared_ptr<const GrpcAuthenticationProvider> authentication,
std::shared_ptr<const GrpcAuthorizationPolicy> authorization,
GrpcSecurityAuditSink audit_sink)
: config_(std::move(config)),
authentication_(std::move(authentication)),
authorization_(std::move(authorization)),
audit_sink_(std::move(audit_sink))
{
}
GrpcCallGuard GrpcSecurityGateway::beginCall(
grpc::ServerContext* server_context,
const GrpcMethodPolicy& method) const
{
return beginCall(
factsFrom(
server_context, method,
config_.transport != GrpcTransportSecurity::Insecure),
method);
}
GrpcCallGuard GrpcSecurityGateway::beginCall(
GrpcCallFacts facts,
const GrpcMethodPolicy& method) const
{
if (facts.full_method_name.empty()) {
facts.full_method_name = method.full_method_name;
}
if (facts.received_at == std::chrono::steady_clock::time_point{}) {
facts.received_at = std::chrono::steady_clock::now();
}
if (facts.deadline == std::chrono::steady_clock::time_point{}) {
facts.deadline = std::chrono::steady_clock::time_point::max();
}
if (facts.correlation_id.empty()) {
facts.correlation_id = correlationIdFrom(facts.metadata);
}
if (!facts.local_peer) {
facts.local_peer = isLocalGrpcPeer(facts.peer);
}
const auto authentication = authentication_->authenticate(facts);
GrpcRequestContext context{
facts.correlation_id,
facts.full_method_name,
facts.peer,
authentication.principal,
facts.transport_encrypted,
facts.local_peer,
facts.received_at,
facts.deadline};
GrpcAuthorizationDecision decision;
if (!authentication.ok()) {
decision = {false, authentication.status};
} else {
decision = authorization_->authorize(context, method);
}
if (audit_sink_) {
audit_sink_(GrpcSecurityAuditRecord{
context.correlation_id,
context.full_method_name,
context.principal.id,
context.peer,
context.principal.method,
method.access,
context.principal.authenticated,
decision.allowed,
decision.status.error_code()});
}
return GrpcCallGuard(std::move(context), std::move(decision));
}
std::shared_ptr<GrpcSecurityGateway> makeGrpcSecurityGateway(
const GrpcSecurityRuntimeConfig& config,
GrpcSecurityAuditSink audit_sink)
{
return std::make_shared<GrpcSecurityGateway>(
config,
std::make_shared<DisabledGrpcAuthenticationProvider>(),
std::make_shared<CompatibilityGrpcAuthorizationPolicy>(
config.recovery_exposure),
std::move(audit_sink));
}
std::shared_ptr<GrpcSecurityGateway> makeDefaultGrpcSecurityGateway()
{
GrpcSecurityRuntimeConfig config;
config.transport = GrpcTransportSecurity::Insecure;
config.authentication = GrpcAuthenticationMethod::Disabled;
config.recovery_exposure = GrpcRecoveryExposure::Disabled;
config.legacy_compatibility = true;
return makeGrpcSecurityGateway(config);
}
GrpcCallGuard beginRegisteredGrpcCall(
const std::shared_ptr<GrpcSecurityGateway>& gateway,
grpc::ServerContext* server_context,
const std::string& full_method_name)
{
const auto policy =
defaultGrpcMethodPolicyRegistry().find(full_method_name);
if (!policy.has_value()) {
GrpcRequestContext context;
context.full_method_name = full_method_name;
context.received_at = std::chrono::steady_clock::now();
context.deadline = std::chrono::steady_clock::time_point::max();
if (server_context) {
context.peer = server_context->peer();
context.local_peer = isLocalGrpcPeer(context.peer);
}
return GrpcCallGuard(
std::move(context),
{false,
grpc::Status(
grpc::StatusCode::INTERNAL,
"gRPC method has no registered security policy")});
}
const auto active_gateway =
gateway ? gateway : makeDefaultGrpcSecurityGateway();
return active_gateway->beginCall(server_context, *policy);
}
} // namespace cmvr::service

View File

@ -1,6 +1,10 @@
#include "common/base/logging/logger.h"
#include "service/grpc/include/grpc_command_transaction.h"
#include "service/grpc/include/media_activity_coordinator.h"
#include "service/grpc/include/grpc_security.h"
#include <memory>
#include <optional>
#include <utility>
//
// Created by xtkuang on 2025/6/10.
//
@ -94,10 +98,20 @@ private:
};
}
gRPCSpeakerServiceImpl::gRPCSpeakerServiceImpl(): dmgr_(DeviceManager::getInstance()) {}
gRPCSpeakerServiceImpl::gRPCSpeakerServiceImpl()
: gRPCSpeakerServiceImpl(makeDefaultGrpcSecurityGateway()) {}
gRPCSpeakerServiceImpl::gRPCSpeakerServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: dmgr_(DeviceManager::getInstance()),
security_gateway_(security_gateway
? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway()) {}
grpc::Status gRPCSpeakerServiceImpl::GetStatus(grpc::ServerContext* context,
const api::GetSpeakerStateCommand_Request* request, api::GetSpeakerStateCommand_Feedback* response) {
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.SpeakerService/GetStatus");
try {
string dev_id = request->header().device_id();
//CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (GetStatus): id=" << dev_id;
@ -132,12 +146,15 @@ grpc::Status gRPCSpeakerServiceImpl::GetStatus(grpc::ServerContext* context,
grpc::Status gRPCSpeakerServiceImpl::PlayAudio(grpc::ServerContext* context,
const api::PlayAudioCommand_Request* request, api::PlayAudioCommand_Feedback* response) {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.SpeakerService/PlayAudio", request, response,
[this, request, response](GrpcCommandTransaction& command) {
auto media_session = globalMediaActivityCoordinator().beginSession();
if (!media_session) {
return failResponse(
response, "Media activities are temporarily paused by StopAll");
}
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (PlayAudio): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
@ -148,29 +165,32 @@ grpc::Status gRPCSpeakerServiceImpl::PlayAudio(grpc::ServerContext* context,
return failResponse(
response, "Speaker is already controlled by another media session: " + dev_id);
}
bool dispatch_allowed = false;
if (!media_session.runIfCurrent([&] {
dispatch_allowed = command.beginDispatch();
if (dispatch_allowed) {
dev->play(request->audio_path());
}
})) {
return failResponse(
response, "Speaker playback start was canceled by StopAll");
}
if (!dispatch_allowed) {
return command.dispatchStatus();
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (PlayAudio): success, id=" << dev_id
<< ", path=" << request->audio_path();
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCSpeakerServiceImpl::StreamAudio(grpc::ServerContext* context,
grpc::ServerReader<api::StreamSpeakerAudioCommand_Request>* reader,
api::StreamSpeakerAudioCommand_Feedback* response) {
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.SpeakerService/StreamAudio");
auto media_session = globalMediaActivityCoordinator().beginSession(
[context] { context->TryCancel(); });
if (!media_session) {
@ -181,6 +201,7 @@ grpc::Status gRPCSpeakerServiceImpl::StreamAudio(grpc::ServerContext* context,
api::StreamSpeakerAudioCommand_Request request;
std::shared_ptr<AbstractSpeaker> dev;
std::unique_ptr<SpeakerStreamingLease> stream_lease;
std::optional<GrpcStreamingSafetySession> safety_session;
std::string dev_id;
while (reader->Read(&request)) {
@ -201,11 +222,54 @@ grpc::Status gRPCSpeakerServiceImpl::StreamAudio(grpc::ServerContext* context,
"Speaker is already controlled by another media session: " + dev_id);
}
stream_lease = std::make_unique<SpeakerStreamingLease>(dev);
const auto& header = request.header();
GrpcStreamingSafetyOpen safety_open;
safety_open.full_method_name =
"/cmvr.api.SpeakerService/StreamAudio";
safety_open.device_id = dev_id;
safety_open.session_id = header.command_id().empty()
? cmvr_grpc_call_guard.context().correlation_id
: header.command_id();
safety_open.expected_service_instance_id =
header.expected_service_instance_id();
if (header.has_expected_device_generation()) {
safety_open.expected_device_generation =
header.expected_device_generation();
}
safety_open.deadline =
cmvr_grpc_call_guard.context().deadline;
safety_session.emplace(
dmgr_.safetyCoordinator(),
cmvr_grpc_call_guard.context(),
std::move(safety_open));
if (!safety_session->admitted()) {
return safety_session->status();
}
} else if (!request.header().device_id().empty() &&
request.header().device_id() != dev_id) {
return grpc::Status(
grpc::StatusCode::INVALID_ARGUMENT,
"speaker stream cannot change device_id after its first frame");
}
if (!safety_session || !safety_session->revalidate()) {
return safety_session
? safety_session->status()
: grpc::Status(
grpc::StatusCode::INTERNAL,
"speaker stream safety session was not initialized");
}
const auto frame = fromProtoAudioData(request.audio());
bool pushed = false;
grpc::Status dispatch_status = grpc::Status::OK;
const bool push_allowed = media_session.runIfCurrent([&] {
auto dispatch = safety_session->beginDispatch();
if (!dispatch.acquired()) {
dispatch_status = safety_session->status();
return;
}
if (!frame.data.empty()) {
stream_lease->arm();
}
@ -214,6 +278,9 @@ grpc::Status gRPCSpeakerServiceImpl::StreamAudio(grpc::ServerContext* context,
if (!push_allowed || media_session.cancelled()) {
return mediaStoppedStatus();
}
if (!dispatch_status.ok()) {
return dispatch_status;
}
if (!pushed) {
return failResponse(response, "Failed to push speaker audio frame: " + dev_id);
}
@ -239,13 +306,19 @@ grpc::Status gRPCSpeakerServiceImpl::StreamAudio(grpc::ServerContext* context,
grpc::Status gRPCSpeakerServiceImpl::StopPlayback(grpc::ServerContext* context,
const api::StopSpeakerCommand_Request* request, api::StopSpeakerCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.SpeakerService/StopPlayback", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (StopPlayback): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
if (!dev) {
return failResponse(response, "Speaker device not found: " + dev_id);
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
if (!dev->stopPlayback()) {
return failResponse(response, "Failed to stop speaker: " + dev_id);
}
@ -253,46 +326,43 @@ grpc::Status gRPCSpeakerServiceImpl::StopPlayback(grpc::ServerContext* context,
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (StopPlayback): success, id=" << dev_id;
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCSpeakerServiceImpl::PausePlayback(grpc::ServerContext* context,
const api::PauseSpeakerCommand_Request* request, api::PauseSpeakerCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.SpeakerService/PausePlayback", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (PausePlayback): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
if (!dev) {
return failResponse(response, "Speaker device not found: " + dev_id);
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
dev->pause();
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (PausePlayback): success, id=" << dev_id;
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCSpeakerServiceImpl::ResumePlayback(grpc::ServerContext* context,
const api::ResumeSpeakerCommand_Request* request, api::ResumeSpeakerCommand_Feedback* response) {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.SpeakerService/ResumePlayback", request, response,
[this, request, response](GrpcCommandTransaction& command) {
auto media_session = globalMediaActivityCoordinator().beginSession();
if (!media_session) {
return failResponse(
response, "Media activities are temporarily paused by StopAll");
}
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (ResumePlayback): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
@ -303,49 +373,54 @@ grpc::Status gRPCSpeakerServiceImpl::ResumePlayback(grpc::ServerContext* context
return failResponse(
response, "Speaker is already controlled by another media session: " + dev_id);
}
if (!media_session.runIfCurrent([&] { dev->resume(); })) {
bool dispatch_allowed = false;
if (!media_session.runIfCurrent([&] {
dispatch_allowed = command.beginDispatch();
if (dispatch_allowed) {
dev->resume();
}
})) {
return failResponse(
response, "Speaker playback resume was canceled by StopAll");
}
if (!dispatch_allowed) {
return command.dispatchStatus();
}
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (ResumePlayback): success, id=" << dev_id;
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCSpeakerServiceImpl::SetVolume(grpc::ServerContext* context,
const api::SetSpeakerVolumeCommand_Request* request, api::SetSpeakerVolumeCommand_Feedback* response) {
try {
return executeRegisteredGrpcCommand(
security_gateway_, context, dmgr_.safetyCoordinator(),
"/cmvr.api.SpeakerService/SetVolume", request, response,
[this, request, response](GrpcCommandTransaction& command) {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (SetVolume): id=" << dev_id;
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
if (!dev) {
return failResponse(response, "Speaker device not found: " + dev_id);
}
if (!command.beginDispatch()) {
return command.dispatchStatus();
}
dev->setVolume(request->volume());
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (SetVolume): success, id=" << dev_id
<< ", volume=" << request->volume();
return grpc::Status::OK;
}
catch (const std::exception& e) {
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message(e.what());
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
});
}
grpc::Status gRPCSpeakerServiceImpl::GetVolume(grpc::ServerContext* context,
const api::GetSpeakerVolumeCommand_Request* request, api::GetSpeakerVolumeCommand_Feedback* response) {
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.SpeakerService/GetVolume");
try {
string dev_id = request->header().device_id();
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (GetVolume): id=" << dev_id;

View File

@ -33,6 +33,10 @@
#include "service/action/include/action_queue_executor.h"
#include "service/grpc/include/camera_operational_activity_registry.h"
#include "service/grpc/include/camera_ptz_activity_registry.h"
#include "service/grpc/include/grpc_recovery_audit.h"
#include "service/grpc/include/grpc_safety_proto.h"
#include "service/grpc/include/grpc_safety_participants.h"
#include "service/grpc/include/grpc_security.h"
#include "service/grpc/include/media_activity_coordinator.h"
#include "service/grpc/include/motor_activity_coordinator.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
@ -1035,31 +1039,141 @@ cmvr::api::SystemDeviceHealth toApiDeviceHealth(
return cmvr::api::SYSTEM_DEVICE_HEALTH_UNSPECIFIED;
}
struct ParsedSafetyScope final {
bool valid{false};
bool all_devices{false};
std::vector<std::string> device_ids;
std::string error;
};
ParsedSafetyScope parseSafetyScope(
const cmvr::api::SafetyScope& scope,
const bool default_to_all)
{
ParsedSafetyScope result;
switch (scope.target_case()) {
case cmvr::api::SafetyScope::kAllDevices:
if (!scope.all_devices()) {
result.error = "all_devices must be explicitly true";
return result;
}
result.valid = true;
result.all_devices = true;
return result;
case cmvr::api::SafetyScope::kDevices: {
if (scope.devices().device_ids().empty()) {
result.error = "device scope must contain at least one device ID";
return result;
}
std::unordered_set<std::string> unique;
result.device_ids.reserve(scope.devices().device_ids_size());
for (const auto& id : scope.devices().device_ids()) {
if (id.empty() || !unique.insert(id).second) {
result.error =
"device scope IDs must be non-empty and unique";
return result;
}
result.device_ids.push_back(id);
}
result.valid = true;
return result;
}
case cmvr::api::SafetyScope::TARGET_NOT_SET:
if (default_to_all) {
result.valid = true;
result.all_devices = true;
} else {
result.error = "an explicit recovery scope is required";
}
return result;
}
result.error = "invalid safety scope";
return result;
}
void setSafetyHeaderFailure(
cmvr::api::CommandHeader_Feedback* header,
const cmvr::safety::SafetyReason reason,
const std::string& detail)
{
header->set_success(false);
header->set_reason_code(toApiSafetyReason(reason));
header->set_error_message(detail);
header->set_execution_state(
cmvr::api::COMMAND_EXECUTION_STATE_REJECTED_BEFORE_DISPATCH);
setCurrentTimestamp(header->mutable_timestamp());
}
bool recoveryCompletedAsRequested(
const cmvr::safety::RecoveryResultCode result) noexcept
{
return result == cmvr::safety::RecoveryResultCode::Recovered ||
result ==
cmvr::safety::RecoveryResultCode::VerifiedButStillBlocked ||
result == cmvr::safety::RecoveryResultCode::NothingToRecover;
}
} // namespace
gRPCSystemServiceImpl::gRPCSystemServiceImpl()
: gRPCSystemServiceImpl(std::chrono::seconds(15))
: gRPCSystemServiceImpl(
std::chrono::seconds(15), makeDefaultGrpcSecurityGateway())
{
}
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
const std::chrono::milliseconds stop_timeout)
: gRPCSystemServiceImpl(stop_timeout, makeDefaultGrpcSecurityGateway())
{
}
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: gRPCSystemServiceImpl(
std::chrono::seconds(15), std::move(security_gateway))
{
}
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
const std::chrono::milliseconds stop_timeout,
std::shared_ptr<GrpcSecurityGateway> security_gateway)
: gRPCSystemServiceImpl(
stop_timeout, std::move(security_gateway), nullptr)
{
}
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
const std::chrono::milliseconds stop_timeout,
std::shared_ptr<GrpcSecurityGateway> security_gateway,
std::shared_ptr<RecoveryAuditSink> recovery_audit_sink)
: dmgr_(DeviceManager::getInstance()),
stop_timeout_(
stop_timeout > std::chrono::milliseconds::zero()
? stop_timeout
: std::chrono::seconds(15)),
action_queue_(std::make_unique<ActionQueueExecutor>(dmgr_))
security_gateway_(
security_gateway ? std::move(security_gateway)
: makeDefaultGrpcSecurityGateway()),
recovery_audit_sink_(std::move(recovery_audit_sink)),
action_queue_(std::make_shared<ActionQueueExecutor>(dmgr_))
{
// Acquire only after ActionQueue construction succeeds. This ensures an
// exception cannot release the last dispatcher outside the registry.
acquireProcessStopDispatcher(stop_dispatcher_);
try {
safety_participant_registration_ = registerGrpcSafetyParticipants(
dmgr_.safetyCoordinator(), action_queue_, stop_dispatcher_);
} catch (...) {
releaseProcessStopDispatcher(stop_dispatcher_);
throw;
}
}
gRPCSystemServiceImpl::~gRPCSystemServiceImpl()
{
// The dispatcher intentionally outlives ActionQueue, then joins any
// deadline-overrunning stop workers before the last service disappears.
safety_participant_registration_.reset();
action_queue_.reset();
releaseProcessStopDispatcher(stop_dispatcher_);
}
@ -1088,11 +1202,27 @@ void gRPCSystemServiceImpl::prepareForShutdown()
grpc::Status gRPCSystemServiceImpl::GetSystemInfo(grpc::ServerContext* context,
const api::GetSystemInfoCommand_Request* request, api::GetSystemInfoCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.SystemService/GetSystemInfo");
try {
response->set_version(dmgr_.version());
response->set_system_name(dmgr_.name());
response->set_action_service_instance_id(
action_queue_->instanceId());
const auto& security = security_gateway_->config();
response->set_grpc_transport_security(toString(security.transport));
response->set_grpc_authentication(toString(security.authentication));
response->set_grpc_recovery_exposure(
toString(security.recovery_exposure));
response->set_grpc_insecure_non_loopback(
security.insecure_non_loopback);
const auto safety = dmgr_.safetyCoordinator().snapshot();
response->set_control_service_instance_id(
safety.service_instance_id);
response->set_safety_enforcement_mode(
cmvr::safety::toString(safety.enforcement_mode));
response->set_safety_schema_version(1);
response->mutable_header()->set_success(true);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
CMVR_LOG(DEBUG) << "[gRPCSystemServiceImpl] (GetSystemInfo): success, name="
@ -1110,6 +1240,9 @@ grpc::Status gRPCSystemServiceImpl::GetSystemInfo(grpc::ServerContext* context,
grpc::Status gRPCSystemServiceImpl::GetSystemStatus(grpc::ServerContext* context,
const api::GetSystemStatusCommand_Request* request, api::GetSystemStatusCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.SystemService/GetSystemStatus");
try {
std::list<std::pair<std::string, std::string>> dev_list;
dmgr_.getDeviceList(dev_list);
@ -1163,7 +1296,9 @@ grpc::Status gRPCSystemServiceImpl::GetDeviceList(
const api::GetDeviceListCommand_Request* request,
api::GetDeviceListCommand_Feedback* response)
{
(void)context;
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.SystemService/GetDeviceList");
(void)request;
try {
const auto snapshot = dmgr_.snapshot();
@ -1207,8 +1342,244 @@ grpc::Status gRPCSystemServiceImpl::GetDeviceList(
}
}
grpc::Status gRPCSystemServiceImpl::GetSafetyState(
grpc::ServerContext* context,
const cmvr::api::GetSafetyStateCommand_Request* request,
cmvr::api::GetSafetyStateCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.SystemService/GetSafetyState");
if (!request || !response) {
return grpc::Status(
grpc::StatusCode::INVALID_ARGUMENT,
"GetSafetyState request and response are required");
}
const auto scope = parseSafetyScope(request->scope(), true);
if (!scope.valid) {
setSafetyHeaderFailure(
response->mutable_header(),
cmvr::safety::SafetyReason::InvalidArgument,
scope.error);
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, scope.error);
}
const auto snapshot = dmgr_.safetyCoordinator().snapshot();
response->set_system_state(
toApiSystemAdmissionState(snapshot.system_state));
response->set_safety_epoch(snapshot.safety_epoch);
response->set_control_service_instance_id(snapshot.service_instance_id);
response->set_enforcement_mode(
cmvr::safety::toString(snapshot.enforcement_mode));
response->set_active_operation_id(snapshot.active_operation_id);
response->set_active_operation_phase(snapshot.active_operation_phase);
response->set_sampled_at_unix_ms(unixTimeMs());
std::unordered_set<std::string> requested_ids(
scope.device_ids.begin(), scope.device_ids.end());
for (const auto& device : snapshot.devices) {
if (!scope.all_devices &&
requested_ids.erase(device.descriptor.device_id) == 0U) {
continue;
}
populateDeviceSafetyState(device, *response->add_devices());
}
for (const auto& participant : snapshot.participants) {
populateSafetyParticipantState(
participant, *response->add_participants());
}
if (!requested_ids.empty()) {
const auto detail =
"safety device is not registered: " + *requested_ids.begin();
response->Clear();
setSafetyHeaderFailure(
response->mutable_header(),
cmvr::safety::SafetyReason::DeviceNotFound,
detail);
return grpc::Status(grpc::StatusCode::NOT_FOUND, detail);
}
response->mutable_header()->set_success(true);
response->mutable_header()->set_reason_code(
cmvr::api::COMMAND_REASON_CODE_NONE);
response->mutable_header()->set_service_instance_id(
snapshot.service_instance_id);
response->mutable_header()->set_safety_epoch(snapshot.safety_epoch);
response->mutable_header()->set_execution_state(
cmvr::api::COMMAND_EXECUTION_STATE_COMPLETED);
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
return grpc::Status::OK;
}
grpc::Status gRPCSystemServiceImpl::RecoverSafetyState(
grpc::ServerContext* context,
const cmvr::api::RecoverSafetyStateCommand_Request* request,
cmvr::api::RecoverSafetyStateCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.SystemService/RecoverSafetyState");
if (!request || !response) {
return grpc::Status(
grpc::StatusCode::INVALID_ARGUMENT,
"RecoverSafetyState request and response are required");
}
const auto scope = parseSafetyScope(request->scope(), false);
if (!scope.valid || request->recovery_id().empty() ||
request->reason().empty() || request->expected_safety_epoch() == 0 ||
request->mode() ==
cmvr::api::RecoverSafetyStateCommand::MODE_UNSPECIFIED) {
std::string detail = scope.valid
? "recovery_id, reason, expected_safety_epoch, and mode are required"
: scope.error;
setSafetyHeaderFailure(
response->mutable_header(),
request->reason().empty()
? cmvr::safety::SafetyReason::RecoveryReasonRequired
: cmvr::safety::SafetyReason::InvalidArgument,
detail);
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, detail);
}
if (!recovery_audit_sink_) {
const std::string detail =
"persistent recovery audit is not configured";
setSafetyHeaderFailure(
response->mutable_header(),
cmvr::safety::SafetyReason::RecoveryAuditFailed,
detail);
return grpc::Status(
grpc::StatusCode::FAILED_PRECONDITION,
"RECOVERY_AUDIT_FAILED");
}
const bool verify_only = request->mode() ==
cmvr::api::RecoverSafetyStateCommand::VERIFY_ONLY;
RecoveryAuditRecord audit;
audit.occurred_at_unix_ms = unixTimeMs();
audit.stage = "accepted";
audit.correlation_id = cmvr_grpc_call_guard.context().correlation_id;
audit.principal_id = cmvr_grpc_call_guard.context().principal.id;
audit.peer = cmvr_grpc_call_guard.context().peer;
audit.recovery_id = request->recovery_id();
audit.reason = request->reason();
audit.mode = verify_only ? "verify_only" : "clear_software_latch";
audit.all_devices = scope.all_devices;
audit.device_ids = scope.device_ids;
audit.expected_safety_epoch = request->expected_safety_epoch();
audit.result = "pending";
std::string audit_error;
if (!recovery_audit_sink_->append(audit, &audit_error)) {
const auto detail = audit_error.empty()
? std::string("persistent recovery audit write failed")
: audit_error;
setSafetyHeaderFailure(
response->mutable_header(),
cmvr::safety::SafetyReason::RecoveryAuditFailed,
detail);
return grpc::Status(
grpc::StatusCode::FAILED_PRECONDITION,
"RECOVERY_AUDIT_FAILED");
}
auto deadline = cmvr_grpc_call_guard.context().deadline;
const auto configured_deadline =
cmvr::safety::SafetyClock::now() +
dmgr_.safetyCoordinator().config().recovery_timeout;
if (deadline == cmvr::safety::SafetyClock::time_point::max() ||
configured_deadline < deadline) {
deadline = configured_deadline;
}
if (request->timeout_ms() != 0) {
deadline = std::min(
deadline,
cmvr::safety::SafetyClock::now() +
std::chrono::milliseconds(request->timeout_ms()));
}
cmvr::safety::RecoveryRequest coordinator_request;
coordinator_request.recovery_id = request->recovery_id();
coordinator_request.device_ids = scope.device_ids;
coordinator_request.all_devices = scope.all_devices;
coordinator_request.expected_safety_epoch =
request->expected_safety_epoch();
coordinator_request.verify_only = verify_only;
coordinator_request.reason = request->reason();
coordinator_request.deadline = deadline;
if (!verify_only) {
auto commit_audit = audit;
commit_audit.stage = "clear_commit";
commit_audit.result = "authorized";
const auto sink = recovery_audit_sink_;
coordinator_request.authorize_clear =
[sink, commit_audit = std::move(commit_audit)]() mutable {
commit_audit.occurred_at_unix_ms = unixTimeMs();
std::string error;
const bool persisted = sink->append(commit_audit, &error);
if (!persisted) {
CMVR_LOG(ERROR)
<< "[gRPCSystemServiceImpl] Recovery clear audit "
"failed: "
<< error;
}
return persisted;
};
}
const auto result =
dmgr_.safetyCoordinator().recover(coordinator_request);
response->set_recovery_id(result.recovery_id);
response->set_result(toApiRecoveryResult(result.result));
response->set_previous_safety_epoch(result.previous_safety_epoch);
response->set_current_safety_epoch(result.current_safety_epoch);
response->set_system_state(
toApiSystemAdmissionState(result.system_state));
for (const auto& target : result.targets) {
populateSafetyTargetResult(target, *response->add_targets());
}
const bool success = recoveryCompletedAsRequested(result.result);
auto* header = response->mutable_header();
header->set_success(success);
header->set_command_id(request->recovery_id());
header->set_service_instance_id(
dmgr_.safetyCoordinator().serviceInstanceId());
header->set_safety_epoch(result.current_safety_epoch);
header->set_execution_state(
success ? cmvr::api::COMMAND_EXECUTION_STATE_COMPLETED
: cmvr::api::COMMAND_EXECUTION_STATE_FAILED);
if (success) {
header->set_reason_code(cmvr::api::COMMAND_REASON_CODE_NONE);
} else if (!result.targets.empty()) {
header->set_reason_code(
toApiSafetyReason(result.targets.front().reason));
header->set_error_message(result.targets.front().detail);
} else {
header->set_reason_code(
cmvr::api::COMMAND_REASON_CODE_INTERNAL_ERROR);
header->set_error_message("recovery failed without a target result");
}
setCurrentTimestamp(header->mutable_timestamp());
audit.occurred_at_unix_ms = unixTimeMs();
audit.stage = "completed";
audit.previous_safety_epoch = result.previous_safety_epoch;
audit.current_safety_epoch = result.current_safety_epoch;
audit.result = cmvr::safety::toString(result.result);
if (!recovery_audit_sink_->append(audit, &audit_error)) {
CMVR_LOG(ERROR)
<< "[gRPCSystemServiceImpl] Recovery completion audit failed: "
<< audit_error;
}
return grpc::Status::OK;
}
grpc::Status gRPCSystemServiceImpl::UpdateParams(grpc::ServerContext* context, const cmvr::api::UpdateParamsCommand_Request* request, cmvr::api::UpdateParamsCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.SystemService/UpdateParams");
response->mutable_header()->set_success(false);
response->mutable_header()->set_error_message("UpdateParams is no longer supported. Use typed device commands or reload configuration.");
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
@ -1218,6 +1589,75 @@ grpc::Status gRPCSystemServiceImpl::UpdateParams(grpc::ServerContext* context, c
grpc::Status gRPCSystemServiceImpl::StopAll(grpc::ServerContext* context,
const cmvr::api::StopAllCommand_Request* request, cmvr::api::StopAllCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context, "/cmvr.api.SystemService/StopAll");
if (!request || !response) {
return grpc::Status(
grpc::StatusCode::INVALID_ARGUMENT,
"StopAll request and response are required");
}
if (dmgr_.safetyCoordinator().config().enforcement_mode !=
cmvr::safety::EnforcementMode::Legacy) {
auto deadline = cmvr_grpc_call_guard.context().deadline;
const auto configured_deadline =
cmvr::safety::SafetyClock::now() + stop_timeout_;
if (deadline == cmvr::safety::SafetyClock::time_point::max() ||
configured_deadline < deadline) {
deadline = configured_deadline;
}
if (request->timeout_ms() != 0) {
deadline = std::min(
deadline,
cmvr::safety::SafetyClock::now() +
std::chrono::milliseconds(request->timeout_ms()));
}
std::string operation_id = request->operation_id();
if (operation_id.empty() && request->has_header()) {
operation_id = request->header().command_id();
}
const auto result = dmgr_.safetyCoordinator().stopAll(
std::move(operation_id), deadline);
response->set_operation_id(result.operation_id);
response->set_previous_safety_epoch(
result.previous_safety_epoch);
response->set_current_safety_epoch(
result.current_safety_epoch);
response->set_system_state(
toApiSystemAdmissionState(result.system_state));
for (const auto& target : result.targets) {
populateSafetyTargetResult(target, *response->add_targets());
}
auto* header = response->mutable_header();
header->set_success(result.success);
header->set_command_id(result.operation_id);
header->set_service_instance_id(
dmgr_.safetyCoordinator().serviceInstanceId());
header->set_safety_epoch(result.current_safety_epoch);
header->set_execution_state(
result.success
? cmvr::api::COMMAND_EXECUTION_STATE_COMPLETED
: cmvr::api::COMMAND_EXECUTION_STATE_FAILED);
if (result.success) {
header->set_reason_code(cmvr::api::COMMAND_REASON_CODE_NONE);
} else if (!result.targets.empty()) {
const auto failed = std::find_if(
result.targets.begin(), result.targets.end(),
[](const auto& target) { return !target.success; });
if (failed != result.targets.end()) {
header->set_reason_code(toApiSafetyReason(failed->reason));
header->set_error_message(failed->detail);
}
} else {
header->set_reason_code(
cmvr::api::COMMAND_REASON_CODE_STOP_UNCONFIRMED);
header->set_error_message(
"StopAll did not produce a participant result");
}
setCurrentTimestamp(header->mutable_timestamp());
return grpc::Status::OK;
}
(void)request;
const auto stop_deadline =
std::chrono::steady_clock::now() + stop_timeout_;
@ -1694,12 +2134,24 @@ grpc::Status gRPCSystemServiceImpl::ExecuteActionQueue(
const cmvr::api::ActionQueueCommand_Request* request,
cmvr::api::ActionQueueCommand_Feedback* response)
{
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
security_gateway_, context,
"/cmvr.api.SystemService/ExecuteActionQueue");
if (!request || !response) {
return grpc::Status(
grpc::StatusCode::INVALID_ARGUMENT,
"ActionQueue request and response are required");
}
try {
cmvr::safety::CommandActor actor;
actor.principal_id =
cmvr_grpc_call_guard.context().principal.id;
actor.authenticated =
cmvr_grpc_call_guard.context().principal.authenticated;
for (const auto role :
cmvr_grpc_call_guard.context().principal.roles) {
actor.roles.emplace_back(cmvr::service::toString(role));
}
// The callback is consumed only on this synchronous handler stack. It
// is never retained by the worker-owned action Record, so returning the
// RPC cannot leave a dangling ServerContext reference.
@ -1708,7 +2160,8 @@ grpc::Status gRPCSystemServiceImpl::ExecuteActionQueue(
*response,
[context]() {
return context && context->IsCancelled();
});
},
std::move(actor));
if (wait_result ==
ActionQueueExecutor::WaitResult::CanceledBeforeAdmission) {
return grpc::Status(

View File

@ -408,12 +408,21 @@ bool MediaActivityCoordinator::waitForStopped(
bool MediaActivityCoordinator::finishStopAll(
const StopAllTicket& ticket,
const bool all_media_stopped)
{
return finishStopAllDetailed(ticket, all_media_stopped)
.participant_stopped;
}
MediaActivityCoordinator::FinishStopAllResult
MediaActivityCoordinator::finishStopAllDetailed(
const StopAllTicket& ticket,
const bool all_media_stopped)
{
std::lock_guard lock(impl_->mutex);
if (!ticket.valid() || impl_->accepting ||
ticket.generation != impl_->generation ||
impl_->stop_all_tickets.erase(ticket.ticket_id) == 0U) {
return false;
return {};
}
const bool caller_succeeded =
@ -425,7 +434,20 @@ bool MediaActivityCoordinator::finishStopAll(
!impl_->hasSessionsBefore(ticket.generation)) {
impl_->accepting = true;
}
return caller_succeeded;
return {true, caller_succeeded, impl_->accepting};
}
void MediaActivityCoordinator::clearForTesting() noexcept
{
try {
std::lock_guard lock(impl_->mutex);
impl_->accepting = true;
impl_->stop_all_failed = false;
++impl_->generation;
impl_->stop_all_tickets.clear();
} catch (...) {
}
impl_->condition.notify_all();
}
MediaActivityCoordinator& globalMediaActivityCoordinator()

View File

@ -436,12 +436,21 @@ bool MotorActivityCoordinator::stopAndWait(
bool MotorActivityCoordinator::finishStopAll(
const StopAllTicket& ticket,
const bool all_motors_stopped)
{
return finishStopAllDetailed(ticket, all_motors_stopped)
.participant_stopped;
}
MotorActivityCoordinator::FinishStopAllResult
MotorActivityCoordinator::finishStopAllDetailed(
const StopAllTicket& ticket,
const bool all_motors_stopped)
{
std::lock_guard lock(impl_->mutex);
if (!ticket.valid() || impl_->accepting ||
ticket.generation != impl_->generation ||
impl_->stop_all_tickets.erase(ticket.ticket_id) == 0U) {
return false;
return {};
}
const bool caller_succeeded =
@ -454,7 +463,7 @@ bool MotorActivityCoordinator::finishStopAll(
impl_->accepting = true;
impl_->round_targets.clear();
}
return caller_succeeded;
return {true, caller_succeeded, impl_->accepting};
}
void MotorActivityCoordinator::notifyStateChanged() noexcept

View File

@ -166,6 +166,35 @@ TEST_F(CameraPtzActivityRegistryTest,
CameraPtzActivityRegistry::DispatchResult::Success);
}
TEST_F(CameraPtzActivityRegistryTest,
ExplicitStopRemainsAvailableWhileStartAdmissionIsLatched)
{
CameraPtzActivityRegistry registry;
auto camera = std::make_shared<TestCamera>("camera");
ASSERT_EQ(
registry.control(
camera->id(), camera, device::PtzCommand::PanLeft, false, 4),
CameraPtzActivityRegistry::DispatchResult::Success);
const auto ticket = globalStopAllAdmissionGate().beginStopAll();
ASSERT_TRUE(ticket.valid());
EXPECT_FALSE(globalStopAllAdmissionGate().finishStopAll(ticket, false));
EXPECT_EQ(
registry.control(
camera->id(), camera, device::PtzCommand::ZoomIn, false, 4),
CameraPtzActivityRegistry::DispatchResult::RejectedByStopAll);
EXPECT_EQ(
registry.control(
camera->id(), camera, device::PtzCommand::PanLeft, true, 4),
CameraPtzActivityRegistry::DispatchResult::Success);
EXPECT_EQ(registry.activeCommandCount(), 0U);
const auto calls = camera->calls();
ASSERT_EQ(calls.size(), 2U);
EXPECT_FALSE(calls.front().stop);
EXPECT_TRUE(calls.back().stop);
}
TEST_F(CameraPtzActivityRegistryTest,
StopForDeviceIsSelectiveAndActiveIdsReflectFailures)
{
@ -260,5 +289,61 @@ TEST_F(CameraPtzActivityRegistryTest,
EXPECT_TRUE(globalStopAllAdmissionGate().finishStopAll(ticket, true));
}
TEST_F(CameraPtzActivityRegistryTest,
DispatchFenceRunsInsidePerDeviceQueueBeforeHardwareMutation)
{
CameraPtzActivityRegistry registry;
auto camera = std::make_shared<TestCamera>("camera");
std::mutex mutex;
std::condition_variable condition;
bool first_fence_entered = false;
bool release_first_fence = false;
std::atomic<bool> second_fence_entered{false};
std::thread first([&] {
EXPECT_EQ(
registry.control(
camera->id(), camera, device::PtzCommand::PanLeft, false, 4,
[&] {
std::unique_lock lock(mutex);
first_fence_entered = true;
condition.notify_all();
condition.wait(lock, [&] { return release_first_fence; });
return true;
}),
CameraPtzActivityRegistry::DispatchResult::Success);
});
{
std::unique_lock lock(mutex);
condition.wait(lock, [&] { return first_fence_entered; });
}
std::thread second([&] {
EXPECT_EQ(
registry.control(
camera->id(), camera, device::PtzCommand::ZoomIn, false, 7,
[&] {
second_fence_entered = true;
return false;
}),
CameraPtzActivityRegistry::DispatchResult::RejectedByDispatchFence);
});
std::this_thread::yield();
EXPECT_FALSE(second_fence_entered.load());
{
std::lock_guard lock(mutex);
release_first_fence = true;
}
condition.notify_all();
first.join();
second.join();
EXPECT_TRUE(second_fence_entered.load());
const auto calls = camera->calls();
ASSERT_EQ(calls.size(), 1U);
EXPECT_EQ(calls.front().command, device::PtzCommand::PanLeft);
}
} // namespace
} // namespace cmvr::service

View File

@ -588,6 +588,25 @@ protected:
response.header().error_message()};
}
grpc::Status identifiedMoveJ(
const std::string& command_id,
const double position,
api::MoveJ_Response& response)
{
api::MoveJ_Request request;
auto* header = request.mutable_header();
header->set_device_id("aubo_arm");
header->set_command_id(command_id);
header->set_expected_service_instance_id(
device::DeviceManager::getInstance()
.safetyCoordinator()
.serviceInstanceId());
header->set_valid_for_ms(1000);
request.mutable_target()->add_position(position);
grpc::ServerContext context;
return service_->moveJ(&context, &request, &response);
}
MoveOutcome moveL(const std::string& device_id)
{
api::MoveL_Request request;
@ -833,6 +852,49 @@ TEST_F(GrpcArmServiceTest, MoveBindsLeaseRevocationCancellation)
EXPECT_FALSE(aubo_arm_->lastMotionCancellationRequested());
}
TEST_F(GrpcArmServiceTest,
IdenticalCommandIdReplaysCachedResultWithoutRedispatch)
{
api::MoveJ_Response first;
api::MoveJ_Response retry;
const auto first_status = identifiedMoveJ(
"arm-movej-idempotency-1", 0.1, first);
const auto retry_status = identifiedMoveJ(
"arm-movej-idempotency-1", 0.1, retry);
ASSERT_TRUE(first_status.ok()) << first_status.error_message();
ASSERT_TRUE(retry_status.ok()) << retry_status.error_message();
EXPECT_TRUE(first.header().success());
EXPECT_TRUE(retry.header().success());
EXPECT_EQ(
retry.header().execution_state(),
api::COMMAND_EXECUTION_STATE_COMPLETED);
EXPECT_EQ(retry.header().command_id(), "arm-movej-idempotency-1");
EXPECT_EQ(aubo_arm_->moveJCalls(), 1);
}
TEST_F(GrpcArmServiceTest,
ReusedCommandIdWithDifferentPayloadIsRejectedWithoutRedispatch)
{
api::MoveJ_Response first;
api::MoveJ_Response conflict;
const auto first_status = identifiedMoveJ(
"arm-movej-conflict-1", 0.1, first);
const auto conflict_status = identifiedMoveJ(
"arm-movej-conflict-1", 0.2, conflict);
ASSERT_TRUE(first_status.ok()) << first_status.error_message();
EXPECT_EQ(
conflict_status.error_code(), grpc::StatusCode::ALREADY_EXISTS);
EXPECT_FALSE(conflict.header().success());
EXPECT_EQ(
conflict.header().reason_code(),
api::COMMAND_REASON_CODE_COMMAND_ID_CONFLICT);
EXPECT_EQ(aubo_arm_->moveJCalls(), 1);
}
TEST_F(GrpcArmServiceTest,
StopAllCancelsInFlightTorqueOnWithoutReportingSuccess)
{
@ -1109,6 +1171,9 @@ TEST_F(GrpcArmServiceTest, StopMotionFailureRetainsSafetyBarrier)
EXPECT_EQ(stop_status.error_code(), grpc::StatusCode::INTERNAL);
EXPECT_FALSE(stop_response.success());
EXPECT_EQ(
stop_response.reason_code(),
api::COMMAND_REASON_CODE_STOP_UNCONFIRMED);
EXPECT_FALSE(authority.validate(action_lease.token));
EXPECT_TRUE(authority.isLeased("aubo_arm"));
EXPECT_EQ(
@ -1143,8 +1208,12 @@ TEST_F(GrpcArmServiceTest, StopMotionExceptionRetainsSafetyBarrier)
const auto stop_status = stopMotion("aubo_arm", stop_response);
const auto rejected_move = moveL("aubo_arm");
EXPECT_EQ(stop_status.error_code(), grpc::StatusCode::INTERNAL);
EXPECT_EQ(
stop_status.error_code(), grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_FALSE(stop_response.success());
EXPECT_EQ(
stop_response.reason_code(),
api::COMMAND_REASON_CODE_STOP_UNCONFIRMED);
EXPECT_FALSE(authority.validate(action_lease.token));
EXPECT_TRUE(authority.isLeased("aubo_arm"));
EXPECT_EQ(

View File

@ -17,6 +17,8 @@
#include <gtest/gtest.h>
#include <unistd.h>
#include "manager/safety/include/device_safety_endpoint.h"
#include "manager/safety/include/safety_coordinator.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
namespace cmvr::service {
@ -253,11 +255,93 @@ private:
mutable std::set<std::thread::id> backend_threads_;
};
class FakeTeleopSafetyEndpoint final
: public safety::DeviceSafetyEndpoint {
public:
FakeTeleopSafetyEndpoint()
{
descriptor_.device_id = makeManifest().robot_id();
descriptor_.kind = device::DeviceKind::Arm;
descriptor_.default_policy =
safety::SafetyPolicyFamily::Control;
descriptor_.maximum_snapshot_age = 1s;
descriptor_.supports_active_refresh = true;
}
safety::DeviceSafetyDescriptor descriptor() const override
{
return descriptor_;
}
void bindPublisher(
safety::SafetySnapshotPublisher publisher) override
{
publisher_ = std::move(publisher);
}
void requestSafetyRefresh() noexcept override
{
if (!publisher_) {
return;
}
safety::DeviceSafetySnapshot snapshot;
snapshot.device_id = descriptor_.device_id;
snapshot.condition = safety::SafetyCondition::Nominal;
snapshot.device_generation = generation_;
snapshot.sample_sequence = ++sequence_;
snapshot.observed_at = safety::SafetyClock::now();
snapshot.connected = safety::TriState::True;
snapshot.operational_ready = safety::TriState::True;
snapshot.quiescent = safety::TriState::True;
snapshot.motion_active = safety::TriState::False;
snapshot.actuator_enabled = safety::TriState::True;
snapshot.emergency_stop_active = safety::TriState::False;
snapshot.protective_stop_active = safety::TriState::False;
snapshot.fault_active = safety::TriState::False;
(void)publisher_(std::move(snapshot));
}
void onDeviceGenerationChanged(
const std::uint64_t generation) noexcept override
{
generation_ = generation;
requestSafetyRefresh();
}
safety::HardwareCheckResult validateBeforeDispatch(
const safety::AdmissionPermit&) override
{
++hardware_checks_;
return {true, safety::SafetyReason::None, {}};
}
safety::RecoveryCheckResult reconcileAdmissionState(
const safety::RecoveryContext&) override
{
return {true, safety::SafetyReason::None, {}};
}
int hardwareChecks() const noexcept
{
return hardware_checks_.load();
}
private:
safety::DeviceSafetyDescriptor descriptor_;
safety::SafetySnapshotPublisher publisher_;
std::uint64_t generation_{1};
std::uint64_t sequence_{0};
std::atomic<int> hardware_checks_{0};
};
class TeleopServerHarness final {
public:
explicit TeleopServerHarness(
std::shared_ptr<ArmTeleopBackend> backend)
: service_(std::move(backend))
std::shared_ptr<ArmTeleopBackend> backend,
safety::SafetyCoordinator* safety_coordinator = nullptr)
: service_(
std::move(backend), nullptr, nullptr,
safety_coordinator)
{
socket_path_ =
"/tmp/cmvr_arm_teleop_service_test_" +
@ -775,5 +859,62 @@ TEST(ArmTeleopServiceTest,
admission.clearForTesting();
}
TEST(ArmTeleopServiceTest,
CoordinatorInvalidationStopsExistingSessionBeforeAnotherSetpoint)
{
safety::SafetyCoordinatorConfig config;
config.enforcement_mode = safety::EnforcementMode::EnforceAll;
safety::SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeTeleopSafetyEndpoint>();
ASSERT_TRUE(coordinator.registerDevice(
{endpoint->descriptor(), endpoint, {}}));
coordinator.updateDeviceRuntimeState(
makeManifest().robot_id(),
device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
auto backend = std::make_shared<FakeArmTeleopBackend>();
TeleopServerHarness harness(backend, &coordinator);
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + 2s);
auto stream = harness.stub().Teleoperate(&context);
ASSERT_TRUE(stream->Write(makeOpenFrame(makeManifest(), 500, 1500)));
expectOpeningFrames(*stream);
EXPECT_EQ(endpoint->hardwareChecks(), 1);
ASSERT_TRUE(stream->Write(makeSetpoint(1)));
arm_teleop::ServerFrame response;
ASSERT_TRUE(stream->Read(&response));
EXPECT_EQ(response.status().phase(), arm_teleop::SESSION_PHASE_ACTIVE);
EXPECT_EQ(response.status().applied_sequence(), 1U);
EXPECT_EQ(endpoint->hardwareChecks(), 2);
coordinator.quarantineDevice(
makeManifest().robot_id(),
safety::SafetyReason::OutcomeUnknown,
"test-quarantine");
ASSERT_TRUE(stream->Read(&response));
EXPECT_EQ(
response.status().phase(),
arm_teleop::SESSION_PHASE_LEASE_LOST);
EXPECT_EQ(
response.status().stop_reason(),
arm_teleop::STOP_REASON_EMERGENCY_STOP);
const auto status = stream->Finish();
EXPECT_TRUE(
status.error_code() == grpc::StatusCode::FAILED_PRECONDITION ||
status.error_code() == grpc::StatusCode::CANCELLED)
<< status.error_message();
EXPECT_EQ(backend->appliedSequences(),
(std::vector<std::uint64_t>{1U}));
ASSERT_FALSE(backend->stopReasons().empty());
EXPECT_EQ(
backend->stopReasons().back(),
arm_teleop::STOP_REASON_EMERGENCY_STOP);
}
} // namespace
} // namespace cmvr::service

View File

@ -0,0 +1,454 @@
#include "service/grpc/include/grpc_command_transaction.h"
#include <atomic>
#include <chrono>
#include <memory>
#include <stdexcept>
#include <string>
#include <gtest/gtest.h>
#include "cmvr/api/arm_command.pb.h"
#include "manager/safety/include/device_safety_endpoint.h"
namespace cmvr::service {
namespace {
class FakeEndpoint final : public safety::DeviceSafetyEndpoint {
public:
explicit FakeEndpoint(std::string device_id)
{
descriptor_.device_id = std::move(device_id);
descriptor_.kind = device::DeviceKind::Arm;
descriptor_.default_policy = safety::SafetyPolicyFamily::Control;
descriptor_.maximum_snapshot_age = std::chrono::seconds(1);
descriptor_.supports_active_refresh = true;
}
safety::DeviceSafetyDescriptor descriptor() const override
{
return descriptor_;
}
void bindPublisher(safety::SafetySnapshotPublisher publisher) override
{
publisher_ = std::move(publisher);
}
void requestSafetyRefresh() noexcept override
{
if (!publisher_) {
return;
}
safety::DeviceSafetySnapshot snapshot;
snapshot.device_id = descriptor_.device_id;
snapshot.condition = safety::SafetyCondition::Nominal;
snapshot.device_generation = 1;
snapshot.sample_sequence = ++sequence_;
snapshot.observed_at = safety::SafetyClock::now();
snapshot.connected = safety::TriState::True;
snapshot.operational_ready = safety::TriState::True;
snapshot.quiescent = safety::TriState::True;
snapshot.motion_active = safety::TriState::False;
snapshot.actuator_enabled = safety::TriState::True;
snapshot.emergency_stop_active = safety::TriState::False;
snapshot.protective_stop_active = safety::TriState::False;
snapshot.fault_active = safety::TriState::False;
(void)publisher_(std::move(snapshot));
}
safety::HardwareCheckResult validateBeforeDispatch(
const safety::AdmissionPermit&) override
{
++hardware_checks;
return final_check;
}
safety::RecoveryCheckResult reconcileAdmissionState(
const safety::RecoveryContext&) override
{
return {true, safety::SafetyReason::None, {}};
}
safety::DeviceSafetyDescriptor descriptor_;
safety::SafetySnapshotPublisher publisher_;
safety::HardwareCheckResult final_check{
true, safety::SafetyReason::None, {}};
std::atomic<std::uint64_t> sequence_{0};
std::atomic<int> hardware_checks{0};
};
api::MoveJ_Request moveRequest(
const safety::SafetyCoordinator& coordinator,
const std::string& command_id,
const double target = 0.25)
{
api::MoveJ_Request request;
request.mutable_header()->set_device_id("arm");
request.mutable_header()->set_command_id(command_id);
request.mutable_header()->set_expected_service_instance_id(
coordinator.serviceInstanceId());
request.mutable_header()->set_valid_for_ms(1000);
request.mutable_target()->add_position(target);
return request;
}
grpc::Status executeMove(
safety::SafetyCoordinator& coordinator,
const api::MoveJ_Request& request,
api::MoveJ_Response& response,
int& dispatches,
const bool throw_after_dispatch = false)
{
grpc::ServerContext context;
return executeRegisteredGrpcCommand(
makeDefaultGrpcSecurityGateway(),
&context,
coordinator,
"/cmvr.api.ArmService/moveJ",
&request,
&response,
[&](GrpcCommandTransaction& transaction) {
if (!transaction.beginDispatch()) {
return transaction.dispatchStatus();
}
++dispatches;
if (throw_after_dispatch) {
throw std::runtime_error("simulated lost driver acknowledgement");
}
response.mutable_header()->set_success(true);
return grpc::Status::OK;
});
}
TEST(GrpcCommandTransactionTest,
DeterministicHashIgnoresRetryIdentityButIncludesPayload)
{
safety::SafetyCoordinator coordinator;
auto first = moveRequest(coordinator, "command-1", 0.25);
auto retry = first;
retry.mutable_header()->set_command_id("command-2");
retry.mutable_header()->set_valid_for_ms(2000);
retry.mutable_header()->mutable_timestamp()->set_seconds(1234);
auto changed = retry;
changed.mutable_target()->set_position(0, 0.5);
const auto first_hash = deterministicGrpcPayloadHash(
"/cmvr.api.ArmService/moveJ", first);
EXPECT_FALSE(first_hash.empty());
EXPECT_EQ(
first_hash,
deterministicGrpcPayloadHash(
"/cmvr.api.ArmService/moveJ", retry));
EXPECT_NE(
first_hash,
deterministicGrpcPayloadHash(
"/cmvr.api.ArmService/moveJ", changed));
EXPECT_NE(
first_hash,
deterministicGrpcPayloadHash(
"/cmvr.api.ArmService/moveL", first));
}
TEST(GrpcCommandTransactionTest, SameIdReturnsCachedResponseWithoutRedispatch)
{
safety::SafetyCoordinator coordinator;
auto endpoint = std::make_shared<FakeEndpoint>("arm");
ASSERT_TRUE(coordinator.registerDevice(
{endpoint->descriptor(), endpoint, {}}));
coordinator.markStartupComplete();
const auto request = moveRequest(coordinator, "command-cache");
api::MoveJ_Response first_response;
int dispatches = 0;
ASSERT_TRUE(executeMove(
coordinator, request, first_response, dispatches).ok());
ASSERT_TRUE(first_response.header().success());
EXPECT_EQ(dispatches, 1);
auto retry = request;
retry.mutable_header()->set_valid_for_ms(2500);
retry.mutable_header()->mutable_timestamp()->set_seconds(42);
api::MoveJ_Response cached_response;
ASSERT_TRUE(executeMove(
coordinator, retry, cached_response, dispatches).ok());
EXPECT_EQ(dispatches, 1);
EXPECT_TRUE(cached_response.header().success());
EXPECT_EQ(
cached_response.header().execution_state(),
api::COMMAND_EXECUTION_STATE_COMPLETED);
EXPECT_EQ(cached_response.header().command_id(), "command-cache");
auto conflict = request;
conflict.mutable_target()->set_position(0, 0.75);
api::MoveJ_Response conflict_response;
const auto conflict_status = executeMove(
coordinator, conflict, conflict_response, dispatches);
EXPECT_EQ(conflict_status.error_code(), grpc::StatusCode::ALREADY_EXISTS);
EXPECT_EQ(dispatches, 1);
EXPECT_EQ(
conflict_response.header().reason_code(),
api::COMMAND_REASON_CODE_COMMAND_ID_CONFLICT);
}
TEST(GrpcCommandTransactionTest,
EnforcedCommandRequiresIdentityAndRunsFinalHardwareCheck)
{
safety::SafetyCoordinatorConfig config;
config.enforcement_mode = safety::EnforcementMode::EnforceAll;
safety::SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>("arm");
ASSERT_TRUE(coordinator.registerDevice(
{endpoint->descriptor(), endpoint, {}}));
coordinator.updateDeviceRuntimeState(
"arm",
device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
auto missing_identity = moveRequest(coordinator, "");
api::MoveJ_Response rejected;
int dispatches = 0;
const auto rejected_status = executeMove(
coordinator, missing_identity, rejected, dispatches);
EXPECT_EQ(
rejected_status.error_code(), grpc::StatusCode::INVALID_ARGUMENT);
EXPECT_EQ(dispatches, 0);
EXPECT_EQ(
rejected.header().reason_code(),
api::COMMAND_REASON_CODE_COMMAND_ID_REQUIRED);
auto accepted = moveRequest(coordinator, "command-enforced");
api::MoveJ_Response accepted_response;
ASSERT_TRUE(executeMove(
coordinator, accepted, accepted_response, dispatches).ok());
EXPECT_EQ(dispatches, 1);
EXPECT_EQ(endpoint->hardware_checks.load(), 1);
EXPECT_EQ(
accepted_response.header().device_generation(), 1U);
}
TEST(GrpcCommandTransactionTest,
ExceptionAfterDispatchIsQuarantinedAndNeverRedispatched)
{
safety::SafetyCoordinatorConfig config;
config.enforcement_mode = safety::EnforcementMode::EnforceAll;
safety::SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>("arm");
ASSERT_TRUE(coordinator.registerDevice(
{endpoint->descriptor(), endpoint, {}}));
coordinator.updateDeviceRuntimeState(
"arm",
device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
const auto request = moveRequest(coordinator, "command-unknown");
api::MoveJ_Response response;
int dispatches = 0;
ASSERT_TRUE(executeMove(
coordinator, request, response, dispatches, true).ok());
EXPECT_EQ(dispatches, 1);
EXPECT_EQ(
response.header().reason_code(),
api::COMMAND_REASON_CODE_OUTCOME_UNKNOWN);
ASSERT_EQ(coordinator.snapshot().devices.size(), 1U);
EXPECT_EQ(
coordinator.snapshot().devices.front().admission_state,
safety::DeviceAdmissionState::Quarantined);
api::MoveJ_Response retry_response;
ASSERT_TRUE(executeMove(
coordinator, request, retry_response, dispatches).ok());
EXPECT_EQ(dispatches, 1);
EXPECT_EQ(
retry_response.header().execution_state(),
api::COMMAND_EXECUTION_STATE_OUTCOME_UNKNOWN);
}
TEST(GrpcCommandTransactionTest,
ScopedDispatchChecksHardwareForEverySubmission)
{
safety::SafetyCoordinatorConfig config;
config.enforcement_mode = safety::EnforcementMode::EnforceAll;
safety::SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>("arm");
ASSERT_TRUE(coordinator.registerDevice(
{endpoint->descriptor(), endpoint, {}}));
coordinator.updateDeviceRuntimeState(
"arm",
device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
const auto request = moveRequest(coordinator, "command-scoped");
api::MoveJ_Response response;
grpc::ServerContext context;
int dispatches = 0;
const auto status = executeRegisteredGrpcCommand(
makeDefaultGrpcSecurityGateway(),
&context,
coordinator,
"/cmvr.api.ArmService/moveJ",
&request,
&response,
[&](GrpcCommandTransaction& transaction) {
for (int index = 0; index < 2; ++index) {
auto dispatch = transaction.beginScopedDispatch();
if (!dispatch.acquired()) {
return transaction.dispatchStatus();
}
++dispatches;
}
response.mutable_header()->set_success(true);
return grpc::Status::OK;
});
EXPECT_TRUE(status.ok());
EXPECT_TRUE(response.header().success());
EXPECT_EQ(dispatches, 2);
EXPECT_EQ(endpoint->hardware_checks.load(), 2);
}
TEST(GrpcCommandTransactionTest,
RevokedActuationPermitCannotSuppressInternalSafetyStop)
{
safety::SafetyCoordinatorConfig config;
config.enforcement_mode = safety::EnforcementMode::EnforceAll;
safety::SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>("arm");
ASSERT_TRUE(coordinator.registerDevice(
{endpoint->descriptor(), endpoint, {}}));
coordinator.updateDeviceRuntimeState(
"arm",
device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
const auto request = moveRequest(coordinator, "command-revoked");
api::MoveJ_Response response;
grpc::ServerContext context;
int actuations = 0;
int safety_stops = 0;
const auto status = executeRegisteredGrpcCommand(
makeDefaultGrpcSecurityGateway(),
&context,
coordinator,
"/cmvr.api.ArmService/moveJ",
&request,
&response,
[&](GrpcCommandTransaction& transaction) {
{
auto dispatch = transaction.beginScopedDispatch();
if (!dispatch.acquired()) {
return transaction.dispatchStatus();
}
++actuations;
}
const auto stop_all = coordinator.stopAll("revoke-scoped-permit");
EXPECT_TRUE(stop_all.success);
EXPECT_FALSE(transaction.revalidate());
const auto revoked_status = transaction.dispatchStatus();
EXPECT_FALSE(transaction.beginScopedDispatch().acquired());
auto stop_dispatch = transaction.beginSafetyStopDispatch();
if (stop_dispatch.acquired()) {
++safety_stops;
}
response.mutable_header()->set_success(false);
return revoked_status;
});
// Ledger-backed commands report their terminal outcome in Feedback.
EXPECT_TRUE(status.ok());
EXPECT_FALSE(response.header().success());
EXPECT_EQ(
response.header().reason_code(),
api::COMMAND_REASON_CODE_SAFETY_LATCHED);
EXPECT_EQ(actuations, 1);
EXPECT_EQ(safety_stops, 1);
EXPECT_EQ(endpoint->hardware_checks.load(), 2);
}
TEST(GrpcCommandTransactionTest,
ServerDerivedStopRemainsDispatchableWhenActuationIsQuarantined)
{
safety::SafetyCoordinatorConfig config;
config.enforcement_mode = safety::EnforcementMode::EnforceAll;
safety::SafetyCoordinator coordinator(config);
auto endpoint = std::make_shared<FakeEndpoint>("arm");
ASSERT_TRUE(coordinator.registerDevice(
{endpoint->descriptor(), endpoint, {}}));
coordinator.updateDeviceRuntimeState(
"arm",
device::ManagedDeviceState::Running,
{device::DeviceHealthState::Healthy, {}});
coordinator.markStartupComplete();
coordinator.quarantineDevice(
"arm", safety::SafetyReason::OutcomeUnknown,
"uncertain-ptz-start");
const auto registered = defaultGrpcMethodPolicyRegistry().find(
"/cmvr.api.CameraService/ControlPtz");
ASSERT_TRUE(registered.has_value());
auto start_request = moveRequest(coordinator, "ptz-start");
api::MoveJ_Response start_response;
grpc::ServerContext start_context;
int start_dispatches = 0;
const auto start_status = executeServerDerivedGrpcCommand(
makeDefaultGrpcSecurityGateway(),
&start_context,
coordinator,
"/cmvr.api.CameraService/ControlPtz",
*registered,
&start_request,
&start_response,
[&](GrpcCommandTransaction& transaction) {
if (!transaction.beginDispatch()) {
return transaction.dispatchStatus();
}
++start_dispatches;
start_response.mutable_header()->set_success(true);
return grpc::Status::OK;
});
EXPECT_TRUE(start_status.ok());
EXPECT_EQ(start_dispatches, 0);
EXPECT_FALSE(start_response.header().success());
EXPECT_EQ(
start_response.header().reason_code(),
api::COMMAND_REASON_CODE_SAFETY_LATCHED);
auto stop_request = moveRequest(coordinator, "ptz-stop");
api::MoveJ_Response stop_response;
grpc::ServerContext stop_context;
auto stop_policy = *registered;
stop_policy.access = GrpcAccessClass::Stop;
stop_policy.command_intent = safety::CommandIntent::Stop;
stop_policy.safety_lane = true;
int stop_dispatches = 0;
const auto stop_status = executeServerDerivedGrpcCommand(
makeDefaultGrpcSecurityGateway(),
&stop_context,
coordinator,
"/cmvr.api.CameraService/ControlPtz",
std::move(stop_policy),
&stop_request,
&stop_response,
[&](GrpcCommandTransaction& transaction) {
if (!transaction.beginDispatch()) {
return transaction.dispatchStatus();
}
++stop_dispatches;
stop_response.mutable_header()->set_success(true);
return grpc::Status::OK;
});
EXPECT_TRUE(stop_status.ok());
EXPECT_TRUE(stop_response.header().success());
EXPECT_EQ(stop_dispatches, 1);
EXPECT_EQ(endpoint->hardware_checks.load(), 1);
}
} // namespace
} // namespace cmvr::service

View File

@ -668,7 +668,8 @@ TEST_F(MotorServiceTest,
EXPECT_TRUE(blocked_response.status().emergency_stopped());
}
TEST_F(MotorServiceTest, ProfileBackendExceptionReturnsInternalAndQuickStops)
TEST_F(MotorServiceTest,
ProfileBackendExceptionReportsUnknownOutcomeAndQuickStops)
{
protocol_->throw_profile_position_ = true;
@ -682,12 +683,22 @@ TEST_F(MotorServiceTest, ProfileBackendExceptionReturnsInternalAndQuickStops)
api::MotorCommandResponse response;
const auto status = service_->profilePosition(&context, &request, &response);
EXPECT_EQ(status.error_code(), grpc::StatusCode::INTERNAL);
EXPECT_EQ(status.error_code(), grpc::StatusCode::ABORTED);
EXPECT_FALSE(response.header().success());
EXPECT_EQ(
response.header().reason_code(),
api::COMMAND_REASON_CODE_OUTCOME_UNKNOWN);
EXPECT_NE(response.header().error_message().find(
"injected profile position exception"),
std::string::npos);
EXPECT_GE(protocol_->quick_stop_count_.load(), 1);
const auto safety = device::DeviceManager::getInstance()
.safetyCoordinator()
.snapshot();
ASSERT_EQ(safety.devices.size(), 1U);
EXPECT_EQ(
safety.devices.front().admission_state,
safety::DeviceAdmissionState::Quarantined);
}
TEST_F(MotorServiceTest, ExceptionCleanupKeepsMotorReservedUntilQuickStopFinishes)
@ -732,7 +743,10 @@ TEST_F(MotorServiceTest, ExceptionCleanupKeepsMotorReservedUntilQuickStopFinishe
&competing_context, &competing_request, &competing_response);
failing.join();
EXPECT_EQ(failing_status.error_code(), grpc::StatusCode::INTERNAL);
EXPECT_EQ(failing_status.error_code(), grpc::StatusCode::ABORTED);
EXPECT_EQ(
failing_response.header().reason_code(),
api::COMMAND_REASON_CODE_OUTCOME_UNKNOWN);
EXPECT_EQ(competing_status.error_code(),
grpc::StatusCode::RESOURCE_EXHAUSTED);
}

View File

@ -0,0 +1,294 @@
#include "service/grpc/include/grpc_security.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <google/protobuf/descriptor.h>
#include <gtest/gtest.h>
#include "cmvr/api/agv_service.pb.h"
#include "cmvr/api/arm_service.pb.h"
#include "cmvr/api/arm_teleop_v1.pb.h"
#include "cmvr/api/biohead_service.pb.h"
#include "cmvr/api/camera_service.pb.h"
#include "cmvr/api/dexhand_service.pb.h"
#include "cmvr/api/hlc_service.pb.h"
#include "cmvr/api/microphone_service.pb.h"
#include "cmvr/api/motor_service.pb.h"
#include "cmvr/api/speaker_service.pb.h"
#include "cmvr/api/system_service.pb.h"
#include "cmvr/api/test_service.pb.h"
namespace cmvr::service {
namespace {
GrpcMethodPolicy readPolicy()
{
return {
"/cmvr.api.SystemService/GetSystemInfo",
GrpcAccessClass::Read,
GrpcRole::Observer};
}
GrpcMethodPolicy recoveryPolicy()
{
return {
"/cmvr.api.SystemService/RecoverSafetyState",
GrpcAccessClass::Recover,
GrpcRole::SafetyAdmin};
}
class SafetyAdminProvider final : public GrpcAuthenticationProvider {
public:
GrpcAuthenticationResult authenticate(const GrpcCallFacts&) const override
{
GrpcPrincipal principal;
principal.id = "maintenance";
principal.method = GrpcAuthenticationMethod::StaticToken;
principal.authenticated = true;
principal.roles = {GrpcRole::SafetyAdmin};
return {std::move(principal), grpc::Status::OK};
}
};
TEST(GrpcSecurityConfigTest,
LegacyConfigPreservesAnonymousAccessButDisablesRecovery)
{
config::GRPCServerConfig config;
const auto result = resolveGrpcSecurityConfig(config, "0.0.0.0");
ASSERT_TRUE(result.valid) << result.error;
EXPECT_TRUE(result.config.legacy_compatibility);
EXPECT_TRUE(result.config.insecure_non_loopback);
EXPECT_EQ(
result.config.authentication, GrpcAuthenticationMethod::Disabled);
EXPECT_EQ(
result.config.recovery_exposure, GrpcRecoveryExposure::Disabled);
EXPECT_FALSE(result.warnings.empty());
}
TEST(GrpcSecurityConfigTest,
ExplicitNonLoopbackInsecureRequiresAcknowledgement)
{
config::GRPCServerConfig config;
auto* security = config.mutable_security();
security->set_transport_mode(config::GRPCSecurityConfig::INSECURE);
security->set_authentication_mode(config::GRPCSecurityConfig::DISABLED);
security->set_recovery_exposure(
config::GRPCSecurityConfig::RECOVERY_DISABLED);
const auto rejected = resolveGrpcSecurityConfig(config, "0.0.0.0");
EXPECT_FALSE(rejected.valid);
security->set_allow_insecure_non_loopback(true);
const auto accepted = resolveGrpcSecurityConfig(config, "0.0.0.0");
ASSERT_TRUE(accepted.valid) << accepted.error;
EXPECT_TRUE(accepted.config.insecure_non_loopback);
}
TEST(GrpcSecurityConfigTest,
UnsupportedAuthenticationNeverFallsBackToDisabled)
{
config::GRPCServerConfig config;
auto* security = config.mutable_security();
security->set_transport_mode(config::GRPCSecurityConfig::INSECURE);
security->set_authentication_mode(
config::GRPCSecurityConfig::STATIC_TOKEN);
security->set_recovery_exposure(
config::GRPCSecurityConfig::RECOVERY_AUTHORIZED);
const auto result = resolveGrpcSecurityConfig(config, "127.0.0.1");
EXPECT_FALSE(result.valid);
EXPECT_NE(result.error.find("DISABLED"), std::string::npos);
}
TEST(GrpcSecurityConfigTest, EnabledRecoveryRequiresPersistentAuditFile)
{
config::GRPCServerConfig config;
auto* security = config.mutable_security();
security->set_transport_mode(config::GRPCSecurityConfig::INSECURE);
security->set_authentication_mode(config::GRPCSecurityConfig::DISABLED);
security->set_recovery_exposure(
config::GRPCSecurityConfig::RECOVERY_LOCAL_ONLY);
const auto rejected =
resolveGrpcSecurityConfig(config, "127.0.0.1");
EXPECT_FALSE(rejected.valid);
EXPECT_NE(rejected.error.find("audit_file"), std::string::npos);
security->set_audit_file("/var/lib/cmvr-es/recovery-audit.jsonl");
const auto accepted =
resolveGrpcSecurityConfig(config, "127.0.0.1");
ASSERT_TRUE(accepted.valid) << accepted.error;
EXPECT_EQ(
accepted.config.recovery_audit_file,
"/var/lib/cmvr-es/recovery-audit.jsonl");
}
TEST(GrpcSecurityGatewayTest, DisabledProviderDoesNotTrustIdentityMetadata)
{
auto gateway = makeDefaultGrpcSecurityGateway();
GrpcCallFacts facts;
facts.peer = "ipv4:10.0.0.5:12345";
facts.metadata.emplace("principal", "safety-admin");
facts.metadata.emplace("role", "SafetyAdmin");
const auto call = gateway->beginCall(std::move(facts), readPolicy());
ASSERT_TRUE(call.allowed()) << call.status().error_message();
EXPECT_EQ(call.context().principal.id, "anonymous");
EXPECT_FALSE(call.context().principal.authenticated);
ASSERT_EQ(call.context().principal.roles.size(), 1U);
EXPECT_EQ(call.context().principal.roles.front(), GrpcRole::Anonymous);
}
TEST(GrpcSecurityGatewayTest, RecoveryIsDisabledBeforeLedgerAdmission)
{
auto gateway = makeDefaultGrpcSecurityGateway();
const auto call = gateway->beginCall(GrpcCallFacts{}, recoveryPolicy());
EXPECT_FALSE(call.allowed());
EXPECT_EQ(
call.status().error_code(), grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_EQ(call.status().error_message(), "RECOVERY_RPC_DISABLED");
}
TEST(GrpcSecurityGatewayTest, LocalOnlyRecoveryUsesActualPeerClassification)
{
GrpcSecurityRuntimeConfig config;
config.recovery_exposure = GrpcRecoveryExposure::LocalOnly;
auto gateway = makeGrpcSecurityGateway(config);
GrpcCallFacts remote;
remote.peer = "ipv4:192.168.1.20:42000";
EXPECT_FALSE(gateway->beginCall(remote, recoveryPolicy()).allowed());
GrpcCallFacts local;
local.peer = "ipv4:127.0.0.1:42000";
EXPECT_TRUE(gateway->beginCall(local, recoveryPolicy()).allowed());
EXPECT_TRUE(isLocalGrpcPeer("ipv6:[::1]"));
EXPECT_TRUE(isLocalGrpcPeer("unix:/run/cmvr-es.sock"));
EXPECT_FALSE(isLocalGrpcPeer("ipv4:10.0.0.1:50051"));
}
TEST(GrpcSecurityGatewayTest, AuthorizedRecoveryAcceptsSafetyAdminProvider)
{
GrpcSecurityRuntimeConfig config;
config.authentication = GrpcAuthenticationMethod::StaticToken;
config.recovery_exposure = GrpcRecoveryExposure::Authorized;
auto gateway = std::make_shared<GrpcSecurityGateway>(
config,
std::make_shared<SafetyAdminProvider>(),
std::make_shared<CompatibilityGrpcAuthorizationPolicy>(
GrpcRecoveryExposure::Authorized));
const auto call = gateway->beginCall(GrpcCallFacts{}, recoveryPolicy());
ASSERT_TRUE(call.allowed()) << call.status().error_message();
EXPECT_EQ(call.context().principal.id, "maintenance");
}
TEST(GrpcSecurityGatewayTest, AuditUsesEffectiveServerPrincipal)
{
std::vector<GrpcSecurityAuditRecord> records;
GrpcSecurityRuntimeConfig config;
auto gateway = makeGrpcSecurityGateway(
config,
[&records](const GrpcSecurityAuditRecord& record) {
records.push_back(record);
});
GrpcCallFacts facts;
facts.metadata.emplace("x-correlation-id", "request-42");
ASSERT_TRUE(gateway->beginCall(std::move(facts), readPolicy()).allowed());
ASSERT_EQ(records.size(), 1U);
EXPECT_EQ(records.front().correlation_id, "request-42");
EXPECT_EQ(records.front().principal_id, "anonymous");
EXPECT_FALSE(records.front().authenticated);
}
TEST(GrpcMethodPolicyRegistryTest, RejectsDuplicatesAndSortsSnapshot)
{
GrpcMethodPolicyRegistry registry;
EXPECT_TRUE(registry.registerPolicy(recoveryPolicy()));
EXPECT_TRUE(registry.registerPolicy(readPolicy()));
EXPECT_FALSE(registry.registerPolicy(readPolicy()));
const auto policies = registry.snapshot();
ASSERT_EQ(policies.size(), 2U);
EXPECT_LT(policies[0].full_method_name, policies[1].full_method_name);
EXPECT_TRUE(registry.find(readPolicy().full_method_name).has_value());
EXPECT_FALSE(registry.find("/unknown/method").has_value());
}
TEST(GrpcMethodPolicyRegistryTest,
DefaultRegistryCoversEveryCompiledApiMethod)
{
const auto& registry = defaultGrpcMethodPolicyRegistry();
const auto* pool = google::protobuf::DescriptorPool::generated_pool();
const std::vector<std::string> services{
"cmvr.api.AgvService",
"cmvr.api.ArmService",
"cmvr.api.armteleop.v1.ArmTeleopService",
"cmvr.api.BioHeadService",
"cmvr.api.CameraService",
"cmvr.api.DexHandService",
"cmvr.api.HlcService",
"cmvr.api.MicPhoneService",
"cmvr.api.MotorService",
"cmvr.api.SpeakerService",
"cmvr.api.SystemService",
"cmvr.api.TestService"};
std::size_t method_count = 0;
for (const auto& service_name : services) {
const auto* service = pool->FindServiceByName(service_name);
ASSERT_NE(service, nullptr) << service_name;
for (int index = 0; index < service->method_count(); ++index) {
const auto* method = service->method(index);
const std::string full_name =
"/" + service_name + "/" +
std::string(method->name());
const auto policy = registry.find(full_name);
ASSERT_TRUE(policy.has_value()) << full_name;
EXPECT_EQ(
policy->mutating,
policy->access != GrpcAccessClass::Read) << full_name;
if (policy->mutating) {
EXPECT_NE(
policy->command_intent,
safety::CommandIntent::Observe) << full_name;
}
++method_count;
}
}
EXPECT_EQ(registry.snapshot().size(), method_count);
}
TEST(GrpcMethodPolicyRegistryTest,
TouchIsAControlActuationRatherThanAReadOnlySensorCall)
{
const auto policy = defaultGrpcMethodPolicyRegistry().find(
"/cmvr.api.HlcService/touch");
ASSERT_TRUE(policy.has_value());
EXPECT_EQ(policy->access, GrpcAccessClass::Mutate);
EXPECT_EQ(policy->command_intent, safety::CommandIntent::Actuate);
EXPECT_EQ(policy->policy_family, safety::SafetyPolicyFamily::Control);
EXPECT_TRUE(policy->mutating);
EXPECT_FALSE(policy->safety_lane);
}
TEST(GrpcMethodPolicyRegistryTest, UnregisteredMethodFailsClosed)
{
const auto call = beginRegisteredGrpcCall(
makeDefaultGrpcSecurityGateway(), nullptr,
"/cmvr.api.UnknownService/Mutate");
EXPECT_FALSE(call.allowed());
EXPECT_EQ(call.status().error_code(), grpc::StatusCode::INTERNAL);
}
} // namespace
} // namespace cmvr::service

View File

@ -34,6 +34,8 @@
#include "service/grpc/include/camera_operational_activity_registry.h"
#include "service/grpc/include/camera_ptz_activity_registry.h"
#include "service/grpc/include/grpc_camera_service.h"
#include "service/grpc/include/grpc_recovery_audit.h"
#include "service/grpc/include/grpc_security.h"
#include "service/grpc/include/media_activity_coordinator.h"
#include "service/grpc/include/motor_activity_coordinator.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
@ -42,6 +44,46 @@
namespace cmvr::service {
namespace {
class AllowAllAuthorizationPolicy final : public GrpcAuthorizationPolicy {
public:
GrpcAuthorizationDecision authorize(
const GrpcRequestContext&,
const GrpcMethodPolicy&) const override
{
return {true, grpc::Status::OK};
}
};
class MemoryRecoveryAuditSink final : public RecoveryAuditSink {
public:
bool append(
const RecoveryAuditRecord& record,
std::string* error) noexcept override
{
if (fail) {
if (error) {
*error = "injected audit failure";
}
return false;
}
records.push_back(record);
return true;
}
bool fail{false};
std::vector<RecoveryAuditRecord> records;
};
std::shared_ptr<GrpcSecurityGateway> makeAllowAllRecoveryGateway()
{
GrpcSecurityRuntimeConfig config;
config.recovery_exposure = GrpcRecoveryExposure::LocalOnly;
return std::make_shared<GrpcSecurityGateway>(
config,
std::make_shared<DisabledGrpcAuthenticationProvider>(),
std::make_shared<AllowAllAuthorizationPolicy>());
}
class SnapshotDevice final : public device::AbstractDevice {
public:
SnapshotDevice(std::string id,
@ -1288,6 +1330,18 @@ const api::SystemDeviceInfo* findDevice(
return nullptr;
}
const api::SafetyOperationTargetResult* findSafetyTarget(
const api::StopAllCommand_Feedback& response,
const std::string& id)
{
for (const auto& target : response.targets()) {
if (target.target_id() == id) {
return &target;
}
}
return nullptr;
}
class GrpcSystemServiceTest : public ::testing::Test {
protected:
void SetUp() override
@ -1299,6 +1353,7 @@ protected:
globalStopAllAdmissionGate().clearForTesting();
globalCameraOperationalActivityRegistry().clearForTesting();
globalCameraPtzActivityRegistry().clearForTesting();
globalMediaActivityCoordinator().clearForTesting();
globalMotorActivityCoordinator().clearForTesting();
device::DeviceManager::destroyInstance();
}
@ -1320,6 +1375,7 @@ protected:
globalStopAllAdmissionGate().clearForTesting();
globalCameraOperationalActivityRegistry().clearForTesting();
globalCameraPtzActivityRegistry().clearForTesting();
globalMediaActivityCoordinator().clearForTesting();
globalMotorActivityCoordinator().clearForTesting();
(void)media::globalMediaSourceHub().stopAllSources();
}
@ -1510,6 +1566,122 @@ TEST_F(GrpcSystemServiceTest, EmptyListReturnsMetadataAndTimestamps)
EXPECT_LE(response.sampled_at_unix_ms(), after_ms);
}
TEST_F(GrpcSystemServiceTest, SafetyStateUsesCoordinatorMemorySnapshot)
{
config::DeviceManagerConfig config;
auto& manager = device::DeviceManager::getInstance(config);
registerDevice(
manager,
std::make_shared<SnapshotDevice>(
"safety-camera",
device::DeviceKind::Camera,
"SafetyCamera",
device::DeviceHealthSnapshot{
device::DeviceHealthState::Healthy, {}}));
service_ = std::make_unique<gRPCSystemServiceImpl>();
api::GetSafetyStateCommand_Request request;
api::GetSafetyStateCommand_Feedback response;
grpc::ServerContext context;
const auto status = service_->GetSafetyState(
&context, &request, &response);
ASSERT_TRUE(status.ok()) << status.error_message();
ASSERT_TRUE(response.header().success())
<< response.header().error_message();
EXPECT_FALSE(response.control_service_instance_id().empty());
EXPECT_EQ(
response.header().service_instance_id(),
response.control_service_instance_id());
EXPECT_GT(response.safety_epoch(), 0U);
ASSERT_EQ(response.devices_size(), 1);
EXPECT_EQ(response.devices(0).device_id(), "safety-camera");
EXPECT_EQ(response.devices(0).policy_family(), "Sensor");
ASSERT_GT(response.participants_size(), 0);
EXPECT_FALSE(response.participants(0).participant_id().empty());
EXPECT_FALSE(response.participants(0).phase().empty());
EXPECT_TRUE(response.participants(0).registered());
api::GetSystemInfoCommand_Request info_request;
api::GetSystemInfoCommand_Feedback info_response;
grpc::ServerContext info_context;
ASSERT_TRUE(service_->GetSystemInfo(
&info_context, &info_request, &info_response).ok());
EXPECT_EQ(
info_response.control_service_instance_id(),
response.control_service_instance_id());
EXPECT_EQ(info_response.safety_schema_version(), 1U);
}
TEST_F(GrpcSystemServiceTest, RecoveryIsDisabledByDefault)
{
config::DeviceManagerConfig config;
auto& manager = device::DeviceManager::getInstance(config);
service_ = std::make_unique<gRPCSystemServiceImpl>();
api::RecoverSafetyStateCommand_Request request;
request.set_recovery_id("recovery-disabled");
request.mutable_scope()->set_all_devices(true);
request.set_expected_safety_epoch(
manager.safetyCoordinator().snapshot().safety_epoch);
request.set_mode(api::RecoverSafetyStateCommand::VERIFY_ONLY);
request.set_reason("diagnostic verification");
api::RecoverSafetyStateCommand_Feedback response;
grpc::ServerContext context;
const auto status = service_->RecoverSafetyState(
&context, &request, &response);
EXPECT_EQ(status.error_code(), grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_EQ(status.error_message(), "RECOVERY_RPC_DISABLED");
}
TEST_F(GrpcSystemServiceTest, RecoveryRequiresDurableAuditBeforeCoordinator)
{
config::DeviceManagerConfig config;
auto& manager = device::DeviceManager::getInstance(config);
manager.registerDevice(std::make_shared<SnapshotDevice>(
"audit-camera", device::DeviceKind::Camera, "AuditCamera"));
auto audit = std::make_shared<MemoryRecoveryAuditSink>();
service_ = std::make_unique<gRPCSystemServiceImpl>(
std::chrono::seconds(1), makeAllowAllRecoveryGateway(), audit);
api::RecoverSafetyStateCommand_Request request;
request.set_recovery_id("recovery-audited");
request.mutable_scope()->set_all_devices(true);
request.set_expected_safety_epoch(
manager.safetyCoordinator().snapshot().safety_epoch);
request.set_mode(api::RecoverSafetyStateCommand::VERIFY_ONLY);
request.set_reason("verify the local work cell");
request.set_timeout_ms(500);
api::RecoverSafetyStateCommand_Feedback response;
grpc::ServerContext context;
const auto status = service_->RecoverSafetyState(
&context, &request, &response);
ASSERT_TRUE(status.ok()) << status.error_message();
EXPECT_EQ(audit->records.size(), 2U);
EXPECT_EQ(audit->records.front().stage, "accepted");
EXPECT_EQ(audit->records.back().stage, "completed");
EXPECT_EQ(response.recovery_id(), "recovery-audited");
const auto epoch_before_failure =
manager.safetyCoordinator().snapshot().safety_epoch;
audit->fail = true;
request.set_recovery_id("recovery-audit-fails");
request.set_expected_safety_epoch(epoch_before_failure);
response.Clear();
grpc::ServerContext failed_context;
const auto failed = service_->RecoverSafetyState(
&failed_context, &request, &response);
EXPECT_EQ(failed.error_code(), grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_EQ(
response.header().reason_code(),
api::COMMAND_REASON_CODE_RECOVERY_AUDIT_FAILED);
EXPECT_EQ(
manager.safetyCoordinator().snapshot().safety_epoch,
epoch_before_failure);
}
TEST_F(GrpcSystemServiceTest, MapsEveryKnownDeviceKind)
{
struct ExpectedMapping {
@ -1605,6 +1777,77 @@ TEST_F(GrpcSystemServiceTest, ActionQueueExecutesFourMoveLStepsSerially)
"arm:L:1", "arm:L:2", "arm:L:3", "arm:L:4"}));
}
TEST_F(GrpcSystemServiceTest,
ActionQueueRejectsStaleDeviceGenerationBeforeStepDispatch)
{
config::DeviceManagerConfig config;
config.mutable_safety()->set_mode(
config::SafetyCoordinatorConfig::ENFORCE_ALL);
auto& manager = device::DeviceManager::getInstance(config);
action_trace_ = std::make_shared<ActionTrace>();
action_arm_ = std::make_shared<ActionTestArm>(
"action-arm", action_trace_);
manager.registerDevice(action_arm_);
manager.safetyCoordinator().markStartupComplete();
std::optional<std::uint64_t> current_device_generation;
const auto snapshot_deadline =
std::chrono::steady_clock::now() + std::chrono::seconds(1);
while (std::chrono::steady_clock::now() < snapshot_deadline) {
const auto safety = manager.safetyCoordinator().snapshot();
ASSERT_EQ(
safety.system_state, safety::SystemAdmissionState::Open);
const auto safety_device = std::find_if(
safety.devices.begin(), safety.devices.end(),
[this](const auto& device) {
return device.descriptor.device_id == action_arm_->id();
});
if (safety_device != safety.devices.end() &&
safety_device->safety.fresh) {
current_device_generation =
safety_device->safety.snapshot.device_generation;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
ASSERT_TRUE(current_device_generation.has_value());
service_ = std::make_unique<gRPCSystemServiceImpl>();
api::GetSystemInfoCommand_Request info_request;
api::GetSystemInfoCommand_Feedback info_response;
grpc::ServerContext info_context;
ASSERT_TRUE(service_->GetSystemInfo(
&info_context, &info_request, &info_response).ok());
api::ActionQueueCommand_Request request;
request.set_action_id("stale-step-generation");
request.set_expected_service_instance_id(
info_response.action_service_instance_id());
auto* step = addMoveLStep(
request, "move", action_arm_->id(), 1.0);
step->mutable_arm_move_l()
->mutable_header()
->set_expected_device_generation(
*current_device_generation + 1U);
api::ActionQueueCommand_Feedback response;
grpc::ServerContext context;
const auto status = service_->ExecuteActionQueue(
&context, &request, &response);
EXPECT_TRUE(status.ok()) << status.error_message();
EXPECT_FALSE(response.header().success());
EXPECT_EQ(response.result(), api::ACTION_RESULT_CODE_REJECTED);
EXPECT_EQ(response.completed_steps(), 0U);
ASSERT_TRUE(response.has_failed_step_index());
EXPECT_EQ(response.failed_step_index(), 0U);
EXPECT_NE(
response.header().error_message().find("generation"),
std::string::npos)
<< response.header().error_message();
EXPECT_EQ(action_arm_->motionCalls(), 0);
EXPECT_TRUE(action_trace_->names().empty());
}
TEST_F(GrpcSystemServiceTest, ActionQueuePreservesArmDelayAgvOrder)
{
initializeActionDevices();
@ -2432,14 +2675,14 @@ TEST_F(GrpcSystemServiceTest,
auto trace = std::make_shared<ActionTrace>();
auto arm = std::make_shared<ActionTestArm>(
"health-blocked-arm", trace);
manager.registerDevice(arm);
service_ = std::make_unique<gRPCSystemServiceImpl>();
arm->blockHealthSnapshot();
auto health_snapshot = std::async(
std::launch::async, [&manager] { return manager.snapshot(); });
auto registration = std::async(
std::launch::async, [&manager, arm] {
manager.registerDevice(arm);
});
const bool health_call_blocked = arm->waitForHealthSnapshot(
std::chrono::milliseconds(500));
service_ = std::make_unique<gRPCSystemServiceImpl>();
api::StopAllCommand_Request request;
auto stop_all = std::async(std::launch::async, [this, &request] {
@ -2456,9 +2699,9 @@ TEST_F(GrpcSystemServiceTest,
arm->releaseHealthSnapshot();
ASSERT_EQ(
health_snapshot.wait_for(std::chrono::seconds(1)),
registration.wait_for(std::chrono::seconds(1)),
std::future_status::ready);
(void)health_snapshot.get();
registration.get();
ASSERT_EQ(
stop_all.wait_for(std::chrono::seconds(1)),
std::future_status::ready);
@ -2634,7 +2877,11 @@ TEST_F(GrpcSystemServiceTest,
EXPECT_TRUE(second_status.ok()) << second_status.error_message();
EXPECT_TRUE(second_response.header().success())
<< second_response.header().error_message();
EXPECT_GE(action_arm_->stopMotionCalls(), 4);
EXPECT_EQ(first_response.operation_id(), second_response.operation_id());
EXPECT_EQ(
first_response.current_safety_epoch(),
second_response.current_safety_epoch());
EXPECT_EQ(action_arm_->stopMotionCalls(), 2);
}
TEST_F(GrpcSystemServiceTest,
@ -2694,9 +2941,13 @@ TEST_F(GrpcSystemServiceTest,
ASSERT_TRUE(status.ok()) << status.error_message();
EXPECT_FALSE(response.header().success());
EXPECT_NE(
response.header().error_message().find("remain quarantined"),
std::string::npos);
const auto* agv_target = findSafetyTarget(response, action_agv_->id());
ASSERT_NE(agv_target, nullptr);
EXPECT_EQ(
agv_target->reason_code(),
api::COMMAND_REASON_CODE_DEVICE_STILL_MOVING);
EXPECT_EQ(
agv_target->result(), api::SAFETY_OPERATION_RESULT_FAILED);
const auto lease =
control::ControlAuthorityManager::instance().tryAcquire(
action_agv_->id(),
@ -2734,11 +2985,11 @@ TEST_F(GrpcSystemServiceTest,
<< response.header().error_message();
EXPECT_EQ(camera->stopRecordingCalls(), 1);
EXPECT_FALSE(camera->isRecording());
EXPECT_EQ(microphone->stopRecordingCalls(), 1);
EXPECT_EQ(microphone->stopRecordingCalls(), 2);
EXPECT_FALSE(microphone->isRecording());
EXPECT_EQ(speaker->stopPlaybackCalls(), 2);
EXPECT_EQ(camera->lifecycleStopCalls(), 0);
EXPECT_EQ(camera->operationalStopCalls(), 1);
EXPECT_GE(camera->operationalStopCalls(), 2);
EXPECT_FALSE(camera->operationalActive());
EXPECT_EQ(microphone->lifecycleStopCalls(), 0);
EXPECT_EQ(speaker->lifecycleStopCalls(), 0);
@ -2765,7 +3016,7 @@ TEST_F(GrpcSystemServiceTest,
ASSERT_TRUE(status.ok()) << status.error_message();
ASSERT_TRUE(response.header().success())
<< response.header().error_message();
EXPECT_EQ(camera->operationalStopCalls(), 1);
EXPECT_EQ(camera->operationalStopCalls(), 2);
EXPECT_FALSE(camera->operationalActive());
EXPECT_EQ(camera->lifecycleStopCalls(), 0);
}
@ -2935,10 +3186,12 @@ TEST_F(GrpcSystemServiceTest,
ASSERT_TRUE(stop_status.ok()) << stop_status.error_message();
EXPECT_FALSE(stop_response.header().success());
EXPECT_NE(
stop_response.header().error_message().find(
"could not confirm that every device stopped"),
std::string::npos);
const auto* camera_target = findSafetyTarget(
stop_response, camera->id());
ASSERT_NE(camera_target, nullptr);
EXPECT_EQ(
camera_target->reason_code(),
api::COMMAND_REASON_CODE_STOP_UNCONFIRMED);
EXPECT_EQ(camera->stopRecordingCalls(), 2);
EXPECT_TRUE(camera->isRecording());
EXPECT_EQ(microphone->stopRecordingCalls(), 2);
@ -3025,7 +3278,7 @@ TEST_F(GrpcSystemServiceTest,
}
TEST_F(GrpcSystemServiceTest,
StopAllSharesTimedOutArmStopAndDetailAcrossServiceInstances)
RetriedStopAllReusesTimedOutArmBarrierAndCanRecover)
{
config::DeviceManagerConfig config;
auto& manager = device::DeviceManager::getInstance(config);
@ -3059,153 +3312,47 @@ TEST_F(GrpcSystemServiceTest,
const bool final_stop_started = initial_stop_completed &&
arm->waitForBlockedStopMotion(std::chrono::milliseconds(500));
std::optional<std::pair<grpc::Status, api::StopAllCommand_Feedback>>
result;
std::optional<std::pair<grpc::Status, api::StopAllCommand_Feedback>>
second_result;
std::unique_ptr<gRPCSystemServiceImpl> second_service;
std::promise<void> last_destroy_started;
auto last_destroy_started_signal = last_destroy_started.get_future();
std::promise<void> replacement_construct_started;
auto replacement_construct_started_signal =
replacement_construct_started.get_future();
std::future<void> first_service_destroy;
std::future<void> last_service_destroy;
std::future<std::unique_ptr<gRPCSystemServiceImpl>>
replacement_service_construct;
std::optional<std::future_status> first_destroy_before_release;
std::optional<std::future_status> last_destroy_before_release;
std::optional<std::future_status> last_destroy_after_release;
std::optional<std::future_status> replacement_before_release;
std::optional<std::future_status> replacement_after_release;
std::unique_ptr<gRPCSystemServiceImpl> replacement_service;
bool dispatcher_destruction_observed{false};
int stop_calls_before_second{-1};
int stop_calls_after_second{-1};
control::ControlAcquireResult control_while_failed_closed;
if (completion == std::future_status::ready) {
result = stop_all.get();
stop_calls_before_second = arm->stopMotionCalls();
second_service = std::make_unique<gRPCSystemServiceImpl>(
std::chrono::milliseconds(300));
ASSERT_EQ(completion, std::future_status::ready);
const auto [status, response] = stop_all.get();
const auto stop_calls_before_retry = arm->stopMotionCalls();
const auto control_while_failed_closed = authority.tryAcquire(
arm->id(), "control-after-timed-out-stop-all",
std::chrono::seconds(30));
// Let the deadline-overrunning final typed stop finish before retrying the
// retained barrier. The old control owner is then allowed to drain.
arm->releaseBlockedStopMotion();
authority.release(old_handler.token);
auto second_service = std::make_unique<gRPCSystemServiceImpl>(
std::chrono::seconds(1));
api::StopAllCommand_Feedback second_response;
grpc::ServerContext second_context;
const auto second_status = second_service->StopAll(
&second_context, &request, &second_response);
second_result.emplace(second_status, std::move(second_response));
stop_calls_after_second = arm->stopMotionCalls();
control_while_failed_closed = authority.tryAcquire(
arm->id(), "control-after-timed-out-stop-all",
const auto recovered_control = authority.tryAcquire(
arm->id(), "control-after-retried-stop-all",
std::chrono::seconds(30));
first_service_destroy = std::async(
std::launch::async,
[this] { service_.reset(); });
first_destroy_before_release =
first_service_destroy.wait_for(std::chrono::seconds(1));
if (*first_destroy_before_release == std::future_status::ready) {
first_service_destroy.get();
last_service_destroy = std::async(
std::launch::async,
[&second_service, &last_destroy_started] {
last_destroy_started.set_value();
second_service.reset();
});
last_destroy_started_signal.wait();
dispatcher_destruction_observed =
gRPCSystemServiceImpl::
waitForStopDispatcherDestructionForTesting(
std::chrono::seconds(1));
if (dispatcher_destruction_observed) {
last_destroy_before_release =
last_service_destroy.wait_for(
std::chrono::milliseconds::zero());
replacement_service_construct = std::async(
std::launch::async,
[&replacement_construct_started] {
replacement_construct_started.set_value();
return std::make_unique<gRPCSystemServiceImpl>(
std::chrono::milliseconds(300));
});
replacement_construct_started_signal.wait();
replacement_before_release =
replacement_service_construct.wait_for(
std::chrono::milliseconds(100));
if (recovered_control.acquired) {
authority.release(recovered_control.token);
}
}
}
// Always release both test blocks before an assertion can abort the test;
// the dispatcher owns the final stop worker past the RPC deadline.
arm->releaseBlockedStopMotion();
authority.release(old_handler.token);
if (first_service_destroy.valid()) {
if (first_service_destroy.wait_for(std::chrono::seconds(1)) ==
std::future_status::ready) {
first_service_destroy.get();
}
}
if (last_service_destroy.valid()) {
last_destroy_after_release =
last_service_destroy.wait_for(std::chrono::seconds(1));
if (*last_destroy_after_release == std::future_status::ready) {
last_service_destroy.get();
}
}
if (replacement_service_construct.valid()) {
replacement_after_release =
replacement_service_construct.wait_for(std::chrono::seconds(1));
if (*replacement_after_release == std::future_status::ready) {
replacement_service = replacement_service_construct.get();
}
}
replacement_service.reset();
EXPECT_TRUE(initial_stop_completed);
EXPECT_TRUE(final_stop_started);
ASSERT_EQ(completion, std::future_status::ready);
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(second_result.has_value());
const auto& [status, response] = *result;
const auto& [second_status, second_response] = *second_result;
ASSERT_TRUE(status.ok()) << status.error_message();
EXPECT_FALSE(response.header().success());
EXPECT_NE(
response.header().error_message().find(
"timed out waiting for the preempted RobotArm control handler "
"to exit"),
std::string::npos);
const auto* failed_arm = findSafetyTarget(response, arm->id());
ASSERT_NE(failed_arm, nullptr);
EXPECT_EQ(
response.header().error_message().find(
"stop operation did not complete before the deadline"),
std::string::npos);
failed_arm->reason_code(),
api::COMMAND_REASON_CODE_PARTICIPANT_TIMEOUT);
ASSERT_TRUE(second_status.ok()) << second_status.error_message();
EXPECT_FALSE(second_response.header().success());
EXPECT_NE(
second_response.header().error_message().find(
"timed out waiting for the preempted RobotArm control handler "
"to exit"),
std::string::npos);
EXPECT_EQ(
second_response.header().error_message().find(
"stop operation did not complete before the deadline"),
std::string::npos);
EXPECT_EQ(stop_calls_before_second, 2);
EXPECT_EQ(stop_calls_after_second, stop_calls_before_second);
EXPECT_TRUE(second_response.header().success())
<< second_response.header().error_message();
EXPECT_EQ(stop_calls_before_retry, 2);
EXPECT_GE(arm->stopMotionCalls(), 4);
EXPECT_FALSE(control_while_failed_closed.acquired);
ASSERT_TRUE(first_destroy_before_release.has_value());
EXPECT_EQ(
*first_destroy_before_release, std::future_status::ready);
EXPECT_TRUE(dispatcher_destruction_observed);
ASSERT_TRUE(last_destroy_before_release.has_value());
ASSERT_TRUE(last_destroy_after_release.has_value());
ASSERT_TRUE(replacement_before_release.has_value());
ASSERT_TRUE(replacement_after_release.has_value());
EXPECT_EQ(
*last_destroy_before_release, std::future_status::timeout);
EXPECT_EQ(*last_destroy_after_release, std::future_status::ready);
EXPECT_EQ(
*replacement_before_release, std::future_status::timeout);
EXPECT_EQ(*replacement_after_release, std::future_status::ready);
EXPECT_TRUE(recovered_control.acquired) << recovered_control.detail;
}
TEST_F(GrpcSystemServiceTest,

View File

@ -257,6 +257,23 @@ int main()
CHECK_TRUE(coordinator.finishStopAll(concurrent_ticket_b, true));
CHECK_TRUE(coordinator.beginSession());
{
cmvr::service::MediaActivityCoordinator detailed_coordinator;
const auto first = detailed_coordinator.beginStopAll(true);
const auto second = detailed_coordinator.beginStopAll(true);
const auto first_result =
detailed_coordinator.finishStopAllDetailed(first, true);
CHECK_TRUE(first_result.ticket_consumed);
CHECK_TRUE(first_result.participant_stopped);
CHECK_TRUE(!first_result.admission_resumed);
const auto second_result =
detailed_coordinator.finishStopAllDetailed(second, true);
CHECK_TRUE(second_result.ticket_consumed);
CHECK_TRUE(second_result.participant_stopped);
CHECK_TRUE(second_result.admission_resumed);
CHECK_TRUE(detailed_coordinator.beginSession());
}
std::cout << "media_activity_coordinator_test: PASS\n";
return 0;
}

View File

@ -261,6 +261,23 @@ int main()
CHECK_TRUE(coordinator.finishStopAll(recovery_ticket, true));
CHECK_TRUE(coordinator.lockAdmission().accepting());
{
MotorActivityCoordinator detailed_coordinator;
const auto first = detailed_coordinator.beginStopAll(true);
const auto second = detailed_coordinator.beginStopAll(true);
const auto first_result =
detailed_coordinator.finishStopAllDetailed(first, true);
CHECK_TRUE(first_result.ticket_consumed);
CHECK_TRUE(first_result.participant_stopped);
CHECK_TRUE(!first_result.admission_resumed);
const auto second_result =
detailed_coordinator.finishStopAllDetailed(second, true);
CHECK_TRUE(second_result.ticket_consumed);
CHECK_TRUE(second_result.participant_stopped);
CHECK_TRUE(second_result.admission_resumed);
CHECK_TRUE(detailed_coordinator.lockAdmission().accepting());
}
std::cout << "motor_activity_coordinator_test: PASS\n";
return 0;
}

View File

@ -24,6 +24,8 @@
#include "cmvr/quic_edge/v1/quic_edge.pb.h"
#include "common/base/logging/logger.h"
#include "manager/device_manager/include/device_manager.h"
#include "manager/media_source_hub/include/device_media_source_adapter.h"
namespace cmvr::quic_edge {
namespace {
@ -1448,6 +1450,18 @@ void QuicEdgeService::refreshMediaTracks(
recordMediaError(source_error);
continue;
}
safety::DispatchGuard source_dispatch;
if (using_global_media_hub_) {
source_dispatch = media::beginMediaSourceStartDispatch(
device::DeviceManager::getInstance().safetyCoordinator(),
track_config.device_id());
if (!source_dispatch.acquired()) {
recordMediaError(
"MediaSourceHub safety admission rejected: " +
track.source_track_id);
continue;
}
}
track.subscription = media_hub_->subscribe(
track.source_track_id,
media::MediaSourceHub::StartPosition::LATEST_AVAILABLE,

View File

@ -13,6 +13,8 @@
namespace cmvr::service {
class ArmTeleopBackend;
class GrpcSecurityGateway;
class RecoveryAuditSink;
}
namespace cmvr::task {
@ -66,6 +68,8 @@ private:
std::unique_ptr<grpc::Service> arm_teleop_service_;
std::shared_ptr<cmvr::service::ArmTeleopBackend>
arm_teleop_backend_;
std::shared_ptr<cmvr::service::GrpcSecurityGateway> security_gateway_;
std::shared_ptr<cmvr::service::RecoveryAuditSink> recovery_audit_sink_;
std::unique_ptr<grpc::Service> motor_service_;
std::unique_ptr<grpc::Service> agv_service_;
std::unique_ptr<grpc::Service> hlc_service_;

View File

@ -1,5 +1,6 @@
#include "task/grpc_server_task/include/grpc_server_task.h"
#include <chrono>
#include <exception>
#include <grpcpp/ext/proto_server_reflection_plugin.h>
@ -14,6 +15,8 @@
#include "service/grpc/include/grpc_arm_service.h"
#include "service/grpc/include/grpc_arm_teleop_service.h"
#include "service/grpc/include/grpc_robot_arm_teleop_backend.h"
#include "service/grpc/include/grpc_recovery_audit.h"
#include "service/grpc/include/grpc_security.h"
#include "service/grpc/include/grpc_camera_service.h"
#include "service/grpc/include/grpc_dexhand_service.h"
#include "service/grpc/include/grpc_error_logging_interceptor.h"
@ -29,6 +32,13 @@ namespace cmvr::task {
namespace {
std::uint64_t unixTimeMs() noexcept
{
const auto value = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
return value > 0 ? static_cast<std::uint64_t>(value) : 1U;
}
std::shared_ptr<Task> createGrpcServerTask(const config::TaskConfigEntry& entry)
{
if (entry.id().empty()) {
@ -104,20 +114,35 @@ bool GrpcServerTask::start()
camera_service_ = std::make_unique<service::gRPCCameraServiceImpl>(
service::makeCameraStreamLowLatencyConfig(
cfg_.camera_stream_max_pending_frames(),
cfg_.camera_stream_max_frame_age_ms()));
system_service_ = std::make_unique<service::gRPCSystemServiceImpl>();
speaker_service_ = std::make_unique<service::gRPCSpeakerServiceImpl>();
microphone_service_ = std::make_unique<service::gRPCMicroPhoneServiceImpl>();
dexhand_service_ = std::make_unique<service::gRPCDexHandServiceImpl>();
biohand_service_ = std::make_unique<service::gRPCMBioHeadServiceImpl>();
arm_service_ = std::make_unique<service::gRPCArmServiceImpl>();
cfg_.camera_stream_max_frame_age_ms()),
security_gateway_);
system_service_ = std::make_unique<service::gRPCSystemServiceImpl>(
std::chrono::seconds(15),
security_gateway_,
recovery_audit_sink_);
speaker_service_ =
std::make_unique<service::gRPCSpeakerServiceImpl>(security_gateway_);
microphone_service_ =
std::make_unique<service::gRPCMicroPhoneServiceImpl>(security_gateway_);
dexhand_service_ =
std::make_unique<service::gRPCDexHandServiceImpl>(security_gateway_);
biohand_service_ =
std::make_unique<service::gRPCMBioHeadServiceImpl>(security_gateway_);
arm_service_ =
std::make_unique<service::gRPCArmServiceImpl>(security_gateway_);
arm_teleop_service_ =
std::make_unique<service::ArmTeleopServiceImpl>(
arm_teleop_backend_ ? arm_teleop_backend_
: service::makeDisabledArmTeleopBackend());
motor_service_ = std::make_unique<service::gRPCMotorServiceImpl>();
agv_service_ = std::make_unique<service::gRPCAgvServiceImpl>();
hlc_service_ = std::make_unique<service::gRPCHlcServiceImpl>();
: service::makeDisabledArmTeleopBackend(),
nullptr,
security_gateway_,
&device::DeviceManager::getInstance().safetyCoordinator());
motor_service_ =
std::make_unique<service::gRPCMotorServiceImpl>(security_gateway_);
agv_service_ =
std::make_unique<service::gRPCAgvServiceImpl>(security_gateway_);
hlc_service_ =
std::make_unique<service::gRPCHlcServiceImpl>(security_gateway_);
grpc::ServerBuilder builder;
builder.AddListeningPort(local_address, grpc::InsecureServerCredentials());
@ -151,7 +176,15 @@ bool GrpcServerTask::start()
address_ = local_address;
state_ = TaskState::RUNNING;
CMVR_LOG(INFO) << "[GrpcServerTask] gRPC server started, address=" << address_;
const auto& security = security_gateway_->config();
CMVR_LOG(INFO) << "[GrpcServerTask] gRPC server started, address=" << address_
<< ", transport=" << service::toString(security.transport)
<< ", authentication="
<< service::toString(security.authentication)
<< ", recovery="
<< service::toString(security.recovery_exposure)
<< ", insecure_non_loopback="
<< security.insecure_non_loopback;
wait_thread_ = std::thread(&GrpcServerTask::waitLoop, this);
return true;
}
@ -170,6 +203,54 @@ bool GrpcServerTask::init()
return false;
}
const std::string effective_host =
cfg_.host().empty() ? "0.0.0.0" : cfg_.host();
const auto security_result =
service::resolveGrpcSecurityConfig(cfg_, effective_host);
if (!security_result.valid) {
last_error_ = "invalid gRPC security config: " + security_result.error;
CMVR_LOG(ERROR) << "[GrpcServerTask] " << last_error_;
state_ = TaskState::FAILED;
return false;
}
for (const auto& warning : security_result.warnings) {
CMVR_LOG(WARNING) << "[GrpcServerTask] " << warning;
}
security_gateway_ = service::makeGrpcSecurityGateway(
security_result.config,
[](const service::GrpcSecurityAuditRecord& record) {
if (!record.allowed) {
CMVR_LOG(WARNING)
<< "[gRPC security] request denied, correlation_id="
<< record.correlation_id
<< ", method=" << record.full_method_name
<< ", principal=" << record.principal_id
<< ", peer=" << record.peer
<< ", code=" << static_cast<int>(record.status_code);
}
});
recovery_audit_sink_.reset();
if (security_result.config.recovery_exposure !=
service::GrpcRecoveryExposure::Disabled) {
recovery_audit_sink_ = service::makeFileRecoveryAuditSink(
security_result.config.recovery_audit_file);
service::RecoveryAuditRecord audit_probe;
audit_probe.occurred_at_unix_ms = unixTimeMs();
audit_probe.stage = "sink_initialized";
audit_probe.principal_id = "system";
audit_probe.result = "ready";
std::string audit_error;
if (!recovery_audit_sink_->append(audit_probe, &audit_error)) {
last_error_ =
"recovery audit initialization failed: " + audit_error;
CMVR_LOG(ERROR) << "[GrpcServerTask] " << last_error_;
recovery_audit_sink_.reset();
security_gateway_.reset();
state_ = TaskState::FAILED;
return false;
}
}
arm_teleop_backend_ = service::makeDisabledArmTeleopBackend();
if (cfg_.has_arm_teleop_backend() &&
cfg_.arm_teleop_backend().enable()) {

View File

@ -61,11 +61,22 @@ public:
RETRACTING, // 正在回退离开屏幕。
DONE, // 流程成功完成。
STOPPED, // 被外部 stop() 主动停止。
SAFETY_ADMISSION_REVOKED, // 统一安全会话在硬件下发前失效。
ROBOT_STATE_FAILED, // 读取机器人状态失败。
ROBOT_COMMAND_FAILED, // 向机器人下发控制命令失败。
TASK_BUSY // 已有触屏流程正在运行,新的 touch 请求被拒绝。
};
struct SafetyHooks {
using HardwareOperation = std::function<bool()>;
using Dispatch =
std::function<bool(const HardwareOperation&)>;
std::function<bool()> revalidate;
Dispatch dispatch_actuation;
Dispatch dispatch_stop;
};
explicit TouchScreenTask(const cmvr::config::TouchScreenTaskConfig& cfg);
~TouchScreenTask() = default;
@ -79,7 +90,8 @@ public:
bool touchIfCurrent(
int u,
int v,
const std::function<bool()>& still_admitted);
const std::function<bool()>& still_admitted,
SafetyHooks safety_hooks = {});
bool touch(int u, int v);
bool startFromPixel(int u, int v);
bool step(double dt) override;
@ -110,6 +122,7 @@ public:
int lastTouchNonzeroCount() const;
int lastActiveTagId() const;
Eigen::Vector3d lastAlignErrorCamera() const;
std::string controlDeviceId() const;
const std::shared_ptr<perception::AprilTagPerception>& perception() const { return perception_; }
const perception::TagRelativeTarget3D& tracker() const { return tracker_; }
@ -129,6 +142,12 @@ private:
bool activityControlCurrent() const;
std::function<bool()> activityCancellationRequested() const;
control::ControlDispatchGuard tryBeginActivityDispatch() const;
bool activitySafetyCurrent() const;
bool runArmActuationIfCurrent(
const SafetyHooks::HardwareOperation& operation) const;
bool runArmStopIfCurrent(
const SafetyHooks::HardwareOperation& operation) const;
void clearActivitySafetyHooksUnlocked() noexcept;
void releaseActivityControlUnlocked() noexcept;
void finishActivityUnlocked(Phase phase, Status status) noexcept;
bool startFromPixelUnlocked(int u, int v);
@ -183,6 +202,7 @@ private:
std::atomic<bool> activity_active_{false};
std::atomic<std::uint64_t> activity_generation_{1U};
control::ControlLeaseToken activity_control_token_;
SafetyHooks activity_safety_hooks_;
bool target_locked_{false};
bool ibvs_target_initialized_{false};
bool touch_command_started_{false};

View File

@ -52,6 +52,12 @@ public:
task.phase_ = TouchScreenTask::Phase::IDLE;
task.last_status_ = TouchScreenTask::Status::NOT_INITIALIZED;
}
static bool sendJointVelocity(TouchScreenTask& task)
{
std::lock_guard lock(task.mutex_);
return task.sendJointVelocity({});
}
};
} // namespace cmvr::task
@ -106,6 +112,7 @@ public:
cmvr::device::Result speedJ(
const cmvr::device::JointVelocityCommand&, double, double) override
{
++speed_j_calls_;
return success();
}
cmvr::device::Result stopJ(double) override { return success(); }
@ -128,7 +135,11 @@ public:
{
return success();
}
cmvr::device::Result stopMotion() override { return success(); }
cmvr::device::Result stopMotion() override
{
++stop_motion_calls_;
return success();
}
cmvr::device::Result startServoMode(
const cmvr::device::ServoOptions&) override
{
@ -199,11 +210,20 @@ public:
}
bool busy() const override { return false; }
int speedJCalls() const noexcept { return speed_j_calls_.load(); }
int stopMotionCalls() const noexcept
{
return stop_motion_calls_.load();
}
private:
static cmvr::device::Result success()
{
return cmvr::device::Result::success();
}
std::atomic<int> speed_j_calls_{0};
std::atomic<int> stop_motion_calls_{0};
};
class AdmissionCamera final : public cmvr::device::AbstractCamera {
@ -356,6 +376,82 @@ TEST_F(TouchScreenAdmissionTest,
EXPECT_TRUE(task.stopActivity());
}
TEST_F(TouchScreenAdmissionTest,
SafetyHooksFenceEveryArmActuation)
{
auto arm = std::make_shared<AdmissionRobotArm>();
auto camera = std::make_shared<AdmissionCamera>(
"touch-safety-hooks-camera");
cmvr::task::TouchScreenTask task(cmvr::config::TouchScreenTaskConfig{});
cmvr::task::TouchScreenTaskAdmissionTestPeer::prepare(
task, arm, camera);
bool admitted = true;
int revalidations = 0;
int dispatches = 0;
cmvr::task::TouchScreenTask::SafetyHooks hooks;
hooks.revalidate = [&] {
++revalidations;
return admitted;
};
hooks.dispatch_actuation = [&](const auto& operation) {
++dispatches;
return operation();
};
hooks.dispatch_stop = [](const auto& operation) {
return operation();
};
ASSERT_TRUE(task.touchIfCurrent(
10, 20, [] { return true; }, std::move(hooks)));
EXPECT_TRUE(
cmvr::task::TouchScreenTaskAdmissionTestPeer::sendJointVelocity(task));
EXPECT_EQ(arm->speedJCalls(), 1);
EXPECT_EQ(dispatches, 1);
EXPECT_GE(revalidations, 2);
admitted = false;
EXPECT_FALSE(
cmvr::task::TouchScreenTaskAdmissionTestPeer::sendJointVelocity(task));
EXPECT_EQ(arm->speedJCalls(), 1);
EXPECT_EQ(dispatches, 1);
EXPECT_TRUE(task.stopActivity());
}
TEST_F(TouchScreenAdmissionTest,
RevokedSafetySessionStopsActivityThroughStopLane)
{
auto arm = std::make_shared<AdmissionRobotArm>();
auto camera = std::make_shared<AdmissionCamera>(
"touch-revoked-safety-camera");
cmvr::task::TouchScreenTask task(cmvr::config::TouchScreenTaskConfig{});
cmvr::task::TouchScreenTaskAdmissionTestPeer::prepare(
task, arm, camera);
bool admitted = true;
int stop_dispatches = 0;
cmvr::task::TouchScreenTask::SafetyHooks hooks;
hooks.revalidate = [&] { return admitted; };
hooks.dispatch_actuation = [](const auto& operation) {
return operation();
};
hooks.dispatch_stop = [&](const auto& operation) {
++stop_dispatches;
return operation();
};
ASSERT_TRUE(task.touchIfCurrent(
10, 20, [] { return true; }, std::move(hooks)));
admitted = false;
EXPECT_FALSE(task.step(0.01));
EXPECT_EQ(task.phase(), cmvr::task::TouchScreenTask::Phase::FAILED);
EXPECT_EQ(
task.lastStatus(),
cmvr::task::TouchScreenTask::Status::SAFETY_ADMISSION_REVOKED);
EXPECT_EQ(stop_dispatches, 1);
EXPECT_EQ(arm->stopMotionCalls(), 1);
}
TEST_F(TouchScreenAdmissionTest,
StopAllStopsCameraActivityAndNextTouchRestartsIt)
{

View File

@ -467,7 +467,8 @@ bool TouchScreenTask::touch(const int u, const int v) {
bool TouchScreenTask::touchIfCurrent(
const int u,
const int v,
const std::function<bool()>& still_admitted)
const std::function<bool()>& still_admitted,
SafetyHooks safety_hooks)
{
auto& admission_gate = service::globalStopAllAdmissionGate();
std::uint64_t admission_generation = 0U;
@ -534,9 +535,21 @@ bool TouchScreenTask::touchIfCurrent(
return false;
}
resetActivityUnlocked();
activity_safety_hooks_ = std::move(safety_hooks);
if (!activitySafetyCurrent()) {
last_status_ = Status::SAFETY_ADMISSION_REVOKED;
activity_active_.store(false, std::memory_order_release);
clearActivitySafetyHooksUnlocked();
releaseActivityControlUnlocked();
rollback_camera();
return false;
}
const bool started = startFromPixelUnlocked(u, v);
if (!started) {
activity_active_.store(false, std::memory_order_release);
clearActivitySafetyHooksUnlocked();
releaseActivityControlUnlocked();
rollback_camera();
return false;
@ -556,6 +569,7 @@ bool TouchScreenTask::touchIfCurrent(
}
activity_active_.store(false, std::memory_order_release);
resetActivityUnlocked();
clearActivitySafetyHooksUnlocked();
releaseActivityControlUnlocked();
rollback_camera();
return false;
@ -643,6 +657,61 @@ TouchScreenTask::tryBeginActivityDispatch() const
token);
}
bool TouchScreenTask::activitySafetyCurrent() const
{
if (!activity_safety_hooks_.revalidate) {
return true;
}
try {
return activity_safety_hooks_.revalidate();
} catch (...) {
return false;
}
}
bool TouchScreenTask::runArmActuationIfCurrent(
const SafetyHooks::HardwareOperation& operation) const
{
if (!operation || !activitySafetyCurrent()) {
return false;
}
auto authority_dispatch = tryBeginActivityDispatch();
if (!authority_dispatch.acquired()) {
return false;
}
try {
return activity_safety_hooks_.dispatch_actuation
? activity_safety_hooks_.dispatch_actuation(operation)
: operation();
} catch (...) {
return false;
}
}
bool TouchScreenTask::runArmStopIfCurrent(
const SafetyHooks::HardwareOperation& operation) const
{
if (!operation) {
return false;
}
auto authority_dispatch = tryBeginActivityDispatch();
if (!authority_dispatch.acquired()) {
return false;
}
try {
return activity_safety_hooks_.dispatch_stop
? activity_safety_hooks_.dispatch_stop(operation)
: operation();
} catch (...) {
return false;
}
}
void TouchScreenTask::clearActivitySafetyHooksUnlocked() noexcept
{
activity_safety_hooks_ = {};
}
void TouchScreenTask::releaseActivityControlUnlocked() noexcept
{
control::ControlLeaseToken token;
@ -663,6 +732,7 @@ void TouchScreenTask::finishActivityUnlocked(
touch_command_started_ = false;
retract_command_started_ = false;
activity_active_.store(false, std::memory_order_release);
clearActivitySafetyHooksUnlocked();
releaseActivityControlUnlocked();
}
@ -680,7 +750,6 @@ bool TouchScreenTask::startFromPixelUnlocked(int u, int v) {
return false;
}
resetActivityUnlocked();
if (!moveToInitPositionBeforeStartIfEnabled()) {
return false;
}
@ -724,9 +793,19 @@ bool TouchScreenTask::step(const double dt) {
!activityControlCurrent()) {
activity_active_.store(false, std::memory_order_release);
resetActivityUnlocked();
clearActivitySafetyHooksUnlocked();
releaseActivityControlUnlocked();
return true;
}
if (activity_active_.load(std::memory_order_acquire) &&
!activitySafetyCurrent()) {
(void)runArmStopIfCurrent([this] {
return arm_ && arm_->stopMotion().ok();
});
finishActivityUnlocked(
Phase::FAILED, Status::SAFETY_ADMISSION_REVOKED);
return false;
}
if (!initialized_) {
last_status_ = Status::NOT_INITIALIZED;
return false;
@ -840,6 +919,7 @@ bool TouchScreenTask::stopActivity() {
isBusyUnlocked()) {
resetActivityUnlocked();
}
clearActivitySafetyHooksUnlocked();
releaseActivityControlUnlocked();
}
@ -977,6 +1057,14 @@ Eigen::Vector3d TouchScreenTask::lastAlignErrorCamera() const {
return last_align_error_camera_;
}
std::string TouchScreenTask::controlDeviceId() const {
std::lock_guard<std::mutex> lock(mutex_);
if (arm_ && !arm_->id().empty()) {
return arm_->id();
}
return config_.devices().arm_id();
}
std::string TouchScreenTask::stateString() const {
return taskStateToString(state());
}
@ -1020,6 +1108,7 @@ const char* TouchScreenTask::statusToString(const Status status) {
case Status::RETRACTING: return "RETRACTING";
case Status::DONE: return "DONE";
case Status::STOPPED: return "STOPPED";
case Status::SAFETY_ADMISSION_REVOKED: return "SAFETY_ADMISSION_REVOKED";
case Status::ROBOT_STATE_FAILED: return "ROBOT_STATE_FAILED";
case Status::ROBOT_COMMAND_FAILED: return "ROBOT_COMMAND_FAILED";
case Status::TASK_BUSY: return "TASK_BUSY";
@ -1688,9 +1777,9 @@ bool TouchScreenTask::stepRetracting() {
<< ", final_tcp_delta_base=unavailable";
}
try {
arm_->stopL();
} catch (...) {
if (!runArmStopIfCurrent([this] {
return arm_ && arm_->stopL().ok();
})) {
enterFailed(Status::ROBOT_COMMAND_FAILED);
return false;
}
@ -1738,17 +1827,11 @@ bool TouchScreenTask::sendJointVelocity(const std::vector<double>& qdot) const {
return false;
}
auto dispatch = tryBeginActivityDispatch();
if (!dispatch.acquired()) {
return false;
}
device::JointVelocityCommand cmd;
cmd.velocity = qdot;
const auto result = arm_->speedJ(cmd, 0.0, 0.0);
if (!result.ok()) {
return false;
}
return true;
return runArmActuationIfCurrent([this, &cmd] {
return arm_ && arm_->speedJ(cmd, 0.0, 0.0).ok();
});
}
bool TouchScreenTask::sendZeroJointVelocity() const {
@ -1835,15 +1918,9 @@ bool TouchScreenTask::holdCurrentControlledPosition() const {
joints.position.push_back(it->second);
}
auto dispatch = tryBeginActivityDispatch();
if (!dispatch.acquired()) {
return false;
}
const auto result = arm_->servoJ(joints);
if (!result.ok()) {
return false;
}
return true;
return runArmActuationIfCurrent([this, &joints] {
return arm_ && arm_->servoJ(joints).ok();
});
}
bool TouchScreenTask::buildInitJointPositions(std::vector<double>& positions_out) const {
@ -1870,8 +1947,9 @@ bool TouchScreenTask::moveToInitPositionBeforeStartIfEnabled() {
options.velocity = config_.initialization().velocity();
options.acceleration = config_.initialization().acceleration();
options.cancellation_requested = activityCancellationRequested();
const auto result = arm_->moveJ(init_cmd, options);
if (!result.ok()) {
if (!runArmActuationIfCurrent([this, &init_cmd, &options] {
return arm_ && arm_->moveJ(init_cmd, options).ok();
})) {
last_status_ = Status::ROBOT_COMMAND_FAILED;
return false;
}
@ -1892,17 +1970,16 @@ bool TouchScreenTask::moveToInitPositionIfEnabled() const {
options.velocity = config_.initialization().velocity();
options.acceleration = config_.initialization().acceleration();
options.cancellation_requested = activityCancellationRequested();
const auto result = arm_->moveJ(init_cmd, options);
if (!result.ok()) {
return false;
}
return true;
return runArmActuationIfCurrent([this, &init_cmd, &options] {
return arm_ && arm_->moveJ(init_cmd, options).ok();
});
}
bool TouchScreenTask::handleTouchTriggered(const bool stop_forward_motion) {
if (stop_forward_motion) {
const auto result = arm_->stopL();
if (!result.ok()) {
if (!runArmStopIfCurrent([this] {
return arm_ && arm_->stopL().ok();
})) {
return false;
}
}
@ -1936,17 +2013,15 @@ bool TouchScreenTask::startTouchPhase() {
last_status_ = Status::ROBOT_STATE_FAILED;
return false;
}
auto dispatch = tryBeginActivityDispatch();
if (!dispatch.acquired()) {
last_status_ = Status::ROBOT_COMMAND_FAILED;
return false;
}
const auto result = arm_->speedL(toCartesianVelocity(
cmvr::common::math::toEigenVec6(speed_l.twist_tool())),
const auto velocity = toCartesianVelocity(
cmvr::common::math::toEigenVec6(speed_l.twist_tool()));
if (!runArmActuationIfCurrent([this, velocity, &speed_l] {
return arm_ && arm_->speedL(
velocity,
speed_l.acceleration(),
0.0,
device::FrameType::Tool);
if (!result.ok()) {
device::FrameType::Tool).ok();
})) {
last_status_ = Status::ROBOT_COMMAND_FAILED;
return false;
}
@ -1973,8 +2048,10 @@ bool TouchScreenTask::startTouchPhase() {
options.joint_velocity_limits.assign(move_l.joint_velocity_limits().begin(),
move_l.joint_velocity_limits().end());
options.cancellation_requested = activityCancellationRequested();
const auto result = arm_->moveL(pose_cmd, options, device::FrameType::Tool);
if (!result.ok()) {
if (!runArmActuationIfCurrent([this, &pose_cmd, &options] {
return arm_ && arm_->moveL(
pose_cmd, options, device::FrameType::Tool).ok();
})) {
last_status_ = Status::ROBOT_COMMAND_FAILED;
return false;
}
@ -2018,17 +2095,14 @@ bool TouchScreenTask::startRetractPhase(const Phase next_phase_after_retract,
<< ", start_tcp_base=unavailable";
}
auto dispatch = tryBeginActivityDispatch();
if (!dispatch.acquired()) {
return false;
}
const auto result = arm_->speedL(retract_cmd,
if (!runArmActuationIfCurrent([this, &retract_cmd, &retract] {
return arm_ && arm_->speedL(
retract_cmd,
retract.acceleration(),
0.0,
device::FrameType::Tool);
if (!result.ok()) {
CMVR_LOG(ERROR) << "[TouchScreenTask][RETRACT_START] speedL failed: "
<< result.message;
device::FrameType::Tool).ok();
})) {
CMVR_LOG(ERROR) << "[TouchScreenTask][RETRACT_START] speedL failed";
return false;
}
@ -2043,12 +2117,9 @@ bool TouchScreenTask::startRetractPhase(const Phase next_phase_after_retract,
}
void TouchScreenTask::enterFailed(const Status status) {
try {
if (arm_) {
arm_->stopL();
}
} catch (...) {
}
(void)runArmStopIfCurrent([this] {
return arm_ && arm_->stopL().ok();
});
hardStopIbvsMotion();
holdCurrentControlledPosition();
const auto final_status = moveToInitPositionIfEnabled()

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,59 @@ package cmvr.api;
import "google/protobuf/timestamp.proto";
enum CommandReasonCode {
COMMAND_REASON_CODE_UNSPECIFIED = 0;
COMMAND_REASON_CODE_NONE = 1;
COMMAND_REASON_CODE_INVALID_ARGUMENT = 2;
COMMAND_REASON_CODE_UNAUTHENTICATED = 3;
COMMAND_REASON_CODE_PERMISSION_DENIED = 4;
COMMAND_REASON_CODE_RECOVERY_RPC_DISABLED = 5;
COMMAND_REASON_CODE_DEVICE_NOT_FOUND = 6;
COMMAND_REASON_CODE_DEVICE_UNAVAILABLE = 7;
COMMAND_REASON_CODE_UNSUPPORTED_COMMAND = 8;
COMMAND_REASON_CODE_SYSTEM_STARTING = 9;
COMMAND_REASON_CODE_SYSTEM_STOPPING = 10;
COMMAND_REASON_CODE_SAFETY_LATCHED = 11;
COMMAND_REASON_CODE_SAFETY_STATE_MISSING = 12;
COMMAND_REASON_CODE_SAFETY_STATE_STALE = 13;
COMMAND_REASON_CODE_HARDWARE_UNSAFE = 14;
COMMAND_REASON_CODE_EMERGENCY_STOP_ACTIVE = 15;
COMMAND_REASON_CODE_PROTECTIVE_STOP_ACTIVE = 16;
COMMAND_REASON_CODE_DEVICE_DISCONNECTED = 17;
COMMAND_REASON_CODE_DEVICE_FAULT = 18;
COMMAND_REASON_CODE_DEVICE_NOT_READY = 19;
COMMAND_REASON_CODE_DEVICE_STILL_MOVING = 20;
COMMAND_REASON_CODE_CONTROL_BUSY = 21;
COMMAND_REASON_CODE_GENERATION_MISMATCH = 22;
COMMAND_REASON_CODE_COMMAND_ID_REQUIRED = 23;
COMMAND_REASON_CODE_COMMAND_ID_CONFLICT = 24;
COMMAND_REASON_CODE_RESULT_EVICTED = 25;
COMMAND_REASON_CODE_LEDGER_EXHAUSTED = 26;
COMMAND_REASON_CODE_BACKPRESSURE = 27;
COMMAND_REASON_CODE_DEADLINE_EXCEEDED_BEFORE_DISPATCH = 28;
COMMAND_REASON_CODE_OUTCOME_UNKNOWN = 29;
COMMAND_REASON_CODE_PARTICIPANT_TIMEOUT = 30;
COMMAND_REASON_CODE_STOP_UNCONFIRMED = 31;
COMMAND_REASON_CODE_RECOVERY_EPOCH_MISMATCH = 32;
COMMAND_REASON_CODE_RECOVERY_REASON_REQUIRED = 33;
COMMAND_REASON_CODE_RECOVERY_AUDIT_FAILED = 34;
COMMAND_REASON_CODE_INTERNAL_ERROR = 35;
}
enum CommandExecutionState {
COMMAND_EXECUTION_STATE_UNSPECIFIED = 0;
COMMAND_EXECUTION_STATE_RECEIVED = 1;
COMMAND_EXECUTION_STATE_RESERVED = 2;
COMMAND_EXECUTION_STATE_REJECTED_BEFORE_DISPATCH = 3;
COMMAND_EXECUTION_STATE_ADMITTED = 4;
COMMAND_EXECUTION_STATE_DISPATCHING = 5;
COMMAND_EXECUTION_STATE_ACCEPTED_BY_HARDWARE = 6;
COMMAND_EXECUTION_STATE_COMPLETED = 7;
COMMAND_EXECUTION_STATE_FAILED = 8;
COMMAND_EXECUTION_STATE_CANCELED_BEFORE_DISPATCH = 9;
COMMAND_EXECUTION_STATE_OUTCOME_UNKNOWN = 10;
}
message DeviceLifecycle {
enum Lifecycle {
STATE_INIT = 0;
@ -20,12 +73,22 @@ message CommandHeader {
message Request {
string device_id = 1; // 目标设备名称
google.protobuf.Timestamp timestamp = 2; // 请求时间
string command_id = 3;
string expected_service_instance_id = 4;
optional uint64 expected_device_generation = 5;
uint32 valid_for_ms = 6;
}
message Feedback {
bool success = 1; // 是否成功
string error_message = 2; // 错误信息(成功时为空)
google.protobuf.Timestamp timestamp = 3; // 回复时间
CommandReasonCode reason_code = 4;
string command_id = 5;
string service_instance_id = 6;
uint64 safety_epoch = 7;
uint64 device_generation = 8;
CommandExecutionState execution_state = 9;
}
}

View File

@ -0,0 +1,186 @@
syntax = "proto3";
package cmvr.api;
import "cmvr/api/common.proto";
enum SafetyTriState {
SAFETY_TRI_STATE_UNKNOWN = 0;
SAFETY_TRI_STATE_FALSE = 1;
SAFETY_TRI_STATE_TRUE = 2;
}
enum SafetyCondition {
SAFETY_CONDITION_UNKNOWN = 0;
SAFETY_CONDITION_NOMINAL = 1;
SAFETY_CONDITION_RESTRICTED = 2;
SAFETY_CONDITION_UNSAFE = 3;
}
enum SystemAdmissionState {
SYSTEM_ADMISSION_STATE_UNSPECIFIED = 0;
SYSTEM_ADMISSION_STATE_STARTING = 1;
SYSTEM_ADMISSION_STATE_OPEN = 2;
SYSTEM_ADMISSION_STATE_STOPPING = 3;
SYSTEM_ADMISSION_STATE_LATCHED = 4;
SYSTEM_ADMISSION_STATE_RECOVERING = 5;
SYSTEM_ADMISSION_STATE_SHUTTING_DOWN = 6;
}
enum DeviceAdmissionState {
DEVICE_ADMISSION_STATE_UNSPECIFIED = 0;
DEVICE_ADMISSION_STATE_OBSERVING = 1;
DEVICE_ADMISSION_STATE_OPEN = 2;
DEVICE_ADMISSION_STATE_BLOCKED = 3;
DEVICE_ADMISSION_STATE_QUARANTINED = 4;
DEVICE_ADMISSION_STATE_RECOVERING = 5;
DEVICE_ADMISSION_STATE_REMOVED = 6;
}
enum SafetyBlockerScope {
SAFETY_BLOCKER_SCOPE_UNSPECIFIED = 0;
SAFETY_BLOCKER_SCOPE_DEVICE = 1;
SAFETY_BLOCKER_SCOPE_SYSTEM = 2;
}
enum SafetyRecoveryRequirement {
SAFETY_RECOVERY_REQUIREMENT_UNSPECIFIED = 0;
SAFETY_RECOVERY_REQUIREMENT_REFRESH_ONLY = 1;
SAFETY_RECOVERY_REQUIREMENT_CLEAR_SOFTWARE_LATCH = 2;
SAFETY_RECOVERY_REQUIREMENT_HARDWARE_RELEASE_REQUIRED = 3;
SAFETY_RECOVERY_REQUIREMENT_MANUAL_INSPECTION_REQUIRED = 4;
}
enum SafetyOperationResult {
SAFETY_OPERATION_RESULT_UNSPECIFIED = 0;
SAFETY_OPERATION_RESULT_SUCCEEDED = 1;
SAFETY_OPERATION_RESULT_RECOVERED = 2;
SAFETY_OPERATION_RESULT_VERIFIED_BUT_STILL_BLOCKED = 3;
SAFETY_OPERATION_RESULT_BLOCKER_REMAINS = 4;
SAFETY_OPERATION_RESULT_EPOCH_MISMATCH = 5;
SAFETY_OPERATION_RESULT_NOTHING_TO_RECOVER = 6;
SAFETY_OPERATION_RESULT_TIMED_OUT = 7;
SAFETY_OPERATION_RESULT_FAILED = 8;
}
message DeviceIdList {
repeated string device_ids = 1;
}
message SafetyScope {
oneof target {
bool all_devices = 1;
DeviceIdList devices = 2;
}
}
message SafetyBlockerInfo {
CommandReasonCode reason_code = 1;
SafetyBlockerScope scope = 2;
SafetyRecoveryRequirement recovery_requirement = 3;
string source_id = 4;
string operation_id = 5;
uint64 first_observed_at_unix_ms = 6;
uint64 last_observed_at_unix_ms = 7;
}
message DeviceSafetyStateInfo {
string device_id = 1;
string device_kind = 2;
string policy_family = 3;
string lifecycle_state = 4;
string health_state = 5;
DeviceAdmissionState admission_state = 6;
SafetyCondition condition = 7;
bool has_sample = 8;
bool snapshot_fresh = 9;
uint64 sample_age_ms = 10;
uint64 sample_sequence = 11;
uint64 observed_at_unix_ms = 12;
uint64 device_generation = 13;
SafetyTriState connected = 14;
SafetyTriState operational_ready = 15;
SafetyTriState quiescent = 16;
SafetyTriState motion_active = 17;
SafetyTriState actuator_enabled = 18;
SafetyTriState emergency_stop_active = 19;
SafetyTriState protective_stop_active = 20;
SafetyTriState fault_active = 21;
repeated SafetyBlockerInfo blockers = 22;
}
message SafetyOperationTargetResult {
string target_id = 1;
SafetyOperationResult result = 2;
CommandReasonCode reason_code = 3;
string detail = 4;
DeviceAdmissionState before_state = 5;
DeviceAdmissionState after_state = 6;
}
message SafetyParticipantResultInfo {
bool recorded = 1;
bool success = 2;
CommandReasonCode reason_code = 3;
string detail = 4;
}
message SafetyParticipantStateInfo {
string participant_id = 1;
string phase = 2;
bool required = 3;
bool registered = 4;
bool barrier_active = 5;
bool barrier_retained = 6;
string operation_id = 7;
uint64 safety_epoch = 8;
SafetyParticipantResultInfo last_request = 9;
SafetyParticipantResultInfo last_verify = 10;
SafetyParticipantResultInfo last_release = 11;
}
message GetSafetyStateCommand {
message Request {
SafetyScope scope = 1;
}
message Feedback {
CommandHeader.Feedback header = 1;
SystemAdmissionState system_state = 2;
uint64 safety_epoch = 3;
string control_service_instance_id = 4;
string enforcement_mode = 5;
repeated DeviceSafetyStateInfo devices = 6;
string active_operation_id = 7;
string active_operation_phase = 8;
uint64 sampled_at_unix_ms = 9;
repeated SafetyParticipantStateInfo participants = 10;
}
}
message RecoverSafetyStateCommand {
enum Mode {
MODE_UNSPECIFIED = 0;
VERIFY_ONLY = 1;
CLEAR_SOFTWARE_LATCH = 2;
}
message Request {
string recovery_id = 1;
SafetyScope scope = 2;
uint64 expected_safety_epoch = 3;
Mode mode = 4;
string reason = 5;
uint32 timeout_ms = 6;
}
message Feedback {
CommandHeader.Feedback header = 1;
SafetyOperationResult result = 2;
uint64 previous_safety_epoch = 3;
uint64 current_safety_epoch = 4;
SystemAdmissionState system_state = 5;
repeated SafetyOperationTargetResult targets = 6;
string recovery_id = 7;
}
}

View File

@ -3,6 +3,7 @@ syntax = "proto3";
import "cmvr/api/agv_command.proto";
import "cmvr/api/arm_command.proto";
import "cmvr/api/common.proto";
import "cmvr/api/safety_command.proto";
package cmvr.api;
@ -109,6 +110,16 @@ message GetSystemInfoCommand {
// Changes whenever the in-process ActionQueue idempotency ledger is
// recreated. Clients bind submissions and retries to this value.
string action_service_instance_id = 8;
// Effective server-side control-plane settings. These fields describe
// what is running, not merely what the configuration requested.
string grpc_transport_security = 9;
string grpc_authentication = 10;
string grpc_recovery_exposure = 11;
bool grpc_insecure_non_loopback = 12;
string control_service_instance_id = 13;
string safety_enforcement_mode = 14;
uint32 safety_schema_version = 15;
}
}
@ -141,10 +152,18 @@ message UpdateParamsCommand {
message StopAllCommand {
message Request {
CommandHeader.Request header = 1;
string operation_id = 2;
string expected_service_instance_id = 3;
uint32 timeout_ms = 4;
}
message Feedback {
CommandHeader.Feedback header = 1;
string operation_id = 2;
uint64 previous_safety_epoch = 3;
uint64 current_safety_epoch = 4;
SystemAdmissionState system_state = 5;
repeated SafetyOperationTargetResult targets = 6;
}
}

View File

@ -1,6 +1,7 @@
syntax = "proto3";
import "cmvr/api/system_command.proto";
import "cmvr/api/safety_command.proto";
package cmvr.api;
@ -15,4 +16,7 @@ service SystemService {
rpc StopAll(StopAllCommand.Request) returns (StopAllCommand.Feedback) {}
rpc ExecuteActionQueue(ActionQueueCommand.Request) returns (ActionQueueCommand.Feedback) {}
rpc GetSafetyState(GetSafetyStateCommand.Request) returns (GetSafetyStateCommand.Feedback) {}
rpc RecoverSafetyState(RecoverSafetyStateCommand.Request) returns (RecoverSafetyStateCommand.Feedback) {}
}

View File

@ -1,6 +1,25 @@
syntax = "proto3";
package cmvr.config;
message SafetyCoordinatorConfig {
enum EnforcementMode {
ENFORCEMENT_MODE_UNSPECIFIED = 0;
LEGACY = 1;
SHADOW = 2;
ENFORCE_SELECTED = 3;
ENFORCE_ALL = 4;
}
EnforcementMode mode = 1;
repeated string enforced_device_ids = 2;
uint32 stop_all_timeout_ms = 3;
uint32 recovery_timeout_ms = 4;
uint32 command_ledger_result_capacity = 5;
uint32 command_ledger_total_id_capacity = 6;
uint32 event_history_capacity = 7;
bool fail_startup_on_missing_control_capability = 8;
}
message DeviceConfigEntry {
enum DeviceType {
reserved 1, 2, 3, 4, 5, 6, 7, 10, 11;
@ -27,6 +46,10 @@ message DeviceConfigEntry {
DeviceType type = 2;
string config_file = 3;
bool enable = 4;
bool safety_enforce = 5;
uint32 maximum_safety_snapshot_age_ms = 6;
uint32 safety_stop_timeout_ms = 7;
bool required_control_device = 8;
}
message DeviceManagerConfig {
@ -35,6 +58,7 @@ message DeviceManagerConfig {
string description = 3;
repeated DeviceConfigEntry devices = 4;
bool init_all_motors_when_no_active_joints = 20;
SafetyCoordinatorConfig safety = 21;
}
message DeviceManagerRootConfig {
DeviceManagerConfig device_manager = 1;

View File

@ -24,6 +24,45 @@ message ArmTeleopBackendConfig {
double max_position_step_rad = 11;
}
message GRPCSecurityConfig {
enum TransportMode {
TRANSPORT_MODE_UNSPECIFIED = 0;
INSECURE = 1;
SERVER_TLS = 2;
MUTUAL_TLS = 3;
}
enum AuthenticationMode {
AUTHENTICATION_MODE_UNSPECIFIED = 0;
DISABLED = 1;
STATIC_TOKEN = 2;
JWT = 3;
TLS_CLIENT_CERTIFICATE = 4;
}
enum RecoveryExposure {
RECOVERY_EXPOSURE_UNSPECIFIED = 0;
RECOVERY_DISABLED = 1;
RECOVERY_LOCAL_ONLY = 2;
RECOVERY_AUTHORIZED = 3;
}
TransportMode transport_mode = 1;
AuthenticationMode authentication_mode = 2;
RecoveryExposure recovery_exposure = 3;
bool allow_insecure_non_loopback = 4;
// Reserved for optional providers. Selecting an unsupported provider causes
// startup to fail; it never falls back to DISABLED.
string server_certificate_file = 5;
string server_private_key_file = 6;
string client_ca_file = 7;
string static_token_file = 8;
string jwt_issuer = 9;
string jwt_audience = 10;
string audit_file = 11;
}
message GRPCServerConfig {
string host = 1;
string port = 2;
@ -36,6 +75,7 @@ message GRPCServerConfig {
// default so configurations written before these fields remain low-latency.
uint32 camera_stream_max_frame_age_ms = 6;
ArmTeleopBackendConfig arm_teleop_backend = 7;
GRPCSecurityConfig security = 8;
}
message GRPCServerRootConfig {
GRPCServerConfig grpc_server = 1;