The websockets server that intermediates between Lux Agent (that lives on the robot) and the users' browsers. It subscribes to changes in the robot state sent from the agent and maintains a copy in memory. It sends the changes on to any attached browsers that have access to the robot. It also receives commands from the browser (such as 'launch a node' and sends them on to the agent).
Multiple browsers can view the same ROS instance by connecting to the server. The server will only receive one set of updates from the agent, regardless of how many browsers are viewing.
Communication is via websockets. Currently messages are in JSON or optionally BSON (at least ebtween server and agent) and will eventually be compressed.
node --harmony luxserver.js
The server can be run locally or deployed to heroku with
git push heroku master
mocha --harmony -w
mocha --harmony -w -g 'string for matching test names'
Error condition signaled by mbody containing error key:
{...., error: 'message', ...}
A full RosInstanceId looks like this:
"<org_id> <network_id> <base_ros_instance_id>"
A full machineId looks like this: "<org_id> <network_id> <base_ros_instance_id> <machine_id>"
Graph Structure transmitted from agent is a superset of the ROS computational graph, extended to include installed packages. This is stored on the server and forwarded to browsers but is not interpreted as such by the server. So far, at least.
The protocol by default consists of JSON messages like the following:
{
mtype: <command>,
mbody: <data>
}
Where "" is a simple string and "" is a further set of JSON data that comprises the payload.
This protocol may be optionally converted to BSON (at least between server and agent) and should eventually be compressed, perhaps with LZW or with command-specific compressions schemes (e.g. streaming JPEG). Encryption should be added, or at least some secure authentication method.
Currently, all updates generated by the ROS instance are passed to the server, without regard for bandwidth.
mtype: keepAlive
Dummy message sent by any party to keep the connection alive. Should be ignored.
These are the commands sent from the browser to the server.
Sent by the browser on connection to the server's websocket to request access to a ROS instance owened by an organization. The userName and secret is the (primitive) authentication method for the time being.
mtype: browserConnect
mbody: {rosinstance: rosInstanceBase, org: orgId, user: userName, secret: secret}
Reply:
mtype: browserConnected
mbody: instanceN
The reply acknowledges a successful connection.
mtype: subscribeRosInstances
Reply:
mtype: rosInstancesUpdate
mbody: [{add: {rosInstanceId: fullRosInstanceId1, rosInstanceHumanId: "foo instance"}}, {add: {rosInstanceId: fullRosInstanceId2, rosInstanceHumanId: "bar instance"}}}... ]
and as changes occur:
mtype: rosInstancesUpdate
mbody: [{add: fullRosInstanceId1}, {del: fullRosInstanceId2}... {add: fullRosInstanceIdN}]
mtype: subscribeRosInstance
mbody: rosInstance: fullRosInstanceId
Reply:
mtype: rosInstanceGraph
mbody: {rosInstance: fullRosInstanceId, graph: graph}
mtype: unsubscribeRosInstance
mbody: {rosInstance: fullRosInstanceId}
Reply:
mtype: unsubscribedRosInstance
mbody: {rosInstance: fullRosInstanceId}
mtype: rosrun
mbody: {rosmachine: fullMachineId, args: args}
Reply:
mtype: rosrunSent
mbody: {rosmachine: fullMachineId, args: args}
Performs a 'roslaunch' command on a machine in the ROS instance.
mtype: roslaunch
mbody: {rosmachine: fullMachineId, args: args}
Reply:
mtype: roslaunchSent
mbody: {rosmachine: fullMachineId, args: args}
Kills a process on one of the hosts in the ROS instance.
mtype: kill
mbody: {rosmachine: fullMachineId, args: args}
Reply:
mtype: kill
mbody: {rosmachine: fullMachineId, args: args}
TODO: Incorporate PID identifier.
Sends message to a ROS topic, as if it had been published by a regular ROS node.
mtype: message
mbody: {rosInstance: rosInstanceId, topic: topicName, message: <JSON message>}
mtype: rosInstanceGraphAdd
mbody: {rosInstance: rosInstanceId, graph: hash}
mtype: rosInstanceGraphAdd
mbody: {rosInstance: rosInstanceId, graph: ['key1', 'key2'...]}
mtype: rosInstanceGraphUpd
mbody: {rosInstance: rosInstanceId, graph: [ [node1Name, node1Value], [node2Name, node2Value], ... ]}
node1Value becomes the 'data' attribute of the uiGraph node.
mtype: agentConnect
mbody: {rosinstance: rosInstanceBase, org: orgId, user: userName, secret: secret}
Reply:
mtype: agentConnected
mbody: instanceN
or
mtype: agentRefused
mbody: "Human readable error message"
mtype: graphAdd
mbody: [[key1, value1], [key2, value2], ... [keyN, valueN]]
mtype: graphUpd
mbody: [[key1, value1], [key2, value2], ... [keyN, valueN]]
mtype: graphDel
mbody: [key1, key2, ... keyN] ??? Is it that or [[key, value], [key, value] ...]
MAYBE ****************
mtype: rosrunResult
mbody: ?
END NEW *****************
mtype: subscribeGraph
mtype: rosrun
mbody: {args: args}
mtype: roslaunch
mbody: {args: args}
mtype: kill
mbody: {args: args}
Publish a ROS message to a topic.
mtype: message
mbody: {topic: topicName, message: {...JSON encoded version of ROS message..}}
if an error then will get:
mtype: messageStatus
error:
- Add general compression to protocol. Have tried LZW but without success so far (apparent incompatibilities between JS and Python implementations).
- Add specific, efficient compression for images and streams of images.
- Add more robust authentication, perhaps involving Rails server.
- Improve robustness, error trapping in sockets. One connection failure should not bring down other clients. A general failure should crash out directly to let Heroku reboot.
- Ideally, agents and browsers should be able to invisibly reconnect after server reboot.
- Test scaling to many simulataneous clients with heavey messaging loads. How big / how many servers needed?
More generally, should meta-information on ROS instances, messages etc. be added here or on the Rails server? Right now, luxserver has no persistent state which is kind of nice.
(c) 2015 Robot Lux. All Rights Reserved.