diff --git a/.npmignore b/.npmignore index 793d73dd..b961a594 100644 --- a/.npmignore +++ b/.npmignore @@ -30,3 +30,4 @@ scripts/cpplint.js scripts/npm-pack.sh scripts/npmjs-readme.md scripts/run_test.js +ts_demo/ diff --git a/README.md b/README.md index cb2bdaa6..86987d33 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ rclnodejs.init().then(() => { - [Using TypeScript](#using-rclnodejs-with-typescript) - [ROS2 Interface Message Generation](#ros2-interface-message-generation-important) - [Examples](https://github.com/RobotWebTools/rclnodejs/tree/develop/example) -- [Electron demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo) +- [Electron Demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/electron_demo) +- [TypeScript Demo](https://github.com/RobotWebTools/rclnodejs/tree/develop/ts_demo) - [Efficient Usage Tips](./docs/EFFICIENCY.md) - [FAQ and Known Issues](./docs/FAQ.md) - [Building from Scratch](./docs/BUILDING.md) diff --git a/ts_demo/services/README.md b/ts_demo/services/README.md new file mode 100644 index 00000000..a7029545 --- /dev/null +++ b/ts_demo/services/README.md @@ -0,0 +1,287 @@ +# rclnodejs TypeScript Services Demo + +This demo demonstrates how to use **rclnodejs** with TypeScript to create ROS2 service servers and clients. The demo includes a service server that provides an `AddTwoInts` service and a client that calls the service with random numbers. + +## Features + +- šŸš€ **TypeScript Support**: Fully typed ROS2 service implementation using rclnodejs TypeScript interfaces +- šŸ”¢ **Service Server**: Provides an AddTwoInts service that adds two integers +- šŸ“ž **Service Client**: Calls the AddTwoInts service with random numbers at regular intervals +- šŸ›”ļø **Type Safety**: Leverages TypeScript's type system for compile-time safety +- šŸŽÆ **Error Handling**: Comprehensive error handling and graceful shutdown +- šŸ“Š **Request Counting**: Tracks service requests and responses +- šŸŽØ **Console Output**: Colorful and informative console messages +- ā±ļø **Service Discovery**: Client waits for service availability before making requests + +## 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/services +npm install +``` + +## Usage + +### Build and Run + +1. **Build the TypeScript code:** + + ```bash + npm run build + ``` + +2. **Run the service server (in one terminal):** + + ```bash + npm run start:server + ``` + +3. **Run the service client (in another terminal):** + + ```bash + npm run start:client + ``` + +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 service server:** + + ```bash + npm run dev:server + ``` + +2. **Run the service client:** + + ```bash + npm run dev:client + ``` + +3. **Type check only:** + ```bash + npm run check-types + ``` + +## How It Works + +### Service Server (`server.ts`) + +The service server: + +- Creates a ROS2 node named `ts_server_demo` +- Provides an `AddTwoInts` service at `/add_two_ints` +- Receives requests with two integers (`a` and `b`) +- Calculates the sum and returns it in the response +- Logs each request and response with timestamps + +### Service Client (`client.ts`) + +The service client: + +- Creates a ROS2 node named `ts_client_demo` +- Waits for the `AddTwoInts` service to become available +- Sends requests every 3 seconds with random integers +- Displays the results and verifies the calculations +- Handles service availability and error cases + +### Example Output + +**Service Server:** + +``` +Starting TypeScript Service Server Demo... +āœ“ rclnodejs initialized +āœ“ Created node: /mock_namespace/ts_server_demo +āœ“ Created service server: add_two_ints +šŸš€ Service server is running. Press Ctrl+C to stop... + +šŸ”¢ [1] Received request: a=42, b=17 +šŸ“¤ [1] Sending response: sum=59 + Calculation: 42 + 17 = 59 + Timestamp: 2025-07-16T02:30:15.123Z +``` + +**Service Client:** + +``` +Starting TypeScript Service Client Demo... +āœ“ rclnodejs initialized +āœ“ Created node: /mock_namespace/ts_client_demo +āœ“ Created service client: add_two_ints +ā³ Waiting for service to be available... +āœ“ Service is available +šŸš€ Starting to send requests. Press Ctrl+C to stop... + +šŸ“ž [1] Sending request: a=42, b=17 + Timestamp: 2025-07-16T02:30:15.120Z +šŸ“Ø [1] Received response: sum=59 + Verification: 42 + 17 = 59 āœ“ + Response time: 2025-07-16T02:30:15.125Z +``` + +## TypeScript Configuration + +This demo includes: + +- **Local Type Definitions**: Custom TypeScript definitions for rclnodejs in `types/rclnodejs.d.ts` +- **Service Types**: Type definitions for `example_interfaces/srv/AddTwoInts` +- **Strict Type Checking**: Full TypeScript strict mode enabled +- **Build Pipeline**: Automated compilation with shebang fixing for executable JavaScript + +## Service Interface + +The `AddTwoInts` service interface: + +```typescript +export interface AddTwoInts { + Request: { + a: number; // First integer + b: number; // Second integer + }; + Response: { + sum: number; // Sum of a and b + }; +} +``` + +## Customization + +You can easily customize this demo: + +### Change Service Name + +Edit the `SERVICE_NAME` constant in both files: + +```typescript +const SERVICE_NAME = 'your_custom_service'; +``` + +### Change Request Interval + +Modify the `REQUEST_INTERVAL` in the client: + +```typescript +const REQUEST_INTERVAL = 1000; // Send requests every 1 second +``` + +### Use Different Service Types + +To use a different service type, update the type string and interface: + +```typescript +// For example, using a custom service +const service = node.createService( + 'your_package/srv/YourService', + 'service_name', + callback +); +``` + +### Add Custom Logic + +Modify the service callback to implement your own business logic: + +```typescript +(request, response) => { + // Your custom service logic here + response.result = processRequest(request); +}; +``` + +## Troubleshooting + +### Common Issues + +1. **Service not available:** + + ``` + Service not available after 5 seconds + ``` + + **Solution**: Make sure the service server is running before starting the client. + +2. **Module not found error:** + + ``` + Cannot find module 'rclnodejs' + ``` + + **Solution**: Ensure rclnodejs is properly linked and you're in the correct directory. + +3. **TypeScript compilation errors:** + ``` + Type errors in service definitions + ``` + **Solution**: Check the type definitions in `types/rclnodejs.d.ts` match your usage. + +### Debugging Tips + +1. **Check service list:** + + ```bash + ros2 service list + ros2 service type /add_two_ints + ``` + +2. **Call service manually:** + + ```bash + ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 1, b: 2}" + ``` + +3. **Monitor service info:** + ```bash + ros2 service info /add_two_ints + ``` + +## Next Steps + +After running this demo, you might want to explore: + +1. **Custom Service Types**: Create your own service definitions +2. **Asynchronous Services**: Implement long-running service operations +3. **Service Parameters**: Use ROS2 parameters in your services +4. **Service Discovery**: Implement dynamic service discovery +5. **Lifecycle Services**: Create managed lifecycle service nodes + +## License + +This demo is licensed under the Apache License 2.0, same as the rclnodejs project. diff --git a/ts_demo/services/package.json b/ts_demo/services/package.json new file mode 100644 index 00000000..73b33f57 --- /dev/null +++ b/ts_demo/services/package.json @@ -0,0 +1,38 @@ +{ + "name": "rclnodejs-ts-services-demo", + "version": "1.0.0", + "description": "TypeScript demo for rclnodejs services (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", + "service", + "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/services/src/client.ts b/ts_demo/services/src/client.ts new file mode 100644 index 00000000..b78fe2d9 --- /dev/null +++ b/ts_demo/services/src/client.ts @@ -0,0 +1,115 @@ +/** + * TypeScript Service Client Demo for rclnodejs + * + * This demo shows how to create a ROS2 service client using TypeScript + * with rclnodejs. It calls the AddTwoInts service with random numbers. + */ + +import * as rclnodejs from 'rclnodejs'; + +const SERVICE_NAME = 'add_two_ints'; +const REQUEST_INTERVAL = 3000000000n; // 3 seconds in nanoseconds + +/** + * Main service client function + */ +async function main(): Promise { + try { + console.log('Starting TypeScript Service Client Demo...'); + + // Initialize rclnodejs + await rclnodejs.init(); + console.log('āœ“ rclnodejs initialized'); + + // Create a node + const node = new rclnodejs.Node('ts_client_demo'); + console.log(`āœ“ Created node: ${node.getFullyQualifiedName()}`); + + // Create a client for AddTwoInts service + const client = node.createClient( + 'example_interfaces/srv/AddTwoInts', + SERVICE_NAME + ); + console.log(`āœ“ Created service client: ${SERVICE_NAME}`); + + // Wait for the service to be available + console.log('ā³ Waiting for service to be available...'); + const serviceAvailable = await client.waitForService(5000); + + if (!serviceAvailable) { + console.error('āŒ Service not available after 5 seconds'); + console.log('šŸ’” Make sure the service server is running!'); + process.exit(1); + } + + console.log('āœ“ Service is available'); + console.log('šŸš€ Starting to send requests. Press Ctrl+C to stop...\n'); + + // Request counter + let requestCount = 0; + + // Create a timer to send requests at regular intervals + const timer = node.createTimer(REQUEST_INTERVAL, () => { + try { + requestCount++; + + // Generate random numbers for the request + const a = Math.floor(Math.random() * 100); + const b = Math.floor(Math.random() * 100); + + // Create request message + const request = { + a: BigInt(a), + b: BigInt(b), + }; + + console.log(`šŸ“ž [${requestCount}] Sending request: a=${a}, b=${b}`); + console.log(` Timestamp: ${new Date().toISOString()}`); + + // Send the request and wait for response + client.sendRequest(request, (response: any) => { + console.log( + `šŸ“Ø [${requestCount}] Received response: sum=${response.sum}` + ); + console.log( + ` Verification: ${a} + ${b} = ${Number(response.sum)} āœ“` + ); + console.log(` Response time: ${new Date().toISOString()}\n`); + }); + } catch (error) { + console.error(`āŒ [${requestCount}] Error sending request:`, error); + } + }); + + console.log( + `āœ“ Created timer with ${Number(REQUEST_INTERVAL / 1000000n)}ms interval` + ); + + // Spin the node to process callbacks + rclnodejs.spin(node); + } catch (error) { + console.error('āŒ Error in service client:', error); + process.exit(1); + } +} + +// Handle shutdown gracefully +process.on('SIGINT', async () => { + console.log('\nšŸ›‘ Shutting down service client...'); + try { + await rclnodejs.shutdown(); + console.log('āœ“ Service client 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/services/src/server.ts b/ts_demo/services/src/server.ts new file mode 100644 index 00000000..36721670 --- /dev/null +++ b/ts_demo/services/src/server.ts @@ -0,0 +1,88 @@ +/** + * TypeScript Service Server Demo for rclnodejs + * + * This demo shows how to create a ROS2 service server using TypeScript + * with rclnodejs. It provides an AddTwoInts service that adds two integers. + */ + +import * as rclnodejs from 'rclnodejs'; + +const SERVICE_NAME = 'add_two_ints'; + +/** + * Main service server function + */ +async function main(): Promise { + try { + console.log('Starting TypeScript Service Server Demo...'); + + // Initialize rclnodejs + await rclnodejs.init(); + console.log('āœ“ rclnodejs initialized'); + + // Create a node + const node = new rclnodejs.Node('ts_server_demo'); + console.log(`āœ“ Created node: ${node.getFullyQualifiedName()}`); + + // Request counter + let requestCount = 0; + + // Create a service server for AddTwoInts + const service = node.createService( + 'example_interfaces/srv/AddTwoInts', + SERVICE_NAME, + (request: any, response: any) => { + requestCount++; + + // Log the incoming request + console.log( + `šŸ”¢ [${requestCount}] Received request: a=${request.a}, b=${request.b}` + ); + + // Calculate the sum using response.template + let result = response.template; + result.sum = request.a + request.b; + + // Log the response + console.log(`šŸ“¤ [${requestCount}] Sending response: sum=${result.sum}`); + console.log( + ` Calculation: ${request.a} + ${request.b} = ${result.sum}` + ); + console.log(` Timestamp: ${new Date().toISOString()}\n`); + + // Send the response + response.send(result); + } + ); + + console.log(`āœ“ Created service server: ${SERVICE_NAME}`); + console.log('šŸš€ Service server is running. Press Ctrl+C to stop...\n'); + + // Spin the node to process callbacks + rclnodejs.spin(node); + } catch (error) { + console.error('āŒ Error in service server:', error); + process.exit(1); + } +} + +// Handle shutdown gracefully +process.on('SIGINT', async () => { + console.log('\nšŸ›‘ Shutting down service server...'); + try { + await rclnodejs.shutdown(); + console.log('āœ“ Service server 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/services/tsconfig.json b/ts_demo/services/tsconfig.json new file mode 100644 index 00000000..be68180b --- /dev/null +++ b/ts_demo/services/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "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/**/*", + "types/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts" + ] +}