From 71832ea5692c516b5cb82fd80545bbd8b730ab34 Mon Sep 17 00:00:00 2001 From: Minggang Wang Date: Tue, 29 Jul 2025 12:53:51 +0800 Subject: [PATCH 1/2] Add a demo for actions using typescript --- .../action_client/action-client-example.js | 7 +- ts_demo/actions/README.md | 305 ++++++++++++++++++ ts_demo/actions/package.json | 44 +++ ts_demo/actions/src/client.ts | 137 ++++++++ ts_demo/actions/src/server.ts | 205 ++++++++++++ ts_demo/actions/tsconfig.json | 30 ++ 6 files changed, 724 insertions(+), 4 deletions(-) create mode 100644 ts_demo/actions/README.md create mode 100644 ts_demo/actions/package.json create mode 100644 ts_demo/actions/src/client.ts create mode 100644 ts_demo/actions/src/server.ts create mode 100644 ts_demo/actions/tsconfig.json diff --git a/example/actions/action_client/action-client-example.js b/example/actions/action_client/action-client-example.js index 27c0e83b..e13d697a 100644 --- a/example/actions/action_client/action-client-example.js +++ b/example/actions/action_client/action-client-example.js @@ -68,13 +68,12 @@ class FibonacciActionClient { rclnodejs .init() - .then(() => { + .then(async () => { const node = rclnodejs.createNode('action_client_example_node'); const client = new FibonacciActionClient(node); - - client.sendGoal(); - rclnodejs.spin(node); + + await client.sendGoal(); }) .catch((err) => { console.error(err); diff --git a/ts_demo/actions/README.md b/ts_demo/actions/README.md new file mode 100644 index 00000000..06d6cf4d --- /dev/null +++ b/ts_demo/actions/README.md @@ -0,0 +1,305 @@ +# TypeScript Actions Demo for rclnodejs + +This demo showcases how to use ROS2 actions with rclnodejs in TypeScript. It includes both an action client and server implementation using the Fibonacci action as an example. + +## What are ROS2 Actions? + +Actions in ROS2 are a communication pattern for long-running tasks that: + +- Have a **goal** (what you want to achieve) +- Provide **feedback** during execution (progress updates) +- Return a **result** when completed (final outcome) +- Can be **canceled** while running + +Actions are perfect for tasks like navigation, manipulation, or any long-running computation where you need progress updates. + +## Demo Overview + +This demo implements a Fibonacci sequence calculator using ROS2 actions: + +- **Action Server** (`server.ts`): Calculates Fibonacci sequences up to a given order +- **Action Client** (`client.ts`): Sends goals to the server and receives feedback + +### Features + +- ✅ **Goal handling**: Accept/reject goals based on input validation +- ✅ **Feedback**: Real-time progress updates during calculation +- ✅ **Result**: Final Fibonacci sequence +- ✅ **Cancellation**: Support for canceling running goals +- ✅ **Error handling**: Graceful error handling and logging +- ✅ **TypeScript**: Full type safety and modern TypeScript features + +## Project Structure + +``` +ts_demo/actions/ +├── src/ +│ ├── client.ts # Action client implementation +│ └── server.ts # Action server implementation +├── types/ +│ └── rclnodejs.d.ts # Type definitions +├── package.json # Project configuration +├── tsconfig.json # TypeScript configuration +├── .gitignore # Git ignore rules +└── README.md # This file +``` + +## Prerequisites + +Before running this demo, ensure you have: + +1. **ROS2** installed (tested with ROS2 Humble/Iron/Jazzy) +2. **Node.js** (version 16 or higher) +3. **rclnodejs** built and configured in the parent directory (`../../`) +4. **test_msgs** package available (usually included with ROS2) + +**Important**: This demo uses rclnodejs as a peer dependency, so you must ensure that the main rclnodejs package in the parent directory is properly built and configured with ROS2 before running this demo. + +## Setup and Installation + +1. **Ensure ROS2 is sourced**: + + ```bash + source /opt/ros/humble/setup.bash # or your ROS2 distribution + ``` + +2. **Build the main rclnodejs package** (if not already done): + + ```bash + cd ../../ # Go to main rclnodejs directory + npm install + npm run build + ``` + +3. **Navigate to the demo directory**: + + ```bash + cd ts_demo/actions + ``` + +4. **Install dependencies**: + + ```bash + npm install + ``` + +5. **Build the TypeScript code**: + ```bash + npm run build + ``` + +## Running the Demo + +### Option 1: Run Server and Client Separately + +1. **Start the action server** (in terminal 1): + + ```bash + npm run start:server + ``` + + You should see output like: + + ``` + 🚀 Starting TypeScript Action Server Demo... + ✓ rclnodejs initialized + ✓ Created node: /ts_action_server_demo + ✓ Fibonacci action server created on topic 'fibonacci' + ✓ Fibonacci action server is ready to receive goals + ``` + +2. **Start the action client** (in terminal 2): + + ```bash + npm run start:client + ``` + + You should see the client sending a goal and receiving feedback: + + ``` + 🚀 Starting TypeScript Action Client Demo... + ✓ rclnodejs initialized + ✓ Created node: /ts_action_client_demo + ✓ Action server is available + Sending goal request for Fibonacci(10)... + ✓ Goal accepted by server + 📊 Received feedback: [0, 1] + 📊 Received feedback: [0, 1, 1] + ... + ✓ Goal succeeded! Fibonacci(10) = 0,1,1,2,3,5,8,13,21,34,55 + ``` + +### Option 2: Run Both Simultaneously + +Run both server and client together using concurrently: + +```bash +npm run start:both +``` + +This will start both the server and client in parallel, showing interleaved output. + +### Option 3: Development Mode (with ts-node) + +For development with hot reloading: + +1. **Server**: + + ```bash + npm run dev:server + ``` + +2. **Client**: + ```bash + npm run dev:client + ``` + +## Testing with ROS2 CLI Tools + +You can also test the action server using ROS2 command-line tools: + +1. **Start the server**: + + ```bash + npm run start:server + ``` + +2. **Send a goal using ros2 action**: + + ```bash + ros2 action send_goal /fibonacci test_msgs/action/Fibonacci "{order: 15}" + ``` + +3. **List available actions**: + + ```bash + ros2 action list + ``` + +4. **Get action info**: + ```bash + ros2 action info /fibonacci + ``` + +## Available Scripts + +- `npm run build` - Compile TypeScript to JavaScript +- `npm run clean` - Remove compiled files +- `npm run start:server` - Run the action server +- `npm run start:client` - Run the action client +- `npm run start:both` - Run both server and client concurrently +- `npm run dev:server` - Run server in development mode +- `npm run dev:client` - Run client in development mode +- `npm run check-types` - Type check without compilation + +## Understanding the Code + +### Action Server (`server.ts`) + +The action server implements three main callbacks: + +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 + } + ``` + +2. **Execute Callback**: Performs the actual work (Fibonacci calculation) + + ```typescript + async executeCallback(goalHandle: any): Promise { + // Calculate Fibonacci sequence and provide feedback + } + ``` + +3. **Cancel Callback**: Handles goal cancellation requests + ```typescript + cancelCallback(goalHandle: any): rclnodejs.CancelResponse { + // Return ACCEPT to allow cancellation + } + ``` + +### Action Client (`client.ts`) + +The action client: + +1. Waits for the action server to be available +2. Creates and sends a goal +3. Handles feedback during execution +4. Processes the final result + +```typescript +const goalHandle = await this.actionClient.sendGoal(goal, (feedback) => + this.feedbackCallback(feedback) +); +``` + +## Customization + +You can modify the demo to: + +- **Change the Fibonacci order**: Edit `FIBONACCI_ORDER` in `client.ts` +- **Adjust timing**: Modify the delay in the server's execute callback +- **Add validation**: Enhance goal validation in the server +- **Handle errors**: Add more sophisticated error handling + +## Troubleshooting + +### Common Issues + +1. **"Cannot find module 'rclnodejs'"**: + + - Ensure rclnodejs is properly built in the parent directory + - Run `npm install` in the main rclnodejs directory + +2. **"Action server not available"**: + + - Make sure the action server is running before starting the client + - Check that both nodes are using the same action name + +3. **TypeScript compilation errors**: + + - Run `npm run check-types` to see detailed type errors + - Ensure all dependencies are installed: `npm install` + +4. **ROS2 environment not sourced**: + ```bash + source /opt/ros/humble/setup.bash # or your ROS2 distribution + ``` + +### Debugging + +Enable debug logging by setting the environment variable: + +```bash +export RCUTILS_LOGGING_SEVERITY=DEBUG +npm run start:server +``` + +## Next Steps + +After exploring this demo, you might want to: + +1. **Create custom actions**: Define your own `.action` files +2. **Handle multiple goals**: Implement concurrent goal handling +3. **Add persistence**: Store goal state across restarts +4. **Integrate with other nodes**: Combine actions with topics and services +5. **Add visualization**: Create a web interface to monitor action progress + +## Related Examples + +- **Topics Demo**: `../topics/` - Publisher/Subscriber pattern +- **Services Demo**: `../services/` - Request/Response pattern +- **JavaScript Actions**: `../../example/actions/` - JavaScript implementation + +## Resources + +- [ROS2 Actions Documentation](https://docs.ros2.org/latest/Tutorials/Understanding-ROS2-Actions.html) +- [rclnodejs Documentation](https://github.com/RobotWebTools/rclnodejs) +- [test_msgs Package](https://github.com/ros2/rcl_interfaces/tree/master/test_msgs) + +--- + +**Happy coding with ROS2 Actions and TypeScript! 🚀** diff --git a/ts_demo/actions/package.json b/ts_demo/actions/package.json new file mode 100644 index 00000000..b5bea775 --- /dev/null +++ b/ts_demo/actions/package.json @@ -0,0 +1,44 @@ +{ + "name": "rclnodejs-ts-actions-demo", + "version": "1.0.0", + "description": "TypeScript demo for rclnodejs actions (client and server)", + "main": "dist/client.js", + "scripts": { + "prebuild": "npm run clean", + "build": "tsc", + "start:server": "npm run build && node dist/server.js", + "start:client": "npm run build && node dist/client.js", + "start:both": "npm run build && concurrently \"node dist/server.js\" \"node dist/client.js\"", + "clean": "rimraf dist", + "dev:server": "ts-node src/server.ts", + "dev:client": "ts-node src/client.ts", + "check-types": "tsc --noEmit" + }, + "keywords": [ + "rclnodejs", + "ros2", + "typescript", + "client", + "server", + "action", + "fibonacci", + "demo" + ], + "author": "rclnodejs contributors", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.16.5", + "concurrently": "^9.2.0", + "rimraf": "^6.0.1", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "rclnodejs": "^1.0.0" + }, + "peerDependenciesMeta": { + "rclnodejs": { + "optional": false + } + } +} diff --git a/ts_demo/actions/src/client.ts b/ts_demo/actions/src/client.ts new file mode 100644 index 00000000..ec270915 --- /dev/null +++ b/ts_demo/actions/src/client.ts @@ -0,0 +1,137 @@ +/** + * TypeScript Action Client Demo for rclnodejs + * + * This demo shows how to create a ROS2 action client using TypeScript + * with rclnodejs. It sends Fibonacci calculation goals to an action server. + */ + +import * as rclnodejs from 'rclnodejs'; + +const ACTION_NAME = 'fibonacci'; +const FIBONACCI_ORDER = 10; + +/** + * Fibonacci Action Client Class + */ +class FibonacciActionClient { + private node: rclnodejs.Node; + private actionClient: rclnodejs.ActionClient<'test_msgs/action/Fibonacci'>; + + constructor(node: rclnodejs.Node) { + this.node = node; + + // Start spinning the node to handle callbacks + rclnodejs.spin(node); + + // Create action client for Fibonacci action + this.actionClient = new rclnodejs.ActionClient( + node, + 'test_msgs/action/Fibonacci', + ACTION_NAME + ); + } + + /** + * Send a goal to the Fibonacci action server + */ + async sendGoal(): Promise { + this.node.getLogger().info('Waiting for action server...'); + + // Wait for the action server to be available + 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 + const goal = new Fibonacci.Goal(); + goal.order = FIBONACCI_ORDER; + + this.node + .getLogger() + .info(`Sending goal request for Fibonacci(${goal.order})...`); + + try { + // Send the goal with feedback callback + console.log(goal); + const goalHandle = await this.actionClient.sendGoal( + goal, + (feedback: any) => this.feedbackCallback(feedback) + ); + + if (!goalHandle.isAccepted()) { + this.node.getLogger().error('❌ Goal was rejected by the server'); + return; + } + + this.node.getLogger().info('✓ Goal accepted by server'); + + // Wait for the result + const result = await goalHandle.getResult(); + + if (goalHandle.isSucceeded()) { + this.node + .getLogger() + .info( + `✓ Goal succeeded! Fibonacci(${FIBONACCI_ORDER}) = ${result.sequence}` + ); + } else { + this.node + .getLogger() + .error(`❌ Goal failed with status: ${goalHandle.status}`); + } + } catch (error) { + this.node.getLogger().error(`❌ Error during goal execution: ${error}`); + } finally { + // Shutdown rclnodejs + rclnodejs.shutdown(); + } + } + + /** + * Callback function for receiving feedback from the action server + */ + private feedbackCallback(feedback: any): void { + this.node + .getLogger() + .info(`📊 Received feedback: [${feedback.sequence.join(', ')}]`); + } +} + +/** + * Main function + */ +async function main(): Promise { + try { + console.log('🚀 Starting TypeScript Action Client Demo...'); + + // Initialize rclnodejs + await rclnodejs.init(); + console.log('✓ rclnodejs initialized'); + + // Create a node + const node = new rclnodejs.Node('ts_action_client_demo'); + console.log(`✓ Created node: ${node.getFullyQualifiedName()}`); + + // Create action client and send goal + const client = new FibonacciActionClient(node); + await client.sendGoal(); + } catch (error) { + console.error('❌ Error in action client demo:', error); + process.exit(1); + } +} + +// Handle process termination gracefully +process.on('SIGINT', () => { + console.log('\n🛑 Received SIGINT, shutting down gracefully...'); + rclnodejs.shutdown(); + process.exit(0); +}); + +// Run the main function +main().catch((error) => { + console.error('❌ Fatal error:', error); + process.exit(1); +}); diff --git a/ts_demo/actions/src/server.ts b/ts_demo/actions/src/server.ts new file mode 100644 index 00000000..035bff46 --- /dev/null +++ b/ts_demo/actions/src/server.ts @@ -0,0 +1,205 @@ +/** + * TypeScript Action Server Demo for rclnodejs + * + * This demo shows how to create a ROS2 action server using TypeScript + * with rclnodejs. It provides a Fibonacci calculation service. + */ + +import * as rclnodejs from 'rclnodejs'; + +const ACTION_NAME = 'fibonacci'; + +/** + * Fibonacci Action Server Class + */ +class FibonacciActionServer { + private node: rclnodejs.Node; + private actionServer: rclnodejs.ActionServer<'test_msgs/action/Fibonacci'>; + + constructor(node: rclnodejs.Node) { + this.node = node; + + // Create action server for Fibonacci action + this.actionServer = new rclnodejs.ActionServer( + node, + 'test_msgs/action/Fibonacci', + ACTION_NAME, + this.executeCallback.bind(this), + this.goalCallback.bind(this), + undefined, // handleAcceptedCallback + this.cancelCallback.bind(this) + ); + + this.node + .getLogger() + .info(`✓ Fibonacci action server created on topic '${ACTION_NAME}'`); + } + + /** + * Execute callback - performs the Fibonacci calculation + */ + async executeCallback( + goalHandle: rclnodejs.ServerGoalHandle<'test_msgs/action/Fibonacci'> + ): Promise { + 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]; + + // Check for invalid input + if (goalHandle.request.order < 0) { + this.node.getLogger().error('❌ Invalid order: must be non-negative'); + goalHandle.abort(); + return new Fibonacci.Result(); + } + + if (goalHandle.request.order === 0) { + const result = new Fibonacci.Result(); + result.sequence = [0]; + goalHandle.succeed(); + this.node + .getLogger() + .info('✓ Goal completed immediately: Fibonacci(0) = [0]'); + return result; + } + + // Start executing the action + for (let i = 1; i < goalHandle.request.order; i++) { + // Check if the goal has been canceled + if (goalHandle.isCancelRequested) { + goalHandle.canceled(); + this.node.getLogger().info('🛑 Goal was canceled'); + return new Fibonacci.Result(); + } + + // Update Fibonacci sequence + sequence.push(sequence[i] + sequence[i - 1]); + + // Prepare feedback message + feedbackMessage.sequence = [...sequence]; // Create a copy + this.node + .getLogger() + .info( + `📊 Publishing feedback: [${feedbackMessage.sequence.join(', ')}]` + ); + + // Publish the feedback + goalHandle.publishFeedback(feedbackMessage); + + // Wait for 1 second to simulate computation time + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + // Mark goal as succeeded + goalHandle.succeed(); + + // Prepare result message + const result = new Fibonacci.Result(); + result.sequence = sequence; + + this.node + .getLogger() + .info( + `✅ Goal completed! Fibonacci(${goalHandle.request.order}) = [${result.sequence.join(', ')}]` + ); + + return result; + } + + /** + * Goal callback - decides whether to accept or reject incoming goals + * + * Note: According to the type definition, this should receive ActionGoal, + * but the actual implementation may pass different types. Using 'any' to handle + * this inconsistency and support both ActionGoal and ServerGoalHandle. + */ + goalCallback(goalHandle: any): rclnodejs.GoalResponse { + // Handle both ActionGoal (with direct .order) and ServerGoalHandle (with .request.order) + const order = + goalHandle.order !== undefined + ? goalHandle.order + : goalHandle.request?.order; + + this.node + .getLogger() + .info(`📥 Received goal request for Fibonacci(${order})`); + + // Accept goals with reasonable order values + if (order > 50) { + this.node + .getLogger() + .warn(`⚠️ Rejecting goal: order ${order} is too large (max: 50)`); + return rclnodejs.GoalResponse.REJECT; + } + + if (order < 0) { + this.node + .getLogger() + .warn(`⚠️ Rejecting goal: order ${order} is negative`); + return rclnodejs.GoalResponse.REJECT; + } + + this.node.getLogger().info('✅ Goal accepted'); + return rclnodejs.GoalResponse.ACCEPT; + } + + /** + * Cancel callback - handles goal cancellation requests + */ + cancelCallback( + goalHandle: + | rclnodejs.ServerGoalHandle<'test_msgs/action/Fibonacci'> + | undefined + ): rclnodejs.CancelResponse { + this.node.getLogger().info('📥 Received cancel request'); + return rclnodejs.CancelResponse.ACCEPT; + } +} + +/** + * Main function + */ +async function main(): Promise { + try { + console.log('🚀 Starting TypeScript Action Server Demo...'); + + // Initialize rclnodejs + await rclnodejs.init(); + console.log('✓ rclnodejs initialized'); + + // Create a node + const node = new rclnodejs.Node('ts_action_server_demo'); + console.log(`✓ Created node: ${node.getFullyQualifiedName()}`); + + // Create action server + const server = new FibonacciActionServer(node); + console.log('✓ Fibonacci action server is ready to receive goals'); + console.log( + '🔗 Use "ros2 action send_goal /fibonacci test_msgs/action/Fibonacci "{order: 10}" to test' + ); + + // Start spinning the node to handle incoming requests + rclnodejs.spin(node); + } catch (error) { + console.error('❌ Error in action server demo:', error); + process.exit(1); + } +} + +/** + * Handle process termination gracefully + */ +process.on('SIGINT', () => { + console.log('\n🛑 Received SIGINT, shutting down gracefully...'); + rclnodejs.shutdown(); + process.exit(0); +}); + +// Run the main function +main().catch((error) => { + console.error('❌ Fatal error:', error); + process.exit(1); +}); diff --git a/ts_demo/actions/tsconfig.json b/ts_demo/actions/tsconfig.json new file mode 100644 index 00000000..aed65ddb --- /dev/null +++ b/ts_demo/actions/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": ["node"], + "typeRoots": ["./types", "./node_modules/@types"] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts" + ] +} From 2d7bbc0112921d4f45dab3560baedfec30f68af1 Mon Sep 17 00:00:00 2001 From: Minggang Wang Date: Tue, 29 Jul 2025 13:18:40 +0800 Subject: [PATCH 2/2] Address comments --- ts_demo/actions/src/client.ts | 1 - ts_demo/actions/src/server.ts | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/ts_demo/actions/src/client.ts b/ts_demo/actions/src/client.ts index ec270915..1363eec6 100644 --- a/ts_demo/actions/src/client.ts +++ b/ts_demo/actions/src/client.ts @@ -54,7 +54,6 @@ class FibonacciActionClient { try { // Send the goal with feedback callback - console.log(goal); const goalHandle = await this.actionClient.sendGoal( goal, (feedback: any) => this.feedbackCallback(feedback) diff --git a/ts_demo/actions/src/server.ts b/ts_demo/actions/src/server.ts index 035bff46..77c547fb 100644 --- a/ts_demo/actions/src/server.ts +++ b/ts_demo/actions/src/server.ts @@ -114,14 +114,10 @@ class FibonacciActionServer { * * Note: According to the type definition, this should receive ActionGoal, * but the actual implementation may pass different types. Using 'any' to handle - * this inconsistency and support both ActionGoal and ServerGoalHandle. + * this inconsistency and support ActionGoal. */ - goalCallback(goalHandle: any): rclnodejs.GoalResponse { - // Handle both ActionGoal (with direct .order) and ServerGoalHandle (with .request.order) - const order = - goalHandle.order !== undefined - ? goalHandle.order - : goalHandle.request?.order; + goalCallback(goal: any): rclnodejs.GoalResponse { + const order = goal.order; this.node .getLogger()