From 32191cdd961326f1c740ad25861005624105acb8 Mon Sep 17 00:00:00 2001 From: FlorianLebecque Date: Wed, 5 Feb 2025 15:17:43 +0100 Subject: [PATCH 1/6] Add namespace support for the new version of Rosbridge_suite --- src/core/Param.js | 8 ++++---- src/core/Ros.js | 36 ++++++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/core/Param.js b/src/core/Param.js index 2ba6a143f..f63f11233 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..d6926ab0a 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: no leading slash, with trailing slash + if (this.namespace) { + this.namespace = this.namespace.replace(/^\//, '').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' }); From 7e3283ce2a1654908ecfdd90946fecdfba3f3812 Mon Sep 17 00:00:00 2001 From: FlorianLebecque Date: Fri, 7 Feb 2025 11:22:44 +0100 Subject: [PATCH 2/6] Add test file for the different use case of the namespaces --- test/namespaces.test.js | 128 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 test/namespaces.test.js diff --git a/test/namespaces.test.js b/test/namespaces.test.js new file mode 100644 index 000000000..cc7f55fd9 --- /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 array to not be empty + expect(topics).be.an('array'); + 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('array'); + 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('array'); + 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('array'); + 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('array'); + 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('array'); + 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('array'); + 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('array'); + expect(topics).not.hasLength(0); + }) + +}) \ No newline at end of file From a453e31d6a4a5940cc33938e85df99fa6f7d2127 Mon Sep 17 00:00:00 2001 From: FlorianLebecque Date: Fri, 7 Feb 2025 13:05:34 +0100 Subject: [PATCH 3/6] Allow absolute namespaces Add missing string template Add missing EOF newline --- src/core/Param.js | 2 +- src/core/Ros.js | 4 ++-- test/namespaces.test.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/Param.js b/src/core/Param.js index f63f11233..d3b843c3c 100644 --- a/src/core/Param.js +++ b/src/core/Param.js @@ -36,7 +36,7 @@ export default class Param { get(callback, failedCallback) { var paramClient = new Service({ ros: this.ros, - name: this.ros.namespace + 'rosapi/get_param', + name: `${this.ros.namespace}rosapi/get_param`, serviceType: 'rosapi/GetParam' }); diff --git a/src/core/Ros.js b/src/core/Ros.js index d6926ab0a..f46b1b110 100644 --- a/src/core/Ros.js +++ b/src/core/Ros.js @@ -46,9 +46,9 @@ export default class Ros extends EventEmitter { this.groovyCompatibility = options.groovyCompatibility ?? true; this.namespace = options.namespace || ''; - // Normalize namespace format: no leading slash, with trailing slash + // Normalize namespace format: with trailing slash if (this.namespace) { - this.namespace = this.namespace.replace(/^\//, '').replace(/\/?$/, '/'); + this.namespace = this.namespace.replace(/\/?$/, '/'); } // begin by checking if a URL was given diff --git a/test/namespaces.test.js b/test/namespaces.test.js index cc7f55fd9..c2a4b4055 100644 --- a/test/namespaces.test.js +++ b/test/namespaces.test.js @@ -125,4 +125,4 @@ describe('Namespaces', () => { expect(topics).not.hasLength(0); }) -}) \ No newline at end of file +}) From d7055e95a6d8094f285305a042a4e3c7e6b4be91 Mon Sep 17 00:00:00 2001 From: FlorianLebecque Date: Fri, 7 Feb 2025 13:28:37 +0100 Subject: [PATCH 4/6] Tests strings are now single quotes --- test/namespaces.test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/namespaces.test.js b/test/namespaces.test.js index c2a4b4055..8ded6d3ae 100644 --- a/test/namespaces.test.js +++ b/test/namespaces.test.js @@ -3,7 +3,7 @@ import { Ros } from '../'; describe('Namespaces', () => { - it("Can retrieve a list of topics using a namespace using a trailing slash", async () => { + it('Can retrieve a list of topics using a namespace using a trailing slash', async () => { const ros = new Ros({ url: 'ws://localhost:9091', @@ -20,7 +20,7 @@ describe('Namespaces', () => { }); - it("Can retrieve a list of topics using a namespace with a leading slash", async () => { + it('Can retrieve a list of topics using a namespace with a leading slash', async () => { const ros = new Ros({ url: 'ws://localhost:9091', @@ -35,7 +35,7 @@ describe('Namespaces', () => { expect(topics).not.hasLength(0); }); - it("Can retrieve a list of topics using a namespace with a leading and no trailing slash", async () => { + 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', @@ -50,7 +50,7 @@ describe('Namespaces', () => { expect(topics).not.hasLength(0); }) - it("Can retrieve a list of topics using a nested namespace", async () => { + it('Can retrieve a list of topics using a nested namespace', async () => { const ros = new Ros({ url: 'ws://localhost:9092', @@ -65,7 +65,7 @@ describe('Namespaces', () => { expect(topics).not.hasLength(0); }); - it("Can retrieve a list of topics using a nested namespace with a leading slash", async () => { + it('Can retrieve a list of topics using a nested namespace with a leading slash', async () => { const ros = new Ros({ url: 'ws://localhost:9092', @@ -80,7 +80,7 @@ describe('Namespaces', () => { expect(topics).not.hasLength(0); }); - it("Can retrieve a list of topics using a nested namespace with a leading and no trailing slash", async () => { + 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', @@ -96,7 +96,7 @@ describe('Namespaces', () => { }); - it("Can retrieve a list of topics using an empty namespaces", async () => { + it('Can retrieve a list of topics using an empty namespaces', async () => { const ros = new Ros({ url: 'ws://localhost:9090', @@ -111,7 +111,7 @@ describe('Namespaces', () => { expect(topics).not.hasLength(0); }) - it("Can retrieve a list of topics using an empty namespace, not set in constructor", async () => { + it('Can retrieve a list of topics using an empty namespace, not set in constructor', async () => { const ros = new Ros({ url: 'ws://localhost:9090' From b36a5c859e3a571629f914a81175b278c238eb17 Mon Sep 17 00:00:00 2001 From: FlorianLebecque Date: Mon, 3 Mar 2025 09:56:39 +0100 Subject: [PATCH 5/6] Fix test, topic result is an object --- test/namespaces.test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/namespaces.test.js b/test/namespaces.test.js index 8ded6d3ae..8ff6bae98 100644 --- a/test/namespaces.test.js +++ b/test/namespaces.test.js @@ -14,8 +14,8 @@ describe('Namespaces', () => { ros.getTopics(resolve, reject); }); - // we expect the topics array to not be empty - expect(topics).be.an('array'); + // we expect the topics object to not be empty + expect(topics).be.an('object'); expect(topics).not.hasLength(0); }); @@ -31,7 +31,7 @@ describe('Namespaces', () => { ros.getTopics(resolve, reject); }); - expect(topics).be.an('array'); + expect(topics).be.an('object'); expect(topics).not.hasLength(0); }); @@ -46,7 +46,7 @@ describe('Namespaces', () => { ros.getTopics(resolve, reject); }); - expect(topics).be.an('array'); + expect(topics).be.an('object'); expect(topics).not.hasLength(0); }) @@ -61,7 +61,7 @@ describe('Namespaces', () => { ros.getTopics(resolve, reject); }); - expect(topics).be.an('array'); + expect(topics).be.an('object'); expect(topics).not.hasLength(0); }); @@ -76,7 +76,7 @@ describe('Namespaces', () => { ros.getTopics(resolve, reject); }); - expect(topics).be.an('array'); + expect(topics).be.an('object'); expect(topics).not.hasLength(0); }); @@ -91,7 +91,7 @@ describe('Namespaces', () => { ros.getTopics(resolve, reject); }); - expect(topics).be.an('array'); + expect(topics).be.an('object'); expect(topics).not.hasLength(0); }); @@ -107,7 +107,7 @@ describe('Namespaces', () => { ros.getTopics(resolve, reject); }); - expect(topics).be.an('array'); + expect(topics).be.an('object'); expect(topics).not.hasLength(0); }) @@ -121,7 +121,7 @@ describe('Namespaces', () => { ros.getTopics(resolve, reject); }); - expect(topics).be.an('array'); + expect(topics).be.an('object'); expect(topics).not.hasLength(0); }) From 94345804a52999ef510b275de2d17a29594db7bc Mon Sep 17 00:00:00 2001 From: FlorianLebecque Date: Mon, 3 Mar 2025 09:56:54 +0100 Subject: [PATCH 6/6] Add different bridge --- test/examples/setup_examples.launch | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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 @@ - + + + + + + + + + + + + + + +