diff --git a/.gitignore b/.gitignore
index da3b6cb..20d9d67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -164,3 +164,13 @@ cython_debug/
# Project-level settings
/.tgitconfig
+.python-version
+.venv
+__pycache__
+.DS_Store
+.idea
+.vscode
+.pytest_cache
+.mypy_cache
+.ruff_cache
+.tmp
diff --git a/README.md b/README.md
index 3307e0f..2fdb3c0 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,49 @@
-# alkaid_release_platform
+# 前言
-alkaid release sdk的工具
\ No newline at end of file
+这是基于Alkaid Release Platform的发布平台,用于发布SDK。
+
+# 使用说明
+
+## 项目结构
+
+## 项目依赖
+
+项目依赖于 python3.6 + py-trees + yaml 库,这两个第三方库已经porting内置到thirdparty目录下了。
+其他的 libreoffice unoconv openpyxl 会在requirements.txt中列出,如果你的环境中不存在,需要请MIS安装。
+
+## 开发环境搭建
+
+为了使得开发过程中测试方便,使用pytest进行单元测试。但是pytest并不默认被安装,所以需要手动安装。
+当你需要测试时,请在L0将G:\ekko.bao\pyenv.tgz 目录拷贝到本地,并解压到$HOME目录下。
+然后在终端内source该文件,即可使用pytest进行单元测试。
+
+```bash
+# 设置pyenv环境变量
+export PYENV_ROOT="$HOME/.pyenv"
+export PATH="$PYENV_ROOT/bin:$PATH"
+
+# 初始化pyenv
+eval "$(pyenv init -)"
+
+# 初始化pyenv-virtualenv
+eval "$(pyenv virtualenv-init -)"
+
+# 设置Python版本为3.6
+pyenv local 3.6
+```
+
+这样会使用3.6版本的python,并使用pyenv-virtualenv管理虚拟环境。
+当然这样会覆盖原本MIS提供的环境,如果你运行比如编译时遇到问题,请使用MIS提供的环境。
+运行 `pyenv local system` 即可恢复。
+
+## 单元测试
+
+```bash
+pytest -k tests -vv
+```
+
+## 运行
+
+```bash
+./main.py -a sss -t xxx -s xxx -b dddd -p ipv --log inf
+```
diff --git a/alkaid_release_flow_status.dot b/alkaid_release_flow_status.dot
new file mode 100644
index 0000000..4af1832
--- /dev/null
+++ b/alkaid_release_flow_status.dot
@@ -0,0 +1 @@
+None
\ No newline at end of file
diff --git a/alkaid_release_flow_status.html b/alkaid_release_flow_status.html
new file mode 100644
index 0000000..69f32d2
--- /dev/null
+++ b/alkaid_release_flow_status.html
@@ -0,0 +1,28 @@
+
SStar Alkaid Release Flow Status
+[-] AlkaidReleaseFlow [✕]
+ [-] PhaseEnv [✕]
+ [-] Pre [✓]
+ [-] Process [✕]
+ --> plugins.baseline.phase_env._PhaseEnv_Process [✕]
+ [-] Post [-]
+ [-] PhaseConfig [-]
+ [-] Pre [-]
+ [-] Process [-]
+ [-] Post [-]
+ [-] PhaseBuild [-]
+ [-] Pre [-]
+ [-] Process [-]
+ [-] Post [-]
+ [-] PhaseCheck [-]
+ [-] Pre [-]
+ [-] Process [-]
+ [-] Post [-]
+ [-] PhasePackage [-]
+ [-] Pre [-]
+ [-] Process [-]
+ [-] Post [-]
+ [-] PhaseVerify [-]
+ [-] Pre [-]
+ [-] Process [-]
+ [-] Post [-]
+
\ No newline at end of file
diff --git a/configs/customer_test.yaml b/configs/customer_test.yaml
new file mode 100644
index 0000000..c1d5a32
--- /dev/null
+++ b/configs/customer_test.yaml
@@ -0,0 +1,141 @@
+ConfigList:
+ - ipc_iford.nor.glibc-11.1.0-squashfs.ssc029a.512.bga12_ddr4_defconfig
+ - ipc-rtos_iford.spinand.glibc-11.1.0-ramdisk.ssc029a.512.bga12_ddr4_defconfig
+MiReleaseList: []
+MhalReleaseList: []
+
+RtosReleaseList:
+ - coremark
+ - common
+ - font
+ - iqserver
+ - disp_app
+ - dualos_pipeline
+ - aov_preload
+ - cm4_preload
+ - preload_rtos
+ - preload_sample
+ - lvgl
+ - usb_gadget_app
+ - aesdma
+ - bdma
+ - camclk
+ - cpufreq
+ - drvutil
+ - emac
+ - fcie
+ - flash
+ - fsp_qspi
+ - gpio
+ - i2c
+ - input
+ - int
+ - ir
+ - kernel
+ - loadns
+ - miu
+ - mmupte
+ - mspi
+ - msys
+ - padmux
+ - pwm
+ - rtc
+ - rtcpwc
+ - saradc
+ - sdmmc
+ - str
+ - timer
+ - tsensor
+ - uart
+ - voltage
+ - watchdog
+ - algo
+ - decompress
+ - freertos_plus_fat
+ - lwfs
+ - proxyfs
+ - tcpip
+ - arm
+ - riscv
+ - libc
+ - newlib_stub
+ - cam_dev_wrapper
+ - cam_drv_poll
+ - cam_fs_wrapper
+ - cam_os_wrapper
+ - cam_proc_wrapper
+ - env_util
+ - initcall
+ - loadable_module
+ - memmang
+ - memmap
+ - mempool
+ - MsWrapper
+ - newlib_stub
+ - freertos_main
+ - freertos_posix
+ - sys_I_SW
+
+
+PmRtosReleaseList:
+ - pm_aesdma
+ - pm_bdma
+ - pm_camclk
+ - pm_cpufreq
+ - pm_drvutil
+ - pm_loadns
+ - pm_emac
+ - pm_flash
+ - pm_fsp_qspi
+ - pm_gpio
+ - pm_i2c
+ - pm_int
+ - pm_ir
+ - pm_kernel
+ - pm_miu
+ - pm_mmupte
+ - pm_mspi
+ - pm_msys
+ - pm_padmux
+ - pm_pcie
+ - pm_pl011
+ - pm_mhal_pm_clk
+ - pm_pm_idle
+ - pm_mhal_pm_mbx
+ - pm_rtcpwc
+ - pm_mhal_pm_sys
+ - pm_mhal_pm_wdt
+ - pm_pspi
+ - pm_pwm
+ - pm_pm_power
+ - pm_rtc
+ - pm_rtcpwc
+ - pm_saradc
+ - pm_sdmmc
+ - pm_timer
+ - pm_tsensor
+ - pm_uart
+ - pm_voltage
+ - pm_watchdog
+ - pm_decompress
+ - pm_freertos_plus_fat
+ - pm_lwfs
+ - pm_proxyfs
+ - pm_tcpip
+ - pm_arm
+ - pm_riscv
+ - pm_cam_dev_wrapper
+ - pm_cam_drv_poll
+ - pm_cam_fs_wrapper
+ - pm_cam_os_wrapper
+ - pm_cam_proc_wrapper
+ - pm_env_util
+ - pm_initcall
+ - pm_libc
+ - pm_memmang
+ - pm_memmap
+ - pm_mempool
+ - pm_MsWrapper
+ - pm_freertos
+ - pm_freertos_posix
+ - pm_sys_I_SW
diff --git a/core/behavior_tree.py b/core/behavior_tree.py
new file mode 100644
index 0000000..63620f5
--- /dev/null
+++ b/core/behavior_tree.py
@@ -0,0 +1,214 @@
+import os, sys
+from abc import ABC, abstractmethod
+from enum import Enum
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+from core.defines import ReleaseErr
+from thirdparty.py_trees.behaviour import Behaviour
+from thirdparty.py_trees.common import Access
+from thirdparty.py_trees.composites import Selector, Parallel, Sequence, Composite
+# from thirdparty.py_trees.decorators import Decorator
+from thirdparty.py_trees import blackboard
+from thirdparty.py_trees.common import ParallelPolicy
+from thirdparty.py_trees.behaviour import Behaviour
+import typing
+from core.logger import get_logger
+
+log = get_logger()
+
+class ReleaseFlowBlackboard(blackboard.Client):
+ """
+ 重载blackboard.Client用于适配一些我们的需求
+ 每个节点访问其所需要的数据的时候都需要先进行注册,注册之后方可访问
+ """
+ def register_key(self, key: str, access: Access = Access.WRITE, required: bool=False, remap_to: str=None):
+ """
+ 重写register_key方法,用于注册key,默认具备可读可写的属性
+ """
+ super().register_key(key=key, access=access, required=required, remap_to=remap_to)
+
+ def require_key(self, key: str):
+ """
+ 强制要求key存在
+ """
+ self.register_key(key=key, access=Access.WRITE, required=True)
+
+class ReleaseFlowAction(Behaviour, ABC):
+ """
+ Release Flow 中的最小单元,其本质是一个Behaviour Tree的节点
+ 继承了py_trees.behaviour.Behaviour的接口
+ 同时针对我们的Release Flow添加了一些实现的细节
+ - setup() 方法用于初始化节点。需要主动调用
+ - process() 方法在每次节点开始执行时调用。
+ - update() 方法是节点的核心逻辑,返回节点的状态(SUCCESS, FAILURE, RUNNING)。
+ - shutdown() 方法用于清理节点。和setup()成对使用
+ """
+ def __init__(self, name: str, description: str = ""):
+ Behaviour.__init__(self, name)
+ # blackboard 是所有 flow action 共享的,每个action自己注册自己需要的key
+ self.blackboard = self.attach_blackboard_client(name=name)
+ self.description = description
+
+ # 重写attach_blackboard_client方法,用于创建ReleaseFlowBlackboard实例
+ def attach_blackboard_client(self, name: str=None, namespace: str=None
+ ) -> ReleaseFlowBlackboard:
+ if name is None:
+ count = len(self.blackboards)
+ name = self.name if (count == 0) else self.name + "-{}".format(count)
+ new_blackboard = ReleaseFlowBlackboard(
+ name=name,
+ namespace=namespace
+ )
+ self.blackboards.append(new_blackboard)
+ return new_blackboard
+
+ # 子类可以重写setup方法,用于初始化
+ def setup(self):
+ pass
+
+ @abstractmethod
+ def update(self):
+ pass
+
+ def initialise(self):
+ """
+ Behaviour 基类中每次tick都会call该函数执行实际的业务逻辑,我们为了方便理解,将名字改为process,
+ 只要子类实现了process方法,就可以在Release Flow中使用
+ """
+ try:
+ self.process()
+ except Exception as e:
+ from thirdparty.py_trees.common import Status
+ self.status = Status.FAILURE
+ log.error(f"Action {self.name} 执行失败: {str(e)}")
+
+ @abstractmethod
+ def process(self):
+ """
+ 子类实现process以用于实际的业务逻辑
+ """
+ pass
+
+def ReleaseFlowActionDecorator(cls):
+ """
+ 装饰器,用于自动生成行为树节点的名称,并自动处理__init__函数
+ 目的就是为了使得自动生成一个name,而不需要手动去写
+ 使用方法:
+ @ReleaseFlowActionDecorator
+ class MyAction(ReleaseFlowAction):
+ pass
+ """
+ # 保存原始的__init__(如果存在的话)
+ original_init = cls.__init__ if '__init__' in cls.__dict__ else None
+ # 如果不是ReleaseFlowAction的子类,则不进行装饰
+ if not issubclass(cls, ReleaseFlowAction):
+ return cls
+
+ def new_init(self, name:str=None, description:str=''):
+ if name is None:
+ name = f"{self.__module__}.{self.__class__.__name__}"
+ # 如果原始类有自定义的__init__,则调用它
+ if original_init:
+ original_init(self, name, description)
+ else:
+ super(cls, self).__init__(name, description)
+
+ cls.__init__ = new_init
+ return cls
+
+
+class ReleaseFlowBuilder:
+ """行为树构建器"""
+ def __init__(self):
+ self.current_node = None
+ self.root = None
+ self.node_stack = []
+
+ def sequence(self, name: str, description: str = "") -> 'ReleaseFlowBuilder':
+ """创建序列节点"""
+ if isinstance(name, Enum):
+ name = name.value
+ node = Sequence(name, description)
+ self._add_node(node)
+ self.node_stack.append(node)
+ return self
+
+ def selector(self, name: str, description: str = "") -> 'ReleaseFlowBuilder':
+ """创建选择节点"""
+ if isinstance(name, Enum):
+ name = name.value
+ node = Selector(name, description)
+ self._add_node(node)
+ self.node_stack.append(node)
+ return self
+
+ def parallel(self, name: str, description: str = "",
+ policy: ParallelPolicy.Base = ParallelPolicy.SuccessOnAll(),
+ children: typing.List[Behaviour] = None) -> 'ReleaseFlowBuilder':
+ """创建并行节点"""
+ if isinstance(name, Enum):
+ name = name.value
+ node = Parallel(name, policy, children)
+ self._add_node(node)
+ self.node_stack.append(node)
+ return self
+
+ def action(self, action: ReleaseFlowAction) -> 'ReleaseFlowBuilder':
+ """创建动作节点"""
+ if isinstance(action, list):
+ for a in action:
+ self._add_node(a)
+ else:
+ self._add_node(action)
+ return self
+
+ def end(self) -> 'ReleaseFlowBuilder':
+ """结束当前复合节点或装饰器节点"""
+ if self.node_stack:
+ self.node_stack.pop()
+ return self
+
+ def build(self) -> Behaviour:
+ """构建并返回行为树根节点"""
+ if not self.root:
+ raise ReleaseErr("No nodes added to behavior tree")
+
+ # 确保所有节点都已关闭
+ if self.node_stack:
+ raise ReleaseErr(f"Unclosed nodes: {[node.name for node in self.node_stack]}")
+
+ return self.root
+
+ def _add_node(self, node: Behaviour):
+ """添加节点到当前位置"""
+ if not self.root:
+ # 第一个节点作为根节点
+ self.root = node
+ self.current_node = node
+ return
+
+ if not self.node_stack:
+ # 如果没有开放的节点,直接替换根节点
+ self.root = node
+ self.current_node = node
+ return
+
+ parent = self.node_stack[-1]
+
+ # 将节点添加到父节点
+ if isinstance(parent, Composite):
+ parent.add_child(node)
+ else:
+ raise ReleaseErr(f"Parent node {parent.name} is not a Composite")
+ # elif isinstance(parent, Decorator):
+ # parent.set_child(node)
+ # # 装饰器只能有一个子节点,所以自动结束
+ # if isinstance(node, (Composite, Decorator)):
+ # # 但如果子节点是复合节点或装饰器,保持它开放
+ # pass
+ # else:
+ # self.node_stack.pop()
+ # 添加如下magic函数以用于支持with语句,可简化代码编写,依靠缩进使得层次关系更加清晰
+ def __enter__(self):
+ return self
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.end() # 结束序列
\ No newline at end of file
diff --git a/core/defines.py b/core/defines.py
new file mode 100644
index 0000000..954d76b
--- /dev/null
+++ b/core/defines.py
@@ -0,0 +1,85 @@
+from enum import Enum
+import json
+
+
+class ReleasePhase(str, Enum):
+ ENV = "PhaseEnv"
+ CONFIG = "PhaseConfig"
+ BUILD = "PhaseBuild"
+ CHECK = "PhaseCheck"
+ PACKAGE = "PhasePackage"
+ VERIFY = "PhaseVerify"
+
+
+class ProcessStep(str, Enum):
+ """
+ ReleaseFlow中每个阶段的几个执行步骤,顺序执行
+ """
+
+ PRE = "Pre"
+ PROCESS = "Process"
+ POST = "Post"
+
+
+class ReleaseErr(Exception):
+ """Release tool专用异常基类,包含错误信息和上下文"""
+
+ def __init__(self, message: str, ctx: dict = None):
+ self.message = message
+ self.ctx = ctx or {}
+ super().__init__(message)
+
+ def __str__(self):
+ return f"{self.message} (context: {self.ctx})" if self.ctx else self.message
+
+
+class Dict(dict):
+ """
+ 一个字典,支持点号访问,从xxx[xxx][xxx] 的方式转化为 xxx.xxx.xxx 的形式访问字典中的值
+ 如果key不存在,返回一个空的字典
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ # 将字典中的所有值递归转换为 Dict
+ for key, value in self.items():
+ if isinstance(value, dict):
+ self[key] = Dict(value)
+
+ @classmethod
+ def from_dict(cls, data: dict) -> "Dict":
+ """
+ 从普通字典创建一个 Dict 对象
+ :param data: 要转换的字典
+ :return: Dict 对象
+ """
+ return cls(data)
+
+ def __getattr__(self, key):
+ if key in self:
+ value = self[key]
+ # 如果值是字典,递归转换为 Dict
+ if isinstance(value, dict):
+ return Dict(value)
+ return value
+ # 如果key不存在,返回None
+ # return Dict()
+ raise AttributeError(f"In object not found '{key}'")
+
+ def __setattr__(self, key, value):
+ self[key] = value
+
+ def __str__(self):
+ string = json.dumps(self, indent=4)
+ return string
+
+ def __repr__(self):
+ return self.__str__()
+
+
+class List(list):
+ def __str__(self):
+ return json.dumps(self, indent=4)
+
+ def __repr__(self):
+ return self.__str__()
diff --git a/core/logger.py b/core/logger.py
new file mode 100644
index 0000000..1b4d82f
--- /dev/null
+++ b/core/logger.py
@@ -0,0 +1,82 @@
+import logging
+import sys
+import os
+
+TRACE = 5
+DEBUG = logging.DEBUG
+INFO = logging.INFO
+WARNING = logging.WARNING
+ERROR = logging.ERROR
+CRITICAL = logging.CRITICAL
+NOTSET = logging.NOTSET
+
+def get_logger(name=None):
+ if name is None:
+ name = 'alkaid_release_platform'
+ # 创建一个 logger
+ logger = logging.getLogger(name)
+ if logger.handlers:
+ return logger
+
+ #定义一个新的日志级别,用于输出trace信息
+ logging.TRACE = TRACE
+ logging.addLevelName(logging.TRACE, "TRACE")
+ def trace(self, message, *args, **kws):
+ self._log(logging.TRACE, message, args, **kws)
+ logging.Logger.trace = trace
+
+ logger.setLevel(INFO)
+ class LogColors:
+ RESET = "\033[0m"
+ TRACE = "\033[35m" # 紫色
+ DEBUG = "\033[34m" # 蓝色
+ INFO = "\033[32m" # 绿色
+ WARNING = "\033[33m" # 黄色
+ ERROR = "\033[31m" # 红色
+ CRITICAL = "\033[41m" # 红底白字
+
+ # 创建一个log的颜色格式化器,使得输出的log信息在控制台上有颜色区分
+ class ColoredFormatter(logging.Formatter):
+ def __init__(self, msg, use_color=True):
+ logging.Formatter.__init__(self, msg)
+ self.use_color = use_color
+ def format(self, record):
+ level_color = {
+ logging.TRACE: LogColors.TRACE,
+ logging.DEBUG: LogColors.DEBUG,
+ logging.INFO: LogColors.INFO,
+ logging.WARNING: LogColors.WARNING,
+ logging.ERROR: LogColors.ERROR,
+ logging.CRITICAL: LogColors.CRITICAL
+ }
+ color = level_color.get(record.levelno, LogColors.RESET)
+ message = super().format(record)
+ return f"{color}{message}{LogColors.RESET}" if os.isatty(sys.stdout.fileno()) else message
+
+ # 创建一个带颜色的控制台处理器
+ console_handler = logging.StreamHandler(sys.stdout)
+ console_handler.setLevel(INFO)
+ # 创建一个带颜色的格式化器并将其添加到处理器
+ formatter = ColoredFormatter('[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]: %(message)s')
+ console_handler.setFormatter(formatter)
+
+ # 将处理器添加到 logger
+ logger.addHandler(console_handler)
+
+ # 定义一个方法,用于设置日志级别,并将其绑定到 logger 对象
+ def set_level(self, level):
+ lv_map = {
+ "trace":TRACE,
+ "debug":DEBUG,
+ "info":INFO,
+ "warning":WARNING,
+ "error":ERROR,
+ "critical":CRITICAL
+ }
+ level = lv_map.get(level, DEBUG)
+ for handler in self.handlers:
+ handler.setLevel(level)
+ logger.setLevel(level)
+ logging.Logger.set_level = set_level
+ return logger
+
diff --git a/core/plugin.py b/core/plugin.py
new file mode 100644
index 0000000..ee2b391
--- /dev/null
+++ b/core/plugin.py
@@ -0,0 +1,219 @@
+# core/plugin.py
+from abc import ABC, abstractmethod
+import importlib
+from enum import Enum
+import os,sys
+import pkgutil
+import inspect
+from typing import Dict, List, Type
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+from core.behavior_tree import ReleaseFlowAction
+from core.defines import ReleasePhase, ProcessStep, Dict
+import core.logger as logger
+log = logger.get_logger()
+
+"""
+# format: PluginName.ReleaseStep.ProcessStep
+ReleaseFlowCommon.StepEnv.Pre
+"""
+
+class PluginType(str, Enum):
+ """
+ 为插件分类,目前仅有一种插件,FLOW类型的插件用于执行Release Flow
+ """
+ FLOW = 'flow'
+
+class PluginInterface(ABC):
+ """PluginInterface 插件基类"""
+ def __init__(self, type: PluginType):
+ self.type = type
+
+ def get_type(self) -> PluginType:
+ return self.type
+
+ @abstractmethod
+ def get_version(self) -> str:
+ pass
+
+ @abstractmethod
+ def get_description(self) -> str:
+ """返回插件描述"""
+ pass
+
+class ReleaseFlowPlugin(PluginInterface, ABC):
+ """ReleaseFlowPlugin 插件基类"""
+ def __init__(self):
+ PluginInterface.__init__(self, PluginType.FLOW)
+ self.btrees:Dict = Dict()
+
+ @abstractmethod
+ def get_version(self) -> str:
+ """返回插件版本"""
+ pass
+
+ @abstractmethod
+ def get_description(self) -> str:
+ """返回插件描述"""
+ pass
+ # 支持使用.操作符访问btrees
+ def __getattr__(self, name: str) -> Dict:
+ return self.btrees[name]
+
+def ReleaseFlowPluginDecorator(cls):
+ """装饰器,用于自动注册ReleaseFlow插件, 每个 ReleaseFlowPlugin 插件上加上这个装饰器, 插件初始化时会自动搜索并注册行为树
+ Args:
+ cls: 插件类
+ Returns:
+ 插件类
+ """
+ original_init = cls.__init__
+ # 如果不是ReleaseFlowPlugin的子类,则不进行装饰
+ if not issubclass(cls, ReleaseFlowPlugin):
+ return cls
+ def behavior_tree_register(self: ReleaseFlowPlugin):
+ """
+ 搜索当前目录下的所有行为树节点,并根据命名注册到btrees中
+ """
+ def _load_actions_from_package(self, package_name: str):
+ """从包中加载插件"""
+ package = importlib.import_module(package_name)
+ for _, name, is_pkg in pkgutil.iter_modules(package.__path__, f"{package_name}."):
+ if not is_pkg:
+ _load_actions(self, name)
+ else:
+ _load_actions_from_package(self, f"{package_name}.{name}")
+ def _load_actions(self, module_name: str):
+ """加载单个behavior action"""
+ # log.debug(f"Try to check action: {module_name}")
+ module = importlib.import_module(module_name)
+ # 遍历模块中的所有属性名称
+ for attribute_name in dir(module):
+ attribute = getattr(module, attribute_name)
+ if not isinstance(attribute, type): # 检查是否是类
+ continue
+ if not issubclass(attribute, ReleaseFlowAction): # 检查是否是插件类
+ continue
+ if inspect.isabstract(attribute): # 检查是否是抽象类
+ continue
+ log.info(f"Loaded action: {attribute.__name__}")
+ self.sub_actions[attribute.__name__] = attribute
+ def discover_actions(self:ReleaseFlowPlugin):
+ module = inspect.getmodule(self)
+ path = os.path.relpath(os.path.dirname(module.__file__)).replace('/', '.')
+ # log.debug(f"Try to search action in: {path}")
+ self.sub_actions = {}
+ for _, name, is_pkg in pkgutil.iter_modules([os.path.dirname(module.__file__)]):
+ # log.trace(f"Try to load action: {name}")
+ if is_pkg:
+ _load_actions_from_package(self, f"{path}.{name}")
+ else:
+ _load_actions(self, f"{path}.{name}")
+
+ if not isinstance(self, ReleaseFlowPlugin):
+ log.warning(f"Plugin class {self.__class__.__name__} is not a ReleaseFlowPlugin")
+ raise ReleaseErr("The plugin class decorator is not a ReleaseFlowPlugin")
+ discover_actions(self)
+ for phase in ReleasePhase:
+ phase = phase.value
+ self.btrees[phase] = Dict()
+ for step in ProcessStep:
+ step = step.value
+ action_class = f"_{phase}_{step}"
+ if not self.sub_actions.get(action_class):
+ log.trace(f"Action {action_class} not found")
+ continue
+ log.debug(f"Register ReleaseFlow behavior tree node: {action_class}")
+ if not self.btrees[phase].get(step):
+ self.btrees[phase][step] = self.sub_actions[action_class]
+ else:
+ log.warning(f"Behavior tree node {action_class} already exists: {self.btrees[phase][step]}")
+ def new_init(self, *args, **kwargs):
+ original_init(self, *args, **kwargs)
+ behavior_tree_register(self)
+ # 添加标记,表示该类使用了flow action 装饰器,用于检查是否注册了行为树
+ cls._is_action_registered = True
+ cls.__init__ = new_init
+ return cls
+
+class PluginManager:
+ """插件管理器"""
+ def __init__(self, plugin_dir: str = "plugins"):
+ self.plugin_dir = plugin_dir
+ self.plugins: Dict[str, Type[PluginInterface]] = {}
+ self.loaded_plugins: Dict[str, PluginInterface] = {}
+ self.discover_plugins()
+
+ def discover_plugins(self):
+ """发现并加载所有插件"""
+ for _, name, is_pkg in pkgutil.iter_modules([self.plugin_dir]):
+ plugin_dir = f"{self.plugin_dir}.{name}"
+ log.trace(f"Try to search plugin in: {plugin_dir}")
+ if is_pkg:
+ self._load_plugins_from_package(plugin_dir)
+ else:
+ self._load_plugin(plugin_dir)
+
+ def _load_plugins_from_package(self, package_name: str):
+ """从包中加载插件"""
+ package = importlib.import_module(package_name)
+ for _, name, is_pkg in pkgutil.iter_modules(package.__path__, f"{package_name}."):
+ if not is_pkg:
+ self._load_plugin(name)
+ else:
+ self._load_plugins_from_package(f"{package_name}.{name}")
+
+ def _load_plugin(self, module_name: str):
+ """加载单个插件"""
+ module = importlib.import_module(module_name)
+ for attribute_name in dir(module):
+ plugin_class = getattr(module, attribute_name)
+ if not isinstance(plugin_class, type): # 检查是否是类
+ continue
+ if not issubclass(plugin_class, PluginInterface): # 检查是否是插件类
+ continue
+ if inspect.isabstract(plugin_class): # 检查是否是抽象类
+ continue
+ # 检查插件类是否使用了 ReleaseFlowActionRegister 装饰器
+ if issubclass(plugin_class, ReleaseFlowPlugin) and not hasattr(plugin_class, '_is_action_registered'):
+ log.warning(f"Plugin class {plugin_class.__name__} does not have the ReleaseFlowActionRegister decorator.")
+ continue
+
+ log.info(f"Loaded plugin: {plugin_class.__name__}")
+ self.plugins[plugin_class.__name__] = plugin_class
+
+ def get_plugin(self, name: str) -> PluginInterface:
+ """获取插件实例, 每个插件实例只初始化一次
+ Args:
+ name: 插件名称
+ Returns:
+ 插件实例
+ """
+ if name not in self.loaded_plugins:
+ if name in self.plugins:
+ self.loaded_plugins[name] = self.plugins[name]()
+ else:
+ raise ValueError(f"Plugin {name} not found")
+ return self.loaded_plugins[name]
+
+
+ def get_available_plugins(self) -> List[str]:
+ """获取所有可用插件名称"""
+ return list(self.plugins.keys())
+
+ def __str__(self) -> str:
+ string = ""
+ string += f"Plugin directory: {self.plugin_dir}\n"
+ string += "Available plugins:"
+ string += "[" + " \n".join(self.plugins.keys()) + "]\n"
+ string += "Loaded plugins:"
+ string += "[" + " \n".join(self.loaded_plugins.keys()) + "]\n"
+ return string
+
+ def __repr__(self) -> str:
+ return str(self)
+
+ def __getattr__(self, name: str) -> PluginInterface:
+ # 获取插件实例
+ plugin = self.get_plugin(name)
+ return plugin
\ No newline at end of file
diff --git a/core/release_flow.py b/core/release_flow.py
new file mode 100644
index 0000000..07939ac
--- /dev/null
+++ b/core/release_flow.py
@@ -0,0 +1,157 @@
+import functools
+import os,sys
+import threading
+sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
+import thirdparty.py_trees as py_trees
+from thirdparty.py_trees.trees import BehaviourTree
+from core.behavior_tree import ReleaseFlowBuilder
+from core.plugin import PluginManager
+from core.defines import ReleasePhase, ProcessStep, ReleaseErr, Dict
+from database import *
+import core.logger as logger
+log = logger.get_logger()
+
+def draw_release_flow_status(snapshot_visitor, behaviour_tree):
+ """保存行为树状态为HTML文件"""
+ mutex = threading.Lock()
+ with mutex:
+ # timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ filename = f"alkaid_release_flow_status.html"
+ with open(filename, 'w') as f:
+ f.write('SStar Alkaid Release Flow Status')
+ f.write(py_trees.display.xhtml_tree(
+ behaviour_tree.root,
+ show_status=True,
+ visited=snapshot_visitor.visited,
+ previously_visited=snapshot_visitor.visited
+ ))
+ f.write("")
+ filename = f"alkaid_release_flow_status.dot"
+ with open(filename, 'w') as f:
+ f.write(str(py_trees.display.dot_tree(behaviour_tree.root, with_blackboard_variables=True)))
+ # log.info(f"Behavior tree saved to {filename}")
+
+# 构建环境阶段 (验证配置、拉取code等基础环境的准备)
+def _construct_env_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
+ with builder.sequence(ReleasePhase.ENV):
+ with builder.sequence(ProcessStep.PRE):
+ pass
+ with builder.sequence(ProcessStep.PROCESS):
+ if not ReleaseOptions.SkipSyncCode:
+ builder.action(plugins.BaseLine.PhaseEnv.Process())
+ with builder.sequence(ProcessStep.POST):
+ pass
+
+# 配置编译文件阶段 (defconfig mkfile 等编译控制文件的配置)
+def _construct_config_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
+ with builder.sequence(ReleasePhase.CONFIG):
+ with builder.sequence(ProcessStep.PRE):
+ pass
+ with builder.sequence(ProcessStep.PROCESS):
+ pass
+ with builder.sequence(ProcessStep.POST):
+ pass
+
+# 构建阶段(编译构建sdk,根据上一步的配置执行编译构建)
+def _construct_build_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
+ with builder.sequence(ReleasePhase.BUILD):
+ with builder.sequence(ProcessStep.PRE):
+ pass
+ with builder.sequence(ProcessStep.PROCESS):
+ pass
+ with builder.sequence(ProcessStep.POST):
+ pass
+
+# 检查阶段(检查SDK编译结果是否符合预期)
+def _construct_check_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
+ with builder.sequence(ReleasePhase.CHECK):
+ with builder.sequence(ProcessStep.PRE):
+ pass
+ with builder.sequence(ProcessStep.PROCESS):
+ pass
+ with builder.sequence(ProcessStep.POST):
+ pass
+
+# 打包阶段 (将SDK其他文件诸如doc,tool等打包)
+def _construct_package_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
+ with builder.sequence(ReleasePhase.PACKAGE):
+ with builder.sequence(ProcessStep.PRE):
+ pass
+ with builder.sequence(ProcessStep.PROCESS):
+ pass
+ with builder.sequence(ProcessStep.POST):
+ pass
+
+# 验证阶段 (验证Release的SDK是否可以正常工作)
+def _construct_verify_phase(builder: ReleaseFlowBuilder, plugins: PluginManager):
+ with builder.sequence(ReleasePhase.VERIFY):
+ with builder.sequence(ProcessStep.PRE):
+ pass
+ with builder.sequence(ProcessStep.PROCESS):
+ pass
+ with builder.sequence(ProcessStep.POST):
+ pass
+
+class ReleaseFlow():
+ def __init__(self):
+ # 我们的Flow默认是一个串行的流程,所以根节点用Sequence
+ self.builder = ReleaseFlowBuilder()
+ self.tree = None
+ self.flow = None
+ self.timer = None
+ self.status_update_interval = 1 # 定时器间隔时间,单位秒
+ self.running = False
+
+ # 构建 Alkaid Release流程
+ def construct(self, plugins: PluginManager):
+ with self.builder.sequence("AlkaidReleaseFlow") as builder:
+ _construct_env_phase(builder, plugins)
+ _construct_config_phase(builder, plugins)
+ _construct_build_phase(builder, plugins)
+ _construct_check_phase(builder, plugins)
+ _construct_package_phase(builder, plugins)
+ _construct_verify_phase(builder, plugins)
+ self.flow = self.builder.build()
+
+ def _start_update_status(self, snapshot_visitor):
+ """启动定时器"""
+ if self.timer is not None:
+ return
+
+ def timer_callback():
+ if self.tree is not None:
+ draw_release_flow_status(snapshot_visitor, self.tree)
+ # 重新设置定时器
+ self.timer = threading.Timer(self.status_update_interval, timer_callback)
+ self.timer.start()
+
+ # 启动第一个定时器
+ self.timer = threading.Timer(self.status_update_interval, timer_callback)
+ self.tree.add_post_tick_handler(functools.partial(draw_release_flow_status, snapshot_visitor))
+ self.timer.start()
+
+ # 运行Release 流程
+ def run(self):
+ if self.flow is None:
+ raise ReleaseErr("Flow is not built")
+ # 使用封装好的BehaviourTree去管理流程
+ self.tree = BehaviourTree(root=self.flow)
+ snapshot_visitor = py_trees.visitors.SnapshotVisitor()
+ self.tree.visitors.append(snapshot_visitor)
+ self.tree.setup() # 初始化树
+ # 启动定时器
+ self._start_update_status(snapshot_visitor)
+ # print(
+ # py_trees.display.dot_tree(
+ # self.tree.root
+ # )
+ # )
+ self.tree.tick() # 触发tick进行执行
+ return self.tree.root.status
+
+ def shutdown(self):
+ if self.timer is not None:
+ self.timer.cancel()
+ self.timer = None
+ if self.tree is not None:
+ self.tree.shutdown()
diff --git a/database/__init__.py b/database/__init__.py
new file mode 100644
index 0000000..7fb1e18
--- /dev/null
+++ b/database/__init__.py
@@ -0,0 +1,30 @@
+from .base import *
+from .dirs import *
+from .configs import *
+from .options import *
+from .common import *
+from .customer import *
+from .black import *
+
+def load_from_file(config_path:str=None):
+ """从文件加载customer配置
+ """
+ if not os.path.exists(config_path):
+ log.warning(f"{config_path} doesn't exists")
+ return
+ with open(config_path, 'r') as f:
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+ from thirdparty import yaml
+ obj = yaml.safe_load(f.read())
+ # 遍历该package下的所有数据,如果是 BaseConfig 类型的实例则进行更新操作
+ for class_name in globals():
+ instance = globals()[class_name]
+ if isinstance(instance, BaseConfig):
+ class_name = instance.__class__.__name__
+ if class_name not in obj:
+ log.debug(f"No Customer Config update ==> {class_name}")
+ continue
+ log.info(f"Update Customer Config Cnt: {len(obj[class_name]):3d} ==> {class_name}")
+ instance = instance.update_from_extra(obj[class_name])
+ globals()[class_name] = instance
+
diff --git a/database/base.py b/database/base.py
new file mode 100644
index 0000000..89daef5
--- /dev/null
+++ b/database/base.py
@@ -0,0 +1,355 @@
+from typing import Any
+from core.defines import Dict, List
+import json
+import sys, os
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+import core.logger as logger
+log = logger.get_logger()
+
+class BaseConfig():
+ """
+ 所有配置类的基类, 所有配置类都继承自该类
+ 提供配置的统一使用方式:
+ 1. 基类通过 __getattr__ 方法,实现通过点号访问配置
+ 2. 基类通过 __setattr__ 方法,实现通过点号设置配置
+ 3. 基类通过 __getitem__ 方法,实现通过索引访问配置
+ 4. 基类通过 __setitem__ 方法,实现通过索引设置配置
+ 5. 基类通过 __delitem__ 方法,实现通过索引删除配置
+ 6. 基类通过 __len__ 方法,实现获取配置的长度
+ 7. 基类通过 __str__ 方法,实现获取配置的json字符串
+ 8. 基类通过 __repr__ 方法,实现获取配置的json字符串
+ 9. 子类通过 check 方法,实现检查配置是否合法
+ 10. 子类通过 @AutoInstanceDecorator 装饰器,自动将类替换为它的实例
+ """
+ # 存储原始配置数据,子类复写该属性以表明其配置,其可以是列表或字典
+ _config = None
+ _list = List() # 用于存储配置是一个list的形式
+ _dict = Dict() # 配置的key-value形式,以及list也会扩展为dict存储,方便使用
+ _value = None # 配置仅仅是一个value的情况
+ # 用于存储实例, 单例模式,防止重复实例化 子类无须操作该属性
+ _instance = None
+
+ def __new__(cls):
+ """
+ 如果实例已经存在,则直接返回实例,实现单例模式。
+ """
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ return cls._instance
+ def _value_raise(self):
+ if self._value is not None:
+ raise AttributeError(f"{self.__class__.__name__} not support this method")
+ def __init__(self) -> None:
+ """
+ 初始化实例的_list和_dict,
+ 并将其绑定到类属性上,
+ """
+ # log.info(f"Load config: {self.__class__.__name__}")
+ # 获取子类的 _config 属性
+ _config = getattr(self.__class__, '_config', None)
+ if _config is None:
+ raise ValueError(f"_config must be defined in {self.__class__.__name__}")
+ self._list = List()
+ self._dict = Dict()
+ if isinstance(_config, list):
+ self._list = List(_config)
+ for key in self._list:
+ # 将列表项同时添加到属性中 以便于通过Dot方式访问
+ self._dict[key] = key
+ elif isinstance(_config, dict):
+ self._dict = Dict(_config)
+ # 配置是单个值的情况
+ elif isinstance(_config, (int, float, str, bool)):
+ self._value = _config
+ self._dict = Dict({str(_config) : _config})
+ self._list = List([_config])
+ else:
+ raise ValueError(f"{self.__class__.__name__} _config must be a list or dict")
+ log.debug(f"Load config cnt: {len(self._dict):3} => {self.__class__.__name__}")
+
+ def __getattr__(self, key):
+ """
+ 当通过类属性方式访问不存在的属性时,尝试从 _dict 中获取
+ """
+ return self[key]
+
+ def __setattr__(self, key, value):
+ """
+ 当通过类属性方式设置值时,同时更新 _dict
+ """
+ # log.info(f"setattr: {self.__class__.__name__}, key: {key}, value: {value}")
+ # 如果是非字符串,或者是非_开头的字符串,则将其添加到_dict中
+ if not isinstance(key, str) or not key.startswith('_'):
+ self[key] = value
+ # 如果key是字符串且是合法的标识符,则将其添加到类属性中
+ elif isinstance(key, str):
+ super().__setattr__(key, value)
+ else:
+ raise AttributeError(f"Attribute {key} is not a valid identifier")
+ def __delattr__(self, key:str):
+ """
+ 当通过类属性方式删除值时,同时删除 _dict 中的值
+ """
+ del self[key]
+
+ def items(self):
+ return self._dict.items()
+
+ def get(self, key=None, default=None):
+ """
+ 获取配置值
+ """
+ if key is None:
+ if self._value is not None:
+ return self._value
+ else:
+ raise KeyError(f"{self.__class__.__name__} must set key to get")
+ return self[key] if key in self else default
+
+ def __getitem__(self, key):
+ """
+ 当通过索引方式访问值:
+ 1. 如果key是整数,按列表方式访问
+ 2. 如果key是字符串,按字典方式访问
+ 3. 如果key既不是整数也不是字符串,抛出TypeError
+ 例如:
+ config = MyConfig()
+ print(config[0])
+ print(config["key"])
+ """
+ if isinstance(key, int):
+ return self._list[key]
+ elif isinstance(key, str):
+ # 如果key中包含/,说明可能是个路径。返回key本身
+ # 因为有些配置直接将路径作为配置项
+ if '/' in key:
+ return key
+ else:
+ # 如果 key 是字符串,按字典方式访问
+ return self._dict[key]
+ else:
+ raise TypeError("Key must be either an integer or a string")
+
+ def __setitem__(self, key, value):
+ """
+ 当通过索引方式设置值:
+ 1. 如果key是整数,按列表方式设置
+ 2. 如果key是字符串,按字典方式设置
+ 3. 如果key既不是整数也不是字符串,抛出TypeError
+ 例如:
+ config = MyConfig()
+ config[0] = "value"
+ config["key"] = "value"
+ """
+ # log.info(f"setitem: {self.__class__.__name__}, key: {key}, value: {value}")
+ if isinstance(key, int):
+ old_value = self._list[key]
+ self._list[key] = value
+ self._dict.pop(old_value)
+ key = value
+ elif isinstance(key, str):
+ if self._list:
+ # 我们会将新的key和value同时添加到_list和_dict中 确保key和value是相同的
+ if key in self._list:
+ self._list[self._list.index(key)] = value
+ self._dict.pop(key)
+ key = value
+ else:
+ self._list.append(key)
+ else:
+ raise TypeError("Key must be either an integer or a string")
+ self._dict[key] = value
+ # log.info(f"setitem ok: {self.__class__.__name__}, key: {key}, value: {self._dict[key]}")
+
+ def __delitem__(self, key):
+ """
+ 当通过索引方式删除值:
+ 1. 如果key是整数,按列表方式删除
+ 2. 如果key是字符串,按字典方式删除
+ 3. 如果key既不是整数也不是字符串,抛出TypeError
+ 例如:
+ config = MyConfig()
+ del config[0]
+ del config["key"]
+ """
+ if isinstance(key, int):
+ self.__delattr__(self._list[key])
+ elif isinstance(key, str):
+ # 如果 key 是字符串,按字典方式删除
+ if key not in self._dict:
+ raise KeyError(f"Key {key} not found in the dictionary")
+ self._dict.pop(key)
+ if key in self._list:
+ self._list.pop(self._list.index(key))
+ else:
+ raise TypeError("Key must be either an integer or a string")
+
+ def __contains__(self, key):
+ """
+ 检查配置是否包含某个key
+ """
+ return key in self._dict
+ def __iter__(self):
+ """
+ 返回配置的迭代器
+ """
+ if self._value is not None:
+ yield self._value
+ elif self._list:
+ yield from self._list
+ elif self._dict:
+ yield from self._dict
+ else:
+ raise StopIteration
+ def __len__(self):
+ """
+ 返回配置的长度
+ """
+ if self._value is not None:
+ return 1
+ elif self._list:
+ return len(self._list)
+ elif self._dict:
+ return len(self._dict)
+ else:
+ return 0
+
+ def __str__(self):
+ """
+ 返回对象的字符串表示,使用json 风格来呈现
+ """
+ # log.info(f"str: {self.__class__.__name__}, len: {len(self._list)}")
+ if self._value is not None:
+ return f"{self._value}"
+ elif self._list:
+ return json.dumps(self._list, indent=4)
+ elif self._dict:
+ return json.dumps(self._dict, indent=4)
+ else:
+ return ""
+
+ def __repr__(self):
+ """
+ 返回对象的字符串表示,使用json 风格来呈现
+ """
+ return self.__str__()
+
+ def __eq__(self, other):
+ """
+ 实现对象的相等比较
+ 1. 如果other是字符串,且self._value不为None,则与_value进行比较
+ 2. 如果other是BaseConfig实例:
+ - 如果两者都有_value,则比较_value
+ - 如果两者都有_list,则比较_list
+ - 如果两者都有_dict,则比较_dict
+ 3. 如果other是列表,且self._list不为空,则与_list进行比较
+ 4. 如果other是字典,且self._dict不为空,则与_dict进行比较
+ 5. 否则,使用默认的比较行为
+ """
+ # 处理字符串比较
+ if isinstance(other, (int, float, str, bool)) and self._value is not None:
+ return self._value == other
+ # 处理BaseConfig实例比较
+ elif isinstance(other, BaseConfig):
+ if self._value is not None and other._value is not None:
+ return self._value == other._value
+ elif self._list and other._list:
+ return self._list == other._list
+ elif self._dict and other._dict:
+ return self._dict == other._dict
+ else:
+ return False
+ elif isinstance(other, list) and self._list:
+ return self._list == other
+ elif isinstance(other, dict) and self._dict:
+ return self._dict == other
+ else:
+ return super().__eq__(other)
+
+ def check(self) -> bool:
+ """检查配置是否合法
+ Returns:
+ bool: 如果配置合法,返回True,否则返回False
+ 子类可以复写该方法,以检查配置是否合法
+ """
+ return True
+
+ def update(self, _config):
+ """更新配置
+ Args:
+ _config (dict): 需要更新的配置
+
+ Raises:
+ ValueError: 如果_config不是字典或列表
+ """
+ if isinstance(_config, dict):
+ for key, value in _config.items():
+ self[key] = value
+ elif isinstance(_config, list):
+ for item in _config:
+ self[item] = item
+ elif isinstance(_config, (int, float, str, bool)):
+ self._value = _config
+ self._dict = Dict({str(_config) : _config})
+ self._list = List([_config])
+ else:
+ raise ValueError(f"_config type {type(_config)} is not support!")
+
+ def _clean(self):
+ """清除所有配置
+ """
+ [self.__delitem__(_) for _ in self._dict.copy()]
+
+ def update_from_extra(self, new_data:Any):
+ """ 从json中更新数据,默认行为是加载,子类可以重载该函数使得其可以具备不同的行为
+
+ Args:
+ new_data (Any): 带有直接属于该配置的数据
+ """
+ self.load(new_data)
+
+ def load(self, new_data:Any):
+ """ 加载数据:会先清除当前配置,然后更新配置
+
+ Args:
+ new_data (Any): 带有直接属于该配置的数据
+ 例如:
+ ```python
+ MyConfig.load({"key1": "value1", "key2": "value2"})
+ MyConfig1.load([1, 2, 3])
+ MyConfig2.load(123)
+ MyConfig3.load("123")
+ MyConfig4.load(True)
+ ```
+ """
+
+ if not isinstance(new_data, (dict, list, int, float, str, bool)):
+ raise ValueError(f"Datatype {type(new_data)} is not support!")
+ self._clean()
+ self.update(new_data)
+
+ def remove(self, new_data: Any):
+ """删除数据
+ """
+ if isinstance(new_data, (list, dict)):
+ for item in new_data:
+ del self[item]
+ elif isinstance(new_data, (int, float, str, bool)):
+ del self[new_data]
+ else:
+ raise ValueError(f"Datatype {type(new_data)} is not support!")
+
+def AutoInstanceDecorator(cls):
+ """自动实例化装饰器:自动将类替换为它的实例,实现类名访问即实例访问,
+ 例如:
+ ```python
+ print(MyConfig.key)
+ ```
+ 等价于
+ ```python
+ config = MyConfig()
+ print(config["key"])
+ ```
+ """
+ instance = cls() # 创建实例
+ return instance # 返回实例,替换原始类
\ No newline at end of file
diff --git a/database/black.py b/database/black.py
new file mode 100644
index 0000000..d4046e7
--- /dev/null
+++ b/database/black.py
@@ -0,0 +1,120 @@
+from functools import wraps
+import sys, os
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+import core.logger as logger
+log = logger.get_logger()
+from database.base import BaseConfig, AutoInstanceDecorator
+from database.dirs import CheckDirAliasDecorator
+from database.common import *
+
+
+def BlackListUpdateRemoveDecorator(cls:BaseConfig):
+ """黑名单更新装饰器:对于黑名单来说更新配置就是将默认的黑名单里面的项目删除
+ Args:
+ cls: 被装饰的类
+ Returns:
+ cls: 装饰后的类
+ """
+ cls.update_from_extra = cls.remove
+ return cls
+
+@AutoInstanceDecorator
+@BlackListUpdateRemoveDecorator
+class ProjectBlackList(BaseConfig):
+ _config = [
+ "project/tools/codesize",
+ "project/image/codesize",
+ ]
+ def check(self) -> bool:
+ for item in self._list:
+ if not check_file_exists(item):
+ log.warning(f"path {item} not exists")
+ return True
+
+@AutoInstanceDecorator
+@CheckDirAliasDecorator
+@BlackListUpdateRemoveDecorator
+class BlackListDefault(BaseConfig):
+ """BLACK_LIST_DEFAULT
+ """
+ _config = [
+ "mi_ai",
+ "mi_ao",
+ "mi_aio",
+ "mi_alsa",
+ "mi_cipher",
+ "mi_common",
+ "mi_cus3a",
+ "mi_debug",
+ "mi_dummy",
+ "mi_disp",
+ "mi_dpu",
+ "mi_dsp",
+ "mi_fb",
+ "mi_gfx",
+ "mi_hdmi",
+ "mi_hdmirx",
+ "mi_hvp",
+ "mi_ipu",
+ "mi_iqserver",
+ "mi_ive",
+ "mi_jpd",
+ "mi_mipitx",
+ "mi_nir",
+ "mi_panel",
+ "mi_pcie",
+ "mi_pspi",
+ "mi_rgn",
+ "mi_scl",
+ "mi_sed",
+ "mi_sensor",
+ "mi_shadow",
+ "sdk/impl/sys/",
+ "mi_vdec",
+ "mi_vdf",
+ "sdk/interface/src/vcodec/",
+ "mi_vdisp",
+ "mi_wlan",
+ "dualos",
+ "otp",
+ "usb_gadget_udc_usb30",
+ "freertos",
+ "context_switch",
+ "lh_monitor",
+ "pm_mhal_pm_default",
+ "pm_mhal_pm_aio",
+ "pm_mhal_pm_idle",
+ "pm_mhal_pm_jpe",
+ "pm_mhal_pm_md",
+ "pm_mhal_pm_radar",
+ "pm_mhal_pm_usbpoc",
+ "pm_mhal_pm_vif",
+ "pm_mhal_pm_usb_gadget_udc_usb30",
+ ]
+
+@AutoInstanceDecorator
+@BlackListUpdateRemoveDecorator
+@CheckDirAliasDecorator
+class BlackListInternal(BaseConfig):
+ """BLACK_LIST_INTERNAL
+ """
+ _config = [
+ "mi_isp",
+ "mi_ispalgo",
+ "mi_ldc",
+ "mi_venc",
+ "mi_vif",
+ "pm_mhal_pm_isp",
+ "pm_mhal_pm_ispalgo",
+ "pm_mhal_pm_radar_algo",
+ ]
+
+@AutoInstanceDecorator
+@BlackListUpdateRemoveDecorator
+class DemoBlackList(BaseConfig):
+ """verify 目录下面的黑名单,不允许被 release 出去
+ """
+ _config = [
+ "sdk/verify/mi_demo/source/ipu",
+ "sdk/verify/vectorcast",
+ ]
\ No newline at end of file
diff --git a/database/common.py b/database/common.py
new file mode 100644
index 0000000..021bead
--- /dev/null
+++ b/database/common.py
@@ -0,0 +1,54 @@
+import sys, os
+from typing import Any
+from pathlib import Path
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+from core.logger import get_logger
+log = get_logger()
+sys.path.append(os.path.join(os.path.dirname(__file__)))
+from .base import BaseConfig
+from .options import ReleaseOptions
+import dirs
+
+def config_expand(instance: Any):
+ """配置展开:
+ 1. 将DirAlias中的alias展开,为实际路径
+ 2. 将BaseConfig中的配置展开 将字符串中的{key}替换为Args._dict[key]
+ 3. 如果为多层嵌套,则递归展开
+ Returns:
+ List: 展开后的列表
+ """
+ _ret = []
+ if isinstance(instance, str):
+ # 将字符串中的{key}替换为ReleaseOptions._dict[key] 实现动态路径
+ instance = dirs.str_format(instance)
+ # 尝试从DirAlias中获取instance的值
+ value = dirs.DirAlias.get(instance, instance)
+ if value == instance:
+ # 如果value和instance相同,说明其已经不可以再拓展了 已经是最终路径了
+ _ret.append(instance)
+ else:
+ # 如果value和instance不同,说明其还可以继续拓展
+ _ret.extend(config_expand(value))
+ elif isinstance(instance, list):
+ for item in instance:
+ _ret.extend(config_expand(item))
+ elif isinstance(instance, dict):
+ for _, value in instance.items():
+ _ret.extend(config_expand(value))
+ elif isinstance(instance, BaseConfig):
+ if instance._list:
+ _ret.extend(config_expand(instance._list))
+ elif instance._dict:
+ _ret.extend(config_expand(instance._dict))
+ else:
+ raise ValueError(f"Invalid config type: {type(instance)}")
+ #log.info(f"config_expand: {instance.__class__.__name__} {_ret}")
+ return sorted(set(_ret))
+
+def check_file_exists(item: str) -> bool:
+ """检查文件是否存在
+ """
+ file = Path(ReleaseOptions.AlkaidRootPath) / item
+ if not file.exists():
+ return False
+ return True
\ No newline at end of file
diff --git a/database/configs.py b/database/configs.py
new file mode 100644
index 0000000..a61d632
--- /dev/null
+++ b/database/configs.py
@@ -0,0 +1,306 @@
+from functools import wraps
+import sys, os
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+import core.logger as logger
+log = logger.get_logger()
+from database.base import BaseConfig, AutoInstanceDecorator
+from database.dirs import CheckDirAliasDecorator
+from database.common import *
+
+@AutoInstanceDecorator
+class GIT_REMOTE_REPO(BaseConfig):
+ _config = "origin"
+
+@AutoInstanceDecorator
+class TAG_IGNORE_PROJECT(BaseConfig):
+ _config = "build"
+
+@AutoInstanceDecorator
+class TAR_EXFILES(BaseConfig):
+ _config = "--exclude=.git --exclude=.svn --exclude=.gitignore --exclude=.vimproj"
+
+@AutoInstanceDecorator
+class RELEASE_SNAPSHOT(BaseConfig):
+ _config = ".repo/manifests/release_snapshot/"
+
+@AutoInstanceDecorator
+class REPO_URL(BaseConfig):
+ _config = "http://hcgit04:9080/manifest/alkaid"
+
+
+@AutoInstanceDecorator
+class ProductIngoreList(BaseConfig):
+ _config = [
+ "include",
+ "sigma_common_libs",
+ ]
+ def check(self) -> bool:
+ # 检查当前 配置是否合法
+ return True
+
+@AutoInstanceDecorator
+class ChipIngoreList(BaseConfig):
+ _config = [
+ "ifado",
+ "iford",
+ "maruko",
+ "opera",
+ "pcupid",
+ "souffle",
+ "mercury6p",
+ "infinity6c",
+ "infinity6e",
+ "infinity6f",
+ "infinity7",
+ "pioneer5",
+ "ibopper",
+ #---chip support OpenRtosCm4SourceEx after ifackel
+ "ifackel",
+ ]
+
+@AutoInstanceDecorator
+class ChipCheckConfigConsistencyList(BaseConfig):
+ _config = [
+ "iford",
+ "ifado",
+ "ibopper",
+ "pcupid",
+ "ifackel",
+ ]
+
+@AutoInstanceDecorator
+class ReleaseDirDefaultList(BaseConfig):
+ _config = [
+ "boot",
+ "kernel",
+ "optee",
+ "pm_rtos",
+ "project",
+ "riscv",
+ "rtos",
+ "sdk",
+ ]
+ def check(self) -> bool:
+ for item in self._list:
+ if not check_file_exists(item):
+ log.warning(f"path {item} not exists")
+ return True
+
+@AutoInstanceDecorator
+class ExReleaseList(BaseConfig):
+ _config = [
+ "sdk/driver",
+ ]
+ def check(self) -> bool:
+ for item in self._list:
+ if not check_file_exists(item):
+ raise ValueError(f"path {item} not exiest")
+ return True
+
+@AutoInstanceDecorator
+class AppDemoDefaultList(BaseConfig):
+ """DEMO_LIST default files
+ """
+ _config = [
+ "sdk/makefile",
+ "sdk/sdk.mk",
+ "sdk/verify/makefile",
+ ]
+ def check(self) -> bool:
+ for item in self._list:
+ if not check_file_exists(item):
+ raise ValueError(f"path {item} not exists")
+ return True
+
+@AutoInstanceDecorator
+@CheckDirAliasDecorator
+class RtosDefaultList(BaseConfig):
+ """RTOS default drivers
+ """
+ _config = [
+ "bench",
+ "bootloader",
+ "dualos_camera",
+ "cust_isp",
+ "cust_usb_gadget",
+ "earlyinit_setting",
+ "earlyinit_main",
+ "earlyinit_vsrc",
+ "sysdesc",
+ "sensordriver",
+ ]
+
+@AutoInstanceDecorator
+@CheckDirAliasDecorator
+class RtosWhiteList(BaseConfig):
+ """RTOS white drivers
+ """
+ _config = [
+ "application_selector",
+ "aov_preload",
+ "cm4_preload",
+ "coremark",
+ "common",
+ "audio_app",
+ "disp_app",
+ "dualos_camera",
+ "font",
+ "iqserver",
+ "lvgl",
+ "sample_code",
+ "preload_rtos",
+ "preload_sample",
+ "usb_gadget_app",
+ "usb_gadget_app_uvc",
+ "sensordriver",
+ "aesdma",
+ "bdma",
+ "camclk",
+ "cpufreq",
+ "drvutil",
+ "emac",
+ "fcie",
+ "flash",
+ "fsp_qspi",
+ "gpio",
+ "i2c",
+ "input",
+ "int",
+ "ir",
+ "kernel",
+ "loadns",
+ "mbx",
+ "miu",
+ "mmupte",
+ "mspi",
+ "msys",
+ "padmux",
+ "pwm",
+ "riu",
+ "rtc",
+ "rtcpwc",
+ "saradc",
+ "sdmmc",
+ "str",
+ "timer",
+ "tsensor",
+ "uart",
+ "voltage",
+ "watchdog",
+ "algo",
+ "decompress",
+ "freertos_plus_fat",
+ "lwfs",
+ "proxyfs",
+ "tcpip",
+ "arm",
+ "riscv",
+ "libc",
+ "newlib_stub",
+ "cam_dev_wrapper",
+ "cam_drv_poll",
+ "cam_fs_wrapper",
+ "cam_os_wrapper",
+ "cam_proc_wrapper",
+ "env_util",
+ "initcall",
+ "loadable_module",
+ "memmang",
+ "memmap",
+ "mempool",
+ "MsWrapper",
+ "newlib_stub",
+ "freertos_main",
+ "freertos_posix",
+ "sys_I_SW",
+ ]
+
+@AutoInstanceDecorator
+@CheckDirAliasDecorator
+class PmRtosDefaultList(BaseConfig):
+ """PM_RTOS default drivers
+ """
+ _config = [
+ "pm_slnn_raw_hpd",
+ "pm_cm4_pipeline",
+ "pm_sysdesc",
+ ]
+
+@AutoInstanceDecorator
+@CheckDirAliasDecorator
+class PmRtosWhiteList(BaseConfig):
+ """PM_RTOS white drivers
+ """
+ _config = [
+ "pm_aesdma",
+ "pm_bdma",
+ "pm_camclk",
+ "pm_cpufreq",
+ "pm_drvutil",
+ "pm_loadns",
+ "pm_emac",
+ "pm_flash",
+ "pm_fsp_qspi",
+ "pm_gpio",
+ "pm_i2c",
+ "pm_int",
+ "pm_ir",
+ "pm_kernel",
+ "pm_miu",
+ "pm_mmupte",
+ "pm_mspi",
+ "pm_msys",
+ "pm_padmux",
+ "pm_pcie",
+ "pm_pl011",
+ "pm_mhal_pm_clk",
+ "pm_pm_idle",
+ "pm_mhal_pm_mbx",
+ "pm_mhal_imi_heap",
+ "pm_mhal_pm_pir",
+ "pm_rtcpwc",
+ "pm_mhal_pm_sys",
+ "pm_mhal_pm_wdt",
+ "pm_pspi",
+ "pm_pwm",
+ "pm_pm_power",
+ "pm_rtc",
+ "pm_rtcpwc",
+ "pm_saradc",
+ "pm_sdmmc",
+ "pm_timer",
+ "pm_tsensor",
+ "pm_uart",
+ "pm_voltage",
+ "pm_watchdog",
+ "pm_decompress",
+ "pm_freertos_plus_fat",
+ "pm_lwfs",
+ "pm_proxyfs",
+ "pm_tcpip",
+ "pm_arm",
+ "pm_riscv",
+ "pm_cam_dev_wrapper",
+ "pm_cam_drv_poll",
+ "pm_cam_fs_wrapper",
+ "pm_cam_os_wrapper",
+ "pm_cam_proc_wrapper",
+ "pm_env_util",
+ "pm_initcall",
+ "pm_libc",
+ "pm_memmang",
+ "pm_memmap",
+ "pm_mempool",
+ "pm_MsWrapper",
+ "pm_freertos",
+ "pm_freertos_posix",
+ "pm_sys_I_SW",
+ ]
+
+
+@AutoInstanceDecorator
+class DemoListDefault(BaseConfig):
+ """DEMO_LIST_DEFAULT
+ """
+ _config = [
+ ]
diff --git a/database/customer.py b/database/customer.py
new file mode 100644
index 0000000..3d1d89e
--- /dev/null
+++ b/database/customer.py
@@ -0,0 +1,45 @@
+import json
+import sys, os
+from functools import wraps
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+import core.logger as logger
+log = logger.get_logger()
+sys.path.append(os.path.dirname(__file__))
+from .base import BaseConfig, AutoInstanceDecorator
+from .dirs import CheckDirAliasDecorator
+from .common import *
+
+# 定义配置类
+@AutoInstanceDecorator
+class ConfigList(BaseConfig):
+ """支持的配置列表
+ """
+ _config = []
+
+@AutoInstanceDecorator
+@CheckDirAliasDecorator
+class MiReleaseList(BaseConfig):
+ """MI release 列表
+ """
+ _config = []
+
+@AutoInstanceDecorator
+@CheckDirAliasDecorator
+class MhalReleaseList(BaseConfig):
+ """MHAL release 列表
+ """
+ _config = []
+
+@AutoInstanceDecorator
+@CheckDirAliasDecorator
+class RtosReleaseList(BaseConfig):
+ """RTOS release 列表
+ """
+ _config = []
+
+@AutoInstanceDecorator
+@CheckDirAliasDecorator
+class PmRtosReleaseList(BaseConfig):
+ """PM RTOS release 列表
+ """
+ _config = []
diff --git a/database/dirs.py b/database/dirs.py
new file mode 100644
index 0000000..8e43b81
--- /dev/null
+++ b/database/dirs.py
@@ -0,0 +1,1021 @@
+from pathlib import Path
+from typing import Any
+from functools import wraps
+import sys, os
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+import core.logger as logger
+log = logger.get_logger()
+from database.base import BaseConfig, AutoInstanceDecorator
+from database.options import ReleaseOptions
+from database.common import *
+
+@AutoInstanceDecorator
+class DirAlias(BaseConfig):
+ """给alkaid相关的目录文件其一个别名,方便其他配置进行使用。
+ 1. 所有的路径都是相对于ReleaseOptions.AlkaidRootPath的相对路径
+ 2. 所有的路径都是字符串
+ 3. 所有的路径都是列表
+ """
+ _config = {
+ "mi_default": [
+ "sdk/makefile",
+ "sdk/sdk.mk",
+ "sdk/.clang-format",
+ "sdk/.clang-tidy",
+ "sdk/interface/add-config.mk",
+ "sdk/interface/clear-config.mk",
+ "sdk/interface/compile_options.mk",
+ "sdk/interface/hal-impl-config.mk",
+ "sdk/interface/makefile",
+ "sdk/interface/modules.mk",
+ "sdk/interface/raw-impl-config.mk",
+ "sdk/interface/tag-maker.mk",
+ "sdk/interface/tidy_ignore.mk",
+ "sdk/interface/tidy_ruler.mk",
+ "sdk/interface/include_scanner.sh",
+ "sdk/interface/header_blacklist.txt",
+ "sdk/interface/src_whitelist.txt",
+ "sdk/interface/sdk_coding_style_sanitize.py",
+ "sdk/interface/.scan_header_whitelist.json",
+ "sdk/interface/include/common/",
+ "sdk/interface/include/internal/kernel/",
+ "sdk/interface/include/internal/rtos/",
+ "sdk/interface/include/internal/user/",
+ "sdk/interface/include/sys/",
+ ],
+ "mi_ai": [
+ "sdk/interface/include/aio/",
+ "sdk/interface/include/ai/",
+ "sdk/interface/src/ai/",
+ "sdk/impl/ai/",
+ ],
+ "mi_aio": [
+ "sdk/interface/include/aio/",
+ "sdk/interface/src/aio/",
+ "sdk/impl/aio/",
+ ],
+ "mi_ao": [
+ "sdk/interface/include/aio/",
+ "sdk/interface/include/ao/",
+ "sdk/interface/src/ao/",
+ "sdk/impl/ao/",
+ ],
+ "mi_alsa": [
+ "sdk/interface/include/alsa/",
+ "sdk/interface/src/alsa/",
+ "sdk/impl/alsa/",
+ ],
+ "mi_cipher": [
+ "sdk/interface/include/cipher/",
+ "sdk/interface/src/cipher/",
+ "sdk/impl/cipher/",
+ ],
+ "mi_common": [
+ "sdk/interface/src/common/",
+ "sdk/impl/common/",
+ ],
+ "mi_cus3a": [
+ "sdk/interface/include/cus3a/",
+ "sdk/interface/src/cus3a/",
+ ],
+ "mi_debug": [
+ "sdk/interface/include/debug/",
+ "sdk/interface/src/debug/",
+ "sdk/impl/debug/",
+ ],
+ "mi_dummy": [
+ "sdk/interface/include/dummy/",
+ "sdk/interface/src/dummy/",
+ "sdk/impl/dummy/",
+ ],
+ "mi_disp": [
+ "sdk/interface/include/disp/",
+ "sdk/interface/src/disp/",
+ "sdk/impl/disp/",
+ ],
+ "mi_dpu": [
+ "sdk/interface/include/dpu/",
+ "sdk/interface/src/dpu/",
+ "sdk/impl/dpu/",
+ ],
+ "mi_dsp": [
+ "sdk/interface/include/dsp/",
+ "sdk/interface/src/dsp/",
+ "sdk/impl/dsp/",
+ ],
+ "mi_fb": [
+ "sdk/interface/include/fb/",
+ "sdk/interface/src/fb/",
+ "sdk/impl/fb/",
+ ],
+ "mi_gfx": [
+ "sdk/interface/include/gfx/",
+ "sdk/interface/src/gfx/",
+ "sdk/impl/gfx/",
+ ],
+ "mi_hdmi": [
+ "sdk/interface/include/hdmi/",
+ "sdk/interface/src/hdmi/",
+ "sdk/impl/hdmi/",
+ ],
+ "mi_hdmirx": [
+ "sdk/interface/include/hdmirx/",
+ "sdk/interface/src/hdmirx/",
+ "sdk/impl/hdmirx/",
+ ],
+ "mi_hvp": [
+ "sdk/interface/include/hvp/",
+ "sdk/interface/src/hvp/",
+ "sdk/impl/hvp/",
+ ],
+ "mi_ipu": [
+ "sdk/interface/include/ipu/",
+ "sdk/interface/src/ipu/",
+ "sdk/impl/ipu/",
+ ],
+ "mi_iqserver": [
+ "sdk/interface/include/iqserver/",
+ "sdk/interface/src/iqserver/",
+ ],
+ "mi_isp": [
+ "sdk/interface/include/isp/",
+ "sdk/interface/src/isp/",
+ "sdk/impl/isp/",
+ ],
+ "mi_ispalgo": [
+ "sdk/interface/include/ispalgo/",
+ "sdk/interface/src/ispalgo/",
+ ],
+ "mi_ive": [
+ "sdk/interface/include/ive/",
+ "sdk/interface/src/ive/",
+ "sdk/impl/ive/",
+ ],
+ "mi_jpd": [
+ "sdk/interface/include/jpd/",
+ "sdk/interface/src/jpd/",
+ "sdk/impl/jpd",
+ ],
+ "mi_ldc": [
+ "sdk/interface/include/ldc/",
+ "sdk/interface/src/ldc/",
+ "sdk/impl/ldc/",
+ ],
+ "mi_mipitx": [
+ "sdk/interface/include/mipitx/",
+ "sdk/interface/src/mipitx/",
+ "sdk/impl/mipitx/",
+ ],
+ "mi_nir": [
+ "sdk/interface/include/nir/",
+ "sdk/interface/src/nir/",
+ "sdk/impl/nir/",
+ ],
+ "mi_panel": [
+ "sdk/interface/include/panel/",
+ "sdk/interface/src/panel/",
+ "sdk/impl/panel/",
+ ],
+ "mi_pcie": [
+ "sdk/interface/include/pcie/",
+ "sdk/interface/src/pcie/",
+ "sdk/impl/pcie/",
+ ],
+ "mi_pspi": [
+ "sdk/interface/include/pspi/",
+ "sdk/interface/src/pspi/",
+ "sdk/impl/pspi/",
+ ],
+ "mi_rgn": [
+ "sdk/interface/include/rgn/",
+ "sdk/interface/src/rgn/",
+ "sdk/impl/rgn/",
+ ],
+ "mi_scl": [
+ "sdk/interface/include/scl/",
+ "sdk/interface/src/scl/",
+ "sdk/impl/scl/",
+ ],
+ "mi_sed": [
+ "sdk/interface/include/sed/",
+ "sdk/interface/include/vg/",
+ "sdk/interface/src/sed/",
+ ],
+ "mi_sensor": [
+ "sdk/interface/include/sensor/",
+ "sdk/interface/src/sensor/",
+ "sdk/impl/sensor/",
+ ],
+ "mi_shadow": [
+ "sdk/interface/include/shadow/",
+ "sdk/interface/src/shadow/",
+ "sdk/impl/shadow/",
+ ],
+ "mi_sys": [
+ "sdk/interface/include/sys/",
+ "sdk/interface/src/sys/",
+ "sdk/impl/sys/",
+ ],
+ "mi_sys_earlyinit": [
+ "sdk/interface/include/sys_earlyinit/",
+ "sdk/interface/src/sys_earlyinit/",
+ "sdk/impl/sys_earlyinit/",
+ ],
+ "mi_vdec": [
+ "sdk/interface/include/vdec/",
+ "sdk/interface/src/vdec/",
+ "sdk/impl/vdec/",
+ ],
+ "mi_vdf": [
+ "sdk/interface/include/md/",
+ "sdk/interface/include/od/",
+ "sdk/interface/include/vg/",
+ "sdk/interface/include/vdf/",
+ "sdk/interface/src/vdf/",
+ ],
+ "mi_vcodec": [
+ "sdk/interface/src/vcodec/",
+ "sdk/interface/src/vcodec/config.mk",
+ ],
+ "mi_vdisp": [
+ "sdk/interface/include/vdisp/",
+ "sdk/interface/src/vdisp/",
+ "sdk/impl/vdisp/",
+ ],
+ "mi_venc": [
+ "sdk/interface/include/venc/",
+ "sdk/interface/src/venc/",
+ "sdk/impl/venc/",
+ ],
+ "mi_vif": [
+ "sdk/interface/include/vif/",
+ "sdk/interface/src/vif/",
+ "sdk/impl/vif/",
+ ],
+ "mi_wlan": [
+ "sdk/interface/include/wlan/",
+ "sdk/interface/src/wlan/",
+ ],
+ "bench": [
+ "rtos/proj/sc/application/bench"
+ ],
+ "bootloader": [
+ "rtos/proj/sc/application/bootloader"
+ ],
+ "coremark": [
+ "rtos/proj/sc/application/coremark"
+ ],
+ "common": [
+ "rtos/proj/sc/application/pipeline_demo/common"
+ ],
+ "font": [
+ "rtos/proj/sc/application/pipeline_demo/font"
+ ],
+ "iqserver": [
+ "rtos/proj/sc/application/pipeline_demo/iqserver"
+ ],
+ "audio_app": [
+ "rtos/proj/sc/application/pipeline_demo/audio_app"
+ ],
+ "disp_app": [
+ "rtos/proj/sc/application/pipeline_demo/disp_app"
+ ],
+ "dualos_camera": [
+ "rtos/proj/sc/application/pipeline_demo/dualos_camera"
+ ],
+ "dualos_pipeline": [
+ "rtos/proj/sc/application/pipeline_demo/dualos_pipeline"
+ ],
+ "application_selector": [
+ "rtos/proj/sc/application/pipeline_demo/application_selector"
+ ],
+ "ptree": [
+ "sdk/verify/ptree"
+ ],
+ "rtos_bsp_demo": [
+ "sdk/verify/bsp_demo/rtos_bsp_demo"
+ ],
+ "preload_rtos": [
+ r"sdk/verify/sample_code/source/{SWBoardAlias}/preload/rtos"
+ ],
+ "preload_sample": [
+ r"sdk/verify/sample_code/source/{SWBoardAlias}/preload_sample/rtos"
+ ],
+ "aov_preload": [
+ r"sdk/verify/sample_code/source/{SWBoardAlias}/aov/preload/rtos/aov_preload"
+ ],
+ "cm4_preload": [
+ r"sdk/verify/sample_code/source/{SWBoardAlias}/cm4/preload/rtos/cm4_preload"
+ ],
+ "sample_code": [
+ "sdk/verify/sample_code/build"
+ ],
+ "sensordriver": [
+ "sdk/driver/SensorDriver"
+ ],
+ "lvgl": [
+ "rtos/proj/sc/application/pipeline_demo/lvgl"
+ ],
+ "usb_gadget_app": [
+ "rtos/proj/sc/application/pipeline_demo/usb_gadget_app"
+ ],
+ "usb_gadget_app_uvc": [
+ "rtos/proj/sc/application/pipeline_demo/usb_gadget_app"
+ ],
+ "cust_isp": [
+ "rtos/proj/sc/customer/cust_isp"
+ ],
+ "cust_usb_gadget": [
+ "rtos/proj/sc/customer/usb_gadget"
+ ],
+ "adc": [
+ "rtos/proj/sc/driver/sysdriver_common/adc"
+ ],
+ "aesdma": [
+ "rtos/proj/sc/driver/sysdriver/aesdma"
+ ],
+ "bdma": [
+ "rtos/proj/sc/driver/sysdriver/bdma",
+ "rtos/proj/sc/driver/sysdriver_common/bdma"
+ ],
+ "capture": [
+ "rtos/proj/sc/driver/sysdriver_common/capture"
+ ],
+ "crypto": [
+ "rtos/proj/sc/driver/sysdriver_common/crypto"
+ ],
+ "camclk": [
+ "rtos/proj/sc/driver/sysdriver/camclk",
+ "rtos/proj/sc/driver/sysdriver_common/clk"
+ ],
+ "cpufreq": [
+ "rtos/proj/sc/driver/sysdriver/cpufreq"
+ ],
+ "drvutil": [
+ "rtos/proj/sc/driver/sysdriver/drvutil"
+ ],
+ "dualos": [
+ "rtos/proj/sc/driver/sysdriver/dualos",
+ "rtos/proj/sc_priv/driver/sysdriver/dualos"
+ ],
+ "earlyinit_setting": [
+ "rtos/proj/sc/customer/earlyinit_setting"
+ ],
+ "earlyinit_main": [
+ "rtos/proj/sc/driver/sysdriver/earlyinit_main"
+ ],
+ "earlyinit_impl": [
+ "rtos/proj/sc/driver/sysdriver/earlyinit_impl",
+ "rtos/proj/sc/driver/sysdriver/sensor_init/impl"
+ ],
+ "earlyinit_rtos_api": [
+ "rtos/proj/sc/driver/sysdriver/earlyinit_rtos_api",
+ "rtos/proj/sc/driver/sysdriver/sensor_init/rtos_api"
+ ],
+ "earlyinit_vsrc": [
+ "rtos/proj/sc/driver/sysdriver/sensor_init/vsrc"
+ ],
+ "ipl_early_fw": [
+ "rtos/proj/sc/driver/sysdriver/ipl_early_fw"
+ ],
+ "emac": [
+ "rtos/proj/sc/driver/sysdriver/emac"
+ ],
+ "fcie": [
+ "rtos/proj/sc/driver/sysdriver/fcie",
+ "rtos/proj/sc/driver/sysdriver_common/fcie"
+ ],
+ "flash": [
+ "rtos/proj/sc/driver/sysdriver/flash",
+ "rtos/proj/sc/driver/sysdriver_common/flash"
+ ],
+ "fsp_qspi": [
+ "rtos/proj/sc/driver/sysdriver/fsp_qspi",
+ "rtos/proj/sc/driver/sysdriver_common/fsp_qspi"
+ ],
+ "gpio": [
+ "rtos/proj/sc/driver/sysdriver/gpio",
+ "rtos/proj/sc/driver/sysdriver_common/gpio"
+ ],
+ "i2c": [
+ "rtos/proj/sc/driver/sysdriver/i2c",
+ "rtos/proj/sc/driver/sysdriver_common/i2c"
+ ],
+ "input": [
+ "rtos/proj/sc/driver/sysdriver/input"
+ ],
+ "int": [
+ "rtos/proj/sc/driver/sysdriver/int"
+ ],
+ "ir": [
+ "rtos/proj/sc/driver/sysdriver/ir",
+ "rtos/proj/sc/driver/sysdriver_common/ir"
+ ],
+ "ive": [
+ "rtos/proj/sc/driver/sysdriver/ive",
+ "rtos/proj/sc_priv/driver/sysdriver/ive"
+ ],
+ "kernel": [
+ "rtos/proj/sc/driver/sysdriver/kernel"
+ ],
+ "loadns": [
+ "rtos/proj/sc/driver/sysdriver/loadns"
+ ],
+ "mbx": [
+ "rtos/proj/sc/driver/sysdriver/mbx"
+ ],
+ "miu": [
+ "rtos/proj/sc/driver/sysdriver/miu",
+ "rtos/proj/sc/driver/sysdriver_common/miu",
+ "rtos/proj/sc/driver/sysdriver_common/miu_dq"
+ ],
+ "mmupte": [
+ "rtos/proj/sc/driver/sysdriver/mmupte"
+ ],
+ "mspi": [
+ "rtos/proj/sc/driver/sysdriver/mspi",
+ "rtos/proj/sc/driver/sysdriver_common/mspi"
+ ],
+ "riu": [
+ "rtos/proj/sc/driver/sysdriver/riu"
+ ],
+ "msys": [
+ "rtos/proj/sc/driver/sysdriver/msys"
+ ],
+ "otp": [
+ "rtos/proj/sc/driver/sysdriver/otp",
+ "rtos/proj/sc_priv/driver/sysdriver/otp"
+ ],
+ "padmux": [
+ "rtos/proj/sc/driver/sysdriver/padmux",
+ "rtos/proj/sc/driver/sysdriver_common/padmux"
+ ],
+ "pcie": [
+ "rtos/proj/sc/driver/sysdriver/pcie"
+ ],
+ "pl011": [
+ "rtos/proj/sc/driver/sysdriver/pl011"
+ ],
+ "power_manag": [
+ "rtos/proj/sc/driver/sysdriver/power_manag",
+ "rtos/proj/sc_priv/driver/sysdriver/power_manag"
+ ],
+ "pwm": [
+ "rtos/proj/sc/driver/sysdriver/pwm",
+ "rtos/proj/sc/driver/sysdriver_common/pwm"
+ ],
+ "rpmsg_lite": [
+ "rtos/proj/sc/driver/sysdriver/rpmsg-lite",
+ "rtos/proj/sc_priv/driver/sysdriver/rpmsg-lite"
+ ],
+ "rtc": [
+ "rtos/proj/sc/driver/sysdriver/rtc"
+ ],
+ "rtcpwc": [
+ "rtos/proj/sc/driver/sysdriver/rtcpwc",
+ "rtos/proj/sc/driver/sysdriver_common/rtcpwc"
+ ],
+ "saradc": [
+ "rtos/proj/sc/driver/sysdriver/saradc"
+ ],
+ "sdmmc": [
+ "rtos/proj/sc/driver/sysdriver/sdmmc",
+ "rtos/proj/sc/driver/sysdriver_common/sdmmc"
+ ],
+ "str": [
+ "rtos/proj/sc/driver/sysdriver/str"
+ ],
+ "sysdesc": [
+ "rtos/proj/sc/driver/sysdriver/sysdesc",
+ "rtos/proj/sc/driver/sysdriver_common/include"
+ ],
+ "timer": [
+ "rtos/proj/sc/driver/sysdriver/timer",
+ "rtos/proj/sc/driver/sysdriver_common/timer"
+ ],
+ "tsensor": [
+ "rtos/proj/sc/driver/sysdriver/tsensor",
+ "rtos/proj/sc/driver/sysdriver_common/tsensor"
+ ],
+ "uart": [
+ "rtos/proj/sc/driver/sysdriver/uart"
+ ],
+ "usb_gadget_udc_usb20": [
+ "rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usb20",
+ "rtos/proj/sc_priv/driver/sysdriver/usb_gadget/udc/usb20"
+ ],
+ "usb_gadget_udc_usb30": [
+ "rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usb30",
+ "rtos/proj/sc_priv/driver/sysdriver/usb_gadget/udc/usb30"
+ ],
+ "usbphy_drv": [
+ "rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usbphy",
+ "rtos/proj/sc_priv/driver/sysdriver/usb_gadget/udc/usbphy"
+ ],
+ "usbhost": [
+ "rtos/proj/sc/driver/sysdriver/usbhost",
+ "rtos/proj/sc_priv/driver/sysdriver/usbhost"
+ ],
+ "voltage": [
+ "rtos/proj/sc/driver/sysdriver/voltage",
+ "rtos/proj/sc/driver/sysdriver_common/voltage"
+ ],
+ "watchdog": [
+ "rtos/proj/sc/driver/sysdriver/watchdog",
+ "rtos/proj/sc/driver/sysdriver_common/watchdog"
+ ],
+ "wifi_ssw6x5x": [
+ "rtos/proj/sc/driver/thirdparty/wifi/ssw6x5x"
+ ],
+ "algo": [
+ "rtos/proj/sc/middleware/algo"
+ ],
+ "decompress": [
+ "rtos/proj/sc/middleware/decompress"
+ ],
+ "firmwarefs": [
+ "rtos/proj/sc/middleware/fs/firmwarefs",
+ "rtos/proj/sc_priv/middleware/fs/firmwarefs"
+ ],
+ "freertos_plus_fat": [
+ "rtos/proj/sc/middleware/fs/Lab-Project-FreeRTOS-FAT"
+ ],
+ "littlefs": [
+ "rtos/proj/sc/middleware/fs/littlefs",
+ "rtos/proj/sc_priv/middleware/fs/littlefs"
+ ],
+ "lwfs": [
+ "rtos/proj/sc/middleware/fs/lwfs"
+ ],
+ "nfs": [
+ "rtos/proj/sc/middleware/fs/nfs",
+ "rtos/proj/sc_priv/middleware/fs/nfs"
+ ],
+ "proxyfs": [
+ "rtos/proj/sc/middleware/fs/proxyfs"
+ ],
+ "tcpip": [
+ "rtos/proj/sc/middleware/tcpip"
+ ],
+ "arm": [
+ "rtos/proj/sc/system/arch"
+ ],
+ "riscv": [
+ "rtos/proj/sc/system/riscv"
+ ],
+ "cam_dev_wrapper": [
+ "rtos/proj/sc/system/cam_dev_wrapper",
+ "rtos/proj/sc/driver/sysdriver_common/cam_dev_wrapper"
+ ],
+ "cam_drv_poll": [
+ "rtos/proj/sc/system/cam_drv_poll",
+ "rtos/proj/sc/driver/sysdriver_common/cam_drv_poll"
+ ],
+ "cam_fs_wrapper": [
+ "rtos/proj/sc/system/cam_fs_wrapper",
+ "rtos/proj/sc/driver/sysdriver_common/cam_fs_wrapper"
+ ],
+ "cam_os_wrapper": [
+ "rtos/proj/sc/system/cam_os_wrapper",
+ "rtos/proj/sc/driver/sysdriver_common/cam_os_wrapper"
+ ],
+ "cam_proc_wrapper": [
+ "rtos/proj/sc/system/cam_proc_wrapper",
+ "rtos/proj/sc/driver/sysdriver_common/cam_proc_wrapper"
+ ],
+ "env_util": [
+ "rtos/proj/sc/system/env_util"
+ ],
+ "initcall": [
+ "rtos/proj/sc/system/initcall"
+ ],
+ "libc": [
+ "rtos/proj/sc/system/libc"
+ ],
+ "libstdcpp_plugin": [
+ "rtos/proj/sc/system/libstdcpp_plugin",
+ "rtos/proj/sc_priv/system/libstdcpp_plugin"
+ ],
+ "newlib_stub": [
+ "rtos/proj/sc/system/newlib_stub"
+ ],
+ "loadable_module": [
+ "rtos/proj/sc/system/loadable_module"
+ ],
+ "memmang": [
+ "rtos/proj/sc/system/memmang"
+ ],
+ "memmap": [
+ "rtos/proj/sc/system/memmap"
+ ],
+ "mempool": [
+ "rtos/proj/sc/system/mempool"
+ ],
+ "MsWrapper": [
+ "rtos/proj/sc/system/MsWrapper"
+ ],
+ "newlib_stub": [
+ "rtos/proj/sc/system/newlib_stub"
+ ],
+ "freertos": [
+ "rtos/proj/sc/system/rtos/freertos",
+ "rtos/proj/sc_priv/system/rtos/freertos"
+ ],
+ "freertos_main": [
+ "rtos/proj/sc/system/rtos/freertos_main"
+ ],
+ "freertos_posix": [
+ "rtos/proj/sc/system/rtos/freertos_posix"
+ ],
+ "sys_I_SW": [
+ "rtos/proj/sc/system/sys"
+ ],
+ "context_switch": [
+ "rtos/proj/sc_priv/arch/arm/cortex-a/v7_aarch32/context_switch"
+ ],
+ "lh_monitor": [
+ "rtos/proj/sc_priv/arch/arm/cortex-a/v7_aarch32/lh_monitor"
+ ],
+ "pm_bench": [
+ "pm_rtos/proj/sc/bench"
+ ],
+ "pm_bootloader": [
+ "pm_rtos/proj/sc/application/bootloader"
+ ],
+ "pm_coremark": [
+ "pm_rtos/proj/sc/application/coremark"
+ ],
+ "pm_common": [
+ "pm_rtos/proj/sc/application/common"
+ ],
+ "pm_dhrystone": [
+ "pm_rtos/proj/sc/application/dhrystone"
+ ],
+ "pm_dualos_pipeline": [
+ "pm_rtos/proj/sc/application/dualos_pipeline"
+ ],
+ "pm_dualos_camera": [
+ "pm_rtos/proj/sc/application/dualos_camera"
+ ],
+ "pm_usb_gadget_app": [
+ "pm_rtos/proj/sc/application/usb_gadget_app"
+ ],
+ "pm_algo_test_app": [
+ "pm_rtos/proj/sc/application/algo_test_app"
+ ],
+ "pm_slnn_raw_hpd": [
+ "pm_rtos/proj/sc/application/slnn_raw_hpd"
+ ],
+ "pm_slnn_sypd": [
+ "pm_rtos/proj/sc/application/slnn_sypd"
+ ],
+ "pm_slnn_syfd": [
+ "pm_rtos/proj/sc/application/slnn_syfd"
+ ],
+ "pm_cm4_pipeline": [
+ "pm_rtos/proj/sc/application/cm4_pipeline"
+ ],
+ "pm_audio_aed": [
+ "pm_rtos/proj/sc/application/audio_aed"
+ ],
+ "pm_audio_wos": [
+ "pm_rtos/proj/sc/application/audio_wos"
+ ],
+ "pm_cust_isp": [
+ "pm_rtos/proj/sc/customer/cust_isp"
+ ],
+ "pm_cust_usb_gadget": [
+ "pm_rtos/proj/sc/customer/usb_gadget"
+ ],
+ "pm_aesdma": [
+ "pm_rtos/proj/sc/driver/sysdriver/aesdma"
+ ],
+ "pm_bdma": [
+ "pm_rtos/proj/sc/driver/sysdriver/bdma"
+ ],
+ "pm_camclk": [
+ "pm_rtos/proj/sc/driver/sysdriver/camclk"
+ ],
+ "pm_cpufreq": [
+ "pm_rtos/proj/sc/driver/sysdriver/cpufreq"
+ ],
+ "pm_drvutil": [
+ "pm_rtos/proj/sc/driver/sysdriver/drvutil"
+ ],
+ "pm_dualos": [
+ "pm_rtos/proj/sc/driver/sysdriver/dualos"
+ ],
+ "pm_loadns": [
+ "pm_rtos/proj/sc/driver/sysdriver/loadns"
+ ],
+ "pm_emac": [
+ "pm_rtos/proj/sc/driver/sysdriver/emac"
+ ],
+ "pm_flash": [
+ "pm_rtos/proj/sc/driver/sysdriver/flash"
+ ],
+ "pm_fsp_qspi": [
+ "pm_rtos/proj/sc/driver/sysdriver/fsp_qspi"
+ ],
+ "pm_gpio": [
+ "pm_rtos/proj/sc/driver/sysdriver/gpio"
+ ],
+ "pm_i2c": [
+ "pm_rtos/proj/sc/driver/sysdriver/i2c"
+ ],
+ "pm_int": [
+ "pm_rtos/proj/sc/driver/sysdriver/int"
+ ],
+ "pm_ir": [
+ "pm_rtos/proj/sc/driver/sysdriver/ir"
+ ],
+ "pm_ive": [
+ "pm_rtos/proj/sc/driver/sysdriver/ive"
+ ],
+ "pm_kernel": [
+ "pm_rtos/proj/sc/driver/sysdriver/kernel"
+ ],
+ "pm_miu": [
+ "pm_rtos/proj/sc/driver/sysdriver/miu"
+ ],
+ "pm_mmupte": [
+ "pm_rtos/proj/sc/driver/sysdriver/mmupte"
+ ],
+ "pm_mspi": [
+ "pm_rtos/proj/sc/driver/sysdriver/mspi"
+ ],
+ "pm_msys": [
+ "pm_rtos/proj/sc/driver/sysdriver/msys"
+ ],
+ "pm_padmux": [
+ "pm_rtos/proj/sc/driver/sysdriver/padmux"
+ ],
+ "pm_pcie": [
+ "pm_rtos/proj/sc/driver/sysdriver/pcie"
+ ],
+ "pm_pl011": [
+ "pm_rtos/proj/sc/driver/sysdriver/pl011"
+ ],
+ "pm_mhal_pm_clk": [
+ "pm_rtos/proj/sc/driver/sysdriver/pm_clk"
+ ],
+ "pm_pm_idle": [
+ "pm_rtos/proj/sc/driver/sysdriver/pm_idle"
+ ],
+ "pm_mhal_pm_mbx": [
+ "pm_rtos/proj/sc/driver/sysdriver/pm_mbx"
+ ],
+ "pm_pm_power": [
+ "pm_rtos/proj/sc/driver/sysdriver/pm_power"
+ ],
+ "pm_pm_rtcpwc": [
+ "pm_rtos/proj/sc/driver/sysdriver/pm_rtcpwc"
+ ],
+ "pm_mhal_pm_sys": [
+ "pm_rtos/proj/sc/driver/sysdriver/pm_sys"
+ ],
+ "pm_mhal_pm_wdt": [
+ "pm_rtos/proj/sc/driver/sysdriver/pm_wdt"
+ ],
+ "pm_power_manag": [
+ "pm_rtos/proj/sc/driver/sysdriver/power_manag"
+ ],
+ "pm_pspi": [
+ "pm_rtos/proj/sc/driver/sysdriver/pspi"
+ ],
+ "pm_pwm": [
+ "pm_rtos/proj/sc/driver/sysdriver/pwm"
+ ],
+ "pm_rpmsg_lite": [
+ "pm_rtos/proj/sc/driver/sysdriver/rpmsg-lite"
+ ],
+ "pm_rtc": [
+ "pm_rtos/proj/sc/driver/sysdriver/rtc"
+ ],
+ "pm_rtcpwc": [
+ "pm_rtos/proj/sc/driver/sysdriver/rtcpwc"
+ ],
+ "pm_saradc": [
+ "pm_rtos/proj/sc/driver/sysdriver/saradc"
+ ],
+ "pm_sdmmc": [
+ "pm_rtos/proj/sc/driver/sysdriver/sdmmc"
+ ],
+ "pm_sysdesc": [
+ "pm_rtos/proj/sc/driver/sysdriver/sysdesc"
+ ],
+ "pm_timer": [
+ "pm_rtos/proj/sc/driver/sysdriver/timer"
+ ],
+ "pm_tsensor": [
+ "pm_rtos/proj/sc/driver/sysdriver/tsensor"
+ ],
+ "pm_uart": [
+ "pm_rtos/proj/sc/driver/sysdriver/uart"
+ ],
+ "pm_usb_gadget_udc_usb20": [
+ "pm_rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usb20"
+ ],
+ "pm_usb_gadget_udc_usb30": [
+ "pm_rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usb30"
+ ],
+ "pm_usbphy_drv": [
+ "pm_rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usbphy"
+ ],
+ "pm_usbhost": [
+ "pm_rtos/proj/sc/driver/sysdriver/usbhost"
+ ],
+ "pm_voltage": [
+ "pm_rtos/proj/sc/driver/sysdriver/voltage"
+ ],
+ "pm_watchdog": [
+ "pm_rtos/proj/sc/driver/sysdriver/watchdog"
+ ],
+ "pm_decompress": [
+ "pm_rtos/proj/sc/middleware/decompress"
+ ],
+ "pm_firmwarefs": [
+ "pm_rtos/proj/sc/middleware/fs/firmwarefs"
+ ],
+ "pm_freertos_plus_fat": [
+ "pm_rtos/proj/sc/middleware/fs/Lab-Project-FreeRTOS-FAT"
+ ],
+ "pm_littlefs": [
+ "pm_rtos/proj/sc/middleware/fs/littlefs"
+ ],
+ "pm_lwfs": [
+ "pm_rtos/proj/sc/middleware/fs/lwfs"
+ ],
+ "pm_proxyfs": [
+ "pm_rtos/proj/sc/middleware/fs/proxyfs"
+ ],
+ "pm_tcpip": [
+ "pm_rtos/proj/sc/middleware/tcpip"
+ ],
+ "pm_arm": [
+ "pm_rtos/proj/sc/system/arch"
+ ],
+ "pm_riscv": [
+ "pm_rtos/proj/sc/system/riscv"
+ ],
+ "pm_cam_dev_wrapper": [
+ "pm_rtos/proj/sc/system/cam_dev_wrapper"
+ ],
+ "pm_cam_drv_poll": [
+ "pm_rtos/proj/sc/system/cam_drv_poll"
+ ],
+ "pm_cam_fs_wrapper": [
+ "pm_rtos/proj/sc/system/cam_fs_wrapper"
+ ],
+ "pm_cam_os_wrapper": [
+ "pm_rtos/proj/sc/system/cam_os_wrapper"
+ ],
+ "pm_cam_proc_wrapper": [
+ "pm_rtos/proj/sc/system/cam_proc_wrapper"
+ ],
+ "pm_env_util": [
+ "pm_rtos/proj/sc/system/env_util"
+ ],
+ "pm_initcall": [
+ "pm_rtos/proj/sc/system/initcall"
+ ],
+ "pm_libc": [
+ "pm_rtos/proj/sc/system/libc"
+ ],
+ "pm_memmang": [
+ "pm_rtos/proj/sc/system/memmang"
+ ],
+ "pm_memmap": [
+ "pm_rtos/proj/sc/system/memmap"
+ ],
+ "pm_mempool": [
+ "pm_rtos/proj/sc/system/mempool"
+ ],
+ "pm_MsWrapper": [
+ "pm_rtos/proj/sc/system/MsWrapper"
+ ],
+ "pm_freertos": [
+ "pm_rtos/proj/sc/system/rtos/freertos"
+ ],
+ "pm_freertos_posix": [
+ "pm_rtos/proj/sc/system/rtos/freertos_posix"
+ ],
+ "pm_sys_I_SW": [
+ "pm_rtos/proj/sc/system/sys"
+ ],
+ "pm_mhal_pm_default": [
+ "pm_rtos/proj/sc/driver/camdriver/common"
+ ],
+ "pm_mhal_imi_heap": [
+ "pm_rtos/proj/sc/driver/sysdriver/imi_heap"
+ ],
+ "pm_mhal_pm_aio": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_aio"
+ ],
+ "pm_mhal_pm_idle": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_idle"
+ ],
+ "pm_mhal_pm_isp": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_isp"
+ ],
+ "pm_mhal_pm_ispalgo": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_ispalgo"
+ ],
+ "pm_mhal_pm_jpe": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_jpe"
+ ],
+ "pm_mhal_pm_md": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_md"
+ ],
+ "pm_mhal_pm_pir": [
+ "pm_rtos/proj/sc/driver/sysdriver/pm_pir"
+ ],
+ "pm_mhal_pm_radar": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_radar"
+ ],
+ "pm_mhal_pm_radar_algo": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_radar_algo"
+ ],
+ "pm_mhal_pm_usb_gadget_udc_usb30": [
+ "pm_rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usb30"
+ ],
+ "pm_mhal_pm_usbpoc": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_usbpoc"
+ ],
+ "pm_mhal_pm_vif": [
+ "pm_rtos/proj/sc/driver/camdriver/pm_vif"
+ ]
+ }
+
+ def check(self) -> bool:
+ for key, value in self.items():
+ if not isinstance(value, list):
+ raise ValueError(f"Invalid value type for {key}: {type(value)}")
+ for item in value:
+ if not isinstance(item, str):
+ raise ValueError(f"Invalid item type for {key}: {type(item)}")
+ # 格式化路径 将一些动态路径在运行时确定下来
+ file = self._path_format(item)
+ if not check_file_exists(file):
+ log.warning(f"{self.__class__.__name__} config [{file}] not exists")
+ self[key] = file # 将格式化后的路径更新到_dict中
+ return True
+
+ def _path_format(self, item: str) -> str:
+ return str_format(Path(ReleaseOptions.AlkaidRootPath) / item)
+
+
+def str_format(item: Any) -> str:
+ """格式化字符串,将字符串中的{key}替换为ReleaseOptions._dict[key]
+ 我们目的是将一些会跟随实际chip分支会变化的文件路径在运行时才确定下来
+ ## 例如:
+ ```python
+ item = "sdk/verify/sample_code/source/{SWBoardAlias}/preload/rtos"
+ item = str_format(item)
+ print(item)
+ ```
+ ## 输出:
+ sdk/verify/sample_code/source/iford/preload/rtos
+
+ Args:
+ item (str): 需要格式化的字符串
+
+ Returns:
+ str: 格式化后的字符串
+ """
+ item = str(item)
+ if '{' in item:
+ # log.info(f"format item: {item}")
+ item = item.format(**ReleaseOptions._dict) # 使用keyargs的方式格式化字符串
+ # log.info(f"formated item: {item} use {ReleaseOptions._dict}")
+ return item
+
+def CheckDirAliasDecorator(cls):
+ """配置检查装饰器:检查配置类中的所有项是否在 DirAlias 中存在, 如果是一个路径则检查其是否存在
+ 例如:
+ ```python
+ @CheckDirAliasDecorator
+ class Test(BaseConfig):
+ _config = ["mi_sys", "mi_isp", "sdk/verify/sample_code/source/{SWBoardAlias}/preload/rtos"]
+ ```
+ 那么会检查mi_sys和mi_isp是否在DirAlias中存在,如果存在则返回True,否则返回False
+ 同时会检查sdk/verify/sample_code/source/{SWBoardAlias}/preload/rtos是否存在
+ Args:
+ cls: 被装饰的类
+ Returns:
+ cls: 装饰后的类
+ """
+ original_check = cls.check
+ @wraps(original_check)
+ def wrapped_check(self) -> bool:
+ for item in self:
+ # 如果不是一个路径那么判断其是否在DirAlias中存在
+ if '/' not in item and not DirAlias[item]:
+ raise ValueError(f"{item} not found in DirAlias")
+ # 如果是路径那么判断其是否存在
+ if '/' in item:
+ if not check_file_exists(item):
+ log.warning(f"{self.__class__.__name__} config [{item}] not exists")
+ return True
+ cls.check = wrapped_check
+ return cls
\ No newline at end of file
diff --git a/database/options.py b/database/options.py
new file mode 100644
index 0000000..c0e7358
--- /dev/null
+++ b/database/options.py
@@ -0,0 +1,241 @@
+import argparse
+import sys, os
+from typing import List
+import textwrap
+import argparse
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+import core.logger as logger
+from utils.utils import get_alkaid_root
+log = logger.get_logger()
+from database.base import BaseConfig, AutoInstanceDecorator
+
+class ConfigureStyleHelpFormatter(argparse.HelpFormatter):
+ """自定义的帮助信息格式化类,用于美化 argparse 的帮助输出
+
+ 特点:
+ 1. 带有彩色标记的输出样式
+ 2. 优化了选项和描述的对齐
+ """
+
+ # 颜色代码定义
+ BLUE = "\033[1;34m" # 蓝色,用于位置参数
+ GREEN = "\033[1;32m" # 绿色,用于普通选项
+ YELLOW = "\033[1;33m" # 黄色,用于必选参数
+ RESET = "\033[0m" # 重置颜色
+
+ def __init__(self, prog, indent_increment=2, max_help_position=8, width=None):
+ # 根据终端宽度自动调整
+ try:
+ width = width or min(120, os.get_terminal_size().columns)
+ except OSError:
+ width = width or 120 # 使用默认宽度
+ super().__init__(prog, indent_increment, max_help_position, width)
+
+ def _format_action_invocation(self, action):
+ """格式化参数选项的显示方式,添加彩色显示"""
+ if not action.option_strings:
+ # 位置参数,显示为蓝色
+ metavar, = self._metavar_formatter(action, action.dest)(1)
+ return f"{self.BLUE}{metavar}{self.RESET}"
+
+ # 处理可选参数
+ parts = []
+
+ # 判断是否为必选参数,是则使用黄色,否则使用绿色
+ color = self.YELLOW if action.required else self.GREEN
+
+ # 如果同时有短选项和长选项
+ if len(action.option_strings) > 1:
+ short, long = action.option_strings
+ parts.append(f"{color}{short}{self.RESET}, {color}{long}{self.RESET}")
+ else:
+ # 只有一个选项
+ parts.append(f"{color}{action.option_strings[0]}{self.RESET}")
+
+ # 添加 metavar(如果有)
+ if action.metavar is not None:
+ parts.append(f" {action.metavar}")
+ elif action.nargs != 0:
+ parts.append(f" {action.dest.upper()}")
+
+ return ''.join(parts)
+
+ def add_usage(self, usage, actions, groups, prefix=None):
+ """重写帮助信息的用法部分,优化了显示格式"""
+ if prefix is None:
+ prefix = 'usage: '
+ return super().add_usage(usage, actions, groups, prefix)
+
+ def _format_action(self, action):
+ """重写格式化动作方法,确保帮助文本正确对齐和显示"""
+ # 获取基础格式化的帮助文本
+ result = super()._format_action(action)
+
+ # 如果有choices且不是帮助动作,则添加选项信息
+ if hasattr(action, 'choices') and action.choices and action.option_strings:
+ # 查找最后一个换行符后的位置
+ last_line_start = result.rfind('\n') + 1
+ # indentation = ' ' * (last_line_start - result.rfind('\n', 0, last_line_start - 1) - 1)
+ indentation = ' ' * 8
+
+ # 格式化choices选项
+ choices_str = f"{indentation} 可选值: {', '.join([str(c) for c in action.choices])}"
+ result = f"{result}\n{choices_str} \n"
+
+ return result
+
+ def _split_lines(self, text, width):
+ """优化帮助文本的换行处理"""
+ if not text:
+ return []
+ if '\n' in text:
+ # 开头的文本按原样分行显示
+ ret = []
+ for _ in text.splitlines():
+ _ = _.strip()
+ ret.append(_[1:] if _[0] == '|' else _)
+ return ret
+ # 正常文本使用 textwrap 换行
+ return textwrap.wrap(text, width)
+
+ def _fill_text(self, text, width, indent):
+ """优化帮助文本的填充显示"""
+ if not text:
+ return ""
+ # 使用textwrap对文本进行缩进和填充
+ text = self._whitespace_matcher.sub(' ', text).strip()
+ paragraphs = text.split('.\n ')
+ multi_paragraphs = '\n'.join([textwrap.fill(p, width) for p in paragraphs])
+ return textwrap.indent(multi_paragraphs, indent)
+
+
+@AutoInstanceDecorator
+class ReleaseOptions(BaseConfig):
+ """Release 选项
+
+ Args:
+ SkipCheckout (bool, optional): 是否跳过checkout kernel/mboot 的操作. Defaults to False.
+ OpenSource (bool, optional):
+ #是否开启SDK开源模式:
+ #闭源模式: MI编译为lib和ko对外提供功能
+ #开源模式:MI源码发布, 客户可直接获得MI原始代码
+ # 开源的list:
+ # sdk/interface/src/vpe/
+ # sdk/interface/src/venc/
+ # sdk/impl/vpe
+ # sdk/impl/venc/. Defaults to False.
+ Cm4Support (bool, optional): 支持release CM4. Defaults to False.
+ RiscVSupport (bool, optional): 支持release RiscV. Defaults to False.
+ MounRiverSDK (int, optional): 选择MounRiver SDK版本. Defaults to 0.
+ ThreadNum (int, optional): 编译线程数. Defaults to 1.
+ Doc2Pdf (bool, optional): 编译文档为pdf格式. Defaults to False.
+ VerifyBuild (bool, optional): 验证编译结果. Defaults to False.
+ Doc2Html (bool, optional): 编译文档为html格式. Defaults to False.
+ ReduceCodeSize (bool, optional): 减小编译代码大小. Defaults to False.
+ SkipSyncCode (bool, optional): 跳过代码同步. Defaults to False.
+ Ipl_version (str, optional): 选择IPL版本. Defaults to "CN".
+ """
+ _config = {
+ # 必需参数
+ "SWBoardAlias": "", # -a: 板子别名
+ "TagId": "", # -t: 标签ID
+ "SnapShotXml": "", # -s: 快照XML文件
+ "SWBoardType": "", # -b: 板子类型
+ "SWProductType": "", # -p: 产品类型
+
+ # 可选参数
+ "AlkaidRootPath": '', # 仓库根路径
+ "DocsPath": "", # -d: 文档路径
+ "HWChipType": "", # -c: 硬件芯片类型
+ "HWPackageType": "", # -k: 硬件包类型
+ "OutPath": "", # -o: 输出路径
+ "ThreadNum": 1, # -j: 线程数
+ "IplVersion": "CN", # -l: IPL版本
+
+ # 布尔参数
+ "SkipCheckout": False, # -i: 跳过从bin检出kernel/mboot
+ "OpenSource": False, # -f: 开放源代码
+ "MounRiverSDK": False, # -m: 启用RiscVSupport时支持MounRiverSDK
+ "RiscVSupport": False, # -n: 支持riscv源码
+ "Cm4Support": False, # -y: 支持cm4源码
+ "Doc2Pdf": False, # -z: 文档转换为pdf
+ "Doc2Html": False, # -h: 文档转换为html
+ "VerifyBuild": False, # -v: 验证构建
+ "ReduceCodeSize": False, # -u: 减小代码大小
+ "SkipSyncCode": False, # -x: 跳过通过快照XML同步代码
+ "OpenRtosWhiteList": False, # -X: 开放所有rtos白名单
+ "OpenCm4WhiteList": False, # -Y: 开放所有cm4白名单
+ }
+
+ def __init__(self):
+ super().__init__()
+ parser = argparse.ArgumentParser(description='Sigmastar Release SDK Options',
+ formatter_class=ConfigureStyleHelpFormatter,
+ epilog= "more help info see: http://xxxxxx",
+ add_help=False) #-hhelp 信息被占用了
+ add = parser.add_argument
+ add('-a', '--SWBoardAlias' , required=True, type=str, help='板子别名 iford souffle muffin...')
+ add('-t', '--TagId' , required=True, type=str, help='Release的Tag')
+ add('-s', '--SnapShotXml' , required=True, type=str, help='Alkaid仓库快照XML文件')
+ add('-b', '--SWBoardType' , required=True, type=str, help='板子类型')
+ add('-p', '--SWProductType' , required=True, type=str, help='Release产品类型列表')
+ add('-r', '--AlkaidRootPath', default = get_alkaid_root(), type=str, help='Alkaid仓库根目录路径')
+ add('-d', '--DocsPath' , default = '', type=str, help='文档路径')
+ add('-c', '--HWChipType' , default = '', type=str, help='硬件芯片类型')
+ add('-k', '--HWPackageType' , default = '', type=str, help='硬件封装类型')
+ add('-o', '--OutPath' , default = "release", type=str, help='Release输出路径')
+ add('-j', '--ThreadNum' , default = 1, type=int, help='编译使用的线程数')
+ add('-i', '--SkipCheckout' , default = False, action = "store_true", help='跳过从bin检出kernel/mboot')
+ add('-f', '--OpenSource' , default = False, action = "store_true",
+ help="""是否开启SDK开源模式:
+ | 闭源模式: MI编译为lib和ko对外提供功能
+ | 开源模式: MI源码发布, 客户可直接获得MI原始代码""")
+ add('-n', '--RiscVSupport' , default = False, action = "store_true", help='支持riscv源码')
+ add('-y', '--Cm4Support' , default = False, action = "store_true", help='支持cm4源码')
+ add('-m', '--MounRiverSDK' , default = False, action = "store_true", help='启用RiscVSupport时支持MounRiverSDK')
+ add('-z', '--Doc2Pdf' , default = False, action = "store_true", help='将文档转换为PDF格式')
+ add('-l', '--IplVersion' , default = "CN", type=str, choices=["CN", "WW"], help='选择IPL版本')
+ add('-h', '--Doc2Html' , default = False, action = "store_true", help='将文档转换为HTML格式')
+ add('-v', '--VerifyBuild' , default = False, action = "store_true", help='验证构建')
+ add('-u', '--ReduceCodeSize', default = False, action = "store_true", help='减小代码体积')
+ add('-x', '--SkipSyncCode' , default = False, action = "store_true", help='跳过通过快照XML同步代码')
+ add('--help', action='help', help='显示帮助信息')
+ add('--log', type=str,choices=['error', 'warn', 'info', 'debug', 'trace'], default='info', help='设置日志级别')
+ self.parser = parser
+
+ def parse(self, args:List[str]):
+ args = self.parser.parse_args(args)
+ for k,v in args.__dict__.items():
+ if k.startswith('__'):
+ continue
+ self[k] = v
+ def check(self) -> bool:
+ """
+ 检查参数配置是否合法
+ """
+ # 检查必需参数
+ required_params = ["SWBoardAlias", "TagId", "SWBoardType", "SWProductType"]
+ for param in required_params:
+ if not self[param]:
+ raise ValueError(f"必需参数 {param} 不能为空")
+
+ # 检查数值参数
+ if not isinstance(self.ThreadNum, int) or self.ThreadNum <= 0:
+ raise ValueError("ThreadNum 必须为正整数")
+
+ # 检查布尔参数
+ bool_params = [
+ "SkipCheckout", "OpenSource", "RiscVSupport", "Cm4Support",
+ "Doc2Pdf", "Doc2Html", "VerifyBuild", "ReduceCodeSize",
+ "SkipSyncCode", "OpenRtosWhiteList", "OpenCm4WhiteList", "MounRiverSDK"
+ ]
+ for param in bool_params:
+ if not isinstance(self[param], bool):
+ raise ValueError(f"参数 {param} 必须为布尔值")
+ # 检查快照XML文件是否存在
+ if not self.SkipSyncCode and not self.SnapShotXml:
+ raise ValueError("SnapShotXml is required when SkipSyncCode is False")
+ return True
+
+ def help(self):
+ self.parser.print_help()
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100755
index 0000000..d94bd9d
--- /dev/null
+++ b/main.py
@@ -0,0 +1,63 @@
+#! /usr/bin/env python3
+import os, sys
+from pathlib import Path
+import core.plugin as plugin
+from core.release_flow import ReleaseFlow
+import thirdparty.py_trees as py_trees
+import core.logger as logger
+from database import *
+log = logger.get_logger()
+
+def load_config():
+ config_path = Path(__file__).parent.joinpath("configs", "customer.yaml")
+ if not config_path.exists():
+ config_path = config_path.parent.joinpath('customer_test.yaml')
+ if not config_path.exists():
+ log.error(f"customer config not found! {config_path}")
+ sys.exit(-1)
+ load_from_file(config_path)
+ #
+ check_config()
+
+
+def check_config():
+ """检查各种配置是否ok
+ 1. 比如参数是否合法
+ 2. 比如路径是否存在
+ 3. 比如配置是否存在
+ """
+ ReleaseOptions.check()
+ BlackListDefault.check()
+ BlackListInternal.check()
+ ProductIngoreList.check()
+ ChipIngoreList.check()
+ DirAlias.check()
+
+def set_log_level(level:str):
+ """设置日志级别
+ Args:
+ level (str): 日志级别
+ """
+ map = {
+ 'debug' : py_trees.logging.Level.DEBUG,
+ 'info' : py_trees.logging.Level.INFO,
+ 'warn' : py_trees.logging.Level.WARN,
+ 'error' : py_trees.logging.Level.ERROR,
+ }
+ py_trees.logging.level = map.get(level, py_trees.logging.Level.INFO)
+ log.set_level(level)
+
+def main():
+ args = sys.argv[1:]
+ ReleaseOptions.parse(args)
+ set_log_level(ReleaseOptions.log)
+ load_config()
+ # py_trees.logging.level = py_trees.logging.Level.DEBUG
+ plugins = plugin.PluginManager()
+ release_flow = ReleaseFlow()
+ release_flow.construct(plugins)
+ release_flow.run()
+ release_flow.shutdown()
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/plugins/baseline/__init__.py b/plugins/baseline/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/baseline/phase_config.py b/plugins/baseline/phase_config.py
new file mode 100644
index 0000000..494d429
--- /dev/null
+++ b/plugins/baseline/phase_config.py
@@ -0,0 +1,31 @@
+import shutil
+import sys
+import os
+import time
+sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
+import core.logger as logger
+from core.behavior_tree import ReleaseFlowAction, ReleaseFlowActionDecorator
+from thirdparty.py_trees.common import Status, Access
+from database import *
+from utils.shell import shell
+log = logger.get_logger()
+
+@ReleaseFlowActionDecorator
+class _PhaseConfig_Process(ReleaseFlowAction):
+ def setup(self):
+ self.process_over = False
+
+ def process(self):
+ self.status = Status.RUNNING
+ if self.process_over:
+ self.status = Status.SUCCESS
+ return self.Status
+ self.blackboard.register_key(key="test_key")
+ log.info(f"test_key: {self.blackboard.test_key}")
+
+ self.process_over = True
+ self.status = Status.SUCCESS
+ return self.status
+
+ def update(self):
+ return self.status
diff --git a/plugins/baseline/phase_env.py b/plugins/baseline/phase_env.py
new file mode 100644
index 0000000..95345a0
--- /dev/null
+++ b/plugins/baseline/phase_env.py
@@ -0,0 +1,61 @@
+import shutil
+import sys
+import os
+import time
+sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
+import core.logger as logger
+from core.behavior_tree import ReleaseFlowAction, ReleaseFlowActionDecorator
+from thirdparty.py_trees.common import Status, Access
+from database import *
+from utils.shell import shell
+log = logger.get_logger()
+
+def sync_source(repo_url, xml_path):
+ """
+ 同步源码
+ """
+ # 检查xml文件是否存在
+ if not os.path.exists(xml_path):
+ log.error(f"XML file {xml_path} does not exist!")
+ raise Exception(f"XML file {xml_path} does not exist!")
+
+ # 检查是否有人修改build目录下的文件
+ _, out, _ = shell(f'cd {ReleaseOptions.AlkaidRootPath}/build && git status -s')
+ lines = out.strip().split('\n')
+ if lines:
+ warning_msg = "WARNING!!!! You MODIFY build/* files:\n"
+ log.warning(warning_msg + "\n".join(lines))
+
+ # 复制xml文件到.repo/manifests目录
+ shutil.copy(xml_path, f"{ReleaseOptions.AlkaidRootPath}/.repo/manifests")
+
+ # reset当前环境,根据xml文件重新拉取代码
+ shell('repo forall -p -v build -c "git clean -fd && git reset --hard;"')
+ shell(f'repo init -u {repo_url} -m {xml_path}')
+ shell('repo forall -p -c -v build "repo sync -c --no-tags . ;"')
+
+@ReleaseFlowActionDecorator
+class _PhaseEnv_Process(ReleaseFlowAction):
+ def setup(self):
+ self.process_over = False
+
+ def process(self):
+ self.status = Status.RUNNING
+ if self.process_over:
+ self.status = Status.SUCCESS
+ return self.status
+ self.blackboard.register_key(key="test_key")
+ self.blackboard.test_key = "test_value"
+ # 获取参数
+ repo_url = REPO_URL
+ xml_path = ReleaseOptions.SnapShotXml
+
+ log.info(f"Syncing source with repo_url: {repo_url}, xml_path: {xml_path}")
+
+ sync_source(repo_url, xml_path)
+ self.process_over = True
+ self.status = Status.SUCCESS
+ return self.status
+
+ def update(self):
+ return self.status
diff --git a/plugins/baseline/plugin.py b/plugins/baseline/plugin.py
new file mode 100644
index 0000000..792136d
--- /dev/null
+++ b/plugins/baseline/plugin.py
@@ -0,0 +1,17 @@
+import sys
+import os
+sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
+import core.logger as logger
+from core.plugin import ReleaseFlowPlugin, ReleaseFlowPluginDecorator
+log = logger.get_logger()
+
+@ReleaseFlowPluginDecorator
+class BaseLine(ReleaseFlowPlugin):
+ def get_description(self) -> str:
+ return """
+ Baseline plugin
+ 只做最基本的PureLinux IPC release的事情
+ """
+
+ def get_version(self) -> str:
+ return "1.0.0"
\ No newline at end of file
diff --git a/plugins/cm4/__init__.py b/plugins/cm4/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/cm4/plugin.py b/plugins/cm4/plugin.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/riscv/__init__.py b/plugins/riscv/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/riscv/plugin.py b/plugins/riscv/plugin.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/rtos/__init__.py b/plugins/rtos/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/rtos/plugin.py b/plugins/rtos/plugin.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/rtos_opensource/__init__.py b/plugins/rtos_opensource/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/rtos_opensource/plugin.py b/plugins/rtos_opensource/plugin.py
new file mode 100644
index 0000000..e69de29
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..f8ae0d9
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+# libreoffice
+# unoconv
+openpyxl
+pydot
\ No newline at end of file
diff --git a/tests/data/customer_test.yaml b/tests/data/customer_test.yaml
new file mode 100644
index 0000000..c1d5a32
--- /dev/null
+++ b/tests/data/customer_test.yaml
@@ -0,0 +1,141 @@
+ConfigList:
+ - ipc_iford.nor.glibc-11.1.0-squashfs.ssc029a.512.bga12_ddr4_defconfig
+ - ipc-rtos_iford.spinand.glibc-11.1.0-ramdisk.ssc029a.512.bga12_ddr4_defconfig
+MiReleaseList: []
+MhalReleaseList: []
+
+RtosReleaseList:
+ - coremark
+ - common
+ - font
+ - iqserver
+ - disp_app
+ - dualos_pipeline
+ - aov_preload
+ - cm4_preload
+ - preload_rtos
+ - preload_sample
+ - lvgl
+ - usb_gadget_app
+ - aesdma
+ - bdma
+ - camclk
+ - cpufreq
+ - drvutil
+ - emac
+ - fcie
+ - flash
+ - fsp_qspi
+ - gpio
+ - i2c
+ - input
+ - int
+ - ir
+ - kernel
+ - loadns
+ - miu
+ - mmupte
+ - mspi
+ - msys
+ - padmux
+ - pwm
+ - rtc
+ - rtcpwc
+ - saradc
+ - sdmmc
+ - str
+ - timer
+ - tsensor
+ - uart
+ - voltage
+ - watchdog
+ - algo
+ - decompress
+ - freertos_plus_fat
+ - lwfs
+ - proxyfs
+ - tcpip
+ - arm
+ - riscv
+ - libc
+ - newlib_stub
+ - cam_dev_wrapper
+ - cam_drv_poll
+ - cam_fs_wrapper
+ - cam_os_wrapper
+ - cam_proc_wrapper
+ - env_util
+ - initcall
+ - loadable_module
+ - memmang
+ - memmap
+ - mempool
+ - MsWrapper
+ - newlib_stub
+ - freertos_main
+ - freertos_posix
+ - sys_I_SW
+
+
+PmRtosReleaseList:
+ - pm_aesdma
+ - pm_bdma
+ - pm_camclk
+ - pm_cpufreq
+ - pm_drvutil
+ - pm_loadns
+ - pm_emac
+ - pm_flash
+ - pm_fsp_qspi
+ - pm_gpio
+ - pm_i2c
+ - pm_int
+ - pm_ir
+ - pm_kernel
+ - pm_miu
+ - pm_mmupte
+ - pm_mspi
+ - pm_msys
+ - pm_padmux
+ - pm_pcie
+ - pm_pl011
+ - pm_mhal_pm_clk
+ - pm_pm_idle
+ - pm_mhal_pm_mbx
+ - pm_rtcpwc
+ - pm_mhal_pm_sys
+ - pm_mhal_pm_wdt
+ - pm_pspi
+ - pm_pwm
+ - pm_pm_power
+ - pm_rtc
+ - pm_rtcpwc
+ - pm_saradc
+ - pm_sdmmc
+ - pm_timer
+ - pm_tsensor
+ - pm_uart
+ - pm_voltage
+ - pm_watchdog
+ - pm_decompress
+ - pm_freertos_plus_fat
+ - pm_lwfs
+ - pm_proxyfs
+ - pm_tcpip
+ - pm_arm
+ - pm_riscv
+ - pm_cam_dev_wrapper
+ - pm_cam_drv_poll
+ - pm_cam_fs_wrapper
+ - pm_cam_os_wrapper
+ - pm_cam_proc_wrapper
+ - pm_env_util
+ - pm_initcall
+ - pm_libc
+ - pm_memmang
+ - pm_memmap
+ - pm_mempool
+ - pm_MsWrapper
+ - pm_freertos
+ - pm_freertos_posix
+ - pm_sys_I_SW
diff --git a/tests/data/test.mk b/tests/data/test.mk
new file mode 100644
index 0000000..60a40fd
--- /dev/null
+++ b/tests/data/test.mk
@@ -0,0 +1,133 @@
+# 这是一个测试用的复杂 Makefile 示例
+# 包含了各种 Makefile 语法特性用于测试解析器
+
+# 简单变量赋值
+SIMPLE_VAR = simple value
+
+# 不同分隔符的变量赋值
+COLON_VAR := immediately expanded value
+
+# 多行变量定义
+MULTILINE_VAR = first line \
+ second line \
+ third line
+
+# 带注释的变量定义
+WITH_COMMENT = value with trailing comment # 这是一个注释
+
+# 定义函数
+define MULTI_LINE_FUNCTION
+ echo "This is line 1"
+ echo "This is line 2"
+ echo "This is line 3"
+endef
+
+# 使用 += 追加变量内容
+APPEND_VAR = initial value
+APPEND_VAR += appended value
+
+# 条件语句
+ifdef DEBUG
+ LOG_LEVEL = debug
+else
+ LOG_LEVEL = info
+endif
+
+# 使用函数
+SOURCES = main.c util.c driver.c
+OBJECTS = $(patsubst %.c,%.o,$(SOURCES))
+
+# 空变量定义
+EMPTY_VAR =
+
+# 带特殊字符的变量定义
+SPECIAL_CHARS = $$HOME/bin:/usr/local/bin:$$PATH
+
+# 引用其他变量
+COMPOUND_VAR = $(SIMPLE_VAR) and $(COLON_VAR)
+
+# 内容中包含等号的变量
+EQUAL_IN_VALUE = key1=value1 key2=value2
+
+# 目标定义
+all: build test
+
+# 带多条命令的目标
+build:
+ @echo "Building..." # 注释
+ $(CC) -o output $(SOURCES)
+ @echo "Build complete"
+
+# 依赖于多个目标的规则
+test: unit_test integration_test
+ @echo "All tests completed"
+
+# 模式规则
+%.o: %.c
+ $(CC) -c -o $@ $<
+
+# 伪目标
+.PHONY: all clean test
+
+# 复杂的多行命令
+clean:
+ @echo "Cleaning up..."
+ rm -f *.o
+ rm -f output
+ @echo "Clean complete"
+
+# 变量中包含换行
+NEWLINE := line1$(shell echo)line2
+
+# 带续行符的shell命令
+test_shell:
+ for file in $(SOURCES); do \
+ echo "Processing $$file"; \
+ cat $$file | grep "TODO"; \
+ done
+
+# 使用 ?= 条件赋值
+CC ?= gcc
+
+# 使用 != 执行命令并赋值
+CURRENT_DIR != pwd
+
+# 包含其他 Makefile
+-include optional.mk
+include required.mk
+
+# 导出变量到环境变量
+export PATH
+export JAVA_HOME = /usr/lib/jvm/default-java
+
+# 嵌套使用变量
+NESTED = $($(VARNAME))
+
+# 使用自动变量
+auto_vars: file1.c file2.c
+ @echo "First prerequisite: $<"
+ @echo "All prerequisites: $^"
+ @echo "Target name: $@"
+
+# 使用通配符
+HEADERS = $(wildcard *.h)
+
+# 使用 vpath 指令
+vpath %.c src
+vpath %.h include
+
+# 静态模式规则
+MODULES = mod1 mod2 mod3
+$(MODULES:%=%.o): %.o: %.c
+ $(CC) -c -o $@ $<
+
+# 字符串替换
+TEXT = Hello World
+REPLACED = $(subst Hello,Goodbye,$(TEXT))
+
+# shell 命令输出作为变量内容
+VERSION := $(shell git describe --tags)
+
+# 路径处理
+SRC_PATH = src/module/file.c
+FILE_NAME = $(notdir $(SRC_PATH))
\ No newline at end of file
diff --git a/tests/data/test_defconfig b/tests/data/test_defconfig
new file mode 100644
index 0000000..fee736e
--- /dev/null
+++ b/tests/data/test_defconfig
@@ -0,0 +1,52 @@
+# 这是一个测试用的复杂 defconfig 文件示例
+# 包含了各种 defconfig 语法特性用于测试解析器
+
+# 简单的键值对,使用等号
+CONFIG_SIMPLE = y
+
+# 使用等号前后有空格的键值对
+CONFIG_WITH_SPACES = y
+
+# 没有空格的键值对
+CONFIG_NO_SPACES=y
+
+# 带有数值的配置
+CONFIG_INT_VALUE = 100
+CONFIG_HEX_VALUE = 0x1000
+CONFIG_OCTAL_VALUE = 0755
+
+# 带有字符串的配置
+CONFIG_STRING = "hello world"
+CONFIG_STRING_NO_QUOTES = hello world
+
+# 带有特殊字符的配置
+CONFIG_PATH = /usr/local/bin:/usr/bin:/bin
+CONFIG_SPECIAL_CHARS = $HOME/.config
+
+# 带有注释的配置
+CONFIG_WITH_COMMENT = y # 这是一个注释
+
+# 多种格式混合
+CONFIG_MIXED = "value with spaces" # 和注释
+
+# 空值配置
+CONFIG_EMPTY =
+
+# 带有布尔值的配置
+CONFIG_BOOL_TRUE = y
+CONFIG_BOOL_FALSE = n
+
+# 带有多行内容的配置(实际 defconfig 中不常见,但为测试解析器的健壮性添加)
+CONFIG_MULTILINE = first line \
+ second line \
+ third line
+
+# 含有等号的值
+CONFIG_WITH_EQUAL = key=value
+
+# 重复的键(后面的应该覆盖前面的)
+CONFIG_DUPLICATE = first
+CONFIG_DUPLICATE = second
+
+# 长路径配置
+CONFIG_LONG_PATH = /very/long/path/to/some/file/that/might/wrap/around/in/editor/and/cause/parsing/issues/file.conf
diff --git a/tests/test_common.py b/tests/test_common.py
new file mode 100644
index 0000000..bbc7200
--- /dev/null
+++ b/tests/test_common.py
@@ -0,0 +1,41 @@
+import sys
+import os
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+import core.plugin as plugin
+
+
+def test_plugins():
+ plugins = plugin.PluginManager()
+ print("we found {} plugins".format(len(plugins.get_available_plugins())))
+ print(plugins)
+
+ print("Step of BaseLine is:")
+ print(plugins.BaseLine.PhaseEnv.Pre)
+ print(plugins.BaseLine.PhaseEnv.Process)
+ print(plugins.BaseLine.PhaseEnv.Post)
+
+
+def test_ReleaseStep():
+ from core.defines import ReleasePhase, ProcessStep
+ for release_step in ReleasePhase:
+ print(release_step.value)
+ for process_step in ProcessStep:
+ class_name = f" _{release_step.value}_{process_step.value}"
+ print(class_name)
+
+from database import DirAlias
+def test_config():
+ print("DirAlias.mi_sys: ", DirAlias.mi_sys)
+ assert DirAlias.mi_sys
+
+
+def test_demo_list_default():
+ from database import DemoListDefault
+ DemoListDefault.check()
+ print(f"DemoListDefault: {len(DemoListDefault)}")
+ assert not DemoListDefault
+
+
+def test_config_single_item():
+ from database import REPO_URL
+ assert REPO_URL == "http://hcgit04:9080/manifest/alkaid"
diff --git a/tests/test_config_expend.py b/tests/test_config_expend.py
new file mode 100644
index 0000000..b97809a
--- /dev/null
+++ b/tests/test_config_expend.py
@@ -0,0 +1,232 @@
+from database import *
+
+def test_black_list_expand():
+ # 如下的参考数据来源于 原本的脚本里面的 BLACK_LIST_DEFAULT
+ ref = """
+ sdk/interface/include/aio/
+ sdk/interface/include/ai/
+ sdk/interface/src/ai/
+ sdk/impl/ai/
+ sdk/interface/src/aio
+ sdk/impl/aio
+ sdk/interface/include/ao/
+ sdk/interface/src/ao/
+ sdk/impl/ao/
+ sdk/interface/include/alsa/
+ sdk/interface/src/alsa/
+ sdk/impl/alsa/
+ sdk/interface/include/cipher/
+ sdk/interface/src/cipher/
+ sdk/impl/cipher/
+ sdk/interface/src/common
+ sdk/impl/common
+ sdk/interface/include/cus3a/
+ sdk/interface/src/cus3a/
+ sdk/interface/include/debug
+ sdk/interface/src/debug
+ sdk/impl/debug
+ sdk/interface/include/dummy/
+ sdk/interface/src/dummy/
+ sdk/impl/dummy/
+ sdk/interface/include/disp
+ sdk/interface/src/disp
+ sdk/impl/disp
+ sdk/interface/include/dpu/
+ sdk/interface/src/dpu/
+ sdk/impl/dpu/
+ sdk/interface/include/dsp/
+ sdk/interface/src/dsp/
+ sdk/impl/dsp/
+ sdk/interface/include/fb/
+ sdk/interface/src/fb/
+ sdk/impl/fb/
+ sdk/interface/include/gfx/
+ sdk/interface/src/gfx/
+ sdk/impl/gfx/
+ sdk/interface/include/hdmi/
+ sdk/interface/src/hdmi/
+ sdk/impl/hdmi/
+ sdk/interface/include/hdmirx
+ sdk/interface/src/hdmirx
+ sdk/impl/hdmirx
+ sdk/interface/include/hvp
+ sdk/interface/src/hvp
+ sdk/impl/hvp
+ sdk/interface/include/ipu
+ sdk/interface/src/ipu
+ sdk/impl/ipu
+ sdk/interface/include/iqserver/
+ sdk/interface/src/iqserver/
+ sdk/interface/include/ive
+ sdk/interface/src/ive
+ sdk/impl/ive
+ sdk/interface/include/jpd/
+ sdk/interface/src/jpd/
+ sdk/impl/jpd
+ sdk/interface/include/mipitx/
+ sdk/interface/src/mipitx/
+ sdk/impl/mipitx/
+ sdk/interface/include/nir
+ sdk/interface/src/nir
+ sdk/impl/nir
+ sdk/interface/include/panel/
+ sdk/interface/src/panel/
+ sdk/impl/panel/
+ sdk/interface/include/pcie/
+ sdk/interface/src/pcie/
+ sdk/impl/pcie/
+ sdk/interface/include/pspi
+ sdk/interface/src/pspi
+ sdk/impl/pspi
+ sdk/interface/include/rgn
+ sdk/interface/src/rgn
+ sdk/impl/rgn
+ sdk/interface/include/scl
+ sdk/interface/src/scl
+ sdk/impl/scl
+ sdk/interface/include/sed/
+ sdk/interface/src/sed/
+ sdk/interface/include/sensor
+ sdk/interface/src/sensor
+ sdk/impl/sensor
+ sdk/interface/include/shadow/
+ sdk/interface/src/shadow/
+ sdk/impl/shadow/
+ sdk/impl/sys
+ sdk/interface/include/vdec/
+ sdk/interface/src/vdec/
+ sdk/impl/vdec/
+ sdk/interface/include/md/
+ sdk/interface/include/od/
+ sdk/interface/include/vg/
+ sdk/interface/include/vdf/
+ sdk/interface/src/vdf/
+ sdk/interface/src/vcodec
+ sdk/interface/include/vdisp/
+ sdk/interface/src/vdisp/
+ sdk/impl/vdisp/
+ sdk/interface/include/wlan/
+ sdk/interface/src/wlan/
+ rtos/proj/sc/driver/sysdriver/dualos
+ rtos/proj/sc/driver/sysdriver/otp
+ rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usb30
+ rtos/proj/sc/system/rtos/freertos
+ rtos/proj/sc_priv/arch/arm/cortex-a/v7_aarch32/context_switch
+ rtos/proj/sc_priv/arch/arm/cortex-a/v7_aarch32/lh_monitor
+ rtos/proj/sc_priv/driver/sysdriver/dualos
+ rtos/proj/sc_priv/driver/sysdriver/otp
+ rtos/proj/sc_priv/driver/sysdriver/usb_gadget/udc/usb30
+ rtos/proj/sc_priv/system/rtos/freertos
+ pm_rtos/proj/sc/driver/sysdriver/usb_gadget/udc/usb30
+ pm_rtos/proj/sc/driver/camdriver/common
+ pm_rtos/proj/sc/driver/camdriver/pm_aio
+ pm_rtos/proj/sc/driver/camdriver/pm_idle
+ pm_rtos/proj/sc/driver/camdriver/pm_jpe
+ pm_rtos/proj/sc/driver/camdriver/pm_md
+ pm_rtos/proj/sc/driver/camdriver/pm_radar
+ pm_rtos/proj/sc/driver/camdriver/pm_usbpoc
+ pm_rtos/proj/sc/driver/camdriver/pm_vif
+ """
+ BlackListDefault.check()
+ ret = sorted(set(config_expand(BlackListDefault)))
+ ret = [_.strip('/') for _ in ret]
+ # with open("temp/test_expand.txt", "w") as f:
+ # for item in ret:
+ # item = item.strip('/')
+ # f.write(item + "\n")
+ exp = [_.strip() for _ in ref.split()]
+ exp = sorted(set(exp))
+ exp = [_.strip('/') for _ in exp]
+ # with open("temp/test_expand_ref.txt", "w") as f:
+ # for item in exp:
+ # f.write(item + "\n")
+ assert ret == exp
+ #print("BLACK_LIST_DEFAULT.expand: ", ret)
+
+def test_black_list_internal_expand():
+ BlackListInternal.check()
+ ref = """
+ sdk/interface/include/isp/
+ sdk/interface/src/isp/
+ sdk/impl/isp/
+ sdk/interface/include/ispalgo/
+ sdk/interface/src/ispalgo/
+ sdk/interface/include/ldc/
+ sdk/interface/src/ldc/
+ sdk/impl/ldc/
+ sdk/interface/include/venc
+ sdk/interface/src/venc
+ sdk/impl/venc
+ sdk/interface/include/vif
+ sdk/interface/src/vif
+ sdk/impl/vif
+ pm_rtos/proj/sc/driver/camdriver/pm_isp/
+ pm_rtos/proj/sc/driver/camdriver/pm_ispalgo/
+ pm_rtos/proj/sc/driver/camdriver/pm_radar_algo/
+ """
+ ret = sorted(set(config_expand(BlackListInternal)))
+ ret = [_.strip('/') for _ in ret]
+ # with open("temp/test_expand_internal.txt", "w") as f:
+ # for item in ret:
+ # f.write(item + "\n")
+ exp = [_.strip() for _ in ref.split()]
+ exp = sorted(set(exp))
+ exp = [_.strip('/') for _ in exp]
+ # with open("temp/test_expand_ref_internal.txt", "w") as f:
+ # for item in exp:
+ # f.write(item + "\n")
+ assert ret == exp
+
+def test_config_format():
+ ReleaseOptions.SWBoardAlias = "iford"
+ @AutoInstanceDecorator
+ class TestAlias(BaseConfig):
+ _config = [
+ "preload_sample",
+ ]
+ ret = config_expand(TestAlias)
+ assert ret == ["sdk/verify/sample_code/source/iford/preload_sample/rtos"]
+
+
+def test_config_test_item():
+ @AutoInstanceDecorator
+ class TestAlias(BaseConfig):
+ _config = [
+ "preload_sample",
+ ]
+ TestAlias.update(["xxx/xxx/yyy", "zzz/zzz/www", "test_item"])
+ ret = config_expand(TestAlias)
+ # 使用list方式访问
+ assert [_ for _ in ["xxx/xxx/yyy", "zzz/zzz/www", "test_item"] if _ in ret]
+ # 使用dict方式访问
+ assert TestAlias.get("xxx/xxx/yyy") == "xxx/xxx/yyy"
+ assert TestAlias["zzz/zzz/www"] == "zzz/zzz/www"
+ # 使用索引方式修改item 因为这是一个list类型的配置,所以修改了值其key也会跟着修改
+ TestAlias["zzz/zzz/www"] = "zzz/zzz/www_new"
+ assert TestAlias["zzz/zzz/www_new"] == "zzz/zzz/www_new"
+ # 获取不存在的item
+ assert TestAlias.get("zzz/zzz/www", 'default') == 'default'
+ # 特殊访问方式。直接使用一个路径去访问,这样即使这个路径不存在,也不会抛出异常而是返回路径本身
+ assert TestAlias["zzz/zzz/www"] == "zzz/zzz/www"
+ # 通过索引的方式访问不存在的item 会抛出异常
+ try:
+ TestAlias["unexist_item"]
+ except KeyError:
+ assert True
+ else:
+ assert False
+ # 使用点号方式访问
+ assert TestAlias.test_item == "test_item"
+ # 使用点号方式设置
+ TestAlias.test_item = "test_item_new" # 会自动将test_item从_list中删除,并添加test_item_new
+ assert TestAlias.test_item_new == "test_item_new"
+ # 使用点号方式删除后,再次访问会抛出异常
+ try:
+ del TestAlias.test_item_new
+ TestAlias.test_item_new
+ except KeyError:
+ assert True
+ else:
+ assert False
+ # 删除后,使用get方式访问会返回默认值
+ assert TestAlias.get("test_item", "default") == "default"
diff --git a/tests/test_defconfig_parser.py b/tests/test_defconfig_parser.py
new file mode 100644
index 0000000..7fb83c8
--- /dev/null
+++ b/tests/test_defconfig_parser.py
@@ -0,0 +1,209 @@
+import sys
+import os
+import tempfile
+import shutil
+from pathlib import Path
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+from utils.utils import DefconfigParser
+
+def test_defconfig_parser_read():
+ """测试 DefconfigParser 类的读取功能"""
+ defconfig_path = os.path.join(os.path.dirname(__file__), 'data', 'test_defconfig')
+ parser = DefconfigParser(defconfig_path)
+ temp_file1 = os.path.join(os.path.dirname(__file__), '.tmp', 'test_defconfig_parsed.json')
+ with open(temp_file1, 'w') as f:
+ f.write(f"{parser}")
+ # 测试获取简单键值对
+ assert parser.get('CONFIG_SIMPLE') == 'y'
+ assert parser['CONFIG_SIMPLE'] == 'y' # 使用索引方式访问
+ assert parser.get('CONFIG_WITH_SPACES') == 'y'
+ assert parser.get('CONFIG_NO_SPACES') == 'y'
+
+ # 测试获取数值配置
+ assert parser.get('CONFIG_INT_VALUE') == '100'
+ assert parser.get('CONFIG_HEX_VALUE') == '0x1000'
+ assert parser.get('CONFIG_OCTAL_VALUE') == '0755'
+
+ # 测试获取字符串配置
+ assert parser.get('CONFIG_STRING') == '"hello world"'
+ assert parser.get('CONFIG_STRING_NO_QUOTES') == 'hello world'
+
+ # 测试获取特殊字符配置
+ assert parser.get('CONFIG_PATH') == '/usr/local/bin:/usr/bin:/bin'
+ assert parser.get('CONFIG_SPECIAL_CHARS') == '$HOME/.config'
+
+ # 测试带有注释的配置
+ assert parser.get('CONFIG_WITH_COMMENT') == 'y'
+
+ # 测试混合格式
+ assert parser.get('CONFIG_MIXED') == '"value with spaces"'
+
+ # 测试空值配置
+ assert parser.get('CONFIG_EMPTY') == ''
+
+ # 测试布尔值配置
+ assert parser.get('CONFIG_BOOL_TRUE') == 'y'
+ assert parser.get('CONFIG_BOOL_FALSE') == 'n'
+
+ # 测试多行内容配置 (在实际解析中可能会有所不同,取决于实现)
+ multiline_value = parser.get('CONFIG_MULTILINE')
+ assert 'first line' in multiline_value
+
+ # 测试含有等号的值
+ assert parser.get('CONFIG_WITH_EQUAL') == 'key=value'
+
+ # 测试重复的键 (应该获取最后一个值)
+ assert parser.get('CONFIG_DUPLICATE') == 'second'
+
+ # 测试长路径配置
+ assert '/very/long/path' in parser.get('CONFIG_LONG_PATH')
+
+ # 测试不存在的键
+ assert parser.get('CONFIG_NOT_EXIST', 'default') == 'default'
+
+def test_defconfig_parser_rw():
+ """测试 DefconfigParser 类的读写功能,看是否能够正确写回文件"""
+ src_file = os.path.join(os.path.dirname(__file__), 'data', 'test_defconfig')
+ parser = DefconfigParser(src_file)
+ temp_dir = tempfile.mkdtemp()
+ if 1:
+ new_file = os.path.join(os.path.dirname(__file__), '.tmp', 'test_defconfig_copy')
+ else:
+ new_file = os.path.join(temp_dir, 'test_defconfig_copy')
+ # temp_file1 = os.path.join(os.path.dirname(__file__), 'test_defconfig_parsed.json')
+ # with open(temp_file1, 'w') as f:
+ # f.write(f"{parser}")
+ parser.flush(new_file)
+ try:
+ with open(new_file, 'r') as f:
+ with open(src_file, 'r') as f2:
+ # 由于一些格式控制符,导致读取的文件与原文件不一致,这里忽略空格进行比较
+ src = f2.read().replace(' ', '')
+ dst = f.read().replace(' ', '')
+ assert src == dst
+ finally:
+ # 清理临时文件
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
+
+
+def test_defconfig_parser_modify():
+ """测试 DefconfigParser 类的修改功能"""
+ # 创建测试文件的副本
+ temp_dir = tempfile.mkdtemp()
+ temp_file = os.path.join(temp_dir, 'test_defconfig_copy')
+ original_file = os.path.join(os.path.dirname(__file__), 'data', 'test_defconfig')
+ shutil.copy(original_file, temp_file)
+
+ try:
+ parser = DefconfigParser(temp_file)
+
+ # 修改现有配置
+ parser['CONFIG_SIMPLE'] = 'n'
+ parser['CONFIG_INT_VALUE'] = '200'
+ parser['CONFIG_STRING'] = '"modified string"'
+
+ # 添加新配置
+ parser['CONFIG_NEW'] = 'new_value'
+
+ # 删除配置
+ del parser['CONFIG_EMPTY']
+
+ idx = parser.index('CONFIG_SIMPLE')
+ parser.insert(idx, 'CONFIG_SIMPLE_NEW', '=', 'new_value_new')
+ # 刷新到文件
+ parser.flush()
+
+ # temp_file1 = os.path.join(os.path.dirname(__file__), 'test_defconfig_parsed.json')
+ # with open(temp_file1, 'w') as f:
+ # f.write(f"{parser}")
+
+ # 重新加载验证修改
+ new_parser = DefconfigParser(temp_file)
+ assert new_parser.get('CONFIG_SIMPLE') == 'n'
+ assert new_parser.get('CONFIG_INT_VALUE') == '200'
+ assert new_parser.get('CONFIG_STRING') == '"modified string"'
+ assert new_parser.get('CONFIG_NEW') == 'new_value'
+ assert new_parser.get('CONFIG_EMPTY') is None # 删除的配置
+ assert new_parser.get('CONFIG_SIMPLE_NEW') == 'new_value_new' # 插入的配置
+ finally:
+ # 清理临时文件
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
+
+def test_defconfig_parser_content_preservation():
+ """测试 DefconfigParser 类是否保留原始文件的注释和结构"""
+ # 创建测试文件的副本
+ temp_dir = tempfile.mkdtemp()
+ temp_file = os.path.join(temp_dir, 'test_defconfig_copy')
+ original_file = os.path.join(os.path.dirname(__file__), 'data', 'test_defconfig')
+ shutil.copy(original_file, temp_file)
+
+ try:
+ # 记录原始文件的行数和注释行数
+ with open(temp_file, 'r') as f:
+ original_lines = f.readlines()
+ original_line_count = len(original_lines)
+ original_comment_count = sum(1 for line in original_lines if line.strip().startswith('#'))
+
+ # 进行一些简单修改
+ parser = DefconfigParser(temp_file)
+ parser['CONFIG_SIMPLE'] = 'n'
+ parser.flush()
+
+ # 检查修改后的文件结构是否保留
+ with open(temp_file, 'r') as f:
+ modified_lines = f.readlines()
+ modified_line_count = len(modified_lines)
+ modified_comment_count = sum(1 for line in modified_lines if line.strip().startswith('#'))
+
+ # 行数应该大致相同
+ assert abs(original_line_count - modified_line_count) < 5
+
+ # 注释应该完全保留
+ assert original_comment_count == modified_comment_count
+ finally:
+ # 清理临时文件
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
+
+def test_edge_cases():
+ """测试 DefconfigParser 类的边缘情况处理"""
+ # 创建一个临时的 defconfig 文件,包含一些边缘情况
+ temp_dir = tempfile.mkdtemp()
+ temp_file = os.path.join(temp_dir, 'edge_case_defconfig')
+
+ try:
+ # 创建一个包含边缘情况的 defconfig 文件
+ with open(temp_file, 'w') as f:
+ f.write("# 空文件开始\n\n")
+ f.write("# 只有注释的行\n")
+ f.write("CONFIG_NO_VALUE=\n") # 无值
+ f.write("CONFIG_SPACES_AROUND_EQUAL = value_with_spaces_around_equal\n") # 等号周围有多个空格
+ f.write("=invalid_line_no_key\n") # 无效行,没有键
+ f.write("invalid_line_no_equal\n") # 无效行,没有等号
+
+ # 解析文件并测试
+ parser = DefconfigParser(temp_file)
+
+ # 检查无值配置
+ assert parser.get('CONFIG_NO_VALUE') == ''
+
+ # 检查等号周围有多个空格的配置
+ assert parser.get('CONFIG_SPACES_AROUND_EQUAL') == 'value_with_spaces_around_equal'
+
+ # 检查无效行处理
+ assert parser.get('invalid_line_no_equal', 'not_found') == 'not_found'
+
+ # 修改并保存
+ parser['CONFIG_NO_VALUE'] = 'now_has_value'
+ parser.flush()
+
+ # 重新加载并验证
+ new_parser = DefconfigParser(temp_file)
+ assert new_parser.get('CONFIG_NO_VALUE') == 'now_has_value'
+ finally:
+ # 清理临时文件
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
\ No newline at end of file
diff --git a/tests/test_flow.py b/tests/test_flow.py
new file mode 100644
index 0000000..385a8d7
--- /dev/null
+++ b/tests/test_flow.py
@@ -0,0 +1,442 @@
+from pathlib import Path
+import pytest
+import sys, os
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+from thirdparty.py_trees.trees import BehaviourTree
+from core.behavior_tree import ReleaseFlowActionDecorator, ReleaseFlowBuilder, ReleaseFlowAction
+from thirdparty.py_trees.common import Status, Access
+from core.logger import get_logger
+
+log = get_logger()
+
+class ActionTest(ReleaseFlowAction):
+ """测试用的Action节点"""
+ def __init__(self, name: str, return_status: Status, description: str = ""):
+ super().__init__(name, description)
+ self.return_status = return_status
+ self.process_called = False
+ self.setup_called = False
+ self.shutdown_called = False
+
+ def setup(self):
+ self.setup_called = True
+ log.info(f"setup: {self.name}")
+
+ def process(self):
+ self.process_called = True
+ self.status = self.return_status
+ log.info(f"process: {self.name}, status: {self.status}")
+
+ def update(self):
+ return self.status
+
+ def shutdown(self):
+ self.shutdown_called = True
+
+class ActionTestBlackboard(ReleaseFlowAction):
+ """测试Blackboard功能的Action节点"""
+ def __init__(self, name: str, description: str = ""):
+ super().__init__(name, description)
+ self.blackboard.register_key("test_key", access=Access.WRITE)
+ self.blackboard.register_key("read_only_key", access=Access.READ)
+
+ def process(self):
+ self.status = Status.SUCCESS
+ log.info(f"process: {self.name}, status: {self.status}, test_key: {self.blackboard.test_key}")
+
+ def update(self):
+ return self.status
+
+class ActionTestBlackboard1(ReleaseFlowAction):
+ """测试Blackboard功能的Action节点"""
+ def __init__(self, name: str, description: str = ""):
+ super().__init__(name, description)
+ self.blackboard.require_key("test_key")
+
+ def process(self):
+ self.status = Status.SUCCESS
+ log.info(f"process: {self.name}, status: {self.status}, test_key: {self.blackboard.test_key}")
+
+ def update(self):
+ return self.status
+
+@pytest.fixture
+def builder():
+ """创建ReleaseFlowBuilder实例的fixture"""
+ return ReleaseFlowBuilder()
+
+def test_single_action():
+ """测试单个Action节点的执行"""
+ action = ActionTest("test_action", Status.SUCCESS)
+ action.setup()
+ assert action.setup_called
+
+ action.tick_once()
+ assert action.process_called
+ assert action.status == Status.SUCCESS
+
+ action.shutdown()
+ assert action.shutdown_called
+
+def test_sequence(builder):
+ """测试Sequence节点的执行"""
+ success_action = ActionTest("success_action", Status.SUCCESS)
+ failure_action = ActionTest("failure_action", Status.FAILURE)
+
+ with builder.sequence("test_sequence") as b:
+ b.action(success_action)
+ b.action(failure_action)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+ tree.tick()
+
+ assert failure_action.process_called
+ assert flow.status == Status.FAILURE
+ assert success_action.process_called
+
+def test_selector(builder):
+ """测试Selector节点的执行"""
+ failure_action = ActionTest("failure_action", Status.FAILURE)
+ success_action = ActionTest("success_action", Status.SUCCESS)
+
+ with builder.selector("test_selector") as b:
+ b.action(failure_action)
+ b.action(success_action)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+ tree.tick()
+
+ assert flow.status == Status.SUCCESS
+ assert failure_action.process_called
+ assert success_action.process_called
+
+def test_blackboard():
+ """测试Blackboard功能"""
+ action = ActionTestBlackboard("test_blackboard")
+ action1 = ActionTestBlackboard1("test_blackboard_read")
+
+ # 测试写入权限
+ action.blackboard.test_key = "test_value"
+ # 测试共享
+ assert action1.blackboard.test_key == "test_value"
+
+ # 测试只读权限
+ with pytest.raises(AttributeError):
+ action.blackboard.read_only_key = "new_value"
+
+ # 测试必需key
+ with pytest.raises(AttributeError):
+ _ = action.blackboard.required_key
+
+def test_parallel(builder):
+ """测试Parallel节点的执行"""
+ success_action1 = ActionTest("success_action1", Status.SUCCESS)
+ success_action2 = ActionTest("success_action2", Status.SUCCESS)
+ failure_action = ActionTest("failure_action", Status.FAILURE)
+
+ with builder.parallel("test_parallel") as b:
+ b.action(success_action1)
+ b.action(success_action2)
+ b.action(failure_action)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+ tree.tick()
+
+ assert flow.status == Status.FAILURE
+ assert success_action1.process_called
+ assert success_action2.process_called
+ assert failure_action.process_called
+
+def test_nested_behavior_tree(builder):
+ """测试嵌套的行为树结构"""
+ success_action1 = ActionTest("success_action1", Status.SUCCESS)
+ success_action2 = ActionTest("success_action2", Status.SUCCESS)
+ failure_action = ActionTest("failure_action", Status.FAILURE)
+
+ with builder.sequence("root_sequence") as b:
+ with b.selector("nested_selector") as s:
+ s.action(failure_action)
+ s.action(success_action1)
+ b.action(success_action2)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+ tree.tick()
+
+ assert flow.status == Status.SUCCESS
+ assert failure_action.process_called
+ assert success_action1.process_called
+ assert success_action2.process_called
+
+def test_parallel_success_on_all(builder):
+ """测试并行节点SuccessOnAll策略"""
+ success_action1 = ActionTest("success_action1", Status.SUCCESS)
+ success_action2 = ActionTest("success_action2", Status.SUCCESS)
+ success_action3 = ActionTest("success_action3", Status.SUCCESS)
+
+ with builder.parallel("test_parallel_success_on_all") as b:
+ b.action(success_action1)
+ b.action(success_action2)
+ b.action(success_action3)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+ tree.tick()
+
+ assert flow.status == Status.SUCCESS
+ assert success_action1.process_called
+ assert success_action2.process_called
+ assert success_action3.process_called
+
+def test_parallel_success_on_one(builder):
+ """测试并行节点SuccessOnOne策略"""
+ from thirdparty.py_trees.common import ParallelPolicy
+
+ running_action1 = ActionTest("running_action1", Status.RUNNING)
+ success_action = ActionTest("success_action", Status.SUCCESS)
+ running_action2 = ActionTest("running_action2", Status.RUNNING)
+
+ with builder.parallel("test_parallel_success_on_one",
+ policy=ParallelPolicy.SuccessOnOne()) as b:
+ b.action(running_action1)
+ b.action(success_action)
+ b.action(running_action2)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+ tree.tick()
+
+ assert flow.status == Status.SUCCESS
+ assert running_action1.process_called
+ assert success_action.process_called
+ assert running_action2.process_called
+
+def test_multiple_executions(builder):
+ """测试行为树的多次执行"""
+ action1 = ActionTest("action1", Status.SUCCESS)
+ action2 = ActionTest("action2", Status.SUCCESS)
+
+ with builder.sequence("test_multiple_executions") as b:
+ b.action(action1)
+ b.action(action2)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+
+ # 第一次执行
+ tree.tick()
+ assert flow.status == Status.SUCCESS
+ assert action1.process_called
+ assert action2.process_called
+
+ # 重置状态
+ action1.process_called = False
+ action2.process_called = False
+
+ # 第二次执行
+ tree.tick()
+ assert flow.status == Status.SUCCESS
+ assert action1.process_called
+ assert action2.process_called
+
+def test_error_handling(builder):
+ """测试行为树的错误处理"""
+ # 测试空的行为树
+ with pytest.raises(Exception) as excinfo:
+ builder.build()
+ assert "No nodes added to behavior tree" in str(excinfo.value)
+ # 测试未闭合的节点
+ with pytest.raises(Exception) as excinfo:
+ with builder.sequence("unclosed_sequence") as b:
+ b.action(ActionTest("action", Status.SUCCESS))
+ b.action(ActionTest("action", Status.SUCCESS))
+ # 不调用end()或使用with语句的__exit__
+ builder.build()
+ assert "Unclosed nodes" in str(excinfo.value)
+
+
+
+
+class ActionWithError(ReleaseFlowAction):
+ """测试异常处理的Action节点"""
+ def __init__(self, name: str, description: str = ""):
+ super().__init__(name, description)
+ self.process_called = False
+
+ def process(self):
+ self.process_called = True
+ raise Exception("测试异常")
+
+ def update(self):
+ return Status.FAILURE
+
+def test_action_exception_handling(builder):
+ """测试Action节点异常处理"""
+ error_action = ActionWithError("error_action")
+
+ with builder.sequence("test_error_handling") as b:
+ b.action(error_action)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+
+ # 执行时应该捕获异常并返回FAILURE状态
+ tree.tick()
+ assert error_action.process_called
+ assert flow.status == Status.FAILURE
+
+def test_decorator():
+ """测试ReleaseFlowActionDecorator装饰器"""
+ @ReleaseFlowActionDecorator
+ class DecoratedAction(ReleaseFlowAction):
+ def process(self):
+ self.status = Status.SUCCESS
+
+ def update(self):
+ return self.status
+
+ # 测试自动生成名称
+ action1 = DecoratedAction()
+ assert action1.name == f"{Path(__file__).stem}.DecoratedAction"
+
+ # 测试自定义名称
+ action2 = DecoratedAction(name="custom_name")
+ assert action2.name == "custom_name"
+
+ # 测试功能
+ action2.setup()
+ action2.tick_once()
+ assert action2.status == Status.SUCCESS
+
+def test_complex_behavior_tree(builder):
+ """测试复杂的行为树结构"""
+ success_action1 = ActionTest("success_action1", Status.SUCCESS)
+ success_action2 = ActionTest("success_action2", Status.SUCCESS)
+ failure_action1 = ActionTest("failure_action1", Status.FAILURE)
+ success_action3 = ActionTest("success_action3", Status.SUCCESS)
+
+ with builder.sequence("root") as b:
+ with b.parallel("parallel_group") as p:
+ p.action(success_action1)
+ p.action(success_action2)
+ with b.selector("selector_group") as s:
+ s.action(failure_action1)
+ s.action(success_action3)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+ tree.tick()
+
+ assert flow.status == Status.SUCCESS
+ assert success_action1.process_called
+ assert success_action2.process_called
+ assert failure_action1.process_called
+ assert success_action3.process_called
+
+def test_blackboard_advanced():
+ """测试Blackboard的高级功能"""
+ class ActionWrite(ReleaseFlowAction):
+ def __init__(self, name: str, description: str = ""):
+ super().__init__(name, description)
+ self.blackboard.register_key("counter", access=Access.WRITE)
+ self.blackboard.counter = 0
+
+ def process(self):
+ self.blackboard.counter += 1
+ self.status = Status.SUCCESS
+
+ def update(self):
+ return self.status
+
+ class ActionRead(ReleaseFlowAction):
+ def __init__(self, name: str, description: str = ""):
+ super().__init__(name, description)
+ self.blackboard.register_key("counter", access=Access.READ)
+ self.value = 0
+
+ def process(self):
+ self.value = self.blackboard.counter
+ self.status = Status.SUCCESS
+
+ def update(self):
+ return self.status
+
+ # 创建动作节点
+ write_action = ActionWrite("write_action")
+ read_action = ActionRead("read_action")
+
+ # 设置初始值
+ write_action.blackboard.counter = 10
+
+ # 测试读取
+ assert read_action.blackboard.counter == 10
+
+ # 测试写入
+ write_action.blackboard.counter = 20
+ assert read_action.blackboard.counter == 20
+
+ # 测试只读权限
+ with pytest.raises(AttributeError):
+ read_action.blackboard.counter = 30
+
+def test_action_exception_handling_with_sequence(builder):
+ """测试Action节点异常处理后行为树继续执行的能力"""
+ error_action = ActionWithError("error_action")
+ success_action = ActionTest("success_action", Status.SUCCESS)
+
+ with builder.sequence("test_error_handling_sequence") as b:
+ b.action(error_action)
+ b.action(success_action)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+
+ # 执行时应该捕获异常并返回FAILURE状态,但不会中断行为树的执行
+ tree.tick()
+
+ # 验证error_action被标记为已处理
+ assert error_action.process_called
+
+ # 验证序列节点状态为FAILURE(因为第一个节点失败)
+ assert flow.status == Status.FAILURE
+
+ # 验证success_action没有被执行(因为序列节点在第一个节点失败后停止)
+ assert not success_action.process_called
+
+def test_action_exception_handling_with_selector(builder):
+ """测试Action节点异常处理后选择器继续尝试其他节点"""
+ error_action = ActionWithError("error_action")
+ success_action = ActionTest("success_action", Status.SUCCESS)
+
+ with builder.selector("test_error_handling_selector") as b:
+ b.action(error_action)
+ b.action(success_action)
+
+ flow = builder.build()
+ tree = BehaviourTree(root=flow)
+ tree.setup()
+
+ # 执行时应该捕获异常并继续尝试下一个节点
+ tree.tick()
+
+ # 验证error_action被标记为已处理
+ assert error_action.process_called
+
+ # 验证success_action被执行
+ assert success_action.process_called
+
+ # 验证选择器节点状态为SUCCESS(因为第二个节点成功)
+ assert flow.status == Status.SUCCESS
diff --git a/tests/test_makefile_parser.py b/tests/test_makefile_parser.py
new file mode 100644
index 0000000..7c0189d
--- /dev/null
+++ b/tests/test_makefile_parser.py
@@ -0,0 +1,141 @@
+import sys
+import os
+import tempfile
+import shutil
+from pathlib import Path
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+from utils.utils import MakefileParser
+
+def test_makefile_parser_read():
+ """测试 MakefileParser 类的读取功能"""
+ parser = MakefileParser(os.path.join(os.path.dirname(__file__), "data", "test.mk"))
+ with open(os.path.join(os.path.dirname(__file__), '.tmp', 'test_parsed.json'), 'w') as f:
+ f.write(f"{parser}")
+ # 测试获取简单变量
+ assert parser.get('SIMPLE_VAR') == ['simple', 'value']
+
+ # 测试获取使用不同分隔符的变量
+ assert parser.get('COLON_VAR') == ['immediately', 'expanded', 'value']
+
+ # 测试获取多行变量
+ multiline = parser.get('MULTILINE_VAR')
+ assert len(multiline) == 6
+ # 测试带注释的变量
+ assert parser.get('WITH_COMMENT')[0].startswith('value')
+
+ # 测试不存在的变量返回默认值
+ assert parser.get('NON_EXISTENT', 'default') == 'default'
+
+ # 测试复合变量
+ assert parser.get('COMPOUND_VAR') == ['$(SIMPLE_VAR)', 'and', '$(COLON_VAR)']
+
+ # 测试空变量
+ assert parser.get('EMPTY_VAR') == []
+
+def test_makefile_parser_rw():
+ """测试 MakefileParser 类的读写功能,看是否能够正确写回文件"""
+ src_file = os.path.join(os.path.dirname(__file__), "data", "test.mk")
+ parser = MakefileParser(src_file)
+ # new_file = os.path.join(os.path.dirname(__file__), 'test_rw.mk')
+ temp_dir = tempfile.mkdtemp()
+ new_file = os.path.join(temp_dir, 'test_copy.mk')
+ shutil.copy(os.path.join(os.path.dirname(__file__), "data", "test.mk"), new_file)
+
+ parser.flush(new_file)
+ try:
+ with open(new_file, 'r') as f:
+ with open(src_file, 'r') as f2:
+ # 由于一些格式控制符,导致读取的文件与原文件不一致,这里忽略空格进行比较
+ src = f2.read().replace(' ', '')
+ dst = f.read().replace(' ', '')
+ assert src == dst
+ finally:
+ # 清理临时文件
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
+
+def test_makefile_parser_modify():
+ """测试 MakefileParser 类的修改功能"""
+ # 创建测试文件的副本
+ temp_dir = tempfile.mkdtemp()
+ temp_file = os.path.join(temp_dir, 'test_copy.mk')
+ shutil.copy(os.path.join(os.path.dirname(__file__), "data", "test.mk"), temp_file)
+
+ try:
+ parser = MakefileParser(temp_file)
+
+ # 修改现有变量
+ parser['SIMPLE_VAR'] = 'modified value'
+
+ # 添加新变量
+ parser['NEW_VAR'] = 'new value'
+
+ #在SIMPLE_VAR前面插入新变量
+ # idx = parser.index('SIMPLE_VAR')
+ parser.insert('SIMPLE_VAR', 'NEW_VAR1', ':=', 'new value', '# comment')
+
+ # 添加多行变量
+ parser['NEW_MULTILINE'] = ['line 1', 'line 2', 'line 3']
+
+ # 删除变量
+ del parser['EMPTY_VAR']
+
+ # 使用正则
+ parser['REGEX_VAR'] = 'new value'
+ parser['REGEX_.* := new value'] = 'REGEX_VAR := regex value'
+
+
+ # 刷新到文件
+ parser.flush()
+ with open(os.path.join(os.path.dirname(__file__), '.tmp', 'test_parsed.json'), 'w') as f:
+ f.write(f"{parser}")
+ # 重新加载解析器验证修改
+ new_parser = MakefileParser(temp_file)
+ assert new_parser.get('SIMPLE_VAR') == ['modified', 'value']
+ assert new_parser.get('NEW_VAR') == ['new', 'value']
+ assert new_parser.get('NEW_VAR1') == ['new', 'value']
+ assert len(new_parser.get('NEW_MULTILINE')) == 6
+ assert new_parser.get('EMPTY_VAR') is None
+ assert new_parser.get('REGEX_VAR') == ['regex', 'value']
+ del new_parser['REGEX_VAR']
+ assert new_parser.get('REGEX_VAR') is None
+ finally:
+ # 清理临时文件
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
+
+def test_makefile_parser_content_preservation():
+ """测试 MakefileParser 类是否保留原始文件的注释和结构"""
+ # 创建测试文件的副本
+ temp_dir = tempfile.mkdtemp()
+ temp_file = os.path.join(temp_dir, 'test_copy.mk')
+ shutil.copy(os.path.join(os.path.dirname(__file__), "data", "test.mk"), temp_file)
+
+ try:
+ # 记录原始文件内容的行数和注释行数
+ with open(temp_file, 'r') as f:
+ original_lines = f.readlines()
+ original_line_count = len(original_lines)
+ original_comment_count = sum(1 for line in original_lines if line.strip().startswith('#'))
+
+ # 使用解析器进行一些简单修改
+ parser = MakefileParser(temp_file)
+ parser['SIMPLE_VAR'] = 'modified but preserving structure'
+ parser.flush()
+
+ # 检查修改后的文件是否保留了原始结构
+ with open(temp_file, 'r') as f:
+ modified_lines = f.readlines()
+ modified_line_count = len(modified_lines)
+ modified_comment_count = sum(1 for line in modified_lines if line.strip().startswith('#'))
+
+ assert original_line_count == modified_line_count
+
+ # 注释应该完全保留
+ assert original_comment_count == modified_comment_count
+
+ finally:
+ # 清理临时文件
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
\ No newline at end of file
diff --git a/tests/test_release_options.py b/tests/test_release_options.py
new file mode 100644
index 0000000..758009c
--- /dev/null
+++ b/tests/test_release_options.py
@@ -0,0 +1,291 @@
+import sys
+import os
+from unittest.mock import patch, MagicMock
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+from database.options import ReleaseOptions
+
+def test_basic_required_args():
+ """测试基本必需参数"""
+ # 构建必需参数
+ test_args = [
+ '--SWBoardAlias', 'testboard',
+ '--TagId', 'v1.0.0',
+ '--SnapShotXml', 'snapshot.xml',
+ '--SWBoardType', 'boardtype',
+ '--SWProductType', 'producttype'
+ ]
+
+ # 解析参数
+ ReleaseOptions.parse(test_args)
+
+ # 验证必需参数被正确解析 使用属性的方式访问
+ assert ReleaseOptions.SWBoardAlias == 'testboard'
+ assert ReleaseOptions.TagId == 'v1.0.0'
+ assert ReleaseOptions.SnapShotXml == 'snapshot.xml'
+ assert ReleaseOptions.SWBoardType == 'boardtype'
+ assert ReleaseOptions.SWProductType == 'producttype'
+
+ # 验证非必需参数使用默认值
+ assert ReleaseOptions['ThreadNum'] == 1
+ assert ReleaseOptions['IplVersion'] == 'CN'
+ assert ReleaseOptions['SkipCheckout'] is False
+ assert ReleaseOptions['OpenSource'] is False
+
+ assert ReleaseOptions.check() is True
+
+
+def test_bool_args():
+ """测试布尔类型的参数"""
+ test_args = [
+ '--SWBoardAlias', 'testboard',
+ '--TagId', 'v1.0.0',
+ '--SnapShotXml', 'snapshot.xml',
+ '--SWBoardType', 'boardtype',
+ '--SWProductType', 'producttype',
+ '--SkipCheckout',
+ '--OpenSource',
+ '--RiscVSupport',
+ '--Cm4Support',
+ '--Doc2Pdf',
+ '--Doc2Html',
+ '--VerifyBuild',
+ '--ReduceCodeSize',
+ '--SkipSyncCode'
+ ]
+
+ ReleaseOptions.parse(test_args)
+
+ # 验证布尔参数被正确解析
+ assert ReleaseOptions['SkipCheckout'] is True
+ assert ReleaseOptions['OpenSource'] is True
+ assert ReleaseOptions['RiscVSupport'] is True
+ assert ReleaseOptions['Cm4Support'] is True
+ assert ReleaseOptions['Doc2Pdf'] is True
+ assert ReleaseOptions['Doc2Html'] is True
+ assert ReleaseOptions['VerifyBuild'] is True
+ assert ReleaseOptions['ReduceCodeSize'] is True
+ assert ReleaseOptions['SkipSyncCode'] is True
+
+ assert ReleaseOptions.check() is True
+
+
+def test_optional_args():
+ """测试可选参数"""
+ test_args = [
+ '--SWBoardAlias', 'testboard',
+ '--TagId', 'v1.0.0',
+ '--SnapShotXml', 'snapshot.xml',
+ '--SWBoardType', 'boardtype',
+ '--SWProductType', 'producttype',
+ '--DocsPath', '/path/to/docs',
+ '--HWChipType', 'chiptype',
+ '--HWPackageType', 'package',
+ '--OutPath', 'output_dir',
+ '--ThreadNum', '8',
+ '--IplVersion', 'WW'
+ ]
+
+ ReleaseOptions.parse(test_args)
+
+ # 验证可选参数被正确解析
+ assert ReleaseOptions['DocsPath'] == '/path/to/docs'
+ assert ReleaseOptions['HWChipType'] == 'chiptype'
+ assert ReleaseOptions['HWPackageType'] == 'package'
+ assert ReleaseOptions['OutPath'] == 'output_dir'
+ assert ReleaseOptions['ThreadNum'] == 8
+ assert ReleaseOptions['IplVersion'] == 'WW'
+
+ assert ReleaseOptions.check() is True
+
+
+def test_mix_args():
+ """测试混合使用短格式和长格式参数"""
+ test_args = [
+ '-a', 'testboard',
+ '-t', 'v1.0.0',
+ '-s', 'snapshot.xml',
+ '-b', 'boardtype',
+ '-p', 'producttype',
+ '-d', '/path/to/docs',
+ '-j', '4',
+ '-i', # SkipCheckout
+ '-f' # OpenSource
+ ]
+
+ ReleaseOptions.parse(test_args)
+
+ # 验证短格式参数被正确解析
+ assert ReleaseOptions['SWBoardAlias'] == 'testboard'
+ assert ReleaseOptions['TagId'] == 'v1.0.0'
+ assert ReleaseOptions['DocsPath'] == '/path/to/docs'
+ assert ReleaseOptions['ThreadNum'] == 4
+ assert ReleaseOptions['SkipCheckout'] is True
+ assert ReleaseOptions['OpenSource'] is True
+
+ assert ReleaseOptions.check() is True
+
+
+def test_missing_required_args():
+ """测试缺少必需参数的情况"""
+ # 缺少必需参数 SWBoardType
+ test_args = [
+ '--SWBoardAlias', 'testboard',
+ '--TagId', 'v1.0.0',
+ '--SnapShotXml', 'snapshot.xml',
+ '--SWProductType', 'producttype'
+ ]
+
+ try:
+ ReleaseOptions.parse(test_args)
+ except SystemExit as e:
+ assert True
+
+
+def test_invalid_thread_num():
+ """测试无效的线程数"""
+ test_args = [
+ '--SWBoardAlias', 'testboard',
+ '--TagId', 'v1.0.0',
+ '--SnapShotXml', 'snapshot.xml',
+ '--SWBoardType', 'boardtype',
+ '--SWProductType', 'producttype',
+ '--ThreadNum', '-1' # 无效的线程数
+ ]
+
+ ReleaseOptions.parse(test_args)
+
+ # 线程数被解析但无效
+ assert ReleaseOptions['ThreadNum'] == -1
+
+ # 检查应该失败
+ try:
+ ReleaseOptions.check()
+ except ValueError as e:
+ assert str(e) == "ThreadNum 必须为正整数"
+
+
+def test_ipl_version_choices():
+ """测试 IplVersion 的选择限制"""
+ # 备份原始的 ReleaseOptions 实例
+ original_instance = ReleaseOptions._instance
+
+ try:
+ # 有效的 IplVersion: 'CN'
+ test_args = [
+ '--SWBoardAlias', 'testboard',
+ '--TagId', 'v1.0.0',
+ '--SnapShotXml', 'snapshot.xml',
+ '--SWBoardType', 'boardtype',
+ '--SWProductType', 'producttype',
+ '--IplVersion', 'CN'
+ ]
+
+ ReleaseOptions.parse(test_args)
+ assert ReleaseOptions['IplVersion'] == 'CN'
+ assert ReleaseOptions.check() is True
+
+ # 有效的 IplVersion: 'WW'
+ test_args[-1] = 'WW'
+ ReleaseOptions.parse(test_args)
+ assert ReleaseOptions['IplVersion'] == 'WW'
+ assert ReleaseOptions.check() is True
+
+ # 无效的 IplVersion
+ test_args[-1] = 'INVALID'
+ try:
+ ReleaseOptions.parse(test_args)
+ except SystemExit as e:
+ assert True
+ finally:
+ # 恢复原始的 ReleaseOptions 实例
+ ReleaseOptions._instance = original_instance
+
+@patch('database.options.get_alkaid_root') # 修改patch路径为正确的模块路径
+def test_default_alkaid_root_path(mock_get_alkaid_root):
+ """测试默认的 AlkaidRootPath"""
+ # 设置 mock 返回值
+ mock_get_alkaid_root.return_value = '/mocked/alkaid/root'
+
+ test_args = [
+ '--SWBoardAlias', 'testboard',
+ '--TagId', 'v1.0.0',
+ '--SnapShotXml', 'snapshot.xml',
+ '--SWBoardType', 'boardtype',
+ '--SWProductType', 'producttype'
+ ]
+
+ # 重置 ReleaseOptions 实例,确保使用新的 mock
+ ReleaseOptions._instance = None
+
+ # 重新初始化 ReleaseOptions 实例
+ ReleaseOptions.__class__().parse(test_args)
+
+ # 验证 AlkaidRootPath 使用 get_alkaid_root 的返回值
+ assert ReleaseOptions['AlkaidRootPath'] == '/mocked/alkaid/root'
+ mock_get_alkaid_root.assert_called_once()
+
+
+def test_dict_interface():
+ """测试字典接口功能"""
+ test_args = [
+ '--SWBoardAlias', 'testboard',
+ '--TagId', 'v1.0.0',
+ '--SnapShotXml', 'snapshot.xml',
+ '--SWBoardType', 'boardtype',
+ '--SWProductType', 'producttype'
+ ]
+
+ ReleaseOptions.parse(test_args)
+
+ # 测试 __getitem__
+ assert ReleaseOptions['SWBoardAlias'] == 'testboard'
+
+ # 测试 __setitem__
+ ReleaseOptions['OutPath'] = 'custom_output'
+ assert ReleaseOptions['OutPath'] == 'custom_output'
+
+ # 测试 __contains__
+ assert 'SWBoardAlias' in ReleaseOptions
+ assert 'OutPath' in ReleaseOptions
+ assert 'NonExistentKey' not in ReleaseOptions
+
+
+def test_mounriver_sdk_with_riscv():
+ """测试 MounRiverSDK 和 RiscVSupport 的组合"""
+ test_args = [
+ '--SWBoardAlias', 'testboard',
+ '--TagId', 'v1.0.0',
+ '--SnapShotXml', 'snapshot.xml',
+ '--SWBoardType', 'boardtype',
+ '--SWProductType', 'producttype',
+ '--RiscVSupport',
+ '--MounRiverSDK'
+ ]
+
+ ReleaseOptions.parse(test_args)
+
+ # 验证两个参数都被正确设置
+ assert ReleaseOptions['RiscVSupport'] is True
+ assert ReleaseOptions['MounRiverSDK'] is True
+
+ assert ReleaseOptions.check() is True
+
+
+def test_help_method():
+ """测试 help 方法"""
+ # 备份原始解析器
+ original_parser = getattr(ReleaseOptions, 'parser', None)
+
+ try:
+ # 准备一个 MagicMock 来捕获 print_help 的调用
+ ReleaseOptions.parser = MagicMock()
+
+ # 调用 help 方法
+ ReleaseOptions.help()
+
+ # 验证 print_help 被调用
+ ReleaseOptions.parser.print_help.assert_called_once()
+ finally:
+ # 恢复原始解析器
+ ReleaseOptions.parser = original_parser
diff --git a/tests/test_yaml.py b/tests/test_yaml.py
new file mode 100644
index 0000000..083b7cb
--- /dev/null
+++ b/tests/test_yaml.py
@@ -0,0 +1,25 @@
+import sys,os
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+from thirdparty import yaml
+
+
+
+
+def test_yaml_base():
+ yaml_str = """
+name: Alice
+age: 25
+hobbies:
+ - reading
+ - hiking
+"""
+ data = yaml.safe_load(yaml_str) # 推荐使用 safe_load 避免潜在的安全风险
+ print(data, f"{type(data)}")
+
+
+def test_load_customer_yaml():
+ file = os.path.join(os.path.dirname(__file__), "data", "customer_test.yaml")
+ with open(file, "r") as fp:
+ yaml_str = fp.read()
+ data = yaml.safe_load(yaml_str) # 推荐使用 safe_load 避免潜在的安全风险
+ print(data, f"{type(data)}")
\ No newline at end of file
diff --git a/thirdparty/py_trees/__init__.py b/thirdparty/py_trees/__init__.py
new file mode 100644
index 0000000..5de95f5
--- /dev/null
+++ b/thirdparty/py_trees/__init__.py
@@ -0,0 +1,35 @@
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+This is the top-level namespace of the py_trees package.
+"""
+##############################################################################
+# Imports
+##############################################################################
+
+from . import behaviour # noqa
+from . import behaviours # noqa
+from . import blackboard # noqa
+from . import common # noqa
+from . import composites # noqa
+from . import console # noqa
+from . import decorators # noqa
+from . import demos # noqa
+from . import display # noqa
+from . import idioms # noqa
+from . import logging # noqa
+from . import meta # noqa
+from . import programs # noqa
+from . import syntax_highlighting # noqa
+from . import tests # noqa
+from . import timers # noqa
+from . import trees # noqa
+from . import utilities # noqa
+from . import version # noqa
+from . import visitors # noqa
diff --git a/thirdparty/py_trees/behaviour.py b/thirdparty/py_trees/behaviour.py
new file mode 100644
index 0000000..90d00df
--- /dev/null
+++ b/thirdparty/py_trees/behaviour.py
@@ -0,0 +1,382 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+The core behaviour template. All behaviours, standalone and composite, inherit
+from this class.
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import re
+import typing
+import uuid
+
+from . import blackboard
+from . import common
+from . import logging
+
+##############################################################################
+# Behaviour BluePrint
+##############################################################################
+
+
+class Behaviour(object):
+ """
+ Defines the basic properties and methods required of a node in a
+ behaviour tree. When implementing your own behaviour,
+ subclass this class.
+
+ Args:
+ name: the behaviour name, defaults to auto-generating from the class name
+
+ Raises:
+ TypeError: if the provided name is not a string
+
+ Attributes:
+ ~py_trees.behaviours.Behaviour.id (:class:`uuid.UUID`): automagically generated unique identifier for the behaviour
+ ~py_trees.behaviours.Behaviour.name (:obj:`str`): the behaviour name
+ ~py_trees.behaviours.Behaviour.blackboards (typing.List[py_trees.blackboard.Client]): collection of attached blackboard clients
+ ~py_trees.behaviours.Behaviour.status (:class:`~py_trees.common.Status`): the behaviour status (:data:`~py_trees.common.Status.INVALID`, :data:`~py_trees.common.Status.RUNNING`, :data:`~py_trees.common.Status.FAILURE`, :data:`~py_trees.common.Status.SUCCESS`)
+ ~py_trees.behaviours.Behaviour.parent (:class:`~py_trees.behaviour.Behaviour`): a :class:`~py_trees.composites.Composite` instance if nested in a tree, otherwise None
+ ~py_trees.behaviours.Behaviour.children ([:class:`~py_trees.behaviour.Behaviour`]): empty for regular behaviours, populated for composites
+ ~py_trees.behaviours.Behaviour.logger (:class:`logging.Logger`): a simple logging mechanism
+ ~py_trees.behaviours.Behaviour.feedback_message(:obj:`str`): improve debugging with a simple message
+ ~py_trees.behaviours.Behaviour.blackbox_level (:class:`~py_trees.common.BlackBoxLevel`): a helper variable for dot graphs and runtime gui's to collapse/explode entire subtrees dependent upon the blackbox level.
+
+ .. seealso::
+ * :ref:`Skeleton Behaviour Template `
+ * :ref:`The Lifecycle Demo `
+ * :ref:`The Action Behaviour Demo `
+
+ """
+ def __init__(
+ self,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED
+ ):
+ if not name or name == common.Name.AUTO_GENERATED:
+ name = self.__class__.__name__
+ if not isinstance(name, str):
+ raise TypeError("a behaviour name should be a string, but you passed in {}".format(type(name)))
+ self.id = uuid.uuid4() # used to uniquely identify this node (helps with removing children from a tree)
+ self.name: str = name
+ self.blackboards: typing.List[blackboard.Client] = []
+ self.qualified_name = "{}/{}".format(self.__class__.__qualname__, self.name) # convenience
+ self.status = common.Status.INVALID
+ self.iterator = self.tick()
+ self.parent: typing.Optional[Behaviour] = None # will get set if a behaviour is added to a composite
+ self.children: typing.List[Behaviour] = [] # only set by composite behaviours
+ self.logger = logging.Logger(name)
+ self.feedback_message = "" # useful for debugging, or human readable updates, but not necessary to implement
+ self.blackbox_level = common.BlackBoxLevel.NOT_A_BLACKBOX
+
+ ############################################
+ # User Customisable Callbacks
+ ############################################
+
+ def setup(self, **kwargs):
+ """
+ .. note:: User Customisable Callback
+
+ Subclasses may override this method for any one-off delayed construction &
+ validation that is necessary prior to ticking the tree. Such construction is best
+ done here rather than in __init__ so that trees can be instantiated on the fly for
+ easy rendering to dot graphs without imposing runtime requirements (e.g. establishing
+ a middleware connection to a sensor or a driver to a serial port).
+
+ Equally as important, executing methods which validate the configuration of
+ behaviours will increase confidence that your tree will successfully tick
+ without logical software errors before actually ticking. This is useful both
+ before a tree's first tick and immediately after any modifications to a tree
+ has been made between ticks.
+
+ .. tip::
+
+ Faults are notified to the user of the behaviour via exceptions.
+ Choice of exception to use is left to the user.
+
+ .. warning::
+
+ The kwargs argument is for distributing objects at runtime to behaviours
+ before ticking. For example, a simulator instance with which behaviours can
+ interact with the simulator's python api, a ros2 node for setting up
+ communications. Use sparingly, as this is not proof against keyword conflicts
+ amongst disparate libraries of behaviours.
+
+ Args:
+ **kwargs (:obj:`dict`): distribute arguments to this
+ behaviour and in turn, all of it's children
+
+ Raises:
+ Exception: if this behaviour has a fault in construction or configuration
+
+ .. seealso:: :meth:`py_trees.behaviour.Behaviour.shutdown`
+ """
+ pass
+
+ def initialise(self):
+ """
+ .. note:: User Customisable Callback
+
+ Subclasses may override this method to perform any necessary initialising/clearing/resetting
+ of variables when when preparing to enter this behaviour if it was not previously
+ :data:`~py_trees.common.Status.RUNNING`. i.e. Expect this to trigger more than once!
+ """
+ pass
+
+ def terminate(self, new_status):
+ """
+ .. note:: User Customisable Callback
+
+ Subclasses may override this method to clean up. It will be triggered when a behaviour either
+ finishes execution (switching from :data:`~py_trees.common.Status.RUNNING`
+ to :data:`~py_trees.common.Status.FAILURE` || :data:`~py_trees.common.Status.SUCCESS`)
+ or it got interrupted by a higher priority branch (switching to
+ :data:`~py_trees.common.Status.INVALID`). Remember that the :meth:`~py_trees.behaviour.Behaviour.initialise` method
+ will handle resetting of variables before re-entry, so this method is about
+ disabling resources until this behaviour's next tick. This could be a indeterminably
+ long time. e.g.
+
+ * cancel an external action that got started
+ * shut down any tempoarary communication handles
+
+ Args:
+ new_status (:class:`~py_trees.common.Status`): the behaviour is transitioning to this new status
+
+ .. warning:: Do not set `self.status = new_status` here, that is automatically handled
+ by the :meth:`~py_trees.behaviour.Behaviour.stop` method. Use the argument purely for introspection purposes (e.g.
+ comparing the current state in `self.status` with the state it will transition to in
+ `new_status`.
+ """
+ pass
+
+ def update(self):
+ """
+ .. note:: User Customisable Callback
+
+ Returns:
+ :class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
+
+ Subclasses may override this method to perform any logic required to
+ arrive at a decision on the behaviour's new status. It is the primary worker function called on
+ by the :meth:`~py_trees.behaviour.Behaviour.tick` mechanism.
+
+ .. tip:: This method should be almost instantaneous and non-blocking
+
+ """
+ return common.Status.INVALID
+
+ def shutdown(self):
+ """
+ .. note:: User Customisable Callback
+
+ Subclasses may override this method for any custom destruction of infrastructure
+ usually brought into being in :meth:`~py_trees.behaviour.Behaviour.setup`.
+
+ Raises:
+ Exception: of whatever flavour the child raises when errors occur on destruction
+
+ .. seealso:: :meth:`py_trees.behaviour.Behaviour.setup`
+ """
+ pass
+
+ ############################################
+ # Private Methods - use inside a behaviour
+ ############################################
+
+ def attach_blackboard_client(
+ self,
+ name: str=None,
+ namespace: str=None
+ ) -> blackboard.Client:
+ """
+ Create and attach a blackboard to this behaviour.
+
+ Args:
+ name: human-readable (not necessarily unique) name for the client
+ namespace: sandbox the client to variables behind this namespace
+
+ Returns:
+ a handle to the attached blackboard client
+ """
+ if name is None:
+ count = len(self.blackboards)
+ name = self.name if (count == 0) else self.name + "-{}".format(count)
+ new_blackboard = blackboard.Client(
+ name=name,
+ namespace=namespace
+ )
+ self.blackboards.append(new_blackboard)
+ return new_blackboard
+
+ ############################################
+ # Public - lifecycle API
+ ############################################
+
+ def setup_with_descendants(self):
+ """
+ Iterates over this child, it's children (it's children's children, ...)
+ calling the user defined :meth:`~py_trees.behaviour.Behaviour.setup`
+ on each in turn.
+ """
+ for child in self.children:
+ for node in child.iterate():
+ node.setup()
+ self.setup()
+
+ def tick_once(self):
+ """
+ A direct means of calling tick on this object without
+ using the generator mechanism.
+ """
+ # no logger necessary here...it directly relays to tick
+ for unused in self.tick():
+ pass
+
+ def tick(self):
+ """
+ This function is a generator that can be used by an iterator on
+ an entire behaviour tree. It handles the logic for deciding when to
+ call the user's :meth:`~py_trees.behaviour.Behaviour.initialise` and :meth:`~py_trees.behaviour.Behaviour.terminate` methods as well as making the
+ actual call to the user's :meth:`~py_trees.behaviour.Behaviour.update` method that determines the
+ behaviour's new status once the tick has finished. Once done, it will
+ then yield itself (generator mechanism) so that it can be used as part of
+ an iterator for the entire tree.
+
+ .. code-block:: python
+
+ for node in my_behaviour.tick():
+ print("Do something")
+
+ .. note::
+
+ This is a generator function, you must use this with *yield*. If you need a direct call,
+ prefer :meth:`~py_trees.behaviour.Behaviour.tick_once` instead.
+
+ Yields:
+ :class:`~py_trees.behaviour.Behaviour`: a reference to itself
+
+ .. warning:: Override this method only in exceptional circumstances, prefer overriding :meth:`~py_trees.behaviour.Behaviour.update` instead.
+
+ """
+ self.logger.debug("%s.tick()" % (self.__class__.__name__))
+ if self.status != common.Status.RUNNING:
+ self.initialise()
+ # don't set self.status yet, terminate() may need to check what the current state is first
+ new_status = self.update()
+ if new_status not in list(common.Status):
+ self.logger.error("A behaviour returned an invalid status, setting to INVALID [%s][%s]" % (new_status, self.name))
+ new_status = common.Status.INVALID
+ if new_status != common.Status.RUNNING:
+ self.stop(new_status)
+ self.status = new_status
+ yield self
+
+ def iterate(self, direct_descendants=False):
+ """
+ Generator that provides iteration over this behaviour and all its children.
+ To traverse the entire tree:
+
+ .. code-block:: python
+
+ for node in my_behaviour.iterate():
+ print("Name: {0}".format(node.name))
+
+ Args:
+ direct_descendants (:obj:`bool`): only yield children one step away from this behaviour.
+
+ Yields:
+ :class:`~py_trees.behaviour.Behaviour`: one of it's children
+ """
+ for child in self.children:
+ if not direct_descendants:
+ for node in child.iterate():
+ yield node
+ else:
+ yield child
+ yield self
+
+ def visit(self, visitor):
+ """
+ This is functionality that enables external introspection into the behaviour. It gets used
+ by the tree manager classes to collect information as ticking traverses a tree.
+
+ Args:
+ visitor (:obj:`object`): the visiting class, must have a run(:class:`~py_trees.behaviour.Behaviour`) method.
+ """
+ visitor.run(self)
+
+ def stop(self, new_status=common.Status.INVALID):
+ """
+ Args:
+ new_status (:class:`~py_trees.common.Status`): the behaviour is transitioning to this new status
+
+ This calls the user defined :meth:`~py_trees.behaviour.Behaviour.terminate` method and also resets the
+ generator. It will finally set the new status once the user's :meth:`~py_trees.behaviour.Behaviour.terminate`
+ function has been called.
+
+ .. warning:: Override this method only in exceptional circumstances, prefer overriding :meth:`~py_trees.behaviour.Behaviour.terminate` instead.
+ """
+ self.logger.debug("%s.stop(%s)" % (self.__class__.__name__, "%s->%s" % (self.status, new_status) if self.status != new_status else "%s" % new_status))
+ self.terminate(new_status)
+ self.status = new_status
+ self.iterator = self.tick()
+
+ ############################################
+ # Public - introspection API
+ ############################################
+ def has_parent_with_name(self, name):
+ """
+ Searches through this behaviour's parents, and their parents, looking for
+ a behaviour with the same name as that specified.
+
+ Args:
+ name (:obj:`str`): name of the parent to match, can be a regular expression
+
+ Returns:
+ bool: whether a parent was found or not
+ """
+ pattern = re.compile(name)
+ b = self
+ while b.parent is not None:
+ if pattern.match(b.parent.name) is not None:
+ return True
+ b = b.parent
+ return False
+
+ def has_parent_with_instance_type(self, instance_type):
+ """
+ Moves up through this behaviour's parents looking for
+ a behaviour with the same instance type as that specified.
+
+ Args:
+ instance_type (:obj:`str`): instance type of the parent to match
+
+ Returns:
+ bool: whether a parent was found or not
+ """
+ b = self
+ while b.parent is not None:
+ if isinstance(b.parent, instance_type):
+ return True
+ b = b.parent
+ return False
+
+ def tip(self):
+ """
+ Get the *tip* of this behaviour's subtree (if it has one) after it's last
+ tick. This corresponds to the the deepest node that was running before the
+ subtree traversal reversed direction and headed back to this node.
+
+ Returns:
+ :class:`~py_trees.behaviour.Behaviour` or :obj:`None`: child behaviour, itself or :obj:`None` if its status is :data:`~py_trees.common.Status.INVALID`
+ """
+ return self if self.status != common.Status.INVALID else None
diff --git a/thirdparty/py_trees/behaviours.py b/thirdparty/py_trees/behaviours.py
new file mode 100644
index 0000000..2b96ee0
--- /dev/null
+++ b/thirdparty/py_trees/behaviours.py
@@ -0,0 +1,704 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+"""
+A library of fundamental behaviours for use.
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import copy
+import functools
+import operator
+import typing
+
+from . import behaviour
+from . import blackboard
+from . import common
+from . import meta
+
+##############################################################################
+# Function Behaviours
+##############################################################################
+
+
+def success(self):
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ self.feedback_message = "success"
+ return common.Status.SUCCESS
+
+
+def failure(self):
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ self.feedback_message = "failure"
+ return common.Status.FAILURE
+
+
+def running(self):
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ self.feedback_message = "running"
+ return common.Status.RUNNING
+
+
+def dummy(self):
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ self.feedback_message = "crash test dummy"
+ return common.Status.RUNNING
+
+
+Success = meta.create_behaviour_from_function(success)
+"""
+Do nothing but tick over with :data:`~py_trees.common.Status.SUCCESS`.
+"""
+
+Failure = meta.create_behaviour_from_function(failure)
+"""
+Do nothing but tick over with :data:`~py_trees.common.Status.FAILURE`.
+"""
+
+Running = meta.create_behaviour_from_function(running)
+"""
+Do nothing but tick over with :data:`~py_trees.common.Status.RUNNING`.
+"""
+
+Dummy = meta.create_behaviour_from_function(dummy)
+"""
+Crash test dummy used for anything dangerous.
+"""
+
+##############################################################################
+# Standalone Behaviours
+##############################################################################
+
+
+class Periodic(behaviour.Behaviour):
+ """
+ Simply periodically rotates it's status over the
+ :data:`~py_trees.common.Status.RUNNING`, :data:`~py_trees.common.Status.SUCCESS`,
+ :data:`~py_trees.common.Status.FAILURE` states.
+ That is, :data:`~py_trees.common.Status.RUNNING` for N ticks,
+ :data:`~py_trees.common.Status.SUCCESS` for N ticks,
+ :data:`~py_trees.common.Status.FAILURE` for N ticks...
+
+ Args:
+ name (:obj:`str`): name of the behaviour
+ n (:obj:`int`): period value (in ticks)
+
+ .. note:: It does not reset the count when initialising.
+ """
+ def __init__(self, name, n):
+ super(Periodic, self).__init__(name)
+ self.count = 0
+ self.period = n
+ self.response = common.Status.RUNNING
+
+ def update(self):
+ self.count += 1
+ if self.count > self.period:
+ if self.response == common.Status.FAILURE:
+ self.feedback_message = "flip to running"
+ self.response = common.Status.RUNNING
+ elif self.response == common.Status.RUNNING:
+ self.feedback_message = "flip to success"
+ self.response = common.Status.SUCCESS
+ else:
+ self.feedback_message = "flip to failure"
+ self.response = common.Status.FAILURE
+ self.count = 0
+ else:
+ self.feedback_message = "constant"
+ return self.response
+
+
+class StatusSequence(behaviour.Behaviour):
+ """
+ Cycle through the specified sequence of states.
+
+ Args:
+ name: name of the behaviour
+ sequence: list of status values to cycle through
+ eventually: status to use eventually, None to re-cycle the sequence
+ """
+ def __init__(
+ self,
+ name: str,
+ sequence: typing.List[common.Status],
+ eventually: typing.Optional[common.Status]
+ ):
+ super(StatusSequence, self).__init__(name)
+ self.sequence = sequence
+ self.eventually = eventually
+ self.current_sequence = copy.copy(sequence)
+
+ def update(self):
+ if self.current_sequence:
+ status = self.current_sequence.pop(0)
+ elif self.eventually is not None:
+ status = self.eventually
+ else:
+ self.current_sequence = copy.copy(self.sequence)
+ status = self.current_sequence.pop(0)
+ return status
+
+
+class SuccessEveryN(behaviour.Behaviour):
+ """
+ This behaviour updates it's status with :data:`~py_trees.common.Status.SUCCESS`
+ once every N ticks, :data:`~py_trees.common.Status.FAILURE` otherwise.
+
+ Args:
+ name (:obj:`str`): name of the behaviour
+ n (:obj:`int`): trigger success on every n'th tick
+
+ .. tip::
+ Use with decorators to change the status value as desired, e.g.
+ :meth:`py_trees.decorators.FailureIsRunning`
+ """
+ def __init__(self, name, n):
+ super(SuccessEveryN, self).__init__(name)
+ self.count = 0
+ self.every_n = n
+
+ def update(self):
+ self.count += 1
+ self.logger.debug("%s.update()][%s]" % (self.__class__.__name__, self.count))
+ if self.count % self.every_n == 0:
+ self.feedback_message = "now"
+ return common.Status.SUCCESS
+ else:
+ self.feedback_message = "not yet"
+ return common.Status.FAILURE
+
+
+class TickCounter(behaviour.Behaviour):
+ """
+ A useful utility behaviour for demos and tests. Simply
+ ticks with :data:`~py_trees.common.Status.RUNNING` for
+ the specified number of ticks before returning the
+ requested completion status (:data:`~py_trees.common.Status.SUCCESS`
+ or :data:`~py_trees.common.Status.FAILURE`).
+
+ This behaviour will reset the tick counter when initialising.
+
+ Args:
+ name: name of the behaviour
+ duration: number of ticks to run
+ completion_status: status to switch to once the counter has expired
+ """
+ def __init__(
+ self,
+ duration: int,
+ name=common.Name.AUTO_GENERATED,
+ completion_status: common.Status=common.Status.SUCCESS
+ ):
+ super().__init__(name=name)
+ self.completion_status = completion_status
+ self.duration = duration
+ self.counter = 0
+
+ def initialise(self):
+ """
+ Reset the tick counter.
+ """
+ self.counter = 0
+
+ def update(self):
+ """
+ Increment the tick counter and return the appropriate status for this behaviour
+ based on the tick count.
+
+ Returns
+ :data:`~py_trees.common.Status.RUNNING` while not expired, the given completion status otherwise
+ """
+ self.counter += 1
+ if self.counter <= self.duration:
+ return common.Status.RUNNING
+ else:
+ return self.completion_status
+
+
+class Count(behaviour.Behaviour):
+ """
+ A counting behaviour that updates its status at each tick depending on
+ the value of the counter. The status will move through the states in order -
+ :data:`~py_trees.common.Status.FAILURE`, :data:`~py_trees.common.Status.RUNNING`,
+ :data:`~py_trees.common.Status.SUCCESS`.
+
+ This behaviour is useful for simple testing and demo scenarios.
+
+ Args:
+ name (:obj:`str`): name of the behaviour
+ fail_until (:obj:`int`): set status to :data:`~py_trees.common.Status.FAILURE` until the counter reaches this value
+ running_until (:obj:`int`): set status to :data:`~py_trees.common.Status.RUNNING` until the counter reaches this value
+ success_until (:obj:`int`): set status to :data:`~py_trees.common.Status.SUCCESS` until the counter reaches this value
+ reset (:obj:`bool`): whenever invalidated (usually by a sequence reinitialising, or higher priority interrupting)
+
+ Attributes:
+ count (:obj:`int`): a simple counter which increments every tick
+ """
+ def __init__(self, name="Count", fail_until=3, running_until=5, success_until=6, reset=True):
+ super(Count, self).__init__(name)
+ self.count = 0
+ self.fail_until = fail_until
+ self.running_until = running_until
+ self.success_until = success_until
+ self.number_count_resets = 0
+ self.number_updated = 0
+ self.reset = reset
+
+ def terminate(self, new_status):
+ self.logger.debug("%s.terminate(%s->%s)" % (self.__class__.__name__, self.status, new_status))
+ # reset only if udpate got us into an invalid state
+ if new_status == common.Status.INVALID and self.reset:
+ self.count = 0
+ self.number_count_resets += 1
+ self.feedback_message = ""
+
+ def update(self):
+ self.number_updated += 1
+ self.count += 1
+ if self.count <= self.fail_until:
+ self.logger.debug("%s.update()[%s: failure]" % (self.__class__.__name__, self.count))
+ self.feedback_message = "failing"
+ return common.Status.FAILURE
+ elif self.count <= self.running_until:
+ self.logger.debug("%s.update()[%s: running]" % (self.__class__.__name__, self.count))
+ self.feedback_message = "running"
+ return common.Status.RUNNING
+ elif self.count <= self.success_until:
+ self.logger.debug("%s.update()[%s: success]" % (self.__class__.__name__, self.count))
+ self.feedback_message = "success"
+ return common.Status.SUCCESS
+ else:
+ self.logger.debug("%s.update()[%s: failure]" % (self.__class__.__name__, self.count))
+ self.feedback_message = "failing forever more"
+ return common.Status.FAILURE
+
+ def __repr__(self):
+ """
+ Simple string representation of the object.
+
+ Returns:
+ :obj:`str`: string representation
+ """
+ s = "%s\n" % self.name
+ s += " Status : %s\n" % self.status
+ s += " Count : %s\n" % self.count
+ s += " Resets : %s\n" % self.number_count_resets
+ s += " Updates: %s\n" % self.number_updated
+ return s
+
+##############################################################################
+# Blackboard Behaviours
+##############################################################################
+
+
+class BlackboardToStatus(behaviour.Behaviour):
+ """
+ This behaviour reverse engineers the :class:`~py_trees.decorators.StatusToBlackboard`
+ decorator. Used in conjuction with that decorator, this behaviour can be used to
+ reflect the status of a decision elsewhere in the tree.
+
+ .. note::
+
+ A word of caution. The consequences of a behaviour's status should be discernable
+ upon inspection of the tree graph. If using StatusToBlackboard
+ and BlackboardToStatus to reflect a behaviour's status across a tree,
+ this is no longer true. The graph of the tree communicates the local consequences,
+ but not the reflected consequences at the point BlackboardToStatus is used. A
+ recommendation, use this class only where other options are infeasible or impractical.
+
+ Args:
+ variable_name: name of the variable look for, may be nested, e.g. battery.percentage
+ name: name of the behaviour
+
+ Raises:
+ KeyError: if the variable doesn't exist
+ TypeError: if the variable isn't of type :py:data:`~py_trees.common.Status`
+ """
+ def __init__(
+ self,
+ variable_name: str,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED
+ ):
+ super().__init__(name=name)
+ name_components = variable_name.split('.')
+ self.key = name_components[0]
+ self.key_attributes = '.'.join(name_components[1:]) # empty string if no other parts
+ self.variable_name = variable_name
+ self.blackboard = self.attach_blackboard_client()
+ self.blackboard.register_key(key=self.key, access=common.Access.READ)
+
+ def update(self) -> common.Status:
+ """
+ Check for existence.
+
+ Returns:
+ :data:`~py_trees.common.Status.SUCCESS` if key found, :data:`~py_trees.common.Status.FAILURE` otherwise.
+ """
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ # raises a KeyError if the variable doesn't exist
+ status = self.blackboard.get(self.variable_name)
+ if type(status) != common.Status:
+ raise TypeError(f"{self.variable_name} is not of type py_trees.common.Status")
+ self.feedback_message = f"{self.variable_name}: {status}"
+ return status
+
+
+class CheckBlackboardVariableExists(behaviour.Behaviour):
+ """
+ Check the blackboard to verify if a specific variable (key-value pair)
+ exists. This is non-blocking, so will always tick with
+ status :data:`~py_trees.common.Status.FAILURE`
+ :data:`~py_trees.common.Status.SUCCESS`.
+
+ .. seealso::
+
+ :class:`~py_trees.behaviours.WaitForBlackboardVariable` for
+ the blocking counterpart to this behaviour.
+
+ Args:
+ variable_name: name of the variable look for, may be nested, e.g. battery.percentage
+ name: name of the behaviour
+ """
+ def __init__(
+ self,
+ variable_name: str,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED
+ ):
+ super().__init__(name=name)
+ self.variable_name = variable_name
+ name_components = variable_name.split('.')
+ self.key = name_components[0]
+ self.key_attributes = '.'.join(name_components[1:]) # empty string if no other parts
+ self.blackboard = self.attach_blackboard_client()
+ self.blackboard.register_key(key=self.key, access=common.Access.READ)
+
+ def update(self) -> common.Status:
+ """
+ Check for existence.
+
+ Returns:
+ :data:`~py_trees.common.Status.SUCCESS` if key found, :data:`~py_trees.common.Status.FAILURE` otherwise.
+ """
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ try:
+ unused_value = self.blackboard.get(self.variable_name)
+ self.feedback_message = "variable '{}' found".format(self.variable_name)
+ return common.Status.SUCCESS
+ except KeyError:
+ self.feedback_message = "variable '{}' not found".format(self.variable_name)
+ return common.Status.FAILURE
+
+
+class WaitForBlackboardVariable(CheckBlackboardVariableExists):
+ """
+ Wait for the blackboard variable to become available on the blackboard.
+ This is blocking, so it will tick with
+ status :data:`~py_trees.common.Status.SUCCESS` if the variable is found,
+ and :data:`~py_trees.common.Status.RUNNING` otherwise.
+
+ .. seealso::
+
+ :class:`~py_trees.behaviours.CheckBlackboardVariableExists` for
+ the non-blocking counterpart to this behaviour.
+
+ Args:
+ variable_name: name of the variable to wait for, may be nested, e.g. battery.percentage
+ name: name of the behaviour
+ """
+ def __init__(
+ self,
+ variable_name: str,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED
+ ):
+ super().__init__(name=name, variable_name=variable_name)
+
+ def update(self) -> common.Status:
+ """
+ Check for existence, wait otherwise.
+
+ Returns:
+ :data:`~py_trees.common.Status.SUCCESS` if key found, :data:`~py_trees.common.Status.RUNNING` otherwise.
+ """
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ new_status = super().update()
+ # CheckBlackboardExists only returns SUCCESS || FAILURE
+ if new_status == common.Status.SUCCESS:
+ self.feedback_message = "'{}' found".format(self.key)
+ return common.Status.SUCCESS
+ else: # new_status == common.Status.FAILURE
+ self.feedback_message = "waiting for key '{}'...".format(self.key)
+ return common.Status.RUNNING
+
+
+class UnsetBlackboardVariable(behaviour.Behaviour):
+ """
+ Unset the specified variable (key-value pair) from the blackboard.
+
+ This always returns
+ :data:`~py_trees.common.Status.SUCCESS` regardless of whether
+ the variable was already present or not.
+
+ Args:
+ key: unset this key-value pair
+ name: name of the behaviour
+ """
+ def __init__(self,
+ key: str,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED,
+ ):
+ super().__init__(name=name)
+ self.key = key
+ self.blackboard = self.attach_blackboard_client()
+ self.blackboard.register_key(key=self.key, access=common.Access.WRITE)
+
+ def update(self) -> common.Status:
+ """
+ Unset and always return success.
+
+ Returns:
+ :data:`~py_trees.common.Status.SUCCESS`
+ """
+ if self.blackboard.unset(self.key):
+ self.feedback_message = "'{}' found and removed".format(self.key)
+ else:
+ self.feedback_message = "'{}' not found, nothing to remove"
+ return common.Status.SUCCESS
+
+
+class SetBlackboardVariable(behaviour.Behaviour):
+ """
+ Set the specified variable on the blackboard.
+
+ Args:
+ variable_name: name of the variable to set, may be nested, e.g. battery.percentage
+ variable_value: value of the variable to set
+ overwrite: when False, do not set the variable if it already exists
+ name: name of the behaviour
+ """
+ def __init__(
+ self,
+ variable_name: str,
+ variable_value: typing.Union[typing.Any, typing.Callable[[], typing.Any]],
+ overwrite: bool = True,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED,
+ ):
+ super().__init__(name=name)
+ self.variable_name = variable_name
+ name_components = variable_name.split('.')
+ self.key = name_components[0]
+ self.key_attributes = '.'.join(name_components[1:]) # empty string if no other parts
+ self.blackboard = self.attach_blackboard_client()
+ self.blackboard.register_key(key=self.key, access=common.Access.WRITE)
+ self.variable_value_generator = variable_value if callable(variable_value) else lambda: variable_value
+ self.overwrite = overwrite
+
+ def update(self) -> common.Status:
+ """
+ Always return success.
+
+ Returns:
+ :data:`~py_trees.common.Status.FAILURE` if no overwrite requested and the variable exists, :data:`~py_trees.common.Status.SUCCESS` otherwise
+ """
+ if self.blackboard.set(
+ self.variable_name,
+ self.variable_value_generator(),
+ overwrite=self.overwrite
+ ):
+ return common.Status.SUCCESS
+ else:
+ return common.Status.FAILURE
+
+
+class CheckBlackboardVariableValue(behaviour.Behaviour):
+ """
+ Inspect a blackboard variable and if it exists, check that it
+ meets the specified criteria (given by operation type and expected value).
+ This is non-blocking, so it will always tick with
+ :data:`~py_trees.common.Status.SUCCESS` or
+ :data:`~py_trees.common.Status.FAILURE`.
+
+ Args:
+ check: a comparison expression to check against
+ name: name of the behaviour
+
+ .. note::
+ If the variable does not yet exist on the blackboard, the behaviour will
+ return with status :data:`~py_trees.common.Status.FAILURE`.
+
+ .. tip::
+ The python `operator module`_ includes many useful comparison operations.
+ """
+ def __init__(
+ self,
+ check: common.ComparisonExpression,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED
+ ):
+ super().__init__(name=name)
+ self.check = check
+ name_components = self.check.variable.split('.')
+ self.key = name_components[0]
+ self.key_attributes = '.'.join(name_components[1:]) # empty string if no other parts
+ self.blackboard = self.attach_blackboard_client()
+ self.blackboard.register_key(key=self.key, access=common.Access.READ)
+
+ def update(self):
+ """
+ Check for existence, or the appropriate match on the expected value.
+
+ Returns:
+ :class:`~py_trees.common.Status`: :data:`~py_trees.common.Status.FAILURE` if not matched, :data:`~py_trees.common.Status.SUCCESS` otherwise.
+ """
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ try:
+ value = self.blackboard.get(self.key)
+ if self.key_attributes:
+ try:
+ value = operator.attrgetter(self.key_attributes)(value)
+ except AttributeError:
+ self.feedback_message = 'blackboard key-value pair exists, but the value does not have the requested nested attributes [{}]'.format(self.variable_name)
+ return common.Status.FAILURE
+ except KeyError:
+ self.feedback_message = "key '{}' does not yet exist on the blackboard".format(self.check.variable)
+ return common.Status.FAILURE
+
+ success = self.check.operator(value, self.check.value)
+
+ if success:
+ self.feedback_message = "'%s' comparison succeeded [v: %s][e: %s]" % (self.check.variable, value, self.check.value)
+ return common.Status.SUCCESS
+ else:
+ self.feedback_message = "'%s' comparison failed [v: %s][e: %s]" % (self.check.variable, value, self.check.value)
+ return common.Status.FAILURE
+
+
+class WaitForBlackboardVariableValue(CheckBlackboardVariableValue):
+ """
+ Inspect a blackboard variable and if it exists, check that it
+ meets the specified criteria (given by operation type and expected value).
+ This is blocking, so it will always tick with
+ :data:`~py_trees.common.Status.SUCCESS` or
+ :data:`~py_trees.common.Status.RUNNING`.
+
+ .. seealso::
+
+ :class:`~py_trees.behaviours.CheckBlackboardVariableValue` for
+ the non-blocking counterpart to this behaviour.
+
+ .. note::
+ If the variable does not yet exist on the blackboard, the behaviour will
+ return with status :data:`~py_trees.common.Status.RUNNING`.
+
+ Args:
+ check: a comparison expression to check against
+ name: name of the behaviour
+ """
+ def __init__(
+ self,
+ check: common.ComparisonExpression,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED
+ ):
+ super().__init__(
+ check=check,
+ name=name
+ )
+
+ def update(self):
+ """
+ Check for existence, or the appropriate match on the expected value.
+
+ Returns:
+ :class:`~py_trees.common.Status`: :data:`~py_trees.common.Status.FAILURE` if not matched, :data:`~py_trees.common.Status.SUCCESS` otherwise.
+ """
+ new_status = super().update()
+ if new_status == common.Status.FAILURE:
+ return common.Status.RUNNING
+ else:
+ return new_status
+
+
+class CheckBlackboardVariableValues(behaviour.Behaviour):
+ """
+ Apply a logical operation across a set of blackboard variable checks.
+ This is non-blocking, so will always tick with status
+ :data:`~py_trees.common.Status.FAILURE` or
+ :data:`~py_trees.common.Status.SUCCESS`.
+
+ Args:
+ checks: a list of comparison checks to apply to blackboard variables
+ logical_operator: a logical check to apply across the results of the blackboard variable checks
+ name: name of the behaviour
+ namespace: optionally store results of the checks (boolean) under this namespace
+
+ .. tip::
+ The python `operator module`_ includes many useful logical operators, e.g. operator.xor.
+
+ Raises:
+ ValueError if less than two variable checks are specified (insufficient for logical operations)
+ """
+ def __init__(
+ self,
+ checks: typing.List[common.ComparisonExpression],
+ operator: typing.Callable[[bool, bool], bool],
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED,
+ namespace: typing.Optional[str]=None,
+ ):
+ super().__init__(name=name)
+ self.checks = checks
+ self.operator = operator
+ self.blackboard = self.attach_blackboard_client()
+ if len(checks) < 2:
+ raise ValueError("Must be at least two variables to operate on [only {} provided]".format(len(checks)))
+ for check in self.checks:
+ self.blackboard.register_key(
+ key=blackboard.Blackboard.key(check.variable),
+ access=common.Access.READ
+ )
+ self.blackboard_results = None
+ if namespace is not None:
+ self.blackboard_results = self.attach_blackboard_client(namespace=namespace)
+ for counter in range(1, len(self.checks) + 1):
+ self.blackboard_results.register_key(
+ key=str(counter),
+ access=common.Access.WRITE
+ )
+
+ def update(self) -> common.Status:
+ """
+ Applies comparison checks on each variable and a logical check across the
+ complete set of variables.
+
+ Returns:
+ :data:`~py_trees.common.Status.FAILURE` if key retrieval or logical checks failed, :data:`~py_trees.common.Status.SUCCESS` otherwise.
+ """
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ results = []
+ for check in self.checks:
+ try:
+ value = self.blackboard.get(check.variable)
+ except KeyError:
+ self.feedback_message = "variable '{}' does not yet exist on the blackboard".format(check.variable)
+ return common.Status.FAILURE
+ results.append(check.operator(value, check.value))
+ if self.blackboard_results is not None:
+ for counter in range(1, len(results) + 1):
+ self.blackboard_results.set(str(counter), results[counter - 1])
+ logical_result = functools.reduce(self.operator, results)
+ if logical_result:
+ self.feedback_message = "[{}]".format(
+ "|".join(["T" if result else "F" for result in results])
+ )
+ return common.Status.SUCCESS
+ else:
+ self.feedback_message = "[{}]".format(
+ "|".join(["T" if result else "F" for result in results])
+ )
+ return common.Status.FAILURE
diff --git a/thirdparty/py_trees/blackboard.py b/thirdparty/py_trees/blackboard.py
new file mode 100755
index 0000000..6f18c76
--- /dev/null
+++ b/thirdparty/py_trees/blackboard.py
@@ -0,0 +1,1351 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+Blackboards are not a necessary component of behaviour tree implementations,
+but are nonetheless, a fairly common mechanism for sharing data between
+behaviours in the tree. See, for example, the `design notes`_
+for blackboards in Unreal Engine.
+
+.. image:: images/blackboard.jpg
+ :width: 300px
+ :align: center
+
+Implementations vary widely depending on the needs of
+the framework using them. The simplest implementations take the
+form of a key-value store with global access, while more
+rigorous implementations scope access or form a secondary
+graph overlaying the tree connecting data ports between behaviours.
+
+The *'Zen of PyTrees'* is to enable rapid development, yet be rich
+enough so that *all* of the magic is exposed for debugging purposes.
+The first implementation of a blackboard was merely a global key-value
+store with an api that lent itself to ease of use, but did not
+expose the data sharing between behaviours which meant any tooling
+used to introspect or visualise the tree, only told half the story.
+
+The current implementation adopts a strategy similar to that of a
+filesystem. Each client (subsequently behaviour) registers itself
+for read/write access to keys on the blackboard. This is less to
+do with permissions and more to do with tracking users of keys
+on the blackboard - extremely helpful with debugging.
+
+The alternative approach of layering a secondary data graph
+with parameter and input-output ports on each behaviour was
+discarded as being too heavy for the zen requirements of py_trees.
+This is in part due to the wiring costs, but also due to
+complexity arising from a tree's partial graph execution
+(a feature which makes trees different from most computational
+graph frameworks) and not to regress on py_trees' capability to
+dynamically insert and prune subtrees on the fly.
+
+A high-level list of existing / planned features:
+
+* [+] Centralised key-value store
+* [+] Client connections with namespaced read/write access to the store
+* [+] Integration with behaviours for key-behaviour associations (debugging)
+* [+] Activity stream that logs read/write operations by clients
+* [+] Exclusive locks for writing
+* [+] Framework for key remappings
+
+.. include:: weblinks.rst
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import enum
+import itertools
+import operator
+import re
+import typing
+import uuid
+
+from . import common
+from . import console
+from . import utilities
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+class KeyMetaData(object):
+ """
+ Stores the aggregated metadata for a key on the blackboard.
+ """
+ def __init__(self):
+ self.read = set()
+ self.write = set()
+ self.exclusive = set()
+
+
+class ActivityType(enum.Enum):
+ """An enumerator representing the operation on a blackboard variable"""
+
+ READ = "READ"
+ """Read from the blackboard"""
+ INITIALISED = "INITIALISED"
+ """Initialised a key-value pair on the blackboard"""
+ WRITE = "WRITE"
+ """Wrote to the blackboard."""
+ ACCESSED = "ACCESSED"
+ """Key accessed, either for reading, or modification of the value's internal attributes (e.g. foo.bar)."""
+ ACCESS_DENIED = "ACCESS_DENIED"
+ """Client did not have access to read/write a key."""
+ NO_KEY = "NO_KEY"
+ """Tried to access a key that does not yet exist on the blackboard."""
+ NO_OVERWRITE = "NO_OVERWRITE"
+ """Tried to write but variable already exists and a no-overwrite request was respected."""
+ UNSET = "UNSET"
+ """Key was removed from the blackboard"""
+
+
+class ActivityItem(object):
+ """
+ Recorded data pertaining to activity on the blackboard.
+
+ Args:
+ key: name of the variable on the blackboard
+ client_name: convenient name of the client performing the operation
+ client_id: unique id of the client performing the operation
+ activity_type: type of activity
+ previous_value: of the given key (None if this field is not relevant)
+ current_value: current value for the given key (None if this field is not relevant)
+ """
+ def __init__(
+ self,
+ key,
+ client_name: str,
+ client_id: uuid.UUID,
+ activity_type: str,
+ previous_value: typing.Any=None,
+ current_value: typing.Any=None):
+ # TODO validity checks for values passed/not passed on the
+ # respective activity types. Note: consider using an enum
+ # for 'no value here' since None is a perfectly acceptable
+ # value for a key
+ self.key = key
+ self.client_name = client_name
+ self.client_id = client_id
+ self.activity_type = activity_type
+ self.previous_value = previous_value
+ self.current_value = current_value
+
+
+class ActivityStream(object):
+ """
+ Storage container with convenience methods for manipulating the stored
+ activity stream.
+
+ Attributes:
+ data (typing.List[ActivityItem]: list of activity items, earliest first
+ maximum_size (int): pop items if this size is exceeded
+ """
+
+ def __init__(self, maximum_size: int=500):
+ """
+ Initialise the stream with a maximum storage limit.
+
+ Args:
+ maximum_size: pop items from the stream if this size is exceeded
+ """
+ self.data: typing.List[ActivityItem] = []
+ self.maximum_size = maximum_size
+
+ def push(self, activity_item: ActivityItem):
+ """
+ Push the next activity item to the stream.
+
+ Args:
+ activity_item: new item to append to the stream
+ """
+ if len(self.data) > self.maximum_size:
+ self.data.pop()
+ self.data.append(activity_item)
+
+ def clear(self):
+ """
+ Delete all activities from the stream.
+ """
+ self.data = []
+
+
+class Blackboard(object):
+ """
+ Centralised key-value store for sharing data between behaviours.
+ This class is a coat-hanger for the centralised data store, metadata
+ for it's administration and static methods for interacting with it.
+
+ This api is intended for authors of debugging and introspection
+ tools on the blackboard. Users should make use of the :class:`Client`.
+
+ Attributes:
+ Blackboard.clients (typing.Dict[uuid.UUID, str]): client uuid-name registry
+ Blackboard.storage (typing.Dict[str, typing.Any]): key-value data store
+ Blackboard.metadata (typing.Dict[str, KeyMetaData]): key associated metadata
+ Blackboard.activity_stream (ActivityStream): logged activity
+ Blackboard.separator (char): namespace separator character
+ """
+ storage: typing.Dict[str, typing.Any] = {} # key-value storage
+ metadata: typing.Dict[str, KeyMetaData] = {} # key-metadata information
+ clients: typing.Dict[uuid.UUID, str] = {} # client id-name pairs
+ activity_stream: typing.Optional[ActivityStream] = None
+ separator: str = "/"
+
+ @staticmethod
+ def keys() -> typing.Set[str]:
+ """
+ Get the set of blackboard keys.
+
+ Returns:
+ the complete set of keys registered by clients
+ """
+ # return registered keys, those on the blackboard are not
+ # necessarily written to yet
+ return set(Blackboard.metadata.keys())
+
+ @staticmethod
+ def get(variable_name: str) -> typing.Any:
+ """
+ Extract the value associated with the given a variable name,
+ can be nested, e.g. battery.percentage. This differs from the
+ client get method in that it doesn't pass through the client access
+ checks. To be used for utility tooling (e.g. display methods) and not by
+ users directly.
+
+ Args:
+ variable_name: of the variable to get, can be nested, e.g. battery.percentage
+
+ Raises:
+ KeyError: if the variable or it's nested attributes do not yet exist on the blackboard
+
+ Return:
+ The stored value for the given variable
+ """
+ variable_name = Blackboard.absolute_name(Blackboard.separator, variable_name)
+ name_components = variable_name.split('.')
+ key = name_components[0]
+ key_attributes = '.'.join(name_components[1:])
+ # can raise KeyError
+ value = Blackboard.storage[key]
+ if key_attributes:
+ try:
+ value = operator.attrgetter(key_attributes)(value)
+ except AttributeError:
+ raise KeyError("Key exists, but does not have the specified nested attributes [{}]".format(variable_name))
+ return value
+
+ @staticmethod
+ def set(variable_name: str, value: typing.Any):
+ """
+ Set the value associated with the given a variable name,
+ can be nested, e.g. battery.percentage. This differs from the
+ client get method in that it doesn't pass through the client access
+ checks. To be used for utility tooling (e.g. display methods) and not by
+ users directly.
+
+ Args:
+ variable_name: of the variable to set, can be nested, e.g. battery.percentage
+
+ Raises:
+ AttributeError: if it is attempting to set a nested attribute tha does not exist.
+ """
+ variable_name = Blackboard.absolute_name(Blackboard.separator, variable_name)
+ name_components = variable_name.split('.')
+ key = name_components[0]
+ key_attributes = '.'.join(name_components[1:])
+ if not key_attributes:
+ Blackboard.storage[key] = value
+ else:
+ setattr(Blackboard.storage[key], key_attributes, value)
+ Blackboard.metadata.setdefault(key, KeyMetaData())
+
+ @staticmethod
+ def unset(key: str):
+ """
+ For when you need to completely remove a blackboard variable (key-value pair),
+ this provides a convenient helper method.
+
+ Args:
+ key: name of the variable to remove
+
+ Returns:
+ True if the variable was removed, False if it was already absent
+ """
+ try:
+ key = Blackboard.absolute_name(Blackboard.separator, key)
+ del Blackboard.storage[key]
+ return True
+ except KeyError:
+ return False
+
+ @staticmethod
+ def exists(name: str) -> bool:
+ """
+ Check if the specified variable exists on the blackboard.
+
+ Args:
+ name: name of the variable, can be nested, e.g. battery.percentage
+
+ Raises:
+ AttributeError: if the client does not have read access to the variable
+ """
+ try:
+ name = Blackboard.absolute_name(Blackboard.separator, name)
+ unused_value = Blackboard.get(name)
+ return True
+ except KeyError:
+ return False
+
+ @staticmethod
+ def keys_filtered_by_regex(regex: str) -> typing.Set[str]:
+ """
+ Get the set of blackboard keys filtered by regex.
+
+ Args:
+ regex: a python regex string
+
+ Returns:
+ subset of keys that have been registered and match the pattern
+ """
+ pattern = re.compile(regex)
+ return {key for key in Blackboard.metadata.keys() if pattern.search(key) is not None}
+
+ @staticmethod
+ def keys_filtered_by_clients(
+ client_ids: typing.Union[typing.Set[uuid.UUID], typing.List[uuid.UUID]]
+ ) -> typing.Set[str]:
+ """
+ Get the set of blackboard keys filtered by client unique identifiers.
+
+ Args:
+ client_ids: set of client uuid's.
+
+ Returns:
+ subset of keys that have been registered by the specified clients
+ """
+ # forgive users if they sent a list instead of a set
+ if isinstance(client_ids, list):
+ client_ids = set(client_ids)
+ keys = set()
+ for key in Blackboard.metadata.keys():
+ # for sets, | is union, & is intersection
+ key_clients = (
+ set(Blackboard.metadata[key].read) |
+ set(Blackboard.metadata[key].write) |
+ set(Blackboard.metadata[key].exclusive)
+ )
+ if key_clients & client_ids:
+ keys.add(key)
+ return keys
+
+ @staticmethod
+ def enable_activity_stream(maximum_size: int=500):
+ """
+ Enable logging of activities on the blackboard.
+
+ Args:
+ maximum_size: pop items from the stream if this size is exceeded
+
+ Raises:
+ RuntimeError if the activity stream is already enabled
+ """
+ if Blackboard.activity_stream is None:
+ Blackboard.activity_stream = ActivityStream(maximum_size)
+ else:
+ RuntimeError("activity stream is already enabled for this blackboard")
+
+ @staticmethod
+ def disable_activity_stream():
+ """
+ Disable logging of activities on the blackboard
+ """
+ Blackboard.activity_stream = None
+
+ @staticmethod
+ def clear():
+ """
+ Completely clear all key, value and client information from the blackboard.
+ Also deletes the activity stream.
+ """
+ Blackboard.storage.clear()
+ Blackboard.metadata.clear()
+ Blackboard.clients.clear()
+ Blackboard.activity_stream = None
+
+ @staticmethod
+ def absolute_name(namespace: str, key: str) -> str:
+ """
+ Generate the fully qualified key name from namespace and name arguments.
+
+ **Examples**
+
+ .. code-block:: python
+
+ '/' + 'foo' = '/foo'
+ '/' + '/foo' = '/foo'
+ '/foo' + 'bar' = '/foo/bar'
+ '/foo/' + 'bar' = '/foo/bar'
+ '/foo' + '/foo/bar' = '/foo/bar'
+ '/foo' + '/bar' = '/bar'
+ '/foo' + 'foo/bar' = '/foo/foo/bar'
+
+ Args:
+ namespace: namespace the key should be embedded in
+ key: key name (relative or absolute)
+
+ Returns:
+ the absolute name
+
+ .. warning::
+
+ To expedite the method call (it's used with high frequency
+ in blackboard key lookups), no checks are made to ensure
+ the namespace argument leads with a "/". Nor does it check
+ that a name in absolute form is actually embedded in the
+ specified namespace, it just returns the given (absolute)
+ name directly.
+ """
+ # it's already absolute
+ if key.startswith(Blackboard.separator):
+ return key
+ # remove leading and trailing separators
+ namespace = namespace if namespace.endswith(Blackboard.separator) else namespace + Blackboard.separator
+ key = key.strip(Blackboard.separator)
+ return "{}{}".format(namespace, key)
+
+ @staticmethod
+ def relative_name(namespace: str, key: str) -> str:
+ """
+ **Examples**
+
+ .. code-block:: python
+
+ '/' + 'foo' = '/foo'
+ '/' + '/foo' = '/foo'
+ '/foo' + 'bar' = '/foo/bar'
+ '/foo/' + 'bar' = '/foo/bar'
+ '/foo' + '/bar' => KeyError('/bar' is not in 'foo')
+ '/foo' + 'foo/bar' = '/foo/foo/bar'
+
+ Args:
+ namespace: namespace the key should be embedded in
+ key: key name (relative or absolute)
+
+ Returns:
+ the absolute name
+
+ Raises:
+ KeyError if the key is not in the specified namespace
+
+ .. warning::
+
+ To expedite the method call (it's used with high frequency
+ in blackboard key lookups), no checks are made to ensure
+ the namespace argument leads with a "/"
+ """
+ # it's already relative
+ if not key.startswith(Blackboard.separator):
+ return key
+ # remove leading and trailing separators
+ namespace = namespace if namespace.endswith(Blackboard.separator) else namespace + Blackboard.separator
+ if key.startswith(namespace):
+ return key.lstrip(namespace)
+ else:
+ raise KeyError("key '{}' is not in namespace '{}'".format(
+ key, namespace)
+ )
+
+ @staticmethod
+ def key(variable_name: str) -> str:
+ """
+ Extract the key for an arbitrary blackboard variable, keeping in mind that blackboard variable
+ names can be pointing to a nested attribute within a key.
+
+ Example: '/foo/bar.woohoo -> /foo/bar'.
+
+ Args:
+ variable_name: blackboard variable name - can be nested, e.g. battery.percentage
+
+ Returns:
+ name of the underlying key
+ """
+ name_components = variable_name.split('.')
+ key = name_components[0]
+ return key
+
+ @staticmethod
+ def key_with_attributes(variable_name: str) -> typing.Tuple[str, str]:
+ """
+ Extract the key for an arbitrary blackboard variable, keeping in mind that blackboard variable
+ names can be pointing to a nested attribute within a key,
+
+ Example: '/foo/bar.woohoo -> (/foo/bar'. 'woohoo')
+
+ Args:
+ variable_name: blackboard variable name - can be nested, e.g. battery.percentage
+
+ Returns:
+ a tuple consisting of the key and it's attributes (in string form)
+ """
+ name_components = variable_name.split('.')
+ key = name_components[0]
+ key_attributes = '.'.join(name_components[1:])
+ return (key, key_attributes)
+
+
+class Client(object):
+ """
+ Client to the key-value store for sharing data between behaviours.
+
+ **Examples**
+
+ Blackboard clients will accept a user-friendly name or create one
+ for you if none is provided. Regardless of what name is chosen, clients
+ are always uniquely identified via a uuid generated on construction.
+
+ .. code-block:: python
+
+ provided = py_trees.blackboard.Client(name="Provided")
+ print(provided)
+ generated = py_trees.blackboard.Client()
+ print(generated)
+
+ .. figure:: images/blackboard_client_instantiation.png
+ :align: center
+
+ Client Instantiation
+
+ Register read/write access for keys on the blackboard. Note, registration is
+ not initialisation.
+
+ .. code-block:: python
+
+ blackboard = py_trees.blackboard.Client(name="Client")
+ blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE)
+ blackboard.register_key(key="bar", access=py_trees.common.Access.READ)
+ blackboard.foo = "foo"
+ print(blackboard)
+
+ .. figure:: images/blackboard_read_write.png
+ :align: center
+
+ Variable Read/Write Registration
+
+ Keys and clients can make use of namespaces, designed by the '/' char. Most
+ methods permit a flexible expression of either relative or absolute names.
+
+ .. code-block:: python
+
+ blackboard = py_trees.blackboard.Client(name="Global")
+ parameters = py_trees.blackboard.Client(name="Parameters", namespace="parameters")
+
+ blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE)
+ blackboard.register_key(key="/bar", access=py_trees.common.Access.WRITE)
+ blackboard.register_key(key="/parameters/default_speed", access=py_trees.common.Access.WRITE)
+ parameters.register_key(key="aggressive_speed", access=py_trees.common.Access.WRITE)
+
+ blackboard.foo = "foo"
+ blackboard.bar = "bar"
+ blackboard.parameters.default_speed = 20.0
+ parameters.aggressive_speed = 60.0
+
+ miss_daisy = blackboard.parameters.default_speed
+ van_diesel = parameters.aggressive_speed
+
+ print(blackboard)
+ print(parameters)
+
+ .. figure:: images/blackboard_namespaces.png
+ :align: center
+
+ Namespaces and Namespaced Clients
+
+
+ Disconnected instances will discover the centralised
+ key-value store.
+
+ .. code-block:: python
+
+ def check_foo():
+ blackboard = py_trees.blackboard.Client(name="Reader")
+ blackboard.register_key(key="foo", access=py_trees.common.Access.READ)
+ print("Foo: {}".format(blackboard.foo))
+
+
+ blackboard = py_trees.blackboard.Client(name="Writer")
+ blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE)
+ blackboard.foo = "bar"
+ check_foo()
+
+ To respect an already initialised key on the blackboard:
+
+ .. code-block:: python
+
+ blackboard = Client(name="Writer")
+ blackboard.register_key(key="foo", access=py_trees.common.Access.READ)
+ result = blackboard.set("foo", "bar", overwrite=False)
+
+ Store complex objects on the blackboard:
+
+ .. code-block:: python
+
+ class Nested(object):
+ def __init__(self):
+ self.foo = None
+ self.bar = None
+
+ def __str__(self):
+ return str(self.__dict__)
+
+
+ writer = py_trees.blackboard.Client(name="Writer")
+ writer.register_key(key="nested", access=py_trees.common.Access.WRITE)
+ reader = py_trees.blackboard.Client(name="Reader")
+ reader.register_key(key="nested", access=py_trees.common.Access.READ)
+
+ writer.nested = Nested()
+ writer.nested.foo = "I am foo"
+ writer.nested.bar = "I am bar"
+
+ foo = reader.nested.foo
+ print(writer)
+ print(reader)
+
+ .. figure:: images/blackboard_nested.png
+ :align: center
+
+ Log and display the activity stream:
+
+ .. code-block:: python
+
+ py_trees.blackboard.Blackboard.enable_activity_stream(maximum_size=100)
+ reader = py_trees.blackboard.Client(name="Reader")
+ reader.register_key(key="foo", access=py_trees.common.Access.READ)
+ writer = py_trees.blackboard.Client(name="Writer")
+ writer.register_key(key="foo", access=py_trees.common.Access.WRITE)
+ writer.foo = "bar"
+ writer.foo = "foobar"
+ unused_result = reader.foo
+ print(py_trees.display.unicode_blackboard_activity_stream())
+ py_trees.blackboard.Blackboard.activity_stream.clear()
+
+ .. figure:: images/blackboard_activity_stream.png
+ :align: center
+
+ Display the blackboard on the console, or part thereof:
+
+ .. code-block:: python
+
+ writer = py_trees.blackboard.Client(name="Writer")
+ for key in {"foo", "bar", "dude", "dudette"}:
+ writer.register_key(key=key, access=py_trees.common.Access.WRITE)
+
+ reader = py_trees.blackboard.Client(name="Reader")
+ for key in {"foo", "bar"}:
+ reader.register_key(key="key", access=py_trees.common.Access.READ)
+
+ writer.foo = "foo"
+ writer.bar = "bar"
+ writer.dude = "bob"
+
+ # all key-value pairs
+ print(py_trees.display.unicode_blackboard())
+ # various filtered views
+ print(py_trees.display.unicode_blackboard(key_filter={"foo"}))
+ print(py_trees.display.unicode_blackboard(regex_filter="dud*"))
+ print(py_trees.display.unicode_blackboard(client_filter={reader.unique_identifier}))
+ # list the clients associated with each key
+ print(py_trees.display.unicode_blackboard(display_only_key_metadata=True))
+
+ .. figure:: images/blackboard_display.png
+ :align: center
+
+ Behaviours are not automagically connected to the blackboard but you may
+ manually attach one or more clients so that associations between behaviours
+ and variables can be tracked - this is very useful for introspection and
+ debugging.
+
+ Creating a custom behaviour with blackboard variables:
+
+ .. code-block:: python
+
+ class Foo(py_trees.behaviour.Behaviour):
+
+ def __init__(self, name):
+ super().__init__(name=name)
+ self.blackboard = self.attach_blackboard_client(name="Foo Global")
+ self.parameters = self.attach_blackboard_client(name="Foo Params", namespace="foo_parameters_")
+ self.state = self.attach_blackboard_client(name="Foo State", namespace="foo_state_")
+
+ # create a key 'foo_parameters_init' on the blackboard
+ self.parameters.register_key("init", access=py_trees.common.Access.READ)
+ # create a key 'foo_state_number_of_noodles' on the blackboard
+ self.state.register_key("number_of_noodles", access=py_trees.common.Access.WRITE)
+
+ def initialise(self):
+ self.state.number_of_noodles = self.parameters.init
+
+ def update(self):
+ self.state.number_of_noodles += 1
+ self.feedback_message = self.state.number_of_noodles
+ if self.state.number_of_noodles > 5:
+ return py_trees.common.Status.SUCCESS
+ else:
+ return py_trees.common.Status.RUNNING
+
+
+ # could equivalently do directly via the Blackboard static methods if
+ # not interested in tracking / visualising the application configuration
+ configuration = py_trees.blackboard.Client(name="App Config")
+ configuration.register_key("foo_parameters_init", access=py_trees.common.Access.WRITE)
+ configuration.foo_parameters_init = 3
+
+ foo = Foo(name="The Foo")
+ for i in range(1, 8):
+ foo.tick_once()
+ print("Number of Noodles: {}".format(foo.feedback_message))
+
+ Rendering a dot graph for a behaviour tree, complete with blackboard variables:
+
+ .. code-block:: python
+
+ # in code
+ py_trees.display.render_dot_tree(py_trees.demos.blackboard.create_root())
+ # command line tools
+ py-trees-render --with-blackboard-variables py_trees.demos.blackboard.create_root
+
+ .. graphviz:: dot/demo-blackboard.dot
+ :align: center
+ :caption: Tree with Blackboard Variables
+
+ And to demonstrate that it doesn't become a tangled nightmare at scale, an example of
+ a more complex tree:
+
+ .. graphviz:: dot/blackboard-with-variables.dot
+ :align: center
+ :caption: A more complex tree
+
+ Debug deeper with judicious application of the tree, blackboard and activity stream
+ display methods around the tree tick (refer to
+ :class:`py_trees.visitors.DisplaySnapshotVisitor` for examplar code):
+
+ .. figure:: images/blackboard_trees.png
+ :align: center
+
+ Tree level debugging
+
+ .. seealso::
+
+ * :ref:`py-trees-demo-blackboard `
+ * :ref:`py-trees-demo-namespaces `
+ * :ref:`py-trees-demo-remappings `
+ * :class:`py_trees.visitors.DisplaySnapshotVisitor`
+ * :class:`py_trees.behaviours.SetBlackboardVariable`
+ * :class:`py_trees.behaviours.UnsetBlackboardVariable`
+ * :class:`py_trees.behaviours.CheckBlackboardVariableExists`
+ * :class:`py_trees.behaviours.WaitForBlackboardVariable`
+ * :class:`py_trees.behaviours.CheckBlackboardVariableValue`
+ * :class:`py_trees.behaviours.WaitForBlackboardVariableValue`
+
+ Attributes:
+ name (str): client's convenient, but not necessarily unique identifier
+ namespace (str): apply this as a prefix to any key/variable name operations
+ unique_identifier (uuid.UUID): client's unique identifier
+ read (typing.Set[str]): set of absolute key names with read access
+ write (typing.Set[str]): set of absolute key names with write access
+ exclusive (typing.Set[str]): set of absolute key names with exclusive write access
+ required (typing.Set[str]): set of absolute key names required to have data present
+ remappings (typing.Dict[str, str]: client key names with blackboard remappings
+ namespaces (typing.Set[str]: a cached list of namespaces this client accesses
+ """
+ def __init__(
+ self, *,
+ name: str=None,
+ namespace: str=None):
+ """
+ Args:
+ name: client's convenient identifier (stringifies the uuid if None)
+ namespace: prefix to apply to key/variable name operations
+ read: list of keys for which this client has read access
+ write: list of keys for which this client has write access
+ exclusive: list of keys for which this client has exclusive write access
+
+ Raises:
+ TypeError: if the provided name is not of type str
+ ValueError: if the unique identifier has already been registered
+ """
+
+ # unique identifier
+ super().__setattr__("unique_identifier", uuid.uuid4())
+ if super().__getattribute__("unique_identifier") in Blackboard.clients.keys():
+ raise ValueError("this unique identifier has already been registered")
+
+ # name
+ if name is None or not name:
+ name = utilities.truncate(
+ original=str(super().__getattribute__("unique_identifier")).replace('-', '_'),
+ length=7
+ )
+ super().__setattr__("name", name)
+ else:
+ if not isinstance(name, str):
+ raise TypeError("provided name is not of type str [{}]".format(type(name)))
+ super().__setattr__("name", name)
+
+ # namespaces
+ namespace = "" if namespace is None else namespace
+ if not namespace.startswith(Blackboard.separator):
+ namespace = Blackboard.separator + namespace
+ super().__setattr__("namespace", namespace)
+ super().__setattr__("namespaces", set())
+
+ super().__setattr__("read", set())
+ super().__setattr__("write", set())
+ super().__setattr__("exclusive", set())
+ super().__setattr__("required", set())
+ super().__setattr__("remappings", {})
+ Blackboard.clients[
+ super().__getattribute__("unique_identifier")
+ ] = self.name
+
+ def id(self) -> uuid.UUID:
+ """
+ The unique identifier for this client.
+
+ Returns:
+ The uuid.UUID object
+ """
+ return super().__getattribute__("unique_identifier")
+
+ def __setattr__(self, name: str, value: typing.Any):
+ """
+ Convenience attribute style referencing with checking against
+ permissions.
+
+ Raises:
+ AttributeError: if the client does not have write access to the variable
+ """
+ # print("__setattr__ [{}][{}]".format(name, value))
+ name = Blackboard.absolute_name(super().__getattribute__("namespace"), name)
+ if (
+ (name not in super().__getattribute__("write")) and
+ (name not in super().__getattribute__("exclusive"))
+ ):
+ if Blackboard.activity_stream is not None:
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(name, ActivityType.ACCESS_DENIED)
+ )
+ raise AttributeError("client '{}' does not have write access to '{}'".format(self.name, name))
+ remapped_name = super().__getattribute__("remappings")[name]
+ if Blackboard.activity_stream is not None:
+ if remapped_name in Blackboard.storage.keys():
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(
+ key=remapped_name,
+ activity_type=ActivityType.WRITE,
+ previous_value=Blackboard.storage[remapped_name],
+ current_value=value
+ )
+ )
+ else:
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(
+ key=remapped_name,
+ activity_type=ActivityType.INITIALISED,
+ current_value=value
+ )
+ )
+ Blackboard.storage[remapped_name] = value
+
+ def __getattr__(self, name: str):
+ """
+ Convenience attribute style referencing with checking against
+ permissions.
+
+ Raises:
+ AttributeError: if the client does not have read access to the variable
+ KeyError: if the variable does not yet exist on the blackboard
+ """
+ # print("__getattr__ [{}]".format(name))
+ name = Blackboard.absolute_name(super().__getattribute__("namespace"), name)
+ read_key = False
+ write_key = False
+ if name in super().__getattribute__("read"):
+ read_key = True
+ elif name in super().__getattribute__("write"):
+ write_key = True
+ elif name in super().__getattribute__("exclusive"):
+ write_key = True
+ else:
+ if name in super().__getattribute__("namespaces"):
+ return IntermediateVariableFetcher(blackboard=self, namespace=name)
+ if Blackboard.activity_stream is not None:
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(name, ActivityType.ACCESS_DENIED)
+ )
+ raise AttributeError("client '{}' does not have read/write access to '{}'".format(self.name, name))
+ remapped_name = super().__getattribute__("remappings")[name]
+ try:
+ if write_key:
+ if Blackboard.activity_stream is not None:
+ if utilities.is_primitive(Blackboard.storage[remapped_name]):
+ activity_type = ActivityType.READ
+ else: # could be a nested class object being accessed to write an attribute
+ activity_type = ActivityType.ACCESSED
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(
+ key=remapped_name,
+ activity_type=activity_type,
+ current_value=Blackboard.storage[remapped_name],
+ )
+ )
+ return Blackboard.storage[remapped_name]
+ if read_key:
+ if Blackboard.activity_stream is not None:
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(
+ key=remapped_name,
+ activity_type=ActivityType.READ,
+ current_value=Blackboard.storage[remapped_name],
+ )
+ )
+ return Blackboard.storage[remapped_name]
+ except KeyError as e:
+ if Blackboard.activity_stream is not None:
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(remapped_name, ActivityType.NO_KEY)
+ )
+ raise KeyError("client '{}' tried to access '{}' but it does not yet exist on the blackboard".format(self.name, remapped_name)) from e
+
+ def set(self, name: str, value: typing.Any, overwrite: bool=True) -> bool:
+ """
+ Set, conditionally depending on whether the variable already exists or otherwise.
+
+ This is most useful when initialising variables and multiple elements
+ seek to do so. A good policy to adopt for your applications in these situations is
+ a first come, first served policy. Ensure global configuration has the first
+ opportunity followed by higher priority behaviours in the tree and so forth.
+ Lower priority behaviours would use this to respect the pre-configured
+ setting and at most, just validate that it is acceptable to the functionality
+ of it's own behaviour.
+
+ Args:
+ name: name of the variable to set
+ value: value of the variable to set
+ overwrite: do not set if the variable already exists on the blackboard
+
+ Returns:
+ success or failure (overwrite is False and variable already set)
+
+ Raises:
+ AttributeError: if the client does not have write access to the variable
+ KeyError: if the variable does not yet exist on the blackboard
+ """
+ name = Blackboard.absolute_name(super().__getattribute__("namespace"), name)
+ name_components = name.split('.')
+ key = name_components[0]
+ key_attributes = '.'.join(name_components[1:])
+ if (
+ (key not in super().__getattribute__("write")) and
+ (key not in super().__getattribute__("exclusive"))
+ ):
+ if Blackboard.activity_stream is not None:
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(key, ActivityType.ACCESS_DENIED)
+ )
+ raise AttributeError("client '{}' does not have write access to '{}'".format(self.name, name))
+ remapped_key = super().__getattribute__("remappings")[key]
+ if not overwrite:
+ if remapped_key in Blackboard.storage:
+ if Blackboard.activity_stream is not None:
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(
+ key=remapped_key,
+ activity_type=ActivityType.NO_OVERWRITE,
+ current_value=Blackboard.storage[remapped_key])
+ )
+ return False
+ if not key_attributes:
+ setattr(self, key, value)
+ return True
+ else:
+ blackboard_object = getattr(self, key)
+ try:
+ setattr(blackboard_object, key_attributes, value)
+ return True
+ except AttributeError: # when the object doesn't have the attributes
+ return False
+
+ def exists(self, name: str) -> bool:
+ """
+ Check if the specified variable exists on the blackboard.
+
+ Args:
+ name: name of the variable to get, can be nested, e.g. battery.percentage
+
+ Raises:
+ AttributeError: if the client does not have read access to the variable
+ """
+ try:
+ unused_value = self.get(name)
+ return True
+ except KeyError:
+ return False
+
+ def absolute_name(self, key: str) -> str:
+ """
+ Generate the fully qualified key name for this key.
+
+ .. code-block:: python
+
+ blackboard = Client(name="FooBar", namespace="foo")
+ blackboard.register_key(key="bar", access=py_trees.common.Access.READ)
+ print("{}".format(blackboard.absolute_name("bar"))) # "/foo/bar"
+
+ Args:
+ key: name of the key
+
+ Returns:
+ the absolute name
+
+ Raises:
+ KeyError: if the key is not registered with this client
+ """
+ if not self.is_registered(key=key):
+ raise KeyError("key '{}' is not in namespace '{}'".format(
+ key, super().__getattribute__("namespace"))
+ )
+ return Blackboard.absolute_name(
+ super().__getattribute__("namespace"),
+ key
+ )
+
+ def get(self, name: str) -> typing.Any:
+ """
+ Method based accessor to the blackboard variables (as opposed to simply using
+ '.').
+
+ Args:
+ name: name of the variable to get, can be nested, e.g. battery.percentage
+
+ Raises:
+ AttributeError: if the client does not have read access to the variable
+ KeyError: if the variable or it's nested attributes do not yet exist on the blackboard
+ """
+ # key attributes is an empty string if not a nested variable name
+ name_components = name.split('.')
+ key = name_components[0]
+ key_attributes = '.'.join(name_components[1:])
+ value = getattr(self, key) # will run through client access checks in __getattr__
+ if key_attributes:
+ try:
+ value = operator.attrgetter(key_attributes)(value)
+ except AttributeError:
+ raise KeyError("Key exists, but does not have the specified nested attributes [{}]".format(name))
+ return value
+
+ def unset(self, key: str):
+ """
+ For when you need to completely remove a blackboard variable (key-value pair),
+ this provides a convenient helper method.
+
+ Args:
+ key: name of the variable to remove
+
+ Returns:
+ True if the variable was removed, False if it was already absent
+ """
+ key = Blackboard.absolute_name(super().__getattribute__("namespace"), key)
+ remapped_key = super().__getattribute__("remappings")[key]
+ if Blackboard.activity_stream is not None:
+ Blackboard.activity_stream.push(
+ self._generate_activity_item(remapped_key, ActivityType.UNSET)
+ )
+ # Three means of handling a non-existent key - 1) raising a KeyError, 2) catching
+ # the KeyError and passing, 3) catch the KeyError and return True/False.
+ # Option 1) is inconvenient - requires a redundant try/catch 99% of cases
+ # Option 2) hides information - bad
+ # Option 3) no extra code necessary and information is there if desired
+ try:
+ del Blackboard.storage[remapped_key]
+ return True
+ except KeyError:
+ return False
+
+ def _generate_activity_item(self, key, activity_type, previous_value=None, current_value=None):
+ return ActivityItem(
+ key=key,
+ client_name=super().__getattribute__("name"),
+ client_id=super().__getattribute__("unique_identifier"),
+ # use strings here, so displaying the streams is agnostic of the enum
+ activity_type=activity_type.value,
+ previous_value=previous_value,
+ current_value=current_value
+ )
+
+ def _update_namespaces(self, added_key=None):
+ """
+ Update the namespace cache.
+
+ Args:
+ added_key: hint on the most recent operation to enable an smart check/rebuild
+ """
+ if added_key is not None:
+ namespace = added_key.rsplit("/", 1)[0]
+ while namespace:
+ super().__getattribute__("namespaces").add(namespace)
+ namespace = namespace.rsplit("/", 1)[0]
+ else:
+ # completely rebuild
+ super().__getattribute__("namespaces").clear()
+ for key in itertools.chain(
+ super().__getattribute__("read"),
+ super().__getattribute__("write"),
+ super().__getattribute__("exclusive")
+ ):
+ namespace = key.rsplit("/", 1)[0]
+ while namespace:
+ super().__getattribute__("namespaces").add(namespace)
+ namespace = namespace.rsplit("/", 1)[0]
+
+ def __str__(self):
+ indent = " "
+ s = console.green + "Blackboard Client" + console.reset + "\n"
+ s += console.white + indent + "Client Data" + console.reset + "\n"
+ keys = ["name", "namespace", "unique_identifier", "read", "write", "exclusive"]
+ s += self._stringify_key_value_pairs(keys, self.__dict__, 2 * indent)
+ keys = {k for k, v in self.remappings.items() if k != v}
+ if keys:
+ s += console.white + indent + "Remappings" + console.reset + "\n"
+ s += self._stringify_key_value_pairs(
+ keys=keys,
+ key_value_dict=self.remappings,
+ indent=2 * indent,
+ separator=console.right_arrow
+ )
+ s += console.white + indent + "Variables" + console.reset + "\n"
+ keys = self.remappings.values()
+ s += self._stringify_key_value_pairs(keys, Blackboard.storage, 2 * indent)
+ return s
+
+ def _stringify_key_value_pairs(self, keys, key_value_dict, indent, separator=":"):
+ s = ""
+ max_length = 0
+ for key in keys:
+ max_length = len(key) if len(key) > max_length else max_length
+ for key in keys:
+ try:
+ value = key_value_dict[key]
+ lines = ('{0}'.format(value)).split('\n')
+ if len(lines) > 1:
+ s += console.cyan + indent + '{0: <{1}}'.format(key, max_length + 1) + console.reset + separator + "\n"
+ for line in lines:
+ s += console.yellow + indent + " {0}\n".format(line) + console.reset
+ else:
+ s += console.cyan + indent + '{0: <{1}}'.format(key, max_length + 1) + console.reset + separator + " " + console.yellow + '{0}\n'.format(value) + console.reset
+ except KeyError:
+ s += console.cyan + indent + '{0: <{1}}'.format(key, max_length + 1) + console.reset + separator + " " + console.yellow + "-\n" + console.reset
+ s += console.reset
+ return s
+
+ def unregister(self, clear: bool=True):
+ """
+ Unregister this blackboard client and if requested, clear key-value pairs if this
+ client is the last user of those variables.
+
+ Args:
+ clear: remove key-values pairs from the blackboard
+ """
+ self.unregister_all_keys(clear)
+ del Blackboard.clients[super().__getattribute__("unique_identifier")]
+
+ def unregister_all_keys(self, clear: bool=True):
+ """
+ Unregister all keys currently registered by this blackboard client and if requested,
+ clear key-value pairs if this client is the last user of those variables.
+
+ Args:
+ clear: remove key-values pairs from the blackboard
+ """
+ for key in itertools.chain(set(self.read), set(self.write), set(self.exclusive)):
+ self.unregister_key(key=key, clear=clear, update_namespace_cache=False)
+ self._update_namespaces()
+
+ def verify_required_keys_exist(self):
+ """
+ En-masse check of existence on the blackboard for all keys that were hitherto
+ registered as 'required'.
+
+ Raises: KeyError if any of the required keys do not exist on the blackboard
+ """
+ absent = set()
+ for key in super().__getattribute__("required"):
+ if not self.exists(key):
+ absent.add(key)
+ if absent:
+ raise KeyError("keys required, but not yet on the blackboard [{}]".format(absent))
+
+ def is_registered(
+ self,
+ key: str,
+ access: typing.Union[None, common.Access]=None
+ ) -> bool:
+ """
+ Check to see if the specified key is registered.
+
+ Args:
+ key: in either relative or absolute form
+ access: access property, if None, just checks for registration, regardless of property
+
+ Returns:
+ if registered, True otherwise False
+ """
+ absolute_name = Blackboard.absolute_name(
+ super().__getattribute__("namespace"),
+ key
+ )
+ if access == common.Access.READ:
+ return absolute_name in self.read
+ elif access == common.Access.WRITE:
+ return absolute_name in self.write
+ elif access == common.Access.EXCLUSIVE_WRITE:
+ return absolute_name in self.exclusive
+ else:
+ return absolute_name in self.read | self.write | self.exclusive
+
+ def register_key(
+ self,
+ key: str,
+ access: common.Access,
+ required: bool=False,
+ remap_to: str=None,
+ ):
+ """
+ Register a key on the blackboard to associate with this client.
+
+ Args:
+ key: key to register
+ access: access level (read, write, exclusive write)
+ required: if true, check key exists when calling
+ :meth:`~verify_required_keys_exist`
+ remap_to: remap the key to this location on the blackboard
+
+ Note the remap simply changes the storage location. From the perspective of
+ the client, access via the specified 'key' remains the same.
+
+ Raises:
+ AttributeError if exclusive write access is requested, but write access has already been given to another client
+ TypeError if the access argument is of incorrect type
+ """
+ key = Blackboard.absolute_name(super().__getattribute__("namespace"), key)
+ super().__getattribute__("remappings")[key] = key if remap_to is None else remap_to
+ remapped_key = super().__getattribute__("remappings")[key]
+ if access == common.Access.READ:
+ super().__getattribute__("read").add(key)
+ Blackboard.metadata.setdefault(remapped_key, KeyMetaData())
+ Blackboard.metadata[remapped_key].read.add(super().__getattribute__("unique_identifier"))
+ elif access == common.Access.WRITE:
+ conflicts = set()
+ try:
+ for unique_identifier in Blackboard.metadata[remapped_key].exclusive:
+ conflicts.add(Blackboard.clients[unique_identifier])
+ if conflicts:
+ raise AttributeError("'{}' requested write on key '{}', but this key already associated with an exclusive writer[{}]".format(
+ super().__getattribute__("name"),
+ remapped_key,
+ conflicts)
+ )
+ except KeyError:
+ pass # no readers or writers on the key yet
+ super().__getattribute__("write").add(key)
+ Blackboard.metadata.setdefault(remapped_key, KeyMetaData())
+ Blackboard.metadata[remapped_key].write.add(super().__getattribute__("unique_identifier"))
+ elif access == common.Access.EXCLUSIVE_WRITE:
+ try:
+ key_metadata = Blackboard.metadata[remapped_key]
+ conflicts = set()
+ for unique_identifier in (key_metadata.write | key_metadata.exclusive):
+ conflicts.add(Blackboard.clients[unique_identifier])
+ if conflicts:
+ raise AttributeError("'{}' requested exclusive write on key '{}', but this key is already associated [{}]".format(
+ super().__getattribute__("name"),
+ remapped_key,
+ conflicts)
+ )
+ except KeyError:
+ pass # no readers or writers on the key yet
+ super().__getattribute__("exclusive").add(key)
+ Blackboard.metadata.setdefault(remapped_key, KeyMetaData())
+ Blackboard.metadata[remapped_key].exclusive.add(super().__getattribute__("unique_identifier"))
+ else:
+ raise TypeError("access argument is of incorrect type [{}]".format(type(access)))
+ if required:
+ super().__getattribute__("required").add(key)
+ self._update_namespaces(added_key=key)
+
+ def unregister_key(
+ self,
+ key: str,
+ clear: bool=True,
+ update_namespace_cache: bool=True):
+ """
+ Unegister a key associated with this client.
+
+ Args:
+ key: key to unregister
+ clear: remove key-values pairs from the blackboard
+ update_namespace_cache: disable if you are batching
+
+ A method that batches calls to this method is :meth:`unregister_all_keys()`.
+
+ Raises:
+ KeyError if the key has not been previously registered
+ """
+ key = Blackboard.absolute_name(super().__getattribute__("namespace"), key)
+ remapped_key = super().__getattribute__("remappings")[key]
+ super().__getattribute__("read").discard(key) # doesn't throw exceptions if it not present
+ super().__getattribute__("write").discard(key)
+ super().__getattribute__("exclusive").discard(key)
+ Blackboard.metadata[remapped_key].read.discard(super().__getattribute__("unique_identifier"))
+ Blackboard.metadata[remapped_key].write.discard(super().__getattribute__("unique_identifier"))
+ Blackboard.metadata[remapped_key].exclusive.discard(super().__getattribute__("unique_identifier"))
+ if (
+ (not Blackboard.metadata[remapped_key].read) and
+ (not Blackboard.metadata[remapped_key].write) and
+ (not Blackboard.metadata[remapped_key].exclusive)
+ ):
+ del Blackboard.metadata[remapped_key]
+ if clear:
+ try:
+ del Blackboard.storage[remapped_key]
+ except KeyError:
+ pass # perfectly legitimate for a registered key to not exist on the blackboard
+ del super().__getattribute__("remappings")[key]
+ if update_namespace_cache:
+ self._update_namespaces()
+
+
+class IntermediateVariableFetcher(object):
+ def __init__(self, blackboard, namespace):
+ super().__setattr__("blackboard", blackboard)
+ super().__setattr__("namespace", namespace)
+
+ def __getattr__(self, name: str):
+ # print("Fetcher:__getattr__ [{}]".format(name))
+ name = Blackboard.absolute_name(self.namespace, name)
+ return self.blackboard.get(name)
+
+ def __setattr__(self, name: str, value: typing.Any):
+ # print("Fetcher:__setattr__ [{}][{}]".format(name, value))
+ name = Blackboard.absolute_name(self.namespace, name)
+ return self.blackboard.set(name, value)
diff --git a/thirdparty/py_trees/blackboard_demo.dot b/thirdparty/py_trees/blackboard_demo.dot
new file mode 100644
index 0000000..a3acd5f
--- /dev/null
+++ b/thirdparty/py_trees/blackboard_demo.dot
@@ -0,0 +1,15 @@
+digraph pastafarianism {
+ordering=out;
+graph [fontname="times-roman"];
+node [fontname="times-roman"];
+edge [fontname="times-roman"];
+"Blackboard Demo" [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Blackboard Demo", shape=box, style=filled];
+"Set Nested" [fillcolor=gray, fontcolor=black, fontsize=9, label="Set Nested", shape=ellipse, style=filled];
+"Blackboard Demo" -> "Set Nested";
+Writer [fillcolor=gray, fontcolor=black, fontsize=9, label=Writer, shape=ellipse, style=filled];
+"Blackboard Demo" -> Writer;
+"Check Nested Foo" [fillcolor=gray, fontcolor=black, fontsize=9, label="Check Nested Foo", shape=ellipse, style=filled];
+"Blackboard Demo" -> "Check Nested Foo";
+ParamsAndState [fillcolor=gray, fontcolor=black, fontsize=9, label=ParamsAndState, shape=ellipse, style=filled];
+"Blackboard Demo" -> ParamsAndState;
+}
diff --git a/thirdparty/py_trees/common.py b/thirdparty/py_trees/common.py
new file mode 100644
index 0000000..645e57e
--- /dev/null
+++ b/thirdparty/py_trees/common.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+Common definitions, methods and variables used by the py_trees library.
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import enum
+import math
+import typing
+
+##############################################################################
+# General
+##############################################################################
+
+
+class Name(enum.Enum):
+ """
+ Naming conventions.
+ """
+ AUTO_GENERATED = "AUTO_GENERATED"
+ """:py:data:`~py_trees.common.Name.AUTO_GENERATED` leaves it to the behaviour to generate a useful, informative name."""
+
+
+class Status(enum.Enum):
+ """An enumerator representing the status of a behaviour """
+
+ SUCCESS = "SUCCESS"
+ """Behaviour check has passed, or execution of its action has finished with a successful result."""
+ FAILURE = "FAILURE"
+ """Behaviour check has failed, or execution of its action finished with a failed result."""
+ RUNNING = "RUNNING"
+ """Behaviour is in the middle of executing some action, result still pending."""
+ INVALID = "INVALID"
+ """Behaviour is uninitialised and inactive, i.e. this is the status before first entry, and after a higher priority switch has occurred."""
+
+
+class Duration(enum.Enum):
+ """
+ Naming conventions.
+ """
+ INFINITE = math.inf
+ """:py:data:`~py_trees.common.Duration.INFINITE` oft used for perpetually blocking operations."""
+ UNTIL_THE_BATTLE_OF_ALFREDO = math.inf
+ """:py:data:`~py_trees.common.Duration.UNTIL_THE_BATTLE_OF_ALFREDO` is an alias for :py:data:`~py_trees.common.Duration.INFINITE`."""
+
+
+class Access(enum.Enum):
+ """
+ Use to distinguish types of access to, e.g. variables on a blackboard.
+ """
+
+ READ = "READ"
+ """Read access."""
+ WRITE = "WRITE"
+ """Write access, implicitly also grants read access."""
+ EXCLUSIVE_WRITE = "EXCLUSIVE_WRITE"
+ """Exclusive lock for writing, i.e. no other writer permitted."""
+
+
+##############################################################################
+# Policies
+##############################################################################
+
+class ParallelPolicy(object):
+ """
+ Configurable policies for :py:class:`~py_trees.composites.Parallel` behaviours.
+ """
+ class Base(object):
+ """
+ Base class for parallel policies. Should never be used directly.
+ """
+ def __init__(self, synchronise=False):
+ """
+ Default policy configuration.
+
+ Args:
+ synchronise (:obj:`bool`): stop ticking of children with status :py:data:`~py_trees.common.Status.SUCCESS` until the policy criteria is met
+ """
+ self.synchronise = synchronise
+
+ class SuccessOnAll(Base):
+ """
+ Return :py:data:`~py_trees.common.Status.SUCCESS` only when each and every child returns
+ :py:data:`~py_trees.common.Status.SUCCESS`. If synchronisation is requested, any children that
+ tick with :data:`~py_trees.common.Status.SUCCESS` will be skipped on subsequent ticks until
+ the policy criteria is met, or one of the children returns status :data:`~py_trees.common.Status.FAILURE`.
+ """
+ def __init__(self, synchronise=True):
+ """
+ Policy configuration.
+
+ Args:
+ synchronise (:obj:`bool`): stop ticking of children with status :py:data:`~py_trees.common.Status.SUCCESS` until the policy criteria is met
+ """
+ super().__init__(synchronise=synchronise)
+
+ class SuccessOnOne(Base):
+ """
+ Return :py:data:`~py_trees.common.Status.SUCCESS` so long as at least one child has :py:data:`~py_trees.common.Status.SUCCESS`
+ and the remainder are :py:data:`~py_trees.common.Status.RUNNING`
+ """
+ def __init__(self):
+ """
+ No configuration necessary for this policy.
+ """
+ super().__init__(synchronise=False)
+
+ class SuccessOnSelected(Base):
+ """
+ Return :py:data:`~py_trees.common.Status.SUCCESS` so long as each child in a specified list returns
+ :py:data:`~py_trees.common.Status.SUCCESS`. If synchronisation is requested, any children that
+ tick with :data:`~py_trees.common.Status.SUCCESS` will be skipped on subsequent ticks until
+ the policy criteria is met, or one of the children returns status :data:`~py_trees.common.Status.FAILURE`.
+ """
+ def __init__(self, children, synchronise=True):
+ """
+ Policy configuraiton.
+
+ Args:
+ children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to succeed on
+ synchronise (:obj:`bool`): stop ticking of children with status :py:data:`~py_trees.common.Status.SUCCESS` until the policy criteria is met
+ """
+ super().__init__(synchronise=synchronise)
+ self.children = children
+
+
+class OneShotPolicy(enum.Enum):
+ """Policy rules for :py:class:`~py_trees.decorators.OneShot` (decorator) or :py:meth:`~py_trees.idioms.oneshot (idiom) oneshots."""
+
+ ON_COMPLETION = [Status.SUCCESS, Status.FAILURE]
+ """Return :py:data:`~py_trees.common.Status.SUCCESS` after the specified child/subtree reaches completion (:py:data:`~py_trees.common.Status.SUCCESS` || :py:data:`~py_trees.common.Status.FAILURE`)."""
+ ON_SUCCESSFUL_COMPLETION = [Status.SUCCESS]
+ """Permits the oneshot to keep trying until it's first success."""
+
+
+class ClearingPolicy(enum.IntEnum):
+ """
+ Policy rules for behaviours to dictate when data should be cleared/reset.
+ """
+ ON_INITIALISE = 1
+ """Clear when entering the :py:meth:`~py_trees.behaviour.Behaviour.initialise` method."""
+ ON_SUCCESS = 2
+ """Clear when returning :py:data:`~py_trees.common.Status.SUCCESS`."""
+ NEVER = 3
+ """Never clear the data"""
+
+##############################################################################
+# Blackboards
+##############################################################################
+
+
+class ComparisonExpression(object):
+ """
+ Store the parameters for a univariate comparison operation
+ (i.e. between a variable and a value).
+
+ Args:
+ variable: name of the variable to compare
+ value: value to compare against
+ operator: a callable comparison operator
+
+ .. tip::
+ The python `operator module`_ includes many useful comparison operations, e.g. operator.ne
+ """
+ def __init__(
+ self,
+ variable: str,
+ value: typing.Any,
+ operator: typing.Callable[[typing.Any, typing.Any], bool]
+ ):
+ self.variable = variable
+ self.value = value
+ self.operator = operator
+
+
+##############################################################################
+# BlackBoxes
+##############################################################################
+
+class BlackBoxLevel(enum.IntEnum):
+ """
+ Whether a behaviour is a blackbox entity that may be considered collapsible
+ (i.e. everything in its subtree will not be visualised) by
+ visualisation tools.
+
+ Blackbox levels are increasingly persistent in visualisations.
+
+ Visualisations by default, should always collapse blackboxes that represent
+ `DETAIL`.
+ """
+ DETAIL = 1
+ """A blackbox that encapsulates detailed activity."""
+ COMPONENT = 2
+ """A blackbox that encapsulates a subgroup of functionalities as a single group."""
+ BIG_PICTURE = 3
+ """A blackbox that represents a big picture part of the entire tree view."""
+ NOT_A_BLACKBOX = 4
+ """Not a blackbox, do not ever collapse."""
+
+
+class VisibilityLevel(enum.IntEnum):
+ """
+ Closely associated with the :py:class:`~py_trees.common.BlackBoxLevel` for a
+ behaviour. This sets the visibility level to be used for visualisations.
+
+ Visibility levels correspond to reducing levels of visibility in a visualisation.
+ """
+ ALL = 0
+ """Do not collapse any behaviour."""
+ DETAIL = BlackBoxLevel.DETAIL
+ """Collapse blackboxes marked with :py:data:`~py_trees.common.BlackBoxLevel.DETAIL` or lower."""
+ COMPONENT = BlackBoxLevel.COMPONENT
+ """Collapse blackboxes marked with :py:data:`~py_trees.common.BlackBoxLevel.COMPONENT` or lower."""
+ BIG_PICTURE = BlackBoxLevel.BIG_PICTURE
+ """Collapse any blackbox that isn't marked with :py:data:`~py_trees.common.BlackBoxLevel.BIG_PICTURE`."""
+
+
+visibility_level_strings = ["all", "detail", "component", "big_picture"]
+"""Convenient string representations to use for command line input (amongst other things)."""
+
+
+def string_to_visibility_level(level):
+ """
+ Will convert a string to a visibility level. Note that it will quietly return ALL if
+ the string is not matched to any visibility level string identifier.
+
+ Args:
+ level (str): visibility level as a string
+
+ Returns:
+ :class:`~py_trees.common.VisibilityLevel`: visibility level enum
+ """
+ if level == "detail":
+ return VisibilityLevel.DETAIL
+ elif level == "component":
+ return VisibilityLevel.COMPONENT
+ elif level == "big_picture":
+ return VisibilityLevel.BIG_PICTURE
+ else:
+ return VisibilityLevel.ALL
diff --git a/thirdparty/py_trees/composites.py b/thirdparty/py_trees/composites.py
new file mode 100644
index 0000000..5c096e5
--- /dev/null
+++ b/thirdparty/py_trees/composites.py
@@ -0,0 +1,654 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+Composites are responsible for directing the path traced through
+the tree on a given tick (execution). They are the **factories**
+(Sequences and Parallels) and **decision makers** (Selectors) of a behaviour
+tree.
+
+.. graphviz:: dot/composites.dot
+ :align: center
+ :caption: PyTree Composites
+
+Composite behaviours typically manage children and apply some logic to the way
+they execute and return a result, but generally don't do anything themselves.
+Perform the checks or actions you need to do in the non-composite behaviours.
+
+Most any desired functionality can be authored with a combination of these
+three composites. In fact, it is precisely this feature that makes behaviour
+trees attractive - it breaks down complex decision making logic to just three
+primitive elements. It is possible and often desirable to extend this set with
+custom composites of your own, but think carefully before you do - in almost
+every case, a combination of the existing composites will serve and as a
+result, you will merely compound the complexity inherent in your tree logic.
+This this makes it confoundingly difficult to design, introspect and debug. As
+an example, design sessions often revolve around a sketched graph on a
+whiteboard. When these graphs are composed of just five elements (Selectors,
+Sequences, Parallels, Decorators and Behaviours), it is very easy to understand
+the logic at a glance. Double the number of fundamental elements and you may as
+well be back at the terminal parsing code.
+
+.. tip:: You should never need to subclass or create new composites.
+
+The basic operational modes of the three composites in this library are as follows:
+
+* :class:`~py_trees.composites.Selector`: select a child to execute based on cascading priorities
+* :class:`~py_trees.composites.Sequence`: execute children sequentially
+* :class:`~py_trees.composites.Parallel`: execute children concurrently
+
+This library does provide some flexibility in *how* each composite is implemented without
+breaking the fundamental nature of each (as described above). Selectors and Sequences can
+be configured with or without memory (resumes or resets if children are RUNNING) and
+the results of a parallel can be configured to wait upon all children completing, succeed
+on one, all or a subset thereof.
+
+.. tip:: Follow the links in each composite's documentation to the relevant demo programs.
+
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import itertools
+import typing
+
+from . import behaviour
+from . import common
+
+##############################################################################
+# Composites
+##############################################################################
+
+
+class Composite(behaviour.Behaviour):
+ """
+ The parent class to all composite behaviours, i.e. those that
+ have children.
+
+ Args:
+ name (:obj:`str`): the composite behaviour name
+ children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add
+ """
+ def __init__(self,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED,
+ children: typing.List[behaviour.Behaviour]=None
+ ):
+ super(Composite, self).__init__(name)
+ if children is not None:
+ for child in children:
+ self.add_child(child)
+ else:
+ self.children = []
+ self.current_child = None
+
+ ############################################
+ # Worker Overrides
+ ############################################
+
+ def stop(self, new_status=common.Status.INVALID):
+ """
+ There is generally two use cases that must be supported here.
+
+ 1) Whenever the composite has gone to a recognised state (i.e. :data:`~py_trees.common.Status.FAILURE` or SUCCESS),
+ or 2) when a higher level parent calls on it to truly stop (INVALID).
+
+ In only the latter case will children need to be forcibly stopped as well. In the first case, they will
+ have stopped themselves appropriately already.
+
+ Args:
+ new_status (:class:`~py_trees.common.Status`): behaviour will transition to this new status
+ """
+ self.logger.debug("%s.stop()[%s]" % (self.__class__.__name__, "%s->%s" % (self.status, new_status) if self.status != new_status else "%s" % new_status))
+ # priority interrupted
+ if new_status == common.Status.INVALID:
+ self.current_child = None
+ for child in self.children:
+ child.stop(new_status)
+ # This part just replicates the Behaviour.stop function. We replicate it here so that
+ # the Behaviour logging doesn't duplicate the composite logging here, just a bit cleaner this way.
+ self.terminate(new_status)
+ self.status = new_status
+ self.iterator = self.tick()
+
+ def tip(self):
+ """
+ Recursive function to extract the last running node of the tree.
+
+ Returns:
+ :class::`~py_trees.behaviour.Behaviour`: the tip function of the current child of this composite or None
+ """
+ if self.current_child is not None:
+ return self.current_child.tip()
+ else:
+ return super().tip()
+
+ ############################################
+ # Children
+ ############################################
+
+ def add_child(self, child):
+ """
+ Adds a child.
+
+ Args:
+ child (:class:`~py_trees.behaviour.Behaviour`): child to add
+
+ Raises:
+ TypeError: if the child is not an instance of :class:`~py_trees.behaviour.Behaviour`
+ RuntimeError: if the child already has a parent
+
+ Returns:
+ uuid.UUID: unique id of the child
+ """
+ if not isinstance(child, behaviour.Behaviour):
+ raise TypeError("children must be behaviours, but you passed in {}".format(type(child)))
+ self.children.append(child)
+ if child.parent is not None:
+ raise RuntimeError("behaviour '{}' already has parent '{}'".format(child.name, child.parent.name))
+ child.parent = self
+ return child.id
+
+ def add_children(self, children):
+ """
+ Append a list of children to the current list.
+
+ Args:
+ children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add
+ """
+ for child in children:
+ self.add_child(child)
+ return self
+
+ def remove_child(self, child):
+ """
+ Remove the child behaviour from this composite.
+
+ Args:
+ child (:class:`~py_trees.behaviour.Behaviour`): child to delete
+
+ Returns:
+ :obj:`int`: index of the child that was removed
+
+ .. todo:: Error handling for when child is not in this list
+ """
+ if self.current_child is not None and (self.current_child.id == child.id):
+ self.current_child = None
+ if child.status == common.Status.RUNNING:
+ child.stop(common.Status.INVALID)
+ child_index = self.children.index(child)
+ self.children.remove(child)
+ child.parent = None
+ return child_index
+
+ def remove_all_children(self):
+ """
+ Remove all children. Makes sure to stop each child if necessary.
+ """
+ self.current_child = None
+ for child in self.children:
+ if child.status == common.Status.RUNNING:
+ child.stop(common.Status.INVALID)
+ child.parent = None
+ # makes sure to delete it for this class and all references to it
+ # http://stackoverflow.com/questions/850795/clearing-python-lists
+ del self.children[:]
+
+ def replace_child(self, child, replacement):
+ """
+ Replace the child behaviour with another.
+
+ Args:
+ child (:class:`~py_trees.behaviour.Behaviour`): child to delete
+ replacement (:class:`~py_trees.behaviour.Behaviour`): child to insert
+ """
+ self.logger.debug("%s.replace_child()[%s->%s]" % (self.__class__.__name__, child.name, replacement.name))
+ child_index = self.children.index(child)
+ self.remove_child(child)
+ self.insert_child(replacement, child_index)
+ child.parent = None
+
+ def remove_child_by_id(self, child_id):
+ """
+ Remove the child with the specified id.
+
+ Args:
+ child_id (uuid.UUID): unique id of the child
+
+ Raises:
+ IndexError: if the child was not found
+ """
+ child = next((c for c in self.children if c.id == child_id), None)
+ if child is not None:
+ self.remove_child(child)
+ else:
+ raise IndexError('child was not found with the specified id [%s]' % child_id)
+
+ def prepend_child(self, child):
+ """
+ Prepend the child before all other children.
+
+ Args:
+ child (:class:`~py_trees.behaviour.Behaviour`): child to insert
+
+ Returns:
+ uuid.UUID: unique id of the child
+ """
+ self.children.insert(0, child)
+ child.parent = self
+ return child.id
+
+ def insert_child(self, child, index):
+ """
+ Insert child at the specified index. This simply directly calls
+ the python list's :obj:`insert` method using the child and index arguments.
+
+ Args:
+ child (:class:`~py_trees.behaviour.Behaviour`): child to insert
+ index (:obj:`int`): index to insert it at
+
+ Returns:
+ uuid.UUID: unique id of the child
+ """
+ self.children.insert(index, child)
+ child.parent = self
+ return child.id
+
+##############################################################################
+# Selector
+##############################################################################
+
+
+class Selector(Composite):
+ """
+ Selectors are the decision makers.
+
+ .. graphviz:: dot/selector.dot
+
+ A selector executes each of its child behaviours in turn until one of them
+ succeeds (at which point it itself returns :data:`~py_trees.common.Status.RUNNING` or :data:`~py_trees.common.Status.SUCCESS`,
+ or it runs out of children at which point it itself returns :data:`~py_trees.common.Status.FAILURE`.
+ We usually refer to selecting children as a means of *choosing between priorities*.
+ Each child and its subtree represent a decreasingly lower priority path.
+
+ .. note::
+
+ Switching from a low -> high priority branch causes a `stop(INVALID)` signal to be sent to the previously
+ executing low priority branch. This signal will percolate down that child's own subtree. Behaviours
+ should make sure that they catch this and *destruct* appropriately.
+
+ .. seealso:: The :ref:`py-trees-demo-selector-program` program demos higher priority switching under a selector.
+
+ Args:
+ name (:obj:`str`): the composite behaviour name
+ memory (:obj:`bool`): if :data:`~py_trees.common.Status.RUNNING` on the previous tick, resume with the :data:`~py_trees.common.Status.RUNNING` child
+ children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add
+ """
+
+ def __init__(self, name="Selector", memory=False, children=None):
+ super(Selector, self).__init__(name, children)
+ self.memory = memory
+
+ def tick(self):
+ """
+ Run the tick behaviour for this selector. Note that the status
+ of the tick is always determined by its children, not
+ by the user customised update function.
+
+ Yields:
+ :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children
+ """
+ self.logger.debug("%s.tick()" % self.__class__.__name__)
+ # initialise
+ if self.status != common.Status.RUNNING:
+ # selector specific initialisation - leave initialise() free for users to
+ # re-implement without having to make calls to super()
+ self.logger.debug("%s.tick() [!RUNNING->reset current_child]" % self.__class__.__name__)
+ self.current_child = self.children[0] if self.children else None
+
+ # reset the children - don't need to worry since they will be handled
+ # a) prior to a remembered starting point, or
+ # b) invalidated by a higher level priority
+
+ # user specific initialisation
+ self.initialise()
+
+ # customised work
+ self.update()
+
+ # nothing to do
+ if not self.children:
+ self.current_child = None
+ self.stop(common.Status.FAILURE)
+ yield self
+ return
+
+ # starting point
+ if self.memory:
+ index = self.children.index(self.current_child)
+ # clear out preceding status' - not actually necessary but helps
+ # visualise the case of memory vs no memory
+ for child in itertools.islice(self.children, None, index):
+ child.stop(common.Status.INVALID)
+ else:
+ index = 0
+
+ # actual work
+ previous = self.current_child
+ for child in itertools.islice(self.children, index, None):
+ for node in child.tick():
+ yield node
+ if node is child:
+ if node.status == common.Status.RUNNING or node.status == common.Status.SUCCESS:
+ self.current_child = child
+ self.status = node.status
+ if previous is None or previous != self.current_child:
+ # we interrupted, invalidate everything at a lower priority
+ passed = False
+ for child in self.children:
+ if passed:
+ if child.status != common.Status.INVALID:
+ child.stop(common.Status.INVALID)
+ passed = True if child == self.current_child else passed
+ yield self
+ return
+ # all children failed, set failure ourselves and current child to the last bugger who failed us
+ self.status = common.Status.FAILURE
+ try:
+ self.current_child = self.children[-1]
+ except IndexError:
+ self.current_child = None
+ yield self
+
+ def stop(self, new_status=common.Status.INVALID):
+ """
+ Stopping a selector requires setting the current child to none. Note that it
+ is important to implement this here instead of terminate, so users are free
+ to subclass this easily with their own terminate and not have to remember
+ that they need to call this function manually.
+
+ Args:
+ new_status (:class:`~py_trees.common.Status`): the composite is transitioning to this new status
+ """
+ # retain information about the last running child if the new status is
+ # SUCCESS or FAILURE
+ if new_status == common.Status.INVALID:
+ self.current_child = None
+ Composite.stop(self, new_status)
+
+##############################################################################
+# Sequence
+##############################################################################
+
+
+class Sequence(Composite):
+ """
+ Sequences are the factory lines of Behaviour Trees
+
+ .. graphviz:: dot/sequence.dot
+
+ A sequence will progressively tick over each of its children so long as
+ each child returns :data:`~py_trees.common.Status.SUCCESS`. If any child returns
+ :data:`~py_trees.common.Status.FAILURE` or :data:`~py_trees.common.Status.RUNNING` the sequence will halt and the parent will adopt
+ the result of this child. If it reaches the last child, it returns with
+ that result regardless.
+
+ .. note::
+
+ The sequence halts once it sees a child is RUNNING and then returns
+ the result. *It does not get stuck in the running behaviour*.
+
+ .. seealso:: The :ref:`py-trees-demo-sequence-program` program demos a simple sequence in action.
+
+ Args:
+ name: the composite behaviour name
+ memory: if :data:`~py_trees.common.Status.RUNNING` on the previous tick, resume with the :data:`~py_trees.common.Status.RUNNING` child
+ children: list of children to add
+
+ """
+ def __init__(
+ self,
+ name: str="Sequence",
+ memory: bool=True,
+ children: typing.List[behaviour.Behaviour]=None
+ ):
+ super(Sequence, self).__init__(name, children)
+ self.memory = memory
+
+ def tick(self):
+ """
+ Tick over the children.
+
+ Yields:
+ :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children
+ """
+ self.logger.debug("%s.tick()" % self.__class__.__name__)
+
+ # initialise
+ index = 0
+ if self.status != common.Status.RUNNING or not self.memory:
+ self.current_child = self.children[0] if self.children else None
+ for child in self.children:
+ if child.status != common.Status.INVALID:
+ child.stop(common.Status.INVALID)
+ # user specific initialisation
+ self.initialise()
+ else: # self.memory is True and status is RUNNING
+ index = self.children.index(self.current_child)
+
+ # customised work
+ self.update()
+
+ # nothing to do
+ if not self.children:
+ self.current_child = None
+ self.stop(common.Status.SUCCESS)
+ yield self
+ return
+
+ # actual work
+ for child in itertools.islice(self.children, index, None):
+ for node in child.tick():
+ yield node
+ if node is child and node.status != common.Status.SUCCESS:
+ self.status = node.status
+ yield self
+ return
+ try:
+ # advance if there is 'next' sibling
+ self.current_child = self.children[index + 1]
+ index += 1
+ except IndexError:
+ pass
+
+ self.stop(common.Status.SUCCESS)
+ yield self
+
+
+##############################################################################
+# Parallel
+##############################################################################
+
+
+class Parallel(Composite):
+ """
+ Parallels enable a kind of concurrency
+
+ .. graphviz:: dot/parallel.dot
+
+ Ticks every child every time the parallel is run (a poor man's form of parallelism).
+
+ * Parallels will return :data:`~py_trees.common.Status.FAILURE` if any
+ child returns :py:data:`~py_trees.common.Status.FAILURE`
+ * Parallels with policy :class:`~py_trees.common.ParallelPolicy.SuccessOnAll`
+ only returns :py:data:`~py_trees.common.Status.SUCCESS` if **all** children
+ return :py:data:`~py_trees.common.Status.SUCCESS`
+ * Parallels with policy :class:`~py_trees.common.ParallelPolicy.SuccessOnOne`
+ return :py:data:`~py_trees.common.Status.SUCCESS` if **at least one** child
+ returns :py:data:`~py_trees.common.Status.SUCCESS` and others are
+ :py:data:`~py_trees.common.Status.RUNNING`
+ * Parallels with policy :class:`~py_trees.common.ParallelPolicy.SuccessOnSelected`
+ only returns :py:data:`~py_trees.common.Status.SUCCESS` if a **specified subset**
+ of children return :py:data:`~py_trees.common.Status.SUCCESS`
+
+ Policies :class:`~py_trees.common.ParallelPolicy.SuccessOnAll` and
+ :class:`~py_trees.common.ParallelPolicy.SuccessOnSelected` may be configured to be
+ *synchronised* in which case children that tick with
+ :data:`~py_trees.common.Status.SUCCESS` will be skipped on subsequent ticks until
+ the policy criteria is met, or one of the children returns
+ status :data:`~py_trees.common.Status.FAILURE`.
+
+ Parallels with policy :class:`~py_trees.common.ParallelPolicy.SuccessOnSelected` will
+ check in both the :meth:`~py_trees.behaviour.Behaviour.setup` and
+ :meth:`~py_trees.behaviour.Behaviour.tick` methods to to verify the
+ selected set of children is actually a subset of the children of this parallel.
+
+ .. seealso::
+ * :ref:`Context Switching Demo `
+ """
+ def __init__(self,
+ name: typing.Union[str, common.Name]=common.Name.AUTO_GENERATED,
+ policy: common.ParallelPolicy.Base=common.ParallelPolicy.SuccessOnAll(),
+ children: typing.List[behaviour.Behaviour]=None
+ ):
+ """
+ Args:
+ name (:obj:`str`): the composite behaviour name
+ policy (:class:`~py_trees.common.ParallelPolicy`): policy to use for deciding success or otherwise
+ children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add
+ """
+ super(Parallel, self).__init__(name, children)
+ self.policy = policy
+
+ def setup(self, **kwargs):
+ """
+ Detect before ticking whether the policy configuration is invalid.
+
+ Args:
+ **kwargs (:obj:`dict`): distribute arguments to this
+ behaviour and in turn, all of it's children
+
+ Raises:
+ RuntimeError: if the parallel's policy configuration is invalid
+ Exception: be ready to catch if any of the children raise an exception
+ """
+ self.logger.debug("%s.setup()" % (self.__class__.__name__))
+ self.validate_policy_configuration()
+
+ def tick(self):
+ """
+ Tick over the children.
+
+ Yields:
+ :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children
+
+ Raises:
+ RuntimeError: if the policy configuration was invalid
+ """
+ self.logger.debug("%s.tick()" % self.__class__.__name__)
+ self.validate_policy_configuration()
+
+ # reset
+ if self.status != common.Status.RUNNING:
+ self.logger.debug("%s.tick(): re-initialising" % self.__class__.__name__)
+ for child in self.children:
+ # reset the children, this ensures old SUCCESS/FAILURE status flags
+ # don't break the synchronisation logic below
+ if child.status != common.Status.INVALID:
+ child.stop(common.Status.INVALID)
+ self.current_child = None
+ # subclass (user) handling
+ self.initialise()
+
+ # nothing to do
+ if not self.children:
+ self.current_child = None
+ self.stop(common.Status.SUCCESS)
+ yield self
+ return
+
+ # process them all first
+ for child in self.children:
+ if self.policy.synchronise and child.status == common.Status.SUCCESS:
+ continue
+ for node in child.tick():
+ yield node
+
+ # determine new status
+ new_status = common.Status.RUNNING
+ self.current_child = self.children[-1]
+ try:
+ failed_child = next(child for child in self.children if child.status == common.Status.FAILURE)
+ self.current_child = failed_child
+ new_status = common.Status.FAILURE
+ except StopIteration:
+ if type(self.policy) is common.ParallelPolicy.SuccessOnAll:
+ if all([c.status == common.Status.SUCCESS for c in self.children]):
+ new_status = common.Status.SUCCESS
+ self.current_child = self.children[-1]
+ elif type(self.policy) is common.ParallelPolicy.SuccessOnOne:
+ successful = [child for child in self.children if child.status == common.Status.SUCCESS]
+ if successful:
+ new_status = common.Status.SUCCESS
+ self.current_child = successful[-1]
+ elif type(self.policy) is common.ParallelPolicy.SuccessOnSelected:
+ if all([c.status == common.Status.SUCCESS for c in self.policy.children]):
+ new_status = common.Status.SUCCESS
+ self.current_child = self.policy.children[-1]
+ else:
+ raise RuntimeError("this parallel has been configured with an unrecognised policy [{}]".format(type(self.policy)))
+ # this parallel may have children that are still running
+ # so if the parallel itself has reached a final status, then
+ # these running children need to be terminated so they don't dangle
+ if new_status != common.Status.RUNNING:
+ self.stop(new_status)
+ self.status = new_status
+ yield self
+
+ def stop(self, new_status: common.Status=common.Status.INVALID):
+ """
+ For interrupts or any of the termination conditions, ensure that any
+ running children are stopped.
+
+ Args:
+ new_status : the composite is transitioning to this new status
+ """
+ # clean up dangling (running) children
+ for child in self.children:
+ if child.status == common.Status.RUNNING:
+ # interrupt it exactly as if it was interrupted by a higher priority
+ child.stop(common.Status.INVALID)
+ # only nec. thing here is to make sure the status gets set to INVALID if
+ # it was a higher priority interrupt (new_status == INVALID)
+ Composite.stop(self, new_status)
+
+ def validate_policy_configuration(self):
+ """
+ Policy configuration can be invalid if:
+
+ * Policy is SuccessOnSelected and no behaviours have been specified
+ * Policy is SuccessOnSelected and behaviours that are not children exist
+
+ Raises:
+ RuntimeError: if policy configuration was invalid
+ """
+ if type(self.policy) is common.ParallelPolicy.SuccessOnSelected:
+ if not self.policy.children:
+ error_message = ("policy SuccessOnSelected requires a non-empty "
+ "selection of children [{}]".format(self.name))
+ self.logger.error(error_message)
+ raise RuntimeError(error_message)
+ missing_children_names = [child.name for child in self.policy.children if child not in self.children]
+
+ if missing_children_names:
+ error_message = ("policy SuccessOnSelected has selected behaviours that are "
+ "not children of this parallel {}[{}]""".format(missing_children_names, self.name))
+ self.logger.error(error_message)
+ raise RuntimeError(error_message)
diff --git a/thirdparty/py_trees/console.py b/thirdparty/py_trees/console.py
new file mode 100644
index 0000000..15fc0e6
--- /dev/null
+++ b/thirdparty/py_trees/console.py
@@ -0,0 +1,340 @@
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+
+##############################################################################
+# Description
+##############################################################################
+
+"""
+Simple colour definitions and syntax highlighting for the console.
+
+----
+
+**Colour Definitions**
+
+The current list of colour definitions include:
+
+ * ``Regular``: black, red, green, yellow, blue, magenta, cyan, white,
+ * ``Bold``: bold, bold_black, bold_red, bold_green, bold_yellow, bold_blue, bold_magenta, bold_cyan, bold_white
+
+These colour definitions can be used in the following way:
+
+.. code-block:: python
+
+ import py_trees.console as console
+ print(console.cyan + " Name" + console.reset + ": " + console.yellow + "Dude" + console.reset)
+
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import os
+import sys
+
+
+##############################################################################
+# Special Characters
+##############################################################################
+
+
+def has_unicode(encoding: str=sys.stdout.encoding) -> bool:
+ """
+ Define whether the specified encoding has unicode symbols. Usually used to check
+ if the stdout is capable or otherwise (e.g. Jenkins CI can often be configured
+ with unicode disabled).
+
+ Args:
+ encoding (:obj:`str`, optional): the encoding to check against.
+
+ Returns:
+ :obj:`bool`: true if capable, false otherwise
+ """
+ try:
+ u'\u26A1'.encode(encoding)
+ except TypeError:
+ # if sys.stdout.encoding is not available, it is None
+ # this will occur if you run nosetests3 without -s
+ return False
+ except UnicodeError:
+ return False
+ return True
+
+
+def define_symbol_or_fallback(original: str, fallback: str, encoding: str=sys.stdout.encoding):
+ """
+ Return the correct encoding according to the specified encoding. Used to
+ make sure we get an appropriate symbol, even if the shell is merely ascii as
+ is often the case on, e.g. Jenkins CI.
+
+ Args:
+ original (:obj:`str`): the unicode string (usually just a character)
+ fallback (:obj:`str`): the fallback ascii string
+ encoding (:obj:`str`, optional): the encoding to check against.
+
+ Returns:
+ :obj:`str`: either original or fallback depending on whether exceptions were thrown.
+ """
+ try:
+ original.encode(encoding)
+ except UnicodeError:
+ return fallback
+ return original
+
+
+circle = u'\u26ac'
+lightning_bolt = u'\u26A1'
+double_vertical_line = u'\u2016'
+check_mark = u'\u2713'
+multiplication_x = u'\u2715'
+left_arrow = u'\u2190' # u'\u2190'
+right_arrow = u'\u2192'
+left_right_arrow = u'\u2194'
+forbidden_circle = u'\u29B8'
+circled_m = u'\u24c2'
+
+##############################################################################
+# Keypress
+##############################################################################
+
+
+def read_single_keypress():
+ """Waits for a single keypress on stdin.
+
+ This is a silly function to call if you need to do it a lot because it has
+ to store stdin's current setup, setup stdin for reading single keystrokes
+ then read the single keystroke then revert stdin back after reading the
+ keystroke.
+
+ Returns:
+ :obj:`int`: the character of the key that was pressed
+
+ Raises:
+ KeyboardInterrupt: if CTRL-C was pressed (keycode 0x03)
+ """
+ def read_single_keypress_unix():
+ """For Unix case, where fcntl, termios is available."""
+ import fcntl
+ import termios
+ fd = sys.stdin.fileno()
+ # save old state
+ flags_save = fcntl.fcntl(fd, fcntl.F_GETFL)
+ attrs_save = termios.tcgetattr(fd)
+ # make raw - the way to do this comes from the termios(3) man page.
+ attrs = list(attrs_save) # copy the stored version to update
+ # iflag
+ attrs[0] &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK |
+ termios.ISTRIP | termios.INLCR | termios. IGNCR |
+ termios.ICRNL | termios.IXON)
+ # oflag
+ attrs[1] &= ~termios.OPOST
+ # cflag
+ attrs[2] &= ~(termios.CSIZE | termios. PARENB)
+ attrs[2] |= termios.CS8
+ # lflag
+ attrs[3] &= ~(termios.ECHONL | termios.ECHO | termios.ICANON |
+ termios.ISIG | termios.IEXTEN)
+ termios.tcsetattr(fd, termios.TCSANOW, attrs)
+ # turn off non-blocking
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags_save & ~os.O_NONBLOCK)
+ # read a single keystroke
+ ret = sys.stdin.read(1) # returns a single character
+ if ord(ret) == 3: # CTRL-C
+ termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save)
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags_save)
+ raise KeyboardInterrupt("Ctrl-c")
+ # restore old state
+ termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save)
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags_save)
+ return ret
+
+ def read_single_keypress_windows():
+ """Windows case, can't use fcntl and termios.
+ Not same implementation as for Unix, requires a newline to continue.
+ """
+ import msvcrt # noqa
+ # read a single keystroke
+ ret = sys.stdin.read(1)
+ if ord(ret) == 3: # CTRL-C
+ raise KeyboardInterrupt("Ctrl-c")
+ return ret
+ try:
+ return read_single_keypress_unix()
+ except ImportError as e_unix:
+ try:
+ return read_single_keypress_windows()
+ except ImportError as e_windows:
+ raise ImportError("Neither unix nor windows implementations supported [{}][{}]".format(str(e_unix), str(e_windows)))
+
+##############################################################################
+# Methods
+##############################################################################
+
+
+def console_has_colours():
+ """
+ Detects if the console (stdout) has colourising capability.
+ """
+ if os.environ.get("PY_TREES_DISABLE_COLORS"):
+ return False
+ # From django.core.management.color.supports_color
+ # https://github.com/django/django/blob/master/django/core/management/color.py
+ plat = sys.platform
+ supported_platform = plat != 'Pocket PC' and (plat != 'win32' or
+ 'ANSICON' in os.environ)
+ # isatty is not always implemented, #6223.
+ is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
+ if not supported_platform or not is_a_tty:
+ return False
+ return True
+
+
+has_colours = console_has_colours()
+""" Whether the loading program has access to colours or not."""
+
+
+if has_colours:
+ # reset = "\x1b[0;0m"
+ reset = "\x1b[0m"
+ bold = "\x1b[%sm" % '1'
+ dim = "\x1b[%sm" % '2'
+ underlined = "\x1b[%sm" % '4'
+ blink = "\x1b[%sm" % '5'
+ black, red, green, yellow, blue, magenta, cyan, white = ["\x1b[%sm" % str(i) for i in range(30, 38)]
+ bold_black, bold_red, bold_green, bold_yellow, bold_blue, bold_magenta, bold_cyan, bold_white = ["\x1b[%sm" % ('1;' + str(i)) for i in range(30, 38)]
+else:
+ reset = ""
+ bold = ""
+ dim = ""
+ underlined = ""
+ blink = ""
+ black, red, green, yellow, blue, magenta, cyan, white = ["" for i in range(30, 38)]
+ bold_black, bold_red, bold_green, bold_yellow, bold_blue, bold_magenta, bold_cyan, bold_white = ["" for i in range(30, 38)]
+
+colours = [bold, dim, underlined, blink,
+ black, red, green, yellow, blue, magenta, cyan, white,
+ bold_black, bold_red, bold_green, bold_yellow, bold_blue, bold_magenta, bold_cyan, bold_white
+ ]
+"""List of all available colours."""
+
+
+def pretty_print(msg, colour=white):
+ if has_colours:
+ seq = colour + msg + reset
+ sys.stdout.write(seq)
+ else:
+ sys.stdout.write(msg)
+
+
+def pretty_println(msg, colour=white):
+ if has_colours:
+ seq = colour + msg + reset
+ sys.stdout.write(seq)
+ sys.stdout.write("\n")
+ else:
+ sys.stdout.write(msg)
+
+
+##############################################################################
+# Console
+##############################################################################
+
+
+def banner(msg):
+ print(green + "\n" + 80 * "*" + reset)
+ print(green + "* " + bold_white + msg.center(80) + reset)
+ print(green + 80 * "*" + "\n" + reset)
+
+
+def debug(msg):
+ print(green + msg + reset)
+
+
+def warning(msg):
+ print(yellow + msg + reset)
+
+
+def info(msg):
+ print(msg)
+
+
+def error(msg):
+ print(red + msg + reset)
+
+
+def logdebug(message):
+ '''
+ Prefixes ``[DEBUG]`` and colours the message green.
+
+ Args:
+ message (:obj:`str`): message to log.
+ '''
+ print(green + "[DEBUG] " + message + reset)
+
+
+def loginfo(message):
+ '''
+ Prefixes ``[ INFO]`` to the message.
+
+ Args:
+ message (:obj:`str`): message to log.
+ '''
+ print("[ INFO] " + message)
+
+
+def logwarn(message):
+ '''
+ Prefixes ``[ WARN]`` and colours the message yellow.
+
+ Args:
+ message (:obj:`str`): message to log.
+ '''
+ print(yellow + "[ WARN] " + message + reset)
+
+
+def logerror(message):
+ '''
+ Prefixes ``[ERROR]`` and colours the message red.
+
+ Args:
+ message (:obj:`str`): message to log.
+ '''
+ print(red + "[ERROR] " + message + reset)
+
+
+def logfatal(message):
+ '''
+ Prefixes ``[FATAL]`` and colours the message bold red.
+
+ Args:
+ message (:obj:`str`): message to log.
+ '''
+ print(bold_red + "[FATAL] " + message + reset)
+
+
+##############################################################################
+# Main
+##############################################################################
+
+if __name__ == '__main__':
+ for colour in colours:
+ pretty_print("dude\n", colour)
+ logdebug("loginfo message")
+ logwarn("logwarn message")
+ logerror("logerror message")
+ logfatal("logfatal message")
+ pretty_print("red\n", red)
+ print("some normal text")
+ print(cyan + " Name" + reset + ": " + yellow + "Dude" + reset)
+ print("special characters are\n")
+ print("lightning_bolt: {}".format(lightning_bolt))
+ print("double_vertical_line: {}".format(double_vertical_line))
+ print("check_mark: {}".format(check_mark))
+ print("multiplication_x: {}".format(multiplication_x))
+ print("left_arrow: {}".format(left_arrow))
+ print("right_arrow: {}".format(right_arrow))
+ print("circled_m: {}".format(circled_m))
+ # print("has unicode: {}".format(has_unicode()))
diff --git a/thirdparty/py_trees/decorators.py b/thirdparty/py_trees/decorators.py
new file mode 100644
index 0000000..1e05c57
--- /dev/null
+++ b/thirdparty/py_trees/decorators.py
@@ -0,0 +1,663 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+Decorators are behaviours that manage a single child and provide common
+modifications to their underlying child behaviour (e.g. inverting the result).
+That is, they provide a means for behaviours to wear different 'hats' and
+this combinatorially expands the capabilities of your behaviour library.
+
+.. image:: images/many-hats.png
+ :width: 40px
+ :align: center
+
+An example:
+
+.. graphviz:: dot/decorators.dot
+ :align: center
+
+.. literalinclude:: examples/decorators.py
+ :language: python
+ :linenos:
+
+
+**Decorators (Hats)**
+
+Decorators with very specific functionality:
+
+* :class:`py_trees.decorators.Condition`
+* :class:`py_trees.decorators.EternalGuard`
+* :class:`py_trees.decorators.Inverter`
+* :class:`py_trees.decorators.OneShot`
+* :class:`py_trees.decorators.StatusToBlackboard`
+* :class:`py_trees.decorators.Timeout`
+
+And the X is Y family:
+
+* :class:`py_trees.decorators.FailureIsRunning`
+* :class:`py_trees.decorators.FailureIsSuccess`
+* :class:`py_trees.decorators.RunningIsFailure`
+* :class:`py_trees.decorators.RunningIsSuccess`
+* :class:`py_trees.decorators.SuccessIsFailure`
+* :class:`py_trees.decorators.SuccessIsRunning`
+
+**Decorators for Blocking Behaviours**
+
+It is worth making a note of the effect of decorators on
+behaviours that return :data:`~py_trees.common.Status.RUNNING` for
+some time before finally returning :data:`~py_trees.common.Status.SUCCESS`
+or :data:`~py_trees.common.Status.FAILURE` (blocking behaviours) since
+the results are often at first, surprising.
+
+A decorator, such as :func:`py_trees.decorators.RunningIsSuccess` on
+a blocking behaviour will immediately terminate the underlying child and
+re-intialise on it's next tick. This is necessary to ensure the underlying
+child isn't left in a dangling state (i.e.
+:data:`~py_trees.common.Status.RUNNING`), but is often not what is being
+sought.
+
+The typical use case being attempted is to convert the blocking
+behaviour into a non-blocking behaviour. If the underlying child has no
+state being modified in either the :meth:`~py_trees.behaviour.Behaviour.initialise`
+or :meth:`~py_trees.behaviour.Behaviour.terminate` methods (e.g. machinery is
+entirely launched at init or setup time), then conversion to a non-blocking
+representative of the original succeeds. Otherwise, another approach is
+needed. Usually this entails writing a non-blocking counterpart, or
+combination of behaviours to affect the non-blocking characteristics.
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import functools
+import inspect
+import time
+import typing
+
+from typing import Callable, List, Set, Union # noqa
+
+from . import behaviour
+from . import blackboard
+from . import common
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+class Decorator(behaviour.Behaviour):
+ """
+ A decorator is responsible for handling the lifecycle of a single
+ child beneath
+
+ Args:
+ child: the child to be decorated
+ name: the decorator name
+
+ Raises:
+ TypeError: if the child is not an instance of :class:`~py_trees.behaviour.Behaviour`
+ """
+ def __init__(
+ self,
+ child: behaviour.Behaviour,
+ name=common.Name.AUTO_GENERATED
+ ):
+ # Checks
+ if not isinstance(child, behaviour.Behaviour):
+ raise TypeError("A decorator's child must be an instance of py_trees.behaviours.Behaviour")
+ # Initialise
+ super().__init__(name=name)
+ self.children.append(child)
+ # Give a convenient alias
+ self.decorated = self.children[0]
+ self.decorated.parent = self
+
+ def tick(self):
+ """
+ A decorator's tick is exactly the same as a normal proceedings for
+ a Behaviour's tick except that it also ticks the decorated child node.
+
+ Yields:
+ :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children
+ """
+ self.logger.debug("%s.tick()" % self.__class__.__name__)
+ # initialise just like other behaviours/composites
+ if self.status != common.Status.RUNNING:
+ self.initialise()
+ # interrupt proceedings and process the child node
+ # (including any children it may have as well)
+ for node in self.decorated.tick():
+ yield node
+ # resume normal proceedings for a Behaviour's tick
+ new_status = self.update()
+ if new_status not in list(common.Status):
+ self.logger.error("A behaviour returned an invalid status, setting to INVALID [%s][%s]" % (new_status, self.name))
+ new_status = common.Status.INVALID
+ if new_status != common.Status.RUNNING:
+ self.stop(new_status)
+ self.status = new_status
+ yield self
+
+ def stop(self, new_status):
+ """
+ As with other composites, it checks if the child is running
+ and stops it if that is the case.
+
+ Args:
+ new_status (:class:`~py_trees.common.Status`): the behaviour is transitioning to this new status
+ """
+ self.logger.debug("%s.stop(%s)" % (self.__class__.__name__, new_status))
+ self.terminate(new_status)
+ # priority interrupt handling
+ if new_status == common.Status.INVALID:
+ self.decorated.stop(new_status)
+ # if the decorator returns SUCCESS/FAILURE and should stop the child
+ if self.decorated.status == common.Status.RUNNING:
+ self.decorated.stop(common.Status.INVALID)
+ self.status = new_status
+
+ def tip(self):
+ """
+ Get the *tip* of this behaviour's subtree (if it has one) after it's last
+ tick. This corresponds to the the deepest node that was running before the
+ subtree traversal reversed direction and headed back to this node.
+
+ Returns:
+ :class:`~py_trees.behaviour.Behaviour` or :obj:`None`: child behaviour, itself or :obj:`None` if its status is :data:`~py_trees.common.Status.INVALID`
+ """
+ if self.decorated.status != common.Status.INVALID:
+ return self.decorated.tip()
+ else:
+ return super().tip()
+
+##############################################################################
+# Decorators
+##############################################################################
+
+
+class StatusToBlackboard(Decorator):
+ """
+ Reflect the status of the decorator's child to the blackboard.
+
+ Args:
+ child: the child behaviour or subtree
+ variable_name: name of the blackboard variable, may be nested, e.g. foo.status
+ name: the decorator name
+ """
+ def __init__(
+ self,
+ *,
+ child: behaviour.Behaviour,
+ variable_name: str,
+ name: Union[str, common.Name]=common.Name.AUTO_GENERATED,
+ ):
+ super().__init__(child=child, name=name)
+ self.variable_name = variable_name
+ name_components = variable_name.split('.')
+ self.key = name_components[0]
+ self.key_attributes = '.'.join(name_components[1:]) # empty string if no other parts
+ self.blackboard = self.attach_blackboard_client(self.name)
+ self.blackboard.register_key(key=self.key, access=common.Access.WRITE)
+
+ def update(self):
+ """
+ Reflect the decorated child's status to the blackboard and return
+
+ Returns: the decorated child's status
+ """
+ self.blackboard.set(
+ name=self.variable_name,
+ value=self.decorated.status,
+ overwrite=True
+ )
+ return self.decorated.status
+
+
+ConditionType = Union[
+ Callable[[], bool],
+ Callable[[], common.Status],
+ Callable[[blackboard.Blackboard], bool],
+ Callable[[blackboard.Blackboard], common.Status]
+]
+
+
+class EternalGuard(Decorator):
+ """
+ A decorator that continually guards the execution of a subtree.
+ If at any time the guard's condition check fails, then the child
+ behaviour/subtree is invalidated.
+
+ .. note:: This decorator's behaviour is stronger than the
+ :term:`guard` typical of a conditional check at the beginning of a
+ sequence of tasks as it continues to check on every tick whilst the
+ task (or sequence of tasks) runs.
+
+ Args:
+ child: the child behaviour or subtree
+ condition: a functional check that determines execution or not of the subtree
+ blackboard_keys: provide read access for the conditional function to these keys
+ name: the decorator name
+
+ Examples:
+
+ Simple conditional function returning True/False:
+
+ .. code-block:: python
+
+ def check():
+ return True
+
+ foo = py_trees.behaviours.Foo()
+ eternal_guard = py_trees.decorators.EternalGuard(
+ name="Eternal Guard",
+ condition=check,
+ child=foo
+ )
+
+ Simple conditional function returning SUCCESS/FAILURE:
+
+ .. code-block:: python
+
+ def check():
+ return py_trees.common.Status.SUCCESS
+
+ foo = py_trees.behaviours.Foo()
+ eternal_guard = py_trees.decorators.EternalGuard(
+ name="Eternal Guard",
+ condition=check,
+ child=foo
+ )
+
+ Conditional function that makes checks against data on the blackboard (the
+ blackboard client with pre-configured access is provided by the EternalGuard
+ instance):
+
+ .. code-block:: python
+
+ def check(blackboard):
+ return blackboard.velocity > 3.0
+
+ foo = py_trees.behaviours.Foo()
+ eternal_guard = py_trees.decorators.EternalGuard(
+ name="Eternal Guard",
+ condition=check,
+ blackboard_keys={"velocity"},
+ child=foo
+ )
+
+ .. seealso:: :meth:`py_trees.idioms.eternal_guard`
+ """
+ def __init__(
+ self,
+ *,
+ child: behaviour.Behaviour,
+ # Condition is one of 4 callable types illustrated in the docstring, partials complicate
+ # it as well. When typing_extensions are available (very recent) more generally, can use
+ # Protocols to handle it. Probably also a sign that it's not a very clean api though...
+ condition: typing.Any,
+ blackboard_keys: Union[List[str], Set[str]]=[],
+ name: Union[str, common.Name]=common.Name.AUTO_GENERATED,
+ ):
+ super().__init__(name=name, child=child)
+ self.blackboard = self.attach_blackboard_client(self.name)
+ for key in blackboard_keys:
+ self.blackboard.register_key(key=key, access=common.Access.READ)
+ condition_signature = inspect.signature(condition)
+ if "blackboard" in [p.name for p in condition_signature.parameters.values()]:
+ self.condition = functools.partial(condition, self.blackboard)
+ else:
+ self.condition = condition
+
+ def tick(self):
+ """
+ A decorator's tick is exactly the same as a normal proceedings for
+ a Behaviour's tick except that it also ticks the decorated child node.
+
+ Yields:
+ :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children
+ """
+ self.logger.debug("%s.tick()" % self.__class__.__name__)
+
+ # condition check
+ result = self.condition()
+ if type(result) == common.Status:
+ result = False if result == common.Status.FAILURE else True
+ elif type(result) != bool:
+ error_message = "conditional check must return 'bool' or 'common.Status' [{}]".format(type(result))
+ self.logger.error("The {}".format(error_message))
+ raise RuntimeError(error_message)
+
+ if not result:
+ # abort, abort, the FSM is losing his noodles!!!
+ self.stop(common.Status.FAILURE)
+ yield self
+ else:
+ # normal behaviour
+ for node in super().tick():
+ yield node
+
+ def update(self):
+ """
+ The update method is only ever triggered in the child's post-tick, which implies
+ that the condition has already been checked and passed (refer to the :meth:`tick` method).
+ """
+ return self.decorated.status
+
+
+class Timeout(Decorator):
+ """
+ A decorator that applies a timeout pattern to an existing behaviour.
+ If the timeout is reached, the encapsulated behaviour's
+ :meth:`~py_trees.behaviour.Behaviour.stop` method is called with
+ status :data:`~py_trees.common.Status.FAILURE` otherwise it will
+ simply directly tick and return with the same status
+ as that of it's encapsulated behaviour.
+ """
+ def __init__(self,
+ child: behaviour.Behaviour,
+ name: Union[str, common.Name]=common.Name.AUTO_GENERATED,
+ duration: float=5.0):
+ """
+ Init with the decorated child and a timeout duration.
+
+ Args:
+ child: the child behaviour or subtree
+ name: the decorator name
+ duration: timeout length in seconds
+ """
+ super(Timeout, self).__init__(name=name, child=child)
+ self.duration = duration
+ self.finish_time = None
+
+ def initialise(self):
+ """
+ Reset the feedback message and finish time on behaviour entry.
+ """
+ self.finish_time = time.monotonic() + self.duration
+ self.feedback_message = ""
+
+ def update(self):
+ """
+ Terminate the child and return :data:`~py_trees.common.Status.FAILURE`
+ if the timeout is exceeded.
+ """
+ current_time = time.monotonic()
+ if self.decorated.status == common.Status.RUNNING and current_time > self.finish_time:
+ self.feedback_message = "timed out"
+ self.logger.debug("{}.update() {}".format(self.__class__.__name__, self.feedback_message))
+ # invalidate the decorated (i.e. cancel it), could also put this logic in a terminate() method
+ self.decorated.stop(common.Status.INVALID)
+ return common.Status.FAILURE
+ if self.decorated.status == common.Status.RUNNING:
+ self.feedback_message = "time still ticking ... [remaining: {}s]".format(
+ self.finish_time - current_time
+ )
+ else:
+ self.feedback_message = "child finished before timeout triggered"
+ return self.decorated.status
+
+
+class OneShot(Decorator):
+ """
+ A decorator that implements the oneshot pattern.
+
+ This decorator ensures that the underlying child is ticked through
+ to completion just once and while doing so, will return
+ with the same status as it's child. Thereafter it will return
+ with the final status of the underlying child.
+
+ Completion status is determined by the policy given on construction.
+
+ * With policy :data:`~py_trees.common.OneShotPolicy.ON_SUCCESSFUL_COMPLETION`, the oneshot will activate only when the underlying child returns :data:`~py_trees.common.Status.SUCCESS` (i.e. it permits retries).
+ * With policy :data:`~py_trees.common.OneShotPolicy.ON_COMPLETION`, the oneshot will activate when the child returns :data:`~py_trees.common.Status.SUCCESS` || :data:`~py_trees.common.Status.FAILURE`.
+
+ .. seealso:: :meth:`py_trees.idioms.oneshot`
+ """
+ def __init__(self, child,
+ name=common.Name.AUTO_GENERATED,
+ policy=common.OneShotPolicy.ON_SUCCESSFUL_COMPLETION):
+ """
+ Init with the decorated child.
+
+ Args:
+ name (:obj:`str`): the decorator name
+ child (:class:`~py_trees.behaviour.Behaviour`): behaviour to time
+ policy (:class:`~py_trees.common.OneShotPolicy`): policy determining when the oneshot should activate
+ """
+ super(OneShot, self).__init__(name=name, child=child)
+ self.final_status = None
+ self.policy = policy
+
+ def update(self):
+ """
+ Bounce if the child has already successfully completed.
+ """
+ if self.final_status:
+ self.logger.debug("{}.update()[bouncing]".format(self.__class__.__name__))
+ return self.final_status
+ return self.decorated.status
+
+ def tick(self):
+ """
+ Select between decorator (single child) and behaviour (no children) style
+ ticks depending on whether or not the underlying child has been ticked
+ successfully to completion previously.
+ """
+ if self.final_status:
+ # ignore the child
+ for node in behaviour.Behaviour.tick(self):
+ yield node
+ else:
+ # tick the child
+ for node in Decorator.tick(self):
+ yield node
+
+ def terminate(self, new_status):
+ """
+ If returning :data:`~py_trees.common.Status.SUCCESS` for the first time,
+ flag it so future ticks will block entry to the child.
+ """
+ if not self.final_status and new_status in self.policy.value:
+ self.logger.debug("{}.terminate({})[oneshot completed]".format(self.__class__.__name__, new_status))
+ self.feedback_message = "oneshot completed"
+ self.final_status = new_status
+ else:
+ self.logger.debug("{}.terminate({})".format(self.__class__.__name__, new_status))
+
+
+class Inverter(Decorator):
+ """
+ A decorator that inverts the result of a class's update function.
+ """
+ def __init__(self, child, name=common.Name.AUTO_GENERATED):
+ """
+ Init with the decorated child.
+
+ Args:
+ child (:class:`~py_trees.behaviour.Behaviour`): behaviour to time
+ name (:obj:`str`): the decorator name
+ """
+ super(Inverter, self).__init__(name=name, child=child)
+
+ def update(self):
+ """
+ Flip :data:`~py_trees.common.Status.FAILURE` and
+ :data:`~py_trees.common.Status.SUCCESS`
+
+ Returns:
+ :class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
+ """
+ if self.decorated.status == common.Status.SUCCESS:
+ self.feedback_message = "success -> failure"
+ return common.Status.FAILURE
+ elif self.decorated.status == common.Status.FAILURE:
+ self.feedback_message = "failure -> success"
+ return common.Status.SUCCESS
+ self.feedback_message = self.decorated.feedback_message
+ return self.decorated.status
+
+
+class RunningIsFailure(Decorator):
+ """
+ Got to be snappy! We want results...yesterday!
+ """
+ def update(self):
+ """
+ Return the decorated child's status unless it is
+ :data:`~py_trees.common.Status.RUNNING` in which case, return
+ :data:`~py_trees.common.Status.FAILURE`.
+
+ Returns:
+ :class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
+ """
+ if self.decorated.status == common.Status.RUNNING:
+ self.feedback_message = "running is failure" + (" [%s]" % self.decorated.feedback_message if self.decorated.feedback_message else "")
+ return common.Status.FAILURE
+ else:
+ self.feedback_message = self.decorated.feedback_message
+ return self.decorated.status
+
+
+class RunningIsSuccess(Decorator):
+ """
+ Don't hang around...
+ """
+ def update(self):
+ """
+ Return the decorated child's status unless it is
+ :data:`~py_trees.common.Status.RUNNING` in which case, return
+ :data:`~py_trees.common.Status.SUCCESS`.
+
+ Returns:
+ :class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
+ """
+ if self.decorated.status == common.Status.RUNNING:
+ self.feedback_message = "running is success" + (" [%s]" % self.decorated.feedback_message if self.decorated.feedback_message else "")
+ return common.Status.SUCCESS
+ self.feedback_message = self.decorated.feedback_message
+ return self.decorated.status
+
+
+class FailureIsSuccess(Decorator):
+ """
+ Be positive, always succeed.
+ """
+ def update(self):
+ """
+ Return the decorated child's status unless it is
+ :data:`~py_trees.common.Status.FAILURE` in which case, return
+ :data:`~py_trees.common.Status.SUCCESS`.
+
+ Returns:
+ :class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
+ """
+ if self.decorated.status == common.Status.FAILURE:
+ self.feedback_message = "failure is success" + (" [%s]" % self.decorated.feedback_message if self.decorated.feedback_message else "")
+ return common.Status.SUCCESS
+ self.feedback_message = self.decorated.feedback_message
+ return self.decorated.status
+
+
+class FailureIsRunning(Decorator):
+ """
+ Dont stop running.
+ """
+ def update(self):
+ """
+ Return the decorated child's status unless it is
+ :data:`~py_trees.common.Status.FAILURE` in which case, return
+ :data:`~py_trees.common.Status.RUNNING`.
+
+ Returns:
+ :class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
+ """
+ if self.decorated.status == common.Status.FAILURE:
+ self.feedback_message = "failure is running" + (" [%s]" % self.decorated.feedback_message if self.decorated.feedback_message else "")
+ return common.Status.RUNNING
+ self.feedback_message = self.decorated.feedback_message
+ return self.decorated.status
+
+
+class SuccessIsFailure(Decorator):
+ """
+ Be depressed, always fail.
+ """
+ def update(self):
+ """
+ Return the decorated child's status unless it is
+ :data:`~py_trees.common.Status.SUCCESS` in which case, return
+ :data:`~py_trees.common.Status.FAILURE`.
+
+ Returns:
+ :class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
+ """
+ if self.decorated.status == common.Status.SUCCESS:
+ self.feedback_message = "success is failure" + (" [%s]" % self.decorated.feedback_message if self.decorated.feedback_message else "")
+ return common.Status.FAILURE
+ self.feedback_message = self.decorated.feedback_message
+ return self.decorated.status
+
+
+class SuccessIsRunning(Decorator):
+ """
+ It never ends...
+ """
+ def update(self):
+ """
+ Return the decorated child's status unless it is
+ :data:`~py_trees.common.Status.SUCCESS` in which case, return
+ :data:`~py_trees.common.Status.RUNNING`.
+
+ Returns:
+ :class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
+ """
+ if self.decorated.status == common.Status.SUCCESS:
+ self.feedback_message = "success is running [%s]" % self.decorated.feedback_message
+ return common.Status.RUNNING
+ self.feedback_message = self.decorated.feedback_message
+ return self.decorated.status
+
+
+class Condition(Decorator):
+ """
+ Encapsulates a behaviour and wait for it's status to flip to the
+ desired state. This behaviour will tick with
+ :data:`~py_trees.common.Status.RUNNING` while waiting and
+ :data:`~py_trees.common.Status.SUCCESS` when the flip occurs.
+ """
+ def __init__(self,
+ child,
+ name=common.Name.AUTO_GENERATED,
+ status=common.Status.SUCCESS):
+ """
+ Initialise with child and optional name, status variables.
+
+ Args:
+ child (:class:`~py_trees.behaviour.Behaviour`): the child to be decorated
+ name (:obj:`str`): the decorator name (can be None)
+ status (:class:`~py_trees.common.Status`): the desired status to watch for
+ """
+ super(Condition, self).__init__(child, name)
+ self.succeed_status = status
+
+ def update(self):
+ """
+ :data:`~py_trees.common.Status.SUCCESS` if the decorated child has returned
+ the specified status, otherwise :data:`~py_trees.common.Status.RUNNING`.
+ This decorator will never return :data:`~py_trees.common.Status.FAILURE`
+
+ Returns:
+ :class:`~py_trees.common.Status`: the behaviour's new status :class:`~py_trees.common.Status`
+ """
+ self.logger.debug("%s.update()" % self.__class__.__name__)
+ self.feedback_message = "'{0}' has status {1}, waiting for {2}".format(self.decorated.name, self.decorated.status, self.succeed_status)
+ if self.decorated.status == self.succeed_status:
+ return common.Status.SUCCESS
+ return common.Status.RUNNING
diff --git a/thirdparty/py_trees/demos/Readme.md b/thirdparty/py_trees/demos/Readme.md
new file mode 100644
index 0000000..3647914
--- /dev/null
+++ b/thirdparty/py_trees/demos/Readme.md
@@ -0,0 +1,8 @@
+# Guidelines
+
+Each module here is a self-contained code sample for one of the demo scripts.
+That means there is a fair bit of copy and paste happening, but that is an
+intentional decision to ensure each demo script is self-contained and easy
+for beginners to follow and/or copy-paste from.
+
+Keep this in mind when adding additional programs.
\ No newline at end of file
diff --git a/thirdparty/py_trees/demos/__init__.py b/thirdparty/py_trees/demos/__init__.py
new file mode 100644
index 0000000..7762a95
--- /dev/null
+++ b/thirdparty/py_trees/demos/__init__.py
@@ -0,0 +1,29 @@
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+This package contains py_trees demo script code.
+"""
+##############################################################################
+# Imports
+##############################################################################
+
+import os,sys
+sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)),'..','..'))
+from . import action
+from . import blackboard
+from . import blackboard_namespaces
+from . import blackboard_remappings
+from . import context_switching
+from . import display_modes
+from . import dot_graphs
+from . import either_or
+from . import lifecycle
+from . import selector
+from . import sequence
+from . import stewardship
diff --git a/thirdparty/py_trees/demos/action.py b/thirdparty/py_trees/demos/action.py
new file mode 100644
index 0000000..5ea463b
--- /dev/null
+++ b/thirdparty/py_trees/demos/action.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+.. argparse::
+ :module: py_trees.demos.action
+ :func: command_line_argument_parser
+ :prog: py-trees-demo-action-behaviour
+
+.. image:: images/action.gif
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import argparse
+import atexit
+import multiprocessing
+import py_trees.common
+import time
+
+import py_trees.console as console
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+def description():
+ content = "Demonstrates the characteristics of a typical 'action' behaviour.\n"
+ content += "\n"
+ content += "* Mocks an external process and connects to it in the setup() method\n"
+ content += "* Kickstarts new goals with the external process in the initialise() method\n"
+ content += "* Monitors the ongoing goal status in the update() method\n"
+ content += "* Determines RUNNING/SUCCESS pending feedback from the external process\n"
+
+ if py_trees.console.has_colours:
+ banner_line = console.green + "*" * 79 + "\n" + console.reset
+ s = "\n"
+ s += banner_line
+ s += console.bold_white + "Action Behaviour".center(79) + "\n" + console.reset
+ s += banner_line
+ s += "\n"
+ s += content
+ s += "\n"
+ s += banner_line
+ else:
+ s = content
+ return s
+
+
+def epilog():
+ if py_trees.console.has_colours:
+ return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset
+ else:
+ return None
+
+
+def command_line_argument_parser():
+ return argparse.ArgumentParser(description=description(),
+ epilog=epilog(),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+
+def planning(pipe_connection):
+ """
+ Emulates an external process which might accept long running planning jobs.
+ """
+ idle = True
+ percentage_complete = 0
+ try:
+ while(True):
+ if pipe_connection.poll():
+ pipe_connection.recv()
+ percentage_complete = 0
+ idle = False
+ if not idle:
+ percentage_complete += 10
+ pipe_connection.send([percentage_complete])
+ if percentage_complete == 100:
+ idle = True
+ time.sleep(0.5)
+ except KeyboardInterrupt:
+ pass
+
+
+class Action(py_trees.behaviour.Behaviour):
+ """
+ Connects to a subprocess to initiate a goal, and monitors the progress
+ of that goal at each tick until the goal is completed, at which time
+ the behaviour itself returns with success or failure (depending on
+ success or failure of the goal itself).
+
+ This is typical of a behaviour that is connected to an external process
+ responsible for driving hardware, conducting a plan, or a long running
+ processing pipeline (e.g. planning/vision).
+
+ Key point - this behaviour itself should not be doing any work!
+ """
+ def __init__(self, name="Action"):
+ """
+ Default construction.
+ """
+ super(Action, self).__init__(name)
+ self.logger.debug("%s.__init__()" % (self.__class__.__name__))
+
+ def setup(self):
+ """
+ No delayed initialisation required for this example.
+ """
+ self.logger.debug("%s.setup()->connections to an external process" % (self.__class__.__name__))
+ self.parent_connection, self.child_connection = multiprocessing.Pipe()
+ self.planning = multiprocessing.Process(target=planning, args=(self.child_connection,))
+ atexit.register(self.planning.terminate)
+ self.planning.start()
+
+ def initialise(self):
+ """
+ Reset a counter variable.
+ """
+ self.logger.debug("%s.initialise()->sending new goal" % (self.__class__.__name__))
+ self.parent_connection.send(['new goal'])
+ self.percentage_completion = 0
+
+ def update(self):
+ """
+ Increment the counter and decide upon a new status result for the behaviour.
+ """
+ new_status = py_trees.common.Status.RUNNING
+ if self.parent_connection.poll():
+ self.percentage_completion = self.parent_connection.recv().pop()
+ if self.percentage_completion == 100:
+ new_status = py_trees.common.Status.SUCCESS
+ if new_status == py_trees.common.Status.SUCCESS:
+ self.feedback_message = "Processing finished"
+ self.logger.debug("%s.update()[%s->%s][%s]" % (self.__class__.__name__, self.status, new_status, self.feedback_message))
+ else:
+ self.feedback_message = "{0}%".format(self.percentage_completion)
+ self.logger.debug("%s.update()[%s][%s]" % (self.__class__.__name__, self.status, self.feedback_message))
+ return new_status
+
+ def terminate(self, new_status):
+ """
+ Nothing to clean up in this example.
+ """
+ self.logger.debug("%s.terminate()[%s->%s]" % (self.__class__.__name__, self.status, new_status))
+
+
+##############################################################################
+# Main
+##############################################################################
+
+def main():
+ """
+ Entry point for the demo script.
+ """
+ command_line_argument_parser().parse_args()
+
+ print(description())
+
+ py_trees.logging.level = py_trees.logging.Level.DEBUG
+
+ action = Action()
+ action.setup()
+ try:
+ for unused_i in range(0, 12):
+ action.tick_once()
+ time.sleep(0.5)
+ print("\n")
+ except KeyboardInterrupt:
+ pass
diff --git a/thirdparty/py_trees/demos/blackboard.py b/thirdparty/py_trees/demos/blackboard.py
new file mode 100644
index 0000000..2bb4263
--- /dev/null
+++ b/thirdparty/py_trees/demos/blackboard.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+.. argparse::
+ :module: py_trees.demos.blackboard
+ :func: command_line_argument_parser
+ :prog: py-trees-demo-blackboard
+
+.. graphviz:: dot/demo-blackboard.dot
+ :align: center
+ :caption: Dot Graph
+
+.. figure:: images/blackboard_demo.png
+ :align: center
+
+ Console Screenshot
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import argparse
+import operator
+import py_trees
+import sys
+
+import py_trees.console as console
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+def description():
+ content = "Demonstrates usage of the blackboard and related behaviours.\n"
+ content += "\n"
+ content += "A sequence is populated with a few behaviours that exercise\n"
+ content += "reading and writing on the Blackboard in interesting ways.\n"
+
+ if py_trees.console.has_colours:
+ banner_line = console.green + "*" * 79 + "\n" + console.reset
+ s = "\n"
+ s += banner_line
+ s += console.bold_white + "Blackboard".center(79) + "\n" + console.reset
+ s += banner_line
+ s += "\n"
+ s += content
+ s += "\n"
+ s += banner_line
+ else:
+ s = content
+ return s
+
+
+def epilog():
+ if py_trees.console.has_colours:
+ return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset
+ else:
+ return None
+
+
+def command_line_argument_parser():
+ parser = argparse.ArgumentParser(description=description(),
+ epilog=epilog(),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ render_group = parser.add_mutually_exclusive_group()
+ render_group.add_argument('-r', '--render', action='store_true', help='render dot tree to file')
+ render_group.add_argument(
+ '--render-with-blackboard-variables',
+ action='store_true',
+ help='render dot tree to file with blackboard variables'
+ )
+ return parser
+
+
+class Nested(object):
+ """
+ A more complex object to interact with on the blackboard.
+ """
+ def __init__(self):
+ self.foo = "bar"
+
+ def __str__(self):
+ return str({"foo": self.foo})
+
+
+class BlackboardWriter(py_trees.behaviour.Behaviour):
+ """
+ Custom writer that submits a more complicated variable to the blackboard.
+ """
+ def __init__(self, name="Writer"):
+ super().__init__(name=name)
+ self.blackboard = self.attach_blackboard_client()
+ self.blackboard.register_key(key="dude", access=py_trees.common.Access.READ)
+ self.blackboard.register_key(key="spaghetti", access=py_trees.common.Access.WRITE)
+
+ self.logger.debug("%s.__init__()" % (self.__class__.__name__))
+
+ def update(self):
+ """
+ Write a dictionary to the blackboard and return :data:`~py_trees.common.Status.SUCCESS`.
+ """
+ self.logger.debug("%s.update()" % (self.__class__.__name__))
+ try:
+ unused = self.blackboard.dude
+ except KeyError:
+ pass
+ try:
+ unused = self.blackboard.dudette
+ except AttributeError:
+ pass
+ try:
+ self.blackboard.dudette = "Jane"
+ except AttributeError:
+ pass
+ self.blackboard.spaghetti = {"type": "Carbonara", "quantity": 1}
+ self.blackboard.spaghetti = {"type": "Gnocchi", "quantity": 2}
+ try:
+ self.blackboard.set("spaghetti", {"type": "Bolognese", "quantity": 3}, overwrite=False)
+ except AttributeError:
+ pass
+ return py_trees.common.Status.SUCCESS
+
+
+class ParamsAndState(py_trees.behaviour.Behaviour):
+ """
+ A more esotoric use of multiple blackboards in a behaviour to represent
+ storage of parameters and state.
+ """
+ def __init__(self, name="ParamsAndState"):
+ super().__init__(name=name)
+ # namespaces can include the separator or may leave it out
+ # they can also be nested, e.g. /agent/state, /agent/parameters
+ self.parameters = self.attach_blackboard_client("Params", "parameters")
+ self.state = self.attach_blackboard_client("State", "state")
+ self.parameters.register_key(
+ key="default_speed",
+ access=py_trees.common.Access.READ
+ )
+ self.state.register_key(
+ key="current_speed",
+ access=py_trees.common.Access.WRITE
+ )
+
+ def initialise(self):
+ try:
+ self.state.current_speed = self.parameters.default_speed
+ except KeyError as e:
+ raise RuntimeError("parameter 'default_speed' not found [{}]".format(str(e)))
+
+ def update(self):
+ if self.state.current_speed > 40.0:
+ return py_trees.common.Status.SUCCESS
+ else:
+ self.state.current_speed += 1.0
+ return py_trees.common.Status.RUNNING
+
+
+def create_root():
+ root = py_trees.composites.Sequence("Blackboard Demo")
+ set_blackboard_variable = py_trees.behaviours.SetBlackboardVariable(
+ name="Set Nested", variable_name="nested", variable_value=Nested()
+ )
+ write_blackboard_variable = BlackboardWriter(name="Writer")
+ check_blackboard_variable = py_trees.behaviours.CheckBlackboardVariableValue(
+ name="Check Nested Foo",
+ check=py_trees.common.ComparisonExpression(
+ variable="nested.foo",
+ value="bar",
+ operator=operator.eq
+ )
+ )
+ params_and_state = ParamsAndState()
+ root.add_children([
+ set_blackboard_variable,
+ write_blackboard_variable,
+ check_blackboard_variable,
+ params_and_state
+ ])
+ return root
+
+##############################################################################
+# Main
+##############################################################################
+
+
+def main():
+ """
+ Entry point for the demo script.
+ """
+ args = command_line_argument_parser().parse_args()
+ print(description())
+ py_trees.logging.level = py_trees.logging.Level.DEBUG
+ py_trees.blackboard.Blackboard.enable_activity_stream(maximum_size=100)
+ blackboard = py_trees.blackboard.Client(name="Configuration")
+ blackboard.register_key(key="dude", access=py_trees.common.Access.WRITE)
+ blackboard.register_key(key="/parameters/default_speed", access=py_trees.common.Access.WRITE)
+ blackboard.dude = "Bob"
+ blackboard.parameters.default_speed = 30.0
+
+ root = create_root()
+
+ ####################
+ # Rendering
+ ####################
+ if args.render:
+ py_trees.display.render_dot_tree(root, with_blackboard_variables=False)
+ sys.exit()
+ if args.render_with_blackboard_variables:
+ py_trees.display.render_dot_tree(root, with_blackboard_variables=True)
+ sys.exit()
+
+ ####################
+ # Execute
+ ####################
+ root.setup_with_descendants()
+ unset_blackboard = py_trees.blackboard.Client(name="Unsetter")
+ unset_blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE)
+ print("\n--------- Tick 0 ---------\n")
+ root.tick_once()
+ print("\n")
+ print(py_trees.display.unicode_tree(root, show_status=True))
+ print("--------------------------\n")
+ print(py_trees.display.unicode_blackboard())
+ print("--------------------------\n")
+ print(py_trees.display.unicode_blackboard(display_only_key_metadata=True))
+ print("--------------------------\n")
+ unset_blackboard.unset("foo")
+ print(py_trees.display.unicode_blackboard_activity_stream())
+
+if __name__ == "__main__":
+ main()
diff --git a/thirdparty/py_trees/demos/blackboard_namespaces.py b/thirdparty/py_trees/demos/blackboard_namespaces.py
new file mode 100644
index 0000000..97166a2
--- /dev/null
+++ b/thirdparty/py_trees/demos/blackboard_namespaces.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+.. argparse::
+ :module: py_trees.demos.blackboard_namespaces
+ :func: command_line_argument_parser
+ :prog: py-trees-demo-blackboard-namespaces
+
+.. figure:: images/blackboard_namespaces.png
+ :align: center
+
+ Console Screenshot
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import argparse
+import py_trees
+
+
+import py_trees.console as console
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+def description():
+ content = "Demonstrates usage of blackboard namespaces.\n"
+ content += "\n"
+
+ if py_trees.console.has_colours:
+ banner_line = console.green + "*" * 79 + "\n" + console.reset
+ s = "\n"
+ s += banner_line
+ s += console.bold_white + "Blackboard".center(79) + "\n" + console.reset
+ s += banner_line
+ s += "\n"
+ s += content
+ s += "\n"
+ s += banner_line
+ else:
+ s = content
+ return s
+
+
+def epilog():
+ if py_trees.console.has_colours:
+ return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset
+ else:
+ return None
+
+
+def command_line_argument_parser():
+ parser = argparse.ArgumentParser(description=description(),
+ epilog=epilog(),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ return parser
+
+
+##############################################################################
+# Main
+##############################################################################
+
+
+def main():
+ """
+ Entry point for the demo script.
+ """
+ unused_args = command_line_argument_parser().parse_args()
+ print(description())
+ print("-------------------------------------------------------------------------------")
+ print("$ py_trees.blackboard.Client(name='Blackboard')")
+ print("$ foo.register_key(key='dude', access=py_trees.common.Access.WRITE)")
+ print("$ foo.register_key(key='/dudette', access=py_trees.common.Access.WRITE)")
+ print("$ foo.register_key(key='/foo/bar/wow', access=py_trees.common.Access.WRITE)")
+ print("-------------------------------------------------------------------------------")
+ blackboard = py_trees.blackboard.Client(name="Blackboard")
+ blackboard.register_key(key="dude", access=py_trees.common.Access.WRITE)
+ blackboard.register_key(key="/dudette", access=py_trees.common.Access.WRITE)
+ blackboard.register_key(key="/foo/bar/wow", access=py_trees.common.Access.WRITE)
+ print(blackboard)
+ print("-------------------------------------------------------------------------------")
+ print("$ blackboard.dude = 'Bob'")
+ print("$ blackboard.dudette = 'Jade'")
+ print("-------------------------------------------------------------------------------")
+ blackboard.dude = "Bob"
+ blackboard.dudette = "Jade"
+ print(py_trees.display.unicode_blackboard())
+ print("-------------------------------------------------------------------------------")
+ print("$ blackboard.foo.bar.wow = 'foobar'")
+ print("-------------------------------------------------------------------------------")
+ blackboard.foo.bar.wow = "foobar"
+ print(py_trees.display.unicode_blackboard())
+ print("-------------------------------------------------------------------------------")
+ print("$ py_trees.blackboard.Client(name='Foo', namespace='foo')")
+ print("$ foo.register_key(key='awesome', access=py_trees.common.Access.WRITE)")
+ print("$ foo.register_key(key='/brilliant', access=py_trees.common.Access.WRITE)")
+ print("$ foo.register_key(key='/foo/clever', access=py_trees.common.Access.WRITE)")
+ print("-------------------------------------------------------------------------------")
+ foo = py_trees.blackboard.Client(name="Foo", namespace="foo")
+ foo.register_key(key="awesome", access=py_trees.common.Access.WRITE)
+ # TODO: should /brilliant be namespaced or go directly to root?
+ foo.register_key(key="/brilliant", access=py_trees.common.Access.WRITE)
+ # absolute names are ok, so long as they include the namespace
+ foo.register_key(key="/foo/clever", access=py_trees.common.Access.WRITE)
+ print(foo)
+ print("-------------------------------------------------------------------------------")
+ print("$ foo.awesome = True")
+ print("$ foo.set('/brilliant', False)")
+ print("$ foo.clever = True")
+ print("-------------------------------------------------------------------------------")
+ foo.awesome = True
+ # Only accessable via set since it's not in the namespace
+ foo.set("/brilliant", False)
+ # This will fail since it looks for the namespaced /foo/brilliant key
+ # foo.brilliant = False
+ foo.clever = True
+ print(py_trees.display.unicode_blackboard())
diff --git a/thirdparty/py_trees/demos/blackboard_remappings.py b/thirdparty/py_trees/demos/blackboard_remappings.py
new file mode 100644
index 0000000..bf58f25
--- /dev/null
+++ b/thirdparty/py_trees/demos/blackboard_remappings.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+.. argparse::
+ :module: py_trees.demos.blackboard_remappings
+ :func: command_line_argument_parser
+ :prog: py-trees-demo-blackboard-remappings
+
+.. figure:: images/blackboard_remappings.png
+ :align: center
+
+ Console Screenshot
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import argparse
+import py_trees
+import typing
+
+import py_trees.console as console
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+def description():
+ content = "Demonstrates usage of blackbord remappings.\n"
+ content += "\n"
+ content += "Demonstration is via an exemplar behaviour making use of remappings..\n"
+
+ if py_trees.console.has_colours:
+ banner_line = console.green + "*" * 79 + "\n" + console.reset
+ s = "\n"
+ s += banner_line
+ s += console.bold_white + "Blackboard".center(79) + "\n" + console.reset
+ s += banner_line
+ s += "\n"
+ s += content
+ s += "\n"
+ s += banner_line
+ else:
+ s = content
+ return s
+
+
+def epilog():
+ if py_trees.console.has_colours:
+ return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset
+ else:
+ return None
+
+
+def command_line_argument_parser():
+ parser = argparse.ArgumentParser(description=description(),
+ epilog=epilog(),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ return parser
+
+
+class Remap(py_trees.behaviour.Behaviour):
+ """
+ Custom writer that submits a more complicated variable to the blackboard.
+ """
+ def __init__(self, name: str, remap_to: typing.Dict[str, str]):
+ super().__init__(name=name)
+ self.logger.debug("%s.__init__()" % (self.__class__.__name__))
+ self.blackboard = self.attach_blackboard_client()
+ self.blackboard.register_key(
+ key="/foo/bar/wow",
+ access=py_trees.common.Access.WRITE,
+ remap_to=remap_to["/foo/bar/wow"]
+ )
+
+ def update(self):
+ """
+ Write a dictionary to the blackboard and return :data:`~py_trees.common.Status.SUCCESS`.
+ """
+ self.logger.debug("%s.update()" % (self.__class__.__name__))
+ self.blackboard.foo.bar.wow = "colander"
+
+ return py_trees.common.Status.SUCCESS
+
+##############################################################################
+# Main
+##############################################################################
+
+
+def main():
+ """
+ Entry point for the demo script.
+ """
+ args = command_line_argument_parser().parse_args()
+ print(description())
+ py_trees.logging.level = py_trees.logging.Level.DEBUG
+ py_trees.blackboard.Blackboard.enable_activity_stream(maximum_size=100)
+ root = Remap(name="Remap", remap_to={"/foo/bar/wow": "/parameters/wow"})
+
+ ####################
+ # Execute
+ ####################
+ root.tick_once()
+ print(root.blackboard)
+ print(py_trees.display.unicode_blackboard())
+ print(py_trees.display.unicode_blackboard_activity_stream())
diff --git a/thirdparty/py_trees/demos/context_switching.py b/thirdparty/py_trees/demos/context_switching.py
new file mode 100644
index 0000000..bce4a58
--- /dev/null
+++ b/thirdparty/py_trees/demos/context_switching.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+.. argparse::
+ :module: py_trees.demos.context_switching
+ :func: command_line_argument_parser
+ :prog: py-trees-demo-context-switching
+
+.. graphviz:: dot/demo-context_switching.dot
+
+.. image:: images/context_switching.gif
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import argparse
+import py_trees
+import sys
+import time
+
+import os,sys
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+import py_trees.console as console
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+def description():
+ content = "Demonstrates context switching with parallels and sequences.\n"
+ content += "\n"
+ content += "A context switching behaviour is run in parallel with a work sequence.\n"
+ content += "Switching the context occurs in the initialise() and terminate() methods\n"
+ content += "of the context switching behaviour. Note that whether the sequence results\n"
+ content += "in failure or success, the context switch behaviour will always call the\n"
+ content += "terminate() method to restore the context. It will also call terminate()\n"
+ content += "to restore the context in the event of a higher priority parent cancelling\n"
+ content += "this parallel subtree.\n"
+ if py_trees.console.has_colours:
+ banner_line = console.green + "*" * 79 + "\n" + console.reset
+ s = "\n"
+ s += banner_line
+ s += console.bold_white + "Context Switching".center(79) + "\n" + console.reset
+ s += banner_line
+ s += "\n"
+ s += content
+ s += "\n"
+ s += banner_line
+ else:
+ s = content
+ return s
+
+
+def epilog():
+ if py_trees.console.has_colours:
+ return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset
+ else:
+ return None
+
+
+def command_line_argument_parser():
+ parser = argparse.ArgumentParser(description=description(),
+ epilog=epilog(),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument('-r', '--render', action='store_true', help='render dot tree to file')
+ return parser
+
+
+class ContextSwitch(py_trees.behaviour.Behaviour):
+ """
+ An example of a context switching class that sets (in ``initialise()``)
+ and restores a context (in ``terminate()``). Use in parallel with a
+ sequence/subtree that does the work while in this context.
+
+ .. attention:: Simply setting a pair of behaviours (set and reset context) on
+ either end of a sequence will not suffice for context switching. In the case
+ that one of the work behaviours in the sequence fails, the final reset context
+ switch will never trigger.
+
+ """
+ def __init__(self, name="ContextSwitch"):
+ super(ContextSwitch, self).__init__(name)
+ self.feedback_message = "no context"
+
+ def initialise(self):
+ """
+ Backup and set a new context.
+ """
+ self.logger.debug("%s.initialise()[switch context]" % (self.__class__.__name__))
+ # Some actions that:
+ # 1. retrieve the current context from somewhere
+ # 2. cache the context internally
+ # 3. apply a new context
+ self.feedback_message = "new context"
+
+ def update(self):
+ """
+ Just returns RUNNING while it waits for other activities to finish.
+ """
+ self.logger.debug("%s.update()[RUNNING][%s]" % (self.__class__.__name__, self.feedback_message))
+ return py_trees.common.Status.RUNNING
+
+ def terminate(self, new_status):
+ """
+ Restore the context with the previously backed up context.
+ """
+ self.logger.debug("%s.terminate()[%s->%s][restore context]" % (self.__class__.__name__, self.status, new_status))
+ # Some actions that:
+ # 1. restore the cached context
+ self.feedback_message = "restored context"
+
+
+def create_root():
+ root = py_trees.composites.Parallel(name="Parallel", policy=py_trees.common.ParallelPolicy.SuccessOnOne())
+ context_switch = ContextSwitch(name="Context")
+ sequence = py_trees.composites.Sequence(name="Sequence")
+ for job in ["Action 1", "Action 2"]:
+ success_after_two = py_trees.behaviours.Count(name=job,
+ fail_until=0,
+ running_until=2,
+ success_until=10)
+ sequence.add_child(success_after_two)
+ root.add_child(context_switch)
+ root.add_child(sequence)
+ return root
+
+
+##############################################################################
+# Main
+##############################################################################
+
+def main():
+ """
+ Entry point for the demo script.
+ """
+ args = command_line_argument_parser().parse_args()
+ print(description())
+ py_trees.logging.level = py_trees.logging.Level.DEBUG
+
+ root = create_root()
+
+ ####################
+ # Rendering
+ ####################
+ if args.render:
+ py_trees.display.render_dot_tree(root)
+ sys.exit()
+
+ ####################
+ # Execute
+ ####################
+ root.setup_with_descendants()
+ for i in range(1, 6):
+ try:
+ print("\n--------- Tick {0} ---------\n".format(i))
+ root.tick_once()
+ print("\n")
+ print("{}".format(py_trees.display.unicode_tree(root, show_status=True)))
+ time.sleep(1.0)
+ except KeyboardInterrupt:
+ break
+ print("\n")
diff --git a/thirdparty/py_trees/demos/display_modes.py b/thirdparty/py_trees/demos/display_modes.py
new file mode 100644
index 0000000..72c03ec
--- /dev/null
+++ b/thirdparty/py_trees/demos/display_modes.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+.. argparse::
+ :module: py_trees.demos.display_modes
+ :func: command_line_argument_parser
+ :prog: py-trees-demo-display-modes
+
+.. figure:: images/display_modes.png
+ :align: center
+
+ Console Screenshot
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import argparse
+import itertools
+import py_trees
+
+import py_trees.console as console
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+def description():
+ content = "Demonstrates usage of the ascii/unicode display modes.\n"
+ content += "\n"
+ content += "...\n"
+ content += "...\n"
+
+ if py_trees.console.has_colours:
+ banner_line = console.green + "*" * 79 + "\n" + console.reset
+ s = "\n"
+ s += banner_line
+ s += console.bold_white + "Display Modes".center(79) + "\n" + console.reset
+ s += banner_line
+ s += "\n"
+ s += content
+ s += "\n"
+ s += banner_line
+ else:
+ s = content
+ return s
+
+
+def epilog():
+ if py_trees.console.has_colours:
+ return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset
+ else:
+ return None
+
+
+def command_line_argument_parser():
+ parser = argparse.ArgumentParser(description=description(),
+ epilog=epilog(),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ return parser
+
+
+def create_root() -> py_trees.behaviour.Behaviour:
+ """
+ Create the tree to be ticked/displayed.
+
+ Returns:
+ the root of the tree
+ """
+ root = py_trees.composites.Sequence(name="root")
+ child = py_trees.composites.Sequence(name="child1")
+ child2 = py_trees.composites.Sequence(name="child2")
+ child3 = py_trees.composites.Sequence(name="child3")
+ root.add_child(child)
+ root.add_child(child2)
+ root.add_child(child3)
+
+ child.add_child(py_trees.behaviours.Count(name='Count', fail_until=0, running_until=1, success_until=6,))
+ child2.add_child(py_trees.behaviours.Count(name='Count', fail_until=0, running_until=1, success_until=6,))
+ child2_child1 = py_trees.composites.Sequence(name="Child2_child1")
+ child2_child1.add_child(py_trees.behaviours.Count(name='Count', fail_until=0, running_until=1, success_until=6,))
+ child2.add_child(child2_child1)
+ child3.add_child(py_trees.behaviours.Count(name='Count', fail_until=0, running_until=1, success_until=6,))
+ return root
+
+
+##############################################################################
+# Main
+##############################################################################
+
+
+def main():
+ """
+ Entry point for the demo script.
+ """
+ unused_args = command_line_argument_parser().parse_args()
+ print(description())
+ print("-------------------------------------------------------------------------------")
+ print("$ py_trees.blackboard.Client(name='Blackboard')")
+ print("$ foo.register_key(key='dude', access=py_trees.common.Access.WRITE)")
+ print("$ foo.register_key(key='/dudette', access=py_trees.common.Access.WRITE)")
+ print("$ foo.register_key(key='/foo/bar/wow', access=py_trees.common.Access.WRITE)")
+ print("-------------------------------------------------------------------------------")
+
+ snapshot_visitor = py_trees.visitors.SnapshotVisitor()
+ tree = py_trees.trees.BehaviourTree(create_root())
+ tree.add_visitor(snapshot_visitor)
+
+ for tick in range(2):
+ tree.tick()
+ for show_visited, show_status in itertools.product([False, True], [False, True]):
+ console.banner("Tick {} / show_only_visited=={} / show_status=={}".format(tick, show_visited, show_status))
+ print(
+ py_trees.display.unicode_tree(
+ tree.root,
+ show_status=show_status,
+ show_only_visited=show_visited,
+ visited=snapshot_visitor.visited,
+ previously_visited=snapshot_visitor.previously_visited
+ )
+ )
+ print()
diff --git a/thirdparty/py_trees/demos/dot_graphs.py b/thirdparty/py_trees/demos/dot_graphs.py
new file mode 100644
index 0000000..f8526b0
--- /dev/null
+++ b/thirdparty/py_trees/demos/dot_graphs.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+.. argparse::
+ :module: py_trees.demos.dot_graphs
+ :func: command_line_argument_parser
+ :prog: py-trees-demo-dot-graphs
+
+.. graphviz:: dot/demo-dot-graphs.dot
+
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import argparse
+import subprocess
+import py_trees
+
+import py_trees.console as console
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+def description():
+ name = "py-trees-demo-dot-graphs"
+ content = "Renders a dot graph for a simple tree, with blackboxes.\n"
+ if py_trees.console.has_colours:
+ banner_line = console.green + "*" * 79 + "\n" + console.reset
+ s = "\n"
+ s += banner_line
+ s += console.bold_white + "Dot Graphs".center(79) + "\n" + console.reset
+ s += banner_line
+ s += "\n"
+ s += content
+ s += "\n"
+ s += console.white
+ s += console.bold + " Generate Full Dot Graph" + console.reset + "\n"
+ s += "\n"
+ s += console.cyan + " {0}".format(name) + console.reset + "\n"
+ s += "\n"
+ s += console.bold + " With Varying Visibility Levels" + console.reset + "\n"
+ s += "\n"
+ s += console.cyan + " {0}".format(name) + console.yellow + " --level=all" + console.reset + "\n"
+ s += console.cyan + " {0}".format(name) + console.yellow + " --level=detail" + console.reset + "\n"
+ s += console.cyan + " {0}".format(name) + console.yellow + " --level=component" + console.reset + "\n"
+ s += console.cyan + " {0}".format(name) + console.yellow + " --level=big_picture" + console.reset + "\n"
+ s += "\n"
+ s += banner_line
+ else:
+ s = content
+ return s
+
+
+def epilog():
+ if py_trees.console.has_colours:
+ return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset
+ else:
+ return None
+
+
+def command_line_argument_parser():
+ parser = argparse.ArgumentParser(description=description(),
+ epilog=epilog(),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument('-l', '--level', action='store',
+ default='fine_detail',
+ choices=['all', 'fine_detail', 'detail', 'component', 'big_picture'],
+ help='visibility level')
+ return parser
+
+
+def create_tree(level):
+ root = py_trees.composites.Selector("Demo Dot Graphs %s" % level)
+ first_blackbox = py_trees.composites.Sequence("BlackBox 1")
+ first_blackbox.add_child(py_trees.behaviours.Running("Worker"))
+ first_blackbox.add_child(py_trees.behaviours.Running("Worker"))
+ first_blackbox.add_child(py_trees.behaviours.Running("Worker"))
+ first_blackbox.blackbox_level = py_trees.common.BlackBoxLevel.BIG_PICTURE
+ second_blackbox = py_trees.composites.Sequence("Blackbox 2")
+ second_blackbox.add_child(py_trees.behaviours.Running("Worker"))
+ second_blackbox.add_child(py_trees.behaviours.Running("Worker"))
+ second_blackbox.add_child(py_trees.behaviours.Running("Worker"))
+ second_blackbox.blackbox_level = py_trees.common.BlackBoxLevel.COMPONENT
+ third_blackbox = py_trees.composites.Sequence("Blackbox 3")
+ third_blackbox.add_child(py_trees.behaviours.Running("Worker"))
+ third_blackbox.add_child(py_trees.behaviours.Running("Worker"))
+ third_blackbox.add_child(py_trees.behaviours.Running("Worker"))
+ third_blackbox.blackbox_level = py_trees.common.BlackBoxLevel.DETAIL
+ root.add_child(first_blackbox)
+ root.add_child(second_blackbox)
+ first_blackbox.add_child(third_blackbox)
+ return root
+
+
+##############################################################################
+# Main
+##############################################################################
+
+def main():
+ """
+ Entry point for the demo script.
+ """
+ args = command_line_argument_parser().parse_args()
+ args.enum_level = py_trees.common.string_to_visibility_level(args.level)
+ print(description())
+ py_trees.logging.level = py_trees.logging.Level.DEBUG
+
+ root = create_tree(args.level)
+ py_trees.display.render_dot_tree(root, args.enum_level)
+
+ if py_trees.utilities.which("xdot"):
+ try:
+ subprocess.call(["xdot", "demo_dot_graphs_%s.dot" % args.level])
+ except KeyboardInterrupt:
+ pass
+ else:
+ print("")
+ console.logerror("No xdot viewer found, skipping display [hint: sudo apt install xdot]")
+ print("")
diff --git a/thirdparty/py_trees/demos/either_or.py b/thirdparty/py_trees/demos/either_or.py
new file mode 100644
index 0000000..59bd04a
--- /dev/null
+++ b/thirdparty/py_trees/demos/either_or.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python
+#
+# License: BSD
+# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
+#
+##############################################################################
+# Documentation
+##############################################################################
+
+"""
+.. argparse::
+ :module: py_trees.demos.either_or
+ :func: command_line_argument_parser
+ :prog: py-trees-demo-either-or
+
+.. graphviz:: dot/demo-either-or.dot
+
+.. image:: images/either_or.gif
+"""
+
+##############################################################################
+# Imports
+##############################################################################
+
+import argparse
+import functools
+import operator
+import py_trees
+import sys
+import time
+
+import py_trees.console as console
+
+##############################################################################
+# Classes
+##############################################################################
+
+
+def description(root):
+ content = "A demonstration of the 'either_or' idiom.\n\n"
+ content += "This behaviour tree pattern enables triggering of subtrees\n"
+ content += "with equal priority (first in, first served).\n"
+ content += "\n"
+ content += "EVENTS\n"
+ content += "\n"
+ content += " - 3 : joystick one enabled, task one starts\n"
+ content += " - 5 : task one finishes\n"
+ content += " - 6 : joystick two enabled, task two starts\n"
+ content += " - 7 : joystick one enabled, task one ignored, task two continues\n"
+ content += " - 8 : task two finishes\n"
+ content += "\n"
+ if py_trees.console.has_colours:
+ banner_line = console.green + "*" * 79 + "\n" + console.reset
+ s = "\n"
+ s += banner_line
+ s += console.bold_white + "Either Or".center(79) + "\n" + console.reset
+ s += banner_line
+ s += "\n"
+ s += content
+ s += "\n"
+ s += banner_line
+ else:
+ s = content
+ return s
+
+
+def epilog():
+ if py_trees.console.has_colours:
+ return console.cyan + "And his noodly appendage reached forth to tickle the blessed...\n" + console.reset
+ else:
+ return None
+
+
+def command_line_argument_parser():
+ parser = argparse.ArgumentParser(description=description(create_root()),
+ epilog=epilog(),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument('-r', '--render', action='store_true', help='render dot tree to file')
+ group.add_argument('-i', '--interactive', action='store_true', help='pause and wait for keypress at each tick')
+ return parser
+
+
+def pre_tick_handler(behaviour_tree):
+ print("\n--------- Run %s ---------\n" % behaviour_tree.count)
+
+
+def post_tick_handler(snapshot_visitor, behaviour_tree):
+ print(
+ "\n" + py_trees.display.unicode_tree(
+ root=behaviour_tree.root,
+ visited=snapshot_visitor.visited,
+ previously_visited=snapshot_visitor.previously_visited
+ )
+ )
+ print(py_trees.display.unicode_blackboard())
+
+
+def create_root():
+ trigger_one = py_trees.decorators.FailureIsRunning(
+ name="FisR",
+ child=py_trees.behaviours.SuccessEveryN(
+ name="Joystick 1",
+ n=4
+ )
+ )
+ trigger_two = py_trees.decorators.FailureIsRunning(
+ name="FisR",
+ child=py_trees.behaviours.SuccessEveryN(
+ name="Joystick 2",
+ n=7
+ )
+ )
+ enable_joystick_one = py_trees.behaviours.SetBlackboardVariable(
+ name="Joy1 - Enabled",
+ variable_name="joystick_one",
+ variable_value="enabled")
+ enable_joystick_two = py_trees.behaviours.SetBlackboardVariable(
+ name="Joy2 - Enabled",
+ variable_name="joystick_two",
+ variable_value="enabled")
+ reset_joystick_one = py_trees.behaviours.SetBlackboardVariable(
+ name="Joy1 - Disabled",
+ variable_name="joystick_one",
+ variable_value="disabled")
+ reset_joystick_two = py_trees.behaviours.SetBlackboardVariable(
+ name="Joy2 - Disabled",
+ variable_name="joystick_two",
+ variable_value="disabled")
+ task_one = py_trees.behaviours.TickCounter(
+ name="Task 1",
+ duration=2,
+ completion_status=py_trees.common.Status.SUCCESS
+ )
+ task_two = py_trees.behaviours.TickCounter(
+ name="Task 2",
+ duration=2,
+ completion_status=py_trees.common.Status.SUCCESS
+ )
+ idle = py_trees.behaviours.Running(name="Idle")
+ either_or = py_trees.idioms.either_or(
+ name="Either Or",
+ conditions=[
+ py_trees.common.ComparisonExpression("joystick_one", "enabled", operator.eq),
+ py_trees.common.ComparisonExpression("joystick_two", "enabled", operator.eq),
+ ],
+ subtrees=[task_one, task_two],
+ namespace="either_or",
+ )
+ root = py_trees.composites.Parallel(
+ name="Root",
+ policy=py_trees.common.ParallelPolicy.SuccessOnAll(synchronise=False)
+ )
+ reset = py_trees.composites.Sequence(name="Reset")
+ reset.add_children([reset_joystick_one, reset_joystick_two])
+ joystick_one_events = py_trees.composites.Sequence(name="Joy1 Events")
+ joystick_one_events.add_children([trigger_one, enable_joystick_one])
+ joystick_two_events = py_trees.composites.Sequence(name="Joy2 Events")
+ joystick_two_events.add_children([trigger_two, enable_joystick_two])
+ tasks = py_trees.composites.Selector(name="Tasks")
+ tasks.add_children([either_or, idle])
+ root.add_children([reset, joystick_one_events, joystick_two_events, tasks])
+ return root
+
+
+##############################################################################
+# Main
+##############################################################################
+
+
+def main():
+ """
+ Entry point for the demo script.
+ """
+ args = command_line_argument_parser().parse_args()
+ # py_trees.logging.level = py_trees.logging.Level.DEBUG
+ root = create_root()
+ print(description(root))
+
+ ####################
+ # Rendering
+ ####################
+ if args.render:
+ py_trees.display.render_dot_tree(root)
+ sys.exit()
+
+ ####################
+ # Tree Stewardship
+ ####################
+ behaviour_tree = py_trees.trees.BehaviourTree(root)
+ behaviour_tree.add_pre_tick_handler(pre_tick_handler)
+ behaviour_tree.visitors.append(py_trees.visitors.DebugVisitor())
+ snapshot_visitor = py_trees.visitors.SnapshotVisitor()
+ behaviour_tree.add_post_tick_handler(functools.partial(post_tick_handler, snapshot_visitor))
+ behaviour_tree.visitors.append(snapshot_visitor)
+ behaviour_tree.setup(timeout=15)
+
+ ####################
+ # Tick Tock
+ ####################
+ if args.interactive:
+ py_trees.console.read_single_keypress()
+ for unused_i in range(1, 11):
+ try:
+ behaviour_tree.tick()
+ if args.interactive:
+ py_trees.console.read_single_keypress()
+ else:
+ time.sleep(0.5)
+ except KeyboardInterrupt:
+ break
+ print("\n")
diff --git a/thirdparty/py_trees/demos/get-pip.py b/thirdparty/py_trees/demos/get-pip.py
new file mode 100644
index 0000000..efc3a26
--- /dev/null
+++ b/thirdparty/py_trees/demos/get-pip.py
@@ -0,0 +1,27079 @@
+#!/usr/bin/env python
+#
+# Hi There!
+#
+# You may be wondering what this giant blob of binary data here is, you might
+# even be worried that we're up to something nefarious (good for you for being
+# paranoid!). This is a base85 encoding of a zip file, this zip file contains
+# an entire copy of pip (version 21.3.1).
+#
+# Pip is a thing that installs packages, pip itself is a package that someone
+# might want to install, especially if they're looking to run this get-pip.py
+# script. Pip has a lot of code to deal with the security of installing
+# packages, various edge cases on various platforms, and other such sort of
+# "tribal knowledge" that has been encoded in its code base. Because of this
+# we basically include an entire copy of pip inside this blob. We do this
+# because the alternatives are attempt to implement a "minipip" that probably
+# doesn't do things correctly and has weird edge cases, or compress pip itself
+# down into a single file.
+#
+# If you're wondering how this is created, it is generated using
+# `scripts/generate.py` in https://github.com/pypa/get-pip.
+
+import sys
+
+this_python = sys.version_info[:2]
+min_version = (3, 6)
+if this_python < min_version:
+ message_parts = [
+ "This script does not work on Python {}.{}.".format(*this_python),
+ "The minimum supported Python version is {}.{}.".format(*min_version),
+ "Please use https://bootstrap.pypa.io/pip/{}.{}/get-pip.py instead.".format(*this_python),
+ ]
+ print("ERROR: " + " ".join(message_parts))
+ sys.exit(1)
+
+
+import os.path
+import pkgutil
+import shutil
+import tempfile
+import argparse
+import importlib
+from base64 import b85decode
+
+
+def include_setuptools(args):
+ """
+ Install setuptools only if absent, not excluded and when using Python <3.12.
+ """
+ cli = not args.no_setuptools
+ env = not os.environ.get("PIP_NO_SETUPTOOLS")
+ absent = not importlib.util.find_spec("setuptools")
+ python_lt_3_12 = this_python < (3, 12)
+ return cli and env and absent and python_lt_3_12
+
+
+def include_wheel(args):
+ """
+ Install wheel only if absent, not excluded and when using Python <3.12.
+ """
+ cli = not args.no_wheel
+ env = not os.environ.get("PIP_NO_WHEEL")
+ absent = not importlib.util.find_spec("wheel")
+ python_lt_3_12 = this_python < (3, 12)
+ return cli and env and absent and python_lt_3_12
+
+
+def determine_pip_install_arguments():
+ pre_parser = argparse.ArgumentParser()
+ pre_parser.add_argument("--no-setuptools", action="store_true")
+ pre_parser.add_argument("--no-wheel", action="store_true")
+ pre, args = pre_parser.parse_known_args()
+
+ args.append("pip<22.0")
+
+ if include_setuptools(pre):
+ args.append("setuptools")
+
+ if include_wheel(pre):
+ args.append("wheel")
+
+ return ["install", "--upgrade", "--force-reinstall"] + args
+
+
+def monkeypatch_for_cert(tmpdir):
+ """Patches `pip install` to provide default certificate with the lowest priority.
+
+ This ensures that the bundled certificates are used unless the user specifies a
+ custom cert via any of pip's option passing mechanisms (config, env-var, CLI).
+
+ A monkeypatch is the easiest way to achieve this, without messing too much with
+ the rest of pip's internals.
+ """
+ from pip._internal.commands.install import InstallCommand
+
+ # We want to be using the internal certificates.
+ cert_path = os.path.join(tmpdir, "cacert.pem")
+ with open(cert_path, "wb") as cert:
+ cert.write(pkgutil.get_data("pip._vendor.certifi", "cacert.pem"))
+
+ install_parse_args = InstallCommand.parse_args
+
+ def cert_parse_args(self, args):
+ if not self.parser.get_default_values().cert:
+ # There are no user provided cert -- force use of bundled cert
+ self.parser.defaults["cert"] = cert_path # calculated above
+ return install_parse_args(self, args)
+
+ InstallCommand.parse_args = cert_parse_args
+
+
+def bootstrap(tmpdir):
+ monkeypatch_for_cert(tmpdir)
+
+ # Execute the included pip and use it to install the latest pip and
+ # any user-requested packages from PyPI.
+ from pip._internal.cli.main import main as pip_entry_point
+ args = determine_pip_install_arguments()
+ sys.exit(pip_entry_point(args))
+
+
+def main():
+ tmpdir = None
+ try:
+ # Create a temporary working directory
+ tmpdir = tempfile.mkdtemp()
+
+ # Unpack the zipfile into the temporary directory
+ pip_zip = os.path.join(tmpdir, "pip.zip")
+ with open(pip_zip, "wb") as fp:
+ fp.write(b85decode(DATA.replace(b"\n", b"")))
+
+ # Add the zipfile to sys.path so that we can import it
+ sys.path.insert(0, pip_zip)
+
+ # Run the bootstrap
+ bootstrap(tmpdir=tmpdir)
+ finally:
+ # Clean up our temporary working directory
+ if tmpdir:
+ shutil.rmtree(tmpdir, ignore_errors=True)
+
+
+DATA = b"""
+P)h>@6aWAK2mt$eR#TliG(h+O003nH000jF003}la4%n9X>MtBUtcb8c|B0UO2j}6z0X&KUUXrdvMRV
+16ubz6s0VM$QfAw<4YV^ulDhQoop$MlK*;0e$L01LzdVw?IP-tnf*qTlkJj!Mom=viw7qw3H>hKz6
+^g4|bOsV`^+*aO7_tw^Cd$4zs{Pl#j>6{|X*AaQ6!2wJ?w>%d+2&1X4Rc!^r6h-hMtH_d5{IF3D`nKTt~p1QY-O00;p4c~(;+8{@xi0ssK6
+1ONaJ0001RX>c!JUu|J&ZeL$6aCu!*!ET%|5WVviBXU@FMMw_qp;5O|rCxIBA*z%^Qy~|I#aghDZI*1
+mzHbcdCgFtbH*em&nbG}VT_EcdJ^%Uh<#$rfXmjvMazjtt+Y{4fL(0@tjn1(F!nz|6RBOjouLCQKB%tCsn
+f_O;(TkT9D!5I2G1vZWcORK<
+*}iONjWAr8Zm1&KuL0jC{@?djd+x5R}RGfYPBawx08>U(W?WmDk1T9S4?epCt{Z(ueTz)EC*E`5mT15
+-&2~-DsS-6=uU3I|BmObEPJI*Sr)^2!Om@h-$wOJl_c@O>A_3OHg5wqIeD(E7`y@m0ou*N^~8Scf|wu
+`N_HtL5`*k&gASg%W(oQp9a7<~IpnR_S}F8z9z|q{`1rb)-o!>My0eex)q(ByedFLGyO7=Ikq8}(HcH
+6i;acy-%V$hD`fEosH@wgA+8z#{H{ToXOd_?&uMj~(yRVmD7BE?-`X6FU!78rkLs#HE1jqSOWnjp~Z3(}j4wN{#<0DmEaw
+w2fbN$l@K=F!>KqO9KQH000080Q-4XQ_aV~HNXG>03HDV01N;C0B~t=FK~G-ba`-PWF?NVPQ@?`MfZN
+i-B_Ob4-5=!Z$M%;t!XVKc9b}U{5?(?Egj!;iWEo#VY8e`cO+3psdiM#D?U$24DrcGE{QX%^A1rwho7
+bo%%^4nEOe11`ih5ds}r~C4-D(by*bnzy~VhcmspFPs+92he4iKm495?R6(6IB9*bzqWO6Z``e?dj4>
+$ei>cuLo8^bh>J0qwmAsn45g@9MQ{TAMQ=}M~B1K+Woqz5;+g_LK&{q3XhT~awQHE!$j2T)4`1QY-O0
+0;p4c~(=FwQ!+P0RR9!0ssIR0001RX>c!JX>N37a&BR4FJE72ZfSI1UoLQYb&)Yo#4rqn_xuX$SgsPJ
+3leY=j7%q3*bq8})@_Z_B-k#f{~ot+ASB2V>&bck^4xJALFYoL2O3Leg*}O$!hKQ7DMaVKUUslOCh)if@+itrPZeClT~
+1iR*^N=_&VilHX7ezR{Ys!P3i6v#8#CnCLX(r^h#(D9Q2`wcYz#AqB@vfzGIq$A8sk{)NWEK&TeAplO
+P?6fq6Q1^6a*0l)grsP?n#H~**AHt%UnWjY1bq&q0|@WSC{?>xZeNm!(&pOOE&dqH}AXz$)6~;-HFq;
+xJFdD4^T@31QY-O00;p4c~(c!JX>N37a&BR4FJg6RY-C?$ZgwtkdDU9
+obKAHPf7f4uG7lkBlGE!=dmYW`dihW;o~E`ZcCNi@JUohoY@8{Q1wh-1$NzhG@j(J4?IbgOIlV{(b{C
+7?A9fc@1wrttV^vAk^$p`qy{EM#ouDPzHJmWfRJmkLP0Eh5`jUu}2}!od0gsCy2o?*rZyPR2(bSUO$%
+<|5NYz|kB9(b;g#Fd#^2(tThkgbn-15A&&!1SkV-;QOc(aEUs)`n$97g+j+@~e@<#e6Bez$)8kE7$CVsa!Y&$ksdzhuK>@*WHllam(J%Bz^1
+QFuJ>TBK59wcM7qX?8>Fvf*h#xnw(L7rDKHEljCe&?`s#rJVk^W1OOEdeuJ+V^6W(P%hAYhU;hjIOt?
+2vJB0fWh56koK;Ps{O-tR;9d?}OpA)80<2VnFw5Vxw9d@n9FLVJJhuS000yys;B?3CXqmx?Fhd=u2$L
+Ckdn)rXm$@sB4hWuO=_IQ}D!OgUn}Uj7lOnIGY#4r=RnmQ%m5le`faf>hg931HhzU-^Y`1Ea-aQB*l>FFREh)$5jY2R>#sl
+UWuDTJ2(W2$w`i9+Bh+a@^EZli~*{QY3)2@XMbNRCX=Qyv-{?{i!Xhm5ElvxeI#=`~
+D?LO(6baf!usZ{VAodthv$pdbX0
+|q%mA+?{9piAA{!!stmrt0q3V$EuC6g`A+nT|BM2+3s>qh=U=AFthLvH+k0!N|r9wJ!j!=fA$q`6UAS{-Ga
+(eL*g?2>_1%t|33HNce3`{^o3NV21-EIponyvONX0_bnWq$thPGHCZ|R4{P7TcWCqn9dCn}ym+Ans=a
+>N`DS#m=&*%-GN%tdJ~G8IL_C>r_Dv+ba=a!
+VgIRWan$LZbsL6xMVoz~81qqTbPQPn$khB?V(*t%SlRv3Mo`_qk>@hbk}Cq^~|6y?>LfkAH@&35JAK5
+1H1mT%Gd{5bFm(8}bk^PW|M^=@6rHY?DanXt&!q{>@Tx$k!rQJLg}2s(t4ScUH=DGin8&9C__68M{q(n|SlK>S*QE*7Gx8RY1
+y_%fH?47QqMfUPB}6DyjinRLhBK%obEtt2A~Q7~W)A$hSzb)&uj}Tv)}7c^_QTFovq0k$sq=1AQ=^!BI_En|a4Ggg|)oU`+0c4al`EZ^4``D&Jo4JP3a?0bO$_CR%5e3
+;C?%8VZV>f;=0;gV_JE2j!!vqIu#XFj{2k`%(_hZtog5#Zd^}r!I6FFD4`YhLjqyVzYvPBOINd1HRAuH7*&S^3x&tM`*%12XpXMC8OX0OawPi)z@ur}DW+Ps43vA!#)8oaph8K7Qr=zY?W=&dW*ZMPZ18Dt|G;kK{
+AiVjxgnL587Vp4G7UWB8xZ(w71#7OpxuiK^#`{kx*0~-@h@ox+-R-gUCgZ+yuT3l!DoqOa7ypKZ>Yg>
+z|kR2?e8i|`TDmVHU%*J>b1&?5-QBhwE>Opiex9vY;4iv*of}Mn21$91#TvwkZQj%szLUU)K5W{bCh(
+2Yclp_+C7LKSr6XH=Z$l@y0|F&G?qQO;cJjb*=-vK(_jaq);Q-KvB1#&Vlm%a>)MdAlWL9EkQ4GqgQ3
+#cym3KhY)n&Bg7+YWJ#OscgtVdS0DZRpX()g-PD3
+%YgU&v7i+@)gc%pnw*b)O0bLkv_MU&s$$2iVCK{?YTI$3Ud-omnn$bD)cA_YTv0sIx$}Gdd
+HE%@ukz>K$yxvWLX&7GC<|s~W~5iG3FtTNLAap0_*CC#LCUAJrYs>D8_w*_}zSS*WgOg}n3GpON#EHz
+!Lt<@@G_s`eq-R!wn@Z((Y6c~b9xzD@s1MAzcYD$_XxN;c0jQirMpBeZU6GkcjOB60
+4Q`YS~WSoHZvD_R*%G~z9xkChTKxJ1E^rGw9VcCl1l(C164}0Jz$lMqMWYkNE*J~={u=?%UHGEOpX0q
+^cAsRY|r%$zgMlp?`EP0w$iWxjR176^{_S`Zof(V1*vr;4qTY(QrNgTe60CC*NJ)h%*`!1Eywg(oQ}I
+Pr?VR6({Xd?{0URA{RqlRR%j;=CEU}SaFrh&@c(BNS=wIUSH=(Q1dn=jzMl@*CZk0sr`CVmFM+YisCo
+{Pgk555Ea`*%l%j5uPIzW86SMD~Otez{h#5(@Q2izPO;ch~?nv;iGy3%%Rt)Ri4qs&7(D(F)RuHScAK
+vM`S-<-DlYcDGhPL|{Bsb303kw^4`BcY)HCSI|*hkQHnVZOsfv!E-gd539!X|u&fb&!9z5|JbT+#&cS!ES;s51
+gt5Rd=K63LopF(|!6rx;Y+xYW{ORIiT95)Y&of1ZPIJh=Szb)z;%J3Lu_uZv0WMh35$G&8jj}$R5XFj
+T1gnbG*Ql2T1bk&U_VmURq)QZC5GxrMj-65NRU@P$SMpAP6EhtCjA%oeASnpPuM6+0U_`>XZ*DV;m~e
+PGttebj=R^-C0J>mK5*~iYJo@Z>P6ALS_LMCiAsf$_-MMkt@tpFfx#M7mC&*5ZPP4P~m&b2jzCSr$XR
+p^E&V!}?2T2$W4S>G2ZU2)Inpmx>A~WXiXY@CS5Y>w<>B@Y^zD_IeX?Tft+?=%I7ir;mAnISOy@Z-CX
+52MBZ
+08mQ<1QY-O00;p4c~(;e)76y~3jhG&Bme*w0001RX>c!JX>N37a&BR4FJob2Xk{*NdEHuVkK4u({(iq
+=s~|8{$h^QvT~vSyB+j`u;G{+D!XFL?Vnwc`wJ9#kUEWy<`rrG^?2=rP(#1`IHmE-6#C@5a_jzV{i^b
+xF%nwR@FDtoMM^(A2#bR-FrH{2~oH$5(DD}2`{9sMh{VvUZud99cXzbOlF-PG}HAY1k{iZst#CJM(EA
+d8KeE+p}+ElV!iMPsK`7O1s)9hYVg=x}S<{u@|O`Y7^j?6o`UkP0~)zpo`cUH-x8jswo#)9%=6kDguo
+@6d7Q|Vlm`X|NYVrG~yxJ=cjTrtP}zSq?~_7v|AN|i5lsd(#|okvrs(xyAp9Hq;0Q@O^J9g&wj`oa%B
+vb)sP$8OIX{C;HV12NRCW$w-`W)-AP9qX*nO|M=&f2SLjJJY~kG>zHpqpk{jnM&IX+N`BJWX@z5ySgI
+JP>tAhE|Tt*d&6T%#;VS;<<-?yp>`r82Lmg)ONuo+%B^+HO5p2mDW3kBeypzqKJdyPm1~le#qdQhJVy;s&HBxYVpYXwJHFUdEMVhhn^4oBqqr=o7my)Kl6X
+Hq~G!5$hTa3WDiCk5MroWg=I(OQ!LN56$Ex)$%Sw=u?%S{#1!R2nZHyW|=%D$Mo+4x=q2&kVddgENoX
+F%kM~H55&ZZ573Oqh#S(JAa@oOY@+L%pYvm;^Cn4L*T>GsXGLc9d^UArY#HD*))L^eUc}9@ac(=RUw{
+O(>A%nL!)@BsmfD#mOzlU$}T&Fdu_4D!Hu=cvZN<#Rk>TmDr66wYH6gH)m$c|GjiQKCd;n-gQjZMOL5RNQliq-}*DPR8_>VzZeX5t$RYCG%o)4uZ!yn|PB_psYD>vOTB(v55wwz%%}$D0?;D5
+9tSdNjhJ$TAS*}lvR9QtW>N4|ft*M~t;G{gX`A5WNJJ~~fJish
+6W8sG$bBFd8ugSml7IjG$2Z_8m-MW`q23>;K;MH&Oe2{iZzCaBym;5hJy-LA9tBN*TuxCVx27d|jg6u
+VYxFWW-Jm_v9Ja+DtsZ6x7QSNIi>2w9>xbVLZQkegb0SJGwqbgRgS$aV!B)Oz>Zwi
+@}5%Gz$H8n7a`zDHyVRRiBp`ZeC-^$Dh_`qMFlG+a;$Q%0selk?yP2#4o&4_)5mqx`T7Ya+o8cK
+yP)a-ANEKOCtgY=W4sYzTQKkcAH}Hb$zPkH9*6)wibE#`j5~4^nC7Nw~HyJV}ncwqf~ieYY=+2JB(8u
+9@xF{Qc@s-9ET^{!WVQ3$tPtgeAKbp`jCV735!Zt$|j;`Ro*tF7gTWMct?d1MkaE9c)o%uou@CUtTp5
+}&Nx|u0as%#f&OFnIJ8!v8d^_REmQFdDe|7SjqGBB(T?&X;tN=d>UZoQg9Qmckhmm9F7btQ5k))&75r
+}#qp@Dm%L^H<0=@|x4M4>j#62dV{(Ev-I5zp5#8}0E#MBYB5{t^w{s&@=l9W$w?5i!~62#|dCFs#%5w
+(|Z2Z_4;b?ZgDT|c{91u<`*t-l@~zFt2c9-go7?gnWC++$L+dV|OVAXD>7vl<$U%y%BXyI@o?lp*v*Q
+5nLP3<=TKF|bX^aZ=l1L5~m45$|S+jW|1w=#CR&knT1Tcqrdzzye|TY*MY0^V}?B7J5;pm7c>CE>5UD
+=_>s%@;GRota}$3903*>Cr%j)fNDl6N$6|D)qt#^+k}2jj;4s|&!P-ys2Q{F!tya|sjMkCCrLlFVg{G
+XsdEi`1`nIFe-_R3jS+p~=BO`YoP-EL`!ZAv0D+|As@Jtj%#zf|3_lq6`dF8I6QGKlrZG*IJp*$S;M_
+k&F%X$0j)1QBXAm|l0y3r^623u&W$gn59e-F7f(FFr;#x|4)FVSw8H-6qM#5H~sHCnuKzbngny?Q85t
+l%u0iVQYe4c7TgZEa`95>$F>m~fX99q5r29V3Rl>4r3*Mc2#FtoHKdg}A7%CG29KBoie2~KIP0Q@@Gz
+Wi@^W~33$Vg2@RL-BQ77znd|A1{{GNf5kb=)_<;bkXr?JuyOFYy(h(hg2(uK6oI2gPy+(*ni;jf#ynM
+K2jBH>nIHonS(|i8+d&mT2L5MaT|e66mB?n=xjB2k`6gy2|KouR2;)d)#7(t}HxH18|3lB!DL
+6rGYF1jBsLD-wX~;Nf@2gKU$TFn{=Ow^g2Xl+(i`TCslD9)VU)a>Cr>f{FBZ)c-nzY?D;DFDosr33<_8}GlmACBq!Hdaop=IM+oB4)ND&j^o8Pi6S?Wv*N{(TJEe
+mfa^TDPYVVRY;{5HL;)7huq4eyf|DM<(7CptEq3?0@r>Xf?8Q5A@1Mz}*B5xaKs62i~PO{%STE&R&jI
+`upaym&|7n2U3BqS~Z&Ruy3LSJ}%|s#P2qjAnNP@f03IOYTNFU*(`k)ulHzqDR$#bF23~X8GjJ3sKbl
+%n+v1-O#o^SN2P)SYUExJqKIuYnkULHY~3$W9#>}xMV34}UyfWn{>1XnS1dnUOweg=u
+&0?;&ukXDNiNQ>x*QRXb1iy`pe}2`)+Dri3s&gVPFgZr*pni=Z)gLsNEA3--n7{znBK#Yw{-G^cXn&x
+4|Iixc)`ZX8aCl>?!mfXft{#l-~U9)y?4(2)gv5Z9bFrWtpNb`b!?(8MiiTIm(3Hyc1#ZsJ)4)ig7=NAXHLYZY33{pB&8s
+r1i;8+UXAYv%!G~9Wc%E^Z)C1^EsOxI-~rx`OuCAYmZDQb!e&onY7BYNO98@2?Zd3QuicrJ;GIV+mid
+dfi=A$)OFPkibA8O%^?f*ZH!id8?IO)79oAw`XPORXj@#+v*Yr{$W6k)#c;hiT%`^HRof*mcS!ezxfG
+62eQHqG~hoa$t>_$jna?t4RD5i+On7_n`3)EyR+M5mqtg}$e)c-loh)xEilAIA#c
+j347t7`ay9E~MLX#9smFUaA|NaUukZ1WpZv|Y%gPPZf0p`b#h^JX>V>WaCyxe{cq#8^>_ajtPY2hQAgM7ieW
+X7ZMnvG3z|!UA!`-^Wu5d2i+3vsuWNhOM$t&
+%*s<13z5Oz~=64hA>HinEH#mB@>%xZ8~fM=VcPe8AX=Vp}Pyisww^Y)*jKLS$S;uxOKHYh3ja|FT4>V
+lI-3r)(>#B}+7rBX-Ysu;>DQ0EE@8$n6SIy-7s61_*#vyAlsJk5BU
+5h@FagHDYJLz~naLBX%wn{J!AZ_q!5)UY3Ybl8xB=bqTOFoKlogEOOWcuOj|1=d?^&$RUu;m?yc3l!Y
+91pT7Zd{8X&7^rEO<^YbD}c{&;l`_5TcBCC%`$}$yF?Ohjvu*#&e%Ril6oL+vq*}oiA=g#5H9k0&e3G
+jCBj+IbzyPW50EqM$Wjo|xwH5gncTTSN`iHIG05{ufe*)w*t1W3yyPX|AXJcSKL2w{M~gAr4e91aFQU
+0%F7dmFz#xtUy?yqmzf0I?If2$)z{LK)8#*KhFLU@*D(7~}ez`0VY)<@MwgH*UC8AOnCMEO}Ofc0FV7
+K_BnoK*frMub2vT6*M-HJR0aF$3(3b_lKLw^>MHUY5*S4^8x9)DfwJ1#GF>VJ->W?a(*1#WyNih=~Xv
+7Rq+-3BvMXmZqD9MjsqnsuHR2T3R$g_Y{n+}M#v&3+xNf%X~zN2H+lof>+0+(HjH|6c0RGo;*TfSv=r
+=1I?G+qAJK5Z6ci}o<;ThO_1WnpzPvu2Tm!X4b)@MSnO{h^{f^k%?{J>;6^|Z#JUKr*jn6MnPUFjq^I
+vO#E(jku0vrr7Qbkx^t7RC+=xO2@Gy;Tnaru5SX77^SEoUGBaw-McjxKvt$k`AEQnl1w&d1YE7$DmB>n=^9_R|c&Tw}!J2+Qo}pliJlnBS@&zz1E5NdWABr|
+e2plrk{(YdSPlbX2z*ivm7#PzcAARB!e$2)eogfN;l@*2+T3RE*(apucRs~@SFbeB8#J->Tj->@xv>C
+WpB>*8UFqna3py*>G3OE9kQN#it#1)szq*QEItl1VK3~T|pqR?MxyNVv4UI1bsmL&aKvw0XTP{dWJBb
+0qC69HSht~&H68MYZ0sWKB)2z(f^S3|=_(9YQN7%>IgkeG;pW{RF{)bP_VRO4;7>OH`^X^mr{B5>u)=
+%0niL;N;kEiX7^KpewYC=wGJBJ?5_Dn1C&9~zyS4d{=%1P_LDz0)9cMyN#Mp?f9)$UyJsyF(y4WblU)
+go}+5grs{R3NMqP4}#&%4Lv>IH=VgnIKfz>ez^9%4oXjjR64iQLm^nNK6(@4Q02(Xm`FVsLkxjF;y42ekvepIo*=8c&2p*kW6pS@c
+F0XF2s~=V4A34<5FZ81^dC97mIzej3x(FBk0fD^TXP@Sv}R0n}iJ4t;$3%0pL03i&Zl8#a8Fb_}_*#+pzpWJx`C0cMAlnrd4w=F&X|O(x-_f
+;Ize&?OykV@No34K1e($x$rdejFIiDKT(OS&Oyl2?CF+BIYS%FEw?wKWLIXL*_L_Kka%cq>{T`in}FO
+75F((NKx&Y_JX0?r4SQKS+(`v@fcu#7hI=tV6{q@Ht;*qC+f$DFhri9F_KE|T5d!~YRwHKR?8mAC3V<
+^!|8XkCRL@fot;6Xcp!Jv3k?x$SO_6}r5e83wt=fxq|v=R^!pl$WdbUR6igw~V22Ey4@8a}DJ7O?)DN
+hEc|3NH7__j~JV4keSGlt%_{u=Ym}mjWH3>h^;8F0FLwvRVmKrJT$Q8Lr9F|Oj)f5ix$OB4*K56U=5s
+ToWfH*Z@A_d_7AK}ka;1H_z5IWNIjFH%W6MsiaQxo17u%oUin^wp&+3>hlcQJ2fBt&w+@aXUwnw={E3fUed3M1{cJ@GzBm!;e{5xYbAeOeb#M6y_IbfO_TL=x%}L+-(D*LP9S0oAef_fBQAN_G@6eC8$KVj``=-k_(^&8c?
+aNx2r#b$soo8|*4Da~l2aObv;w5%4y+xrv9GaP%xZmGsSUJ~x_X?@t{LuDH;5+p~HPjYZ$^(m%=T->8HW;G5E%pi;@1g*j0rX7&%dVu@027%k
+)iBj%q^3b`Pglf+P9FKxVKaE6E4t5425m|;yw}rf?OD^Qob5)l
+xh!fsrct{$S{JoUGQCaO8;+tftq1dmUJXJkm&4zv1+Axu
+orv4~I(IQ_8@a?M+;U>oIw+z>({mVi&bdF(CvI=STr@1#m7gtZmdR&OB89Mv_MsNUo#S+@ZDV@f}lyR
+H5%N=oAkW+7YEpI835ucq~l%53`G_!XovP_}}_rhJpHvuxJuEwpZSyD2yKqNE#9B!5p|kELr;|84{co
+MQ8ZYTr455g^0{H6OsREkqDYulz>_jk?6te6SUbPjBs3aXGE)i_K>XeUpdx*Vf9dwqcS6dw;8K^Ecf&
+V_ss_z@lDejcRV1G6lj{-ALc-p1jVP#p9df*2*p+9RWA;B%k%-xq89Ex|i}?4Q+@R*<-qWE&SlcjL6q
+~(0SX+>j*hKD{O?-6M{6Se&(ETt6N!3RiW_yChhF;8d-rpIf&7Vu-FYJlR`1F=Pa
+FTqgFB%N#&NOO#Gu?2S(hA*EGjm77FOa0oaelDAX~k511&W~WIrSK!)q$P?m!sZ{5!BcbXyj5>e|J!(
+)ZaeGh*x*?Fy?87dcwut({UXazoluEZ+l00e5-5DbUJ`-C2tn_&2mpu4&DGblO2X?djpUFq#QOt6>g?
+1A$@RI1r#HA{Z5ZvP+ILW4jBa4*ZhG%U?B9T#JUIQ%?=UBo`#HRt-0U4`4p
+ygUOoGK1{CCvuxDM$DBYqhwYNx-Q)zfv#737^kxoDKSLS9%<_lQu8LnT=+~Z0vk8r@KrBgY7XtF%5I68bf!$>&6*S_f`_3ZYsT^ZntRyydsc4L|>kZvd~RoJ&@$
+ikE7T@VWku)Buwm9-w@?|>n_?WRXy$5!%ckQ@p2(@@Q^#U(KE21s2xj{pyCh~S5#W+vb$G^dNQ@=P<%
+1-T|HgVs95BNDScJs=R!DMnQA!9bm3?g$Ys#pd)roB=A+V(`oZ8hl0%4s0Psub!E4(+z?RU8R+mwT6shwm=>*Vuke
+2eg}m$Jop^qiz03w!_y&@TiZ7X#g>fBReJj6h5|x55AF4!(i|qP)h>@6aWAK2mt$eR#W78W^4=)007!
+C000{R003}la4%nJZggdGZeeUMWq4y{aCB*JZgVbhdDU85bKAI*e)q4yG7pieB%Yl)mj`Wmt2~YqyJm
+9P#Fj}t*Tn<+z3BpEwf@GRv=R@wi8jQQpws5uD4}Yto+F9|
+Ne9_Kfk;<|Mlv_yNP&{CG|x7mKps2ky(=YM0_pq<-|@evofCFt0L7^T;8qbl`^`i64kE#29v97(a_}K
+luG@xQKmNWMyIM{__O_af-k0o929oD>@znz5%@5{wKVHITlmTIOFW-+uX(+!fKb4Fyiv7GWi9>aU!+k
+z9uLd|r}Pg$m|Et!pMGT@iQ%kL8&%XNCnrfRjS-)+@}jDAHEQ)awoF5Nv??tilz+!6bu-UdrA;O2g{9
+$%btK-YLRB*FD2S|Z#^7d#BpshWNHJ|HHjZF&NEC+fe<9lxhX{Yrg?jH4b%-qg{VX%`kcYJ@giK&|h6
+qRRFRsttoL!$qLRTXC^y|Cn)rYqqBhe~Gg!
+|JAj|6W&(5+D&wTB-VlNwj-0!cxLyn=F@Az9ok3#@o$|<5m*L_yW*Z=p!
+fR^uvdsnBPX$O7*V8o@Z;%Pa{hCRW8MbKHN?;|n8t&!POWMTno~uyF9$$>x>#3a@7?xFu($9Vs$ukTV
+8h9c(8OAs-Rk&{XU(TV{!D~2M};#L$HgzOu-~muo#mC1>DDc-(mlB;T%F^VMqFviX|1Pll+HU5)}+UDWKz-0~m1VbZ5@qDU^QR;^0ds0tn&AqcDs`Xf#{AM`d
+HN=+j$Z1uAt|}p3~_ScQc~T5NOfM>gAl5I(A6EFRDqYzz>~}C>rX_~4YNBsCac%VRVf&uO
+Kb_fvsp5)Q4=?t?Ki~rDWY~<<~7z_XslQlx3r-nG`hDKd19MASG?|0PWX0VWHN>z=&BNvRdBzu%RM!zcHKn9T>KK~pwa{
+anP8<0>ntgtJoq(vJMw+ttHfJS&LM&O<|Q}&7^`w+fW18t
+rtWWYjQGBvm#VT&nADF+S^x3{m7D`~6TT
+VY?#IFmd|Rf5CL|hX3xoBGUAV{_db*_lo}>YwNYzlsP58)15F7eQp69peQ`KZw`-75K4c7*8mTG{I}|
+9LN!)r4*utE+5ixFZak`O#WV>7GYKRy3AR4oTULK*7G#M23sE8
+sqJYlH;YsX*r|$jm8z<9NfJ(y8^^Jk>*YM5USQ!nmO*mFsF2cp&MKSFcXB&(2@{pEb6_>aPsXFO&QbozQ
+u;Uv|h9WzJgM75br=uC!(}wN}RRI!o>)N+#0SOq@}Z5iV-&n8yWeJ?0C>u-F2UFPB*Y4?zlD275|0*5
+8Uzt{1r807$>8rX^caa=~SNQDz0DXSW$+vBBDAhE%DNS_Q@+y|~`U@
+8@5-ZkQtTP(_l_Yse;5NNW^{VsVy(n>!FNuEeq4%T_4^3bm>>a9~qsi4{^DNR5n+I$y?>7W7OzcKsD3
+pe=3WUWrmut^>>m=0G6ZwkxRaEP#7pKtXqzYeLR4T7*#pHOhv-!D1bh->a3X_#h#3+lf76
+)C%<)uAf9f2I9eIDOqTdkEHX?a&u>3k-3u?|Q18um$(R6SWf&k?wqd0p?aVgnm-;RoW?ay>qxGlsWe==QIT+&
+q*l1VPlCNY!hi7?1g)Qqw&*^HrtkwrA4BD`bS|OY;5k)8Z7m)#)tX{g}>`7sF{vayYJfP8rs~y#*Rw3
+MN%}I14;4dtxX(1jXB^O&1{wd4dz!l4Z@3nqMR(Hl7j@-I<*BU!~?!&gm@l`TuYLItdezhk2e7wnNT}7C$*)3
+Cu!=Cw?FYQjPAC&sfO)rL$5;JRqCEYp4^n2g<>(fTGC`_({|fXUsL8D$SV|%ESRGSO;9#|qJFc{kZ?p
+(YwD<2G+;aN#kR$rJs%|z(hBjoHK8`t_bc9&t^t+iH26?~yXOqDH|yGo-bd&W+}~sihF*D=Q0r&xbpR
+*qrP4ndQwOR#+)E8sgk=U#Map`eJ^0_T
+{q6HC_j&{dQ}RVfyvWD*zA{RYIp=5Zl+!zYQZ7K(xnDG!`LYm4PuDyhF_>RrnLo;o(aq8S}%S>PMc@9666OwsN*7nUgIbRDoN*B4T=fVkNw47cO27l%3r3$Dl4qZ3tM|I}*Ly6T9W5SJ__(5jbawvh*}ID`;
+>}0#;p4SD%okyq_Rpn4#$
+w}V1L9VCdwM+$)t867IA?{Y&GLr9H4BpXu##6lg=?SF)nd_>vEt+O=F^xmRCX%oMcXkoYdL8UXk1o(|
+l8@T<5Z#OrbhfVk{;%j%&&2jm-C4PoroiK8e`3eEEUT@gM-~t3N8RKe3SH=$FdA%63R^LHnMb`-Tq(={QHn+EJiR#frEY&o}o(aqVJ(X+(lQ8tM
+f!}AfL?WVl)m7VE5(0n!5eo7T4V@mNQF@&)(E`#8q;JAlw9%^#zO@4l$dZ^u*3@+(fgMKmYY_r~r~w9
+sA!P=Y=1HjGun~^I!gY?06?A9NCzETK8ftC;I*#1HL#JJv$P_juU>qg{OV7MntL
+wdsaTG*yXBfo#F@`N$g_eQYPa3oHF&1YhUcYh|q=neMuib*2~$ZOmxr1OPS8GW!mS`=$EKS-?QumUsHJHVN<(I9-
+96BfB6RO^w521kd#WMAfbC4l4$$N4=-O>>i)!xFPg=iXegPV?;(97|G4|uYc&|V$WTHPg@kXg}VfG?4
+5y_uX>tEDwT#k)~H>W$c{@&ePqB7r}r=Tw7`
+(E01=4Jl|H3#}1x?x)(x4WZP>QyJipm2BIY{?jPgO}F7+ZMR<0lqTS4syqM?V#|!f6QC#&)Ij#xA*eO
+xVMGaa?Yp0^rioD>F&KQ+lvqFD~rM0g`Z=)y}nGcbc3rueJ6IvZjRkjn|~RROFecopMsNKs%rYP=`^a
+ULoN^9F&eBB`_?nhBS#xHs?P_O;#ji+e0mJ2K1C=015ir?1QY-O00;p4c~(;=0Hdeq0000~0RR9M000
+1RX>c!JX>N37a&BR4FKuCIZZ2?nJ&?g_!!Qhn?|urA(+Zt^8Egw|$DPJ@*{zh~CQ2f3Y#}KddHcC3tq
+@2^;@8JNNSVP_raS`8T*Tm$)b{YrMkUAOoa=FbIZ}RzGHQF@94?0kH8~#P4Zcdo9X!4RWosSOXqx6{B
+88ePs3^bK!%zfD>Y*!HOG402h)uz!X!XeoYLpV35d;Sm%v~khP8MJf
+%P)h>@6aWAK2mt$eR#Q9GlIB1O001u>000^Q003}la4%nJZggdGZeeUMaCvZYZ)#;@bS`jt)mh(<+qe
+;a_g}#(54HhYNP!j&4ETyWT#7Cbw814n9~KLPmMEK9nN&$?H$t%gduN8EMEXav=*!`Z0Er~daAr93%{
+PoZb=o+l?W{5S#46pkqH9#n-aq)gwQE($a|k_R@%xP;T7+PCfBf*1t`kRxEi)cazEq0~VCxYbCnOi#ufbCrf72{SVo7PL~DQ5O8=0+reU<)XVYjF@*n;?o
+s3+tBt1b(ArPr{F3WU>Lh%pP^$))+L}wG{_m4FF^{><78GVj5nXX9?dq^Eei@EHVo+@bRFM#cY+Wj-J
+Lt4*9;it#Y!JiiB~9i1e@o$-)~*Fbp!feG!?yBj@&*$V{jwX|y8rOBaphPL4w^=@EEQ)*I+OTZE@Iu3
+WAz_A>&Z@=2hMBn_C++D+-Vj73C$L&i>(4M-B9Nm|U9MV_n6QH1j9a(T?jkO6SkO1rZ?5P0KTT0bR-;
+dtN|sGpyBQ+$j0`@(81ENSCiC%8e+_n0yt2X};e2zzc=ai&5Ei3!H$u|Vda1s?O{nL{7wRb3WI@S?Sj;>pUiGeb+4!co)rbTtlg}vjmE;C@e1z!YvB=w)Wo
+&FCtniHn)Tc)ac_ILV*TYgnq^{uegP%o_g!3Ktr*FrT-D360k)kfVvkbsdD^w4xX6EyOM;(Osu4EQgc-2kr;f0}fVIZ`kuuDB8peNsIaLBx_jxBs
+rKER3xPdOO53F7ErPuwliP~4w|>S6in@Q3u
+iqrQ!Yhz#dh8=$OA_YSL!pcJYiK?^nrw+f;d#vc&FnK6dh(4Fyv9`=h0bDjSd>mHh0`xz%BUdZX{h6QqplZhZ$rtraGpNnF()eE4EvMkrvzFKu-%T5zME<2kyQ%~2+R?u9uPv%
+IFP7uhS$>1G=c0*tXzB*RqwaPA_>eBvrM%3Cbfn=!7LLXcf^W;9$&6<-v`5vFC8H5Uy
+~4Of{HadEa6h?FTwbsx7|PPqFL1fpm9)p74vZhqA&-qUsMQG?hT*dT0wnWB_s2L-Y=l1qkT=aLiEQyx
+Puj;k~lG2?mV=L@20OM(hIjiB>>xMs}q`EZ+rC#J;ry6&+?NWWm(Kif{fl2MF~|WcB8Z-MrGEXGj`SL
+FFHI+%Vwi+-mjW7c8S|pD08_guBc$N4yJ5A2bQOh2IMouj>qSlnSByqF*zp97l;fX!;qThx}$?Gs?G|FN+^Xlf(I>S1UAJ?P)TRH4cIuKnfMl+jk
+_>Xp;%Rc}7d7_&77qJ=EK!s@fjr@mKI1k8wzJTIo5~WTG~n
+G9_vjBPJ>#>Z$t&nPmV-nN^uB*(aToK)mq>n-nnw9j-n?X9n7rUlww@E)^CFRs-!!^h7oT9>sgXfp95*bX`4Z8XXBEODC~Q*hd$2)Hz3>uO3(MQm2N;YVS!Rt;s
+jj$Nx(_sZIL$eAdz@Sdx`uVIWjaWPqO7E1Qe#_)K3Y3Mn=>40jq1-&}<6n%!lQ4UQ><#nEZ}He#`BT*
+!C>6Rhri>|2t&tn5(H0Nb@Q~lI84wP)h>@6aWAK2mt$eR#T2=^A6+)008+I001Na003}la4%nJZggdG
+ZeeUMb7gF1UvG7EWMOn=WM5-wWn*hDaCwzjZI9cy5&rI9!75m+TzG{dIJAX(E}AAA+~IQ9AiHgGy#_k
+2M6H>XM3to0YXtf4eTEceQS#ok{$N`qXE?9V%t$t!(w3Fn3M(72lKy$m&Ayg*;qjAEZTMfS`+M2mhey
+@fj%zbgDwB2G?!%)wnpLG$!|bsG6&sdcwZ{#6BMZCoyPfQ^{86-}(jYG$I9-uF3T>on1ChIjapV8w!|
+s%WY^~5OuQS<};wdXsU5mmh9XPy`?ZfM^_&lALK;#uYj>PZ%>RY#Xj<^w)!;m}>+zXqRqT+pRbJ0FZt
+=dMk_AIF?MQt)8NHi#wcUn{?FuDoL@3AVhXbWM^acPA;DE$C7W@@+hvb*ss=ZJbMadRbW0bg0s1S(#B
+;swObZPVqny(6c0JMH&=&N=nd1Nt8waizKt|R;3!(tYmt{yuU0qL@7})t=KA$_`I}d_*ZJG;Z`qC
+|7e8KIG*=hp?Zr3Si|@A=H~&gjs})5Y+^`Fwm%*^_+*+FFEpJ4guW<~fW;xm1SVS{P>^9Q}aojRv^_p
+G%nSQq`h7VTryQ38beDObnQQ?Dh?KX)H>q8b~X3t-~{3;zu*4bV>mGWK~I}m7Ld)+!ZNK(|?81h>6nk
+;rh^7vbwjIfckd7i@C6^zPZRx-*-$RAWYoTm>R%bZSImoh)$*oHFbBSifC<;*#!JGlu5h}UX7^Mc*#B
+eM#o^`GBdZNdh~5QKvN`K6#~-F%M_l43tB>2oB?k#f
+R43Z>jEEcN91KNwNpGvGKPGEJlJU@zU92lqBoNHVZs|xBOC_EP(OH)M?dDo*1zrEa>s}21zY|CIZ@s+
+f1-pLgYFS8IADQVpU*K3+ME|uw~hCc{KNCxY2OA-MLouv`ru
+Cg5G*Uf=54e0`kQ_#oqtk=IL(GThnM7V@4JiL~%B;<0svSfG2`#qj|L%ID^zj@ysEl+1=
+`%o&_2p4o8?>nrr~0$u^l9(*fb4e4ljzVPF-?UAS$VrC;SEdt%tYk5G*Y
+^+y0;HCG>W!UoQ813X8V<$zN9w!`Ia7nIMbZljZsn6Ho8^XUL((dvVB4Ha?-NuHFg>93T$jJ1s54Pju
+z7rk$4A#?9JoOZ2Y+;uC0c?Uv6pra_ooVer&R4ZCrRRv!$2$HAPEPF0~ddANEt$O2QdjmPS|eecC#R?9t@1loO~Ga#phx=^tIbbN4uc`gev5AoJ2
+BLcFnAU1V1o%781aYr3*?tQQzu~|4ug2Ix|;V{HI6sADOCr#2NC0qCiJ&L>P;QjJeK^bTi0Cm|95ZVc
+9xv#0Tz>$O#zC_PmDW7)>L-To4ii%l>|I{ULw-3Sg4I`St_B95|`U7w@5{5<4}lz}KeGva_!C#ojta<
+^S(D=MwL^p=`z6=0)F|j9eAwwB3v%3~>J71plJGGy;@R2;$~B9UI8q;O=Z(nLyJNslj8>RA444T6IWI
+Snz_q5tvsRiLjKey9g))yQ2-;JCiS3<7t5)Z@L3Rqv)f1iv6<_G2)y!os}5=>0G^8^lKB2KTYNNxM9o
+M^b5T-)JY@5T}@J!w4M1dvG_RoM?OL7P~8
+B$&%Jfvx}7>Gk^uP`car|TcfvZDjR$+;M*OJ;IZAG%l%UT5?bD@2HtRIz7GQo6ax?)>ClnM;)Y}HZ!+lMSW5>e;O^s2n9Krh
+J4lNpV`xD8c*Mbp`3018M;92M^q{&t7Lm+ndw5)wSyDB^Hj^X|%oIbGEOXlw}_$&lN
+8k(dM}4l$%eZ=S}cy)8;>2q-kd}StL^?F^`ji|89w&*{i;GRy2|#
+_=MSyk~@>l7+hYAGY7l63UdU6xeUh5lFf5Tuz3~|mL#y*W)iRTJeG;xsX|)umHq4pvicZkVZni;+^5npMd4>c3D+0|XQR000O8`*~JVvbSILlnej>*DnA79smFUaA|NaUuk
+Z1WpZv|Y%h0cWo2w%Vs&Y3WMy(LaCzMtYmeJD^1FWpqacWcYIRM~$DIKeNYdmGplymI?R{7*1g*uj)s
+-bxly>75{qHw3d{dIWz1{;j1Dn_+XE-yQHyK6I+kU&}V(5#Z?b!|dU5`~=R?Uvx?>VmpyXo5ld(()as
+Oxw9m$B;kfj5K5R#6nKR@I?v`+?Q%ZU;d6XDhO<820*S&-FL4ABU=55z^t<;XZ2Sd2>wJOW35iu6fGd
+47``$zOBTNvbWt(wM|i{?8DgAd?itIRhQ*=yeZorHr(D8NJNHP2#t4JG;LFDi@N%i=S^_{jNZ^4?*(8
+!g-#lFNTsVeV
+*SQ^D1_iC{kPi*`0e*i2;@g7DniakT8++>n&>7`Jo5R=~z}?oYgs-a=ik|muqup8t=JG9##X0$qJWp1
+uobPsk7a1i04YB+|AVGlxse{d;kZr!%Gg4NY6XGOy_u53lX>#pdF0|Ue#EX2^lQT2jn>
+{YhQARe_BpJmVVX7qm#de=8ZMeLpcXlntnEEY+k*%1471Y0HDjTP`O>lq_VX|my58TOjc%Te&w+uQt_
+jwdhV`K;Oeaiy!Ngx*PdwRk`f)BTyGlwEUjFKFx_i9&|pOmkk{ApM|Z4aRF&BKN@0V;~-)lypwG%7ke
++k78g2Xy}3Wygo7uE)2Mm>Fc5v+}%n$3%e0aIASux_>o4Fk-B&jB#8I7RY&3eiAY&Fay;sy?s-ujfo+
+p-WKlYSMM^02c{9m8^_u)SDj%~d1JwfCmz$-mX$ShLoMDLaLq95vkJ)QVS@xA+T?@iX<#&+g7JP^xwR
+bSV#NkjhC2O1ds4&EqrVSCBWR~1^AU;Jq_FMAW>(l(4GfuqCN5NHOo=|GVrZ}6kp%{=P7Iaa2tmeoBK{AG!s-sDSy5d5q667>Uv0PRSp;Ap1
+>YOqG57WRICxPJ0wt1=&1g4S(&lCZZN^*omX+rRMDf}qNqe`q#DfnjHQKNWj1QPjR0JR0hq*Ru(r?ma
+k$^L&%K;Eg7=XD)#4wNa$DVsRsiUSv_u=~y&fK$iBH@J5?t7aSca$g)pKERS>i9RKX2U3WWe=01@^5j
+U^XO2X@z}?*7N*WfWkCSHS_oh{3rt(I^~FUl$C@%$fr)7HWgCHh
+W>$37P3xvo1b33m&e_XL&m;or4yyX(JmUBnzHFZ1|(v$z#{c9O1e;)VHT7B%)o)1Ii$IoO;Zj9^S%ho
+EYO)>gEN5&8S+f{g_*G)AxjcKzkI3#$g)
+q(%5x@M#n!6l-g@S}v%D@xra30ilP8T(2MgJfY4KX^a28u$Y$
+d+#31H@V#g=&FOISUL;+GMd#0#F&ENHTE`D@mPV)#1xf*=f1mFG=h3L6dYO=Af5t<_g+c3P
+h)mx{uhJIk_-1R!jyoLG8RCC9aK$V2MrW$G(m8faU}CYijkcV}m`Yo*p^op+%43DOHUm9TPC#~VNFjw
+%Vxe?|xB*azp#D-=5+m*;NJ+MX|SDvR(mo!^#$iF269|Bw5L|B6>2)$(pW;@hezH_9|g!1|)Z%Gt@EQ
+C@DTwba84z)Y|(I={41BAQfnYP&!T-t>$%Oy=~XkMhfN9IKPV0sRZ;V
+2?_O2^H0-B=>7v_PJ6%l<2jnStIv6&aof$jOq+&Qj}11ad70bsCH{T)u?d3TWVvU=8$WVR7N3=?y7<
+74A5K*t-LEgKq*ZBss#Nx%N-hR{JR0cdwrXNG4X~(v;7J&_bYKTCWe}Tpgxb6YSR{nbieBnuhxX20ms
+%pHASZ7C@e{r5d4c7kY!}Huzc#t-20WsG+I91!gzbyZzW8!8N%-{Vv}IZ$&Cg3&oPwpU>7IWqs~s>LC
++9p*$baJbWNbjq7i>gqd+g|uHidYC~l-&$Kt>4MIkeHQ~(xBO_
+vyZKpip$~9?%1wD%_-<@a37EMsq;1u4I|;{3k0W@W6I+5)w!N28&*{5|qBSk|P-`buAjbkRj$V`QNdl
+2%Ren|d61C1|ZnNBf?qP0b*5*m!?4uZ&WG}VNaq{2T4j?^o-s}X0zkGQ)SM}G}e1wXB=EjZQd5Cel<-
+Al~ctTbWOTkqXs_kNwN^!wskFAjOr3w1l|eMB9c+kG
+^`O0exUDlDNRx&XgGFoxD~k~*KDZ3vXAWC%YAvyBwHwVgf7KVQe)52xE0SDV1ahpk3
+s0L?9(5I*3^W+|d28+zv&J5L8=yGJq?x&g4o?mjNid7QaAG=mes;F8O{x>}I6(Zj6hI;b`1uIm9+kgv
+2Ju(DM!Z8&45Hu#HLstrHb{XtK6P6Bp%`75qzL#_M8*F;DV8i%Hd_py&525q5@ALjOBK3ynX?
+(07QWVA;PoL}se%Nhio$*Jje*#cT0|XQR000O8`*~JV&UflGXaE2Jga7~l9RL6TaA|NaUukZ1WpZv|Y
+%gPMX)j-2X>MtBUtcb8c_oZ74g(c!JX>N37a&BR4FJo+JFJX0bZ)0z5aBO9CX>V>WaCx0rO^@
+3)5WV|Xu*zX2#H%(rx^;n|U9>^d?jlLigJDQqnYP);qDoS`_P_58KO|C;)1X{d4onw2wx?aK3)VbwDVh^&^kT7q(GX^qX5{uq@`q^
+HYC+%uNbedgFXTahcCr^Tz?_IZL01TvK~(qXEJEyIR^@mesN@CtSu{7=OESXuq(fAWRN=T1oviR!sX7
+*c`aQ2%ZZv>E^6>Vdc=PAS`{Jkj-yh!HeY{IZBQ!(>oNeyBvPR=0neJp`UaMyz0nBxZAl%LC|iwv3hRFE31~BE7ofAw%M`sos>e(iDC@nh
+zp$cOw6Qq?*Vaiu7;pYqe!u<++o0q&356AV~`vvDjIrXt3I?hO3N)sVmz3Yc>cyGK;8N{xBzG5rvl4{
+`I((SHHt(_9>LvD6Fa>dJ{rb~xH7>o1g=xivWnB1R8)amH%C^tH=$MtK#+@+U)f*{Cxhb2$e{~_#~Fg
+>Rd#h-JQ-4p3b^YMkk}4}l|fV;#k0wwu5r`7E|}`-U4bf!L3C}Lbup8}sMPA2>tmYSCfO((9X<`&M20
+80X|jyR`u&56ZG_64IWZ!TEMVUi#!0hiZCzn}J2z@1{n3KZ<=B3F5W&2njc7Q4YaE@dL40u?A^?WuNc
+w~61x`u+*qQTB%^?+{sW0n~vSZmq8$d9#X?Oy4HI>2xnGy5!<;cR5&lGrkUf|yP$Rs0FqNt0X-jyZU!
+!K}X^@(U7(g9h=xbDf7;~Qi4nPvF25;+nVPu&i4V+iOW`PQSGEFA@HAcYozM=`hLmJM_3strmobv5=B
+=5tJ4^3A$03-S+tK0Lg?`|zRo_3qunJOr?|faowc45%n-(Hsi+r^rh?0NEh58JP#i--EPm8Mv1^g-av
+dPU;NuIt?;tvf=%50U?1_s`0b>|4*>l!&ATc*SeWJX^dSt?0C~NJ6q&d6GEm>NfNS&`
+wI8e%TEHq!HwJY`1G{k4dgdBFGV1G(RLJL8mF52c&G>v+?4dUnH4{f78&wB4sL-JGs0+!44ZEK;D9W8
+}b}nD^P_;{e~4O9r1oOJTHFUTdii%M>$}Mgd^=Sx(2|q5ll!VR?4lKdh3J;~2>*EhJ|KsInXxS(b252
+8zn^2NeFXESLBn#NjUi)Zqo4gtw6VN7~|;$Mtx4SWJ3wn1M?9Lt2hC#FP;R0Lm@NcQyv_#e^3s+m1QG
+b_QO0HC9=`A+wsS-2zB~5v|(fBsc4ufi@L+KJw}aMdUvnH;j8wGpvI8UTw(`IiP*Bdc8Hp!tHG`Wre|
+@m#}1=#1I-T?Uz_|SgqoU25SXVbh`r4Ta33Bm<>_hu!<=~_f!@|)qE8bV;As$yvr2g|4I1xga!4>^?#CnuOmJqdrn_QLU3VU=W
+%?#f`J_AIHDQhl#Rppv&lRL!*v&5mD|D1W&=ARN&Yu*v|nH7KTCH
+=T^bW`%v7xrAZTcl8_S@wW@p{YKqS-v<8z2MYCyaDCQ5O8q%M$Xn
+AKakhB!gMup>->6qof)h(ad+xiBIbsW6V_kG36*FxZ@QtOFv?BHYKbhC`NHbkF}uFupbYUF?)kNk4vBn)U80
+jZism=>o_Tvt?QPVvammNgoydroFyw{!^f>8*Oh3tpUYJ0Et#u!#Iq5UO*LMhSPA*C62!@?thyQgXM&
+&6OE83{4$gJId57YLKZ3WDcT&iKWQ)E=o4RUCAB6)iUzkE?1QIO0MvvuL;Lz<23@6aWAK2mt$eR#SE#e
++IY-003?t001EX003}la4%nJZggdGZeeUMV{B_f5Liacd
+WCXAUfdet%$mBinou2e2f6h)(v+Lnc`*t*V>swgt!7qVirQTb-@87D=)R_$t0R;AEJw%;wf*|)Ei7Kv
+PmQ)Epgs@g!VR2olwJYsmR@9%H#r}p*k{`&L$_Tdx1e3;){@vHeg@9aJ-ep=lAvgj^-p5HGY-rrm=
+KJ&%xl7GB?SX@m|^oO{GeU3``Krz*vfxp7;LKQpqbOYT
+p}NqSpl5>IR+D_-hiGi5pXK2!gZR;JU~P@&|hYGZ4OZ`cOLEA)Q3FXg@5;8f^66UJ})r9Vz}lNBqGaa
+zVIhcCs3qSi0y-=+AhbXAK?=Fgs{u5);8!|ObinjiK@BhKMT&e@DTMor{IUq&OcBo5K6%`~vqja?gao
+{O#Id@>r1Y&!E1Hx3-q$%T!X+CYa32C+dBd5+f926mTF=ifDK$e$t31zREM0pz2|kj5-nD%XrdLsfA;
+HZohoU*KV5O2Oks0spU#Ax|BWq*Tsm3kF?9(|CUHx6GCJ&mM@%e$Pk;Iw4MXfTZ~g5i`OE@
+&XWT-z7MWQb`#SCIQe4i~(XT#6$CKaKuO!TGQ6F&a)7I?Oq&A-P(L|c^CjfAljc3+QeGuX{qhI}HWWP
+i!hh?SbJS`H=`%vByGyihf9$I=B))#F}c^n*}FEA^)@j&3rhn`{Psi?D*6k|<=VO7DZCuN#%wn3pSTU
+0FC1>SfX#%>)SC!$o9%?M<8Cy$Bxa-)=y^$c)*4UHY-4@7s|35RK)+oY?&dk9=TNn|4=LXmmlTL`RsH
+izOFZ^c*H_>Nn`{ov|AHgp|S-$`8wJMIZ=#}qF;c3!NZHxGcupD8-P-DQFc9LW-yh5|#N4*B3&?TINJ83xd40`1EGbTW!)Y8bjI&g!LB=R|l_#_O@wjMW_Qx@d~w}zfQQA0bQY>p*i2|#7N6y7dhn5>BYHd3^wzCMNo@M3QZ4m!kC+At?O*o|9c1DJ#^+N`
+@qlWnF$fp3IoTYVa9pD*He+Dj(B{8ZmSIXsDg?08j)n$E{LsYiZXWUhqqj-5+4*mIq29!fpD17BruB!D>i`XpHJW*=-Y*
+^W%=tun?=2^6T-CY0Jw5VLuZIYn&&b145T1?r#sQoZyWk}T#x`!5TEd@FnX}HX-;f_0(*McHCKZkzj4Ut)o4g3oHoGuo~JtE&
+TX;bAiV5$hIJCStoZ9wd-YT42$zi=x>YKS;#+15cLd??Im0)Qw!?c#!eS4zI
+XcEMfG1%g8t7E6>diR1_Lr-W+TE#8(Tt|nxU@7|>bg6*2lP30`Fl#ka7I0GSv!1!x3D3=;q~zFdF>Xs
+`%M1Swtv;o-5c?5(jxlKI5GYdYMi?L4(oEpF89;`W1!ZJq_);kU6nTXYDtqOm9Q;FJB%lEWp;v=snAV
+nN}s}(FDpbRpHm2#IEY+oBm$U+RJ&kzs0b@C&w*pbQG`NRPxuu9re}q<--qQYtY
+mF=PR}%+3bD9@_+WuPjbI1gidKH)w(VN374Lu5W7XXcJp>Z*)z*%UfA0ZsmI8|L@!vSz0IWL
+>o$z(SHF@O9KQH000080Q-4XQ{G90L}?xX0OxK103QGV0B~t=FJEbHbY*gGVQepBY-ulJZDen7bZKvH
+b1ras-97zt+s1Og>#w-N(;@jD3EOegbU0C_isEN`n#3AO$(xR<;RC@Vi5LX%08lcY>;Jv`bzdMT%XBj
+BR5O-H;BN13cW=LLZyiO^NwZFBy-;Pam({TlJ@+Z2zRPqG^+&&5~BDcC9xjtGt{idA^I)Tj*Bnq$vwE*IBWG_epC3-DmAuB`JP69VAtyi^
+V{tNdf=rdZz$tt54>-z1O->RZ=&)iB*+V@>#8Pq3a~K@Y?$}0p53>eDEOe8tiufb~ES@}3h%J7N>q^Vb20+MQ2)EXo@(wT!>ut&n
+V77#b!Q>D{YPoH@W|Frzw4+X;`I?}`SR*&_WI?I7i0C7#x{RnAy1eu7uc;Ht6!6Rt7R-R69a9b7qE
+VLx2q5*^5(1nLYxC?lX(t&^8+l@08a9;y50h}op48>Z9KaJHYn@3O44`93gE=sHgih?_9%-iP4es?dR
+8QxpsOK$oaR|fi>*z+^RifGD}Ox)`_<*D)=e{;Lo?I;`?Jg0^?zQxyz1+v-=|u*#*Vhx0Pnr5>#}#8y
+iAjz!!K7aFK6dx=dWMRet2^^2G(w8O`XoF8us4%J`kI7q4OqQmh~oSXTZg6UFHVdU+C#MKYXBfRxI>;
+)9GS&@#cZHZBpZ`s$IK=JtTdu>EQ)E>+4Iji`PlB)^@E|*Oza9el>f2cJ;a+pWJDaHL2+W`=c%Az}hD
+#Cm@Yfon(!k@phSM#PX2IJ?*e8H=Q-ZU%PZ6h`LPW@aKdU~SZ2lq7oy+pVmHKZR28f0Y}LT%|Y4;1
+sLqxgh6p4F>{K`CWs0$}{fh@4De+jBcJH`{Kn1gL(}jv
+2e(sCaRqOrqh-h(0EEsCJMy(b_1fdB>}#G39u>|pgb6m&>x5xAEAM+mN}kW=+bmzqSm6%6**8;
+>)??{l9~;Sg!|!ilg}4t+_IJb&S%V^5-LJJq0lZX|t|tx5tct50NMcW0f>EL1jqHXg;3?U-si3Y5wu0
+5zrP@{t)W&|l&Wy3Q*MZ)SaXMgqsAgLM{{k}rZjx=@Vv`Q8n{dx$%WwcRa@@M{CwzTs!0_6T1|af~^u
+xl*5{p*c1rC(Z5jrGoTMq-sV+8^UM;nXfvESLuPM6e)_;%FRnf_Le=T;J*Z^egvf6l^3|TRQ`FPX?IVhkN^nwck{s5(3PTfwsS8{(=@5P7bB
+PLW4frWChtmFy6LlQPt&1i}((<4xv{sK6Cr@Y#KAozpgc)7FyP{OU$;xc}ynU2>G6kav;DW+#_AA%wT
+WBs*$%W9&Z9%n<=q*07e`rPEff0Z3G2P|LbI)9e4xQK(5iwCgaJqVdj@@L`=Gyl|{ZoLiv`^5SB3C~F-+_hQe9jevf05PGV8~Aqj;3`EMiv6wl0Cii#9plc+sbmI5rf+z2dAsEIszp^9?
+5!_C2C*<*dT0cht$Ix3%ykKM1Q9-_
+dHxTWSV0H<07jV=#c-2YA5&9+N&{t{j(ZIxB@g8{JBOW+h+z&=lOrUybQaVr*T?VyWxK8n-CnDk%y7d
++Z!e&y*lKAg+6gVCr>PkJPDqktaV<^rZ%*26UVU$EFfSBelTwe6vsS8m%TTTuKCTs?4Ln(Z8%4iLNAmw~hwb+SNvEiYFq4(aI)%B7}VfY&H;Xhe9)ni96XUVH&+42
+S?b0o>mKL6_Tt;))shp9Z=@=-_Ce0;eEAahKI)L2QJ!mUtk+t|irfW(VM*1~&^Cu4wmPF@QTzCzA_q1p_5ED;7p38rC
+=QJ7=+9ZUi09jwWKJeB~_6?n-Q(^Lco1gBeH-)3OFKRb{at$y%zvo?ZU@=I2*q_44xa?InXczkGB3=K
+SnOBY}N5Nfy>STnvx@#(LY;f#`i*YYP$n={|16(IJ`MrZk3^ld&MfwzZ{&ZOEVEquPr%mm
+8Zl;qG$rABPc@)dR5~Z`-GBQNxh21EPP6Kp=%@`fayiB?Xc^+Wo3f*lWWK*W^G%p8$sQrb)a3HYlQ
+pqs{&DFHcjM;tYMTi4ePuw^wiJlnsalfV$Z#*8)Yp}O>4A2R~$%IFk@CaVulb0Nf@4fAj&c1}ci8#27
+zwYtZ9DgOztq-4(b>lrVyPBnq2iKBhZz8g}1%>>Ym!0%19QKB%<%Z4ZK5NtUSPiogh1cNI{V1#O?|cM
+aD4typlhIuE81^FnS=Wzc!F@4~oddS=9_-2EKB3)JXhvFJy!>gRixiv<(Cudm!D3_t-Lm->eYV(N{2S
+7IcVO}_#;OEKQo~1zg*1&_)c@Q7hm~A1AGEW7
+2`^T0v)u^wsn4#1k9Z1BR~@X72mR`T
+13l^ur4Q^$#F62^=o%AeL}73O3|S|&uF56@6M&=2d6pD%gnRMwLVf-0h1Dt~fN5%er?ZPUBMKyPa2jm
+C8k7^h$&-wn8v{U;ag%IxNDz_IZJo?4sva$;N&G_BJ!&}ocR)9NLeY`pK_S{FahW_XaS)_WLk6)>g$C
+7pxY`gqvMU0F3&O`fH^u6Tsbn*r1Znk_f*j4Eex7tCQxBdW?f6pfee2OZ{Cw;24XK9^HEKwqle{e7(b
+;6HCSZ~mi%Fgp?;1v@#JK{Uj%{Yt<}(;&=@ByW$Jyf;2mV;5u#6Qlq+nvH65h%+D_Go#g<7|poP^yNX
+)bAYWMVTmLXJu5bPX;o?CC8Sg}@bb(4&TURuHfLL7S3}m*CO;Iv)|_rmMfqwxwx64{S7j`jlpe34#M)
+IJc*R_#@2`DMwSZ(~)iN$pM%a=8pM&e>4mF_52ETC}x&0wdDkik_o_M&@7;bGa_
+l7+-W?+KtWs6+0N_h>x-){%DgkV&%jt3ZWjk+4|^W`FWxVnL+1;ltZ)L}Gqa$>2=I4ez#64saiVW{1UiTo82>9PY8{Q30#@7#R3mm15`?J42NudG
+MDL6bjW89p??7{gXP4bWO_u_=m;C2)WvDr3Ajtu1GH>v&IqJkFjpR&8M&g~M<4CFx(nv_g-Hymlb*vFq_N3t>Lld#a8;N0!3O2z3_ri}*lo~Fv!v(-DwWe-1oZMgZ;v3cDVDizw
+n(j+exK%BSc?-d1i^_PGi+=Z*Yggoo5UL`dp_=RDsDibXnchpZDaCBEuRn}V|A8
+df=h!JAo(kpy7i8wd%3AFTIKs`fVWMuQ%g((fF>e`Il$U_mR8oVl~
+s}hwdtBy{to{m`Ps{v`(rq|cQ5}gJS|C=|JQiBGpz@B+A^)j(~sw0;u;eW45CEX8chz_U13%#->y5zo
+gSD^GpbD|KJpAux`)Sr2IgKoKWyFW5bOt}yP^NL7=S|Vk!*OETgyoP)(rDqtbTA88@wnqfpJNd9GH6Re|IY9ZCs6*@-7mzONw>G*O`~%L%%|j(Yl57KN95
+T%K<^7Y@_O-2MEfFo5q}fEh{#HY4lRy@*hZJ%Wb3n7o2oPQ)r1=i1@H_{OvZdEYlfGp#z=73SAVmQ{s
+x?FX#hKPf=t8rI2Ja=tEEne%_O{%ws*4yr4nPv0_Cf!yyaz~S>pn{2}gDKOo`&5B6BPN5!*vzXaB+q?
+kc>n!tMDUSrkiZDNF|3h9R{XsqfT_y-P7eR1Xm-w++Un+I6k9r3i!eu0}*ItJwp4da9mxR$8cgO&ujB
+m?nI^1n9~H{U<Gj1q>yLonNU&t`*U&8F4hbhozwD!0##0zPFDJa`fFUI+Uf;(Y|~^-P6HQ^k4Ml+MR3qc
+S;QQdbPd3PYyX07AGvS`hkw%MWAP9FHbZJ8U?NrV}*BO=vurX56-OQbyUZwe&UJ@9U%M+MNNV3DBsaj`2hU1R<{GMf@y&HYN>c9(;ojoYn&m84F5`8d``ZoOj9SAeT*PyBfV>S3~hrd
+(&>?e8x9tT-9h}9dp2WwbD&hbd~NxUt{-*+6|nTA-{u_Ci^Q0)fDR6wv)AD;XqSrwqY?X9xV48)s!#}
+N^F29!fcU3$4Te~=X}l*8*v-@-3Pm|eA-{mb(~_gwQ+bnuanK-*;Hur
+&}{O$&Y{2W9u)-_$KJUUelAx!-9BRUT^a696!Q&}byp${BXNfIogNc^Z2&v=qHK`-T7Jj3ymW4zF@oy
+kF;;S2qRi@HoP@n1Yu=Qf~qfpg_+GzLxPaOXtKy^M~dydog+TXXEp=H{p1_;X?Jn&$zWO&OFdO2IBNz
+I+D`jE*VcFk!sU3IhqGl&{oceh@g?AU0D*y{G}qz@X%NiUr!yz{FIq!9@n{8w;S;PDu<#Kd|6I#5h2m
+t-v_Mdx5Ci=9tnbQ2Oq>jX6ki@#bRo^IOMOr?`piwQwG~*Q*j2owJN62Eq0scLBwNO3LDqJfH~~(s<-
+zp#;7){@D6JzK@4LdoDCusqnDpM)RZHE*NYGAV&5E|69OeCKbmT-BK{Ciy^W8d6^W^w^JTj@{`xf3iz
+2CayV*d?7$`BhD-4qo+#W}_es5II*t+-hy(RU?!+OGB#&bO^#(RCE%3esc
+v!gYKG`*NLX)+&^**;-Ns$>Qr-PLt`S=uv*~C2#wL`X4{@`p!dVKsfzekDj$9l%T+zs;p_)Nf((Om+<
+`UOYN9y>>!2e#Nl=ydHDz&QRg5Zyj9=LP0;!Ut{gt$9#MIcmh*IMSgcSSF{c5hJ&6jh*2>wag$z6$#X
+dDufC5hHP9Mh%YH0?Ihy?GF98LaDw%8#^_xL3#O1@wC{Vk&q89CfWULC855Pjw(BWfVw1?f$JPOny#N
+R(EXo;w{%Pw09tfig7Cx*AfdShBJ
+`(vA|dS}QP_QRiJW8YwZA=d7p_jr%lJVG4iwouu7JH=^x_i&0SXP=O+9y)xf>VP~}0d`_n6d7W1_YE<
+Y6{NkTpUfp13~_Z4V?%qnUM1-cHr6`1%K*yAX`0VlbjW&-TK6N28rSzh%Mf&5>#S;<)2G{}eo9HfPbGhIo)sAGNB
+*)g#V#bj2qTqJwk;V*Rc3)9>@a2`j5x0FsPmwEU{Z%@{2fGGJic@c!-^a=2S~`u9wCTHe8M(
+&SArdZm-Zvu9M>}VCwyaz6im<1GqU`slvR@6RGMWT}0@3{I$1&l3X#c3VsVrk#s&9(R4k4mAKq$iPiT
+z1Ie5*MR*~YvQO|Q(sfoYGO2Y)3G;
+X{mh0bhAx}CeA`30SIIlel6`bJ}Oe`V=Hu+tW(ek+tsMK==8MxtHKmyNQdF=B*Ho9Kv85Yj|gEB8(@t
+Lg{tl&_wr?
+S6~0@v{T73nWqa4`^Bb`2#sAd)8?))Zc9oS3d*TJZfP<-S_p!wfgd*wF)OSTRrH6CZW_Id6rP+B%VEY
+GSJ??BGYWcim(fS)7!-r-(;-v$=5`b_7Y>=wxKXh2aWV#bMkY-D8rLtuM!fil_XA+_Bgb#47@XY5x^x
+%U3`27HVu3T8A>Yqt3Tq`&eqDi{9Xj`Tp;r$j*+B{n5`@pRgq=9(B)wq$CtsLRX@?!hPH`7Xj0~^T@?
+}{CBu&>8UbQE(G$*5fUAfIzy=3Qj+F@Mc)FrYi3ocV6PWJMVb!@l-U4Wjj23;@a9xvyGjAjShdr_1==
+Uq-z$tpo4aC|^8RO?D7tOd=brdVBv(WK0XcPe92*rP#~jTTVn79`FFUNeS`moA7!K}!WPk3K96>SWNN
+=mS-x9xz}prC6-B;A{jg^XeD#-9o-S&Plz-o-l``!^3wwgUj|@y4J2gtdbudnLD)xlF=S%QwvdH-XXSEv^6
+x|*)VRc*&3o^iGT~1e{j&FWx^F~K#Y4XG?>b%gs7#YJ!hy_Kzz!vD_
+nXFhv$tMOu-?r2WLx=5ZhzTeT_U?vE7&CTCwQ1QX3c|lk6@FV6F-mC#>QcJn0vUu1QBkg)jE^2)_q*{b)*8J(6w0%21DB9$O*FM|
+@B^sY6tF>YlioIo^!_2T)4`1QY-O00;p4c~(;(W`@cs0RRB_0ssIc0001RX>c!JX>N37a&BR4FJo+JF
+Jo_QZDDR?Ut@1>bY*ySE^v8;l0j~RFc3xeK82MPNGS)1l&VtIN?EjHJmr=3iI|7ts-CI61@Gg-
+tk@Ic%N>(CYdEc`{B@JzJ>>y=~6FNFc28~+5e~DruP
+;TXD+?_}z6J%loVt}VQtQa;e|i%DaD7}5m@bfL(4JKZxQQ;zUgU=hCz1bQbAt;4b!e{am@R|E5mNRIP
+)h>@6aWAK2mt$eR#R$OWPFeW005{7000>P003}la4%nJZggdGZeeUMV{B?AlhDcG+6Lto-D@SwkX@G#3k8hO^8}WHy3sPrEWKcs1Ud~RHhuH^W+w_xGSO9OH;q8m#+MxP&ty}e>Ld2q8P(j=ZhLXIcMEl(q
+mBiELhXvC{dhcOdlM*UU)283b*lWRhCBfRF>;3%dW&nD~t_iB1|ntPlAJ8twpVX81(du_{&GG!}&-kF
+Ju+ek@N(ZTKUS3PPSBw!^Cd><|9hLmz*!e*Ny}m-o!*l_SxEjI(byq3EKcBbbxI}Q>E;9;m5-_*b~8u
+s*|RLy2ny#{f0jX1Q;BMJZOS@&Uz1trXh!-W3R*!&M>_N2Y&+}>RPd}jc}te49Zf4x%tj6+;
+<4er*D<+1(vDWs)x=`Xd!WlSu(J?jO*}IUyo-=U(sidcUc7+vxFyjj8+9Ue+A%|eTVR{e{4*f=SDCfs
+nG{P46Eqhy!npU$spK!Zf=Vcj3SDCLP{+$h!(iNkLS)yl^XkLn5HkdF=M{K~YR&ZI)MG*ak8iHZyLcI
+6fim3wy_Xc0ppw>(aWUQ%(IyWysSb3A&m~3^jJ~v+CjN6i6(^@)
+vB^K(-+#W}i+>>ZZ@&Jyr1b?-N1le)LR>$qck6|_JE9t0$b%<6>iqhE42)ri8&g?sv(+CMV#pL|jpr-fDhxu2=bXyK
+Hi%a4D1ZoD)Z3bY{Vf|7BKZne0DMO9KQH000080Q-4XQqOyBujqT?j1EGu{mr(NrNQSCN9;2?@BO>h>g
+L9S71HFbFlCHzdy!A^3ALe2RN6ZqV!Q-2+CkHit`y+)LP$3T?%v&dIk@0pJlx2hdI$FmhBtU%e|O)ylff?m@f$dX+Mif9IrFM2
+9;t%GXgj>+(%@{aV_^?pEql71Lpt8l?u}GuE*Bd)EQd&U^xrN{l~=;>v&6A}rl%jQ3a%TIZkMOeL-B}
+(t8zxaZ``RoqYZYbxr8WsXQglMu!iF2cF0@)5d#NBqt(!-3u8z#8nS?6h27D(ijKQXkc{$(9M?0e@Uc
+@*W|_!q-~bPLKo>=wj(ZSIr!>Y)qvB#4fu#0#)4(aFLQ`ttad_v&+6$YV;5#_Z)nzZPTKgY=eubBikL
+a&eZU$QD%PU(d!Iw6Jr6Um9RX^v#V(TjAVos~_uCS0iS^kd_UAp)l$QpYh&cd
RqXxXJJLiw`
+G)fY&@HpsaN4ii=2o*tTHh;6R0r3RhJk^vC^Cx$)o7?4K-IJoCYFf8K~PsKFufGkdx}n2L1J_XXkpbY
+iNYC)X>_J2oZ%cx?e7c{`-QfmiX*jZ9=xa(={n&paj*}cU}WjI+3l=oQ4RL+(
+?Y&Yv`?GhL67?Ux;u$))JfV83?4XJKdE&;WwZpZy=-+$6iGQ$4%8AZfc
+YXz>}K=U8GqQ$CyEqZ?{XWT6WKyTu7|ya=3C(w#Q73Q5&=(khy1$DXE4ox#)MgXeT}CvM*!oV)--Mu)
+tjMn!*9w9>?L}?vLKXLVz6T*K9VG^Xq*`IJ|lz2{b}_%*Vw90cuhi>Zt-h2`6vGZP)h>@6aWAK2mt$eR#U^Vgc8LJ001N
+^000{R003}la4%nJZggdGZeeUMV{BOi0Bj-E$L`4Sx+QWLzg7U(G
+lNwqF1C5+bwUm?h&XprF)jULGfmMI!Je`*sZ$hX?L{g#hz56#TN~~B#w0Gx}HgCzNcmZ=Z=U0Rk
+c-LtaHj*gD1hDu33^-QV5-&xcBEc!iFFm37#QoL_#27=VV=WKOE-~xlbCLNCEtbg8s+Q~KRF9n!?2jp
+bq=<#l}aiRSH$JcB_N?0dXWtmFWY;y99_FK6!rh4}5>Slb*$muKcQMAlVjn}c6r4y2C9q$LJ$R#7R;o
+Fwg9{5rpv50wEC{*Q3ox&Gwud`SceWtA;kDZ7Z^ot35
+G812&%v^#ru}BGRwxMQCLurF-M>oo0Li-Xbxsn^aVazUkh}kEEr(WJ$?}>u0|ZUT1kxBYCPG38a7DNztx!5;1f;%i)IpK$6vO
+ZL*MLkaWwe9QytNDSUr?*UF)1MC};-51JQkbAbOCY1vJ~;qN6=H;lE0p^iYHB^%y@rK~{y@dAZafVPey2Iw^`d1x
+AqTYAwD0o$cNmM8%l@)hAT4$O-_Kqc(1y;-6GTCj>euj=9o!6C`nF{klf-=b
+Hi-i*8-Y>RTHM@zeL!|_88Z&|R6_~rQ70}$uKI?znq48ghAZQg7mE~+iAN{-k>dojzjQU={C4}6ZRpS
+_83OJI2%k{)8$h;D7DPbxAhl$skT4s;C!Gl_u=#ZdNfiDKI615jvu^gbHp6@O7U9euh
+oVC1iYsrURUC16;teea|Wd=^Yiavq&G27M2-i($|d?wniaXqYz!xK#hV0nJW#YTij$pBS>lG`&JlhhZ
+%r?mJGIj@Ha0{`sg5vB)9}fqG(&OFug7n9Y(81T$D!XZavcHRJfN}Z1$(%n8k8q+_Bt9J#N!%y2wF?#
+*wz*nCU+N{Z@ywSuw@VU=$*fFP}doi|xu&X~;!au8|#UvBGuu#sj~29h3Jki8AV<+9h*x!_aewrY7_f
+K|BilJJyLdVn+`~Rm>$uMvq91GdxM`nV=ZA@DT$jq+Rmztu)>jJ?k3S)>0UiM|%PIC4xXl4r5L-iN$y
+4bQWjU$yZ*QMHh92-?kS+H!u5meY*lW^+-8Cm36cEenEx-$b7FS1j`YRf>5;amS4j*>`L{*2ZdTN7Zt
+RwLxJNJwOc{q)5@X~J`RWw*y;tAott@=s@Jd0+rM_bfOu1fQKgm}`cnzXSLX?>8*EOkX9~Iql=BO7zK
+)eD40hQScf6_@Y+4s)o3Mqj6*~!PgE578(yUk07KdKTk;zg4^Q~DzCQ(=;ai7^zDL^@S;v%u8OXpndh
++&uUX2+}DxYc1Qn6|nDsdlWovb&sS6L#n}ys-(5wb#5CEp~MvN+G9ldI{g+ZL84r$0yJ6CzL_=rk0vs
+aVZ*XRjdg1?Z5&*Q-C@i8$V|9DQLodOIrlF0|WdMelW{~PeHdabyIt0vAfU6?t3~?TFXz{!gF%P4i=t
+4!kXHA@ON6A>JL%qXSW?QpJ@sJJI!edR(4ylPR%F&CsONnD=Zml!U;MkDO9>iUIPx*M53w&Ks`072dp
++i$ANkiINZ-ryF6eJ08sqVZ<=BAPxLj^wsjdWDY;^MIR(D!Pp@@jdaYFiTDiFJ@pfQOoeL#uRAP`qwj
+5eJ6@hZE1yDfF@mV9fsgOp5-t(Fnxp22iwvD)?O_EQJ0CmXV5YBv0`!{ER;24GMZuWIkd+>goj)eC2to}8jkTi
+6Rn1IWL9JEYsq!;8d)Sr(#k?;8E=sMiPSHTAV#Vgzl+!^;YeZFB)raA5c8k_CM-E7LE@X_vU1$is*E8
+qsD8bHV$tFTwI1NsI%5_M9+RzOkDP_jt?7gNTt(VpU_F0<)4$JpvMB+LDPTMPE%A*gSi#01aLSm7lE2
+*D90k2zXrXIm^St0ZJxGPD)?r}BW=F%-KD?WcmIq_t!a&@6_Rmd68-ujS}Pnm9QO40wq{Sj}8YA`-Zk^Yym)w!Yc3vU?x8B3W3p#S!LUc#XL#ab%JQisvEC(O8QDg9G|41W9X*|0L7
+k_svj7hCz+=W!mL=Abb&V9Wj)Rvv8DmcaF>M}6TXP^(uUU1BEB`?mE7ICg;JcH*LEX7uZ3^ILZ}3F-i
+kly!<--n@kCxn`Q`xZZ~+Vuf??S3D@G1XK5vCPZ;P@)4L;Y3!=sY3=P$4nRE*Ze*lgDYsrJ_xhwv?H7
+Usg%K_!M(B}={OFm|W=>7?O^4e@%CxxR}hV#6r6Wy3(L1L!kiEB>Fzi~lE&u=X20gK<1}0q&SCTDFBG
+raSqH;g1O(aSa604|^4^Ui-~+bfL%im;w%=&L0B@KA6uS-+-_EPkqdQjfj&O3&Z~w?kwjp6l;SXgHlI
+KH4W@2nd$Oi27)0MPx5?7tCzI8ipUivK}YytYY?w-pTy=zyanhk_*j|)cS~mXAbYC+pcb4sqP@nBkg3
+7!xLFg0rM#k6=Iy_>l_K8RP`3uO2_p21Y+zrhcJ2`U*lM>&>|P~uF=k`I2$0t;b79~tk5+ugD48oSm0CRS(4#7VO?~Tu!T95uroXtfZB6|$
+8|GCKzRzOVz8$p;RyxZ;6aB;sEQj<p8Qe6|dr`rz
+P(Ie7JtpT(og?x(#m2%FIV=7GNN`85xfuEo08L$s$55^ZW=lPrIarW+)EA7P}s8^N#ObRRzjYQK0JO=*K6ekd}YS5%@qq*;av02PK<-2q4+sYCQyxV%
+8--+q>&kAS~+aAF;0c`oqKF~+y2*N1XIq~$ecD?4WK`-Qo~&3sKCyJ_5EGTmN!=ev$vg_2cvhgEJ1xKbzTbNZe3Nm7As
+L%UgG))FN1w6(ek_Ym;K!#v(OMUYpg6ymaA3GZCwBH>izeB`*=OPyi|)tPqT#}5DF5^N>vTht)$5#(O
+l^jw^Jj37Ne2Xo3jzaXIH1VI#;FAe8oqP*k6=2OwCy#=X0U4DHB>NZCK7LtXm1s#4e?oFj?hgQ;@9J8
+)L=lEiGj*78qSQ*6vO=5)ZkkEqm*}poAAEZp+TudsRWu5sc2JMwBAA(}?#pFS#+}S_=rIMRDdWIOktMaeJpDM+vd#Y%N%!swbA+Rcg)FOPH?ZNP073!SF4
+s32ulA6i>>MeQu0p5Vcu}z|0b@v|uL;F*Hr>@NOyzeu9usu6Yh$F{Im?!>Crdjn~pH{o066VAsmgqdjMzS}FE$B}iAb-An;a2~
+d+Hog7`gyY6;(yKl}}DUBT;BCeO*2m|s@w`ps-8@(aD3gNINt+o_@q6Ks&BO*B5V@eAC4qZj41GfO;L
+LyX{cN#hO_FpS~XZn`+M)&3h!W1gOtby5_SGh#WpdHER!MdKBOv%WWB)v06$tkoSY-ffxgz1wc*=HrI
+ai?CIIqAhXDv|m?@L~6pcel50iHpc#cXDK<5qm2-4;hW7Zva1q8>LmbU+sy%@M+tG|O>Henp81o?kFku{*JuDXsx;8dO_@FoDxRiH-EoFg>%<@JXB*zqtmdMfl?Yx%wVdP4dT(`yz7{
+R$`#QKSWnc(Lxi^>IHabP$`ONYUP%^FH9Riqs@#*a*n3%$XPJdB(1+1
+Itk9&9JE*r{23>xMv0G2hj6;I+8mKFJO4sH}gd=8`;#YWW!
+-CMIw7VD+Vm#_q4VhN^LEFNk&7giRcQf4Lo1+iM
+gJOd7pdJ|ItKGA0!%DmKq7n>FdD{`$bVZDcQ9s
+g?_V+cRLHa7FDyd`IF%3yU`i7LTOaF*1t*y=3$;kq4-pr^iIjJ}B`08Y}sW&GQSxlIdSFc}x+Y+H-2@
+N-Cb#T&mf~T&`ZEA7G(3&YNMja`IYLqp$a7Y%uha1N}&n@#$(XL>zO-DVMQI7*b8Zwb_7)@f?x|gakE
+e2~Ch%&3J5V0;VP`lFO_rWPVaaq`>YFZAvTL=#a&V2Mz@bFoQz(+zT3&(TG>snNk9y5wdIAN&p6Jofc
+CLppSCquDk~`efch_+t
+@CnGchN`kzN6nOZuT!4dB0vcQi}DT~&x1S6M^$hg+87FygYq8l4P!tpQN`@*dAH<84sde&ZT4R2D7Bn
+A(uD7T2aOkTZuovj%C);!W9zw=4Bo5|{1fCJj~AH1oN9Kr|$e$MAz3&(q@ZW3LIixhb_#egW%lc2wb}
+?{9Hqoc!~r)6>)E?_Rz9j=JUc>f5cWS4pSn_n}yQ2w3-etN*eZ{Z18R%e*USz!!LEtaf++qo-`VGZe#
+#zhN&@`roJWHu)pG4l>wHi$SdCqUh`n{`*~VH$}UJ8EyG6Q1gbjnVMP*ZuN*=+$&kIpM}QkBJZW^@BG
+l6W&25*-(#*lXQ$ceaJ=_aNWddGrq^?J(lRFek`t4Bt%7i7i;)il+JJIVXsq}uY3B)6vR@+2)E2+)3$
+v?XuhpVMrr_Ap9!ZGQDm_qwl6JeeZKOa3d)T`7h`o7v`pOUdxxxsPTO>GFR#9ZcjwT0#oj9
+_007aX-69=QV^cIzB7E9M}A`xPCaXTg+)k-nvrKVPuJF`m2B*{)L38#>*5B4p1hrsbr-Cd2W>`c?|+J
+E0k;}N@Diu^7>$6;vQR28X?Od4G`H2ljrMYV^ID^)b5=(qLJ1mTEB^O~tf`(r-`v9yC&Mz#lfW=DVCd
+Hn@Of*S?-UcMN&3x>;K7mcm=IfwG&aXil8G-Tjs-fQzPl}f{rse8SX=SBleAB-)TQl@5~=}a*mUk2>B
+cT^X!JN$T2Y=N_-dk_K;lzcTSkkGnE1agg*dO*(KsP(*Twi?F&htf`lqdG?^+cpy!X19r>jcS&Izo8
+#s(=Iv2=O~9S(C)oP8s@aO^r%d6()cR0`g6%Ku8}Cbu^PLTztO8E?R&4|{`DVtP_K`p{%7ijpy9^SAI
+TW^Zov07i6_xXdLa9TXON_Q6Pe{3*uv*tKXG-bA~ct-5$n_UCi3HxV;5NciG)3Nm)(3xOyioK*1aJ&0
+_c{6s~s+IEEmDyy}*|8pSG@gl1=s=&};=@qp0*606h2CIS~iRgNTFF{`vO(+x9BmIY4x6RauQmMM{xx
+E`FTe{NuyhYv}unKWkO$bg$%%KoljL*(;gpX$JC=r8aP6z5w
+TA4nC)xcnMkv+l0I_dS|Ade0VK;>?KAia(sY7o>-qLUI@W5DyCJl#DKEKHVFpTg!dw?{NF+`7OFWib+
+Rs@ZHl8>FMp5hY|n)$wB}CAOHXWaA|NaUukZ1WpZv|Y%gPMX)
+khRabII^ZEaz0WG--d)mv?k+c=W`?q9)Ee~3G<)w4P5F3y1Nb|&fFG%lI+CA}Lg27w?;w9Rc<(uz`de
+1rM#S5+iMQlk1LGl#<&4YW;>#bU8uo+?sN6uqviE)$hx-GQ^$_3@Zh1>0tlv%JitP5%;s^U5Dk+q^cF>!3_wnMrLf7~(452E7jiQ&4kvSgx@><-pax4h8H;#D
+rVTru9|@gj<%#X{37>-`?sGCyl+zR8nZ?ArRsc72t1bzMrs0<5(YS*f1ZO$Wb0mipn}va;uTWOr(C#r
+nzj)oI1v3E8dKho%GJX61_A^i9bZs(T}vI2BdLTX+=bu&jaV=L6^EYitR2ErscrAB&oZ%bAWbZhWeD#
+*ETRF*ii_hEdS^wHu9ktyWSl1q$pm~gRfJ}M|7AR*nR?T=P+OE$^
+Ufr2FTZj;#4r!0Zo7(4#1$uSZip0407%c1^wJp=M%8oou&ligY#WkkQNP8vlW1?CHeB1eEK^$ZB!2jQ
+?u*i-$fbGB`8Lw)>r3rcI7F}Da4S0DT4|M2D!FMc$)ru}nm$-H3Z2j--$GYMhdX692gF(Z{q2IZE1S4
+WYd^5zBpR9xU0)jwtYPvrWdq)obrm*v)Um{1D_H8Mbc?rQ%N0N;WYQ)w+9wXULu#aE9ST12nA9>fehz
+Uw~&gvZ_q6~>L+S*?7cHOk~ohTqW>nUO*RAu>w4dDByFSdP$K!O3MS=Y5oJ#@vQs#dH=EB5ECl>FRsg
+dd;;gr>uWd0-rh=iD0wv%w>ulFwb7H3%)1@ZAp8Y)@wcHEJ_Q#FI(YgwmhUnGFc&*`ytG#yI162Vv0sd!w*$pa-Iu_Okt7V=V{DpU*1DebltO9~;!8AF4w|Bk
+3)vptZI?0o?mSh}36&%fU;4Z$LCSWvXI_u50Ax;$knlL%&7nb)n$G#qrdfE`*f8+QlFL@N
+a)$w<5nqOzx3E&x7c7EK;-_+UJobcpV*oHkR*^LabckE#Pg(8-X)7Zpe1)#F0Z3d#Ky2lIexwx
+{MPc$V@>Al)47w1LhD5nQi5A(#7?kMyc#HOHZaWC|OQiftlhTwWy{ufX(A(M(3#FQx*Xaa(_o?xxP|p
+IVSo)QX0P6Jyo>K6uN^?2wu{e_&p%%`!BTeuj
+!^ls-72>6e&pNM!grx8=G+XankMC-yxi!Vq)6f?kBU!$+quLwXc2^z%gjm40u0GOKVm`Eb8qT;AVs@a
+soN8IZ?&SOml}7z5F4&>X^B;D5noqW@K%yQM