ProB contains external functions to read and write JSON data and to communicate via sockets using JSON-RPC:
We have used this library to control Crazyflie drones via ProB:
LibraryJSON.mch defines a FREETYPE to represent JSON data in a type-safe way:
JsonValue = JsonNull, JsonBoolean(BOOL), JsonNumber(FLOAT), JsonString(STRING), JsonArray(seq(JsonValue)), JsonObject(STRING +-> JsonValue)
Free Types are inductive data types. They are not standard B, but stem from Z and Rodin's theory plug-in.
JsonValue maps quite naturally to the specification of JSON itself.
LibraryJSON.def contains the definitions for the the external functions to interact with JSON text data:
You need to include both LibraryJson.def and LibraryJson.mch to interact with JSON. One contains the freetype definition, the other one the external function definitions.
Implements the JSON RPC protocol.
Despite their names the LibraryZMQ_RPC libraries can use both ZeroMQ library and native sockets.
LibraryZMQ_RPC.mch defines a FREETYPE to represent JSON-RPC response object in a type-safe way:
RpcResult = RpcSuccess(JsonValue), RpcError(STRING)
LibraryZMQ_RPC.def contains the definitions for the the external functions to interact with JSON RPC as a client or server:
LibraryZMQ_RPC.def will transitively include LibraryJson.def.
You need to include all of LibraryJson.mch, LibraryZMQ_RPC.def and LibraryZMQ_RPC.mch to interact with JSON-RPC, because the freetype definitions have to be separate from the external function definitions.
Here is a small example of a server that listens on a classical (non-ZMQ) socket. You can run the server by calling probcli Simple_RPC_JSON_Server.mch.
MACHINE Simple_RPC_JSON_Server // start with probcli Simple_RPC_JSON_Server.mch // other B Models can connect then to this server with socket := SOCKET_RPC_INIT(9999); // and send JSON-RPC request to this server via // result := ZMQ_RPC_SEND(socket, "prob_statistics", {("name" |-> JsonString("prolog-walltime"))}) INCLUDES LibraryZMQ_RPC, LibraryJSON DEFINITIONS "LibraryZMQ_RPC.def"; "LibraryMeta.def"; "LibraryIO.def" CONSTANTS Port PROPERTIES Port:NATURAL & Port = 9999 VARIABLES request, terminated INVARIANT request : JsonValue & terminated : BOOL INITIALISATION request := JsonNull || terminated := FALSE OPERATIONS GetRequest = SELECT request = JsonNull & terminated = FALSE THEN request := SOCKET_RPC_ACCEPT(Port); PRINTF("~nReceived request: ~w~n",[request]) END; b <-- SendSuccessAnswer(Method) = SELECT request /= JsonNull & Method:STRING & "method"|->JsonString(Method) : JsonObject~(request) & Method : {"ping", "halt"} THEN b := SOCKET_RPC_REPLY(Port,RpcSuccess(JsonNull)) || request := JsonNull || terminated := bool(Method = "halt") END; b <-- SendNumberAnswer(Method,Params,Name) = SELECT request /= JsonNull & Method:STRING & "method"|->JsonString(Method) : JsonObject~(request) & "params"|->JsonObject(Params) : JsonObject~(request) & "name"|->JsonString(Name) : Params & Method : {"prob_statistics"} THEN IF Name:{"now-timestamp", "prolog-walltime", "prolog-runtime"} THEN b := SOCKET_RPC_REPLY(Port,RpcSuccess(JsonNumber(real(PROB_STATISTICS(Name))))) ELSE b := SOCKET_RPC_REPLY(Port,RpcError(```Not Implemented: ${Method}```)) END || request := JsonNull END; b <-- SendNotImplemented(Method) = SELECT request /= JsonNull & Method:STRING & "method"|->JsonString(Method) : JsonObject~(request) THEN b := SOCKET_RPC_REPLY(Port,RpcError(```Not Implemented: ${Method}```)) || request := JsonNull END END
After starting the above server, you can start this client on the same machine, e.g., via probcli Simple_RPC_JSON_client.mch -p MAXINT
MACHINE Simple_RPC_JSON_client INCLUDES LibraryZMQ_RPC, LibraryJSON // A small demo client which alternatively sends PING and PROB_STATISTICS requests via JSON RPC to a server DEFINITIONS "LibraryZMQ_RPC.def"; "LibraryIO.def"; DRONE_URL == "none" CONSTANTS Port PROPERTIES Port:NATURAL & Port = 9999 VARIABLES socket, count INVARIANT socket:INTEGER & count:NATURAL INITIALISATION socket := SOCKET_RPC_INIT(9999) || count := MAXINT OPERATIONS result <-- Ping = SELECT count > 1 & count mod 2 = 0THEN PRINTF("~nSending PING request on socket ~w (~w left)~n",[socket,count-1]) ; result := ZMQ_RPC_SEND(socket, "ping", {}) ; PRINTF("Result = ~w~n",[result]) ; count := count -1 END; result <-- GetStats = SELECT count > 1 & count mod 2 = 1 THEN PRINTF("~nSending PROB_STATISTICS request on socket ~w (~w left)~n",[socket,count-1]) ; result := ZMQ_RPC_SEND(socket, "prob_statistics", {("name" |-> JsonString("prolog-walltime"))}) ; PRINTF("Result = ~w~n",[result]) ; count := count -1 END; result <-- Terminate = SELECT count = 1 THEN result := ZMQ_RPC_SEND(socket, "halt", {}); count := 0; PRINTF("~nZMQ_RPC_DESTROY (~w)~n",[socket]) ; ZMQ_RPC_DESTROY(socket) END END
This is an excerpt from the B machine used to communicate with the Crazyflie drone. Parallel to this a Python script is running, which implements all the JSON-RPC methods and uses cflib to communicate with the drone over radio.
MACHINE DroneCommunicator USES LibraryJSON, LibraryZMQ_RPC DEFINITIONS "LibraryZMQ_RPC.def"; "LibraryReals.def"; SET_PREF_MAX_OPERATIONS == 0; CONSTANTS DRONE_URL PROPERTIES DRONE_URL:STRING & DRONE_URL = "radio://0/80/2M/5700B500F7" VARIABLES init, socket, cycle INVARIANT init:BOOL & socket:SOCKET & cycle:INTEGER INITIALISATION init := FALSE; socket := 0; cycle := 1 OPERATIONS Init = SELECT init = FALSE THEN socket := ZMQ_RPC_INIT("tcp://localhost:22272"); init := TRUE END; Destroy = SELECT init = TRUE THEN ZMQ_RPC_DESTROY(socket); init := FALSE END; register_sensors = SELECT init = TRUE THEN VAR res IN res := RpcSuccess~(ZMQ_RPC_SEND(socket, "register_log", { ("url" |-> JsonString(DRONE_URL)), ("name" |-> JsonString("Telemetry")), ("variables" |-> JsonArray([ JsonString("stateEstimate.x"), JsonString("stateEstimate.y"), JsonString("stateEstimate.z"), JsonString("stateEstimate.roll"), JsonString("stateEstimate.pitch"), JsonString("stateEstimate.yaw") ]))})); END END; Drone_Takeoff = SELECT init = TRUE THEN VAR res IN res := RpcSuccess~(ZMQ_RPC_SEND(socket, "takeoff", {("url" |-> JsonString(DRONE_URL))})) END END; Drone_Land = SELECT init = TRUE THEN VAR res IN res := RpcSuccess~(ZMQ_RPC_SEND(socket, "land", {("url" |-> JsonString(DRONE_URL))})) END END; Drone_Left(dist) = SELECT init = TRUE & dist : 0..2000 THEN VAR res IN res := RpcSuccess~(ZMQ_RPC_SEND(socket, "left", {("url" |-> JsonString(DRONE_URL)), ("distance" |-> JsonNumber(RDIV(real(dist), real(1000))))})) END END; Drone_Right(dist) = SELECT init = TRUE & dist : 0..2000 THEN VAR res IN res := RpcSuccess~(ZMQ_RPC_SEND(socket, "right", {("url" |-> JsonString(DRONE_URL)), ("distance" |-> JsonNumber(RDIV(real(dist), real(1000))))})) END END; Drone_Up(dist) = SELECT init = TRUE & dist : 0..2000 THEN VAR res IN res := RpcSuccess~(ZMQ_RPC_SEND(socket, "up", {("url" |-> JsonString(DRONE_URL)), ("distance" |-> JsonNumber(RDIV(real(dist), real(1000))))})) END END; Drone_Down(dist) = SELECT init = TRUE & dist : 0..2000 THEN VAR res IN res := RpcSuccess~(ZMQ_RPC_SEND(socket, "down", {("url" |-> JsonString(DRONE_URL)), ("distance" |-> JsonNumber(RDIV(real(dist), real(1000))))})) END END; Drone_Forward(dist) = SELECT init = TRUE & dist : 0..2000 THEN VAR res IN res := RpcSuccess~(ZMQ_RPC_SEND(socket, "forward", {("url" |-> JsonString(DRONE_URL)), ("distance" |-> JsonNumber(RDIV(real(dist), real(1000))))})) END END; Drone_Backward(dist) = SELECT init = TRUE & dist : 0..2000 THEN VAR res IN res := RpcSuccess~(ZMQ_RPC_SEND(socket, "backward", {("url" |-> JsonString(DRONE_URL)), ("distance" |-> JsonNumber(RDIV(real(dist), real(1000))))})) END END; out <-- Drone_GetX = SELECT init = TRUE THEN out := floor(1000.0 * JsonNumber~(RpcSuccess~(ZMQ_RPC_SEND(socket, "get_log_var", {("url" |-> JsonString(DRONE_URL)), ("name" |-> JsonString("stateEstimate.x"))})))); cycle := cycle + 1 END END