├── .dockerignore
├── .eslintrc.json
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── docker
├── Dockerfile
├── README.md
├── compose.yaml
├── entrypoint.bash
└── turtlesim.json
├── docs
├── FIWAREPublisher.gif
├── FIWAREPublisher.jpg
├── FIWAREPublisherDialog.jpg
├── FIWARESubscriber.gif
├── FIWARESubscriber.jpg
├── FIWAREsettings.jpg
├── IDLType.png
├── ROS2ConfigNode.jpg
├── ROS2Inject.png
├── ROS2InjectIDL.jpg
├── ROS2InjectIDLDialog.jpg
├── ROS2InjectPackage.jpg
├── ROS2InjectPackageDialog.jpg
├── ROS2Publisher.gif
├── ROS2Publisher.jpg
├── ROS2PublisherDialog.jpg
├── ROS2Subscriber.gif
├── ROS2Subscriber.jpg
├── ROS2Type.png
├── eu_flag.jpg
├── idl-definition.jpg
├── idl-type-name.jpg
├── packages.jpg
├── palette.jpg
├── ros2-type-name.png
├── turtlesim.gif
└── turtlesim.json
├── mkdocs.yml
├── package.json
├── src
├── dds-settings
│ ├── dds-settings.html
│ ├── dds-settings.js
│ └── icons
│ │ └── ros2-icon.svg
├── fiware-settings
│ ├── fiware-settings.html
│ ├── fiware-settings.js
│ └── icons
│ │ └── fiware.png
├── fiware_publisher
│ ├── fiware_publisher.html
│ ├── fiware_publisher.js
│ └── icons
│ │ └── fiware.png
├── fiware_subscriber
│ ├── fiware_subscriber.html
│ ├── fiware_subscriber.js
│ └── icons
│ │ └── fiware.png
├── idl-types
│ ├── icons
│ │ └── ros2-icon.svg
│ ├── idl-types.html
│ └── idl-types.js
├── publisher
│ ├── icons
│ │ └── ros2-icon.svg
│ ├── publisher.html
│ ├── publisher.js
│ └── ros2-icon.png
├── qos-description.json
├── ros2-message-inject
│ ├── icons
│ │ └── ros2-icon.svg
│ ├── ros2-message-inject.html
│ └── ros2-message-inject.js
├── ros2-types
│ ├── icons
│ │ └── ros2-icon.svg
│ ├── ros2-types.html
│ └── ros2-types.js
└── subscriber
│ ├── icons
│ └── ros2-icon.svg
│ ├── subscriber.html
│ └── subscriber.js
├── test
└── test.js
└── theme
├── README.md
├── theme.css
└── theme.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tamia",
3 | "parserOptions": {
4 | "sourceType": "module"
5 | },
6 | "plugins": ["prettier"],
7 | "rules": {
8 | "prettier/prettier": "error",
9 | "valid-jsdoc": 0
10 | }
11 | }
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | 'on':
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | branches:
8 | - master
9 | jobs:
10 | lint-markdown:
11 | name: Lint Markdown
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Git checkout
15 | uses: actions/checkout@v2
16 | - name: Use Node.js 12.x
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: 12.x
20 | - name: Run Remark Markdown Linter
21 | run: |
22 | npm install
23 | npm run lint:md
24 | - name: Run Textlint Markdown Linter
25 | run: npm run lint:text
26 |
27 | lint-code:
28 | name: Lint JavaScript
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Git checkout
32 | uses: actions/checkout@v2
33 | - name: Use Node.js 12.x
34 | uses: actions/setup-node@v1
35 | with:
36 | node-version: 12.x
37 | - name: Run EsLint Node.js Linter
38 | run: |
39 | npm install
40 | npm run lint
41 |
42 | unit-test:
43 | name: Unit Tests
44 | runs-on: ubuntu-latest
45 | services:
46 | mongodb:
47 | image: mongo:3.6
48 | ports:
49 | - 27017:27017
50 | strategy:
51 | matrix:
52 | node-version:
53 | - 10.x
54 | - 12.x
55 | steps:
56 | - name: Git checkout
57 | uses: actions/checkout@v2
58 | - name: 'Install Node.js ${{ matrix.node-version }}'
59 | uses: actions/setup-node@v1
60 | with:
61 | node-version: '${{ matrix.node-version }}'
62 | - name: 'Unit Tests with Node.js ${{ matrix.node-version }}'
63 | run: |
64 | npm install
65 | npm test
66 |
67 | code-coverage:
68 | name: Coveralls Code Coverage
69 | runs-on: ubuntu-latest
70 | needs: unit-test
71 | services:
72 | mongodb:
73 | image: mongo:3.6
74 | ports:
75 | - 27017:27017
76 | steps:
77 | - name: Git checkout
78 | uses: actions/checkout@v2
79 | - name: 'Test Coverage with Node.js 12.x'
80 | uses: actions/setup-node@v1
81 | with:
82 | node-version: 12.x
83 | - run: |
84 | npm install
85 | npm run test:coverage
86 | - name: Push to Coveralls
87 | uses: coverallsapp/github-action@master
88 | with:
89 | github-token: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | .fuse_hidden*
3 | .directory
4 | .Trash-*
5 | .nfs*
6 | *.DS_Store
7 | .AppleDouble
8 | .LSOverride
9 | Icon
10 | ._*
11 | .DocumentRevisions-V100
12 | .fseventsd
13 | .Spotlight-V100
14 | .TemporaryItems
15 | .Trashes
16 | .VolumeIcon.icns
17 | .com.apple.timemachine.donotpresent
18 | .AppleDB
19 | .AppleDesktop
20 | Network Trash Folder
21 | Temporary Items
22 | .apdisk
23 | logs
24 | *.log
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 | pids
29 | *.pid
30 | *.seed
31 | *.pid.lock
32 | node_modules/
33 | jspm_packages/
34 | .npm
35 | .eslintcache
36 | .nyc_output/
37 | coverage/
38 | *.tgz
39 | .env
40 | .next
41 | push.sh
42 | package-lock.json
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSpacing": true,
4 | "singleQuote": true,
5 | "parser": "flow",
6 | "printWidth": 120,
7 | "trailingComma": "none",
8 | "tabWidth": 4
9 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020-2023 www.eprosima.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node-RED ROS 2 Plugin
2 |
3 | [](https://opensource.org/licenses/MIT)
4 |
5 | This project is part of [DIH^2](http://www.dih-squared.eu/). The main goal is provide [Node-RED](https://nodered.org/docs/) interoperability with
6 | [ROS2](https://docs.ros.org/) and [FIWARE](https://fiware-orion.readthedocs.io/en/master/). The plugin introduces in the
7 | Node-RED palette new nodes dealing with:
8 |
9 | ### Type definition
10 |
11 | In order to transmit information it is necessary to precisely define the composition of the data delivered.
12 |
13 | Node-RED approach is based on [JSON](https://www.json.org/json-en.html) which is versatile and user friendly
14 | but cannot be used to interoperate with industrial protocols that require language-independent type Description.
15 |
16 | In order to provide this interoperability ROS2 introduced [IDL](https://www.omg.org/spec/IDL/4.2/About-IDL). Which is
17 | a data type and interfaces descriptive language customary in industrial applications.
18 |
19 | The new nodes make both: IDL type descriptions and well known ROS2 types available.
20 |
21 | ### ROS2 Publisher-Subscriber interface
22 |
23 | Publisher and Subscriber nodes are provided to directly access its ROS2
24 | counterparts.
25 |
26 | Different topics and QoS can be selected. Also a global configuration node allows to select the ROS domain to enforce.
27 |
28 | ### FIWARE Context Broker Publisher-Subscriber interface
29 |
30 | The Context Broker doesn't provide a Publisher-Subscriber
31 | interface (works more like a database) but a translation can be easily performed if:
32 |
33 | - Entities are understood as topics.
34 | - Creating or setting an entry is understood as publishing.
35 | - Notification callbacks on an entity are understood as subscribtion callbacks.
36 |
37 | ## Contents
38 |
39 | - [Background](#background)
40 | - [Install](#install)
41 | - [Usage](#usage)
42 | + [ROS2 nodes usage](#ros2-nodes-usage)
43 | + [ROS2 Examples](#ros2-examples)
44 | + [FIWARE nodes usage](#fiware-nodes-usage)
45 | + [FIWARE Examples](#fiware-examples)
46 |
47 | ## Background
48 |
49 | The interoperability between the plugin and the ROS2 and FIWARE Broker environments is achieved using [WebSocket](https://websockets.spec.whatwg.org//)
50 | bridges to them. This was the natural choice given that Node-RED relies on WebSocket for front-end/back-end
51 | communication.
52 |
53 | These bridges are generated using [Integration-Service](https://integration-service.docs.eprosima.com/en/latest/) an
54 | [eProsima](https://www.eprosima.com/) open-source tool.
55 |
56 | Using Integration-Service directly from the plugin was possible, but it was considered a better choice to create another
57 | Node.js library ([is-web-api](https://github.com/eProsima/is-web-api), to abstract the bridge operation. This way:
58 | + The plugin can rely on any other bridge technology.
59 | + Development is simplified by enforcing separation of concerns.
60 | + Any other Node.js project (besides the plugin) can profit from the bridge library.
61 |
62 | ## Install
63 |
64 | A [Dockerfile](./docker/Dockerfile) is provided to exemplify the set up on an argument provided ROS2 distro.
65 |
66 | ### Dependencies
67 |
68 | Some of the following installation steps can be skipped if the target system already fulfils some of the requirements:
69 |
70 | 1. ROS2 installation. Follow the [official ROS2 installation guide](https://docs.ros.org/en/humble/Installation.html)
71 | for the distro of choice. The Dockerfile is based on a ROS2 image, so this is not exemplified.
72 |
73 | 1. Install Node.js. The usual OS package managers (like `apt` on Ubuntu or `winget/chocolatey` on windows) provide it.
74 | An exhaustive list is available [here](https://nodejs.org/en/download/package-manager).
75 | Some package managers constrain the user to a specific version of Node.js. The Node.js [site](https://nodejs.org/en/download)
76 | hints on how to install specific versions.
77 |
78 | For example, in `apt` is possible to add via location configuration file a new remote repository where all Node.js
79 | versions are available. This is the strategy that the Dockerfile uses:
80 |
81 | ```bash
82 | $ curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
83 | $ chmod +x nodesource_setup.sh && sudo sh -c ./nodesource_setup.sh
84 | $ sudo apt-get install -y nodejs
85 | ```
86 | 1. Install Node-RED. Follow the [official Node-RED installation guide](https://nodered.org/docs/getting-started/local).
87 | The Dockerfile favors the easiest procedure which relies on `npm` (default Node.js package manager) which is
88 | available after Node.js installation step:
89 |
90 | ```bash
91 | $ npm install -g node-red
92 | ```
93 |
94 | 1. Install Integration-Service. Follow the [Integration-Service installation manual](https://integration-service.docs.eprosima.com/en/latest/installation_manual/installation_manual.html#installation-manual).
95 | This is exemplified in the Dockerfile, basically it is build from sources downloaded from github. Dependencies
96 | associated with the build and bridge environments are required:
97 |
98 | ```bash
99 | $ apt-get update
100 | $ apt-get install -y libyaml-cpp-dev libboost-program-options-dev libwebsocketpp-dev \
101 | libboost-system-dev libboost-dev libssl-dev libcurlpp-dev \
102 | libasio-dev libcurl4-openssl-dev git
103 | $ mkdir -p /is_ws/src && cd "$_"
104 | $ git clone https://github.com/eProsima/Integration-Service.git is
105 | $ git clone https://github.com/eProsima/WebSocket-SH.git
106 | $ git clone https://github.com/eProsima/ROS2-SH.git
107 | $ git clone https://github.com/eProsima/FIWARE-SH.git
108 |
109 | $ . /opt/ros/humble/setup.sh # customize the ROS2 distro: foxy, galactic, humble ...
110 | $ colcon build --cmake-args -DIS_ROS2_SH_MODE=DYNAMIC --install-base /opt/is
111 | ```
112 |
113 | Note that it uses the ROS2 build tool: [colcon](https://colcon.readthedocs.io)
114 | As ROS2 it is necessary to source and
115 | [overlay](https://colcon.readthedocs.io/en/released/developer/environment.html).
116 | In order to simplify sourcing `/opt/is` was chosen as deployment dir. The overlay can be then sourced as:
117 |
118 | ```bash
119 | $ . /opt/is/setup.bash
120 | ```
121 | It will automatically load the ROS2 overlay too. After the overlay is sourced it must be possible to access the
122 | integration-service help as:
123 |
124 | ```bash
125 | $ integration-service --help
126 | ```
127 |
128 | ### Plugin installation
129 |
130 | Once all the dependencies are available we can deploy the plugin via npm:
131 | + From npm repo:
132 |
133 | ```bash
134 | $ npm install -g node-red-ros2-plugin
135 | ```
136 | + From sources. `npm` allows direct deployment from github repo:
137 |
138 | ```bash
139 | $ npm install -g https://github.com/eProsima/node-red-ros2-plugin
140 | ```
141 |
142 | Or, as in the Dockerfile, from a local sources directory. The docker favors this approach to allow tampering with the
143 | sources.
144 |
145 | ```bash
146 | $ git clone https://github.com/eProsima/node-red-ros2-plugin.git plugin_sources
147 | $ npm install -g ./plugin_sources
148 |
149 | ```
150 |
151 | ## Usage
152 |
153 | In order to test the plugin there are two options: follow the installation steps [above](#install) or run the
154 | test container provided [here](./docker/README.md).
155 |
156 | ### Node-RED palette
157 |
158 | The main concepts associated with Node-RED operation are explained [here](https://nodered.org/docs/user-guide/concepts).
159 | The plugin nodes are displayed on the Node-RED palette as shown in the image. From there, they can be dragged into the
160 | workspace.
161 |
162 | 
163 |
164 | The palette is the pane on the left where all available nodes are classified by sections. Plugin ones appear under `ROS2`
165 | and `FIWARE` (figure's red frame). The worksplace is the central pane where different flows are associated to the upper tabs.
166 |
167 | > **_Note:_** the text that labels the node, changes from palette to workspace, and may change depending on the node
168 | configuration.
169 |
170 | ### Definining a type
171 |
172 | In order the publish or subscribe data we need first to specify the associated type. The plugin provides two options:
173 |
174 | #### Choosing a predefined ROS2 type
175 |
176 |
177 |
178 |
179 |
This node represents a specific ROS2 Builtin Type. Once in the workspace, its set up dialog can be opened by
180 | doble clicking over it.
181 |
182 |
183 |
184 |
185 |
The dialog provides a Package drop-down control where all ROS2 msg packages are listed.
186 | Once a package is selected the Message drop-down control allows selection of a package specific message.
187 | In this example the package selected is geometry_msgs.
188 | From this package the Point message is selected.
189 |
190 |
191 |
192 |
Once the dialog set up is saved, the node label changes to highligh the selected type in a package/message pattern.
193 |
194 |
195 |
196 | #### Defining a new type via IDL
197 |
198 |
199 |
200 |
201 |
This node represents a type defined by means of an IDL.
202 | IDL is a language that allows unambiguous specification of the interfaces that may be used to define the
203 | data types.
204 |
205 |
206 |
207 |
208 |
The dialog provides an edit box where the desired IDL can be introduced
209 | Note that in order to use the type in ROS2 it must follow several conventions:
210 |
211 |
There must be an outer module which should match the message package name.
212 |
There must be an inner module called msg.
213 |
The type name must follow PascalCase convention.
214 |
215 | By default a dummy message that follows the above guidelines is provided.
216 |
217 |
218 |
219 |
Once the dialog set up is saved, the node label changes to highligh the selected type in a package/message pattern.
220 |
221 |
222 |
223 | #### Injecting a type instance into the pipeline
224 |
225 | Node-RED pipelines start in *source nodes*. The most popular one is the [inject
226 | node](https://nodered.org/docs/user-guide/nodes#inject) which requires the user to manually defined each field
227 | associated to the type. In order to simplify this a specific node is introduced:
228 |
229 |
230 |
231 | This node mimics the inject node behaviour but automatically populates the input dialog with the fields associated with
232 | any *type node* linked to it. For example, if we wire together a `ROS Inject` and a `ROS Type` or `IDL Type` nodes as
233 | shown in the figure:
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 | The associated dialogs are populated with the linked type fields and types.
247 |
248 | ### ROS2 nodes usage
249 |
250 | In order to interact with a ROS2 environment we must specify the same [domain id](https://docs.ros.org/en/humble/Concepts/About-Domain-ID.html)
251 | in use for that environment.
252 |
253 | The *domain id* is a number in the range `[0, 166]` that provides isolation for ROS2 nodes.
254 | It defaults to 0 and its main advantage is reduce the incomming traffic for each ROS2 node, discharging them and
255 | speeding things up.
256 |
257 | Another key concepts in the ROS2 environment are:
258 |
259 | - [topic](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Topics/Understanding-ROS2-Topics.html)
260 | one. A *topic* is a text string ROS2 nodes use to notify all other nodes in which data they are interested.
261 | When a ROS2 node wants to send or receive data it must specify:
262 | + Which type is the data they want receive. For example the `geometry_msgs/Pose` we introduced [above](#choosing-a-predefined-ros2-type).
263 | + Topic associated with the data. For example `/marker_pose`, but in ROS2 topics are often *decorated* using
264 | namespaces to simplify identification, as in `/eProsima/buildings/E3g1/room/F2h3/marker/4Rg1/pose`.
265 |
266 | - [Quality of Service (QoS)](https://docs.ros.org/en/humble/Concepts/About-Quality-of-Service-Settings.html). Those are
267 | policies that allow fine tunning of the communication between nodes. For example:
268 | + *History QoS* allows to discard messages if only the most recent one is meaningful for our purposes.
269 | + *Reliable QoS* enforces message reception by resending it until the receiver acknowledges it.
270 | + *Durability QoS* assures messages published before the receiver node creation would be delivered.
271 |
272 | > **_Note:_** ROS2 nodes can only communicate if their respective QoS are compatible. Information on QoS compatibility
273 | is available [here](https://docs.ros.org/en/humble/Concepts/About-Quality-of-Service-Settings.html#qos-compatibilities).
274 |
275 | #### ROS2 configuration node
276 |
277 | A [Node-RED config node](https://nodered.org/docs/user-guide/concepts#config-node) is provided to set up the domain ID,
278 | which is a global selection:
279 |
280 | 
281 |
282 | > **_Note:_** The ROS2 default domain value is 0
283 |
284 | #### ROS2 Publisher
285 |
286 |
287 |
288 |
289 |
This node represents a ROS2 publisher. It is able to publish messages on a specific topic with specific QoS
290 |
291 |
292 |
293 |
The dialog provides controls to configure:
294 |
295 |
Topic
Note that the backslash / typical of ROS2 topics is not necessary
296 |
Domain ID
Selected globally via the configuration node explained
297 | above
298 |
QoS
The +add button at the bottom adds new combo-boxes to the control where the
299 | available options for each policy can be selected
300 |
301 |
302 |
303 |
304 |
305 | #### ROS2 Subscriber
306 |
307 |
308 |
309 |
310 |
This node represents a ROS2 subscriber. It is able to subscribe on a specific topic and receive all messages
311 | published for it.
312 |
313 |
314 |
315 |
The dialog provides controls to configure:
316 |
317 |
Topic
Note that the backslash / typical of ROS2 topics is not necessary
318 |
Domain ID
Selected globally via the configuration node explained
319 | above
320 |
QoS
The +add button at the bottom adds new combo-boxes to the control where the
321 | available options for each policy can be selected
322 |
323 |
324 |
325 |
326 |
327 | ### ROS2 Examples
328 |
329 | #### ROS2 Basic Publication Example
330 |
331 | Let's show how to use a custom type.
332 |
333 | 1. Launch docker compose as explained [here](./docker/README.md).
334 | 1. Create and wire the following nodes:
335 | + An `IDL Type` node. Open the associated dialog and introduce the following idl:
336 |
337 | ```c
338 | module custom_msgs {
339 | module msg {
340 | struct Message {
341 | string text;
342 | uint64 value;
343 | };
344 | };
345 | };
346 | ```
347 | + A `ROS Publisher` node. Open the associated dialog and set up the publisher:
348 |
349 | `Topic`
350 | : hope
351 |
352 | `Domain`
353 | : 42
354 |
355 | + A `ROS Inject` node. Open the associated dialog and fill in the fields:
356 |
357 | `text`
358 | : Hello World!
359 |
360 | `value`
361 | : 42
362 |
363 | 1. Deploy the flow pressing the corresponding button. Once deployed, the custom type has been registered in the ROS2 distro.
364 |
365 | 1. Let's launch a subscriber from the
366 | [ROS2 cli](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Topics/Understanding-ROS2-Topics.html#id7).
367 |
368 | ```bash
369 | $ docker exec -ti --env ROS_DOMAIN_ID=42 docker-visual-ros-1 /ros_entrypoint.sh ros2 topic echo /hope custom_msgs/msg/Message
370 | ```
371 | 1. Now click on the inject node button within the editor and see how the terminal receives the data.
372 |
373 | > **_Note:_** In the example the ROS2 domain value selected is 42, different from the default value of 0.
374 |
375 | 
376 |
377 | #### ROS2 Basic Subscription Example
378 |
379 | In this case, a builtin ROS2 type (`geometry_msgs/Point`) will be used.
380 |
381 | 1. Launch docker compose as explained [here](./docker/README.md).
382 | 1. Create and wire the following nodes:
383 | + A `ROS2 Type` node. Open the associated dialog and select:
384 |
385 | `Package`
386 | : geometry_msgs
387 |
388 | `Message`
389 | : Point
390 |
391 | + A `ROS2 Subscriber` node. Open the associated dialog and set up the subscriber:
392 |
393 | `Topic`
394 | : hope
395 |
396 | `Domain`
397 | : 17
398 |
399 | + A `debug` node from the `common` palette section. Open the associated dialog and set it up to show the x
400 | coordinate of the point:
401 |
402 | `Output`
403 | : `msg.x`
404 |
405 | 1. Deploy the flow pressing the corresponding button.
406 | 1. Publish a message on that topic from the ROS2 cli. In this example we launch a new container connected to the same
407 | network using the standard `ros:humble` image.
408 |
409 | ```bash
410 | $ docker run --rm -ti --env ROS_DOMAIN_ID=17 --network docker_visualros ros:humble \
411 | ros2 topic pub /hope geometry_msgs/msg/Point "{ x: 42, y: 0, z: 0 }"
412 | ```
413 |
414 | 
415 |
416 | #### ROS2 Mandatory Turtlesim Example
417 |
418 | [Turtlesim](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Introducing-Turtlesim/Introducing-Turtlesim.html)
419 | is an *ad hoc* package that ROS2 provides as GUI node example.
420 |
421 | ##### Set up
422 |
423 | Unlike the previous examples turtlesim cannot work on terminal mode. There are several ways to workaround this:
424 | 1. Run GUI application in a docker container as shown [here](https://www.howtogeek.com/devops/how-to-run-gui-applications-in-a-docker-container/).
425 | 1. Share the container network stack with the host.
426 | 1. Run Node-RED backend directly in your host (see [installation steps](#install)).
427 |
428 | Here we favour the second option as the most simple. This only requires:
429 | + A ROS2 installation in the host. Follow the [official ROS2 installation guide](https://docs.ros.org/en/humble/Installation.html)
430 | for the distro of choice.
431 | + Launch the Visual-ROS container sharing host network stack:
432 |
433 | ```bash
434 | node-red-ros2-plugin/docker$ docker build --build-arg ROS_DISTRO=humble -t visualros:humble .
435 | $ docker run -ti --name turtledemo --network host --ipc host visualros:humble /node_entrypoint.sh node-red
436 | ```
437 |
438 | ##### Demo steps
439 |
440 | 1. Launch `turtlesim` on the host by doing:
441 |
442 | ```bash
443 | $ . /opt/ros/humble/setup.sh
444 | $ ros2 run turtlesim turtlesim_node
445 | ```
446 | A window should appear with a turtle in the middle.
447 |
448 | 1. Open a web browser on [http://localhost:1880](http://localhost:1880).
449 |
450 | 1. Create and wire the following nodes:
451 | + A `ROS2 Type` node. In the associated dialog set up the turtlesim pose type: `geometry_msgs/Twist`.
452 | + A couple of `ROS2 Inject` nodes: one to move the turtle forward and another to spin it.
453 | Wire both nodes to the `ROS2 Type`. Open the associated dialogs and take into account that:
454 | - mover forward means `linear.x = 1`
455 | - spin means `angular.z = 1`
456 | + A `ROS2 Publisher` node. Wire it to the `ROS2 Type` node. Open the associated dialog and set up as:
457 | - Topic the `turtle/cmd_vel`.
458 | - Use the default ROS2 domain 0.
459 |
460 | 1. Click the `Deploy` button.
461 |
462 | 
463 |
464 | Now click on the inject nodes buttons within the editor and see how the turtle moves.
465 |
466 | This [json file](./docs/turtlesim.json) can be imported to Node-RED in order to reproduce the flow.
467 |
468 | ### FIWARE nodes usage
469 |
470 | The [FIWARE Context Broker](https://fiwaretourguide.readthedocs.io/en/latest/core/introduction/) uses a REST API to
471 | provide information. This interface do not exactly follows a Publisher/Subscriber model but can be adapted to do so:
472 | - Broker entities are mapped as Publisher/Subscriber topics.
473 | - Types are described using IDL which works as a subset of the NGSI type system.
474 |
475 | #### FIWARE configuration node
476 |
477 | A [Node-RED config node](https://nodered.org/docs/user-guide/concepts#config-node) is provided to set up the FIWARE
478 | Context Broker IPv4 address which is a global selection.
479 |
480 | 
481 |
482 | #### FIWARE Publisher
483 |
484 |
485 |
486 |
487 |
This node represents a FIWARE publisher able to publish messages on a specific topic.
488 |
489 |
490 |
491 |
The dialog provides controls to configure:
492 |
493 |
Topic
Element that acts as a bus for nodes to exhange messages. It matches the Context
494 | Broker entity concept.
495 |
Context Broker
496 | Address of the FIWARE Context Broker this node will connect to.
497 | A specific config node dialog will be open in order to ease address and port selection.
498 |
499 |
500 |
501 |
502 |
503 |
504 | #### FIWARE Subscriber
505 |
506 |
507 |
508 |
509 |
This node represents a FIWARE subscriber. It is able to subscribe on a specific topic and receive all messages
510 | published for it.
511 |
512 |
513 |
514 |
The dialog provides controls to configure:
515 |
516 |
Topic
Element that acts as a bus for nodes to exchange messages.
517 | It matches the Context Broker entity concept.
518 |
Context Broker
519 | Address of the FIWARE Context Broker this node will connect to.
520 | A specific config node dialog will be open in order to ease address and port selection.
521 |
522 |
523 |
524 |
525 |
526 |
527 | ### FIWARE Examples
528 |
529 | #### FIWARE Basic Publication Example
530 |
531 | Let's use a custom type.
532 |
533 | 1. Launch docker compose as explained [here](./docker/README.md).
534 | 1. Create and wire the following nodes:
535 | + An `IDL Type` node. Open the associated dialog and introduce the following idl:
536 |
537 | ```c
538 | module custom_msgs {
539 | module msg {
540 | struct Message {
541 | string text;
542 | uint64 value;
543 | };
544 | };
545 | };
546 | ```
547 | + A `FIWARE Publisher` node. Open the associated dialog and set up the publisher:
548 |
549 | `Topic`
550 | : hope
551 |
552 | `Context Broker`
553 | : `192.168.42.14:1026`
554 |
555 | Note the address and port of the Context Broker was specified in the `compose.yaml` file.
556 |
557 | + A `ROS Inject` node. Open the associated dialog and fill in the fields:
558 |
559 | `text`
560 | : Hello World!
561 |
562 | `value`
563 | : 42
564 |
565 | 1. Deploy the flow pressing the corresponding button.
566 | 1. Once deployed, click on the inject node button within the editor. The Context Broker should have created an entity
567 | associated to the topic (`hope`) with the values provided in the inject node.
568 |
569 | In order to check it, the FIWARE Context Broker can be directly queried using its REST API (note that in the
570 | `compose.yaml` file the Context Broker port 1026 is mapped to the host machine). Open a console on the host and type:
571 |
572 | ```bash
573 | $ curl -G -X GET "http://localhost:1026/v2/entities" -d "type=custom_msgs::msg::Message" -d "id=hope"
574 | ```
575 |
576 | It should return the following json:
577 | ```json
578 | [
579 | {
580 | "id": "hope",
581 | "type": "custom_msgs::msg::Message",
582 | "text": {
583 | "type": "Text",
584 | "value": "Hello World!",
585 | "metadata": {}
586 | },
587 | "value": {
588 | "type": "Number",
589 | "value": 42,
590 | "metadata": {}
591 | }
592 | }
593 | ]
594 | ```
595 |
596 | 
597 |
598 | #### FIWARE Basic Subscription Example
599 |
600 | In this case, a builtin ROS2 type (`geometry_msgs/Point`) will be used.
601 |
602 | 1. Launch docker compose as explained [here](./docker/README.md).
603 | 1. Create and wire the following nodes:
604 | + A `ROS2 Type` node. Open the associated dialog and select:
605 |
606 | `Package`
607 | : geometry_msgs
608 |
609 | `Message`
610 | : Point
611 |
612 | + A `FIWARE Subscriber` node. Open the associated dialog and set up the subscriber:
613 |
614 | `Topic`
615 | : position
616 |
617 | `Context Broker`
618 | : `192.168.42.14:1026`
619 |
620 | + A `debug` node from the `common` palette section. Open the associated dialog and set it up to show the x
621 | coordinate of the point:
622 |
623 | `Output`
624 | : `msg.x`
625 |
626 | 1. Deploy the flow pressing the corresponding button.
627 | 1. Publish a message on that topic from the FIWARE REST API. Because the `compose.yaml` maps the FIWARE Context Broker
628 | port to a host one (1026) is possible to reach it from the host machine.
629 | Open a console on the host and type:
630 |
631 | ```bash
632 | $ curl http://localhost:1026/v2/entities -H 'Content-Type: application/json' -d @- <
661 |
662 | This project (DIH² - A Pan‐European Network of Robotics DIHs for Agile Production) has received funding from the
663 | European Union’s Horizon 2020 research and innovation programme under grant agreement No 824964
664 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG ROS_DISTRO=humble
2 |
3 | FROM eprosima/vulcanexus:$ROS_DISTRO
4 |
5 | # Avoid interactuation with installation of some package that needs the locale.
6 | ENV TZ=Europe/Madrid
7 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
8 |
9 | # Avoids using interactions during building
10 | ENV DEBIAN_FRONTEND=noninteractive
11 |
12 | # Use a bash shell so it is possigle to run things like `source` (required for colcon builds)
13 | SHELL ["/bin/bash", "-c"]
14 |
15 | # Install Integration Service dependencies
16 | RUN apt-get update && \
17 | apt-get install -y \
18 | cmake \
19 | curl \
20 | g++ \
21 | git \
22 | libasio-dev \
23 | libboost-dev \
24 | libboost-program-options-dev \
25 | libboost-system-dev \
26 | libcurl4-openssl-dev \
27 | libcurlpp-dev \
28 | libssl-dev \
29 | libwebsocketpp-dev \
30 | libyaml-cpp-dev \
31 | python3-pip \
32 | wget && \
33 | pip3 install -U \
34 | colcon-common-extensions \
35 | vcstool && \
36 | curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.bash && \
37 | chmod +x nodesource_setup.bash && \
38 | bash -c ./nodesource_setup.bash && \
39 | apt-get install -y nodejs
40 |
41 | # Install eProsima Integration Service
42 | WORKDIR /is_ws
43 | RUN mkdir src && cd src && \
44 | git clone https://github.com/eProsima/Integration-Service.git is && \
45 | git clone https://github.com/eProsima/WebSocket-SH.git && \
46 | git clone https://github.com/eProsima/ROS2-SH.git && \
47 | git clone https://github.com/eProsima/FIWARE-SH.git && \
48 | source /opt/vulcanexus/$ROS_DISTRO/setup.bash && \
49 | colcon build --cmake-args -DIS_ROS2_SH_MODE=DYNAMIC --install-base /opt/is
50 |
51 | # Install Node-RED and node-red-ros2-plugin
52 | RUN npm install -g --unsafe-perm \
53 | node-red \
54 | node-red-ros2-plugin \
55 | node-red-contrib-keypress
56 |
57 | # Enable overlay execution
58 | WORKDIR /
59 | COPY entrypoint.bash .
60 | RUN chmod +x /entrypoint.bash
61 |
62 | CMD [ "node", "/usr/bin/node-red" ]
63 | ENTRYPOINT [ "/entrypoint.bash" ]
64 |
--------------------------------------------------------------------------------
/docker/README.md:
--------------------------------------------------------------------------------
1 | ## Plugin testing using docker containers
2 |
3 | Testing the plugin capabilities requires access to some services: FIWARE context broker and its associated database.
4 |
5 | That makes direct use of the docker `cli` cumbersome. In order to simplify the setup a docker compose file is
6 | [provided](./compose.yaml). The compose file will set up the FIWARE services and launch `Node-RED` as a server.
7 |
8 | - Run the compose file:
9 |
10 | ```bash
11 | $ docker compose up
12 | ```
13 |
14 | - Open a browser and go to [http://localhost:1880/](http://localhost:1880/)
15 |
--------------------------------------------------------------------------------
/docker/compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | database:
3 | image: mongo:4.4
4 | hostname: mongo-db
5 | networks:
6 | visualros:
7 | ipv4_address: 192.168.42.13
8 | expose:
9 | - "27017"
10 | command: --bind_ip_all
11 | broker:
12 | image: fiware/orion
13 | hostname: orion
14 | networks:
15 | visualros:
16 | ipv4_address: 192.168.42.14
17 | ports:
18 | - "1026:1026"
19 | command: -dbhost mongo-db
20 | depends_on:
21 | - database
22 | visual-ros:
23 | build:
24 | context: .
25 | args:
26 | ROS_DISTRO: humble
27 | networks:
28 | visualros:
29 | ipv4_address: 192.168.42.15
30 | ports:
31 | - "1880:1880"
32 | - "9229:9229"
33 | command: /node_entrypoint.sh node --inspect=192.168.42.15:9229 /usr/bin/node-red
34 | depends_on:
35 | - broker
36 | networks:
37 | visualros:
38 | driver: bridge
39 | ipam:
40 | config:
41 | - subnet: 192.168.42.0/24
42 | gateway: 192.168.42.1
43 |
--------------------------------------------------------------------------------
/docker/entrypoint.bash:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Setup ROS 2 environment
4 | source /opt/vulcanexus/${ROS_DISTRO}/setup.bash
5 | source /opt/is/setup.bash
6 | export NODE_PATH=/usr/lib/node_modules
7 | exec "$@"
--------------------------------------------------------------------------------
/docker/turtlesim.json:
--------------------------------------------------------------------------------
1 | [{"id":"0f5651828663e38a","type":"ROS2 Type","z":"a3ebc0eea59c5b7b","ros2pkg":"geometry_msgs","ros2message":"Twist","x":580,"y":380,"wires":[["00e62d1805b9648e"]]},{"id":"00e62d1805b9648e","type":"Publisher","z":"a3ebc0eea59c5b7b","topic":"turtle1/cmd_vel","domain":"7739e2a243db81fa","props":[],"selectedtype":"geometry_msgs/Twist","x":820,"y":380,"wires":[["d82ca8702b9768dc"]]},{"id":"0cc99e3c58a28f0e","type":"ROS2 Inject","z":"a3ebc0eea59c5b7b","types":[],"props":[{"p":"linear.x","v":"","vt":"num","t":"double"},{"p":"linear.y","v":"","vt":"num","t":"double"},{"p":"linear.z","v":"","vt":"num","t":"double"},{"p":"angular.x","v":"","vt":"num","t":"double"},{"p":"angular.y","v":"","vt":"num","t":"double"},{"p":"angular.z","v":"-1","vt":"num","t":"double"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","ros2type":"geometry_msgs/Twist","idltype":"","x":310,"y":340,"wires":[["0f5651828663e38a"]]},{"id":"8bedf41c9305eb23","type":"comment","z":"a3ebc0eea59c5b7b","name":"spin clockwise","info":"","x":310,"y":300,"wires":[]},{"id":"3cb9ed69ef56267c","type":"debug","z":"a3ebc0eea59c5b7b","name":"Sent command","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1200,"y":380,"wires":[]},{"id":"d82ca8702b9768dc","type":"template","z":"a3ebc0eea59c5b7b","name":"msg prettify","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Forward {{linear.x}} Spin {{angular.z}}","output":"str","x":1010,"y":380,"wires":[["3cb9ed69ef56267c"]]},{"id":"aa5b04131c62bb19","type":"comment","z":"a3ebc0eea59c5b7b","name":"move forward","info":"Move forward","x":310,"y":200,"wires":[]},{"id":"e1a6f79f4c19935c","type":"ROS2 Inject","z":"a3ebc0eea59c5b7b","types":[],"props":[{"p":"linear.x","v":"1","vt":"num","t":"double"},{"p":"linear.y","v":"","vt":"num","t":"double"},{"p":"linear.z","v":"","vt":"num","t":"double"},{"p":"angular.x","v":"","vt":"num","t":"double"},{"p":"angular.y","v":"","vt":"num","t":"double"},{"p":"angular.z","v":"","vt":"num","t":"double"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","ros2type":"geometry_msgs/Twist","idltype":"","x":310,"y":240,"wires":[["0f5651828663e38a"]],"outputLabels":["forward"]},{"id":"1e1fe9ea4149458e","type":"keypress","z":"a3ebc0eea59c5b7b","key":"","x":120,"y":460,"wires":[["d53d1a1a97438805"]]},{"id":"d53d1a1a97438805","type":"function","z":"a3ebc0eea59c5b7b","name":"Key to cmd_vel","func":"let vel = { \n \"linear\": { \n \"x\": 0, \"y\": 0, \"z\": 0 \n }, \n \"angular\": { \n \"x\": 0, \"y\": 0, \"z\": 0 \n } \n}\n\nswitch (msg.payload.key) {\n case 'up':\n vel.linear.x = 1\n break;\n case 'down':\n vel.linear.x = -1\n break;\n case 'left':\n vel.angular.z = 1\n break;\n case 'right':\n vel.angular.z = -1\n break;\n default:\n console.log(`Not recognized key \"${msg.payload.key}\".`);\n}\nreturn vel","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":460,"wires":[["0f5651828663e38a"]]},{"id":"7739e2a243db81fa","type":"dds-settings","domain":"0"}]
--------------------------------------------------------------------------------
/docs/FIWAREPublisher.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/FIWAREPublisher.gif
--------------------------------------------------------------------------------
/docs/FIWAREPublisher.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/FIWAREPublisher.jpg
--------------------------------------------------------------------------------
/docs/FIWAREPublisherDialog.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/FIWAREPublisherDialog.jpg
--------------------------------------------------------------------------------
/docs/FIWARESubscriber.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/FIWARESubscriber.gif
--------------------------------------------------------------------------------
/docs/FIWARESubscriber.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/FIWARESubscriber.jpg
--------------------------------------------------------------------------------
/docs/FIWAREsettings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/FIWAREsettings.jpg
--------------------------------------------------------------------------------
/docs/IDLType.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/IDLType.png
--------------------------------------------------------------------------------
/docs/ROS2ConfigNode.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2ConfigNode.jpg
--------------------------------------------------------------------------------
/docs/ROS2Inject.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2Inject.png
--------------------------------------------------------------------------------
/docs/ROS2InjectIDL.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2InjectIDL.jpg
--------------------------------------------------------------------------------
/docs/ROS2InjectIDLDialog.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2InjectIDLDialog.jpg
--------------------------------------------------------------------------------
/docs/ROS2InjectPackage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2InjectPackage.jpg
--------------------------------------------------------------------------------
/docs/ROS2InjectPackageDialog.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2InjectPackageDialog.jpg
--------------------------------------------------------------------------------
/docs/ROS2Publisher.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2Publisher.gif
--------------------------------------------------------------------------------
/docs/ROS2Publisher.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2Publisher.jpg
--------------------------------------------------------------------------------
/docs/ROS2PublisherDialog.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2PublisherDialog.jpg
--------------------------------------------------------------------------------
/docs/ROS2Subscriber.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2Subscriber.gif
--------------------------------------------------------------------------------
/docs/ROS2Subscriber.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2Subscriber.jpg
--------------------------------------------------------------------------------
/docs/ROS2Type.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ROS2Type.png
--------------------------------------------------------------------------------
/docs/eu_flag.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/eu_flag.jpg
--------------------------------------------------------------------------------
/docs/idl-definition.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/idl-definition.jpg
--------------------------------------------------------------------------------
/docs/idl-type-name.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/idl-type-name.jpg
--------------------------------------------------------------------------------
/docs/packages.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/packages.jpg
--------------------------------------------------------------------------------
/docs/palette.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/palette.jpg
--------------------------------------------------------------------------------
/docs/ros2-type-name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/ros2-type-name.png
--------------------------------------------------------------------------------
/docs/turtlesim.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/docs/turtlesim.gif
--------------------------------------------------------------------------------
/docs/turtlesim.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "a3ebc0eea59c5b7b",
4 | "type": "tab",
5 | "label": "Flow 1",
6 | "disabled": false,
7 | "info": "",
8 | "env": []
9 | },
10 | {
11 | "id": "42037154b751165b",
12 | "type": "ROS2 Inject",
13 | "z": "a3ebc0eea59c5b7b",
14 | "types": [],
15 | "props": [
16 | {
17 | "p": "linear.x",
18 | "v": "1",
19 | "vt": "num",
20 | "t": "double"
21 | },
22 | {
23 | "p": "linear.y",
24 | "v": "",
25 | "vt": "num",
26 | "t": "double"
27 | },
28 | {
29 | "p": "linear.z",
30 | "v": "",
31 | "vt": "num",
32 | "t": "double"
33 | },
34 | {
35 | "p": "angular.x",
36 | "v": "",
37 | "vt": "num",
38 | "t": "double"
39 | },
40 | {
41 | "p": "angular.y",
42 | "v": "",
43 | "vt": "num",
44 | "t": "double"
45 | },
46 | {
47 | "p": "angular.z",
48 | "v": "",
49 | "vt": "num",
50 | "t": "double"
51 | }
52 | ],
53 | "repeat": "",
54 | "crontab": "",
55 | "once": false,
56 | "onceDelay": 0.1,
57 | "topic": "",
58 | "ros2type": "geometry_msgs/Twist",
59 | "idltype": "",
60 | "x": 170,
61 | "y": 80,
62 | "wires": [
63 | [
64 | "0f5651828663e38a"
65 | ]
66 | ],
67 | "outputLabels": [
68 | "forward"
69 | ]
70 | },
71 | {
72 | "id": "0f5651828663e38a",
73 | "type": "ROS2 Type",
74 | "z": "a3ebc0eea59c5b7b",
75 | "ros2pkg": "geometry_msgs",
76 | "ros2message": "Twist",
77 | "x": 400,
78 | "y": 80,
79 | "wires": [
80 | [
81 | "00e62d1805b9648e"
82 | ]
83 | ]
84 | },
85 | {
86 | "id": "00e62d1805b9648e",
87 | "type": "Publisher",
88 | "z": "a3ebc0eea59c5b7b",
89 | "topic": "turtle1/cmd_vel",
90 | "domain": "7739e2a243db81fa",
91 | "props": [],
92 | "selectedtype": "geometry_msgs/Twist",
93 | "x": 680,
94 | "y": 80,
95 | "wires": [
96 | [
97 | "d82ca8702b9768dc"
98 | ]
99 | ]
100 | },
101 | {
102 | "id": "0cc99e3c58a28f0e",
103 | "type": "ROS2 Inject",
104 | "z": "a3ebc0eea59c5b7b",
105 | "types": [],
106 | "props": [
107 | {
108 | "p": "linear.x",
109 | "v": "",
110 | "vt": "num",
111 | "t": "double"
112 | },
113 | {
114 | "p": "linear.y",
115 | "v": "",
116 | "vt": "num",
117 | "t": "double"
118 | },
119 | {
120 | "p": "linear.z",
121 | "v": "",
122 | "vt": "num",
123 | "t": "double"
124 | },
125 | {
126 | "p": "angular.x",
127 | "v": "",
128 | "vt": "num",
129 | "t": "double"
130 | },
131 | {
132 | "p": "angular.y",
133 | "v": "",
134 | "vt": "num",
135 | "t": "double"
136 | },
137 | {
138 | "p": "angular.z",
139 | "v": "1",
140 | "vt": "num",
141 | "t": "double"
142 | }
143 | ],
144 | "repeat": "",
145 | "crontab": "",
146 | "once": false,
147 | "onceDelay": 0.1,
148 | "topic": "",
149 | "ros2type": "geometry_msgs/Twist",
150 | "idltype": "",
151 | "x": 170,
152 | "y": 200,
153 | "wires": [
154 | [
155 | "0f5651828663e38a"
156 | ]
157 | ]
158 | },
159 | {
160 | "id": "ce0dc95062627c74",
161 | "type": "comment",
162 | "z": "a3ebc0eea59c5b7b",
163 | "name": "move forward",
164 | "info": "Move forward",
165 | "x": 150,
166 | "y": 40,
167 | "wires": []
168 | },
169 | {
170 | "id": "8bedf41c9305eb23",
171 | "type": "comment",
172 | "z": "a3ebc0eea59c5b7b",
173 | "name": "spin",
174 | "info": "",
175 | "x": 130,
176 | "y": 160,
177 | "wires": []
178 | },
179 | {
180 | "id": "3cb9ed69ef56267c",
181 | "type": "debug",
182 | "z": "a3ebc0eea59c5b7b",
183 | "name": "debug 1",
184 | "active": true,
185 | "tosidebar": true,
186 | "console": false,
187 | "tostatus": false,
188 | "complete": "payload",
189 | "targetType": "msg",
190 | "statusVal": "",
191 | "statusType": "auto",
192 | "x": 1080,
193 | "y": 80,
194 | "wires": []
195 | },
196 | {
197 | "id": "d82ca8702b9768dc",
198 | "type": "template",
199 | "z": "a3ebc0eea59c5b7b",
200 | "name": "",
201 | "field": "payload",
202 | "fieldType": "msg",
203 | "format": "handlebars",
204 | "syntax": "mustache",
205 | "template": "Forward {{linear.x}} Spin {{angular.z}}",
206 | "output": "str",
207 | "x": 880,
208 | "y": 80,
209 | "wires": [
210 | [
211 | "3cb9ed69ef56267c"
212 | ]
213 | ]
214 | },
215 | {
216 | "id": "7739e2a243db81fa",
217 | "type": "dds-settings",
218 | "domain": "0"
219 | }
220 | ]
221 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name:
2 | site_url: https://
3 | repo_url: https://github.com/
4 | site_description: RAMP Documentation
5 | docs_dir: docs
6 | site_dir: html
7 | edit_uri: edit/master/doc/
8 | markdown_extensions: [toc,fenced_code]
9 | use_directory_urls: false
10 | theme: readthedocs
11 | pages:
12 | - Home: 'index.md'
13 | - 'Getting Started' : 'getting-started.md'
14 | - 'User & Programmers Manual':
15 | - 'Architecture' : 'architecture.md'
16 | - 'API' : 'api.md'
17 | - 'User Guide': 'usermanual.md'
18 | - 'Installation & Administration Manual':
19 | - 'Installation Guide': 'installationguide.md'
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-red-ros2-plugin",
3 | "license": "MIT",
4 | "description": "A collection of Node-RED nodes for connecting with ROS2.",
5 | "version": "1.0.1",
6 | "engines": {
7 | "node": ">=10"
8 | },
9 | "os": [
10 | "!win32"
11 | ],
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/eProsima/node-red-ros2-plugin.git"
15 | },
16 | "dependencies": {
17 | "cron": "~1.8.2",
18 | "is-web-api": "~1.0.1"
19 | },
20 | "devDependencies": {
21 | "coveralls": "^3.1.0",
22 | "eslint": "~7.5.0",
23 | "eslint-config-tamia": "~7.2.5",
24 | "eslint-plugin-prettier": "~3.1.2",
25 | "husky": "~4.2.5",
26 | "mocha": "8.0.1",
27 | "mocha-lcov-reporter": "^1.3.0",
28 | "nyc": "^15.1.0",
29 | "prettier": "~2.0.5",
30 | "remark-cli": "^6.0.1",
31 | "remark-preset-lint-recommended": "^3.0.4",
32 | "textlint": "~11.7.6",
33 | "textlint-rule-common-misspellings": "~1.0.1",
34 | "textlint-rule-terminology": "~2.1.4",
35 | "textlint-rule-write-good": "~1.6.2"
36 | },
37 | "keywords": [
38 | "ros2",
39 | "node-red",
40 | "contrib",
41 | "fiware",
42 | "ngsi",
43 | "eProsima",
44 | "context",
45 | "process",
46 | "data",
47 | "json"
48 | ],
49 | "husky": {
50 | "hooks": {
51 | "pre-commit": "lint-staged"
52 | }
53 | },
54 | "lint-staged": {
55 | "*.js": [
56 | "prettier --config .prettierrc.json --write",
57 | "git add"
58 | ],
59 | "*.md": [
60 | "prettier --no-config --tab-width 4 --print-width 120 --write --prose-wrap always",
61 | "git add"
62 | ],
63 | "*.yml": [
64 | "prettier --no-config --write",
65 | "git add"
66 | ]
67 | },
68 | "textlint": {
69 | "rules": {
70 | "common-misspellings": true,
71 | "terminology": {
72 | "defaultTerms": true
73 | }
74 | }
75 | },
76 | "node-red": {
77 | "nodes": {
78 | "DDS Settings": "src/dds-settings/dds-settings.js",
79 | "publisher": "src/publisher/publisher.js",
80 | "subscriber": "src/subscriber/subscriber.js",
81 | "IDL Type": "src/idl-types/idl-types.js",
82 | "ROS 2 Type": "src/ros2-types/ros2-types.js",
83 | "ROS2 Inject": "src/ros2-message-inject/ros2-message-inject.js",
84 | "FIWARE Publisher": "src/fiware_publisher/fiware_publisher.js",
85 | "FIWARE Subscriber": "src/fiware_subscriber/fiware_subscriber.js",
86 | "FIWARE Settings": "src/fiware-settings/fiware-settings.js"
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/dds-settings/dds-settings.html:
--------------------------------------------------------------------------------
1 |
22 |
23 |
29 |
--------------------------------------------------------------------------------
/src/dds-settings/dds-settings.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 |
3 | function ddsSettingsNode(n) {
4 | RED.nodes.createNode(this,n);
5 | this.domain = RED.settings.visualRosDomain || n.domain;
6 | }
7 |
8 | // If the domain value is enforce via settings don't allow the user to modify it
9 | let settings = null;
10 | if (RED.settings.visualRosDomain)
11 | {
12 | settings = {
13 | settings: {
14 | ddsSettingsForceDomain : {
15 | value: RED.settings.visualRosDomain || 0,
16 | exportable:true
17 | }
18 | }
19 | };
20 | }
21 |
22 | RED.nodes.registerType("dds-settings",ddsSettingsNode, settings);
23 | }
24 |
--------------------------------------------------------------------------------
/src/dds-settings/icons/ros2-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/fiware-settings/fiware-settings.html:
--------------------------------------------------------------------------------
1 |
18 |
19 |
29 |
--------------------------------------------------------------------------------
/src/fiware-settings/fiware-settings.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 |
3 | function FIWARESettingsNode(config) {
4 | RED.nodes.createNode(this, config);
5 | this.host = config.host;
6 | this.port = config.port;
7 | }
8 |
9 | RED.nodes.registerType("fiware-settings", FIWARESettingsNode);
10 | }
11 |
--------------------------------------------------------------------------------
/src/fiware-settings/icons/fiware.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/src/fiware-settings/icons/fiware.png
--------------------------------------------------------------------------------
/src/fiware_publisher/fiware_publisher.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
100 |
101 |
102 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/src/fiware_publisher/fiware_publisher.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 |
3 | var is_web_api = require('is-web-api').fiware;
4 |
5 | /*
6 | * @function PublisherNode constructor
7 | * This node is defined by the constructor function PublisherNode,
8 | * which is called when a new instance of the node is created
9 | *
10 | * @param {Object} config - Contains the properties set in the flow editor
11 | */
12 | function PublisherNode(config) {
13 | RED.nodes.createNode(this, config);
14 | var node = this;
15 | node.ready = true;
16 |
17 | node.status({fill: "yellow", shape: "dot", text: "Wait until Visual-ROS is ready to be used."});
18 |
19 | if(config.broker)
20 | {
21 | // modify the global borker
22 | node.broker = RED.nodes.getNode(config.broker);
23 | is_web_api.set_fiware_host(node.broker.host);
24 | is_web_api.set_fiware_port(node.broker.port);
25 | }
26 |
27 | let {color, message} = is_web_api.add_publisher(config['id'], config['topic'], config['selectedtype']);
28 | if (message && color)
29 | {
30 | node.status({ fill: color, shape: "dot", text: message});
31 | }
32 |
33 | // Event emitted when the deploy is finished
34 | RED.events.once('flows:started', function()
35 | {
36 | let {color, message} = is_web_api.launch(config['id']);
37 | if (message && color)
38 | {
39 | node.status({ fill: color, shape: "dot", text: message});
40 | }
41 | });
42 |
43 | var event_emitter = is_web_api.get_event_emitter();
44 | if (event_emitter)
45 | {
46 | // Event emitted if the integration server failed
47 | event_emitter.on('IS-ERROR', function(status)
48 | {
49 | node.ready = false;
50 | node.status(status);
51 | });
52 |
53 | event_emitter.on('FIWARE_connected', function()
54 | {
55 | node.ready = true;
56 | node.status({ fill: null, shape: null, text: null});
57 | });
58 | }
59 |
60 | node.on('input', function(msg, send, done) {
61 |
62 | if (node.ready)
63 | {
64 | node.status({ fill: "green", shape: "dot", text: "Message Published"});
65 |
66 | // Passes the message to the next node in the flow
67 | send(msg);
68 | is_web_api.send_message(config['topic'], msg);
69 |
70 | done();
71 | }
72 | else
73 | {
74 | done("node was not ready to process flow data");
75 | }
76 | });
77 |
78 | node.on('close', function(removed, done) {
79 |
80 | // Stops the IS execution and resets the yaml
81 | is_web_api.new_config();
82 | is_web_api.stop();
83 | node.status({ fill: null, shape: null, text: ""});
84 | done()
85 | });
86 | }
87 |
88 | RED.nodes.registerType("FIWARE Publisher", PublisherNode);
89 | }
90 |
--------------------------------------------------------------------------------
/src/fiware_publisher/icons/fiware.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/src/fiware_publisher/icons/fiware.png
--------------------------------------------------------------------------------
/src/fiware_subscriber/fiware_subscriber.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
100 |
101 |
102 |
161 |
--------------------------------------------------------------------------------
/src/fiware_subscriber/fiware_subscriber.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 |
3 | var is_web_api = require('is-web-api').fiware;
4 |
5 | /*
6 | * @function SubscriberNode constructor
7 | * This node is defined by the constructor function SubscriberNode,
8 | * which is called when a new instance of the node is created
9 | *
10 | * @param {Object} config - Contains the properties set in the flow editor
11 | */
12 | function SubscriberNode(config) {
13 | RED.nodes.createNode(this, config);
14 | var node = this;
15 | node.ready = false;
16 |
17 | node.status({fill: "yellow", shape: "dot", text: "Wait until Visual-ROS is ready to be used."});
18 |
19 | if(config.broker)
20 | {
21 | // modify the global borker
22 | var broker = RED.nodes.getNode(config.broker);
23 | is_web_api.set_fiware_host(broker.host);
24 | is_web_api.set_fiware_port(broker.port);
25 | }
26 |
27 | let {color, message} = is_web_api.add_subscriber(config['id'], config["topic"], config['selectedtype']);
28 | if (message && color)
29 | {
30 | node.status({ fill: color, shape: "dot", text: message });
31 | }
32 |
33 | // Event emitted when the deploy is finished
34 | RED.events.once('flows:started', function() {
35 |
36 | let {color, message, event_emitter} = is_web_api.launch(config['id']);
37 | if (message && color)
38 | {
39 | node.status({ fill: color, shape: "dot", text: message});
40 | }
41 |
42 | if (event_emitter)
43 | {
44 | // Event emitted when a new message is received
45 | event_emitter.on(config["topic"] + '_data', function(msg_json)
46 | {
47 | node.status({ fill: "green", shape: "dot", text: "Message Received" });
48 | // Passes the message to the next node in the flow
49 | node.send(msg_json['msg']);
50 | });
51 |
52 | event_emitter.on('IS-ERROR', function(status)
53 | {
54 | node.ready = false;
55 | node.status(status);
56 | });
57 |
58 | event_emitter.on('FIWARE_connected', function()
59 | {
60 | node.ready = true;
61 | node.status({ fill: null, shape: null, text: null});
62 | });
63 | }
64 | });
65 |
66 | node.on('close', function(removed, done) {
67 |
68 | // Stops the IS execution and resets the yaml
69 | is_web_api.stop();
70 | is_web_api.new_config();
71 | done()
72 | });
73 | }
74 |
75 | RED.nodes.registerType("FIWARE Subscriber", SubscriberNode);
76 | }
77 |
--------------------------------------------------------------------------------
/src/fiware_subscriber/icons/fiware.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/src/fiware_subscriber/icons/fiware.png
--------------------------------------------------------------------------------
/src/idl-types/icons/ros2-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/idl-types/idl-types.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
256 |
257 |
258 |
270 |
--------------------------------------------------------------------------------
/src/idl-types/idl-types.js:
--------------------------------------------------------------------------------
1 | // RED argument provides the module access to Node-RED runtime api
2 | module.exports = function(RED)
3 | {
4 | var execFile = require('child_process').execFile;
5 | var fs = require('fs');
6 | var is_web_api = require('is-web-api').ros2;
7 | var home = process.env.HOME;
8 |
9 | /*
10 | * @function IDLType constructor
11 | * This node is defined by the constructor function IDLType,
12 | * which is called when a new instance of the node is created
13 | *
14 | * @param {Object} config - Contains the properties set in the flow editor
15 | */
16 | function IDLType(config)
17 | {
18 | // Initiliaze the features shared by all nodes
19 | RED.nodes.createNode(this, config);
20 | var node = this;
21 |
22 | let {color, message} = is_web_api.add_idl_type(config["idltype"], config["name"]);
23 | if (message && color)
24 | {
25 | node.status({ fill: color, shape: "dot", text: message});
26 | }
27 |
28 | // Event emitted when the deploy is finished
29 | RED.events.once('flows:started', function() {
30 | let {color, message} = is_web_api.launch(config['id']);
31 | if (message && color)
32 | {
33 | node.status({ fill: color, shape: "dot", text: message});
34 | }
35 | });
36 |
37 | // Registers a listener to the input event,
38 | // which will be called whenever a message arrives at this node
39 | node.on('input', function(msg)
40 | {
41 | // Passes the message to the next node in the flow
42 | node.send(msg);
43 | });
44 |
45 | // Called when there is a re-deploy or the program is closed
46 | node.on('close', function()
47 | {
48 | // Stops the IS execution and resets the yaml
49 | is_web_api.stop();
50 | });
51 | }
52 |
53 | // The node is registered in the runtime using the name IDL Type
54 | RED.nodes.registerType("IDL Type", IDLType);
55 | RED.library.register("idl-type");
56 |
57 | /**
58 | * @brief Function that returns all the occurences of the substring in the main string
59 | * @param {String} substring - Contains the characters that want to be located
60 | * @param {String} string - Contains the main string
61 | * @returns
62 | */
63 | function locations (substring, string) {
64 | var a = [], i = -1;
65 | while ((i = string.indexOf(substring, i+1)) >= 0)
66 | {
67 | a.push(i);
68 | }
69 | return a;
70 | };
71 |
72 | /**
73 | * @brief Function to sort the type structures according to its dependencies
74 | * @param {Array} result - Array for storing the sort result
75 | * @param {Array} visited - Array containing the objects already visited
76 | * @param {Map} map - Map containing all the objects that need to be sorted
77 | * @param {Object} obj - Current object
78 | */
79 | function sort_util(result, visited, map, obj){
80 | visited[obj[0]] = true;
81 | Object.entries(obj[1]).forEach(function(dep){
82 | if(!visited[dep[1]] && Object.keys(map).includes(dep[1])) {
83 | sort_util(result, visited, map, map[dep[1]]);
84 | }
85 | });
86 | result.push(obj);
87 | }
88 |
89 | // Function that parse the IDL Type defined by the user and returns the parsing result to the html
90 | RED.httpAdmin.get("/checkidl", RED.auth.needsPermission('IDL Type.read'), function(req,res)
91 | {
92 | console.log(req.query["idl"]);
93 |
94 | // Execute the command line xtypes validator with the idl set by the user
95 | execFile("xtypes_idl_validator", [String(req.query["idl"])], function(error, stdout, stderr)
96 | {
97 | var result = {};
98 | var type_dict = {};
99 |
100 | if (error)
101 | {
102 | var init_index = stdout.indexOf('PEGLIB_PARSER:');
103 | var end_index = stdout.indexOf('[DEBUG] RESULT:');
104 | if (init_index != -1 && end_index != -1)
105 | {
106 | // If the validator returns an error it is passed to the html
107 | result["error"] = stdout.substr(init_index, end_index - init_index);
108 | res.json(result);
109 | }
110 | else
111 | {
112 | var index = stdout.indexOf('[ERROR]');
113 | if (index != -1)
114 | {
115 | // If the validator returns an error it is passed to the html
116 | result["error"] = stdout.substr(index);
117 | res.json(result);
118 | }
119 | }
120 | return;
121 | }
122 |
123 | // Defined Structure Position
124 | stdout = stdout.substr(stdout.indexOf('Struct Name:'));
125 | console.log(stdout);
126 | var occurences = locations('Struct Name:', stdout);
127 |
128 | var i = 0;
129 | occurences.forEach( s_pos =>
130 | {
131 | var cpy_stdout = stdout;
132 | console.log("i", i);
133 | if (occurences.length > i + 1)
134 | {
135 | cpy_stdout = stdout.substr(s_pos, occurences[i+1]);
136 | }
137 | else
138 | {
139 | cpy_stdout = stdout.substr(s_pos);
140 | }
141 |
142 | var members = locations('Struct Member:', cpy_stdout);
143 | var struct_name = stdout.substr(s_pos + 12/*Struct Name:*/, members[0] - (12 + 1) /*\n*/);
144 | struct_name = struct_name.replace("\n", "");
145 | type_dict[struct_name] = {};
146 |
147 | // Check the ROS 2 naming convention
148 | var modules = locations("::", struct_name);
149 | console.log("Modules", modules);
150 | if (!Array.isArray(modules) || !modules.length || modules.length != 2)
151 | {
152 | result["error"] =
153 | "The type needs two modules to follow the ROS 2 naming convention: the package_name and msg/srv.";
154 | res.json(result);
155 | return;
156 | }
157 | else
158 | {
159 | console.log("Checking ROS 2 naming convention");
160 | var pkg_name = struct_name.substr(0, modules[0]);
161 | var pkg_rgx = new RegExp("[a-z]+(([a-z0-9]*)_?[a-z0-9]+)+");
162 | if (!pkg_rgx.test(pkg_name))
163 | {
164 | console.log("Error package name");
165 | result["error"] = "The type package_name needs to follow the ROS 2 naming convention.";
166 | res.json(result);
167 | return;
168 | }
169 | var inner_module = struct_name.substr(modules[0] + 2, modules[1] - (modules[0] + 2));
170 | if (inner_module != "msg" && inner_module != "srv")
171 | {
172 | console.log("Error msg");
173 | result["error"] = "The second module must be msg or srv to follow the ROS 2 naming convention.";
174 | res.json(result);
175 | return;
176 | }
177 | var type_name = struct_name.substr(modules[1] + 2);
178 | var type_rgx = new RegExp("[A-Z]([a-zA-Z0-9])*");
179 | if (!type_rgx.test(type_name))
180 | {
181 | console.log("Error type name");
182 | result["error"] = "The type name needs to follow the ROS 2 naming convention.";
183 | res.json(result);
184 | return;
185 | }
186 |
187 | members.forEach( pos => {
188 | var init_pos = cpy_stdout.indexOf('[', pos);
189 | var inner_name = cpy_stdout.substr(pos + 14/*Struct Member:*/, init_pos - (pos + 14));
190 | if (inner_name == struct_name)
191 | {
192 | var member = cpy_stdout.substr(init_pos + 1, cpy_stdout.indexOf(']', pos) - init_pos - 1);
193 | var data = member.split(',');
194 | type_dict[inner_name][data[0]] = data[1];
195 | }
196 | });
197 |
198 | i++;
199 |
200 | console.log(type_dict);
201 | }
202 | });
203 |
204 | var map = {}; // Creates key value pair of name and object
205 | var result_array = []; // the result array
206 | var visited = {}; // takes a note of the traversed dependency
207 |
208 | Object.entries(type_dict).forEach( function(obj){ // build the map
209 | map[obj[0]] = obj;
210 | });
211 |
212 | Object.entries(type_dict).forEach(function(obj){ // Traverse array
213 | if(!visited[obj[0]]) { // check for visited object
214 | sort_util(result_array, visited, map, obj);
215 | }
216 | });
217 |
218 | if (!Object.keys(result).includes("error"))
219 | {
220 | // turn :: int backslashes as is ros2 convention
221 | result["name"] = result_array[result_array.length - 1][0];
222 | res.json(result);
223 | return;
224 | }
225 | });
226 | });
227 | }
228 |
229 |
--------------------------------------------------------------------------------
/src/publisher/icons/ros2-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/publisher/publisher.html:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
269 |
270 |
271 |
289 |
--------------------------------------------------------------------------------
/src/publisher/publisher.js:
--------------------------------------------------------------------------------
1 | // RED argument provides the module access to Node-RED runtime api
2 | module.exports = function(RED)
3 | {
4 | var fs = require('fs');
5 | var is_web_api = require('is-web-api').ros2;
6 | /*
7 | * @function PublisherNode constructor
8 | * This node is defined by the constructor function PublisherNode,
9 | * which is called when a new instance of the node is created
10 | *
11 | * @param {Object} config - Contains the properties set in the flow editor
12 | */
13 | function PublisherNode(config)
14 | {
15 | // Initiliaze the features shared by all nodes
16 | RED.nodes.createNode(this, config);
17 | this.props = config.props;
18 | var node = this;
19 | node.ready = false;
20 |
21 | node.status({fill: "yellow", shape: "dot", text: "Wait until Visual-ROS is ready to be used."});
22 |
23 | if(config.domain)
24 | {
25 | // modify the global domain
26 | var selected_domain = RED.nodes.getNode(config.domain).domain;
27 | is_web_api.set_dds_domain(selected_domain);
28 | }
29 |
30 | let {color, message} = is_web_api.add_publisher(config['id'], config['topic'], config['selectedtype'], config['props']);
31 | if (message && color)
32 | {
33 | node.status({ fill: color, shape: "dot", text: message});
34 | }
35 |
36 | // Event emitted when the deploy is finished
37 | RED.events.once('flows:started', function()
38 | {
39 | let {color, message} = is_web_api.launch(config['id']);
40 | if (message && color)
41 | {
42 | node.status({ fill: color, shape: "dot", text: message});
43 | }
44 | });
45 |
46 | var event_emitter = is_web_api.get_event_emitter();
47 | if (event_emitter)
48 | {
49 | event_emitter.on('IS-ERROR', function(status)
50 | {
51 | node.ready = false;
52 | node.status(status);
53 | });
54 |
55 | event_emitter.on('ROS2_connected', function()
56 | {
57 | node.ready = true;
58 | node.status({ fill: null, shape: null, text: null});
59 | });
60 | }
61 |
62 | // Registers a listener to the input event,
63 | // which will be called whenever a message arrives at this node
64 | node.on('input', function(msg)
65 | {
66 | if (node.ready)
67 | {
68 | node.status({ fill: "green", shape: "dot", text: "Message Published"});
69 |
70 | // Passes the message to the next node in the flow
71 | node.send(msg);
72 | is_web_api.send_message(config['topic'], msg);
73 | }
74 | else
75 | {
76 | done("node was not ready to process flow data");
77 | }
78 | });
79 |
80 | // Called when there is a re-deploy or the program is closed
81 | node.on('close', function()
82 | {
83 | // Stops the IS execution and resets the yaml
84 | is_web_api.new_config();
85 | is_web_api.stop();
86 | node.status({ fill: null, shape: null, text: ""});
87 | });
88 | }
89 |
90 | // The node is registered in the runtime using the name Publisher
91 | RED.nodes.registerType("Publisher", PublisherNode);
92 |
93 | // Function that sends to the html file the qos descriptions read from the json file
94 | RED.httpAdmin.get("/pubqosdescription", RED.auth.needsPermission('Publisher.read'), function(req,res)
95 | {
96 | var description_path = __dirname + "/../qos-description.json";
97 | var rawdata = fs.readFileSync(description_path);
98 | let json = JSON.parse(rawdata);
99 | res.json(json);
100 | });
101 | }
102 |
--------------------------------------------------------------------------------
/src/publisher/ros2-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eProsima/node-red-ros2-plugin/8b844432d9d299498b2f431077c75d9ca2b568e4/src/publisher/ros2-icon.png
--------------------------------------------------------------------------------
/src/qos-description.json:
--------------------------------------------------------------------------------
1 | {
2 | "history":
3 | {
4 | "description": "This QoS Policy controls the behavior of the system when the value of an instance changes one or more times before it can be successfully communicated to the existing DataReader entities. \n - kind: Controls if the service should deliver only the most recent values, all the intermediate values or do something in between. \n\t - KEEP_LAST: The service will only attempt to keep the most recent values of the instance and discard the older ones. \n\t - KEEP_ALL: The service will attempt to keep all the values of the instance until it can be delivered to all the existing Subscribers. \n - depth: Establishes the maximum number of samples that must be kept on the history. It only has effect if the kind is set to KEEP_LAST."
5 | },
6 | "reliability":
7 | {
8 | "description": "This QoS Policy indicates the level of reliability offered and requested by the service.\n - kind: Specifies the behavior of the service regarding delivery of the samples. \n\t - BEST_EFFORT: It indicates that it is acceptable not to retransmit the missing samples, so the messages are sent without waiting for an arrival confirmation. Presumably new values for the samples are generated often enough that it is not necessary to re-send any sample. However, the data samples sent by the same DataWriter will be stored in the DataReader history in the same order they occur. In other words, even if the DataReader misses some data samples, an older value will never overwrite a newer value. \n\t - RELIABLE: It indicates that the service will attempt to deliver all samples of the DataWriter’s history expecting an arrival confirmation from the DataReader. The data samples sent by the same DataWriter cannot be made available to the DataReader if there are previous samples that have not been received yet. The service will retransmit the lost data samples in order to reconstruct a correct snapshot of the DataWriter history before it is accessible by the DataReader."
9 | },
10 | "durability":
11 | {
12 | "description": "A DataWriter can send messages throughout a Topic even if there are no DataReaders on the network. Moreover, a DataReader that joins to the Topic after some data has been written could be interested in accessing that information. \n The DurabilityQoSPolicy defines how the system will behave regarding those samples that existed on the Topic before the DataReader joins. The behavior of the system depends on the value of the DurabilityQosPolicyKind. \n - VOLATILE: Past samples are ignored and a joining DataReader receives samples generated after the moment it matches. \n - TRANSIENT_LOCAL: When a new DataReader joins, its History is filled with past samples."
13 | },
14 | "deadline":
15 | {
16 | "description": "This QoS policy raises an alarm when the frequency of new samples falls below a certain threshold. It is useful for cases where data is expected to be updated periodically. \n On the publishing side, the deadline defines the maximum period in which the application is expected to supply a new sample. On the subscribing side, it defines the maximum period in which new samples should be received. \n For Topics with keys, this QoS is applied by key. Suppose that the positions of some vehicles have to be published periodically. In that case, it is possible to set the ID of the vehicle as the key of the data type and the deadline QoS to the desired publication period."
17 | },
18 | "lifespan":
19 | {
20 | "description": "Each data sample written by a DataWriter has an associated expiration time beyond which the data is removed from the DataWriter and DataReader history as well as from the transient and persistent information caches. \n By default, the duration is infinite, which means that there is not a maximum duration for the validity of the samples written by the DataWriter. \n The expiration time is computed by adding the duration to the source timestamp, which can be calculated automatically if write() member function is called or supplied by the application by means of write_w_timestamp() member function. The DataReader is allowed to use the reception timestamp instead of the source timestamp."
21 | },
22 | "liveliness":
23 | {
24 | "description": "This QoS Policy controls the mechanism used by the service to ensure that a particular entity on the network is still alive. There are different settings that allow distinguishing between applications where data is updated periodically and applications where data is changed sporadically. It also allows customizing the application regarding the kind of failures that should be detected by the liveliness mechanism. \n - kind: This data member establishes if the service needs to assert the liveliness automatically or if it needs to wait until the liveliness is asserted by the publishing side. \n\t - AUTOMATIC: The service takes the responsibility for renewing the leases at the required rates, as long as the local process where the participant is running and the link connecting it to remote participants exists, the entities within the remote participant will be considered alive. This kind is suitable for applications that only need to detect whether a remote application is still running. \n\t - MANUAL_BY_TOPIC: This mode requires requires that at least one instance within the DataWriter asserts the liveliness periodically before the lease_duration timer expires to consider that the DataWriter is alive. Publishing any new data value implicitly asserts the DataWriter’s liveliness, but it can be done explicitly by calling the assert_liveliness member function. \n - lease_duration: Amount of time to wait since the last time the DataWriter asserts its liveliness to consider that it is no longer alive."
25 | }
26 | }
--------------------------------------------------------------------------------
/src/ros2-message-inject/icons/ros2-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/ros2-message-inject/ros2-message-inject.js:
--------------------------------------------------------------------------------
1 | // RED argument provides the module access to Node-RED runtime api
2 | module.exports = function(RED)
3 | {
4 | var execFile = require('child_process').execFile;
5 | var cron = require('cron');
6 | var fs = require('fs');
7 | var home = process.env.HOME;
8 | var idl_path = "";
9 | var ros2_home = '/opt/ros/' + process.env.ROS_DISTRO;
10 | if (process.env.IS_ROS2_PATH)
11 | {
12 | ros2_home = process.env.IS_ROS2_PATH;
13 | }
14 | var is_web_api = require('is-web-api').ros2;
15 |
16 | /*
17 | * @function ROS2InjectNode constructor
18 | * This node is defined by the constructor function ROS2InjectNode,
19 | * which is called when a new instance of the node is created
20 | *
21 | * @param {Object} n - Contains the properties set in the flow editor
22 | */
23 | function ROS2InjectNode(n) {
24 | RED.nodes.createNode(this, n);
25 |
26 | this.props = n.props;
27 | this.repeat = n.repeat;
28 | this.crontab = n.crontab;
29 | this.once = n.once;
30 | this.onceDelay = (n.onceDelay || 0.1) * 1000;
31 | this.interval_id = null;
32 | this.cronjob = null;
33 | var node = this;
34 |
35 | node.status({fill: null, shape: null, text: ""});
36 |
37 | if (node.repeat > 2147483) {
38 | node.error(RED._("inject.errors.toolong", this));
39 | delete node.repeat;
40 | }
41 |
42 | node.repeaterSetup = function () {
43 | if (this.repeat && !isNaN(this.repeat) && this.repeat > 0) {
44 | this.repeat = this.repeat * 1000;
45 | if (RED.settings.verbose) {
46 | this.log(RED._("inject.repeat", this));
47 | }
48 | this.interval_id = setInterval(function() {
49 | node.emit("input", {});
50 | }, this.repeat);
51 | } else if (this.crontab) {
52 | if (RED.settings.verbose) {
53 | this.log(RED._("inject.crontab", this));
54 | }
55 | this.cronjob = new cron.CronJob(this.crontab, function() { node.emit("input", {}); }, null, true);
56 | }
57 | };
58 |
59 | if (this.once) {
60 | this.onceTimeout = setTimeout( function() {
61 | node.emit("input",{});
62 | node.repeaterSetup();
63 | }, this.onceDelay);
64 | } else {
65 | node.repeaterSetup();
66 | }
67 |
68 | var event_emitter = is_web_api.get_event_emitter();
69 | if (event_emitter)
70 | {
71 | // Event emitted if the integration server failed
72 | event_emitter.on('IS-ERROR', function(status)
73 | {
74 | node.status(status);
75 | });
76 | }
77 |
78 | this.on("input", function(msg, send, done) {
79 | var errors = [];
80 |
81 | this.props.forEach(p => {
82 | var property = p.p;
83 | var value = p.v ? p.v : '';
84 | var valueType = p.vt ? p.vt : 'str';
85 |
86 | if (!property) return;
87 |
88 | try {
89 | RED.util.setMessageProperty(msg,property,RED.util.evaluateNodeProperty(value, valueType, this, msg),true);
90 | } catch (err) {
91 | errors.push(err.toString());
92 | }
93 | });
94 |
95 | if (errors.length) {
96 | done(errors.join('; '));
97 | } else {
98 | send(msg);
99 | done();
100 | }
101 | });
102 | }
103 |
104 | // The node is registered in the runtime using the name ROS2 Inject
105 | RED.nodes.registerType("ROS2 Inject", ROS2InjectNode);
106 |
107 | ROS2InjectNode.prototype.close = function() {
108 | if (this.onceTimeout) {
109 | clearTimeout(this.onceTimeout);
110 | }
111 | if (this.interval_id != null) {
112 | clearInterval(this.interval_id);
113 | if (RED.settings.verbose) { this.log(RED._("ROS2 Inject.stopped")); }
114 | } else if (this.cronjob != null) {
115 | this.cronjob.stop();
116 | if (RED.settings.verbose) { this.log(RED._("ROS2 Inject.stopped")); }
117 | delete this.cronjob;
118 | }
119 | };
120 |
121 | function updateNodeProps(node, props) {
122 | node.props = JSON.parse(props);
123 | }
124 |
125 |
126 | RED.httpAdmin.post("/inject/:id/:props", RED.auth.needsPermission("ROS2 Inject.write"), function(req,res) {
127 | var node = RED.nodes.getNode(req.params.id);
128 | if (node != null) {
129 | try {
130 | updateNodeProps(node, req.params.props);
131 | node.receive();
132 | res.sendStatus(200);
133 | } catch(err) {
134 | res.sendStatus(500);
135 | node.error(RED._("ROS2 Inject.failed",{error:err.toString()}));
136 | }
137 | } else {
138 | res.sendStatus(404);
139 | }
140 | });
141 |
142 | /**
143 | * @brief Function that returns all the occurences of the substring in the main string
144 | * @param {String} substring - Contains the characters that want to be located
145 | * @param {String} string - Contains the main string
146 | * @returns
147 | */
148 | function locations (substring, string) {
149 | var a = [],i = -1;
150 | while ((i = string.indexOf(substring, i+1)) >= 0)
151 | {
152 | a.push(i);
153 | }
154 | return a;
155 | };
156 |
157 | /**
158 | * @brief Function to sort the type structures according to its dependencies
159 | * @param {Array} result - Array for storing the sort result
160 | * @param {Array} visited - Array containing the objects already visited
161 | * @param {Map} map - Map containing all the objects that need to be sorted
162 | * @param {Object} obj - Current object
163 | */
164 | function sort_util(result, visited, map, obj){
165 | visited[obj[0]] = true;
166 | Object.entries(obj[1]).forEach(function(dep){
167 | if(!visited[dep[1]] && Object.keys(map).includes(dep[1])) {
168 | sort_util(result, visited, map, map[dep[1]]);
169 | }
170 | });
171 | result.push(obj);
172 | }
173 |
174 | // Function that returns the IDL associated with the selected message type
175 | RED.httpAdmin.get("/getidl", RED.auth.needsPermission("ROS2 Inject.write"), function(req,res)
176 | {
177 | var idl = "";
178 | if (req.query['idl'])
179 | {
180 | idl = req.query['idl'];
181 | }
182 | else
183 | {
184 | var msg_path = ros2_home + "/share/" + req.query['package'] + "/msg/" + req.query['msg'] + ".idl";
185 |
186 | idl = fs.readFileSync(msg_path).toString();
187 | }
188 |
189 | var type_dict = {};
190 |
191 | // Executes the xtypes command line validator to get the type members
192 | execFile("xtypes_idl_validator", [String(idl)], function(error, stdout, stderr) {
193 | // Defined Structure Position
194 | stdout = stdout.substr(stdout.indexOf('Struct Name:'));
195 | console.log(stdout);
196 | var occurences = locations('Struct Name:', stdout);
197 |
198 | var i = 0;
199 | occurences.forEach( s_pos =>
200 | {
201 | var members = locations('Struct Member:', stdout);
202 | var struct_name = stdout.substr(s_pos + 12/*Struct Name:*/, members[i] - (s_pos + 12 + 1) /*\n*/);
203 | type_dict[struct_name] = {};
204 |
205 | members.forEach( pos => {
206 | var init_pos = stdout.indexOf('[', pos);
207 | var inner_name = stdout.substr(pos + 14/*Struct Member:*/, init_pos - (pos + 14));
208 | if (inner_name == struct_name)
209 | {
210 | var member = stdout.substr(init_pos + 1, stdout.indexOf(']', pos) - init_pos - 1);
211 | var data = member.split(',');
212 | type_dict[inner_name][data[0]] = data[1];
213 | i++;
214 | }
215 | });
216 | });
217 |
218 | var map = {}; // Creates key value pair of name and object
219 | var result = []; // the result array
220 | var visited = {}; // takes a note of the traversed dependency
221 |
222 | Object.entries(type_dict).forEach( function(obj){ // build the map
223 | map[obj[0]] = obj;
224 | });
225 |
226 | Object.entries(type_dict).forEach(function(obj){ // Traverse array
227 | if(!visited[obj[0]]) { // check for visited object
228 | sort_util(result, visited, map, obj);
229 | }
230 | });
231 |
232 | console.log(result);
233 | res.json(result);
234 | });
235 | });
236 | }
237 |
--------------------------------------------------------------------------------
/src/ros2-types/icons/ros2-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/ros2-types/ros2-types.html:
--------------------------------------------------------------------------------
1 |
2 |
43 |
44 |
45 |
304 |
305 |
306 |
319 |
--------------------------------------------------------------------------------
/src/ros2-types/ros2-types.js:
--------------------------------------------------------------------------------
1 | // RED argument provides the module access to Node-RED runtime api
2 | module.exports = function(RED)
3 | {
4 | var fs = require('fs');
5 | var path = require('path');
6 | var is_web_api = require('is-web-api').ros2;
7 | var home = process.env.HOME;
8 | var ros2_home = '/opt/ros/' + process.env.ROS_DISTRO;
9 | if (process.env.IS_ROS2_PATH)
10 | {
11 | ros2_home = process.env.IS_ROS2_PATH;
12 | }
13 |
14 | var util = require('util');
15 |
16 | /*
17 | * @function ROS2Types constructor
18 | * This node is defined by the constructor function ROS2Types,
19 | * which is called when a new instance of the node is created
20 | *
21 | * @param {Object} config - Contains the properties set in the flow editor
22 | */
23 | function ROS2Types(config)
24 | {
25 | // Initiliaze the features shared by all nodes
26 | RED.nodes.createNode(this, config);
27 | var node = this;
28 |
29 | let {color, message} = is_web_api.add_ros2_type(config.ros2pkg, config.ros2message, config.wires[0]);
30 | if (message && color)
31 | {
32 | node.status({ fill: color, shape: "dot", text: message});
33 | node.emit("error", message);
34 | }
35 |
36 | // Event emitted when the deploy is finished
37 | RED.events.once("flows:started", function() {
38 | let {color, message} = is_web_api.launch(config['id']);
39 | if (message && color)
40 | {
41 | node.status({ fill: color, shape: "dot", text: message});
42 | }
43 | });
44 |
45 | // Registers a listener to the input event,
46 | // which will be called whenever a message arrives at this node
47 | node.on('input', function(msg)
48 | {
49 | // Passes the message to the next node in the flow
50 | node.send(msg);
51 | });
52 |
53 | // Called when there is a re-deploy or the program is closed
54 | node.on('close', function()
55 | {
56 | // Stops the IS execution and resets the yaml
57 | is_web_api.stop();
58 | });
59 | }
60 |
61 | // The node is registered in the runtime using the name publisher
62 | RED.nodes.registerType("ROS2 Type", ROS2Types);
63 |
64 | // Function that pass the IS ROS 2 compiled packages to the html file
65 | RED.httpAdmin.get("/ros2packages", RED.auth.needsPermission('ROS2 Type.read'), function(req,res)
66 | {
67 | var files = fs.readdirSync(ros2_home + "/share/");
68 |
69 | // Check if it is a msg package
70 | files.forEach( function(value)
71 | {
72 | if (!fs.existsSync(ros2_home + "/share/" + value + "/msg"))
73 | {
74 | files = files.filter(f => f != value);
75 | }
76 | });
77 |
78 | res.json(files);
79 | });
80 |
81 | // Function that pass the IS ROS 2 package compiled msgs to the html file
82 | RED.httpAdmin.get("/ros2msgs", RED.auth.needsPermission('ROS2 Type.read'), function(req,res)
83 | {
84 | var msgs_path = ros2_home + "/share/" + req.query["package"] + "/msg/";
85 |
86 | var files = fs.readdirSync(msgs_path);
87 |
88 | // Check that it is a file with .idl
89 | files.forEach( function(filename)
90 | {
91 | if (path.extname(filename) != ".idl")
92 | {
93 | files = files.filter(f => f != filename);
94 | }
95 | });
96 |
97 | files.forEach( function(filename, index)
98 | {
99 | files[index] = path.parse(filename).name;
100 | })
101 |
102 | res.json(files);
103 | });
104 |
105 | // Function that pass the selected message idl and msg codes
106 | RED.httpAdmin.get("/msgidl", RED.auth.needsPermission('ROS2 Type.read'), function(req,res)
107 | {
108 | if (req.query['msg'])
109 | {
110 | var json_data = {}
111 | // IDL
112 | var msg_path = ros2_home + "/share/" + req.query['package'] + "/msg/" + req.query['msg'];
113 |
114 | var idl = fs.readFileSync(msg_path + ".idl").toString();
115 | json_data["idl"] = idl;
116 |
117 | // MSG
118 | if (fs.existsSync(msg_path + ".msg"))
119 | {
120 | var msg = fs.readFileSync(msg_path + ".msg").toString();
121 | json_data["msg"] = msg;
122 | }
123 | else
124 | {
125 | json_data["msg"] = "";
126 | }
127 | res.json(json_data);
128 | }
129 | });
130 | }
131 |
--------------------------------------------------------------------------------
/src/subscriber/icons/ros2-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/subscriber/subscriber.html:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
268 |
269 |
270 |
288 |
--------------------------------------------------------------------------------
/src/subscriber/subscriber.js:
--------------------------------------------------------------------------------
1 | const { send } = require('process');
2 |
3 | // RED argument provides the module access to Node-RED runtime api
4 | module.exports = function(RED)
5 | {
6 | var events = require('events');
7 | var fs = require('fs');
8 | var is_web_api = require('is-web-api').ros2;
9 | /*
10 | * @function SubscriberNode constructor
11 | * This node is defined by the constructor function SubscriberNode,
12 | * which is called when a new instance of the node is created
13 | *
14 | * @param {Object} config - Contains the properties set in the flow editor
15 | */
16 | function SubscriberNode(config)
17 | {
18 | // Initiliaze the features shared by all nodes
19 | RED.nodes.createNode(this, config);
20 | this.props = config.props;
21 | var node = this;
22 | node.ready = false;
23 |
24 | node.status({fill: "yellow", shape: "dot", text: "Wait until Visual-ROS is ready to be used."});
25 |
26 | if(config.domain)
27 | {
28 | // modify the global domain
29 | node.domain = RED.nodes.getNode(config.domain).domain;
30 | is_web_api.set_dds_domain(node.domain);
31 | }
32 |
33 | let {color, message} = is_web_api.add_subscriber(config['id'], config["topic"], config['selectedtype'],
34 | config['props']);
35 | if (message && color)
36 | {
37 | node.status({ fill: color, shape: "dot", text: message });
38 | }
39 |
40 | // Event emitted when the deploy is finished
41 | RED.events.once('flows:started', function() {
42 | let {color, message, event_emitter} = is_web_api.launch(config['id']);
43 | if (message && color)
44 | {
45 | node.status({ fill: color, shape: "dot", text: message});
46 | }
47 | if (event_emitter)
48 | {
49 | // Event emitted when a new message is received
50 | event_emitter.on(config["topic"] + '_data', function(msg_json)
51 | {
52 | node.status({ fill: "green", shape: "dot", text: "Message Received" });
53 | // Passes the message to the next node in the flow
54 | node.send(msg_json['msg']);
55 | });
56 |
57 | // Event emitted when the WebSocket Client is connected correctly
58 | event_emitter.on('ROS2_connected', function()
59 | {
60 | node.ready = true;
61 | node.status({ fill: null, shape: null, text: null});
62 | });
63 |
64 | event_emitter.on('IS-ERROR', function(status)
65 | {
66 | node.ready = false;
67 | node.status(status);
68 | });
69 | }
70 | });
71 |
72 | // Called when there is a re-deploy or the program is closed
73 | node.on('close', function()
74 | {
75 | // Stops the IS execution and resets the yaml
76 | is_web_api.stop();
77 | is_web_api.new_config();
78 | });
79 | }
80 |
81 | // The node is registered in the runtime using the name Subscriber
82 | RED.nodes.registerType("Subscriber", SubscriberNode);
83 |
84 | //Function that sends to the html file the qos descriptions read from the json file
85 | RED.httpAdmin.get("/subqosdescription", RED.auth.needsPermission('Subscriber.read'), function(req,res)
86 | {
87 | var description_path = __dirname + "/../qos-description.json";
88 | var rawdata = fs.readFileSync(description_path);
89 | let json = JSON.parse(rawdata);
90 | res.json(json);
91 | });
92 | }
93 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const hello = require('../src/hello');
3 |
4 | describe('Array', function () {
5 | describe('getText()', function () {
6 | it('should return 0 when the value is present', function () {
7 | assert.equal(hello.getText().includes('Hello'), true);
8 | });
9 | it('should return -1 when the value is not present', function () {
10 | assert.equal(hello.getText().includes('Goodbye'), false);
11 | });
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/theme/README.md:
--------------------------------------------------------------------------------
1 | ## How to apply the theme to the Node-RED instance
2 | The theme included in this folder can be applied to Node-RED editor by following the next instructions:
3 | 1. Find the Node-RED user directory. By default, it is set to `/root/.node-red`, but it can be changed using the `-u` option provided by Node-RED.
4 | 2. When Node-RED is launched with a new user directory it creates all the files and directories necessaries for its execution. Find the `settings.js` location within the user directory and open it.
5 | 3. Modify it to add the following lines:
6 | ```
7 | editorTheme: {
8 | page: {
9 | css: [
10 | "/theme/theme.css"
11 | ]
12 | }
13 | }
14 | ```
15 | 4. Then launch again Node-RED, and the new theme will be applied.
--------------------------------------------------------------------------------
/theme/theme.css:
--------------------------------------------------------------------------------
1 | #red-ui-header {
2 | background-color: #1a1737;
3 | }
4 |
5 | #red-ui-header .red-ui-deploy-button {
6 | background: #da1f4d;
7 | color: #eee;
8 | }
9 |
10 | #red-ui-header .button-group > a:hover {
11 | background: #bf1a43;
12 | }
13 |
14 | #red-ui-header .button {
15 | text-align: center;
16 | color: #7dc2b8;
17 | }
18 |
19 | .red-ui-tray-toolbar button.primary {
20 | border-color: #eee;
21 | color: #eee !important;
22 | background: #da1f4d;
23 | }
24 |
25 | #node-dialog-ok:hover {
26 | background: #bf1a43;
27 | }
28 |
29 | .red-ui-debug-msg-type-string {
30 | color: #da1f4d;
31 | }
32 |
33 | .red-ui-editor a:hover {
34 | color: #58bfb0;
35 | }
36 |
37 | .red-ui-help dl.message-properties > dt {
38 | color: #da1f4d;
39 | }
--------------------------------------------------------------------------------
/theme/theme.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 | RED.plugins.registerPlugin("ramp", {
3 | type: "node-red-theme",
4 | css: [
5 | "theme.css"
6 | ]
7 | })
8 | }
9 |
--------------------------------------------------------------------------------