mNo edit summary |
|||
Line 34: | Line 34: | ||
** Type Signature: <tt>STRING --> JsonValue</tt> | ** Type Signature: <tt>STRING --> JsonValue</tt> | ||
** Parses the given JSON text into the freetype representation. | ** Parses the given JSON text into the freetype representation. | ||
* <tt>WRITE_JSON (json, file)</tt> | * <tt>WRITE_JSON(json, file)</tt> | ||
** Type Signature: <tt>JsonValue * STRING</tt> (predicate, always returns <tt>true</tt>) | ** Type Signature: <tt>JsonValue * STRING</tt> (predicate, always returns <tt>true</tt>) | ||
** Converts the given JSON data from the freetype representation into text and writes it to the given file path. | ** Converts the given JSON data from the freetype representation into text and writes it to the given file path. | ||
Line 46: | Line 46: | ||
== JSON-RPC Library == | == JSON-RPC Library == | ||
Implements the [ | Implements the [https://www.jsonrpc.org/specification JSON-RPC 2.0] protocol. | ||
Despite their names the <tt>LibraryZMQ_RPC</tt> libraries can use both [https://zeromq.org/ ZeroMQ] library and native sockets. | Despite their names the <tt>LibraryZMQ_RPC</tt> libraries can use both [https://zeromq.org/ ZeroMQ] library and native sockets. |
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 2.0 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
Here is a sample log when running the server:
$ probcli Simple_RPC_JSON_Server.mch % unused_constants(3,[JsonArray,JsonBoolean,RpcResult]) % Runtime for SOLUTION for SETUP_CONSTANTS: 1 ms (walltime: 1 ms) Opened RPC-JSON socket (for ndjson, UTF8 streams) on port 9999 Client connected: 127.0.0.1 Received request line: {"jsonrpc":"2.0","method":"prob_statistics","params":{"name":"prolog-walltime"},"id":1} Prolog: [jsonrpc=string(2.0),method=string(prob_statistics),params=json([name=string(prolog-walltime)]),id=number(1)] Received request: JsonObject({("id"|->JsonNumber(1.0)),("jsonrpc"|->JsonString("2.0")),("method"|->JsonString("prob_statistics")),("params"|->JsonObject({("name"|->JsonString("prolog-walltime"))}))}) Response for request 1 >>> {"jsonrpc":"2.0","result":3667.0,"id":1} ... Received request: JsonObject({("id"|->JsonNumber(5.0)),("jsonrpc"|->JsonString("2.0")),("method"|->JsonString("halt")),("params"|->JsonObject({}))}) Response for request 5 >>> {"jsonrpc":"2.0","result":null,"id":5} Deadlock reached after 12 steps (after SendSuccessAnswer). VARIABLES (use -v to see constants or -silent to suppress output): ( request=JsonNull & terminated=TRUE ) % Runtime for -execute: 5 ms (with gc: 5 ms, walltime: 3144 ms); since start: 3 sec 671 ms
Here is a sample log when running the client (starting it after the above server):
$ probcli Simple_RPC_JSON_Client.mch -p MAXINT 5 % unused_constants(10,[JsonArray,JsonBoolean,JsonNull,JsonNumber,JsonObject,JsonValue,Port,RpcError,RpcResult,RpcSuccess]) % Runtime for SOLUTION for SETUP_CONSTANTS: 0 ms (walltime: 0 ms) Sending PROB_STATISTICS request on socket -1 (4 left) Result = RpcSuccess(JsonNumber(3667.0)) Sending PING request on socket -1 (3 left) Result = RpcSuccess(JsonNull) Sending PROB_STATISTICS request on socket -1 (2 left) Result = RpcSuccess(JsonNumber(3669.0)) Sending PING request on socket -1 (1 left) Result = RpcSuccess(JsonNull) ZMQ_RPC_DESTROY (-1) Deadlock reached after 7 steps (after Terminate). VARIABLES (use -v to see constants or -silent to suppress output): ( count=0 & socket=-1 ) % Runtime for -execute: 2 ms (with gc: 2 ms, walltime: 70 ms); since start: 0 sec 431 ms
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