Skip to content

Commit a890726

Browse files
authored
Merge pull request #196 from kenny-y/take-in-javascript-object-as-message
make publisher be able to take-in a plain JavaScript object as message to be published.
2 parents 366a57c + 32ab91b commit a890726

8 files changed

Lines changed: 472 additions & 8 deletions

File tree

index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const path = require('path');
2525
const QoS = require('./lib/qos.js');
2626
const rclnodejs = require('bindings')('rclnodejs');
2727
const validator = require('./lib/validator.js');
28+
const util = require('./lib/utility.js');
2829

2930
function inherits(target, source) {
3031
let properties = Object.getOwnPropertyNames(source.prototype);
@@ -223,7 +224,9 @@ let rcl = {
223224
*/
224225
expandTopicName(topicName, nodeName, nodeNamespace) {
225226
return rclnodejs.expandTopicName(topicName, nodeName, nodeNamespace);
226-
}
227+
},
228+
229+
util: util,
227230
};
228231

229232
module.exports = rcl;

lib/message_translator.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright (c) 2017 Intel Corporation. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const debug = require('debug')('rclnodejs:message_translator');
18+
19+
/* eslint-disable max-depth */
20+
function copyMsgObject(msg, obj) {
21+
if (typeof obj === 'object') {
22+
for (let i in obj) {
23+
if (typeof msg[i] !== 'undefined') {
24+
const type = typeof obj[i];
25+
if (type === 'string' || type === 'number' || type === 'boolean') {
26+
// A primitive-type value
27+
msg[i] = obj[i];
28+
} else if (Array.isArray(obj[i])) { // TODO(Kenny): deal with TypedArray
29+
// It's an array
30+
if (typeof obj[i][0] === 'object') {
31+
// It's an array of objects: converting to ROS message objects
32+
33+
// 1. Extract the element-type first
34+
const t = new MessageTranslator(msg[i].classType.elementType);
35+
// 2. Build the array by translate every elements
36+
let msgArray = [];
37+
obj[i].forEach((o) => {
38+
msgArray.push(t.toROSMessage(o));
39+
});
40+
// 3. Assign
41+
msg[i].fill(msgArray);
42+
} else {
43+
// It's an array of primitive-type elements
44+
msg[i] = obj[i];
45+
}
46+
} else {
47+
// Proceed further of this object
48+
copyMsgObject(msg[i], obj[i]); // TODO(Kenny): deal with TypedArray
49+
}
50+
} else {
51+
// Extra fields in obj (but not in msg)
52+
}
53+
} // for
54+
}
55+
}
56+
/* eslint-enable max-depth */
57+
58+
class MessageTranslator {
59+
constructor(typeClass) {
60+
this._typeClass = typeClass;
61+
}
62+
63+
static toPlainObject(message) {
64+
// TODO(Kenny): deal with primitive-type array (convert to TypedArray)
65+
66+
if (message.isROSArray) {
67+
// It's a ROS message array
68+
// Note: there won't be any JavaScript array in message
69+
let array = [];
70+
const data = message.data;
71+
data.forEach((e) => {
72+
// Translate every elements
73+
array.push(MessageTranslator.toPlainObject(e));
74+
});
75+
return array;
76+
// eslint-disable-next-line no-else-return
77+
} else {
78+
// It's a ROS message
79+
const def = message.classType.ROSMessageDef;
80+
let obj = {};
81+
for (let i in def.fields) {
82+
const name = def.fields[i].name;
83+
if (def.fields[i].type.isPrimitiveType) {
84+
// Direct assignment
85+
obj[name] = message[name];
86+
} else {
87+
// Proceed further
88+
obj[name] = MessageTranslator.toPlainObject(message[name]);
89+
}
90+
}
91+
return obj;
92+
}
93+
}
94+
95+
toROSMessage(obj) {
96+
let msg = new this._typeClass();
97+
const type = typeof obj;
98+
if (type === 'string' || type === 'number' || type === 'boolean') {
99+
msg.data = obj;
100+
} else if (type === 'object') {
101+
copyMsgObject(msg, obj);
102+
}
103+
return msg;
104+
}
105+
}
106+
107+
module.exports = {
108+
MessageTranslator: MessageTranslator
109+
};

lib/publisher.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
const rclnodejs = require('bindings')('rclnodejs');
1818
const debug = require('debug')('rclnodejs:publisher');
1919
const Entity = require('./entity.js');
20+
const {MessageTranslator} = require('./message_translator.js');
2021

2122
/**
2223
* @class - Class representing a Publisher in ROS
@@ -27,6 +28,7 @@ class Publisher extends Entity {
2728
constructor(handle, nodeHandle, typeClass, topic, qos) {
2829
super(handle, typeClass, qos);
2930
this._topic = topic;
31+
this.translator = new MessageTranslator(typeClass);
3032
}
3133

3234
/**
@@ -42,12 +44,15 @@ class Publisher extends Entity {
4244
* @return {undefined}
4345
*/
4446
publish(message) {
45-
// TODO(minggang): Support to convert a plain JavaScript value/object to a ROS message,
46-
// thus we can invoke this function like: publisher.publish('hello world').
47-
let rclMessage = message;
48-
if (!(message instanceof this._typeClass)) {
49-
rclMessage = new this._typeClass();
50-
rclMessage.data = message;
47+
let rclMessage;
48+
if (message instanceof this._typeClass) {
49+
rclMessage = message;
50+
} else {
51+
// Use translator to enable call by plain object/number/string argument
52+
// e.g. publisher.publish(3.14);
53+
// publisher.publish('The quick brown fox...');
54+
// publisher.publish({linear: {x: 0, y: 1, z: 2}, ...});
55+
rclMessage = this.translator.toROSMessage(message);
5156
}
5257

5358
let rawRosMessage = rclMessage.serialize();

lib/utility.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2017 Intel Corporation. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
/* eslint-disable max-depth */
18+
function deepEqual(msg, obj) {
19+
if (typeof obj !== 'object') {
20+
return msg === obj;
21+
}
22+
23+
for (let i in obj) {
24+
const m = msg[i];
25+
const o = obj[i];
26+
27+
if (typeof m === 'undefined') {
28+
return false;
29+
}
30+
31+
const t = typeof o;
32+
if (t === 'string' || t === 'number' || t === 'boolean') {
33+
// A primitive value
34+
if (m !== o) {
35+
return false;
36+
}
37+
} else if (m.isROSArray) {
38+
// An array of message objects
39+
if (!Array.isArray(o)) return false;
40+
41+
const data = m.data;
42+
for (let j in data) {
43+
if (!deepEqual(data[j], o[j])) {
44+
return false;
45+
}
46+
}
47+
} else {
48+
// A regular message object
49+
const equal = deepEqual(m, o);
50+
if (!equal) {
51+
return false;
52+
}
53+
}
54+
}
55+
56+
return true;
57+
}
58+
/* eslint-enable max-depth */
59+
60+
module.exports = {
61+
deepEqual: deepEqual,
62+
};

rosidl_gen/templates/message.dot

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ let arrayWrapper = it.spec.msgName + 'ArrayWrapper';
1818
let refObjectType = it.spec.msgName + 'RefStruct';
1919
let refObjectArrayType = it.spec.msgName + 'RefStructArray';
2020
let refArrayType = it.spec.msgName + 'RefArray';
21+
const compactMsgDefJSON = JSON.stringify(JSON.parse(it.json));
2122

2223
if (it.spec.fields.length === 0) {
2324
/* Following rosidl_generator_c style, put a bool member named '_dummy' */
@@ -326,6 +327,14 @@ class {{=objectWrapper}} {
326327
{{?}}
327328
{{~}}
328329
}
330+
331+
get classType() {
332+
return {{=objectWrapper}};
333+
}
334+
335+
static get ROSMessageDef() {
336+
return {{=compactMsgDefJSON}};
337+
}
329338
}
330339

331340
// Define the wrapper of array class.
@@ -445,6 +454,18 @@ class {{=arrayWrapper}} {
445454
{{=objectWrapper}}.freeStruct(refObjectArray[index]);
446455
}
447456
}
457+
458+
static get elementType() {
459+
return {{=objectWrapper}};
460+
}
461+
462+
get isROSArray() {
463+
return true;
464+
}
465+
466+
get classType() {
467+
return {{=arrayWrapper}};
468+
}
448469
}
449470

450471
{{? it.spec.constants != undefined && it.spec.constants.length}}

test/publisher_array_setup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ rclnodejs.init().then(function() {
4848
node.destroy();
4949
rclnodejs.shutdown();
5050
process.exit(0);
51-
});
51+
});
5252
}).catch(function(err) {
5353
console.log(err);
5454
});

0 commit comments

Comments
 (0)