rainbow-rb-flow-manager 0.1.2.dev2__tar.gz → 0.1.2.dev4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/PKG-INFO +7 -7
  2. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/pyproject.toml +7 -7
  3. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/context.py +159 -30
  4. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/executor.py +79 -46
  5. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/step.py +9 -22
  6. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow_rb_flow_manager.egg-info/PKG-INFO +7 -7
  7. rainbow_rb_flow_manager-0.1.2.dev4/src/rainbow_rb_flow_manager.egg-info/requires.txt +6 -0
  8. rainbow_rb_flow_manager-0.1.2.dev2/src/rainbow_rb_flow_manager.egg-info/requires.txt +0 -6
  9. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/README.md +0 -0
  10. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/setup.cfg +0 -0
  11. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/.claude/skills/pyfm/SKILL.md +0 -0
  12. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/.claude/skills/pyfm/references/internals.md +0 -0
  13. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/.claude/skills/pyfm/references/testing.md +0 -0
  14. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/__init__.py +0 -0
  15. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/controller/base_controller.py +0 -0
  16. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/controller/zenoh_controller.py +0 -0
  17. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/examples/01_multiple_tasks.py +0 -0
  18. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/examples/02_single_task.py +0 -0
  19. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/examples/03_post_tree.py +0 -0
  20. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/examples/04_sub_task.py +0 -0
  21. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/examples/05_event_task.py +0 -0
  22. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/examples/README.md +0 -0
  23. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/examples/subtasks/gripper_subtask.py +0 -0
  24. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/exception.py +0 -0
  25. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/global_resolver.py +0 -0
  26. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/py.typed +0 -0
  27. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/schema.py +0 -0
  28. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/utils.py +0 -0
  29. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow_rb_flow_manager.egg-info/SOURCES.txt +0 -0
  30. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow_rb_flow_manager.egg-info/dependency_links.txt +0 -0
  31. {rainbow_rb_flow_manager-0.1.2.dev2 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow_rb_flow_manager.egg-info/top_level.txt +0 -0
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainbow-rb-flow-manager
3
- Version: 0.1.2.dev2
3
+ Version: 0.1.2.dev4
4
4
  Summary: Rainbow Python Flow Manager
5
5
  Author-email: Derek <dfd1123@rainbow-robotics.com>
6
6
  Requires-Python: <3.14,>=3.11
7
7
  Description-Content-Type: text/markdown
8
- Requires-Dist: rainbow-rb-utils==0.1.2.dev2
9
- Requires-Dist: rainbow-rb-zenoh==0.1.2.dev2
10
- Requires-Dist: rainbow-rb-flat-buffers==0.1.2.dev2
11
- Requires-Dist: rainbow-rb-schemas==0.1.2.dev2
12
- Requires-Dist: rainbow-rb-sdk==0.1.2.dev2
13
- Requires-Dist: rainbow-rb-log==0.1.2.dev2
8
+ Requires-Dist: rainbow-rb-utils==0.1.2.dev4
9
+ Requires-Dist: rainbow-rb-zenoh==0.1.2.dev4
10
+ Requires-Dist: rainbow-rb-flat-buffers==0.1.2.dev4
11
+ Requires-Dist: rainbow-rb-schemas==0.1.2.dev4
12
+ Requires-Dist: rainbow-rb-sdk==0.1.2.dev4
13
+ Requires-Dist: rainbow-rb-log==0.1.2.dev4
14
14
 
15
15
  # rb_flow_manager 사용 설명서
16
16
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rainbow-rb-flow-manager"
7
- version = "0.1.2.dev2"
7
+ version = "0.1.2.dev4"
8
8
  requires-python = ">=3.11,<3.14"
9
9
  description = "Rainbow Python Flow Manager"
10
10
  authors = [
@@ -13,12 +13,12 @@ authors = [
13
13
  readme = "README.md"
14
14
 
15
15
  dependencies = [
16
- "rainbow-rb-utils==0.1.2.dev2",
17
- "rainbow-rb-zenoh==0.1.2.dev2",
18
- "rainbow-rb-flat-buffers==0.1.2.dev2",
19
- "rainbow-rb-schemas==0.1.2.dev2",
20
- "rainbow-rb-sdk==0.1.2.dev2",
21
- "rainbow-rb-log==0.1.2.dev2",
16
+ "rainbow-rb-utils==0.1.2.dev4",
17
+ "rainbow-rb-zenoh==0.1.2.dev4",
18
+ "rainbow-rb-flat-buffers==0.1.2.dev4",
19
+ "rainbow-rb-schemas==0.1.2.dev4",
20
+ "rainbow-rb-sdk==0.1.2.dev4",
21
+ "rainbow-rb-log==0.1.2.dev4",
22
22
  ]
23
23
 
24
24
  [tool.setuptools]
@@ -16,9 +16,8 @@ from multiprocessing.synchronize import (
16
16
  )
17
17
  from typing import Any, Literal, cast
18
18
 
19
- from rainbow.rb_sdk.base import RBBaseSDK
20
- from rainbow.rb_sdk.rb_sdk_cpp import RBAmrSDK, RBManipulateSDK, RBProgramSDK
21
- from rainbow.rb_utils.flow_manager import eval_value, make_builtins_allow_most
19
+ from rainbow.rb_sdk import RBAmrSDK, RBBaseSDK, RBManipulateSDK, RBProgramSDK
20
+ from rainbow.rb_utils.flow_manager import collect_rb_globals, eval_value, make_builtins_allow_most
22
21
  from rainbow.rb_utils.parser import t_to_dict
23
22
 
24
23
  from .exception import BreakFolder, StopExecution, SubTaskHaltException
@@ -135,9 +134,17 @@ class ExecutionContext:
135
134
  """
136
135
  code = compile(contents, "<custom_script>", "exec")
137
136
 
138
- script_globals = {"__builtins__": make_builtins_allow_most(), **script_locals}
137
+ script_globals = {
138
+ "__builtins__": make_builtins_allow_most(),
139
+ **self.program_aliases, # P_0, P_1, ... → process_id
140
+ **script_locals,
141
+ }
139
142
  exec(code, script_globals) # pylint: disable=exec-used
140
143
 
144
+ @property
145
+ def program_aliases(self) -> dict[str, str]:
146
+ return dict((getattr(self, "state_dict", None) or {}).get("program_aliases") or {})
147
+
141
148
  def _resolve_variable_owner_state(self) -> ExecutorState:
142
149
  if self.parent_process_id is not None and self.shared_state_dicts is not None:
143
150
  parent_state = self.shared_state_dicts.get(self.parent_process_id)
@@ -157,7 +164,7 @@ class ExecutionContext:
157
164
  normalized[key] = {"value": payload.get("value"), "type": var_type}
158
165
  return normalized
159
166
 
160
- def eval_value(self, value: Any, use_get_var: bool = False):
167
+ def eval_value(self, value: Any):
161
168
  """
162
169
  표현식 평가의 단일 진입점.
163
170
  읽기는 매번 owner state에서 최신값을, 쓰기(aa = 3, aa += 1)는 owner state까지 반영한다.
@@ -171,8 +178,9 @@ class ExecutionContext:
171
178
  result = eval_value(
172
179
  value,
173
180
  variables=self.variables,
174
- get_global_variable=self.get_global_variable,
175
- get_var=self.get_var if use_get_var else None,
181
+ get_global_variable=self.global_variable_getter(value),
182
+ get_var=self.get_var,
183
+ update_var=self.update_var,
176
184
  on_write=lambda name, scope: (
177
185
  written_global if scope == "global" else written_local
178
186
  ).add(name),
@@ -251,32 +259,80 @@ class ExecutionContext:
251
259
  if category is None or robot_model is None:
252
260
  return None
253
261
 
254
- try:
255
- if category == "manipulate":
256
- fn = self.get_sdk_function("rb_manipulate_sdk.get_data.get_variable")
262
+ if category not in ("manipulate", "amr"):
263
+ return None
257
264
 
258
- res = fn(robot_model=robot_model, name=var_name) if fn is not None else None
265
+ res = self._fetch_core_variable(category, robot_model, var_name)
266
+ if res is None:
267
+ raise RuntimeError(f"Failed to get global variable: {var_name}")
259
268
 
260
- if res is None:
261
- raise RuntimeError(f"Failed to get global variable: {var_name}")
269
+ self.update_global_variables({var_name: res})
262
270
 
263
- self.update_global_variables({var_name: res})
271
+ return res
264
272
 
265
- return res
266
- elif category == "amr":
267
- # SLAMNAV2랑 얘기해서 추가해야함 (RB_로 시작하는 global variable)
268
- fn = self.get_sdk_function("rb_amr_sdk.status.get_variable")
273
+ def global_variable_getter(self, value: Any) -> Callable[[str], Any]:
274
+ """RB_ 이름이 여러 개면 한 번에 선조회한 값을 쓰는 getter. 못 읽은 이름은 개별 조회."""
275
+ prefetched = self._prefetch_global_variables(value)
276
+ if not prefetched:
277
+ return self.get_global_variable
269
278
 
270
- res = fn(robot_model, var_name) if fn is not None else None
279
+ return lambda name: (
280
+ prefetched[name] if name in prefetched else self.get_global_variable(name)
281
+ )
271
282
 
272
- if res is None:
273
- raise RuntimeError(f"Failed to get global variable: {var_name}")
283
+ def _prefetch_global_variables(self, value: Any) -> dict[str, Any]:
284
+ """RB_ 이름 2개 이상이면 get_core_data_multi 로 한 번에 읽는다. 실패하면 {}."""
285
+ names = collect_rb_globals(value)
286
+ if len(names) < 2:
287
+ return {}
274
288
 
275
- self.update_global_variables({var_name: res})
289
+ robot_model = self.state_dict.get("robot_model", None)
290
+ category = self.state_dict.get("category", None)
291
+ if category is None or robot_model is None:
292
+ return {}
276
293
 
277
- return res
278
- except Exception as e:
279
- raise e
294
+ fetched = self._fetch_core_variables_multi(category, robot_model, names)
295
+ if not fetched:
296
+ return {}
297
+
298
+ self.update_global_variables(fetched)
299
+ return fetched
300
+
301
+ def _fetch_core_variables_multi(self, category: Any, robot_model: Any, names: list[str]) -> dict[str, Any]:
302
+ """get_core_data_multi 1회. SDK 에 없거나(구버전/amr) core 가 안 띄웠으면 {} → 개별 조회.
303
+ queryable 이 없으면 ZenohNoReply 가 즉시 나므로 미지원을 기억할 필요 없다."""
304
+ if category == "manipulate":
305
+ fn = self.get_sdk_function("rb_manipulate_sdk.get_data.get_variables")
306
+ elif category == "amr":
307
+ return {} # amr 은 아직 multi 없음
308
+ else:
309
+ return {}
310
+ if fn is None:
311
+ return {}
312
+
313
+ try:
314
+ res = fn(robot_model=robot_model, names=names)
315
+ except Exception:
316
+ return {}
317
+
318
+ if not isinstance(res, dict):
319
+ return {}
320
+
321
+ # valid=0(None) 은 캐시하지 않고 개별 조회로 넘긴다
322
+ return {name: value for name, value in res.items() if value is not None}
323
+
324
+ def _fetch_core_variable(self, category: Any, robot_model: Any, var_name: str) -> Any:
325
+ """대상 로봇 core(get_core_data)에서 RB_ 변수 라이브 조회. 다른 category/실패 시 None."""
326
+ if robot_model is None:
327
+ return None
328
+ if category == "manipulate":
329
+ fn = self.get_sdk_function("rb_manipulate_sdk.get_data.get_variable")
330
+ return fn(robot_model=robot_model, name=var_name) if fn is not None else None
331
+ if category == "amr":
332
+ # SLAMNAV2랑 얘기해서 추가해야함 (RB_로 시작하는 global variable)
333
+ fn = self.get_sdk_function("rb_amr_sdk.status.get_variable")
334
+ return fn(robot_model, var_name) if fn is not None else None
335
+ return None
280
336
 
281
337
  def merge_variable_meta(self, name: str, meta: dict[str, Any]) -> None:
282
338
  """변수 entry 의 meta 에 merge. value 는 건드리지 않는다 (dict 값은 얕게 merge)."""
@@ -314,8 +370,17 @@ class ExecutionContext:
314
370
  continue
315
371
  merge_variable_entries(sd, variables, meta_by_name=meta_by_name)
316
372
 
317
- def get_var(self, pid_or_robot_model: str, var_name: str, default: Any = _MISSING) -> Any:
373
+ def get_var(
374
+ self, pid_or_robot_model: str, var_name: Any = _MISSING, default: Any = _MISSING
375
+ ) -> Any:
376
+ """get_var(name[, default=]) = 자기 변수, get_var(pid|robot_model|plugin_id, name[, default])."""
377
+ if var_name is _MISSING:
378
+ return self._read_state_var(
379
+ self._resolve_variable_owner_state(), pid_or_robot_model, default, "self"
380
+ )
381
+
318
382
  all_dicts = self.shared_state_dicts or {}
383
+ pid_or_robot_model = self.program_aliases.get(pid_or_robot_model, pid_or_robot_model)
319
384
 
320
385
  # pid 직접 매치 우선, 없으면 robot_model로 탐색
321
386
  target_state = all_dicts.get(pid_or_robot_model)
@@ -338,17 +403,77 @@ class ExecutionContext:
338
403
  return default
339
404
  raise RuntimeError(f"get_var: process '{pid_or_robot_model}' not found")
340
405
 
341
- raw_vars: dict[str, Any] = dict(target_state.get("variables", {}))
342
- entry = raw_vars.get(var_name)
406
+ return self._read_state_var(target_state, var_name, default, pid_or_robot_model)
407
+
408
+ def _read_state_var(self, state: Any, var_name: Any, default: Any, label: str) -> Any:
409
+ # RB_ 는 core 라이브 조회 우선, 실패 시 state 캐시 폴백 (safe_eval_expr 와 동일)
410
+ live_err: Exception | None = None
411
+ if isinstance(var_name, str) and var_name.startswith("RB_"):
412
+ try:
413
+ live = self._fetch_core_variable(
414
+ state.get("category"), state.get("robot_model"), var_name
415
+ )
416
+ except Exception as e: # noqa: BLE001
417
+ live, live_err = None, e
418
+ if live is not None:
419
+ return live
420
+
421
+ entry = dict(state.get("variables", {})).get(var_name)
343
422
  if entry is None:
344
423
  if default is not _MISSING:
345
424
  return default
346
425
  raise RuntimeError(
347
- f"get_var: variable '{var_name}' not found in '{pid_or_robot_model}'"
348
- )
426
+ f"get_var: variable '{var_name}' not found in '{label}'"
427
+ ) from live_err
349
428
 
350
429
  return entry.get("value") if isinstance(entry, dict) else entry
351
430
 
431
+ def update_var(self, target: Any, variables: dict[str, Any] | None = None) -> None:
432
+ """update({...}) = 자기, update(pid|robot_model|plugin_id, {...}) = 다른 프로그램.
433
+
434
+ plugin_id 는 플러그인 set_variable 과 동일(RBP_ 만, 전체 main + writer 기록).
435
+ """
436
+ if variables is None:
437
+ self.update_local_variables(target)
438
+ return
439
+
440
+ variables = dict(variables)
441
+ all_dicts = self.shared_state_dicts or {}
442
+ target = self.program_aliases.get(target, target)
443
+ own_pid = (
444
+ self.parent_process_id if self.parent_process_id in all_dicts else self.process_id
445
+ )
446
+ active_mains = [
447
+ pid
448
+ for pid, sd in all_dicts.items()
449
+ if sd.get("parent_process_id") is None and sd.get("state") in VARIABLE_TARGET_STATES
450
+ ]
451
+ meta_by_name: dict[str, dict[str, Any]] | None = None
452
+
453
+ if target in all_dicts:
454
+ parent = all_dicts[target].get("parent_process_id")
455
+ pids = [parent if parent in all_dicts else target]
456
+ elif any(sd.get("robot_model") == target for sd in all_dicts.values()):
457
+ pids = [pid for pid in active_mains if all_dicts[pid].get("robot_model") == target]
458
+ else:
459
+ bad = [name for name in variables if not str(name).startswith("RBP_")]
460
+ if bad:
461
+ raise ValueError(
462
+ f"update: '{target}' is not a running robot model; "
463
+ f"plugin variable must start with 'RBP_': {bad}"
464
+ )
465
+ pids = active_mains
466
+ meta_by_name = {name: {"writers": {target: v}} for name, v in variables.items()}
467
+
468
+ for pid in pids:
469
+ if pid == own_pid:
470
+ # 자기 트리는 캐시/dirty 경로로 즉시 반영
471
+ self.update_local_variables(variables)
472
+ for name, meta in (meta_by_name or {}).items():
473
+ self.merge_variable_meta(name, meta)
474
+ else:
475
+ merge_variable_entries(all_dicts[pid], variables, meta_by_name=meta_by_name)
476
+
352
477
  def _ensure_sdk_roots(self):
353
478
  if self._sdk_roots is not None:
354
479
  return
@@ -837,6 +962,10 @@ class ExecutionContext:
837
962
  with contextlib.suppress(Exception):
838
963
  RBBaseSDK.close_all_for_pid()
839
964
 
965
+ # AMR 은 아직 Python SDK 라 C++ 레지스트리가 아니라 Python RBBaseSDK._instances 에 있다.
966
+ with contextlib.suppress(Exception):
967
+ RBAmrSDK.close_all_for_pid()
968
+
840
969
  def close(self):
841
970
  """안전하게 close"""
842
971
  self._safe_close_sdk()
@@ -114,6 +114,24 @@ def _build_executor_state(
114
114
  }
115
115
 
116
116
 
117
+ def _declared_variables(step: Step | None) -> dict[str, dict[str, Any]]:
118
+ """트리의 set_variables 스텝 init_value 수집."""
119
+ out: dict[str, dict[str, Any]] = {}
120
+
121
+ def walk(s: Any):
122
+ if getattr(s, "disabled", False):
123
+ return
124
+ if getattr(s, "func_name", None) == "rb_program_sdk.set_variables":
125
+ for v in (getattr(s, "args", None) or {}).get("variables") or []:
126
+ if isinstance(v, dict) and isinstance(v.get("name"), str):
127
+ out.setdefault(v["name"], {"value": v.get("init_value"), "type": "local"})
128
+ for c in getattr(s, "children", None) or []:
129
+ walk(c)
130
+
131
+ walk(step)
132
+ return out
133
+
134
+
117
135
  def _apply_executor_state(state_dict: MutableMapping[str, Any], state: ExecutorState):
118
136
  for key, value in state.items():
119
137
  state_dict[key] = value
@@ -245,12 +263,19 @@ def _execute_tree_in_process(
245
263
  state_dict["state"] = RB_Flow_Manager_ProgramState.ERROR
246
264
  state_dict["error"] = msg
247
265
 
248
- def emit_error_or_stop(step_id: str, msg: str):
249
- if ctx is None:
250
- return
251
- if "Execution stopped by user" in msg:
266
+ def mark_stopped(step_id: str):
267
+ state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
268
+ state_dict["stop_requested"] = True
269
+ # 이미 알려진 정지(부모 stop()/ctx.stop())는 되쏘지 않는다
270
+ if ctx is not None and not ctx.stop_event.is_set():
252
271
  ctx.emit_stop(step_id)
253
- else:
272
+
273
+ def set_error_or_stop(step_id: str, msg: str):
274
+ if "Execution stopped by user" in msg:
275
+ mark_stopped(step_id)
276
+ return
277
+ set_error(msg)
278
+ if ctx is not None:
254
279
  ctx.emit_error(step_id, RuntimeError(msg))
255
280
 
256
281
  def init_state():
@@ -519,15 +544,11 @@ def _execute_tree_in_process(
519
544
 
520
545
  except StopExecution:
521
546
  # stop 요청 상태로 마킹 (post_step는 정책상 실행 가능)
522
- state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
523
- state_dict["stop_requested"] = True
524
- if ctx is not None:
525
- ctx.emit_stop(step.step_id)
547
+ mark_stopped(step.step_id)
526
548
 
527
549
  except (RuntimeError, ValueError) as e:
528
550
  # post_step 유무와 무관하게 항상 ERROR 로 표시한다. STOPPED 는 이미 그렇다.
529
- set_error(str(e))
530
- emit_error_or_stop(step.step_id, str(e))
551
+ set_error_or_stop(step.step_id, str(e))
531
552
 
532
553
  except JumpToStepException:
533
554
  # 메인 루프 바깥으로 점프 예외가 올라오는 경우는 무시
@@ -548,14 +569,10 @@ def _execute_tree_in_process(
548
569
  pass
549
570
 
550
571
  except StopExecution:
551
- state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
552
- state_dict["stop_requested"] = True
553
- if ctx is not None:
554
- ctx.emit_stop(step.step_id)
572
+ mark_stopped(step.step_id)
555
573
 
556
574
  except (RuntimeError, ValueError) as e:
557
- set_error(str(e))
558
- emit_error_or_stop(step.step_id, str(e))
575
+ set_error_or_stop(step.step_id, str(e))
559
576
 
560
577
  except JumpToStepException as e:
561
578
  raise RuntimeError("JumpToStep: Post program tree is not allowed.") from e
@@ -947,6 +964,32 @@ class ScriptExecutor:
947
964
  if self.controller is not None:
948
965
  self.controller.on_all_resume()
949
966
 
967
+ def _apply_stop_transition(
968
+ self,
969
+ process_id: str,
970
+ *,
971
+ step_id: str | None,
972
+ sub_task: tuple[str | None, str | None],
973
+ ):
974
+ self.state_dicts[process_id]["state"] = RB_Flow_Manager_ProgramState.STOPPED
975
+ self.state_dicts[process_id]["ignore_pause"] = False
976
+
977
+ sub_task_id, sub_task_type = sub_task
978
+ if step_id is not None:
979
+ if self._on_stop is not None:
980
+ self._on_stop(process_id, step_id, sub_task_id, sub_task_type)
981
+ if self.controller is not None:
982
+ self.controller.on_stop(process_id, step_id, sub_task_id, sub_task_type)
983
+
984
+ if all(
985
+ self.state_dicts[pid].get("state") == RB_Flow_Manager_ProgramState.STOPPED
986
+ for pid in self.processes
987
+ ):
988
+ if self._on_all_stop is not None:
989
+ self._on_all_stop()
990
+ if self.controller is not None:
991
+ self.controller.on_all_stop()
992
+
950
993
  def _ensure_pre_done_event(self, process_id: str, pre_step: Step | None) -> EventType:
951
994
  """
952
995
  pre_step 완료 신호를 만든다(이미 있으면 재사용).
@@ -1006,11 +1049,6 @@ class ScriptExecutor:
1006
1049
 
1007
1050
  self._sync_state[flag] = st
1008
1051
 
1009
- print(
1010
- f"[SyncRegistry] Registered {process_id} to flag '{flag}', "
1011
- f"parties_next: {st['parties_next']}"
1012
- )
1013
-
1014
1052
  return flags
1015
1053
 
1016
1054
  def _unregister_process_sync_flags(self, process_id: str):
@@ -1662,8 +1700,15 @@ class ScriptExecutor:
1662
1700
  self.state_dicts[process_id]["state"] = RB_Flow_Manager_ProgramState.RUNNING
1663
1701
  self.state_dicts[process_id]["current_step_id"] = None
1664
1702
  self.state_dicts[process_id]["ignore_pause"] = False
1665
- # process_id 재사용 이전 실행의 변수 캐시가 남지 않도록 초기화
1666
- self.state_dicts[process_id]["variables"] = {}
1703
+ # 이전 실행 캐시 제거. main Variables 스텝 init_value 선등록 (Begin 전에도 타 프로세스가 읽게)
1704
+ self.state_dicts[process_id]["variables"] = (
1705
+ {} if parent_process_id is not None else _declared_variables(step)
1706
+ )
1707
+ # P_0, P_1, ... → process_id (main task 순서). 나중에 spawn 되는 자식은 부모 것 상속
1708
+ aliases = args.get("program_aliases")
1709
+ if not aliases and parent_process_id is not None:
1710
+ aliases = self._state_or_empty(parent_process_id).get("program_aliases")
1711
+ self.state_dicts[process_id]["program_aliases"] = dict(aliases or {"P_0": process_id})
1667
1712
 
1668
1713
 
1669
1714
  if invoke_callbacks:
@@ -1763,6 +1808,7 @@ class ScriptExecutor:
1763
1808
  batch_group_id = f"batch-{int(time.time() * 1000)}-{self._run_generation + 1}"
1764
1809
  parent_pids = [arg["process_id"] for arg in batch if arg.get("parent_process_id") is None]
1765
1810
  self._batch_group_parents[batch_group_id] = set(parent_pids)
1811
+ program_aliases = {f"P_{i}": pid for i, pid in enumerate(parent_pids)}
1766
1812
  self._batch_parent_wait_round[batch_group_id] = {}
1767
1813
  self._batch_group_released_round[batch_group_id] = 0
1768
1814
  self._batch_group_child_templates[batch_group_id] = {}
@@ -1789,6 +1835,7 @@ class ScriptExecutor:
1789
1835
  pid = arg["process_id"]
1790
1836
  arg["batch_group_id"] = batch_group_id
1791
1837
  arg["batch_round_sync_parent"] = True
1838
+ arg["program_aliases"] = program_aliases
1792
1839
  if arg.get("parent_process_id") is not None:
1793
1840
  arg["batch_round_sync_parent"] = False
1794
1841
  # 배치 자식은 라운드마다 재생성되므로 repeat_count는 1회로 고정한다.
@@ -2027,8 +2074,8 @@ class ScriptExecutor:
2027
2074
  )
2028
2075
 
2029
2076
  elif evt_type == "stop":
2030
- self.state_dicts[pid]["state"] = RB_Flow_Manager_ProgramState.STOPPED
2031
- self.state_dicts[pid]["ignore_pause"] = False
2077
+ # 워커가 낸 정지(HALT 스텝 등). 부모 stop() 발 정지는 워커가 되쏘지 않는다
2078
+ self._apply_stop_transition(pid, step_id=step_id, sub_task=(sub_task_id, sub_task_type))
2032
2079
 
2033
2080
  elif evt_type == "round_wait":
2034
2081
  group_id = evt.get("group_id")
@@ -2693,28 +2740,14 @@ class ScriptExecutor:
2693
2740
 
2694
2741
  self.pause_events[process_id].clear()
2695
2742
 
2696
- step_id = self.state_dicts[process_id].get("current_step_id")
2697
- self.state_dicts[process_id]["state"] = RB_Flow_Manager_ProgramState.STOPPED
2698
- self.state_dicts[process_id]["ignore_pause"] = False
2699
-
2700
2743
  self.stop_events[process_id].set()
2701
2744
 
2702
- if step_id is not None:
2703
- sub_task_id, sub_task_type = self._current_sub_task_info(process_id)
2704
- if self._on_stop is not None:
2705
- self._on_stop(process_id, step_id, sub_task_id, sub_task_type)
2706
- if self.controller is not None:
2707
- self.controller.on_stop(process_id, step_id, sub_task_id, sub_task_type)
2708
-
2709
-
2710
- if all(
2711
- self.state_dicts[pid].get("state") == RB_Flow_Manager_ProgramState.STOPPED
2712
- for pid in self.processes
2713
- ):
2714
- if self._on_all_stop is not None:
2715
- self._on_all_stop()
2716
- if self.controller is not None:
2717
- self.controller.on_all_stop()
2745
+ step_id = self.state_dicts[process_id].get("current_step_id")
2746
+ self._apply_stop_transition(
2747
+ process_id,
2748
+ step_id=step_id,
2749
+ sub_task=self._current_sub_task_info(process_id),
2750
+ )
2718
2751
 
2719
2752
  print("Process stopped", flush=True)
2720
2753
 
@@ -420,7 +420,7 @@ class Step:
420
420
  if self.done_script is not None:
421
421
  print(f"[{ctx.process_id}] executing done_script for step '{self.name}'", flush=True)
422
422
  if isinstance(self.done_script, str):
423
- ctx.eval_value(self.done_script, use_get_var=True)
423
+ ctx.eval_value(self.done_script)
424
424
  else:
425
425
  self.done_script()
426
426
 
@@ -454,6 +454,9 @@ class Step:
454
454
  eval_args = {}
455
455
 
456
456
  try:
457
+ # args/variable 의 RB_ 전역 변수를 한 번에 선조회 (core 쿼리 1번)
458
+ get_global_variable = ctx.global_variable_getter((args, self.variable))
459
+
457
460
  for k, v in args.items():
458
461
  if self.not_ast_eval or k.startswith("_"):
459
462
  eval_args[k] = v
@@ -461,7 +464,7 @@ class Step:
461
464
  eval_args[k] = eval_value(
462
465
  v,
463
466
  variables=ctx.variables,
464
- get_global_variable=ctx.get_global_variable,
467
+ get_global_variable=get_global_variable,
465
468
  get_var=ctx.get_var,
466
469
  )
467
470
 
@@ -483,7 +486,7 @@ class Step:
483
486
  v = eval_value(
484
487
  v,
485
488
  variables=ctx.variables,
486
- get_global_variable=ctx.get_global_variable,
489
+ get_global_variable=get_global_variable,
487
490
  get_var=ctx.get_var,
488
491
  )
489
492
 
@@ -807,7 +810,7 @@ class RepeatStep(Step):
807
810
  elif self.while_cond is not None:
808
811
  if isinstance(self.while_cond, str):
809
812
  while landing_target_step_id is not None or ctx.eval_value(
810
- self.while_cond, use_get_var=True
813
+ self.while_cond
811
814
  ):
812
815
  try:
813
816
  self._pre_execute(ctx, is_skip=landing_is_skip)
@@ -860,7 +863,7 @@ class RepeatStep(Step):
860
863
  landing_is_skip = False
861
864
  continue
862
865
  if isinstance(self.do_while, str):
863
- if not ctx.eval_value(self.do_while, use_get_var=True):
866
+ if not ctx.eval_value(self.do_while):
864
867
  break
865
868
  else:
866
869
  if not self.do_while(ctx):
@@ -1030,7 +1033,7 @@ class ConditionStep(Step):
1030
1033
  parent_switch_value = ctx.lookup("switch_value")
1031
1034
  condition_expr = condition_expr.replace("$parent.switch_value", parent_switch_value)
1032
1035
 
1033
- if ctx.eval_value(condition_expr, use_get_var=True):
1036
+ if ctx.eval_value(condition_expr):
1034
1037
  post_execute_and_execute_children()
1035
1038
  return
1036
1039
 
@@ -1692,11 +1695,6 @@ class SyncStep(Step):
1692
1695
  state["arrived"] = arrived
1693
1696
  ctx.sync_state[flag] = state
1694
1697
 
1695
- print(
1696
- f"[Sync {flag}] {pid} arrived ({arrived}/{parties_cur}) phase={current_phase}",
1697
- flush=True,
1698
- )
1699
-
1700
1698
  # 마지막 도착자
1701
1699
  if arrived >= parties_cur:
1702
1700
  # 마지막 도착자
@@ -1734,9 +1732,6 @@ class SyncStep(Step):
1734
1732
 
1735
1733
  # phase가 넘어갔으면 해제
1736
1734
  if updated_phase > current_phase:
1737
- print(
1738
- f"[Sync {flag}] {pid} released from phase {current_phase}", flush=True
1739
- )
1740
1735
  break
1741
1736
 
1742
1737
  # 중간에 parties가 줄어서(프로세스 제거) 프로세스가 사실상 마지막이 된 경우 처리
@@ -1757,17 +1752,9 @@ class SyncStep(Step):
1757
1752
  updated["parties_next"] = next_parties
1758
1753
  ctx.sync_state[flag] = updated
1759
1754
 
1760
- print(
1761
- f"[Sync {flag}] {pid} becoming last due to parties reduction, "
1762
- f"phase {updated_phase} -> {updated_phase + 1}, "
1763
- f"parties: {updated_parties_cur} -> {next_parties}",
1764
- flush=True,
1765
- )
1766
1755
  ctx.sync_cond.notify_all()
1767
1756
  break
1768
1757
 
1769
- print(f"[Sync {flag}] {pid} exiting sync", flush=True)
1770
-
1771
1758
  finally:
1772
1759
  if acquired:
1773
1760
  ctx.sync_cond.release()
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainbow-rb-flow-manager
3
- Version: 0.1.2.dev2
3
+ Version: 0.1.2.dev4
4
4
  Summary: Rainbow Python Flow Manager
5
5
  Author-email: Derek <dfd1123@rainbow-robotics.com>
6
6
  Requires-Python: <3.14,>=3.11
7
7
  Description-Content-Type: text/markdown
8
- Requires-Dist: rainbow-rb-utils==0.1.2.dev2
9
- Requires-Dist: rainbow-rb-zenoh==0.1.2.dev2
10
- Requires-Dist: rainbow-rb-flat-buffers==0.1.2.dev2
11
- Requires-Dist: rainbow-rb-schemas==0.1.2.dev2
12
- Requires-Dist: rainbow-rb-sdk==0.1.2.dev2
13
- Requires-Dist: rainbow-rb-log==0.1.2.dev2
8
+ Requires-Dist: rainbow-rb-utils==0.1.2.dev4
9
+ Requires-Dist: rainbow-rb-zenoh==0.1.2.dev4
10
+ Requires-Dist: rainbow-rb-flat-buffers==0.1.2.dev4
11
+ Requires-Dist: rainbow-rb-schemas==0.1.2.dev4
12
+ Requires-Dist: rainbow-rb-sdk==0.1.2.dev4
13
+ Requires-Dist: rainbow-rb-log==0.1.2.dev4
14
14
 
15
15
  # rb_flow_manager 사용 설명서
16
16
 
@@ -0,0 +1,6 @@
1
+ rainbow-rb-utils==0.1.2.dev4
2
+ rainbow-rb-zenoh==0.1.2.dev4
3
+ rainbow-rb-flat-buffers==0.1.2.dev4
4
+ rainbow-rb-schemas==0.1.2.dev4
5
+ rainbow-rb-sdk==0.1.2.dev4
6
+ rainbow-rb-log==0.1.2.dev4
@@ -1,6 +0,0 @@
1
- rainbow-rb-utils==0.1.2.dev2
2
- rainbow-rb-zenoh==0.1.2.dev2
3
- rainbow-rb-flat-buffers==0.1.2.dev2
4
- rainbow-rb-schemas==0.1.2.dev2
5
- rainbow-rb-sdk==0.1.2.dev2
6
- rainbow-rb-log==0.1.2.dev2