├── .devcontainer ├── Dockerfile ├── Dockerfile copy ├── Makefile ├── README.md ├── devcontainer.json └── opengl_experiments │ ├── README.md │ ├── hybrid │ ├── Dockerfile │ └── Makefile │ └── rocker │ ├── Dockerfile │ └── Makefile ├── .github └── workflows │ ├── pre-merge.yaml │ ├── push-containers.yaml │ └── update-poetry-cache.yaml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── docs └── console.md ├── images ├── screenshot.png └── splash.png ├── js ├── jointjs │ ├── README │ ├── backbone-1.4.0.js │ ├── dagre-0.8.4.min.js │ ├── graphlib-2.1.7.min.js │ ├── joint-3.0.2.min.css │ ├── joint-3.0.2.min.js │ ├── joint-3.0.4.js │ ├── joint-3.0.4.min.css │ ├── joint-3.0.4.min.js │ ├── jquery-3.4.1.min.js │ └── lodash-4.17.11.min.js ├── py_trees-0.6.css └── py_trees-0.6.js ├── package.xml ├── poetry.lock ├── py_trees_js ├── __init__.py ├── gen.bash ├── resources.py ├── resources.qrc └── viewer │ ├── __init__.py │ ├── console.py │ ├── gen.bash │ ├── html │ └── index.html │ ├── images.qrc │ ├── images │ └── tuxrobot.png │ ├── images_rc.py │ ├── main_window.py │ ├── main_window.ui │ ├── main_window_ui.py │ ├── trees.py │ ├── viewer.py │ ├── web_app.qrc │ ├── web_app_rc.py │ ├── web_view.py │ ├── web_view.ui │ └── web_view_ui.py ├── pyproject.toml ├── scripts └── py-trees-devel-viewer ├── setup.py ├── tests ├── README.md ├── __init__.py └── test_launch.py └── tox.ini /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Base 3 | # - a single-version python slim-bullseye image 4 | # Installs 5 | # - poetry in /opt/poetry 6 | # - adds a user called 'zen' 7 | # Size 8 | # - 300MB 9 | ################################################################################ 10 | 11 | 12 | ARG PYTHON_VERSION=3.8.15 13 | ARG DEBIAN_VERSION=bullseye 14 | 15 | ARG NAME=py-trees-js 16 | ARG POETRY_VERSION=1.3.2 17 | 18 | FROM python:${PYTHON_VERSION}-slim-${DEBIAN_VERSION} 19 | # FROM ubuntu:20.04 20 | 21 | ENV POETRY_HOME=/opt/poetry 22 | ENV PATH="${POETRY_HOME}/bin:${PATH}" 23 | 24 | ################################################################################ 25 | # Debs 26 | ################################################################################ 27 | 28 | #################### 29 | # Core 30 | #################### 31 | RUN apt-get update && apt-get install -y --no-install-recommends \ 32 | # poetry 33 | curl \ 34 | python3-dev \ 35 | # pytrees 36 | graphviz \ 37 | # development 38 | bash \ 39 | bash-completion \ 40 | ca-certificates \ 41 | git \ 42 | less \ 43 | make \ 44 | ssh \ 45 | vim \ 46 | wget 47 | 48 | #################### 49 | # OpenGL 50 | #################### 51 | # mesa-utils : glxgears + gl libs (libgl# libglvnd#, libglx#) 52 | # egl: not needed (libegl1, libgles2) 53 | # vulkan: not needed 54 | RUN apt-get install -y --no-install-recommends \ 55 | mesa-utils 56 | 57 | #################### 58 | # Qt5 webengine 59 | #################### 60 | # Should just install pyqt5webengine from debs instead? 61 | RUN apt-get install -y --no-install-recommends \ 62 | libasound2 \ 63 | libdbus-1-dev \ 64 | libgl1 \ 65 | libnss3 \ 66 | libxcomposite1 \ 67 | libxcursor1 \ 68 | libxdamage1 \ 69 | libxi6 \ 70 | libxkbcommon0 \ 71 | libxkbcommon-x11-0 \ 72 | libxrandr2 \ 73 | libxtst6 74 | 75 | ################################################################################ 76 | # Poetry 77 | ################################################################################ 78 | 79 | # Don't permit virtualenvs for root / ci jobs (this configuration). 80 | # Do permit virtualenvs for user zen (uses the default). 81 | RUN curl -sSL https://install.python-poetry.org | POETRY_VERSION=${POETRY_VERSION} python3 - && \ 82 | poetry config virtualenvs.create false && \ 83 | poetry completions bash >> ~/.bash_completion 84 | 85 | 86 | ################################################################################ 87 | # NVIDIA 88 | ################################################################################ 89 | 90 | ENV NVIDIA_VISIBLE_DEVICES ${NVIDIA_VISIBLE_DEVICES:-all} 91 | ENV NVIDIA_DRIVER_CAPABILITIES ${NVIDIA_DRIVER_CAPABILITIES:+$NVIDIA_DRIVER_CAPABILITIES,}graphics,display,video,utility,compute 92 | 93 | ################################################################################ 94 | # Login Shells for Debugging & Development 95 | ################################################################################ 96 | 97 | # In a login shell (below), the PATH env doesn't survive, configure it at ground zero 98 | RUN echo "export PATH=${POETRY_HOME}/bin:${PATH}" >> /etc/profile 99 | ENV TERM xterm-256color 100 | ENTRYPOINT ["/bin/bash", "--login", "-i"] 101 | 102 | ################################################################################ 103 | # Development with a user, e.g. for vscode devcontainers 104 | ################################################################################ 105 | 106 | ARG USERNAME=zen 107 | ARG USER_UID=1001 108 | ARG USER_GID=${USER_UID} 109 | 110 | RUN groupadd --gid $USER_GID $USERNAME && \ 111 | useradd --uid $USER_UID --gid $USER_GID -s "/bin/bash" -m $USERNAME && \ 112 | apt-get install -y sudo && \ 113 | echo "${USERNAME} ALL=NOPASSWD: ALL" > /etc/sudoers.d/${USERNAME} && \ 114 | chmod 0440 /etc/sudoers.d/${USERNAME} 115 | RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@${NAME}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/${USERNAME}/.bashrc && \ 116 | echo "alias ll='ls --color=auto -alFNh'" >> /home/${USERNAME}/.bashrc && \ 117 | echo "alias ls='ls --color=auto -Nh'" >> /home/${USERNAME}/.bashrc && \ 118 | poetry completions bash >> /home/${USERNAME}/.bash_completion 119 | 120 | # touch /home/${USERNAME}/.bash_completion && chown ${USERNAME}:${USERNAME} /home/${USERNAME}/.bash_completion 121 | 122 | ################################################################################ 123 | # Debugging with root 124 | ################################################################################ 125 | 126 | RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@${NAME}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> ${HOME}/.bashrc && \ 127 | echo "alias ll='ls --color=auto -alFNh'" >> ${HOME}/.bashrc && \ 128 | echo "alias ls='ls --color=auto -Nh'" >> ${HOME}/.bashrc 129 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile copy: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Base 3 | # - a single-version python slim-bullseye image 4 | # Installs 5 | # - poetry in /opt/poetry 6 | # - adds a user called 'zen' 7 | # Size 8 | # - 300MB 9 | ################################################################################ 10 | 11 | # FROM nvidia/opengl:1.2-glvnd-devel-ubuntu20.04 as glvnd 12 | 13 | ARG PYTHON_VERSION=3.8.15 14 | ARG DEBIAN_VERSION=bullseye 15 | 16 | FROM python:${PYTHON_VERSION}-slim-${DEBIAN_VERSION} 17 | 18 | ARG NAME=poetry-zen 19 | ARG POETRY_VERSION=1.3.2 20 | ENV POETRY_HOME=/opt/poetry 21 | ENV PATH="${POETRY_HOME}/bin:${PATH}" 22 | 23 | ################################################################################ 24 | # Poetry 25 | ################################################################################ 26 | 27 | RUN apt-get update && apt-get install -y --no-install-recommends \ 28 | # For poetry 29 | curl \ 30 | # For pytrees 31 | graphviz \ 32 | make \ 33 | # For the qt5 viewer 34 | libasound2 \ 35 | libdbus-1-dev \ 36 | libgl1 \ 37 | libnss3 \ 38 | libxcomposite1 \ 39 | libxcursor1 \ 40 | libxi6 \ 41 | libxkbcommon0 \ 42 | libxkbcommon-x11-0 \ 43 | libxrandr2 \ 44 | libxtst6 \ 45 | # For convenience 46 | bash \ 47 | bash-completion \ 48 | ca-certificates \ 49 | git \ 50 | less \ 51 | ssh \ 52 | vim \ 53 | wget \ 54 | && \ 55 | rm -rf /var/lib/apt/lists/* && \ 56 | curl -sSL https://install.python-poetry.org | POETRY_VERSION=${POETRY_VERSION} python3 - && \ 57 | poetry config virtualenvs.create false && \ 58 | poetry completions bash >> ~/.bash_completion 59 | 60 | ################################################################################ 61 | # NVIDIA 62 | ################################################################################ 63 | 64 | # COPY --from=glvnd /usr/share/glvnd/egl_vendor.d/10_nvidia.json /usr/share/glvnd/egl_vendor.d/10_nvidia.json 65 | # ENV NVIDIA_VISIBLE_DEVICES ${NVIDIA_VISIBLE_DEVICES:-all} 66 | # ENV NVIDIA_DRIVER_CAPABILITIES ${NVIDIA_DRIVER_CAPABILITIES:+$NVIDIA_DRIVER_CAPABILITIES,}graphics 67 | 68 | ################################################################################ 69 | # Login Shells for Debugging & Development 70 | ################################################################################ 71 | 72 | # In a login shell (below), the PATH env doesn't survive, configure it at ground zero 73 | RUN echo "export PATH=${POETRY_HOME}/bin:${PATH}" >> /etc/profile 74 | ENV TERM xterm-256color 75 | ENTRYPOINT ["/bin/bash", "--login", "-i"] 76 | 77 | ################################################################################ 78 | # Development with a user, e.g. for vscode devcontainers 79 | ################################################################################ 80 | 81 | ARG USERNAME=zen 82 | ARG USER_UID=1000 83 | ARG USER_GID=${USER_UID} 84 | 85 | RUN groupadd --gid $USER_GID $USERNAME && \ 86 | useradd --uid $USER_UID --gid $USER_GID -s "/bin/bash" -m $USERNAME && \ 87 | apt-get install -y sudo && \ 88 | echo "${USERNAME} ALL=NOPASSWD: ALL" > /etc/sudoers.d/${USERNAME} && \ 89 | chmod 0440 /etc/sudoers.d/${USERNAME} 90 | RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@${NAME}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/${USERNAME}/.bashrc && \ 91 | echo "alias ll='ls --color=auto -alFNh'" >> /home/${USERNAME}/.bashrc && \ 92 | echo "alias ls='ls --color=auto -Nh'" >> /home/${USERNAME}/.bashrc && \ 93 | poetry completions bash >> /home/${USERNAME}/.bash_completion 94 | 95 | # touch /home/${USERNAME}/.bash_completion && chown ${USERNAME}:${USERNAME} /home/${USERNAME}/.bash_completion 96 | 97 | ################################################################################ 98 | # Debugging with root 99 | ################################################################################ 100 | 101 | RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@${NAME}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> ${HOME}/.bashrc && \ 102 | echo "alias ll='ls --color=auto -alFNh'" >> ${HOME}/.bashrc && \ 103 | echo "alias ls='ls --color=auto -Nh'" >> ${HOME}/.bashrc 104 | -------------------------------------------------------------------------------- /.devcontainer/Makefile: -------------------------------------------------------------------------------- 1 | TAG=original 2 | REPO=devel 3 | 4 | help: 5 | @echo "Usage:" 6 | @echo "" 7 | @echo " image : build an image" 8 | @echo " container : create a container that persists" 9 | @echo " start : execute from the container" 10 | @echo " clean : clean up container" 11 | @echo " pristine : clean image and container" 12 | @echo "" 13 | @echo "Be Froody." 14 | 15 | image: 16 | docker build \ 17 | --build-arg POETRY_VERSION=1.3.2 \ 18 | --build-arg PYTHON_VERSION=3.8.16 \ 19 | --build-arg DEBIAN_VERSION=bullseye \ 20 | --build-arg NAME=${TAG} \ 21 | -t ${REPO}:${TAG} . 22 | 23 | container: 24 | docker container create --tty --network host -i --gpus all -v /tmp/.X11-unix:/tmp/.X11-unix:ro -e DISPLAY --name=${TAG} ${REPO}:${TAG} 25 | 26 | run: clean 27 | docker run -it --mount type=bind,source=/mnt/mervin/workspaces/foo,target=/mnt/foo --name=${TAG} --network host --gpus all --volume /tmp/.X11-unix:/tmp/.X11-unix:ro --env DISPLAY --env NVIDIA_VISIBLE_DEVICES=all --env NVIDIA_DRIVER_CAPABILITIES=graphics,display,video,utility,compute --user zen ${REPO}:${TAG} -i 28 | 29 | start: 30 | docker container start -i ${TAG} 31 | 32 | clean: 33 | -docker container rm ${TAG} 34 | 35 | pristine: clean 36 | docker image rm ${REPO}:${TAG} 37 | -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # Development Environment 2 | 3 | These VSCode devcontainers setup multiple environments for testing against 4 | different python versins. 5 | 6 | ## Setup 7 | 8 | ``` 9 | $ git clone git@github.com:splintered-reality/py_trees.git 10 | $ code ./py_trees 11 | ``` 12 | 13 | ## VSCode DevContainer 14 | 15 | At this point you can either "Re-open project in container" to develop against 16 | the default python version. 17 | 18 | Alternatively "Open Folder in Container" and point it at one of the 19 | `py` subfolders in this directory to develop against a different 20 | python version. 21 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "py_trees-js-38", 3 | 4 | "build": { 5 | "dockerfile": "./Dockerfile", 6 | "args": { 7 | "NAME": "py_trees_js", 8 | "POETRY_VERSION": "1.3.2", 9 | "PYTHON_VERSION": "3.8.16", 10 | "DEBIAN_VERSION": "bullseye" 11 | }, 12 | "context": ".." 13 | }, 14 | "containerEnv": { 15 | "DISPLAY": "${localEnv:DISPLAY}", 16 | "POETRY_HTTP_BASIC_PYPI_USERNAME": "${localEnv:POETRY_HTTP_BASIC_PYPI_USERNAME}", 17 | "POETRY_HTTP_BASIC_PYPI_PASSWORD": "${localEnv:POETRY_HTTP_BASIC_PYPI_PASSWORD}", 18 | "QT_DEBUG_PLUGINS": "1" 19 | }, 20 | "remoteUser": "zen", 21 | "customizations": { 22 | "vscode": { 23 | "extensions": [ 24 | "bierner.github-markdown-preview", 25 | "bierner.markdown-preview-github-styles", 26 | "bungcip.better-toml", 27 | "streetsidesoftware.code-spell-checker", 28 | "ms-python.python", 29 | "omnilib.ufmt", 30 | "tht13.rst-vscode" 31 | ] 32 | } 33 | }, 34 | "postCreateCommand": "poetry install --all-extras", 35 | "mounts": [ 36 | { 37 | "source": "/tmp/.X11-unix", 38 | "target": "/tmp/.X11-unix:ro", 39 | "type": "bind" 40 | } 41 | ], 42 | "runArgs": [ 43 | "--runtime=nvidia", 44 | "--gpus", 45 | "all", 46 | "--network", 47 | "host" 48 | ] 49 | // Breaks codespaces 50 | // "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces,type=bind", 51 | // "workspaceFolder": "/workspaces" 52 | } 53 | -------------------------------------------------------------------------------- /.devcontainer/opengl_experiments/README.md: -------------------------------------------------------------------------------- 1 | ## XAuth 2 | 3 | Read some articles that were using XAuth to help remotely forward across ssh connections 4 | with different `DISPLAY` values. 5 | 6 | What hasn't worked: 7 | 8 | * Install `xauth` in the `Dockerfile` / mount `.Xauthority` in the `devcontainer.json`: 9 | 10 | ``` 11 | { 12 | "source": "${localEnv:HOME}${localEnv:USERPROFILE}/.Xauthority", 13 | "target": "/home/zen/.Xauthority", 14 | "type": "bind" 15 | }, 16 | ``` -------------------------------------------------------------------------------- /.devcontainer/opengl_experiments/hybrid/Dockerfile: -------------------------------------------------------------------------------- 1 | # Preamble from extension [nvidia] 2 | FROM nvidia/opengl:1.2-glvnd-devel-ubuntu20.04 as glvnd 3 | 4 | # Preamble from extension [user] 5 | 6 | 7 | FROM ubuntu:20.04 8 | USER root 9 | # Snippet from extension [nvidia] 10 | RUN DEBIAN__FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends \ 11 | glmark2 \ 12 | libglvnd0 \ 13 | libgl1 \ 14 | libglx0 \ 15 | libegl1 \ 16 | libgles2 \ 17 | libvulkan1 \ 18 | libvulkan-dev \ 19 | vulkan-utils \ 20 | && rm -rf /var/lib/apt/lists/* && \ 21 | VULKAN_API_VERSION=`dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | grep -oP '[0-9|\.]+'` && \ 22 | mkdir -p /etc/vulkan/icd.d/ && \ 23 | echo \ 24 | "{\ 25 | \"file_format_version\" : \"1.0.0\",\ 26 | \"ICD\": {\ 27 | \"library_path\": \"libGLX_nvidia.so.0\",\ 28 | \"api_version\" : \"${VULKAN_API_VERSION}\"\ 29 | }\ 30 | }" > /etc/vulkan/icd.d/nvidia_icd.json 31 | 32 | # needed? Seems to work fine without this for glmark2 and vkcube 33 | # COPY --from=glvnd /usr/share/glvnd/egl_vendor.d/10_nvidia.json /usr/share/glvnd/egl_vendor.d/10_nvidia.json 34 | 35 | ENV NVIDIA_VISIBLE_DEVICES ${NVIDIA_VISIBLE_DEVICES:-all} 36 | ENV NVIDIA_DRIVER_CAPABILITIES ${NVIDIA_DRIVER_CAPABILITIES:+$NVIDIA_DRIVER_CAPABILITIES,}graphics,display,video,utility,compute 37 | 38 | # Snippet from extension [user] 39 | # make sure sudo is installed to be able to give user sudo access in docker 40 | RUN if ! command -v sudo >/dev/null; then \ 41 | apt-get update \ 42 | && apt-get install -y sudo \ 43 | && apt-get clean; \ 44 | fi 45 | 46 | ################################################################################ 47 | # Login Shells for Debugging & Development 48 | ################################################################################ 49 | 50 | # In a login shell (below), the PATH env doesn't survive, configure it at ground zero 51 | # RUN echo "export PATH=${POETRY_HOME}/bin:${PATH}" >> /etc/profile 52 | ENV TERM xterm-256color 53 | ENTRYPOINT ["/bin/bash", "--login", "-i"] 54 | 55 | ################################################################################ 56 | # Development with a user, e.g. for vscode devcontainers 57 | ################################################################################ 58 | 59 | ARG USERNAME=zen 60 | ARG USER_UID=1000 61 | ARG USER_GID=${USER_UID} 62 | 63 | RUN groupadd --gid $USER_GID $USERNAME && \ 64 | useradd --uid $USER_UID --gid $USER_GID -s "/bin/bash" -m $USERNAME && \ 65 | apt-get install -y sudo && \ 66 | echo "${USERNAME} ALL=NOPASSWD: ALL" > /etc/sudoers.d/${USERNAME} && \ 67 | chmod 0440 /etc/sudoers.d/${USERNAME} 68 | RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@${NAME}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/${USERNAME}/.bashrc && \ 69 | echo "alias ll='ls --color=auto -alFNh'" >> /home/${USERNAME}/.bashrc && \ 70 | echo "alias ls='ls --color=auto -Nh'" >> /home/${USERNAME}/.bashrc 71 | # && \ 72 | # poetry completions bash >> /home/${USERNAME}/.bash_completion 73 | 74 | # RUN usermod -a -G video zen <- doesn't do anything 75 | 76 | ################################################################################ 77 | # Debugging with root 78 | ################################################################################ 79 | 80 | RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@${NAME}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> ${HOME}/.bashrc && \ 81 | echo "alias ll='ls --color=auto -alFNh'" >> ${HOME}/.bashrc && \ 82 | echo "alias ls='ls --color=auto -Nh'" >> ${HOME}/.bashrc 83 | 84 | # RUN existing_user_by_uid=`getent passwd "1001" | cut -f1 -d: || true` && \ 85 | # if [ -n "${existing_user_by_uid}" ]; then userdel -r "${existing_user_by_uid}"; fi && \ 86 | # existing_user_by_name=`getent passwd "danielstonier" | cut -f1 -d: || true` && \ 87 | # if [ -n "${existing_user_by_name}" ]; then userdel -r "${existing_user_by_name}"; fi && \ 88 | # existing_group_by_gid=`getent group "1001" | cut -f1 -d: || true` && \ 89 | # if [ -z "${existing_group_by_gid}" ]; then \ 90 | # groupadd -g "1001" "danielstonier"; \ 91 | # fi && \ 92 | # useradd --no-log-init --no-create-home --uid "1001" -s "/bin/bash" -c "Daniel Stonier,,," -g "1001" -d "/home/danielstonier" "danielstonier" && \ 93 | # echo "danielstonier ALL=NOPASSWD: ALL" >> /etc/sudoers.d/rocker 94 | # 95 | # # Making sure a home directory exists if we haven't mounted the user's home directory explicitly 96 | # RUN mkdir -p "$(dirname "/home/danielstonier")" && mkhomedir_helper danielstonier 97 | # # Commands below run as the developer user 98 | # USER danielstonier 99 | # WORKDIR /home/danielstonier 100 | # RUN echo "alias ll='ls --color=auto -alFNh'" >> ~/.bashrc 101 | # RUN echo "alias ls='ls --color=auto -Nh'" >> ~/.bashrc 102 | 103 | # RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@foo\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/danielstonier/.bashrc 104 | -------------------------------------------------------------------------------- /.devcontainer/opengl_experiments/hybrid/Makefile: -------------------------------------------------------------------------------- 1 | TAG=hybrid 2 | REPO=devel 3 | 4 | help: 5 | @echo "Usage:" 6 | @echo "" 7 | @echo " image : build an image" 8 | @echo " container : create a container that persists" 9 | @echo " start : execute from the container" 10 | @echo " clean : clean up container" 11 | @echo " pristine : clean image and container" 12 | @echo "" 13 | @echo "Be Froody." 14 | 15 | image: 16 | docker build \ 17 | --build-arg POETRY_VERSION=1.3.2 \ 18 | --build-arg PYTHON_VERSION=3.8.16 \ 19 | --build-arg DEBIAN_VERSION=bullseye \ 20 | --build-arg NAME=${TAG} \ 21 | -t ${REPO}:${TAG} . 22 | 23 | container: 24 | docker container create --tty --network host -i --gpus 'all,"capabilities=graphics,utility,display,video,compute"' -v /tmp/.X11-unix:/tmp/.X11-unix:ro -e DISPLAY --name=${TAG} ${REPO}:${TAG} 25 | 26 | run: clean 27 | docker run -it --mount type=bind,source=/mnt/mervin/workspaces/foo,target=/mnt/foo --name=${TAG} --network host --gpus 'all,"capabilities=graphics,utility,display,video,compute"' --volume /tmp/.X11-unix:/tmp/.X11-unix:ro --env DISPLAY --user zen ${REPO}:${TAG} -i 28 | 29 | start: 30 | docker container start -i ${TAG} 31 | 32 | clean: 33 | -docker container rm ${TAG} 34 | 35 | pristine: clean 36 | docker image rm ${REPO}:${TAG} 37 | -------------------------------------------------------------------------------- /.devcontainer/opengl_experiments/rocker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Preamble from extension [nvidia] 2 | FROM nvidia/opengl:1.2-glvnd-devel-ubuntu20.04 as glvnd 3 | 4 | # Preamble from extension [user] 5 | 6 | 7 | FROM ubuntu:20.04 8 | USER root 9 | # Snippet from extension [nvidia] 10 | RUN DEBIAN__FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends \ 11 | glmark2 \ 12 | libglvnd0 \ 13 | libgl1 \ 14 | libglx0 \ 15 | libegl1 \ 16 | libgles2 \ 17 | libvulkan1 \ 18 | libvulkan-dev \ 19 | vulkan-utils \ 20 | && rm -rf /var/lib/apt/lists/* && \ 21 | VULKAN_API_VERSION=`dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | grep -oP '[0-9|\.]+'` && \ 22 | mkdir -p /etc/vulkan/icd.d/ && \ 23 | echo \ 24 | "{\ 25 | \"file_format_version\" : \"1.0.0\",\ 26 | \"ICD\": {\ 27 | \"library_path\": \"libGLX_nvidia.so.0\",\ 28 | \"api_version\" : \"${VULKAN_API_VERSION}\"\ 29 | }\ 30 | }" > /etc/vulkan/icd.d/nvidia_icd.json 31 | 32 | # needed? 33 | COPY --from=glvnd /usr/share/glvnd/egl_vendor.d/10_nvidia.json /usr/share/glvnd/egl_vendor.d/10_nvidia.json 34 | 35 | ENV NVIDIA_VISIBLE_DEVICES ${NVIDIA_VISIBLE_DEVICES:-all} 36 | ENV NVIDIA_DRIVER_CAPABILITIES ${NVIDIA_DRIVER_CAPABILITIES:+$NVIDIA_DRIVER_CAPABILITIES,}graphics,display,video,utility,compute 37 | 38 | # Snippet from extension [user] 39 | # make sure sudo is installed to be able to give user sudo access in docker 40 | RUN if ! command -v sudo >/dev/null; then \ 41 | apt-get update \ 42 | && apt-get install -y sudo \ 43 | && apt-get clean; \ 44 | fi 45 | 46 | RUN existing_user_by_uid=`getent passwd "1001" | cut -f1 -d: || true` && \ 47 | if [ -n "${existing_user_by_uid}" ]; then userdel -r "${existing_user_by_uid}"; fi && \ 48 | existing_user_by_name=`getent passwd "danielstonier" | cut -f1 -d: || true` && \ 49 | if [ -n "${existing_user_by_name}" ]; then userdel -r "${existing_user_by_name}"; fi && \ 50 | existing_group_by_gid=`getent group "1001" | cut -f1 -d: || true` && \ 51 | if [ -z "${existing_group_by_gid}" ]; then \ 52 | groupadd -g "1001" "danielstonier"; \ 53 | fi && \ 54 | useradd --no-log-init --no-create-home --uid "1001" -s "/bin/bash" -c "Daniel Stonier,,," -g "1001" -d "/home/danielstonier" "danielstonier" && \ 55 | echo "danielstonier ALL=NOPASSWD: ALL" >> /etc/sudoers.d/rocker 56 | 57 | # Making sure a home directory exists if we haven't mounted the user's home directory explicitly 58 | RUN mkdir -p "$(dirname "/home/danielstonier")" && mkhomedir_helper danielstonier 59 | # Commands below run as the developer user 60 | USER danielstonier 61 | WORKDIR /home/danielstonier 62 | RUN echo "alias ll='ls --color=auto -alFNh'" >> ~/.bashrc 63 | RUN echo "alias ls='ls --color=auto -Nh'" >> ~/.bashrc 64 | 65 | RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@foo\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/danielstonier/.bashrc 66 | -------------------------------------------------------------------------------- /.devcontainer/opengl_experiments/rocker/Makefile: -------------------------------------------------------------------------------- 1 | TAG=bar 2 | 3 | # Private variables 4 | REPO=devel 5 | 6 | help: 7 | @echo "Usage:" 8 | @echo "" 9 | @echo " image : build an image" 10 | @echo " container : create a container that persists" 11 | @echo " start : execute from the container" 12 | @echo " clean : clean up container" 13 | @echo " pristine : clean image and container" 14 | @echo "" 15 | @echo "Be Froody." 16 | 17 | image: 18 | docker build \ 19 | --build-arg POETRY_VERSION=1.3.2 \ 20 | --build-arg PYTHON_VERSION=3.8.16 \ 21 | --build-arg DEBIAN_VERSION=bullseye \ 22 | --build-arg NAME=${TAG} \ 23 | -t ${REPO}:${TAG} . 24 | 25 | container: 26 | docker container create --tty --network host -i --gpus 'all,"capabilities=graphics,utility,display,video,compute"' -v /tmp/.X11-unix:/tmp/.X11-unix:ro -e DISPLAY --name=${TAG} ${REPO}:${TAG} 27 | 28 | run: clean 29 | docker run -it --mount type=bind,source=/mnt/mervin/workspaces/foo,target=/mnt/foo --name=${TAG} --network host --gpus 'all,"capabilities=graphics,utility,display,video,compute"' --volume /tmp/.X11-unix:/tmp/.X11-unix:ro --env DISPLAY ${REPO}:${TAG} /bin/bash --login -i 30 | # docker run -it --mount type=bind,source=/mnt/mervin/workspaces/foo,target=/mnt/foo 31 | # --name foo -v /home/danielstonier/.gitconfig:/home/danielstonier/.gitconfig:ro 32 | # --network host 33 | # --gpus 'all,"capabilities=graphics,utility,display,video,compute"' 34 | #--volume /tmp/.X11-unix:/tmp/.X11-unix:ro --env=DISPLAY devel:foo /bin/bash --login -i 35 | 36 | start: 37 | docker container start -i ${TAG} 38 | 39 | clean: 40 | -docker container rm ${TAG} 41 | 42 | pristine: clean 43 | docker image rm ${REPO}:${TAG} 44 | -------------------------------------------------------------------------------- /.github/workflows/pre-merge.yaml: -------------------------------------------------------------------------------- 1 | name: pre-merge 2 | 3 | env: 4 | REGISTRY: ghcr.io 5 | IMAGE_NAME: ${{ github.repository }} 6 | 7 | on: 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | pre-merge: 13 | runs-on: ubuntu-22.04 14 | strategy: 15 | matrix: 16 | python-version: ["3.8", "3.10"] 17 | include: 18 | - python-version: "3.8" 19 | python-py-version: "py38" 20 | - python-version: "3.10" 21 | python-py-version: "py310" 22 | container: 23 | image: ghcr.io/${{ github.repository }}-ci:${{ matrix.python-py-version }}-poetry-gl-bullseye 24 | credentials: 25 | username: ${{ github.actor }} 26 | password: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | - name: Poetry Venv Dir 31 | run: | 32 | echo "VENV_DIR=$(poetry config virtualenvs.path)" >> $GITHUB_ENV 33 | 34 | - name: Restore the Cache 35 | id: cache-deps 36 | uses: actions/cache@v3 37 | with: 38 | path: ${{ env.VENV_DIR }} 39 | # bump the suffix if you need to force-refresh the cache 40 | key: py-trees-ci-cache-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock', '**/tox.ini') }}-1 41 | 42 | # Install all deps, sans the project (--no-root) 43 | - name: Poetry - Install Dependencies 44 | run: poetry install --no-interaction --no-root 45 | if: steps.cache-deps.outputs.cache-hit != 'true' 46 | 47 | # Project is installed separately to avoid always invalidating the cache 48 | - name: Poetry - Install Project 49 | run: poetry install --no-interaction 50 | 51 | - name: Tox - Tests 52 | run: poetry run tox --workdir ${{ env.VENV_DIR }} -e ${{ matrix.python-py-version }} 53 | - name: Tox - Formatters, Linters 54 | run: poetry run tox --workdir ${{ env.VENV_DIR }} -e check 55 | -------------------------------------------------------------------------------- /.github/workflows/push-containers.yaml: -------------------------------------------------------------------------------- 1 | name: push-containers 2 | 3 | env: 4 | REGISTRY: ghcr.io 5 | IMAGE_NAME: ${{ github.repository }}-ci 6 | POETRY_VERSION: 1.3.2 7 | PYTHON_PRIMARY_VERSION: 3.8.16 8 | PYTHON_PRIMARY_TAG: py38 9 | PYTHON_SECONDARY_VERSION: 3.10.9 10 | PYTHON_SECONDARY_TAG: py310 11 | DEBIAN_VERSION: bullseye 12 | 13 | on: 14 | push: 15 | paths: 16 | - .devcontainer/Dockerfile 17 | branches: 18 | - devel 19 | workflow_dispatch: 20 | 21 | jobs: 22 | push-poetry-container: 23 | runs-on: ubuntu-22.04 24 | permissions: 25 | contents: read 26 | packages: write 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Login to GCR 31 | uses: docker/login-action@v2 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Metadata 37 | id: meta 38 | uses: docker/metadata-action@v4 39 | with: 40 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 41 | - name: Echo 42 | run: | 43 | echo "USER: ${{ github.actor }}" 44 | echo "REPOSITORY: ${{ github.repository }}" 45 | echo "POETRY_VERSION: ${POETRY_VERSION}" 46 | echo "PYTHON_PRIMARY_VERSION: ${PYTHON_PRIMARY_VERSION}" 47 | echo "PYTHON_SECONDARY_VERSION: ${PYTHON_SECONDARY_VERSION}" 48 | echo "TAGS: ${{ steps.meta.outputs.tags }}" 49 | echo "LABELS: ${{ steps.meta.outputs.labels }}" 50 | - name: Image - poetry${{ env.POETRY_VERSION }}-python${{ env.PYTHON_PRIMARY_VERSION }} 51 | uses: docker/build-push-action@v3 52 | with: 53 | file: ./.devcontainer/Dockerfile 54 | push: true 55 | build-args: | 56 | PYTHON_VERSION=${{ env.PYTHON_PRIMARY_VERSION }} 57 | POETRY_VERSION=${{ env.POETRY_VERSION }} 58 | DEBIAN_VERSION=${{ env.DEBIAN_VERSION }} 59 | tags: | 60 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.PYTHON_PRIMARY_TAG }}-poetry-gl-${{ env.DEBIAN_VERSION }} 61 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:python${{ env.PYTHON_PRIMARY_VERSION }}-poetry${{ env.POETRY_VERSION }}-gl-${{ env.DEBIAN_VERSION }} 62 | labels: ${{ steps.meta.outputs.labels }} 63 | - name: Image - poetry${{ env.POETRY_VERSION }}-python${{ env.PYTHON_SECONDARY_VERSION }} 64 | uses: docker/build-push-action@v3 65 | with: 66 | file: ./.devcontainer/Dockerfile 67 | push: true 68 | build-args: | 69 | PYTHON_VERSION=${{ env.PYTHON_SECONDARY_VERSION }} 70 | POETRY_VERSION=${{ env.POETRY_VERSION }} 71 | DEBIAN_VERSION=${{ env.DEBIAN_VERSION }} 72 | tags: | 73 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.PYTHON_SECONDARY_TAG }}-poetry-gl-${{ env.DEBIAN_VERSION }} 74 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:python${{ env.PYTHON_SECONDARY_VERSION }}-poetry${{ env.POETRY_VERSION }}-gl-${{ env.DEBIAN_VERSION }} 75 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/update-poetry-cache.yaml: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Ensure poetry and tox installed dependencies are in the cache. 3 | # 4 | # All PR's can reuse devel's caches, but a PR's cache cannot be reused from 5 | # one PR to the next. This jobs' sole purpose is to make sure every PR updates 6 | # devel's cache (if changes are needed) on merging. 7 | # 8 | # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows 9 | ################################################################################ 10 | name: update-poetry-cache 11 | 12 | on: 13 | push: 14 | branches: 15 | - devel 16 | workflow_dispatch: 17 | 18 | jobs: 19 | update-poetry-cache: 20 | runs-on: ubuntu-22.04 21 | strategy: 22 | matrix: 23 | python-version: ["3.8", "3.10"] 24 | include: 25 | - python-version: "3.8" 26 | python-py-version: "py38" 27 | - python-version: "3.10" 28 | python-py-version: "py310" 29 | container: 30 | image: ghcr.io/${{ github.repository }}-ci:${{ matrix.python-py-version }}-poetry-gl-bullseye 31 | credentials: 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | - name: Poetry Venv Dir 38 | run: | 39 | echo "VENV_DIR=$(poetry config virtualenvs.path)" >> $GITHUB_ENV 40 | 41 | - name: Restore the Cache 42 | id: cache-deps 43 | uses: actions/cache@v3 44 | with: 45 | path: ${{ env.VENV_DIR }} 46 | # bump the suffix if you need to force-refresh the cache 47 | key: py-trees-ci-cache-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock', '**/tox.ini') }}-1 48 | 49 | # Install all deps, sans the project (--no-root) 50 | - name: Poetry - Install Dependencies 51 | run: poetry install --no-interaction --no-root 52 | if: steps.cache-deps.outputs.cache-hit != 'true' 53 | 54 | - name: Tox - Install Dependencies 55 | run: poetry run tox --workdir ${{ env.VENV_DIR }} --notest -e ${{ matrix.python-py-version }} check 56 | if: steps.cache-deps.outputs.cache-hit != 'true' 57 | 58 | 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | __pycache__ 4 | *doc/html 5 | *egg-info 6 | *.md.html 7 | .tox 8 | .coverage 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bierner.github-markdown-preview", 4 | "bierner.markdown-preview-github-styles", 5 | "bungcip.better-toml", 6 | "streetsidesoftware.code-spell-checker", 7 | "ms-python.python", 8 | "ms-vscode-remote.vscode-remote-extensionpack", 9 | "tht13.rst-vscode" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.enabled": true, 3 | "python.formatting.provider": "none", 4 | "[python]": { 5 | "editor.defaultFormatter": "omnilib.ufmt", 6 | "editor.formatOnSave": true 7 | }, 8 | "cSpell.words": [ 9 | "behaviour", 10 | "behaviours", 11 | "bierner", 12 | "bungcip", 13 | "omnilib", 14 | "py_trees", 15 | "pydot", 16 | "pypi", 17 | "ufmt", 18 | "usort", 19 | "Visualise", 20 | "visualising", 21 | ] 22 | } -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 0.6.6 (2025-01-13) 6 | ------------------ 7 | * [infra] Add dummy test to make buildfarm happy 8 | * [infra] Fix repo URL in package.xml 9 | * Contributors: Sebastian Castro 10 | 11 | 0.6.5 (2025-01-11) 12 | ------------------ 13 | * [formatter] reformat auto-generated resources.py 14 | * [linter] 2 lines between methods 15 | * [readme] fix preview instructions, elucidate non-nvidia alternatives 16 | * [vscode] devcontainer DISPLAY fix 17 | * Contributors: Daniel Stonier 18 | 19 | 0.6.4 (2022-02-24) 20 | ------------------ 21 | * [actions] pre-merge and update-cache, `#146 `_ 22 | * [actions] push containers, `#144 `_ 23 | * [poetry] update project to use poetry, `#143 `_ 24 | * [vscode] devcontainer workflows, `#143 `_ 25 | * [tests] basic tests, formatting, linting, `#143 `_ 26 | 27 | 0.6.3 (2020-05-05) 28 | ------------------ 29 | * [js] remove buggy early view update and optimise them, `#142 `_ 30 | 31 | 0.6.2 (2020-03-02) 32 | ------------------ 33 | * [js] bugfix accidentally ignored tree_cache size, `#123 `_ 34 | * ... missed a few pull requests inbetween 35 | 36 | 0.6.0 (2019-12-27) 37 | ------------------ 38 | * [js] accepting trees flagged with no status/graph change, `#122 `_ 39 | * [js] blackboard views, `#125 `_, `#133 `_, `#122 `_, `#135 `_, `#136 `_ 40 | 41 | 0.5.1 (2019-10-26) 42 | ------------------ 43 | * [js] performance improvements, `#120 `_ 44 | * [js] highlighted links, `#115 `_ 45 | * [js] orthogonal link connections, for better visualisation 46 | * [qt] capture screenshots, `#114 `_ 47 | 48 | 0.5.0 (2019-08-29) 49 | ------------------ 50 | * [html] disable scrollbars, `#110 `_ 51 | * [js] robustness against identical timestamps, `#109 `_ 52 | * [js] improved window resize handling, `#111 `_ 53 | * new public api ``py_trees.canvas.on_window_resize`` and ``py_trees.timeline.on_window_resize`` 54 | 55 | 0.4.0 (2019-08-13) 56 | ------------------ 57 | * `Milestone - Polish & Release `_ 58 | 59 | 0.3.1 (2019-08-07) 60 | ------------------ 61 | * `Milestone - Interactive Event Timeline `_ 62 | 63 | 0.2.0 (2019-08-01) 64 | ------------------ 65 | * `Milestone - Streaming Trees / Qt `_ 66 | 67 | 0.1.0 (2019-07-25) 68 | ------------------ 69 | * `Milestone - Render a Tree `_ 70 | 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2019 Daniel Stonier 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions 8 | # are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following 14 | # disclaimer in the documentation and/or other materials provided 15 | # with the distribution. 16 | # * Neither the name of Yujin Robot nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyTrees Js 2 | 3 | [[About](#about)] [[Features](#features)] [[Preview](#preview)] [[Exmaple - Simple Web App](#example---simple-web-app)] [[Example - PyQt App](#example---pyqt-app)] [[The JSON Specification](#the-json-specification)] 4 | 5 | ## About 6 | 7 | Libraries for visualisation of runtime or replayed behaviour trees. 8 | 9 | * [./js](./js) - a self-contained javascript library to build apps around 10 | * [py_trees_js](./py_trees_js) - a python package that makes the js available as a pyqt resource 11 | * [py_trees_js.viewer](./py_trees_js/viewer) - a demo pyqtwebengine app 12 | 13 | See [py_trees_ros_viewer](https://github.com/splintered-reality/py_trees_ros_viewer) for a fully fledged pyqt integration that uses [py_trees_js](./py_trees_js). 14 | 15 | ## Features 16 | 17 | * Visualise the runtime state of a behaviour tree 18 | * Collapsible subtrees 19 | * Zoom and scale contents to fit 20 | * Timeline rewind & resume 21 | * Blackboard key-value storage view 22 | * Activity log view 23 | 24 | Although designed for py_trees, the js libs (in particular, the interfaces) are not dependent on py_trees and could be used for other behaviour tree applications. 25 | 26 | ## Preview 27 | 28 | With VSCode DevContainers and on a PC with an NVIDIA GPU: 29 | 30 | * Install [VSCode](https://code.visualstudio.com/docs/setup/linux#_debian-and-ubuntu-based-distributions) 31 | * Install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#docker) 32 | * Clone and launch the demo: 33 | 34 | ``` 35 | $ git clone https://github.com/splintered-reality/py_trees_js 36 | $ code . 37 | $ cd py_trees_js 38 | 39 | # Reopen the project in the devcontainer 40 | $ (use CTRL-SHIFT-P if you miss VSCode's helper dialog) 41 | 42 | # Setup and launch 43 | $ poetry install 44 | $ poetry run py-trees-demo-viewer 45 | ``` 46 |

47 | 48 | 49 |

50 | 51 | If you do not have a PC that meets those requirements, some alternative options: 52 | 53 | * Install [Poetry](https://python-poetry.org/) and PyQt on your system or in a venv. Clone and launch. 54 | * If you're just interested in seeing the demo viewer, `pip install --user py_trees_js` and launch the viewer 55 | * Create your own devcontainer with something like the [desktop-lite](https://github.com/devcontainers/features/tree/main/src/desktop-lite) feature. If this works, send me a PR! 56 | 57 | ## Example - Simple Web App 58 | 59 | Building a complete application that can render a behaviour tree stream is an effort that can be decomposed into two tasks: 60 | 61 | 1. Creating the web app for rendering trees and visualising a timeline 62 | 2. Wrapping the web app in a framework and connecting it to an external stream 63 | 64 | The first stage is purely an exercise with html, css and javascript. The latter will depend on your use case - it could be a qt-js hybrid application (as exemplified here) for developers, an electron application for cross-platform and mobile deployment or a cloud based service. 65 | 66 | This section will walk through how to build a web application with the provided js libraries. An example of wrapping the web app within a Qt-Js application will follow. 67 | 68 | To get started, let's begin with a basic html page with two divs, one for the tree canvas and one for the timeline: 69 | 70 | ```xhtml 71 | 72 | 73 | 74 | 75 | PyTrees Viewer 76 | 86 | 87 | 88 |
89 |
90 | 91 | 92 | ``` 93 | 94 | Next, bring in the javascript libraries. For exemplar purposes, it is assumed here that the libraries 95 | have been made available alongside the html page - how is an integration detail depending on the mode 96 | of deployment (see next section for an example). 97 | 98 | Note that the `py_trees-.js` library has only one dependency, [jointjs](https://resources.jointjs.com/docs/jointjs/v3.0/joint.html), 99 | but that in turn has a few dependencies of it's own. The bundled libraries in the `js/jointjs` folder 100 | of this repository correspond to the requirements for a specific version of jointjs and 101 | have been tested to work with the accompany `py_trees-.js` library. 102 | 103 | You can verify that the libraries have been properly imported by calling `py_trees.hello()` which 104 | will print version information of the loaded javascript libraries (if found) to the javascript console. 105 | 106 | ```xhtml 107 | 108 | 109 | 110 | 111 | PyTrees Viewer 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 132 | 133 | 134 | 137 |
138 |
139 |
140 |
141 | 142 | 143 | ``` 144 | 145 | Output from `py_trees.hello()`: 146 | 147 | ``` 148 | ******************************************************************************** 149 | Py Trees JS 150 | 151 | A javascript library for visualisation of executing behaviour trees. 152 | 153 | Version & Dependency Info: 154 | - py_trees: 0.6.0 155 | - jointjs : 3.1.0 156 | - backbone: 1.4.0 157 | - dagre : 0.8.4 158 | - jquery : 3.4.1 159 | - lodash : 4.17.11 160 | ******************************************************************************** 161 | ``` 162 | 163 | In the next iteration, the canvas is initialised and a callback for 164 | accepting incoming trees from an external source is prepared. To test it, 165 | pass it the demo tree provided by the library. 166 | 167 | ```xhtml 168 | 169 | 170 | 171 | 172 | PyTrees Viewer 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 193 | 194 | 195 | 198 |
199 |
200 |
201 |
202 | 217 | 218 | 219 | ``` 220 | 221 | At this point, your web app should be visualising a single tree and 222 | zoom/collapse/scale to fit interactions functional. I'm happy, you should be too! 223 | 224 | Adding a timeline to the application is optional, but the code does not 225 | change significantly and is a very useful feature to have. The built-in demo 226 | app's [index.html](py_trees_js/viewer/html/index.html) does exactly this. The code is reproduced below for convenience. 227 | 228 | ```xhtml 229 | 230 | 231 | 232 | 233 | PyTrees Viewer 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 254 | 255 | 256 | 259 |
260 |
261 |
262 |
263 | 292 | 293 | 294 | ``` 295 | 296 | ## Example - PyQt App 297 | 298 | The `py-trees-demo-viewer` app is a qt-js hybrid application using `qtwebengine`. 299 | Every time a qt button is pressed, an internally generated tree snapshot is sent to `render_tree()` in the embedded web application. From here, it is not too hard to 300 | imagine connecting the qt application to an actual external source. The qt layer 301 | then acts as a shim or relay transferring messages to the internal web app. 302 | 303 | How does it work? 304 | 305 | * The js libs are made available as a `.qrc` resource [1] 306 | * A simple web app is made available as another `.qrc` resource 307 | * Both resources are consumed by the QWebEngine View to serve the app 308 | 309 | [1] This can be made available separately and as a dependency to the actual 310 | pyqt application. For instance, the [py_trees_js](./py_trees_js) package is a dependency of [py_trees_ros_viewer](https://github.com/splintered-reality/py_trees_ros_viewer). 311 | 312 | In more detail... 313 | 314 | ### The JS Libraries 315 | 316 | 1. Bundle the javascript resources into a `.qrc` file 317 | 2. Generate the resources as a c++ library / python module 318 | 3. Deploy the c++ library/python module in your development environment 319 | 320 | In this case, the py_trees and jointjs javascript libraries have been listed 321 | in [py_trees_js/resources.qrc](py_trees_js/resources.qrc), generated using 322 | [py_trees_js/gen.bash](py_trees_js/gen.bash), resulting in the importable module 323 | [py_trees_js/resources.py](py_trees_js/resources.py). From this point, any pythonic 324 | Qt application wishing to visualise behaviour trees need only import this module from the `py_trees_js` package. 325 | 326 | ### The Web App 327 | 328 | 1. Bundle the `.html`/`.css` pages into a `.qrc` file 329 | 2. Import into directly into designer when building your Qt application 330 | 331 | In this case, our web app ([py_trees_js/viewer/html/index.html](py_trees_js/viewer/html/index.html)) has been rolled into [py_trees_js/viewer/web_app.qrc](py_trees_js/viewer/web_app.qrc) which is directly loaded into [py_trees_js/viewer/web_view.ui](py_trees_js/viewer/web_view.ui) where the URL property of the QWebEngineView widget has been configured with the resources `index.html`. 332 | 333 | You could alternatively, generate a module from the `.qrc` and import that into the 334 | relevant python code as was done for the javascript resources. 335 | 336 | ### The Qt Application 337 | 338 | The Qt application can be designed in whatever way you're most comfortable with - via 339 | Designer, pure C++ or python. In this case, Qt's Designer is used to produce the `.ui` 340 | files which are generated into python modules and finally customised and brought together 341 | as a PyQt5 application. Refer to [py_trees_js/viewer](py_trees_js/viewer) for more details 342 | or as a reference example from which to start your own Qt-Js hybrid application. 343 | 344 | Key elements: 345 | 346 | 1. Build your Qt application around a QWebEngineView widget 347 | 2. Link/import the javascript module in the web engine view class 348 | 3. Load the html page into the QWebEngineView view 349 | 350 | Do not use the QWebView widget - this is deprecating in favour of the QWebEngineView widget. The most notable difference is that QWebView uses Qt's old webkit, while QWebEngineView makes use of Chromium's webkit. 351 | 352 | Note that the second step automagically makes available the javascript resources to the application 353 | when it's loaded. It's not terribly fussy about where it gets loaded, see [py_trees_js/viewer/web_view.py](py_trees_js/viewer/web_view.py) for an example: 354 | 355 | ``` 356 | # This is the module generated by running pyrcc5 on the js libraries .qrc 357 | # It could have been equivalently deployed in a completely different python package 358 | import py_trees_js.resources 359 | ``` 360 | 361 | Loading the web page can be accomplished in designer. Simply point it at your qresource file 362 | and set the dynamic URL property on the QWebEngineView widget. Alternatively you can import 363 | the resource module and load it via QWebEngineView's `load` api. 364 | 365 | #### Qt-Js Interactions 366 | 367 | Qt and JS can interact directly over snippets of javascript code (via `runJavaScript()` 368 | or over QWebChannel (a mechanism similar to sigslots) where more complexity is needed. 369 | The example application here calls on the `render_tree()` method we created earlier in 370 | the web application to send trees to the app. Example code from [py_trees_js/viewer/viewer.py](py_trees_js/viewer/viewer.py) which handles button clicks to cycle through a list of 371 | demonstration trees: 372 | 373 | ``` 374 | def send_tree_response(reply): 375 | console.logdebug("reply: '{}' [viewer]".format(reply)) 376 | 377 | 378 | @qt_core.pyqtSlot() 379 | def send_tree(web_view_page, demo_trees, unused_checked): 380 | demo_trees[send_tree.index]['timestamp'] = time.time() 381 | console.logdebug("send: tree '{}' [{}][viewer]".format( 382 | send_tree.index, demo_trees[send_tree.index]['timestamp']) 383 | ) 384 | javascript_command = "render_tree({{tree: {}}})".format(demo_trees[send_tree.index]) 385 | web_view_page.runJavaScript(javascript_command, send_tree_response) 386 | send_tree.index = 0 if send_tree.index == 2 else send_tree.index + 1 387 | 388 | send_tree.index = 0 389 | ``` 390 | 391 | ## The JSON Specification 392 | 393 | TODO: A JSon schema 394 | 395 | Roughly, the specification expects json objects of the form: 396 | 397 | * timestamp: int 398 | * behaviours: dict[str, dict] 399 | * (optional) visited_path: list[str] 400 | * (optional) blackboard: { 401 | * behaviours: dict[str, dict[str, str]], 402 | * data: dict[str, str] 403 | * } 404 | * (optional) activity: list[str] 405 | 406 | where each behaviour in the dict has specification: 407 | 408 | * id: str 409 | * status: Union[`INVALID`,`FAILURE`, `RUNNING`, `SUCCESS`] 410 | * name: str 411 | * colour: 412 | * (optional) children: List[str] 413 | * (optional) data: 414 | 415 | Identification strings (id's) must be unique and are used as both keys for the 416 | behaviours dictionary, children and visited_path variables. 417 | 418 | An example (extracted from `py_trees.experimental.create_demo_tree_definition()`): 419 | 420 | ``` 421 | { 422 | timestamp: 1563938995, 423 | visited_path: ['1', '2', '3', '4', '5', '7', '8'], 424 | behaviours: { 425 | '1': { 426 | id: '1', 427 | status: 'RUNNING', 428 | name: 'Selector', 429 | colour: '#00FFFF', 430 | children: ['2', '3', '4', '6'], 431 | data: { 432 | Type: 'py_trees.composites.Selector', 433 | Feedback: "Decision maker", 434 | }, 435 | }, 436 | '2': { 437 | id: '2', 438 | status: 'FAILURE', 439 | name: 'Worker', 440 | colour: '#FFA500', 441 | children: ['7', '8', '9'], 442 | data: { 443 | Type: 'py_trees.composites.Sequence', 444 | Feedback: "Worker" 445 | }, 446 | }, 447 | } 448 | 'blackboard': { 449 | 'behaviours': { # key metadata per behaviour 450 | '2': { 451 | '/parameters/initial_value': 'r', 452 | '/state/worker': 'w' 453 | }, 454 | }, 455 | 'data': { 456 | '/parameters/initial_value': 'foo', 457 | '/state/worker': 'bar', 458 | }, 459 | 'activity': [ 460 | "Worker initialised with 'foo''", 461 | "Worker wrote 'bar''", 462 | ] 463 | } 464 | -------------------------------------------------------------------------------- /docs/console.md: -------------------------------------------------------------------------------- 1 | # Dev Console 2 | 3 | ## Sandboxed Chrome 4 | 5 | See `../scripts/py-trees-devel-viewer`. 6 | 7 | ## Timestamps 8 | 9 | Go to chrome's `Settings->Console->Timestamps` and check it. -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_js/1725c768305e1f19e79cfffb166013c876b835e4/images/screenshot.png -------------------------------------------------------------------------------- /images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_js/1725c768305e1f19e79cfffb166013c876b835e4/images/splash.png -------------------------------------------------------------------------------- /js/jointjs/README: -------------------------------------------------------------------------------- 1 | JointJS 2 | 3 | Download: https://www.jointjs.com/opensource#Download-JointJS 4 | -------------------------------------------------------------------------------- /js/jointjs/joint-3.0.2.min.css: -------------------------------------------------------------------------------- 1 | /*! JointJS v3.0.2 (2019-06-28) - JavaScript diagramming library 2 | 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | */ 8 | .joint-viewport{-webkit-user-select:none;-moz-user-select:none;user-select:none}.joint-paper-background,.joint-paper-grid,.joint-paper>svg{position:absolute;top:0;left:0;right:0;bottom:0}[magnet=true]:not(.joint-element){cursor:crosshair}[magnet=true]:not(.joint-element):hover{opacity:.7}.joint-element{cursor:move}.joint-element *{user-drag:none}.joint-element .scalable *{vector-effect:non-scaling-stroke}.marker-source,.marker-target{vector-effect:non-scaling-stroke}.joint-paper{position:relative}.joint-highlight-opacity{opacity:.3}.joint-link .connection,.joint-link .connection-wrap{fill:none}.marker-vertices{opacity:0;cursor:move}.marker-arrowheads{opacity:0;cursor:move;cursor:-webkit-grab;cursor:-moz-grab}.link-tools{opacity:0;cursor:pointer}.link-tools .tool-options{display:none}.joint-link:hover .link-tools,.joint-link:hover .marker-arrowheads,.joint-link:hover .marker-vertices{opacity:1}.marker-vertex-remove{cursor:pointer;opacity:.1}.marker-vertex-group:hover .marker-vertex-remove{opacity:1}.marker-vertex-remove-area{opacity:.1;cursor:pointer}.marker-vertex-group:hover .marker-vertex-remove-area{opacity:1}.joint-element .fobj{overflow:hidden}.joint-element .fobj body{background-color:transparent;margin:0;position:static}.joint-element .fobj div{text-align:center;vertical-align:middle;display:table-cell;padding:0 5px 0 5px}.joint-paper.joint-theme-dark{background-color:#18191b}.joint-link.joint-theme-dark .connection-wrap{stroke:#8f8ff3;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-dark .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-dark .connection{stroke-linejoin:round}.joint-link.joint-theme-dark .link-tools .tool-remove circle{fill:#f33636}.joint-link.joint-theme-dark .link-tools .tool-remove path{fill:#fff}.joint-link.joint-theme-dark .link-tools [event="link:options"] circle{fill:green}.joint-link.joint-theme-dark .marker-vertex{fill:#5652db}.joint-link.joint-theme-dark .marker-vertex:hover{fill:#8e8ce1;stroke:none}.joint-link.joint-theme-dark .marker-arrowhead{fill:#5652db}.joint-link.joint-theme-dark .marker-arrowhead:hover{fill:#8e8ce1;stroke:none}.joint-link.joint-theme-dark .marker-vertex-remove-area{fill:green;stroke:#006400}.joint-link.joint-theme-dark .marker-vertex-remove{fill:#fff;stroke:#fff}.joint-paper.joint-theme-default{background-color:#fff}.joint-link.joint-theme-default .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-default .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-default .connection{stroke-linejoin:round}.joint-link.joint-theme-default .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-default .link-tools .tool-remove path{fill:#fff}.joint-link.joint-theme-default .marker-vertex{fill:#1abc9c}.joint-link.joint-theme-default .marker-vertex:hover{fill:#34495e;stroke:none}.joint-link.joint-theme-default .marker-arrowhead{fill:#1abc9c}.joint-link.joint-theme-default .marker-arrowhead:hover{fill:#f39c12;stroke:none}.joint-link.joint-theme-default .marker-vertex-remove{fill:#fff}@font-face{font-family:lato-light;src:url(data:application/font-woff;charset=utf-8;base64,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) format('woff');font-weight:400;font-style:normal}.joint-link.joint-theme-material .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-material .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-material .connection{stroke-linejoin:round}.joint-link.joint-theme-material .link-tools .tool-remove circle{fill:#c64242}.joint-link.joint-theme-material .link-tools .tool-remove path{fill:#fff}.joint-link.joint-theme-material .marker-vertex{fill:#d0d8e8}.joint-link.joint-theme-material .marker-vertex:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-arrowhead{fill:#d0d8e8}.joint-link.joint-theme-material .marker-arrowhead:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-vertex-remove-area{fill:#5fa9ee}.joint-link.joint-theme-material .marker-vertex-remove{fill:#fff}.joint-link.joint-theme-modern .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-modern .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-modern .connection{stroke-linejoin:round}.joint-link.joint-theme-modern .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-modern .link-tools .tool-remove path{fill:#fff}.joint-link.joint-theme-modern .marker-vertex{fill:#1abc9c}.joint-link.joint-theme-modern .marker-vertex:hover{fill:#34495e;stroke:none}.joint-link.joint-theme-modern .marker-arrowhead{fill:#1abc9c}.joint-link.joint-theme-modern .marker-arrowhead:hover{fill:#f39c12;stroke:none}.joint-link.joint-theme-modern .marker-vertex-remove{fill:#fff} -------------------------------------------------------------------------------- /js/py_trees-0.6.css: -------------------------------------------------------------------------------- 1 | #timeline { 2 | position: absolute; 3 | bottom: 0px; 4 | box-sizing: border-box; 5 | pointer-events: none; 6 | /* 7 | padding: 0px; 8 | background: #111111; 9 | border-style: groove; 10 | border-color: blue; 11 | */ 12 | } 13 | 14 | #activity_view { 15 | border: 1px groove white; 16 | position: absolute; 17 | bottom: 0px; 18 | left: 0px; 19 | margin: 0px 1% 45px 1%; /* top, right, bottom, left */ 20 | padding: 3px; 21 | background-color: rgba(255, 255, 255, 0.2); 22 | color: white; 23 | font-size: 9px; 24 | max-width: 47%; 25 | /* max-height: 50%; */ 26 | overflow-y: auto; 27 | } 28 | 29 | .activity_view_header { 30 | text-align: center; 31 | font-size: 10px; 32 | } 33 | 34 | .activity_view_items { 35 | text-align: left; 36 | /* font-family: monospace; */ 37 | } 38 | 39 | #blackboard_view { 40 | border: 1px groove white; 41 | position: absolute; 42 | bottom: 0px; 43 | right: 0px; 44 | margin: 0px 1% 45px 1%; /* top, right, bottom, left */ 45 | padding: 3px; 46 | background-color: rgba(255, 255, 255, 0.2); 47 | color: white; 48 | font-size: 9px; 49 | max-width: 47%; 50 | /* max-height: 50%; */ 51 | overflow-y: auto; 52 | } 53 | 54 | .blackboard_view_header { 55 | text-align: center; 56 | font-size: 10px; 57 | } 58 | 59 | .blackboard_view_variables { 60 | text-align: left; 61 | /* font-family: monospace; */ 62 | } 63 | 64 | .html-element { 65 | position: absolute; 66 | background: none; 67 | pointer-events: none; 68 | -webkit-user-select: none; 69 | z-index: 2; 70 | box-sizing: border-box; 71 | /* 72 | border: 1px groove red; 73 | */ 74 | } 75 | .html-name { 76 | color: #f1f1f1; 77 | text-decoration: underline; 78 | font-size: 14px; 79 | font-family: 'serif'; 80 | font-weight: normal; 81 | text-align: center; 82 | margin-left: 5px; 83 | margin-right: 5px; 84 | /* All four of these are necessary for eliding text */ 85 | text-overflow: ellipsis; 86 | overflow: hidden; 87 | display: block; 88 | white-space: nowrap; 89 | /* 90 | background: green; 91 | */ 92 | } 93 | .html-detail { 94 | text-align: center; 95 | margin-left: 5px; 96 | margin-right: 5px; 97 | color: #f1f1f1; 98 | font-size: 10px; 99 | font-family: 'serif'; 100 | /* All four of these are necessary for eliding text */ 101 | text-overflow: ellipsis; 102 | overflow: hidden; 103 | display: block; 104 | white-space: nowrap; 105 | /* 106 | background: blue; 107 | */ 108 | } 109 | 110 | .html-tooltip { 111 | display: none; 112 | position: absolute; 113 | top: 0px; 114 | border-radius: 8px; 115 | background: #333333; 116 | border: 1px solid #000000; 117 | z-index: 2; 118 | font-size: 10px; 119 | font-family: 'serif'; 120 | color: #F1F1F1; 121 | padding: 0.5em; 122 | box-sizing: border-box; 123 | /* 124 | box-shadow: inset 0 0 5px black, 2px 2px 1px gray; 125 | box-sizing: border-box; 126 | padding: 5px; 127 | */ 128 | } 129 | .html-tooltip div.id { 130 | display: block; 131 | text-align: center; 132 | /* 133 | border: 1px solid #1c87c9; 134 | */ 135 | } -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | py_trees_js 5 | 0.6.6 6 | 7 | Javascript library for visualising behaviour trees. 8 | 9 | 10 | Daniel Stonier 11 | Daniel Stonier 12 | 13 | BSD 14 | 15 | https://github.com/splintered-reality/py_trees_js 16 | https://github.com/splintered-reality/py_trees_js 17 | https://github.com/splintered-reality/py_trees_js/issues 18 | 19 | python3-setuptools 20 | pyqt5-dev-tools 21 | qttools5-dev-tools 22 | 23 | python3-qt5-bindings 24 | python3-pyqt5.qtwebengine 25 | 26 | 27 | ament_python 28 | 29 | 30 | -------------------------------------------------------------------------------- /py_trees_js/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://github.com/splintered-reality/py_trees_js/raw/devel/LICENSE 4 | # 5 | ############################################################################## 6 | # Documentation 7 | ############################################################################## 8 | 9 | """PyTrees javascript libraries and development/demo qt-js hybrid viewer.""" 10 | 11 | ############################################################################## 12 | # Imports 13 | ############################################################################## 14 | 15 | # fmt: off 16 | from . import resources # usort:skip 17 | from . import viewer # usort:skip 18 | # fmt: on 19 | -------------------------------------------------------------------------------- /py_trees_js/gen.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for generating *.py modules from .qrc resources. 4 | 5 | ############################################################################## 6 | # Colours 7 | ############################################################################## 8 | 9 | BOLD="\e[1m" 10 | CYAN="\e[36m" 11 | GREEN="\e[32m" 12 | RED="\e[31m" 13 | YELLOW="\e[33m" 14 | RESET="\e[0m" 15 | 16 | padded_message () 17 | { 18 | line="........................................" 19 | printf "%s%s${2}\n" ${1} "${line:${#1}}" 20 | } 21 | 22 | pretty_header () 23 | { 24 | echo -e "${BOLD}${1}${RESET}" 25 | } 26 | 27 | pretty_print () 28 | { 29 | echo -e "${GREEN}${1}${RESET}" 30 | } 31 | 32 | pretty_warning () 33 | { 34 | echo -e "${YELLOW}${1}${RESET}" 35 | } 36 | 37 | pretty_error () 38 | { 39 | echo -e "${RED}${1}${RESET}" 40 | } 41 | 42 | ############################################################################## 43 | # Methods 44 | ############################################################################## 45 | 46 | install_package () 47 | { 48 | PACKAGE_NAME=$1 49 | dpkg -s ${PACKAGE_NAME} > /dev/null 50 | if [ $? -ne 0 ]; then 51 | sudo apt-get -q -y install ${PACKAGE_NAME} > /dev/null 52 | else 53 | pretty_print " $(padded_message ${PACKAGE_NAME} "found")" 54 | return 0 55 | fi 56 | if [ $? -ne 0 ]; then 57 | pretty_error " $(padded_message ${PACKAGE_NAME} "failed")" 58 | return 1 59 | fi 60 | pretty_warning " $(padded_message ${PACKAGE_NAME} "installed")" 61 | return 0 62 | } 63 | 64 | generate_ui () 65 | { 66 | NAME=$1 67 | pyuic5 --from-imports -o ${NAME}_ui.py ${NAME}.ui 68 | if [ $? -ne 0 ]; then 69 | pretty_error " $(padded_message ${NAME} "failed")" 70 | return 1 71 | fi 72 | pretty_print " $(padded_message ${NAME} "generated")" 73 | return 0 74 | } 75 | 76 | generate_qrc () 77 | { 78 | NAME=$1 79 | pyrcc5 -o ${NAME}.py ${NAME}.qrc 80 | if [ $? -ne 0 ]; then 81 | pretty_error " $(padded_message ${NAME} "failed")" 82 | return 1 83 | fi 84 | pretty_print " $(padded_message ${NAME} "generated")" 85 | return 0 86 | } 87 | 88 | ############################################################################## 89 | 90 | echo "" 91 | 92 | echo -e "${CYAN}Dependencies${RESET}" 93 | install_package pyqt5-dev-tools || return 94 | 95 | echo "" 96 | 97 | echo -e "${CYAN}Generating UIs${RESET}" 98 | # generate_ui main_window 99 | 100 | echo "" 101 | 102 | echo -e "${CYAN}Generating QRCs${RESET}" 103 | generate_qrc resources 104 | 105 | echo "" 106 | echo "I'm grooty, you should be too." 107 | echo "" 108 | 109 | -------------------------------------------------------------------------------- /py_trees_js/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | ../js/jointjs/dagre-0.8.4.min.js 4 | ../js/jointjs/graphlib-2.1.7.min.js 5 | ../js/jointjs/lodash-4.17.11.min.js 6 | ../js/jointjs/jquery-3.4.1.min.js 7 | ../js/jointjs/joint-3.0.4.min.js 8 | ../js/jointjs/joint-3.0.4.min.css 9 | ../js/jointjs/backbone-1.4.0.js 10 | ../js/py_trees-0.6.css 11 | ../js/py_trees-0.6.js 12 | 13 | 14 | -------------------------------------------------------------------------------- /py_trees_js/viewer/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://github.com/splintered-reality/py_trees_js/raw/devel/LICENSE 4 | # 5 | ############################################################################## 6 | # Documentation 7 | ############################################################################## 8 | 9 | """A qt-js hybrid viewer demonstrating js library integration.""" 10 | 11 | ############################################################################## 12 | # Imports 13 | ############################################################################## 14 | 15 | # fmt: off 16 | from . import console # usort:skip 17 | from . import main_window # usort:skip 18 | from . import trees # usort:skip 19 | from . import viewer # usort:skip 20 | from . import web_view # usort:skip 21 | # fmt: on 22 | -------------------------------------------------------------------------------- /py_trees_js/viewer/console.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://raw.githubusercontent.com/splintered-reality/py_trees_js/devel/LICENSE 4 | # 5 | 6 | ############################################################################## 7 | # Description 8 | ############################################################################## 9 | 10 | """ 11 | Simple colour definitions and syntax highlighting for the console. 12 | 13 | ---- 14 | 15 | **Colour Definitions** 16 | 17 | The current list of colour definitions include: 18 | 19 | * ``Regular``: black, red, green, yellow, blue, magenta, cyan, white, 20 | * ``Bold``: bold, bold_black, bold_red, bold_green, bold_yellow, bold_blue, bold_magenta, bold_cyan, bold_white 21 | 22 | These colour definitions can be used in the following way: 23 | 24 | .. code-block:: python 25 | 26 | from . import console 27 | print(console.cyan + " Name" + console.reset + ": " + console.yellow + "Dude" + console.reset) 28 | 29 | """ 30 | 31 | ############################################################################## 32 | # Imports 33 | ############################################################################## 34 | 35 | import enum 36 | import os 37 | import sys 38 | 39 | 40 | ############################################################################## 41 | # Special Characters 42 | ############################################################################## 43 | 44 | 45 | def has_unicode(encoding: str = sys.stdout.encoding) -> bool: 46 | """ 47 | Define whether the specified encoding has unicode symbols. 48 | 49 | This is usually used to check 50 | if stdout is capable of unicode or otherwise (e.g. 51 | Jenkins CI is often be configured with unicode disabled). 52 | 53 | Args: 54 | encoding: the encoding to check against. 55 | 56 | Returns: 57 | true if capable, false otherwise 58 | """ 59 | try: 60 | "\u26A1".encode(encoding) 61 | except TypeError: 62 | # if sys.stdout.encoding is not available, it is None 63 | # this will occur if you run nosetests3 or pytest-3 without -s 64 | return False 65 | except UnicodeError: 66 | return False 67 | return True 68 | 69 | 70 | def define_symbol_or_fallback( 71 | original: str, fallback: str, encoding: str = sys.stdout.encoding 72 | ) -> str: 73 | """ 74 | Go unicode, or fallback to ascii. 75 | 76 | Return the correct encoding according to the specified encoding. Used to 77 | make sure we get an appropriate symbol, even if the shell is merely ascii as 78 | is often the case on, e.g. Jenkins CI. 79 | 80 | Args: 81 | original: the unicode string (usually just a character) 82 | fallback: the fallback ascii string 83 | encoding: the encoding to check against. 84 | 85 | Returns: 86 | either original or fallback depending on whether exceptions were thrown. 87 | """ 88 | try: 89 | original.encode(encoding) 90 | except UnicodeError: 91 | return fallback 92 | return original 93 | 94 | 95 | circle = "\u26ac" 96 | lightning_bolt = "\u26A1" 97 | double_vertical_line = "\u2016" 98 | check_mark = "\u2713" 99 | multiplication_x = "\u2715" 100 | left_arrow = "\u2190" # u'\u2190' 101 | right_arrow = "\u2192" 102 | left_right_arrow = "\u2194" 103 | forbidden_circle = "\u29B8" 104 | circled_m = "\u24c2" 105 | 106 | ############################################################################## 107 | # Keypress 108 | ############################################################################## 109 | 110 | 111 | def read_single_keypress() -> str: 112 | """Wait for a single keypress on stdin. 113 | 114 | This is a silly function to call if you need to do it a lot because it has 115 | to store stdin's current setup, setup stdin for reading single keystrokes 116 | then read the single keystroke then revert stdin back after reading the 117 | keystroke. 118 | 119 | Returns: 120 | the character of the key that was pressed 121 | 122 | Raises: 123 | KeyboardInterrupt: if CTRL-C was pressed (keycode 0x03) 124 | """ 125 | 126 | def read_single_keypress_unix() -> str: 127 | """For Unix case, where fcntl, termios is available.""" 128 | import fcntl 129 | import termios 130 | 131 | fd = sys.stdin.fileno() 132 | # save old state 133 | flags_save = fcntl.fcntl(fd, fcntl.F_GETFL) 134 | attrs_save = termios.tcgetattr(fd) 135 | # make raw - the way to do this comes from the termios(3) man page. 136 | attrs = list(attrs_save) # copy the stored version to update 137 | # iflag 138 | attrs[0] &= ~( 139 | termios.IGNBRK 140 | | termios.BRKINT 141 | | termios.PARMRK 142 | | termios.ISTRIP 143 | | termios.INLCR 144 | | termios.IGNCR 145 | | termios.ICRNL 146 | | termios.IXON 147 | ) 148 | # oflag 149 | attrs[1] &= ~termios.OPOST 150 | # cflag 151 | attrs[2] &= ~(termios.CSIZE | termios.PARENB) 152 | attrs[2] |= termios.CS8 153 | # lflag 154 | attrs[3] &= ~( 155 | termios.ECHONL 156 | | termios.ECHO 157 | | termios.ICANON 158 | | termios.ISIG 159 | | termios.IEXTEN 160 | ) 161 | termios.tcsetattr(fd, termios.TCSANOW, attrs) 162 | # turn off non-blocking 163 | fcntl.fcntl(fd, fcntl.F_SETFL, flags_save & ~os.O_NONBLOCK) 164 | # read a single keystroke 165 | ret = sys.stdin.read(1) # returns a single character 166 | if ord(ret) == 3: # CTRL-C 167 | termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save) 168 | fcntl.fcntl(fd, fcntl.F_SETFL, flags_save) 169 | raise KeyboardInterrupt("Ctrl-c") 170 | # restore old state 171 | termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save) 172 | fcntl.fcntl(fd, fcntl.F_SETFL, flags_save) 173 | return ret 174 | 175 | def read_single_keypress_windows() -> str: 176 | """Implement keypress functionality for windows, which can't use fcntl and termios.""" 177 | import msvcrt # noqa 178 | 179 | # read a single keystroke 180 | ret = sys.stdin.read(1) 181 | if ord(ret) == 3: # CTRL-C 182 | raise KeyboardInterrupt("Ctrl-c") 183 | return ret 184 | 185 | try: 186 | return read_single_keypress_unix() 187 | except ImportError as e_unix: 188 | try: 189 | return read_single_keypress_windows() 190 | except ImportError as e_windows: 191 | raise ImportError( 192 | "Neither unix nor windows implementations supported [{}][{}]".format( 193 | str(e_unix), str(e_windows) 194 | ) 195 | ) 196 | 197 | 198 | ############################################################################## 199 | # Methods 200 | ############################################################################## 201 | 202 | 203 | def console_has_colours() -> bool: 204 | """Detect if the console (stdout) has colourising capability.""" 205 | if os.environ.get("PY_TREES_DISABLE_COLORS"): 206 | return False 207 | # From django.core.management.color.supports_color 208 | # https://github.com/django/django/blob/master/django/core/management/color.py 209 | plat = sys.platform 210 | supported_platform = plat != "Pocket PC" and ( 211 | plat != "win32" or "ANSICON" in os.environ 212 | ) 213 | # isatty is not always implemented, #6223. 214 | is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() 215 | if not supported_platform or not is_a_tty: 216 | return False 217 | return True 218 | 219 | 220 | has_colours = console_has_colours() 221 | """ Whether the loading program has access to colours or not.""" 222 | 223 | 224 | if has_colours: 225 | # reset = "\x1b[0;0m" 226 | reset = "\x1b[0m" 227 | bold = "\x1b[%sm" % "1" 228 | dim = "\x1b[%sm" % "2" 229 | underlined = "\x1b[%sm" % "4" 230 | blink = "\x1b[%sm" % "5" 231 | black, red, green, yellow, blue, magenta, cyan, white = [ 232 | "\x1b[%sm" % str(i) for i in range(30, 38) 233 | ] 234 | ( 235 | bold_black, 236 | bold_red, 237 | bold_green, 238 | bold_yellow, 239 | bold_blue, 240 | bold_magenta, 241 | bold_cyan, 242 | bold_white, 243 | ) = ["\x1b[%sm" % ("1;" + str(i)) for i in range(30, 38)] 244 | else: 245 | reset = "" 246 | bold = "" 247 | dim = "" 248 | underlined = "" 249 | blink = "" 250 | black, red, green, yellow, blue, magenta, cyan, white = ["" for i in range(30, 38)] 251 | ( 252 | bold_black, 253 | bold_red, 254 | bold_green, 255 | bold_yellow, 256 | bold_blue, 257 | bold_magenta, 258 | bold_cyan, 259 | bold_white, 260 | ) = ["" for i in range(30, 38)] 261 | 262 | colours = [ 263 | bold, 264 | dim, 265 | underlined, 266 | blink, 267 | black, 268 | red, 269 | green, 270 | yellow, 271 | blue, 272 | magenta, 273 | cyan, 274 | white, 275 | bold_black, 276 | bold_red, 277 | bold_green, 278 | bold_yellow, 279 | bold_blue, 280 | bold_magenta, 281 | bold_cyan, 282 | bold_white, 283 | ] 284 | """List of all available colours.""" 285 | 286 | 287 | def pretty_print(msg: str, colour: str = white) -> None: 288 | """Pretty print a coloured message. 289 | 290 | Args: 291 | msg: text to print 292 | colour: ascii colour to use 293 | """ 294 | if has_colours: 295 | seq = colour + msg + reset 296 | sys.stdout.write(seq) 297 | else: 298 | sys.stdout.write(msg) 299 | 300 | 301 | def pretty_println(msg: str, colour: str = white) -> None: 302 | """Pretty print a coloured message with a newline. 303 | 304 | Args: 305 | msg: text to print 306 | colour: ascii colour to use 307 | """ 308 | if has_colours: 309 | seq = colour + msg + reset 310 | sys.stdout.write(seq) 311 | sys.stdout.write("\n") 312 | else: 313 | sys.stdout.write(msg) 314 | 315 | 316 | ############################################################################## 317 | # Console 318 | ############################################################################## 319 | 320 | 321 | def banner(msg: str) -> None: 322 | """Print a banner with centred text to stdout. 323 | 324 | Args: 325 | msg: text to centre in the banner 326 | """ 327 | print(green + "\n" + 80 * "*" + reset) 328 | print(green + "* " + bold_white + msg.center(80) + reset) 329 | print(green + 80 * "*" + "\n" + reset) 330 | 331 | 332 | def debug(msg: str) -> None: 333 | """Print a debug message. 334 | 335 | Args: 336 | str: message to print 337 | """ 338 | print(green + msg + reset) 339 | 340 | 341 | def warning(msg: str) -> None: 342 | """Print a warning message. 343 | 344 | Args: 345 | str: message to print 346 | """ 347 | print(yellow + msg + reset) 348 | 349 | 350 | def info(msg: str) -> None: 351 | """Print an info message. 352 | 353 | Args: 354 | str: message to print 355 | """ 356 | print(msg) 357 | 358 | 359 | def error(msg: str) -> None: 360 | """Print an error message. 361 | 362 | Args: 363 | str: message to print 364 | """ 365 | print(red + msg + reset) 366 | 367 | 368 | class LogLevel(enum.Enum): 369 | """Log levels.""" 370 | 371 | DEBUG = 1 372 | INFO = 2 373 | WARNING = 3 374 | ERROR = 4 375 | 376 | 377 | log_level = LogLevel.INFO 378 | """ Console's current log level.""" 379 | 380 | 381 | def logdebug(message: str) -> None: 382 | """ 383 | Prefixes ``[DEBUG]`` and colours the message green. 384 | 385 | Args: 386 | message: message to log. 387 | """ 388 | if log_level.value < LogLevel.INFO.value: 389 | print(green + "[DEBUG] " + message + reset) 390 | 391 | 392 | def loginfo(message: str) -> None: 393 | """ 394 | Prefixes ``[ INFO]`` to the message. 395 | 396 | Args: 397 | message: message to log. 398 | """ 399 | if log_level.value < LogLevel.WARNING.value: 400 | print("[ INFO] " + message) 401 | 402 | 403 | def logwarn(message: str) -> None: 404 | """ 405 | Prefixes ``[ WARN]`` and colours the message yellow. 406 | 407 | Args: 408 | message: message to log. 409 | """ 410 | if log_level.value < LogLevel.ERROR.value: 411 | print(yellow + "[ WARN] " + message + reset) 412 | 413 | 414 | def logerror(message: str) -> None: 415 | """ 416 | Prefixes ``[ERROR]`` and colours the message red. 417 | 418 | Args: 419 | message: message to log. 420 | """ 421 | print(red + "[ERROR] " + message + reset) 422 | 423 | 424 | def logfatal(message: str) -> None: 425 | """ 426 | Prefixes ``[FATAL]`` and colours the message bold red. 427 | 428 | Args: 429 | message: message to log. 430 | """ 431 | print(bold_red + "[FATAL] " + message + reset) 432 | 433 | 434 | ############################################################################## 435 | # Main 436 | ############################################################################## 437 | 438 | if __name__ == "__main__": 439 | # To test without unicode, configure a non utf-8 locale: 440 | # 441 | # $ cat /usr/share/i18n/SUPPORTED | grep en_US 442 | # $ sudo locale-gen en_US.ISO-8859-15 443 | # $ sudo update-locale 444 | # $ locale -a 445 | # $ python3 ./console.py 446 | 447 | for colour in colours: 448 | pretty_print("dude\n", colour) 449 | logdebug("loginfo message") 450 | logwarn("logwarn message") 451 | logerror("logerror message") 452 | logfatal("logfatal message") 453 | pretty_print("red\n", red) 454 | print("some normal text") 455 | print(cyan + " Name" + reset + ": " + yellow + "Dude" + reset) 456 | print(f"Has Unicode: {has_unicode()}") 457 | print("Unicode Characters:\n") 458 | print("lightning_bolt: {}".format(lightning_bolt)) 459 | print("double_vertical_line: {}".format(double_vertical_line)) 460 | print("check_mark: {}".format(check_mark)) 461 | print("multiplication_x: {}".format(multiplication_x)) 462 | print("left_arrow: {}".format(left_arrow)) 463 | print("right_arrow: {}".format(right_arrow)) 464 | print("circled_m: {}".format(circled_m)) 465 | -------------------------------------------------------------------------------- /py_trees_js/viewer/gen.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for setting up the development environment. 4 | #source /usr/share/virtualenvwrapper/virtualenvwrapper.sh 5 | 6 | NAME=py_trees 7 | 8 | ############################################################################## 9 | # Colours 10 | ############################################################################## 11 | 12 | BOLD="\e[1m" 13 | CYAN="\e[36m" 14 | GREEN="\e[32m" 15 | RED="\e[31m" 16 | YELLOW="\e[33m" 17 | RESET="\e[0m" 18 | 19 | padded_message () 20 | { 21 | line="........................................" 22 | printf "%s%s${2}\n" ${1} "${line:${#1}}" 23 | } 24 | 25 | pretty_header () 26 | { 27 | echo -e "${BOLD}${1}${RESET}" 28 | } 29 | 30 | pretty_print () 31 | { 32 | echo -e "${GREEN}${1}${RESET}" 33 | } 34 | 35 | pretty_warning () 36 | { 37 | echo -e "${YELLOW}${1}${RESET}" 38 | } 39 | 40 | pretty_error () 41 | { 42 | echo -e "${RED}${1}${RESET}" 43 | } 44 | 45 | ############################################################################## 46 | # Methods 47 | ############################################################################## 48 | 49 | install_package () 50 | { 51 | PACKAGE_NAME=$1 52 | dpkg -s ${PACKAGE_NAME} > /dev/null 53 | if [ $? -ne 0 ]; then 54 | sudo apt-get -q -y install ${PACKAGE_NAME} > /dev/null 55 | else 56 | pretty_print " $(padded_message ${PACKAGE_NAME} "found")" 57 | return 0 58 | fi 59 | if [ $? -ne 0 ]; then 60 | pretty_error " $(padded_message ${PACKAGE_NAME} "failed")" 61 | return 1 62 | fi 63 | pretty_warning " $(padded_message ${PACKAGE_NAME} "installed")" 64 | return 0 65 | } 66 | 67 | generate_ui () 68 | { 69 | NAME=$1 70 | pyuic5 --from-imports -o ${NAME}_ui.py ${NAME}.ui 71 | if [ $? -ne 0 ]; then 72 | pretty_error " $(padded_message ${NAME} "failed")" 73 | return 1 74 | fi 75 | pretty_print " $(padded_message ${NAME} "generated")" 76 | return 0 77 | } 78 | 79 | generate_qrc () 80 | { 81 | NAME=$1 82 | pyrcc5 -o ${NAME}_rc.py ${NAME}.qrc 83 | if [ $? -ne 0 ]; then 84 | pretty_error " $(padded_message ${NAME} "failed")" 85 | return 1 86 | fi 87 | pretty_print " $(padded_message ${NAME} "generated")" 88 | return 0 89 | } 90 | 91 | ############################################################################## 92 | 93 | # Ensure reproducible results so we can avoid committing ad-nauseum to github 94 | # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=872285 95 | export QT_HASH_SEED=0 96 | 97 | echo "" 98 | 99 | echo -e "${CYAN}Dependencies${RESET}" 100 | install_package pyqt5-dev-tools || return 101 | 102 | echo "" 103 | 104 | echo -e "${CYAN}Generating UIs${RESET}" 105 | generate_ui main_window 106 | generate_ui web_view 107 | 108 | echo "" 109 | 110 | echo -e "${CYAN}Generating QRCs${RESET}" 111 | generate_qrc images 112 | generate_qrc web_app 113 | 114 | echo "" 115 | echo "I'm grooty, you should be too." 116 | echo "" 117 | 118 | -------------------------------------------------------------------------------- /py_trees_js/viewer/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PyTrees Viewer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 | 31 |
32 |
33 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /py_trees_js/viewer/images.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | images/tuxrobot.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /py_trees_js/viewer/images/tuxrobot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_js/1725c768305e1f19e79cfffb166013c876b835e4/py_trees_js/viewer/images/tuxrobot.png -------------------------------------------------------------------------------- /py_trees_js/viewer/images_rc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.9.5) 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore 10 | 11 | qt_resource_data = b"\ 12 | \x00\x00\x1b\xe0\ 13 | \x89\ 14 | \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ 15 | \x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ 16 | \x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ 17 | \xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0a\xf0\x00\x00\ 18 | \x0a\xf0\x01\x42\xac\x34\x98\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ 19 | \xe0\x0c\x10\x02\x10\x31\x57\xe1\xcd\xb9\x00\x00\x1b\x6d\x49\x44\ 20 | \x41\x54\x78\x5e\xb5\x9b\x07\x90\x55\x55\x9a\xc7\xcf\x7b\xfd\x3a\ 21 | \x27\xe8\x4c\x77\x93\x41\x54\x90\x20\x20\x41\x09\x0a\x38\x8a\x61\ 22 | \x24\x94\x82\x52\xea\xae\x2c\xa2\x8c\x8e\x5a\xe3\x8e\x22\xb5\x33\ 23 | \xab\x22\xee\x58\xea\x80\xae\x58\x2a\x83\x5a\x82\xae\xe8\x8a\x8a\ 24 | \xa0\x28\xa8\x64\x41\x82\x0a\x08\x4a\x0e\x4d\x68\x9a\xa6\x9b\xce\ 25 | \x71\xff\xbf\xc3\x3b\x6f\x6e\x27\x68\xd4\x3d\x55\xa7\xee\x7d\xf7\ 26 | \x9e\xfb\x9d\x2f\xfc\xbf\x70\xce\xbd\xcf\xf7\xd4\x53\x4f\xf9\x8d\ 27 | \x31\x91\xea\x31\xea\xd5\xea\xe5\x7e\xbf\xbf\x22\x2b\x2b\x2b\x3c\ 28 | \x2c\x2c\x2c\xa2\xb6\xb6\xb6\xe8\xb6\xdb\x6e\xab\xd1\xf5\xdf\xa4\ 29 | \xdd\x34\x72\x24\xf3\x85\xa9\x27\x9e\x2e\xc8\x2f\xdf\xbb\x6b\x67\ 30 | \xd8\x9e\x63\x27\xc3\xd3\x13\xe2\xfc\xc7\x0a\x8b\x72\x3b\xb6\x6a\ 31 | \xe9\xdb\x7d\x24\xbf\x3a\x2e\x2a\xc2\x14\x95\x55\xfc\xe2\x39\x25\ 32 | \x97\xc9\xc8\xc8\x30\xe5\xe5\xe5\xd1\x3e\x9f\x2f\xe9\xd4\xa9\x53\ 33 | \xb9\x8f\x3c\xf2\x48\x03\x82\x30\x53\xab\x8e\x80\x30\x15\xa5\x1e\ 34 | \x11\x08\x04\xc2\x75\x8c\xaf\xa9\xa9\xa9\x0e\x0f\x0f\xaf\x99\x3f\ 35 | \x7f\xfe\x2f\x66\xc4\xfb\xa0\x84\xf7\xe9\x77\x92\x7a\x77\x31\x35\ 36 | \x34\x2f\x2f\xf7\xa2\xc4\xd4\x8c\xc4\xd4\xf8\x98\xc7\xc2\xc3\xfc\ 37 | \x59\xad\x5a\xc6\x5d\x7b\xba\xb8\x22\x3b\x31\x26\x2a\xe2\xd7\x08\ 38 | \xef\xe6\xdc\xb3\x67\x8f\x91\x0c\xb2\x61\x6d\x84\x5a\xcc\xe3\x8f\ 39 | \x3f\x8e\xbc\x75\x9a\x7f\xea\xd4\xa9\x28\xa0\x4a\x1d\x25\xc4\xaa\ 40 | \xc7\x57\x56\x56\x46\x56\x57\x57\xa7\x96\x96\x96\xd6\x1c\x3c\x78\ 41 | \xd0\x77\xeb\xad\xb7\xd6\x7f\xee\x17\xfd\x5e\xb8\x78\x31\x73\xe5\ 42 | \x4b\xf8\x56\xc5\xc5\x25\x19\x35\xb5\xbe\xa9\xa7\x4f\xe6\x3d\x15\ 43 | \xe6\xf7\x57\x56\xd5\x56\xff\x21\xe0\x0b\x8b\xca\x6e\x19\x7f\xac\ 44 | \xa0\xa4\xac\x02\x04\xfc\xd2\x86\xf5\xd5\x7c\x31\x31\x31\x01\xc9\ 45 | \x60\xa4\x80\x30\x19\x32\x4d\x3d\xc0\x8d\xe0\x7d\x4b\xde\xe7\x06\ 46 | \xeb\x1c\xe1\xd3\xd5\xa3\xa4\xad\xda\xa4\xa4\xa4\xd6\x7a\x78\x6d\ 47 | \x61\x61\x61\x11\x2a\x0c\x2a\xca\x3e\xf4\x6b\xdb\x88\xa1\x43\x86\ 48 | \xe5\x9f\x38\x31\x35\x2f\xf7\x58\x0f\xbf\xcf\x5f\x51\x5c\x56\x52\ 49 | \x1d\xee\xf7\x2f\xa9\xaa\xae\x79\xab\xb8\xbc\x72\x65\xeb\x94\x84\ 50 | \xda\x6d\x87\x72\x7f\xed\x34\x66\xfa\xf4\xe9\x7e\x09\x9d\x14\x19\ 51 | \x19\xd9\x5a\xc7\xac\xfc\xfc\xfc\x15\x52\x7e\xa9\x08\x63\xec\x1a\ 52 | \x64\x02\x01\x6e\xa2\x4a\x9d\x14\xab\x87\xcb\x05\x3a\xc9\x77\x5a\ 53 | \x94\x95\x95\xe1\x06\xb8\xc3\x6f\xd2\xf0\x49\xda\xfe\x3d\xbb\x5a\ 54 | \xe4\x1e\x3b\xda\x5b\xa6\x89\x29\xad\xac\x8c\x0b\x84\x05\x62\x2b\ 55 | \xaa\xaa\xf3\x2b\xaa\xab\xf7\x15\x96\x95\xff\x26\xc2\x33\xcf\x63\ 56 | \x8f\x3d\x56\x23\x59\x8a\x85\x66\x7e\xb6\x91\xf0\xa9\x3a\x46\x4b\ 57 | \xe6\x6a\x67\x50\xbf\x10\x80\x5f\xe2\x1b\x68\xa5\x4c\xbd\x5c\xc1\ 58 | \x2f\x4d\x6e\x10\x23\xc3\x67\xe9\x77\x9c\x7a\x00\xa4\x78\xa1\xa3\ 59 | \x6b\xe7\xdd\x72\x73\x73\xdb\xb7\x6a\x95\xf9\xd7\xd2\xd2\xb2\x99\ 60 | \xb5\x7e\x7f\x42\x79\x4d\x75\x74\x20\xcc\x17\x2f\x7c\xc6\xd7\x86\ 61 | \x47\xf5\x32\x61\x81\xd1\x8e\x68\xa7\x4e\x9d\xce\x9b\x7e\x63\x0f\ 62 | \xc8\xf2\xb5\xea\x95\x32\x64\xa2\x14\xd0\x46\x63\xaa\xbd\x72\xb8\ 63 | \x20\xc8\xb3\x04\x41\xa3\x41\x65\x52\x40\xbc\x84\x8f\xd3\x79\x96\ 64 | \x7a\xb2\x2e\x93\x25\xbc\x68\x69\x6c\xae\x06\xd7\xe4\x83\xf6\x5a\ 65 | \x4a\x4a\xca\xa0\x8b\x2f\xbe\x78\x63\xfb\xf6\xed\xf7\x64\xa4\xa5\ 66 | \xfc\xa5\x5b\xcf\x9e\x59\x81\xb0\x30\x5f\x54\x78\x84\xee\xa5\x9a\ 67 | \xae\x97\xf4\x08\xf4\xea\xdd\x7b\x44\x4a\x46\xe6\xf3\x17\x5c\x70\ 68 | \x41\x8d\xfa\xf4\x5d\xbb\x76\x61\x98\x5f\xd4\xbc\x02\xca\x90\x46\ 69 | \x59\x2d\x43\xf2\xb4\x14\x1a\xc8\x6a\x95\x1e\xd4\x1b\x3b\x49\x10\ 70 | \x05\x44\x9d\x18\x0d\x4e\x6c\xd1\xa2\xc5\x28\x02\x61\x45\x45\xc5\ 71 | \x09\xb9\xc2\x56\x5d\xdf\xa7\x9e\xa7\x5e\xe7\xe1\x66\x70\x97\xd0\ 72 | \xbd\x7b\xf7\xf7\x25\xfc\xf0\x2b\xaf\xbc\xd2\x0c\x1c\x38\xd0\xa4\ 73 | \xcb\x0d\x3e\x78\x6f\x81\x99\x3f\x77\x8e\x49\x4a\x4d\x33\xe1\x11\ 74 | \x11\xa6\xff\xc0\x2b\xcc\xf8\x09\x13\x0c\x50\xdd\xba\x75\xab\x59\ 75 | \xb9\x72\xa5\xf9\xea\xab\xaf\xaa\x0f\x1d\x3a\x74\xfb\xfe\xfd\xfb\ 76 | \xe7\x2b\x1e\x99\x93\x27\x4f\x36\x63\xba\x33\x01\xce\x09\x88\x22\ 77 | \x64\xc0\xe8\xc4\xc4\xc4\x09\x92\xa7\x67\x55\x55\xd5\x47\x8a\x6b\ 78 | \x5f\x4b\xc6\x32\x37\xc6\x5a\x7d\xd8\xb0\x61\x1c\x88\xd0\x01\x1e\ 79 | \x50\xeb\x8a\xbf\x48\x01\xb9\x62\x8a\xa0\x51\xa4\x5e\xa2\x5e\xc5\ 80 | \xd8\x65\xcb\x96\x31\xde\xba\x84\x3b\xb7\x17\x3c\x2d\x33\x33\x73\ 81 | \xc8\xb5\xd7\x5e\xbb\xfd\xa1\x87\x1e\xea\x34\x66\xcc\x18\xd3\xaa\ 82 | \x55\x2b\x7b\x97\xa8\xbc\x77\xd7\x6e\x7b\x8e\x60\xdd\x7b\xf4\x32\ 83 | \x29\xe9\xe9\x26\x36\x2e\xce\x28\xe6\x98\xf8\xf8\x78\xd3\xa3\x47\ 84 | \x0f\xa3\x67\xfd\x09\x09\x09\x63\x94\xbf\x2f\x55\x3a\x7b\x47\x75\ 85 | \x89\x39\x7d\xfa\xb4\x77\x8a\x46\xcf\xe1\x4f\xdd\xa7\x6e\x53\x9e\ 86 | \x68\xfa\xe2\xe2\xe2\x46\xe0\xce\x92\x65\x9b\x94\xb0\x4f\xb2\x55\ 87 | \x39\xbe\x6d\x5a\xa0\x49\x23\x35\x12\xa8\x0c\x81\x35\x20\x20\x2d\ 88 | \xb5\xd4\x65\xe2\x02\x63\x40\x07\x04\x1d\x62\xdc\x33\x28\x2d\xd4\ 89 | \x86\x6b\xf2\x2f\xa4\x9c\xae\x5d\xbb\xde\x39\x6e\xdc\xb8\xb9\xd7\ 90 | \x5d\x77\x9d\x15\x2a\x2f\x0f\xf0\x9c\x69\xa2\x6d\x92\x53\x53\x4c\ 91 | \xbf\xcb\x07\x99\x8e\xf2\x73\x65\x19\x13\x1b\x1b\x6b\x0a\x0a\x0a\ 92 | \x8c\x7c\xd5\x4b\xce\x0c\x1f\x3e\xdc\xf4\xed\xdb\xf7\xc6\x99\x33\ 93 | \x67\xee\x58\xb4\x68\x51\xb7\xb6\x6d\xdb\x56\x09\x11\x75\xc6\x34\ 94 | \xf1\xc3\x1a\x53\x3d\x52\xb0\xe7\x3c\x4c\xf3\x26\x92\x0e\x99\xdf\ 95 | \xeb\x02\x16\x01\x4e\x1b\xd2\x9a\x62\x93\x3f\x4c\xbe\xdb\x55\x97\ 96 | \xd3\x05\x9b\x83\x52\x48\xa1\xce\xe9\xa0\x00\x62\x8e\xcb\x1a\x2f\ 97 | \x1a\xee\xbd\xe7\x1e\x23\xc8\x76\x54\xcd\xb0\x5a\x3e\x3c\x31\x35\ 98 | \x25\xc5\xec\xdb\xb7\xcf\x14\xca\x6a\xa2\x69\xc4\x88\xc1\x1f\xe9\ 99 | \xd1\x31\xb1\x26\x3e\x31\xd1\x9e\x87\xe9\x7a\x95\xa0\x0f\xfc\x85\ 100 | \xb8\x3a\x1d\xe5\xc1\xb0\xdc\x27\xa5\xa4\xa4\xe4\xb6\xd5\xab\x57\ 101 | \xcf\x8a\x8a\x8a\x32\xb2\x22\x6c\x37\xda\x90\xc5\x83\xe8\x38\xc5\ 102 | \xb3\x2c\x29\xb6\x83\xe8\x74\xd1\x7c\x5f\x2a\x18\xee\x5f\xbe\x7c\ 103 | \xb9\x4d\x0b\xb4\x10\x02\xf8\x41\x6a\x78\xee\xb9\xe7\x2a\xa4\xa9\ 104 | \x1c\xfd\xec\xa5\x8e\x82\x98\x0d\xeb\xb7\x50\x27\x4b\x80\x43\xd2\ 105 | \x88\x0e\x67\xda\x55\xf2\xef\x25\x8b\x17\xa7\x4d\x9d\x36\x6d\xd7\ 106 | \xc4\x89\x13\xcd\x1b\x6f\xbc\x61\x14\x47\xcc\xe5\x97\x5f\x6e\xf6\ 107 | \xee\xdd\x6b\x76\xfc\xf8\xa3\xd9\xb4\x69\x93\x29\x2a\x2a\x32\xa7\ 108 | \xd5\x63\x24\x44\x8c\xac\x4e\x90\x8c\x8a\x8c\x34\xaa\x3b\x4c\x54\ 109 | \x74\xb4\x91\xeb\x59\x14\x88\x69\xab\x34\x84\x17\xc3\x46\xf9\xdb\ 110 | \x4c\x98\x30\xa1\xa3\xe2\xc0\xd7\x85\xa7\x4e\x4d\x5a\xb7\x7e\xfd\ 111 | \xce\xd0\xe4\x8d\x9c\x04\x79\xab\x15\xa2\x4f\x8b\x46\x5b\x52\xb9\ 112 | \x0c\x80\x5c\x85\xea\x75\xca\xfa\x90\x02\xf0\x67\x1e\xd4\xc0\x2a\ 113 | \x0d\x3a\x20\x06\xfc\xb8\x42\x90\x3e\x56\x47\x09\xe5\xea\xd4\x0b\ 114 | \x75\xa0\xbf\xfc\xcb\x2f\xcd\xa3\x8f\x3e\xba\xe6\x96\x5b\x6e\xb1\ 115 | \xcc\xbe\xf6\xda\x6b\x46\x2e\x60\x05\x6e\xd3\xa6\x8d\x69\xdd\xba\ 116 | \x75\x08\x05\x20\x41\xd6\xb4\xf7\xb0\xe4\x89\x13\x27\xac\x02\x70\ 117 | \x01\xae\x15\x17\x17\x9b\x72\x59\xbe\x48\xc7\x32\xc5\x0b\xbf\x94\ 118 | \x81\x42\x22\x35\x66\xf4\x98\x31\x83\x75\x6d\x87\x02\xe7\xa3\x2b\ 119 | \x57\xad\x7a\xba\x11\xd9\x43\x97\x82\x99\xa0\x52\xcf\xa2\x04\xb2\ 120 | \xd8\x5e\xf5\x02\x94\x1a\x0c\xfa\x8c\xad\xb5\x2e\x40\x03\x3a\xba\ 121 | \xe1\x57\xd0\xa9\x55\x20\x8a\xd1\x83\xc3\x70\x01\xf5\x3c\x3d\x84\ 122 | \x0b\x9c\x52\xa7\x50\xaa\xf2\x5a\x9f\x67\xaf\x1b\x39\xf2\x5e\xf9\ 123 | \xf3\x84\xa3\x47\x8f\x9a\x23\x47\x8e\x58\xa1\x76\xee\xdc\x69\x54\ 124 | \x81\x99\x1f\x65\x7d\x50\x20\xf7\x30\xdc\xe7\x1e\x0a\xc0\xc2\x1f\ 125 | \x7c\xf0\x81\xf9\xf6\xdb\x6f\x6d\x30\xbc\xf0\xc2\x0b\xed\x31\x39\ 126 | \x39\xd9\xa4\xa4\xa6\xda\xa0\x99\x2d\xc5\x71\x3c\x76\xec\x98\xc1\ 127 | \xf7\x15\x03\x4c\xc7\x8e\x1d\xcd\xc9\xfc\xfc\xe1\x05\xf9\xf9\xcf\ 128 | \x5e\x72\xc9\x25\x15\xfb\x9a\x88\x09\xc8\x73\xd5\x55\x57\xa1\xdc\ 129 | \x0c\xc9\x32\x44\x28\xc8\x95\xc2\x77\xca\xb8\xc0\x9f\x45\x91\x45\ 130 | \x42\x1d\x04\xe8\xb7\x4f\x3e\x16\xd0\x20\x22\xff\x21\x4a\x48\x45\ 131 | \xed\xcd\x3a\x27\x8a\x21\x3c\x69\xb0\x8e\xf5\xb3\xb3\xb3\x8d\x10\ 132 | \xb0\xac\x83\x18\xc3\x67\xf1\x7b\x04\xc5\xaa\x8b\x3f\xf9\xc4\x42\ 133 | \x5b\xf4\x4c\xaa\x84\x22\x92\xab\x26\x30\x4a\x4b\x16\xfe\x64\x84\ 134 | \x9f\x7f\xfe\xd9\xfe\x26\x06\x80\x02\x82\x22\xd1\x9e\x7b\x34\x14\ 135 | \x85\x32\x99\x67\xdd\xba\x75\x46\xd9\xc5\x28\x3b\x98\x0e\x9d\x3a\ 136 | \x3d\xfb\xe9\xa7\x9f\xde\x6d\x07\x35\xd1\x14\x53\xc8\x00\x19\xa2\ 137 | \x91\xa9\x74\xbe\x5d\x7c\x20\x83\x35\x22\x8f\x60\xc8\x50\xb1\x11\ 138 | \x84\x0c\x0a\x89\x52\x54\x4e\x97\x22\x7e\xa7\xf3\x0b\xd5\x67\x0b\ 139 | \xda\x7b\xe4\xd3\xc0\xbf\xd1\x26\xb8\xaf\x14\x53\x57\x70\x13\x6b\ 140 | \x23\x4c\x9f\x3e\x7d\x0c\xf1\x80\x8a\x0e\xd8\xe5\x1c\x3e\x6c\x0e\ 141 | \x0a\x05\x3f\xfd\xf4\x93\x59\xbd\x6a\x95\xf1\x49\x30\x10\x42\x0c\ 142 | \xd0\x00\xeb\x0e\xae\x33\x1e\xc1\x5d\x1c\x40\x99\xd0\x45\x81\x5c\ 143 | \x93\x30\x16\x51\x0b\x17\x2e\x3c\x67\xb1\xf4\xd2\x4b\x2f\xdd\x2e\ 144 | \xb6\xfa\xea\x99\x55\x42\xde\xe7\x2a\x8f\x4f\x3a\x77\x87\xdf\x3a\ 145 | \x04\x82\xbe\x41\xaa\x48\x96\xc5\xfa\xca\x92\x93\x36\x6c\xd8\xd0\ 146 | \x41\xc7\x12\x59\xa7\x5a\x30\xb2\xe3\xa5\x55\x1b\xe4\x88\xe2\xba\ 147 | \x5e\x3b\x68\xd0\xa0\xfe\x4e\x70\xe0\xdd\xbf\x7f\x7f\x33\x62\xc4\ 148 | \x08\x3b\x06\x8b\x61\x75\x1a\x48\xa0\x31\x76\xda\xb4\x69\xd6\x1d\ 149 | \xb8\x86\x50\x58\x9d\x74\x58\x2d\x45\x10\xf8\x88\x15\x5a\x25\x9a\ 150 | \x68\x5d\x43\x49\x64\x0a\x14\xc6\xb5\x52\x21\x8d\xb9\x85\x96\x4d\ 151 | \x52\x36\x65\xae\x45\x8e\xa3\x4f\x20\x95\x01\xc9\x68\x45\x37\xdc\ 152 | \x70\xc3\xc6\xb4\xb4\x34\x85\x97\xa2\x15\x52\xc2\x26\x29\x80\x6c\ 153 | \x16\x6a\x75\xb2\x00\x3c\xaa\x57\x29\xe2\x9e\x9c\x32\x65\xca\xc3\ 154 | \x62\x7e\x30\xd0\x45\xe3\x30\x09\x44\x11\x1e\x0b\xa9\xac\xb5\x1b\ 155 | \x0e\x58\xe2\xc0\x81\x03\x36\xd7\x33\xee\xa2\x8b\x2e\xb2\x69\x08\ 156 | \x98\x22\x10\x6e\x71\xfc\xf8\x71\xeb\x02\x30\x08\xb3\xc0\xf9\x8e\ 157 | \x3b\xee\x30\xcf\x3c\xf3\x8c\x55\x06\x7e\xde\x52\xca\x4a\x4f\x4b\ 158 | \x33\x97\x49\x79\xaa\x1e\xad\xb5\x49\x79\xab\x54\x15\x12\x2b\x8e\ 159 | \xe7\xe6\x5a\xb7\x20\xa0\xe2\x12\xd4\x08\xa2\x75\x29\xa8\x21\x3e\ 160 | \xc0\x1f\xe7\xf0\x80\xe2\x71\x31\xe6\xd7\x72\x7e\xb8\xd6\x20\xf3\ 161 | \x3a\x77\xee\xfc\x86\x5c\xe2\x8c\x5f\x79\x14\x10\x0a\x82\x5c\x23\ 162 | \x70\xd0\x55\x8a\x3e\xa0\x80\x34\x09\x02\x10\x55\x60\xb4\x16\xe2\ 163 | \x1c\x01\x10\x9e\x68\xed\xd2\x15\xbe\x49\x0e\x6f\xd9\xb2\xa5\x11\ 164 | \x1a\x4c\x97\x2e\x5d\x6c\x45\x87\x00\x58\x92\x86\xc5\xb0\x20\x4a\ 165 | \xc0\x45\x08\x78\x5f\x2a\x7b\x30\x26\x41\x4a\xed\x3f\x60\x80\x49\ 166 | \x96\x22\xf6\x2e\x5f\x6e\xf6\xbc\xfb\xae\x39\x22\x25\x6f\x10\x5d\ 167 | \x9f\xac\x39\xe5\xbe\xfb\x4c\xbe\x14\x4c\xc0\x83\x0f\x14\x8e\xa2\ 168 | \x41\x1b\x4a\x81\x0f\x14\xce\x75\x37\x1f\x47\xf8\x81\xbe\xd2\x67\ 169 | \xf7\x8d\x1b\x37\xbe\x23\xde\x0f\x6d\xde\x4c\x48\xfb\x67\xab\xa3\ 170 | \x00\x77\x59\x91\x76\x99\xb4\xa8\x25\xba\xdf\x0a\x4a\x27\xb0\xb9\ 171 | \xf4\xa5\xda\xde\x46\x6b\xae\x33\xf1\x8a\x15\x2b\xac\x72\xb0\xec\ 172 | \x8d\x37\xde\x68\x21\xef\x84\x87\x06\x8d\xfb\x30\x85\x3f\xbf\xf9\ 173 | \xe6\x9b\x58\xe6\x8c\x35\x95\x25\x2e\xbb\xec\x32\x53\x22\x08\xfb\ 174 | \x67\xce\x34\x03\x65\xdd\xae\x42\x4c\x77\xd1\x1d\x78\xd3\x4d\x26\ 175 | \x56\xcf\xbd\x2d\x85\x8c\x19\x3f\xde\xec\xd4\xd8\xfd\x12\x12\x5e\ 176 | \x98\x17\x25\x62\x6d\x94\x0d\x02\x0f\x2b\xce\x80\x38\xa7\x6c\xee\ 177 | \xb9\xea\x52\x28\x48\xff\xe8\xa3\x8f\xde\xa9\x23\xbd\x7e\x34\xd8\ 178 | \x22\x12\x54\xda\x08\x3e\xd1\x0c\x44\xdb\x40\x0a\xab\x43\x94\x1c\ 179 | \xcf\x91\x6b\xc0\xdf\x45\x72\xac\x80\x72\x88\xe6\x30\x85\xa5\x99\ 180 | \xd8\x09\x0f\x2d\xdc\x86\xe7\xb4\x2d\x65\x2d\x4f\xbd\xcf\x31\x55\ 181 | \x8a\x24\x49\xc7\xbc\xfd\xb6\x19\x2a\xe6\x3b\xe8\xbc\x9d\x7a\xdc\ 182 | \x87\x1f\x9a\x78\xa1\xb1\xa3\xe6\x1c\xa5\xb9\x96\x2d\x58\x60\x46\ 183 | \x5e\x7d\xb5\x29\x56\xad\x00\x82\xb0\x36\xf3\x10\x63\xe8\xa0\x14\ 184 | \x83\xc0\x23\xbc\x30\xbf\xe3\x3d\x18\x53\x46\xc0\x47\xfd\x56\x3f\ 185 | \x06\x40\x34\x03\x4b\x01\x55\x04\x63\x32\x3a\xee\x40\x3e\xc6\x82\ 186 | \x10\x04\x5e\x4c\xca\xbe\x9b\xf3\x6d\x14\xe6\x84\x87\x01\x9e\xf3\ 187 | \x36\x95\x63\xe6\x95\xd9\xb3\xcd\x29\xc5\x92\x5c\x21\x60\xeb\x86\ 188 | \x0d\xe6\x3e\xad\x02\xa3\x64\xf5\x81\x5a\xed\xb5\xd6\x60\x34\x4f\ 189 | \xc9\xc9\xb1\x5c\x96\x8f\xe8\xd5\xcb\xb4\xd2\x9c\x19\x9a\x4b\xc1\ 190 | \xcc\x64\x09\x11\xf9\xba\x57\x2c\x8b\x87\x89\xc7\x78\xb9\x4f\x20\ 191 | \x28\xac\xcb\x22\xc4\x29\x1a\xbf\x51\x06\x4d\x06\xb0\x46\xad\xdf\ 192 | \x1a\x20\x40\x5a\x3b\xca\x83\x58\x8c\x86\xc5\x39\x87\x28\xb0\x47\ 193 | \x30\x3a\xca\xa1\xbb\x85\x0e\xd0\x23\xa0\xe1\x8f\x28\xd0\xf9\xa2\ 194 | \x77\x42\x52\x1f\xcb\x5f\xed\x05\x98\xb5\xeb\xd7\x9b\x4a\xd1\xad\ 195 | \xd4\x58\xad\xbe\x6c\x79\x49\x72\xe6\xe8\x7a\x19\x08\x94\xc5\x4b\ 196 | \xe5\x1e\xc5\x42\x4f\x9e\xe6\xe8\x2a\xab\x27\xa9\xb6\x28\xd2\xdc\ 197 | \x91\xf2\xef\x03\x52\x64\x91\xc6\x51\x46\xa3\x7c\xdc\x0f\x63\x81\ 198 | \x3e\xf8\x46\x16\x0c\xa4\xde\x20\x00\xc2\x5b\x03\x05\xa8\x30\x39\ 199 | \x20\xc1\x4a\x79\xc8\xc1\x18\x61\x98\x80\x48\xce\x24\x40\x1f\x3f\ 200 | \x04\x09\xde\x25\x2a\xd1\x98\x6b\x4c\x8e\x22\xe8\xae\x41\x8f\x40\ 201 | \xd9\x5f\x7b\x02\xdd\x04\x7f\x84\xd4\xce\x8b\xd9\x49\x80\x55\xec\ 202 | \xf8\x46\xc2\xb0\x00\x39\xa1\x7e\x58\x9d\x35\xdf\x11\x3d\x73\x42\ 203 | \xf4\xf6\x48\xc0\x9f\x64\xfd\xad\x82\xf8\x2e\x29\xe4\x6a\x09\x59\ 204 | \x24\xc3\x28\xcf\x59\x54\xe6\xa8\xfa\xc4\xf5\x88\x0b\xf0\xe8\xd6\ 205 | \x14\xcc\xe9\xc9\x0e\x4b\x43\xcc\x78\x4e\x1a\x0d\x82\xb2\x74\x09\ 206 | \x85\x10\x81\xcc\x35\xb4\x09\x31\x14\xc0\x44\xe4\x5d\xdc\x81\x52\ 207 | \x17\xbf\xa3\x01\x79\x6a\x00\x8a\x1f\x94\xc6\x78\x10\xe4\x22\x3f\ 208 | \x3e\x0a\x4d\x94\x44\x09\x12\x13\x1b\x6f\x76\xed\xde\x63\xda\x75\ 209 | \x68\x67\x72\x14\xb0\x6a\x76\xef\xb6\xa5\x29\x48\x00\xb8\x85\x42\ 210 | \xda\x51\xd1\x5c\xae\xf1\xd1\x43\x86\x98\x95\x0a\xb6\xbb\x35\xe6\ 211 | \xf7\x8a\x09\x9f\x29\x2d\xf6\xeb\xd7\xcf\xe4\xe4\xe4\x58\xc5\x32\ 212 | \x1f\x2e\x09\x6f\xf0\xe5\x3a\x46\x80\x3f\x65\x82\xb1\x8a\x5b\xb9\ 213 | \x28\xcc\xdb\x1a\x55\x80\xea\xf9\x75\x22\xd4\x47\x0f\x5f\x00\x01\ 214 | \x04\x03\xe2\x08\x4d\xc4\xe5\x08\x51\x16\x3a\x2a\x94\x42\x65\x2b\ 215 | \x84\xf1\xb9\x91\x23\x47\x86\x18\xe0\x37\x30\x64\xa1\x73\xf4\xb8\ 216 | \x2a\xea\xaa\xd3\x26\x29\x6c\x9f\x19\xd6\x33\x60\x06\x5d\x12\x30\ 217 | \x83\xbb\x47\x9b\x84\x48\xa5\x54\xe9\x7a\x7d\xca\xc5\xe6\x98\x38\ 218 | \x2a\x64\x07\x48\x9b\x24\x3b\xe4\x72\x5b\x94\x72\x63\x25\xfc\xd6\ 219 | \x2d\x5b\xec\x5e\xc3\x69\x29\xe3\x22\x09\x9b\x2e\x14\x46\x48\xd1\ 220 | \x07\x15\x0c\xed\x2a\x53\x28\x01\xb1\xcc\x87\x92\x51\x00\x46\xa3\ 221 | \x76\x11\xaf\x7f\xfc\xee\xbb\xef\x16\xd5\x17\x1e\x7e\x1b\x04\x41\ 222 | \x2e\x52\x88\x68\x91\x72\x83\x16\x1b\xff\x22\x62\xe3\x64\xc1\x18\ 223 | \x11\x2e\xd6\x31\x51\x3e\x6f\x83\x83\xce\x3b\xeb\x90\x84\x32\xbc\ 224 | \xed\xf3\xcf\x3f\x37\xab\x54\xea\x5e\x73\xcd\x35\xd6\xf2\xce\x02\ 225 | \x87\x73\x8e\x99\x56\x45\x73\x4c\xef\x8c\x2a\x13\xd9\xb9\x9d\xf1\ 226 | \x47\xb6\x33\xbe\x88\x96\x3a\x26\xeb\xa8\xd8\x12\x95\x68\xf6\xef\ 227 | \xda\x6c\x9e\x7e\x25\xce\xec\xab\x89\xb1\xcf\x45\x0b\x6d\x55\x12\ 228 | \x6c\x91\x0a\xa1\x1f\x15\x28\x5d\xb3\xeb\x71\x8a\x33\x19\x41\x70\ 229 | \x3f\x2c\x77\x3c\x2d\x25\xe7\x4a\x60\x36\x6e\x2a\x55\x61\x96\x8a\ 230 | \x6f\x22\xe1\x51\x79\xf3\xdf\xb6\x6d\xdb\xb6\xae\x0e\x93\x9e\x1f\ 231 | \xe7\xac\xa5\x1b\x7b\x50\x65\xee\x1a\xad\xcc\x06\x50\x05\x7e\xf6\ 232 | \xd9\x67\x56\x50\x6f\x03\x86\xe4\x7a\xf6\x03\x80\xfb\xe6\xcd\xdf\ 233 | \x99\xe4\x92\x85\x26\x39\xef\x25\xd3\x22\xad\xad\x89\x49\xeb\x63\ 234 | \xc2\xb3\x46\x1b\x5f\x7c\x17\xa3\x77\x21\x32\x43\xa2\xba\xf8\x0d\ 235 | \x8b\x31\x85\xa7\x72\xcd\x93\x33\x66\x2a\xe0\x26\xd9\x58\xf3\xca\ 236 | \xab\xaf\xda\x7d\x42\x6f\xbb\x4a\x28\xdc\xac\x20\x38\xa5\x67\x4f\ 237 | \xb3\x3a\x31\x31\x3f\x59\x29\x49\xae\x30\x6b\xcd\x9a\x35\x7f\xac\ 238 | \xcf\xaf\x76\xa7\x8c\x14\x50\xff\x72\xe8\x77\xb3\x15\xc0\xa6\xa6\ 239 | \x5c\x21\x5a\x9a\xdd\x3f\x60\xc0\x00\xde\x1a\xd9\x5a\x00\x1f\xd4\ 240 | \x0e\x8b\xf9\xe1\x87\x1f\xac\xbf\xbb\xf6\xb6\xf2\x3a\x01\x09\x38\ 241 | \x16\x15\x15\x9b\xca\xbc\x8d\xa6\x4f\xc2\xc7\x26\xbc\x60\xad\x89\ 242 | \x55\x42\x0a\x57\xf8\x0d\x4b\xbd\x5c\x70\x7b\x59\xef\xa2\xba\x85\ 243 | \x9e\x3b\xfe\xc3\x5c\xb3\xaf\xac\x97\xc9\xca\x4c\xb5\xd1\x1c\xff\ 244 | \x1e\x35\x6a\x94\x91\x70\xa1\x31\x89\x52\x40\x1f\x95\xc4\x09\x2a\ 245 | \xc8\xb2\xc5\x43\xb4\x14\xc5\x38\x6d\xba\x2c\x55\x51\xf6\x3b\x78\ 246 | \xa5\xc6\x68\x4e\x6b\x34\x06\xd4\x7f\x10\x82\x82\x58\x9c\x18\x3a\ 247 | \x78\xff\xfd\xf7\x27\xcb\x22\x3e\xfc\x1a\x0b\x01\x75\x6a\xf9\xed\ 248 | \xdb\xb7\x87\x82\xa1\x56\x69\x36\x0e\x50\xa0\x30\x86\x8c\x5a\x15\ 249 | \x48\x31\x39\x95\xdd\x4c\x46\x4a\x84\xa9\x2d\xd8\x2a\x88\x2b\x05\ 250 | \x95\x1f\x34\x66\xcf\x6c\x21\x48\xa9\x36\x75\x98\xc0\x9b\x6f\xd6\ 251 | \x7f\xbb\xd9\xa4\x67\x5f\x68\x8b\x2a\x97\x72\xf5\x72\xd6\xbc\xf7\ 252 | \xde\x7b\xa1\x9d\x61\xf6\x09\x06\x6b\xbf\xb1\x97\x10\x40\xea\x25\ 253 | \x00\xa6\x2b\x66\x68\xff\xb0\xa3\x8c\x30\x54\xe5\xfc\x1b\xf0\xbc\ 254 | \x4f\x4b\xf3\x73\xb5\x73\x2a\x40\x84\x10\x56\x4a\x8f\x3f\xa8\x05\ 255 | \x52\x0b\xa1\xcd\xa7\xd8\x60\xf3\x3f\xf5\x38\x91\x1d\x34\x68\x6d\ 256 | \x6e\xcf\x67\xcd\x9a\x65\xc6\x8e\x1d\x6b\x03\x27\x41\x88\xac\x81\ 257 | \x30\x2c\x76\xc2\x23\xe3\xcd\x8e\x13\x99\x26\x2d\x49\x7b\x04\xa7\ 258 | \xb6\x68\x65\xa7\x58\x22\xe5\x54\x1c\x59\x69\xca\x0b\x7e\x36\x9b\ 259 | \xbf\xdf\x66\xa2\xb3\xaf\x55\x2e\x4f\xb2\x41\xcc\x5b\x49\xb2\xc3\ 260 | \xf4\xe2\x8b\x2f\x86\x8a\x30\x77\x1f\x1e\xa8\xf8\x7a\x4a\x19\x2c\ 261 | \xa0\xe4\x9a\xed\x34\xf7\x08\xb9\xe6\x3f\x9a\xa3\x84\x06\x75\x80\ 262 | \x57\x63\x4e\x78\x09\x80\xf0\x89\xf2\x6d\x1f\x82\x01\xfb\x76\xed\ 263 | \x14\xc8\x94\xe6\xc8\xb9\x2e\xda\xde\x75\xd7\x5d\x76\x0f\x00\x74\ 264 | \xb8\xc6\x3d\x2c\x04\x12\x28\x52\xda\xb4\xeb\x6c\xb6\x94\xfc\xde\ 265 | \x54\xa5\x8f\x55\x31\xa3\x6d\x72\xed\x32\x14\x6b\x7f\x66\xd7\x8e\ 266 | \xcd\xa6\x22\x65\xb4\x49\x4b\x4d\xb2\x6e\xe3\xad\x21\xa0\x45\x5c\ 267 | \xf9\x50\xe5\x31\x0d\xfa\xd0\x04\xf6\xc4\x21\xee\xa1\x7c\x78\x63\ 268 | \xc7\xe8\xce\x3b\xef\xbc\x5c\x7b\x18\x6b\x71\x03\x94\x70\xb6\xd6\ 269 | \x24\x02\x86\x0e\x1d\x8a\xe5\x5b\xaa\xc0\x38\x70\xf7\xdd\x77\xc7\ 270 | \xe3\x8f\x68\x9a\x1a\x1c\xe6\xf8\x8d\x50\x58\x18\x85\xb0\xb8\x99\ 271 | \x33\x67\x4e\xa8\x02\x43\x70\x6f\x47\x59\x30\x0d\xac\xc3\x02\x91\ 272 | \x66\x7f\x61\xba\x4d\x87\xc5\x05\x39\xa6\xb0\x22\xc1\x1c\xef\xf0\ 273 | \x96\x69\xdb\xba\x95\x15\x86\x31\xee\x59\x17\x60\x39\xb2\x0a\x25\ 274 | \xfd\x2a\xa5\x19\xad\x59\xac\x42\xa9\x50\x5d\x6d\xc1\x18\xd2\x34\ 275 | \xb1\x47\x5b\x6c\xd9\xa2\x33\x52\x4b\xe9\xd7\xce\x86\x84\x26\x15\ 276 | \x80\xff\x74\xeb\xd6\x2d\x7f\xf2\xe4\xc9\x31\x58\x99\xfc\x8a\xa0\ 277 | \x4e\x70\xae\x01\x43\xea\x03\x18\x92\xd6\xad\xf6\xeb\x0b\xee\x7e\ 278 | \x63\x05\x57\x21\x22\x60\x8d\x3f\xd6\x94\x04\xba\x98\x92\x32\x9f\ 279 | \x29\x6c\xf7\x37\x93\x95\xdd\x36\xe4\xf7\xce\xfa\x5e\xe1\x9d\xe5\ 280 | \x07\xa8\x92\xfc\x44\x48\x68\x11\xdc\x3f\x44\x61\x28\x01\xbe\x30\ 281 | \x06\x59\x87\x73\xca\x76\x15\x64\x59\xa2\xd1\x57\xc8\x99\xdf\x14\ 282 | \x0a\x1a\xad\x03\x18\xac\x14\x96\xa1\xfd\x7d\xd1\x8c\xb4\x96\xa7\ 283 | \xf4\x65\x22\xaf\xe0\x5c\x23\xe7\x33\x06\x5f\x74\x0c\x23\x34\x0d\ 284 | \x41\xdc\x9a\xc0\x5d\x73\x15\x1a\x56\x2b\x2d\x4d\x36\xd5\x6d\x06\ 285 | \x6a\xc7\x37\x3c\x54\xbe\x7a\xfd\x1e\x7a\x74\xe6\x77\xb4\x95\x86\ 286 | \xcc\x93\x7f\xfa\x93\xb9\xeb\xd1\x47\x2d\x6d\xba\x43\x01\x88\x84\ 287 | \x3f\xb2\x11\x73\x13\x84\x55\xac\xd9\xad\xba\xa6\x5a\xa3\x0a\x60\ 288 | \x1d\x2d\x46\x2e\x24\xb2\x92\xe7\x21\x2c\x34\xd8\xc9\x60\x86\x8a\ 289 | \x8a\x4d\x90\x4f\xb4\xe9\xc9\x39\xef\xf2\x80\x1e\x4c\x3a\x41\x11\ 290 | \xe4\xad\xb7\xde\x32\xda\x88\xb0\xdb\x4c\x2e\xdf\xd6\x06\x95\x23\ 291 | \x53\xd5\xa9\x1f\x42\x90\x0f\x72\xea\xe8\xb0\x45\x3e\x63\xc6\x0c\ 292 | \x0b\x73\xa7\x84\x8e\x2a\x81\x53\x84\x80\x77\xde\x79\xc7\xa2\x8e\ 293 | \xdd\x5f\x96\xd7\x8c\x71\xa5\xfa\x02\x2d\x9f\xd9\x3d\x52\xaa\x56\ 294 | \x91\x61\x5a\x2a\x66\x9d\x12\xaa\xeb\x16\x2c\xba\xd1\x40\x01\x08\ 295 | \x4f\x4d\xad\x00\xf3\x34\x82\x2f\x5d\xba\xd4\xb0\xdf\xcf\xfe\x1d\ 296 | \x41\x87\xd2\x17\xa5\xf0\x9b\x46\x74\x66\x52\x2f\x6c\x61\x94\xdf\ 297 | \x5b\x54\xbe\x7e\xf4\xfe\xfb\x26\x02\x68\xea\x5a\xa2\xaa\x46\xad\ 298 | \xb2\x4c\x91\x94\x53\x25\x65\x46\x88\x61\xdc\xc1\x5a\x5a\x0a\x29\ 299 | \x93\x72\x13\x65\xc1\x2a\x1d\x4b\x14\xd4\x8a\x24\x7c\x8a\xf2\xfc\ 300 | \x01\xbd\x75\x12\x1a\xed\x7c\x8c\xe5\x19\x02\xee\x83\x0f\x3e\x68\ 301 | \xd7\x06\xf4\x0e\x1d\x3a\x18\xed\xff\xd9\x17\xb0\x28\xef\x8b\x2f\ 302 | \xbe\xb0\x7c\xc3\xc7\xcd\x37\xdf\x7c\xe0\xdd\x77\xdf\xcd\x96\x6c\ 303 | \x05\xc8\xe6\x6d\x0d\x14\xc0\x80\xc1\x83\x07\xcf\x9a\x34\x69\x52\ 304 | \xbf\x95\xab\x57\xdb\xbd\x78\x2c\x0d\xb4\xd8\x2e\xf3\xbe\xe7\x83\ 305 | \x10\x45\x0a\xf7\x9c\x75\x98\xdc\x0a\xa4\xce\x96\x38\xbe\x9a\x2e\ 306 | \x81\xd3\x14\x43\x7e\x12\x93\x35\x2a\x6d\x13\x82\x59\x81\x60\x05\ 307 | \xe3\x34\x60\xcb\xdb\x23\x8a\x86\x4a\xa5\x55\x9f\x9e\xaf\x90\xa0\ 308 | \x2c\x9f\x11\xc2\xd1\xe7\xc8\x7c\x54\x99\xb8\x13\x46\xa1\xb1\x2f\ 309 | \x41\x0a\x86\x57\x04\xef\xdd\xbb\xb7\xad\x4d\x92\xb4\x8a\xd4\xc7\ 310 | \x51\xb1\x1a\xbb\x46\x88\xe4\x95\x5f\x9d\x56\x27\x0d\x06\x37\x13\ 311 | \x7b\xdd\x7e\xfb\xed\xf7\x55\x28\xd5\xb4\x97\x25\x02\xb2\xf4\x48\ 312 | \x15\x3b\xaa\xb0\xca\x71\x09\x26\x75\x8d\x28\xcc\xca\x8f\x6b\x67\ 313 | \x56\x78\x67\x2c\xe4\xe0\x1b\x2e\x0b\x5f\x29\x61\xbb\x8b\xe1\x02\ 314 | \xd5\x0e\x59\x82\x24\x42\xb3\x99\x42\x15\xc9\xf3\xc4\x0e\xea\x0a\ 315 | \x8e\x6c\x78\x50\xdc\x74\xd2\x9e\x62\x92\xfc\x37\x49\xc2\x46\x4a\ 316 | \x09\x2e\x2e\x38\xc5\xe2\x8a\xa4\xbc\xab\xb5\x43\xe4\x6d\xbd\xb4\ 317 | \x79\xa2\x94\xfd\xd5\xec\xd9\xb3\x73\x0e\x2b\x60\x47\x2b\x63\x64\ 318 | \x2a\x4e\x2d\x9a\x37\xcf\x37\x7e\xfc\xf8\x8b\x55\xc1\x4e\x3b\xab\ 319 | \x02\xb0\xbe\x2a\xb8\xd9\x14\x14\xc7\xb5\xbf\x56\xa3\xdf\x17\xa9\ 320 | \xdc\x5c\xb4\x64\xc9\x52\x05\xbc\x98\x2b\xae\xb8\xc2\x06\x1c\xd7\ 321 | \x80\x9b\x5b\x8a\x72\xcd\x6b\x25\x14\x12\xa9\x68\xdc\x55\x42\x1c\ 322 | \xd2\x5b\x9f\x16\xc1\x00\x0a\x7c\xa1\x41\x71\x84\x32\x70\x33\x2a\ 323 | \x49\xe6\x74\xa9\x96\x3c\xcf\x5a\xbf\x4c\x63\x13\xa4\xbc\xfa\x0a\ 324 | \x80\x36\xa8\x90\x40\x21\x5e\xf8\xcd\xde\xa2\x94\xf3\x88\x96\xbe\ 325 | \x59\x47\x72\x72\x9e\x88\xd1\x6a\xb5\x84\x7d\x42\x29\x0c\xda\x7a\ 326 | \x57\xf1\xc4\x59\x15\xa0\x9b\x59\x62\xa6\x1f\x9b\x8b\x47\xb5\xde\ 327 | \xde\x1a\x19\x59\xf3\xfc\xfc\xf9\x63\x3e\x59\xb4\x88\x97\x24\x7c\ 328 | \x6f\x76\x14\xeb\xb9\x46\xe0\x41\x68\x18\xf4\x0a\xef\x2c\x05\xf3\ 329 | \x25\x9a\xbc\x56\x02\x51\x2d\xba\x3d\x46\x94\x00\x6a\x08\x5a\x04\ 330 | \x55\x7e\xbb\xec\x82\x52\x40\x82\x45\xa3\xe8\x86\x05\x15\x50\x9f\ 331 | \x3e\x28\xa0\xfa\x73\x8d\xe7\x89\x45\xdf\x7c\xf3\xcd\x0e\xae\x09\ 332 | \xb1\xff\xf1\x9f\x33\x66\x74\x5e\x1d\x11\x71\x28\x4f\x86\xe4\x85\ 333 | \x0c\x59\x41\x88\xfd\xb7\xd0\x43\x3a\xa9\x13\x03\x04\xc3\xdb\xc8\ 334 | \xf7\x04\x39\x45\xcc\x79\x0a\x78\xbc\x7a\xe2\x55\x92\x6d\x62\x76\ 335 | \xad\x14\x34\x8a\xa0\x43\xa3\x18\x71\x70\x87\x41\x67\x99\x90\x02\ 336 | \xa4\xf5\x37\x75\x7d\x90\x94\xb9\x42\x3e\x8e\x65\x51\x84\xb7\x51\ 337 | \xc1\x79\x37\x4d\xd6\xae\x5d\x6b\x6b\x8b\x7c\xad\xe3\xa3\x74\x4f\ 338 | \x9f\x79\x59\x6b\x43\xdb\xab\x04\x68\xb8\x8f\x2e\x38\xc7\x30\xe2\ 339 | \x1d\xe2\x05\x8e\xbe\x6a\x81\x5d\xef\x2f\x58\xd0\x5a\x45\xd1\x34\ 340 | \x19\xf5\x89\xe0\xab\xb9\x71\xfa\xfc\xe6\x55\x37\xa6\x8e\x02\x44\ 341 | \x60\x95\x34\xf8\xa2\x16\x3b\x4f\x6a\xc0\x31\x97\x11\xdc\x60\xb9\ 342 | \xc1\xd7\x4a\x27\xa3\xc8\xfd\x34\xfc\xd8\x1b\xf4\xea\x2b\x81\x22\ 343 | \xe5\xa8\x82\x54\xa5\xb6\xac\xaa\x05\x7b\x7d\x11\x16\x8a\x15\x8e\ 344 | \x26\x82\xb1\xa1\xe1\xb2\x81\xdd\xdc\x90\x11\x32\x15\x2c\xb7\x4b\ 345 | \x61\x6d\x83\x41\x90\xf1\x4e\xb1\x5e\x45\x80\x16\x5e\xbc\xb0\x25\ 346 | \x2f\x77\xfc\xda\xd1\xe5\xe8\x02\xf6\x8e\x1d\x3b\x9e\x54\x9f\x2e\ 347 | \xeb\xff\x59\xca\xac\xf3\x99\x49\x9d\x20\xa8\x3d\xbd\x35\x12\xfe\ 348 | \x3e\x09\x6e\xf7\x8d\xea\xa7\x0c\xa5\xc4\x45\x54\x58\xf8\x2d\x0d\ 349 | \x05\xd1\x08\x48\xf5\x2d\xc4\x6f\x7c\x9d\x68\xbe\x44\x50\x1f\x22\ 350 | \x81\x06\x0b\xd2\x49\xb8\x8b\x7d\xea\x4c\xab\x15\x94\x8b\x94\x1e\ 351 | \xc3\x54\x62\x67\xc9\x1d\x3a\x8b\xd6\x35\x3a\x9e\xd2\xb1\x44\x0a\ 352 | \xc0\x4d\x5c\x0c\x70\x73\x38\x45\xf3\x1b\x25\x63\x04\xd0\xa8\x75\ 353 | \xc1\x27\x1e\xd2\xf5\x4f\x6b\x65\xf9\xa7\xf5\x56\xe9\xbf\xbd\x37\ 354 | \x1a\x5d\x0c\xd5\x17\xdc\xf3\xc0\x6e\x59\x28\x97\x9a\x9c\x86\xd5\ 355 | \x60\x0e\x05\x38\xeb\x78\x99\x74\x15\x5a\xbc\xfc\xf3\x0b\x29\xeb\ 356 | \x5b\x09\x54\x29\x78\xd3\xaa\xf5\x5c\x6b\xf2\xb7\xea\x8c\x38\xa5\ 357 | \xd7\x42\x59\x91\xdc\x9f\x2f\x94\x2c\x95\xd0\x27\x65\xf9\x88\x60\ 358 | \x6c\x70\x2e\xc0\x73\xd0\x77\x9d\xdf\xc4\x12\xe6\xc1\x05\x94\xa6\ 359 | \x1b\xbc\xf8\xb0\x93\x9d\xa5\x35\xa8\x03\xce\x32\xd6\xde\xd2\x7e\ 360 | \xe1\x1c\x7d\xfb\xf3\x08\xaf\xb5\x5d\x0e\x76\xab\x3f\xaf\x12\xb0\ 361 | \x0a\xd6\xbb\x89\xb7\x3b\x8a\x05\xf8\x39\xbe\xbd\x49\xbb\x3b\x7d\ 362 | \xb4\x70\xca\x56\x7a\xcd\xb9\xf4\x52\x53\xa8\x48\x5e\xa3\xeb\xb1\ 363 | \x42\xd5\x69\x05\xdf\x29\x93\x27\xdb\x15\x1e\x1b\x2c\xbc\xc6\x0a\ 364 | \x56\xa5\x56\xc1\x4e\xb9\xde\x8d\x17\x68\xf2\x3a\x4e\x3c\x6c\x11\ 365 | \x7b\x67\xaa\xb3\x73\x09\xe1\xb9\x7f\x5e\x0a\xa0\xf8\x58\xbc\x78\ 366 | \xf1\x4c\x95\x9e\x8f\x50\x89\x11\xd5\x29\x81\x61\x08\xe6\x68\xde\ 367 | \x98\x00\x3c\x11\x02\x0b\x95\x28\x16\xb4\x92\xd0\xc3\x24\xdc\x6a\ 368 | \x15\x57\x3b\xd4\xb3\x55\x07\x90\xeb\xcb\xf5\xac\x4f\x82\x10\x10\ 369 | \x9f\xff\xfb\xdf\x4d\x8d\x82\x5e\x85\x62\x81\x5e\x67\xd9\x00\xe8\ 370 | \xb2\x8c\x53\xb0\x53\x00\x73\x51\x3f\xf0\x2d\xf3\xdc\xb9\x73\x9f\ 371 | \x3f\x0f\xb9\x43\x43\xcf\x4b\x01\xfa\x48\x89\x07\x8f\x2a\x56\x2c\ 372 | \x55\x11\x72\xb5\xd7\xe2\x6e\x8d\x0e\x53\x8e\x61\x94\xb3\x59\x16\ 373 | \xcf\xd6\x71\x9c\x04\xca\xd6\xc3\xeb\x84\x8a\xbe\x52\x5c\x84\x10\ 374 | \x14\xa3\xe0\x48\xa5\xc7\x2b\xf1\x68\xb9\x48\xa5\x94\x74\x42\xf1\ 375 | \x20\x49\x31\xe0\x7f\x94\xba\x8e\xe8\x6d\x11\x0a\x70\x2e\xe0\xa0\ 376 | \x0f\xf2\x98\x9b\x14\x4a\xd5\xa7\x39\xcb\x54\xfa\xbe\xf9\xff\xae\ 377 | \x00\x26\x40\xa8\x17\x5e\x78\xe1\x01\xd5\xdd\xdb\xf1\x3f\x17\x91\ 378 | \x61\x06\x46\x1d\x54\x19\x1b\x29\xcb\x4f\x54\x3a\xcb\xd4\xf5\xb7\ 379 | \xe5\x06\x03\xc4\xf8\x3e\x09\xbb\x91\x22\x48\xd6\xe7\x75\xb8\xb6\ 380 | \x98\xec\x1a\xa0\x58\x08\xa0\xfe\xc8\x14\x9a\x40\x04\xc8\x68\xaf\ 381 | \xd8\xc1\x07\x55\x0e\x55\x28\x00\x45\x7b\xd3\x26\x25\xef\xbc\x79\ 382 | \xf3\x1a\x54\x78\xcd\x55\x46\xa3\x41\xf0\x6c\x0f\x07\xdf\x04\xfd\ 383 | \xa8\xff\x10\xfc\x2f\xfe\x49\x00\xa4\xa3\x00\x98\x73\x9d\x6b\xc4\ 384 | \x80\x8d\x2a\x4e\x16\xaa\x8f\x64\xdb\x4c\x0a\xdb\xaa\xa8\xaf\x07\ 385 | \x4c\x85\x04\xe6\x23\xa8\x12\x21\xa3\x4c\xd0\x2f\x91\xc5\x8b\x74\ 386 | \x6f\xb9\x82\xe3\xdb\xba\x47\x11\x74\x44\xca\xd0\xbf\x36\xac\xc0\ 387 | \xd4\x0f\xf8\xbb\x2b\xa6\x40\x01\x8a\xd1\x8e\x6f\x9e\x3e\xec\x78\ 388 | \xf6\x6c\x3c\x9f\xed\xde\x79\xb9\x80\x23\x24\x88\xfb\x5e\x7f\xfd\ 389 | \xf5\x7f\xd5\x52\x74\x34\xe5\x28\xc2\x03\x7b\x98\x04\x15\x6e\x9d\ 390 | \x0e\x5a\x36\xc8\xef\x07\x89\xd9\xff\xd2\xf5\x70\x09\x8a\x82\x8a\ 391 | \xe5\x02\x8c\x21\x8b\xd0\xb1\x2c\x42\x22\x14\x71\x80\x62\xec\x90\ 392 | \x84\xa5\xe8\xe6\x1d\x20\x2f\x37\xa0\x6d\x9f\x0d\xd2\xe0\x9c\xe7\ 393 | \x1e\x7e\xf8\xe1\x89\x52\x44\x9c\x90\xc7\x96\x34\x6f\x63\x09\x46\ 394 | \x75\x3e\x85\xfb\x2d\x15\xc0\xb2\x3e\xa0\x89\x23\x35\x69\x84\xbe\ 395 | \xbf\x99\x24\x21\x5f\xa1\x10\x61\x5f\x00\xe6\xb0\x0a\x75\x02\x05\ 396 | \x0a\xa5\xf2\x46\x5e\x63\x4b\x78\x36\x32\x28\x57\x5d\x4e\x6f\x8a\ 397 | \x29\x97\x2d\x10\xb8\x5a\x6e\xb0\x47\x2b\xc4\x25\x4b\x96\xd8\xad\ 398 | \x38\xb6\xc3\x50\x12\x4a\xe3\x5d\xa0\x8a\xb6\x95\x8a\x47\x5a\x42\ 399 | \xda\x7f\xa1\xa0\x00\xde\xa8\xb9\x4f\xf9\x50\x42\x83\xf5\x7f\xfd\ 400 | \x79\xdd\x3e\x45\xfd\xeb\x8d\xfd\x66\xfb\x8c\x57\xf9\xf1\xea\x7c\ 401 | \x41\x3e\x59\xfd\x46\xf5\xb6\xbc\x22\xe3\xa3\x09\x84\x86\x39\x2c\ 402 | \xc6\x8b\x52\xf6\x03\x10\x88\xec\x41\xb0\xe2\x88\x12\x50\x90\x37\ 403 | \xba\x3b\xdf\x46\x68\x94\xc8\xf3\xf2\x6b\xb3\x54\x25\x39\x2f\x3e\ 404 | \xa1\xcf\x0b\x0e\xe8\x83\x30\x47\x9f\xe5\xae\x5c\x42\x7b\x2c\xbe\ 405 | \x2f\x85\x80\xd7\xc5\x0b\x95\x20\x0a\xa0\x7c\xa7\xe0\x00\x11\x67\ 406 | \x55\x42\x73\x15\x80\xf0\xbc\x5f\x4f\x50\x9f\xa2\x09\xa7\x52\x78\ 407 | \x8c\x1e\x3d\xda\x0c\x1d\x3a\xd4\xae\xec\x1c\x24\x75\xdf\xa2\x00\ 408 | \x01\x39\xc2\xe4\xc7\x1f\x7f\x6c\xc8\x20\xa4\x2c\xfd\x8b\xc3\xee\ 409 | \xe2\xe0\x1e\xae\xfc\x45\x49\x08\x4e\x47\x70\xb6\xbf\xb1\x30\xdb\ 410 | \xeb\x4d\xd1\x07\x49\xd0\xe7\xe5\xac\xbe\xfc\xb0\x2f\x4e\xa4\xc8\ 411 | \x1f\x34\xfd\x1f\xd4\x59\xac\xb0\x26\x38\xa7\x12\x9a\xa3\x00\xc6\ 412 | \x20\x3c\x30\xfb\x87\x04\x1b\xc1\xba\x1b\xe1\x69\x30\xe1\xba\xbd\ 413 | \x10\xbc\xe6\xea\x02\x8e\x30\xcb\xae\x31\x9f\xd0\xe2\xdf\x2f\xbf\ 414 | \xfc\x32\x2f\x31\xac\x12\x68\x5c\xe3\x1d\xa3\xbe\x36\xb5\xdb\x6b\ 415 | \x2c\x6b\xa1\xef\xa5\xcd\xb9\xa3\xed\x32\x8f\xcb\x38\xdc\xc3\x45\ 416 | \x78\x1d\x07\x22\xd4\xc6\x6a\x2c\xef\x03\xd9\xfe\x01\x11\x75\xbf\ 417 | \xd4\xb0\x94\xce\xb4\xe6\x28\x80\x40\x89\xf0\x7f\x91\xf0\xf7\x12\ 418 | \xf9\xaf\xbf\xfe\xfa\x90\x95\x11\xae\x29\xbf\x76\x0c\x3a\x88\xe3\ 419 | \x16\xec\x25\x92\x21\x40\x05\xcb\x53\x98\x27\xb2\x6b\x13\xc3\xee\ 420 | \x21\xb2\x1c\x66\x6b\x0b\x9a\x04\x4a\x2f\x7d\xce\xbd\xc2\x3b\xfa\ 421 | \xd0\x03\x81\x5a\xc1\x5a\x24\x28\x5b\xf0\x62\xa2\xb7\xfa\x21\x75\ 422 | \x16\x3f\xff\x7c\x67\x17\x14\xdc\x1d\x9a\x93\x05\xd8\x02\x02\xfa\ 423 | \xf7\x02\x59\xe0\xce\x72\xd8\x45\x7b\x07\x45\x67\x21\x47\x18\xe6\ 424 | \x5c\x87\x41\x17\xe5\xa1\xc1\xb9\xb2\x88\xd1\x6b\x36\xeb\x2a\x28\ 425 | \x00\xe1\x89\x0d\x28\x18\x41\x1c\x7d\xe7\x4a\x8d\xd1\x67\x2e\x94\ 426 | \x0b\x7d\x82\x23\x74\xd9\x13\xd0\x31\xa0\x6b\x53\x74\xfb\xaf\xea\ 427 | \xf6\x7f\x0e\xea\x8d\xc6\x82\x73\x29\x00\x84\x50\x2b\xf0\x3f\x3f\ 428 | \xcb\x2c\x7e\xca\x76\x38\x0c\xba\x40\xd6\x18\x3c\xf5\x4c\xa3\x0a\ 429 | \xe0\x19\xfa\xf7\xdf\x7f\x1f\x4a\x9f\xc4\x09\x1a\xd7\x09\x84\xd0\ 430 | \x47\x10\xb7\xd8\x72\x08\xf3\x2a\xc1\x29\xd7\xab\x00\x9e\x05\x35\ 431 | \xd0\x91\x02\x40\x80\xe3\xdf\xd2\x6f\xac\x9d\x4b\x01\xee\x99\x32\ 432 | \x07\x37\xa0\xe6\x72\xb2\x53\x80\x1b\xe4\x18\x64\x2c\xe7\x5e\x26\ 433 | \x1d\x02\x1c\x1d\x87\x04\x2f\xc4\x11\x86\x71\xd0\x77\xe7\x0e\x01\ 434 | \x5e\xe6\xbd\xb4\xa1\xe7\x10\x40\xa1\xe4\xe6\xd4\xf8\x26\x3f\xed\ 435 | \xf5\xd2\x3a\x97\x02\x80\x0d\xf0\xf9\x4e\xbd\x58\xcc\xc5\x02\x35\ 436 | \xe7\x8b\x8e\xb9\xfa\xf0\x64\x02\x18\x71\x47\x17\x03\x1c\x4c\x61\ 437 | \x98\x4f\x69\xb8\xce\xb3\xec\x3c\x13\x10\x29\xa8\x18\x03\x5d\x1a\ 438 | \xe3\x9a\x72\x31\x47\xdb\xab\x00\xf7\x3c\x46\x52\xfb\x5c\x1d\x26\ 439 | \x9a\x0c\x80\x0c\x3a\x97\x02\x18\x03\x35\xb4\xf9\x67\x11\x7e\x11\ 440 | \x2d\xc3\x38\x16\x74\x08\x70\x16\xa9\x0f\x51\x1e\x66\xac\x53\x00\ 441 | \xd6\x85\x49\xb5\xbd\x5a\x29\xde\xaf\xe2\xa9\x48\x02\xd7\xa8\x5e\ 442 | \x50\xd5\x1c\xd5\x47\xb1\x60\xba\xa3\x85\x10\x0e\xce\x5c\x73\x9d\ 443 | \x87\x69\x5e\x74\xb9\x18\x03\x6d\xd0\xa3\x7b\xfb\x35\xe4\x75\x75\ 444 | \xb6\xc8\xce\x5a\x0b\x34\x27\x0b\x30\x86\x02\x88\xca\xf4\x61\xf5\ 445 | \xab\xc4\x4c\x3b\x09\x9f\xe2\x75\x81\xfa\x28\x70\x0c\x3a\x0b\x49\ 446 | \x09\x39\xea\xc7\xf5\x3c\x9f\xde\x3f\xa0\x7e\x50\xdd\xed\x37\x92\ 447 | \x66\xc9\x89\xc3\xd5\xff\x5d\x56\xef\x4a\x8c\xf1\xc6\x80\x73\x29\ 448 | \xc0\xa1\x4b\xf3\xad\x17\x8d\x7b\xd4\xc9\x00\x7c\xbf\x13\xfa\x6f\ 449 | \x80\xce\x1b\xb4\xe6\x28\x80\x87\x08\x84\x64\x03\xf6\xc2\xf8\xf7\ 450 | \x08\xc8\xe1\xd9\x4c\xf5\x2c\x75\x3e\x27\xe3\x1f\x67\x60\x57\x15\ 451 | \xac\xc5\x3f\xa9\x07\x01\x11\x14\xa1\x1d\x1c\xb9\x0e\x0c\x5c\x91\ 452 | \xa2\x53\x4b\x0f\x1a\xfc\x7d\x17\xfa\xa4\x5d\x82\xd8\x05\x52\x46\ 453 | \x3b\xd1\xe5\x73\xf3\x78\x1d\x51\x14\xbc\x80\xca\x12\x4d\x53\xa0\ 454 | \x7e\x5c\x8a\xdd\xab\xdf\x44\x52\x36\x45\x98\xd3\x55\x83\x08\x7f\ 455 | \x56\x17\x68\xae\x02\x44\x27\x14\x51\x61\x00\x41\x39\xba\x0e\x1d\ 456 | \x2f\x2d\x97\x72\x9c\xd0\xee\xe8\xea\xf3\xfa\x0b\x16\x9e\x85\x26\ 457 | \x1d\x45\xa3\x04\xd7\xbd\xb4\xdd\x1c\x8e\xbe\x86\xd9\x86\x90\x28\ 458 | \x96\x8e\xd0\x28\x88\x7e\xce\xf5\xc0\xff\x01\x2c\xfd\x4c\x2c\xf3\ 459 | \x1b\x74\x1b\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ 460 | " 461 | 462 | qt_resource_name = b"\ 463 | \x00\x06\ 464 | \x07\x03\x7d\xc3\ 465 | \x00\x69\ 466 | \x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ 467 | \x00\x0c\ 468 | \x06\x1e\xa5\xe7\ 469 | \x00\x74\ 470 | \x00\x75\x00\x78\x00\x72\x00\x6f\x00\x62\x00\x6f\x00\x74\x00\x2e\x00\x70\x00\x6e\x00\x67\ 471 | " 472 | 473 | qt_resource_struct_v1 = b"\ 474 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 475 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 476 | \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 477 | " 478 | 479 | qt_resource_struct_v2 = b"\ 480 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 481 | \x00\x00\x00\x00\x00\x00\x00\x00\ 482 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 483 | \x00\x00\x00\x00\x00\x00\x00\x00\ 484 | \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 485 | \x00\x00\x01\x6d\xc5\xcb\x71\x27\ 486 | " 487 | 488 | qt_version = QtCore.qVersion().split(".") 489 | if qt_version < ["5", "8", "0"]: 490 | rcc_version = 1 491 | qt_resource_struct = qt_resource_struct_v1 492 | else: 493 | rcc_version = 2 494 | qt_resource_struct = qt_resource_struct_v2 495 | 496 | 497 | def qInitResources(): 498 | QtCore.qRegisterResourceData( 499 | rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data 500 | ) 501 | 502 | 503 | def qCleanupResources(): 504 | QtCore.qUnregisterResourceData( 505 | rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data 506 | ) 507 | 508 | 509 | qInitResources() 510 | -------------------------------------------------------------------------------- /py_trees_js/viewer/main_window.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License: BSD 5 | # https://github.com/splintered-reality/py_trees_js/raw/devel/LICENSE 6 | # 7 | ############################################################################## 8 | # Documentation 9 | ############################################################################## 10 | 11 | """Launch a qt dashboard for the tutorials.""" 12 | ############################################################################## 13 | # Imports 14 | ############################################################################## 15 | 16 | import PyQt5.QtCore as qt_core 17 | import PyQt5.QtWidgets as qt_widgets 18 | 19 | from . import console, main_window_ui 20 | 21 | ############################################################################## 22 | # Helpers 23 | ############################################################################## 24 | 25 | 26 | class Parameters(object): 27 | """Parameters configuring the ui to save/load.""" 28 | 29 | def __init__(self): 30 | self.send_blackboard_data = False 31 | self.send_activity_stream = False 32 | 33 | 34 | ############################################################################## 35 | # Main Window 36 | ############################################################################## 37 | 38 | 39 | class MainWindow(qt_widgets.QMainWindow): 40 | """Main window for the application.""" 41 | 42 | request_shutdown = qt_core.pyqtSignal(name="requestShutdown") 43 | 44 | def __init__(self): 45 | super().__init__() 46 | self.ui = main_window_ui.Ui_MainWindow() 47 | self.ui.setupUi(self) 48 | self.readSettings() 49 | self.ui.web_view_group_box.ui.web_engine_view.loadFinished.connect( 50 | self.on_load_finished 51 | ) 52 | self.parameters = Parameters() 53 | self.ui.send_blackboard_data_checkbox.stateChanged.connect( 54 | self.on_blackboard_data_checked 55 | ) 56 | self.ui.send_activity_stream_checkbox.stateChanged.connect( 57 | self.on_activity_stream_checked 58 | ) 59 | 60 | @qt_core.pyqtSlot() 61 | def on_load_finished(self): 62 | """On load callback.""" 63 | console.logdebug("web page loaded [main window]") 64 | self.ui.send_button.setEnabled(True) 65 | self.ui.screenshot_button.setEnabled(True) 66 | self.ui.send_blackboard_data_checkbox.setEnabled(True) 67 | self.parameters.send_blackboard_data = ( 68 | True 69 | if self.ui.send_blackboard_data_checkbox.checkState() == qt_core.Qt.Checked 70 | else False 71 | ) 72 | self.ui.send_activity_stream_checkbox.setEnabled(True) 73 | self.parameters.send_activity_stream = ( 74 | True 75 | if self.ui.send_activity_stream_checkbox.checkState() == qt_core.Qt.Checked 76 | else False 77 | ) 78 | 79 | def on_blackboard_data_checked(self, state): 80 | """Enable/disable blackboard data.""" 81 | self.parameters.send_blackboard_data = ( 82 | True if state == qt_core.Qt.Checked else False 83 | ) 84 | console.logdebug( 85 | "received blackboard data parameter change signal [send_blackboard_data: {}]".format( 86 | self.parameters.send_blackboard_data 87 | ) 88 | ) 89 | 90 | def on_activity_stream_checked(self, state): 91 | """Enable/disable the activity stream.""" 92 | self.parameters.send_activity_stream = ( 93 | True if state == qt_core.Qt.Checked else False 94 | ) 95 | console.logdebug( 96 | "received blackboard activity parameter change signal [send_activity_stream: {}]".format( 97 | self.parameters.send_activity_stream 98 | ) 99 | ) 100 | 101 | def closeEvent(self, event): 102 | """Termination event, save settings.""" 103 | console.logdebug("received close event [main_window]") 104 | self.request_shutdown.emit() 105 | self.writeSettings() 106 | super().closeEvent(event) 107 | 108 | def readSettings(self): 109 | """Load settings.""" 110 | console.logdebug("read settings [main_window]") 111 | settings = qt_core.QSettings("Splintered Reality", "PyTrees Viewer") 112 | geometry = settings.value("geometry") 113 | if geometry is not None: 114 | self.restoreGeometry(geometry) 115 | window_state = settings.value( 116 | "window_state" 117 | ) # full size, maximised, minimised, no state 118 | if window_state is not None: 119 | self.restoreState(window_state) 120 | self.ui.send_blackboard_data_checkbox.setChecked( 121 | settings.value("send_blackboard_data", defaultValue=True, type=bool) 122 | ) 123 | self.ui.send_activity_stream_checkbox.setChecked( 124 | settings.value("send_activity_stream", defaultValue=False, type=bool) 125 | ) 126 | 127 | def writeSettings(self): 128 | """Write settings to be loaded in future launches.""" 129 | console.logdebug("write settings [main_window]") 130 | settings = qt_core.QSettings("Splintered Reality", "PyTrees Viewer") 131 | settings.setValue("geometry", self.saveGeometry()) 132 | settings.setValue( 133 | "window_state", self.saveState() 134 | ) # full size, maximised, minimised, no state 135 | settings.setValue( 136 | "send_blackboard_data", self.ui.send_blackboard_data_checkbox.isChecked() 137 | ) 138 | settings.setValue( 139 | "send_activity_stream", self.ui.send_activity_stream_checkbox.isChecked() 140 | ) 141 | -------------------------------------------------------------------------------- /py_trees_js/viewer/main_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1645 10 | 851 11 | 12 | 13 | 14 | PyTrees Viewer 15 | 16 | 17 | 18 | :/images/tuxrobot.png:/images/tuxrobot.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | Tree View 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 0 35 | 0 36 | 1645 37 | 29 38 | 39 | 40 | 41 | false 42 | 43 | 44 | 45 | 46 | 47 | 1 48 | 49 | 50 | 51 | 52 | 53 | 54 | false 55 | 56 | 57 | Send Tree 58 | 59 | 60 | 61 | 62 | 63 | 64 | false 65 | 66 | 67 | Screenshot 68 | 69 | 70 | 71 | 72 | 73 | 74 | false 75 | 76 | 77 | Send Blackboard Data 78 | 79 | 80 | true 81 | 82 | 83 | true 84 | 85 | 86 | 87 | 88 | 89 | 90 | false 91 | 92 | 93 | Send Activity Stream 94 | 95 | 96 | false 97 | 98 | 99 | 100 | 101 | 102 | 103 | Qt::Vertical 104 | 105 | 106 | 107 | 20 108 | 218 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | WebViewGroupBox 120 | QGroupBox 121 |
py_trees_js.viewer.web_view
122 | 1 123 |
124 |
125 | 126 | 127 | 128 | 129 |
130 | -------------------------------------------------------------------------------- /py_trees_js/viewer/main_window_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'main_window.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.10.1 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_MainWindow(object): 13 | def setupUi(self, MainWindow): 14 | MainWindow.setObjectName("MainWindow") 15 | MainWindow.resize(1645, 851) 16 | icon = QtGui.QIcon() 17 | icon.addPixmap( 18 | QtGui.QPixmap(":/images/tuxrobot.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off 19 | ) 20 | MainWindow.setWindowIcon(icon) 21 | self.central_display = QtWidgets.QWidget(MainWindow) 22 | self.central_display.setObjectName("central_display") 23 | self.central_horizontal_layout = QtWidgets.QHBoxLayout(self.central_display) 24 | self.central_horizontal_layout.setObjectName("central_horizontal_layout") 25 | self.web_view_group_box = WebViewGroupBox(self.central_display) 26 | self.web_view_group_box.setObjectName("web_view_group_box") 27 | self.central_horizontal_layout.addWidget(self.web_view_group_box) 28 | MainWindow.setCentralWidget(self.central_display) 29 | self.menubar = QtWidgets.QMenuBar(MainWindow) 30 | self.menubar.setGeometry(QtCore.QRect(0, 0, 1645, 29)) 31 | self.menubar.setDefaultUp(False) 32 | self.menubar.setObjectName("menubar") 33 | MainWindow.setMenuBar(self.menubar) 34 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 35 | self.statusbar.setObjectName("statusbar") 36 | MainWindow.setStatusBar(self.statusbar) 37 | self.dock_widget = QtWidgets.QDockWidget(MainWindow) 38 | self.dock_widget.setObjectName("dock_widget") 39 | self.dock_widget_contents = QtWidgets.QWidget() 40 | self.dock_widget_contents.setObjectName("dock_widget_contents") 41 | self.verticalLayout = QtWidgets.QVBoxLayout(self.dock_widget_contents) 42 | self.verticalLayout.setObjectName("verticalLayout") 43 | self.send_button = QtWidgets.QPushButton(self.dock_widget_contents) 44 | self.send_button.setEnabled(False) 45 | self.send_button.setObjectName("send_button") 46 | self.verticalLayout.addWidget(self.send_button) 47 | self.screenshot_button = QtWidgets.QPushButton(self.dock_widget_contents) 48 | self.screenshot_button.setEnabled(False) 49 | self.screenshot_button.setObjectName("screenshot_button") 50 | self.verticalLayout.addWidget(self.screenshot_button) 51 | self.send_blackboard_data_checkbox = QtWidgets.QCheckBox( 52 | self.dock_widget_contents 53 | ) 54 | self.send_blackboard_data_checkbox.setEnabled(False) 55 | self.send_blackboard_data_checkbox.setCheckable(True) 56 | self.send_blackboard_data_checkbox.setChecked(True) 57 | self.send_blackboard_data_checkbox.setObjectName( 58 | "send_blackboard_data_checkbox" 59 | ) 60 | self.verticalLayout.addWidget(self.send_blackboard_data_checkbox) 61 | self.send_activity_stream_checkbox = QtWidgets.QCheckBox( 62 | self.dock_widget_contents 63 | ) 64 | self.send_activity_stream_checkbox.setEnabled(False) 65 | self.send_activity_stream_checkbox.setChecked(False) 66 | self.send_activity_stream_checkbox.setObjectName( 67 | "send_activity_stream_checkbox" 68 | ) 69 | self.verticalLayout.addWidget(self.send_activity_stream_checkbox) 70 | spacerItem = QtWidgets.QSpacerItem( 71 | 20, 218, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding 72 | ) 73 | self.verticalLayout.addItem(spacerItem) 74 | self.dock_widget.setWidget(self.dock_widget_contents) 75 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.dock_widget) 76 | 77 | self.retranslateUi(MainWindow) 78 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 79 | 80 | def retranslateUi(self, MainWindow): 81 | _translate = QtCore.QCoreApplication.translate 82 | MainWindow.setWindowTitle(_translate("MainWindow", "PyTrees Viewer")) 83 | self.web_view_group_box.setTitle(_translate("MainWindow", "Tree View")) 84 | self.send_button.setText(_translate("MainWindow", "Send Tree")) 85 | self.screenshot_button.setText(_translate("MainWindow", "Screenshot")) 86 | self.send_blackboard_data_checkbox.setText( 87 | _translate("MainWindow", "Send Blackboard Data") 88 | ) 89 | self.send_activity_stream_checkbox.setText( 90 | _translate("MainWindow", "Send Activity Stream") 91 | ) 92 | 93 | 94 | from py_trees_js.viewer.web_view import WebViewGroupBox 95 | from . import images_rc 96 | -------------------------------------------------------------------------------- /py_trees_js/viewer/trees.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License: BSD 5 | # https://github.com/splintered-reality/py_trees_js/raw/devel/LICENSE 6 | # 7 | ############################################################################## 8 | # Documentation 9 | ############################################################################## 10 | 11 | """Demo trees to feed to the web app.""" 12 | 13 | ############################################################################## 14 | # Imports 15 | ############################################################################## 16 | 17 | import copy 18 | import typing 19 | 20 | ############################################################################## 21 | # Methods 22 | ############################################################################## 23 | 24 | 25 | def create_demo_tree_definition(): 26 | """Demo tree.""" 27 | tree = { 28 | "changed": "true", 29 | "timestamp": 1563938995, 30 | "visited_path": ["1", "2", "7"], 31 | "behaviours": { 32 | "1": { 33 | "id": "1", 34 | "status": "RUNNING", 35 | "name": "Selector", 36 | "colour": "#00FFFF", 37 | "children": ["2", "3", "4", "6"], 38 | "data": { 39 | "Type": "py_trees.composites.Selector", 40 | "Feedback": "Decision maker", 41 | }, 42 | }, 43 | "2": { 44 | "id": "2", 45 | "status": "RUNNING", 46 | "name": "Sequence", 47 | "colour": "#FFA500", 48 | "children": ["7", "8", "9"], 49 | "data": {"Type": "py_trees.composites.Sequence", "Feedback": "Worker"}, 50 | }, 51 | "3": { 52 | "id": "3", 53 | "status": "INVALID", 54 | "name": "Parallel", 55 | "details": "SuccessOnOne", 56 | "colour": "#FFFF00", 57 | "children": ["10", "11"], 58 | "data": { 59 | "Type": "py_trees.composites.Parallel", 60 | "Feedback": "Baked beans is good for your heart, baked beans makes you", 61 | }, 62 | }, 63 | "4": { 64 | "id": "4", 65 | "status": "RUNNING", 66 | "name": "̂ ̂ Decorator", 67 | "colour": "#DDDDDD", 68 | "children": ["5"], 69 | "data": { 70 | "Type": "py_trees.composites.Decorator", 71 | "Feedback": "Wearing the hats", 72 | }, 73 | }, 74 | "5": { 75 | "id": "5", 76 | "status": "INVALID", 77 | "name": "Decorated Beyond The Beliefs of an Agnostic Rhino", 78 | "colour": "#555555", 79 | "data": {"Type": "py_trees.composites.Behaviour", "Feedback": "...."}, 80 | }, 81 | "6": { 82 | "id": "6", 83 | "status": "INVALID", 84 | "name": "Behaviour", 85 | "colour": "#555555", 86 | "data": {"Type": "py_trees.composites.Behaviour", "Feedback": "..."}, 87 | }, 88 | "7": { 89 | "id": "7", 90 | "status": "RUNNING", 91 | "name": "Worker A", 92 | "colour": "#555555", 93 | "data": { 94 | "Type": "py_trees.composites.Behaviour", 95 | "Feedback": "...", 96 | "Blackboard": ["/state/worker_a (x)"], 97 | }, 98 | }, 99 | "8": { 100 | "id": "8", 101 | "status": "INVALID", 102 | "name": "Worker B", 103 | "colour": "#555555", 104 | "data": { 105 | "Type": "py_trees.composites.Behaviour", 106 | "Feedback": "...", 107 | "Blackboard": [ 108 | "/foobar (w)", 109 | "/state/worker_b (x)", 110 | ], 111 | }, 112 | }, 113 | "9": { 114 | "id": "9", 115 | "status": "INVALID", 116 | "name": "Worker C", 117 | "colour": "#555555", 118 | "data": { 119 | "Type": "py_trees.composites.Behaviour", 120 | "Feedback": "...", 121 | "Blackboard": [ 122 | "/foobar (r)", 123 | "/state/worker_c (x)", 124 | ], 125 | }, 126 | }, 127 | "10": { 128 | "id": "10", 129 | "status": "INVALID", 130 | "name": "Foo", 131 | "colour": "#555555", 132 | "data": {"Type": "py_trees.composites.Behaviour", "Feedback": "..."}, 133 | }, 134 | "11": { 135 | "id": "11", 136 | "status": "INVALID", 137 | "name": "Bar", 138 | "colour": "#555555", 139 | "data": { 140 | "Type": "py_trees.composites.Behaviour", 141 | "Feedback": "...", 142 | "Blackboard": [ 143 | "/foobar (r)", 144 | ], 145 | }, 146 | }, 147 | }, 148 | "blackboard": { 149 | "behaviours": { # key metadata per behaviour 150 | "7": {"/state/worker_a": "x"}, 151 | "8": { 152 | "/foobar": "w", 153 | "/state/worker_b": "x", 154 | }, 155 | "9": { 156 | "/foobar": "r", 157 | "/state/worker_c": "x", 158 | }, 159 | "11": { 160 | "/foobar": "r", 161 | }, 162 | }, 163 | "data": {}, # key-value store 164 | }, 165 | "activity": [], # list of xhtml snippets 166 | } 167 | return tree 168 | 169 | 170 | def generate_activity_timeline() -> typing.List[typing.List[str]]: 171 | """Generate activity feed.""" 172 | space = " " 173 | left_arrow = "" 174 | right_arrow = "" 175 | # left_right_arrow = '' 176 | reset = "" 177 | normal = "" 178 | cyan = '' 179 | green = '' 180 | yellow = '' 181 | # red = '' 182 | # monospace = '' 183 | # colon_separator = space + ":" + space 184 | # bar_separator = space + "|" + space 185 | activity = [ 186 | [ 187 | ( 188 | "/state/worker_a", 189 | "INITIALISED", 190 | "Worker A", 191 | right_arrow 192 | + space 193 | + "And his noodly appendage reached forth to tickle the blessed...", 194 | ) 195 | ], 196 | [ 197 | ("/foobar", "INITIALISED", "Worker B", right_arrow + space + "oi"), 198 | ("/state/worker_b", "INITIALISED", "Worker B", right_arrow + space + "5"), 199 | ], 200 | [("/state/worker_b", "WRITE", "Worker B", right_arrow + space + "6")], 201 | [ 202 | ("/foobar", "READ", "Bar", left_arrow + space + "oi"), 203 | ( 204 | "/state/worker_c", 205 | "INITIALISED", 206 | "Worker C", 207 | right_arrow + space + "boinked", 208 | ), 209 | ], 210 | ] 211 | formatted_activity = [] 212 | for snippets in activity: 213 | # formatted_activity.append(formatted_snippets) 214 | xhtml_snippet = "" 215 | for key, activity_type, client_name, info in snippets: 216 | xhtml_snippet += ( 217 | "" 218 | "" 219 | "" 220 | "" 225 | "" 226 | "" 227 | ) 228 | xhtml_snippet += "
" + cyan + key + reset + "" + yellow + activity_type + reset + "" 221 | + normal 222 | + client_name 223 | + reset 224 | + "" + green + info + reset + "
" 229 | formatted_activity.append([xhtml_snippet]) 230 | return formatted_activity 231 | 232 | 233 | def create_demo_tree_list(): 234 | """Create sequence of tree snapshots.""" 235 | activity_timeline = generate_activity_timeline() 236 | trees = [] 237 | tree = create_demo_tree_definition() 238 | tree["blackboard"]["data"][ 239 | "/state/worker_a" 240 | ] = "And his noodly appendage reached forth to tickle the blessed..." 241 | tree["activity"] = activity_timeline[0] 242 | trees.append(copy.deepcopy(tree)) 243 | # sequence progressed, but running 244 | tree["visited_path"] = ["1", "2", "7", "8"] 245 | tree["behaviours"]["7"]["status"] = "SUCCESS" # first worker 246 | tree["behaviours"]["8"]["status"] = "RUNNING" # middle worker 247 | tree["blackboard"]["data"][ 248 | "/foobar" 249 | ] = "oi" # TODO: flip to True, and fix dump/load problems 250 | tree["blackboard"]["data"]["/state/worker_b"] = 5 251 | tree["activity"] = activity_timeline[1] 252 | trees.append(copy.deepcopy(tree)) 253 | # tree not changed, only blackboard values 254 | tree["blackboard"]["data"]["/state/worker_b"] = 6 255 | tree["changed"] = "false" 256 | tree["activity"] = activity_timeline[2] 257 | trees.append(copy.deepcopy(tree)) 258 | # sequence failed 259 | tree["changed"] = "true" 260 | tree["visited_path"] = ["1", "2", "3", "4", "5", "8", "9", "10", "11"] 261 | tree["behaviours"]["2"]["status"] = "FAILURE" # sequence 262 | tree["behaviours"]["8"]["status"] = "SUCCESS" # middle worker 263 | tree["behaviours"]["9"]["status"] = "FAILURE" # final worker 264 | tree["behaviours"]["3"]["status"] = "SUCCESS" # parallel 265 | tree["behaviours"]["10"]["status"] = "SUCCESS" # first parallelised 266 | tree["behaviours"]["11"]["status"] = "RUNNING" # second parallelised 267 | tree["behaviours"]["4"]["status"] = "RUNNING" # decorator 268 | tree["behaviours"]["5"]["status"] = "RUNNING" # decorator child 269 | # del tree['blackboard']['data']['/state/worker_a'] 270 | tree["blackboard"]["data"]["/state/worker_c"] = "boinked" 271 | tree["activity"] = activity_timeline[3] 272 | trees.append(copy.deepcopy(tree)) 273 | return trees 274 | -------------------------------------------------------------------------------- /py_trees_js/viewer/viewer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License: BSD 5 | # https://github.com/splintered-reality/py_trees_js/raw/devel/LICENSE 6 | # 7 | ############################################################################## 8 | # Documentation 9 | ############################################################################## 10 | 11 | """A qt-javascript application for viewing executing or replaying py_trees.""" 12 | 13 | ############################################################################## 14 | # Imports 15 | ############################################################################## 16 | 17 | import copy 18 | import datetime 19 | import functools 20 | import os 21 | import signal 22 | import sys 23 | import time 24 | 25 | import PyQt5.QtCore as qt_core 26 | import PyQt5.QtWidgets as qt_widgets 27 | 28 | from . import console, main_window, trees 29 | 30 | ############################################################################## 31 | # Helpers 32 | ############################################################################## 33 | 34 | 35 | def send_tree_response(reply): 36 | """Confirm receipt of the tree by the js library.""" 37 | console.logdebug("[{}] reply: '{}' [viewer]".format(time.monotonic(), reply)) 38 | 39 | 40 | @qt_core.pyqtSlot() 41 | def send_tree(parameters, web_view_page, demo_trees, unused_checked): 42 | """Send a tree snapshot to the tree library.""" 43 | number_of_trees = len(demo_trees) 44 | tree = copy.deepcopy(demo_trees[send_tree.index]) 45 | tree["timestamp"] = time.time() 46 | # demo_trees[send_tree.index]['timestamp'] = time.time() 47 | console.logdebug( 48 | "[{}] send: tree '{}' [{}][viewer]".format( 49 | time.monotonic(), send_tree.index, tree["timestamp"] 50 | ) 51 | ) 52 | if not parameters.send_blackboard_data: 53 | del tree["blackboard"]["data"] 54 | if not parameters.send_activity_stream: 55 | del tree["activity"] 56 | javascript_command = "render_tree({{tree: {}}})".format(tree) 57 | web_view_page.runJavaScript(javascript_command, send_tree_response) 58 | send_tree.index = ( 59 | 0 if send_tree.index == (number_of_trees - 1) else send_tree.index + 1 60 | ) 61 | 62 | 63 | send_tree.index = 0 64 | 65 | 66 | @qt_core.pyqtSlot() 67 | def capture_screenshot(parent, web_engine_view, unused_checked): 68 | """Capture a screenshot.""" 69 | console.logdebug("captured screenshot [viewer]") 70 | file_dialog = qt_widgets.QFileDialog(parent) 71 | file_dialog.setNameFilters( 72 | ["BMP Files (*.bmp)", "JPEG Files (*.jpeg)", "PNG Files (*.png)"] 73 | ) 74 | file_dialog.selectNameFilter("PNG Files (*.png)") 75 | file_dialog.setDefaultSuffix((".png")) 76 | file_dialog.setAcceptMode(qt_widgets.QFileDialog.AcceptSave) 77 | # unfortunately creates a fair amount of spam on stdout 78 | # 'kf5.kio.core: Invalid URL: QUrl("screenshot.jpeg")' 79 | # 'kf5.kio.core: Invalid URL: QUrl("screenshot.png")' 80 | # but...it ain't broke 81 | file_dialog.selectFile( 82 | "screenshot_{}.png".format(datetime.datetime.now().strftime("%S%M%H%d%m%y")) 83 | ) 84 | _ = file_dialog.exec() 85 | # should be able to restrict it to one file? 86 | for filename in file_dialog.selectedFiles(): 87 | console.logdebug("capturing screenshot: {}".format(filename)) 88 | # This would be simpler, but you can't specify a default filename, nor suffix on linux... 89 | # filename, _ = qt_widgets.QFileDialog.getSaveFileName( 90 | # parent=parent, 91 | # caption="Export to Png", 92 | # directory="screenshot_{}.png".format(datetime.datetime.now().strftime("%S%M%H%d%m%y")), 93 | # # For multiple options, use ;;, e.g. 94 | # # 'All Files (*);;BMP Files (*.bmp);;JPEG Files (*.jpeg);;PNG Files (*.png)' 95 | # filter="BMP Files (*.bmp);;JPEG Files (*.jpeg);;PNG Files (*.png)", 96 | # initialFilter="PNG Files (*.png)", 97 | # options=options 98 | # ) 99 | # if filename: 100 | # console.loginfo("capturing screenshot: {}".format(filename)) 101 | extension = os.path.splitext(filename)[-1].upper() 102 | if filename.endswith(".png"): 103 | extension = b"PNG" 104 | elif filename.endswith(".bmp"): 105 | extension = b"BMP" 106 | elif filename.endswith(".jpeg"): 107 | extension = b"JPEG" 108 | else: 109 | extension = b"PNG" 110 | web_engine_view.grab().save(filename, extension) 111 | 112 | 113 | ############################################################################## 114 | # Main 115 | ############################################################################## 116 | 117 | 118 | def main(): 119 | """Entrypoint.""" 120 | # logging 121 | console.log_level = console.LogLevel.DEBUG 122 | 123 | # the players 124 | app = qt_widgets.QApplication(sys.argv) 125 | window = main_window.MainWindow() 126 | 127 | # sig interrupt handling 128 | # use a timer to get out of the gui thread and 129 | # permit python a chance to catch the signal 130 | # https://stackoverflow.com/questions/4938723/what-is-the-correct-way-to-make-my-pyqt-application-quit-when-killed-from-the-co 131 | def on_shutdown(unused_signal, unused_frame): 132 | console.logdebug("received interrupt signal [viewer]") 133 | window.close() 134 | 135 | signal.signal(signal.SIGINT, on_shutdown) 136 | timer = qt_core.QTimer() 137 | timer.timeout.connect(lambda: None) 138 | timer.start(250) 139 | 140 | # sigslots 141 | window.ui.send_button.clicked.connect( 142 | functools.partial( 143 | send_tree, 144 | window.parameters, 145 | window.ui.web_view_group_box.ui.web_engine_view.page(), 146 | trees.create_demo_tree_list(), 147 | ) 148 | ) 149 | window.ui.screenshot_button.clicked.connect( 150 | functools.partial( 151 | capture_screenshot, 152 | window, 153 | window.ui.web_view_group_box.ui.web_engine_view, 154 | ) 155 | ) 156 | 157 | # qt bringup 158 | window.show() 159 | result = app.exec_() 160 | 161 | # shutdown 162 | sys.exit(result) 163 | -------------------------------------------------------------------------------- /py_trees_js/viewer/web_app.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | html/index.html 4 | 5 | 6 | -------------------------------------------------------------------------------- /py_trees_js/viewer/web_app_rc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.9.5) 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore 10 | 11 | qt_resource_data = b"\ 12 | \x00\x00\x09\x86\ 13 | \x3c\ 14 | \x21\x64\x6f\x63\x74\x79\x70\x65\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\ 15 | \x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x20\x20\x3c\ 16 | \x6d\x65\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\ 17 | \x66\x2d\x38\x22\x3e\x0a\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x50\ 18 | \x79\x54\x72\x65\x65\x73\x20\x56\x69\x65\x77\x65\x72\x3c\x2f\x74\ 19 | \x69\x74\x6c\x65\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x6b\x20\x72\x65\ 20 | \x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\x65\x74\x22\x20\x68\ 21 | \x72\x65\x66\x3d\x22\x6a\x73\x2f\x70\x79\x5f\x74\x72\x65\x65\x73\ 22 | \x2d\x30\x2e\x36\x2e\x63\x73\x73\x22\x2f\x3e\x0a\x20\x20\x3c\x6c\ 23 | \x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\ 24 | \x65\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\ 25 | \x63\x73\x73\x22\x20\x68\x72\x65\x66\x3d\x22\x6a\x73\x2f\x6a\x6f\ 26 | \x69\x6e\x74\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\x2d\x33\x2e\x30\x2e\ 27 | \x34\x2e\x6d\x69\x6e\x2e\x63\x73\x73\x22\x2f\x3e\x0a\x20\x20\x3c\ 28 | \x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\x3d\x22\x6a\x73\x2f\x6a\ 29 | \x6f\x69\x6e\x74\x6a\x73\x2f\x64\x61\x67\x72\x65\x2d\x30\x2e\x38\ 30 | \x2e\x34\x2e\x6d\x69\x6e\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\ 31 | \x69\x70\x74\x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x73\ 32 | \x72\x63\x3d\x22\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\x6a\x73\x2f\x67\ 33 | \x72\x61\x70\x68\x6c\x69\x62\x2d\x32\x2e\x31\x2e\x37\x2e\x6d\x69\ 34 | \x6e\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\ 35 | \x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\x3d\x22\x6a\ 36 | \x73\x2f\x6a\x6f\x69\x6e\x74\x6a\x73\x2f\x6a\x71\x75\x65\x72\x79\ 37 | \x2d\x33\x2e\x34\x2e\x31\x2e\x6d\x69\x6e\x2e\x6a\x73\x22\x3e\x3c\ 38 | \x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\ 39 | \x70\x74\x20\x73\x72\x63\x3d\x22\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\ 40 | \x6a\x73\x2f\x6c\x6f\x64\x61\x73\x68\x2d\x34\x2e\x31\x37\x2e\x31\ 41 | \x31\x2e\x6d\x69\x6e\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\ 42 | \x70\x74\x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\ 43 | \x63\x3d\x22\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\x6a\x73\x2f\x62\x61\ 44 | \x63\x6b\x62\x6f\x6e\x65\x2d\x31\x2e\x34\x2e\x30\x2e\x6a\x73\x22\ 45 | \x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x20\x20\x3c\x73\x63\ 46 | \x72\x69\x70\x74\x20\x73\x72\x63\x3d\x22\x6a\x73\x2f\x6a\x6f\x69\ 47 | \x6e\x74\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\x2d\x33\x2e\x30\x2e\x34\ 48 | \x2e\x6d\x69\x6e\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\ 49 | \x74\x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\ 50 | \x3d\x22\x6a\x73\x2f\x70\x79\x5f\x74\x72\x65\x65\x73\x2d\x30\x2e\ 51 | \x36\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\ 52 | \x20\x20\x3c\x21\x2d\x2d\x20\x57\x65\x62\x20\x61\x70\x70\x20\x69\ 53 | \x6e\x74\x65\x67\x72\x61\x74\x69\x6f\x6e\x20\x63\x73\x73\x20\x68\ 54 | \x65\x72\x65\x20\x2d\x2d\x3e\x0a\x20\x20\x3c\x73\x74\x79\x6c\x65\ 55 | \x3e\x0a\x20\x20\x20\x20\x68\x74\x6d\x6c\x20\x7b\x0a\x20\x20\x20\ 56 | \x20\x20\x20\x68\x65\x69\x67\x68\x74\x3a\x20\x31\x30\x30\x25\x20\ 57 | \x20\x2f\x2a\x20\x63\x61\x6e\x76\x61\x73\x20\x69\x73\x20\x69\x6e\ 58 | \x74\x65\x6e\x64\x65\x64\x20\x74\x6f\x20\x66\x69\x6c\x6c\x20\x74\ 59 | \x68\x65\x20\x73\x63\x72\x65\x65\x6e\x2c\x20\x63\x61\x73\x63\x61\ 60 | \x64\x69\x6e\x67\x20\x68\x65\x69\x67\x68\x74\x73\x20\x61\x63\x68\ 61 | \x69\x65\x76\x65\x73\x20\x74\x68\x69\x73\x20\x2a\x2f\x0a\x20\x20\ 62 | \x20\x20\x7d\x0a\x20\x20\x20\x20\x62\x6f\x64\x79\x20\x7b\x0a\x20\ 63 | \x20\x20\x20\x20\x20\x6d\x61\x72\x67\x69\x6e\x3a\x20\x30\x3b\x0a\ 64 | \x20\x20\x20\x20\x20\x20\x6f\x76\x65\x72\x66\x6c\x6f\x77\x3a\x68\ 65 | \x69\x64\x64\x65\x6e\x3b\x20\x20\x2f\x2a\x20\x6e\x6f\x20\x73\x63\ 66 | \x72\x6f\x6c\x6c\x62\x61\x72\x73\x20\x2a\x2f\x0a\x20\x20\x20\x20\ 67 | \x20\x20\x68\x65\x69\x67\x68\x74\x3a\x20\x31\x30\x30\x25\x20\x20\ 68 | \x2f\x2a\x20\x63\x61\x6e\x76\x61\x73\x20\x69\x73\x20\x69\x6e\x74\ 69 | \x65\x6e\x64\x65\x64\x20\x74\x6f\x20\x66\x69\x6c\x6c\x20\x74\x68\ 70 | \x65\x20\x73\x63\x72\x65\x65\x6e\x2c\x20\x63\x61\x73\x63\x61\x64\ 71 | \x69\x6e\x67\x20\x68\x65\x69\x67\x68\x74\x73\x20\x61\x63\x68\x69\ 72 | \x65\x76\x65\x73\x20\x74\x68\x69\x73\x20\x2a\x2f\x0a\x20\x20\x20\ 73 | \x20\x7d\x0a\x20\x20\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x0a\x3c\x2f\ 74 | \x68\x65\x61\x64\x3e\x0a\x3c\x62\x6f\x64\x79\x3e\x0a\x20\x20\x3c\ 75 | \x73\x63\x72\x69\x70\x74\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\ 76 | \x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x3e\x0a\x20\ 77 | \x20\x20\x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x68\x65\x6c\x6c\ 78 | \x6f\x28\x29\x0a\x20\x20\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\ 79 | \x20\x20\x3c\x64\x69\x76\x20\x69\x64\x3d\x22\x63\x61\x6e\x76\x61\ 80 | \x73\x22\x3e\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x3c\x64\x69\x76\ 81 | \x20\x69\x64\x3d\x22\x74\x69\x6d\x65\x6c\x69\x6e\x65\x22\x3e\x3c\ 82 | \x2f\x64\x69\x76\x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\ 83 | \x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\ 84 | \x63\x72\x69\x70\x74\x22\x3e\x0a\x20\x20\x20\x20\x2f\x2f\x20\x72\ 85 | \x65\x6e\x64\x65\x72\x69\x6e\x67\x20\x63\x61\x6e\x76\x61\x73\x0a\ 86 | \x20\x20\x20\x20\x63\x61\x6e\x76\x61\x73\x5f\x67\x72\x61\x70\x68\ 87 | \x20\x3d\x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x63\x61\x6e\x76\ 88 | \x61\x73\x2e\x63\x72\x65\x61\x74\x65\x5f\x67\x72\x61\x70\x68\x28\ 89 | \x29\x0a\x20\x20\x20\x20\x63\x61\x6e\x76\x61\x73\x5f\x70\x61\x70\ 90 | \x65\x72\x20\x3d\x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x63\x61\ 91 | \x6e\x76\x61\x73\x2e\x63\x72\x65\x61\x74\x65\x5f\x70\x61\x70\x65\ 92 | \x72\x28\x7b\x67\x72\x61\x70\x68\x3a\x20\x63\x61\x6e\x76\x61\x73\ 93 | \x5f\x67\x72\x61\x70\x68\x7d\x29\x0a\x0a\x20\x20\x20\x20\x2f\x2f\ 94 | \x20\x65\x76\x65\x6e\x74\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x0a\ 95 | \x20\x20\x20\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x5f\x67\x72\x61\ 96 | \x70\x68\x20\x3d\x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x74\x69\ 97 | \x6d\x65\x6c\x69\x6e\x65\x2e\x63\x72\x65\x61\x74\x65\x5f\x67\x72\ 98 | \x61\x70\x68\x28\x7b\x65\x76\x65\x6e\x74\x5f\x63\x61\x63\x68\x65\ 99 | \x5f\x6c\x69\x6d\x69\x74\x3a\x20\x31\x30\x30\x7d\x29\x3b\x0a\x20\ 100 | \x20\x20\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x5f\x70\x61\x70\x65\ 101 | \x72\x20\x3d\x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x74\x69\x6d\ 102 | \x65\x6c\x69\x6e\x65\x2e\x63\x72\x65\x61\x74\x65\x5f\x70\x61\x70\ 103 | \x65\x72\x28\x7b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x74\x69\x6d\ 104 | \x65\x6c\x69\x6e\x65\x5f\x67\x72\x61\x70\x68\x3a\x20\x74\x69\x6d\ 105 | \x65\x6c\x69\x6e\x65\x5f\x67\x72\x61\x70\x68\x2c\x0a\x20\x20\x20\ 106 | \x20\x20\x20\x20\x20\x63\x61\x6e\x76\x61\x73\x5f\x67\x72\x61\x70\ 107 | \x68\x3a\x20\x63\x61\x6e\x76\x61\x73\x5f\x67\x72\x61\x70\x68\x2c\ 108 | \x0a\x20\x20\x20\x20\x20\x20\x20\x20\x63\x61\x6e\x76\x61\x73\x5f\ 109 | \x70\x61\x70\x65\x72\x3a\x20\x63\x61\x6e\x76\x61\x73\x5f\x70\x61\ 110 | \x70\x65\x72\x2c\x0a\x20\x20\x20\x20\x7d\x29\x0a\x0a\x20\x20\x20\ 111 | \x20\x2f\x2f\x20\x72\x65\x61\x63\x74\x20\x74\x6f\x20\x77\x69\x6e\ 112 | \x64\x6f\x77\x20\x72\x65\x73\x69\x7a\x69\x6e\x67\x20\x65\x76\x65\ 113 | \x6e\x74\x73\x0a\x20\x20\x20\x20\x24\x28\x77\x69\x6e\x64\x6f\x77\ 114 | \x29\x2e\x72\x65\x73\x69\x7a\x65\x28\x66\x75\x6e\x63\x74\x69\x6f\ 115 | \x6e\x28\x29\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\x70\x79\x5f\x74\ 116 | \x72\x65\x65\x73\x2e\x63\x61\x6e\x76\x61\x73\x2e\x6f\x6e\x5f\x77\ 117 | \x69\x6e\x64\x6f\x77\x5f\x72\x65\x73\x69\x7a\x65\x28\x63\x61\x6e\ 118 | \x76\x61\x73\x5f\x70\x61\x70\x65\x72\x29\x0a\x20\x20\x20\x20\x20\ 119 | \x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x74\x69\x6d\x65\x6c\x69\ 120 | \x6e\x65\x2e\x6f\x6e\x5f\x77\x69\x6e\x64\x6f\x77\x5f\x72\x65\x73\ 121 | \x69\x7a\x65\x28\x74\x69\x6d\x65\x6c\x69\x6e\x65\x5f\x70\x61\x70\ 122 | \x65\x72\x29\x0a\x20\x20\x20\x20\x7d\x29\x0a\x0a\x20\x20\x20\x20\ 123 | \x72\x65\x6e\x64\x65\x72\x5f\x74\x72\x65\x65\x20\x3d\x20\x66\x75\ 124 | \x6e\x63\x74\x69\x6f\x6e\x28\x7b\x74\x72\x65\x65\x7d\x29\x20\x7b\ 125 | \x0a\x20\x20\x20\x20\x20\x20\x2f\x2f\x20\x69\x66\x20\x74\x68\x65\ 126 | \x72\x65\x20\x69\x73\x20\x6e\x6f\x20\x74\x69\x6d\x65\x6c\x69\x6e\ 127 | \x65\x0a\x20\x20\x20\x20\x20\x20\x2f\x2a\x0a\x20\x20\x20\x20\x20\ 128 | \x20\x76\x61\x72\x20\x67\x72\x61\x70\x68\x5f\x63\x68\x61\x6e\x67\ 129 | \x65\x64\x20\x3d\x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x63\x61\ 130 | \x6e\x76\x61\x73\x2e\x75\x70\x64\x61\x74\x65\x5f\x67\x72\x61\x70\ 131 | \x68\x28\x7b\x67\x72\x61\x70\x68\x3a\x20\x63\x61\x6e\x76\x61\x73\ 132 | \x5f\x67\x72\x61\x70\x68\x2c\x20\x74\x72\x65\x65\x3a\x20\x74\x72\ 133 | \x65\x65\x7d\x29\x0a\x20\x20\x20\x20\x20\x20\x69\x66\x20\x28\x20\ 134 | \x67\x72\x61\x70\x68\x5f\x63\x68\x61\x6e\x67\x65\x64\x20\x29\x20\ 135 | \x7b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x70\x79\x5f\x74\ 136 | \x72\x65\x65\x73\x2e\x63\x61\x6e\x76\x61\x73\x2e\x6c\x61\x79\x6f\ 137 | \x75\x74\x5f\x67\x72\x61\x70\x68\x28\x7b\x67\x72\x61\x70\x68\x3a\ 138 | \x20\x63\x61\x6e\x76\x61\x73\x5f\x67\x72\x61\x70\x68\x7d\x29\x0a\ 139 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x69\x66\x20\x28\x20\x63\ 140 | \x61\x6e\x76\x61\x73\x5f\x67\x72\x61\x70\x68\x2e\x67\x65\x74\x28\ 141 | \x27\x73\x63\x61\x6c\x65\x5f\x63\x6f\x6e\x74\x65\x6e\x74\x5f\x74\ 142 | \x6f\x5f\x66\x69\x74\x27\x29\x20\x29\x20\x7b\x0a\x20\x20\x20\x20\ 143 | \x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x70\x79\x5f\x74\x72\x65\ 144 | \x65\x73\x2e\x63\x61\x6e\x76\x61\x73\x2e\x73\x63\x61\x6c\x65\x5f\ 145 | \x63\x6f\x6e\x74\x65\x6e\x74\x5f\x74\x6f\x5f\x66\x69\x74\x28\x63\ 146 | \x61\x6e\x76\x61\x73\x5f\x70\x61\x70\x65\x72\x29\x0a\x20\x20\x20\ 147 | \x20\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x20\x20\x7d\ 148 | \x0a\x20\x20\x20\x20\x20\x20\x2a\x2f\x0a\x0a\x20\x20\x20\x20\x20\ 149 | \x20\x2f\x2f\x20\x69\x66\x20\x74\x68\x65\x72\x65\x20\x69\x73\x20\ 150 | \x61\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x0a\x20\x20\x20\x20\x20\ 151 | \x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x74\x69\x6d\x65\x6c\x69\ 152 | \x6e\x65\x2e\x61\x64\x64\x5f\x74\x72\x65\x65\x5f\x74\x6f\x5f\x63\ 153 | \x61\x63\x68\x65\x28\x7b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ 154 | \x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x5f\x67\x72\x61\x70\x68\x3a\ 155 | \x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x5f\x67\x72\x61\x70\x68\x2c\ 156 | \x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x63\x61\x6e\x76\x61\ 157 | \x73\x5f\x67\x72\x61\x70\x68\x3a\x20\x63\x61\x6e\x76\x61\x73\x5f\ 158 | \x67\x72\x61\x70\x68\x2c\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ 159 | \x20\x63\x61\x6e\x76\x61\x73\x5f\x70\x61\x70\x65\x72\x3a\x20\x63\ 160 | \x61\x6e\x76\x61\x73\x5f\x70\x61\x70\x65\x72\x2c\x0a\x20\x20\x20\ 161 | \x20\x20\x20\x20\x20\x20\x20\x74\x72\x65\x65\x3a\x20\x74\x72\x65\ 162 | \x65\x0a\x20\x20\x20\x20\x20\x20\x7d\x29\x0a\x20\x20\x20\x20\x20\ 163 | \x20\x72\x65\x74\x75\x72\x6e\x20\x22\x72\x65\x6e\x64\x65\x72\x65\ 164 | \x64\x22\x0a\x20\x20\x20\x20\x7d\x0a\x20\x20\x3c\x2f\x73\x63\x72\ 165 | \x69\x70\x74\x3e\x0a\x3c\x2f\x62\x6f\x64\x79\x3e\x0a\x3c\x2f\x68\ 166 | \x74\x6d\x6c\x3e\x0a\ 167 | " 168 | 169 | qt_resource_name = b"\ 170 | \x00\x0a\ 171 | \x0c\xba\xf2\x7c\ 172 | \x00\x69\ 173 | \x00\x6e\x00\x64\x00\x65\x00\x78\x00\x2e\x00\x68\x00\x74\x00\x6d\x00\x6c\ 174 | " 175 | 176 | qt_resource_struct_v1 = b"\ 177 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 178 | \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 179 | " 180 | 181 | qt_resource_struct_v2 = b"\ 182 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 183 | \x00\x00\x00\x00\x00\x00\x00\x00\ 184 | \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 185 | \x00\x00\x01\x6f\x4d\x6c\xf1\xdf\ 186 | " 187 | 188 | qt_version = QtCore.qVersion().split(".") 189 | if qt_version < ["5", "8", "0"]: 190 | rcc_version = 1 191 | qt_resource_struct = qt_resource_struct_v1 192 | else: 193 | rcc_version = 2 194 | qt_resource_struct = qt_resource_struct_v2 195 | 196 | 197 | def qInitResources(): 198 | QtCore.qRegisterResourceData( 199 | rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data 200 | ) 201 | 202 | 203 | def qCleanupResources(): 204 | QtCore.qUnregisterResourceData( 205 | rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data 206 | ) 207 | 208 | 209 | qInitResources() 210 | -------------------------------------------------------------------------------- /py_trees_js/viewer/web_view.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License: BSD 5 | # https://github.com/splintered-reality/py_trees_js/raw/devel/LICENSE 6 | # 7 | ############################################################################## 8 | # Documentation 9 | ############################################################################## 10 | 11 | """Launch a qt dashboard for the tutorials.""" 12 | 13 | ############################################################################## 14 | # Imports 15 | ############################################################################## 16 | 17 | import PyQt5.QtCore as qt_core 18 | import PyQt5.QtWidgets as qt_widgets 19 | 20 | # Import the javascript libraries (via qrc bundles) 21 | import py_trees_js.resources # noqa: F401 22 | 23 | from . import web_view_ui 24 | 25 | ############################################################################## 26 | # Helpers 27 | ############################################################################## 28 | 29 | 30 | class WebViewGroupBox(qt_widgets.QGroupBox): 31 | """Promotable class (in Designer) for the web view group box.""" 32 | 33 | change_battery_percentage = qt_core.pyqtSignal( 34 | float, name="changeBatteryPercentage" 35 | ) 36 | change_battery_charging_status = qt_core.pyqtSignal( 37 | bool, name="changeBatteryChargingStatus" 38 | ) 39 | change_safety_sensors_enabled = qt_core.pyqtSignal( 40 | bool, name="safetySensorsEnabled" 41 | ) 42 | 43 | def __init__(self, parent): 44 | super().__init__(parent) 45 | self.ui = web_view_ui.Ui_WebViewGroupBox() 46 | self.ui.setupUi(self) 47 | # Currently auto-loading the web app (index.html) from the url setting 48 | # via Designer, so everything is self-contained in Ui_WebViewGroupBox setup. 49 | # 50 | # Alternatively, set the dynamic property for the 51 | # WebEngineView's URL to about:blank and load manually: 52 | # 53 | # self.ui.web_engine_view.load(qt_core.QUrl("qrc:/index.html")) 54 | -------------------------------------------------------------------------------- /py_trees_js/viewer/web_view.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebViewGroupBox 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Tree View 15 | 16 | 17 | GroupBox 18 | 19 | 20 | 21 | 22 | 23 | 24 | qrc:/index.html 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | QWebEngineView 34 | QWidget 35 |
QtWebEngineWidgets/QWebEngineView
36 |
37 |
38 | 39 | 40 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /py_trees_js/viewer/web_view_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'web_view.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.10.1 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_WebViewGroupBox(object): 13 | def setupUi(self, WebViewGroupBox): 14 | WebViewGroupBox.setObjectName("WebViewGroupBox") 15 | WebViewGroupBox.resize(400, 300) 16 | self.web_view_layout = QtWidgets.QVBoxLayout(WebViewGroupBox) 17 | self.web_view_layout.setObjectName("web_view_layout") 18 | self.web_engine_view = QtWebEngineWidgets.QWebEngineView(WebViewGroupBox) 19 | self.web_engine_view.setUrl(QtCore.QUrl("qrc:/index.html")) 20 | self.web_engine_view.setObjectName("web_engine_view") 21 | self.web_view_layout.addWidget(self.web_engine_view) 22 | 23 | self.retranslateUi(WebViewGroupBox) 24 | QtCore.QMetaObject.connectSlotsByName(WebViewGroupBox) 25 | 26 | def retranslateUi(self, WebViewGroupBox): 27 | _translate = QtCore.QCoreApplication.translate 28 | WebViewGroupBox.setWindowTitle(_translate("WebViewGroupBox", "Tree View")) 29 | WebViewGroupBox.setTitle(_translate("WebViewGroupBox", "GroupBox")) 30 | 31 | 32 | from PyQt5 import QtWebEngineWidgets 33 | 34 | from . import web_app_rc 35 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "py_trees_js" 3 | version = "0.6.6" 4 | # version needs to be updated in several places 5 | # package.xml 6 | # pyproject.toml 7 | # py_trees-.js (and version variable therein) 8 | # py_trees- 9 | # py_trees_js/viewer/html/index.html 10 | # py_trees_js/resources.qrc 11 | # setup.py 12 | description = "javascript libraries for visualising behaviour trees" 13 | authors = ["Daniel Stonier"] 14 | maintainers = ["Daniel Stonier "] 15 | readme = "README.md" 16 | license = "BSD" 17 | homepage = "https://github.com/splintered-reality/py_trees_js" 18 | repository = "https://github.com/splintered-reality/py_trees_js" 19 | documentation = "https://github.com/splintered-reality/py_trees_js" 20 | packages = [ 21 | { include = "py_trees_js" }, 22 | ] 23 | include = [ 24 | "py_trees_js/viewer/*.ui", 25 | "py_trees_js/viewer/html/*", 26 | "py_trees_js/viewer/images/*" 27 | ] 28 | classifiers = [ 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: BSD License', 31 | 'Programming Language :: Python', 32 | 'Topic :: Scientific/Engineering :: Artificial Intelligence', 33 | 'Topic :: Software Development :: Libraries' 34 | ] 35 | keywords=["py_trees", "py-trees", "behaviour-trees"] 36 | 37 | [tool.poetry.dependencies] 38 | python = "^3.8" 39 | pyqt5 = "~5.14" 40 | pyqtwebengine = "~5.14" 41 | 42 | [tool.poetry.group.dev.dependencies] 43 | tox = ">=3.26" 44 | tox-poetry-installer = {extras = ["poetry"], version = ">=0.9.0"} 45 | pytest = [ 46 | { version = ">=7.1", python = "^3.7" } 47 | ] 48 | pytest-console-scripts = ">=1.3" 49 | pytest-cov = ">=3.0.0" # transitively depends on coverage[toml] 50 | 51 | [tool.poetry.group.format.dependencies] 52 | ufmt = ">=2.0" # black (style) + usort (import order) 53 | 54 | [tool.poetry.group.static.dependencies] 55 | mypy = ">=0.991" 56 | 57 | [tool.poetry.group.lint.dependencies] 58 | # strongly recommended 59 | flake8 = ">=5.0" # combines pyflakes (errors) & pycodestyle (pep8 style) 60 | flake8-docstrings = ">=1.6" # docstrings (integrates pydocstyle) 61 | darglint = ">=1.8" # checks docstrings match implementation 62 | # optional, these go above and beyond 63 | flake8-bugbear = ">=22.9" # bugs & design not strictly pep8 64 | 65 | [tool.poetry.scripts] 66 | py-trees-demo-viewer = "py_trees_js.viewer.viewer:main" 67 | 68 | [build-system] 69 | requires = ["poetry-core>=1.0.0"] 70 | build-backend = "poetry.core.masonry.api" 71 | -------------------------------------------------------------------------------- /scripts/py-trees-devel-viewer: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple script to make sure the developer console is open and 4 | # the qrc's for the html/js are generated. 5 | 6 | ########################### 7 | # Environment 8 | ########################### 9 | 10 | PWD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 11 | export QTWEBENGINE_REMOTE_DEBUGGING=127.0.0.1:12345 12 | DIR=/tmp/chrome-devel-configuration 13 | 14 | ########################### 15 | # Generate Resources 16 | ########################### 17 | 18 | cd ${PWD}/../py_trees_js && ./gen.bash 19 | cd ${PWD}/../py_trees_js/viewer && ./gen.bash 20 | 21 | ########################### 22 | # Setup 23 | ########################### 24 | 25 | # Clear out the old user data 26 | rm -rf ${DIR} 27 | 28 | # Sync'ing to a user directory doesn't seem to be necessary if I use $OPTIONS1 29 | 30 | #if [ ! -d "${DIR}" ]; then 31 | # mkdir -p ${DIR} 32 | # rsync -av --delete --exclude=/Singleton* --exclude=/Session* ~/.config/google-chrome/ ${DIR} 33 | #fi 34 | 35 | ########################### 36 | # Launch 37 | ########################### 38 | OPTIONS1="--no-first-run --activate-on-launch --no-default-browser-check --allow-file-access-from-files" 39 | # These are to work around chrome 80+ incompatibility: 40 | # https://stackoverflow.com/questions/60182668/chrome-devtools-inspector-showing-blank-white-screen-while-debugging-with-samsun 41 | OPTIONS2="--enable-blink-features=ShadowDOMV0 --enable-blink-features=CustomElementsV0" 42 | google-chrome ${OPTIONS1} ${OPTIONS2} --user-data-dir=${DIR} --app=http://${QTWEBENGINE_REMOTE_DEBUGGING} > /dev/null 2>&1 & 43 | pid=$! 44 | 45 | py-trees-demo-viewer 46 | 47 | kill -s 9 $pid > /dev/null 2>&1 || exit 0 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ################################################################################ 4 | # This is a minimal setup.py for enabling ROS builds. 5 | # 6 | # For all other modes of development, use poetry and pyproject.toml 7 | ################################################################################ 8 | 9 | import os 10 | 11 | from setuptools import find_packages, setup 12 | 13 | 14 | def gather_js_files(): 15 | data_files = [] 16 | for root, unused_subdirs, files in os.walk("js"): 17 | destination = os.path.join("share", package_name, root) 18 | js_files = [] 19 | for file in files: 20 | pathname = os.path.join(root, file) 21 | js_files.append(pathname) 22 | data_files.append((destination, js_files)) 23 | return data_files 24 | 25 | 26 | package_name = "py_trees_js" 27 | 28 | setup( 29 | name=package_name, 30 | # version needs to be updated in several places 31 | # package.xml 32 | # pyproject.toml 33 | # py_trees-.js (and version variable therein) 34 | # py_trees- 35 | # py_trees_js/viewer/html/index.html 36 | # py_trees_js/resources.qrc 37 | version="0.6.6", 38 | packages=find_packages(exclude=["tests*", "docs*"]), 39 | data_files=[("share/" + package_name, ["package.xml"])] + gather_js_files(), 40 | package_data={"py_trees_js": ["viewer/*.ui", "viewer/html/*", "viewer/images/*"]}, 41 | author="Daniel Stonier", 42 | maintainer="Daniel Stonier ", 43 | url="https://github.com/splintered-reality/py_trees_js", 44 | keywords=["ROS", "ROS2", "behaviour-trees", "Qt"], 45 | zip_safe=True, 46 | classifiers=[ 47 | "Intended Audience :: Developers", 48 | "License :: OSI Approved :: BSD License", 49 | "Programming Language :: Python", 50 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 51 | "Topic :: Software Development :: Libraries", 52 | ], 53 | description=( 54 | "Javascript libraries for visualising executing or log-replayed behaviour trees" 55 | ), 56 | long_description=( 57 | "Javascript libraries for visualising executing or log-replayed behaviour trees." 58 | "Includes a qt-js hybrid viewer for development and demonstration purposes." 59 | ), 60 | license="BSD", 61 | entry_points={ 62 | "console_scripts": [ 63 | "py-trees-demo-viewer = py_trees_js.viewer.viewer:main", 64 | ], 65 | }, 66 | ) 67 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | ## Setup 4 | 5 | Make sure you run `poetry install` from the root folder of the package. 6 | 7 | ## Local Usage 8 | 9 | ``` 10 | # No stdout 11 | $ poetry run pytest 12 | 13 | # With stdout 14 | $ poetry run pytest -s 15 | ``` 16 | 17 | or in the poetry shell 18 | 19 | ``` 20 | $ poetry shell 21 | (e) $ cd tests && pytest -s 22 | (e) exit 23 | ``` 24 | 25 | ## Tox 26 | 27 | ``` 28 | $ tox -e py38 # tests + coverage 29 | $ tox -e format # formats with ufmt 30 | $ tox -e check # formatting, linting and mypy checkers 31 | ``` 32 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import unittest 3 | 4 | 5 | class ImportTest(unittest.TestCase): 6 | def test_import(self) -> None: 7 | """ 8 | This test serves to make the buildfarm happy in Python 3.12 and later. 9 | See https://github.com/colcon/colcon-core/issues/678 for more information. 10 | """ 11 | assert importlib.util.find_spec("py_trees_js") 12 | -------------------------------------------------------------------------------- /tests/test_launch.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://github.com/splintered-reality/py_trees_js/raw/devel/LICENSE 4 | # 5 | ############################################################################## 6 | # Documentation 7 | ############################################################################## 8 | 9 | """PyTrees javascript libraries and development/demo qt-js hybrid viewer.""" 10 | 11 | ############################################################################## 12 | # Tests 13 | # 14 | # TODO: Disable ignore on script_runner: pytest_console_scripts.ScriptRunner 15 | # 16 | # Q: Are all pytest plugins undiscoverable by mypy? 17 | ############################################################################## 18 | 19 | # The venv tox creates isn't sufficent for qwebengine's opengl requirements. 20 | # def test_launch(script_runner) -> None: # type: ignore[no-untyped-def] 21 | # ret = script_runner.run("py-trees-demo-viewer") 22 | # assert ret.success 23 | 24 | 25 | def test_launch() -> None: 26 | assert True 27 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Tox Configuration 3 | ################################################################################ 4 | 5 | [constants] 6 | source_locations = 7 | py_trees_js 8 | tests 9 | 10 | [tox] 11 | envlist = py38, py310, format, check 12 | 13 | ################################################################################ 14 | # PyTest 15 | ################################################################################ 16 | 17 | [testenv] 18 | require_locked_deps = true 19 | require_poetry = true 20 | locked_deps = 21 | pytest 22 | pytest-console-scripts 23 | pytest-cov 24 | commands = 25 | pytest -s tests/ 26 | pytest --cov 27 | 28 | [coverage:run] 29 | # ensure conditional branches are explored (important) 30 | # https://coverage.readthedocs.io/en/latest/branch.html#branch 31 | branch = true 32 | 33 | ###################################################################### 34 | # Ufmt (black + isort) 35 | ###################################################################### 36 | 37 | [testenv:format] 38 | description = Un-opinionated auto-formatting. 39 | locked_deps = 40 | ufmt 41 | commands = 42 | ufmt format {[constants]source_locations} 43 | 44 | ################################################################################ 45 | # Flake 8 46 | ################################################################################ 47 | 48 | [testenv:check] 49 | skip_install = true 50 | description = Formatting checks and linting (flake8 & ufmt check). 51 | locked_deps = 52 | darglint 53 | flake8 54 | flake8-docstrings 55 | ufmt 56 | commands = 57 | flake8 {[constants]source_locations} 58 | ufmt check {[constants]source_locations} 59 | 60 | 61 | ################################################################################ 62 | # Flake 8 Configuration 63 | # 64 | # Don't require docstrings, but parse them correctly if they are there. 65 | # 66 | # D100 Missing docstring in public module 67 | # D101 Missing docstring in public class 68 | # D102 Missing docstring in public method 69 | # D103 Missing docstring in public function 70 | # D105 Missing docstring in magic method 71 | # D107 Missing docstring in __init__ 72 | # 73 | # Jamming docstrings into a single line looks cluttered. 74 | # 75 | # D200 One-line docstring should fit on one line with quotes 76 | # 77 | # Weakly prefer breaking before a binary operator, so suppress that warning. 78 | # See https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b 79 | # 80 | # W503 line break before binary operator 81 | # 82 | ################################################################################ 83 | 84 | [flake8] 85 | # Relax various checks in the tests dir 86 | # - D*** documentation (docstrings) 87 | # - S101 use of assert warning (bandit) 88 | # - F401 unused import (from . import in __init__.py files) 89 | per-file-ignores = 90 | tests/*: D, S101, 91 | py_trees_js/__init__.py: F401, 92 | py_trees_js/viewer/__init__.py: F401 93 | # auto-generated files 94 | py_trees_js/resources.py: D100, D103 95 | py_trees_js/viewer/images_rc.py: D100, D103 96 | py_trees_js/viewer/web_app_rc.py: D100, D103 97 | py_trees_js/viewer/main_window_ui.py: D100, D101, D102, E402, F401 98 | py_trees_js/viewer/web_view_ui.py: D100, D101, D102, E402, F401 99 | 100 | # Match black line lengths 101 | # max-line-length = 88 102 | max-line-length = 120 103 | 104 | # Avoid overly complex functions 105 | # NB: this option by default is off, recommend complexity is 10 106 | # https://en.wikipedia.org/wiki/Cyclomatic_complexity 107 | max-complexity: 15 108 | 109 | # darglint docstring matching implementation checks 110 | # - short: one liners are not checked 111 | # - long: one liners and descriptions without args/returns are not checked 112 | strictness = long 113 | docstring_style=sphinx 114 | 115 | # Relax some of the more annoying checks 116 | # - C901 have a couple of stubborn methods (TODO) 117 | # - D105 magic method docstrings 118 | # - D107 prefer to include init args in the class documentation 119 | # - W503 deprecated PEP8, pay attention to 504 instead 120 | # https://www.flake8rules.com/rules/W503.html 121 | # - W504 line break after operator (TODO) 122 | ignore = C901, D105, D107, W503 123 | 124 | --------------------------------------------------------------------------------