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.
This commit is contained in:
parent
b2b0b63fe0
commit
f4be2ffaaa
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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` 的通道允许写入,否则返回
|
||||
|
||||
@ -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 ||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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()
|
||||
@ -509,6 +609,8 @@ void DeviceManager::update_device_status_(
|
||||
const ManagedDeviceState state,
|
||||
const std::string& error_message)
|
||||
{
|
||||
DeviceHealthSnapshot health;
|
||||
{
|
||||
std::unique_lock lock(devices_mutex_);
|
||||
auto& status = device_statuses_[device_id];
|
||||
if (status.id.empty()) {
|
||||
@ -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;
|
||||
|
||||
1022
cmvr-es/manager/device_manager/src/device_safety_adapters.cpp
Normal file
1022
cmvr-es/manager/device_manager/src/device_safety_adapters.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
65
cmvr-es/manager/safety/CMakeLists.txt
Normal file
65
cmvr-es/manager/safety/CMakeLists.txt
Normal 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()
|
||||
130
cmvr-es/manager/safety/include/command_ledger.h
Normal file
130
cmvr-es/manager/safety/include/command_ledger.h
Normal 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
|
||||
39
cmvr-es/manager/safety/include/device_safety_endpoint.h
Normal file
39
cmvr-es/manager/safety/include/device_safety_endpoint.h
Normal 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
|
||||
225
cmvr-es/manager/safety/include/safety_coordinator.h
Normal file
225
cmvr-es/manager/safety/include/safety_coordinator.h
Normal 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
|
||||
88
cmvr-es/manager/safety/include/safety_participant.h
Normal file
88
cmvr-es/manager/safety/include/safety_participant.h
Normal 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
|
||||
48
cmvr-es/manager/safety/include/safety_reason.h
Normal file
48
cmvr-es/manager/safety/include/safety_reason.h
Normal 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
|
||||
54
cmvr-es/manager/safety/include/safety_snapshot_store.h
Normal file
54
cmvr-es/manager/safety/include/safety_snapshot_store.h
Normal 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
|
||||
235
cmvr-es/manager/safety/include/safety_types.h
Normal file
235
cmvr-es/manager/safety/include/safety_types.h
Normal 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
|
||||
276
cmvr-es/manager/safety/src/command_ledger.cpp
Normal file
276
cmvr-es/manager/safety/src/command_ledger.cpp
Normal 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
|
||||
2424
cmvr-es/manager/safety/src/safety_coordinator.cpp
Normal file
2424
cmvr-es/manager/safety/src/safety_coordinator.cpp
Normal file
File diff suppressed because it is too large
Load Diff
92
cmvr-es/manager/safety/src/safety_reason.cpp
Normal file
92
cmvr-es/manager/safety/src/safety_reason.cpp
Normal 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
|
||||
305
cmvr-es/manager/safety/src/safety_snapshot_store.cpp
Normal file
305
cmvr-es/manager/safety/src/safety_snapshot_store.cpp
Normal 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
|
||||
122
cmvr-es/manager/safety/tests/command_ledger_test.cpp
Normal file
122
cmvr-es/manager/safety/tests/command_ledger_test.cpp
Normal 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
|
||||
507
cmvr-es/manager/safety/tests/safety_coordinator_test.cpp
Normal file
507
cmvr-es/manager/safety/tests/safety_coordinator_test.cpp
Normal 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
|
||||
111
cmvr-es/manager/safety/tests/safety_snapshot_store_test.cpp
Normal file
111
cmvr-es/manager/safety/tests/safety_snapshot_store_test.cpp
Normal 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
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
211
cmvr-es/service/grpc/include/grpc_command_transaction.h
Normal file
211
cmvr-es/service/grpc/include/grpc_command_transaction.h
Normal 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
|
||||
@ -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_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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_;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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_;
|
||||
|
||||
38
cmvr-es/service/grpc/include/grpc_recovery_audit.h
Normal file
38
cmvr-es/service/grpc/include/grpc_recovery_audit.h
Normal 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
|
||||
43
cmvr-es/service/grpc/include/grpc_safety_participants.h
Normal file
43
cmvr-es/service/grpc/include/grpc_safety_participants.h
Normal 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
|
||||
37
cmvr-es/service/grpc/include/grpc_safety_proto.h
Normal file
37
cmvr-es/service/grpc/include/grpc_safety_proto.h
Normal 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
|
||||
268
cmvr-es/service/grpc/include/grpc_security.h
Normal file
268
cmvr-es/service/grpc/include/grpc_security.h
Normal 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(); \
|
||||
}
|
||||
@ -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_;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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_;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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_;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
1269
cmvr-es/service/grpc/src/grpc_command_transaction.cpp
Normal file
1269
cmvr-es/service/grpc/src/grpc_command_transaction.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@ -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) {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
164
cmvr-es/service/grpc/src/grpc_recovery_audit.cpp
Normal file
164
cmvr-es/service/grpc/src/grpc_recovery_audit.cpp
Normal 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
|
||||
1139
cmvr-es/service/grpc/src/grpc_safety_participants.cpp
Normal file
1139
cmvr-es/service/grpc/src/grpc_safety_participants.cpp
Normal file
File diff suppressed because it is too large
Load Diff
397
cmvr-es/service/grpc/src/grpc_safety_proto.cpp
Normal file
397
cmvr-es/service/grpc/src/grpc_safety_proto.cpp
Normal 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
|
||||
688
cmvr-es/service/grpc/src/grpc_security.cpp
Normal file
688
cmvr-es/service/grpc/src/grpc_security.cpp
Normal 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
|
||||
@ -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;
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
454
cmvr-es/service/grpc/tests/grpc_command_transaction_test.cpp
Normal file
454
cmvr-es/service/grpc/tests/grpc_command_transaction_test.cpp
Normal 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
|
||||
@ -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);
|
||||
}
|
||||
|
||||
294
cmvr-es/service/grpc/tests/grpc_security_test.cpp
Normal file
294
cmvr-es/service/grpc/tests/grpc_security_test.cpp
Normal 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
|
||||
@ -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 ⌖
|
||||
}
|
||||
}
|
||||
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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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_;
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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()
|
||||
|
||||
1821
docs/device_safety_control_plane_architecture.md
Normal file
1821
docs/device_safety_control_plane_architecture.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
186
protos/cmvr/api/safety_command.proto
Normal file
186
protos/cmvr/api/safety_command.proto
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user