Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
- Promise-based service calls implementation
- Add ParameterClient for external parameter access
- Add structured error handling with class error hierarchy
- Add ParameterWatcher for real-time parameter monitoring

- **[Martins Mozeiko](https://github.com/martins-mozeiko)**
- QoS new/delete fix
Expand Down
82 changes: 81 additions & 1 deletion example/parameter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,25 @@ Parameters are ideal for:
- Lifecycle management
- **Run Command**: `node parameter-client-advanced-example.js`

#### 5. ParameterWatcher (`parameter-watcher-example.js`)

**Purpose**: Demonstrates watching parameter changes on a remote node in real-time.

- **Functionality**:
- Creates a watcher for turtlesim's background color parameters
- Listens for parameter change events
- Displays current parameter values
- Shows real-time updates when parameters change
- **Features**:
- Event-driven parameter monitoring
- Automatic filtering by node and parameter names
- Real-time change notifications
- Built on top of ParameterClient
- Simple EventEmitter API
- **Target Node**: `turtlesim` (run: `ros2 run turtlesim turtlesim_node`)
- **Run Command**: `node parameter-watcher-example.js`
- **Test Changes**: In another terminal, run `ros2 param set /turtlesim background_r 200`

**ParameterClient Key Features**:

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

**Public API**:
**ParameterClient Public API**:

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

**ParameterWatcher Key Features**:

- **Real-Time Monitoring**: Automatically notified when watched parameters change
- **Event-Driven**: Uses EventEmitter pattern for clean async code
- **Filtered**: Only notifies about relevant parameter changes
- **Flexible**: Add/remove parameters from watch list dynamically
- **Built on ParameterClient**: Can query current values at any time
- **Lifecycle Management**: Automatic cleanup when parent node is destroyed

**ParameterWatcher Public API**:

- `remoteNodeName` (getter) - Get the target node name
- `watchedParameters` (getter) - Get list of watched parameter names
- `start(timeout?)` - Start watching for parameter changes
- `getCurrentValues(options?)` - Get current values of all watched parameters
- `addParameter(name)` - Add a parameter to the watch list
- `removeParameter(name)` - Remove a parameter from the watch list
- `on('change', callback)` - Listen for parameter change events
- `isDestroyed()` - Check if watcher has been destroyed
- `destroy()` - Clean up and destroy the watcher

## How to Run the Examples

### Prerequisites
Expand Down Expand Up @@ -245,6 +285,46 @@ max_speed descriptor: {
}
```

### Running ParameterWatcher Example

First, in one terminal, run turtlesim:

```bash
ros2 run turtlesim turtlesim_node
```

Then in another terminal:

```bash
cd example/parameter
node parameter-watcher-example.js
```

**Expected Output**:

```
Watching: [ 'background_r', 'background_g' ]
Added background_b. Now watching: [ 'background_r', 'background_g', 'background_b' ]
Removed background_g. Now watching: [ 'background_r', 'background_b' ]
```

Now in a third terminal, change a parameter to see real-time updates:

```bash
ros2 param set /turtlesim background_r 200
ros2 param set /turtlesim background_b 100
ros2 param set /turtlesim background_g 50 # This won't trigger (removed from watch list)
```

You'll see output like:

```
background_r changed to 200
background_b changed to 100
```

Note: `background_g` changes won't be displayed since it was removed from the watch list.

## Using ROS 2 Parameter Tools

You can interact with these examples using standard ROS 2 parameter tools:
Expand Down
63 changes: 63 additions & 0 deletions example/parameter/parameter-watcher-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

const rclnodejs = require('../../index.js');

async function main() {
await rclnodejs.init();

const node = rclnodejs.createNode('watcher_node');

const watcher = node.createParameterWatcher('turtlesim', [
'background_r',
'background_g',
]);

watcher.on('change', (params) => {
params.forEach((p) => {
console.log(`${p.name} changed to ${p.value.integer_value}`);
});
});

try {
const available = await watcher.start(10000);

if (!available) {
console.log('Turtlesim node not available. Please run:');
console.log(' ros2 run turtlesim turtlesim_node');
return;
}

console.log('Watching:', watcher.watchedParameters);

watcher.addParameter('background_b');
console.log('Added background_b. Now watching:', watcher.watchedParameters);

watcher.removeParameter('background_g');
console.log(
'Removed background_g. Now watching:',
watcher.watchedParameters
);

rclnodejs.spin(node);
} catch (error) {
console.error('Error:', error.message);
node.destroy();
rclnodejs.shutdown();
}
}

main();
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const {
} = require('./lib/serialization.js');
const ParameterClient = require('./lib/parameter_client.js');
const errors = require('./lib/errors.js');
const ParameterWatcher = require('./lib/parameter_watcher.js');
const { spawn } = require('child_process');

/**
Expand Down Expand Up @@ -222,6 +223,9 @@ let rcl = {
/** {@link ParameterClient} class */
ParameterClient: ParameterClient,

/** {@link ParameterWatcher} class */
ParameterWatcher: ParameterWatcher,

/** {@link QoS} class */
QoS: QoS,

Expand Down
41 changes: 41 additions & 0 deletions lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const {
} = require('./errors.js');
const ParameterService = require('./parameter_service.js');
const ParameterClient = require('./parameter_client.js');
const ParameterWatcher = require('./parameter_watcher.js');
const Publisher = require('./publisher.js');
const QoS = require('./qos.js');
const Rates = require('./rate.js');
Expand Down Expand Up @@ -121,6 +122,7 @@ class Node extends rclnodejs.ShadowNode {
this._actionClients = [];
this._actionServers = [];
this._parameterClients = [];
this._parameterWatchers = [];
this._rateTimerServer = null;
this._parameterDescriptors = new Map();
this._parameters = new Map();
Expand Down Expand Up @@ -949,6 +951,30 @@ class Node extends rclnodejs.ShadowNode {
return parameterClient;
}

/**
* Create a ParameterWatcher for watching parameter changes on a remote node.
* @param {string} remoteNodeName - The name of the remote node whose parameters to watch.
* @param {string[]} parameterNames - Array of parameter names to watch.
* @param {object} [options] - Options for parameter watcher.
* @param {number} [options.timeout=5000] - Default timeout in milliseconds for service calls.
* @return {ParameterWatcher} - An instance of ParameterWatcher.
*/
createParameterWatcher(remoteNodeName, parameterNames, options = {}) {
const watcher = new ParameterWatcher(
this,
remoteNodeName,
parameterNames,
options
);
debug(
'Finish creating parameter watcher for remote node = %s.',
remoteNodeName
);
this._parameterWatchers.push(watcher);

return watcher;
}

/**
* Create a guard condition.
* @param {Function} callback - The callback to be called when the guard condition is triggered.
Expand Down Expand Up @@ -986,6 +1012,7 @@ class Node extends rclnodejs.ShadowNode {
this._actionServers.forEach((actionServer) => actionServer.destroy());

this._parameterClients.forEach((paramClient) => paramClient.destroy());
this._parameterWatchers.forEach((watcher) => watcher.destroy());

this.context.onNodeDestroyed(this);

Expand All @@ -1000,6 +1027,7 @@ class Node extends rclnodejs.ShadowNode {
this._actionClients = [];
this._actionServers = [];
this._parameterClients = [];
this._parameterWatchers = [];

if (this._rateTimerServer) {
this._rateTimerServer.shutdown();
Expand Down Expand Up @@ -1099,6 +1127,19 @@ class Node extends rclnodejs.ShadowNode {
parameterClient.destroy();
}

/**
* Destroy a ParameterWatcher.
* @param {ParameterWatcher} watcher - The ParameterWatcher to be destroyed.
* @return {undefined}
*/
destroyParameterWatcher(watcher) {
if (!(watcher instanceof ParameterWatcher)) {
throw new TypeError('Invalid argument');
}
this._removeEntityFromArray(watcher, this._parameterWatchers);
watcher.destroy();
}

/**
* Destroy a Timer.
* @param {Timer} timer - The Timer to be destroyed.
Expand Down
13 changes: 2 additions & 11 deletions lib/parameter_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const {
OperationError,
} = require('./errors.js');
const validator = require('./validator.js');
const { normalizeNodeName } = require('./utils.js');
const debug = require('debug')('rclnodejs:parameter_client');

/**
Expand Down Expand Up @@ -58,7 +59,7 @@ class ParameterClient {
}

this.#node = node;
this.#remoteNodeName = this.#normalizeNodeName(remoteNodeName);
this.#remoteNodeName = normalizeNodeName(remoteNodeName);
validator.validateNodeName(this.#remoteNodeName);

this.#timeout = options.timeout || 5000;
Expand Down Expand Up @@ -474,16 +475,6 @@ class ParameterClient {
}
}

/**
* Normalize a node name by removing leading slash if present.
* @private
* @param {string} nodeName - The node name to normalize.
* @return {string} - The normalized node name.
*/
#normalizeNodeName(nodeName) {
return nodeName.startsWith('/') ? nodeName.substring(1) : nodeName;
}

/**
* Convert a service type name from PascalCase to snake_case.
* @private
Expand Down
Loading
Loading