├── .gitignore ├── .project ├── .pydevproject ├── .settings └── org.eclipse.core.resources.prefs ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── images ├── collapse.png ├── reconfigure.png ├── resume.png ├── rewind.png ├── select.png ├── splash.png └── track.png ├── package.xml ├── py_trees_ros_viewer ├── __init__.py ├── backend.py ├── console.py ├── conversions.py ├── exceptions.py ├── gen.bash ├── html │ └── index.html ├── images.qrc ├── images │ └── tuxrobot.png ├── images_rc.py ├── main_window.py ├── main_window.ui ├── main_window_ui.py ├── utilities.py ├── viewer.py ├── web_app.qrc ├── web_app_rc.py ├── web_view.py ├── web_view.ui └── web_view_ui.py ├── scripts └── py-trees-devel-viewer ├── setup.cfg ├── setup.py └── tests └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *doc/html 3 | *egg-info 4 | *.md.html 5 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | py_trees_ros_viewer 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /${PROJECT_DIR_NAME} 5 | 6 | python interpreter 7 | Default 8 | 9 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//py_trees_ros_viewer/backend.py=utf-8 3 | encoding//py_trees_ros_viewer/conversions.py=utf-8 4 | encoding//py_trees_ros_viewer/exceptions.py=utf-8 5 | encoding//py_trees_ros_viewer/main_window.py=utf-8 6 | encoding//py_trees_ros_viewer/utilities.py=utf-8 7 | encoding//py_trees_ros_viewer/viewer.py=utf-8 8 | encoding//py_trees_ros_viewer/web_view.py=utf-8 9 | encoding//py_trees_ros_viewer/web_view_ui.py=utf-8 10 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 0.2.5 (2025-01-13) 6 | ------------------ 7 | * [readme] Small tweaks (`#41 `_) 8 | * [infra] Fix repo URL in package.xml 9 | * [infra] Add dummy test to make buildfarm happy (`#40 `_) 10 | * Contributors: Sebastian Castro 11 | 12 | 0.2.4 (2025-01-11) 13 | ------------------ 14 | * [code] Fixed Namespace switching error (`#35 `_) 15 | * [code] Update deprecated QoS settings (`#34 `_) 16 | * [infra] resolve chrome 80+ incompatibility (`#32 `_) 17 | * Contributors: Daniel Stonier, Sebastian Castro, J Keshav Bhupathy Vignesh 18 | 19 | 0.2.3 (2020-02-24) 20 | ------------------ 21 | * [backend] capture paralell policy details 22 | 23 | 0.2.2 (2019-12-30) 24 | ------------------ 25 | * [docs] using ros2 launch, not ros2 run for the tutorials 26 | 27 | 0.2.1 (2019-12-28) 28 | ------------------ 29 | * [docs] usage instructions 30 | 31 | 0.2.0 (2019-12-28) 32 | ------------------ 33 | * [backend] feed the viewer blackboard access data, `#27 `_ 34 | * [all] machinery for activity streams, `#28 `_ 35 | 36 | 0.1.4 (2019-10-26) 37 | ------------------ 38 | * [infra] update to make use of usability / performance improvements in ``py_trees_js v0.5.1`` 39 | 40 | 0.1.3 (2019-08-29) 41 | ------------------ 42 | * [html] disable scrollbars, `#24 `_ 43 | * [html] use new 0.5.0 window resizing api ``from py_trees_js`` 44 | 45 | 0.1.2 (2019-08-14) 46 | ------------------ 47 | * [backend] bugfix dependency on ``py_trees_js``, not ``py_trees_ros_js`` 48 | 49 | 0.1.1 (2019-08-13) 50 | ------------------ 51 | * [backend] bugfix colour mixup between decorators and behaviours 52 | 53 | 0.1.0 (2019-08-13) 54 | ------------------ 55 | * `Milestone - Discover & Stream `_ 56 | 57 | 58 | 0.0.0 (2019-08-09) 59 | ------------------ 60 | * Initial package structure 61 | 62 | -------------------------------------------------------------------------------- /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 ROS Viewer 2 | 3 | [[About](#about)] [[Features](#features)] [[Quickstart](#quickstart)] [[Usage](#usage)] [[Modding](#modding)] 4 | 5 | ## About 6 | 7 | A Qt-JS application for visualisation of behaviour trees in a ROS 2 ecosystem. 8 | This implementation serves as the GUI visualisation tool for 9 | [PyTrees ROS](https://github.com/splintered-reality/py_trees_ros#pytrees-ros-ecosystem). 10 | 11 | ## Features 12 | 13 | * Tree Visualisation 14 | * Collapsible Subtrees 15 | * Zoom and Scale Contents to Fit 16 | * Timeline Rewind & Resume 17 | * Blackboard View 18 | * Activity View 19 | 20 | ## Quickstart 21 | 22 | ``` 23 | sudo apt install ros--py-trees-ros-tutorials 24 | sudo apt install ros--py-trees-ros-viewer 25 | 26 | # In a first shell 27 | py-trees-tree-viewer 28 | # In a second shell 29 | ros2 launch py_trees_ros_tutorials tutorial_eight_dynamic_application_loading_launch.py 30 | # Click 'Scan' on the qt robot dashboard interface 31 | # Wear a colander, I am too. 32 | ``` 33 | 34 | ## Usage 35 | 36 | Until a snapshot stream is discovered or selected, you'll land at the splash screen which 37 | enumerates the interactive options available. 38 | 39 | ![Splash](images/splash.png?raw=true "Splash Screen") 40 | 41 | The viewer will continuously scan for and update the namespace drop-down 42 | with a list of py_tree instances (specifically, the namespace of their snapshot 43 | stream services). If not yet connected, it will provide the convenience of 44 | automatically making a connection to the first instance it discovers. Beyond that, 45 | it is possible to switch between streaming services via the drop-down. 46 | 47 | ![Select](images/select.png?raw=true "Select a Tree Stream") 48 | 49 | At a minimum, the stream will send updates to the tree graph when the tree changes 50 | (i.e. when any one of the behaviours modifies it's status). Additional configuration 51 | of the stream can be managed via the checkboxes in the top right of the window - introspect 52 | on the blackboard and/or request more frequent updates (useful when tracking 53 | blackboard changes). 54 | 55 | ![Reconfigure](images/reconfigure.png?raw=true "Reconfigure the Stream") 56 | 57 | If you have a large tree, collapse sections of it. This is useful in tandem with 58 | the screenshot capability when you wish to highlight an area of interest to communicate 59 | a design problem or report a bug. 60 | 61 | ![Collapsible Subtrees](images/collapse.png?raw=true "Collapsible Subtrees") 62 | 63 | In the same vein, filter the blackboard to those keys expicitly associated (used) 64 | by manually selected behaviours. 65 | 66 | ![Track Data](images/track.png?raw=true "Track Blackboard Data") 67 | 68 | Problems debugging a catastrophe and the final state of the tree doesn't uncover 69 | the root cause, merely the mayhem that resulted? This is a very common situation - use 70 | the timeline! Rewind to a previous state and time travel. Note: the stream configuration 71 | only applies to future messages - you can't change the past! If you are concerned about 72 | being able to debug affectively, make sure you have at least the 'Blackboard Data' 73 | and 'Periodic' checkboxes enabled so that time travelling along the timeline has 74 | sufficient data for analysis. 75 | 76 | ![Rewind](images/rewind.png?raw=true "Rewind") 77 | 78 | Travelling along the timeline is possible even while online. Simply hit the 'Resume' 79 | button to resume visualisation of the live feed. 80 | 81 | ![Resume](images/resume.png?raw=true "Resume") 82 | 83 | ## Modding 84 | 85 | The underlying `py_trees_js` library lends itself to being used in various ways. This 86 | application can be used as a baseline reference (since it utilises the underlying 87 | `py_trees_js` library fully) for modifications to suit your own use case. Open an 88 | [issue](https://github.com/splintered-reality/py_trees_ros_viewer/issues) if you have 89 | questions and/or would like assistance. 90 | 91 | 1) Visualisation for a behaviour trees implementation that is not `py_trees`. 92 | 93 | Replicate the hybrid qt-js application here and replace [backend.py](https://github.com/splintered-reality/py_trees_ros_viewer/blob/devel/py_trees_ros_viewer/backend.py). 94 | The data structure eventually passed to the js library is a json dictionary that only 95 | makes use of the most fundamental properties of behaviour trees (e.g. tree graph 96 | structure, behaviour status) and consequently, it should be a matter of merely 97 | wiring connections and converting data. 98 | 99 | 2) Visualisation for a different middleware architecture 100 | 101 | This follows much the same procedure as for the first use case - replace 102 | [backend.py](https://github.com/splintered-reality/py_trees_ros_viewer/blob/devel/py_trees_ros_viewer/backend.py). For example, a ROS1 viewer for py_trees 103 | could be built in this manner. 104 | 105 | 3) Visualisation in a mobile device 106 | 107 | Since the core of the application is a js library, the bridge to moving from a 108 | developer friendly application to a lightweight web application, or a widget embedded 109 | in another web application is much smaller. Again it is merely a matter of 110 | generating the required input channel and data conversions. Refer to the 111 | `py_trees_js` [README](https://github.com/splintered-reality/py_trees_js#usage) for more 112 | details. 113 | -------------------------------------------------------------------------------- /images/collapse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_ros_viewer/1befdf4ce3f0d434ee5ac77147686ea4ae8759b6/images/collapse.png -------------------------------------------------------------------------------- /images/reconfigure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_ros_viewer/1befdf4ce3f0d434ee5ac77147686ea4ae8759b6/images/reconfigure.png -------------------------------------------------------------------------------- /images/resume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_ros_viewer/1befdf4ce3f0d434ee5ac77147686ea4ae8759b6/images/resume.png -------------------------------------------------------------------------------- /images/rewind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_ros_viewer/1befdf4ce3f0d434ee5ac77147686ea4ae8759b6/images/rewind.png -------------------------------------------------------------------------------- /images/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_ros_viewer/1befdf4ce3f0d434ee5ac77147686ea4ae8759b6/images/select.png -------------------------------------------------------------------------------- /images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_ros_viewer/1befdf4ce3f0d434ee5ac77147686ea4ae8759b6/images/splash.png -------------------------------------------------------------------------------- /images/track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_ros_viewer/1befdf4ce3f0d434ee5ac77147686ea4ae8759b6/images/track.png -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | py_trees_ros_viewer 5 | 0.2.5 6 | 7 | A Qt-JS application for visualisation of executing/log-replayed behaviour trees in a ROS2 ecosystem. 8 | 9 | 10 | Daniel Stonier 11 | Daniel Stonier 12 | Sebastian Castro 13 | 14 | BSD 15 | 16 | https://github.com/splintered-reality/py_trees_ros_viewer 17 | https://github.com/splintered-reality/py_trees_ros_viewer 18 | https://github.com/splintered-reality/py_trees_ros_viewer/issues 19 | 20 | python3-setuptools 21 | pyqt5-dev-tools 22 | qttools5-dev-tools 23 | 24 | py_trees_ros_interfaces 25 | py_trees_js 26 | python3-qt5-bindings 27 | python3-pyqt5.qtwebengine 28 | rclpy 29 | unique_identifier_msgs 30 | 31 | 32 | ament_python 33 | 34 | 35 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://github.com/splintered-reality/py_trees_ros_viewer/raw/devel/LICENSE 4 | # 5 | ############################################################################## 6 | # Documentation 7 | ############################################################################## 8 | 9 | """ 10 | A Qt-JS hybrid viewer for visualisation of executing behaviour trees in a 11 | ROS2 ecosystem. 12 | """ 13 | ############################################################################## 14 | # Imports 15 | ############################################################################## 16 | 17 | # Pythonic helper modules 18 | from . import console 19 | from . import conversions 20 | from . import utilities 21 | 22 | # Qt widgets 23 | from . import main_window 24 | from . import web_view 25 | 26 | # Application entry point 27 | from . import viewer 28 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/backend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License: BSD 5 | # https://github.com/splintered-reality/py_trees_ros_viewer/raw/devel/LICENSE 6 | # 7 | ############################################################################## 8 | # Documentation 9 | ############################################################################## 10 | """ 11 | Ros backend for the viewer. 12 | """ 13 | ############################################################################## 14 | # Imports 15 | ############################################################################## 16 | 17 | import copy 18 | import math 19 | import os 20 | import threading 21 | import time 22 | import typing 23 | 24 | import PyQt5.QtCore as qt_core 25 | 26 | import py_trees_ros_interfaces.msg as py_trees_msgs 27 | import py_trees_ros_interfaces.srv as py_trees_srvs 28 | import rcl_interfaces.msg as rcl_msgs 29 | import rcl_interfaces.srv as rcl_srvs 30 | import rclpy 31 | import rclpy.node 32 | 33 | from . import console 34 | from . import conversions 35 | from . import exceptions 36 | from . import utilities 37 | 38 | ############################################################################## 39 | # Helpers 40 | ############################################################################## 41 | 42 | 43 | class SnapshotStream(object): 44 | """ 45 | The tree watcher sits on the other side of a running 46 | :class:`~py_trees_ros.trees.BehaviourTree` and manages the dynamic 47 | connection of a snapshot stream. 48 | """ 49 | 50 | class Parameters(object): 51 | """ 52 | Reconfigurable parameters for the snapshot stream. 53 | 54 | Args: 55 | blackboard_data: publish blackboard variables on the visited path 56 | blackboard_activity: enable and publish blackboard activity in the last tick 57 | snapshot_period: period between snapshots (use /inf to only publish on tree status changes) 58 | """ 59 | def __init__( 60 | self, 61 | blackboard_data: bool=False, 62 | blackboard_activity: bool=False, 63 | snapshot_period: float=math.inf 64 | ): 65 | self.blackboard_data = blackboard_data 66 | self.blackboard_activity = blackboard_activity 67 | self.snapshot_period = snapshot_period 68 | 69 | def __eq__(self, other): 70 | return ((self.blackboard_data == other.blackboard_data) and 71 | (self.blackboard_activity == other.blackboard_activity) and 72 | (self.snapshot_period == other.snapshot_period) 73 | ) 74 | 75 | def __init__( 76 | self, 77 | node: rclpy.node.Node, 78 | namespace: str, 79 | parameters: 'SnapshotStream.Parameters', 80 | callback: typing.Callable[[py_trees_msgs.BehaviourTree], None], 81 | ): 82 | """ 83 | Args: 84 | namespace: connect to the snapshot stream services in this namespace 85 | parameters: snapshot stream configuration controlling both on-the-fly stream creation and display 86 | statistics: display statistics 87 | 88 | .. seealso:: :mod:`py_trees_ros.programs.tree_watcher` 89 | """ 90 | 91 | self.namespace = namespace 92 | self.parameters = copy.copy(parameters) if parameters is not None else SnapshotStream.Parameters() 93 | self.node = node 94 | self.callback = callback 95 | 96 | self.topic_name = None 97 | self.subscriber = None 98 | 99 | self.services = { 100 | 'open': None, 101 | 'close': None, 102 | 'reconfigure': None 103 | } 104 | 105 | self.service_names = { 106 | 'open': self.namespace + "/open", 107 | 'close': self.namespace + "/close", 108 | 'reconfigure': self.namespace + "/reconfigure", 109 | } 110 | self.service_type_strings = { 111 | 'open': 'py_trees_ros_interfaces/srv/OpenSnapshotStream', 112 | 'close': 'py_trees_ros_interfaces/srv/CloseSnapshotStream', 113 | 'reconfigure': 'py_trees_ros_interfaces/srv/ReconfigureSnapshotStream' 114 | } 115 | self.service_types = { 116 | 'open': py_trees_srvs.OpenSnapshotStream, 117 | 'close': py_trees_srvs.CloseSnapshotStream, 118 | 'reconfigure': py_trees_srvs.ReconfigureSnapshotStream 119 | } 120 | # create service clients 121 | self.services["open"] = self.create_service_client(key="open") 122 | self.services["close"] = self.create_service_client(key="close") 123 | self.services["reconfigure"] = self.create_service_client(key="reconfigure") 124 | 125 | # create connection 126 | self._connect_on_init() 127 | 128 | def reconfigure(self, parameters: 'SnapshotStream.Parameters'): 129 | """ 130 | Reconfigure the stream. 131 | 132 | Args: 133 | parameters: new configuration 134 | """ 135 | if self.parameters == parameters: 136 | return 137 | self.parameters = copy.copy(parameters) 138 | request = self.service_types["reconfigure"].Request() 139 | request.topic_name = self.topic_name 140 | request.parameters.blackboard_data = self.parameters.blackboard_data 141 | request.parameters.blackboard_activity = self.parameters.blackboard_activity 142 | request.parameters.snapshot_period = self.parameters.snapshot_period 143 | unused_future = self.services["reconfigure"].call_async(request) 144 | 145 | def _connect_on_init(self, timeout_sec=1.0): 146 | """ 147 | Request a snapshot stream and make a connection to it. 148 | 149 | Args: 150 | timeout_sec: how long to hold on making connections 151 | 152 | Raises: 153 | :class:`~py_trees_ros.exceptions.NotReadyError`: if setup() wasn't called to identify the relevant services to connect to. 154 | :class:`~py_trees_ros.exceptions.TimedOutError`: if it times out waiting for the server 155 | """ 156 | # request a stream 157 | request = self.service_types["open"].Request() 158 | request.parameters.blackboard_data = self.parameters.blackboard_data 159 | request.parameters.blackboard_activity = self.parameters.blackboard_activity 160 | request.parameters.snapshot_period = self.parameters.snapshot_period 161 | console.logdebug("establishing a snapshot stream connection [{}][backend]".format(self.namespace)) 162 | future = self.services["open"].call_async(request) 163 | rclpy.spin_until_future_complete(self.node, future) 164 | response = future.result() 165 | self.topic_name = response.topic_name 166 | # connect to a snapshot stream 167 | start_time = time.monotonic() 168 | while True: 169 | elapsed_time = time.monotonic() - start_time 170 | if elapsed_time > timeout_sec: 171 | raise exceptions.TimedOutError("timed out waiting for a snapshot stream publisher [{}]".format(self.topic_name)) 172 | if self.node.count_publishers(self.topic_name) > 0: 173 | break 174 | time.sleep(0.1) 175 | self.subscriber = self.node.create_subscription( 176 | msg_type=py_trees_msgs.BehaviourTree, 177 | topic=self.topic_name, 178 | callback=self.callback, 179 | qos_profile=utilities.qos_profile_latched() 180 | ) 181 | console.logdebug(" ...ok [backend]") 182 | 183 | def shutdown(self): 184 | if rclpy.ok() and self.services["close"] is not None: 185 | request = self.service_types["close"].Request() 186 | request.topic_name = self.topic_name 187 | future = self.services["close"].call_async(request) 188 | rclpy.spin_until_future_complete( 189 | node=self.node, 190 | future=future, 191 | timeout_sec=0.5) 192 | unused_response = future.result() 193 | 194 | def create_service_client(self, key: str): 195 | """ 196 | Convenience api for opening a service client and waiting for the service to appear. 197 | 198 | Args: 199 | key: one of 'open', 'close'. 200 | 201 | Raises: 202 | :class:`~py_trees_ros.exceptions.NotReadyError`: if setup() wasn't called to identify the relevant services to connect to. 203 | :class:`~py_trees_ros.exceptions.TimedOutError`: if it times out waiting for the server 204 | """ 205 | if self.service_names[key] is None: 206 | raise exceptions.NotReadyError( 207 | "no known '{}' service known [did you call setup()?]".format(self.service_types[key]) 208 | ) 209 | client = self.node.create_client( 210 | srv_type=self.service_types[key], 211 | srv_name=self.service_names[key], 212 | qos_profile=rclpy.qos.qos_profile_services_default 213 | ) 214 | # hardcoding timeouts will get us into trouble 215 | if not client.wait_for_service(timeout_sec=3.0): 216 | raise exceptions.TimedOutError( 217 | "timed out waiting for {}".format(self.service_names['close']) 218 | ) 219 | return client 220 | 221 | ############################################################################## 222 | # Backend 223 | ############################################################################## 224 | 225 | 226 | class Backend(qt_core.QObject): 227 | 228 | discovered_namespaces_changed = qt_core.pyqtSignal(list, name="discoveredNamespacesChanged") 229 | tree_snapshot_arrived = qt_core.pyqtSignal(dict, name="treeSnapshotArrived") 230 | 231 | def __init__(self, parameters): 232 | super().__init__() 233 | default_node_name = "tree_viewer_" + str(os.getpid()) 234 | self.node = rclpy.create_node(default_node_name) 235 | self.shutdown_requested = False 236 | self.snapshot_stream_type = py_trees_msgs.BehaviourTree 237 | self.discovered_namespaces = [] 238 | self.discovered_timestamp = time.monotonic() 239 | self.discovery_loop_time_sec = 3.0 240 | self.cached_blackboard = {"behaviours": {}, "data": {}} 241 | self.snapshot_stream = None 242 | self.parameters = parameters 243 | 244 | self.lock = threading.Lock() 245 | self.enqueued_connection_request_namespace = None 246 | 247 | def spin(self): 248 | with self.lock: 249 | old_parameters = copy.copy(self.parameters) 250 | while rclpy.ok() and not self.shutdown_requested: 251 | self.discover_namespaces() 252 | with self.lock: 253 | if self.parameters != old_parameters: 254 | if self.snapshot_stream is not None: 255 | self.snapshot_stream.reconfigure(self.parameters) 256 | old_parameters = copy.copy(self.parameters) 257 | if self.enqueued_connection_request_namespace is not None: 258 | self.connect(self.enqueued_connection_request_namespace) 259 | self.enqueued_connection_request_namespace = None 260 | rclpy.spin_once(self.node, timeout_sec=0.1) 261 | if self.snapshot_stream is not None: 262 | self.snapshot_stream.shutdown() 263 | self.node.destroy_node() 264 | 265 | def terminate_ros_spinner(self): 266 | self.node.get_logger().info("shutdown requested [backend]") 267 | self.shutdown_requested = True 268 | 269 | def discover_namespaces(self): 270 | """ 271 | Oneshot lookup for namespaces within which snapshot stream services exist. 272 | This is additionally conditioned on 'discovery_loop_time_sec' so that it 273 | doesn't spam the check at the same rate as the node is spinning. 274 | 275 | If a change in the result occurs, it emits a signal for the qt ui. 276 | """ 277 | timeout = self.discovered_timestamp + self.discovery_loop_time_sec 278 | if self.discovered_namespaces and (time.monotonic() < timeout): 279 | return 280 | open_service_type_string = "py_trees_ros_interfaces/srv/OpenSnapshotStream" 281 | service_names_and_types = self.node.get_service_names_and_types() 282 | new_service_names = [name for name, types in service_names_and_types if open_service_type_string in types] 283 | new_service_names.sort() 284 | new_namespaces = [utilities.parent_namespace(name) for name in new_service_names] 285 | if self.discovered_namespaces != new_namespaces: 286 | self.discovered_namespaces = new_namespaces 287 | self.discovered_namespaces_changed.emit(self.discovered_namespaces) 288 | console.logdebug("discovered namespaces changed {}[backend]".format(self.discovered_namespaces)) 289 | self.discovered_timestamp = time.monotonic() 290 | 291 | def connect(self, namespace): 292 | """ 293 | Cancel the current connection and create a new one to the specified namespace. 294 | 295 | Args: 296 | namespace: in which to find snapshot stream services 297 | """ 298 | if self.snapshot_stream is not None: 299 | console.logdebug("cancelling existing snapshot stream connection [{}][backend]".format(self.snapshot_stream)) 300 | self.snapshot_stream.shutdown() 301 | self.snapshot_stream = None 302 | console.logdebug("creating a new snapshot stream connection [{}][backend]".format(namespace)) 303 | self.snapshot_stream = SnapshotStream( 304 | node=self.node, 305 | namespace=namespace, 306 | callback=self.tree_snapshot_handler, 307 | parameters=self.parameters 308 | ) 309 | 310 | def snapshot_blackboard_data(self, snapshot: bool): 311 | if self.parameter_client is not None: 312 | request = rcl_srvs.SetParameters.Request() # noqa 313 | parameter = rcl_msgs.Parameter() 314 | parameter.name = "snapshot_blackboard_data" 315 | parameter.value.type = rcl_msgs.ParameterType.PARAMETER_BOOL # noqa 316 | parameter.value.bool_value = snapshot 317 | request.parameters.append(parameter) 318 | unused_future = self.parameter_client.call_async(request) 319 | self.parameters.snapshot_blackboard_data = snapshot 320 | 321 | def tree_snapshot_handler(self, msg: py_trees_msgs.BehaviourTree): 322 | """ 323 | Callback to receive incoming tree snapshots before relaying them to the web application. 324 | 325 | Args: 326 | msg: incoming serialised tree snapshot 327 | 328 | Note: this uses a clever(?) hack to accumulate visited path snapshots of the blackboard 329 | to gain a representation of the entire blackboard without having to transmit the 330 | entire blackboard on every update. Special care is needed to make sure what has been 331 | removed from the blackboard (does not get transmitted), actually gets removed. 332 | """ 333 | console.logdebug("handling incoming tree snapshot [backend]") 334 | colours = { 335 | 'Sequence': '#FFA500', 336 | 'Selector': '#00FFFF', 337 | 'Parallel': '#FFFF00', 338 | 'Behaviour': '#555555', 339 | 'Decorator': '#DDDDDD', 340 | } 341 | tree = { 342 | 'changed': "true" if msg.changed else "false", 343 | 'timestamp': msg.statistics.stamp.sec + float(msg.statistics.stamp.nanosec) / 1.0e9, 344 | 'behaviours': {}, 345 | 'blackboard': {'behaviours': {}, 'data': {}}, 346 | 'visited_path': [] 347 | } 348 | # hack, update the blackboard from visited path contexts 349 | blackboard_variables = {} 350 | for blackboard_variable in msg.blackboard_on_visited_path: 351 | blackboard_variables[blackboard_variable.key] = blackboard_variable.value 352 | for behaviour in msg.behaviours: 353 | behaviour_id = str(conversions.msg_to_uuid4(behaviour.own_id)) 354 | behaviour_type = conversions.msg_constant_to_behaviour_str(behaviour.type) 355 | if behaviour.is_active: 356 | tree['visited_path'].append(behaviour_id) 357 | tree['behaviours'][behaviour_id] = { 358 | 'id': behaviour_id, 359 | 'status': conversions.msg_constant_to_status_str(behaviour.status), 360 | 'name': utilities.normalise_name_strings(behaviour.name), 361 | 'colour': colours[behaviour_type], 362 | 'details': behaviour.additional_detail, 363 | 'children': [str(conversions.msg_to_uuid4(child_id)) for child_id in behaviour.child_ids], 364 | 'data': { 365 | 'Class': behaviour.class_name, 366 | 'Feedback': behaviour.message, 367 | }, 368 | } 369 | if behaviour.blackboard_access: 370 | variables = [] 371 | for variable in behaviour.blackboard_access: 372 | variables.append(variable.key + " ({})".format(variable.value)) 373 | tree['blackboard']['behaviours'].setdefault(behaviour_id, {})[variable.key] = variable.value 374 | tree['behaviours'][behaviour_id]['data']['Blackboard'] = variables 375 | # delete keys from the cache if they aren't in the visited variables list when 376 | # they should be (i.e. their parent behaviour is on the visited path and has 377 | # 'w' or 'x' permissions on the variable). 378 | if ( 379 | variable.key in self.cached_blackboard and 380 | variable.value != 'r' and 381 | behaviour.is_active and 382 | variable.key not in blackboard_variables 383 | ): 384 | del self.cached_blackboard[variable.key] 385 | # hack, update the blackboard from visited path contexts 386 | self.cached_blackboard.update(blackboard_variables) 387 | if self.snapshot_stream.parameters.blackboard_data: 388 | tree['blackboard']['data'] = copy.deepcopy(self.cached_blackboard) 389 | if self.snapshot_stream.parameters.blackboard_activity: 390 | xhtml = utilities.XhtmlSymbols() 391 | xhtml_snippet = "" 392 | for item in msg.blackboard_activity: 393 | if item.activity_type == "READ": 394 | info = xhtml.normal + xhtml.left_arrow + xhtml.space + item.current_value + xhtml.reset 395 | elif item.activity_type == "WRITE": 396 | info = xhtml.green + xhtml.right_arrow + xhtml.space + item.current_value + xhtml.reset 397 | elif item.activity_type == "ACCESSED": 398 | info = xhtml.yellow + xhtml.left_right_arrow + xhtml.space + item.current_value + xhtml.reset 399 | elif item.activity_type == "ACCESS_DENIED": 400 | info = xhtml.red + xhtml.multiplication_x + xhtml.space + "client has no read/write access" + xhtml.reset 401 | elif item.activity_type == "NO_KEY": 402 | info = xhtml.red + xhtml.multiplication_x + xhtml.space + "key does not yet exist" + xhtml.reset 403 | elif item.activity_type == "NO_OVERWRITE": 404 | info = xhtml.yellow + xhtml.forbidden_circle + xhtml.space + item.current_value + xhtml.reset 405 | elif item.activity_type == "UNSET": 406 | info = "" 407 | elif item.activity_type == "INITIALISED": 408 | info = xhtml.green + xhtml.right_arrow + xhtml.space + item.current_value + xhtml.reset 409 | else: 410 | info = "" 411 | xhtml_snippet += ( 412 | "" 413 | "" 414 | "" 415 | "" 416 | "" 417 | "" 418 | ) 419 | xhtml_snippet += "
" + xhtml.cyan + item.key + xhtml.reset + "" + xhtml.yellow + item.activity_type + xhtml.reset + "" + xhtml.normal + item.client_name + xhtml.reset + "" + info + "
" 420 | tree['activity'] = [xhtml_snippet] 421 | 422 | self.tree_snapshot_arrived.emit(tree) 423 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/console.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://raw.githubusercontent.com/splintered-reality/py_trees_ros_viewer/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. Usually used to check 48 | if the stdout is capable or otherwise (e.g. Jenkins CI can often be configured 49 | with unicode disabled). 50 | 51 | Args: 52 | encoding (:obj:`str`, optional): the encoding to check against. 53 | 54 | Returns: 55 | :obj:`bool`: true if capable, false otherwise 56 | """ 57 | try: 58 | u'\u26A1'.encode(encoding) 59 | except UnicodeError: 60 | return False 61 | return True 62 | 63 | 64 | def define_symbol_or_fallback(original: str, fallback: str, encoding: str=sys.stdout.encoding): 65 | """ 66 | Return the correct encoding according to the specified encoding. Used to 67 | make sure we get an appropriate symbol, even if the shell is merely ascii as 68 | is often the case on, e.g. Jenkins CI. 69 | 70 | Args: 71 | original (:obj:`str`): the unicode string (usually just a character) 72 | fallback (:obj:`str`): the fallback ascii string 73 | encoding (:obj:`str`, optional): the encoding to check against. 74 | 75 | Returns: 76 | :obj:`str`: either original or fallback depending on whether exceptions were thrown. 77 | """ 78 | try: 79 | original.encode(encoding) 80 | except UnicodeError: 81 | return fallback 82 | return original 83 | 84 | 85 | lightning_bolt = u'\u26A1' 86 | double_vertical_line = u'\u2016' 87 | check_mark = u'\u2713' 88 | multiplication_x = u'\u2715' 89 | 90 | ############################################################################## 91 | # Keypress 92 | ############################################################################## 93 | 94 | 95 | def read_single_keypress(): 96 | """Waits for a single keypress on stdin. 97 | 98 | This is a silly function to call if you need to do it a lot because it has 99 | to store stdin's current setup, setup stdin for reading single keystrokes 100 | then read the single keystroke then revert stdin back after reading the 101 | keystroke. 102 | 103 | Returns: 104 | :obj:`int`: the character of the key that was pressed 105 | 106 | Raises: 107 | KeyboardInterrupt: if CTRL-C was pressed (keycode 0x03) 108 | """ 109 | def read_single_keypress_unix(): 110 | """For Unix case, where fcntl, termios is available.""" 111 | import fcntl 112 | import termios 113 | fd = sys.stdin.fileno() 114 | # save old state 115 | flags_save = fcntl.fcntl(fd, fcntl.F_GETFL) 116 | attrs_save = termios.tcgetattr(fd) 117 | # make raw - the way to do this comes from the termios(3) man page. 118 | attrs = list(attrs_save) # copy the stored version to update 119 | # iflag 120 | attrs[0] &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK | 121 | termios.ISTRIP | termios.INLCR | termios. IGNCR | 122 | termios.ICRNL | termios.IXON) 123 | # oflag 124 | attrs[1] &= ~termios.OPOST 125 | # cflag 126 | attrs[2] &= ~(termios.CSIZE | termios. PARENB) 127 | attrs[2] |= termios.CS8 128 | # lflag 129 | attrs[3] &= ~(termios.ECHONL | termios.ECHO | termios.ICANON | 130 | termios.ISIG | termios.IEXTEN) 131 | termios.tcsetattr(fd, termios.TCSANOW, attrs) 132 | # turn off non-blocking 133 | fcntl.fcntl(fd, fcntl.F_SETFL, flags_save & ~os.O_NONBLOCK) 134 | # read a single keystroke 135 | ret = sys.stdin.read(1) # returns a single character 136 | if ord(ret) == 3: # CTRL-C 137 | termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save) 138 | fcntl.fcntl(fd, fcntl.F_SETFL, flags_save) 139 | raise KeyboardInterrupt("Ctrl-c") 140 | # restore old state 141 | termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save) 142 | fcntl.fcntl(fd, fcntl.F_SETFL, flags_save) 143 | return ret 144 | 145 | def read_single_keypress_windows(): 146 | """Windows case, can't use fcntl and termios. 147 | Not same implementation as for Unix, requires a newline to continue. 148 | """ 149 | import msvcrt # noqa 150 | # read a single keystroke 151 | ret = sys.stdin.read(1) 152 | if ord(ret) == 3: # CTRL-C 153 | raise KeyboardInterrupt("Ctrl-c") 154 | return ret 155 | try: 156 | return read_single_keypress_unix() 157 | except ImportError as e_unix: 158 | try: 159 | return read_single_keypress_windows() 160 | except ImportError as e_windows: 161 | raise ImportError("Neither unix nor windows implementations supported [{}][{}]".format(str(e_unix), str(e_windows))) 162 | 163 | ############################################################################## 164 | # Methods 165 | ############################################################################## 166 | 167 | 168 | def console_has_colours(): 169 | """ 170 | Detects if the console (stdout) has colourising capability. 171 | """ 172 | if os.environ.get("PY_TREES_DISABLE_COLORS"): 173 | return False 174 | # From django.core.management.color.supports_color 175 | # https://github.com/django/django/blob/master/django/core/management/color.py 176 | plat = sys.platform 177 | supported_platform = plat != 'Pocket PC' and (plat != 'win32' or 178 | 'ANSICON' in os.environ) 179 | # isatty is not always implemented, #6223. 180 | is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() 181 | if not supported_platform or not is_a_tty: 182 | return False 183 | return True 184 | 185 | 186 | has_colours = console_has_colours() 187 | """ Whether the loading program has access to colours or not.""" 188 | 189 | 190 | if has_colours: 191 | # reset = "\x1b[0;0m" 192 | reset = "\x1b[0m" 193 | bold = "\x1b[%sm" % '1' 194 | dim = "\x1b[%sm" % '2' 195 | underlined = "\x1b[%sm" % '4' 196 | blink = "\x1b[%sm" % '5' 197 | black, red, green, yellow, blue, magenta, cyan, white = ["\x1b[%sm" % str(i) for i in range(30, 38)] 198 | bold_black, bold_red, bold_green, bold_yellow, bold_blue, bold_magenta, bold_cyan, bold_white = ["\x1b[%sm" % ('1;' + str(i)) for i in range(30, 38)] 199 | else: 200 | reset = "" 201 | bold = "" 202 | dim = "" 203 | underlined = "" 204 | blink = "" 205 | black, red, green, yellow, blue, magenta, cyan, white = ["" for i in range(30, 38)] 206 | bold_black, bold_red, bold_green, bold_yellow, bold_blue, bold_magenta, bold_cyan, bold_white = ["" for i in range(30, 38)] 207 | 208 | colours = [bold, dim, underlined, blink, 209 | black, red, green, yellow, blue, magenta, cyan, white, 210 | bold_black, bold_red, bold_green, bold_yellow, bold_blue, bold_magenta, bold_cyan, bold_white 211 | ] 212 | """List of all available colours.""" 213 | 214 | 215 | def pretty_print(msg, colour=white): 216 | if has_colours: 217 | seq = colour + msg + reset 218 | sys.stdout.write(seq) 219 | else: 220 | sys.stdout.write(msg) 221 | 222 | 223 | def pretty_println(msg, colour=white): 224 | if has_colours: 225 | seq = colour + msg + reset 226 | sys.stdout.write(seq) 227 | sys.stdout.write("\n") 228 | else: 229 | sys.stdout.write(msg) 230 | 231 | 232 | ############################################################################## 233 | # Console 234 | ############################################################################## 235 | 236 | 237 | def banner(msg): 238 | print(green + "\n" + 80 * "*" + reset) 239 | print(green + "* " + bold_white + msg.center(80) + reset) 240 | print(green + 80 * "*" + "\n" + reset) 241 | 242 | 243 | def debug(msg): 244 | print(green + msg + reset) 245 | 246 | 247 | def warning(msg): 248 | print(yellow + msg + reset) 249 | 250 | 251 | def info(msg): 252 | print(msg) 253 | 254 | 255 | def error(msg): 256 | print(red + msg + reset) 257 | 258 | 259 | class LogLevel(enum.Enum): 260 | """ 261 | Log levels. 262 | """ 263 | DEBUG = 1 264 | INFO = 2 265 | WARNING = 3 266 | ERROR = 4 267 | 268 | 269 | log_level = LogLevel.INFO 270 | """ Console's current log level.""" 271 | 272 | 273 | def logdebug(message): 274 | ''' 275 | Prefixes ``[DEBUG]`` and colours the message green. 276 | 277 | Args: 278 | message (:obj:`str`): message to log. 279 | ''' 280 | if log_level.value < LogLevel.INFO.value: 281 | print(green + "[DEBUG] " + message + reset) 282 | 283 | 284 | def loginfo(message): 285 | ''' 286 | Prefixes ``[ INFO]`` to the message. 287 | 288 | Args: 289 | message (:obj:`str`): message to log. 290 | ''' 291 | if log_level.value < LogLevel.WARNING.value: 292 | print("[ INFO] " + message) 293 | 294 | 295 | def logwarn(message): 296 | ''' 297 | Prefixes ``[ WARN]`` and colours the message yellow. 298 | 299 | Args: 300 | message (:obj:`str`): message to log. 301 | ''' 302 | if log_level.value < LogLevel.ERROR.value: 303 | print(yellow + "[ WARN] " + message + reset) 304 | 305 | 306 | def logerror(message): 307 | ''' 308 | Prefixes ``[ERROR]`` and colours the message red. 309 | 310 | Args: 311 | message (:obj:`str`): message to log. 312 | ''' 313 | print(red + "[ERROR] " + message + reset) 314 | 315 | 316 | def logfatal(message): 317 | ''' 318 | Prefixes ``[FATAL]`` and colours the message bold red. 319 | 320 | Args: 321 | message (:obj:`str`): message to log. 322 | ''' 323 | print(bold_red + "[FATAL] " + message + reset) 324 | 325 | 326 | ############################################################################## 327 | # Main 328 | ############################################################################## 329 | 330 | if __name__ == '__main__': 331 | for colour in colours: 332 | pretty_print("dude\n", colour) 333 | logdebug("loginfo message") 334 | logwarn("logwarn message") 335 | logerror("logerror message") 336 | logfatal("logfatal message") 337 | pretty_print("red\n", red) 338 | print("some normal text") 339 | print(cyan + " Name" + reset + ": " + yellow + "Dude" + reset) 340 | print("special characters are\n") 341 | print("lightning_bolt: {}".format(lightning_bolt)) 342 | print("double_vertical_line: {}".format(double_vertical_line)) 343 | print("check_mark: {}".format(check_mark)) 344 | print("multiplication_x: {}".format(multiplication_x)) 345 | # print("has unicode: {}".format(has_unicode())) 346 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/conversions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License: BSD 5 | # https://github.com/splintered-reality/py_trees_ros_viewer/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 uuid 18 | 19 | import py_trees_ros_interfaces.msg as py_trees_msgs 20 | import unique_identifier_msgs.msg as unique_identifier_msgs 21 | 22 | ############################################################################## 23 | # Methods 24 | ############################################################################## 25 | 26 | 27 | def msg_to_uuid4(msg: unique_identifier_msgs.UUID) -> uuid.UUID: 28 | """ 29 | Convert a uuid4 python object to a ros unique identifier, UUID type. 30 | 31 | Args: 32 | msg: the ros message type 33 | 34 | Returns: 35 | the behaviour's uuid, python style 36 | """ 37 | return uuid.UUID(bytes=bytes(msg.uuid), version=4) 38 | 39 | 40 | def msg_constant_to_behaviour_str(value: int) -> str: 41 | """ 42 | Convert one of the behaviour type constants in a 43 | :class:`py_trees_ros_interfaces.msg.Behaviour` message to 44 | a human readable string. 45 | 46 | Args: 47 | value: see the message definition for details 48 | 49 | Returns: 50 | the bheaviour class type as a string (e.g. 'Sequence') 51 | 52 | Raises: 53 | TypeError: if the message type is unrecognised 54 | """ 55 | if value == py_trees_msgs.Behaviour.SEQUENCE: 56 | return 'Sequence' 57 | elif value == py_trees_msgs.Behaviour.CHOOSER: 58 | return 'Chooser' 59 | elif value == py_trees_msgs.Behaviour.SELECTOR: 60 | return 'Selector' 61 | elif value == py_trees_msgs.Behaviour.PARALLEL: 62 | return 'Parallel' 63 | elif value == py_trees_msgs.Behaviour.DECORATOR: 64 | return 'Decorator' 65 | elif value == py_trees_msgs.Behaviour.BEHAVIOUR: 66 | return 'Behaviour' 67 | else: 68 | raise TypeError("invalid type specified in message [{}]".format(value)) 69 | 70 | 71 | def msg_constant_to_status_str(value: int) -> str: 72 | """ 73 | Convert one of the status constants in a 74 | :class:`py_trees_ros_interfaces.msg.Behaviour` message to 75 | a human readable string. 76 | 77 | Args: 78 | value: see the message definition for details 79 | 80 | Returns: 81 | status as a string ('Invalid', 'Failure', 'Running', 'Success') 82 | 83 | Raises: 84 | TypeError: if the status type is unrecognised 85 | """ 86 | if value == py_trees_msgs.Behaviour.INVALID: 87 | return 'INVALID' 88 | elif value == py_trees_msgs.Behaviour.RUNNING: 89 | return 'RUNNING' 90 | elif value == py_trees_msgs.Behaviour.SUCCESS: 91 | return 'SUCCESS' 92 | elif value == py_trees_msgs.Behaviour.FAILURE: 93 | return 'FAILURE' 94 | else: 95 | raise TypeError("invalid status specified in message [{}]".format(value)) 96 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License: BSD 5 | # https://raw.githubusercontent.com/splintered-reality/py_trees_ros/devel/LICENSE 6 | # 7 | ############################################################################## 8 | # Documentation 9 | ############################################################################## 10 | 11 | """ 12 | Custom exception types for py_trees_ros_viewer. 13 | """ 14 | 15 | ############################################################################## 16 | # Imports 17 | ############################################################################## 18 | 19 | 20 | class NotReadyError(Exception): 21 | """ 22 | Typically used when methods have been called that expect, but have not 23 | pre-engaged in the ROS2 specific setup typical of py_trees_ros classes 24 | and behaviours. 25 | """ 26 | pass 27 | 28 | 29 | class TimedOutError(Exception): 30 | """ 31 | Timed out waiting (typically) for middleware connections to be established. 32 | """ 33 | pass 34 | -------------------------------------------------------------------------------- /py_trees_ros_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_ros_viewer/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PyTrees Viewer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 25 | 26 | 27 | 30 |
31 |
32 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/images.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | images/tuxrobot.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/images/tuxrobot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees_ros_viewer/1befdf4ce3f0d434ee5ac77147686ea4ae8759b6/py_trees_ros_viewer/images/tuxrobot.png -------------------------------------------------------------------------------- /py_trees_ros_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\x88\x53\ 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 | def qInitResources(): 497 | QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 498 | 499 | def qCleanupResources(): 500 | QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 501 | 502 | qInitResources() 503 | -------------------------------------------------------------------------------- /py_trees_ros_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_ros_viewer/raw/devel/LICENSE 6 | # 7 | ############################################################################## 8 | # Documentation 9 | ############################################################################## 10 | 11 | """ 12 | Launch a qt dashboard for the tutorials. 13 | """ 14 | ############################################################################## 15 | # Imports 16 | ############################################################################## 17 | 18 | import PyQt5.QtCore as qt_core 19 | import PyQt5.QtWidgets as qt_widgets 20 | 21 | from . import main_window_ui 22 | from . import console 23 | 24 | ############################################################################## 25 | # Helpers 26 | ############################################################################## 27 | 28 | 29 | class MainWindow(qt_widgets.QMainWindow): 30 | 31 | request_shutdown = qt_core.pyqtSignal(name="requestShutdown") 32 | topic_selected_automagically = qt_core.pyqtSignal(str, name="topicSelectedAutomagically") 33 | 34 | def __init__(self): 35 | super().__init__() 36 | self.ui = main_window_ui.Ui_MainWindow() 37 | self.ui.setupUi(self) 38 | self.readSettings() 39 | self.ui.web_view_group_box.ui.web_engine_view.loadFinished.connect(self.onLoadFinished) 40 | self.web_app_loaded = False 41 | self.pre_loaded_tree = None 42 | 43 | @qt_core.pyqtSlot(dict) 44 | def on_tree_snapshot_arrived(self, tree): 45 | if self.web_app_loaded: 46 | javascript_command = "render_tree({{tree: {}}})".format(tree) 47 | web_view_page = self.ui.web_view_group_box.ui.web_engine_view.page() 48 | web_view_page.runJavaScript(javascript_command, self.on_tree_rendered) 49 | else: 50 | self.pre_loaded_tree = tree 51 | 52 | def on_tree_rendered(self, response): 53 | """ 54 | Callback triggered on a response being received from the js render_tree method. 55 | """ 56 | console.logdebug("response from js/render_tree ['{}'][window]".format(response)) 57 | 58 | @qt_core.pyqtSlot(list) 59 | def on_discovered_namespaces_changed(self, discovered_namespaces): 60 | console.logdebug("discovered namespaces changed callback {}[window]".format(discovered_namespaces)) 61 | if (discovered_namespaces): 62 | self.ui.send_button.setEnabled(False) 63 | else: 64 | self.ui.send_button.setEnabled(True) 65 | 66 | discovered_namespaces.sort() 67 | 68 | current_selection = self.ui.topic_combo_box.currentText() 69 | if current_selection: 70 | console.logdebug("currently selected namespace: {}".format(current_selection)) 71 | else: 72 | console.logdebug("currently selected namespace: none") 73 | 74 | # if current_selection and current_selection not in discovered_topics: 75 | # discovered_topics.append(current_selection) 76 | 77 | for namespace in discovered_namespaces: 78 | if self.ui.topic_combo_box.findText(namespace) < 0: 79 | self.ui.topic_combo_box.addItem(namespace) 80 | for index in range(self.ui.topic_combo_box.count()): 81 | topic = self.ui.topic_combo_box.itemText(index) 82 | if topic != current_selection: 83 | if topic not in discovered_namespaces: 84 | self.ui.topic_combo_box.removeItem(index) 85 | 86 | if current_selection: 87 | self.ui.topic_combo_box.setCurrentText(current_selection) 88 | else: 89 | self.ui.topic_combo_box.setCurrentIndex(0) 90 | # can't seem to get at the str version of this, always requires the int 91 | # self.ui.topic_combo_box.activated.emit(discovered_namespaces[0]) 92 | # self.topic_selected_automagically.emit(discovered_namespaces[0]) 93 | 94 | @qt_core.pyqtSlot() 95 | def onLoadFinished(self): 96 | console.logdebug("web page loaded [window]") 97 | self.web_app_loaded = True 98 | self.ui.send_button.setEnabled((self.ui.topic_combo_box.currentIndex() == -1)) 99 | self.ui.screenshot_button.setEnabled(True) 100 | self.ui.blackboard_activity_checkbox.setEnabled(True) 101 | self.ui.blackboard_data_checkbox.setEnabled(True) 102 | self.ui.periodic_checkbox.setEnabled(True) 103 | if self.pre_loaded_tree: 104 | javascript_command = "render_tree({{tree: {}}})".format(self.pre_loaded_tree) 105 | web_view_page = self.ui.web_view_group_box.ui.web_engine_view.page() 106 | web_view_page.runJavaScript(javascript_command, self.on_tree_rendered) 107 | 108 | def closeEvent(self, event): 109 | console.logdebug("received close event [window]") 110 | self.request_shutdown.emit() 111 | self.writeSettings() 112 | super().closeEvent(event) 113 | 114 | def readSettings(self): 115 | console.logdebug("read settings [window]") 116 | settings = qt_core.QSettings("Splintered Reality", "PyTrees Viewer") 117 | geometry = settings.value("geometry") 118 | if geometry is not None: 119 | self.restoreGeometry(geometry) 120 | window_state = settings.value("window_state") # full size, maximised, minimised, no state 121 | if window_state is not None: 122 | self.restoreState(window_state) 123 | self.ui.blackboard_data_checkbox.setChecked( 124 | settings.value("blackboard_data", defaultValue=True, type=bool) 125 | ) 126 | self.ui.blackboard_activity_checkbox.setChecked( 127 | settings.value("blackboard_activity", defaultValue=False, type=bool) 128 | ) 129 | self.ui.periodic_checkbox.setChecked( 130 | settings.value("periodic", defaultValue=True, type=bool) 131 | ) 132 | 133 | def writeSettings(self): 134 | console.logdebug("write settings [window]") 135 | settings = qt_core.QSettings("Splintered Reality", "PyTrees Viewer") 136 | settings.setValue("geometry", self.saveGeometry()) 137 | settings.setValue("window_state", self.saveState()) # full size, maximised, minimised, no state 138 | settings.setValue( 139 | "blackboard_data", 140 | self.ui.blackboard_data_checkbox.isChecked() 141 | ) 142 | settings.setValue( 143 | "blackboard_activity", 144 | self.ui.blackboard_activity_checkbox.isChecked() 145 | ) 146 | settings.setValue( 147 | "periodic", 148 | self.ui.periodic_checkbox.isChecked() 149 | ) 150 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/main_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1051 10 | 567 11 | 12 | 13 | 14 | PyTrees Viewer 15 | 16 | 17 | 18 | :/images/tuxrobot.png:/images/tuxrobot.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 0 27 | 0 28 | 29 | 30 | 31 | QFrame::StyledPanel 32 | 33 | 34 | QFrame::Sunken 35 | 36 | 37 | 38 | 39 | 40 | Namespace 41 | 42 | 43 | 44 | 45 | 46 | 47 | QComboBox::InsertAlphabetically 48 | 49 | 50 | QComboBox::AdjustToContents 51 | 52 | 53 | 54 | 55 | 56 | 57 | false 58 | 59 | 60 | Send Demo Tree 61 | 62 | 63 | 64 | 65 | 66 | 67 | Qt::Horizontal 68 | 69 | 70 | 71 | 186 72 | 20 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | false 81 | 82 | 83 | Blackboard Data 84 | 85 | 86 | true 87 | 88 | 89 | 90 | 91 | 92 | 93 | false 94 | 95 | 96 | Blackboard Activity 97 | 98 | 99 | false 100 | 101 | 102 | 103 | 104 | 105 | 106 | false 107 | 108 | 109 | Periodic 110 | 111 | 112 | true 113 | 114 | 115 | 116 | 117 | 118 | 119 | false 120 | 121 | 122 | Screenshot 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 0 134 | 0 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 0 148 | 0 149 | 1051 150 | 29 151 | 152 | 153 | 154 | false 155 | 156 | 157 | 158 | 159 | 160 | 161 | WebViewGroupBox 162 | QGroupBox 163 |
py_trees_ros_viewer.web_view
164 | 1 165 |
166 |
167 | 168 | 169 | 170 | 171 |
172 | -------------------------------------------------------------------------------- /py_trees_ros_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 | class Ui_MainWindow(object): 12 | def setupUi(self, MainWindow): 13 | MainWindow.setObjectName("MainWindow") 14 | MainWindow.resize(1051, 567) 15 | icon = QtGui.QIcon() 16 | icon.addPixmap(QtGui.QPixmap(":/images/tuxrobot.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) 17 | MainWindow.setWindowIcon(icon) 18 | self.central_display = QtWidgets.QWidget(MainWindow) 19 | self.central_display.setObjectName("central_display") 20 | self.verticalLayout = QtWidgets.QVBoxLayout(self.central_display) 21 | self.verticalLayout.setObjectName("verticalLayout") 22 | self.tools_frame = QtWidgets.QFrame(self.central_display) 23 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) 24 | sizePolicy.setHorizontalStretch(0) 25 | sizePolicy.setVerticalStretch(0) 26 | sizePolicy.setHeightForWidth(self.tools_frame.sizePolicy().hasHeightForWidth()) 27 | self.tools_frame.setSizePolicy(sizePolicy) 28 | self.tools_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) 29 | self.tools_frame.setFrameShadow(QtWidgets.QFrame.Sunken) 30 | self.tools_frame.setObjectName("tools_frame") 31 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.tools_frame) 32 | self.horizontalLayout.setObjectName("horizontalLayout") 33 | self.label = QtWidgets.QLabel(self.tools_frame) 34 | self.label.setObjectName("label") 35 | self.horizontalLayout.addWidget(self.label) 36 | self.topic_combo_box = QtWidgets.QComboBox(self.tools_frame) 37 | self.topic_combo_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) 38 | self.topic_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) 39 | self.topic_combo_box.setObjectName("topic_combo_box") 40 | self.horizontalLayout.addWidget(self.topic_combo_box) 41 | self.send_button = QtWidgets.QPushButton(self.tools_frame) 42 | self.send_button.setEnabled(False) 43 | self.send_button.setObjectName("send_button") 44 | self.horizontalLayout.addWidget(self.send_button) 45 | spacerItem = QtWidgets.QSpacerItem(186, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 46 | self.horizontalLayout.addItem(spacerItem) 47 | self.blackboard_data_checkbox = QtWidgets.QCheckBox(self.tools_frame) 48 | self.blackboard_data_checkbox.setEnabled(False) 49 | self.blackboard_data_checkbox.setChecked(True) 50 | self.blackboard_data_checkbox.setObjectName("blackboard_data_checkbox") 51 | self.horizontalLayout.addWidget(self.blackboard_data_checkbox) 52 | self.blackboard_activity_checkbox = QtWidgets.QCheckBox(self.tools_frame) 53 | self.blackboard_activity_checkbox.setEnabled(False) 54 | self.blackboard_activity_checkbox.setChecked(False) 55 | self.blackboard_activity_checkbox.setObjectName("blackboard_activity_checkbox") 56 | self.horizontalLayout.addWidget(self.blackboard_activity_checkbox) 57 | self.periodic_checkbox = QtWidgets.QCheckBox(self.tools_frame) 58 | self.periodic_checkbox.setEnabled(False) 59 | self.periodic_checkbox.setChecked(True) 60 | self.periodic_checkbox.setObjectName("periodic_checkbox") 61 | self.horizontalLayout.addWidget(self.periodic_checkbox) 62 | self.screenshot_button = QtWidgets.QPushButton(self.tools_frame) 63 | self.screenshot_button.setEnabled(False) 64 | self.screenshot_button.setObjectName("screenshot_button") 65 | self.horizontalLayout.addWidget(self.screenshot_button) 66 | self.verticalLayout.addWidget(self.tools_frame) 67 | self.web_view_group_box = WebViewGroupBox(self.central_display) 68 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding) 69 | sizePolicy.setHorizontalStretch(0) 70 | sizePolicy.setVerticalStretch(0) 71 | sizePolicy.setHeightForWidth(self.web_view_group_box.sizePolicy().hasHeightForWidth()) 72 | self.web_view_group_box.setSizePolicy(sizePolicy) 73 | self.web_view_group_box.setTitle("") 74 | self.web_view_group_box.setObjectName("web_view_group_box") 75 | self.verticalLayout.addWidget(self.web_view_group_box) 76 | MainWindow.setCentralWidget(self.central_display) 77 | self.menubar = QtWidgets.QMenuBar(MainWindow) 78 | self.menubar.setGeometry(QtCore.QRect(0, 0, 1051, 29)) 79 | self.menubar.setDefaultUp(False) 80 | self.menubar.setObjectName("menubar") 81 | MainWindow.setMenuBar(self.menubar) 82 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 83 | self.statusbar.setObjectName("statusbar") 84 | MainWindow.setStatusBar(self.statusbar) 85 | 86 | self.retranslateUi(MainWindow) 87 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 88 | 89 | def retranslateUi(self, MainWindow): 90 | _translate = QtCore.QCoreApplication.translate 91 | MainWindow.setWindowTitle(_translate("MainWindow", "PyTrees Viewer")) 92 | self.label.setText(_translate("MainWindow", "Namespace")) 93 | self.send_button.setText(_translate("MainWindow", "Send Demo Tree")) 94 | self.blackboard_data_checkbox.setText(_translate("MainWindow", "Blackboard Data")) 95 | self.blackboard_activity_checkbox.setText(_translate("MainWindow", "Blackboard Activity")) 96 | self.periodic_checkbox.setText(_translate("MainWindow", "Periodic")) 97 | self.screenshot_button.setText(_translate("MainWindow", "Screenshot")) 98 | 99 | from py_trees_ros_viewer.web_view import WebViewGroupBox 100 | from . import images_rc 101 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License: BSD 5 | # https://github.com/splintered-reality/py_trees_ros_viewer/raw/devel/LICENSE 6 | # 7 | ############################################################################## 8 | # Documentation 9 | ############################################################################## 10 | 11 | """ 12 | Utilities for the py_trees ros viewer. 13 | """ 14 | ############################################################################## 15 | # Imports 16 | ############################################################################## 17 | 18 | import rclpy.qos 19 | 20 | ############################################################################## 21 | # Symbols 22 | ############################################################################## 23 | 24 | 25 | class XhtmlSymbols(object): 26 | 27 | def __init__(self): 28 | self.space = ' ' 29 | self.left_arrow = '' 30 | self.right_arrow = '' 31 | self.left_right_arrow = '' 32 | self.bold = '' 33 | self.bold_reset = '' 34 | self.reset = '' 35 | self.normal = '' 36 | self.cyan = '' 37 | self.green = '' 38 | self.yellow = '' 39 | self.red = '' 40 | self.monospace = '' 41 | self.multiplication_x = '' 42 | self.forbidden_circle = '' 43 | 44 | ############################################################################## 45 | # Methods 46 | ############################################################################## 47 | 48 | 49 | def parent_namespace( 50 | name: str 51 | ): 52 | """ 53 | Get the parent namespace of this ros name. 54 | """ 55 | return "/".join(name.split("/")[:-1]) 56 | 57 | 58 | def qos_profile_latched() -> rclpy.qos.QoSProfile: 59 | """ 60 | Convenience retrieval for a latched topic (publisher / subscriber) 61 | """ 62 | return rclpy.qos.QoSProfile( 63 | history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST, 64 | depth=1, 65 | durability=rclpy.qos.QoSDurabilityPolicy.TRANSIENT_LOCAL, 66 | reliability=rclpy.qos.QoSReliabilityPolicy.RELIABLE 67 | ) 68 | 69 | 70 | def normalise_name_strings(name) -> str: 71 | """ 72 | To prepare them for a json dump, they need to have newlines and 73 | superflous apostrophe's removed. 74 | 75 | Args: 76 | name: name to normalise 77 | """ 78 | return name.replace('\n', ' ') 79 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/viewer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License: BSD 5 | # https://github.com/splintered-reality/py_trees_ros_viewer/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 datetime 18 | import functools 19 | import math 20 | import os 21 | import signal 22 | import sys 23 | import threading 24 | import time 25 | 26 | import PyQt5.QtCore as qt_core 27 | import PyQt5.QtWidgets as qt_widgets 28 | 29 | import py_trees_js 30 | import rclpy 31 | 32 | from . import backend as ros_backend 33 | from . import console 34 | from . import main_window 35 | 36 | ############################################################################## 37 | # Helpers 38 | ############################################################################## 39 | 40 | 41 | def send_tree_response(reply): 42 | console.logdebug("reply: '{}' [viewer]".format(reply)) 43 | 44 | 45 | @qt_core.pyqtSlot() 46 | def send_tree(web_view_page, demo_trees, unused_checked): 47 | number_of_trees = len(demo_trees) 48 | send_tree.index = 0 if send_tree.index == (number_of_trees - 1) else send_tree.index + 1 49 | demo_trees[send_tree.index]['timestamp'] = time.time() 50 | console.logdebug("send: tree '{}' [{}][viewer]".format( 51 | send_tree.index, demo_trees[send_tree.index]['timestamp']) 52 | ) 53 | javascript_command = "render_tree({{tree: {}}})".format(demo_trees[send_tree.index]) 54 | web_view_page.runJavaScript(javascript_command, send_tree_response) 55 | 56 | 57 | send_tree.index = 0 58 | 59 | 60 | @qt_core.pyqtSlot() 61 | def capture_screenshot(parent, web_engine_view, unused_checked): 62 | console.logdebug("captured screenshot [viewer]") 63 | file_dialog = qt_widgets.QFileDialog(parent) 64 | file_dialog.setNameFilters( 65 | [ 66 | "BMP Files (*.bmp)", 67 | "JPEG Files (*.jpeg)", 68 | "PNG Files (*.png)" 69 | ] 70 | ) 71 | file_dialog.selectNameFilter("PNG Files (*.png)") 72 | file_dialog.setDefaultSuffix((".png")) 73 | file_dialog.setAcceptMode(qt_widgets.QFileDialog.AcceptSave) 74 | # unfortunately creates a fair amount of spam on stdout 75 | # 'kf5.kio.core: Invalid URL: QUrl("screenshot.jpeg")' 76 | # 'kf5.kio.core: Invalid URL: QUrl("screenshot.png")' 77 | # but...it ain't broke 78 | file_dialog.selectFile("screenshot_{}.png".format(datetime.datetime.now().strftime("%S%M%H%d%m%y"))) 79 | unused_result = file_dialog.exec() 80 | # should be able to restrict it to one file? 81 | for filename in file_dialog.selectedFiles(): 82 | console.logdebug("capturing screenshot: {}".format(filename)) 83 | # This would be simpler, but you can't specify a default filename, nor suffix on linux... 84 | # filename, _ = qt_widgets.QFileDialog.getSaveFileName( 85 | # parent=parent, 86 | # caption="Export to Png", 87 | # directory="screenshot_{}.png".format(datetime.datetime.now().strftime("%S%M%H%d%m%y")), 88 | # filter="BMP Files (*.bmp);;JPEG Files (*.jpeg);;PNG Files (*.png)", # for multiple options, use ;;, e.g. 'All Files (*);;BMP Files (*.bmp);;JPEG Files (*.jpeg);;PNG Files (*.png)' 89 | # initialFilter="PNG Files (*.png)", 90 | # options=options 91 | # ) 92 | # if filename: 93 | # console.loginfo("capturing screenshot: {}".format(filename)) 94 | extension = os.path.splitext(filename)[-1].upper() 95 | if filename.endswith(".png"): 96 | extension = b'PNG' 97 | elif filename.endswith(".bmp"): 98 | extension = b'BMP' 99 | elif filename.endswith(".jpeg"): 100 | extension = b'JPEG' 101 | else: 102 | extension = b'PNG' 103 | web_engine_view.grab().save(filename, extension) 104 | 105 | 106 | def on_blackboard_data_checked(backend, state: qt_core.Qt.Checked): 107 | with backend.lock: 108 | backend.parameters.blackboard_data = True if state == qt_core.Qt.Checked else False 109 | if state == qt_core.Qt.Checked: 110 | console.logdebug("Blackboard data requested") 111 | else: 112 | console.logdebug("Blackboard data disabled") 113 | 114 | 115 | def on_blackboard_activity_checked(backend, state: qt_core.Qt.Checked): 116 | with backend.lock: 117 | backend.parameters.blackboard_activity = True if state == qt_core.Qt.Checked else False 118 | if state == qt_core.Qt.Checked: 119 | console.logdebug("Blackboard activity requested") 120 | else: 121 | console.logdebug("Blackboard activity disabled") 122 | 123 | 124 | def on_periodic_checked(backend, state: qt_core.Qt.Checked): 125 | with backend.lock: 126 | backend.parameters.snapshot_period = 2.0 if state == qt_core.Qt.Checked else math.inf 127 | if state == qt_core.Qt.Checked: 128 | console.logdebug("Periodic snapshots requested") 129 | else: 130 | console.logdebug("Periodic snapshots disabled") 131 | 132 | 133 | def on_connection_request(backend, namespace: str): 134 | """ 135 | Enqueue a connection request. 136 | 137 | Cannot directly make the connection here since this is invariably the qt thread. 138 | """ 139 | with backend.lock: 140 | backend.enqueued_connection_request_namespace = namespace 141 | 142 | ############################################################################## 143 | # Main 144 | ############################################################################## 145 | 146 | 147 | def main(): 148 | # logging 149 | console.log_level = console.LogLevel.DEBUG 150 | 151 | # ros init 152 | rclpy.init() 153 | 154 | # the players 155 | app = qt_widgets.QApplication(sys.argv) 156 | demo_trees = py_trees_js.viewer.trees.create_demo_tree_list() 157 | window = main_window.MainWindow() 158 | snapshot_period = 2.0 if window.ui.periodic_checkbox.isChecked() else math.inf 159 | backend = ros_backend.Backend( 160 | parameters=ros_backend.SnapshotStream.Parameters( 161 | blackboard_data=window.ui.blackboard_data_checkbox.isChecked(), 162 | blackboard_activity=window.ui.blackboard_activity_checkbox.isChecked(), 163 | snapshot_period=snapshot_period) 164 | ) 165 | 166 | # sig interrupt handling 167 | # use a timer to get out of the gui thread and 168 | # permit python a chance to catch the signal 169 | # https://stackoverflow.com/questions/4938723/what-is-the-correct-way-to-make-my-pyqt-application-quit-when-killed-from-the-co 170 | def on_shutdown(unused_signal, unused_frame): 171 | console.logdebug("received interrupt signal [viewer]") 172 | window.close() 173 | 174 | signal.signal(signal.SIGINT, on_shutdown) 175 | timer = qt_core.QTimer() 176 | timer.timeout.connect(lambda: None) 177 | timer.start(250) 178 | 179 | # sigslots 180 | window.ui.send_button.clicked.connect( 181 | functools.partial( 182 | send_tree, 183 | window.ui.web_view_group_box.ui.web_engine_view.page(), 184 | demo_trees 185 | ) 186 | ) 187 | window.ui.screenshot_button.clicked.connect( 188 | functools.partial( 189 | capture_screenshot, 190 | window, 191 | window.ui.web_view_group_box.ui.web_engine_view, 192 | ) 193 | ) 194 | window.ui.blackboard_data_checkbox.stateChanged.connect( 195 | functools.partial( 196 | on_blackboard_data_checked, 197 | backend, 198 | ) 199 | ) 200 | window.ui.blackboard_activity_checkbox.stateChanged.connect( 201 | functools.partial( 202 | on_blackboard_activity_checked, 203 | backend, 204 | ) 205 | ) 206 | window.ui.periodic_checkbox.stateChanged.connect( 207 | functools.partial( 208 | on_periodic_checked, 209 | backend, 210 | ) 211 | ) 212 | window.ui.topic_combo_box.currentTextChanged.connect( 213 | functools.partial( 214 | on_connection_request, 215 | backend 216 | ) 217 | ) 218 | 219 | backend.discovered_namespaces_changed.connect(window.on_discovered_namespaces_changed) 220 | backend.tree_snapshot_arrived.connect(window.on_tree_snapshot_arrived) 221 | # two signals for the combo box are relevant 222 | # activated - only when there is a user interaction 223 | # currentTextChanged - when there is a programmatic OR user interaction 224 | # window.ui.topic_combo_box.activated.connect(backend.connect) 225 | window.request_shutdown.connect(backend.terminate_ros_spinner) 226 | 227 | # qt/ros bringup 228 | ros_thread = threading.Thread(target=backend.spin) 229 | ros_thread.start() 230 | window.show() 231 | result = app.exec_() 232 | 233 | # shutdown 234 | backend.node.get_logger().info("waiting for backend to terminate [viewer]") 235 | ros_thread.join() 236 | rclpy.shutdown() 237 | sys.exit(result) 238 | -------------------------------------------------------------------------------- /py_trees_ros_viewer/web_app.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | html/index.html 4 | 5 | 6 | -------------------------------------------------------------------------------- /py_trees_ros_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\x08\x0b\ 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\x3e\x0a\x20\x20\x3c\x6c\x69\ 23 | \x6e\x6b\x20\x72\x65\x6c\x3d\x22\x73\x74\x79\x6c\x65\x73\x68\x65\ 24 | \x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x63\ 25 | \x73\x73\x22\x20\x68\x72\x65\x66\x3d\x22\x6a\x73\x2f\x6a\x6f\x69\ 26 | \x6e\x74\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\x2d\x33\x2e\x30\x2e\x34\ 27 | \x2e\x6d\x69\x6e\x2e\x63\x73\x73\x22\x2f\x3e\x0a\x20\x20\x3c\x73\ 28 | \x63\x72\x69\x70\x74\x20\x73\x72\x63\x3d\x22\x6a\x73\x2f\x6a\x6f\ 29 | \x69\x6e\x74\x6a\x73\x2f\x64\x61\x67\x72\x65\x2d\x30\x2e\x38\x2e\ 30 | \x34\x2e\x6d\x69\x6e\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\ 31 | \x70\x74\x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\ 32 | \x63\x3d\x22\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\x6a\x73\x2f\x67\x72\ 33 | \x61\x70\x68\x6c\x69\x62\x2d\x32\x2e\x31\x2e\x37\x2e\x6d\x69\x6e\ 34 | \x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x20\ 35 | \x20\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\x3d\x22\x6a\x73\ 36 | \x2f\x6a\x6f\x69\x6e\x74\x6a\x73\x2f\x6a\x71\x75\x65\x72\x79\x2d\ 37 | \x33\x2e\x34\x2e\x31\x2e\x6d\x69\x6e\x2e\x6a\x73\x22\x3e\x3c\x2f\ 38 | \x73\x63\x72\x69\x70\x74\x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\x70\ 39 | \x74\x20\x73\x72\x63\x3d\x22\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\x6a\ 40 | \x73\x2f\x6c\x6f\x64\x61\x73\x68\x2d\x34\x2e\x31\x37\x2e\x31\x31\ 41 | \x2e\x6d\x69\x6e\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\ 42 | \x74\x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\ 43 | \x3d\x22\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\x6a\x73\x2f\x62\x61\x63\ 44 | \x6b\x62\x6f\x6e\x65\x2d\x31\x2e\x34\x2e\x30\x2e\x6a\x73\x22\x3e\ 45 | \x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x20\x20\x3c\x73\x63\x72\ 46 | \x69\x70\x74\x20\x73\x72\x63\x3d\x22\x6a\x73\x2f\x6a\x6f\x69\x6e\ 47 | \x74\x6a\x73\x2f\x6a\x6f\x69\x6e\x74\x2d\x33\x2e\x30\x2e\x34\x2e\ 48 | \x6d\x69\x6e\x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\ 49 | \x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\x3d\ 50 | \x22\x6a\x73\x2f\x70\x79\x5f\x74\x72\x65\x65\x73\x2d\x30\x2e\x36\ 51 | \x2e\x6a\x73\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x20\ 52 | \x20\x3c\x73\x74\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x68\x74\x6d\ 53 | \x6c\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\x68\x65\x69\x67\x68\x74\ 54 | \x3a\x20\x31\x30\x30\x25\x20\x20\x2f\x2a\x20\x66\x6f\x72\x20\x74\ 55 | \x68\x65\x20\x63\x61\x6e\x76\x61\x73\x20\x74\x6f\x20\x66\x69\x6c\ 56 | \x6c\x20\x74\x68\x65\x20\x73\x63\x72\x65\x65\x6e\x2c\x20\x6e\x65\ 57 | \x65\x64\x20\x68\x65\x69\x67\x68\x74\x73\x20\x63\x61\x73\x63\x61\ 58 | \x64\x65\x64\x20\x66\x72\x6f\x6d\x20\x74\x68\x65\x20\x74\x6f\x70\ 59 | \x20\x2a\x2f\x0a\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x62\x6f\ 60 | \x64\x79\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\x6d\x61\x72\x67\x69\ 61 | \x6e\x3a\x20\x30\x3b\x0a\x20\x20\x20\x20\x20\x20\x6f\x76\x65\x72\ 62 | \x66\x6c\x6f\x77\x3a\x68\x69\x64\x64\x65\x6e\x3b\x20\x20\x2f\x2a\ 63 | \x20\x6e\x6f\x20\x73\x63\x72\x6f\x6c\x6c\x62\x61\x72\x73\x20\x2a\ 64 | \x2f\x0a\x20\x20\x20\x20\x20\x20\x68\x65\x69\x67\x68\x74\x3a\x20\ 65 | \x31\x30\x30\x25\x20\x20\x2f\x2a\x20\x66\x6f\x72\x20\x74\x68\x65\ 66 | \x20\x63\x61\x6e\x76\x61\x73\x20\x74\x6f\x20\x66\x69\x6c\x6c\x20\ 67 | \x74\x68\x65\x20\x73\x63\x72\x65\x65\x6e\x2c\x20\x6e\x65\x65\x64\ 68 | \x20\x68\x65\x69\x67\x68\x74\x73\x20\x63\x61\x73\x63\x61\x64\x65\ 69 | \x64\x20\x66\x72\x6f\x6d\x20\x74\x68\x65\x20\x74\x6f\x70\x20\x2a\ 70 | \x2f\x0a\x20\x20\x20\x20\x7d\x0a\x20\x20\x3c\x2f\x73\x74\x79\x6c\ 71 | \x65\x3e\x0a\x3c\x2f\x68\x65\x61\x64\x3e\x0a\x3c\x62\x6f\x64\x79\ 72 | \x3e\x0a\x20\x20\x3c\x73\x63\x72\x69\x70\x74\x20\x74\x79\x70\x65\ 73 | \x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\ 74 | \x74\x22\x3e\x0a\x20\x20\x20\x20\x70\x79\x5f\x74\x72\x65\x65\x73\ 75 | \x2e\x68\x65\x6c\x6c\x6f\x28\x29\x0a\x20\x20\x3c\x2f\x73\x63\x72\ 76 | \x69\x70\x74\x3e\x0a\x20\x20\x3c\x64\x69\x76\x20\x69\x64\x3d\x22\ 77 | \x63\x61\x6e\x76\x61\x73\x22\x3e\x3c\x2f\x64\x69\x76\x3e\x0a\x20\ 78 | \x20\x3c\x64\x69\x76\x20\x69\x64\x3d\x22\x74\x69\x6d\x65\x6c\x69\ 79 | \x6e\x65\x22\x3e\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x3c\x73\x63\ 80 | \x72\x69\x70\x74\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\ 81 | \x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x3e\x0a\x20\x20\x20\ 82 | \x20\x2f\x2f\x20\x72\x65\x6e\x64\x65\x72\x69\x6e\x67\x20\x63\x61\ 83 | \x6e\x76\x61\x73\x0a\x20\x20\x20\x20\x63\x61\x6e\x76\x61\x73\x5f\ 84 | \x67\x72\x61\x70\x68\x20\x3d\x20\x70\x79\x5f\x74\x72\x65\x65\x73\ 85 | \x2e\x63\x61\x6e\x76\x61\x73\x2e\x63\x72\x65\x61\x74\x65\x5f\x67\ 86 | \x72\x61\x70\x68\x28\x29\x0a\x20\x20\x20\x20\x63\x61\x6e\x76\x61\ 87 | \x73\x5f\x70\x61\x70\x65\x72\x20\x3d\x20\x70\x79\x5f\x74\x72\x65\ 88 | \x65\x73\x2e\x63\x61\x6e\x76\x61\x73\x2e\x63\x72\x65\x61\x74\x65\ 89 | \x5f\x70\x61\x70\x65\x72\x28\x7b\x67\x72\x61\x70\x68\x3a\x20\x63\ 90 | \x61\x6e\x76\x61\x73\x5f\x67\x72\x61\x70\x68\x7d\x29\x0a\x0a\x20\ 91 | \x20\x20\x20\x2f\x2f\x20\x65\x76\x65\x6e\x74\x20\x74\x69\x6d\x65\ 92 | \x6c\x69\x6e\x65\x0a\x20\x20\x20\x20\x74\x69\x6d\x65\x6c\x69\x6e\ 93 | \x65\x5f\x67\x72\x61\x70\x68\x20\x3d\x20\x70\x79\x5f\x74\x72\x65\ 94 | \x65\x73\x2e\x74\x69\x6d\x65\x6c\x69\x6e\x65\x2e\x63\x72\x65\x61\ 95 | \x74\x65\x5f\x67\x72\x61\x70\x68\x28\x7b\x65\x76\x65\x6e\x74\x5f\ 96 | \x63\x61\x63\x68\x65\x5f\x6c\x69\x6d\x69\x74\x3a\x20\x31\x30\x30\ 97 | \x7d\x29\x3b\x0a\x20\x20\x20\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\ 98 | \x5f\x70\x61\x70\x65\x72\x20\x3d\x20\x70\x79\x5f\x74\x72\x65\x65\ 99 | \x73\x2e\x74\x69\x6d\x65\x6c\x69\x6e\x65\x2e\x63\x72\x65\x61\x74\ 100 | \x65\x5f\x70\x61\x70\x65\x72\x28\x7b\x0a\x20\x20\x20\x20\x20\x20\ 101 | \x20\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x5f\x67\x72\x61\x70\x68\ 102 | \x3a\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x5f\x67\x72\x61\x70\x68\ 103 | \x2c\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x63\x61\x6e\x76\x61\x73\ 104 | \x5f\x67\x72\x61\x70\x68\x3a\x20\x63\x61\x6e\x76\x61\x73\x5f\x67\ 105 | \x72\x61\x70\x68\x2c\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x63\x61\ 106 | \x6e\x76\x61\x73\x5f\x70\x61\x70\x65\x72\x3a\x20\x63\x61\x6e\x76\ 107 | \x61\x73\x5f\x70\x61\x70\x65\x72\x2c\x0a\x20\x20\x20\x20\x7d\x29\ 108 | \x0a\x0a\x20\x20\x20\x20\x2f\x2f\x20\x72\x65\x61\x63\x74\x20\x74\ 109 | \x6f\x20\x77\x69\x6e\x64\x6f\x77\x20\x72\x65\x73\x69\x7a\x69\x6e\ 110 | \x67\x20\x65\x76\x65\x6e\x74\x73\x0a\x20\x20\x20\x20\x24\x28\x77\ 111 | \x69\x6e\x64\x6f\x77\x29\x2e\x72\x65\x73\x69\x7a\x65\x28\x66\x75\ 112 | \x6e\x63\x74\x69\x6f\x6e\x28\x29\x20\x7b\x0a\x20\x20\x20\x20\x20\ 113 | \x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x63\x61\x6e\x76\x61\x73\ 114 | \x2e\x6f\x6e\x5f\x77\x69\x6e\x64\x6f\x77\x5f\x72\x65\x73\x69\x7a\ 115 | \x65\x28\x63\x61\x6e\x76\x61\x73\x5f\x70\x61\x70\x65\x72\x29\x0a\ 116 | \x20\x20\x20\x20\x20\x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x74\ 117 | \x69\x6d\x65\x6c\x69\x6e\x65\x2e\x6f\x6e\x5f\x77\x69\x6e\x64\x6f\ 118 | \x77\x5f\x72\x65\x73\x69\x7a\x65\x28\x74\x69\x6d\x65\x6c\x69\x6e\ 119 | \x65\x5f\x70\x61\x70\x65\x72\x29\x0a\x20\x20\x20\x20\x7d\x29\x0a\ 120 | \x0a\x20\x20\x20\x20\x72\x65\x6e\x64\x65\x72\x5f\x74\x72\x65\x65\ 121 | \x20\x3d\x20\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x7b\x74\x72\x65\ 122 | \x65\x7d\x29\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\x63\x6f\x6e\x73\ 123 | \x6f\x6c\x65\x2e\x6c\x6f\x67\x28\x22\x67\x6f\x74\x20\x73\x6f\x6d\ 124 | \x65\x74\x68\x69\x6e\x67\x22\x2c\x20\x74\x72\x65\x65\x29\x0a\x0a\ 125 | \x20\x20\x20\x20\x20\x20\x2f\x2f\x20\x69\x66\x20\x74\x68\x65\x72\ 126 | \x65\x20\x69\x73\x20\x61\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x0a\ 127 | \x20\x20\x20\x20\x20\x20\x70\x79\x5f\x74\x72\x65\x65\x73\x2e\x74\ 128 | \x69\x6d\x65\x6c\x69\x6e\x65\x2e\x61\x64\x64\x5f\x74\x72\x65\x65\ 129 | \x5f\x74\x6f\x5f\x63\x61\x63\x68\x65\x28\x7b\x0a\x20\x20\x20\x20\ 130 | \x20\x20\x20\x20\x20\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x5f\x67\ 131 | \x72\x61\x70\x68\x3a\x20\x74\x69\x6d\x65\x6c\x69\x6e\x65\x5f\x67\ 132 | \x72\x61\x70\x68\x2c\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ 133 | \x63\x61\x6e\x76\x61\x73\x5f\x67\x72\x61\x70\x68\x3a\x20\x63\x61\ 134 | \x6e\x76\x61\x73\x5f\x67\x72\x61\x70\x68\x2c\x0a\x20\x20\x20\x20\ 135 | \x20\x20\x20\x20\x20\x20\x63\x61\x6e\x76\x61\x73\x5f\x70\x61\x70\ 136 | \x65\x72\x3a\x20\x63\x61\x6e\x76\x61\x73\x5f\x70\x61\x70\x65\x72\ 137 | \x2c\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x74\x72\x65\x65\ 138 | \x3a\x20\x74\x72\x65\x65\x0a\x20\x20\x20\x20\x20\x20\x7d\x29\x0a\ 139 | \x20\x20\x20\x20\x20\x20\x72\x65\x74\x75\x72\x6e\x20\x22\x72\x65\ 140 | \x6e\x64\x65\x72\x65\x64\x22\x0a\x20\x20\x20\x20\x7d\x0a\x20\x20\ 141 | \x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x0a\x3c\x2f\x62\x6f\x64\x79\ 142 | \x3e\x0a\x3c\x2f\x68\x74\x6d\x6c\x3e\x0a\ 143 | " 144 | 145 | qt_resource_name = b"\ 146 | \x00\x0a\ 147 | \x0c\xba\xf2\x7c\ 148 | \x00\x69\ 149 | \x00\x6e\x00\x64\x00\x65\x00\x78\x00\x2e\x00\x68\x00\x74\x00\x6d\x00\x6c\ 150 | " 151 | 152 | qt_resource_struct_v1 = b"\ 153 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 154 | \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 155 | " 156 | 157 | qt_resource_struct_v2 = b"\ 158 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 159 | \x00\x00\x00\x00\x00\x00\x00\x00\ 160 | \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 161 | \x00\x00\x01\x6f\x4d\x6e\xc0\x02\ 162 | " 163 | 164 | qt_version = QtCore.qVersion().split('.') 165 | if qt_version < ['5', '8', '0']: 166 | rcc_version = 1 167 | qt_resource_struct = qt_resource_struct_v1 168 | else: 169 | rcc_version = 2 170 | qt_resource_struct = qt_resource_struct_v2 171 | 172 | def qInitResources(): 173 | QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 174 | 175 | def qCleanupResources(): 176 | QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 177 | 178 | qInitResources() 179 | -------------------------------------------------------------------------------- /py_trees_ros_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_ros_viewer/raw/devel/LICENSE 6 | # 7 | ############################################################################## 8 | # Documentation 9 | ############################################################################## 10 | 11 | """ 12 | Launch a qt dashboard for the tutorials. 13 | """ 14 | ############################################################################## 15 | # Imports 16 | ############################################################################## 17 | 18 | import PyQt5.QtWidgets as qt_widgets 19 | import PyQt5.QtCore as qt_core 20 | 21 | from . import web_view_ui 22 | 23 | # Import the javascript libraries (via qrc bundles) 24 | import py_trees_js.resources 25 | 26 | ############################################################################## 27 | # Helpers 28 | ############################################################################## 29 | 30 | 31 | class WebViewGroupBox(qt_widgets.QGroupBox): 32 | """ 33 | Convenience class that Designer can use to promote 34 | elements for layouts in applications. 35 | """ 36 | 37 | change_battery_percentage = qt_core.pyqtSignal(float, name="changeBatteryPercentage") 38 | change_battery_charging_status = qt_core.pyqtSignal(bool, name="changeBatteryChargingStatus") 39 | change_safety_sensors_enabled = qt_core.pyqtSignal(bool, name="safetySensorsEnabled") 40 | 41 | def __init__(self, parent): 42 | super().__init__(parent) 43 | self.ui = web_view_ui.Ui_WebViewGroupBox() 44 | self.ui.setupUi(self) 45 | # Currently auto-loading the web app (index.html) from the url setting 46 | # via Designer, so everything is self-contained in Ui_WebViewGroupBox setup. 47 | # 48 | # Alternatively, set the dynamic property for the 49 | # WebEngineView's URL to about:blank and load manually: 50 | # 51 | # self.ui.web_engine_view.load(qt_core.QUrl("qrc:/index.html")) 52 | -------------------------------------------------------------------------------- /py_trees_ros_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_ros_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 | class Ui_WebViewGroupBox(object): 12 | def setupUi(self, WebViewGroupBox): 13 | WebViewGroupBox.setObjectName("WebViewGroupBox") 14 | WebViewGroupBox.resize(400, 300) 15 | self.web_view_layout = QtWidgets.QVBoxLayout(WebViewGroupBox) 16 | self.web_view_layout.setObjectName("web_view_layout") 17 | self.web_engine_view = QtWebEngineWidgets.QWebEngineView(WebViewGroupBox) 18 | self.web_engine_view.setUrl(QtCore.QUrl("qrc:/index.html")) 19 | self.web_engine_view.setObjectName("web_engine_view") 20 | self.web_view_layout.addWidget(self.web_engine_view) 21 | 22 | self.retranslateUi(WebViewGroupBox) 23 | QtCore.QMetaObject.connectSlotsByName(WebViewGroupBox) 24 | 25 | def retranslateUi(self, WebViewGroupBox): 26 | _translate = QtCore.QCoreApplication.translate 27 | WebViewGroupBox.setWindowTitle(_translate("WebViewGroupBox", "Tree View")) 28 | WebViewGroupBox.setTitle(_translate("WebViewGroupBox", "GroupBox")) 29 | 30 | from PyQt5 import QtWebEngineWidgets 31 | from . import web_app_rc 32 | -------------------------------------------------------------------------------- /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=12345 12 | DIR=/tmp/chrome-devel-configuration 13 | 14 | ########################### 15 | # Generate Resources 16 | ########################### 17 | 18 | cd ${PWD}/../py_trees_ros_viewer && ./gen.bash 19 | 20 | ########################### 21 | # Setup 22 | ########################### 23 | 24 | # Clear out the old user data 25 | rm -rf ${DIR} 26 | 27 | # Sync'ing to a user directory doesn't seem to be necessary if I use $OPTIONS1 28 | 29 | #if [ ! -d "${DIR}" ]; then 30 | # mkdir -p ${DIR} 31 | # rsync -av --delete --exclude=/Singleton* --exclude=/Session* ~/.config/google-chrome/ ${DIR} 32 | #fi 33 | 34 | ########################### 35 | # Launch 36 | ########################### 37 | OPTIONS1="--no-first-run --activate-on-launch --no-default-browser-check --allow-file-access-from-files" 38 | # These are to work around chrome 80+ incompatibility: 39 | # https://stackoverflow.com/questions/60182668/chrome-devtools-inspector-showing-blank-white-screen-while-debugging-with-samsun 40 | OPTIONS2="--enable-blink-features=ShadowDOMV0 --enable-blink-features=CustomElementsV0" 41 | google-chrome ${OPTIONS1} ${OPTIONS2} --user-data-dir=${DIR} --app=http://127.0.0.1:12345 > /dev/null 2>&1 & 42 | pid=$! 43 | 44 | py-trees-tree-viewer 45 | 46 | kill -s 9 $pid > /dev/null 2>&1 || exit 0 47 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pep8] 2 | max-line-length=299 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | from distutils import log 6 | from setuptools import find_packages, setup 7 | from setuptools.command.develop import develop 8 | from setuptools.command.install import install 9 | 10 | package_name = 'py_trees_ros_viewer' 11 | 12 | 13 | # This is somewhat dodgy as it will escape any override from, e.g. the command 14 | # line or a setup.cfg configuration. It does however, get us around the problem 15 | # of setup.cfg influencing requirements install on rtd installs 16 | # 17 | # TODO: should be a way of detecting whether scripts_dir has been influenced 18 | # from outside 19 | def redirect_install_dir(command_subclass): 20 | 21 | original_run = command_subclass.run 22 | 23 | def modified_run(self): 24 | try: 25 | old_script_dir = self.script_dir # develop 26 | except AttributeError: 27 | old_script_dir = self.install_scripts # install 28 | # TODO: A more intelligent way of stitching this together... 29 | # Warning: script_dir is typically a 'bin' path alongside the 30 | # lib path, if ever that is somewhere wildly different, this 31 | # will break. 32 | # Note: Consider making use of self.prefix, but in some cases 33 | # that is mislading, e.g. points to /usr when actually 34 | # everything goes to /usr/local 35 | new_script_dir = os.path.abspath( 36 | os.path.join( 37 | old_script_dir, os.pardir, 'lib', package_name 38 | ) 39 | ) 40 | log.info("redirecting scripts") 41 | log.info(" from: {}".format(old_script_dir)) 42 | log.info(" to: {}".format(new_script_dir)) 43 | if hasattr(self, "script_dir"): 44 | self.script_dir = new_script_dir # develop 45 | else: 46 | self.install_scripts = new_script_dir # install 47 | original_run(self) 48 | 49 | command_subclass.run = modified_run 50 | return command_subclass 51 | 52 | 53 | @redirect_install_dir 54 | class OverrideDevelop(develop): 55 | pass 56 | 57 | 58 | @redirect_install_dir 59 | class OverrideInstall(install): 60 | pass 61 | 62 | 63 | setup( 64 | # cmdclass={ 65 | # 'develop': OverrideDevelop, 66 | # 'install': OverrideInstall 67 | # }, 68 | name=package_name, 69 | version='0.2.5', # also package.xml 70 | packages=find_packages(exclude=['tests*', 'docs*']), 71 | data_files=[('share/' + package_name, ['package.xml'])], 72 | # scripts=['scripts/py-trees-devel-viewer'], not working, but not critical 73 | package_data={'py_trees_ros_viewer': ['*.ui', 'html/*', 'images/*']}, 74 | install_requires=[], # it's all lies (c.f. package.xml, but no use case for this yet) 75 | extras_require={}, 76 | author='Daniel Stonier', 77 | maintainer='Daniel Stonier , Sebastian Castro ', 78 | url='https://github.com/splintered-reality/py_trees_ros_viewer', 79 | keywords=['ROS', 'ROS2', 'behaviour-trees', 'Qt', 'Visualisation'], 80 | zip_safe=True, 81 | classifiers=[ 82 | 'Intended Audience :: Developers', 83 | 'License :: OSI Approved :: BSD License', 84 | 'Programming Language :: Python', 85 | 'Topic :: Scientific/Engineering :: Artificial Intelligence', 86 | 'Topic :: Software Development :: Libraries' 87 | ], 88 | description=( 89 | "A Qt-JS hybrid viewer for visualising executing or log-replayed behaviour trees" 90 | ), 91 | long_description=( 92 | "A Qt-JS hybrid viewer for visualising executing or log-replayed behaviour trees" 93 | ), 94 | license='BSD', 95 | # test_suite="tests" 96 | # tests_require=['nose', 'pytest', 'flake8', 'yanc', 'nose-htmloutput'] 97 | entry_points={ 98 | 'console_scripts': [ 99 | 'py-trees-tree-viewer = py_trees_ros_viewer.viewer:main', 100 | ], 101 | }, 102 | ) 103 | -------------------------------------------------------------------------------- /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_ros_viewer") 12 | --------------------------------------------------------------------------------