Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions demo/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Asynchronous actions with progress feedback and cancellation
- **Full typing** for all ROS2 messages, services, and actions
- **Compile-time validation** of message structures
- **IntelliSense support** in VS Code and other TypeScript editors
- **Two equivalent forms**: identify a type by its **string name**
(e.g. `'std_msgs/msg/String'`) or by its **class/constructor** obtained from
`rclnodejs.require(...)`. With the class form, TypeScript infers the concrete
message/service/action types automatically\u2014no explicit annotations needed.

### Modern Development

Expand Down
59 changes: 48 additions & 11 deletions demo/typescript/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Before running this demo, ensure you have:
1. **Ensure ROS2 is sourced**:

```bash
source /opt/ros/humble/setup.bash # or your ROS2 distribution
source /opt/ros/lyrical/setup.bash # or your ROS2 distribution
```

2. **Build the main rclnodejs package** (if not already done):
Expand All @@ -79,8 +79,13 @@ Before running this demo, ensure you have:

4. **Install dependencies**:

This demo depends on the **local rclnodejs** in this repository
(`"rclnodejs": "file:../../.."`). Install with `--ignore-scripts` so npm
links the local package without trying to rebuild its native addon (the
prebuilt binary in the repo is reused):

```bash
npm install
npm install --ignore-scripts
```

5. **Build the TypeScript code**:
Expand Down Expand Up @@ -194,29 +199,52 @@ You can also test the action server using ROS2 command-line tools:

## Understanding the Code

Both the server and client show the **two equivalent, fully type-safe ways**
to identify an action type:

- **String name**: pass the type string, e.g. `'test_msgs/action/Fibonacci'`.
- **Action class**: obtain the constructor with `rclnodejs.require(...)` and
pass it directly. TypeScript then infers the goal, feedback and result types
from the constructor, so no explicit `any` annotations are needed.

This demo uses the **action class** form. The constructor is loaded once at
module scope and reused for the type annotations via `typeof Fibonacci`:

```typescript
const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');
```

### Action Server (`server.ts`)

The action server implements three main callbacks:
The action server implements three main callbacks, all typed from
`typeof Fibonacci`:

1. **Goal Callback**: Decides whether to accept or reject incoming goals

```typescript
goalCallback(goalHandle: any): rclnodejs.GoalResponse {
// Validate the goal and return ACCEPT or REJECT
goalCallback(
goal: rclnodejs.ActionGoal<typeof Fibonacci>
): rclnodejs.GoalResponse {
// Validate the goal (goal.order is typed) and return ACCEPT or REJECT
}
```

2. **Execute Callback**: Performs the actual work (Fibonacci calculation)

```typescript
async executeCallback(goalHandle: any): Promise<any> {
// Calculate Fibonacci sequence and provide feedback
async executeCallback(
goalHandle: rclnodejs.ServerGoalHandle<typeof Fibonacci>
): Promise<rclnodejs.ActionResult<typeof Fibonacci>> {
// Calculate the sequence and publish typed feedback
}
```

3. **Cancel Callback**: Handles goal cancellation requests

```typescript
cancelCallback(goalHandle: any): rclnodejs.CancelResponse {
cancelCallback(
goalHandle: rclnodejs.ServerGoalHandle<typeof Fibonacci> | undefined
): rclnodejs.CancelResponse {
// Return ACCEPT to allow cancellation
}
```
Expand All @@ -226,14 +254,23 @@ The action server implements three main callbacks:
The action client:

1. Waits for the action server to be available
2. Creates and sends a goal
3. Handles feedback during execution
2. Creates and sends a goal (`new Fibonacci.Goal()`)
3. Handles typed feedback during execution
4. Processes the final result

```typescript
const goal = new Fibonacci.Goal();
goal.order = FIBONACCI_ORDER;

const goalHandle = await this.actionClient.sendGoal(goal, (feedback) =>
this.feedbackCallback(feedback)
);

private feedbackCallback(
feedback: rclnodejs.ActionFeedback<typeof Fibonacci>
): void {
// feedback.sequence is typed
}
```

## Customization
Expand Down Expand Up @@ -263,7 +300,7 @@ You can modify the demo to:

4. **ROS2 environment not sourced**:
```bash
source /opt/ros/humble/setup.bash # or your ROS2 distribution
source /opt/ros/lyrical/setup.bash # or your ROS2 distribution
```

### Debugging
Expand Down
9 changes: 2 additions & 7 deletions demo/typescript/actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"peerDependencies": {
"rclnodejs": "^2.0.0"
},
"peerDependenciesMeta": {
"rclnodejs": {
"optional": false
}
"dependencies": {
"rclnodejs": "file:../../.."
}
Comment on lines +36 to 38
}
36 changes: 24 additions & 12 deletions demo/typescript/actions/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,44 @@
*
* This demo shows how to create a ROS2 action client using TypeScript
* with rclnodejs. It sends Fibonacci calculation goals to an action server.
*
* It also demonstrates the two equivalent, fully type-safe ways to
* identify an action type:
* - String name: new rclnodejs.ActionClient(node, 'test_msgs/action/Fibonacci', ...)
* - Action class: new rclnodejs.ActionClient(node, Fibonacci, ...)
*
* This demo uses the class form (obtained from `rclnodejs.require(...)`):
* TypeScript infers the goal, feedback and result types directly from the
* constructor, so the feedback callback needs no explicit `any` annotation.
*/

import * as rclnodejs from 'rclnodejs';

const ACTION_NAME = 'fibonacci';
const FIBONACCI_ORDER = 10;

// The Fibonacci action class (type-based form). Passing this constructor to
// ActionClient lets TypeScript infer the goal/feedback/result types. The
// string form 'test_msgs/action/Fibonacci' works identically.
const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');

/**
* Fibonacci Action Client Class
*/
class FibonacciActionClient {
private node: rclnodejs.Node;
private actionClient: rclnodejs.ActionClient<'test_msgs/action/Fibonacci'>;
private actionClient: rclnodejs.ActionClient<typeof Fibonacci>;

constructor(node: rclnodejs.Node) {
this.node = node;

// Start spinning the node to handle callbacks
rclnodejs.spin(node);

// Create action client for Fibonacci action
// Create action client for Fibonacci action using the action class
this.actionClient = new rclnodejs.ActionClient(
node,
'test_msgs/action/Fibonacci',
Fibonacci,
ACTION_NAME
);
}
Expand All @@ -41,10 +55,7 @@ class FibonacciActionClient {
await this.actionClient.waitForServer();
this.node.getLogger().info('✓ Action server is available');

// Get the Fibonacci action interface
const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');

// Create a new goal
// Create a new goal (goal.order is typed from the inferred goal type)
const goal = new Fibonacci.Goal();
goal.order = FIBONACCI_ORDER;

Expand All @@ -53,10 +64,9 @@ class FibonacciActionClient {
.info(`Sending goal request for Fibonacci(${goal.order})...`);

try {
// Send the goal with feedback callback
const goalHandle = await this.actionClient.sendGoal(
goal,
(feedback: any) => this.feedbackCallback(feedback)
// Send the goal with feedback callback (feedback is typed)
const goalHandle = await this.actionClient.sendGoal(goal, (feedback) =>
this.feedbackCallback(feedback)
);

if (!goalHandle.isAccepted()) {
Expand Down Expand Up @@ -91,7 +101,9 @@ class FibonacciActionClient {
/**
* Callback function for receiving feedback from the action server
*/
private feedbackCallback(feedback: any): void {
private feedbackCallback(
feedback: rclnodejs.ActionFeedback<typeof Fibonacci>
): void {
this.node
.getLogger()
.info(`📊 Received feedback: [${feedback.sequence.join(', ')}]`);
Expand Down
40 changes: 25 additions & 15 deletions demo/typescript/actions/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,40 @@
*
* This demo shows how to create a ROS2 action server using TypeScript
* with rclnodejs. It provides a Fibonacci calculation service.
*
* It also demonstrates the two equivalent, fully type-safe ways to
* identify an action type:
* - String name: new rclnodejs.ActionServer(node, 'test_msgs/action/Fibonacci', ...)
* - Action class: new rclnodejs.ActionServer(node, Fibonacci, ...)
*
* This demo uses the class form (obtained from `rclnodejs.require(...)`):
* TypeScript infers the goal, feedback and result types directly from the
* constructor, so the callbacks need no explicit `any` annotations.
*/

import * as rclnodejs from 'rclnodejs';

const ACTION_NAME = 'fibonacci';

// The Fibonacci action class (type-based form). Passing this constructor to
// ActionServer lets TypeScript infer the goal/feedback/result types. The
// string form 'test_msgs/action/Fibonacci' works identically.
const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');

/**
* Fibonacci Action Server Class
*/
class FibonacciActionServer {
private node: rclnodejs.Node;
private actionServer: rclnodejs.ActionServer<'test_msgs/action/Fibonacci'>;
private actionServer: rclnodejs.ActionServer<typeof Fibonacci>;

constructor(node: rclnodejs.Node) {
this.node = node;

// Create action server for Fibonacci action
// Create action server for Fibonacci action using the action class
this.actionServer = new rclnodejs.ActionServer(
node,
'test_msgs/action/Fibonacci',
Fibonacci,
ACTION_NAME,
this.executeCallback.bind(this),
this.goalCallback.bind(this),
Expand All @@ -39,13 +53,12 @@ class FibonacciActionServer {
* Execute callback - performs the Fibonacci calculation
*/
async executeCallback(
goalHandle: rclnodejs.ServerGoalHandle<'test_msgs/action/Fibonacci'>
): Promise<any> {
goalHandle: rclnodejs.ServerGoalHandle<typeof Fibonacci>
): Promise<rclnodejs.ActionResult<typeof Fibonacci>> {
this.node
.getLogger()
.info(`🚀 Executing goal for Fibonacci(${goalHandle.request.order})`);

const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');
const feedbackMessage = new Fibonacci.Feedback();
const sequence: number[] = [0, 1];

Expand Down Expand Up @@ -110,13 +123,12 @@ class FibonacciActionServer {
}

/**
* Goal callback - decides whether to accept or reject incoming goals
*
* Note: According to the type definition, this should receive ActionGoal<T>,
* but the actual implementation may pass different types. Using 'any' to handle
* this inconsistency and support ActionGoal<T>.
* Goal callback - decides whether to accept or reject incoming goals.
* The goal type is inferred from the action constructor.
*/
goalCallback(goal: any): rclnodejs.GoalResponse {
goalCallback(
goal: rclnodejs.ActionGoal<typeof Fibonacci>
): rclnodejs.GoalResponse {
const order = goal.order;

this.node
Expand Down Expand Up @@ -146,9 +158,7 @@ class FibonacciActionServer {
* Cancel callback - handles goal cancellation requests
*/
cancelCallback(
goalHandle:
| rclnodejs.ServerGoalHandle<'test_msgs/action/Fibonacci'>
| undefined
goalHandle: rclnodejs.ServerGoalHandle<typeof Fibonacci> | undefined
): rclnodejs.CancelResponse {
this.node.getLogger().info('📥 Received cancel request');
return rclnodejs.CancelResponse.ACCEPT;
Expand Down
Loading
Loading