diff --git a/src/core/Param.js b/src/core/Param.js index 2ba6a143f..d3b843c3c 100644 --- a/src/core/Param.js +++ b/src/core/Param.js @@ -36,11 +36,11 @@ export default class Param { get(callback, failedCallback) { var paramClient = new Service({ ros: this.ros, - name: 'rosapi/get_param', + name: `${this.ros.namespace}rosapi/get_param`, serviceType: 'rosapi/GetParam' }); - var request = {name: this.name}; + var request = { name: this.name }; paramClient.callService( request, @@ -69,7 +69,7 @@ export default class Param { set(value, callback, failedCallback) { var paramClient = new Service({ ros: this.ros, - name: 'rosapi/set_param', + name: `${this.ros.namespace}rosapi/set_param`, serviceType: 'rosapi/SetParam' }); @@ -89,7 +89,7 @@ export default class Param { delete(callback, failedCallback) { var paramClient = new Service({ ros: this.ros, - name: 'rosapi/delete_param', + name: `${this.ros.namespace}rosapi/delete_param`, serviceType: 'rosapi/DeleteParam' }); diff --git a/src/core/Ros.js b/src/core/Ros.js index 51e1fa864..f46b1b110 100644 --- a/src/core/Ros.js +++ b/src/core/Ros.js @@ -29,12 +29,14 @@ export default class Ros extends EventEmitter { idCounter = 0; isConnected = false; groovyCompatibility = true; + namespace = ''; /** * @param {Object} [options] * @param {string} [options.url] - The WebSocket URL for rosbridge. Can be specified later with `connect`. * @param {boolean} [options.groovyCompatibility=true] - Don't use interfaces that changed after the last groovy release or rosbridge_suite and related tools. * @param {'websocket'|RTCPeerConnection} [options.transportLibrary='websocket'] - 'websocket', or an RTCPeerConnection instance controlling how the connection is created in `connect`. * @param {Object} [options.transportOptions={}] - The options to use when creating a connection. Currently only used if `transportLibrary` is RTCPeerConnection. + * @param {string} [options.namespace = ""] - The namespace to use for internal ROS2 services. Defaults to an empty string. Must be the same as the namespace of rosbridge_suite. */ constructor(options) { super(); @@ -42,6 +44,12 @@ export default class Ros extends EventEmitter { this.transportLibrary = options.transportLibrary || 'websocket'; this.transportOptions = options.transportOptions || {}; this.groovyCompatibility = options.groovyCompatibility ?? true; + this.namespace = options.namespace || ''; + + // Normalize namespace format: with trailing slash + if (this.namespace) { + this.namespace = this.namespace.replace(/\/?$/, '/'); + } // begin by checking if a URL was given if (options.url) { @@ -180,7 +188,7 @@ export default class Ros extends EventEmitter { /** @satisfies {Service} */ var getActionServers = new Service({ ros: this, - name: 'rosapi/action_servers', + name: `${this.namespace}rosapi/action_servers`, serviceType: 'rosapi/GetActionServers' }); @@ -220,7 +228,7 @@ export default class Ros extends EventEmitter { getTopics(callback, failedCallback) { var topicsClient = new Service({ ros: this, - name: 'rosapi/topics', + name: `${this.namespace}rosapi/topics`, serviceType: 'rosapi/Topics' }); @@ -259,7 +267,7 @@ export default class Ros extends EventEmitter { getTopicsForType(topicType, callback, failedCallback) { var topicsForTypeClient = new Service({ ros: this, - name: 'rosapi/topics_for_type', + name: `${this.namespace}rosapi/topics_for_type`, serviceType: 'rosapi/TopicsForType' }); @@ -299,7 +307,7 @@ export default class Ros extends EventEmitter { getServices(callback, failedCallback) { var servicesClient = new Service({ ros: this, - name: 'rosapi/services', + name: `${this.namespace}rosapi/services`, serviceType: 'rosapi/Services' }); @@ -338,7 +346,7 @@ export default class Ros extends EventEmitter { getServicesForType(serviceType, callback, failedCallback) { var servicesForTypeClient = new Service({ ros: this, - name: 'rosapi/services_for_type', + name: `${this.namespace}rosapi/services_for_type`, serviceType: 'rosapi/ServicesForType' }); @@ -380,7 +388,7 @@ export default class Ros extends EventEmitter { getServiceRequestDetails(type, callback, failedCallback) { var serviceTypeClient = new Service({ ros: this, - name: 'rosapi/service_request_details', + name: `${this.namespace}rosapi/service_request_details`, serviceType: 'rosapi/ServiceRequestDetails' }); var request = { @@ -422,7 +430,7 @@ export default class Ros extends EventEmitter { /** @satisfies {Service<{},{typedefs: string[]}>} */ var serviceTypeClient = new Service({ ros: this, - name: 'rosapi/service_response_details', + name: `${this.namespace}rosapi/service_response_details`, serviceType: 'rosapi/ServiceResponseDetails' }); var request = { @@ -462,7 +470,7 @@ export default class Ros extends EventEmitter { getNodes(callback, failedCallback) { var nodesClient = new Service({ ros: this, - name: 'rosapi/nodes', + name: `${this.namespace}rosapi/nodes`, serviceType: 'rosapi/Nodes' }); @@ -522,7 +530,7 @@ export default class Ros extends EventEmitter { getNodeDetails(node, callback, failedCallback) { var nodesClient = new Service({ ros: this, - name: 'rosapi/node_details', + name: `${this.namespace}rosapi/node_details`, serviceType: 'rosapi/NodeDetails' }); @@ -563,7 +571,7 @@ export default class Ros extends EventEmitter { getParams(callback, failedCallback) { var paramsClient = new Service({ ros: this, - name: 'rosapi/get_param_names', + name: `${this.namespace}rosapi/get_param_names`, serviceType: 'rosapi/GetParamNames' }); var request = {}; @@ -601,7 +609,7 @@ export default class Ros extends EventEmitter { getTopicType(topic, callback, failedCallback) { var topicTypeClient = new Service({ ros: this, - name: 'rosapi/topic_type', + name: `${this.namespace}rosapi/topic_type`, serviceType: 'rosapi/TopicType' }); var request = { @@ -642,7 +650,7 @@ export default class Ros extends EventEmitter { getServiceType(service, callback, failedCallback) { var serviceTypeClient = new Service({ ros: this, - name: 'rosapi/service_type', + name: `${this.namespace}rosapi/service_type`, serviceType: 'rosapi/ServiceType' }); var request = { @@ -683,7 +691,7 @@ export default class Ros extends EventEmitter { getMessageDetails(message, callback, failedCallback) { var messageDetailClient = new Service({ ros: this, - name: 'rosapi/message_details', + name: `${this.namespace}rosapi/message_details`, serviceType: 'rosapi/MessageDetails' }); var request = { @@ -775,7 +783,7 @@ export default class Ros extends EventEmitter { getTopicsAndRawTypes(callback, failedCallback) { var topicsAndRawTypesClient = new Service({ ros: this, - name: 'rosapi/topics_and_raw_types', + name: `${this.namespace}rosapi/topics_and_raw_types`, serviceType: 'rosapi/TopicsAndRawTypes' }); diff --git a/test/examples/setup_examples.launch b/test/examples/setup_examples.launch index 7b36ff1db..91b8d33b5 100644 --- a/test/examples/setup_examples.launch +++ b/test/examples/setup_examples.launch @@ -1,5 +1,19 @@ - + + + + + + + + + + + + + + + diff --git a/test/namespaces.test.js b/test/namespaces.test.js new file mode 100644 index 000000000..8ff6bae98 --- /dev/null +++ b/test/namespaces.test.js @@ -0,0 +1,128 @@ +import { it, describe, expect } from 'vitest'; +import { Ros } from '../'; + +describe('Namespaces', () => { + + it('Can retrieve a list of topics using a namespace using a trailing slash', async () => { + + const ros = new Ros({ + url: 'ws://localhost:9091', + namespace: 'hello/' + }); + + const topics = await new Promise((resolve, reject) => { + ros.getTopics(resolve, reject); + }); + + // we expect the topics object to not be empty + expect(topics).be.an('object'); + expect(topics).not.hasLength(0); + + }); + + it('Can retrieve a list of topics using a namespace with a leading slash', async () => { + + const ros = new Ros({ + url: 'ws://localhost:9091', + namespace: '/hello/' + }); + + const topics = await new Promise((resolve, reject) => { + ros.getTopics(resolve, reject); + }); + + expect(topics).be.an('object'); + expect(topics).not.hasLength(0); + }); + + it('Can retrieve a list of topics using a namespace with a leading and no trailing slash', async () => { + + const ros = new Ros({ + url: 'ws://localhost:9091', + namespace: '/hello' + }); + + const topics = await new Promise((resolve, reject) => { + ros.getTopics(resolve, reject); + }); + + expect(topics).be.an('object'); + expect(topics).not.hasLength(0); + }) + + it('Can retrieve a list of topics using a nested namespace', async () => { + + const ros = new Ros({ + url: 'ws://localhost:9092', + namespace: 'hello/world/' + }); + + const topics = await new Promise((resolve, reject) => { + ros.getTopics(resolve, reject); + }); + + expect(topics).be.an('object'); + expect(topics).not.hasLength(0); + }); + + it('Can retrieve a list of topics using a nested namespace with a leading slash', async () => { + + const ros = new Ros({ + url: 'ws://localhost:9092', + namespace: '/hello/world/' + }); + + const topics = await new Promise((resolve, reject) => { + ros.getTopics(resolve, reject); + }); + + expect(topics).be.an('object'); + expect(topics).not.hasLength(0); + }); + + it('Can retrieve a list of topics using a nested namespace with a leading and no trailing slash', async () => { + + const ros = new Ros({ + url: 'ws://localhost:9092', + namespace: '/hello/world' + }); + + const topics = await new Promise((resolve, reject) => { + ros.getTopics(resolve, reject); + }); + + expect(topics).be.an('object'); + expect(topics).not.hasLength(0); + }); + + + it('Can retrieve a list of topics using an empty namespaces', async () => { + + const ros = new Ros({ + url: 'ws://localhost:9090', + namespace: '' + }); + + const topics = await new Promise((resolve, reject) => { + ros.getTopics(resolve, reject); + }); + + expect(topics).be.an('object'); + expect(topics).not.hasLength(0); + }) + + it('Can retrieve a list of topics using an empty namespace, not set in constructor', async () => { + + const ros = new Ros({ + url: 'ws://localhost:9090' + }); + + const topics = await new Promise((resolve, reject) => { + ros.getTopics(resolve, reject); + }); + + expect(topics).be.an('object'); + expect(topics).not.hasLength(0); + }) + +})