Remote Procedure Call (RPC) over Websockets (uses MbedJSONValue)

Dependents:   RPC_mbed_client RPC_Wifly_HelloWorld RPC_Ethernet_HelloWorld

MbedJSONRpc.h

Committer:
samux
Date:
2011-09-22
Revision:
4:2f83c922c234
Parent:
3:4a3bc3a2314f
Child:
5:88e1902947e5

File content as of revision 4:2f83c922c234:

/**
* @author Samuel Mokrani
*
* @section LICENSE
*
* Copyright (c) 2011 mbed
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @section DESCRIPTION
*       Main part of MbedJSONRpc: you can register methods or procedures, call a registered method or procedure
*       on a client over a websocket communication, see all methods available on a certain client, listen for 
*       CALL incoming messages to execute a method or procedure.
*
*/


#ifndef _MbedJSONRPC_H_
#define _MbedJSONRPC_H_

#include "Websocket.h"
#include "Wifly.h"
#include "mbed.h"
#include "MbedJSONValue.h"
#include <string>

#define NB_METH 30

#define MSG_CALL                    "{\"from\": \"%s\",\"to\": \"%s\",\"msg\": \"CALL\",\"id_msg\": %d,\"method\": \"%s\",\"params\":%s}"
/**< Message skeleton which will be use for a call */
#define MSG_RESULT                  "{\"from\": \"%s\",\"to\": \"%s\",\"msg\": \"RESULT\",\"id_msg\": %d,\"res\":%s}"
/**< Message skeleton which will be use for a result  */
#define MSG_REGISTER                "{\"from\": \"%s\",\"to\": \"gateway\",\"msg\": \"REGISTER\",\"id_msg\": %d,\"fn\":\"%s\"}"
/**< Message skeleton which will be use for register a method or a procedure */
#define MSG_INFO_METHODS            "{\"from\": \"%s\",\"to\": \"%s\",\"msg\": \"INFO_METHODS\",\"id_msg\": %d}"
/**< Message skeleton which will be send to see all methods available of a certain client */


/**
* \enum RPC_TYPE
* \brief Type of an RPC transaction
*/
enum RPC_TYPE {
    JSON_PARSE_ERROR,       /*!< Json parse error */
    RPC_PARSE_ERROR,        /*!< rpc parse error (the message doesn't contain good identifiers in a TypeObject MbedJSONValue)*/ 
    PROC_NOT_FOUND,         /*!< The user wants to call an inexisting method or procedure */
    CLIENT_NOT_CONNECTED,   /*!< The user wants to call a method or procedure on a client not connected (no response from the client within 3s) */
    SERVER_NOT_CONNECTED,   /*!< No response from the server within 5s */
    ERR_ID,                 /*!< Each messages have an id, if the id of the answer doesn't match with the id of the request, there is an id error */
    CALL_OK,                /*!< A call has been successfully executed */
    REGISTER_OK             /*!< A request to register a method or procedure is successful */
};



class ObjBase {
public:
    virtual void execute(MbedJSONValue& val, MbedJSONValue& res) = 0;
};

template <class T>
class Obj : public ObjBase {
public:

    Obj(T * ptr, void (T::*fn_ptr)(MbedJSONValue& , MbedJSONValue& )) {
        obj_ptr = ptr;
        fn = fn_ptr;
    };

    virtual void execute(MbedJSONValue& val, MbedJSONValue& res) {
        ((*obj_ptr).*fn)(val, res);
    }

    //obj
    T * obj_ptr;

    //method
    void (T::*fn)(MbedJSONValue& , MbedJSONValue& );

};


/** MbedJSONRpc class
 *
 * Warning: you must use a wifi module (Wifly RN131-C) or an ethernet network to use this class
 *
 * Example (client which registers one method):
 * @code
 * #include "mbed.h"
 * #include "MbedJSONRpc.h"
 * #include "MbedJSONValue.h"
 * #include "Websocket.h"
 *
 * #define ETHERNET
 *
 * #ifdef WIFI
 * #include "Wifly.h"
 * #endif
 *
 * DigitalOut led1(LED1);
 * Serial pc(USBTX, USBRX);
 *
 *
 * class Test {
 * public:
 *   Test() {};
 *   void fn(MbedJSONValue& val, MbedJSONValue& res) {
 *       printf("first arg: %s\r\n", val[0].get<string>().c_str());
 *       res[0] = "coucou";
 *       led1 = 1;
 *   }
 * };
 * 
 *
 * #ifdef WIFI
 * //wifi and websocket
 *  Wifly wifly(p9, p10, p30, "NETGEAR", "hello", true);
 *  Websocket webs("ws://sockets.mbed.org:888/ws/samux/server",&wifly);
 * #endif
 *
 * #ifdef ETHERNET
 *  Websocket webs("ws://sockets.mbed.org:888/ws/samux/server");
 * #endif
 *
 * //RPC object identified with the name "server" on the network and attached to the websocket server
 * MbedJSONRpc rpc(&webs);
 * 
 * Test test;
 *
 * int main() {
 *   
 *   RPC_TYPE t;
 *
 *
 *   //connection to the router and to the websocket server
 *   #ifdef WIFI
 *   while (1) {
 *
 *       while (!wifly.Join())  //we connect to the network
 *           wifly.reset();
 *
 *       if (!webs.connect())    //we connect to the server
 *           wifly.reset();
 *       else
 *           break;
 *   }
 *   #endif
 *  
 *   #ifdef ETHERNET
 *   while(!webs.connect())
 *       pc.printf("cannot connect websocket, retrying\r\n");
 *   #endif
 *
 *   //register the method Test::fn as "fn1"
 *   if((t = rpc.registerMethod("fn1", &test, &Test::fn)) == REGISTER_OK)
 *       printf("register ok\r\n");
 *   else
 *       printType(t);
 *   
 *   //Listen CALL requests
 *   rpc.work();
 * }
 * @endcode
 *
 *
 *
 * Example (client which calls the previous registered method):
 * @code
 * #include "mbed.h"
 * #include "MbedJSONRpc.h"
 * #include "MbedJSONValue.h"
 * #include "Websocket.h"
 *
 * #define WIFI
 *
 * #ifdef WIFI
 *   #include "Wifly.h"
 * #endif
 *
 * Serial pc(USBTX, USBRX);
 *
 * #ifdef WIFI
 *   //wifi and websocket
 *   Wifly wifly(p9, p10, p30, "NETGEAR", "hello", true);
 *   Websocket webs("ws://sockets.mbed.org:888/ws/samux/client",&wifly);
 * #endif
 *
 * #ifdef ETHERNET
 *   Websocket webs("ws://sockets.mbed.org:888/ws/samux/client");
 * #endif
 * 
 * //RPC object identified by the name "client" on the network and attached to the websocket server
 * MbedJSONRpc rpc(&webs);
 *
 * int main() {
 *
 *   //connection to the router and to the websocket server
 *   #ifdef WIFI
 *   while (1) {
 *
 *       while (!wifly.Join())  //we connect to the network
 *           wifly.reset();
 *
 *       if (!webs.connect())    //we connect to the server
 *           wifly.reset();
 *       else
 *           break;
 *   }
 *   #endif
 *  
 *   #ifdef ETHERNET
 *   while(!webs.connect())
 *       pc.printf("cannot connect websocket, retrying\r\n");
 *   #endif
 *   
 *   RPC_TYPE t;
 *   MbedJSONValue arg, resp;
 *   arg[0] = "Hello";
 *
 *   // print all methods and procedure available on the client "server"
 *   rpc.checkMethods("server");
 *       
 *   // Try to call the function "fn1" registered by server previously
 *   if((t = rpc.call("fn1", "server", arg, resp)) == CALL_OK)
 *   {
 *       printf("call success\r\n");
 *       printf("res: %s\r\n", resp[0].get<string>().c_str());
 *   }
 *   else
 *       printType(t);
 * }
 * @endcode
 */
class MbedJSONRpc {
public:

    /**
    *   Constructor
    *
    * @param id Name of the client on the network
    * @param ws All communication between clients will be established over this websocket
    */
    MbedJSONRpc(Websocket * webs) : webs(webs), index(0), index_proc(0) {
        std::string path = webs->getPath();
        std::string token = "/";
        size_t found;
        found = path.find(token, 3);
        if (found != std::string::npos)
            path = path.substr(found + 1, std::string::npos);
        strcpy(my_id, path.c_str());
    };

    /**
    * Register a method of an object
    *
    * @param public_name the method will be seen and called by this name
    * @param obj_ptr a pointeur on the object which contains the method to register
    * @param fn the method to register (this method must have this signature: void fn(MbedJSONValue& val, MbedJSONValue& res)
    * @return if REGISTER_OK, the method is registered, otherwise, there is an error
    *
    */
    template<typename T> RPC_TYPE registerMethod(const char * public_name, T * obj_ptr, void (T::*fn)(MbedJSONValue& val, MbedJSONValue& res)) {
        char json[100];
        RPC_TYPE t;
        Timer tmr;
        int id = rand() % 100;
        MbedJSONValue tmp;

        sprintf(json, (const char *)MSG_REGISTER, my_id, id, public_name);
        webs->Send(json);
        tmr.start();
        t = waitAnswer(tmp, id, json);
        if (t != REGISTER_OK)
            return t;
        if( index == NB_METH )
            index = NB_METH - 1;
        obj[index] = new Obj<T>(obj_ptr, fn);
        name[index++] = public_name;
        return REGISTER_OK;
    }


    /**
    * Register a procedure
    *
    * @param public_name the method will be seen and called by this name
    * @param fn the procedure to register (this procedure must have this signature: void fn(MbedJSONValue& val, MbedJSONValue& res)
    * @return if REGISTER_OK, the procedure is registered, otherwise, there is an error
    *
    */
    RPC_TYPE registerMethod(const char * public_name, void (*fn)(MbedJSONValue& val, MbedJSONValue& res) );
    
    
    /**
    * Call a method or procedure on a client
    *
    * @param fn name of the method or procedure to be called
    * @param dest name of the client where will be executed the method or procedure
    * @param val Input parameters of the method or procedure "fn"
    * @param resp Once the method or procedure executed, the result will be stored in the "resp" variable.
    * @return If CALL_OK, the method has been executed and you can access the result with the "resp" variable. 
    *         If CLIENT_NOT_CONNECTED, means that the client hasn't answered within 3s.
    *         If SERVER_NOT_CONNECTED, means that the server hasn't answered within 5s.
    *         Refer to the RPC_TYPE description
    *
    */
    RPC_TYPE call(char * fn, char * dest,  MbedJSONValue& val, MbedJSONValue& resp);
    
    
    /**
    * Listen for CALL requests.
    * Warning: infinite loop
    */
    void work();
    
    /**
    * Print by the usb serial port all methods or procedure available on "dest" client
    */
    void checkMethods(char * dest);

private:

    typedef void (*FNPTR)(MbedJSONValue& , MbedJSONValue& );

    int methodAlreadyRegistered(char *);
    int procAlreadyRegistered(char *);
    RPC_TYPE decodeMsg(MbedJSONValue& , int);
    RPC_TYPE waitAnswer(MbedJSONValue& , int, char *);

    Websocket * webs;

    ObjBase * obj[NB_METH];
    const char * name[NB_METH];
    int index;

    FNPTR proc[NB_METH];
    const char * name_proc[NB_METH];
    int index_proc;


    char my_id[20];

};


inline void printType(RPC_TYPE t)
{
    switch(t)
    {
        case JSON_PARSE_ERROR: printf("json parse error\r\n"); break;
        case RPC_PARSE_ERROR: printf("rpc parse error\r\n"); break;
        case PROC_NOT_FOUND: printf("proc or method not found\r\n"); break;
        case CLIENT_NOT_CONNECTED: printf("client not connected\r\n"); break;
        case SERVER_NOT_CONNECTED: printf("server not connected\r\n"); break;
        case ERR_ID: printf("id error\r\n"); break;
        case CALL_OK: printf("call ok\r\n"); break;
        case REGISTER_OK: printf("register ok\r\n"); break;
    }
}

#endif