No edit summary |
|||
(9 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
ProB contains external functions to read and write JSON data and to communicate via sockets using JSON: | == Introduction == | ||
ProB contains external functions to read and write JSON data and to communicate via sockets using JSON-RPC: | |||
* <tt>LibraryJSON.def</tt> and <tt>LibraryJSON.mch</tt> providing the functions <tt>READ_JSON</tt>, <tt>READ_JSON_FROM_STRING</tt>, <tt>WRITE_JSON</tt>, <tt>WRITE_JSON_TO_STRING</tt> and the freetype <tt>JsonValue</tt> to read and write JSON data. | * <tt>LibraryJSON.def</tt> and <tt>LibraryJSON.mch</tt> providing the functions <tt>READ_JSON</tt>, <tt>READ_JSON_FROM_STRING</tt>, <tt>WRITE_JSON</tt>, <tt>WRITE_JSON_TO_STRING</tt> and the freetype <tt>JsonValue</tt> to read and write JSON data. | ||
* <tt>LibraryZMQ_RPC.def</tt> and <tt>LibraryZMQ_RPC.mch</tt>: providing access to a JSON RPC (Remote Procedure Call) | * <tt>LibraryZMQ_RPC.def</tt> and <tt>LibraryZMQ_RPC.mch</tt>: providing access to a JSON-RPC (Remote Procedure Call) protocol implementation over either TCP sockets using [[wikipedia:NDJSON|NDJSON]] or over [https://zeromq.org/ ZeroMQ] sockets. | ||
We have used this library to control Crazyflie drones via ProB: | |||
[[File:prob_drone.jpeg||800px]] | |||
<tt>LibraryJSON.mch</tt> defines a FREETYPE to represent JSON data in a type-safe way: | == JSON Library == | ||
<tt>LibraryJSON.mch</tt> defines a FREETYPE to represent [https://json.org/ JSON] data in a type-safe way: | |||
<pre> | <pre> | ||
JsonValue = JsonNull, | JsonValue = JsonNull, | ||
Line 18: | Line 24: | ||
They are not standard B, but stem from Z and Rodin's theory plug-in. | They are not standard B, but stem from Z and Rodin's theory plug-in. | ||
<tt>JsonValue</tt> maps quite naturally to the specification of JSON itself. | |||
<tt>LibraryJSON.def</tt> contains the definitions for the the external functions to interact with JSON text data: | |||
* <tt>READ_JSON(file)</tt> | |||
** Type Signature: <tt>STRING --> JsonValue</tt> | |||
** Reads a file path on the disk and parses the contained JSON data into the freetype representation. | |||
** <i>Warning: this function does external IO and should be guarded by a <tt>MAX_OPERATIONS == 0</tt> definition</i> | |||
* <tt>READ_JSON_FROM_STRING(contents)</tt> | |||
** Type Signature: <tt>STRING --> JsonValue</tt> | |||
** Parses the given JSON text into the freetype representation. | |||
* <tt>WRITE_JSON(json, file)</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. | |||
** <i>Warning: this function does external IO and should be guarded by a <tt>MAX_OPERATIONS == 0</tt> definition</i> | |||
* <tt>WRITE_JSON_TO_STRING(json)</tt> | |||
** Type Signature: <tt>JsonValue --> STRING</tt> | |||
** Converts the given JSON data from the freetype representation into text. | |||
You need to include <b>both</b> <tt>LibraryJson.def</tt> and <tt>LibraryJson.mch</tt> to interact with JSON. One contains the freetype definition, the other one the external function definitions. | |||
<tt> LibraryZMQ_RPC.mch </tt> defines a FREETYPE to represent RPC in a type-safe way: | == JSON-RPC Library == | ||
Implements the [https://www.jsonrpc.org/specification JSON-RPC 2.0] protocol. | |||
Despite their names, the <tt>LibraryZMQ_RPC</tt> libraries also support using NDJSON instead of ZeroMQ. | |||
<tt>LibraryZMQ_RPC.mch</tt> defines a FREETYPE to represent JSON-RPC response object in a type-safe way: | |||
<pre> | <pre> | ||
RpcResult = RpcSuccess(JsonValue), RpcError(STRING) | RpcResult = RpcSuccess(JsonValue), RpcError(STRING) | ||
</pre> | </pre> | ||
<tt>LibraryZMQ_RPC.def</tt> contains the definitions for the the external functions to interact with JSON RPC as a client or server. | |||
[[ | |||
''Warning: '''all''' of the following functions/predicates/substitutions perform external IO and should be guarded by a <tt>MAX_OPERATIONS == 0</tt> definition.'' | |||
* <tt>ZMQ_RPC_INIT(endpoint)</tt> | |||
** Type Signature: <tt>STRING --> SOCKET</tt> | |||
** Creates a ZeroMQ client socket for JSON-RPC requests and binds it to the given ZeroMQ endpoint. | |||
* <tt>SOCKET_RPC_INIT(port)</tt> | |||
** Type Signature: <tt>INTEGER --> SOCKET</tt> | |||
** Creates a plain TCP client socket for JSON-RPC over NDJSON and binds it to the given port. | |||
* <tt>ZMQ_RPC_DESTROY(socket)</tt> | |||
** Type Signature: <tt>SOCKET</tt> (substitution, no return value) | |||
** Closes the given ZeroMQ or plain TCP socket. | |||
** <i>Note: despite its name this function works with both ZeroMQ and plain TCP sockets</i> | |||
* <tt>ZMQ_RPC_SEND(socket, name, args)</tt> | |||
** Type Signature: <tt>(SOCKET*STRING*(STRING+->JsonValue)) --> RpcResult</tt> | |||
** Sends a JSON-RPC request object over the given ZeroMQ or plain TCP socket. | |||
** <i>Note: despite its name this function works with both ZeroMQ and plain TCP sockets</i> | |||
* <tt>SOCKET_RPC_ACCEPT(port)</tt> | |||
** Type Signature: <tt>INTEGER --> JsonValue</tt> | |||
** Open a plain TCP server socket, wait for a client to connect, receive a JSON-RPC request object over NDJSON and return it. | |||
* <tt>SOCKET_RPC_REPLY(port,rpcresult)</tt> | |||
** Type Signature: <tt>(INTEGER * RpcResult) --> BOOL</tt> | |||
** Respond to a previously received JSON-RPC request object with the given JSON-RPC response object. Will always return <tt>TRUE</tt>. | |||
** <i>Note: must be called right after <tt>SOCKET_RPC_ACCEPT</tt></i> | |||
<tt>LibraryZMQ_RPC.def</tt> will transitively include <tt>LibraryJson.def</tt>. | |||
You need to include <b>all</b> of <tt>LibraryJson.mch</tt>, <tt>LibraryZMQ_RPC.def</tt> and <tt>LibraryZMQ_RPC.mch</tt> to interact with JSON-RPC, because the freetype definitions have to be separate from the external function definitions. | |||
== Drone Control Example == | |||
For a real-world example application, see our [https://gitlab.cs.uni-duesseldorf.de/general/stups/cf-srv cf-srv] project for controlling Crazyflie drones using ProB. | |||
The drones are radio-controlled using the manufacturer-provided Python library [https://pypi.org/project/cflib/ cflib], | |||
based on which we implemented a ZeroMQ server ([https://gitlab.cs.uni-duesseldorf.de/general/stups/cf-srv/-/blob/master/cf_rpc.py?ref_type=heads cf_rpc.py]) that provides a simple drone control API over JSON-RPC. | |||
The client is a B machine ([https://gitlab.cs.uni-duesseldorf.de/general/stups/cf-srv/-/blob/master/DroneCommunicator.mch?ref_type=heads DroneCommunicator.mch]), which can then be used to control the drones from a B machine inside ProB. | |||
== Client-Server Example Machines == | |||
Here is a small example of a server that listens for NDJSON JSON-RPC messages on a plain (non-ZeroMQ) TCP socket. | |||
You can run the server by calling <tt>probcli Simple_RPC_JSON_Server.mch</tt>. | |||
<pre> | |||
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) & | |||
Method /: {"ping", "halt", "prob_statistics"} THEN | |||
b := SOCKET_RPC_REPLY(Port,RpcError(```Not Implemented: ${Method}```)) | |||
|| | |||
request := JsonNull | |||
END | |||
END | |||
</pre> | |||
After starting the above server, you can start this client on the same machine, e.g., via <tt>probcli Simple_RPC_JSON_client.mch -p MAXINT</tt> | |||
<pre> | |||
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 | |||
</pre> | |||
Here is a sample log when running the server: | |||
<pre> | |||
$ 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 | |||
</pre> | |||
Here is a sample log when running the client (starting it after the above server): | |||
<pre> | |||
$ 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 | |||
</pre> |
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 also support using NDJSON instead of ZeroMQ.
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.
Warning: all of the following functions/predicates/substitutions perform external IO and should be guarded by a MAX_OPERATIONS == 0 definition.
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.
For a real-world example application, see our cf-srv project for controlling Crazyflie drones using ProB. The drones are radio-controlled using the manufacturer-provided Python library cflib, based on which we implemented a ZeroMQ server (cf_rpc.py) that provides a simple drone control API over JSON-RPC. The client is a B machine (DroneCommunicator.mch), which can then be used to control the drones from a B machine inside ProB.
Here is a small example of a server that listens for NDJSON JSON-RPC messages on a plain (non-ZeroMQ) TCP 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) & Method /: {"ping", "halt", "prob_statistics"} 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