Skip to content

Commit ea34fab

Browse files
feat: add ParameterWatcher for real time parameter monitoring (#1326)
Implement ParameterWatcher class to enable real-time monitoring of parameter changes on remote nodes. Subscribes to /parameter_events topic and emits 'change' events when watched parameters are modified. Features: - Watch specific parameters on remote nodes - Real-time change notifications via EventEmitter - Dynamic parameter list management (add/remove) - Async/await support with timeout and AbortSignal - Proper lifecycle management and cleanup
1 parent 08a4d33 commit ea34fab

File tree

12 files changed

+1253
-12
lines changed

12 files changed

+1253
-12
lines changed

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
- Promise-based service calls implementation
4444
- Add ParameterClient for external parameter access
4545
- Add structured error handling with class error hierarchy
46+
- Add ParameterWatcher for real-time parameter monitoring
4647

4748
- **[Martins Mozeiko](https://github.com/martins-mozeiko)**
4849
- QoS new/delete fix

example/parameter/README.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,25 @@ Parameters are ideal for:
110110
- Lifecycle management
111111
- **Run Command**: `node parameter-client-advanced-example.js`
112112

113+
#### 5. ParameterWatcher (`parameter-watcher-example.js`)
114+
115+
**Purpose**: Demonstrates watching parameter changes on a remote node in real-time.
116+
117+
- **Functionality**:
118+
- Creates a watcher for turtlesim's background color parameters
119+
- Listens for parameter change events
120+
- Displays current parameter values
121+
- Shows real-time updates when parameters change
122+
- **Features**:
123+
- Event-driven parameter monitoring
124+
- Automatic filtering by node and parameter names
125+
- Real-time change notifications
126+
- Built on top of ParameterClient
127+
- Simple EventEmitter API
128+
- **Target Node**: `turtlesim` (run: `ros2 run turtlesim turtlesim_node`)
129+
- **Run Command**: `node parameter-watcher-example.js`
130+
- **Test Changes**: In another terminal, run `ros2 param set /turtlesim background_r 200`
131+
113132
**ParameterClient Key Features**:
114133

115134
- **Remote Access**: Query/modify parameters on any node
@@ -119,7 +138,7 @@ Parameters are ideal for:
119138
- **Cancellation**: AbortController integration for request cancellation
120139
- **Lifecycle Management**: Automatic cleanup when parent node is destroyed
121140

122-
**Public API**:
141+
**ParameterClient Public API**:
123142

124143
- `remoteNodeName` (getter) - Get the target node name
125144
- `waitForService(timeout?)` - Wait for remote services to be available
@@ -133,6 +152,27 @@ Parameters are ideal for:
133152
- `isDestroyed()` - Check if client has been destroyed
134153
- `destroy()` - Clean up and destroy the client
135154

155+
**ParameterWatcher Key Features**:
156+
157+
- **Real-Time Monitoring**: Automatically notified when watched parameters change
158+
- **Event-Driven**: Uses EventEmitter pattern for clean async code
159+
- **Filtered**: Only notifies about relevant parameter changes
160+
- **Flexible**: Add/remove parameters from watch list dynamically
161+
- **Built on ParameterClient**: Can query current values at any time
162+
- **Lifecycle Management**: Automatic cleanup when parent node is destroyed
163+
164+
**ParameterWatcher Public API**:
165+
166+
- `remoteNodeName` (getter) - Get the target node name
167+
- `watchedParameters` (getter) - Get list of watched parameter names
168+
- `start(timeout?)` - Start watching for parameter changes
169+
- `getCurrentValues(options?)` - Get current values of all watched parameters
170+
- `addParameter(name)` - Add a parameter to the watch list
171+
- `removeParameter(name)` - Remove a parameter from the watch list
172+
- `on('change', callback)` - Listen for parameter change events
173+
- `isDestroyed()` - Check if watcher has been destroyed
174+
- `destroy()` - Clean up and destroy the watcher
175+
136176
## How to Run the Examples
137177

138178
### Prerequisites
@@ -245,6 +285,46 @@ max_speed descriptor: {
245285
}
246286
```
247287

288+
### Running ParameterWatcher Example
289+
290+
First, in one terminal, run turtlesim:
291+
292+
```bash
293+
ros2 run turtlesim turtlesim_node
294+
```
295+
296+
Then in another terminal:
297+
298+
```bash
299+
cd example/parameter
300+
node parameter-watcher-example.js
301+
```
302+
303+
**Expected Output**:
304+
305+
```
306+
Watching: [ 'background_r', 'background_g' ]
307+
Added background_b. Now watching: [ 'background_r', 'background_g', 'background_b' ]
308+
Removed background_g. Now watching: [ 'background_r', 'background_b' ]
309+
```
310+
311+
Now in a third terminal, change a parameter to see real-time updates:
312+
313+
```bash
314+
ros2 param set /turtlesim background_r 200
315+
ros2 param set /turtlesim background_b 100
316+
ros2 param set /turtlesim background_g 50 # This won't trigger (removed from watch list)
317+
```
318+
319+
You'll see output like:
320+
321+
```
322+
background_r changed to 200
323+
background_b changed to 100
324+
```
325+
326+
Note: `background_g` changes won't be displayed since it was removed from the watch list.
327+
248328
## Using ROS 2 Parameter Tools
249329

250330
You can interact with these examples using standard ROS 2 parameter tools:
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const rclnodejs = require('../../index.js');
18+
19+
async function main() {
20+
await rclnodejs.init();
21+
22+
const node = rclnodejs.createNode('watcher_node');
23+
24+
const watcher = node.createParameterWatcher('turtlesim', [
25+
'background_r',
26+
'background_g',
27+
]);
28+
29+
watcher.on('change', (params) => {
30+
params.forEach((p) => {
31+
console.log(`${p.name} changed to ${p.value.integer_value}`);
32+
});
33+
});
34+
35+
try {
36+
const available = await watcher.start(10000);
37+
38+
if (!available) {
39+
console.log('Turtlesim node not available. Please run:');
40+
console.log(' ros2 run turtlesim turtlesim_node');
41+
return;
42+
}
43+
44+
console.log('Watching:', watcher.watchedParameters);
45+
46+
watcher.addParameter('background_b');
47+
console.log('Added background_b. Now watching:', watcher.watchedParameters);
48+
49+
watcher.removeParameter('background_g');
50+
console.log(
51+
'Removed background_g. Now watching:',
52+
watcher.watchedParameters
53+
);
54+
55+
rclnodejs.spin(node);
56+
} catch (error) {
57+
console.error('Error:', error.message);
58+
node.destroy();
59+
rclnodejs.shutdown();
60+
}
61+
}
62+
63+
main();

index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const {
6060
} = require('./lib/serialization.js');
6161
const ParameterClient = require('./lib/parameter_client.js');
6262
const errors = require('./lib/errors.js');
63+
const ParameterWatcher = require('./lib/parameter_watcher.js');
6364
const { spawn } = require('child_process');
6465

6566
/**
@@ -222,6 +223,9 @@ let rcl = {
222223
/** {@link ParameterClient} class */
223224
ParameterClient: ParameterClient,
224225

226+
/** {@link ParameterWatcher} class */
227+
ParameterWatcher: ParameterWatcher,
228+
225229
/** {@link QoS} class */
226230
QoS: QoS,
227231

lib/node.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const {
3939
} = require('./errors.js');
4040
const ParameterService = require('./parameter_service.js');
4141
const ParameterClient = require('./parameter_client.js');
42+
const ParameterWatcher = require('./parameter_watcher.js');
4243
const Publisher = require('./publisher.js');
4344
const QoS = require('./qos.js');
4445
const Rates = require('./rate.js');
@@ -121,6 +122,7 @@ class Node extends rclnodejs.ShadowNode {
121122
this._actionClients = [];
122123
this._actionServers = [];
123124
this._parameterClients = [];
125+
this._parameterWatchers = [];
124126
this._rateTimerServer = null;
125127
this._parameterDescriptors = new Map();
126128
this._parameters = new Map();
@@ -949,6 +951,30 @@ class Node extends rclnodejs.ShadowNode {
949951
return parameterClient;
950952
}
951953

954+
/**
955+
* Create a ParameterWatcher for watching parameter changes on a remote node.
956+
* @param {string} remoteNodeName - The name of the remote node whose parameters to watch.
957+
* @param {string[]} parameterNames - Array of parameter names to watch.
958+
* @param {object} [options] - Options for parameter watcher.
959+
* @param {number} [options.timeout=5000] - Default timeout in milliseconds for service calls.
960+
* @return {ParameterWatcher} - An instance of ParameterWatcher.
961+
*/
962+
createParameterWatcher(remoteNodeName, parameterNames, options = {}) {
963+
const watcher = new ParameterWatcher(
964+
this,
965+
remoteNodeName,
966+
parameterNames,
967+
options
968+
);
969+
debug(
970+
'Finish creating parameter watcher for remote node = %s.',
971+
remoteNodeName
972+
);
973+
this._parameterWatchers.push(watcher);
974+
975+
return watcher;
976+
}
977+
952978
/**
953979
* Create a guard condition.
954980
* @param {Function} callback - The callback to be called when the guard condition is triggered.
@@ -986,6 +1012,7 @@ class Node extends rclnodejs.ShadowNode {
9861012
this._actionServers.forEach((actionServer) => actionServer.destroy());
9871013

9881014
this._parameterClients.forEach((paramClient) => paramClient.destroy());
1015+
this._parameterWatchers.forEach((watcher) => watcher.destroy());
9891016

9901017
this.context.onNodeDestroyed(this);
9911018

@@ -1000,6 +1027,7 @@ class Node extends rclnodejs.ShadowNode {
10001027
this._actionClients = [];
10011028
this._actionServers = [];
10021029
this._parameterClients = [];
1030+
this._parameterWatchers = [];
10031031

10041032
if (this._rateTimerServer) {
10051033
this._rateTimerServer.shutdown();
@@ -1099,6 +1127,19 @@ class Node extends rclnodejs.ShadowNode {
10991127
parameterClient.destroy();
11001128
}
11011129

1130+
/**
1131+
* Destroy a ParameterWatcher.
1132+
* @param {ParameterWatcher} watcher - The ParameterWatcher to be destroyed.
1133+
* @return {undefined}
1134+
*/
1135+
destroyParameterWatcher(watcher) {
1136+
if (!(watcher instanceof ParameterWatcher)) {
1137+
throw new TypeError('Invalid argument');
1138+
}
1139+
this._removeEntityFromArray(watcher, this._parameterWatchers);
1140+
watcher.destroy();
1141+
}
1142+
11021143
/**
11031144
* Destroy a Timer.
11041145
* @param {Timer} timer - The Timer to be destroyed.

lib/parameter_client.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
OperationError,
2626
} = require('./errors.js');
2727
const validator = require('./validator.js');
28+
const { normalizeNodeName } = require('./utils.js');
2829
const debug = require('debug')('rclnodejs:parameter_client');
2930

3031
/**
@@ -58,7 +59,7 @@ class ParameterClient {
5859
}
5960

6061
this.#node = node;
61-
this.#remoteNodeName = this.#normalizeNodeName(remoteNodeName);
62+
this.#remoteNodeName = normalizeNodeName(remoteNodeName);
6263
validator.validateNodeName(this.#remoteNodeName);
6364

6465
this.#timeout = options.timeout || 5000;
@@ -474,16 +475,6 @@ class ParameterClient {
474475
}
475476
}
476477

477-
/**
478-
* Normalize a node name by removing leading slash if present.
479-
* @private
480-
* @param {string} nodeName - The node name to normalize.
481-
* @return {string} - The normalized node name.
482-
*/
483-
#normalizeNodeName(nodeName) {
484-
return nodeName.startsWith('/') ? nodeName.substring(1) : nodeName;
485-
}
486-
487478
/**
488479
* Convert a service type name from PascalCase to snake_case.
489480
* @private

0 commit comments

Comments
 (0)