From 4a9511e5856644a4ea259756f2178c1d6851533e Mon Sep 17 00:00:00 2001 From: Minggang Wang Date: Wed, 16 Jul 2025 13:31:44 +0800 Subject: [PATCH 1/2] Add a subscriber/publisher demo for typescript --- ts_demo/topics/README.md | 341 +++++++++++++++++++++++++++++++ ts_demo/topics/package.json | 37 ++++ ts_demo/topics/src/publisher.ts | 76 +++++++ ts_demo/topics/src/subscriber.ts | 71 +++++++ ts_demo/topics/tsconfig.json | 29 +++ 5 files changed, 554 insertions(+) create mode 100644 ts_demo/topics/README.md create mode 100644 ts_demo/topics/package.json create mode 100644 ts_demo/topics/src/publisher.ts create mode 100644 ts_demo/topics/src/subscriber.ts create mode 100644 ts_demo/topics/tsconfig.json diff --git a/ts_demo/topics/README.md b/ts_demo/topics/README.md new file mode 100644 index 00000000..9029d0d3 --- /dev/null +++ b/ts_demo/topics/README.md @@ -0,0 +1,341 @@ +# rclnodejs TypeScript Topics Demo + +This demo demonstrates how to use **rclnodejs** with TypeScript to create ROS2 publishers and subscribers. The demo includes a publisher that sends string messages to a topic called `ts_demo` and a subscriber that receives and displays those messages. + +## Features + +- šŸš€ **TypeScript Support**: Fully typed ROS2 node implementation using rclnodejs TypeScript interfaces +- šŸ“¤ **Publisher**: Sends timestamped string messages at regular intervals +- šŸ“„ **Subscriber**: Receives and displays messages with timestamps +- šŸ›”ļø **Type Safety**: Leverages TypeScript's type system for compile-time safety +- šŸŽÆ **Error Handling**: Comprehensive error handling and graceful shutdown +- šŸ“Š **Message Counting**: Tracks published and received message counts +- šŸŽØ **Console Output**: Colorful and informative console messages + +## Prerequisites + +Before running this demo, ensure you have: + +1. **Node.js** (>= 16.13.0) +2. **ROS 2** installed and sourced +3. **rclnodejs** built and available + +### ROS 2 Setup + +Make sure your ROS 2 environment is properly sourced before running the demo: + +```bash +# For example, if using ROS 2 Jazzy +source /opt/ros/jazzy/setup.bash + +# Or if you have a custom workspace +source /path/to/your/ros2_ws/install/setup.bash +``` + +### Verify rclnodejs Installation + +From the root of the rclnodejs project, ensure it's built: + +```bash +cd /path/to/rclnodejs +npm install +npm run build +``` + +## Installation + +Navigate to this demo directory and install dependencies: + +```bash +cd ts_demo/topics +npm install +``` + +## Usage + +### Build and Run + +1. **Build the TypeScript code:** + + ```bash + npm run build + ``` + +2. **Run the publisher (in one terminal):** + + ```bash + npm run start:publisher + ``` + +3. **Run the subscriber (in another terminal):** + + ```bash + npm run start:subscriber + ``` + +4. **Or run both simultaneously:** + ```bash + npm run start:both + ``` + +### Development Mode + +For development, you can run TypeScript files directly with ts-node: + +1. **Run the publisher:** + + ```bash + npm run dev:publisher + ``` + +2. **Run the subscriber:** + + ```bash + npm run dev:subscriber + ``` + +3. **Type check only:** + ```bash + npm run check-types + ``` + +## TypeScript Setup + +This demo includes a self-contained TypeScript configuration that allows compilation without requiring the full rclnodejs installation to be built first. The key components are: + +### Local Type Definitions + +The `types/rclnodejs.d.ts` file provides basic TypeScript declarations for rclnodejs, enabling: + +- āœ… TypeScript compilation without dependencies +- āœ… Type checking and IntelliSense support +- āœ… Proper interface definitions for common ROS2 operations + +### TypeScript Configuration + +The `tsconfig.json` is configured to: + +- Use local type definitions from the `types/` directory +- Compile TypeScript to JavaScript in the `dist/` directory +- Generate source maps and declaration files +- Enable strict type checking + +### Development vs Runtime + +- **Compilation**: Uses local type definitions (no rclnodejs dependency) +- **Runtime**: Requires actual rclnodejs installation and ROS 2 environment + +## Expected Output + +### Publisher Output: + +``` +Starting TypeScript Publisher Demo... +āœ“ rclnodejs initialized +āœ“ Created node: /ts_publisher_demo +āœ“ Created publisher on topic: ts_demo +āœ“ Created timer with 1000ms interval +šŸš€ Publisher is running. Press Ctrl+C to stop... + +šŸ“¤ Published: "Hello from TypeScript publisher! Message #1 at 2025-07-16T10:30:00.123Z" +šŸ“¤ Published: "Hello from TypeScript publisher! Message #2 at 2025-07-16T10:30:01.125Z" +šŸ“¤ Published: "Hello from TypeScript publisher! Message #3 at 2025-07-16T10:30:02.127Z" +... +``` + +### Subscriber Output: + +``` +Starting TypeScript Subscriber Demo... +āœ“ rclnodejs initialized +āœ“ Created node: /ts_subscriber_demo +āœ“ Created subscription on topic: ts_demo +šŸ‘‚ Subscriber is listening. Press Ctrl+C to stop... + +šŸ“„ [1] Received: "Hello from TypeScript publisher! Message #1 at 2025-07-16T10:30:00.123Z" + Timestamp: 2025-07-16T10:30:00.124Z +šŸ“„ [2] Received: "Hello from TypeScript publisher! Message #2 at 2025-07-16T10:30:01.125Z" + Timestamp: 2025-07-16T10:30:01.126Z +... +``` + +## Project Structure + +``` +ts_demo/topics/ +ā”œā”€ā”€ package.json # Project dependencies and scripts +ā”œā”€ā”€ tsconfig.json # TypeScript configuration +ā”œā”€ā”€ README.md # This file +ā”œā”€ā”€ src/ # TypeScript source files +│ ā”œā”€ā”€ publisher.ts # Publisher implementation +│ └── subscriber.ts # Subscriber implementation +└── dist/ # Compiled JavaScript (after build) + ā”œā”€ā”€ publisher.js + └── subscriber.js +``` + +## Code Explanation + +### Publisher (`src/publisher.ts`) + +The publisher demonstrates: + +- **Node Creation**: Creates a ROS2 node using TypeScript +- **Publisher Setup**: Creates a publisher for `std_msgs/msg/String` messages +- **Timer Usage**: Uses `node.createTimer()` for periodic message publishing +- **Message Creation**: Uses `rclnodejs.createMessageObject()` with proper typing +- **Graceful Shutdown**: Handles SIGINT for clean shutdown + +Key TypeScript features used: + +```typescript +import * as rclnodejs from 'rclnodejs'; + +const node = new rclnodejs.Node('ts_publisher_demo'); +const publisher = node.createPublisher('std_msgs/msg/String', TOPIC_NAME); +const message = rclnodejs.createMessageObject('std_msgs/msg/String'); +``` + +### Subscriber (`src/subscriber.ts`) + +The subscriber demonstrates: + +- **Subscription Creation**: Creates a typed subscription for string messages +- **Callback Handling**: Processes incoming messages with proper TypeScript typing +- **Message Processing**: Displays received messages with timestamps + +Key TypeScript features used: + +```typescript +const subscription = node.createSubscription( + 'std_msgs/msg/String', + TOPIC_NAME, + (message: rclnodejs.std_msgs.msg.String) => { + console.log(`Received: "${message.data}"`); + } +); +``` + +## TypeScript Benefits + +This demo showcases several TypeScript advantages: + +1. **Type Safety**: Compile-time checking prevents common errors +2. **IntelliSense**: Better IDE support with autocompletion +3. **Interface Definitions**: Clear contract definitions for ROS2 messages +4. **Refactoring Safety**: Easier to maintain and modify code +5. **Documentation**: Types serve as inline documentation + +## Troubleshooting + +### Common Issues + +1. **Module not found error for 'rclnodejs':** + + ``` + Cannot find module 'rclnodejs' or its corresponding type declarations + ``` + + **Solution**: Ensure you're in the correct directory and rclnodejs is properly linked: + + ```bash + cd ts_demo/topics + npm install + ``` + +2. **ROS 2 environment not sourced:** + + ``` + Error: Unable to find ROS 2 installation + ``` + + **Solution**: Source your ROS 2 setup file: + + ```bash + source /opt/ros/jazzy/setup.bash + ``` + +3. **Build errors:** + ``` + TypeScript compilation errors + ``` + **Solution**: Check TypeScript configuration and ensure all dependencies are installed: + ```bash + npm install + npm run clean + npm run build + ``` + +### Debugging Tips + +1. **Enable verbose logging:** + + ```bash + export RCUTILS_LOGGING_SEVERITY=DEBUG + ``` + +2. **Check ROS 2 topic list:** + + ```bash + ros2 topic list + ros2 topic echo /ts_demo + ``` + +3. **Monitor topic info:** + ```bash + ros2 topic info /ts_demo + ``` + +## Customization + +You can easily customize this demo: + +### Change Topic Name + +Edit the `TOPIC_NAME` constant in both files: + +```typescript +const TOPIC_NAME = 'your_custom_topic'; +``` + +### Change Message Type + +To use a different message type, update the type string and interface: + +```typescript +// For example, using geometry_msgs/msg/Twist +const publisher = node.createPublisher('geometry_msgs/msg/Twist', 'cmd_vel'); +const message = rclnodejs.createMessageObject('geometry_msgs/msg/Twist'); +``` + +### Adjust Publishing Rate + +Modify the `PUBLISH_INTERVAL` in the publisher: + +```typescript +const PUBLISH_INTERVAL = 500; // Publish every 500ms +``` + +## Next Steps + +After running this demo, you might want to explore: + +1. **Different Message Types**: Try using other ROS2 message types like `geometry_msgs/msg/Twist` +2. **Services**: Implement ROS2 services with TypeScript +3. **Actions**: Create action servers and clients +4. **Parameters**: Use ROS2 parameters in your TypeScript nodes +5. **Lifecycle Nodes**: Implement managed lifecycle nodes + +## Contributing + +This demo is part of the rclnodejs project. To contribute: + +1. Fork the rclnodejs repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## License + +This demo is licensed under the Apache License 2.0, same as the rclnodejs project. diff --git a/ts_demo/topics/package.json b/ts_demo/topics/package.json new file mode 100644 index 00000000..73b0dca0 --- /dev/null +++ b/ts_demo/topics/package.json @@ -0,0 +1,37 @@ +{ + "name": "rclnodejs-ts-topics-demo", + "version": "1.0.0", + "description": "TypeScript demo for rclnodejs topics (publisher and subscriber)", + "main": "index.js", + "scripts": { + "prebuild": "npm run clean", + "build": "tsc", + "start:publisher": "npm run build && node dist/publisher.js", + "start:subscriber": "npm run build && node dist/subscriber.js", + "start:both": "npm run build && concurrently \"node dist/publisher.js\" \"node dist/subscriber.js\"", + "clean": "rimraf dist", + "dev:publisher": "ts-node src/publisher.ts", + "dev:subscriber": "ts-node src/subscriber.ts", + "check-types": "tsc --noEmit" + }, + "keywords": [ + "rclnodejs", + "ros2", + "typescript", + "publisher", + "subscriber", + "demo" + ], + "author": "rclnodejs contributors", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.16.4", + "concurrently": "^9.2.0", + "rimraf": "^6.0.1", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "dependencies": { + "rclnodejs": "file:../../" + } +} diff --git a/ts_demo/topics/src/publisher.ts b/ts_demo/topics/src/publisher.ts new file mode 100644 index 00000000..eeda20e3 --- /dev/null +++ b/ts_demo/topics/src/publisher.ts @@ -0,0 +1,76 @@ +/** + * TypeScript Publisher Demo for rclnodejs + * + * This demo shows how to create a ROS2 publisher using TypeScript + * with rclnodejs. It publishes string messages to the "ts_demo" topic. + */ + +import * as rclnodejs from 'rclnodejs'; + +const TOPIC_NAME = 'ts_demo'; +const PUBLISH_INTERVAL = 1000000000n; // milliseconds + +/** + * Main publisher function + */ +async function main(): Promise { + try { + console.log('Starting TypeScript Publisher Demo...'); + + // Initialize rclnodejs + await rclnodejs.init(); + console.log('āœ“ rclnodejs initialized'); + + // Create a node + const node = new rclnodejs.Node('ts_publisher_demo'); + console.log(`āœ“ Created node: ${node.getFullyQualifiedName()}`); + + // Create a publisher for std_msgs/msg/String messages + const publisher = node.createPublisher('std_msgs/msg/String', TOPIC_NAME); + console.log(`āœ“ Created publisher on topic: ${TOPIC_NAME}`); + + // Message counter + let messageCount = 0; + + // Create a timer to publish messages at regular intervals + const timer = node.createTimer(PUBLISH_INTERVAL, () => { + // Create a string message + const message = rclnodejs.createMessageObject('std_msgs/msg/String'); + message.data = `Hello from TypeScript publisher! Message #${++messageCount} at ${new Date().toISOString()}`; + + // Publish the message + publisher.publish(message); + console.log(`šŸ“¤ Published: "${message.data}"`); + }); + + console.log(`āœ“ Created timer with ${PUBLISH_INTERVAL}ms interval`); + console.log('šŸš€ Publisher is running. Press Ctrl+C to stop...\n'); + + // Spin the node to process callbacks + rclnodejs.spin(node); + } catch (error) { + console.error('āŒ Error in publisher:', error); + process.exit(1); + } +} + +// Handle shutdown gracefully +process.on('SIGINT', async () => { + console.log('\nšŸ›‘ Shutting down publisher...'); + try { + await rclnodejs.shutdown(); + console.log('āœ“ Publisher shutdown complete'); + process.exit(0); + } catch (error) { + console.error('āŒ Error during shutdown:', error); + process.exit(1); + } +}); + +// Run the main function +if (require.main === module) { + main().catch((error) => { + console.error('āŒ Fatal error:', error); + process.exit(1); + }); +} diff --git a/ts_demo/topics/src/subscriber.ts b/ts_demo/topics/src/subscriber.ts new file mode 100644 index 00000000..a17499e7 --- /dev/null +++ b/ts_demo/topics/src/subscriber.ts @@ -0,0 +1,71 @@ +/** + * TypeScript Subscriber Demo for rclnodejs + * + * This demo shows how to create a ROS2 subscriber using TypeScript + * with rclnodejs. It subscribes to string messages from the "ts_demo" topic. + */ + +import * as rclnodejs from 'rclnodejs'; + +const TOPIC_NAME = 'ts_demo'; + +/** + * Main subscriber function + */ +async function main(): Promise { + try { + console.log('Starting TypeScript Subscriber Demo...'); + + // Initialize rclnodejs + await rclnodejs.init(); + console.log('āœ“ rclnodejs initialized'); + + // Create a node + const node = new rclnodejs.Node('ts_subscriber_demo'); + console.log(`āœ“ Created node: ${node.getFullyQualifiedName()}`); + + // Message counter + let receivedCount = 0; + + // Create a subscription for std_msgs/msg/String messages + const subscription = node.createSubscription( + 'std_msgs/msg/String', + TOPIC_NAME, + (message: rclnodejs.std_msgs.msg.String) => { + receivedCount++; + console.log(`šŸ“„ [${receivedCount}] Received: "${message.data}"`); + console.log(` Timestamp: ${new Date().toISOString()}`); + } + ); + + console.log(`āœ“ Created subscription on topic: ${TOPIC_NAME}`); + console.log('šŸ‘‚ Subscriber is listening. Press Ctrl+C to stop...\n'); + + // Spin the node to process callbacks + rclnodejs.spin(node); + } catch (error) { + console.error('āŒ Error in subscriber:', error); + process.exit(1); + } +} + +// Handle shutdown gracefully +process.on('SIGINT', async () => { + console.log('\nšŸ›‘ Shutting down subscriber...'); + try { + await rclnodejs.shutdown(); + console.log('āœ“ Subscriber shutdown complete'); + process.exit(0); + } catch (error) { + console.error('āŒ Error during shutdown:', error); + process.exit(1); + } +}); + +// Run the main function +if (require.main === module) { + main().catch((error) => { + console.error('āŒ Fatal error:', error); + process.exit(1); + }); +} diff --git a/ts_demo/topics/tsconfig.json b/ts_demo/topics/tsconfig.json new file mode 100644 index 00000000..ab3cb478 --- /dev/null +++ b/ts_demo/topics/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "target": "es2020", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "lib": ["es2020"], + "typeRoots": [ + "./types", + "./node_modules/@types" + ] + }, + "include": [ + "src/**/*", + "types/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} From a0d56eec953a6fcca2ea1987a7e45c68403419b8 Mon Sep 17 00:00:00 2001 From: Minggang Wang Date: Wed, 16 Jul 2025 13:39:44 +0800 Subject: [PATCH 2/2] Address comments --- ts_demo/topics/package.json | 2 +- ts_demo/topics/src/publisher.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ts_demo/topics/package.json b/ts_demo/topics/package.json index 73b0dca0..a861d3a5 100644 --- a/ts_demo/topics/package.json +++ b/ts_demo/topics/package.json @@ -2,7 +2,7 @@ "name": "rclnodejs-ts-topics-demo", "version": "1.0.0", "description": "TypeScript demo for rclnodejs topics (publisher and subscriber)", - "main": "index.js", + "main": "dist/publisher.js", "scripts": { "prebuild": "npm run clean", "build": "tsc", diff --git a/ts_demo/topics/src/publisher.ts b/ts_demo/topics/src/publisher.ts index eeda20e3..04130780 100644 --- a/ts_demo/topics/src/publisher.ts +++ b/ts_demo/topics/src/publisher.ts @@ -8,7 +8,7 @@ import * as rclnodejs from 'rclnodejs'; const TOPIC_NAME = 'ts_demo'; -const PUBLISH_INTERVAL = 1000000000n; // milliseconds +const PUBLISH_INTERVAL = 1000000000n; // nanoseconds /** * Main publisher function