rainbow-rb-flow-manager 0.1.2.dev3__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.dev3 → rainbow_rb_flow_manager-0.1.2.dev4}/PKG-INFO +7 -7
  2. {rainbow_rb_flow_manager-0.1.2.dev3 → rainbow_rb_flow_manager-0.1.2.dev4}/pyproject.toml +7 -7
  3. {rainbow_rb_flow_manager-0.1.2.dev3 → rainbow_rb_flow_manager-0.1.2.dev4}/src/rainbow/rb_flow_manager/context.py +154 -28
  4. {rainbow_rb_flow_manager-0.1.2.dev3 → 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.dev3 → 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.dev3 → 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.dev3/src/rainbow_rb_flow_manager.egg-info/requires.txt +0 -6
  9. {rainbow_rb_flow_manager-0.1.2.dev3 → rainbow_rb_flow_manager-0.1.2.dev4}/README.md +0 -0
  10. {rainbow_rb_flow_manager-0.1.2.dev3 → rainbow_rb_flow_manager-0.1.2.dev4}/setup.cfg +0 -0
  11. {rainbow_rb_flow_manager-0.1.2.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3 → 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.dev3
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.dev3
9
- Requires-Dist: rainbow-rb-zenoh==0.1.2.dev3
10
- Requires-Dist: rainbow-rb-flat-buffers==0.1.2.dev3
11
- Requires-Dist: rainbow-rb-schemas==0.1.2.dev3
12
- Requires-Dist: rainbow-rb-sdk==0.1.2.dev3
13
- Requires-Dist: rainbow-rb-log==0.1.2.dev3
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.dev3"
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.dev3",
17
- "rainbow-rb-zenoh==0.1.2.dev3",
18
- "rainbow-rb-flat-buffers==0.1.2.dev3",
19
- "rainbow-rb-schemas==0.1.2.dev3",
20
- "rainbow-rb-sdk==0.1.2.dev3",
21
- "rainbow-rb-log==0.1.2.dev3",
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]
@@ -17,7 +17,7 @@ from multiprocessing.synchronize import (
17
17
  from typing import Any, Literal, cast
18
18
 
19
19
  from rainbow.rb_sdk import RBAmrSDK, RBBaseSDK, RBManipulateSDK, RBProgramSDK
20
- from rainbow.rb_utils.flow_manager import eval_value, make_builtins_allow_most
20
+ from rainbow.rb_utils.flow_manager import collect_rb_globals, eval_value, make_builtins_allow_most
21
21
  from rainbow.rb_utils.parser import t_to_dict
22
22
 
23
23
  from .exception import BreakFolder, StopExecution, SubTaskHaltException
@@ -134,9 +134,17 @@ class ExecutionContext:
134
134
  """
135
135
  code = compile(contents, "<custom_script>", "exec")
136
136
 
137
- 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
+ }
138
142
  exec(code, script_globals) # pylint: disable=exec-used
139
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
+
140
148
  def _resolve_variable_owner_state(self) -> ExecutorState:
141
149
  if self.parent_process_id is not None and self.shared_state_dicts is not None:
142
150
  parent_state = self.shared_state_dicts.get(self.parent_process_id)
@@ -156,7 +164,7 @@ class ExecutionContext:
156
164
  normalized[key] = {"value": payload.get("value"), "type": var_type}
157
165
  return normalized
158
166
 
159
- def eval_value(self, value: Any, use_get_var: bool = False):
167
+ def eval_value(self, value: Any):
160
168
  """
161
169
  표현식 평가의 단일 진입점.
162
170
  읽기는 매번 owner state에서 최신값을, 쓰기(aa = 3, aa += 1)는 owner state까지 반영한다.
@@ -170,8 +178,9 @@ class ExecutionContext:
170
178
  result = eval_value(
171
179
  value,
172
180
  variables=self.variables,
173
- get_global_variable=self.get_global_variable,
174
- 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,
175
184
  on_write=lambda name, scope: (
176
185
  written_global if scope == "global" else written_local
177
186
  ).add(name),
@@ -250,32 +259,80 @@ class ExecutionContext:
250
259
  if category is None or robot_model is None:
251
260
  return None
252
261
 
253
- try:
254
- if category == "manipulate":
255
- fn = self.get_sdk_function("rb_manipulate_sdk.get_data.get_variable")
262
+ if category not in ("manipulate", "amr"):
263
+ return None
256
264
 
257
- 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}")
258
268
 
259
- if res is None:
260
- raise RuntimeError(f"Failed to get global variable: {var_name}")
269
+ self.update_global_variables({var_name: res})
261
270
 
262
- self.update_global_variables({var_name: res})
271
+ return res
263
272
 
264
- return res
265
- elif category == "amr":
266
- # SLAMNAV2랑 얘기해서 추가해야함 (RB_로 시작하는 global variable)
267
- 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
278
+
279
+ return lambda name: (
280
+ prefetched[name] if name in prefetched else self.get_global_variable(name)
281
+ )
282
+
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 {}
268
288
 
269
- res = fn(robot_model, var_name) if fn is not None else None
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 {}
270
293
 
271
- if res is None:
272
- raise RuntimeError(f"Failed to get global variable: {var_name}")
294
+ fetched = self._fetch_core_variables_multi(category, robot_model, names)
295
+ if not fetched:
296
+ return {}
273
297
 
274
- self.update_global_variables({var_name: res})
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 {}
275
312
 
276
- return res
277
- except Exception as e:
278
- raise e
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
279
336
 
280
337
  def merge_variable_meta(self, name: str, meta: dict[str, Any]) -> None:
281
338
  """변수 entry 의 meta 에 merge. value 는 건드리지 않는다 (dict 값은 얕게 merge)."""
@@ -313,8 +370,17 @@ class ExecutionContext:
313
370
  continue
314
371
  merge_variable_entries(sd, variables, meta_by_name=meta_by_name)
315
372
 
316
- 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
+
317
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)
318
384
 
319
385
  # pid 직접 매치 우선, 없으면 robot_model로 탐색
320
386
  target_state = all_dicts.get(pid_or_robot_model)
@@ -337,17 +403,77 @@ class ExecutionContext:
337
403
  return default
338
404
  raise RuntimeError(f"get_var: process '{pid_or_robot_model}' not found")
339
405
 
340
- raw_vars: dict[str, Any] = dict(target_state.get("variables", {}))
341
- 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)
342
422
  if entry is None:
343
423
  if default is not _MISSING:
344
424
  return default
345
425
  raise RuntimeError(
346
- f"get_var: variable '{var_name}' not found in '{pid_or_robot_model}'"
347
- )
426
+ f"get_var: variable '{var_name}' not found in '{label}'"
427
+ ) from live_err
348
428
 
349
429
  return entry.get("value") if isinstance(entry, dict) else entry
350
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
+
351
477
  def _ensure_sdk_roots(self):
352
478
  if self._sdk_roots is not None:
353
479
  return
@@ -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.dev3
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.dev3
9
- Requires-Dist: rainbow-rb-zenoh==0.1.2.dev3
10
- Requires-Dist: rainbow-rb-flat-buffers==0.1.2.dev3
11
- Requires-Dist: rainbow-rb-schemas==0.1.2.dev3
12
- Requires-Dist: rainbow-rb-sdk==0.1.2.dev3
13
- Requires-Dist: rainbow-rb-log==0.1.2.dev3
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.dev3
2
- rainbow-rb-zenoh==0.1.2.dev3
3
- rainbow-rb-flat-buffers==0.1.2.dev3
4
- rainbow-rb-schemas==0.1.2.dev3
5
- rainbow-rb-sdk==0.1.2.dev3
6
- rainbow-rb-log==0.1.2.dev3