ROS 2 parameter를 실행 중 바꿔도 되는가: YAML·검증·원자적 갱신 계약

WBC의 관절 강성을 조금 낮추려고 wbc_gains.yaml의 Kp를 수정했다고 해 봅시다. 파일은 정상적으로 저장됐고, YAML 문법도 맞습니다. 그렇다면 지금 움직이는 로봇은 새 Kp를 사용하고 있을까요? 현재 workspace를 따라가 보면 답은 단순하지 않습니다. load_gains_yaml()을 다시 호출하지 않았다면 실행 중인 PD controller는 이전 값을 그대로 사용합니다. 호출했더라도 그 함수가 읽는 것은 Kp와 Kd뿐입니다. 같은 YAML에 들어 있는 tau_max를 바꿔도 이 경로에서는 소비되지 않습니다. 파일의 내용과 제어기가 실제로 사용하는 값은 이미 서로 다른 상태입니다.

RL backend도 비슷합니다. run_backend_once.py는 YAML을 읽은 뒤 CLI의 --rl-checkpoint--rl-action-scale로 dictionary를 덮어씁니다. 그러나 RLBackend가 생성되면 checkpoint path, action scale, action type, limit 같은 값은 객체의 field로 복사됩니다. Runner는 이 backend를 cache합니다. 그 뒤 원본 YAML이나 dictionary를 고쳐도 이미 만들어진 객체의 값은 저절로 바뀌지 않습니다.

이 글에서 다룰 질문은 “ROS 2 parameter를 어떻게 선언하는가”보다 한 단계 뒤에 있습니다. 어떤 설정값을 누가 변경할 수 있고, 어떤 검증을 통과한 값 묶음이, 정확히 어느 control tick부터 사용됐다는 사실을 어떻게 증명할까요? 현재 workspace에는 아직 ROS 2 parameter callback이나 실물 G1 runtime이 구현돼 있지 않습니다. 따라서 아래 내용은 완성된 기능 소개가 아니라, 현재 YAML·CLI override·hot reload 경로에서 확인한 공백을 ROS 2 parameter 계약으로 옮긴 설계 기록입니다.

코드 기본값, YAML, CLI와 runtime 요청이 선언과 검증, 원자적 parameter 저장, immutable config bundle, control tick 경계의 교체, command trace로 이어지는 ROS 2 parameter 운영 계약 다이어그램
파일을 수정한 사실과 실행 중인 제어기의 값이 바뀐 사실을 분리해야 합니다. 원자적 parameter 저장도 control loop의 원자적 적용이나 부드러운 actuator 효과를 자동으로 보장하지 않습니다. 그림을 누르면 원본 크기로 볼 수 있습니다.

현재 workspace에는 이미 세 가지 다른 설정 경로가 있습니다

첫 번째는 학습 경로입니다. training/rl/train.py는 Isaac Lab의 environment config를 만든 뒤 CLI 인자를 적용하고, 별도의 agent YAML을 읽어 RSL-RL runner config를 구성합니다. 이 경로에는 비교적 강한 시작 시점 검증이 있습니다. 예를 들어 stand command 확률은 0과 0.5 사이인지, tracking sigma는 양수인지, 특정 reward scale의 부호가 의도와 맞는지를 명시적으로 검사합니다. 학습을 시작할 때 쓰인 agent YAML의 SHA-256, effective runner config, parent checkpoint의 경로와 hash, command arguments도 run manifest에 남깁니다.

두 번째는 router 실행 경로입니다. run_backend_once.py가 robot·task·backend YAML을 읽고, RL에 대해서는 CLI override 두 개를 dictionary에 직접 반영합니다. 여기서 precedence는 “파일을 먼저 읽고, 지정된 CLI 값으로 checkpoint와 action scale을 덮어쓴다”로 코드에 드러납니다. 하지만 모든 key에 대한 공통 schema나 허용 범위, canonical serialization, effective config hash는 없습니다. preferred_backend가 task YAML에 있어도 실제 backend 선택은 위에서 아래로 평가되는 selector rule이 담당합니다. 이름이 비슷하다고 같은 source of truth가 되는 것은 아닙니다.

세 번째는 WBC gain 변경 경로입니다. PDController.set_gains(kp, kd)는 실행 중 gain 교체를 염두에 둔 함수지만, 현재는 두 array를 차례로 대입합니다. 길이가 DOF와 정확히 일치하는지, 값이 유한한지, 음수가 아닌지, 한 번에 얼마나 변해도 되는지, joint order가 같은지는 검사하지 않습니다. 이것은 “hot reload 함수가 있다”는 증거이지 “안전한 runtime tuning이 구현됐다”는 증거가 아닙니다.

경로 설정 source 적용 시점 현재 남는 증거 확인한 공백
Isaac Lab 학습 configclass, agent YAML, CLI 학습 시작 전 agent YAML hash, runner config, checkpoint hash, CLI 일반 경로의 전체 effective env_cfg hash는 없음
router robot·task·backend YAML, 일부 CLI backend 생성 시 선택 결과와 response JSON 전체 precedence·effective config hash·generation 없음
WBC PD gain YAML 또는 직접 호출 set_gains() 호출 시 별도 적용 manifest 없음 길이·범위·joint identity·원자성 검증 없음

세 경로를 하나의 “config”라고 부르면 원인을 추적하기 어렵습니다. 적어도 코드 기본값, 입력 파일, CLI나 launch override, runtime 변경 요청, 최종 consumer 객체라는 다섯 표현을 구분해야 합니다. 블로그나 실험 노트에 “action scale은 0.25였다”고 적으려면 어느 YAML에 그렇게 적혀 있었다는 뜻인지, backend 객체가 0.25를 읽었다는 뜻인지, 실제 command trace가 그 generation을 사용했다는 뜻인지부터 밝혀야 합니다.

YAML 파일은 입력 artifact일 뿐 runtime의 진실이 아닙니다

YAML의 가장 큰 장점은 사람이 읽고 diff를 남기기 쉽다는 점입니다. 반대로 파일 자체에는 소비 시점과 소비자가 없습니다. 프로세스 시작 때 한 번 읽는지, 매 step 다시 읽는지, file watcher가 교체를 감지하는지, 변경 후 객체를 재생성하는지에 따라 같은 파일이 전혀 다른 의미를 가집니다.

현재 RLBackend는 constructor에서 action_scale, allow_dummy, action type과 limit, hidden dimension, checkpoint path를 읽어 instance field에 보관합니다. WBCBackend도 damping, step limit, smoothing alpha, dt를 같은 방식으로 가져갑니다. 그리고 Runner._get_backend()는 생성한 backend를 이름별로 cache합니다. 이 구조에서 YAML 파일을 고치는 행위는 다음 프로세스 시작을 위한 입력 artifact를 바꾼 것이지, cache된 live object를 변경한 것이 아닙니다.

더 교묘한 경우는 한 파일을 여러 consumer가 부분적으로 읽는 상황입니다. sim/envs/h1_walk/wbc_gains.yaml에는 Kp, Kd, tau_max뿐 아니라 QP, CoM planner, gait scheduler, foot trajectory, state estimator 설정이 함께 있습니다. 그러나 load_gains_yaml()pd_controller 아래 Kp와 Kd만 반환합니다. 사용자가 tau_max까지 바꾼 뒤 “gain file을 reload했다”고 기록하면 문장은 맞지만, torque limit이 바뀌었다는 결론은 틀립니다.

그래서 runtime의 source of truth는 파일 경로가 아니라 consumer가 실제로 읽은 최종 값의 불변 snapshot이어야 합니다. snapshot에는 source file의 hash도 들어가야 하지만, 그것만으로는 부족합니다. 기본값과 override를 모두 합친 effective value, 어느 component가 소비하는지, 어느 generation부터 유효한지가 함께 있어야 합니다.

ROS 2 parameter는 전역 dictionary가 아니라 node가 소유한 계약입니다

ROS 2 parameter 설계 문서공식 parameter 개념 문서에서 먼저 확인할 사실은 parameter가 개별 node에 연결된다는 점입니다. ROS 1의 중앙 parameter server처럼 전체 시스템의 값이 한곳에 영구 보관되는 모델이 아닙니다. parameter의 lifetime도 기본적으로 node의 lifetime에 묶입니다. 별도의 persistence를 구현하거나 launch·YAML에서 다시 주입하지 않으면 node 재시작 뒤 runtime 변경값은 사라질 수 있습니다.

이 소유 경계는 G1 제어에서 중요합니다. 예를 들어 action_scale이라는 같은 이름이 policy node와 safety limiter node에 모두 존재할 수 있습니다. 한 node의 parameter를 바꾸었다고 다른 node의 값이나 이미 생성된 controller 객체가 자동으로 바뀌지 않습니다. 따라서 parameter manifest에는 이름만 적지 말고 fully qualified node name, lifecycle epoch, schema version을 포함해야 합니다.

또한 parameter service의 성공 응답은 node의 parameter storage가 요청을 받아들였다는 뜻입니다. 그 값이 downstream controller에 전달됐는지, 다음 action에 사용됐는지, actuator가 새 명령에 반응했는지까지 증명하지 않습니다. 이 구분을 놓치면 dashboard에는 새 Kp가 표시되는데 제어기는 이전 Kp를 사용하는 split-brain 상태가 생깁니다.

declare_parameter는 이름 등록이 아니라 변경 가능한 표면을 좁힙니다

ROS 2는 기본적으로 parameter를 먼저 선언하도록 합니다. 공식 static typing 설명에 따르면 default 값에서 type이 추론되고, dynamic typing은 기본값이 아닙니다. undeclared parameter 허용과 자동 선언 옵션은 편리하지만, 안전과 재현성이 중요한 제어 node에서는 오타 난 이름이나 예상하지 못한 type까지 받아들이는 면적을 넓힙니다.

예를 들어 operator가 action_scale 대신 action_sacle을 입력했을 때 명령 자체는 성공하고 실제 consumer는 이전 값을 계속 쓸 수 있습니다. 가장 위험한 실패는 즉시 예외가 나는 실패가 아니라, 변경했다고 믿었는데 아무 것도 바뀌지 않는 실패입니다. 그래서 알려진 parameter만 선언하고, descriptor에 type·범위·설명·read-only 여부를 적는 편이 낫습니다.

descriptor만으로 모든 규칙을 표현할 수는 없습니다. Kp는 양수이고 Kd도 양수라는 개별 범위와, 29개 joint array의 순서가 현재 robot manifest와 같아야 한다는 규칙은 성격이 다릅니다. min_action < max_action, checkpoint action dimension과 joint count 일치, control_dt = physics_dt × decimation처럼 여러 값을 함께 봐야 하는 cross-field invariant는 callback 검증이 필요합니다.

set_parameters의 부분 성공은 Kp와 Kd를 서로 다른 세대로 만들 수 있습니다

ROS 2 interface 문서는 여러 parameter를 개별적으로 설정하는 service와 모두 성공하거나 모두 실패하는 atomic service를 구분합니다. 일반 set_parameters에서는 요청 목록의 일부가 성공하고 나머지가 실패할 수 있습니다. 반면 set_parameters_atomically는 하나라도 거부되면 전체 변경을 실패시킵니다.

현재 PDController.set_gains()의 순서를 보면 이 차이가 현실적인 문제임을 알 수 있습니다.

self.kp = np.asarray(kp, dtype=np.float64)[:self.dof]
self.kd = np.asarray(kd, dtype=np.float64)[:self.dof]

Kp 변환은 성공했는데 Kd 변환이 예외를 내면 객체에는 새 Kp와 이전 Kd가 남습니다. 짧은 array도 slicing 뒤 그대로 저장될 수 있어 실제 torque 계산에서 broadcasting error가 뒤늦게 발생할 수 있습니다. 즉, 현재 함수는 두 줄이지만 하나의 config transaction은 아닙니다.

ROS parameter 경로를 붙일 때도 Kp와 Kd를 별도 callback에서 즉시 controller에 대입하면 같은 문제가 반복됩니다. 먼저 요청 전체를 복사해 candidate config를 만들고, 배열 길이·finite 값·허용 범위·joint name hash·Kp/Kd 조합을 모두 검증한 뒤, parameter storage에는 atomic하게 반영해야 합니다. 검증 callback은 accept 또는 reject 판단만 하고 live controller를 직접 건드리지 않는 것이 좋습니다. 공식 callback 설계 문서도 on-set callback을 side effect 없는 validation에 사용하고, 성공 뒤 작업은 post-set 단계로 분리하는 구조를 설명합니다.

atomic parameter update도 control tick을 원자적으로 만들지는 않습니다

여기서 한 번 더 경계를 나눠야 합니다. set_parameters_atomically가 보장하는 것은 node의 parameter storage에서 요청 목록이 모두 반영되거나 전혀 반영되지 않는다는 것입니다. control loop가 그 값을 한 번에 읽는다는 보장은 아닙니다.

50 Hz policy loop가 Kp를 읽은 직후 별도 executor thread가 Kd를 바꾸고, loop가 새 Kd를 읽는 구조라면 한 step 안에 서로 다른 generation이 섞입니다. 각 field에 lock을 붙이는 것만으로도 부족합니다. lock 순서와 callback 지연이 control deadline을 흔들 수 있고, controller가 여러 object에 흩어진 값을 읽으면 일관된 snapshot을 구성하기 어렵습니다.

목표 구조는 개별 field 변경이 아니라 immutable bundle 교체입니다.

candidate = merge(current_bundle, requested_parameters)
validated = validate(candidate, lifecycle_state, joint_manifest)
new_bundle = compile_immutable(validated, generation=current.generation + 1)

# control tick boundary
active_bundle = new_bundle
action_log.config_generation = active_bundle.generation
action_log.config_hash = active_bundle.sha256

callback은 control thread 밖에서 완성된 bundle을 만들고, control loop는 tick 시작점에서 하나의 reference만 교체합니다. 이때 parameter service 응답의 "accepted"와 bundle의 "staged", 실제 loop의 "activated"를 별도 상태로 기록해야 합니다. 요청이 10:00:00.100에 성공했더라도 robot tick 184,221부터 적용됐다면 그 tick이 물리적 분석의 경계입니다.

gain은 숫자 하나가 아니라 torque jump를 만드는 제어 입력입니다

PD 제어의 기본식을 다시 보면 runtime 변경의 위험이 더 분명해집니다.

tau = Kp * (q_des - q) + Kd * (dq_des - dq)

관절 상태와 목표가 그대로인 한 control tick 사이에서 gain만 바뀐다면 torque 변화는 다음과 같습니다.

delta_tau = delta_Kp * (q_des - q) + delta_Kd * (dq_des - dq)

Kp와 Kd를 원자적으로 바꿔도 torque가 연속적이라는 뜻은 아닙니다. position error가 큰 순간 Kp를 올리면 command에 step이 생깁니다. 뒤쪽의 torque clip이 이를 잘라 주더라도, 포화가 늘었다는 사실을 숨길 뿐 안전을 증명하지 않습니다. 그래서 gain 변경은 허용 범위 검사 외에 generation 간 최대 변화량, ramp 시간, 현재 error와 예상 delta_tau, effort limit까지 함께 평가해야 합니다.

실물 G1에서 첫 구현을 한다면 active 상태의 자유로운 gain 편집보다 inactive 전환 뒤 적용하는 편이 방어적입니다. active tuning이 꼭 필요하다면 safe controller가 actuator authority를 유지한 mock 또는 낮은 torque limit 구간에서 staged ramp를 검증한 뒤 별도 권한으로 열어야 합니다. node 상태와 actuator 권한의 구분은 이전의 Lifecycle Node 활성화 계약 글에서 다뤘습니다.

무엇을 active에서 바꾸고 무엇을 inactive로 돌릴지 먼저 분류합니다

"dynamic parameter를 지원한다"는 하나의 기능 flag로는 부족합니다. 변경이 model identity, tensor shape, transport topology, 제어 law, 단순 관측성 가운데 무엇을 바꾸는지에 따라 허용 상태가 달라져야 합니다.

분류 예시 권장 적용 상태 이유와 추가 조건
process restart 또는 read-only robot profile, joint order, model dimension, frame ID, hardware interface 시작 전 고정 object graph와 tensor 의미를 바꾸므로 같은 lifecycle epoch에서 교체하지 않음
inactive-only checkpoint path, backend, action type·scale, topic, QoS, queue 크기 deactivate→검증→configure/activate model·transport·buffer를 재생성하고 preflight를 다시 통과해야 함
active에서 staged 가능 Kp·Kd, command limit, smoothing 계수 기본은 inactive; 승인된 경우 tick-boundary ramp range, cross-field, slew, 예상 torque jump, authority guard 필요
active에서 비교적 안전 진단 publish 주기, 비제어 log level atomic set 후 즉시 또는 다음 tick control deadline과 executor backpressure에 영향이 없는지 상한 필요

action_scale는 숫자 하나라서 가볍게 보이지만 policy output의 의미를 바꿉니다. 현재 G1 candidate config에는 action_scale: 0.25와 forward-only 범위가 적혀 있습니다. 이를 active에서 0.4로 올리면 checkpoint는 그대로인데 관절 목표 변위가 60% 커집니다. 같은 policy identity로 묶어서는 안 되며, 최소한 새 config generation과 별도 평가 범위를 가져야 합니다.

현재 code에서 가장 위험한 것은 prompt가 validation을 대신하는 부분입니다

robot-control-router/backends/claude_planner.py에는 관측된 metric을 바탕으로 gain patch를 제안하는 경로가 있습니다. prompt는 안전 범위를 넘지 말고 한 번에 한두 parameter만 바꾸라고 지시합니다. 하지만 apply_gain_patch()는 제안된 nested dictionary를 deep_update()로 기존 YAML에 병합합니다. 허용 key schema, type, 숫자 범위, array 길이, joint identity를 실행 코드로 검사하지 않습니다.

이 구분은 AI를 사용해서 더 중요해집니다. 자연어 지시는 제안 품질을 높이는 정책이지, 잘못된 출력이 적용되는 것을 막는 enforcement가 아닙니다. 모델이 존재하지 않는 key를 만들거나 문자열을 반환하거나 상관없는 subsystem 값을 함께 바꿔도 현재 merge 함수 자체는 막지 못합니다. .yaml.bak도 고정된 한 파일이라 다음 적용 때 이전 backup을 덮어씁니다. generation별 immutable history가 아닙니다.

코드에는 auto_apply=True일 때 제안을 파일에 반영할 수 있는 경로가 존재합니다. 다만 저장소에서 이 경로가 실제 G1 제어 중 실행됐거나, 제안된 gain이 평가를 통과해 승인됐다는 증거는 확인하지 못했습니다. 그래서 이 글은 "AI가 gain을 자동 튜닝하고 있다"고 쓰지 않습니다. 현재 결론은 더 좁습니다. prompt 출력은 untrusted change request로 취급하고, 사람이 낸 요청과 똑같은 schema·range·lifecycle·fault gate를 통과시켜야 합니다.

최종 effective config를 hash하지 않으면 재현할 수 없습니다

학습 경로의 run manifest는 좋은 출발점입니다. agent YAML path와 SHA-256, effective runner config, parent checkpoint path와 hash, CLI argument를 저장합니다. 이 덕분에 "어느 checkpoint에서 어떤 PPO runner 설정으로 시작했는가"를 상당 부분 복원할 수 있습니다. 반면 일반 학습 경로에서 CLI로 수정된 전체 effective env_cfg가 canonical form과 hash로 모두 남는 것은 아닙니다. 일부 특별한 experiment에는 별도 contract hash가 있지만, 모든 run이 같은 수준의 identity를 갖지는 않습니다.

runtime parameter manifest는 학습 manifest보다 시간 정보가 더 필요합니다. 값이 무엇이었는지만 아니라 언제 바뀌었는지가 command 해석을 바꾸기 때문입니다. 최소 schema는 다음과 같이 잡을 수 있습니다.

{
  "schema_version": 1,
  "node": "/g1/policy_controller",
  "lifecycle_epoch": 7,
  "parent_generation": 18,
  "generation": 19,
  "effective_parameters": {"action_scale": 0.25, "...": "..."},
  "config_sha256": "...",
  "sources": {
    "defaults_id": "...",
    "yaml_path": "...",
    "yaml_sha256": "...",
    "launch_overrides": {},
    "runtime_request_id": "..."
  },
  "robot_joint_manifest_sha256": "...",
  "checkpoint_sha256": "...",
  "requested_by": "operator-or-agent-id",
  "reason": "bounded gain probe",
  "validated_monotonic_ns": 123456789,
  "activated_robot_tick": 184221,
  "result": "activated"
}

canonical hash를 만들 때는 dictionary key 정렬, float 표현, array order, 단위와 schema version을 고정해야 합니다. YAML 원문의 hash와 effective config hash는 둘 다 보존합니다. 전자는 입력 artifact의 identity이고, 후자는 default와 override가 합쳐진 실제 의미의 identity입니다. checkpoint hash, ordered joint manifest hash도 함께 묶어야 joint order·motor index·checkpoint 호환성 계약과 연결됩니다.

parameter event는 변경 기록이지 actuator 적용 증거가 아닙니다

ROS 2 parameter event는 어떤 node의 값이 추가·변경·삭제됐는지 관찰하는 데 유용합니다. 그러나 event만 저장해서는 controller 적용 시점을 알 수 없습니다. event callback이 도착하기 전후의 executor queue 지연, bundle compile 시간, tick-boundary swap이 따로 있기 때문입니다. transport와 callback 대기 문제는 executor·callback group·backpressure 글의 책임으로 남겨 두고, 여기서는 config generation을 command trace까지 전달하는 것으로 경계를 닫습니다.

각 action 또는 command record에는 최소한 config_generation, config_hash, lifecycle_epoch, checkpoint와 joint manifest identity가 들어가야 합니다. incident가 발생하면 ring buffer·replay 계약이 이 값을 sensor·estimate·raw action·applied action과 함께 freeze합니다. 그래야 "parameter가 14시 03분에 바뀌었다"가 아니라 "robot tick 184,221부터 generation 19가 적용됐고, 첫 torque saturation은 184,227에 발생했다"고 말할 수 있습니다.

재시작도 별도 fault입니다. runtime parameter가 node memory에만 있었다면 crash 후 launch YAML의 이전 값으로 돌아갈 수 있습니다. 반대로 자동 저장한 최신 값을 무조건 복원하면 사고 직전의 잘못된 값까지 재적용할 수 있습니다. persistence는 편의 기능이 아니라 승인 정책입니다. candidate 값을 durable config로 승격하는 절차와 단순 process restart 복원을 분리해야 합니다.

첫 fault test는 잘못된 값보다 절반만 적용시키는 것입니다

범위를 벗어난 Kp 한 개를 거부하는 test는 필요하지만 충분하지 않습니다. 실제 운영에서 더 어려운 실패는 각 component가 따로 보면 정상인데 세대가 섞이는 경우입니다. 첫 test suite는 actuator authority를 0으로 고정한 mock node에서 다음 순서로 만드는 편이 좋습니다.

  1. 선언하지 않은 이름, 잘못된 type, NaN·Inf, array 길이 불일치, 허용 범위 밖 값을 모두 거부합니다.
  2. Kp는 유효하지만 Kd가 잘못된 요청을 atomic set으로 보내고 둘 다 이전 generation에 남는지 확인합니다.
  3. min_action < max_action, joint manifest hash, checkpoint dimension처럼 여러 parameter를 함께 보는 invariant를 깨뜨립니다.
  4. 두 requester가 같은 parent generation을 기준으로 동시에 변경할 때 하나만 성공하고 stale request는 거부되는지 확인합니다.
  5. parameter storage는 갱신됐지만 immutable bundle build가 실패하는 fault를 주입하고 live controller가 이전 bundle을 유지하는지 확인합니다.
  6. bundle이 준비된 순간 control tick을 지연시켜 한 tick 안에 generation이 섞이지 않는지 trace로 검사합니다.
  7. active gain ramp 중 lifecycle deactivate 또는 process crash를 발생시키고 actuator authority가 먼저 0이 되는지 확인합니다.
  8. node 재시작 뒤 volatile candidate가 조용히 durable default가 되지 않는지 검증합니다.

이 test의 pass 조건은 service가 exception 없이 끝나는 것이 아닙니다. rejected request는 이전 hash를 유지해야 하고, accepted request는 정확히 하나의 새 generation을 만들어야 하며, 모든 command가 하나의 generation만 가리켜야 합니다. bundle build나 activation이 실패하면 parameter storage까지 이전 generation으로 되돌릴지, accepted-but-not-active 상태로 남길지 정책을 명시해야 합니다. 애매한 성공 상태를 만들지 않는 것이 핵심입니다.

첫 구현은 ROS wrapper보다 RuntimeConfigBundle부터입니다

현재 코드에서 바로 ROS 2 parameter service를 붙이면 기존 dictionary와 object field 사이에 source가 하나 더 늘어납니다. 먼저 ROS와 무관한 순수 함수로 schema와 candidate merge, cross-field validation, canonical hash, generation compare-and-swap을 구현하는 편이 낫습니다. 이 core를 unit test한 뒤 ROS callback은 요청을 변환하고 결과를 반환하는 얇은 adapter가 됩니다.

구현 순서는 다음 정도가 현실적입니다.

  1. router와 WBC가 소비하는 값을 inventory하고, parameter owner node와 단위·shape·기본값을 한 schema로 만듭니다.
  2. 각 값을 read-only, inactive-only, active-staged, diagnostic으로 분류합니다.
  3. RuntimeConfigBundle과 pure validator를 만들고 현재 PDController.set_gains()의 부분 대입을 bundle swap으로 바꿉니다.
  4. source YAML과 CLI override를 canonical effective config로 합치고 두 종류의 hash를 manifest에 남깁니다.
  5. authority 0 mock에서 partial update·concurrent request·bundle build failure·restart test를 통과합니다.
  6. 그 다음 Lifecycle Node의 inactive 상태에서 parameter callback과 bundle staging을 연결합니다.
  7. 마지막에만 active tick-boundary swap과 bounded gain ramp를 별도 승인합니다.

여기까지 구현돼도 "실물 G1에서 안전하다"는 결론은 아직 낼 수 없습니다. 증명 가능한 범위는 더 좁습니다. 선언되지 않은 값이 들어오지 않고, 한 요청이 부분 적용되지 않으며, 어떤 effective config가 어느 tick부터 사용됐는지 재현할 수 있다는 정도입니다. 그 위에 latency, QoS, lifecycle, actuator lease와 실제 torque response 검증이 더해져야 live control 계약이 완성됩니다.

결국 관리해야 하는 것은 parameter 값이 아니라 config의 세대입니다

YAML은 설정을 전달하기 좋은 문서이고 ROS 2 parameter는 실행 중 node와 설정을 교환하기 좋은 interface입니다. 둘 중 어느 것도 그 자체로 제어 안전이나 재현성을 보장하지 않습니다. 파일을 수정했다는 사실, node storage가 값을 받아들였다는 사실, controller가 새 bundle을 읽었다는 사실, actuator가 그 결과에 반응했다는 사실은 서로 다른 증거입니다.

현재 workspace에는 학습 시작 시점의 검증과 manifest, router의 YAML·CLI override, WBC의 gain reload, AI가 제안한 patch 적용 경로가 각각 존재합니다. 그러나 이들을 하나의 runtime generation으로 묶는 계약은 아직 없습니다. 다음 구현의 기준은 명확합니다. 모든 변경은 선언된 schema를 통과하고, side effect 없는 전체 검증 뒤 하나의 immutable bundle이 되며, control tick 경계에서만 교체되고, 이후 command trace가 그 generation과 hash를 소유해야 합니다.

그때부터 "Kp를 바꿨다"는 말은 파일 편집 기록이 아니라 검증 가능한 제어 사건이 됩니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤