fix(simulation): persist command authority provenance
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from k1link.simulation.contracts import (
|
||||
AckermannControlSetpoint,
|
||||
AuthorityProfile,
|
||||
CommandAuthorityScope,
|
||||
ControlProfile,
|
||||
ControlSetpoint,
|
||||
DifferentialControlSetpoint,
|
||||
@@ -38,6 +39,7 @@ __all__ = [
|
||||
"AckermannControlSetpoint",
|
||||
"AuthorityProfile",
|
||||
"CheckStatus",
|
||||
"CommandAuthorityScope",
|
||||
"ControlProfile",
|
||||
"ControlSetpoint",
|
||||
"DifferentialControlSetpoint",
|
||||
|
||||
@@ -52,6 +52,10 @@ class ControlProfile(StrEnum):
|
||||
ROVER_SPEED_YAW_RATE_V1 = "rover-speed-yaw-rate/v1"
|
||||
|
||||
|
||||
class CommandAuthorityScope(StrEnum):
|
||||
VIRTUAL_ONLY = "virtual-only"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProviderPin:
|
||||
identifier: str
|
||||
@@ -418,6 +422,8 @@ class AckermannControlSetpoint:
|
||||
speed_mps: float
|
||||
steering_normalized: float
|
||||
profile: ControlProfile = ControlProfile.ROVER_SPEED_STEERING_V1
|
||||
authority_scope: CommandAuthorityScope = CommandAuthorityScope.VIRTUAL_ONLY
|
||||
source: str = "simulation-orchestrator"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_command_header(
|
||||
@@ -434,6 +440,7 @@ class AckermannControlSetpoint:
|
||||
raise SimulationContractError("normalized steering must be within -1..1")
|
||||
if self.profile is not ControlProfile.ROVER_SPEED_STEERING_V1:
|
||||
raise SimulationContractError("Ackermann command has an incompatible profile")
|
||||
_command_provenance(self.authority_scope, self.source)
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
@@ -442,6 +449,8 @@ class AckermannControlSetpoint:
|
||||
"command_id": self.command_id,
|
||||
"sequence": self.sequence,
|
||||
"profile": self.profile.value,
|
||||
"authority_scope": self.authority_scope.value,
|
||||
"source": self.source,
|
||||
"issued_at_sim_ns": self.issued_at_sim_ns,
|
||||
"valid_until_sim_ns": self.valid_until_sim_ns,
|
||||
"authority_generation": self.authority_generation,
|
||||
@@ -461,6 +470,8 @@ class DifferentialControlSetpoint:
|
||||
speed_mps: float
|
||||
yaw_rate_rps: float
|
||||
profile: ControlProfile = ControlProfile.ROVER_SPEED_YAW_RATE_V1
|
||||
authority_scope: CommandAuthorityScope = CommandAuthorityScope.VIRTUAL_ONLY
|
||||
source: str = "simulation-orchestrator"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_command_header(
|
||||
@@ -475,6 +486,7 @@ class DifferentialControlSetpoint:
|
||||
_finite(self.yaw_rate_rps, "yaw rate")
|
||||
if self.profile is not ControlProfile.ROVER_SPEED_YAW_RATE_V1:
|
||||
raise SimulationContractError("differential command has an incompatible profile")
|
||||
_command_provenance(self.authority_scope, self.source)
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
@@ -483,6 +495,8 @@ class DifferentialControlSetpoint:
|
||||
"command_id": self.command_id,
|
||||
"sequence": self.sequence,
|
||||
"profile": self.profile.value,
|
||||
"authority_scope": self.authority_scope.value,
|
||||
"source": self.source,
|
||||
"issued_at_sim_ns": self.issued_at_sim_ns,
|
||||
"valid_until_sim_ns": self.valid_until_sim_ns,
|
||||
"authority_generation": self.authority_generation,
|
||||
@@ -500,8 +514,9 @@ def control_setpoint_from_dict(value: object) -> ControlSetpoint:
|
||||
raise SimulationContractError("unsupported control-setpoint schema")
|
||||
try:
|
||||
profile = ControlProfile(_string(document, "profile"))
|
||||
authority_scope = CommandAuthorityScope(_string(document, "authority_scope"))
|
||||
except ValueError as exc:
|
||||
raise SimulationContractError("unknown control profile") from exc
|
||||
raise SimulationContractError("unknown control profile or authority scope") from exc
|
||||
run_id = _string(document, "run_id")
|
||||
command_id = _string(document, "command_id")
|
||||
sequence = _integer(document, "sequence")
|
||||
@@ -519,6 +534,8 @@ def control_setpoint_from_dict(value: object) -> ControlSetpoint:
|
||||
authority_generation=authority_generation,
|
||||
speed_mps=speed_mps,
|
||||
steering_normalized=_number(document, "steering_normalized"),
|
||||
authority_scope=authority_scope,
|
||||
source=_string(document, "source"),
|
||||
)
|
||||
return DifferentialControlSetpoint(
|
||||
run_id=run_id,
|
||||
@@ -529,6 +546,8 @@ def control_setpoint_from_dict(value: object) -> ControlSetpoint:
|
||||
authority_generation=authority_generation,
|
||||
speed_mps=speed_mps,
|
||||
yaw_rate_rps=_number(document, "yaw_rate_rps"),
|
||||
authority_scope=authority_scope,
|
||||
source=_string(document, "source"),
|
||||
)
|
||||
|
||||
|
||||
@@ -552,6 +571,12 @@ def _command_header(
|
||||
raise SimulationContractError("command authority generation must be positive")
|
||||
|
||||
|
||||
def _command_provenance(authority_scope: CommandAuthorityScope, source: str) -> None:
|
||||
if authority_scope is not CommandAuthorityScope.VIRTUAL_ONLY:
|
||||
raise SimulationContractError("S1 commands require virtual-only authority scope")
|
||||
_identifier(source, "command source")
|
||||
|
||||
|
||||
def _identifier(value: str, label: str) -> str:
|
||||
if not IDENTIFIER_PATTERN.fullmatch(value):
|
||||
raise SimulationContractError(f"{label} is not a safe identifier")
|
||||
|
||||
Reference in New Issue
Block a user