Skip to content

Commit eeabfcb

Browse files
authored
Modes observer (#68)
* Initial mode observer class * Mutex modes cache for concurrent read/write * Check for service availability- for the initial service calls, check for service availability first, timeout 500ms. * Minimal unit tests and launch test for modes observer * Documentation of mode observer #66 Signed-off-by: Nordmann Arne (CR/ADT3) <arne.nordmann@de.bosch.com>
1 parent 8ef3b27 commit eeabfcb

12 files changed

Lines changed: 751 additions & 3 deletions

system_modes/CMakeLists.txt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ add_library(mode SHARED
4141
src/system_modes/mode.cpp
4242
src/system_modes/mode_impl.cpp
4343
src/system_modes/mode_handling.cpp
44-
src/system_modes/mode_inference.cpp)
44+
src/system_modes/mode_inference.cpp
45+
src/system_modes/mode_observer.cpp)
4546
target_include_directories(mode PUBLIC
4647
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
4748
$<INSTALL_INTERFACE:include>)
@@ -179,14 +180,32 @@ if(BUILD_TESTING)
179180
target_link_libraries(test_mode_monitor mode)
180181
endif()
181182

183+
ament_add_gtest(test_mode_observer test/test_mode_observer.cpp)
184+
if(TARGET test_mode_observer)
185+
target_include_directories(test_mode_observer PUBLIC
186+
${rclcpp_INCLUDE_DIRS}
187+
${CMAKE_CURRENT_BINARY_DIR}/system_modes/
188+
)
189+
target_link_libraries(test_mode_observer mode)
190+
endif()
191+
192+
# mode observer test node
193+
add_executable(modes_observer_test_node test/launchtest/modes_observer_test_node.cpp)
194+
target_include_directories(modes_observer_test_node PUBLIC
195+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
196+
$<INSTALL_INTERFACE:include>)
197+
target_link_libraries(modes_observer_test_node mode)
198+
install(TARGETS modes_observer_test_node DESTINATION lib/${PROJECT_NAME})
199+
182200
# Launch Tests
183201
find_package(launch_testing_ament_cmake REQUIRED)
184202
set(launch_tests
185203
"two_lifecycle_nodes" # Mode Manager with Lifecycle Nodes
186204
"two_mixed_nodes" # Mode Manager with Lifecycle and non-Lifecycle Nodes
187205
"two_independent_hierarchies" # Mode Manager for two independent hierarchies of nodes
188206
"manager_and_monitor" # Mode Manager and Mode Monitor
189-
"redundant_mode_changes") # Ignore redundant mode changes
207+
"redundant_mode_changes" # Ignore redundant mode changes
208+
"modes_observer") # Mode Observer
190209

191210
# Launch Test: Mode Manager with Lifecycle Nodes
192211
foreach(test_name ${launch_tests})

system_modes/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ Since the introduced (sub-)systems are not concrete software entities, their sta
8989
Before attempting a state or mode change for a system or node, the [mode manager](#mode_manager) publishes information about the request.
9090
The according topics might need to be *latched* in order to allow nodes to do the inference after joining a running system.
9191

92+
#### Mode Observer
93+
94+
Additionally, the library comprises a *Mode Observer* that serves as a local cache of states and modes of all observed system parts. The mode observer will try to obtain the current state and mode initially via sevrice calls (GetState, GetMode) and subsequently monitors according transitions events and mode events.
95+
96+
The mode observer is supposed to be instantiated within a ROS 2 node to have states and modes available locally for fast access, see this exemplary [ModeObserverNode](https://github.com/micro-ROS/system_modes/blob/master/system_modes/test/launchtest/modes_observer_test_node.cpp) used for testing.
97+
9298
### Mode Manager
9399

94100
The mode manager is a ROS node that accepts an SHM file (see [above](#system-modes)) as command line parameter. It parses the SHM file and creates the according services, publishers, and subscribers to manage the system and its modes.

system_modes/include/system_modes/mode_impl.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ struct StateAndMode
9090
}
9191
return state_label_(state) + "." + mode;
9292
}
93+
94+
bool unknown()
95+
{
96+
return state == State::PRIMARY_STATE_UNKNOWN;
97+
}
9398
};
9499

95100
class ModeImpl
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) 2018 - for information on the respective copyright owner
2+
// see the NOTICE file and/or the repository https://github.com/microros/system_modes
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
#pragma once
16+
17+
#include <rclcpp/rclcpp.hpp>
18+
19+
#include <lifecycle_msgs/msg/transition_event.hpp>
20+
#include <lifecycle_msgs/srv/get_state.hpp>
21+
22+
#include <map>
23+
#include <memory>
24+
#include <mutex>
25+
#include <string>
26+
#include <utility>
27+
28+
#include "system_modes/mode.hpp"
29+
#include "system_modes/msg/mode_event.hpp"
30+
#include "system_modes/srv/get_mode.hpp"
31+
32+
namespace system_modes
33+
{
34+
35+
using std::map;
36+
using std::mutex;
37+
using std::string;
38+
39+
using lifecycle_msgs::msg::TransitionEvent;
40+
using lifecycle_msgs::srv::GetState;
41+
using rclcpp::node_interfaces::NodeBaseInterface;
42+
using system_modes::msg::ModeEvent;
43+
using system_modes::srv::GetMode;
44+
45+
/**
46+
* Mode observer provides a local system modes cache.
47+
*
48+
* The mode observer serves as a local system modes cache, instantiated by a node. The mode
49+
* observer will initially try to gain current states/modes from all observes entities and will
50+
* then continuously monitor transitions to keep up to date.
51+
*/
52+
class ModeObserver
53+
{
54+
public:
55+
explicit ModeObserver(std::weak_ptr<rclcpp::Node>);
56+
virtual ~ModeObserver() = default;
57+
58+
/**
59+
* Getter for observed state and mode.
60+
*
61+
* Returns cached observed state and mode for requested system entity (system or node).
62+
*/
63+
virtual StateAndMode
64+
get(const string & part_name);
65+
66+
/**
67+
* Add part to list of observed parts.
68+
*/
69+
virtual void
70+
observe(const string & part_name);
71+
72+
/**
73+
* Remove part from list of observed parts.
74+
*/
75+
virtual void
76+
stop_observing(const string & part_name);
77+
78+
protected:
79+
virtual void
80+
transition_callback(
81+
const TransitionEvent::SharedPtr msg,
82+
const string & part_name);
83+
84+
virtual void
85+
mode_event_callback(
86+
const ModeEvent::SharedPtr msg,
87+
const string & part_name);
88+
89+
private:
90+
std::weak_ptr<rclcpp::Node> node_handle_;
91+
map<string, StateAndMode> cache_;
92+
mutable std::shared_timed_mutex mutex_;
93+
94+
map<string, std::shared_ptr<rclcpp::Subscription<TransitionEvent>>> state_subs_;
95+
map<string, std::shared_ptr<rclcpp::Subscription<ModeEvent>>> mode_subs_;
96+
};
97+
98+
} // namespace system_modes
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) 2018 - for information on the respective copyright owner
2+
// see the NOTICE file and/or the repository https://github.com/microros/system_modes
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
#include "system_modes/mode_observer.hpp"
16+
17+
#include <lifecycle_msgs/msg/state.hpp>
18+
#include <lifecycle_msgs/msg/transition_event.hpp>
19+
20+
#include <functional>
21+
#include <map>
22+
#include <string>
23+
#include <vector>
24+
#include <memory>
25+
26+
#include "system_modes/msg/mode_event.hpp"
27+
28+
using std::function;
29+
using std::map;
30+
using std::mutex;
31+
using std::placeholders::_1;
32+
using std::string;
33+
using std::vector;
34+
35+
using lifecycle_msgs::msg::State;
36+
using lifecycle_msgs::msg::Transition;
37+
using lifecycle_msgs::msg::TransitionEvent;
38+
using lifecycle_msgs::srv::GetState;
39+
40+
using system_modes::msg::ModeEvent;
41+
using system_modes::srv::GetMode;
42+
43+
using namespace std::chrono_literals;
44+
using shared_mutex = std::shared_timed_mutex;
45+
46+
namespace system_modes
47+
{
48+
49+
ModeObserver::ModeObserver(std::weak_ptr<rclcpp::Node> handle)
50+
: node_handle_(handle)
51+
{
52+
}
53+
54+
StateAndMode
55+
ModeObserver::get(const std::string & part_name)
56+
{
57+
std::shared_lock<shared_mutex> lock(this->mutex_);
58+
try {
59+
return cache_.at(part_name);
60+
} catch (const std::exception & e) {
61+
std::cerr << e.what() << '\n';
62+
return StateAndMode();
63+
}
64+
}
65+
66+
void
67+
ModeObserver::observe(const std::string & part_name)
68+
{
69+
std::unique_lock<shared_mutex> lock(mutex_);
70+
71+
cache_[part_name] = StateAndMode();
72+
73+
// Initial try to get current state via service request
74+
std::string topic = part_name + "/get_state";
75+
auto gsrequest = std::make_shared<GetState::Request>();
76+
auto stateclient = node_handle_.lock()->create_client<GetState>(topic);
77+
if (stateclient->wait_for_service(std::chrono::microseconds(500))) {
78+
auto state = stateclient->async_send_request(gsrequest);
79+
if (rclcpp::spin_until_future_complete(node_handle_.lock(), state) ==
80+
rclcpp::FutureReturnCode::SUCCESS)
81+
{
82+
auto result = state.get();
83+
cache_[part_name].state = result->current_state.id;
84+
}
85+
}
86+
87+
// Initial try to get current mode via service request
88+
topic = part_name + "/get_mode";
89+
auto gmrequest = std::make_shared<GetMode::Request>();
90+
auto modeclient = node_handle_.lock()->create_client<GetMode>(topic);
91+
if (modeclient->wait_for_service(std::chrono::microseconds(500))) {
92+
auto mode = modeclient->async_send_request(gmrequest);
93+
if (rclcpp::spin_until_future_complete(node_handle_.lock(), mode) ==
94+
rclcpp::FutureReturnCode::SUCCESS)
95+
{
96+
auto result = mode.get();
97+
cache_[part_name].mode = result->current_mode;
98+
}
99+
}
100+
101+
// Set up transition subscriber and mode event subscriber for continuous observation
102+
topic = part_name + "/transition_event";
103+
function<void(TransitionEvent::SharedPtr)> transition_cb =
104+
bind(&ModeObserver::transition_callback, this, _1, part_name);
105+
auto state_sub = node_handle_.lock()->create_subscription<TransitionEvent>(
106+
topic,
107+
rclcpp::SystemDefaultsQoS(),
108+
transition_cb);
109+
state_subs_.emplace(part_name, state_sub);
110+
topic = part_name + "/mode_event";
111+
function<void(ModeEvent::SharedPtr)> mode_cb =
112+
bind(&ModeObserver::mode_event_callback, this, _1, part_name);
113+
auto mode_sub = node_handle_.lock()->create_subscription<ModeEvent>(
114+
topic,
115+
rclcpp::SystemDefaultsQoS(),
116+
mode_cb);
117+
mode_subs_.emplace(part_name, mode_sub);
118+
}
119+
120+
void
121+
ModeObserver::stop_observing(const std::string & part_name)
122+
{
123+
std::unique_lock<shared_mutex> lock(mutex_);
124+
state_subs_.erase(part_name);
125+
mode_subs_.erase(part_name);
126+
cache_.erase(part_name);
127+
}
128+
129+
void
130+
ModeObserver::transition_callback(
131+
const TransitionEvent::SharedPtr msg,
132+
const string & part_name)
133+
{
134+
std::unique_lock<shared_mutex> lock(mutex_);
135+
cache_[part_name].state = msg->goal_state.id;
136+
}
137+
138+
void
139+
ModeObserver::mode_event_callback(
140+
const ModeEvent::SharedPtr msg,
141+
const string & part_name)
142+
{
143+
std::unique_lock<shared_mutex> lock(mutex_);
144+
cache_[part_name].mode = msg->goal_mode.label;
145+
}
146+
147+
} // namespace system_modes
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import os
2+
3+
import unittest
4+
5+
import ament_index_python
6+
import launch
7+
import launch_ros
8+
import launch_testing.actions
9+
import launch_testing_ros
10+
11+
from launch import LaunchDescription
12+
from launch.actions import ExecuteProcess
13+
14+
15+
def generate_test_description():
16+
os.environ['OSPL_VERBOSITY'] = '8'
17+
os.environ['RCUTILS_CONSOLE_OUTPUT_FORMAT'] = '{message}'
18+
19+
modelfile = '@MODELFILE@'
20+
21+
modes_observer = ExecuteProcess(
22+
cmd=[
23+
"ros2",
24+
"run",
25+
"system_modes",
26+
"modes_observer_test_node"],
27+
name='modes_observer_test_node',
28+
emulate_tty=True,
29+
output='screen')
30+
31+
mode_manager = launch.actions.IncludeLaunchDescription(
32+
launch.launch_description_sources.PythonLaunchDescriptionSource(
33+
ament_index_python.packages.get_package_share_directory(
34+
'system_modes') + '/launch/mode_manager.launch.py'),
35+
launch_arguments={'modelfile': modelfile}.items())
36+
37+
test_nodes = ExecuteProcess(
38+
cmd=[
39+
"@PYTHON_EXECUTABLE@",
40+
"@TEST_NODES@"
41+
],
42+
name='test_nodes',
43+
emulate_tty=True,
44+
output='screen')
45+
46+
launch_description = LaunchDescription()
47+
launch_description.add_action(modes_observer)
48+
launch_description.add_action(mode_manager)
49+
launch_description.add_action(test_nodes)
50+
launch_description.add_action(launch_testing.actions.ReadyToTest())
51+
52+
return launch_description, locals()
53+
54+
class TestModeManagement(unittest.TestCase):
55+
56+
def test_processes_output(self, proc_output, modes_observer):
57+
"""Check manager and nodes logging output for expected strings."""
58+
59+
from launch_testing.tools.output import get_default_filtered_prefixes
60+
output_filter = launch_testing_ros.tools.basic_output_filter(
61+
filtered_prefixes=get_default_filtered_prefixes() + ['service not available, waiting...'],
62+
filtered_rmw_implementation='@RMW_IMPLEMENTATION@'
63+
)
64+
proc_output.assertWaitFor(
65+
expected_output=launch_testing.tools.expected_output_from_file(path="@EXPECTED_OUTPUT@"),
66+
process=modes_observer,
67+
output_filter=output_filter,
68+
timeout=15,
69+
stream='stdout')
70+
71+
import time
72+
time.sleep(1)

0 commit comments

Comments
 (0)