How to create a HOP service for a ROS service?

Konstantinos Panayiotou edited this page May 26, 2016 · 11 revisions
Clone this wiki locally

RAPP Web Services are implemented on top of the hop.js framework.

Hop.js is a multitier extension of JavaScript. It allows a single JavaScript program to describe the client-side and the server-side components of a Web application. Its runtime environment ensures a consistent execution of the application on the server and on the client. Hop programs execute in the context of a builtin web server. They define services, which are super JavaScript functions that get automatically invoked when HTTP requests are received.

A framework has been developed, build ontop of hop.js, for easily implementing Web Services for the RAPP Platform. Zero knowledge of hop.js is required. Additionally, Web Services are fully parametrized through single configuration files:

If you are not familiar with server-side applications, you might also have to read on Nodejs.

RAPP Web Services framework

For easily implementing and launching Web Services for the RAPP Platform, the RAPP Web Services framework has been developed. It consists of a WebService implementation. build ontop of hop.js and an engine that allows to assign specific Web Services to Worker threads (Web Workers).

Web Service implementation

Web Services are implemented in a single function implementation:

function svcImpl(req, resp, ros){
 // Web service implementation here.
}

This is the onrequest callback function to feed to the engine and will be called as soon as a request arrives.

The req (request) and res (response) objects are passed so you can access the request properties and craft and return responses. Additionally a ros object is passed that allows connections to ROS thought the rosbridge-websocket transport layer.

The req object has the following properties:

  • header: Request header.
console.log(req.header)
>> {
>>   host: 'localhost:9001',
>>   content-length: 679,
>>   accept-encoding: 'gzip, deflate',
>>   accept: '*/*',
>>   user-agent: 'rapp-platform-api/python',
>>   accept-token: 'rapp_token',
>>   connection: 'keep-alive',
>>   content-type: 'multipart/form-data; boundary=595d1046de7f4c958ca662c37140215a'
>> }
  • socket: Connection socket
console.log(req.socket)
>>  {
>>    hostname: 'localhost',
>>    hostAddress: '127.0.0.1',
>>    localAddress: '127.0.0.1',
>>    port: 43661
>>  }
  • body: Request body
console.log(req.body)
>>  { fast: true }
  • files: In case of uploading files, this field contains the paths to the uploaded files. Access the files by name using dot notation. For example a service receives a single-file in fieldname single_file and an array of files in fieldname file_array:
console.log(req.files)
>>  {
>>    single_file: ["PATH"],
>>    file_array: ["PATH_FILE_1", "PATH_FILE_2"]
>>  }

Note: Note that even if it is a single-file, the single_file property of req.files (req.files.single_file) is an Array.

  • username: This is the username of the client that requested access to the RAPP Platform resources (Services). It is automatically applied to the req object, after appliance of the RAPP Authentication on request arrival. Note that before execution of the onrequest callback function, we apply authentication to the request. If the authentication is not successful, an HTTP 401 Unauthorized error is returned to the client.
console.log(req.username)
>>  "RAPP_USER"

The resp object has the following properties (methods):

  • sendJson(obj): Send an application/json response
function svcImpl(req, resp, ros){
  ...

  var response = {error: ''};
  resp.sendJson(response);
}
  • sendServerError(): Respond with HTTP 500 Internal Server Error
function svcImpl(req, resp, ros){
  ...

  resp.sendServerError();
}
  • sendUnauthorized(): Respond with HTTP 401 Unauthorized Client Error
function svcImpl(req, resp, ros){
  ...

  resp.sendUnauthorized();
}

Web Service configuration and registration.

Web Services are fully parametrized through the services.json file. This file includes Web Services to be launched (along with the Web Service parameters), that was previously declared in the workers.json file.

Below is the face_detection entry:

"face_detection": {
  "launch": true,
  "anonymous": false,
  "name": "face_detection",
  "url_name": "face_detection",
  "namespace": "",
  "ros_connection": true,
  "timeout": 45000
}

Web Service configuration parameters:

  • launch (Boolean): If true this Web Service will be launched.
  • anonymous (Boolean): If true, this service will be anonymous, which means that it will be assigned a random url path.
  • name (String): The service name.
  • urlname (String): The service urlname. Service name can be different from the urlname.
  • namespace (String): Namespace for the urlname to append as a prefix to the service url name. For example, a service with urlname="faca_detection" and namespace="computervision" will be translated to /computervision/face_detection
  • timeout (String): Request timeout value.
  • ros_connection (Boolean): If true, a ros object that allowes for calls to the ROS Services will be passed to the onrequest callback function.

Run a Web Service within an existing Web Worker

Web services run within server-side workers (Web Workers). A worker can include more than one web service. We consider server-side workers to be forked processes, thus allowing concurrent execution.

To run a Web Service within a Web Worker, just specify the service name in the services field of the worker in the worker.json file.

For example, the weather_report worker holds the weather_report_current and weather_report_forecast Web Services:

"weather_report": {
  "launch": true,
  "path": "workers/weather_report.js",
  "services": [
    "weather_report_forecast",
    "weather_report_current"
  ]
}

Web Worker configuration parameters:

  • launch (Boolean): Weather to launch the Web Worker or not.
  • path (String): Path to the Web Worker source file. Relative to the rapp_web_services directory
  • services: (Array): Services to launch under the Web Worker thread.

Where to store Web Service implementation source file(s) and how to launch it.

Source files are stored under the services directory, of the rapp_web_services package.

Web Services are automatically loaded from single .js files, as node.js modules. Make sure you export the Web Service implementation function:

function svcImpl(req, resp, ros){
 // Web service implementation here.
}

...

module.exports = svcImpl;

Complete Web Service Implementation Example - Face Detection

The following example illustrates the implementation of a WebService that connects to the Face-Detection RAPP-Platform backend Service

ROS Service Message:

# Contains info about time and reference
Header header
# The image's filename to perform face detection
string imageFilepath
# Flag to define if a fast detection if desired
bool fast
---
# Container for detected face positions
geometry_msgs/PointStamped[] faces_up_left
geometry_msgs/PointStamped[] faces_down_right
string error

and the Face-Detection ROS Service url path is: /rapp/rapp_face_detection/detect_faces

Web Service Request:

  • file: Image file.
  • fast (Bool): If true, detection will take less time but it will be less accurate.

Web Service Response:

  • faces (Array): Detected faces.
  • error (String): Error message.

First, create the face_detection svc.js file:

$ cd ~/rapp_platform/rapp-platform-catkin-ws/src/rapp-platform/rapp_web_services/services
$ mkdir face_detection && cd face_detection
$ touch svc.js

Open the svc.js file with your favorite editor:

$ vim svc.js

Implement the structure of the client-response and ros-msg objects:

var clientRes = function(faces, error) {
  return { faces: [], error: '' }
}

var rosReqMsg = function(imageFilepath, fast) {
  return { imageFilepath: '', fast: false }
}

Assign ROS Service url path to a global variable:

var rosSrvUrlPath = "/rapp/rapp_face_detection/detect_faces";

Next, implement the service onrequest callback function:

function svcImpl(req, resp, ros) {
  // If no image file received, return to client with an error
  if( ! req.files.file ){
    // Create a client response object
    response = new client_res();
    response.error = "No image file received";
    // Send response (application/json)
    resp.sendJson(response);
    return;
  }

  // Create a ROS Service Request Message and fill values from client request
  var rosMsg = new rosReqMsg();
  rosMsg.imageFilename = req.files.file[0];
  rosMsg.fast = req.body.fast;

  /***
   * ROS-Service response callback.
   */
  function callback(data){
    // Delete image file from the Platform cache directory.
    fs.exists(_filepath, function(exists){
      if(exists){
        fs.unlink(_filepath)
      }
    })
    // Parse rosbridge message and craft client response
    var response = parseRosbridgeMsg( data );
    resp.sendJson(response);
  }

  /***
   * ROS-Service onerror callback.
   */
  function onerror(e){
    // Delete image file from the Platform cache directory.
    fs.exists(_filepath, function(exists){
      if(exists){
        fs.unlink(_filepath)
      }
    })
    // Respond a "Server Error". HTTP Error 501 - Internal Server Error
    resp.sendServerError();
  }

  /***
   * Call ROS-Service.
   */
  ros.callService(rosSrvUrlPath, rosMsg,
    {success: callback, fail: onerror});
}

function parseRosbridgeMsg( rosbridge_msg )
{
  var faces_up_left = rosbridge_msg.faces_up_left;
  var faces_down_right = rosbridge_msg.faces_down_right;
  var error = rosbridge_msg.error;
  var numFaces = faces_up_left.length;

  // Create a new response object
  var response = new client_res();

  if( error ){
    // If ROS Service responded with an error
    response.error = error;
    return response;
  }

  for (var ii = 0; ii < numFaces; ii++)
  {
    var face = {
      up_left_point: {x: 0, y:0},
      down_right_point: {x: 0, y: 0}
    };

    face.up_left_point.x = faces_up_left[ii].point.x;
    face.up_left_point.y = faces_up_left[ii].point.y;
    face.down_right_point.x = faces_down_right[ii].point.x;
    face.down_right_point.y = faces_down_right[ii].point.y;
    response.faces.push( face );
  }

  return response;
}

Export the service onrequest callback function (svcImpl):

module.exports = svcImpl

Next you will need to create the Web Worker to launch the Web Service:

$ cd ~/rapp_platform/rapp-platform-catkin-ws/src/rapp-platform/rapp_web_services/workers
$ touch face_detection.js

Open the face_detection.js file with your favorite editor:

$ vim face_detection.js

Import the workerUtils module:

var path = require('path');

var ENV = require( path.join(__dirname, '../..', 'env.js') );

// Include it even if not used!!! Sets properties to the thread's global scope.
var workerUtils = require(path.join(ENV.PATHS.INCLUDE_DIR, 'common', 'worker_utils.js'));

Next, you will have to set the worker name and call to launch all services registred to this Web Worker:

// Set worker thread name under the global scope. (WORKER.name)
workerUtils.setWorkerName('face_detection');

// Launch all services assigned to this worker thread.
// Search in workers.json config file for assigned web services.
workerUtils.launchSvcAll();

We need to tell the run-engine to launch the, newly implemented, Web Worker.

The workers.json file containes Web Workers entries. It is located under:

~/rapp_platform/rapp-platform-catkin-ws/src/rapp-platform/rapp_web_services/config/services

Append the following entry in the workers.json file:

"face_detection": {
  "launch": true,
  "path": "workers/face_detection.js",
  "services": [
    "face_detection"
  ]
}

Finally append the following web-service entry in the services.json file (under the same directory):

"face_detection": {
  "launch": true,
  "anonymous": false,
  "name": "face_detection",
  "url_name": "face_detection",
  "namespace": "",
  "authentication": true,
  "ros_connection": true,
  "timeout": 45000
}

Now the RAPP Platform is ready to receive requests for the newly created face_detection service.

$ cd ~/rapp_platform/rapp-platform-catkin-ws/src/rapp-platform/rapp_web_services
$ pm2 start server.yaml

You will notice the following output from the logs:

info: [Service Handler]  Registered worker service {http://rapp-platform-local:9001/hop/face_detection} under worker thread {face_detection}
info: [Service Handler]  
{ worker: 'face_detection',
  path: '/hop/face_detection',
  url: 'http://rapp-platform-local:9001/hop/face_detection',
  frame: [Function] }

Further study

You can check on already implemented Web Services here.

The RAPP WebService code API is documented here.

Documentation of the RAPP Web Services package can be found here.