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(simulate)
|
||||||
add_subdirectory(devices)
|
add_subdirectory(devices)
|
||||||
add_subdirectory(manager/control_authority)
|
add_subdirectory(manager/control_authority)
|
||||||
|
add_subdirectory(manager/safety)
|
||||||
add_subdirectory(manager/device_manager)
|
add_subdirectory(manager/device_manager)
|
||||||
add_subdirectory(service/stop_all)
|
add_subdirectory(service/stop_all)
|
||||||
add_subdirectory(manager/media_source_hub)
|
add_subdirectory(manager/media_source_hub)
|
||||||
|
|||||||
@ -4,6 +4,18 @@ device_manager {
|
|||||||
description: "cmvr edge system version 0.1"
|
description: "cmvr edge system version 0.1"
|
||||||
init_all_motors_when_no_active_joints: true
|
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 {
|
devices {
|
||||||
id: "mujoco_world"
|
id: "mujoco_world"
|
||||||
type: DEVICE_TYPE_MUJOCO_WORLD
|
type: DEVICE_TYPE_MUJOCO_WORLD
|
||||||
|
|||||||
@ -6,6 +6,15 @@ grpc_server {
|
|||||||
camera_stream_max_pending_frames: 2
|
camera_stream_max_pending_frames: 2
|
||||||
camera_stream_max_frame_age_ms: 250
|
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 RobotArm adapter is implemented, but remains explicitly closed until
|
||||||
# the device itself enables teleop group servo, real hashes are provisioned,
|
# the device itself enables teleop group servo, real hashes are provisioned,
|
||||||
# and group-write timing and independent stop behavior pass hardware review.
|
# and group-write timing and independent stop behavior pass hardware review.
|
||||||
|
|||||||
@ -106,16 +106,22 @@ cmake --install build
|
|||||||
- 后端使用独立 SDK RPC 会话持续读取控制器的 `SafetyModeType`、
|
- 后端使用独立 SDK RPC 会话持续读取控制器的 `SafetyModeType`、
|
||||||
`RobotModeType` 和硬件急停来源;首次有效样本前、监控断线或样本过期时,
|
`RobotModeType` 和硬件急停来源;首次有效样本前、监控断线或样本过期时,
|
||||||
所有 Move、Speed、Servo 和程序启动请求均按不安全状态拒绝;
|
所有 Move、Speed、Servo 和程序启动请求均按不安全状态拒绝;
|
||||||
- 硬件急停、防护停机、Safety Fault/Violation 会锁存安全事件,并使当前运动
|
- 硬件急停会立即使当前运动 generation 失效,并在急停输入有效期间保持锁存。
|
||||||
generation 失效。控制器重新报告 `Normal`/`ReducedMode` 不会自动解除锁存;
|
检测到硬件急停输入消失且控制器重新报告 `Normal`/`ReducedMode` 后,后端应
|
||||||
- 锁存后会终止直接运动与程序、关闭 servo 模式并清理控制器轨迹。只有确认
|
自动执行安全恢复确认;防护停机和 Safety Fault/Violation 仍保持显式恢复语义;
|
||||||
`ExecId == -1`、普通队列和轨迹队列均为空、运行时已停止且机械臂稳定后,
|
- `emergencyStop()` 使用独立的 `SoftwareEmergencyStop` 锁存。即使软件急停在真实
|
||||||
显式 `torqueOn`/`clearFault`/`unlockProtectiveStop` 才可能恢复运动权限;
|
硬件急停有效期间触发,后续硬件采样也不能覆盖该锁存,释放硬件急停开关不会
|
||||||
|
自动清除软件急停;它只能通过显式安全恢复流程解除;
|
||||||
|
- 锁存后会终止直接运动与程序、关闭 servo 模式并清理控制器轨迹。硬件急停
|
||||||
|
自动恢复只有在确认 `ExecId == -1`、普通队列和轨迹队列均为空、运行时已停止
|
||||||
|
且机械臂稳定后才能解除锁存;如果自动确认失败,则继续保持 fail-closed,
|
||||||
|
并允许通过 `torqueOn`/`clearFault`/`unlockProtectiveStop` 显式重试恢复;
|
||||||
- 恢复流程不会调用 `resume`、`arbitraryResume`、`startMove`,也不会重新提交
|
- 恢复流程不会调用 `resume`、`arbitraryResume`、`startMove`,也不会重新提交
|
||||||
急停前的目标、速度、servo 指令或程序;
|
急停前的目标、速度、servo 指令或程序;
|
||||||
- AUBO SDK 未在本地文档中保证急停期间 `clearPath` 的可用性,也未说明释放
|
- AUBO SDK 未在本地文档中保证急停期间 `clearPath` 的可用性,也未说明释放
|
||||||
急停开关后的控制器恢复时序。因此本实现保持 fail-closed 并在释放后再次清队列,
|
急停开关后的控制器恢复时序。因此自动恢复必须在释放后再次清队列并完成上述
|
||||||
但“释放开关后零位移”的最终保证仍需真机验证及控制器侧安全配置配合;
|
安全确认;无法确认时不得解除锁存。“释放开关后零位移”的最终保证仍需真机
|
||||||
|
验证及控制器侧安全配置配合;
|
||||||
- 只访问控制柜 Standard 数字 IO,不访问工具端 IO、可配置 IO 或安全 IO;
|
- 只访问控制柜 Standard 数字 IO,不访问工具端 IO、可配置 IO 或安全 IO;
|
||||||
- `set_do` 不修改输出 runstate;
|
- `set_do` 不修改输出 runstate;
|
||||||
- 只有 `StandardOutputRunState::None` 的通道允许写入,否则返回
|
- 只有 `StandardOutputRunState::None` 的通道允许写入,否则返回
|
||||||
|
|||||||
@ -174,6 +174,18 @@ public:
|
|||||||
return true;
|
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:
|
private:
|
||||||
std::shared_ptr<aubo_internal::SafetyState> state_;
|
std::shared_ptr<aubo_internal::SafetyState> state_;
|
||||||
aubo_internal::RecoveryToken token_;
|
aubo_internal::RecoveryToken token_;
|
||||||
@ -304,6 +316,8 @@ const char* safetyConditionName(
|
|||||||
return "SystemEmergencyStop";
|
return "SystemEmergencyStop";
|
||||||
case Condition::RobotEmergencyStop:
|
case Condition::RobotEmergencyStop:
|
||||||
return "RobotEmergencyStop";
|
return "RobotEmergencyStop";
|
||||||
|
case Condition::SoftwareEmergencyStop:
|
||||||
|
return "SoftwareEmergencyStop";
|
||||||
case Condition::Fault:
|
case Condition::Fault:
|
||||||
return "Fault";
|
return "Fault";
|
||||||
case Condition::Unknown:
|
case Condition::Unknown:
|
||||||
@ -328,6 +342,7 @@ SafetyMode publicSafetyMode(
|
|||||||
case Condition::SystemEmergencyStop:
|
case Condition::SystemEmergencyStop:
|
||||||
return SafetyMode::SystemEmergencyStop;
|
return SafetyMode::SystemEmergencyStop;
|
||||||
case Condition::RobotEmergencyStop:
|
case Condition::RobotEmergencyStop:
|
||||||
|
case Condition::SoftwareEmergencyStop:
|
||||||
return SafetyMode::EmergencyStop;
|
return SafetyMode::EmergencyStop;
|
||||||
case Condition::Violation:
|
case Condition::Violation:
|
||||||
case Condition::Fault:
|
case Condition::Fault:
|
||||||
@ -376,6 +391,7 @@ struct AuboSafetyMonitor final {
|
|||||||
std::atomic<int> runtime_state{
|
std::atomic<int> runtime_state{
|
||||||
static_cast<int>(RuntimeState::Stopped)};
|
static_cast<int>(RuntimeState::Stopped)};
|
||||||
std::atomic<int> emergency_stop_source{-1};
|
std::atomic<int> emergency_stop_source{-1};
|
||||||
|
std::atomic<bool> hardware_emergency_stop_latched{false};
|
||||||
std::atomic<int> servo_mode_select{0};
|
std::atomic<int> servo_mode_select{0};
|
||||||
std::atomic<std::int64_t> last_sample_ns{0};
|
std::atomic<std::int64_t> last_sample_ns{0};
|
||||||
std::atomic<bool> cancellation_confirmed{true};
|
std::atomic<bool> cancellation_confirmed{true};
|
||||||
@ -449,6 +465,12 @@ void publishSafetySample(
|
|||||||
monitor->servo_mode_select.store(servo_mode_select);
|
monitor->servo_mode_select.store(servo_mode_select);
|
||||||
monitor->last_sample_ns.store(monotonicNowNs());
|
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 ||
|
if (previous.observed != condition ||
|
||||||
(!previous.latched && current.latched)) {
|
(!previous.latched && current.latched)) {
|
||||||
if (current.latched) {
|
if (current.latched) {
|
||||||
@ -866,7 +888,56 @@ void runSafetyMonitor(
|
|||||||
refreshSafetySample(
|
refreshSafetySample(
|
||||||
rpc_client, monitor, robot_interface);
|
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() &&
|
if (monitor->cancellation_confirmed.load() &&
|
||||||
!controllerStillQuiescent(
|
!controllerStillQuiescent(
|
||||||
rpc_client, robot_interface)) {
|
rpc_client, robot_interface)) {
|
||||||
@ -1987,7 +2058,7 @@ Result AuboArm::emergencyStop()
|
|||||||
std::lock_guard lock(mutex_);
|
std::lock_guard lock(mutex_);
|
||||||
if (sdk_ && sdk_->safety_monitor) {
|
if (sdk_ && sdk_->safety_monitor) {
|
||||||
sdk_->safety_monitor->safety_state->observe(
|
sdk_->safety_monitor->safety_state->observe(
|
||||||
aubo_internal::SafetyCondition::RobotEmergencyStop);
|
aubo_internal::SafetyCondition::SoftwareEmergencyStop);
|
||||||
cancelForSafetyTransition(sdk_->safety_monitor);
|
cancelForSafetyTransition(sdk_->safety_monitor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3625,7 +3696,9 @@ Result AuboArm::ensureMotionReady_(
|
|||||||
if (condition ==
|
if (condition ==
|
||||||
aubo_internal::SafetyCondition::RobotEmergencyStop ||
|
aubo_internal::SafetyCondition::RobotEmergencyStop ||
|
||||||
condition ==
|
condition ==
|
||||||
aubo_internal::SafetyCondition::SystemEmergencyStop) {
|
aubo_internal::SafetyCondition::SystemEmergencyStop ||
|
||||||
|
condition ==
|
||||||
|
aubo_internal::SafetyCondition::SoftwareEmergencyStop) {
|
||||||
code = ArmErrorCode::RobotInEmergencyStop;
|
code = ArmErrorCode::RobotInEmergencyStop;
|
||||||
} else if (
|
} else if (
|
||||||
condition == aubo_internal::SafetyCondition::ProtectiveStop ||
|
condition == aubo_internal::SafetyCondition::ProtectiveStop ||
|
||||||
|
|||||||
@ -19,6 +19,7 @@ enum class SafetyCondition {
|
|||||||
SafeguardStop,
|
SafeguardStop,
|
||||||
SystemEmergencyStop,
|
SystemEmergencyStop,
|
||||||
RobotEmergencyStop,
|
RobotEmergencyStop,
|
||||||
|
SoftwareEmergencyStop,
|
||||||
Fault,
|
Fault,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -74,8 +75,22 @@ struct SafetySnapshot {
|
|||||||
std::uint64_t epoch{0};
|
std::uint64_t epoch{0};
|
||||||
bool latched{false};
|
bool latched{false};
|
||||||
bool recovery_in_progress{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
|
// 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,
|
// observed, returning to Normal only changes the observed level. A separate,
|
||||||
// explicit recovery must prove that the old controller operation has been
|
// explicit recovery must prove that the old controller operation has been
|
||||||
@ -89,6 +104,9 @@ public:
|
|||||||
std::lock_guard lock(mutex_);
|
std::lock_guard lock(mutex_);
|
||||||
const bool changed = observed_ != condition;
|
const bool changed = observed_ != condition;
|
||||||
observed_ = condition;
|
observed_ = condition;
|
||||||
|
if (condition == SafetyCondition::SoftwareEmergencyStop) {
|
||||||
|
software_emergency_stop_latched_ = true;
|
||||||
|
}
|
||||||
if (isMotionSafe(condition)) {
|
if (isMotionSafe(condition)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -98,7 +116,12 @@ public:
|
|||||||
}
|
}
|
||||||
latched_ = true;
|
latched_ = true;
|
||||||
recovery_in_progress_ = false;
|
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
|
std::optional<SafetyPermit> tryPermit() const
|
||||||
@ -144,6 +167,31 @@ public:
|
|||||||
return false;
|
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;
|
latched_ = false;
|
||||||
recovery_in_progress_ = false;
|
recovery_in_progress_ = false;
|
||||||
latched_reason_ = SafetyCondition::Unknown;
|
latched_reason_ = SafetyCondition::Unknown;
|
||||||
@ -167,7 +215,8 @@ public:
|
|||||||
latched_reason_,
|
latched_reason_,
|
||||||
epoch_,
|
epoch_,
|
||||||
latched_,
|
latched_,
|
||||||
recovery_in_progress_};
|
recovery_in_progress_,
|
||||||
|
software_emergency_stop_latched_};
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -177,6 +226,7 @@ private:
|
|||||||
std::uint64_t epoch_{1};
|
std::uint64_t epoch_{1};
|
||||||
bool latched_{false};
|
bool latched_{false};
|
||||||
bool recovery_in_progress_{false};
|
bool recovery_in_progress_{false};
|
||||||
|
bool software_emergency_stop_latched_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace cmvr::device::aubo_internal
|
} // namespace cmvr::device::aubo_internal
|
||||||
|
|||||||
@ -48,10 +48,15 @@ int main()
|
|||||||
CHECK_TRUE(state.snapshot().latched);
|
CHECK_TRUE(state.snapshot().latched);
|
||||||
CHECK_TRUE(!state.beginRecovery(state.snapshot().epoch).has_value());
|
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);
|
state.observe(SafetyCondition::Normal);
|
||||||
CHECK_TRUE(state.snapshot().latched);
|
CHECK_TRUE(state.snapshot().latched);
|
||||||
CHECK_TRUE(!state.tryPermit().has_value());
|
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);
|
const auto recovery = state.beginRecovery(state.snapshot().epoch);
|
||||||
CHECK_TRUE(recovery.has_value());
|
CHECK_TRUE(recovery.has_value());
|
||||||
@ -60,21 +65,54 @@ int main()
|
|||||||
|
|
||||||
const auto retry = state.beginRecovery(state.snapshot().epoch);
|
const auto retry = state.beginRecovery(state.snapshot().epoch);
|
||||||
CHECK_TRUE(retry.has_value());
|
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();
|
const auto recovered_permit = state.tryPermit();
|
||||||
CHECK_TRUE(recovered_permit.has_value());
|
CHECK_TRUE(recovered_permit.has_value());
|
||||||
CHECK_TRUE(state.validate(*recovered_permit));
|
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.
|
// A new safety event invalidates an in-flight recovery token.
|
||||||
state.observe(SafetyCondition::ProtectiveStop);
|
state.observe(SafetyCondition::ProtectiveStop);
|
||||||
state.observe(SafetyCondition::Reduced);
|
state.observe(SafetyCondition::Reduced);
|
||||||
const auto stale_recovery = state.beginRecovery(
|
const auto stale_recovery = state.beginRecovery(
|
||||||
state.snapshot().epoch);
|
state.snapshot().epoch);
|
||||||
CHECK_TRUE(stale_recovery.has_value());
|
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::SafeguardStop);
|
||||||
state.observe(SafetyCondition::Normal);
|
state.observe(SafetyCondition::Normal);
|
||||||
CHECK_TRUE(!state.completeRecovery(
|
CHECK_TRUE(!state.completeRecovery(
|
||||||
*stale_recovery, true, true, true));
|
*explicit_recovery, true, true, true));
|
||||||
CHECK_TRUE(state.snapshot().latched);
|
CHECK_TRUE(state.snapshot().latched);
|
||||||
|
|
||||||
// An old API call must not begin recovery for a newer safety event.
|
// An old API call must not begin recovery for a newer safety event.
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
add_library(device_manager STATIC
|
add_library(device_manager STATIC
|
||||||
src/device_factory.cpp
|
src/device_factory.cpp
|
||||||
|
src/device_safety_adapters.cpp
|
||||||
src/device_manager.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
|
target_link_libraries(device_manager PRIVATE
|
||||||
cmvr_es::proto
|
cmvr_es::proto
|
||||||
|
cmvr_es::safety_coordinator
|
||||||
cmvr_es::device::camera
|
cmvr_es::device::camera
|
||||||
cmvr_es::device::agv
|
cmvr_es::device::agv
|
||||||
cmvr_es::device::speaker
|
cmvr_es::device::speaker
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
#include "device_factory.h"
|
#include "device_factory.h"
|
||||||
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
|
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
|
||||||
|
#include "manager/safety/include/safety_coordinator.h"
|
||||||
|
|
||||||
namespace cmvr::device {
|
namespace cmvr::device {
|
||||||
|
|
||||||
@ -47,6 +48,15 @@ namespace cmvr::device {
|
|||||||
std::vector<DeviceInventoryEntry> inventorySnapshot() const;
|
std::vector<DeviceInventoryEntry> inventorySnapshot() const;
|
||||||
DeviceManagerSnapshot snapshot() 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 version() const;
|
||||||
std::string name() const;
|
std::string name() const;
|
||||||
std::string description() const;
|
std::string description() const;
|
||||||
@ -64,6 +74,7 @@ namespace cmvr::device {
|
|||||||
std::unordered_map<std::string, DeviceRecord> devices_;
|
std::unordered_map<std::string, DeviceRecord> devices_;
|
||||||
std::unordered_map<std::string, ManagedDeviceSnapshot> device_statuses_;
|
std::unordered_map<std::string, ManagedDeviceSnapshot> device_statuses_;
|
||||||
std::unique_ptr<DeviceFactory> dev_factory_;
|
std::unique_ptr<DeviceFactory> dev_factory_;
|
||||||
|
std::unique_ptr<safety::SafetyCoordinator> safety_coordinator_;
|
||||||
bool initialized_{false};
|
bool initialized_{false};
|
||||||
|
|
||||||
explicit DeviceManager(const config::DeviceManagerConfig &cfg);
|
explicit DeviceManager(const config::DeviceManagerConfig &cfg);
|
||||||
@ -76,6 +87,13 @@ namespace cmvr::device {
|
|||||||
void update_device_status_(const std::string& device_id,
|
void update_device_status_(const std::string& device_id,
|
||||||
ManagedDeviceState state,
|
ManagedDeviceState state,
|
||||||
const std::string& error_message = {});
|
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);
|
void stop_devices_(bool update_status = true);
|
||||||
};
|
};
|
||||||
} // cmvr
|
} // 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_manager.h"
|
||||||
|
#include "../include/device_safety_adapters.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@ -35,6 +36,68 @@ namespace {
|
|||||||
using GroupJointSelection = std::unordered_map<std::string, std::unordered_set<std::string>>;
|
using GroupJointSelection = std::unordered_map<std::string, std::unordered_set<std::string>>;
|
||||||
using MotorJointSelections = std::unordered_map<std::string, GroupJointSelection>;
|
using MotorJointSelections = std::unordered_map<std::string, GroupJointSelection>;
|
||||||
constexpr std::size_t kMaxDeviceErrorLength = 512;
|
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
|
std::uint64_t unixTimeMs() noexcept
|
||||||
{
|
{
|
||||||
@ -170,10 +233,12 @@ std::shared_ptr<DeviceManager> DeviceManager::instance_ = nullptr;
|
|||||||
std::mutex DeviceManager::init_mutex_;
|
std::mutex DeviceManager::init_mutex_;
|
||||||
|
|
||||||
|
|
||||||
DeviceManager::DeviceManager(const config::DeviceManagerConfig& cfg) {
|
DeviceManager::DeviceManager(const config::DeviceManagerConfig& cfg)
|
||||||
cfg_ = cfg;
|
: cfg_(cfg),
|
||||||
|
dev_factory_(std::make_unique<DeviceFactory>()),
|
||||||
dev_factory_ = std::make_unique<DeviceFactory>();
|
safety_coordinator_(std::make_unique<safety::SafetyCoordinator>(
|
||||||
|
safetyConfigFrom(cfg)))
|
||||||
|
{
|
||||||
initialize_device_statuses_();
|
initialize_device_statuses_();
|
||||||
logSection("Device Plan");
|
logSection("Device Plan");
|
||||||
log_device_plan_();
|
log_device_plan_();
|
||||||
@ -248,6 +313,7 @@ bool DeviceManager::start(){
|
|||||||
bool started = false;
|
bool started = false;
|
||||||
std::string error_message;
|
std::string error_message;
|
||||||
try {
|
try {
|
||||||
|
(void)safety_coordinator_->advanceDeviceGeneration(id);
|
||||||
started = device->start();
|
started = device->start();
|
||||||
if (!started) {
|
if (!started) {
|
||||||
error_message = "device start returned false: " + id;
|
error_message = "device start returned false: " + id;
|
||||||
@ -264,6 +330,8 @@ bool DeviceManager::start(){
|
|||||||
<< " threw an unknown exception";
|
<< " threw an unknown exception";
|
||||||
}
|
}
|
||||||
if (started) {
|
if (started) {
|
||||||
|
const auto health = sample_device_health_(device);
|
||||||
|
update_device_health_(id, health);
|
||||||
update_device_status_(id, ManagedDeviceState::Running);
|
update_device_status_(id, ManagedDeviceState::Running);
|
||||||
CMVR_LOG(INFO) << "[DeviceManager]: Start device " << id << " Success";
|
CMVR_LOG(INFO) << "[DeviceManager]: Start device " << id << " Success";
|
||||||
} else {
|
} else {
|
||||||
@ -273,13 +341,29 @@ bool DeviceManager::start(){
|
|||||||
all_started = false;
|
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) {
|
if (!all_started) {
|
||||||
CMVR_LOG(ERROR) << "[DeviceManager]: At least one enabled device failed "
|
CMVR_LOG(ERROR) << "[DeviceManager]: Device or safety startup failed; "
|
||||||
"to start; stopping all devices";
|
"stopping all devices";
|
||||||
// Rollback is a physical cleanup operation. Preserve the start
|
// Rollback is a physical cleanup operation. Preserve the start
|
||||||
// results in the status table so the failure is diagnosable; an
|
// results in the status table so the failure is diagnosable; an
|
||||||
// explicit stop() records Stopped/Error transitions.
|
// explicit stop() records Stopped/Error transitions.
|
||||||
stop_devices_(false);
|
stop_devices_(false);
|
||||||
|
} else {
|
||||||
|
safety_coordinator_->markStartupComplete();
|
||||||
}
|
}
|
||||||
return all_started;
|
return all_started;
|
||||||
}
|
}
|
||||||
@ -338,12 +422,18 @@ void DeviceManager::stop_devices_(const bool update_status) {
|
|||||||
update_device_status_(id, ManagedDeviceState::Stopped);
|
update_device_status_(id, ManagedDeviceState::Stopped);
|
||||||
}
|
}
|
||||||
CMVR_LOG(INFO) << "[DeviceManager]: Stop device " << id << " Success";
|
CMVR_LOG(INFO) << "[DeviceManager]: Stop device " << id << " Success";
|
||||||
|
safety_coordinator_->updateDeviceRuntimeState(
|
||||||
|
id, ManagedDeviceState::Stopped,
|
||||||
|
sample_device_health_(device));
|
||||||
} else {
|
} else {
|
||||||
if (update_status) {
|
if (update_status) {
|
||||||
update_device_status_(
|
update_device_status_(
|
||||||
id, ManagedDeviceState::Error, error_message);
|
id, ManagedDeviceState::Error, error_message);
|
||||||
}
|
}
|
||||||
CMVR_LOG(ERROR) << "[DeviceManager]: Stop device " << id << " Failed";
|
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));
|
devices_.emplace(record.id, std::move(record));
|
||||||
device_statuses_[device_id] = std::move(status);
|
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"
|
CMVR_LOG(INFO) << "[DeviceManager]: Register device success"
|
||||||
<< ", id=" << device_id
|
<< ", id=" << device_id
|
||||||
<< ", type=" << device->typeName()
|
<< ", type=" << device->typeName()
|
||||||
@ -508,6 +608,8 @@ void DeviceManager::update_device_status_(
|
|||||||
const std::string& device_id,
|
const std::string& device_id,
|
||||||
const ManagedDeviceState state,
|
const ManagedDeviceState state,
|
||||||
const std::string& error_message)
|
const std::string& error_message)
|
||||||
|
{
|
||||||
|
DeviceHealthSnapshot health;
|
||||||
{
|
{
|
||||||
std::unique_lock lock(devices_mutex_);
|
std::unique_lock lock(devices_mutex_);
|
||||||
auto& status = device_statuses_[device_id];
|
auto& status = device_statuses_[device_id];
|
||||||
@ -530,27 +632,20 @@ void DeviceManager::update_device_status_(
|
|||||||
: error_message)
|
: error_message)
|
||||||
: std::string{};
|
: std::string{};
|
||||||
status.status_updated_at_unix_ms = unixTimeMs();
|
status.status_updated_at_unix_ms = unixTimeMs();
|
||||||
|
health = status.health;
|
||||||
|
}
|
||||||
|
safety_coordinator_->updateDeviceRuntimeState(device_id, state, health);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceManagerSnapshot DeviceManager::snapshot() const
|
DeviceManagerSnapshot DeviceManager::snapshot() const
|
||||||
{
|
{
|
||||||
struct SnapshotSource {
|
std::vector<ManagedDeviceSnapshot> sources;
|
||||||
ManagedDeviceSnapshot status;
|
|
||||||
std::shared_ptr<AbstractDevice> device;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<SnapshotSource> sources;
|
|
||||||
{
|
{
|
||||||
std::shared_lock lock(devices_mutex_);
|
std::shared_lock lock(devices_mutex_);
|
||||||
sources.reserve(device_statuses_.size());
|
sources.reserve(device_statuses_.size());
|
||||||
for (const auto& [id, stored_status] : device_statuses_) {
|
for (const auto& [id, stored_status] : device_statuses_) {
|
||||||
SnapshotSource source;
|
(void)id;
|
||||||
source.status = stored_status;
|
sources.push_back(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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,36 +656,22 @@ DeviceManagerSnapshot DeviceManager::snapshot() const
|
|||||||
result.devices.reserve(sources.size());
|
result.devices.reserve(sources.size());
|
||||||
|
|
||||||
for (auto& source : sources) {
|
for (auto& source : sources) {
|
||||||
if (source.device) {
|
source.health.error_message =
|
||||||
try {
|
truncateDeviceError(source.health.error_message);
|
||||||
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);
|
|
||||||
const bool lifecycle_error =
|
const bool lifecycle_error =
|
||||||
source.status.state == ManagedDeviceState::Error;
|
source.state == ManagedDeviceState::Error;
|
||||||
const bool health_error =
|
const bool health_error =
|
||||||
source.status.health.state == DeviceHealthState::Degraded ||
|
source.health.state == DeviceHealthState::Degraded ||
|
||||||
source.status.health.state == DeviceHealthState::Fault;
|
source.health.state == DeviceHealthState::Fault;
|
||||||
source.status.abnormal = lifecycle_error || health_error;
|
source.abnormal = lifecycle_error || health_error;
|
||||||
if (source.status.error_message.empty()) {
|
if (source.error_message.empty()) {
|
||||||
source.status.error_message =
|
source.error_message = source.health.error_message;
|
||||||
source.status.health.error_message;
|
|
||||||
}
|
}
|
||||||
source.status.error_message =
|
source.error_message = truncateDeviceError(source.error_message);
|
||||||
truncateDeviceError(source.status.error_message);
|
if (source.status_updated_at_unix_ms == 0) {
|
||||||
if (source.status.status_updated_at_unix_ms == 0) {
|
source.status_updated_at_unix_ms = unixTimeMs();
|
||||||
source.status.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(),
|
std::sort(result.devices.begin(), result.devices.end(),
|
||||||
@ -601,6 +682,78 @@ DeviceManagerSnapshot DeviceManager::snapshot() const
|
|||||||
return result;
|
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 {
|
std::string DeviceManager::version() const {
|
||||||
return cfg_.version().empty() ? "1.0" : cfg_.version();
|
return cfg_.version().empty() ? "1.0" : cfg_.version();
|
||||||
}
|
}
|
||||||
@ -914,6 +1067,8 @@ bool DeviceManager::init_devices_() {
|
|||||||
<< ", type=" << record.type_name
|
<< ", type=" << record.type_name
|
||||||
<< ", kind=" << toString(record.kind)
|
<< ", kind=" << toString(record.kind)
|
||||||
<< ", config_file=" << entry.config_file();
|
<< ", config_file=" << entry.config_file();
|
||||||
|
const auto registered_device = record.device;
|
||||||
|
std::string registered_id;
|
||||||
{
|
{
|
||||||
std::unique_lock lock(devices_mutex_);
|
std::unique_lock lock(devices_mutex_);
|
||||||
const auto id = record.id;
|
const auto id = record.id;
|
||||||
@ -941,6 +1096,23 @@ bool DeviceManager::init_devices_() {
|
|||||||
status.abnormal = false;
|
status.abnormal = false;
|
||||||
status.error_message.clear();
|
status.error_message.clear();
|
||||||
status.status_updated_at_unix_ms = unixTimeMs();
|
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;
|
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);
|
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
|
} // namespace
|
||||||
|
|||||||
@ -434,7 +434,7 @@ bool testConcurrentSnapshotAndRegistration()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool testInventorySnapshotDoesNotWaitForDeviceHealth()
|
bool testManagerSnapshotsDoNotWaitForDeviceHealth()
|
||||||
{
|
{
|
||||||
DeviceManager::destroyInstance();
|
DeviceManager::destroyInstance();
|
||||||
cmvr::config::DeviceManagerConfig config;
|
cmvr::config::DeviceManagerConfig config;
|
||||||
@ -443,15 +443,26 @@ bool testInventorySnapshotDoesNotWaitForDeviceHealth()
|
|||||||
std::make_shared<BlockingHealthDevice>("blocked_health_arm");
|
std::make_shared<BlockingHealthDevice>("blocked_health_arm");
|
||||||
auto other_device =
|
auto other_device =
|
||||||
std::make_shared<FakeDevice>("a_camera", DeviceKind::Camera);
|
std::make_shared<FakeDevice>("a_camera", DeviceKind::Camera);
|
||||||
manager.registerDevice(blocking_device);
|
|
||||||
manager.registerDevice(other_device);
|
manager.registerDevice(other_device);
|
||||||
|
|
||||||
auto health_future = std::async(std::launch::async, [&manager] {
|
auto registration_future = std::async(
|
||||||
return manager.snapshot();
|
std::launch::async, [&manager, blocking_device] {
|
||||||
|
manager.registerDevice(blocking_device);
|
||||||
});
|
});
|
||||||
if (!blocking_device->waitForHealthCall(std::chrono::seconds(2))) {
|
if (!blocking_device->waitForHealthCall(std::chrono::seconds(2))) {
|
||||||
blocking_device->releaseHealthCall();
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -462,12 +473,18 @@ bool testInventorySnapshotDoesNotWaitForDeviceHealth()
|
|||||||
std::future_status::ready) {
|
std::future_status::ready) {
|
||||||
blocking_device->releaseHealthCall();
|
blocking_device->releaseHealthCall();
|
||||||
inventory_future.wait();
|
inventory_future.wait();
|
||||||
health_future.wait();
|
registration_future.wait();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto snapshot = snapshot_future.get();
|
||||||
const auto inventory = inventory_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.size() == 2 && isSorted(inventory) &&
|
||||||
inventory[0].id == "a_camera" &&
|
inventory[0].id == "a_camera" &&
|
||||||
inventory[0].kind == DeviceKind::Camera &&
|
inventory[0].kind == DeviceKind::Camera &&
|
||||||
@ -478,8 +495,8 @@ bool testInventorySnapshotDoesNotWaitForDeviceHealth()
|
|||||||
blocking_device->health_calls.load() == 1;
|
blocking_device->health_calls.load() == 1;
|
||||||
|
|
||||||
blocking_device->releaseHealthCall();
|
blocking_device->releaseHealthCall();
|
||||||
health_future.get();
|
registration_future.get();
|
||||||
return inventory_valid && blocking_device->health_calls.load() == 1;
|
return snapshots_valid && blocking_device->health_calls.load() == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@ -491,7 +508,7 @@ int main()
|
|||||||
testCategoryHealthAdapters() &&
|
testCategoryHealthAdapters() &&
|
||||||
testConfiguredAndDynamicSnapshots() &&
|
testConfiguredAndDynamicSnapshots() &&
|
||||||
testConcurrentSnapshotAndRegistration() &&
|
testConcurrentSnapshotAndRegistration() &&
|
||||||
testInventorySnapshotDoesNotWaitForDeviceHealth();
|
testManagerSnapshotsDoNotWaitForDeviceHealth();
|
||||||
DeviceManager::destroyInstance();
|
DeviceManager::destroyInstance();
|
||||||
return success ? 0 : 1;
|
return success ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,8 +39,36 @@ if(NOT CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|||||||
cmvr_es::common
|
cmvr_es::common
|
||||||
cmvr_es::proto
|
cmvr_es::proto
|
||||||
cmvr_es::logging
|
cmvr_es::logging
|
||||||
|
cmvr_es::safety_coordinator
|
||||||
)
|
)
|
||||||
add_library(cmvr_es::device_media_source_adapter ALIAS device_media_source_adapter)
|
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()
|
endif()
|
||||||
|
|
||||||
option(CMVR_MEDIA_SOURCE_HUB_BUILD_TESTS
|
option(CMVR_MEDIA_SOURCE_HUB_BUILD_TESTS
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
#include "devices/camera/abstract_camera.h"
|
#include "devices/camera/abstract_camera.h"
|
||||||
#include "devices/microphone/abstract_microphone.h"
|
#include "devices/microphone/abstract_microphone.h"
|
||||||
#include "manager/media_source_hub/include/media_source_hub.h"
|
#include "manager/media_source_hub/include/media_source_hub.h"
|
||||||
|
#include "manager/safety/include/safety_coordinator.h"
|
||||||
|
|
||||||
namespace cmvr::media {
|
namespace cmvr::media {
|
||||||
|
|
||||||
@ -19,6 +20,13 @@ MediaSourceHub& globalMediaSourceHub();
|
|||||||
std::string cameraColorTrackId(const std::string& device_id);
|
std::string cameraColorTrackId(const std::string& device_id);
|
||||||
std::string microphoneTrackId(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
|
// Registration is idempotent for an already registered track. The adapter owns a
|
||||||
// short-lived pump thread and one startStreaming()/stopStreaming() lease only while
|
// short-lived pump thread and one startStreaming()/stopStreaming() lease only while
|
||||||
// at least one Hub subscription is active. It ensures start() succeeds but deliberately
|
// at least one Hub subscription is active. It ensures start() succeeds but deliberately
|
||||||
|
|||||||
@ -20,6 +20,8 @@
|
|||||||
namespace cmvr::media {
|
namespace cmvr::media {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
std::atomic<std::uint64_t> media_start_sequence{0};
|
||||||
|
|
||||||
std::string normalizedCodec(std::string codec) {
|
std::string normalizedCodec(std::string codec) {
|
||||||
codec.erase(
|
codec.erase(
|
||||||
std::remove_if(codec.begin(), codec.end(), [](const unsigned char c) {
|
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";
|
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(
|
bool ensureCameraMediaSource(
|
||||||
MediaSourceHub& hub,
|
MediaSourceHub& hub,
|
||||||
const std::shared_ptr<device::AbstractCamera>& camera,
|
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/media_activity_coordinator.cpp
|
||||||
grpc/src/motor_activity_coordinator.cpp
|
grpc/src/motor_activity_coordinator.cpp
|
||||||
grpc/src/grpc_camera_service.cpp
|
grpc/src/grpc_camera_service.cpp
|
||||||
|
grpc/src/grpc_command_transaction.cpp
|
||||||
grpc/src/grpc_error_logging_interceptor.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_system_service.cpp
|
||||||
grpc/src/grpc_speaker_service.cpp
|
grpc/src/grpc_speaker_service.cpp
|
||||||
grpc/src/grpc_microphone_service.cpp
|
grpc/src/grpc_microphone_service.cpp
|
||||||
@ -242,6 +247,56 @@ if(BUILD_TESTING)
|
|||||||
ENVIRONMENT "${_grpc_system_test_environment}"
|
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
|
add_executable(grpc_arm_service_test
|
||||||
grpc/tests/grpc_arm_service_test.cpp
|
grpc/tests/grpc_arm_service_test.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "cmvr/api/system_command.pb.h"
|
#include "cmvr/api/system_command.pb.h"
|
||||||
|
#include "manager/safety/include/safety_types.h"
|
||||||
|
|
||||||
namespace cmvr::device {
|
namespace cmvr::device {
|
||||||
class DeviceManager;
|
class DeviceManager;
|
||||||
@ -59,7 +60,8 @@ public:
|
|||||||
WaitResult submitAndWait(
|
WaitResult submitAndWait(
|
||||||
const api::ActionQueueCommand_Request& request,
|
const api::ActionQueueCommand_Request& request,
|
||||||
api::ActionQueueCommand_Feedback& feedback,
|
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
|
// Starts (or joins) a temporary StopAll round. New action IDs are rejected
|
||||||
// and queued/active actions are canceled. By default the executor also
|
// and queued/active actions are canceled. By default the executor also
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
#include "devices/arm/robot_arm.h"
|
#include "devices/arm/robot_arm.h"
|
||||||
#include "manager/control_authority/include/control_authority_manager.h"
|
#include "manager/control_authority/include/control_authority_manager.h"
|
||||||
#include "manager/device_manager/include/device_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"
|
#include "service/stop_all/include/stop_all_admission_gate.h"
|
||||||
|
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
@ -84,6 +85,66 @@ struct PreparedAction {
|
|||||||
std::vector<std::string> resource_ids;
|
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 {
|
struct ValidationResult {
|
||||||
bool valid{false};
|
bool valid{false};
|
||||||
std::string error;
|
std::string error;
|
||||||
@ -743,6 +804,7 @@ struct ActionQueueExecutor::Impl {
|
|||||||
api::ActionQueueCommand_Request request;
|
api::ActionQueueCommand_Request request;
|
||||||
RequestFingerprint fingerprint;
|
RequestFingerprint fingerprint;
|
||||||
PreparedAction prepared;
|
PreparedAction prepared;
|
||||||
|
safety::CommandActor actor;
|
||||||
Clock::time_point deadline;
|
Clock::time_point deadline;
|
||||||
std::atomic<bool> cancel_requested{false};
|
std::atomic<bool> cancel_requested{false};
|
||||||
std::atomic<bool> timed_out{false};
|
std::atomic<bool> timed_out{false};
|
||||||
@ -1226,7 +1288,8 @@ struct ActionQueueExecutor::Impl {
|
|||||||
ActionQueueExecutor::WaitResult submitAndWait(
|
ActionQueueExecutor::WaitResult submitAndWait(
|
||||||
const api::ActionQueueCommand_Request& request,
|
const api::ActionQueueCommand_Request& request,
|
||||||
api::ActionQueueCommand_Feedback& feedback,
|
api::ActionQueueCommand_Feedback& feedback,
|
||||||
const std::function<bool()>& waiter_canceled)
|
const std::function<bool()>& waiter_canceled,
|
||||||
|
safety::CommandActor actor)
|
||||||
{
|
{
|
||||||
api::ActionDeduplicationStatus deduplication_status =
|
api::ActionDeduplicationStatus deduplication_status =
|
||||||
api::ACTION_DEDUPLICATION_STATUS_UNSPECIFIED;
|
api::ACTION_DEDUPLICATION_STATUS_UNSPECIFIED;
|
||||||
@ -1375,6 +1438,7 @@ struct ActionQueueExecutor::Impl {
|
|||||||
candidate->request = request;
|
candidate->request = request;
|
||||||
candidate->fingerprint = *fingerprint;
|
candidate->fingerprint = *fingerprint;
|
||||||
candidate->prepared = validation.prepared;
|
candidate->prepared = validation.prepared;
|
||||||
|
candidate->actor = std::move(actor);
|
||||||
candidate->deadline = Clock::now() + total_timeout;
|
candidate->deadline = Clock::now() + total_timeout;
|
||||||
|
|
||||||
const bool canceled_before_admission =
|
const bool canceled_before_admission =
|
||||||
@ -1870,6 +1934,7 @@ struct ActionQueueExecutor::Impl {
|
|||||||
|
|
||||||
enum class StepOutcome {
|
enum class StepOutcome {
|
||||||
Completed,
|
Completed,
|
||||||
|
Rejected,
|
||||||
Failed,
|
Failed,
|
||||||
Canceled,
|
Canceled,
|
||||||
TimedOut,
|
TimedOut,
|
||||||
@ -1880,6 +1945,49 @@ struct ActionQueueExecutor::Impl {
|
|||||||
std::string message;
|
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(
|
StepResult executeArmStep(
|
||||||
const std::shared_ptr<Record>& record,
|
const std::shared_ptr<Record>& record,
|
||||||
const PreparedStep& prepared,
|
const PreparedStep& prepared,
|
||||||
@ -1889,6 +1997,26 @@ struct ActionQueueExecutor::Impl {
|
|||||||
{
|
{
|
||||||
auto& authority =
|
auto& authority =
|
||||||
control::ControlAuthorityManager::instance();
|
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 =
|
auto cancellation_requested =
|
||||||
[record, token, deadline, &authority]() {
|
[record, token, deadline, &authority]() {
|
||||||
return record->cancel_requested.load(
|
return record->cancel_requested.load(
|
||||||
@ -2030,6 +2158,26 @@ struct ActionQueueExecutor::Impl {
|
|||||||
{
|
{
|
||||||
auto& authority =
|
auto& authority =
|
||||||
control::ControlAuthorityManager::instance();
|
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 =
|
auto cancellation_requested =
|
||||||
[record, token, deadline, &authority]() {
|
[record, token, deadline, &authority]() {
|
||||||
return record->cancel_requested.load(
|
return record->cancel_requested.load(
|
||||||
@ -2267,6 +2415,11 @@ struct ActionQueueExecutor::Impl {
|
|||||||
case StepOutcome::Completed:
|
case StepOutcome::Completed:
|
||||||
++completed_steps;
|
++completed_steps;
|
||||||
break;
|
break;
|
||||||
|
case StepOutcome::Rejected:
|
||||||
|
complete(
|
||||||
|
record, api::ACTION_RESULT_CODE_REJECTED,
|
||||||
|
completed_steps, step_result.message, index);
|
||||||
|
return;
|
||||||
case StepOutcome::Failed:
|
case StepOutcome::Failed:
|
||||||
complete(
|
complete(
|
||||||
record, api::ACTION_RESULT_CODE_FAILED,
|
record, api::ACTION_RESULT_CODE_FAILED,
|
||||||
@ -2330,10 +2483,11 @@ ActionQueueExecutor::~ActionQueueExecutor() = default;
|
|||||||
ActionQueueExecutor::WaitResult ActionQueueExecutor::submitAndWait(
|
ActionQueueExecutor::WaitResult ActionQueueExecutor::submitAndWait(
|
||||||
const api::ActionQueueCommand_Request& request,
|
const api::ActionQueueCommand_Request& request,
|
||||||
api::ActionQueueCommand_Feedback& feedback,
|
api::ActionQueueCommand_Feedback& feedback,
|
||||||
const std::function<bool()>& waiter_canceled)
|
const std::function<bool()>& waiter_canceled,
|
||||||
|
safety::CommandActor actor)
|
||||||
{
|
{
|
||||||
const auto result =
|
const auto result = impl_->submitAndWait(
|
||||||
impl_->submitAndWait(request, feedback, waiter_canceled);
|
request, feedback, waiter_canceled, std::move(actor));
|
||||||
feedback.set_service_instance_id(impl_->instance_id);
|
feedback.set_service_instance_id(impl_->instance_id);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
@ -21,9 +22,12 @@ public:
|
|||||||
enum class DispatchResult {
|
enum class DispatchResult {
|
||||||
Success,
|
Success,
|
||||||
RejectedByStopAll,
|
RejectedByStopAll,
|
||||||
|
RejectedByDispatchFence,
|
||||||
DeviceFailure,
|
DeviceFailure,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using DispatchFence = std::function<bool()>;
|
||||||
|
|
||||||
struct ActivityToken {
|
struct ActivityToken {
|
||||||
std::string device_id;
|
std::string device_id;
|
||||||
std::uint64_t activity_generation{0U};
|
std::uint64_t activity_generation{0U};
|
||||||
@ -38,7 +42,8 @@ public:
|
|||||||
DispatchResult start(
|
DispatchResult start(
|
||||||
const std::string& device_id,
|
const std::string& device_id,
|
||||||
const std::shared_ptr<device::AbstractCamera>& camera,
|
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
|
// 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.
|
// 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.
|
// serialized here so it cannot race an operational StopAll stop.
|
||||||
DispatchResult stopLifecycle(
|
DispatchResult stopLifecycle(
|
||||||
const std::string& device_id,
|
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.
|
// Reconciles a lifecycle stop performed outside CameraService.
|
||||||
void markCameraStopped(const std::string& device_id);
|
void markCameraStopped(const std::string& device_id);
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#define CMVR_ES_CAMERA_PTZ_ACTIVITY_REGISTRY_H
|
#define CMVR_ES_CAMERA_PTZ_ACTIVITY_REGISTRY_H
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
@ -20,15 +21,19 @@ public:
|
|||||||
enum class DispatchResult {
|
enum class DispatchResult {
|
||||||
Success,
|
Success,
|
||||||
RejectedByStopAll,
|
RejectedByStopAll,
|
||||||
|
RejectedByDispatchFence,
|
||||||
DeviceFailure,
|
DeviceFailure,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using DispatchFence = std::function<bool()>;
|
||||||
|
|
||||||
DispatchResult control(
|
DispatchResult control(
|
||||||
const std::string& device_id,
|
const std::string& device_id,
|
||||||
const std::shared_ptr<device::AbstractCamera>& camera,
|
const std::shared_ptr<device::AbstractCamera>& camera,
|
||||||
device::PtzCommand command,
|
device::PtzCommand command,
|
||||||
bool stop,
|
bool stop,
|
||||||
int speed);
|
int speed,
|
||||||
|
DispatchFence dispatch_fence = {});
|
||||||
|
|
||||||
// Reconciles externally stopped camera PTZ state with this registry. A
|
// Reconciles externally stopped camera PTZ state with this registry. A
|
||||||
// camera backend can call this if it stops PTZ outside CameraService.
|
// camera backend can call this if it stops PTZ outside CameraService.
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
#ifndef CMVR_ES_GRPC_AGV_SERVICE_H
|
#ifndef CMVR_ES_GRPC_AGV_SERVICE_H
|
||||||
#define CMVR_ES_GRPC_AGV_SERVICE_H
|
#define CMVR_ES_GRPC_AGV_SERVICE_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "cmvr/api/agv_service.grpc.pb.h"
|
#include "cmvr/api/agv_service.grpc.pb.h"
|
||||||
#include "devices/agv/abstract_agv.h"
|
#include "devices/agv/abstract_agv.h"
|
||||||
#include "manager/device_manager/include/device_manager.h"
|
#include "manager/device_manager/include/device_manager.h"
|
||||||
|
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
|
|
||||||
|
class GrpcSecurityGateway;
|
||||||
|
|
||||||
class gRPCAgvServiceImpl final : public api::AgvService::Service {
|
class gRPCAgvServiceImpl final : public api::AgvService::Service {
|
||||||
public:
|
public:
|
||||||
gRPCAgvServiceImpl();
|
gRPCAgvServiceImpl();
|
||||||
|
explicit gRPCAgvServiceImpl(
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway);
|
||||||
~gRPCAgvServiceImpl() override = default;
|
~gRPCAgvServiceImpl() override = default;
|
||||||
|
|
||||||
grpc::Status getRuntimeState(grpc::ServerContext* context,
|
grpc::Status getRuntimeState(grpc::ServerContext* context,
|
||||||
@ -79,6 +85,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
device::DeviceManager& dmgr_;
|
device::DeviceManager& dmgr_;
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace cmvr::service
|
} // namespace cmvr::service
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "cmvr/api/arm_service.grpc.pb.h"
|
#include "cmvr/api/arm_service.grpc.pb.h"
|
||||||
#include "devices/arm/robot_arm.h"
|
#include "devices/arm/robot_arm.h"
|
||||||
#include "manager/device_manager/include/device_manager.h"
|
#include "manager/device_manager/include/device_manager.h"
|
||||||
|
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
|
|
||||||
|
class GrpcSecurityGateway;
|
||||||
|
|
||||||
class gRPCArmServiceImpl final : public api::ArmService::Service {
|
class gRPCArmServiceImpl final : public api::ArmService::Service {
|
||||||
public:
|
public:
|
||||||
gRPCArmServiceImpl();
|
gRPCArmServiceImpl();
|
||||||
|
explicit gRPCArmServiceImpl(
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway);
|
||||||
~gRPCArmServiceImpl() override = default;
|
~gRPCArmServiceImpl() override = default;
|
||||||
|
|
||||||
grpc::Status torqueOff(grpc::ServerContext* context,
|
grpc::Status torqueOff(grpc::ServerContext* context,
|
||||||
@ -59,6 +65,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
device::DeviceManager& dmgr_;
|
device::DeviceManager& dmgr_;
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace cmvr::service
|
} // namespace cmvr::service
|
||||||
|
|||||||
@ -13,6 +13,16 @@
|
|||||||
|
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
|
|
||||||
|
class GrpcSecurityGateway;
|
||||||
|
|
||||||
|
} // namespace cmvr::service
|
||||||
|
|
||||||
|
namespace cmvr::safety {
|
||||||
|
class SafetyCoordinator;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace cmvr::service {
|
||||||
|
|
||||||
namespace arm_teleop = cmvr::api::armteleop::v1;
|
namespace arm_teleop = cmvr::api::armteleop::v1;
|
||||||
|
|
||||||
struct ArmTeleopBackendResult {
|
struct ArmTeleopBackendResult {
|
||||||
@ -75,7 +85,9 @@ public:
|
|||||||
explicit ArmTeleopServiceImpl(
|
explicit ArmTeleopServiceImpl(
|
||||||
std::shared_ptr<ArmTeleopBackend> backend =
|
std::shared_ptr<ArmTeleopBackend> backend =
|
||||||
makeDisabledArmTeleopBackend(),
|
makeDisabledArmTeleopBackend(),
|
||||||
control::ControlAuthorityManager* authority = nullptr);
|
control::ControlAuthorityManager* authority = nullptr,
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway = nullptr,
|
||||||
|
safety::SafetyCoordinator* safety_coordinator = nullptr);
|
||||||
~ArmTeleopServiceImpl() override = default;
|
~ArmTeleopServiceImpl() override = default;
|
||||||
|
|
||||||
grpc::Status Teleoperate(
|
grpc::Status Teleoperate(
|
||||||
@ -86,6 +98,8 @@ public:
|
|||||||
private:
|
private:
|
||||||
std::shared_ptr<ArmTeleopBackend> backend_;
|
std::shared_ptr<ArmTeleopBackend> backend_;
|
||||||
control::ControlAuthorityManager* authority_{nullptr};
|
control::ControlAuthorityManager* authority_{nullptr};
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
|
||||||
|
safety::SafetyCoordinator* safety_coordinator_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace cmvr::service
|
} // namespace cmvr::service
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
#ifndef GRPC_CAMERA_SERVICE_H
|
#ifndef GRPC_CAMERA_SERVICE_H
|
||||||
#define GRPC_CAMERA_SERVICE_H
|
#define GRPC_CAMERA_SERVICE_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "cmvr/api/camera_service.grpc.pb.h"
|
#include "cmvr/api/camera_service.grpc.pb.h"
|
||||||
#include "common/base/grpc_utils.h"
|
#include "common/base/grpc_utils.h"
|
||||||
#include "manager/device_manager/include/device_manager.h"
|
#include "manager/device_manager/include/device_manager.h"
|
||||||
@ -13,10 +15,13 @@
|
|||||||
|
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
|
|
||||||
|
class GrpcSecurityGateway;
|
||||||
|
|
||||||
class gRPCCameraServiceImpl final: public api::CameraService::Service {
|
class gRPCCameraServiceImpl final: public api::CameraService::Service {
|
||||||
public:
|
public:
|
||||||
explicit gRPCCameraServiceImpl(
|
explicit gRPCCameraServiceImpl(
|
||||||
CameraStreamLowLatencyConfig stream_config = {});
|
CameraStreamLowLatencyConfig stream_config = {},
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway = nullptr);
|
||||||
~gRPCCameraServiceImpl() override = default;
|
~gRPCCameraServiceImpl() override = default;
|
||||||
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetCameraStateCommand_Request* request, api::GetCameraStateCommand_Feedback* response) override;
|
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;
|
grpc::Status StartCamera(grpc::ServerContext* context, const api::StartCameraCommand_Request* request, api::StartCameraCommand_Feedback* response) override;
|
||||||
@ -33,6 +38,7 @@ namespace cmvr::service {
|
|||||||
private:
|
private:
|
||||||
device::DeviceManager& dmgr_;
|
device::DeviceManager& dmgr_;
|
||||||
CameraStreamLowLatencyConfig stream_config_;
|
CameraStreamLowLatencyConfig stream_config_;
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
|
||||||
|
|
||||||
//双向流读写线程
|
//双向流读写线程
|
||||||
std::shared_ptr<std::thread> read_thread_ = nullptr;
|
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
|
#ifndef GRPC_DEXHAND_SERVICE_H
|
||||||
#define GRPC_DEXHAND_SERVICE_H
|
#define GRPC_DEXHAND_SERVICE_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "cmvr/api/dexhand_service.grpc.pb.h"
|
#include "cmvr/api/dexhand_service.grpc.pb.h"
|
||||||
#include "common/base/grpc_utils.h"
|
#include "common/base/grpc_utils.h"
|
||||||
#include "manager/device_manager/include/device_manager.h"
|
#include "manager/device_manager/include/device_manager.h"
|
||||||
#include "devices/dexhand/abstract_dexhand.h"
|
#include "devices/dexhand/abstract_dexhand.h"
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
|
class GrpcSecurityGateway;
|
||||||
class gRPCDexHandServiceImpl final: public api::DexHandService::Service {
|
class gRPCDexHandServiceImpl final: public api::DexHandService::Service {
|
||||||
public:
|
public:
|
||||||
gRPCDexHandServiceImpl();
|
gRPCDexHandServiceImpl();
|
||||||
|
explicit gRPCDexHandServiceImpl(
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway);
|
||||||
~gRPCDexHandServiceImpl() override = default;
|
~gRPCDexHandServiceImpl() override = default;
|
||||||
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetDexHandStateCommand_Request* request,api::GetDexHandStateCommand_Feedback* response) override;
|
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;
|
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;
|
grpc::Status GetSensorDataStream(grpc::ServerContext* context, grpc::ServerReaderWriter<cmvr::api::GetSensorDataStreamCommand_Feedback, cmvr::api::GetSensorDataStreamCommand_Request>* stream) override;
|
||||||
private:
|
private:
|
||||||
device::DeviceManager& dmgr_;
|
device::DeviceManager& dmgr_;
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
#ifndef BIO_HEAD_SERVICE_H
|
#ifndef BIO_HEAD_SERVICE_H
|
||||||
#define BIO_HEAD_SERVICE_H
|
#define BIO_HEAD_SERVICE_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "cmvr/api/biohead_service.grpc.pb.h"
|
#include "cmvr/api/biohead_service.grpc.pb.h"
|
||||||
#include "manager/device_manager/include/device_manager.h"
|
#include "manager/device_manager/include/device_manager.h"
|
||||||
#include "devices/biohead/abstract_biohead.h"
|
#include "devices/biohead/abstract_biohead.h"
|
||||||
|
|
||||||
namespace cmvr::service
|
namespace cmvr::service
|
||||||
{
|
{
|
||||||
|
class GrpcSecurityGateway;
|
||||||
class gRPCMBioHeadServiceImpl : public api::BioHeadService::Service {
|
class gRPCMBioHeadServiceImpl : public api::BioHeadService::Service {
|
||||||
public:
|
public:
|
||||||
gRPCMBioHeadServiceImpl();
|
gRPCMBioHeadServiceImpl();
|
||||||
|
explicit gRPCMBioHeadServiceImpl(
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway);
|
||||||
~gRPCMBioHeadServiceImpl() override = default;
|
~gRPCMBioHeadServiceImpl() override = default;
|
||||||
|
|
||||||
grpc::Status SetExpression(grpc::ServerContext* context,
|
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;
|
grpc::Status ExpressionYawn(grpc::ServerContext* context, const cmvr::api::ExpressionYawn_Request* request, cmvr::api::ExpressionYawn_Feedback* response) override;
|
||||||
private:
|
private:
|
||||||
device::DeviceManager& dmgr_;
|
device::DeviceManager& dmgr_;
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
|
||||||
};
|
};
|
||||||
} // namespace cmvr::service
|
} // namespace cmvr::service
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,24 @@
|
|||||||
//
|
//
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "cmvr/api/hlc_service.grpc.pb.h"
|
#include "cmvr/api/hlc_service.grpc.pb.h"
|
||||||
|
#include "manager/device_manager/include/device_manager.h"
|
||||||
|
|
||||||
namespace cmvr {
|
namespace cmvr {
|
||||||
namespace service {
|
namespace service {
|
||||||
|
class GrpcSecurityGateway;
|
||||||
class gRPCHlcServiceImpl final : public api::HlcService::Service {
|
class gRPCHlcServiceImpl final : public api::HlcService::Service {
|
||||||
public:
|
public:
|
||||||
gRPCHlcServiceImpl();
|
gRPCHlcServiceImpl();
|
||||||
|
explicit gRPCHlcServiceImpl(
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway);
|
||||||
~gRPCHlcServiceImpl() = default;
|
~gRPCHlcServiceImpl() = default;
|
||||||
grpc::Status touch(grpc::ServerContext *context, const cmvr::api::Touch_Request *request, cmvr::api::Touch_Response *response) override;
|
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
|
#ifndef GRPC_MICROPHONE_SERVICE_H
|
||||||
#define GRPC_MICROPHONE_SERVICE_H
|
#define GRPC_MICROPHONE_SERVICE_H
|
||||||
|
#include <memory>
|
||||||
#include "cmvr/api/microphone_service.grpc.pb.h"
|
#include "cmvr/api/microphone_service.grpc.pb.h"
|
||||||
#include "manager/device_manager/include/device_manager.h"
|
#include "manager/device_manager/include/device_manager.h"
|
||||||
#include "devices/microphone/abstract_microphone.h"
|
#include "devices/microphone/abstract_microphone.h"
|
||||||
@ -12,10 +13,13 @@
|
|||||||
|
|
||||||
namespace cmvr::service
|
namespace cmvr::service
|
||||||
{
|
{
|
||||||
|
class GrpcSecurityGateway;
|
||||||
|
|
||||||
class gRPCMicroPhoneServiceImpl: public api::MicPhoneService::Service {
|
class gRPCMicroPhoneServiceImpl: public api::MicPhoneService::Service {
|
||||||
public:
|
public:
|
||||||
gRPCMicroPhoneServiceImpl();
|
gRPCMicroPhoneServiceImpl();
|
||||||
|
explicit gRPCMicroPhoneServiceImpl(
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway);
|
||||||
~gRPCMicroPhoneServiceImpl() override = default;
|
~gRPCMicroPhoneServiceImpl() override = default;
|
||||||
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetMicStateCommand_Request* request,api::GetMicStateCommand_Feedback* response) override;
|
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;
|
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;
|
grpc::Status GetVolume(grpc::ServerContext* context, const api::GetMicPhoneVolumeCommand_Request* request,api::GetMicPhoneVolumeCommand_Feedback* response) override;
|
||||||
private:
|
private:
|
||||||
device::DeviceManager& dmgr_;
|
device::DeviceManager& dmgr_;
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif //GRPC_MICROPHONE_SERVICE_H
|
#endif //GRPC_MICROPHONE_SERVICE_H
|
||||||
|
|||||||
@ -19,6 +19,9 @@ class DeviceManager;
|
|||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
|
|
||||||
class gRPCMotorServiceImplTestAccess;
|
class gRPCMotorServiceImplTestAccess;
|
||||||
|
class GrpcCommandTransaction;
|
||||||
|
class GrpcSecurityGateway;
|
||||||
|
struct GrpcRequestContext;
|
||||||
|
|
||||||
// A deliberately thin synchronous gRPC facade over AbstractMotor. It does not
|
// A deliberately thin synchronous gRPC facade over AbstractMotor. It does not
|
||||||
// schedule trajectories or retain asynchronous operations. The small amount of
|
// schedule trajectories or retain asynchronous operations. The small amount of
|
||||||
@ -27,6 +30,8 @@ class gRPCMotorServiceImplTestAccess;
|
|||||||
class gRPCMotorServiceImpl final : public api::MotorService::Service {
|
class gRPCMotorServiceImpl final : public api::MotorService::Service {
|
||||||
public:
|
public:
|
||||||
gRPCMotorServiceImpl();
|
gRPCMotorServiceImpl();
|
||||||
|
explicit gRPCMotorServiceImpl(
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway);
|
||||||
~gRPCMotorServiceImpl() override = default;
|
~gRPCMotorServiceImpl() override = default;
|
||||||
|
|
||||||
grpc::Status setZero(grpc::ServerContext* context,
|
grpc::Status setZero(grpc::ServerContext* context,
|
||||||
@ -136,7 +141,8 @@ private:
|
|||||||
double max_velocity_rad_s,
|
double max_velocity_rad_s,
|
||||||
double acceleration_rad_s2,
|
double acceleration_rad_s2,
|
||||||
const api::MotorWaitOptions& wait,
|
const api::MotorWaitOptions& wait,
|
||||||
api::MotorCommandResponse* response);
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command);
|
||||||
grpc::Status waitForPosition(grpc::ServerContext* context,
|
grpc::Status waitForPosition(grpc::ServerContext* context,
|
||||||
const ResolvedMotor& resolved,
|
const ResolvedMotor& resolved,
|
||||||
std::uint64_t generation,
|
std::uint64_t generation,
|
||||||
@ -154,39 +160,47 @@ private:
|
|||||||
|
|
||||||
grpc::Status setZeroImpl(grpc::ServerContext* context,
|
grpc::Status setZeroImpl(grpc::ServerContext* context,
|
||||||
const api::SetMotorZeroRequest* request,
|
const api::SetMotorZeroRequest* request,
|
||||||
api::MotorCommandResponse* response);
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command);
|
||||||
grpc::Status moveToZeroImpl(grpc::ServerContext* context,
|
grpc::Status moveToZeroImpl(grpc::ServerContext* context,
|
||||||
const api::MoveMotorToZeroRequest* request,
|
const api::MoveMotorToZeroRequest* request,
|
||||||
api::MotorCommandResponse* response);
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command);
|
||||||
grpc::Status profilePositionImpl(
|
grpc::Status profilePositionImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
const api::ProfilePositionRequest* request,
|
const api::ProfilePositionRequest* request,
|
||||||
api::MotorCommandResponse* response);
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command);
|
||||||
grpc::Status profileVelocityImpl(
|
grpc::Status profileVelocityImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
const api::ProfileVelocityRequest* request,
|
const api::ProfileVelocityRequest* request,
|
||||||
api::MotorCommandResponse* response);
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command);
|
||||||
grpc::Status emergencyStopImpl(
|
grpc::Status emergencyStopImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
const api::EmergencyStopRequest* request,
|
const api::EmergencyStopRequest* request,
|
||||||
api::MotorCommandResponse* response);
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command);
|
||||||
grpc::Status getStatusImpl(grpc::ServerContext* context,
|
grpc::Status getStatusImpl(grpc::ServerContext* context,
|
||||||
const api::GetMotorStatusRequest* request,
|
const api::GetMotorStatusRequest* request,
|
||||||
api::GetMotorStatusResponse* response);
|
api::GetMotorStatusResponse* response);
|
||||||
grpc::Status setEnabledImpl(
|
grpc::Status setEnabledImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
const api::SetMotorEnabledRequest* request,
|
const api::SetMotorEnabledRequest* request,
|
||||||
api::MotorCommandResponse* response);
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command);
|
||||||
grpc::Status streamCyclicPositionImpl(
|
grpc::Status streamCyclicPositionImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
||||||
api::CyclicPositionRequest>* stream,
|
api::CyclicPositionRequest>* stream,
|
||||||
std::optional<api::MotorTarget>& cleanup_target);
|
std::optional<api::MotorTarget>& cleanup_target,
|
||||||
|
const GrpcRequestContext& request_context);
|
||||||
grpc::Status streamCyclicVelocityImpl(
|
grpc::Status streamCyclicVelocityImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
||||||
api::CyclicVelocityRequest>* stream,
|
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,
|
void bestEffortQuickStop(const api::MotorTarget& target,
|
||||||
const std::string& error) noexcept;
|
const std::string& error) noexcept;
|
||||||
@ -199,6 +213,7 @@ private:
|
|||||||
const std::string& error) const;
|
const std::string& error) const;
|
||||||
|
|
||||||
device::DeviceManager& dmgr_;
|
device::DeviceManager& dmgr_;
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
|
||||||
mutable std::mutex states_mutex_;
|
mutable std::mutex states_mutex_;
|
||||||
mutable std::unordered_map<const device::AbstractMotor*,
|
mutable std::unordered_map<const device::AbstractMotor*,
|
||||||
MotorControlEntry> states_;
|
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
|
#ifndef GRPC_SPEAKER_SERVICE_H
|
||||||
#define GRPC_SPEAKER_SERVICE_H
|
#define GRPC_SPEAKER_SERVICE_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "cmvr/api/speaker_service.grpc.pb.h"
|
#include "cmvr/api/speaker_service.grpc.pb.h"
|
||||||
#include "manager/device_manager/include/device_manager.h"
|
#include "manager/device_manager/include/device_manager.h"
|
||||||
#include "devices/speaker/abstract_speaker.h"
|
#include "devices/speaker/abstract_speaker.h"
|
||||||
#include "common/base/grpc_utils.h"
|
#include "common/base/grpc_utils.h"
|
||||||
|
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
|
class GrpcSecurityGateway;
|
||||||
class gRPCSpeakerServiceImpl: public api::SpeakerService::Service {
|
class gRPCSpeakerServiceImpl: public api::SpeakerService::Service {
|
||||||
public:
|
public:
|
||||||
gRPCSpeakerServiceImpl();
|
gRPCSpeakerServiceImpl();
|
||||||
|
explicit gRPCSpeakerServiceImpl(
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway);
|
||||||
~gRPCSpeakerServiceImpl() override = default;
|
~gRPCSpeakerServiceImpl() override = default;
|
||||||
grpc::Status GetStatus(grpc::ServerContext* context, const api::GetSpeakerStateCommand_Request* request,api::GetSpeakerStateCommand_Feedback* response) override;
|
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;
|
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;
|
grpc::Status GetVolume(grpc::ServerContext* context, const api::GetSpeakerVolumeCommand_Request* request,api::GetSpeakerVolumeCommand_Feedback* response) override;
|
||||||
private:
|
private:
|
||||||
device::DeviceManager& dmgr_;
|
device::DeviceManager& dmgr_;
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway_;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,9 @@
|
|||||||
namespace cmvr::service
|
namespace cmvr::service
|
||||||
{
|
{
|
||||||
class ActionQueueExecutor;
|
class ActionQueueExecutor;
|
||||||
|
class RecoveryAuditSink;
|
||||||
|
class GrpcSafetyParticipantRegistration;
|
||||||
|
class GrpcSecurityGateway;
|
||||||
class StopOperationDispatcher;
|
class StopOperationDispatcher;
|
||||||
|
|
||||||
class gRPCSystemServiceImpl: public api::SystemService::Service {
|
class gRPCSystemServiceImpl: public api::SystemService::Service {
|
||||||
@ -22,6 +25,15 @@ namespace cmvr::service
|
|||||||
gRPCSystemServiceImpl();
|
gRPCSystemServiceImpl();
|
||||||
explicit gRPCSystemServiceImpl(
|
explicit gRPCSystemServiceImpl(
|
||||||
std::chrono::milliseconds stop_timeout);
|
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;
|
~gRPCSystemServiceImpl() override;
|
||||||
// Exposed only to synchronize lifecycle concurrency tests.
|
// Exposed only to synchronize lifecycle concurrency tests.
|
||||||
static bool waitForStopDispatcherDestructionForTesting(
|
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 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 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 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:
|
private:
|
||||||
device::DeviceManager& dmgr_;
|
device::DeviceManager& dmgr_;
|
||||||
const std::chrono::milliseconds stop_timeout_;
|
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.
|
// Process instances share running jobs through a lifecycle registry.
|
||||||
// The last service owner joins every worker before replacement.
|
// The last service owner joins every worker before replacement.
|
||||||
std::shared_ptr<StopOperationDispatcher> stop_dispatcher_;
|
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 {
|
class Session final {
|
||||||
public:
|
public:
|
||||||
Session() = default;
|
Session() = default;
|
||||||
@ -108,6 +114,14 @@ public:
|
|||||||
const StopAllTicket& ticket,
|
const StopAllTicket& ticket,
|
||||||
bool all_media_stopped);
|
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:
|
private:
|
||||||
std::shared_ptr<Impl> impl_;
|
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 {
|
class Registration final {
|
||||||
public:
|
public:
|
||||||
Registration() = default;
|
Registration() = default;
|
||||||
@ -131,6 +137,10 @@ public:
|
|||||||
const StopAllTicket& ticket,
|
const StopAllTicket& ticket,
|
||||||
bool all_motors_stopped);
|
bool all_motors_stopped);
|
||||||
|
|
||||||
|
FinishStopAllResult finishStopAllDetailed(
|
||||||
|
const StopAllTicket& ticket,
|
||||||
|
bool all_motors_stopped);
|
||||||
|
|
||||||
// Wakes StopAll after a MotorControlState releases or changes ownership.
|
// Wakes StopAll after a MotorControlState releases or changes ownership.
|
||||||
void notifyStateChanged() noexcept;
|
void notifyStateChanged() noexcept;
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ namespace {
|
|||||||
template <typename Operation>
|
template <typename Operation>
|
||||||
CameraOperationalActivityRegistry::DispatchResult dispatchIfAdmitted(
|
CameraOperationalActivityRegistry::DispatchResult dispatchIfAdmitted(
|
||||||
std::mutex& device_mutex,
|
std::mutex& device_mutex,
|
||||||
|
const CameraOperationalActivityRegistry::DispatchFence& dispatch_fence,
|
||||||
Operation&& operation)
|
Operation&& operation)
|
||||||
{
|
{
|
||||||
std::uint64_t admitted_generation = 0U;
|
std::uint64_t admitted_generation = 0U;
|
||||||
@ -33,6 +34,11 @@ CameraOperationalActivityRegistry::DispatchResult dispatchIfAdmitted(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dispatch_fence && !dispatch_fence()) {
|
||||||
|
return CameraOperationalActivityRegistry::DispatchResult::
|
||||||
|
RejectedByDispatchFence;
|
||||||
|
}
|
||||||
|
|
||||||
return operation();
|
return operation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +67,8 @@ CameraOperationalActivityRegistry::DispatchResult
|
|||||||
CameraOperationalActivityRegistry::start(
|
CameraOperationalActivityRegistry::start(
|
||||||
const std::string& device_id,
|
const std::string& device_id,
|
||||||
const std::shared_ptr<device::AbstractCamera>& camera,
|
const std::shared_ptr<device::AbstractCamera>& camera,
|
||||||
ActivityToken* token)
|
ActivityToken* token,
|
||||||
|
DispatchFence dispatch_fence)
|
||||||
{
|
{
|
||||||
if (token) {
|
if (token) {
|
||||||
*token = {};
|
*token = {};
|
||||||
@ -71,7 +78,7 @@ CameraOperationalActivityRegistry::start(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto state = stateForDevice(device_id, true);
|
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 auto previous_camera = state->active_camera;
|
||||||
const bool was_active = state->active.load(std::memory_order_acquire);
|
const bool was_active = state->active.load(std::memory_order_acquire);
|
||||||
if (was_active && previous_camera != camera) {
|
if (was_active && previous_camera != camera) {
|
||||||
@ -153,14 +160,15 @@ bool CameraOperationalActivityRegistry::stopIfCurrent(
|
|||||||
CameraOperationalActivityRegistry::DispatchResult
|
CameraOperationalActivityRegistry::DispatchResult
|
||||||
CameraOperationalActivityRegistry::stopLifecycle(
|
CameraOperationalActivityRegistry::stopLifecycle(
|
||||||
const std::string& device_id,
|
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) {
|
if (device_id.empty() || !camera) {
|
||||||
return DispatchResult::DeviceFailure;
|
return DispatchResult::DeviceFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto state = stateForDevice(device_id, true);
|
const auto state = stateForDevice(device_id, true);
|
||||||
return dispatchIfAdmitted(state->mutex, [&] {
|
return dispatchIfAdmitted(state->mutex, dispatch_fence, [&] {
|
||||||
if (!camera->stop()) {
|
if (!camera->stop()) {
|
||||||
return DispatchResult::DeviceFailure;
|
return DispatchResult::DeviceFailure;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,17 +33,18 @@ CameraPtzActivityRegistry::control(
|
|||||||
const std::shared_ptr<device::AbstractCamera>& camera,
|
const std::shared_ptr<device::AbstractCamera>& camera,
|
||||||
const device::PtzCommand command,
|
const device::PtzCommand command,
|
||||||
const bool stop,
|
const bool stop,
|
||||||
const int speed)
|
const int speed,
|
||||||
|
DispatchFence dispatch_fence)
|
||||||
{
|
{
|
||||||
if (device_id.empty() || !camera) {
|
if (device_id.empty() || !camera) {
|
||||||
return DispatchResult::DeviceFailure;
|
return DispatchResult::DeviceFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check admission on both sides of the per-device dispatch queue. A
|
// START checks admission on both sides of the per-device dispatch queue. A
|
||||||
// command admitted before StopAll but still queued is rejected; one already
|
// STOP is a safety-lane operation and remains available while StopAll is
|
||||||
// in device I/O is completed before that device's stop begins.
|
// latched; its Coordinator dispatch fence still runs under the same queue.
|
||||||
std::uint64_t admitted_generation = 0U;
|
std::uint64_t admitted_generation = 0U;
|
||||||
{
|
if (!stop) {
|
||||||
auto admission = globalStopAllAdmissionGate().lockAdmission();
|
auto admission = globalStopAllAdmissionGate().lockAdmission();
|
||||||
if (!admission.accepting()) {
|
if (!admission.accepting()) {
|
||||||
return DispatchResult::RejectedByStopAll;
|
return DispatchResult::RejectedByStopAll;
|
||||||
@ -53,7 +54,7 @@ CameraPtzActivityRegistry::control(
|
|||||||
|
|
||||||
const auto state = stateForDevice(device_id, true);
|
const auto state = stateForDevice(device_id, true);
|
||||||
std::lock_guard dispatch_lock(state->mutex);
|
std::lock_guard dispatch_lock(state->mutex);
|
||||||
{
|
if (!stop) {
|
||||||
auto admission = globalStopAllAdmissionGate().lockAdmission();
|
auto admission = globalStopAllAdmissionGate().lockAdmission();
|
||||||
if (!admission.accepting() ||
|
if (!admission.accepting() ||
|
||||||
admission.generation() != admitted_generation) {
|
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)) {
|
if (!camera->controlPtz(command, stop, speed)) {
|
||||||
return DispatchResult::DeviceFailure;
|
return DispatchResult::DeviceFailure;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
#include "common/base/logging/logger.h"
|
#include "common/base/logging/logger.h"
|
||||||
#include "manager/control_authority/include/control_authority_manager.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"
|
#include "service/stop_all/include/stop_all_admission_gate.h"
|
||||||
|
|
||||||
using google::protobuf::util::TimeUtil;
|
using google::protobuf::util::TimeUtil;
|
||||||
@ -605,14 +607,25 @@ void fillUnifiedMapUpdate(msgs::AgvUnifiedMapUpdate* dst, const device::AgvUnifi
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
gRPCAgvServiceImpl::gRPCAgvServiceImpl()
|
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,
|
const api::AgvRuntimeStateCommand_Request* request,
|
||||||
api::AgvRuntimeStateCommand_Feedback* response)
|
api::AgvRuntimeStateCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.AgvService/getRuntimeState");
|
||||||
try {
|
try {
|
||||||
const std::string device_id = request->header().device_id();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(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,
|
const api::AgvNavigationStatusCommand_Request* request,
|
||||||
api::AgvNavigationStatusCommand_Feedback* response)
|
api::AgvNavigationStatusCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.AgvService/getNavigationStatus");
|
||||||
try {
|
try {
|
||||||
const std::string device_id = request->header().device_id();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(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,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -663,20 +682,23 @@ grpc::Status gRPCAgvServiceImpl::emergencyStop(grpc::ServerContext*,
|
|||||||
return setControlLeaseConflict(
|
return setControlLeaseConflict(
|
||||||
response, device_id, control_barrier.detail());
|
response, device_id, control_barrier.detail());
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
return executeConfirmedAgvStop(
|
return executeConfirmedAgvStop(
|
||||||
response, agv, control_barrier, "emergencyStop",
|
response, agv, control_barrier, "emergencyStop",
|
||||||
[&agv]() { return agv->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,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -693,18 +715,21 @@ grpc::Status gRPCAgvServiceImpl::clearFault(grpc::ServerContext*,
|
|||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "clearFault");
|
response, device_id, control_lease, "clearFault");
|
||||||
}
|
}
|
||||||
return setResponseResult(response, agv->clearFault());
|
if (!command.beginDispatch()) {
|
||||||
} catch (const std::exception& e) {
|
return command.dispatchStatus();
|
||||||
fillFeedback(response, false, e.what());
|
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
|
|
||||||
}
|
}
|
||||||
|
return setResponseResult(response, agv->clearFault());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
grpc::Status gRPCAgvServiceImpl::navigateToPose(grpc::ServerContext* context,
|
grpc::Status gRPCAgvServiceImpl::navigateToPose(grpc::ServerContext* context,
|
||||||
const api::AgvNavigateToPoseCommand_Request* request,
|
const api::AgvNavigateToPoseCommand_Request* request,
|
||||||
api::AgvNavigateToPoseCommand_Feedback* response)
|
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()) {
|
if (context && context->IsCancelled()) {
|
||||||
return setNavigationRequestCanceled(response);
|
return setNavigationRequestCanceled(response);
|
||||||
}
|
}
|
||||||
@ -719,27 +744,31 @@ grpc::Status gRPCAgvServiceImpl::navigateToPose(grpc::ServerContext* context,
|
|||||||
return setControlAdmissionFailure(
|
return setControlAdmissionFailure(
|
||||||
response, device_id, control_lease);
|
response, device_id, control_lease);
|
||||||
}
|
}
|
||||||
if (!control_lease.current()) {
|
auto dispatch = control_lease.tryBeginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "navigateToPose");
|
response, device_id, control_lease, "navigateToPose");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
return setResponseResult(response, agv->navigateToPose(
|
return setResponseResult(response, agv->navigateToPose(
|
||||||
toPose2d(request->pose()),
|
toPose2d(request->pose()),
|
||||||
toMotionOptions(
|
toMotionOptions(
|
||||||
request->options(),
|
request->options(),
|
||||||
control_lease.cancellationRequested(context)),
|
control_lease.cancellationRequested(context)),
|
||||||
toAdapterParams(request->adapter_params())));
|
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,
|
grpc::Status gRPCAgvServiceImpl::navigateToStation(grpc::ServerContext* context,
|
||||||
const api::AgvNavigateToStationCommand_Request* request,
|
const api::AgvNavigateToStationCommand_Request* request,
|
||||||
api::AgvNavigateToStationCommand_Feedback* response)
|
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()) {
|
if (context && context->IsCancelled()) {
|
||||||
return setNavigationRequestCanceled(response);
|
return setNavigationRequestCanceled(response);
|
||||||
}
|
}
|
||||||
@ -754,28 +783,32 @@ grpc::Status gRPCAgvServiceImpl::navigateToStation(grpc::ServerContext* context,
|
|||||||
return setControlAdmissionFailure(
|
return setControlAdmissionFailure(
|
||||||
response, device_id, control_lease);
|
response, device_id, control_lease);
|
||||||
}
|
}
|
||||||
if (!control_lease.current()) {
|
auto dispatch = control_lease.tryBeginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease,
|
response, device_id, control_lease,
|
||||||
"navigateToStation");
|
"navigateToStation");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
return setResponseResult(response, agv->navigateToStation(
|
return setResponseResult(response, agv->navigateToStation(
|
||||||
request->station_id(),
|
request->station_id(),
|
||||||
toMotionOptions(
|
toMotionOptions(
|
||||||
request->options(),
|
request->options(),
|
||||||
control_lease.cancellationRequested(context)),
|
control_lease.cancellationRequested(context)),
|
||||||
toAdapterParams(request->adapter_params())));
|
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,
|
grpc::Status gRPCAgvServiceImpl::followPath(grpc::ServerContext* context,
|
||||||
const api::AgvFollowPathCommand_Request* request,
|
const api::AgvFollowPathCommand_Request* request,
|
||||||
api::AgvFollowPathCommand_Feedback* response)
|
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()) {
|
if (context && context->IsCancelled()) {
|
||||||
return setNavigationRequestCanceled(response);
|
return setNavigationRequestCanceled(response);
|
||||||
}
|
}
|
||||||
@ -790,7 +823,8 @@ grpc::Status gRPCAgvServiceImpl::followPath(grpc::ServerContext* context,
|
|||||||
return setControlAdmissionFailure(
|
return setControlAdmissionFailure(
|
||||||
response, device_id, control_lease);
|
response, device_id, control_lease);
|
||||||
}
|
}
|
||||||
if (!control_lease.current()) {
|
auto dispatch = control_lease.tryBeginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "followPath");
|
response, device_id, control_lease, "followPath");
|
||||||
}
|
}
|
||||||
@ -799,6 +833,9 @@ grpc::Status gRPCAgvServiceImpl::followPath(grpc::ServerContext* context,
|
|||||||
for (const auto& segment : request->path()) {
|
for (const auto& segment : request->path()) {
|
||||||
path.push_back(toPathSegment(segment));
|
path.push_back(toPathSegment(segment));
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
return setResponseResult(
|
return setResponseResult(
|
||||||
response,
|
response,
|
||||||
agv->followPath(
|
agv->followPath(
|
||||||
@ -806,10 +843,7 @@ grpc::Status gRPCAgvServiceImpl::followPath(grpc::ServerContext* context,
|
|||||||
toMotionOptions(
|
toMotionOptions(
|
||||||
request->options(),
|
request->options(),
|
||||||
control_lease.cancellationRequested(context))));
|
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,
|
const api::AgvTranslateCommand_Request* request,
|
||||||
api::AgvTranslateCommand_Feedback* response)
|
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()) {
|
if (context && context->IsCancelled()) {
|
||||||
return setNavigationRequestCanceled(response);
|
return setNavigationRequestCanceled(response);
|
||||||
}
|
}
|
||||||
@ -839,24 +876,25 @@ grpc::Status gRPCAgvServiceImpl::translate(
|
|||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "translate");
|
response, device_id, control_lease, "translate");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
return setResponseResult(
|
return setResponseResult(
|
||||||
response,
|
response,
|
||||||
agv->translate(toTranslation(request->translation())));
|
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,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -874,18 +912,21 @@ grpc::Status gRPCAgvServiceImpl::pauseNavigation(grpc::ServerContext*,
|
|||||||
response, device_id, control_lease,
|
response, device_id, control_lease,
|
||||||
"pauseNavigation");
|
"pauseNavigation");
|
||||||
}
|
}
|
||||||
return setResponseResult(response, agv->pauseNavigation());
|
if (!command.beginDispatch()) {
|
||||||
} catch (const std::exception& e) {
|
return command.dispatchStatus();
|
||||||
fillFeedback(response, false, e.what());
|
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
|
|
||||||
}
|
}
|
||||||
|
return setResponseResult(response, agv->pauseNavigation());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
grpc::Status gRPCAgvServiceImpl::resumeNavigation(grpc::ServerContext*,
|
grpc::Status gRPCAgvServiceImpl::resumeNavigation(grpc::ServerContext* context,
|
||||||
const api::CommandHeader_Request* request,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -903,18 +944,21 @@ grpc::Status gRPCAgvServiceImpl::resumeNavigation(grpc::ServerContext*,
|
|||||||
response, device_id, control_lease,
|
response, device_id, control_lease,
|
||||||
"resumeNavigation");
|
"resumeNavigation");
|
||||||
}
|
}
|
||||||
return setResponseResult(response, agv->resumeNavigation());
|
if (!command.beginDispatch()) {
|
||||||
} catch (const std::exception& e) {
|
return command.dispatchStatus();
|
||||||
fillFeedback(response, false, e.what());
|
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
|
|
||||||
}
|
}
|
||||||
|
return setResponseResult(response, agv->resumeNavigation());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
grpc::Status gRPCAgvServiceImpl::cancelNavigation(grpc::ServerContext*,
|
grpc::Status gRPCAgvServiceImpl::cancelNavigation(grpc::ServerContext* context,
|
||||||
const api::CommandHeader_Request* request,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -926,20 +970,23 @@ grpc::Status gRPCAgvServiceImpl::cancelNavigation(grpc::ServerContext*,
|
|||||||
return setControlLeaseConflict(
|
return setControlLeaseConflict(
|
||||||
response, device_id, control_barrier.detail());
|
response, device_id, control_barrier.detail());
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
return executeConfirmedAgvStop(
|
return executeConfirmedAgvStop(
|
||||||
response, agv, control_barrier, "cancelNavigation",
|
response, agv, control_barrier, "cancelNavigation",
|
||||||
[&agv]() { return agv->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,
|
const api::AgvSetVelocityCommand_Request* request,
|
||||||
api::AgvSetVelocityCommand_Feedback* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -956,18 +1003,21 @@ grpc::Status gRPCAgvServiceImpl::setVelocity(grpc::ServerContext*,
|
|||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "setVelocity");
|
response, device_id, control_lease, "setVelocity");
|
||||||
}
|
}
|
||||||
return setResponseResult(response, agv->setVelocity(toVelocity(request->velocity())));
|
if (!command.beginDispatch()) {
|
||||||
} catch (const std::exception& e) {
|
return command.dispatchStatus();
|
||||||
fillFeedback(response->mutable_header(), false, e.what());
|
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
|
|
||||||
}
|
}
|
||||||
|
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,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -979,19 +1029,21 @@ grpc::Status gRPCAgvServiceImpl::stopVelocityControl(grpc::ServerContext*,
|
|||||||
return setControlLeaseConflict(
|
return setControlLeaseConflict(
|
||||||
response, device_id, control_barrier.detail());
|
response, device_id, control_barrier.detail());
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
return executeConfirmedAgvStop(
|
return executeConfirmedAgvStop(
|
||||||
response, agv, control_barrier, "stopVelocityControl",
|
response, agv, control_barrier, "stopVelocityControl",
|
||||||
[&agv]() { return agv->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,
|
const api::AgvListMapsCommand_Request* request,
|
||||||
api::AgvListMapsCommand_Feedback* response)
|
api::AgvListMapsCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.AgvService/listMaps");
|
||||||
try {
|
try {
|
||||||
const std::string device_id = request->header().device_id();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(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,
|
const api::AgvListStationsCommand_Request* request,
|
||||||
api::AgvListStationsCommand_Feedback* response)
|
api::AgvListStationsCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.AgvService/listStations");
|
||||||
try {
|
try {
|
||||||
const std::string device_id = request->header().device_id();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(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,
|
const api::AgvMapCommand_Request* request,
|
||||||
api::AgvMapCommand_Feedback* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -1057,18 +1114,21 @@ grpc::Status gRPCAgvServiceImpl::switchMap(grpc::ServerContext*,
|
|||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "switchMap");
|
response, device_id, control_lease, "switchMap");
|
||||||
}
|
}
|
||||||
return setResponseResult(response, agv->switchMap(request->map_name()));
|
if (!command.beginDispatch()) {
|
||||||
} catch (const std::exception& e) {
|
return command.dispatchStatus();
|
||||||
fillFeedback(response->mutable_header(), false, e.what());
|
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
|
|
||||||
}
|
}
|
||||||
|
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,
|
const api::AgvMapCommand_Request* request,
|
||||||
api::AgvMapCommand_Feedback* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -1085,17 +1145,19 @@ grpc::Status gRPCAgvServiceImpl::uploadMap(grpc::ServerContext*,
|
|||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "uploadMap");
|
response, device_id, control_lease, "uploadMap");
|
||||||
}
|
}
|
||||||
return setResponseResult(response, agv->uploadMap(request->map_name(), request->content()));
|
if (!command.beginDispatch()) {
|
||||||
} catch (const std::exception& e) {
|
return command.dispatchStatus();
|
||||||
fillFeedback(response->mutable_header(), false, e.what());
|
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
|
|
||||||
}
|
}
|
||||||
|
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,
|
const api::AgvMapCommand_Request* request,
|
||||||
api::AgvMapCommand_Feedback* response)
|
api::AgvMapCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.AgvService/downloadMap");
|
||||||
try {
|
try {
|
||||||
const std::string device_id = request->header().device_id();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(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,
|
const api::AgvStartMappingCommand_Request* request,
|
||||||
api::AgvStartMappingCommand_Feedback* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -1139,21 +1204,23 @@ grpc::Status gRPCAgvServiceImpl::startMapping(grpc::ServerContext*,
|
|||||||
options.dimension = toMapDimension(request->dimension());
|
options.dimension = toMapDimension(request->dimension());
|
||||||
options.map_name = request->map_name();
|
options.map_name = request->map_name();
|
||||||
options.real_time = request->real_time();
|
options.real_time = request->real_time();
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
const auto result = agv->startMapping(options);
|
const auto result = agv->startMapping(options);
|
||||||
if (result.ok()) {
|
if (result.ok()) {
|
||||||
response->set_session_id(device_id + "_mapping");
|
response->set_session_id(device_id + "_mapping");
|
||||||
}
|
}
|
||||||
return setResponseResult(response, result);
|
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,
|
grpc::Status gRPCAgvServiceImpl::streamMap(grpc::ServerContext* context,
|
||||||
const api::AgvMapStreamCommand_Request* request,
|
const api::AgvMapStreamCommand_Request* request,
|
||||||
grpc::ServerWriter<api::AgvMapStreamCommand_Feedback>* writer)
|
grpc::ServerWriter<api::AgvMapStreamCommand_Feedback>* writer)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.AgvService/streamMap");
|
||||||
try {
|
try {
|
||||||
const std::string device_id = request->header().device_id();
|
const std::string device_id = request->header().device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(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,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
auto agv = dmgr_.getDevice<device::AbstractAGV>(device_id);
|
||||||
if (!agv) {
|
if (!agv) {
|
||||||
@ -1241,11 +1311,11 @@ grpc::Status gRPCAgvServiceImpl::stopMapping(grpc::ServerContext*,
|
|||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "stopMapping");
|
response, device_id, control_lease, "stopMapping");
|
||||||
}
|
}
|
||||||
return setResponseResult(response, agv->stopMapping());
|
if (!command.beginDispatch()) {
|
||||||
} catch (const std::exception& e) {
|
return command.dispatchStatus();
|
||||||
fillFeedback(response, false, e.what());
|
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
|
|
||||||
}
|
}
|
||||||
|
return setResponseResult(response, agv->stopMapping());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace cmvr::service
|
} // namespace cmvr::service
|
||||||
|
|||||||
@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
#include "common/base/logging/logger.h"
|
#include "common/base/logging/logger.h"
|
||||||
#include "manager/control_authority/include/control_authority_manager.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"
|
#include "service/stop_all/include/stop_all_admission_gate.h"
|
||||||
|
|
||||||
using google::protobuf::util::TimeUtil;
|
using google::protobuf::util::TimeUtil;
|
||||||
@ -361,15 +363,27 @@ grpc::Status setControlDispatchFailure(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
gRPCArmServiceImpl::gRPCArmServiceImpl()
|
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,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -381,6 +395,9 @@ grpc::Status gRPCArmServiceImpl::torqueOff(grpc::ServerContext*,
|
|||||||
return setControlLeaseConflict(
|
return setControlLeaseConflict(
|
||||||
response, device_id, control_barrier.detail());
|
response, device_id, control_barrier.detail());
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
const auto result = executeConfirmedArmStop(
|
const auto result = executeConfirmedArmStop(
|
||||||
control_barrier,
|
control_barrier,
|
||||||
"torqueOff",
|
"torqueOff",
|
||||||
@ -390,17 +407,17 @@ grpc::Status gRPCArmServiceImpl::torqueOff(grpc::ServerContext*,
|
|||||||
logRpcSuccess("torqueOff", device_id);
|
logRpcSuccess("torqueOff", device_id);
|
||||||
}
|
}
|
||||||
return resultToStatus(result);
|
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,
|
grpc::Status gRPCArmServiceImpl::torqueOn(grpc::ServerContext* context,
|
||||||
const api::CommandHeader_Request* request,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -417,6 +434,9 @@ grpc::Status gRPCArmServiceImpl::torqueOn(grpc::ServerContext* context,
|
|||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "torqueOn");
|
response, device_id, control_lease, "torqueOn");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
const auto cancellation_requested =
|
const auto cancellation_requested =
|
||||||
control_lease.cancellationRequested(context);
|
control_lease.cancellationRequested(context);
|
||||||
const auto result = arm->torqueOn(cancellation_requested);
|
const auto result = arm->torqueOn(cancellation_requested);
|
||||||
@ -443,17 +463,17 @@ grpc::Status gRPCArmServiceImpl::torqueOn(grpc::ServerContext* context,
|
|||||||
logRpcSuccess("torqueOn", device_id);
|
logRpcSuccess("torqueOn", device_id);
|
||||||
}
|
}
|
||||||
return resultToStatus(result);
|
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,
|
grpc::Status gRPCArmServiceImpl::moveJ(grpc::ServerContext* context,
|
||||||
const api::MoveJ_Request* request,
|
const api::MoveJ_Request* request,
|
||||||
api::MoveJ_Response* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -465,10 +485,14 @@ grpc::Status gRPCArmServiceImpl::moveJ(grpc::ServerContext* context,
|
|||||||
return setControlAdmissionFailure(
|
return setControlAdmissionFailure(
|
||||||
response, device_id, control_lease);
|
response, device_id, control_lease);
|
||||||
}
|
}
|
||||||
if (!control_lease.current()) {
|
auto dispatch = control_lease.tryBeginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "moveJ");
|
response, device_id, control_lease, "moveJ");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
auto options = toMotionOptions(
|
auto options = toMotionOptions(
|
||||||
request->options(),
|
request->options(),
|
||||||
control_lease.cancellationRequested(context));
|
control_lease.cancellationRequested(context));
|
||||||
@ -479,17 +503,17 @@ grpc::Status gRPCArmServiceImpl::moveJ(grpc::ServerContext* context,
|
|||||||
<< ", positions=" << request->target().position_size();
|
<< ", positions=" << request->target().position_size();
|
||||||
}
|
}
|
||||||
return setResponseResult(response, result);
|
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,
|
grpc::Status gRPCArmServiceImpl::moveL(grpc::ServerContext* context,
|
||||||
const api::MoveL_Request* request,
|
const api::MoveL_Request* request,
|
||||||
api::MoveL_Response* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -501,10 +525,14 @@ grpc::Status gRPCArmServiceImpl::moveL(grpc::ServerContext* context,
|
|||||||
return setControlAdmissionFailure(
|
return setControlAdmissionFailure(
|
||||||
response, device_id, control_lease);
|
response, device_id, control_lease);
|
||||||
}
|
}
|
||||||
if (!control_lease.current()) {
|
auto dispatch = control_lease.tryBeginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "moveL");
|
response, device_id, control_lease, "moveL");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
auto options = toMotionOptions(
|
auto options = toMotionOptions(
|
||||||
request->options(),
|
request->options(),
|
||||||
control_lease.cancellationRequested(context));
|
control_lease.cancellationRequested(context));
|
||||||
@ -517,17 +545,17 @@ grpc::Status gRPCArmServiceImpl::moveL(grpc::ServerContext* context,
|
|||||||
<< ", frame=" << request->frame();
|
<< ", frame=" << request->frame();
|
||||||
}
|
}
|
||||||
return setResponseResult(response, result);
|
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,
|
const api::SpeedJ_Request* request,
|
||||||
api::SpeedJ_Response* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -539,10 +567,14 @@ grpc::Status gRPCArmServiceImpl::speedJ(grpc::ServerContext*,
|
|||||||
return setControlAdmissionFailure(
|
return setControlAdmissionFailure(
|
||||||
response, device_id, control_lease);
|
response, device_id, control_lease);
|
||||||
}
|
}
|
||||||
if (!control_lease.current()) {
|
auto dispatch = control_lease.tryBeginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "speedJ");
|
response, device_id, control_lease, "speedJ");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
const auto result = arm->speedJ(toJointVelocityCommand(request->velocity()),
|
const auto result = arm->speedJ(toJointVelocityCommand(request->velocity()),
|
||||||
request->acceleration(),
|
request->acceleration(),
|
||||||
request->duration());
|
request->duration());
|
||||||
@ -553,17 +585,17 @@ grpc::Status gRPCArmServiceImpl::speedJ(grpc::ServerContext*,
|
|||||||
<< ", duration=" << request->duration();
|
<< ", duration=" << request->duration();
|
||||||
}
|
}
|
||||||
return setResponseResult(response, result);
|
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,
|
const api::SpeedL_Request* request,
|
||||||
api::SpeedL_Response* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -575,10 +607,14 @@ grpc::Status gRPCArmServiceImpl::speedL(grpc::ServerContext*,
|
|||||||
return setControlAdmissionFailure(
|
return setControlAdmissionFailure(
|
||||||
response, device_id, control_lease);
|
response, device_id, control_lease);
|
||||||
}
|
}
|
||||||
if (!control_lease.current()) {
|
auto dispatch = control_lease.tryBeginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "speedL");
|
response, device_id, control_lease, "speedL");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
const auto result = arm->speedL(toCartesianVelocity(request->velocity()),
|
const auto result = arm->speedL(toCartesianVelocity(request->velocity()),
|
||||||
request->acceleration(),
|
request->acceleration(),
|
||||||
request->duration(),
|
request->duration(),
|
||||||
@ -590,17 +626,17 @@ grpc::Status gRPCArmServiceImpl::speedL(grpc::ServerContext*,
|
|||||||
<< ", frame=" << request->frame();
|
<< ", frame=" << request->frame();
|
||||||
}
|
}
|
||||||
return setResponseResult(response, result);
|
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,
|
const api::ServoJ_Request* request,
|
||||||
api::ServoJ_Response* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -617,23 +653,26 @@ grpc::Status gRPCArmServiceImpl::servoJ(grpc::ServerContext*,
|
|||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "servoJ");
|
response, device_id, control_lease, "servoJ");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
const auto result = arm->servoJ(toJointPositionCommand(request->target()));
|
const auto result = arm->servoJ(toJointPositionCommand(request->target()));
|
||||||
if (result.ok()) {
|
if (result.ok()) {
|
||||||
CMVR_LOG(DEBUG) << "[gRPCArmServiceImpl] (servoJ): success, id=" << device_id
|
CMVR_LOG(DEBUG) << "[gRPCArmServiceImpl] (servoJ): success, id=" << device_id
|
||||||
<< ", positions=" << request->target().position_size();
|
<< ", positions=" << request->target().position_size();
|
||||||
}
|
}
|
||||||
return setResponseResult(response, result);
|
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,
|
const api::CommandHeader_Request* request,
|
||||||
api::CommandHeader_Feedback* response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -645,6 +684,9 @@ grpc::Status gRPCArmServiceImpl::stopMotion(grpc::ServerContext*,
|
|||||||
return setControlLeaseConflict(
|
return setControlLeaseConflict(
|
||||||
response, device_id, control_barrier.detail());
|
response, device_id, control_barrier.detail());
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
const auto result = executeConfirmedArmStop(
|
const auto result = executeConfirmedArmStop(
|
||||||
control_barrier,
|
control_barrier,
|
||||||
"stopMotion",
|
"stopMotion",
|
||||||
@ -654,16 +696,15 @@ grpc::Status gRPCArmServiceImpl::stopMotion(grpc::ServerContext*,
|
|||||||
logRpcSuccess("stopMotion", device_id);
|
logRpcSuccess("stopMotion", device_id);
|
||||||
}
|
}
|
||||||
return resultToStatus(result);
|
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,
|
const api::JointRequest* request,
|
||||||
api::JointResponse* response)
|
api::JointResponse* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.ArmService/getJointState");
|
||||||
try {
|
try {
|
||||||
const std::string device_id = request->header().device_id();
|
const std::string device_id = request->header().device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(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,
|
const api::GetPose_Request* request,
|
||||||
api::GetPose_Response* response)
|
api::GetPose_Response* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.ArmService/getPose");
|
||||||
try {
|
try {
|
||||||
const std::string device_id = request->header().device_id();
|
const std::string device_id = request->header().device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(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,
|
const api::CalibrateZeroQ_Request* request,
|
||||||
api::CalibrateZeroQ_Response* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -731,44 +777,53 @@ grpc::Status gRPCArmServiceImpl::calibrateZeroQ(grpc::ServerContext*,
|
|||||||
return setControlAdmissionFailure(
|
return setControlAdmissionFailure(
|
||||||
response, device_id, control_lease);
|
response, device_id, control_lease);
|
||||||
}
|
}
|
||||||
if (!control_lease.current()) {
|
auto dispatch = control_lease.tryBeginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "calibrateZeroQ");
|
response, device_id, control_lease, "calibrateZeroQ");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
const auto result = arm->calibrateZeroQ(request->joint_name());
|
const auto result = arm->calibrateZeroQ(request->joint_name());
|
||||||
if (result.ok()) {
|
if (result.ok()) {
|
||||||
CMVR_LOG(DEBUG) << "[gRPCArmServiceImpl] (calibrateZeroQ): success, id=" << device_id
|
CMVR_LOG(DEBUG) << "[gRPCArmServiceImpl] (calibrateZeroQ): success, id=" << device_id
|
||||||
<< ", joint=" << request->joint_name();
|
<< ", joint=" << request->joint_name();
|
||||||
}
|
}
|
||||||
return setResponseResult(response, result);
|
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*,
|
const api::GetPoseMatrix_Request*,
|
||||||
api::GetPoseMatrix_Response* response)
|
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");
|
fillFeedback(response->mutable_header(), false, "getPoseMatrix is not implemented");
|
||||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "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*,
|
const api::ComputeForwardKinematics_Request*,
|
||||||
api::ComputeForwardKinematics_Response* response)
|
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");
|
fillFeedback(response->mutable_header(), false, "computeForwardKinematics is not implemented");
|
||||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "computeForwardKinematics is not implemented");
|
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "computeForwardKinematics is not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
grpc::Status gRPCArmServiceImpl::ExecuteJsonCommand(
|
grpc::Status gRPCArmServiceImpl::ExecuteJsonCommand(
|
||||||
grpc::ServerContext*,
|
grpc::ServerContext* context,
|
||||||
const api::JsonDeviceCommand_Request* request,
|
const api::JsonDeviceCommand_Request* request,
|
||||||
api::JsonDeviceCommand_Feedback* response)
|
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();
|
const std::string device_id = request->header().device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -785,11 +840,15 @@ grpc::Status gRPCArmServiceImpl::ExecuteJsonCommand(
|
|||||||
return setControlAdmissionFailure(
|
return setControlAdmissionFailure(
|
||||||
response, device_id, control_lease);
|
response, device_id, control_lease);
|
||||||
}
|
}
|
||||||
if (!control_lease.current()) {
|
auto dispatch = control_lease.tryBeginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease,
|
response, device_id, control_lease,
|
||||||
"ExecuteJsonCommand");
|
"ExecuteJsonCommand");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
|
|
||||||
std::string response_json;
|
std::string response_json;
|
||||||
const bool success = arm->executeJsonCommand(
|
const bool success = arm->executeJsonCommand(
|
||||||
@ -803,17 +862,17 @@ grpc::Status gRPCArmServiceImpl::ExecuteJsonCommand(
|
|||||||
logRpcSuccess("ExecuteJsonCommand", device_id);
|
logRpcSuccess("ExecuteJsonCommand", device_id);
|
||||||
}
|
}
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCArmServiceImpl::clearFault(grpc::ServerContext *context,
|
||||||
const cmvr::api::CommandHeader_Request *request,
|
const cmvr::api::CommandHeader_Request *request,
|
||||||
cmvr::api::CommandHeader_Feedback *response)
|
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();
|
const std::string device_id = request->device_id();
|
||||||
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
auto arm = dmgr_.getDevice<device::RobotArm>(device_id);
|
||||||
if (!arm) {
|
if (!arm) {
|
||||||
@ -830,12 +889,12 @@ grpc::Status gRPCArmServiceImpl::clearFault(grpc::ServerContext *context,
|
|||||||
return setControlDispatchFailure(
|
return setControlDispatchFailure(
|
||||||
response, device_id, control_lease, "clearFault");
|
response, device_id, control_lease, "clearFault");
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
const auto result = arm->clearFault();
|
const auto result = arm->clearFault();
|
||||||
fillFeedback(response, result.ok(), result.ok() ? "" : result.message);
|
fillFeedback(response, result.ok(), result.ok() ? "" : result.message);
|
||||||
return resultToStatus(result);
|
return resultToStatus(result);
|
||||||
} catch (const std::exception& e) {
|
});
|
||||||
fillFeedback(response, false, e.what());
|
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} // namespace cmvr::service
|
} // namespace cmvr::service
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "service/stop_all/include/stop_all_admission_gate.h"
|
#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 {
|
namespace cmvr::service {
|
||||||
|
|
||||||
@ -436,11 +438,17 @@ std::shared_ptr<ArmTeleopBackend> makeDisabledArmTeleopBackend()
|
|||||||
|
|
||||||
ArmTeleopServiceImpl::ArmTeleopServiceImpl(
|
ArmTeleopServiceImpl::ArmTeleopServiceImpl(
|
||||||
std::shared_ptr<ArmTeleopBackend> backend,
|
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)),
|
: backend_(std::move(backend)),
|
||||||
authority_(
|
authority_(
|
||||||
authority ? authority
|
authority ? authority
|
||||||
: &control::ControlAuthorityManager::instance())
|
: &control::ControlAuthorityManager::instance()),
|
||||||
|
security_gateway_(security_gateway
|
||||||
|
? std::move(security_gateway)
|
||||||
|
: makeDefaultGrpcSecurityGateway()),
|
||||||
|
safety_coordinator_(safety_coordinator)
|
||||||
{
|
{
|
||||||
if (!backend_) {
|
if (!backend_) {
|
||||||
backend_ = makeDisabledArmTeleopBackend();
|
backend_ = makeDisabledArmTeleopBackend();
|
||||||
@ -452,6 +460,9 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
|
|||||||
grpc::ServerReaderWriter<arm_teleop::ServerFrame,
|
grpc::ServerReaderWriter<arm_teleop::ServerFrame,
|
||||||
arm_teleop::ClientFrame>* stream)
|
arm_teleop::ClientFrame>* stream)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.armteleop.v1.ArmTeleopService/Teleoperate");
|
||||||
if (context == nullptr || stream == nullptr) {
|
if (context == nullptr || stream == nullptr) {
|
||||||
return grpc::Status(
|
return grpc::Status(
|
||||||
grpc::StatusCode::INTERNAL,
|
grpc::StatusCode::INTERNAL,
|
||||||
@ -529,6 +540,25 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
|
|||||||
authority_->release(control_lease);
|
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() &&
|
if (first_frame.open().request_force_feedback() &&
|
||||||
!backend_->supportsForceFeedback()) {
|
!backend_->supportsForceFeedback()) {
|
||||||
const grpc::Status status(
|
const grpc::Status status(
|
||||||
@ -577,6 +607,13 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
|
|||||||
writeBareRejection(stream, status);
|
writeBareRejection(stream, status);
|
||||||
return 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_attempted = true;
|
||||||
backend_open = backend_->open(first_frame.open());
|
backend_open = backend_->open(first_frame.open());
|
||||||
}
|
}
|
||||||
@ -831,6 +868,17 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
|
|||||||
grpc::StatusCode::ABORTED, detail),
|
grpc::StatusCode::ABORTED, detail),
|
||||||
true);
|
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;
|
std::optional<PendingFrame> pending;
|
||||||
bool ended = false;
|
bool ended = false;
|
||||||
@ -1117,6 +1165,20 @@ grpc::Status ArmTeleopServiceImpl::Teleoperate(
|
|||||||
grpc::StatusCode::ABORTED, detail),
|
grpc::StatusCode::ABORTED, detail),
|
||||||
true);
|
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(
|
applied = backend_->applySetpoint(
|
||||||
setpoint, command_deadline);
|
setpoint, command_deadline);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
#include "manager/media_source_hub/include/device_media_source_adapter.h"
|
#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_operational_activity_registry.h"
|
||||||
#include "service/grpc/include/camera_ptz_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/media_activity_coordinator.h"
|
||||||
|
#include "service/grpc/include/grpc_security.h"
|
||||||
//
|
//
|
||||||
// Created by xtkuang on 2025/6/1.
|
// Created by xtkuang on 2025/6/1.
|
||||||
//
|
//
|
||||||
@ -13,6 +15,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace cmvr::service;
|
using namespace cmvr::service;
|
||||||
@ -76,9 +79,15 @@ class CameraStreamingLease final {
|
|||||||
public:
|
public:
|
||||||
CameraStreamingLease(
|
CameraStreamingLease(
|
||||||
std::shared_ptr<AbstractCamera> camera,
|
std::shared_ptr<AbstractCamera> camera,
|
||||||
const MediaActivityCoordinator::Session& session)
|
const MediaActivityCoordinator::Session& session,
|
||||||
|
cmvr::safety::SafetyCoordinator& coordinator)
|
||||||
: camera_(std::move(camera)) {
|
: 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();
|
active_ = camera_ && camera_->startStreaming();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -128,13 +137,19 @@ grpc::Status rejectStreamDuringStopAll(StreamT* stream)
|
|||||||
}
|
}
|
||||||
|
|
||||||
gRPCCameraServiceImpl::gRPCCameraServiceImpl(
|
gRPCCameraServiceImpl::gRPCCameraServiceImpl(
|
||||||
CameraStreamLowLatencyConfig stream_config)
|
CameraStreamLowLatencyConfig stream_config,
|
||||||
|
std::shared_ptr<GrpcSecurityGateway> security_gateway)
|
||||||
: dmgr_(DeviceManager::getInstance()),
|
: 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,
|
grpc::Status gRPCCameraServiceImpl::GetStatus(grpc::ServerContext* context,
|
||||||
const api::GetCameraStateCommand_Request* request, api::GetCameraStateCommand_Feedback* response)
|
const api::GetCameraStateCommand_Request* request, api::GetCameraStateCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.CameraService/GetStatus");
|
||||||
try {
|
try {
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (GetStatus): id=" << dev_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,
|
grpc::Status gRPCCameraServiceImpl::StartCamera(grpc::ServerContext* context,
|
||||||
const api::StartCameraCommand_Request* request, api::StartCameraCommand_Feedback* response)
|
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();
|
auto media_session = globalMediaActivityCoordinator().beginSession();
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Media activities are temporarily paused by StopAll");
|
response, "Media activities are temporarily paused by StopAll");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StartCamera): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StartCamera): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
||||||
@ -184,12 +202,18 @@ grpc::Status gRPCCameraServiceImpl::StartCamera(grpc::ServerContext* context,
|
|||||||
CameraOperationalActivityRegistry::DispatchResult::DeviceFailure;
|
CameraOperationalActivityRegistry::DispatchResult::DeviceFailure;
|
||||||
const bool start_allowed = media_session.runIfCurrent([&] {
|
const bool start_allowed = media_session.runIfCurrent([&] {
|
||||||
dispatch = globalCameraOperationalActivityRegistry().start(
|
dispatch = globalCameraOperationalActivityRegistry().start(
|
||||||
dev_id, dev);
|
dev_id, dev, nullptr,
|
||||||
|
[&command] { return command.beginDispatch(); });
|
||||||
});
|
});
|
||||||
if (!start_allowed) {
|
if (!start_allowed) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Camera start was canceled by StopAll");
|
response, "Camera start was canceled by StopAll");
|
||||||
}
|
}
|
||||||
|
if (dispatch ==
|
||||||
|
CameraOperationalActivityRegistry::DispatchResult::
|
||||||
|
RejectedByDispatchFence) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
if (dispatch ==
|
if (dispatch ==
|
||||||
CameraOperationalActivityRegistry::DispatchResult::
|
CameraOperationalActivityRegistry::DispatchResult::
|
||||||
RejectedByStopAll) {
|
RejectedByStopAll) {
|
||||||
@ -203,19 +227,16 @@ grpc::Status gRPCCameraServiceImpl::StartCamera(grpc::ServerContext* context,
|
|||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCCameraServiceImpl::StopCamera(grpc::ServerContext* context,
|
||||||
const api::StopCameraCommand_Request* request, api::StopCameraCommand_Feedback* response)
|
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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StopCamera): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StopCamera): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
||||||
@ -224,13 +245,19 @@ grpc::Status gRPCCameraServiceImpl::StopCamera(grpc::ServerContext* context,
|
|||||||
}
|
}
|
||||||
const auto dispatch =
|
const auto dispatch =
|
||||||
globalCameraOperationalActivityRegistry().stopLifecycle(
|
globalCameraOperationalActivityRegistry().stopLifecycle(
|
||||||
dev_id, dev);
|
dev_id, dev,
|
||||||
|
[&command] { return command.beginDispatch(); });
|
||||||
if (dispatch ==
|
if (dispatch ==
|
||||||
CameraOperationalActivityRegistry::DispatchResult::
|
CameraOperationalActivityRegistry::DispatchResult::
|
||||||
RejectedByStopAll) {
|
RejectedByStopAll) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Camera control is temporarily paused by StopAll");
|
response, "Camera control is temporarily paused by StopAll");
|
||||||
}
|
}
|
||||||
|
if (dispatch ==
|
||||||
|
CameraOperationalActivityRegistry::DispatchResult::
|
||||||
|
RejectedByDispatchFence) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
if (dispatch ==
|
if (dispatch ==
|
||||||
CameraOperationalActivityRegistry::DispatchResult::DeviceFailure) {
|
CameraOperationalActivityRegistry::DispatchResult::DeviceFailure) {
|
||||||
return failResponse(response, "Failed to stop camera: " + dev_id);
|
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);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCCameraServiceImpl::GetRGBImage(grpc::ServerContext* context,
|
||||||
const api::GetRGBImageCommand_Request* request, api::GetRGBImageCommand_Feedback* response)
|
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(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] {
|
[context] {
|
||||||
if (context) {
|
if (context) {
|
||||||
@ -318,6 +341,8 @@ grpc::Status gRPCCameraServiceImpl::GetRGBImage(grpc::ServerContext* context,
|
|||||||
grpc::Status gRPCCameraServiceImpl::GetDepthImage(grpc::ServerContext* context,
|
grpc::Status gRPCCameraServiceImpl::GetDepthImage(grpc::ServerContext* context,
|
||||||
const api::GetDepthImageCommand_Request* request, api::GetDepthImageCommand_Feedback* response)
|
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(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] {
|
[context] {
|
||||||
if (context) {
|
if (context) {
|
||||||
@ -388,6 +413,8 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImage(grpc::ServerContext* context,
|
|||||||
grpc::Status gRPCCameraServiceImpl::GetRGBDImages(grpc::ServerContext* context,
|
grpc::Status gRPCCameraServiceImpl::GetRGBDImages(grpc::ServerContext* context,
|
||||||
const api::GetRGBDImagesCommand_Request* request, api::GetRGBDImagesCommand_Feedback* response)
|
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(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] {
|
[context] {
|
||||||
if (context) {
|
if (context) {
|
||||||
@ -474,63 +501,90 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImages(grpc::ServerContext* context,
|
|||||||
grpc::Status gRPCCameraServiceImpl::StartRecording(grpc::ServerContext* context,
|
grpc::Status gRPCCameraServiceImpl::StartRecording(grpc::ServerContext* context,
|
||||||
const api::StartCameraRecordingCommand_Request* request, api::StartCameraRecordingCommand_Feedback* response)
|
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();
|
auto media_session = globalMediaActivityCoordinator().beginSession();
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Media activities are temporarily paused by StopAll");
|
response, "Media activities are temporarily paused by StopAll");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StartRecording): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StartRecording): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return failResponse(response, "Camera device not found: " + dev_id);
|
return failResponse(response, "Camera device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
|
bool dispatch_allowed = false;
|
||||||
if (!media_session.runIfCurrent([&] {
|
if (!media_session.runIfCurrent([&] {
|
||||||
|
dispatch_allowed = command.beginDispatch();
|
||||||
|
if (!dispatch_allowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dev->startRecording(request->video_path());
|
dev->startRecording(request->video_path());
|
||||||
})) {
|
})) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Camera recording start was canceled by StopAll");
|
response, "Camera recording start was canceled by StopAll");
|
||||||
}
|
}
|
||||||
|
if (!dispatch_allowed) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCCameraServiceImpl::StopRecording(grpc::ServerContext* context,
|
||||||
const api::StopCameraRecordingCommand_Request* request, api::StopCameraRecordingCommand_Feedback* response)
|
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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StopRecording): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (StopRecording): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractCamera>(dev_id);
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return failResponse(response, "Camera device not found: " + dev_id);
|
return failResponse(response, "Camera device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
dev->stopRecording();
|
dev->stopRecording();
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCCameraServiceImpl::ControlPtz(grpc::ServerContext* context,
|
||||||
const api::ControlPtzCommand_Request* request, api::ControlPtzCommand_Feedback* response)
|
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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (ControlPtz): id=" << dev_id
|
CMVR_LOG(INFO) << "[gRPCCameraServiceImpl] (ControlPtz): id=" << dev_id
|
||||||
<< ", command=" << request->command()
|
<< ", command=" << request->command()
|
||||||
@ -556,11 +610,16 @@ grpc::Status gRPCCameraServiceImpl::ControlPtz(grpc::ServerContext* context,
|
|||||||
dev,
|
dev,
|
||||||
command,
|
command,
|
||||||
stop,
|
stop,
|
||||||
static_cast<int>(request->speed()));
|
static_cast<int>(request->speed()),
|
||||||
|
[&command_tx] { return command_tx.beginDispatch(); });
|
||||||
if (dispatch == CameraPtzActivityRegistry::DispatchResult::RejectedByStopAll) {
|
if (dispatch == CameraPtzActivityRegistry::DispatchResult::RejectedByStopAll) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "PTZ control is temporarily paused by StopAll");
|
response, "PTZ control is temporarily paused by StopAll");
|
||||||
}
|
}
|
||||||
|
if (dispatch ==
|
||||||
|
CameraPtzActivityRegistry::DispatchResult::RejectedByDispatchFence) {
|
||||||
|
return command_tx.dispatchStatus();
|
||||||
|
}
|
||||||
if (dispatch == CameraPtzActivityRegistry::DispatchResult::DeviceFailure) {
|
if (dispatch == CameraPtzActivityRegistry::DispatchResult::DeviceFailure) {
|
||||||
CameraState state{};
|
CameraState state{};
|
||||||
dev->getState(state);
|
dev->getState(state);
|
||||||
@ -572,18 +631,15 @@ grpc::Status gRPCCameraServiceImpl::ControlPtz(grpc::ServerContext* context,
|
|||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
return grpc::Status::OK;
|
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::Status gRPCCameraServiceImpl::GetDepthImageStream(grpc::ServerContext* context
|
||||||
, grpc::ServerReaderWriter<cmvr::api::GetDepthImageStreamCommand_Feedback, cmvr::api::GetDepthImageStreamCommand_Request>* stream){
|
, 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(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] { context->TryCancel(); });
|
[context] { context->TryCancel(); });
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
@ -609,7 +665,8 @@ grpc::Status gRPCCameraServiceImpl::GetDepthImageStream(grpc::ServerContext* con
|
|||||||
stream->Write(response);
|
stream->Write(response);
|
||||||
return grpc::Status::OK;
|
return grpc::Status::OK;
|
||||||
}
|
}
|
||||||
CameraStreamingLease stream_lease(dev, media_session);
|
CameraStreamingLease stream_lease(
|
||||||
|
dev, media_session, dmgr_.safetyCoordinator());
|
||||||
if (!stream_lease) {
|
if (!stream_lease) {
|
||||||
api::GetDepthImageStreamCommand_Feedback response;
|
api::GetDepthImageStreamCommand_Feedback response;
|
||||||
response.mutable_header()->set_success(false);
|
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::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* context
|
||||||
, grpc::ServerReaderWriter<cmvr::api::GetRGBDImagesStreamCommand_Feedback, cmvr::api::GetRGBDImagesStreamCommand_Request>* stream){
|
, 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(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] { context->TryCancel(); });
|
[context] { context->TryCancel(); });
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
@ -702,7 +762,8 @@ grpc::Status gRPCCameraServiceImpl::GetRGBDImagesStream(grpc::ServerContext* con
|
|||||||
stream->Write(response);
|
stream->Write(response);
|
||||||
return grpc::Status::OK;
|
return grpc::Status::OK;
|
||||||
}
|
}
|
||||||
CameraStreamingLease stream_lease(dev, media_session);
|
CameraStreamingLease stream_lease(
|
||||||
|
dev, media_session, dmgr_.safetyCoordinator());
|
||||||
if (!stream_lease) {
|
if (!stream_lease) {
|
||||||
api::GetRGBDImagesStreamCommand_Feedback response;
|
api::GetRGBDImagesStreamCommand_Feedback response;
|
||||||
response.mutable_header()->set_success(false);
|
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){
|
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(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] { context->TryCancel(); });
|
[context] { context->TryCancel(); });
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
@ -820,12 +884,16 @@ grpc::Status gRPCCameraServiceImpl::GetRGBImageStream(grpc::ServerContext* conte
|
|||||||
stream->Write(response);
|
stream->Write(response);
|
||||||
return grpc::Status::OK;
|
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,
|
track_id,
|
||||||
cmvr::media::MediaSourceHub::StartPosition::NEXT_PUBLISHED,
|
cmvr::media::MediaSourceHub::StartPosition::NEXT_PUBLISHED,
|
||||||
[context, &media_session] {
|
[context, &media_session] {
|
||||||
return context->IsCancelled() || media_session.cancelled();
|
return context->IsCancelled() || media_session.cancelled();
|
||||||
});
|
})
|
||||||
|
: cmvr::media::MediaSourceHub::Subscription{};
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
api::GetRGBImageStreamCommand_Feedback response;
|
api::GetRGBImageStreamCommand_Feedback response;
|
||||||
response.mutable_header()->set_success(false);
|
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 "devices/dexhand/rh56dftp_dexhand/include/rh56dftp_dexhand.h"
|
||||||
#include "manager/control_authority/include/control_authority_manager.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/media_activity_coordinator.h"
|
||||||
|
#include "service/grpc/include/grpc_security.h"
|
||||||
#include "service/stop_all/include/stop_all_admission_gate.h"
|
#include "service/stop_all/include/stop_all_admission_gate.h"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
@ -227,12 +229,16 @@ bool dispatchDexHandCommand(
|
|||||||
const std::string& device_id,
|
const std::string& device_id,
|
||||||
const std::shared_ptr<AbstractDexHand>& dev,
|
const std::shared_ptr<AbstractDexHand>& dev,
|
||||||
ScopedDexHandControlLease& lease,
|
ScopedDexHandControlLease& lease,
|
||||||
|
GrpcCommandTransaction& command,
|
||||||
Operation&& operation) {
|
Operation&& operation) {
|
||||||
auto dispatch = lease.tryBeginDispatch();
|
auto dispatch = lease.tryBeginDispatch();
|
||||||
if (!dispatch.acquired()) {
|
if (!dispatch.acquired()) {
|
||||||
(void)failControlDispatch(response, device_id, lease);
|
(void)failControlDispatch(response, device_id, lease);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!dev->resumeOperationalActivity()) {
|
if (!dev->resumeOperationalActivity()) {
|
||||||
(void)failResponse(
|
(void)failResponse(
|
||||||
response,
|
response,
|
||||||
@ -304,10 +310,20 @@ void maybeConfigureRh56FullTactilePolling(const std::shared_ptr<AbstractDexHand>
|
|||||||
|
|
||||||
} // namespace
|
} // 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,
|
grpc::Status gRPCDexHandServiceImpl::GetStatus(grpc::ServerContext* context,
|
||||||
const api::GetDexHandStateCommand_Request* request, api::GetDexHandStateCommand_Feedback* response) {
|
const api::GetDexHandStateCommand_Request* request, api::GetDexHandStateCommand_Feedback* response) {
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.DexHandService/GetStatus");
|
||||||
try {
|
try {
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (GetStatus): id=" << dev_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
|
grpc::Status gRPCDexHandServiceImpl::SetDexHandPos(grpc::ServerContext* context
|
||||||
, const cmvr::api::SetDexHandPositionsCommand_Request* request
|
, const cmvr::api::SetDexHandPositionsCommand_Request* request
|
||||||
, cmvr::api::SetDexHandPositionsCommand_Feedback* response) {
|
, 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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandPos): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandPos): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
||||||
@ -378,27 +397,27 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandPos(grpc::ServerContext* context
|
|||||||
dev_id,
|
dev_id,
|
||||||
dev,
|
dev,
|
||||||
control_lease,
|
control_lease,
|
||||||
|
command,
|
||||||
[&] { dev->setPositions(finger_joint_targets); })) {
|
[&] { dev->setPositions(finger_joint_targets); })) {
|
||||||
return grpc::Status::OK;
|
return command.dispatchStatus().ok()
|
||||||
|
? grpc::Status::OK
|
||||||
|
: command.dispatchStatus();
|
||||||
}
|
}
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandPos): success, id=" << dev_id
|
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandPos): success, id=" << dev_id
|
||||||
<< ", values=" << request->values_size();
|
<< ", values=" << request->values_size();
|
||||||
return grpc::Status::OK;
|
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
|
grpc::Status gRPCDexHandServiceImpl::SetDexHandAngle(grpc::ServerContext* context
|
||||||
, const cmvr::api::SetDexHandAnglesCommand_Request* request
|
, const cmvr::api::SetDexHandAnglesCommand_Request* request
|
||||||
, cmvr::api::SetDexHandAnglesCommand_Feedback* response) {
|
, 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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandAngle): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandAngle): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
||||||
@ -421,8 +440,11 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandAngle(grpc::ServerContext* contex
|
|||||||
dev_id,
|
dev_id,
|
||||||
dev,
|
dev,
|
||||||
control_lease,
|
control_lease,
|
||||||
|
command,
|
||||||
[&] { rh56->setAngles(finger_joint_targets); })) {
|
[&] { rh56->setAngles(finger_joint_targets); })) {
|
||||||
return grpc::Status::OK;
|
return command.dispatchStatus().ok()
|
||||||
|
? grpc::Status::OK
|
||||||
|
: command.dispatchStatus();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
std::vector<int> finger_joint_targets(static_cast<size_t>(kDexHandDofCount), -1);
|
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_id,
|
||||||
dev,
|
dev,
|
||||||
control_lease,
|
control_lease,
|
||||||
|
command,
|
||||||
[&] { dev->setAngles(finger_joint_targets); })) {
|
[&] { 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
|
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandAngle): success, id=" << dev_id
|
||||||
<< ", values=" << request->values_size();
|
<< ", values=" << request->values_size();
|
||||||
return grpc::Status::OK;
|
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
|
grpc::Status gRPCDexHandServiceImpl::SetDexHandForce(grpc::ServerContext* context
|
||||||
, const cmvr::api::SetDexHandForceCommand_Request* request
|
, const cmvr::api::SetDexHandForceCommand_Request* request
|
||||||
, cmvr::api::SetDexHandForceCommand_Feedback* response) {
|
, 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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandForce): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandForce): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
||||||
@ -485,27 +507,27 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandForce(grpc::ServerContext* contex
|
|||||||
dev_id,
|
dev_id,
|
||||||
dev,
|
dev,
|
||||||
control_lease,
|
control_lease,
|
||||||
|
command,
|
||||||
[&] { dev->setForce(finger_joint_targets); })) {
|
[&] { dev->setForce(finger_joint_targets); })) {
|
||||||
return grpc::Status::OK;
|
return command.dispatchStatus().ok()
|
||||||
|
? grpc::Status::OK
|
||||||
|
: command.dispatchStatus();
|
||||||
}
|
}
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandForce): success, id=" << dev_id
|
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandForce): success, id=" << dev_id
|
||||||
<< ", values=" << request->values_size();
|
<< ", values=" << request->values_size();
|
||||||
return grpc::Status::OK;
|
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
|
grpc::Status gRPCDexHandServiceImpl::SetDexHandSpeed(grpc::ServerContext* context
|
||||||
, const cmvr::api::SetDexHandSpeedCommand_Request* request
|
, const cmvr::api::SetDexHandSpeedCommand_Request* request
|
||||||
, cmvr::api::SetDexHandSpeedCommand_Feedback* response) {
|
, 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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandSpeed): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandSpeed): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
||||||
@ -533,27 +555,27 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandSpeed(grpc::ServerContext* contex
|
|||||||
dev_id,
|
dev_id,
|
||||||
dev,
|
dev,
|
||||||
control_lease,
|
control_lease,
|
||||||
|
command,
|
||||||
[&] { dev->setVelocities(finger_joint_targets); })) {
|
[&] { dev->setVelocities(finger_joint_targets); })) {
|
||||||
return grpc::Status::OK;
|
return command.dispatchStatus().ok()
|
||||||
|
? grpc::Status::OK
|
||||||
|
: command.dispatchStatus();
|
||||||
}
|
}
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandSpeed): success, id=" << dev_id
|
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandSpeed): success, id=" << dev_id
|
||||||
<< ", values=" << request->values_size();
|
<< ", values=" << request->values_size();
|
||||||
return grpc::Status::OK;
|
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
|
grpc::Status gRPCDexHandServiceImpl::SetDexHandPresetAct(grpc::ServerContext* context
|
||||||
, const cmvr::api::SetDexHandPresetActCommand_Request* request
|
, const cmvr::api::SetDexHandPresetActCommand_Request* request
|
||||||
, cmvr::api::SetDexHandPresetActCommand_Feedback* response) {
|
, 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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandPresetAct): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCDexHandServiceImpl] (SetDexHandPresetAct): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractDexHand>(dev_id);
|
||||||
@ -578,26 +600,26 @@ grpc::Status gRPCDexHandServiceImpl::SetDexHandPresetAct(grpc::ServerContext* co
|
|||||||
dev_id,
|
dev_id,
|
||||||
dev,
|
dev,
|
||||||
control_lease,
|
control_lease,
|
||||||
|
command,
|
||||||
[&] { dev->setPresetAct(presetActId); })) {
|
[&] { dev->setPresetAct(presetActId); })) {
|
||||||
return grpc::Status::OK;
|
return command.dispatchStatus().ok()
|
||||||
|
? grpc::Status::OK
|
||||||
|
: command.dispatchStatus();
|
||||||
}
|
}
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandPresetAct): success, id=" << dev_id
|
CMVR_LOG(DEBUG) << "[gRPCDexHandServiceImpl] (SetDexHandPresetAct): success, id=" << dev_id
|
||||||
<< ", preset_act_id=" << presetActId;
|
<< ", preset_act_id=" << presetActId;
|
||||||
return grpc::Status::OK;
|
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
|
grpc::Status gRPCDexHandServiceImpl::GetSensorData(grpc::ServerContext* context
|
||||||
, const cmvr::api::GetSensorDataCommand_Request* request
|
, const cmvr::api::GetSensorDataCommand_Request* request
|
||||||
, cmvr::api::GetSensorDataCommand_Feedback* response) {
|
, cmvr::api::GetSensorDataCommand_Feedback* response) {
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.DexHandService/GetSensorData");
|
||||||
|
|
||||||
auto media_session = globalMediaActivityCoordinator().beginSession();
|
auto media_session = globalMediaActivityCoordinator().beginSession();
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
@ -652,6 +674,9 @@ grpc::Status gRPCDexHandServiceImpl::GetSensorData(grpc::ServerContext* context
|
|||||||
grpc::Status gRPCDexHandServiceImpl::GetSensorDataStream(grpc::ServerContext* context
|
grpc::Status gRPCDexHandServiceImpl::GetSensorDataStream(grpc::ServerContext* context
|
||||||
, grpc::ServerReaderWriter<cmvr::api::GetSensorDataStreamCommand_Feedback, cmvr::api::GetSensorDataStreamCommand_Request>* stream)
|
, 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(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] {
|
[context] {
|
||||||
if (context) {
|
if (context) {
|
||||||
|
|||||||
@ -5,12 +5,15 @@
|
|||||||
#include "manager/device_manager/include/device_manager.h"
|
#include "manager/device_manager/include/device_manager.h"
|
||||||
#include "common/base/grpc_utils.h"
|
#include "common/base/grpc_utils.h"
|
||||||
#include "biohead/biohead_esp32/include/biohead_esp32.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/media_activity_coordinator.h"
|
||||||
|
#include "service/grpc/include/grpc_security.h"
|
||||||
#include "service/stop_all/include/stop_all_admission_gate.h"
|
#include "service/stop_all/include/stop_all_admission_gate.h"
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace cmvr::service;
|
using namespace cmvr::service;
|
||||||
@ -49,10 +52,61 @@ grpc::Status failStoppedCommand(ResponseT* response)
|
|||||||
"Biohead command was rejected because StopAll is in progress or "
|
"Biohead command was rejected because StopAll is in progress or "
|
||||||
"the command was preempted");
|
"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()
|
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,
|
grpc::ServerContext* context,
|
||||||
const SetFacialExpression_Request* request,
|
const SetFacialExpression_Request* request,
|
||||||
SetFacialExpression_Feedback* response) {
|
SetFacialExpression_Feedback* response) {
|
||||||
|
return executeHeadOperationalCommand(
|
||||||
try {
|
security_gateway_, context, dmgr_,
|
||||||
std::string dev_id = request->header().device_id();
|
"/cmvr.api.BioHeadService/SetExpression", "SetExpression",
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
request, response,
|
||||||
if (!robot) {
|
[request](AbstractBiohead& robot,
|
||||||
return failResponse(response, "Biohead device not found: " + dev_id);
|
const AbstractBiohead::OperationalToken activity) {
|
||||||
}
|
|
||||||
|
|
||||||
const auto activity = admitHeadCommand(robot);
|
|
||||||
if (!activity) {
|
|
||||||
return failStoppedCommand(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
FacialExpressionState expression_state;
|
FacialExpressionState expression_state;
|
||||||
|
expression_state.left_eyebrow_outside_y =
|
||||||
expression_state.left_eyebrow_outside_y = request->expression().eyebrow().left_outside_y();
|
request->expression().eyebrow().left_outside_y();
|
||||||
expression_state.left_eyebrow_inside_y = request->expression().eyebrow().left_inside_y();
|
expression_state.left_eyebrow_inside_y =
|
||||||
expression_state.right_eyebrow_outside_y = request->expression().eyebrow().right_outside_y();
|
request->expression().eyebrow().left_inside_y();
|
||||||
expression_state.right_eyebrow_inside_y = request->expression().eyebrow().right_inside_y();
|
expression_state.right_eyebrow_outside_y =
|
||||||
|
request->expression().eyebrow().right_outside_y();
|
||||||
expression_state.left_eye_upper_lid_y = request->expression().eyelid().left_upper_y();
|
expression_state.right_eyebrow_inside_y =
|
||||||
expression_state.left_eye_lower_lid_y = request->expression().eyelid().left_lower_y();
|
request->expression().eyebrow().right_inside_y();
|
||||||
expression_state.right_eye_upper_lid_y = request->expression().eyelid().right_upper_y();
|
expression_state.left_eye_upper_lid_y =
|
||||||
expression_state.right_eye_lower_lid_y = request->expression().eyelid().right_lower_y();
|
request->expression().eyelid().left_upper_y();
|
||||||
|
expression_state.left_eye_lower_lid_y =
|
||||||
expression_state.left_eye_ball_x = request->expression().eyeball().left_x();
|
request->expression().eyelid().left_lower_y();
|
||||||
expression_state.left_eye_ball_y = request->expression().eyeball().left_y();
|
expression_state.right_eye_upper_lid_y =
|
||||||
expression_state.right_eye_ball_x = request->expression().eyeball().right_x();
|
request->expression().eyelid().right_upper_y();
|
||||||
expression_state.right_eye_ball_y = request->expression().eyeball().right_y();
|
expression_state.right_eye_lower_lid_y =
|
||||||
|
request->expression().eyelid().right_lower_y();
|
||||||
expression_state.left_nose_y = request->expression().nose().left_y();
|
expression_state.left_eye_ball_x =
|
||||||
expression_state.right_nose_y = request->expression().nose().right_y();
|
request->expression().eyeball().left_x();
|
||||||
|
expression_state.left_eye_ball_y =
|
||||||
expression_state.upper_lip_y = request->expression().mouth().upper_lip_y();
|
request->expression().eyeball().left_y();
|
||||||
expression_state.lower_lip_y = request->expression().mouth().lower_lip_y();
|
expression_state.right_eye_ball_x =
|
||||||
|
request->expression().eyeball().right_x();
|
||||||
if (!robot->setExpressionPoseIfCurrent(
|
expression_state.right_eye_ball_y =
|
||||||
*activity, expression_state)) {
|
request->expression().eyeball().right_y();
|
||||||
return failStoppedCommand(response);
|
expression_state.left_nose_y =
|
||||||
}
|
request->expression().nose().left_y();
|
||||||
|
expression_state.right_nose_y =
|
||||||
response->mutable_header()->set_success(true);
|
request->expression().nose().right_y();
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
expression_state.upper_lip_y =
|
||||||
logSuccess("SetExpression", dev_id);
|
request->expression().mouth().upper_lip_y();
|
||||||
return grpc::Status::OK;
|
expression_state.lower_lip_y =
|
||||||
} catch (const std::exception& e) {
|
request->expression().mouth().lower_lip_y();
|
||||||
response->mutable_header()->set_success(false);
|
return robot.setExpressionPoseIfCurrent(
|
||||||
response->mutable_header()->set_error_message(e.what());
|
activity, expression_state);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
});
|
||||||
return grpc::Status::OK;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -119,11 +164,15 @@ grpc::Status gRPCMBioHeadServiceImpl::StreamExpression(
|
|||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
grpc::ServerReaderWriter<StreamFacialExpression_Feedback, StreamFacialExpression_Request>* stream)
|
grpc::ServerReaderWriter<StreamFacialExpression_Feedback, StreamFacialExpression_Request>* stream)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.BioHeadService/StreamExpression");
|
||||||
StreamFacialExpression_Feedback feedback_msg;
|
StreamFacialExpression_Feedback feedback_msg;
|
||||||
std::string dev_id;
|
std::string dev_id;
|
||||||
std::shared_ptr<AbstractBiohead> robot;
|
std::shared_ptr<AbstractBiohead> robot;
|
||||||
bool first_message = true;
|
bool first_message = true;
|
||||||
AbstractBiohead::OperationalToken activity{0U};
|
AbstractBiohead::OperationalToken activity{0U};
|
||||||
|
std::optional<GrpcStreamingSafetySession> safety_session;
|
||||||
auto media_session = globalMediaActivityCoordinator().beginSession(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] {
|
[context] {
|
||||||
if (context) {
|
if (context) {
|
||||||
@ -178,8 +227,44 @@ grpc::Status gRPCMBioHeadServiceImpl::StreamExpression(
|
|||||||
}
|
}
|
||||||
activity = *admitted;
|
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;
|
first_message = false;
|
||||||
CMVR_LOG(DEBUG) << "[gRPCMBioHeadServiceImpl] (StreamExpression): streaming success, id=" << dev_id;
|
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;
|
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 current_time = std::chrono::steady_clock::now();
|
||||||
auto elapsed_time = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - last_control_time);
|
auto elapsed_time = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - last_control_time);
|
||||||
if (elapsed_time < time_interval) continue;
|
if (elapsed_time < time_interval) continue;
|
||||||
@ -233,10 +333,25 @@ grpc::Status gRPCMBioHeadServiceImpl::StreamExpression(
|
|||||||
expression_state.jaw_y = request_msg.expr().jaw().y();
|
expression_state.jaw_y = request_msg.expr().jaw().y();
|
||||||
|
|
||||||
bool dispatched = false;
|
bool dispatched = false;
|
||||||
|
grpc::Status dispatch_status = grpc::Status::OK;
|
||||||
const bool current_session = media_session.runIfCurrent([&] {
|
const bool current_session = media_session.runIfCurrent([&] {
|
||||||
|
auto dispatch = safety_session->beginDispatch();
|
||||||
|
if (!dispatch.acquired()) {
|
||||||
|
dispatch_status = safety_session->status();
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatched = robot->streamFacialPoseIfCurrent(
|
dispatched = robot->streamFacialPoseIfCurrent(
|
||||||
activity, expression_state, 0, 0);
|
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) {
|
if (!current_session || !dispatched) {
|
||||||
CMVR_LOG(WARNING)
|
CMVR_LOG(WARNING)
|
||||||
<< "[gRPCMBioHeadServiceImpl] StreamExpression was "
|
<< "[gRPCMBioHeadServiceImpl] StreamExpression was "
|
||||||
@ -271,6 +386,9 @@ grpc::Status gRPCMBioHeadServiceImpl::GetSystemStatus(
|
|||||||
const GetStatus_Request* request,
|
const GetStatus_Request* request,
|
||||||
GetStatus_Feedback* response)
|
GetStatus_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.BioHeadService/GetSystemStatus");
|
||||||
try {
|
try {
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
||||||
@ -298,14 +416,19 @@ grpc::Status gRPCMBioHeadServiceImpl::EmergencyStop(
|
|||||||
const EmergencyStop_Request* request,
|
const EmergencyStop_Request* request,
|
||||||
EmergencyStop_Feedback* response)
|
EmergencyStop_Feedback* response)
|
||||||
{
|
{
|
||||||
try {
|
return executeRegisteredGrpcCommand(
|
||||||
string dev_id = request->header().device_id();
|
security_gateway_, context, dmgr_.safetyCoordinator(),
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
"/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) {
|
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()) {
|
if (!robot->stopOperationalActivity()) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response,
|
response,
|
||||||
@ -313,244 +436,126 @@ grpc::Status gRPCMBioHeadServiceImpl::EmergencyStop(
|
|||||||
"stopped: " + dev_id);
|
"stopped: " + dev_id);
|
||||||
}
|
}
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(
|
||||||
|
response->mutable_header()->mutable_timestamp());
|
||||||
logSuccess("EmergencyStop", dev_id);
|
logSuccess("EmergencyStop", dev_id);
|
||||||
return grpc::Status::OK;
|
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)
|
grpc::Status gRPCMBioHeadServiceImpl::SpeakStart(grpc::ServerContext* context, const cmvr::api::SpeakStart_Request* request, cmvr::api::SpeakStart_Feedback* response)
|
||||||
{
|
{
|
||||||
|
return executeHeadOperationalCommand(
|
||||||
{
|
security_gateway_, context, dmgr_,
|
||||||
|
"/cmvr.api.BioHeadService/SpeakStart", "SpeakStart",
|
||||||
try {
|
request, response,
|
||||||
string dev_id = request->header().device_id();
|
[](AbstractBiohead& robot,
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
const AbstractBiohead::OperationalToken activity) {
|
||||||
|
return robot.speakStartIfCurrent(activity);
|
||||||
|
});
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
grpc::Status gRPCMBioHeadServiceImpl::SpeakStop(grpc::ServerContext* context, const cmvr::api::SpeakStop_Request* request, cmvr::api::SpeakStop_Feedback* response)
|
grpc::Status gRPCMBioHeadServiceImpl::SpeakStop(grpc::ServerContext* context, const cmvr::api::SpeakStop_Request* request, cmvr::api::SpeakStop_Feedback* response)
|
||||||
{
|
{
|
||||||
try {
|
return executeRegisteredGrpcCommand(
|
||||||
string dev_id = request->header().device_id();
|
security_gateway_, context, dmgr_.safetyCoordinator(),
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
"/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) {
|
if (!robot) {
|
||||||
return failResponse(response, "Biohead device not found: " + dev_id);
|
return failResponse(
|
||||||
|
response, "Biohead device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
robot->speakstop(); // 停止执行
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
|
robot->speakstop();
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(
|
||||||
|
response->mutable_header()->mutable_timestamp());
|
||||||
logSuccess("SpeakStop", dev_id);
|
logSuccess("SpeakStop", dev_id);
|
||||||
return grpc::Status::OK;
|
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)
|
grpc::Status gRPCMBioHeadServiceImpl::Happy(grpc::ServerContext* context, const cmvr::api::Happy_Request* request, cmvr::api::Happy_Feedback* response)
|
||||||
{
|
{
|
||||||
try {
|
return executeHeadOperationalCommand(
|
||||||
string dev_id = request->header().device_id();
|
security_gateway_, context, dmgr_,
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
"/cmvr.api.BioHeadService/Happy", "Happy", request, response,
|
||||||
if (!robot) {
|
[](AbstractBiohead& robot,
|
||||||
return failResponse(response, "Biohead device not found: " + dev_id);
|
const AbstractBiohead::OperationalToken activity) {
|
||||||
}
|
return robot.expressionHappyIfCurrent(activity);
|
||||||
|
});
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
grpc::Status gRPCMBioHeadServiceImpl::Surprise(grpc::ServerContext* context, const cmvr::api::Surprise_Request* request, cmvr::api::Surprise_Feedback* response)
|
grpc::Status gRPCMBioHeadServiceImpl::Surprise(grpc::ServerContext* context, const cmvr::api::Surprise_Request* request, cmvr::api::Surprise_Feedback* response)
|
||||||
{
|
{
|
||||||
try {
|
return executeHeadOperationalCommand(
|
||||||
string dev_id = request->header().device_id();
|
security_gateway_, context, dmgr_,
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
"/cmvr.api.BioHeadService/Surprise", "Surprise", request,
|
||||||
if (!robot) {
|
response,
|
||||||
return failResponse(response, "Biohead device not found: " + dev_id);
|
[](AbstractBiohead& robot,
|
||||||
}
|
const AbstractBiohead::OperationalToken activity) {
|
||||||
|
return robot.expressionSurprisedIfCurrent(activity);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
grpc::Status gRPCMBioHeadServiceImpl::ExpressionTired(grpc::ServerContext* context, const cmvr::api::ExpressionTired_Request* request, cmvr::api::ExpressionTired_Feedback* response)
|
grpc::Status gRPCMBioHeadServiceImpl::ExpressionTired(grpc::ServerContext* context, const cmvr::api::ExpressionTired_Request* request, cmvr::api::ExpressionTired_Feedback* response)
|
||||||
{
|
{
|
||||||
try {
|
return executeHeadOperationalCommand(
|
||||||
string dev_id = request->header().device_id();
|
security_gateway_, context, dmgr_,
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
"/cmvr.api.BioHeadService/ExpressionTired", "ExpressionTired",
|
||||||
if (!robot) {
|
request, response,
|
||||||
return failResponse(response, "Biohead device not found: " + dev_id);
|
[](AbstractBiohead& robot,
|
||||||
}
|
const AbstractBiohead::OperationalToken activity) {
|
||||||
|
return robot.expressionTiredIfCurrent(activity);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
grpc::Status gRPCMBioHeadServiceImpl::ExpressionAngry(grpc::ServerContext* context, const cmvr::api::ExpressionAngry_Request* request, cmvr::api::ExpressionAngry_Feedback* response)
|
grpc::Status gRPCMBioHeadServiceImpl::ExpressionAngry(grpc::ServerContext* context, const cmvr::api::ExpressionAngry_Request* request, cmvr::api::ExpressionAngry_Feedback* response)
|
||||||
{
|
{
|
||||||
try {
|
return executeHeadOperationalCommand(
|
||||||
string dev_id = request->header().device_id();
|
security_gateway_, context, dmgr_,
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
"/cmvr.api.BioHeadService/ExpressionAngry", "ExpressionAngry",
|
||||||
if (!robot) {
|
request, response,
|
||||||
return failResponse(response, "Biohead device not found: " + dev_id);
|
[](AbstractBiohead& robot,
|
||||||
}
|
const AbstractBiohead::OperationalToken activity) {
|
||||||
|
return robot.expressionAngryIfCurrent(activity);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
grpc::Status gRPCMBioHeadServiceImpl::ExpressionSadness(grpc::ServerContext* context, const cmvr::api::ExpressionSadness_Request* request, cmvr::api::ExpressionSadness_Feedback* response)
|
grpc::Status gRPCMBioHeadServiceImpl::ExpressionSadness(grpc::ServerContext* context, const cmvr::api::ExpressionSadness_Request* request, cmvr::api::ExpressionSadness_Feedback* response)
|
||||||
{
|
{
|
||||||
try {
|
return executeHeadOperationalCommand(
|
||||||
string dev_id = request->header().device_id();
|
security_gateway_, context, dmgr_,
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
"/cmvr.api.BioHeadService/ExpressionSadness",
|
||||||
if (!robot) {
|
"ExpressionSadness", request, response,
|
||||||
return failResponse(response, "Biohead device not found: " + dev_id);
|
[](AbstractBiohead& robot,
|
||||||
}
|
const AbstractBiohead::OperationalToken activity) {
|
||||||
|
return robot.expressionSadnessIfCurrent(activity);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
grpc::Status gRPCMBioHeadServiceImpl::ExpressionYawn(grpc::ServerContext* context, const cmvr::api::ExpressionYawn_Request* request, cmvr::api::ExpressionYawn_Feedback* response)
|
grpc::Status gRPCMBioHeadServiceImpl::ExpressionYawn(grpc::ServerContext* context, const cmvr::api::ExpressionYawn_Request* request, cmvr::api::ExpressionYawn_Feedback* response)
|
||||||
{
|
{
|
||||||
try {
|
return executeHeadOperationalCommand(
|
||||||
string dev_id = request->header().device_id();
|
security_gateway_, context, dmgr_,
|
||||||
auto robot = dmgr_.getDevice<AbstractBiohead>(dev_id);
|
"/cmvr.api.BioHeadService/ExpressionYawn", "ExpressionYawn",
|
||||||
if (!robot) {
|
request, response,
|
||||||
return failResponse(response, "Biohead device not found: " + dev_id);
|
[](AbstractBiohead& robot,
|
||||||
}
|
const AbstractBiohead::OperationalToken activity) {
|
||||||
|
return robot.expressionYawnIfCurrent(activity);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,14 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <google/protobuf/util/time_util.h>
|
#include <google/protobuf/util/time_util.h>
|
||||||
|
|
||||||
#include "common/base/logging/logger.h"
|
#include "common/base/logging/logger.h"
|
||||||
#include "manager/task_manager/include/task_manager.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 "service/stop_all/include/stop_all_admission_gate.h"
|
||||||
#include "task/touch_screen_task/include/touch_screen_task.h"
|
#include "task/touch_screen_task/include/touch_screen_task.h"
|
||||||
|
|
||||||
@ -41,17 +44,59 @@ void fillTouchResponse(Touch_Response* response,
|
|||||||
|
|
||||||
} // namespace
|
} // 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) {
|
gRPCHlcServiceImpl::gRPCHlcServiceImpl(
|
||||||
try {
|
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();
|
auto touch_task = task::TaskManager::getInstance().getTouchScreenTask();
|
||||||
if (!touch_task) {
|
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);
|
fillTouchResponse(response, false, error);
|
||||||
return grpc::Status(grpc::StatusCode::NOT_FOUND, 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();
|
auto& admission_gate = globalStopAllAdmissionGate();
|
||||||
std::uint64_t admission_generation = 0U;
|
std::uint64_t admission_generation = 0U;
|
||||||
{
|
{
|
||||||
@ -65,24 +110,61 @@ grpc::Status gRPCHlcServiceImpl::touch(grpc::ServerContext *context, const cmvr:
|
|||||||
admission_generation = admission.generation();
|
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(
|
if (!touch_task->touchIfCurrent(
|
||||||
request->u(), request->v(),
|
request->u(), request->v(),
|
||||||
[&admission_gate, admission_generation] {
|
[&admission_gate, admission_generation, &command] {
|
||||||
auto admission = admission_gate.lockAdmission();
|
auto admission = admission_gate.lockAdmission();
|
||||||
return admission.accepting() &&
|
return admission.accepting() &&
|
||||||
admission.generation() == admission_generation;
|
admission.generation() == admission_generation &&
|
||||||
})) {
|
command.revalidate();
|
||||||
|
},
|
||||||
|
std::move(safety_hooks))) {
|
||||||
const std::string error =
|
const std::string error =
|
||||||
buildTouchFailureMessage(*touch_task, "TouchScreenTask touch request rejected");
|
buildTouchFailureMessage(*touch_task, "TouchScreenTask touch request rejected");
|
||||||
fillTouchResponse(response, false, error);
|
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()) {
|
while (touch_task->isBusy()) {
|
||||||
if (context != nullptr && context->IsCancelled()) {
|
if (context != nullptr && context->IsCancelled()) {
|
||||||
|
const bool stopped = touch_task->stopActivity();
|
||||||
const std::string error = "touch request cancelled";
|
const std::string error = "touch request cancelled";
|
||||||
fillTouchResponse(response, false, error);
|
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));
|
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 =
|
const std::string error =
|
||||||
buildTouchFailureMessage(*touch_task, "TouchScreenTask touch failed");
|
buildTouchFailureMessage(*touch_task, "TouchScreenTask touch failed");
|
||||||
fillTouchResponse(response, false, error);
|
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, "");
|
fillTouchResponse(response, true, "");
|
||||||
@ -100,8 +184,9 @@ grpc::Status gRPCHlcServiceImpl::touch(grpc::ServerContext *context, const cmvr:
|
|||||||
<< ", phase=" << cmvr::task::TouchScreenTask::phaseToString(touch_task->phase())
|
<< ", phase=" << cmvr::task::TouchScreenTask::phaseToString(touch_task->phase())
|
||||||
<< ", status=" << cmvr::task::TouchScreenTask::statusToString(touch_task->lastStatus());
|
<< ", status=" << cmvr::task::TouchScreenTask::statusToString(touch_task->lastStatus());
|
||||||
return grpc::Status::OK;
|
return grpc::Status::OK;
|
||||||
} catch (const std::exception& e) {
|
} catch (...) {
|
||||||
fillTouchResponse(response, false, e.what());
|
(void)touch_task->stopActivity();
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
|
throw;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
#include "common/base/logging/logger.h"
|
#include "common/base/logging/logger.h"
|
||||||
#include "manager/media_source_hub/include/device_media_source_adapter.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/media_activity_coordinator.h"
|
||||||
|
#include "service/grpc/include/grpc_security.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <utility>
|
||||||
//
|
//
|
||||||
// Created by linbo on 2025/6/13.
|
// Created by linbo on 2025/6/13.
|
||||||
// Created by xtkuang 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,
|
grpc::Status gRPCMicroPhoneServiceImpl::GetStatus(grpc::ServerContext* context,
|
||||||
const api::GetMicStateCommand_Request* request, api::GetMicStateCommand_Feedback* response) {
|
const api::GetMicStateCommand_Request* request, api::GetMicStateCommand_Feedback* response) {
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.MicPhoneService/GetStatus");
|
||||||
try {
|
try {
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (GetStatus): id=" << dev_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,
|
grpc::Status gRPCMicroPhoneServiceImpl::StartRecord(grpc::ServerContext* context,
|
||||||
const api::StartMicRecordingCommand_Request* request, api::StartMicRecordingCommand_Feedback* response) {
|
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();
|
auto media_session = globalMediaActivityCoordinator().beginSession();
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Media activities are temporarily paused by StopAll");
|
response, "Media activities are temporarily paused by StopAll");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (StartRecord): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (StartRecord): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractMicrophone>(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);
|
return failResponse(response, "Microphone device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
bool started = false;
|
bool started = false;
|
||||||
|
bool dispatch_allowed = false;
|
||||||
const bool start_allowed = media_session.runIfCurrent([&] {
|
const bool start_allowed = media_session.runIfCurrent([&] {
|
||||||
|
dispatch_allowed = command.beginDispatch();
|
||||||
|
if (!dispatch_allowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
started = dev->start();
|
started = dev->start();
|
||||||
if (started) {
|
if (started) {
|
||||||
dev->startRecording(request->file_path());
|
dev->startRecording(request->file_path());
|
||||||
@ -93,6 +114,9 @@ grpc::Status gRPCMicroPhoneServiceImpl::StartRecord(grpc::ServerContext* context
|
|||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Microphone recording start was canceled by StopAll");
|
response, "Microphone recording start was canceled by StopAll");
|
||||||
}
|
}
|
||||||
|
if (!dispatch_allowed) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
if (!started) {
|
if (!started) {
|
||||||
return failResponse(response, "Failed to start microphone: " + dev_id);
|
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
|
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (StartRecord): success, id=" << dev_id
|
||||||
<< ", path=" << request->file_path();
|
<< ", path=" << request->file_path();
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCMicroPhoneServiceImpl::StopRecord(grpc::ServerContext* context,
|
||||||
const api::StopMicRecordingCommand_Request* request, api::StopMicRecordingCommand_Feedback* response) {
|
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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (StopRecord): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (StopRecord): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return failResponse(response, "Microphone device not found: " + dev_id);
|
return failResponse(response, "Microphone device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
dev->stopRecording();
|
dev->stopRecording();
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (StopRecord): success, id=" << dev_id;
|
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (StopRecord): success, id=" << dev_id;
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCMicroPhoneServiceImpl::PauseRecord(grpc::ServerContext* context,
|
||||||
const api::PauseMicRecordingCommand_Request* request, api::PauseMicRecordingCommand_Feedback* response) {
|
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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (PauseRecord): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (PauseRecord): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return failResponse(response, "Microphone device not found: " + dev_id);
|
return failResponse(response, "Microphone device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
dev->pause();
|
dev->pause();
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (PauseRecord): success, id=" << dev_id;
|
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (PauseRecord): success, id=" << dev_id;
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCMicroPhoneServiceImpl::ResumeRecord(grpc::ServerContext* context,
|
||||||
const api::ResumeMicRecordingCommand_Request* request, api::ResumeMicRecordingCommand_Feedback* response) {
|
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();
|
auto media_session = globalMediaActivityCoordinator().beginSession();
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Media activities are temporarily paused by StopAll");
|
response, "Media activities are temporarily paused by StopAll");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (ResumeRecord): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (ResumeRecord): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return failResponse(response, "Microphone device not found: " + dev_id);
|
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(
|
return failResponse(
|
||||||
response, "Microphone recording resume was canceled by StopAll");
|
response, "Microphone recording resume was canceled by StopAll");
|
||||||
}
|
}
|
||||||
|
if (!dispatch_allowed) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (ResumeRecord): success, id=" << dev_id;
|
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (ResumeRecord): success, id=" << dev_id;
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCMicroPhoneServiceImpl::StreamAudio(grpc::ServerContext* context,
|
||||||
const api::StreamMicAudioCommand_Request* request,
|
const api::StreamMicAudioCommand_Request* request,
|
||||||
grpc::ServerWriter<api::StreamMicAudioCommand_Feedback>* writer) {
|
grpc::ServerWriter<api::StreamMicAudioCommand_Feedback>* writer) {
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.MicPhoneService/StreamAudio");
|
||||||
auto media_session = globalMediaActivityCoordinator().beginSession(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] { context->TryCancel(); });
|
[context] { context->TryCancel(); });
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
@ -233,12 +259,16 @@ grpc::Status gRPCMicroPhoneServiceImpl::StreamAudio(grpc::ServerContext* context
|
|||||||
return grpc::Status::OK;
|
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,
|
track_id,
|
||||||
cmvr::media::MediaSourceHub::StartPosition::NEXT_PUBLISHED,
|
cmvr::media::MediaSourceHub::StartPosition::NEXT_PUBLISHED,
|
||||||
[context, &media_session] {
|
[context, &media_session] {
|
||||||
return context->IsCancelled() || media_session.cancelled();
|
return context->IsCancelled() || media_session.cancelled();
|
||||||
});
|
})
|
||||||
|
: cmvr::media::MediaSourceHub::Subscription{};
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
api::StreamMicAudioCommand_Feedback feedback;
|
api::StreamMicAudioCommand_Feedback feedback;
|
||||||
feedback.mutable_header()->set_success(false);
|
feedback.mutable_header()->set_success(false);
|
||||||
@ -325,30 +355,32 @@ grpc::Status gRPCMicroPhoneServiceImpl::StreamAudio(grpc::ServerContext* context
|
|||||||
|
|
||||||
grpc::Status gRPCMicroPhoneServiceImpl::SetVolume(grpc::ServerContext* context,
|
grpc::Status gRPCMicroPhoneServiceImpl::SetVolume(grpc::ServerContext* context,
|
||||||
const api::SetMicPhoneVolumeCommand_Request* request, api::SetMicPhoneVolumeCommand_Feedback* response) {
|
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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (SetVolume): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (SetVolume): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractMicrophone>(dev_id);
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return failResponse(response, "Microphone device not found: " + dev_id);
|
return failResponse(response, "Microphone device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
dev->setVolume(request->volume());
|
dev->setVolume(request->volume());
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (SetVolume): success, id=" << dev_id
|
CMVR_LOG(DEBUG) << "[gRPCMicroPhoneServiceImpl] (SetVolume): success, id=" << dev_id
|
||||||
<< ", volume=" << request->volume();
|
<< ", volume=" << request->volume();
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCMicroPhoneServiceImpl::GetVolume(grpc::ServerContext* context,
|
||||||
const api::GetMicPhoneVolumeCommand_Request* request, api::GetMicPhoneVolumeCommand_Feedback* response) {
|
const api::GetMicPhoneVolumeCommand_Request* request, api::GetMicPhoneVolumeCommand_Feedback* response) {
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.MicPhoneService/GetVolume");
|
||||||
try {
|
try {
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (GetVolume): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCMicroPhoneServiceImpl] (GetVolume): id=" << dev_id;
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
#include "common/base/logging/logger.h"
|
#include "common/base/logging/logger.h"
|
||||||
#include "devices/motor/manager/include/motor_manager.h"
|
#include "devices/motor/manager/include/motor_manager.h"
|
||||||
#include "manager/device_manager/include/device_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"
|
#include "service/stop_all/include/stop_all_admission_gate.h"
|
||||||
|
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
@ -161,7 +163,8 @@ template <typename Response, typename Body, typename Cleanup>
|
|||||||
grpc::Status runUnaryGuarded(Response* response,
|
grpc::Status runUnaryGuarded(Response* response,
|
||||||
const char* rpc_name,
|
const char* rpc_name,
|
||||||
Body&& body,
|
Body&& body,
|
||||||
Cleanup&& cleanup)
|
Cleanup&& cleanup,
|
||||||
|
GrpcCommandTransaction* command = nullptr)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return body();
|
return body();
|
||||||
@ -174,6 +177,9 @@ grpc::Status runUnaryGuarded(Response* response,
|
|||||||
}
|
}
|
||||||
response->Clear();
|
response->Clear();
|
||||||
fillFeedback(response->mutable_header(), false, error);
|
fillFeedback(response->mutable_header(), false, error);
|
||||||
|
if (command) {
|
||||||
|
return command->finishException(error);
|
||||||
|
}
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, error);
|
return grpc::Status(grpc::StatusCode::INTERNAL, error);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
const std::string error =
|
const std::string error =
|
||||||
@ -184,6 +190,9 @@ grpc::Status runUnaryGuarded(Response* response,
|
|||||||
}
|
}
|
||||||
response->Clear();
|
response->Clear();
|
||||||
fillFeedback(response->mutable_header(), false, error);
|
fillFeedback(response->mutable_header(), false, error);
|
||||||
|
if (command) {
|
||||||
|
return command->finishException(error);
|
||||||
|
}
|
||||||
return grpc::Status(grpc::StatusCode::INTERNAL, 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,
|
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::Status runCyclicLoop(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
grpc::ServerReaderWriter<api::CyclicControlResponse, Request>* stream,
|
grpc::ServerReaderWriter<api::CyclicControlResponse, Request>* stream,
|
||||||
@ -224,6 +234,7 @@ grpc::Status runCyclicLoop(
|
|||||||
Apply&& apply,
|
Apply&& apply,
|
||||||
FillStatus&& fill_status,
|
FillStatus&& fill_status,
|
||||||
IsPreempted&& is_preempted,
|
IsPreempted&& is_preempted,
|
||||||
|
ValidateSafety&& validate_safety,
|
||||||
SetLastError&& set_last_error,
|
SetLastError&& set_last_error,
|
||||||
Stop&& stop)
|
Stop&& stop)
|
||||||
{
|
{
|
||||||
@ -398,6 +409,15 @@ grpc::Status runCyclicLoop(
|
|||||||
grpc::StatusCode::INTERNAL,
|
grpc::StatusCode::INTERNAL,
|
||||||
"failed to quick-stop preempted cyclic stream");
|
"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;
|
std::optional<Request> request;
|
||||||
bool ended = false;
|
bool ended = false;
|
||||||
@ -548,7 +568,16 @@ gRPCMotorServiceImpl::ControlLease::~ControlLease()
|
|||||||
}
|
}
|
||||||
|
|
||||||
gRPCMotorServiceImpl::gRPCMotorServiceImpl()
|
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,
|
const api::SetMotorZeroRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response)
|
||||||
{
|
{
|
||||||
|
return executeRegisteredGrpcCommand(
|
||||||
|
security_gateway_, context, dmgr_.safetyCoordinator(),
|
||||||
|
"/cmvr.api.MotorService/setZero", request, response,
|
||||||
|
[this, context, request, response](GrpcCommandTransaction& command) {
|
||||||
return runUnaryGuarded(
|
return runUnaryGuarded(
|
||||||
response, "setZero",
|
response, "setZero",
|
||||||
[&]() { return setZeroImpl(context, request, response); },
|
[&]() {
|
||||||
|
return setZeroImpl(
|
||||||
|
context, request, response, command);
|
||||||
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
bestEffortQuickStop(request->target(), error);
|
bestEffortQuickStop(request->target(), error);
|
||||||
|
},
|
||||||
|
&command);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -853,11 +891,20 @@ grpc::Status gRPCMotorServiceImpl::moveToZero(
|
|||||||
const api::MoveMotorToZeroRequest* request,
|
const api::MoveMotorToZeroRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response)
|
||||||
{
|
{
|
||||||
|
return executeRegisteredGrpcCommand(
|
||||||
|
security_gateway_, context, dmgr_.safetyCoordinator(),
|
||||||
|
"/cmvr.api.MotorService/moveToZero", request, response,
|
||||||
|
[this, context, request, response](GrpcCommandTransaction& command) {
|
||||||
return runUnaryGuarded(
|
return runUnaryGuarded(
|
||||||
response, "moveToZero",
|
response, "moveToZero",
|
||||||
[&]() { return moveToZeroImpl(context, request, response); },
|
[&]() {
|
||||||
|
return moveToZeroImpl(
|
||||||
|
context, request, response, command);
|
||||||
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
bestEffortQuickStop(request->target(), error);
|
bestEffortQuickStop(request->target(), error);
|
||||||
|
},
|
||||||
|
&command);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -866,11 +913,20 @@ grpc::Status gRPCMotorServiceImpl::profilePosition(
|
|||||||
const api::ProfilePositionRequest* request,
|
const api::ProfilePositionRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response)
|
||||||
{
|
{
|
||||||
|
return executeRegisteredGrpcCommand(
|
||||||
|
security_gateway_, context, dmgr_.safetyCoordinator(),
|
||||||
|
"/cmvr.api.MotorService/profilePosition", request, response,
|
||||||
|
[this, context, request, response](GrpcCommandTransaction& command) {
|
||||||
return runUnaryGuarded(
|
return runUnaryGuarded(
|
||||||
response, "profilePosition",
|
response, "profilePosition",
|
||||||
[&]() { return profilePositionImpl(context, request, response); },
|
[&]() {
|
||||||
|
return profilePositionImpl(
|
||||||
|
context, request, response, command);
|
||||||
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
bestEffortQuickStop(request->target(), error);
|
bestEffortQuickStop(request->target(), error);
|
||||||
|
},
|
||||||
|
&command);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -879,11 +935,20 @@ grpc::Status gRPCMotorServiceImpl::profileVelocity(
|
|||||||
const api::ProfileVelocityRequest* request,
|
const api::ProfileVelocityRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response)
|
||||||
{
|
{
|
||||||
|
return executeRegisteredGrpcCommand(
|
||||||
|
security_gateway_, context, dmgr_.safetyCoordinator(),
|
||||||
|
"/cmvr.api.MotorService/profileVelocity", request, response,
|
||||||
|
[this, context, request, response](GrpcCommandTransaction& command) {
|
||||||
return runUnaryGuarded(
|
return runUnaryGuarded(
|
||||||
response, "profileVelocity",
|
response, "profileVelocity",
|
||||||
[&]() { return profileVelocityImpl(context, request, response); },
|
[&]() {
|
||||||
|
return profileVelocityImpl(
|
||||||
|
context, request, response, command);
|
||||||
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
bestEffortQuickStop(request->target(), error);
|
bestEffortQuickStop(request->target(), error);
|
||||||
|
},
|
||||||
|
&command);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -892,12 +957,16 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPosition(
|
|||||||
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
||||||
api::CyclicPositionRequest>* stream)
|
api::CyclicPositionRequest>* stream)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.MotorService/streamCyclicPosition");
|
||||||
std::optional<api::MotorTarget> cleanup_target;
|
std::optional<api::MotorTarget> cleanup_target;
|
||||||
return runStreamingGuarded(
|
return runStreamingGuarded(
|
||||||
"streamCyclicPosition",
|
"streamCyclicPosition",
|
||||||
[&]() {
|
[&]() {
|
||||||
return streamCyclicPositionImpl(
|
return streamCyclicPositionImpl(
|
||||||
context, stream, cleanup_target);
|
context, stream, cleanup_target,
|
||||||
|
cmvr_grpc_call_guard.context());
|
||||||
},
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
if (cleanup_target.has_value()) {
|
if (cleanup_target.has_value()) {
|
||||||
@ -911,12 +980,16 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocity(
|
|||||||
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
||||||
api::CyclicVelocityRequest>* stream)
|
api::CyclicVelocityRequest>* stream)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.MotorService/streamCyclicVelocity");
|
||||||
std::optional<api::MotorTarget> cleanup_target;
|
std::optional<api::MotorTarget> cleanup_target;
|
||||||
return runStreamingGuarded(
|
return runStreamingGuarded(
|
||||||
"streamCyclicVelocity",
|
"streamCyclicVelocity",
|
||||||
[&]() {
|
[&]() {
|
||||||
return streamCyclicVelocityImpl(
|
return streamCyclicVelocityImpl(
|
||||||
context, stream, cleanup_target);
|
context, stream, cleanup_target,
|
||||||
|
cmvr_grpc_call_guard.context());
|
||||||
},
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
if (cleanup_target.has_value()) {
|
if (cleanup_target.has_value()) {
|
||||||
@ -930,11 +1003,20 @@ grpc::Status gRPCMotorServiceImpl::emergencyStop(
|
|||||||
const api::EmergencyStopRequest* request,
|
const api::EmergencyStopRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response)
|
||||||
{
|
{
|
||||||
|
return executeRegisteredGrpcCommand(
|
||||||
|
security_gateway_, context, dmgr_.safetyCoordinator(),
|
||||||
|
"/cmvr.api.MotorService/emergencyStop", request, response,
|
||||||
|
[this, context, request, response](GrpcCommandTransaction& command) {
|
||||||
return runUnaryGuarded(
|
return runUnaryGuarded(
|
||||||
response, "emergencyStop",
|
response, "emergencyStop",
|
||||||
[&]() { return emergencyStopImpl(context, request, response); },
|
[&]() {
|
||||||
|
return emergencyStopImpl(
|
||||||
|
context, request, response, command);
|
||||||
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
bestEffortQuickStop(request->target(), error);
|
bestEffortQuickStop(request->target(), error);
|
||||||
|
},
|
||||||
|
&command);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -943,6 +1025,8 @@ grpc::Status gRPCMotorServiceImpl::getStatus(
|
|||||||
const api::GetMotorStatusRequest* request,
|
const api::GetMotorStatusRequest* request,
|
||||||
api::GetMotorStatusResponse* response)
|
api::GetMotorStatusResponse* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.MotorService/getStatus");
|
||||||
return runUnaryGuarded(
|
return runUnaryGuarded(
|
||||||
response, "getStatus",
|
response, "getStatus",
|
||||||
[&]() { return getStatusImpl(context, request, response); },
|
[&]() { return getStatusImpl(context, request, response); },
|
||||||
@ -954,18 +1038,28 @@ grpc::Status gRPCMotorServiceImpl::setEnabled(
|
|||||||
const api::SetMotorEnabledRequest* request,
|
const api::SetMotorEnabledRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response)
|
||||||
{
|
{
|
||||||
|
return executeRegisteredGrpcCommand(
|
||||||
|
security_gateway_, context, dmgr_.safetyCoordinator(),
|
||||||
|
"/cmvr.api.MotorService/setEnabled", request, response,
|
||||||
|
[this, context, request, response](GrpcCommandTransaction& command) {
|
||||||
return runUnaryGuarded(
|
return runUnaryGuarded(
|
||||||
response, "setEnabled",
|
response, "setEnabled",
|
||||||
[&]() { return setEnabledImpl(context, request, response); },
|
[&]() {
|
||||||
|
return setEnabledImpl(
|
||||||
|
context, request, response, command);
|
||||||
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
bestEffortQuickStop(request->target(), error);
|
bestEffortQuickStop(request->target(), error);
|
||||||
|
},
|
||||||
|
&command);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
grpc::Status gRPCMotorServiceImpl::setZeroImpl(
|
grpc::Status gRPCMotorServiceImpl::setZeroImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
const api::SetMotorZeroRequest* request,
|
const api::SetMotorZeroRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command)
|
||||||
{
|
{
|
||||||
const auto started = Clock::now();
|
const auto started = Clock::now();
|
||||||
ResolvedMotor resolved;
|
ResolvedMotor resolved;
|
||||||
@ -1014,6 +1108,10 @@ grpc::Status gRPCMotorServiceImpl::setZeroImpl(
|
|||||||
response->set_elapsed_ms(elapsedMs(started));
|
response->set_elapsed_ms(elapsedMs(started));
|
||||||
return grpc::Status(grpc::StatusCode::ABORTED, error);
|
return grpc::Status(grpc::StatusCode::ABORTED, error);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
lease.reset();
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
calibrated = resolved.motor->calibrateZeroQ();
|
calibrated = resolved.motor->calibrateZeroQ();
|
||||||
{
|
{
|
||||||
std::lock_guard state_lock(resolved.control->mutex);
|
std::lock_guard state_lock(resolved.control->mutex);
|
||||||
@ -1074,7 +1172,8 @@ grpc::Status gRPCMotorServiceImpl::runProfilePosition(
|
|||||||
const double max_velocity_rad_s,
|
const double max_velocity_rad_s,
|
||||||
const double acceleration_rad_s2,
|
const double acceleration_rad_s2,
|
||||||
const api::MotorWaitOptions& wait,
|
const api::MotorWaitOptions& wait,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command)
|
||||||
{
|
{
|
||||||
const auto started = Clock::now();
|
const auto started = Clock::now();
|
||||||
if (!isFinite(target_position_rad) ||
|
if (!isFinite(target_position_rad) ||
|
||||||
@ -1131,6 +1230,10 @@ grpc::Status gRPCMotorServiceImpl::runProfilePosition(
|
|||||||
response->set_elapsed_ms(elapsedMs(started));
|
response->set_elapsed_ms(elapsedMs(started));
|
||||||
return grpc::Status(grpc::StatusCode::ABORTED, error);
|
return grpc::Status(grpc::StatusCode::ABORTED, error);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
lease.reset();
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
resolved.motor->setMode(msgs::RUN_MODE_PROFILE_POSITION);
|
resolved.motor->setMode(msgs::RUN_MODE_PROFILE_POSITION);
|
||||||
submitted = resolved.motor->commandProfilePosition(
|
submitted = resolved.motor->commandProfilePosition(
|
||||||
target_position_rad, max_velocity_rad_s, acceleration_rad_s2);
|
target_position_rad, max_velocity_rad_s, acceleration_rad_s2);
|
||||||
@ -1355,7 +1458,8 @@ grpc::Status gRPCMotorServiceImpl::waitForPosition(
|
|||||||
grpc::Status gRPCMotorServiceImpl::moveToZeroImpl(
|
grpc::Status gRPCMotorServiceImpl::moveToZeroImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
const api::MoveMotorToZeroRequest* request,
|
const api::MoveMotorToZeroRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command)
|
||||||
{
|
{
|
||||||
ResolvedMotor resolved;
|
ResolvedMotor resolved;
|
||||||
auto status = resolveMotor(request->target(), resolved);
|
auto status = resolveMotor(request->target(), resolved);
|
||||||
@ -1365,13 +1469,14 @@ grpc::Status gRPCMotorServiceImpl::moveToZeroImpl(
|
|||||||
}
|
}
|
||||||
return runProfilePosition(
|
return runProfilePosition(
|
||||||
context, resolved, 0.0, request->max_velocity_rad_s(),
|
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::Status gRPCMotorServiceImpl::profilePositionImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
const api::ProfilePositionRequest* request,
|
const api::ProfilePositionRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command)
|
||||||
{
|
{
|
||||||
ResolvedMotor resolved;
|
ResolvedMotor resolved;
|
||||||
auto status = resolveMotor(request->target(), resolved);
|
auto status = resolveMotor(request->target(), resolved);
|
||||||
@ -1382,7 +1487,7 @@ grpc::Status gRPCMotorServiceImpl::profilePositionImpl(
|
|||||||
return runProfilePosition(
|
return runProfilePosition(
|
||||||
context, resolved, request->target_position_rad(),
|
context, resolved, request->target_position_rad(),
|
||||||
request->max_velocity_rad_s(), request->acceleration_rad_s2(),
|
request->max_velocity_rad_s(), request->acceleration_rad_s2(),
|
||||||
request->wait(), response);
|
request->wait(), response, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
grpc::Status gRPCMotorServiceImpl::waitForVelocity(
|
grpc::Status gRPCMotorServiceImpl::waitForVelocity(
|
||||||
@ -1515,7 +1620,8 @@ grpc::Status gRPCMotorServiceImpl::waitForVelocity(
|
|||||||
grpc::Status gRPCMotorServiceImpl::profileVelocityImpl(
|
grpc::Status gRPCMotorServiceImpl::profileVelocityImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
const api::ProfileVelocityRequest* request,
|
const api::ProfileVelocityRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command)
|
||||||
{
|
{
|
||||||
const auto started = Clock::now();
|
const auto started = Clock::now();
|
||||||
ResolvedMotor resolved;
|
ResolvedMotor resolved;
|
||||||
@ -1578,6 +1684,10 @@ grpc::Status gRPCMotorServiceImpl::profileVelocityImpl(
|
|||||||
response->set_elapsed_ms(elapsedMs(started));
|
response->set_elapsed_ms(elapsedMs(started));
|
||||||
return grpc::Status(grpc::StatusCode::ABORTED, error);
|
return grpc::Status(grpc::StatusCode::ABORTED, error);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
lease.reset();
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
resolved.motor->setMode(msgs::RUN_MODE_PROFILE_VELOCITY);
|
resolved.motor->setMode(msgs::RUN_MODE_PROFILE_VELOCITY);
|
||||||
submitted = resolved.motor->commandProfileVelocity(
|
submitted = resolved.motor->commandProfileVelocity(
|
||||||
request->target_velocity_rad_s(),
|
request->target_velocity_rad_s(),
|
||||||
@ -1672,7 +1782,8 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
|
|||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
||||||
api::CyclicPositionRequest>* stream,
|
api::CyclicPositionRequest>* stream,
|
||||||
std::optional<api::MotorTarget>& cleanup_target)
|
std::optional<api::MotorTarget>& cleanup_target,
|
||||||
|
const GrpcRequestContext& request_context)
|
||||||
{
|
{
|
||||||
api::CyclicPositionRequest first;
|
api::CyclicPositionRequest first;
|
||||||
if (!stream->Read(&first) || !first.has_open()) {
|
if (!stream->Read(&first) || !first.has_open()) {
|
||||||
@ -1693,6 +1804,28 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto generation = lease->generation();
|
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);
|
std::lock_guard command_lock(resolved.control->command_mutex);
|
||||||
if (context->IsCancelled() ||
|
if (context->IsCancelled() ||
|
||||||
@ -1710,6 +1843,10 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
|
|||||||
grpc::StatusCode::ABORTED,
|
grpc::StatusCode::ABORTED,
|
||||||
"cyclic position open preempted by a stop request");
|
"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);
|
resolved.motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_POSITION);
|
||||||
std::lock_guard state_lock(resolved.control->mutex);
|
std::lock_guard state_lock(resolved.control->mutex);
|
||||||
if (resolved.control->cancel_generation != generation) {
|
if (resolved.control->cancel_generation != generation) {
|
||||||
@ -1743,6 +1880,10 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
|
|||||||
grpc::StatusCode::INVALID_ARGUMENT,
|
grpc::StatusCode::INVALID_ARGUMENT,
|
||||||
"cyclic position setpoint contains a non-finite value");
|
"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(
|
const bool submitted = resolved.motor->commandCyclicPosition(
|
||||||
setpoint.target_position_rad(),
|
setpoint.target_position_rad(),
|
||||||
setpoint.has_target_velocity_rad_s()
|
setpoint.has_target_velocity_rad_s()
|
||||||
@ -1770,6 +1911,11 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicPositionImpl(
|
|||||||
std::lock_guard lock(resolved.control->mutex);
|
std::lock_guard lock(resolved.control->mutex);
|
||||||
return resolved.control->cancel_generation != expected_generation;
|
return resolved.control->cancel_generation != expected_generation;
|
||||||
},
|
},
|
||||||
|
[&]() {
|
||||||
|
return safety_session.revalidate()
|
||||||
|
? grpc::Status::OK
|
||||||
|
: safety_session.status();
|
||||||
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
setLastError(resolved.control, error);
|
setLastError(resolved.control, error);
|
||||||
},
|
},
|
||||||
@ -1795,7 +1941,8 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
|
|||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
grpc::ServerReaderWriter<api::CyclicControlResponse,
|
||||||
api::CyclicVelocityRequest>* stream,
|
api::CyclicVelocityRequest>* stream,
|
||||||
std::optional<api::MotorTarget>& cleanup_target)
|
std::optional<api::MotorTarget>& cleanup_target,
|
||||||
|
const GrpcRequestContext& request_context)
|
||||||
{
|
{
|
||||||
api::CyclicVelocityRequest first;
|
api::CyclicVelocityRequest first;
|
||||||
if (!stream->Read(&first) || !first.has_open()) {
|
if (!stream->Read(&first) || !first.has_open()) {
|
||||||
@ -1816,6 +1963,28 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto generation = lease->generation();
|
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);
|
std::lock_guard command_lock(resolved.control->command_mutex);
|
||||||
if (context->IsCancelled() ||
|
if (context->IsCancelled() ||
|
||||||
@ -1833,6 +2002,10 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
|
|||||||
grpc::StatusCode::ABORTED,
|
grpc::StatusCode::ABORTED,
|
||||||
"cyclic velocity open preempted by a stop request");
|
"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);
|
resolved.motor->setMode(msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY);
|
||||||
std::lock_guard state_lock(resolved.control->mutex);
|
std::lock_guard state_lock(resolved.control->mutex);
|
||||||
if (resolved.control->cancel_generation != generation) {
|
if (resolved.control->cancel_generation != generation) {
|
||||||
@ -1864,6 +2037,10 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
|
|||||||
grpc::StatusCode::INVALID_ARGUMENT,
|
grpc::StatusCode::INVALID_ARGUMENT,
|
||||||
"cyclic velocity setpoint contains a non-finite value");
|
"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(
|
const bool submitted = resolved.motor->commandCyclicVelocity(
|
||||||
setpoint.target_velocity_rad_s());
|
setpoint.target_velocity_rad_s());
|
||||||
{
|
{
|
||||||
@ -1888,6 +2065,11 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
|
|||||||
std::lock_guard lock(resolved.control->mutex);
|
std::lock_guard lock(resolved.control->mutex);
|
||||||
return resolved.control->cancel_generation != expected_generation;
|
return resolved.control->cancel_generation != expected_generation;
|
||||||
},
|
},
|
||||||
|
[&]() {
|
||||||
|
return safety_session.revalidate()
|
||||||
|
? grpc::Status::OK
|
||||||
|
: safety_session.status();
|
||||||
|
},
|
||||||
[&](const std::string& error) {
|
[&](const std::string& error) {
|
||||||
setLastError(resolved.control, error);
|
setLastError(resolved.control, error);
|
||||||
},
|
},
|
||||||
@ -1912,7 +2094,8 @@ grpc::Status gRPCMotorServiceImpl::streamCyclicVelocityImpl(
|
|||||||
grpc::Status gRPCMotorServiceImpl::emergencyStopImpl(
|
grpc::Status gRPCMotorServiceImpl::emergencyStopImpl(
|
||||||
grpc::ServerContext*,
|
grpc::ServerContext*,
|
||||||
const api::EmergencyStopRequest* request,
|
const api::EmergencyStopRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command)
|
||||||
{
|
{
|
||||||
const auto started = Clock::now();
|
const auto started = Clock::now();
|
||||||
ResolvedMotor resolved;
|
ResolvedMotor resolved;
|
||||||
@ -1922,6 +2105,9 @@ grpc::Status gRPCMotorServiceImpl::emergencyStopImpl(
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
std::lock_guard emergency_lock(resolved.control->emergency_mutex);
|
std::lock_guard emergency_lock(resolved.control->emergency_mutex);
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
bool stopped = false;
|
bool stopped = false;
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@ -1993,7 +2179,8 @@ grpc::Status gRPCMotorServiceImpl::getStatusImpl(
|
|||||||
grpc::Status gRPCMotorServiceImpl::setEnabledImpl(
|
grpc::Status gRPCMotorServiceImpl::setEnabledImpl(
|
||||||
grpc::ServerContext* context,
|
grpc::ServerContext* context,
|
||||||
const api::SetMotorEnabledRequest* request,
|
const api::SetMotorEnabledRequest* request,
|
||||||
api::MotorCommandResponse* response)
|
api::MotorCommandResponse* response,
|
||||||
|
GrpcCommandTransaction& command)
|
||||||
{
|
{
|
||||||
const auto started = Clock::now();
|
const auto started = Clock::now();
|
||||||
ResolvedMotor resolved;
|
ResolvedMotor resolved;
|
||||||
@ -2045,6 +2232,10 @@ grpc::Status gRPCMotorServiceImpl::setEnabledImpl(
|
|||||||
response->set_elapsed_ms(elapsedMs(started));
|
response->set_elapsed_ms(elapsedMs(started));
|
||||||
return grpc::Status(grpc::StatusCode::ABORTED, error);
|
return grpc::Status(grpc::StatusCode::ABORTED, error);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
lease.reset();
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
success = request->enabled()
|
success = request->enabled()
|
||||||
? resolved.motor->torqueOn()
|
? resolved.motor->torqueOn()
|
||||||
: resolved.motor->torqueOff();
|
: 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 "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/media_activity_coordinator.h"
|
||||||
|
#include "service/grpc/include/grpc_security.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
//
|
//
|
||||||
// Created by xtkuang on 2025/6/10.
|
// 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,
|
grpc::Status gRPCSpeakerServiceImpl::GetStatus(grpc::ServerContext* context,
|
||||||
const api::GetSpeakerStateCommand_Request* request, api::GetSpeakerStateCommand_Feedback* response) {
|
const api::GetSpeakerStateCommand_Request* request, api::GetSpeakerStateCommand_Feedback* response) {
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.SpeakerService/GetStatus");
|
||||||
try {
|
try {
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
//CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (GetStatus): id=" << dev_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,
|
grpc::Status gRPCSpeakerServiceImpl::PlayAudio(grpc::ServerContext* context,
|
||||||
const api::PlayAudioCommand_Request* request, api::PlayAudioCommand_Feedback* response) {
|
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();
|
auto media_session = globalMediaActivityCoordinator().beginSession();
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Media activities are temporarily paused by StopAll");
|
response, "Media activities are temporarily paused by StopAll");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (PlayAudio): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (PlayAudio): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
||||||
@ -148,29 +165,32 @@ grpc::Status gRPCSpeakerServiceImpl::PlayAudio(grpc::ServerContext* context,
|
|||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Speaker is already controlled by another media session: " + dev_id);
|
response, "Speaker is already controlled by another media session: " + dev_id);
|
||||||
}
|
}
|
||||||
|
bool dispatch_allowed = false;
|
||||||
if (!media_session.runIfCurrent([&] {
|
if (!media_session.runIfCurrent([&] {
|
||||||
|
dispatch_allowed = command.beginDispatch();
|
||||||
|
if (dispatch_allowed) {
|
||||||
dev->play(request->audio_path());
|
dev->play(request->audio_path());
|
||||||
|
}
|
||||||
})) {
|
})) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Speaker playback start was canceled by StopAll");
|
response, "Speaker playback start was canceled by StopAll");
|
||||||
}
|
}
|
||||||
|
if (!dispatch_allowed) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (PlayAudio): success, id=" << dev_id
|
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (PlayAudio): success, id=" << dev_id
|
||||||
<< ", path=" << request->audio_path();
|
<< ", path=" << request->audio_path();
|
||||||
return grpc::Status::OK;
|
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::Status gRPCSpeakerServiceImpl::StreamAudio(grpc::ServerContext* context,
|
||||||
grpc::ServerReader<api::StreamSpeakerAudioCommand_Request>* reader,
|
grpc::ServerReader<api::StreamSpeakerAudioCommand_Request>* reader,
|
||||||
api::StreamSpeakerAudioCommand_Feedback* response) {
|
api::StreamSpeakerAudioCommand_Feedback* response) {
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.SpeakerService/StreamAudio");
|
||||||
auto media_session = globalMediaActivityCoordinator().beginSession(
|
auto media_session = globalMediaActivityCoordinator().beginSession(
|
||||||
[context] { context->TryCancel(); });
|
[context] { context->TryCancel(); });
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
@ -181,6 +201,7 @@ grpc::Status gRPCSpeakerServiceImpl::StreamAudio(grpc::ServerContext* context,
|
|||||||
api::StreamSpeakerAudioCommand_Request request;
|
api::StreamSpeakerAudioCommand_Request request;
|
||||||
std::shared_ptr<AbstractSpeaker> dev;
|
std::shared_ptr<AbstractSpeaker> dev;
|
||||||
std::unique_ptr<SpeakerStreamingLease> stream_lease;
|
std::unique_ptr<SpeakerStreamingLease> stream_lease;
|
||||||
|
std::optional<GrpcStreamingSafetySession> safety_session;
|
||||||
std::string dev_id;
|
std::string dev_id;
|
||||||
|
|
||||||
while (reader->Read(&request)) {
|
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);
|
"Speaker is already controlled by another media session: " + dev_id);
|
||||||
}
|
}
|
||||||
stream_lease = std::make_unique<SpeakerStreamingLease>(dev);
|
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());
|
const auto frame = fromProtoAudioData(request.audio());
|
||||||
bool pushed = false;
|
bool pushed = false;
|
||||||
|
grpc::Status dispatch_status = grpc::Status::OK;
|
||||||
const bool push_allowed = media_session.runIfCurrent([&] {
|
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()) {
|
if (!frame.data.empty()) {
|
||||||
stream_lease->arm();
|
stream_lease->arm();
|
||||||
}
|
}
|
||||||
@ -214,6 +278,9 @@ grpc::Status gRPCSpeakerServiceImpl::StreamAudio(grpc::ServerContext* context,
|
|||||||
if (!push_allowed || media_session.cancelled()) {
|
if (!push_allowed || media_session.cancelled()) {
|
||||||
return mediaStoppedStatus();
|
return mediaStoppedStatus();
|
||||||
}
|
}
|
||||||
|
if (!dispatch_status.ok()) {
|
||||||
|
return dispatch_status;
|
||||||
|
}
|
||||||
if (!pushed) {
|
if (!pushed) {
|
||||||
return failResponse(response, "Failed to push speaker audio frame: " + dev_id);
|
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,
|
grpc::Status gRPCSpeakerServiceImpl::StopPlayback(grpc::ServerContext* context,
|
||||||
const api::StopSpeakerCommand_Request* request, api::StopSpeakerCommand_Feedback* response) {
|
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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (StopPlayback): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (StopPlayback): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return failResponse(response, "Speaker device not found: " + dev_id);
|
return failResponse(response, "Speaker device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
if (!dev->stopPlayback()) {
|
if (!dev->stopPlayback()) {
|
||||||
return failResponse(response, "Failed to stop speaker: " + dev_id);
|
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());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (StopPlayback): success, id=" << dev_id;
|
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (StopPlayback): success, id=" << dev_id;
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCSpeakerServiceImpl::PausePlayback(grpc::ServerContext* context,
|
||||||
const api::PauseSpeakerCommand_Request* request, api::PauseSpeakerCommand_Feedback* response) {
|
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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (PausePlayback): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (PausePlayback): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return failResponse(response, "Speaker device not found: " + dev_id);
|
return failResponse(response, "Speaker device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
dev->pause();
|
dev->pause();
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (PausePlayback): success, id=" << dev_id;
|
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (PausePlayback): success, id=" << dev_id;
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCSpeakerServiceImpl::ResumePlayback(grpc::ServerContext* context,
|
||||||
const api::ResumeSpeakerCommand_Request* request, api::ResumeSpeakerCommand_Feedback* response) {
|
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();
|
auto media_session = globalMediaActivityCoordinator().beginSession();
|
||||||
if (!media_session) {
|
if (!media_session) {
|
||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Media activities are temporarily paused by StopAll");
|
response, "Media activities are temporarily paused by StopAll");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (ResumePlayback): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (ResumePlayback): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
||||||
@ -303,49 +373,54 @@ grpc::Status gRPCSpeakerServiceImpl::ResumePlayback(grpc::ServerContext* context
|
|||||||
return failResponse(
|
return failResponse(
|
||||||
response, "Speaker is already controlled by another media session: " + dev_id);
|
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(
|
return failResponse(
|
||||||
response, "Speaker playback resume was canceled by StopAll");
|
response, "Speaker playback resume was canceled by StopAll");
|
||||||
}
|
}
|
||||||
|
if (!dispatch_allowed) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (ResumePlayback): success, id=" << dev_id;
|
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (ResumePlayback): success, id=" << dev_id;
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCSpeakerServiceImpl::SetVolume(grpc::ServerContext* context,
|
||||||
const api::SetSpeakerVolumeCommand_Request* request, api::SetSpeakerVolumeCommand_Feedback* response) {
|
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();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (SetVolume): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (SetVolume): id=" << dev_id;
|
||||||
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
const auto dev = dmgr_.getDevice<AbstractSpeaker>(dev_id);
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
return failResponse(response, "Speaker device not found: " + dev_id);
|
return failResponse(response, "Speaker device not found: " + dev_id);
|
||||||
}
|
}
|
||||||
|
if (!command.beginDispatch()) {
|
||||||
|
return command.dispatchStatus();
|
||||||
|
}
|
||||||
dev->setVolume(request->volume());
|
dev->setVolume(request->volume());
|
||||||
response->mutable_header()->set_success(true);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (SetVolume): success, id=" << dev_id
|
CMVR_LOG(DEBUG) << "[gRPCSpeakerServiceImpl] (SetVolume): success, id=" << dev_id
|
||||||
<< ", volume=" << request->volume();
|
<< ", volume=" << request->volume();
|
||||||
return grpc::Status::OK;
|
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,
|
grpc::Status gRPCSpeakerServiceImpl::GetVolume(grpc::ServerContext* context,
|
||||||
const api::GetSpeakerVolumeCommand_Request* request, api::GetSpeakerVolumeCommand_Feedback* response) {
|
const api::GetSpeakerVolumeCommand_Request* request, api::GetSpeakerVolumeCommand_Feedback* response) {
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context, "/cmvr.api.SpeakerService/GetVolume");
|
||||||
try {
|
try {
|
||||||
string dev_id = request->header().device_id();
|
string dev_id = request->header().device_id();
|
||||||
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (GetVolume): id=" << dev_id;
|
CMVR_LOG(INFO) << "[gRPCSpeakerServiceImpl] (GetVolume): id=" << dev_id;
|
||||||
|
|||||||
@ -33,6 +33,10 @@
|
|||||||
#include "service/action/include/action_queue_executor.h"
|
#include "service/action/include/action_queue_executor.h"
|
||||||
#include "service/grpc/include/camera_operational_activity_registry.h"
|
#include "service/grpc/include/camera_operational_activity_registry.h"
|
||||||
#include "service/grpc/include/camera_ptz_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/media_activity_coordinator.h"
|
||||||
#include "service/grpc/include/motor_activity_coordinator.h"
|
#include "service/grpc/include/motor_activity_coordinator.h"
|
||||||
#include "service/stop_all/include/stop_all_admission_gate.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;
|
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
|
} // namespace
|
||||||
|
|
||||||
gRPCSystemServiceImpl::gRPCSystemServiceImpl()
|
gRPCSystemServiceImpl::gRPCSystemServiceImpl()
|
||||||
: gRPCSystemServiceImpl(std::chrono::seconds(15))
|
: gRPCSystemServiceImpl(
|
||||||
|
std::chrono::seconds(15), makeDefaultGrpcSecurityGateway())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
|
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
|
||||||
const std::chrono::milliseconds stop_timeout)
|
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()),
|
: dmgr_(DeviceManager::getInstance()),
|
||||||
stop_timeout_(
|
stop_timeout_(
|
||||||
stop_timeout > std::chrono::milliseconds::zero()
|
stop_timeout > std::chrono::milliseconds::zero()
|
||||||
? stop_timeout
|
? stop_timeout
|
||||||
: std::chrono::seconds(15)),
|
: 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
|
// Acquire only after ActionQueue construction succeeds. This ensures an
|
||||||
// exception cannot release the last dispatcher outside the registry.
|
// exception cannot release the last dispatcher outside the registry.
|
||||||
acquireProcessStopDispatcher(stop_dispatcher_);
|
acquireProcessStopDispatcher(stop_dispatcher_);
|
||||||
|
try {
|
||||||
|
safety_participant_registration_ = registerGrpcSafetyParticipants(
|
||||||
|
dmgr_.safetyCoordinator(), action_queue_, stop_dispatcher_);
|
||||||
|
} catch (...) {
|
||||||
|
releaseProcessStopDispatcher(stop_dispatcher_);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gRPCSystemServiceImpl::~gRPCSystemServiceImpl()
|
gRPCSystemServiceImpl::~gRPCSystemServiceImpl()
|
||||||
{
|
{
|
||||||
// The dispatcher intentionally outlives ActionQueue, then joins any
|
// The dispatcher intentionally outlives ActionQueue, then joins any
|
||||||
// deadline-overrunning stop workers before the last service disappears.
|
// deadline-overrunning stop workers before the last service disappears.
|
||||||
|
safety_participant_registration_.reset();
|
||||||
action_queue_.reset();
|
action_queue_.reset();
|
||||||
releaseProcessStopDispatcher(stop_dispatcher_);
|
releaseProcessStopDispatcher(stop_dispatcher_);
|
||||||
}
|
}
|
||||||
@ -1088,11 +1202,27 @@ void gRPCSystemServiceImpl::prepareForShutdown()
|
|||||||
grpc::Status gRPCSystemServiceImpl::GetSystemInfo(grpc::ServerContext* context,
|
grpc::Status gRPCSystemServiceImpl::GetSystemInfo(grpc::ServerContext* context,
|
||||||
const api::GetSystemInfoCommand_Request* request, api::GetSystemInfoCommand_Feedback* response)
|
const api::GetSystemInfoCommand_Request* request, api::GetSystemInfoCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.SystemService/GetSystemInfo");
|
||||||
try {
|
try {
|
||||||
response->set_version(dmgr_.version());
|
response->set_version(dmgr_.version());
|
||||||
response->set_system_name(dmgr_.name());
|
response->set_system_name(dmgr_.name());
|
||||||
response->set_action_service_instance_id(
|
response->set_action_service_instance_id(
|
||||||
action_queue_->instanceId());
|
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);
|
response->mutable_header()->set_success(true);
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
||||||
CMVR_LOG(DEBUG) << "[gRPCSystemServiceImpl] (GetSystemInfo): success, name="
|
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,
|
grpc::Status gRPCSystemServiceImpl::GetSystemStatus(grpc::ServerContext* context,
|
||||||
const api::GetSystemStatusCommand_Request* request, api::GetSystemStatusCommand_Feedback* response)
|
const api::GetSystemStatusCommand_Request* request, api::GetSystemStatusCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.SystemService/GetSystemStatus");
|
||||||
try {
|
try {
|
||||||
std::list<std::pair<std::string, std::string>> dev_list;
|
std::list<std::pair<std::string, std::string>> dev_list;
|
||||||
dmgr_.getDeviceList(dev_list);
|
dmgr_.getDeviceList(dev_list);
|
||||||
@ -1163,7 +1296,9 @@ grpc::Status gRPCSystemServiceImpl::GetDeviceList(
|
|||||||
const api::GetDeviceListCommand_Request* request,
|
const api::GetDeviceListCommand_Request* request,
|
||||||
api::GetDeviceListCommand_Feedback* response)
|
api::GetDeviceListCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
(void)context;
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.SystemService/GetDeviceList");
|
||||||
(void)request;
|
(void)request;
|
||||||
try {
|
try {
|
||||||
const auto snapshot = dmgr_.snapshot();
|
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)
|
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_success(false);
|
||||||
response->mutable_header()->set_error_message("UpdateParams is no longer supported. Use typed device commands or reload configuration.");
|
response->mutable_header()->set_error_message("UpdateParams is no longer supported. Use typed device commands or reload configuration.");
|
||||||
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
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,
|
grpc::Status gRPCSystemServiceImpl::StopAll(grpc::ServerContext* context,
|
||||||
const cmvr::api::StopAllCommand_Request* request, cmvr::api::StopAllCommand_Feedback* response)
|
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;
|
(void)request;
|
||||||
const auto stop_deadline =
|
const auto stop_deadline =
|
||||||
std::chrono::steady_clock::now() + stop_timeout_;
|
std::chrono::steady_clock::now() + stop_timeout_;
|
||||||
@ -1694,12 +2134,24 @@ grpc::Status gRPCSystemServiceImpl::ExecuteActionQueue(
|
|||||||
const cmvr::api::ActionQueueCommand_Request* request,
|
const cmvr::api::ActionQueueCommand_Request* request,
|
||||||
cmvr::api::ActionQueueCommand_Feedback* response)
|
cmvr::api::ActionQueueCommand_Feedback* response)
|
||||||
{
|
{
|
||||||
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
||||||
|
security_gateway_, context,
|
||||||
|
"/cmvr.api.SystemService/ExecuteActionQueue");
|
||||||
if (!request || !response) {
|
if (!request || !response) {
|
||||||
return grpc::Status(
|
return grpc::Status(
|
||||||
grpc::StatusCode::INVALID_ARGUMENT,
|
grpc::StatusCode::INVALID_ARGUMENT,
|
||||||
"ActionQueue request and response are required");
|
"ActionQueue request and response are required");
|
||||||
}
|
}
|
||||||
try {
|
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
|
// The callback is consumed only on this synchronous handler stack. It
|
||||||
// is never retained by the worker-owned action Record, so returning the
|
// is never retained by the worker-owned action Record, so returning the
|
||||||
// RPC cannot leave a dangling ServerContext reference.
|
// RPC cannot leave a dangling ServerContext reference.
|
||||||
@ -1708,7 +2160,8 @@ grpc::Status gRPCSystemServiceImpl::ExecuteActionQueue(
|
|||||||
*response,
|
*response,
|
||||||
[context]() {
|
[context]() {
|
||||||
return context && context->IsCancelled();
|
return context && context->IsCancelled();
|
||||||
});
|
},
|
||||||
|
std::move(actor));
|
||||||
if (wait_result ==
|
if (wait_result ==
|
||||||
ActionQueueExecutor::WaitResult::CanceledBeforeAdmission) {
|
ActionQueueExecutor::WaitResult::CanceledBeforeAdmission) {
|
||||||
return grpc::Status(
|
return grpc::Status(
|
||||||
|
|||||||
@ -408,12 +408,21 @@ bool MediaActivityCoordinator::waitForStopped(
|
|||||||
bool MediaActivityCoordinator::finishStopAll(
|
bool MediaActivityCoordinator::finishStopAll(
|
||||||
const StopAllTicket& ticket,
|
const StopAllTicket& ticket,
|
||||||
const bool all_media_stopped)
|
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);
|
std::lock_guard lock(impl_->mutex);
|
||||||
if (!ticket.valid() || impl_->accepting ||
|
if (!ticket.valid() || impl_->accepting ||
|
||||||
ticket.generation != impl_->generation ||
|
ticket.generation != impl_->generation ||
|
||||||
impl_->stop_all_tickets.erase(ticket.ticket_id) == 0U) {
|
impl_->stop_all_tickets.erase(ticket.ticket_id) == 0U) {
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool caller_succeeded =
|
const bool caller_succeeded =
|
||||||
@ -425,7 +434,20 @@ bool MediaActivityCoordinator::finishStopAll(
|
|||||||
!impl_->hasSessionsBefore(ticket.generation)) {
|
!impl_->hasSessionsBefore(ticket.generation)) {
|
||||||
impl_->accepting = true;
|
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()
|
MediaActivityCoordinator& globalMediaActivityCoordinator()
|
||||||
|
|||||||
@ -436,12 +436,21 @@ bool MotorActivityCoordinator::stopAndWait(
|
|||||||
bool MotorActivityCoordinator::finishStopAll(
|
bool MotorActivityCoordinator::finishStopAll(
|
||||||
const StopAllTicket& ticket,
|
const StopAllTicket& ticket,
|
||||||
const bool all_motors_stopped)
|
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);
|
std::lock_guard lock(impl_->mutex);
|
||||||
if (!ticket.valid() || impl_->accepting ||
|
if (!ticket.valid() || impl_->accepting ||
|
||||||
ticket.generation != impl_->generation ||
|
ticket.generation != impl_->generation ||
|
||||||
impl_->stop_all_tickets.erase(ticket.ticket_id) == 0U) {
|
impl_->stop_all_tickets.erase(ticket.ticket_id) == 0U) {
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool caller_succeeded =
|
const bool caller_succeeded =
|
||||||
@ -454,7 +463,7 @@ bool MotorActivityCoordinator::finishStopAll(
|
|||||||
impl_->accepting = true;
|
impl_->accepting = true;
|
||||||
impl_->round_targets.clear();
|
impl_->round_targets.clear();
|
||||||
}
|
}
|
||||||
return caller_succeeded;
|
return {true, caller_succeeded, impl_->accepting};
|
||||||
}
|
}
|
||||||
|
|
||||||
void MotorActivityCoordinator::notifyStateChanged() noexcept
|
void MotorActivityCoordinator::notifyStateChanged() noexcept
|
||||||
|
|||||||
@ -166,6 +166,35 @@ TEST_F(CameraPtzActivityRegistryTest,
|
|||||||
CameraPtzActivityRegistry::DispatchResult::Success);
|
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,
|
TEST_F(CameraPtzActivityRegistryTest,
|
||||||
StopForDeviceIsSelectiveAndActiveIdsReflectFailures)
|
StopForDeviceIsSelectiveAndActiveIdsReflectFailures)
|
||||||
{
|
{
|
||||||
@ -260,5 +289,61 @@ TEST_F(CameraPtzActivityRegistryTest,
|
|||||||
EXPECT_TRUE(globalStopAllAdmissionGate().finishStopAll(ticket, true));
|
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
|
||||||
} // namespace cmvr::service
|
} // namespace cmvr::service
|
||||||
|
|||||||
@ -588,6 +588,25 @@ protected:
|
|||||||
response.header().error_message()};
|
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)
|
MoveOutcome moveL(const std::string& device_id)
|
||||||
{
|
{
|
||||||
api::MoveL_Request request;
|
api::MoveL_Request request;
|
||||||
@ -833,6 +852,49 @@ TEST_F(GrpcArmServiceTest, MoveBindsLeaseRevocationCancellation)
|
|||||||
EXPECT_FALSE(aubo_arm_->lastMotionCancellationRequested());
|
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,
|
TEST_F(GrpcArmServiceTest,
|
||||||
StopAllCancelsInFlightTorqueOnWithoutReportingSuccess)
|
StopAllCancelsInFlightTorqueOnWithoutReportingSuccess)
|
||||||
{
|
{
|
||||||
@ -1109,6 +1171,9 @@ TEST_F(GrpcArmServiceTest, StopMotionFailureRetainsSafetyBarrier)
|
|||||||
|
|
||||||
EXPECT_EQ(stop_status.error_code(), grpc::StatusCode::INTERNAL);
|
EXPECT_EQ(stop_status.error_code(), grpc::StatusCode::INTERNAL);
|
||||||
EXPECT_FALSE(stop_response.success());
|
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_FALSE(authority.validate(action_lease.token));
|
||||||
EXPECT_TRUE(authority.isLeased("aubo_arm"));
|
EXPECT_TRUE(authority.isLeased("aubo_arm"));
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
@ -1143,8 +1208,12 @@ TEST_F(GrpcArmServiceTest, StopMotionExceptionRetainsSafetyBarrier)
|
|||||||
const auto stop_status = stopMotion("aubo_arm", stop_response);
|
const auto stop_status = stopMotion("aubo_arm", stop_response);
|
||||||
const auto rejected_move = moveL("aubo_arm");
|
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_FALSE(stop_response.success());
|
||||||
|
EXPECT_EQ(
|
||||||
|
stop_response.reason_code(),
|
||||||
|
api::COMMAND_REASON_CODE_STOP_UNCONFIRMED);
|
||||||
EXPECT_FALSE(authority.validate(action_lease.token));
|
EXPECT_FALSE(authority.validate(action_lease.token));
|
||||||
EXPECT_TRUE(authority.isLeased("aubo_arm"));
|
EXPECT_TRUE(authority.isLeased("aubo_arm"));
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
|
|||||||
@ -17,6 +17,8 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <unistd.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"
|
#include "service/stop_all/include/stop_all_admission_gate.h"
|
||||||
|
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
@ -253,11 +255,93 @@ private:
|
|||||||
mutable std::set<std::thread::id> backend_threads_;
|
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 {
|
class TeleopServerHarness final {
|
||||||
public:
|
public:
|
||||||
explicit TeleopServerHarness(
|
explicit TeleopServerHarness(
|
||||||
std::shared_ptr<ArmTeleopBackend> backend)
|
std::shared_ptr<ArmTeleopBackend> backend,
|
||||||
: service_(std::move(backend))
|
safety::SafetyCoordinator* safety_coordinator = nullptr)
|
||||||
|
: service_(
|
||||||
|
std::move(backend), nullptr, nullptr,
|
||||||
|
safety_coordinator)
|
||||||
{
|
{
|
||||||
socket_path_ =
|
socket_path_ =
|
||||||
"/tmp/cmvr_arm_teleop_service_test_" +
|
"/tmp/cmvr_arm_teleop_service_test_" +
|
||||||
@ -775,5 +859,62 @@ TEST(ArmTeleopServiceTest,
|
|||||||
admission.clearForTesting();
|
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
|
||||||
} // namespace cmvr::service
|
} // 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());
|
EXPECT_TRUE(blocked_response.status().emergency_stopped());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(MotorServiceTest, ProfileBackendExceptionReturnsInternalAndQuickStops)
|
TEST_F(MotorServiceTest,
|
||||||
|
ProfileBackendExceptionReportsUnknownOutcomeAndQuickStops)
|
||||||
{
|
{
|
||||||
protocol_->throw_profile_position_ = true;
|
protocol_->throw_profile_position_ = true;
|
||||||
|
|
||||||
@ -682,12 +683,22 @@ TEST_F(MotorServiceTest, ProfileBackendExceptionReturnsInternalAndQuickStops)
|
|||||||
api::MotorCommandResponse response;
|
api::MotorCommandResponse response;
|
||||||
const auto status = service_->profilePosition(&context, &request, &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_FALSE(response.header().success());
|
||||||
|
EXPECT_EQ(
|
||||||
|
response.header().reason_code(),
|
||||||
|
api::COMMAND_REASON_CODE_OUTCOME_UNKNOWN);
|
||||||
EXPECT_NE(response.header().error_message().find(
|
EXPECT_NE(response.header().error_message().find(
|
||||||
"injected profile position exception"),
|
"injected profile position exception"),
|
||||||
std::string::npos);
|
std::string::npos);
|
||||||
EXPECT_GE(protocol_->quick_stop_count_.load(), 1);
|
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)
|
TEST_F(MotorServiceTest, ExceptionCleanupKeepsMotorReservedUntilQuickStopFinishes)
|
||||||
@ -732,7 +743,10 @@ TEST_F(MotorServiceTest, ExceptionCleanupKeepsMotorReservedUntilQuickStopFinishe
|
|||||||
&competing_context, &competing_request, &competing_response);
|
&competing_context, &competing_request, &competing_response);
|
||||||
|
|
||||||
failing.join();
|
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(),
|
EXPECT_EQ(competing_status.error_code(),
|
||||||
grpc::StatusCode::RESOURCE_EXHAUSTED);
|
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_operational_activity_registry.h"
|
||||||
#include "service/grpc/include/camera_ptz_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_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/media_activity_coordinator.h"
|
||||||
#include "service/grpc/include/motor_activity_coordinator.h"
|
#include "service/grpc/include/motor_activity_coordinator.h"
|
||||||
#include "service/stop_all/include/stop_all_admission_gate.h"
|
#include "service/stop_all/include/stop_all_admission_gate.h"
|
||||||
@ -42,6 +44,46 @@
|
|||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
namespace {
|
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 {
|
class SnapshotDevice final : public device::AbstractDevice {
|
||||||
public:
|
public:
|
||||||
SnapshotDevice(std::string id,
|
SnapshotDevice(std::string id,
|
||||||
@ -1288,6 +1330,18 @@ const api::SystemDeviceInfo* findDevice(
|
|||||||
return nullptr;
|
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 {
|
class GrpcSystemServiceTest : public ::testing::Test {
|
||||||
protected:
|
protected:
|
||||||
void SetUp() override
|
void SetUp() override
|
||||||
@ -1299,6 +1353,7 @@ protected:
|
|||||||
globalStopAllAdmissionGate().clearForTesting();
|
globalStopAllAdmissionGate().clearForTesting();
|
||||||
globalCameraOperationalActivityRegistry().clearForTesting();
|
globalCameraOperationalActivityRegistry().clearForTesting();
|
||||||
globalCameraPtzActivityRegistry().clearForTesting();
|
globalCameraPtzActivityRegistry().clearForTesting();
|
||||||
|
globalMediaActivityCoordinator().clearForTesting();
|
||||||
globalMotorActivityCoordinator().clearForTesting();
|
globalMotorActivityCoordinator().clearForTesting();
|
||||||
device::DeviceManager::destroyInstance();
|
device::DeviceManager::destroyInstance();
|
||||||
}
|
}
|
||||||
@ -1320,6 +1375,7 @@ protected:
|
|||||||
globalStopAllAdmissionGate().clearForTesting();
|
globalStopAllAdmissionGate().clearForTesting();
|
||||||
globalCameraOperationalActivityRegistry().clearForTesting();
|
globalCameraOperationalActivityRegistry().clearForTesting();
|
||||||
globalCameraPtzActivityRegistry().clearForTesting();
|
globalCameraPtzActivityRegistry().clearForTesting();
|
||||||
|
globalMediaActivityCoordinator().clearForTesting();
|
||||||
globalMotorActivityCoordinator().clearForTesting();
|
globalMotorActivityCoordinator().clearForTesting();
|
||||||
(void)media::globalMediaSourceHub().stopAllSources();
|
(void)media::globalMediaSourceHub().stopAllSources();
|
||||||
}
|
}
|
||||||
@ -1510,6 +1566,122 @@ TEST_F(GrpcSystemServiceTest, EmptyListReturnsMetadataAndTimestamps)
|
|||||||
EXPECT_LE(response.sampled_at_unix_ms(), after_ms);
|
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)
|
TEST_F(GrpcSystemServiceTest, MapsEveryKnownDeviceKind)
|
||||||
{
|
{
|
||||||
struct ExpectedMapping {
|
struct ExpectedMapping {
|
||||||
@ -1605,6 +1777,77 @@ TEST_F(GrpcSystemServiceTest, ActionQueueExecutesFourMoveLStepsSerially)
|
|||||||
"arm:L:1", "arm:L:2", "arm:L:3", "arm:L:4"}));
|
"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)
|
TEST_F(GrpcSystemServiceTest, ActionQueuePreservesArmDelayAgvOrder)
|
||||||
{
|
{
|
||||||
initializeActionDevices();
|
initializeActionDevices();
|
||||||
@ -2432,14 +2675,14 @@ TEST_F(GrpcSystemServiceTest,
|
|||||||
auto trace = std::make_shared<ActionTrace>();
|
auto trace = std::make_shared<ActionTrace>();
|
||||||
auto arm = std::make_shared<ActionTestArm>(
|
auto arm = std::make_shared<ActionTestArm>(
|
||||||
"health-blocked-arm", trace);
|
"health-blocked-arm", trace);
|
||||||
manager.registerDevice(arm);
|
|
||||||
service_ = std::make_unique<gRPCSystemServiceImpl>();
|
|
||||||
|
|
||||||
arm->blockHealthSnapshot();
|
arm->blockHealthSnapshot();
|
||||||
auto health_snapshot = std::async(
|
auto registration = std::async(
|
||||||
std::launch::async, [&manager] { return manager.snapshot(); });
|
std::launch::async, [&manager, arm] {
|
||||||
|
manager.registerDevice(arm);
|
||||||
|
});
|
||||||
const bool health_call_blocked = arm->waitForHealthSnapshot(
|
const bool health_call_blocked = arm->waitForHealthSnapshot(
|
||||||
std::chrono::milliseconds(500));
|
std::chrono::milliseconds(500));
|
||||||
|
service_ = std::make_unique<gRPCSystemServiceImpl>();
|
||||||
|
|
||||||
api::StopAllCommand_Request request;
|
api::StopAllCommand_Request request;
|
||||||
auto stop_all = std::async(std::launch::async, [this, &request] {
|
auto stop_all = std::async(std::launch::async, [this, &request] {
|
||||||
@ -2456,9 +2699,9 @@ TEST_F(GrpcSystemServiceTest,
|
|||||||
arm->releaseHealthSnapshot();
|
arm->releaseHealthSnapshot();
|
||||||
|
|
||||||
ASSERT_EQ(
|
ASSERT_EQ(
|
||||||
health_snapshot.wait_for(std::chrono::seconds(1)),
|
registration.wait_for(std::chrono::seconds(1)),
|
||||||
std::future_status::ready);
|
std::future_status::ready);
|
||||||
(void)health_snapshot.get();
|
registration.get();
|
||||||
ASSERT_EQ(
|
ASSERT_EQ(
|
||||||
stop_all.wait_for(std::chrono::seconds(1)),
|
stop_all.wait_for(std::chrono::seconds(1)),
|
||||||
std::future_status::ready);
|
std::future_status::ready);
|
||||||
@ -2634,7 +2877,11 @@ TEST_F(GrpcSystemServiceTest,
|
|||||||
EXPECT_TRUE(second_status.ok()) << second_status.error_message();
|
EXPECT_TRUE(second_status.ok()) << second_status.error_message();
|
||||||
EXPECT_TRUE(second_response.header().success())
|
EXPECT_TRUE(second_response.header().success())
|
||||||
<< second_response.header().error_message();
|
<< 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,
|
TEST_F(GrpcSystemServiceTest,
|
||||||
@ -2694,9 +2941,13 @@ TEST_F(GrpcSystemServiceTest,
|
|||||||
|
|
||||||
ASSERT_TRUE(status.ok()) << status.error_message();
|
ASSERT_TRUE(status.ok()) << status.error_message();
|
||||||
EXPECT_FALSE(response.header().success());
|
EXPECT_FALSE(response.header().success());
|
||||||
EXPECT_NE(
|
const auto* agv_target = findSafetyTarget(response, action_agv_->id());
|
||||||
response.header().error_message().find("remain quarantined"),
|
ASSERT_NE(agv_target, nullptr);
|
||||||
std::string::npos);
|
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 =
|
const auto lease =
|
||||||
control::ControlAuthorityManager::instance().tryAcquire(
|
control::ControlAuthorityManager::instance().tryAcquire(
|
||||||
action_agv_->id(),
|
action_agv_->id(),
|
||||||
@ -2734,11 +2985,11 @@ TEST_F(GrpcSystemServiceTest,
|
|||||||
<< response.header().error_message();
|
<< response.header().error_message();
|
||||||
EXPECT_EQ(camera->stopRecordingCalls(), 1);
|
EXPECT_EQ(camera->stopRecordingCalls(), 1);
|
||||||
EXPECT_FALSE(camera->isRecording());
|
EXPECT_FALSE(camera->isRecording());
|
||||||
EXPECT_EQ(microphone->stopRecordingCalls(), 1);
|
EXPECT_EQ(microphone->stopRecordingCalls(), 2);
|
||||||
EXPECT_FALSE(microphone->isRecording());
|
EXPECT_FALSE(microphone->isRecording());
|
||||||
EXPECT_EQ(speaker->stopPlaybackCalls(), 2);
|
EXPECT_EQ(speaker->stopPlaybackCalls(), 2);
|
||||||
EXPECT_EQ(camera->lifecycleStopCalls(), 0);
|
EXPECT_EQ(camera->lifecycleStopCalls(), 0);
|
||||||
EXPECT_EQ(camera->operationalStopCalls(), 1);
|
EXPECT_GE(camera->operationalStopCalls(), 2);
|
||||||
EXPECT_FALSE(camera->operationalActive());
|
EXPECT_FALSE(camera->operationalActive());
|
||||||
EXPECT_EQ(microphone->lifecycleStopCalls(), 0);
|
EXPECT_EQ(microphone->lifecycleStopCalls(), 0);
|
||||||
EXPECT_EQ(speaker->lifecycleStopCalls(), 0);
|
EXPECT_EQ(speaker->lifecycleStopCalls(), 0);
|
||||||
@ -2765,7 +3016,7 @@ TEST_F(GrpcSystemServiceTest,
|
|||||||
ASSERT_TRUE(status.ok()) << status.error_message();
|
ASSERT_TRUE(status.ok()) << status.error_message();
|
||||||
ASSERT_TRUE(response.header().success())
|
ASSERT_TRUE(response.header().success())
|
||||||
<< response.header().error_message();
|
<< response.header().error_message();
|
||||||
EXPECT_EQ(camera->operationalStopCalls(), 1);
|
EXPECT_EQ(camera->operationalStopCalls(), 2);
|
||||||
EXPECT_FALSE(camera->operationalActive());
|
EXPECT_FALSE(camera->operationalActive());
|
||||||
EXPECT_EQ(camera->lifecycleStopCalls(), 0);
|
EXPECT_EQ(camera->lifecycleStopCalls(), 0);
|
||||||
}
|
}
|
||||||
@ -2935,10 +3186,12 @@ TEST_F(GrpcSystemServiceTest,
|
|||||||
|
|
||||||
ASSERT_TRUE(stop_status.ok()) << stop_status.error_message();
|
ASSERT_TRUE(stop_status.ok()) << stop_status.error_message();
|
||||||
EXPECT_FALSE(stop_response.header().success());
|
EXPECT_FALSE(stop_response.header().success());
|
||||||
EXPECT_NE(
|
const auto* camera_target = findSafetyTarget(
|
||||||
stop_response.header().error_message().find(
|
stop_response, camera->id());
|
||||||
"could not confirm that every device stopped"),
|
ASSERT_NE(camera_target, nullptr);
|
||||||
std::string::npos);
|
EXPECT_EQ(
|
||||||
|
camera_target->reason_code(),
|
||||||
|
api::COMMAND_REASON_CODE_STOP_UNCONFIRMED);
|
||||||
EXPECT_EQ(camera->stopRecordingCalls(), 2);
|
EXPECT_EQ(camera->stopRecordingCalls(), 2);
|
||||||
EXPECT_TRUE(camera->isRecording());
|
EXPECT_TRUE(camera->isRecording());
|
||||||
EXPECT_EQ(microphone->stopRecordingCalls(), 2);
|
EXPECT_EQ(microphone->stopRecordingCalls(), 2);
|
||||||
@ -3025,7 +3278,7 @@ TEST_F(GrpcSystemServiceTest,
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(GrpcSystemServiceTest,
|
TEST_F(GrpcSystemServiceTest,
|
||||||
StopAllSharesTimedOutArmStopAndDetailAcrossServiceInstances)
|
RetriedStopAllReusesTimedOutArmBarrierAndCanRecover)
|
||||||
{
|
{
|
||||||
config::DeviceManagerConfig config;
|
config::DeviceManagerConfig config;
|
||||||
auto& manager = device::DeviceManager::getInstance(config);
|
auto& manager = device::DeviceManager::getInstance(config);
|
||||||
@ -3059,153 +3312,47 @@ TEST_F(GrpcSystemServiceTest,
|
|||||||
const bool final_stop_started = initial_stop_completed &&
|
const bool final_stop_started = initial_stop_completed &&
|
||||||
arm->waitForBlockedStopMotion(std::chrono::milliseconds(500));
|
arm->waitForBlockedStopMotion(std::chrono::milliseconds(500));
|
||||||
|
|
||||||
std::optional<std::pair<grpc::Status, api::StopAllCommand_Feedback>>
|
ASSERT_EQ(completion, std::future_status::ready);
|
||||||
result;
|
const auto [status, response] = stop_all.get();
|
||||||
std::optional<std::pair<grpc::Status, api::StopAllCommand_Feedback>>
|
const auto stop_calls_before_retry = arm->stopMotionCalls();
|
||||||
second_result;
|
const auto control_while_failed_closed = authority.tryAcquire(
|
||||||
std::unique_ptr<gRPCSystemServiceImpl> second_service;
|
arm->id(), "control-after-timed-out-stop-all",
|
||||||
std::promise<void> last_destroy_started;
|
std::chrono::seconds(30));
|
||||||
auto last_destroy_started_signal = last_destroy_started.get_future();
|
|
||||||
std::promise<void> replacement_construct_started;
|
// Let the deadline-overrunning final typed stop finish before retrying the
|
||||||
auto replacement_construct_started_signal =
|
// retained barrier. The old control owner is then allowed to drain.
|
||||||
replacement_construct_started.get_future();
|
arm->releaseBlockedStopMotion();
|
||||||
std::future<void> first_service_destroy;
|
authority.release(old_handler.token);
|
||||||
std::future<void> last_service_destroy;
|
|
||||||
std::future<std::unique_ptr<gRPCSystemServiceImpl>>
|
auto second_service = std::make_unique<gRPCSystemServiceImpl>(
|
||||||
replacement_service_construct;
|
std::chrono::seconds(1));
|
||||||
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));
|
|
||||||
api::StopAllCommand_Feedback second_response;
|
api::StopAllCommand_Feedback second_response;
|
||||||
grpc::ServerContext second_context;
|
grpc::ServerContext second_context;
|
||||||
const auto second_status = second_service->StopAll(
|
const auto second_status = second_service->StopAll(
|
||||||
&second_context, &request, &second_response);
|
&second_context, &request, &second_response);
|
||||||
second_result.emplace(second_status, std::move(second_response));
|
const auto recovered_control = authority.tryAcquire(
|
||||||
stop_calls_after_second = arm->stopMotionCalls();
|
arm->id(), "control-after-retried-stop-all",
|
||||||
control_while_failed_closed = authority.tryAcquire(
|
|
||||||
arm->id(), "control-after-timed-out-stop-all",
|
|
||||||
std::chrono::seconds(30));
|
std::chrono::seconds(30));
|
||||||
first_service_destroy = std::async(
|
if (recovered_control.acquired) {
|
||||||
std::launch::async,
|
authority.release(recovered_control.token);
|
||||||
[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));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(initial_stop_completed);
|
||||||
EXPECT_TRUE(final_stop_started);
|
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();
|
ASSERT_TRUE(status.ok()) << status.error_message();
|
||||||
EXPECT_FALSE(response.header().success());
|
EXPECT_FALSE(response.header().success());
|
||||||
EXPECT_NE(
|
const auto* failed_arm = findSafetyTarget(response, arm->id());
|
||||||
response.header().error_message().find(
|
ASSERT_NE(failed_arm, nullptr);
|
||||||
"timed out waiting for the preempted RobotArm control handler "
|
|
||||||
"to exit"),
|
|
||||||
std::string::npos);
|
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
response.header().error_message().find(
|
failed_arm->reason_code(),
|
||||||
"stop operation did not complete before the deadline"),
|
api::COMMAND_REASON_CODE_PARTICIPANT_TIMEOUT);
|
||||||
std::string::npos);
|
|
||||||
ASSERT_TRUE(second_status.ok()) << second_status.error_message();
|
ASSERT_TRUE(second_status.ok()) << second_status.error_message();
|
||||||
EXPECT_FALSE(second_response.header().success());
|
EXPECT_TRUE(second_response.header().success())
|
||||||
EXPECT_NE(
|
<< second_response.header().error_message();
|
||||||
second_response.header().error_message().find(
|
EXPECT_EQ(stop_calls_before_retry, 2);
|
||||||
"timed out waiting for the preempted RobotArm control handler "
|
EXPECT_GE(arm->stopMotionCalls(), 4);
|
||||||
"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_FALSE(control_while_failed_closed.acquired);
|
EXPECT_FALSE(control_while_failed_closed.acquired);
|
||||||
ASSERT_TRUE(first_destroy_before_release.has_value());
|
EXPECT_TRUE(recovered_control.acquired) << recovered_control.detail;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(GrpcSystemServiceTest,
|
TEST_F(GrpcSystemServiceTest,
|
||||||
|
|||||||
@ -257,6 +257,23 @@ int main()
|
|||||||
CHECK_TRUE(coordinator.finishStopAll(concurrent_ticket_b, true));
|
CHECK_TRUE(coordinator.finishStopAll(concurrent_ticket_b, true));
|
||||||
CHECK_TRUE(coordinator.beginSession());
|
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";
|
std::cout << "media_activity_coordinator_test: PASS\n";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -261,6 +261,23 @@ int main()
|
|||||||
CHECK_TRUE(coordinator.finishStopAll(recovery_ticket, true));
|
CHECK_TRUE(coordinator.finishStopAll(recovery_ticket, true));
|
||||||
CHECK_TRUE(coordinator.lockAdmission().accepting());
|
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";
|
std::cout << "motor_activity_coordinator_test: PASS\n";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@
|
|||||||
|
|
||||||
#include "cmvr/quic_edge/v1/quic_edge.pb.h"
|
#include "cmvr/quic_edge/v1/quic_edge.pb.h"
|
||||||
#include "common/base/logging/logger.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 cmvr::quic_edge {
|
||||||
namespace {
|
namespace {
|
||||||
@ -1448,6 +1450,18 @@ void QuicEdgeService::refreshMediaTracks(
|
|||||||
recordMediaError(source_error);
|
recordMediaError(source_error);
|
||||||
continue;
|
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.subscription = media_hub_->subscribe(
|
||||||
track.source_track_id,
|
track.source_track_id,
|
||||||
media::MediaSourceHub::StartPosition::LATEST_AVAILABLE,
|
media::MediaSourceHub::StartPosition::LATEST_AVAILABLE,
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
namespace cmvr::service {
|
namespace cmvr::service {
|
||||||
class ArmTeleopBackend;
|
class ArmTeleopBackend;
|
||||||
|
class GrpcSecurityGateway;
|
||||||
|
class RecoveryAuditSink;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace cmvr::task {
|
namespace cmvr::task {
|
||||||
@ -66,6 +68,8 @@ private:
|
|||||||
std::unique_ptr<grpc::Service> arm_teleop_service_;
|
std::unique_ptr<grpc::Service> arm_teleop_service_;
|
||||||
std::shared_ptr<cmvr::service::ArmTeleopBackend>
|
std::shared_ptr<cmvr::service::ArmTeleopBackend>
|
||||||
arm_teleop_backend_;
|
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> motor_service_;
|
||||||
std::unique_ptr<grpc::Service> agv_service_;
|
std::unique_ptr<grpc::Service> agv_service_;
|
||||||
std::unique_ptr<grpc::Service> hlc_service_;
|
std::unique_ptr<grpc::Service> hlc_service_;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#include "task/grpc_server_task/include/grpc_server_task.h"
|
#include "task/grpc_server_task/include/grpc_server_task.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
|
|
||||||
#include <grpcpp/ext/proto_server_reflection_plugin.h>
|
#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_service.h"
|
||||||
#include "service/grpc/include/grpc_arm_teleop_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_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_camera_service.h"
|
||||||
#include "service/grpc/include/grpc_dexhand_service.h"
|
#include "service/grpc/include/grpc_dexhand_service.h"
|
||||||
#include "service/grpc/include/grpc_error_logging_interceptor.h"
|
#include "service/grpc/include/grpc_error_logging_interceptor.h"
|
||||||
@ -29,6 +32,13 @@ namespace cmvr::task {
|
|||||||
|
|
||||||
namespace {
|
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)
|
std::shared_ptr<Task> createGrpcServerTask(const config::TaskConfigEntry& entry)
|
||||||
{
|
{
|
||||||
if (entry.id().empty()) {
|
if (entry.id().empty()) {
|
||||||
@ -104,20 +114,35 @@ bool GrpcServerTask::start()
|
|||||||
camera_service_ = std::make_unique<service::gRPCCameraServiceImpl>(
|
camera_service_ = std::make_unique<service::gRPCCameraServiceImpl>(
|
||||||
service::makeCameraStreamLowLatencyConfig(
|
service::makeCameraStreamLowLatencyConfig(
|
||||||
cfg_.camera_stream_max_pending_frames(),
|
cfg_.camera_stream_max_pending_frames(),
|
||||||
cfg_.camera_stream_max_frame_age_ms()));
|
cfg_.camera_stream_max_frame_age_ms()),
|
||||||
system_service_ = std::make_unique<service::gRPCSystemServiceImpl>();
|
security_gateway_);
|
||||||
speaker_service_ = std::make_unique<service::gRPCSpeakerServiceImpl>();
|
system_service_ = std::make_unique<service::gRPCSystemServiceImpl>(
|
||||||
microphone_service_ = std::make_unique<service::gRPCMicroPhoneServiceImpl>();
|
std::chrono::seconds(15),
|
||||||
dexhand_service_ = std::make_unique<service::gRPCDexHandServiceImpl>();
|
security_gateway_,
|
||||||
biohand_service_ = std::make_unique<service::gRPCMBioHeadServiceImpl>();
|
recovery_audit_sink_);
|
||||||
arm_service_ = std::make_unique<service::gRPCArmServiceImpl>();
|
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_ =
|
arm_teleop_service_ =
|
||||||
std::make_unique<service::ArmTeleopServiceImpl>(
|
std::make_unique<service::ArmTeleopServiceImpl>(
|
||||||
arm_teleop_backend_ ? arm_teleop_backend_
|
arm_teleop_backend_ ? arm_teleop_backend_
|
||||||
: service::makeDisabledArmTeleopBackend());
|
: service::makeDisabledArmTeleopBackend(),
|
||||||
motor_service_ = std::make_unique<service::gRPCMotorServiceImpl>();
|
nullptr,
|
||||||
agv_service_ = std::make_unique<service::gRPCAgvServiceImpl>();
|
security_gateway_,
|
||||||
hlc_service_ = std::make_unique<service::gRPCHlcServiceImpl>();
|
&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;
|
grpc::ServerBuilder builder;
|
||||||
builder.AddListeningPort(local_address, grpc::InsecureServerCredentials());
|
builder.AddListeningPort(local_address, grpc::InsecureServerCredentials());
|
||||||
@ -151,7 +176,15 @@ bool GrpcServerTask::start()
|
|||||||
|
|
||||||
address_ = local_address;
|
address_ = local_address;
|
||||||
state_ = TaskState::RUNNING;
|
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);
|
wait_thread_ = std::thread(&GrpcServerTask::waitLoop, this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -170,6 +203,54 @@ bool GrpcServerTask::init()
|
|||||||
return false;
|
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();
|
arm_teleop_backend_ = service::makeDisabledArmTeleopBackend();
|
||||||
if (cfg_.has_arm_teleop_backend() &&
|
if (cfg_.has_arm_teleop_backend() &&
|
||||||
cfg_.arm_teleop_backend().enable()) {
|
cfg_.arm_teleop_backend().enable()) {
|
||||||
|
|||||||
@ -61,11 +61,22 @@ public:
|
|||||||
RETRACTING, // 正在回退离开屏幕。
|
RETRACTING, // 正在回退离开屏幕。
|
||||||
DONE, // 流程成功完成。
|
DONE, // 流程成功完成。
|
||||||
STOPPED, // 被外部 stop() 主动停止。
|
STOPPED, // 被外部 stop() 主动停止。
|
||||||
|
SAFETY_ADMISSION_REVOKED, // 统一安全会话在硬件下发前失效。
|
||||||
ROBOT_STATE_FAILED, // 读取机器人状态失败。
|
ROBOT_STATE_FAILED, // 读取机器人状态失败。
|
||||||
ROBOT_COMMAND_FAILED, // 向机器人下发控制命令失败。
|
ROBOT_COMMAND_FAILED, // 向机器人下发控制命令失败。
|
||||||
TASK_BUSY // 已有触屏流程正在运行,新的 touch 请求被拒绝。
|
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);
|
explicit TouchScreenTask(const cmvr::config::TouchScreenTaskConfig& cfg);
|
||||||
~TouchScreenTask() = default;
|
~TouchScreenTask() = default;
|
||||||
|
|
||||||
@ -79,7 +90,8 @@ public:
|
|||||||
bool touchIfCurrent(
|
bool touchIfCurrent(
|
||||||
int u,
|
int u,
|
||||||
int v,
|
int v,
|
||||||
const std::function<bool()>& still_admitted);
|
const std::function<bool()>& still_admitted,
|
||||||
|
SafetyHooks safety_hooks = {});
|
||||||
bool touch(int u, int v);
|
bool touch(int u, int v);
|
||||||
bool startFromPixel(int u, int v);
|
bool startFromPixel(int u, int v);
|
||||||
bool step(double dt) override;
|
bool step(double dt) override;
|
||||||
@ -110,6 +122,7 @@ public:
|
|||||||
int lastTouchNonzeroCount() const;
|
int lastTouchNonzeroCount() const;
|
||||||
int lastActiveTagId() const;
|
int lastActiveTagId() const;
|
||||||
Eigen::Vector3d lastAlignErrorCamera() const;
|
Eigen::Vector3d lastAlignErrorCamera() const;
|
||||||
|
std::string controlDeviceId() const;
|
||||||
|
|
||||||
const std::shared_ptr<perception::AprilTagPerception>& perception() const { return perception_; }
|
const std::shared_ptr<perception::AprilTagPerception>& perception() const { return perception_; }
|
||||||
const perception::TagRelativeTarget3D& tracker() const { return tracker_; }
|
const perception::TagRelativeTarget3D& tracker() const { return tracker_; }
|
||||||
@ -129,6 +142,12 @@ private:
|
|||||||
bool activityControlCurrent() const;
|
bool activityControlCurrent() const;
|
||||||
std::function<bool()> activityCancellationRequested() const;
|
std::function<bool()> activityCancellationRequested() const;
|
||||||
control::ControlDispatchGuard tryBeginActivityDispatch() 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 releaseActivityControlUnlocked() noexcept;
|
||||||
void finishActivityUnlocked(Phase phase, Status status) noexcept;
|
void finishActivityUnlocked(Phase phase, Status status) noexcept;
|
||||||
bool startFromPixelUnlocked(int u, int v);
|
bool startFromPixelUnlocked(int u, int v);
|
||||||
@ -183,6 +202,7 @@ private:
|
|||||||
std::atomic<bool> activity_active_{false};
|
std::atomic<bool> activity_active_{false};
|
||||||
std::atomic<std::uint64_t> activity_generation_{1U};
|
std::atomic<std::uint64_t> activity_generation_{1U};
|
||||||
control::ControlLeaseToken activity_control_token_;
|
control::ControlLeaseToken activity_control_token_;
|
||||||
|
SafetyHooks activity_safety_hooks_;
|
||||||
bool target_locked_{false};
|
bool target_locked_{false};
|
||||||
bool ibvs_target_initialized_{false};
|
bool ibvs_target_initialized_{false};
|
||||||
bool touch_command_started_{false};
|
bool touch_command_started_{false};
|
||||||
|
|||||||
@ -52,6 +52,12 @@ public:
|
|||||||
task.phase_ = TouchScreenTask::Phase::IDLE;
|
task.phase_ = TouchScreenTask::Phase::IDLE;
|
||||||
task.last_status_ = TouchScreenTask::Status::NOT_INITIALIZED;
|
task.last_status_ = TouchScreenTask::Status::NOT_INITIALIZED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool sendJointVelocity(TouchScreenTask& task)
|
||||||
|
{
|
||||||
|
std::lock_guard lock(task.mutex_);
|
||||||
|
return task.sendJointVelocity({});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace cmvr::task
|
} // namespace cmvr::task
|
||||||
@ -106,6 +112,7 @@ public:
|
|||||||
cmvr::device::Result speedJ(
|
cmvr::device::Result speedJ(
|
||||||
const cmvr::device::JointVelocityCommand&, double, double) override
|
const cmvr::device::JointVelocityCommand&, double, double) override
|
||||||
{
|
{
|
||||||
|
++speed_j_calls_;
|
||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
cmvr::device::Result stopJ(double) override { return success(); }
|
cmvr::device::Result stopJ(double) override { return success(); }
|
||||||
@ -128,7 +135,11 @@ public:
|
|||||||
{
|
{
|
||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
cmvr::device::Result stopMotion() override { return success(); }
|
cmvr::device::Result stopMotion() override
|
||||||
|
{
|
||||||
|
++stop_motion_calls_;
|
||||||
|
return success();
|
||||||
|
}
|
||||||
cmvr::device::Result startServoMode(
|
cmvr::device::Result startServoMode(
|
||||||
const cmvr::device::ServoOptions&) override
|
const cmvr::device::ServoOptions&) override
|
||||||
{
|
{
|
||||||
@ -199,11 +210,20 @@ public:
|
|||||||
}
|
}
|
||||||
bool busy() const override { return false; }
|
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:
|
private:
|
||||||
static cmvr::device::Result success()
|
static cmvr::device::Result success()
|
||||||
{
|
{
|
||||||
return 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 {
|
class AdmissionCamera final : public cmvr::device::AbstractCamera {
|
||||||
@ -356,6 +376,82 @@ TEST_F(TouchScreenAdmissionTest,
|
|||||||
EXPECT_TRUE(task.stopActivity());
|
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,
|
TEST_F(TouchScreenAdmissionTest,
|
||||||
StopAllStopsCameraActivityAndNextTouchRestartsIt)
|
StopAllStopsCameraActivityAndNextTouchRestartsIt)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -467,7 +467,8 @@ bool TouchScreenTask::touch(const int u, const int v) {
|
|||||||
bool TouchScreenTask::touchIfCurrent(
|
bool TouchScreenTask::touchIfCurrent(
|
||||||
const int u,
|
const int u,
|
||||||
const int v,
|
const int v,
|
||||||
const std::function<bool()>& still_admitted)
|
const std::function<bool()>& still_admitted,
|
||||||
|
SafetyHooks safety_hooks)
|
||||||
{
|
{
|
||||||
auto& admission_gate = service::globalStopAllAdmissionGate();
|
auto& admission_gate = service::globalStopAllAdmissionGate();
|
||||||
std::uint64_t admission_generation = 0U;
|
std::uint64_t admission_generation = 0U;
|
||||||
@ -534,9 +535,21 @@ bool TouchScreenTask::touchIfCurrent(
|
|||||||
return false;
|
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);
|
const bool started = startFromPixelUnlocked(u, v);
|
||||||
if (!started) {
|
if (!started) {
|
||||||
activity_active_.store(false, std::memory_order_release);
|
activity_active_.store(false, std::memory_order_release);
|
||||||
|
clearActivitySafetyHooksUnlocked();
|
||||||
releaseActivityControlUnlocked();
|
releaseActivityControlUnlocked();
|
||||||
rollback_camera();
|
rollback_camera();
|
||||||
return false;
|
return false;
|
||||||
@ -556,6 +569,7 @@ bool TouchScreenTask::touchIfCurrent(
|
|||||||
}
|
}
|
||||||
activity_active_.store(false, std::memory_order_release);
|
activity_active_.store(false, std::memory_order_release);
|
||||||
resetActivityUnlocked();
|
resetActivityUnlocked();
|
||||||
|
clearActivitySafetyHooksUnlocked();
|
||||||
releaseActivityControlUnlocked();
|
releaseActivityControlUnlocked();
|
||||||
rollback_camera();
|
rollback_camera();
|
||||||
return false;
|
return false;
|
||||||
@ -643,6 +657,61 @@ TouchScreenTask::tryBeginActivityDispatch() const
|
|||||||
token);
|
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
|
void TouchScreenTask::releaseActivityControlUnlocked() noexcept
|
||||||
{
|
{
|
||||||
control::ControlLeaseToken token;
|
control::ControlLeaseToken token;
|
||||||
@ -663,6 +732,7 @@ void TouchScreenTask::finishActivityUnlocked(
|
|||||||
touch_command_started_ = false;
|
touch_command_started_ = false;
|
||||||
retract_command_started_ = false;
|
retract_command_started_ = false;
|
||||||
activity_active_.store(false, std::memory_order_release);
|
activity_active_.store(false, std::memory_order_release);
|
||||||
|
clearActivitySafetyHooksUnlocked();
|
||||||
releaseActivityControlUnlocked();
|
releaseActivityControlUnlocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -680,7 +750,6 @@ bool TouchScreenTask::startFromPixelUnlocked(int u, int v) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetActivityUnlocked();
|
|
||||||
if (!moveToInitPositionBeforeStartIfEnabled()) {
|
if (!moveToInitPositionBeforeStartIfEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -724,9 +793,19 @@ bool TouchScreenTask::step(const double dt) {
|
|||||||
!activityControlCurrent()) {
|
!activityControlCurrent()) {
|
||||||
activity_active_.store(false, std::memory_order_release);
|
activity_active_.store(false, std::memory_order_release);
|
||||||
resetActivityUnlocked();
|
resetActivityUnlocked();
|
||||||
|
clearActivitySafetyHooksUnlocked();
|
||||||
releaseActivityControlUnlocked();
|
releaseActivityControlUnlocked();
|
||||||
return true;
|
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_) {
|
if (!initialized_) {
|
||||||
last_status_ = Status::NOT_INITIALIZED;
|
last_status_ = Status::NOT_INITIALIZED;
|
||||||
return false;
|
return false;
|
||||||
@ -840,6 +919,7 @@ bool TouchScreenTask::stopActivity() {
|
|||||||
isBusyUnlocked()) {
|
isBusyUnlocked()) {
|
||||||
resetActivityUnlocked();
|
resetActivityUnlocked();
|
||||||
}
|
}
|
||||||
|
clearActivitySafetyHooksUnlocked();
|
||||||
releaseActivityControlUnlocked();
|
releaseActivityControlUnlocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -977,6 +1057,14 @@ Eigen::Vector3d TouchScreenTask::lastAlignErrorCamera() const {
|
|||||||
return last_align_error_camera_;
|
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 {
|
std::string TouchScreenTask::stateString() const {
|
||||||
return taskStateToString(state());
|
return taskStateToString(state());
|
||||||
}
|
}
|
||||||
@ -1020,6 +1108,7 @@ const char* TouchScreenTask::statusToString(const Status status) {
|
|||||||
case Status::RETRACTING: return "RETRACTING";
|
case Status::RETRACTING: return "RETRACTING";
|
||||||
case Status::DONE: return "DONE";
|
case Status::DONE: return "DONE";
|
||||||
case Status::STOPPED: return "STOPPED";
|
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_STATE_FAILED: return "ROBOT_STATE_FAILED";
|
||||||
case Status::ROBOT_COMMAND_FAILED: return "ROBOT_COMMAND_FAILED";
|
case Status::ROBOT_COMMAND_FAILED: return "ROBOT_COMMAND_FAILED";
|
||||||
case Status::TASK_BUSY: return "TASK_BUSY";
|
case Status::TASK_BUSY: return "TASK_BUSY";
|
||||||
@ -1688,9 +1777,9 @@ bool TouchScreenTask::stepRetracting() {
|
|||||||
<< ", final_tcp_delta_base=unavailable";
|
<< ", final_tcp_delta_base=unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!runArmStopIfCurrent([this] {
|
||||||
arm_->stopL();
|
return arm_ && arm_->stopL().ok();
|
||||||
} catch (...) {
|
})) {
|
||||||
enterFailed(Status::ROBOT_COMMAND_FAILED);
|
enterFailed(Status::ROBOT_COMMAND_FAILED);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1738,17 +1827,11 @@ bool TouchScreenTask::sendJointVelocity(const std::vector<double>& qdot) const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto dispatch = tryBeginActivityDispatch();
|
|
||||||
if (!dispatch.acquired()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
device::JointVelocityCommand cmd;
|
device::JointVelocityCommand cmd;
|
||||||
cmd.velocity = qdot;
|
cmd.velocity = qdot;
|
||||||
const auto result = arm_->speedJ(cmd, 0.0, 0.0);
|
return runArmActuationIfCurrent([this, &cmd] {
|
||||||
if (!result.ok()) {
|
return arm_ && arm_->speedJ(cmd, 0.0, 0.0).ok();
|
||||||
return false;
|
});
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TouchScreenTask::sendZeroJointVelocity() const {
|
bool TouchScreenTask::sendZeroJointVelocity() const {
|
||||||
@ -1835,15 +1918,9 @@ bool TouchScreenTask::holdCurrentControlledPosition() const {
|
|||||||
joints.position.push_back(it->second);
|
joints.position.push_back(it->second);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto dispatch = tryBeginActivityDispatch();
|
return runArmActuationIfCurrent([this, &joints] {
|
||||||
if (!dispatch.acquired()) {
|
return arm_ && arm_->servoJ(joints).ok();
|
||||||
return false;
|
});
|
||||||
}
|
|
||||||
const auto result = arm_->servoJ(joints);
|
|
||||||
if (!result.ok()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TouchScreenTask::buildInitJointPositions(std::vector<double>& positions_out) const {
|
bool TouchScreenTask::buildInitJointPositions(std::vector<double>& positions_out) const {
|
||||||
@ -1870,8 +1947,9 @@ bool TouchScreenTask::moveToInitPositionBeforeStartIfEnabled() {
|
|||||||
options.velocity = config_.initialization().velocity();
|
options.velocity = config_.initialization().velocity();
|
||||||
options.acceleration = config_.initialization().acceleration();
|
options.acceleration = config_.initialization().acceleration();
|
||||||
options.cancellation_requested = activityCancellationRequested();
|
options.cancellation_requested = activityCancellationRequested();
|
||||||
const auto result = arm_->moveJ(init_cmd, options);
|
if (!runArmActuationIfCurrent([this, &init_cmd, &options] {
|
||||||
if (!result.ok()) {
|
return arm_ && arm_->moveJ(init_cmd, options).ok();
|
||||||
|
})) {
|
||||||
last_status_ = Status::ROBOT_COMMAND_FAILED;
|
last_status_ = Status::ROBOT_COMMAND_FAILED;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1892,17 +1970,16 @@ bool TouchScreenTask::moveToInitPositionIfEnabled() const {
|
|||||||
options.velocity = config_.initialization().velocity();
|
options.velocity = config_.initialization().velocity();
|
||||||
options.acceleration = config_.initialization().acceleration();
|
options.acceleration = config_.initialization().acceleration();
|
||||||
options.cancellation_requested = activityCancellationRequested();
|
options.cancellation_requested = activityCancellationRequested();
|
||||||
const auto result = arm_->moveJ(init_cmd, options);
|
return runArmActuationIfCurrent([this, &init_cmd, &options] {
|
||||||
if (!result.ok()) {
|
return arm_ && arm_->moveJ(init_cmd, options).ok();
|
||||||
return false;
|
});
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TouchScreenTask::handleTouchTriggered(const bool stop_forward_motion) {
|
bool TouchScreenTask::handleTouchTriggered(const bool stop_forward_motion) {
|
||||||
if (stop_forward_motion) {
|
if (stop_forward_motion) {
|
||||||
const auto result = arm_->stopL();
|
if (!runArmStopIfCurrent([this] {
|
||||||
if (!result.ok()) {
|
return arm_ && arm_->stopL().ok();
|
||||||
|
})) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1936,17 +2013,15 @@ bool TouchScreenTask::startTouchPhase() {
|
|||||||
last_status_ = Status::ROBOT_STATE_FAILED;
|
last_status_ = Status::ROBOT_STATE_FAILED;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto dispatch = tryBeginActivityDispatch();
|
const auto velocity = toCartesianVelocity(
|
||||||
if (!dispatch.acquired()) {
|
cmvr::common::math::toEigenVec6(speed_l.twist_tool()));
|
||||||
last_status_ = Status::ROBOT_COMMAND_FAILED;
|
if (!runArmActuationIfCurrent([this, velocity, &speed_l] {
|
||||||
return false;
|
return arm_ && arm_->speedL(
|
||||||
}
|
velocity,
|
||||||
const auto result = arm_->speedL(toCartesianVelocity(
|
|
||||||
cmvr::common::math::toEigenVec6(speed_l.twist_tool())),
|
|
||||||
speed_l.acceleration(),
|
speed_l.acceleration(),
|
||||||
0.0,
|
0.0,
|
||||||
device::FrameType::Tool);
|
device::FrameType::Tool).ok();
|
||||||
if (!result.ok()) {
|
})) {
|
||||||
last_status_ = Status::ROBOT_COMMAND_FAILED;
|
last_status_ = Status::ROBOT_COMMAND_FAILED;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1973,8 +2048,10 @@ bool TouchScreenTask::startTouchPhase() {
|
|||||||
options.joint_velocity_limits.assign(move_l.joint_velocity_limits().begin(),
|
options.joint_velocity_limits.assign(move_l.joint_velocity_limits().begin(),
|
||||||
move_l.joint_velocity_limits().end());
|
move_l.joint_velocity_limits().end());
|
||||||
options.cancellation_requested = activityCancellationRequested();
|
options.cancellation_requested = activityCancellationRequested();
|
||||||
const auto result = arm_->moveL(pose_cmd, options, device::FrameType::Tool);
|
if (!runArmActuationIfCurrent([this, &pose_cmd, &options] {
|
||||||
if (!result.ok()) {
|
return arm_ && arm_->moveL(
|
||||||
|
pose_cmd, options, device::FrameType::Tool).ok();
|
||||||
|
})) {
|
||||||
last_status_ = Status::ROBOT_COMMAND_FAILED;
|
last_status_ = Status::ROBOT_COMMAND_FAILED;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -2018,17 +2095,14 @@ bool TouchScreenTask::startRetractPhase(const Phase next_phase_after_retract,
|
|||||||
<< ", start_tcp_base=unavailable";
|
<< ", start_tcp_base=unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto dispatch = tryBeginActivityDispatch();
|
if (!runArmActuationIfCurrent([this, &retract_cmd, &retract] {
|
||||||
if (!dispatch.acquired()) {
|
return arm_ && arm_->speedL(
|
||||||
return false;
|
retract_cmd,
|
||||||
}
|
|
||||||
const auto result = arm_->speedL(retract_cmd,
|
|
||||||
retract.acceleration(),
|
retract.acceleration(),
|
||||||
0.0,
|
0.0,
|
||||||
device::FrameType::Tool);
|
device::FrameType::Tool).ok();
|
||||||
if (!result.ok()) {
|
})) {
|
||||||
CMVR_LOG(ERROR) << "[TouchScreenTask][RETRACT_START] speedL failed: "
|
CMVR_LOG(ERROR) << "[TouchScreenTask][RETRACT_START] speedL failed";
|
||||||
<< result.message;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2043,12 +2117,9 @@ bool TouchScreenTask::startRetractPhase(const Phase next_phase_after_retract,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TouchScreenTask::enterFailed(const Status status) {
|
void TouchScreenTask::enterFailed(const Status status) {
|
||||||
try {
|
(void)runArmStopIfCurrent([this] {
|
||||||
if (arm_) {
|
return arm_ && arm_->stopL().ok();
|
||||||
arm_->stopL();
|
});
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
}
|
|
||||||
hardStopIbvsMotion();
|
hardStopIbvsMotion();
|
||||||
holdCurrentControlledPosition();
|
holdCurrentControlledPosition();
|
||||||
const auto final_status = moveToInitPositionIfEnabled()
|
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";
|
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 {
|
message DeviceLifecycle {
|
||||||
enum Lifecycle {
|
enum Lifecycle {
|
||||||
STATE_INIT = 0;
|
STATE_INIT = 0;
|
||||||
@ -20,12 +73,22 @@ message CommandHeader {
|
|||||||
message Request {
|
message Request {
|
||||||
string device_id = 1; // 目标设备名称
|
string device_id = 1; // 目标设备名称
|
||||||
google.protobuf.Timestamp timestamp = 2; // 请求时间
|
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 {
|
message Feedback {
|
||||||
bool success = 1; // 是否成功
|
bool success = 1; // 是否成功
|
||||||
string error_message = 2; // 错误信息(成功时为空)
|
string error_message = 2; // 错误信息(成功时为空)
|
||||||
google.protobuf.Timestamp timestamp = 3; // 回复时间
|
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/agv_command.proto";
|
||||||
import "cmvr/api/arm_command.proto";
|
import "cmvr/api/arm_command.proto";
|
||||||
import "cmvr/api/common.proto";
|
import "cmvr/api/common.proto";
|
||||||
|
import "cmvr/api/safety_command.proto";
|
||||||
|
|
||||||
package cmvr.api;
|
package cmvr.api;
|
||||||
|
|
||||||
@ -109,6 +110,16 @@ message GetSystemInfoCommand {
|
|||||||
// Changes whenever the in-process ActionQueue idempotency ledger is
|
// Changes whenever the in-process ActionQueue idempotency ledger is
|
||||||
// recreated. Clients bind submissions and retries to this value.
|
// recreated. Clients bind submissions and retries to this value.
|
||||||
string action_service_instance_id = 8;
|
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 StopAllCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
|
string operation_id = 2;
|
||||||
|
string expected_service_instance_id = 3;
|
||||||
|
uint32 timeout_ms = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Feedback {
|
message Feedback {
|
||||||
CommandHeader.Feedback header = 1;
|
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";
|
syntax = "proto3";
|
||||||
|
|
||||||
import "cmvr/api/system_command.proto";
|
import "cmvr/api/system_command.proto";
|
||||||
|
import "cmvr/api/safety_command.proto";
|
||||||
|
|
||||||
package cmvr.api;
|
package cmvr.api;
|
||||||
|
|
||||||
@ -15,4 +16,7 @@ service SystemService {
|
|||||||
rpc StopAll(StopAllCommand.Request) returns (StopAllCommand.Feedback) {}
|
rpc StopAll(StopAllCommand.Request) returns (StopAllCommand.Feedback) {}
|
||||||
|
|
||||||
rpc ExecuteActionQueue(ActionQueueCommand.Request) returns (ActionQueueCommand.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";
|
syntax = "proto3";
|
||||||
package cmvr.config;
|
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 {
|
message DeviceConfigEntry {
|
||||||
enum DeviceType {
|
enum DeviceType {
|
||||||
reserved 1, 2, 3, 4, 5, 6, 7, 10, 11;
|
reserved 1, 2, 3, 4, 5, 6, 7, 10, 11;
|
||||||
@ -27,6 +46,10 @@ message DeviceConfigEntry {
|
|||||||
DeviceType type = 2;
|
DeviceType type = 2;
|
||||||
string config_file = 3;
|
string config_file = 3;
|
||||||
bool enable = 4;
|
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 {
|
message DeviceManagerConfig {
|
||||||
@ -35,6 +58,7 @@ message DeviceManagerConfig {
|
|||||||
string description = 3;
|
string description = 3;
|
||||||
repeated DeviceConfigEntry devices = 4;
|
repeated DeviceConfigEntry devices = 4;
|
||||||
bool init_all_motors_when_no_active_joints = 20;
|
bool init_all_motors_when_no_active_joints = 20;
|
||||||
|
SafetyCoordinatorConfig safety = 21;
|
||||||
}
|
}
|
||||||
message DeviceManagerRootConfig {
|
message DeviceManagerRootConfig {
|
||||||
DeviceManagerConfig device_manager = 1;
|
DeviceManagerConfig device_manager = 1;
|
||||||
|
|||||||
@ -24,6 +24,45 @@ message ArmTeleopBackendConfig {
|
|||||||
double max_position_step_rad = 11;
|
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 {
|
message GRPCServerConfig {
|
||||||
string host = 1;
|
string host = 1;
|
||||||
string port = 2;
|
string port = 2;
|
||||||
@ -36,6 +75,7 @@ message GRPCServerConfig {
|
|||||||
// default so configurations written before these fields remain low-latency.
|
// default so configurations written before these fields remain low-latency.
|
||||||
uint32 camera_stream_max_frame_age_ms = 6;
|
uint32 camera_stream_max_frame_age_ms = 6;
|
||||||
ArmTeleopBackendConfig arm_teleop_backend = 7;
|
ArmTeleopBackendConfig arm_teleop_backend = 7;
|
||||||
|
GRPCSecurityConfig security = 8;
|
||||||
}
|
}
|
||||||
message GRPCServerRootConfig {
|
message GRPCServerRootConfig {
|
||||||
GRPCServerConfig grpc_server = 1;
|
GRPCServerConfig grpc_server = 1;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user