Skip to content
This repository was archived by the owner on Nov 26, 2025. It is now read-only.

Commit 59163ba

Browse files
authored
Add set_level and status to bridge (#135)
Prior to this PR, the bridge returned "set_level" in response to requests - this doesn't match the protocol spec which claims set_level is a request to set the reporting level, not a response. This PR makes the bridge return actual status responses, and suppresses status messages below a level configurable with the set_level message. I also updated the error cases to forward the actual messages instead of just using debug(), and added a command-line flag to set the starting status level (for testing and/or other scripting). Fixes #5
1 parent cfe23e7 commit 59163ba

File tree

6 files changed

+157
-30
lines changed

6 files changed

+157
-30
lines changed

bin/rosbridge.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const pkg = require('../package.json');
2323
app
2424
.version(pkg.version)
2525
.option('-p, --port [port_number]', 'Listen port, default to :9090')
26-
.option('-a, --address [address_string]', 'Address')
26+
.option('-a, --address [address_string]', 'Remote server address (client mode); server mode if unset')
2727
.option('-r, --retry_startup_delay [delay_ms]', 'Retry startup delay in millisecond')
2828
.option('-o, --fragment_timeout [timeout_ms]', 'Fragment timeout in millisecond')
2929
.option('-d, --delay_between_messages [delay_ms]', 'Delay between messages in millisecond')
@@ -32,6 +32,7 @@ app
3232
.option('-s, --services_glob [glob_list]', 'A list or None')
3333
.option('-g, --params_glob [glob_list]', 'A list or None')
3434
.option('-b, --bson_only_mode', 'Unsupported in WebSocket server, will be ignored')
35+
.option('-l, --status_level [level_string]', 'Status level (one of "error", "warning", "info", "none"; default "error")')
3536
.parse(process.argv);
3637

3738
rosbridge.createServer(app);

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function createServer(options) {
4848
}
4949

5050
const makeBridge = (ws) => {
51-
let bridge = new Bridge(node, ws);
51+
let bridge = new Bridge(node, ws, options.status_level);
5252
bridgeMap.set(bridge.bridgeId, bridge);
5353

5454
bridge.on('error', (error) => {

lib/bridge.js

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const EventEmitter = require('events');
2020
const uuidv4 = require('uuid/v4');
2121
const {validator} = require('rclnodejs');
2222

23+
const STATUS_LEVELS = ['error', 'warning', 'info', 'none'];
24+
2325
class MessageParser {
2426
constructor() {
2527
this._buffer = '';
@@ -65,7 +67,8 @@ class MessageParser {
6567
}
6668

6769
class Bridge extends EventEmitter {
68-
constructor(node, ws) {
70+
71+
constructor(node, ws, statusLevel) {
6972
super();
7073
this._ws = ws;
7174
this._parser = new MessageParser();
@@ -76,6 +79,7 @@ class Bridge extends EventEmitter {
7679
this._registerConnectionEvent(ws);
7780
this._rebuildOpMap();
7881
this._topicsPublished = new Map();
82+
this._setStatusLevel(statusLevel || 'error');
7983
debug(`Web bridge ${this._bridgeId} is created`);
8084
}
8185

@@ -152,11 +156,17 @@ class Bridge extends EventEmitter {
152156
}
153157

154158
_rebuildOpMap() {
159+
this._registerOpMap('set_level', (command) => {
160+
if (STATUS_LEVELS.indexOf(command.level) === -1) {
161+
throw new Error(`Invalid status level ${command.level}; must be one of ${STATUS_LEVELS}`);
162+
}
163+
this._setStatusLevel(command.level);
164+
});
165+
155166
this._registerOpMap('advertise', (command) => {
156167
let topic = command.topic;
157168
if (this._topicsPublished.has(topic) && (this._topicsPublished.get(topic) !== command.type)) {
158-
debug(`The topic ${topic} already exists with a different type ${this._topicsPublished.get(topic)}.`);
159-
throw new Error();
169+
throw new Error(`The topic ${topic} already exists with a different type ${this._topicsPublished.get(topic)}.`);
160170
}
161171
debug(`advertise a topic: ${topic}`);
162172
this._topicsPublished.set(topic, command.type);
@@ -168,8 +178,7 @@ class Bridge extends EventEmitter {
168178
this._validateTopicOrService(command.topic);
169179

170180
if (!this._topicsPublished.has(topic)) {
171-
debug(`The topic ${topic} does not exist.`);
172-
let error = new Error();
181+
let error = new Error(`The topic ${topic} does not exist`);
173182
error.level = 'warning';
174183
throw error;
175184
}
@@ -200,8 +209,7 @@ class Bridge extends EventEmitter {
200209
this._validateTopicOrService(topic);
201210

202211
if (!this._resourceProvider.hasSubscription(topic)) {
203-
debug(`The topic ${topic} does not exist.`);
204-
let error = new Error();
212+
let error = new Error(`The topic ${topic} does not exist.`);
205213
error.level = 'warning';
206214
throw error;
207215
}
@@ -252,8 +260,7 @@ class Bridge extends EventEmitter {
252260
this._validateTopicOrService(serviceName);
253261

254262
if (!this._resourceProvider.hasService(serviceName)) {
255-
debug(`The service ${serviceName} does not exist.`);
256-
let error = new Error();
263+
let error = new Error(`The service ${serviceName} does not exist.`);
257264
error.level = 'warning';
258265
throw error;
259266
}
@@ -265,18 +272,15 @@ class Bridge extends EventEmitter {
265272
executeCommand(command) {
266273
try {
267274
const op = this._opMap[command.op];
268-
if (op) {
269-
op.apply(this, [command]);
270-
this._sendBackOperationStatus();
271-
} else {
272-
debug(`Operation ${command.op} is not supported.`);
273-
this._sendBackOperationStatus({id: command.id, op: command.op});
275+
if (!op) {
276+
throw new Error(`Operation ${command.op} is not supported`);
274277
}
278+
op.apply(this, [command]);
279+
this._sendBackOperationStatus(command.id, 'none', 'OK');
275280
} catch (e) {
276-
debug(`Exception caught in Bridge.executeCommand(): ${e}`);
277281
e.id = command.id;
278282
e.op = command.op;
279-
this._sendBackOperationStatus(e);
283+
this._sendBackErrorStatus(e);
280284
}
281285
}
282286

@@ -286,19 +290,31 @@ class Bridge extends EventEmitter {
286290
this._ws.send(JSON.stringify(response));
287291
}
288292

289-
_sendBackOperationStatus(error) {
290-
let command;
291-
if (error) {
292-
error.level = error.level || 'error';
293-
command = {op: 'set_level', id: error.id, level: error.level};
294-
debug(`Error: ${error} happened when executing command ${error.op}`);
295-
} else {
296-
command = {op: 'set_level', level: 'none'};
293+
_sendBackErrorStatus(error) {
294+
const msg = `${error.op}: ${error}`;
295+
return this._sendBackOperationStatus(error.id, error.level || 'error', msg);
296+
}
297+
298+
_sendBackOperationStatus(id, level, msg) {
299+
let command = {
300+
op: 'status',
301+
level: level || 'none',
302+
msg: msg || '',
303+
id: id,
304+
};
305+
if (this._statusLevel < STATUS_LEVELS.indexOf(level)) {
306+
debug('Suppressed: ' + JSON.stringify(command));
307+
return;
297308
}
298309
debug('Response: ' + JSON.stringify(command));
299310
this._ws.send(JSON.stringify(command));
300311
}
301312

313+
_setStatusLevel(level) {
314+
this._statusLevel = STATUS_LEVELS.indexOf(level);
315+
debug(`Status level set to ${level} (${this._statusLevel})`);
316+
}
317+
302318
_validateTopicOrService(name) {
303319
if (name.startsWith('/')) {
304320
validator.validateFullTopicName(name);

test/nodejs/protocol/entry.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ var rosbridge = path.resolve(__dirname, '../../../bin/rosbridge.js');
2323

2424
describe('Rosbridge v2.0 protocol testing', function() {
2525
var webSocketServer;
26-
this.timeout(60 * 1000);
26+
this.timeout(5 * 1000);
2727

2828
before(function(done) {
29-
webSocketServer = child.fork(rosbridge, {silent: true});
29+
webSocketServer = child.fork(rosbridge, ['-l', 'none'], {silent: true});
3030
webSocketServer.stdout.on('data', function(data) {
3131
done();
3232
});
@@ -83,6 +83,10 @@ describe('Rosbridge v2.0 protocol testing', function() {
8383
require('./test-unadvertise-service.js')();
8484
});
8585

86+
describe('set_level operation', function() {
87+
require('./test-set-level.js')();
88+
});
89+
8690
// Disable this case temporarily, sine it gets stuck on Windows CI.
8791
// describe('response operations', function() {
8892
// require('./test-response-op.js')();

test/nodejs/protocol/test-call-service.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,10 @@ module.exports = function() {
134134
ws.send(JSON.stringify(testData.callServiceMsg));
135135
});
136136
ws.on('message', function(data) {
137+
console.log(data);
137138
let response = JSON.parse(data);
138139

139-
if (response.op === 'set_level') {
140+
if (response.op === 'status') {
140141
assert.deepStrictEqual(response.level, testData.opStatus);
141142
ws.close();
142143
done();
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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 assert = require('assert');
18+
const WebSocket = require('ws');
19+
20+
module.exports = function() {
21+
let testCasesData = [
22+
{
23+
title: 'set_level to error',
24+
ops: [
25+
{
26+
payload: {op: 'set_level', id: 'id1', level: 'error'},
27+
status: null
28+
}
29+
],
30+
},
31+
{
32+
title: 'set_level to warning',
33+
ops: [
34+
{
35+
payload: {op: 'set_level', id: 'id1', level: 'warning'},
36+
status: null
37+
}
38+
],
39+
},
40+
{
41+
title: 'set_level to info',
42+
ops: [
43+
{
44+
payload: {op: 'set_level', id: 'id1', level: 'info'},
45+
status: null
46+
}
47+
],
48+
},
49+
{
50+
title: 'set_level to none',
51+
ops: [
52+
{
53+
payload: {op: 'set_level', id: 'id1', level: 'none'},
54+
status: 'none'
55+
}
56+
],
57+
},
58+
{
59+
title: 'set_level to invalid',
60+
ops: [
61+
{
62+
payload: {op: 'set_level', id: 'id1', level: 'invalid'},
63+
status: 'error'
64+
}
65+
],
66+
},
67+
];
68+
69+
testCasesData.forEach((testData, index) => {
70+
it(testData.title, function() {
71+
return new Promise((resolve, reject) => {
72+
let ws = new WebSocket('ws://127.0.0.1:9090');
73+
let counter = 0;
74+
let timeout = null;
75+
76+
function handleMessage(data) {
77+
if (timeout !== null) {
78+
clearTimeout(timeout);
79+
timeout = null;
80+
}
81+
if (data !== null || testData.ops[counter].status !== null) {
82+
let response = JSON.parse(data);
83+
assert.deepStrictEqual(response.level, testData.ops[counter].status);
84+
}
85+
86+
counter++;
87+
if (counter === testData.ops.length) {
88+
ws.close();
89+
resolve();
90+
} else {
91+
ws.send(JSON.stringify(testData.ops[counter].payload));
92+
}
93+
}
94+
ws.on('message', handleMessage);
95+
96+
ws.on('open', function() {
97+
ws.send(JSON.stringify(testData.ops[0].payload));
98+
if (testData.ops[0].status === null) {
99+
timeout = setTimeout(() => handleMessage(null), 100);
100+
}
101+
});
102+
});
103+
});
104+
});
105+
};

0 commit comments

Comments
 (0)